%%   The contents of this file are subject to the Mozilla Public License
%%   Version 1.1 (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.mozilla.org/MPL/
%%
%%   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 Original Code is RabbitMQ.
%%
%%   The Initial Developers of the Original Code are LShift Ltd,
%%   Cohesive Financial Technologies LLC, and Rabbit Technologies Ltd.
%%
%%   Portions created before 22-Nov-2008 00:00:00 GMT by LShift Ltd,
%%   Cohesive Financial Technologies LLC, or Rabbit Technologies Ltd
%%   are Copyright (C) 2007-2008 LShift Ltd, Cohesive Financial
%%   Technologies LLC, and Rabbit Technologies Ltd.
%%
%%   Portions created by LShift Ltd are Copyright (C) 2007-2009 LShift
%%   Ltd. Portions created by Cohesive Financial Technologies LLC are
%%   Copyright (C) 2007-2009 Cohesive Financial Technologies
%%   LLC. Portions created by Rabbit Technologies Ltd are Copyright
%%   (C) 2007-2009 Rabbit Technologies Ltd.
%%
%%   All Rights Reserved.
%%
%%   Contributor(s): ______________________________________.
%%

-module(rabbit_access_control).
-include_lib("stdlib/include/qlc.hrl").
-include("rabbit.hrl").

-export([check_login/2, user_pass_login/2,
         check_vhost_access/2, check_resource_access/3]).
-export([add_user/2, delete_user/1, change_password/2, list_users/0,
         lookup_user/1]).
-export([add_vhost/1, delete_vhost/1, list_vhosts/0]).
-export([set_permissions/5, clear_permissions/2,
         list_vhost_permissions/1, list_user_permissions/1]).

%%----------------------------------------------------------------------------

-ifdef(use_specs).

-type(permission_atom() :: 'configure' | 'read' | 'write').

-spec(check_login/2 :: (binary(), binary()) -> user()).
-spec(user_pass_login/2 :: (username(), password()) -> user()).
-spec(check_vhost_access/2 :: (user(), vhost()) -> 'ok').
-spec(check_resource_access/3 ::
      (username(), r(atom()), permission_atom()) -> 'ok').
-spec(add_user/2 :: (username(), password()) -> 'ok').
-spec(delete_user/1 :: (username()) -> 'ok').
-spec(change_password/2 :: (username(), password()) -> 'ok').
-spec(list_users/0 :: () -> [username()]).
-spec(lookup_user/1 :: (username()) -> {'ok', user()} | not_found()).
-spec(add_vhost/1 :: (vhost()) -> 'ok').
-spec(delete_vhost/1 :: (vhost()) -> 'ok').
-spec(list_vhosts/0 :: () -> [vhost()]).
-spec(set_permissions/5 ::
      (username(), vhost(), regexp(), regexp(), regexp()) -> 'ok').
-spec(clear_permissions/2 :: (username(), vhost()) -> 'ok').
-spec(list_vhost_permissions/1 ::
      (vhost()) -> [{username(), regexp(), regexp(), regexp()}]).
-spec(list_user_permissions/1 ::
      (username()) -> [{vhost(), regexp(), regexp(), regexp()}]).

-endif.

%%----------------------------------------------------------------------------

%% SASL PLAIN, as used by the Qpid Java client and our clients. Also,
%% apparently, by OpenAMQ.
check_login(<<"PLAIN">>, Response) ->
    [User, Pass] = [list_to_binary(T) ||
                       T <- string:tokens(binary_to_list(Response), [0])],
    user_pass_login(User, Pass);
%% AMQPLAIN, as used by Qpid Python test suite. The 0-8 spec actually
%% defines this as PLAIN, but in 0-9 that definition is gone, instead
%% referring generically to "SASL security mechanism", i.e. the above.
check_login(<<"AMQPLAIN">>, Response) ->
    LoginTable = rabbit_binary_parser:parse_table(Response),
    case {lists:keysearch(<<"LOGIN">>, 1, LoginTable),
          lists:keysearch(<<"PASSWORD">>, 1, LoginTable)} of
        {{value, {_, longstr, User}},
         {value, {_, longstr, Pass}}} ->
            user_pass_login(User, Pass);
        _ ->
            %% Is this an information leak?
            rabbit_misc:protocol_error(
              access_refused,
              "AMQPPLAIN auth info ~w is missing LOGIN or PASSWORD field",
              [LoginTable])
    end;

check_login(Mechanism, _Response) ->
    rabbit_misc:protocol_error(
      access_refused, "unsupported authentication mechanism '~s'",
      [Mechanism]).

user_pass_login(User, Pass) ->
    ?LOGDEBUG("Login with user ~p pass ~p~n", [User, Pass]),
    case lookup_user(User) of
        {ok, U} ->
            if
                Pass == U#user.password -> U;
                true ->
                    rabbit_misc:protocol_error(
                      access_refused, "login refused for user '~s'", [User])
            end;
        {error, not_found} ->
            rabbit_misc:protocol_error(
              access_refused, "login refused for user '~s'", [User])
    end.

internal_lookup_vhost_access(Username, VHostPath) ->
    %% TODO: use dirty ops instead
    rabbit_misc:execute_mnesia_transaction(
      fun () ->
              case mnesia:read({rabbit_user_permission,
                                #user_vhost{username = Username,
                                            virtual_host = VHostPath}}) of
                  [] -> not_found;
                  [R] -> {ok, R}
              end
      end).

