%% ``The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved via the world wide web at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%%
%% $Id$
%%
-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").
%% We will not make the change to use base64 in stdlib in inets just yet.
%% it will be included in the next major release of inets.
-compile({nowarn_deprecated_function, {http_base_64, encode, 1}}).
-define(VMODULE,"AUTH").
-define(NOPASSWORD,"NoPassword").
%% do
do(Info) ->
case httpd_util:key1search(Info#mod.data,status) of
%% A status code has been generated!
{_StatusCode, _PhraseArgs, _Reason} ->
{proceed, Info#mod.data};
%% No status code has been generated!
undefined ->
case httpd_util:key1search(Info#mod.data,response) 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} ->
%% Authenticate (allow)
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 httpd_util:key1search(
DirectoryData,
auth_type) 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.
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
require(Info, Directory, DirectoryData) ->
ParsedHeader = Info#mod.parsed_header,
ValidUsers = httpd_util:key1search(DirectoryData, require_user),
ValidGroups = httpd_util:key1search(DirectoryData, require_group),
%% Any user or group restrictions?
case ValidGroups of
undefined when ValidUsers == undefined ->
authorized;
_ ->
case httpd_util:key1search(ParsedHeader, "authorization") of
undefined ->
authorization_required(DirectoryData);
%% Check credentials!
"Basic" ++ EncodedString = Credentials ->
case (catch http_base_64:decode(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 httpd_util:key1search(DirectoryData, auth_name) 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;
_ ->
{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 httpd_util:key1search(DirData, auth_type, plain) of
plain -> mod_auth_plain;
mnesia -> mod_auth_mnesia;
dets -> mod_auth_dets
end.
%%
%% Is it a secret area?
%%
%% secretp
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 regexp:match(Path, NewDirectory) of
{match, _, _} when Directory == to_be_found ->
secret_path(Path, Rest, NewDirectory);
{match, _, Length} when Length > length(Directory)->
secret_path(Path, Rest,NewDirectory);
{match, _, _Length} ->
secret_path(Path, Rest, Directory);
nomatch ->
secret_path(Path, Rest, Directory)
end.
%%
%% Authenticate
%%
%% allow
allow({_,RemoteAddr}, _SocketType, _Socket, DirectoryData) ->
Hosts = httpd_util:key1search(DirectoryData, allow_from, 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 regexp:match(RemoteAddr, HostRegExp) of
{match,_,_} ->
true;
nomatch ->
validate_addr(RemoteAddr,Rest)
end.
%% deny
deny({_,RemoteAddr}, _SocketType, _Socket,DirectoryData) ->
Hosts = httpd_util:key1search(DirectoryData, deny_from, none),
case validate_addr(RemoteAddr,Hosts) of
true ->
{denied, ?NICE("Connection from your host is not allowed")};
false ->
not_denied
end.
%%
%% Configuration
%%
%% load/2
%%
%% 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 = httpd_conf:custom_clean(Directory,"",">"),
{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, httpd_conf:clean(AuthName)}|DirData]} | Rest ]};
load("AuthUserFile " ++ AuthUserFile0,
[{directory, Directory, DirData}|Rest]) ->
AuthUserFile = httpd_conf:clean(AuthUserFile0),
{ok,[{directory,Directory,
[ {auth_user_file, AuthUserFile}|DirData]} | Rest ]};
load([$A,$u,$t,$h,$G,$r,$o,$u,$p,$F,$i,$l,$e,$ |AuthGroupFile0],
[{directory,Directory, DirData}|Rest]) ->
AuthGroupFile = httpd_conf:clean(AuthGroupFile0),
{ok,[{directory,Directory,
[ {auth_group_file, AuthGroupFile}|DirData]} | Rest]};
%AuthAccessPassword
load("AuthAccessPassword " ++ AuthAccessPassword0,
[{directory,Directory, DirData}|Rest]) ->
AuthAccessPassword = httpd_conf:clean(AuthAccessPassword0),
{ok,[{directory,Directory,
[{auth_access_password, AuthAccessPassword}|DirData]} | Rest]};
load("AuthDBType " ++ Type,
[{directory, Dir, DirData}|Rest]) ->
case httpd_conf:clean(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(httpd_conf:clean(Type)++" is an invalid AuthDBType")}
end;
load("require " ++ Require,[{directory,Directory, DirData}|Rest]) ->
case regexp:split(Require," ") of
{ok,["user"|Users]} ->
{ok,[{directory,Directory,
[{require_user,Users}|DirData]} | Rest]};
{ok,["group"|Groups]} ->
{ok,[{directory,Directory,
[{require_group,Groups}|DirData]} | Rest]};
{ok,_} ->
{error,?NICE(httpd_conf:clean(Require) ++" is an invalid require")}
end;
load("allow " ++ Allow,[{directory,Directory, DirData}|Rest]) ->
case regexp:split(Allow," ") of
{ok,["from","all"]} ->
{ok,[{directory,Directory,
[{allow_from,all}|DirData]} | Rest]};
{ok,["from"|Hosts]} ->
{ok,[{directory,Directory,
[{allow_from,Hosts}|DirData]} | Rest]};
{ok,_} ->
{error,?NICE(httpd_conf:clean(Allow) ++" is an invalid allow")}
end;
load("deny " ++ Deny,[{directory,Directory, DirData}|Rest]) ->
case regexp:split(Deny," ") of
{ok, ["from", "all"]} ->
{ok,[{directory, Directory,
[{deny_from, all}|DirData]} | Rest]};
{ok, ["from"|Hosts]} ->
{ok,[{directory, Directory,
[{deny_from, Hosts}|DirData]} | Rest]};
{ok, _} ->
{error,?NICE(httpd_conf:clean(Deny) ++" is an invalid deny")}
end;
load("",[{directory,Directory, DirData}|Rest]) ->
directory_config_check(Directory, DirData),
{ok, Rest, {directory, Directory, DirData}};
load("AuthMnesiaDB " ++ AuthMnesiaDB,
[{directory, Dir, DirData}|Rest]) ->
case httpd_conf:clean(AuthMnesiaDB) of
"On" ->
{ok,[{directory,Dir,[{auth_type,mnesia}|DirData]}|Rest]};
"Off" ->
{ok,[{directory,Dir,[{auth_type,plain}|DirData]}|Rest]};
_ ->
{error, ?NICE(httpd_conf:clean(AuthMnesiaDB) ++
" is an invalid AuthMnesiaDB")}
end.
directory_config_check(Directory, DirData) ->
case httpd_util:key1search(DirData,auth_type) of
plain ->
check_filename_present(Directory,auth_user_file,DirData),
check_filename_present(Directory,auth_group_file,DirData);
undefined ->
throw({error,
?NICE("Server configuration missed AuthDBType directive")});
_ ->
ok
end.
check_filename_present(_Dir,AuthFile,DirData) ->
case httpd_util:key1search(DirData,AuthFile) of
Name when list(Name) ->
ok;
_ ->
throw({error,?NICE("Server configuration missed "++
directive(AuthFile)++" directive")})
end.
directive(auth_user_file) ->
"AuthUserFile";
directive(auth_group_file) ->
"AuthGroupFile".
%% store
store({directory,Directory0, DirData0}, ConfigList) ->
Port = httpd_util:key1search(ConfigList, port),
DirData = case httpd_util:key1search(ConfigList, bind_address) of
undefined ->
[{port, Port}|DirData0];
Addr ->
[{port, Port},{bind_address,Addr}|DirData0]
end,
Directory =
case filename:pathtype(Directory0) of
relative ->
SR = httpd_util:key1search(ConfigList, server_root),
filename:join(SR, Directory0);
_ ->
Directory0
end,
AuthMod =
case httpd_util:key1search(DirData0, auth_type) 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}};
_ ->
%% Control that there are a password or add a standard password:
%% "NoPassword"
%% In this way a user must select to use a noPassword
Pwd = case httpd_util:key1search(DirData,auth_access_password)of
undefined->
?NOPASSWORD;
PassW->
PassW
end,
DirDataLast = lists:keydelete(auth_access_password,1,DirData),
case catch AuthMod:store_directory_data(Directory, DirDataLast) of
ok ->
add_auth_password(Directory,Pwd,ConfigList),
{ok, {directory, Directory, DirDataLast}};
{ok, NewDirData} ->
add_auth_password(Directory,Pwd,ConfigList),
{ok, {directory, Directory, NewDirData}};
{error, Reason} ->
{error, Reason};
Other ->
{error, Other}
end
end.
add_auth_password(Dir, Pwd0, ConfigList) ->
Addr = httpd_util:key1search(ConfigList, bind_address),
Port = httpd_util:key1search(ConfigList, port),
mod_auth_server:start(Addr, Port),
mod_auth_server:add_password(Addr, Port, Dir, Pwd0).
%% remove
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 = case lookup(ConfigDB, bind_address) of
[] ->
undefined;
[{bind_address, Address}] ->
Address
end,
[{port, Port}] = lookup(ConfigDB, port),
mod_auth_server:stop(Addr, Port),
ok.
%% --------------------------------------------------------------------
%% update_password
update_password(Port, Dir, Old, New, New)->
update_password(undefined, Port, Dir, Old, New, New).
update_password(Addr, Port, Dir, Old, New, New) when 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}.
%% add_user
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
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
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
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
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
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
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
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
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).
%% Opt = [{port, Port},
%% {addr, Addr},
%% {dir, Dir},
%% {authPassword, AuthPassword} | FunctionSpecificData]
get_options(Opt, mandatory)->
case httpd_util:key1search(Opt, port, undefined) of
Port when integer(Port) ->
case httpd_util:key1search(Opt, dir, undefined) of
Dir when list(Dir) ->
Addr = httpd_util:key1search(Opt,
addr,
undefined),
AuthPwd = httpd_util:key1search(Opt,
authPassword,
?NOPASSWORD),
{Addr, Port, Dir, AuthPwd};
_->
{error, bad_dir}
end;
_ ->
{error, bad_dir}
end;
%% FunctionSpecificData = {userData, UserData} | {password, Password}
get_options(Opt, userData)->
case httpd_util:key1search(Opt, userData, undefined) of
undefined ->
{error, no_userdata};
UserData ->
case httpd_util:key1search(Opt, password, undefined) of
undefined->
{error, no_password};
Pwd ->
{UserData, Pwd}
end
end.
lookup(Db, Key) ->
ets:lookup(Db, Key).