1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1997-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21-module(mod_auth).
22
23%% The functions that the webbserver call on startup stop
24%% and when the server traverse the modules.
25-export([do/1, store/2, remove/1]).
26
27%% User entries to the gen-server.
28-export([add_user/2, add_user/5, add_user/6,
29	 add_group_member/3, add_group_member/4, add_group_member/5,
30	 list_users/1, list_users/2, list_users/3,
31	 delete_user/2, delete_user/3, delete_user/4,
32	 delete_group_member/3, delete_group_member/4, delete_group_member/5,
33	 list_groups/1, list_groups/2, list_groups/3,
34	 delete_group/2, delete_group/3, delete_group/4,
35	 get_user/2, get_user/3, get_user/4,
36	 list_group_members/2, list_group_members/3, list_group_members/4,
37	 update_password/6, update_password/5]).
38
39-include("httpd.hrl").
40-include("mod_auth.hrl").
41-include("httpd_internal.hrl").
42
43-define(VMODULE,"AUTH").
44
45-define(NOPASSWORD,"NoPassword").
46
47%%====================================================================
48%% Internal application API
49%%====================================================================
50
51do(Info) ->
52    case proplists:get_value(status,Info#mod.data) of
53	%% A status code has been generated!
54	{_StatusCode, _PhraseArgs, _Reason} ->
55	    {proceed, Info#mod.data};
56	%% No status code has been generated!
57	undefined ->
58	    case proplists:get_value(response, Info#mod.data) of
59		%% No response has been generated!
60		undefined ->
61		    Path = mod_alias:path(Info#mod.data,Info#mod.config_db,
62					  Info#mod.request_uri),
63		    %% Is it a secret area?
64		    case secretp(Path,Info#mod.config_db) of
65			{yes, {Directory, DirectoryData}} ->
66			    case allow((Info#mod.init_data)#init_data.peername,
67				       Info#mod.socket_type,Info#mod.socket,
68				       DirectoryData) of
69				allowed ->
70				    case deny((Info#mod.init_data)#init_data.peername,
71					      Info#mod.socket_type,
72					      Info#mod.socket,
73					      DirectoryData) of
74					not_denied ->
75					    case proplists:get_value(auth_type,
76								     DirectoryData) of
77						undefined ->
78						    {proceed, Info#mod.data};
79						none ->
80						    {proceed, Info#mod.data};
81						AuthType ->
82						    do_auth(Info,
83							    Directory,
84							    DirectoryData,
85							    AuthType)
86					    end;
87					{denied, Reason} ->
88					    {proceed,
89					     [{status, {403,
90							Info#mod.request_uri,
91							Reason}}|
92					      Info#mod.data]}
93				    end;
94				{not_allowed, Reason} ->
95				    {proceed,[{status,{403,
96						       Info#mod.request_uri,
97						       Reason}} |
98					      Info#mod.data]}
99			    end;
100			no ->
101			    {proceed, Info#mod.data}
102		    end;
103		%% A response has been generated or sent!
104		_Response ->
105		    {proceed, Info#mod.data}
106	    end
107    end.
108
109
110%% mod_auth recognizes the following Configuration Directives:
111%% <Directory /path/to/directory>
112%%  AuthDBType
113%%  AuthName
114%%  AuthUserFile
115%%  AuthGroupFile
116%%  AuthAccessPassword
117%%  require
118%%  allow
119%% </Directory>
120
121%% When a <Directory> directive is found, a new context is set to
122%% [{directory, Directory, DirData}|OtherContext]
123%% DirData in this case is a key-value list of data belonging to the
124%% directory in question.
125%%
126%% When the </Directory> statement is found, the Context created earlier
127%% will be returned as a ConfigList and the context will return to the
128%% state it was previously.
129
130store({directory, {Directory, DirData}}, ConfigList)
131  when is_list(Directory) andalso is_list(DirData) ->
132    try directory_config_check(Directory, DirData) of
133	ok ->
134	    store_directory(Directory, DirData, ConfigList)
135    catch
136	throw:Error ->
137	    {error, Error, {directory, Directory, DirData}}
138    end;
139store({directory, {Directory, DirData}}, _) ->
140    {error, {wrong_type, {directory, {Directory, DirData}}}}.
141
142remove(ConfigDB) ->
143    lists:foreach(fun({directory, {_Dir, DirData}}) ->
144			  AuthMod = auth_mod_name(DirData),
145			  (catch apply(AuthMod, remove, [DirData]))
146		  end,
147		  ets:match_object(ConfigDB,{directory,{'_','_'}})),
148
149    Addr = httpd_util:lookup(ConfigDB, bind_address, undefined),
150    Port = httpd_util:lookup(ConfigDB, port),
151    Profile = httpd_util:lookup(ConfigDB, profile, ?DEFAULT_PROFILE),
152    mod_auth_server:stop(Addr, Port, Profile),
153    ok.
154
155add_user(UserName, Opt) ->
156    case get_options(Opt, mandatory) of
157	{Addr, Port, Dir, AuthPwd}->
158	    case get_options(Opt, userData) of
159		{error, Reason}->
160		    {error, Reason};
161		{UserData, Password}->
162		    User = [#httpd_user{username  = UserName,
163					password  = Password,
164					user_data = UserData}],
165		    mod_auth_server:add_user(Addr, Port, Dir, User, AuthPwd)
166	    end
167    end.
168
169
170add_user(UserName, Password, UserData, Port, Dir) ->
171    add_user(UserName, Password, UserData, undefined, Port, Dir).
172add_user(UserName, Password, UserData, Addr, Port, Dir) ->
173    User = [#httpd_user{username  = UserName,
174			password  = Password,
175			user_data = UserData}],
176    mod_auth_server:add_user(Addr, Port, Dir, User, ?NOPASSWORD).
177
178get_user(UserName, Opt) ->
179    case get_options(Opt, mandatory) of
180	{Addr, Port, Dir, AuthPwd} ->
181	    mod_auth_server:get_user(Addr, Port, Dir, UserName, AuthPwd);
182	{error, Reason} ->
183	    {error, Reason}
184    end.
185
186get_user(UserName, Port, Dir) ->
187    get_user(UserName, undefined, Port, Dir).
188get_user(UserName, Addr, Port, Dir) ->
189    mod_auth_server:get_user(Addr, Port, Dir, UserName, ?NOPASSWORD).
190
191add_group_member(GroupName, UserName, Opt)->
192    case get_options(Opt, mandatory) of
193	{Addr, Port, Dir, AuthPwd}->
194	    mod_auth_server:add_group_member(Addr, Port, Dir,
195					     GroupName, UserName, AuthPwd);
196	{error, Reason} ->
197	    {error, Reason}
198    end.
199
200add_group_member(GroupName, UserName, Port, Dir) ->
201    add_group_member(GroupName, UserName, undefined, Port, Dir).
202
203add_group_member(GroupName, UserName, Addr, Port, Dir) ->
204    mod_auth_server:add_group_member(Addr, Port, Dir,
205				     GroupName, UserName, ?NOPASSWORD).
206
207delete_group_member(GroupName, UserName, Opt) ->
208    case get_options(Opt, mandatory) of
209	{Addr, Port, Dir, AuthPwd} ->
210	    mod_auth_server:delete_group_member(Addr, Port, Dir,
211						GroupName, UserName, AuthPwd);
212	{error, Reason} ->
213	    {error, Reason}
214    end.
215
216delete_group_member(GroupName, UserName, Port, Dir) ->
217    delete_group_member(GroupName, UserName, undefined, Port, Dir).
218delete_group_member(GroupName, UserName, Addr, Port, Dir) ->
219    mod_auth_server:delete_group_member(Addr, Port, Dir,
220					GroupName, UserName, ?NOPASSWORD).
221
222list_users(Opt) ->
223    case get_options(Opt, mandatory) of
224	{Addr, Port, Dir, AuthPwd} ->
225	    mod_auth_server:list_users(Addr, Port, Dir, AuthPwd);
226	{error, Reason} ->
227	    {error, Reason}
228    end.
229
230list_users(Port, Dir) ->
231    list_users(undefined, Port, Dir).
232list_users(Addr, Port, Dir) ->
233    mod_auth_server:list_users(Addr, Port, Dir, ?NOPASSWORD).
234
235delete_user(UserName, Opt) ->
236    case get_options(Opt, mandatory) of
237	{Addr, Port, Dir, AuthPwd} ->
238	    mod_auth_server:delete_user(Addr, Port, Dir, UserName, AuthPwd);
239	{error, Reason} ->
240	    {error, Reason}
241    end.
242
243delete_user(UserName, Port, Dir) ->
244    delete_user(UserName, undefined, Port, Dir).
245delete_user(UserName, Addr, Port, Dir) ->
246    mod_auth_server:delete_user(Addr, Port, Dir, UserName, ?NOPASSWORD).
247
248delete_group(GroupName, Opt) ->
249    case get_options(Opt, mandatory) of
250	{Addr, Port, Dir, AuthPwd} ->
251	    mod_auth_server:delete_group(Addr, Port, Dir, GroupName, AuthPwd);
252	{error, Reason} ->
253	    {error, Reason}
254    end.
255
256delete_group(GroupName, Port, Dir) ->
257    delete_group(GroupName, undefined, Port, Dir).
258delete_group(GroupName, Addr, Port, Dir) ->
259    mod_auth_server:delete_group(Addr, Port, Dir, GroupName, ?NOPASSWORD).
260
261list_groups(Opt) ->
262    case get_options(Opt, mandatory) of
263	{Addr, Port, Dir, AuthPwd} ->
264	    mod_auth_server:list_groups(Addr, Port, Dir, AuthPwd);
265	{error, Reason} ->
266	    {error, Reason}
267    end.
268
269list_groups(Port, Dir) ->
270    list_groups(undefined, Port, Dir).
271list_groups(Addr, Port, Dir) ->
272    mod_auth_server:list_groups(Addr, Port, Dir, ?NOPASSWORD).
273
274list_group_members(GroupName, Opt) ->
275    case get_options(Opt, mandatory) of
276	{Addr, Port, Dir, AuthPwd} ->
277	    mod_auth_server:list_group_members(Addr, Port, Dir, GroupName,
278					       AuthPwd);
279	{error, Reason} ->
280	    {error, Reason}
281    end.
282
283list_group_members(GroupName, Port, Dir) ->
284    list_group_members(GroupName, undefined, Port, Dir).
285list_group_members(GroupName, Addr, Port, Dir) ->
286    mod_auth_server:list_group_members(Addr, Port, Dir,
287				       GroupName, ?NOPASSWORD).
288
289update_password(Port, Dir, Old, New, New)->
290    update_password(undefined, Port, Dir, Old, New, New).
291
292update_password(Addr, Port, Dir, Old, New, New) when is_list(New) ->
293    mod_auth_server:update_password(Addr, Port, Dir, Old, New);
294
295update_password(_Addr, _Port, _Dir, _Old, New, New) ->
296    {error, badtype};
297update_password(_Addr, _Port, _Dir, _Old, _New, _New1) ->
298    {error, notqeual}.
299
300%%--------------------------------------------------------------------
301%%% Internal functions
302%%--------------------------------------------------------------------
303
304do_auth(Info, Directory, DirectoryData, _AuthType) ->
305    %% Authenticate (require)
306    case require(Info, Directory, DirectoryData) of
307	authorized ->
308	    {proceed,Info#mod.data};
309	{authorized, User} ->
310	    {proceed, [{remote_user,User}|Info#mod.data]};
311	{authorization_required, Realm} ->
312	    ReasonPhrase = httpd_util:reason_phrase(401),
313	    Message = httpd_util:message(401,none,Info#mod.config_db),
314	    {proceed,
315	     [{response,
316	       {401,
317		["WWW-Authenticate: Basic realm=\"",Realm,
318		 "\"\r\n\r\n","<HTML>\n<HEAD>\n<TITLE>",
319		 ReasonPhrase,"</TITLE>\n",
320		 "</HEAD>\n<BODY>\n<H1>",ReasonPhrase,
321		 "</H1>\n",Message,"\n</BODY>\n</HTML>\n"]}}|
322	      Info#mod.data]};
323	{status, {StatusCode,PhraseArgs,Reason}} ->
324	    {proceed, [{status,{StatusCode,PhraseArgs,Reason}}|
325		       Info#mod.data]}
326    end.
327
328require(Info, Directory, DirectoryData) ->
329    ParsedHeader = Info#mod.parsed_header,
330    ValidUsers   = proplists:get_value(require_user, DirectoryData),
331    ValidGroups  = proplists:get_value(require_group, DirectoryData),
332    %% Any user or group restrictions?
333    case ValidGroups of
334	undefined when ValidUsers =:= undefined ->
335	    authorized;
336	_ ->
337	    case proplists:get_value("authorization", ParsedHeader) of
338		undefined ->
339		    authorization_required(DirectoryData);
340		%% Check credentials!
341		"Basic" ++ EncodedString = Credentials ->
342		    case (catch base64:decode_to_string(EncodedString)) of
343			{'EXIT',{function_clause, _}} ->
344			    {status, {401, none, ?NICE("Bad credentials "++
345						       Credentials)}};
346			DecodedString ->
347			   validate_user(Info, Directory, DirectoryData,
348					 ValidUsers, ValidGroups,
349					 DecodedString)
350		    end;
351		%% Bad credentials!
352		BadCredentials ->
353		    {status, {401, none, ?NICE("Bad credentials "++
354					       BadCredentials)}}
355	    end
356    end.
357
358authorization_required(DirectoryData) ->
359    case proplists:get_value(auth_name, DirectoryData) of
360	undefined ->
361	    {status,{500, none,?NICE("AuthName directive not specified")}};
362	Realm ->
363	    {authorization_required, Realm}
364    end.
365
366
367validate_user(Info, Directory, DirectoryData, ValidUsers,
368	      ValidGroups, DecodedString) ->
369    case a_valid_user(Info, DecodedString,
370		      ValidUsers, ValidGroups,
371		      Directory, DirectoryData) of
372	{yes, User} ->
373	    {authorized, User};
374	{no, _Reason} ->
375	    authorization_required(DirectoryData);
376	{status, {StatusCode,PhraseArgs,Reason}} ->
377	    {status,{StatusCode,PhraseArgs,Reason}}
378    end.
379
380a_valid_user(Info,DecodedString,ValidUsers,ValidGroups,Dir,DirData) ->
381    case httpd_util:split(DecodedString,":",2) of
382	{ok, [SupposedUser, Password]} ->
383	    case user_accepted(SupposedUser, ValidUsers) of
384		true ->
385		    check_password(SupposedUser, Password, Dir, DirData);
386		false ->
387		    case group_accepted(Info,SupposedUser,
388					ValidGroups,Dir,DirData) of
389			true ->
390			    check_password(SupposedUser,Password,Dir,DirData);
391			false ->
392			    {no,?NICE("No such user exists")}
393		    end
394	    end;
395	{ok, BadCredentials} ->
396	    {status,{401,none,?NICE("Bad credentials "++BadCredentials)}}
397    end.
398
399user_accepted(_SupposedUser, undefined) ->
400    false;
401user_accepted(SupposedUser, ValidUsers) ->
402    lists:member(SupposedUser, ValidUsers).
403
404
405group_accepted(_Info, _User, undefined, _Dir, _DirData) ->
406    false;
407group_accepted(_Info, _User, [], _Dir, _DirData) ->
408    false;
409group_accepted(Info, User, [Group|Rest], Dir, DirData) ->
410    Ret = int_list_group_members(Group, Dir, DirData),
411    case Ret of
412	{ok, UserList} ->
413	    case lists:member(User, UserList) of
414		true ->
415		    true;
416		false ->
417		    group_accepted(Info, User, Rest, Dir, DirData)
418	    end;
419	_ ->
420	    false
421    end.
422
423check_password(User, Password, _Dir, DirData) ->
424    case int_get_user(DirData, User) of
425	{ok, UStruct} ->
426	    case UStruct#httpd_user.password of
427		Password ->
428		    %% FIXME
429		    {yes, UStruct#httpd_user.username};
430		_ ->
431		    {no, "No such user"}   % Don't say 'Bad Password' !!!
432	    end;
433	_Other ->
434	    {no, "No such user"}
435    end.
436
437
438%% Middle API. Theese functions call the appropriate authentication module.
439int_get_user(DirData, User) ->
440    AuthMod = auth_mod_name(DirData),
441    apply(AuthMod, get_user, [DirData, User]).
442
443int_list_group_members(Group, _Dir, DirData) ->
444    AuthMod = auth_mod_name(DirData),
445    apply(AuthMod, list_group_members, [DirData, Group]).
446
447auth_mod_name(DirData) ->
448    case proplists:get_value(auth_type, DirData, plain) of
449	plain ->  mod_auth_plain;
450	mnesia -> mod_auth_mnesia;
451	dets ->	  mod_auth_dets
452    end.
453
454secretp(Path,ConfigDB) ->
455    Directories = ets:match(ConfigDB,{directory, {'$1','_'}}),
456    case secret_path(Path, Directories) of
457	{yes,Directory} ->
458	    {yes, {Directory,
459		   lists:flatten(
460		     ets:match(ConfigDB,{directory, {Directory,'$1'}}))}};
461	no ->
462	    no
463    end.
464
465secret_path(Path, Directories) ->
466    secret_path(Path, httpd_util:uniq(lists:sort(Directories)),to_be_found).
467
468secret_path(_Path, [], to_be_found) ->
469    no;
470secret_path(_Path, [], Directory) ->
471    {yes, Directory};
472secret_path(Path, [[NewDirectory] | Rest], Directory) ->
473    case re:run(Path, NewDirectory, [{capture, first}]) of
474	{match, _} when Directory =:= to_be_found ->
475	    secret_path(Path, Rest, NewDirectory);
476	{match, [{_, Length}]} when Length > length(Directory)->
477	    secret_path(Path, Rest,NewDirectory);
478	{match, _} ->
479	    secret_path(Path, Rest, Directory);
480	nomatch ->
481	    secret_path(Path, Rest, Directory)
482    end.
483
484allow({_,RemoteAddr}, _SocketType, _Socket, DirectoryData) ->
485    Hosts = proplists:get_value(allow_from, DirectoryData, all),
486    case validate_addr(RemoteAddr, Hosts) of
487	true ->
488	    allowed;
489	false ->
490	    {not_allowed, ?NICE("Connection from your host is not allowed")}
491    end.
492
493validate_addr(_RemoteAddr, all) ->            % When called from 'allow'
494    true;
495validate_addr(_RemoteAddr, none) ->           % When called from 'deny'
496    false;
497validate_addr(_RemoteAddr, []) ->
498    false;
499validate_addr(RemoteAddr, [HostRegExp | Rest]) ->
500    case re:run(RemoteAddr, HostRegExp, [{capture, none}]) of
501	match ->
502	    true;
503	nomatch ->
504	    validate_addr(RemoteAddr,Rest)
505    end.
506
507deny({_,RemoteAddr}, _SocketType, _Socket,DirectoryData) ->
508    Hosts = proplists:get_value(deny_from, DirectoryData, none),
509    case validate_addr(RemoteAddr,Hosts) of
510	true ->
511	    {denied, ?NICE("Connection from your host is not allowed")};
512	false ->
513	    not_denied
514    end.
515
516
517directory_config_check(Directory, DirData) ->
518    case proplists:get_value(auth_type, DirData) of
519	plain ->
520	    check_filename_present(Directory,auth_user_file,DirData),
521	    check_filename_present(Directory,auth_group_file,DirData);
522	_ ->
523	    ok
524    end.
525check_filename_present(Dir,AuthFile,DirData) ->
526    case proplists:get_value(AuthFile,DirData) of
527	Name when is_list(Name) ->
528	    ok;
529	_ ->
530	    throw({missing_auth_file, AuthFile, {directory, {Dir, DirData}}})
531    end.
532
533store_directory(Directory0, DirData0, ConfigList) ->
534    Port = proplists:get_value(port, ConfigList),
535    DirData = case proplists:get_value(bind_address, ConfigList) of
536		  undefined ->
537		    [{port, Port}|DirData0];
538		Addr ->
539		    [{port, Port},{bind_address,Addr}|DirData0]
540	    end,
541    Directory =
542	case filename:pathtype(Directory0) of
543	    relative ->
544		SR = proplists:get_value(server_root, ConfigList),
545		filename:join(SR, Directory0);
546	    _ ->
547		Directory0
548	end,
549    AuthMod =
550	case proplists:get_value(auth_type, DirData0) of
551	    mnesia -> mod_auth_mnesia;
552	    dets ->   mod_auth_dets;
553	    plain ->  mod_auth_plain;
554	    _ ->      no_module_at_all
555	end,
556    case AuthMod of
557	no_module_at_all ->
558	    {ok, {directory, {Directory, DirData}}};
559	_ ->
560	    %% Check that there are a password or add a standard password:
561	    %% "NoPassword"
562	    %% In this way a user must select to use a noPassword
563	    Passwd =
564		case proplists:get_value(auth_access_password, DirData) of
565		    undefined ->
566			?NOPASSWORD;
567		    PassW ->
568			PassW
569		end,
570	    DirDataLast = lists:keydelete(auth_access_password,1,DirData),
571	    Server_root = proplists:get_value(server_root, ConfigList),
572	    case catch AuthMod:store_directory_data(Directory,
573						    DirDataLast,
574						    Server_root) of
575		ok ->
576		    add_auth_password(Directory, Passwd, ConfigList),
577		    {ok, {directory, {Directory, DirDataLast}}};
578		{ok, NewDirData} ->
579		    add_auth_password(Directory, Passwd, ConfigList),
580		    {ok, {directory, {Directory, NewDirData}}};
581		{error, Reason} ->
582		    {error, Reason};
583		Other ->
584		    {error, Other}
585	    end
586    end.
587
588add_auth_password(Dir, Pwd0, ConfigList) ->
589    Addr = proplists:get_value(bind_address, ConfigList),
590    Port = proplists:get_value(port, ConfigList),
591    Profile = proplists:get_value(profile, ConfigList, ?DEFAULT_PROFILE),
592    mod_auth_server:start(Addr, Port, Profile),
593    mod_auth_server:add_password(Addr, Port, Dir, Pwd0).
594
595%% Opt = [{port, Port},
596%%        {addr, Addr},
597%%        {dir,  Dir},
598%%        {authPassword, AuthPassword} | FunctionSpecificData]
599get_options(Opt, mandatory)->
600    case proplists:get_value(port, Opt, undefined) of
601	Port when is_integer(Port) ->
602	    case proplists:get_value(dir, Opt, undefined) of
603		Dir when is_list(Dir) ->
604		    Addr = proplists:get_value(addr, Opt,
605					       undefined),
606		    AuthPwd = proplists:get_value(authPassword, Opt,
607						  ?NOPASSWORD),
608		    {Addr, Port, Dir, AuthPwd};
609		_->
610		    {error, bad_dir}
611	    end;
612	_ ->
613	    {error, bad_dir}
614    end;
615
616%% FunctionSpecificData = {userData, UserData} | {password, Password}
617get_options(Opt, userData)->
618    case proplists:get_value(userData, Opt, undefined) of
619	undefined ->
620	    {error, no_userdata};
621	UserData ->
622	    case proplists:get_value(password, Opt, undefined) of
623		undefined->
624		    {error, no_password};
625		Pwd ->
626		    {UserData, Pwd}
627	    end
628    end.
629