%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2016. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
-module(mod_auth).
%% The functions that the webbserver call on startup stop
%% and when the server traverse the modules.
-export([do/1, load/2, store/2, remove/1]).
%% User entries to the gen-server.
-export([add_user/2, add_user/5, add_user/6,
add_group_member/3, add_group_member/4, add_group_member/5,
list_users/1, list_users/2, list_users/3,
delete_user/2, delete_user/3, delete_user/4,
delete_group_member/3, delete_group_member/4, delete_group_member/5,
list_groups/1, list_groups/2, list_groups/3,
delete_group/2, delete_group/3, delete_group/4,
get_user/2, get_user/3, get_user/4,
list_group_members/2, list_group_members/3, list_group_members/4,
update_password/6, update_password/5]).
-include("httpd.hrl").
-include("mod_auth.hrl").
-include("httpd_internal.hrl").
-define(VMODULE,"AUTH").
-define(NOPASSWORD,"NoPassword").
%%====================================================================
%% Internal application API
%%====================================================================
do(Info) ->
case proplists:get_value(status,Info#mod.data) of
%% A status code has been generated!
{_StatusCode, _PhraseArgs, _Reason} ->
{proceed, Info#mod.data};
%% No status code has been generated!
undefined ->
case proplists:get_value(response, Info#mod.data) of
%% No response has been generated!
undefined ->
Path = mod_alias:path(Info#mod.data,Info#mod.config_db,
Info#mod.request_uri),
%% Is it a secret area?
case secretp(Path,Info#mod.config_db) of
{yes, {Directory, DirectoryData}} ->
case allow((Info#mod.init_data)#init_data.peername,
Info#mod.socket_type,Info#mod.socket,
DirectoryData) of
allowed ->
case deny((Info#mod.init_data)#init_data.peername,
Info#mod.socket_type,
Info#mod.socket,
DirectoryData) of
not_denied ->
case proplists:get_value(auth_type,
DirectoryData) of
undefined ->
{proceed, Info#mod.data};
none ->
{proceed, Info#mod.data};
AuthType ->
do_auth(Info,
Directory,
DirectoryData,
AuthType)
end;
{denied, Reason} ->
{proceed,
[{status, {403,
Info#mod.request_uri,
Reason}}|
Info#mod.data]}
end;
{not_allowed, Reason} ->
{proceed,[{status,{403,
Info#mod.request_uri,
Reason}} |
Info#mod.data]}
end;
no ->
{proceed, Info#mod.data}
end;
%% A response has been generated or sent!
_Response ->
{proceed, Info#mod.data}
end
end.
%% mod_auth recognizes the following Configuration Directives:
%%
%% AuthDBType
%% AuthName
%% AuthUserFile
%% AuthGroupFile
%% AuthAccessPassword
%% require
%% allow
%%
%% When a directive is found, a new context is set to
%% [{directory, Directory, DirData}|OtherContext]
%% DirData in this case is a key-value list of data belonging to the
%% directory in question.
%%
%% When the statement is found, the Context created earlier
%% will be returned as a ConfigList and the context will return to the
%% state it was previously.
load("
Dir = string:strip(string:strip(Directory),right, $>),
{ok,[{directory, {Dir, [{path, Dir}]}}]};
load(eof,[{directory, {Directory, _DirData}}|_]) ->
{error, ?NICE("Premature end-of-file in "++ Directory)};
load("AuthName " ++ AuthName, [{directory, {Directory, DirData}}|Rest]) ->
{ok, [{directory, {Directory,
[{auth_name, string:strip(AuthName)} | DirData]}}
| Rest ]};
load("AuthUserFile " ++ AuthUserFile0,
[{directory, {Directory, DirData}}|Rest]) ->
AuthUserFile = string:strip(AuthUserFile0),
{ok, [{directory, {Directory,
[{auth_user_file, AuthUserFile}|DirData]}} | Rest ]};
load("AuthGroupFile " ++ AuthGroupFile0,
[{directory, {Directory, DirData}}|Rest]) ->
AuthGroupFile = string:strip(AuthGroupFile0),
{ok,[{directory, {Directory,
[{auth_group_file, AuthGroupFile}|DirData]}} | Rest]};
load("AuthAccessPassword " ++ AuthAccessPassword0,
[{directory, {Directory, DirData}}|Rest]) ->
AuthAccessPassword = string:strip(AuthAccessPassword0),
{ok,[{directory, {Directory,
[{auth_access_password, AuthAccessPassword}|DirData]}} | Rest]};
load("AuthDBType " ++ Type,
[{directory, {Dir, DirData}}|Rest]) ->
case string:strip(Type) of
"plain" ->
{ok, [{directory, {Dir, [{auth_type, plain}|DirData]}} | Rest ]};
"mnesia" ->
{ok, [{directory, {Dir, [{auth_type, mnesia}|DirData]}} | Rest ]};
"dets" ->
{ok, [{directory, {Dir, [{auth_type, dets}|DirData]}} | Rest ]};
_ ->
{error, ?NICE(string:strip(Type)++" is an invalid AuthDBType")}
end;
load("require " ++ Require,[{directory, {Directory, DirData}}|Rest]) ->
case re:split(Require," ", [{return, list}]) of
["user" | Users] ->
{ok,[{directory, {Directory,
[{require_user,Users}|DirData]}} | Rest]};
["group"|Groups] ->
{ok,[{directory, {Directory,
[{require_group,Groups}|DirData]}} | Rest]};
_ ->
{error,?NICE(string:strip(Require) ++" is an invalid require")}
end;
load("allow " ++ Allow,[{directory, {Directory, DirData}}|Rest]) ->
case re:split(Allow," ", [{return, list}]) of
["from","all"] ->
{ok,[{directory, {Directory,
[{allow_from,all}|DirData]}} | Rest]};
["from"|Hosts] ->
{ok,[{directory, {Directory,
[{allow_from,Hosts}|DirData]}} | Rest]};
_ ->
{error,?NICE(string:strip(Allow) ++" is an invalid allow")}
end;
load("deny " ++ Deny,[{directory, {Directory, DirData}}|Rest]) ->
case re:split(Deny," ", [{return, list}]) of
["from", "all"] ->
{ok,[{{directory, Directory,
[{deny_from, all}|DirData]}} | Rest]};
["from"|Hosts] ->
{ok,[{{directory, Directory,
[{deny_from, Hosts}|DirData]}} | Rest]};
_ ->
{error,?NICE(string:strip(Deny) ++" is an invalid deny")}
end;
load("",[{directory, {Directory, DirData}}|Rest]) ->
{ok, Rest, {directory, {Directory, DirData}}};
load("AuthMnesiaDB " ++ AuthMnesiaDB,
[{directory, {Dir, DirData}}|Rest]) ->
case string:strip(AuthMnesiaDB) of
"On" ->
{ok,[{directory, {Dir,[{auth_type,mnesia}|DirData]}}|Rest]};
"Off" ->
{ok,[{directory, {Dir,[{auth_type,plain}|DirData]}}|Rest]};
_ ->
{error, ?NICE(string:strip(AuthMnesiaDB) ++
" is an invalid AuthMnesiaDB")}
end.
store({directory, {Directory, DirData}}, ConfigList)
when is_list(Directory) andalso is_list(DirData) ->
try directory_config_check(Directory, DirData) of
ok ->
store_directory(Directory, DirData, ConfigList)
catch
throw:Error ->
{error, Error, {directory, Directory, DirData}}
end;
store({directory, {Directory, DirData}}, _) ->
{error, {wrong_type, {directory, {Directory, DirData}}}}.
remove(ConfigDB) ->
lists:foreach(fun({directory, {_Dir, DirData}}) ->
AuthMod = auth_mod_name(DirData),
(catch apply(AuthMod, remove, [DirData]))
end,
ets:match_object(ConfigDB,{directory,{'_','_'}})),
Addr = httpd_util:lookup(ConfigDB, bind_address, undefined),
Port = httpd_util:lookup(ConfigDB, port),
Profile = httpd_util:lookup(ConfigDB, profile, ?DEFAULT_PROFILE),
mod_auth_server:stop(Addr, Port, Profile),
ok.
add_user(UserName, Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd}->
case get_options(Opt, userData) of
{error, Reason}->
{error, Reason};
{UserData, Password}->
User = [#httpd_user{username = UserName,
password = Password,
user_data = UserData}],
mod_auth_server:add_user(Addr, Port, Dir, User, AuthPwd)
end
end.
add_user(UserName, Password, UserData, Port, Dir) ->
add_user(UserName, Password, UserData, undefined, Port, Dir).
add_user(UserName, Password, UserData, Addr, Port, Dir) ->
User = [#httpd_user{username = UserName,
password = Password,
user_data = UserData}],
mod_auth_server:add_user(Addr, Port, Dir, User, ?NOPASSWORD).
get_user(UserName, Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:get_user(Addr, Port, Dir, UserName, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
get_user(UserName, Port, Dir) ->
get_user(UserName, undefined, Port, Dir).
get_user(UserName, Addr, Port, Dir) ->
mod_auth_server:get_user(Addr, Port, Dir, UserName, ?NOPASSWORD).
add_group_member(GroupName, UserName, Opt)->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd}->
mod_auth_server:add_group_member(Addr, Port, Dir,
GroupName, UserName, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
add_group_member(GroupName, UserName, Port, Dir) ->
add_group_member(GroupName, UserName, undefined, Port, Dir).
add_group_member(GroupName, UserName, Addr, Port, Dir) ->
mod_auth_server:add_group_member(Addr, Port, Dir,
GroupName, UserName, ?NOPASSWORD).
delete_group_member(GroupName, UserName, Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:delete_group_member(Addr, Port, Dir,
GroupName, UserName, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
delete_group_member(GroupName, UserName, Port, Dir) ->
delete_group_member(GroupName, UserName, undefined, Port, Dir).
delete_group_member(GroupName, UserName, Addr, Port, Dir) ->
mod_auth_server:delete_group_member(Addr, Port, Dir,
GroupName, UserName, ?NOPASSWORD).
list_users(Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:list_users(Addr, Port, Dir, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
list_users(Port, Dir) ->
list_users(undefined, Port, Dir).
list_users(Addr, Port, Dir) ->
mod_auth_server:list_users(Addr, Port, Dir, ?NOPASSWORD).
delete_user(UserName, Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:delete_user(Addr, Port, Dir, UserName, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
delete_user(UserName, Port, Dir) ->
delete_user(UserName, undefined, Port, Dir).
delete_user(UserName, Addr, Port, Dir) ->
mod_auth_server:delete_user(Addr, Port, Dir, UserName, ?NOPASSWORD).
delete_group(GroupName, Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:delete_group(Addr, Port, Dir, GroupName, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
delete_group(GroupName, Port, Dir) ->
delete_group(GroupName, undefined, Port, Dir).
delete_group(GroupName, Addr, Port, Dir) ->
mod_auth_server:delete_group(Addr, Port, Dir, GroupName, ?NOPASSWORD).
list_groups(Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:list_groups(Addr, Port, Dir, AuthPwd);
{error, Reason} ->
{error, Reason}
end.
list_groups(Port, Dir) ->
list_groups(undefined, Port, Dir).
list_groups(Addr, Port, Dir) ->
mod_auth_server:list_groups(Addr, Port, Dir, ?NOPASSWORD).
list_group_members(GroupName, Opt) ->
case get_options(Opt, mandatory) of
{Addr, Port, Dir, AuthPwd} ->
mod_auth_server:list_group_members(Addr, Port, Dir, GroupName,
AuthPwd);
{error, Reason} ->
{error, Reason}
end.
list_group_members(GroupName, Port, Dir) ->
list_group_members(GroupName, undefined, Port, Dir).
list_group_members(GroupName, Addr, Port, Dir) ->
mod_auth_server:list_group_members(Addr, Port, Dir,
GroupName, ?NOPASSWORD).
update_password(Port, Dir, Old, New, New)->
update_password(undefined, Port, Dir, Old, New, New).
update_password(Addr, Port, Dir, Old, New, New) when is_list(New) ->
mod_auth_server:update_password(Addr, Port, Dir, Old, New);
update_password(_Addr, _Port, _Dir, _Old, _New, _New) ->
{error, badtype};
update_password(_Addr, _Port, _Dir, _Old, _New, _New1) ->
{error, notqeual}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
do_auth(Info, Directory, DirectoryData, _AuthType) ->
%% Authenticate (require)
case require(Info, Directory, DirectoryData) of
authorized ->
{proceed,Info#mod.data};
{authorized, User} ->
{proceed, [{remote_user,User}|Info#mod.data]};
{authorization_required, Realm} ->
ReasonPhrase = httpd_util:reason_phrase(401),
Message = httpd_util:message(401,none,Info#mod.config_db),
{proceed,
[{response,
{401,
["WWW-Authenticate: Basic realm=\"",Realm,
"\"\r\n\r\n","\n
\n",
ReasonPhrase,"\n",
"\n\n",ReasonPhrase,
"
\n",Message,"\n\n\n"]}}|
Info#mod.data]};
{status, {StatusCode,PhraseArgs,Reason}} ->
{proceed, [{status,{StatusCode,PhraseArgs,Reason}}|
Info#mod.data]}
end.
require(Info, Directory, DirectoryData) ->
ParsedHeader = Info#mod.parsed_header,
ValidUsers = proplists:get_value(require_user, DirectoryData),
ValidGroups = proplists:get_value(require_group, DirectoryData),
%% Any user or group restrictions?
case ValidGroups of
undefined when ValidUsers =:= undefined ->
authorized;
_ ->
case proplists:get_value("authorization", ParsedHeader) of
undefined ->
authorization_required(DirectoryData);
%% Check credentials!
"Basic" ++ EncodedString = Credentials ->
case (catch base64:decode_to_string(EncodedString)) of
{'EXIT',{function_clause, _}} ->
{status, {401, none, ?NICE("Bad credentials "++
Credentials)}};
DecodedString ->
validate_user(Info, Directory, DirectoryData,
ValidUsers, ValidGroups,
DecodedString)
end;
%% Bad credentials!
BadCredentials ->
{status, {401, none, ?NICE("Bad credentials "++
BadCredentials)}}
end
end.
authorization_required(DirectoryData) ->
case proplists:get_value(auth_name, DirectoryData) of
undefined ->
{status,{500, none,?NICE("AuthName directive not specified")}};
Realm ->
{authorization_required, Realm}
end.
validate_user(Info, Directory, DirectoryData, ValidUsers,
ValidGroups, DecodedString) ->
case a_valid_user(Info, DecodedString,
ValidUsers, ValidGroups,
Directory, DirectoryData) of
{yes, User} ->
{authorized, User};
{no, _Reason} ->
authorization_required(DirectoryData);
{status, {StatusCode,PhraseArgs,Reason}} ->
{status,{StatusCode,PhraseArgs,Reason}}
end.
a_valid_user(Info,DecodedString,ValidUsers,ValidGroups,Dir,DirData) ->
case httpd_util:split(DecodedString,":",2) of
{ok, [SupposedUser, Password]} ->
case user_accepted(SupposedUser, ValidUsers) of
true ->
check_password(SupposedUser, Password, Dir, DirData);
false ->
case group_accepted(Info,SupposedUser,
ValidGroups,Dir,DirData) of
true ->
check_password(SupposedUser,Password,Dir,DirData);
false ->
{no,?NICE("No such user exists")}
end
end;
{ok, BadCredentials} ->
{status,{401,none,?NICE("Bad credentials "++BadCredentials)}}
end.
user_accepted(_SupposedUser, undefined) ->
false;
user_accepted(SupposedUser, ValidUsers) ->
lists:member(SupposedUser, ValidUsers).
group_accepted(_Info, _User, undefined, _Dir, _DirData) ->
false;
group_accepted(_Info, _User, [], _Dir, _DirData) ->
false;
group_accepted(Info, User, [Group|Rest], Dir, DirData) ->
Ret = int_list_group_members(Group, Dir, DirData),
case Ret of
{ok, UserList} ->
case lists:member(User, UserList) of
true ->
true;
false ->
group_accepted(Info, User, Rest, Dir, DirData)
end;
_ ->
false
end.
check_password(User, Password, _Dir, DirData) ->
case int_get_user(DirData, User) of
{ok, UStruct} ->
case UStruct#httpd_user.password of
Password ->
%% FIXME
{yes, UStruct#httpd_user.username};
_ ->
{no, "No such user"} % Don't say 'Bad Password' !!!
end;
_Other ->
{no, "No such user"}
end.
%% Middle API. Theese functions call the appropriate authentication module.
int_get_user(DirData, User) ->
AuthMod = auth_mod_name(DirData),
apply(AuthMod, get_user, [DirData, User]).
int_list_group_members(Group, _Dir, DirData) ->
AuthMod = auth_mod_name(DirData),
apply(AuthMod, list_group_members, [DirData, Group]).
auth_mod_name(DirData) ->
case proplists:get_value(auth_type, DirData, plain) of
plain -> mod_auth_plain;
mnesia -> mod_auth_mnesia;
dets -> mod_auth_dets
end.
secretp(Path,ConfigDB) ->
Directories = ets:match(ConfigDB,{directory, {'$1','_'}}),
case secret_path(Path, Directories) of
{yes,Directory} ->
{yes, {Directory,
lists:flatten(
ets:match(ConfigDB,{directory, {Directory,'$1'}}))}};
no ->
no
end.
secret_path(Path, Directories) ->
secret_path(Path, httpd_util:uniq(lists:sort(Directories)),to_be_found).
secret_path(_Path, [], to_be_found) ->
no;
secret_path(_Path, [], Directory) ->
{yes, Directory};
secret_path(Path, [[NewDirectory] | Rest], Directory) ->
case re:run(Path, NewDirectory, [{capture, first}]) of
{match, _} when Directory =:= to_be_found ->
secret_path(Path, Rest, NewDirectory);
{match, [{_, Length}]} when Length > length(Directory)->
secret_path(Path, Rest,NewDirectory);
{match, _} ->
secret_path(Path, Rest, Directory);
nomatch ->
secret_path(Path, Rest, Directory)
end.
allow({_,RemoteAddr}, _SocketType, _Socket, DirectoryData) ->
Hosts = proplists:get_value(allow_from, DirectoryData, all),
case validate_addr(RemoteAddr, Hosts) of
true ->
allowed;
false ->
{not_allowed, ?NICE("Connection from your host is not allowed")}
end.
validate_addr(_RemoteAddr, all) -> % When called from 'allow'
true;
validate_addr(_RemoteAddr, none) -> % When called from 'deny'
false;
validate_addr(_RemoteAddr, []) ->
false;
validate_addr(RemoteAddr, [HostRegExp | Rest]) ->
case re:run(RemoteAddr, HostRegExp, [{capture, none}]) of
match ->
true;
nomatch ->
validate_addr(RemoteAddr,Rest)
end.
deny({_,RemoteAddr}, _SocketType, _Socket,DirectoryData) ->
Hosts = proplists:get_value(deny_from, DirectoryData, none),
case validate_addr(RemoteAddr,Hosts) of
true ->
{denied, ?NICE("Connection from your host is not allowed")};
false ->
not_denied
end.
directory_config_check(Directory, DirData) ->
case proplists:get_value(auth_type, DirData) of
plain ->
check_filename_present(Directory,auth_user_file,DirData),
check_filename_present(Directory,auth_group_file,DirData);
_ ->
ok
end.
check_filename_present(Dir,AuthFile,DirData) ->
case proplists:get_value(AuthFile,DirData) of
Name when is_list(Name) ->
ok;
_ ->
throw({missing_auth_file, AuthFile, {directory, {Dir, DirData}}})
end.
store_directory(Directory0, DirData0, ConfigList) ->
Port = proplists:get_value(port, ConfigList),
DirData = case proplists:get_value(bind_address, ConfigList) of
undefined ->
[{port, Port}|DirData0];
Addr ->
[{port, Port},{bind_address,Addr}|DirData0]
end,
Directory =
case filename:pathtype(Directory0) of
relative ->
SR = proplists:get_value(server_root, ConfigList),
filename:join(SR, Directory0);
_ ->
Directory0
end,
AuthMod =
case proplists:get_value(auth_type, DirData0) of
mnesia -> mod_auth_mnesia;
dets -> mod_auth_dets;
plain -> mod_auth_plain;
_ -> no_module_at_all
end,
case AuthMod of
no_module_at_all ->
{ok, {directory, {Directory, DirData}}};
_ ->
%% Check that there are a password or add a standard password:
%% "NoPassword"
%% In this way a user must select to use a noPassword
Passwd =
case proplists:get_value(auth_access_password, DirData) of
undefined ->
?NOPASSWORD;
PassW ->
PassW
end,
DirDataLast = lists:keydelete(auth_access_password,1,DirData),
Server_root = proplists:get_value(server_root, ConfigList),
case catch AuthMod:store_directory_data(Directory,
DirDataLast,
Server_root) of
ok ->
add_auth_password(Directory, Passwd, ConfigList),
{ok, {directory, {Directory, DirDataLast}}};
{ok, NewDirData} ->
add_auth_password(Directory, Passwd, ConfigList),
{ok, {directory, {Directory, NewDirData}}};
{error, Reason} ->
{error, Reason};
Other ->
{error, Other}
end
end.
add_auth_password(Dir, Pwd0, ConfigList) ->
Addr = proplists:get_value(bind_address, ConfigList),
Port = proplists:get_value(port, ConfigList),
Profile = proplists:get_value(profile, ConfigList, ?DEFAULT_PROFILE),
mod_auth_server:start(Addr, Port, Profile),
mod_auth_server:add_password(Addr, Port, Dir, Pwd0).
%% Opt = [{port, Port},
%% {addr, Addr},
%% {dir, Dir},
%% {authPassword, AuthPassword} | FunctionSpecificData]
get_options(Opt, mandatory)->
case proplists:get_value(port, Opt, undefined) of
Port when is_integer(Port) ->
case proplists:get_value(dir, Opt, undefined) of
Dir when is_list(Dir) ->
Addr = proplists:get_value(addr, Opt,
undefined),
AuthPwd = proplists:get_value(authPassword, Opt,
?NOPASSWORD),
{Addr, Port, Dir, AuthPwd};
_->
{error, bad_dir}
end;
_ ->
{error, bad_dir}
end;
%% FunctionSpecificData = {userData, UserData} | {password, Password}
get_options(Opt, userData)->
case proplists:get_value(userData, Opt, undefined) of
undefined ->
{error, no_userdata};
UserData ->
case proplists:get_value(password, Opt, undefined) of
undefined->
{error, no_password};
Pwd ->
{UserData, Pwd}
end
end.