check_vhost_access(#user{username = Username}, VHostPath) ->
    ?LOGDEBUG("Checking VHost access for ~p to ~p~n", [Username, VHostPath]),
    case internal_lookup_vhost_access(Username, VHostPath) of
        {ok, _R} ->
            ok;
        not_found ->
            rabbit_misc:protocol_error(
              access_refused, "access to vhost '~s' refused for user '~s'",
              [VHostPath, Username])
    end.

permission_index(configure) -> #permission.configure;
permission_index(write)     -> #permission.write;
permission_index(read)      -> #permission.read.

check_resource_access(Username,
                      R = #resource{kind = exchange, name = <<"">>},
                      Permission) ->
    check_resource_access(Username,
                          R#resource{name = <<"amq.default">>},
                          Permission);
check_resource_access(_Username,
                      #resource{name = <<"amq.gen",_/binary>>},
                      _Permission) ->
    ok;
check_resource_access(Username,
                      R = #resource{virtual_host = VHostPath, name = Name},
                      Permission) ->
    Res = case mnesia:dirty_read({rabbit_user_permission,
                                  #user_vhost{username = Username,
                                              virtual_host = VHostPath}}) of
              [] ->
                  false;
              [#user_permission{permission = P}] ->
                  case regexp:match(
                         binary_to_list(Name),
                         binary_to_list(element(permission_index(Permission), P))) of
                      {match, _, _} -> true;
                      nomatch       -> false
                  end
          end,
    if Res  -> ok;
       true -> rabbit_misc:protocol_error(
                 access_refused, "access to ~s refused for user '~s'",
                 [rabbit_misc:rs(R), Username])
    end.

add_user(Username, Password) ->
    R = rabbit_misc:execute_mnesia_transaction(
          fun () ->
                  case mnesia:wread({rabbit_user, Username}) of
                      [] ->
                          ok = mnesia:write(rabbit_user,
                                            #user{username = Username,
                                                  password = Password},
                                            write);
                      _ ->
                          mnesia:abort({user_already_exists, Username})
                  end
          end),
    rabbit_log:info("Created user ~p~n", [Username]),
    R.

delete_user(Username) ->
    R = rabbit_misc:execute_mnesia_transaction(
          rabbit_misc:with_user(
            Username,
            fun () ->
                    ok = mnesia:delete({rabbit_user, Username}),
                    [ok = mnesia:delete_object(
                            rabbit_user_permission, R, write) ||
                        R <- mnesia:match_object(
                               rabbit_user_permission,
                               #user_permission{user_vhost = #user_vhost{
                                                  username = Username,
                                                  virtual_host = '_'},
                                                permission = '_'},
                               write)],
                    ok
            end)),
    rabbit_log:info("Deleted user ~p~n", [Username]),
    R.

change_password(Username, Password) ->
    R = rabbit_misc:execute_mnesia_transaction(
          rabbit_misc:with_user(
            Username,
            fun () ->
                    ok = mnesia:write(rabbit_user,
                                      #user{username = Username,
                                            password = Password},
                                      write)
            end)),
    rabbit_log:info("Changed password for user ~p~n", [Username]),
    R.

list_users() ->
    mnesia:dirty_all_keys(rabbit_user).

lookup_user(Username) ->
    rabbit_misc:dirty_read({rabbit_user, Username}).

