%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1998-2025. 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_plain).
-moduledoc false.

-include("httpd.hrl").
-include("mod_auth.hrl").
-include("httpd_internal.hrl").

-define(VMODULE,"AUTH_PLAIN").

%% Internal API
-export([store_directory_data/3]).
-export([get_user/2, 
	 list_group_members/2, 
	 add_user/2, 
	 add_group_member/3, 
	 list_users/1, 
	 delete_user/2, 
	 list_groups/1, 
	 delete_group_member/3, 
	 delete_group/2, 
	 remove/1]).

%%====================================================================
%% Internal application API
%%====================================================================	     

%% Storage format of users in the ets table:
%% {UserName, Password, UserData}
add_user(DirData, #httpd_user{username = User} = UStruct) ->
    PWDB = proplists:get_value(auth_user_file, DirData),
    Record = {User,
	      UStruct#httpd_user.password, 
	      UStruct#httpd_user.user_data}, 
    case ets:lookup(PWDB, User) of
	[{User, _SomePassword, _SomeData}] ->
	    {error, user_already_in_db};
	_ ->
	    ets:insert(PWDB, Record),
	    true
    end.

get_user(DirData, User) ->
    PWDB = proplists:get_value(auth_user_file, DirData),
    case ets:lookup(PWDB, User) of
	[{User, PassWd, Data}] ->
	    {ok, #httpd_user{username  = User, 
			     password  = PassWd, 
			     user_data = Data}};
	_Other ->
	    {error, no_such_user}
    end.

list_users(DirData) ->
    PWDB = proplists:get_value(auth_user_file, DirData),
    Records = ets:match(PWDB, '$1'), 
    {ok, lists:foldr(fun({User, _PassWd, _Data}, A) -> [User | A] end, 
		     [], lists:flatten(Records))}.

delete_user(DirData, UserName) ->
    PWDB = proplists:get_value(auth_user_file, DirData),
    case ets:lookup(PWDB, UserName) of
	[{UserName, _SomePassword, _SomeData}] ->
	    ets:delete(PWDB, UserName),
	    {ok, Groups}  = list_groups(DirData),
	    lists:foreach(fun(Group) -> 
				  delete_group_member(DirData, 
						      Group, UserName) 
			  end, Groups);
	_ ->
	    {error, no_such_user}
    end.

%% Storage of groups in the ets table:
%% {Group, UserList} where UserList is a list of strings.
add_group_member(DirData, Group, UserName) ->
    GDB = proplists:get_value(auth_group_file, DirData),
    case ets:lookup(GDB, Group) of
	[{Group, Users}] ->
	    case lists:member(UserName, Users) of
		true ->
		    true;
		false ->
		    ets:insert(GDB, {Group, [UserName|Users]}),
		    true
	    end;
	[] ->
	    ets:insert(GDB, {Group, [UserName]}),
	    true;
	Other ->
	    {error, Other}
    end.

list_group_members(DirData, Group) ->
    GDB = proplists:get_value(auth_group_file, DirData),
    case ets:lookup(GDB, Group) of
	[{Group, Users}] ->
	    {ok, Users};
	_ ->
	    {error, no_such_group}
    end.

list_groups(DirData) ->
    GDB = proplists:get_value(auth_group_file, DirData),
    Groups = ets:match(GDB, '$1'), 
    {ok, httpd_util:uniq(lists:foldr(fun({G, _}, A) -> [G|A] end,
				     [], lists:flatten(Groups)))}.

delete_group_member(DirData, Group, User) ->
    GDB = proplists:get_value(auth_group_file, DirData),
    case ets:lookup(GDB, Group) of
	[{Group, Users}] when is_list(Users) ->
	    case lists:member(User, Users) of
		true ->
		    ets:delete(GDB, Group),
		    ets:insert(GDB, {Group, lists:delete(User, Users)}),
		    true;
		false ->
		    {error, no_such_group_member}
	    end;
	_ ->
	    {error, no_such_group}
    end.

delete_group(DirData, Group) ->
    GDB = proplists:get_value(auth_group_file, DirData),
    case ets:lookup(GDB, Group) of
	[{Group, _Users}] ->
	    ets:delete(GDB, Group),
	    true;
	_ ->
	    {error, no_such_group}
    end.

store_directory_data(_Directory, DirData, Server_root) ->
    PWFile = absolute_file_name(auth_user_file, DirData, Server_root),
    GroupFile = absolute_file_name(auth_group_file, DirData, Server_root),
    case load_passwd(PWFile) of
	{ok, PWDB} ->
	    case load_group(GroupFile) of
		{ok, GRDB} ->
		    %% Address and port is included in the file names...
		    Addr = proplists:get_value(bind_address, DirData),
		    Port = proplists:get_value(port, DirData),
		    {ok, PasswdDB} = store_passwd(Addr,Port,PWDB),
		    {ok, GroupDB}  = store_group(Addr,Port,GRDB),
		    NDD1 = lists:keyreplace(auth_user_file, 1, DirData, 
					    {auth_user_file, PasswdDB}),
		    NDD2 = lists:keyreplace(auth_group_file, 1, NDD1, 
					    {auth_group_file, GroupDB}),
		    {ok, NDD2};
		Err ->
		    {error, Err}
	    end;
	Err2 ->
	    {error, Err2}
    end.

%% Deletes ets tables used by this auth mod.
remove(DirData) ->
    PWDB = proplists:get_value(auth_user_file, DirData),
    GDB = proplists:get_value(auth_group_file, DirData),
    ets:delete(PWDB),
    ets:delete(GDB).

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
%% Return the absolute path name of File_type. 
absolute_file_name(File_type, DirData, Server_root) ->
    Path = proplists:get_value(File_type, DirData),
    case filename:pathtype(Path) of
	relative ->
	    case Server_root of
		undefined ->
		    {error,
		     ?NICE(Path++
			   " is an invalid file name because "
			   "ServerRoot is not defined")};
		_ ->
		    filename:join(Server_root,Path)
	    end;
	_ ->
	    Path
    end.

store_group(Addr,Port,GroupList) ->
    %% Not a named table so not importante to add Profile to name
    Name = httpd_util:make_name("httpd_group",Addr,Port),
    GroupDB = ets:new(Name, [set, public]),
    store_group(GroupDB, GroupList).

store_group(GroupDB,[]) ->
    {ok, GroupDB};
store_group(GroupDB, [User|Rest]) ->
    ets:insert(GroupDB, User),
    store_group(GroupDB, Rest).

store_passwd(Addr,Port,PasswdList) ->
    %% Not a named table so not importante to add Profile to name
    Name = httpd_util:make_name("httpd_passwd",Addr,Port),
    PasswdDB = ets:new(Name, [set, public]),
    store_passwd(PasswdDB, PasswdList).

store_passwd(PasswdDB, []) ->
    {ok, PasswdDB};
store_passwd(PasswdDB, [User|Rest]) ->
    ets:insert(PasswdDB, User),
    store_passwd(PasswdDB, Rest).

parse_group(Stream, GroupList) ->
    Line =
	case io:get_line(Stream,'') of
	    eof ->
		eof;
	    String ->
		httpd_conf:white_space_clean(String)
	end,
    parse_group(Stream, GroupList, Line).

parse_group(Stream, GroupList, eof) ->
    file:close(Stream),
    {ok, GroupList};
parse_group(Stream, GroupList, "") ->
    parse_group(Stream, GroupList);
parse_group(Stream, GroupList, [$#|_]) ->
    parse_group(Stream, GroupList);
parse_group(Stream, GroupList, Line) ->      
    case re:split(Line, ":", [{return, list}]) of
	[Group,Users] ->
	    UserList = re:split(Users," ", [{return, list}]),
	    parse_group(Stream, [{Group,UserList}|GroupList]);
	_ ->
	    {error, ?NICE(Line)}
    end.

load_passwd(AuthUserFile) ->
    case file:open(AuthUserFile, [read]) of
	{ok,Stream} ->
	    parse_passwd(Stream, []);
	{error, _} ->
	    {error, ?NICE("Can't open " ++ AuthUserFile)}
    end.

parse_passwd(Stream, PasswdList) ->
    Line =
	case io:get_line(Stream, '') of
	    eof ->
		eof;
	    String ->
		httpd_conf:white_space_clean(String)
	end,
    parse_passwd(Stream, PasswdList, Line).

parse_passwd(Stream, PasswdList, eof) ->
    file:close(Stream),
    {ok, PasswdList};
parse_passwd(Stream, PasswdList, "") ->
    parse_passwd(Stream, PasswdList);
parse_passwd(Stream, PasswdList, [$#|_]) ->
    parse_passwd(Stream, PasswdList);
parse_passwd(Stream, PasswdList, Line) ->      
    case re:split(Line,":", [{return, list}]) of
	[User,Password] ->
	    parse_passwd(Stream, [{User,Password, []}|PasswdList]);
	_ ->
	    {error, ?NICE(Line)}
    end.

load_group(AuthGroupFile) ->
    case file:open(AuthGroupFile, [read]) of
	{ok, Stream} ->
	    parse_group(Stream,[]);
	{error, _} ->
	    {error, ?NICE("Can't open " ++ AuthGroupFile)}
    end.
