1%% @author Couchbase <info@couchbase.com>
2%% @copyright 2013 Couchbase, Inc.
3%%
4%% Licensed under the Apache License, Version 2.0 (the "License");
5%% you may not use this file except in compliance with the License.
6%% You may obtain a copy of the License at
7%%
8%%      http://www.apache.org/licenses/LICENSE-2.0
9%%
10%% Unless required by applicable law or agreed to in writing, software
11%% distributed under the License is distributed on an "AS IS" BASIS,
12%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13%% See the License for the specific language governing permissions and
14%% limitations under the License.
15%%
16-module(menelaus_ui_auth).
17
18-include("ns_common.hrl").
19
20-behaviour(gen_server).
21
22-export([start_link/0]).
23-export([init/1, handle_call/3, handle_cast/2,
24         handle_info/2, terminate/2, code_change/3]).
25
26-export([generate_token/1, maybe_refresh/1,
27         check/1, reset/0, logout/1]).
28
29start_link() ->
30    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
31
32generate_token(Memo) ->
33    gen_server:call(?MODULE, {generate_token, Memo}, infinity).
34
35maybe_refresh(Token) ->
36    gen_server:call(?MODULE, {maybe_refresh, tok2bin(Token)}, infinity).
37
38tok2bin(Token) when is_list(Token) ->
39    list_to_binary(Token);
40tok2bin(Token) ->
41    Token.
42
43check(Token) ->
44    gen_server:call(?MODULE, {check, tok2bin(Token)}, infinity).
45
46reset() ->
47    gen_server:call(?MODULE, reset, infinity).
48
49logout(Token) ->
50    gen_server:call(?MODULE, {logout, tok2bin(Token)}, infinity).
51
52-define(MAX_TOKENS, 1024).
53
54init([]) ->
55    _ = ets:new(ui_auth_by_token, [protected, named_table, set]),
56    _ = ets:new(ui_auth_by_expiration, [protected, named_table, ordered_set]),
57    {ok, []}.
58
59maybe_expire() ->
60    Size = ets:info(ui_auth_by_token, size),
61    case Size < ?MAX_TOKENS of
62        true ->
63            ok;
64        _ ->
65            expire_oldest()
66    end.
67
68expire_oldest() ->
69    {Expiration, Token} = ets:first(ui_auth_by_expiration),
70    ets:delete(ui_auth_by_expiration, {Expiration, Token}),
71    ets:delete(ui_auth_by_token, Token).
72
73delete_token(Token) ->
74    case ets:lookup(ui_auth_by_token, Token) of
75        [{Token, Expiration, ReplacedToken, _}] ->
76            ets:delete(ui_auth_by_expiration, {Expiration, Token}),
77            ets:delete(ui_auth_by_token, Token),
78            ReplacedToken;
79        [] ->
80            false
81    end.
82
83get_now() ->
84    misc:time_to_epoch_int(os:timestamp()).
85
86do_generate_token(ReplacedToken, Memo) ->
87    %% NOTE: couch_uuids:random is using crypto-strong random
88    %% generator
89    Token = couch_uuids:random(),
90    Expiration = get_now() + ?UI_AUTH_EXPIRATION_SECONDS,
91    ets:insert(ui_auth_by_token, {Token, Expiration, ReplacedToken, Memo}),
92    ets:insert(ui_auth_by_expiration, {{Expiration, Token}}),
93    Token.
94
95validate_token_maybe_expire(Token) ->
96    case ets:lookup(ui_auth_by_token, Token) of
97        [{Token, Expiration, _, Memo}] ->
98            Now = get_now(),
99            case Expiration < Now of
100                true ->
101                    delete_token(Token),
102                    false;
103                _ ->
104                    {Expiration, Now, Memo}
105            end;
106        [] ->
107            false
108    end.
109
110handle_call(reset, _From, State) ->
111    ets:delete_all_objects(ui_auth_by_token),
112    ets:delete_all_objects(ui_auth_by_expiration),
113    {reply, ok, State};
114handle_call({generate_token, Memo}, _From, State) ->
115    maybe_expire(),
116    Token = do_generate_token(undefined, Memo),
117    {reply, Token, State};
118handle_call({maybe_refresh, Token}, _From, State) ->
119    case validate_token_maybe_expire(Token) of
120        false ->
121            {reply, nothing, State};
122        {Expiration, Now, Memo} ->
123            case Expiration - Now < ?UI_AUTH_EXPIRATION_SECONDS / 2 of
124                true ->
125                    %% NOTE: we take note of current and still valid
126                    %% token for correctness of logout
127                    %%
128                    %% NOTE: condition above ensures that there are at
129                    %% most 2 valid tokens per session
130                    NewToken = do_generate_token(Token, Memo),
131                    {reply, {new_token, NewToken}, State};
132                false ->
133                    {reply, nothing, State}
134            end
135    end;
136handle_call({logout, Token}, _From, State) ->
137    %% NOTE: {maybe_refresh... above is inserting new token when old is
138    %% still valid (to give current requests time to finish). But
139    %% gladly we also store older and potentially valid token, so we
140    %% can delete it as well here
141    OlderButMaybeValidToken = delete_token(Token),
142    case OlderButMaybeValidToken of
143        undefined ->
144            ok;
145        false ->
146            ok;
147        _ ->
148            delete_token(OlderButMaybeValidToken)
149    end,
150    {reply, ok, State};
151handle_call({check, Token}, _From, State) ->
152    case validate_token_maybe_expire(Token) of
153        false ->
154            {reply, false, State};
155        {_, _, Memo} ->
156            {reply, {ok, Memo}, State}
157    end;
158handle_call(Msg, From, _State) ->
159    erlang:error({unknown_call, Msg, From}).
160
161handle_cast(Msg, _State) ->
162    erlang:error({unknown_cast, Msg}).
163
164handle_info(_Msg, State) ->
165    {noreply, State}.
166
167terminate(_Reason, _State) ->
168    ok.
169
170code_change(_OldVsn, State, _Extra) ->
171    {ok, State}.
172