add_vhost(VHostPath) ->
    R = rabbit_misc:execute_mnesia_transaction(
          fun () ->
                  case mnesia:wread({rabbit_vhost, VHostPath}) of
                      [] ->
                          ok = mnesia:write(rabbit_vhost,
                                            #vhost{virtual_host = VHostPath},
                                            write),
                          [rabbit_exchange:declare(
                             rabbit_misc:r(VHostPath, exchange, Name),
                             Type, true, false, []) ||
                              {Name,Type} <-
                                  [{<<"">>,           direct},
                                   {<<"amq.direct">>, direct},
                                   {<<"amq.topic">>,  topic},
                                   {<<"amq.match">>,  headers}, %% per 0-9-1 pdf
                                   {<<"amq.headers">>,  headers}, %% per 0-9-1 xml
                                   {<<"amq.fanout">>, fanout}]],
                          ok;
                      [_] ->
                          mnesia:abort({vhost_already_exists, VHostPath})
                  end
          end),
    rabbit_log:info("Added vhost ~p~n", [VHostPath]),
    R.

delete_vhost(VHostPath) ->
    %%FIXME: We are forced to delete the queues outside the TX below
    %%because queue deletion involves sending messages to the queue
    %%process, which in turn results in further mnesia actions and
    %%eventually the termination of that process.
    lists:foreach(fun (Q) ->
                          {ok,_} = rabbit_amqqueue:delete(Q, false, false)
                  end,
                  rabbit_amqqueue:list(VHostPath)),
    R = rabbit_misc:execute_mnesia_transaction(
          rabbit_misc:with_vhost(
            VHostPath,
            fun () ->
                    ok = internal_delete_vhost(VHostPath)
            end)),
    rabbit_log:info("Deleted vhost ~p~n", [VHostPath]),
    R.

internal_delete_vhost(VHostPath) ->
    lists:foreach(fun (#exchange{name=Name}) ->
                          ok = rabbit_exchange:delete(Name, false)
                  end,
                  rabbit_exchange:list(VHostPath)),
    lists:foreach(fun ({Username, _, _, _}) ->
                          ok = clear_permissions(Username, VHostPath)
                  end,
                  list_vhost_permissions(VHostPath)),
    ok = mnesia:delete({rabbit_vhost, VHostPath}),
    ok.

list_vhosts() ->
    mnesia:dirty_all_keys(rabbit_vhost).

validate_regexp(RegexpBin) ->
    Regexp = binary_to_list(RegexpBin),
    case regexp:parse(Regexp) of
        {ok, _}         -> ok;
        {error, Reason} -> throw({error, {invalid_regexp, Regexp, Reason}})
    end.

set_permissions(Username, VHostPath, ConfigurePerm, WritePerm, ReadPerm) ->
    lists:map(fun validate_regexp/1, [ConfigurePerm, WritePerm, ReadPerm]),
    rabbit_misc:execute_mnesia_transaction(
      rabbit_misc:with_user_and_vhost(
        Username, VHostPath,
        fun () -> ok = mnesia:write(
                         rabbit_user_permission,
                         #user_permission{user_vhost = #user_vhost{
                                            username = Username,
                                            virtual_host = VHostPath},
                                          permission = #permission{
                                            configure = ConfigurePerm,
                                            write = WritePerm,
                                            read = ReadPerm}},
                         write)
        end)).

clear_permissions(Username, VHostPath) ->
    rabbit_misc:execute_mnesia_transaction(
      rabbit_misc:with_user_and_vhost(
        Username, VHostPath,
        fun () ->
                ok = mnesia:delete({rabbit_user_permission,
                                    #user_vhost{username = Username,
                                                virtual_host = VHostPath}})
        end)).

list_vhost_permissions(VHostPath) ->
    [{Username, ConfigurePerm, WritePerm, ReadPerm} ||
        {Username, _, ConfigurePerm, WritePerm, ReadPerm} <-
            list_permissions(rabbit_misc:with_vhost(
                               VHostPath, match_user_vhost('_', VHostPath)))].

list_user_permissions(Username) ->
    [{VHostPath, ConfigurePerm, WritePerm, ReadPerm} ||
        {_, VHostPath, ConfigurePerm, WritePerm, ReadPerm} <-
            list_permissions(rabbit_misc:with_user(
                               Username, match_user_vhost(Username, '_')))].

list_permissions(QueryThunk) ->
    [{Username, VHostPath, ConfigurePerm, WritePerm, ReadPerm} ||
        #user_permission{user_vhost = #user_vhost{username = Username,
                                                  virtual_host = VHostPath},
                         permission = #permission{
                           configure = ConfigurePerm,
                           write = WritePerm,
                           read = ReadPerm}} <-
            %% TODO: use dirty ops instead
            rabbit_misc:execute_mnesia_transaction(QueryThunk)].

match_user_vhost(Username, VHostPath) ->
    fun () -> mnesia:match_object(
                rabbit_user_permission,
                #user_permission{user_vhost = #user_vhost{
                                   username = Username,
                                   virtual_host = VHostPath},
                                 permission = '_'},
                read)
    end.
