├── test
└── src
│ ├── mock_transport.erl
│ ├── rabbit_oauth2_auth_test.erl
│ └── rabbit_oauth2_backend_test.erl
├── .gitignore
├── src
├── rabbitmq_auth_backend_oauth.app.src
├── rabbit_auth_backend_oauth_app.erl
├── rabbit_oauth2_access_token.erl
├── rabbit_oauth2_scope.erl
├── rabbit_auth_backend_oauth.erl
├── rabbit_oauth2_storage.erl
├── rabbit_oauth2_backend.erl
└── rabbit_oauth2_auth.erl
├── .travis.yml
├── Makefile
├── templates
└── auth_form.dtl
├── README.md
└── rabbitmq-components.mk
/test/src/mock_transport.erl:
--------------------------------------------------------------------------------
1 | -module(mock_transport).
2 |
3 | -export([name/0, send/2]).
4 |
5 | name() -> mock.
6 |
7 | send(_,_) -> ok.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sw?
2 | .*.sw?
3 | *.beam
4 | /.erlang.mk/
5 | /cover/
6 | /deps/
7 | /doc/
8 | /ebin/
9 | /logs/
10 | /plugins/
11 |
12 | /rabbitmq_auth_backend_oauth.d
13 |
--------------------------------------------------------------------------------
/src/rabbitmq_auth_backend_oauth.app.src:
--------------------------------------------------------------------------------
1 | %% -*- erlang -*-
2 | {application, rabbitmq_auth_backend_oauth,
3 | [{description, "RabbitMQ OAuth2 Authentication Backend"},
4 | {vsn, ""},
5 | {modules, [rabbit_auth_backend_oauth_app, rabbit_auth_backend_oauth, rabbit_oauth2_backend]},
6 | {registered, []},
7 | {mod, {rabbit_auth_backend_oauth_app, []}},
8 | {env, [
9 | {auth_server,
10 | {internal, [{port, 15672}]}}]},
11 | {applications, [kernel, stdlib, inets, cowlib, cowboy, rabbitmq_web_dispatch]}]}.
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: erlang
3 | notifications:
4 | email:
5 | - alerts@rabbitmq.com
6 | addons:
7 | apt:
8 | packages:
9 | - xsltproc
10 | otp_release:
11 | - "R16B03-1"
12 | - "17.5"
13 | - "18.0"
14 |
15 | # The checkout made by Travis is a "detached HEAD". We switch back
16 | # to a tag or a branch. This pleases our git_rmq fetch method in
17 | # rabbitmq-components.mk and the proper tag/branch is selected in
18 | # dependencies too.
19 | before_script: (test "$TRAVIS_TAG" && git checkout "$TRAVIS_TAG") || (test "$TRAVIS_BRANCH" && git checkout "$TRAVIS_BRANCH")
20 |
21 | script: make tests
22 |
23 | cache:
24 | apt: true
25 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT = rabbitmq_auth_backend_oauth
2 |
3 | DEPS = cowboy rabbitmq_web_dispatch rabbit_common oauth2 erlydtl
4 | TEST_DEPS = rabbit
5 |
6 | DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk
7 |
8 | # FIXME: Use erlang.mk patched for RabbitMQ, while waiting for PRs to be
9 | # reviewed and merged.
10 |
11 | ERLANG_MK_REPO = https://github.com/rabbitmq/erlang.mk.git
12 | ERLANG_MK_COMMIT = rabbitmq-tmp
13 |
14 | include rabbitmq-components.mk
15 | include erlang.mk
16 |
17 |
18 | WITH_BROKER_TEST_COMMANDS:= \
19 | rabbit_oauth2_backend_test:broker_tests() \
20 | rabbit_oauth2_auth_test:tests()
21 |
22 | STANDALONE_TEST_COMMANDS := \
23 | rabbit_oauth2_backend_test:standalone_tests()
24 |
--------------------------------------------------------------------------------
/templates/auth_form.dtl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/rabbit_auth_backend_oauth_app.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_auth_backend_oauth_app).
2 |
3 |
4 | -behaviour(application).
5 | -export([start/2, stop/1, reset_dispatcher/1]).
6 |
7 | -behaviour(supervisor).
8 | -export([init/1]).
9 |
10 | -define(CONTEXT, rabbit_oauth).
11 |
12 | start(_Type, _StartArgs) ->
13 | {ok, AuthServer} = application:get_env(rabbitmq_auth_backend_oauth,
14 | auth_server),
15 | maybe_register_context(AuthServer, []),
16 | supervisor:start_link({local,?MODULE},?MODULE,[]).
17 |
18 | stop(_State) ->
19 | unregister_context(),
20 | ok.
21 |
22 | %% At the point at which this is invoked we have both newly enabled
23 | %% apps and about-to-disable apps running (so that
24 | %% rabbit_mgmt_reset_handler can look at all of them to find
25 | %% extensions). Therefore we have to explicitly exclude
26 | %% about-to-disable apps from our new dispatcher.
27 | reset_dispatcher(IgnoreApps) ->
28 | unregister_context(),
29 | {ok, AuthServer} = application:get_env(rabbitmq_auth_backend_oauth,
30 | auth_server),
31 | maybe_register_context(AuthServer, IgnoreApps).
32 |
33 | maybe_register_context(undefined, _IgnoreApps) -> ok;
34 | maybe_register_context([], _IgnoreApps) -> ok;
35 | maybe_register_context({AuthServerType, Listener}, _IgnoreApps) ->
36 | {Route, Description} = case AuthServerType of
37 | internal ->
38 | {[{'_', [{"/oauth", rabbit_oauth2_auth, []}]}],
39 | "RabbitMQ Oauth2 auth server"};
40 | external ->
41 | {[{'_', [{"/access_token", rabbit_oauth2_access_token, []}]}],
42 | "RabbitMQ Oauth2 access token endpoint"}
43 | end,
44 | % TODO log
45 | rabbit_web_dispatch:register_context_handler(
46 | ?CONTEXT, Listener, "",
47 | cowboy_router:compile(Route), Description).
48 |
49 | unregister_context() ->
50 | rabbit_web_dispatch:unregister_context(?CONTEXT).
51 |
52 | init([]) ->
53 | {ok, {{one_for_one,3,10},[]}}.
54 |
--------------------------------------------------------------------------------
/src/rabbit_oauth2_access_token.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_access_token).
2 |
3 | -export([
4 | init/3
5 | ,rest_init/2
6 | ,allowed_methods/2
7 | ]).
8 |
9 | -export([content_types_accepted/2]).
10 |
11 | -export([process_post/2]).
12 |
13 |
14 | %%%===================================================================
15 | %%% Cowboy callbacks
16 | %%%===================================================================
17 |
18 | init(_Transport, _Req, _Opts) ->
19 | %% Compile the DTL template used for the authentication
20 | %% form in the implicit grant flow.
21 | {upgrade, protocol, cowboy_rest}.
22 |
23 | rest_init(Req, _Opts) ->
24 | {ok, Req, undefined_state}.
25 |
26 | content_types_accepted(Req, State) ->
27 | {[{{<<"application">>, <<"json">>, []}, process_post},
28 | {{<<"application">>, <<"x-www-form-urlencoded">>, []}, process_post}],
29 | Req, State}.
30 |
31 | allowed_methods(Req, State) ->
32 | {[<<"POST">>], Req, State}.
33 |
34 | process_post(Req, State) ->
35 | {ok, Params, Req2} = cowboy_req:body_qs(Req),
36 | Token = proplists:get_value(<<"access_token">>, Params),
37 | Scope = binary:split(proplists:get_value(<<"scope">>, Params),
38 | <<" ">>, [global]),
39 | ExpiresIn = binary_to_integer(proplists:get_value(<<"expires_in">>,
40 | Params)),
41 | CreatedAt = proplists:get_value(<<"created_at">>, Params,
42 | time_compat:os_system_time(seconds)),
43 | % TODO: default scope
44 | {ok, Reply} = case Token == undefined
45 | orelse Scope == undefined
46 | orelse ExpiresIn == undefined of
47 | true -> cowboy_req:reply(400, [], <<"Bad Request.">>, Req2);
48 | false ->
49 | ok = rabbit_oauth2_backend:add_access_token(Token,
50 | Scope,
51 | ExpiresIn,
52 | CreatedAt),
53 | cowboy_req:reply(200, [], <<"Ok">>, Req2)
54 | end,
55 | {halt, Reply, State}.
--------------------------------------------------------------------------------
/src/rabbit_oauth2_scope.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_scope).
2 |
3 | -export([vhost_access/2, resource_access/3]).
4 | -export([parse_scope/1]).
5 |
6 | -include_lib("rabbit_common/include/rabbit.hrl").
7 |
8 | %% API functions --------------------------------------------------------------
9 |
10 | vhost_access(VHost, Ctx) ->
11 | lists:any(
12 | fun({#resource{ virtual_host = VH }, _}) ->
13 | VH == VHost
14 | end,
15 | get_scope_permissions(Ctx)).
16 |
17 | resource_access(Resource, Permission, Ctx) ->
18 | lists:any(
19 | fun({Res, Perm}) ->
20 | Res == Resource andalso Perm == Permission
21 | end,
22 | get_scope_permissions(Ctx)).
23 |
24 | %% Internal -------------------------------------------------------------------
25 |
26 | get_scope_permissions(Ctx) ->
27 | case lists:keyfind(<<"scope">>, 1, Ctx) of
28 | {_, Scope} ->
29 | [ {Res, Perm} || {Res, Perm, _ScopeEl} <- parse_scope(Scope) ];
30 | false -> []
31 | end.
32 |
33 | parse_scope(Scope) when is_list(Scope) ->
34 | lists:filtermap(
35 | fun(ScopeEl) ->
36 | case parse_scope_el(ScopeEl) of
37 | ignore -> false;
38 | Perm -> {true, Perm}
39 | end
40 | end,
41 | Scope).
42 |
43 | parse_scope_el(ScopeEl) when is_binary(ScopeEl) ->
44 | case binary:split(ScopeEl, <<"_">>, [global]) of
45 | [VHost, KindCode, PermCode | Name] ->
46 | Kind = case KindCode of
47 | <<"q">> -> queue;
48 | <<"ex">> -> exchange;
49 | <<"t">> -> topic;
50 | _ -> ignore
51 | end,
52 | Permission = case PermCode of
53 | <<"configure">> -> configure;
54 | <<"write">> -> write;
55 | <<"read">> -> read;
56 | _ -> ignore
57 | end,
58 | case Kind == ignore orelse Permission == ignore orelse Name == [] of
59 | true -> ignore;
60 | false ->
61 | {
62 | #resource{
63 | virtual_host = VHost,
64 | kind = Kind,
65 | name = binary_join(Name, <<"_">>)},
66 | Permission,
67 | ScopeEl
68 | }
69 | end;
70 | _ -> ignore
71 | end.
72 |
73 | binary_join([B|Bs], Sep) ->
74 | iolist_to_binary([B|add_separator(Bs, Sep)]);
75 | binary_join([], _Sep) ->
76 | <<>>.
77 |
78 | add_separator([B|Bs], Sep) ->
79 | [Sep, B | add_separator(Bs, Sep)];
80 | add_separator([], _) ->
81 | [].
82 |
83 |
84 |
--------------------------------------------------------------------------------
/src/rabbit_auth_backend_oauth.erl:
--------------------------------------------------------------------------------
1 | %% The contents of this file are subject to the Mozilla Public License
2 | %% Version 1.1 (the "License"); you may not use this file except in
3 | %% compliance with the License. You may obtain a copy of the License
4 | %% at https://www.mozilla.org/MPL/
5 | %%
6 | %% Software distributed under the License is distributed on an "AS IS"
7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
8 | %% the License for the specific language governing rights and
9 | %% limitations under the License.
10 | %%
11 | %% The Original Code is RabbitMQ.
12 | %%
13 | %% The Initial Developer of the Original Code is GoPivotal, Inc.
14 | %% Copyright (c) 2007-2015 Pivotal Software, Inc. All rights reserved.
15 |
16 | %%
17 | %% This is backend for OAuth 2.0 authorization.
18 | %% It Allows to use Rabbitmq with OAuth authorization servers.
19 | %% Authorization token is used as Username.
20 |
21 | -module(rabbit_auth_backend_oauth).
22 |
23 | -include_lib("rabbit_common/include/rabbit.hrl").
24 |
25 | -behaviour(rabbit_authn_backend).
26 | -behaviour(rabbit_authz_backend).
27 |
28 | -export([description/0]).
29 | -export([user_login_authentication/2, user_login_authorization/1,
30 | check_vhost_access/3, check_resource_access/3]).
31 |
32 | -rabbit_boot_step({rabbit_auth_backend_oauth_mnesia,
33 | [{description, "authosation oauth2: mnesia"},
34 | {mfa, {rabbit_oauth2_storage, setup_schema, []}},
35 | {requires, database},
36 | {enables, external_infrastructure}]}).
37 |
38 | -rabbit_boot_step({rabbit_auth_backend_oauth_backend_env,
39 | [{description, "authosation oauth2: oauth2 backend"},
40 | {mfa, {rabbit_oauth2_backend, oauth2_backend_env, []}},
41 | {requires, pre_boot},
42 | {enables, kernel_ready}]}).
43 |
44 | %%--------------------------------------------------------------------
45 |
46 | description() ->
47 | [{name, <<"OAUTH">>},
48 | {description, <<"OAUTH authentication / authorisation">>}].
49 |
50 | %%--------------------------------------------------------------------
51 |
52 | user_login_authentication(Token, _AuthProps) ->
53 | case oauth2:verify_access_token(Token, []) of
54 | {ok, _} -> {ok, #auth_user{ username = Token, tags = [], impl = none}};
55 | {error, access_denied} -> {refused, "token ~p rejected", [Token]}
56 | end.
57 |
58 | user_login_authorization(Username) ->
59 | case user_login_authentication(Username, []) of
60 | {ok, #auth_user{impl = Impl, tags = Tags}} -> {ok, Impl, Tags};
61 | Else -> Else
62 | end.
63 |
64 | check_vhost_access(#auth_user{username = Username}, VHost, _Sock) ->
65 | with_token_context(Username, fun(Ctx) ->
66 | rabbit_oauth2_scope:vhost_access(VHost, Ctx)
67 | end).
68 |
69 | check_resource_access(#auth_user{username = Username}, Resource, Permission) ->
70 | with_token_context(Username, fun(Ctx) ->
71 | rabbit_oauth2_scope:resource_access(Resource, Permission, Ctx)
72 | end).
73 |
74 | with_token_context(Token, Fun) ->
75 | case oauth2:verify_access_token(Token, []) of
76 | {ok, {_, TokenCtx}} -> Fun(TokenCtx);
77 | {error, access_denied} -> false
78 | end.
79 |
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RabbitMQ OAuth 2.0 Authorization Backend.
2 |
3 | This plugin aims to provide OAuth 2.0 authorization for RabbitMQ clients.
4 |
5 | ## Project Maturity
6 |
7 | This project ws **a spike that's no longer under development**. See [rabbitmq-auth-backend-oauth2](https://github.com/rabbitmq/rabbitmq-auth-backend-oauth2) for an OAuth 2/JWT [authentication and authorisation backend](https://www.rabbitmq.com/access-control.html) for RabbitMQ.
8 |
9 | ## Auth workflow
10 |
11 | ### Token grant.
12 |
13 | Internal:
14 |
15 | Client use some grant to request `access_token` in some `scope`.
16 |
17 | Token is being created with scope and expire after some time.
18 | Client can also be issued `refresh_token` to refresh `access_token`.
19 |
20 | If client use user credentioals, user access permissions to `scope` is being checked.
21 |
22 | External:
23 |
24 | External auth server sends request to token handler to create `access_token` with scope and expiry.
25 |
26 | ### Client access.
27 |
28 | Client connects to RabbitMQ using `access_token` as username and will have access to resources based on `scope`
29 |
30 |
31 | ## Components
32 |
33 | This module contin following parts:
34 |
35 | 1. Rabbit auth backend `rabbit_auth_backend_oauth.erl`. Module to authorize clients with `access_token` used as username.
36 | 2. Oauth backend (yeah, also backend) `rabbit_oauth2_backend.erl`. Module to work with OAuth2 clients and tokens, direct them to mnesia storage, manage scopes. (https://github.com/kivra/oauth2/blob/master/src/oauth2_backend.erl)
37 | 3. OAuth2 http server `rabbit_oauth2_auth.erl`. Cowboy handler to grant access codes and tokens. Has no references to rabbitmq and works with oauth library only. Can be made separate plugin.
38 | 4. Token endpoint for external Auth server `rabbit_oauth2_access_token.erl`. Accepts requests like `{"acess_token":..., "scope":..., "expires_in":..., "created_at":...}` and creates `access_token` record in DB. Can be used by external authorization server to issue tokens for rabbitmq.
39 |
40 | Endpoint is configured by application env `auth_server`, which can be `{internal, Conf}` or `{external, Conf}`. To set up internal (`rabbit_oauth2_auth`) or external (`rabbit_oauth2_access_token`) auth server.
41 |
42 | Grant and client types are managed by authorization server handler only.
43 |
44 | ## Scopes
45 |
46 | *Scopes is discussion topic, because current implementation provide not enough flexibility.*
47 |
48 | To define `access_token` access to specific VHost or resource OAuth2 scopes are used.
49 | Scope can be a set of strings. Each element in scope define access to specific resource permission.
50 |
51 | Format of scope element: `___`, where
52 |
53 | - `` - vhost of recource
54 | - `` can be `q` - queue, `ex` - exchange, or `t` - topic
55 | - `` - access permission (configure, read, write)
56 | - `` - resource name (exact, no regexps allowed)
57 |
58 | When granting `access_code` to scope on behalf of some user scope is checked to be available to this user. For this purpose another `auth_backend` is used. `rabbit_oauth2_backend.erl` currently contains constant `rabbit_auth_backend_internal`, can be configurable.
59 |
60 | As you can see, scope syntax restrict some vhosts and it is not easy to support regex resource names, because granting regex scope to regex user permissions will require solving regex inclusion problem (which is not so easy)
61 |
62 |
--------------------------------------------------------------------------------
/src/rabbit_oauth2_storage.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_storage).
2 |
3 | -export([
4 | save_client/4,
5 | save_access_code/2,
6 | save_access_token/2,
7 | save_refresh_token/2,
8 |
9 | lookup_client/1,
10 | lookup_access_code/1,
11 | lookup_access_token/1,
12 | lookup_refresh_token/1,
13 |
14 | remove_client/1,
15 | remove_access_code/1,
16 | remove_access_token/1,
17 | remove_refresh_token/1
18 | ]).
19 |
20 | -export([setup_schema/0]).
21 |
22 | -record(token, {
23 | token,
24 | context
25 | }).
26 |
27 | -record(client,{
28 | client_id,
29 | client_secret,
30 | redirect_uri,
31 | scope
32 | }).
33 |
34 | -define(ACCESS_TOKEN_TABLE, rabbit_oauth2_access_token).
35 | -define(ACCESS_CODE_TABLE, rabbit_oauth2_access_code).
36 | -define(REFRESH_TOKEN_TABLE, rabbit_oauth2_refresh_token).
37 | -define(CLIENT_TABLE, rabbit_oauth2_client).
38 |
39 | setup_schema() ->
40 | mnesia:create_table(?ACCESS_TOKEN_TABLE,
41 | [{attributes, record_info(fields, token)},
42 | {record_name, token},
43 | {type, set}]),
44 | mnesia:create_table(?REFRESH_TOKEN_TABLE,
45 | [{attributes, record_info(fields, token)},
46 | {record_name, token},
47 | {type, set}]),
48 | mnesia:create_table(?CLIENT_TABLE,
49 | [{attributes, record_info(fields, client)},
50 | {record_name, client},
51 | {type, set}]),
52 | mnesia:create_table(?ACCESS_CODE_TABLE,
53 | [{attributes, record_info(fields, token)},
54 | {record_name, token},
55 | {type, set}]),
56 | mnesia:add_table_copy(?ACCESS_TOKEN_TABLE, node(), ram_copies),
57 | mnesia:add_table_copy(?REFRESH_TOKEN_TABLE, node(), ram_copies),
58 | mnesia:add_table_copy(?CLIENT_TABLE, node(), ram_copies),
59 | mnesia:add_table_copy(?ACCESS_CODE_TABLE, node(), ram_copies),
60 |
61 | mnesia:wait_for_tables([?ACCESS_TOKEN_TABLE,
62 | ?REFRESH_TOKEN_TABLE,
63 | ?CLIENT_TABLE,
64 | ?ACCESS_CODE_TABLE], 30000).
65 |
66 | save_client(ClientId, Secret, RedirUri, Scope) ->
67 | Client = #client{
68 | client_id = ClientId,
69 | client_secret = Secret,
70 | redirect_uri = RedirUri,
71 | scope = Scope},
72 | save(?CLIENT_TABLE, Client).
73 |
74 | save_access_token(Token, Context) ->
75 | save_token(?ACCESS_TOKEN_TABLE, Token, Context).
76 |
77 | save_refresh_token(Token, Context) ->
78 | save_token(?REFRESH_TOKEN_TABLE, Token, Context).
79 |
80 | save_access_code(Code, Context) ->
81 | save_token(?ACCESS_CODE_TABLE, Code, Context).
82 |
83 | save_token(Table, Token, Context) ->
84 | TokenRecord = #token{ token = Token, context = Context },
85 | save(Table, TokenRecord).
86 |
87 | save(Table, Data) ->
88 | rabbit_misc:execute_mnesia_transaction(
89 | fun () ->
90 | ok = mnesia:write(Table, Data, write)
91 | end).
92 |
93 |
94 | lookup_client(ClientId) ->
95 | case lookup(?CLIENT_TABLE, ClientId) of
96 | {error, not_found} -> {error, not_found};
97 | {ok, #client{client_secret = Secret,
98 | redirect_uri = RedirUri,
99 | scope = Scope}} ->
100 | {ok, {ClientId, Secret, RedirUri, Scope}}
101 | end.
102 |
103 | lookup_access_token(Token) ->
104 | lookup_token(?ACCESS_TOKEN_TABLE, Token).
105 |
106 | lookup_refresh_token(Token) ->
107 | lookup_token(?REFRESH_TOKEN_TABLE, Token).
108 |
109 | lookup_access_code(Code) ->
110 | lookup_token(?ACCESS_CODE_TABLE, Code).
111 |
112 | lookup_token(Table, Token) ->
113 | case lookup(Table, Token) of
114 | {error, not_found} -> {error, not_found};
115 | {ok, #token{context = Context}} ->
116 | {ok, {Token, Context}}
117 | end.
118 |
119 | lookup(Table, Key) ->
120 | rabbit_misc:dirty_read({Table, Key}).
121 |
122 | remove_client(ClientId) ->
123 | delete(?CLIENT_TABLE, ClientId).
124 |
125 | remove_access_token(Token) ->
126 | delete(?ACCESS_TOKEN_TABLE, Token).
127 |
128 | remove_refresh_token(Token) ->
129 | delete(?REFRESH_TOKEN_TABLE, Token).
130 |
131 | remove_access_code(Code) ->
132 | delete(?ACCESS_CODE_TABLE, Code).
133 |
134 | delete(Table, Key) ->
135 | rabbit_misc:execute_mnesia_transaction(
136 | fun() ->
137 | ok = mnesia:delete({Table, Key})
138 | end).
139 |
140 |
141 |
--------------------------------------------------------------------------------
/src/rabbit_oauth2_backend.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_backend).
2 |
3 | -behavior(oauth2_backend).
4 |
5 | -include_lib("rabbit_common/include/rabbit.hrl").
6 |
7 | %%% Behavior API
8 | -export([authenticate_user/2]).
9 | -export([authenticate_client/2]).
10 | -export([get_client_identity/2]).
11 | -export([associate_access_code/3]).
12 | -export([associate_refresh_token/3]).
13 | -export([associate_access_token/3]).
14 | -export([resolve_access_code/2]).
15 | -export([resolve_refresh_token/2]).
16 | -export([resolve_access_token/2]).
17 | -export([revoke_access_code/2]).
18 | -export([revoke_access_token/2]).
19 | -export([revoke_refresh_token/2]).
20 | -export([get_redirection_uri/2]).
21 | -export([verify_redirection_uri/3]).
22 | -export([verify_client_scope/3]).
23 | -export([verify_resowner_scope/3]).
24 | -export([verify_scope/3]).
25 |
26 | -export([add_access_token/3, add_access_token/4]).
27 | -export([oauth2_backend_env/0]).
28 |
29 | -define(BACKEND, rabbit_auth_backend_internal).
30 |
31 | -ifdef(TEST).
32 | -compile(export_all).
33 | -endif.
34 |
35 | oauth2_backend_env() ->
36 | application:set_env(oauth2, backend, rabbit_oauth2_backend).
37 |
38 | add_access_token(Token, Scope, ExpiresIn) when is_list(Scope),
39 | is_integer(ExpiresIn) ->
40 | add_access_token(Token, Scope, ExpiresIn, time_compat:os_system_time(seconds)).
41 |
42 | add_access_token(Token, Scope, ExpiresIn, CreatedAt)
43 | when is_list(Scope),
44 | is_integer(ExpiresIn),
45 | is_integer(CreatedAt) ->
46 | {ok, []} = associate_access_token(Token,
47 | [{<<"scope">>, Scope},
48 | {<<"expiry_time">>,
49 | ExpiresIn + CreatedAt}],
50 | []),
51 | ok.
52 |
53 | %% Behaviour functions --------------------------------------------------------
54 |
55 | authenticate_user({Username, Password}, Ctx) ->
56 | rabbit_log:info("User ~p Pass ~p", [Username, Password]),
57 | case ?BACKEND:user_login_authentication(Username, [{password, Password}]) of
58 | {refused, _Err} -> {error, notfound};
59 | {refused, _Format, _Arg} -> {error, notfound};
60 | {ok, AuthUser} -> {ok, {Ctx, AuthUser}}
61 | end.
62 |
63 | %% Access token ---------------------------------------------------------------
64 |
65 | associate_access_token(AccessToken, Context, AppContext) ->
66 | ok = rabbit_oauth2_storage:save_access_token(AccessToken, Context),
67 | {ok, AppContext}.
68 |
69 | resolve_access_token(AccessToken, AppContext) ->
70 | case rabbit_oauth2_storage:lookup_access_token(AccessToken) of
71 | {ok, {_, Context}} -> {ok, {AppContext, Context}};
72 | {error, not_found} -> {error, notfound}
73 | end.
74 |
75 | revoke_access_token(AccessToken, AppContext) ->
76 | ok = rabbit_oauth2_storage:remove_access_token(AccessToken),
77 | {ok, AppContext}.
78 |
79 | %% Access code ----------------------------------------------------------------
80 |
81 | associate_access_code(AccessCode, Context, AppContext) ->
82 | ok = rabbit_oauth2_storage:save_access_code(AccessCode, Context),
83 | {ok, AppContext}.
84 |
85 | resolve_access_code(AccessCode, AppContext) ->
86 | case rabbit_oauth2_storage:lookup_access_code(AccessCode) of
87 | {ok, {_, Context}} -> {ok, {AppContext, Context}};
88 | {error, not_found} -> {error, notfound}
89 | end.
90 |
91 | revoke_access_code(AccessCode, AppContext) ->
92 | ok = rabbit_oauth2_storage:remove_access_code(AccessCode),
93 | {ok, AppContext}.
94 |
95 | %% Refresh token --------------------------------------------------------------
96 |
97 | associate_refresh_token(RefreshToken, Context, AppContext) ->
98 | ok = rabbit_oauth2_storage:save_refresh_token(RefreshToken, Context),
99 | {ok, AppContext}.
100 |
101 | resolve_refresh_token(RefreshToken, AppContext) ->
102 | case rabbit_oauth2_storage:lookup_refresh_token(RefreshToken) of
103 | {ok, {_, Context}} -> {ok, {AppContext, Context}};
104 | {error, not_found} -> {error, notfound}
105 | end.
106 |
107 | revoke_refresh_token(RefreshToken, AppContext) ->
108 | ok = rabbit_oauth2_storage:remove_refresh_token(RefreshToken),
109 | {ok, AppContext}.
110 |
111 | %% Scope ----------------------------------------------------------------------
112 |
113 | verify_scope(RScope, Scope, AppContext) when is_list(RScope), is_list(Scope) ->
114 | case Scope -- RScope of
115 | [] -> {ok, {AppContext, Scope}};
116 | _ -> {error, invalid_scope}
117 | end;
118 | verify_scope(_, _, _) -> {error, invalid_scope}.
119 |
120 | verify_resowner_scope(AuthUser, Scope, Ctx) ->
121 | ScopePermissions = rabbit_oauth2_scope:parse_scope(Scope),
122 | ValidScope = lists:filtermap(
123 | fun({Resource, Permission, ScopeEl}) ->
124 | case ?BACKEND:check_resource_access(AuthUser,
125 | Resource,
126 | Permission) of
127 | false -> false;
128 | true -> {true, ScopeEl}
129 | end
130 | end,
131 | ScopePermissions),
132 | ScopePolicy = application:get_env(rabbitmq_auth_backend_oauth,
133 | scope_policy,
134 | matching),
135 | case {ValidScope, ScopePolicy} of
136 | {[], _} -> {error, invalid_scope};
137 | {Scope, _} -> {ok, {Ctx, Scope}};
138 | {_, matching} -> {ok, {Ctx, Scope}};
139 | _ -> {error, invalid_scope}
140 | end.
141 |
142 | verify_client_scope({_, _, _, CScope}, Scope, Ctx) when is_list(CScope),
143 | is_list(Scope) ->
144 | case Scope -- CScope of
145 | [] -> {ok, {Ctx, Scope}};
146 | _ -> {error, invalid_scope}
147 | end;
148 | verify_client_scope(_, _, _) -> {error, invalid_scope}.
149 |
150 | %% Client ---------------------------------------------------------------------
151 |
152 | authenticate_client({ClientId, Secret}, Ctx) ->
153 | case rabbit_oauth2_storage:lookup_client(ClientId) of
154 | {ok, Client = {ClientId, Secret, _, _}} -> {ok, {Ctx, Client}};
155 | {ok, _} -> {error, badsecret};
156 | {error, not_found} -> {error, notfound}
157 | end;
158 | authenticate_client(ClientId, Ctx) ->
159 | case rabbit_oauth2_storage:lookup_client(ClientId) of
160 | {ok, Client} -> {ok, {Ctx, Client}};
161 | {error, not_found} -> {error, notfound}
162 | end.
163 |
164 | get_client_identity(ClientId, Ctx) ->
165 | case rabbit_oauth2_storage:lookup_client(ClientId) of
166 | {ok, Client} -> {ok, {Ctx, Client}};
167 | {error, not_found} -> {error, notfound}
168 | end.
169 |
170 | get_redirection_uri({ClientId, Secret}, Ctx) ->
171 | case rabbit_oauth2_storage:lookup_client(ClientId) of
172 | {ok, {ClientId, Secret, RedirUrl, _}} -> {ok, {Ctx, RedirUrl}};
173 | _ -> {error, notfound}
174 | end.
175 |
176 |
177 | verify_redirection_uri({_, _, RedirUrl, _}, RedirUrl, Ctx) -> {ok, Ctx};
178 | verify_redirection_uri(_, _, _) -> {error, mismatch}.
179 |
180 |
181 |
--------------------------------------------------------------------------------
/test/src/rabbit_oauth2_auth_test.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_auth_test).
2 |
3 | -compile(export_all).
4 |
5 | tests() ->
6 | test_get(),
7 | test_grant_type(),
8 | test_resp_type(),
9 | % test_refresh(),
10 | passed.
11 |
12 |
13 | test_get() ->
14 | % TODO: error cases.
15 | % There will be same result (auth form) for each response type
16 | RespTypes = [<<"token">>, <<"authorization_code">>],
17 | ClientId = <<"foo">>,
18 | RedirectUri = <<"https://example.com">>,
19 | Scope = <<"sope_scope other_scope">>,
20 | State = <<"random_state">>,
21 | Params = [{<<"client_id">>, ClientId},
22 | {<<"redirect_uri">>, RedirectUri},
23 | {<<"scope">>, Scope},
24 | {<<"state">>, State}],
25 | lists:foreach(fun(RespType) ->
26 | {ok, {200, RespBody}} = http_get(Params ++
27 | [{<<"response_type">>, RespType}]),
28 | <<"", _/binary>> = RespBody,
29 | {_,_} = binary:match(RespBody,
30 | <<"">>),
33 | {_,_} = binary:match(RespBody,
34 | <<"">>),
37 | {_,_} = binary:match(RespBody,
38 | <<"">>),
41 | {_,_} = binary:match(RespBody,
42 | <<"">>),
45 | {_,_} = binary:match(RespBody,
46 | <<"">>)
49 | end,
50 | RespTypes).
51 |
52 | test_grant_type() ->
53 | Scope = <<"/_q_configure_foo /_ex_configure_bar">>,
54 | Scopes = binary:split(Scope, <<" ">>, [global]),
55 | ClientId = <<"foo">>,
56 | ClientSecret = <<"bar">>,
57 | RedirectUri = <<"https://example.com">>,
58 | Username = <<"guest">>,
59 | Password = <<"guest">>,
60 | ClientAuth = base64:encode_to_string(<>),
62 | ok = create_client(ClientId, ClientSecret, RedirectUri, Scopes),
63 | {ok, AuthCode} = create_code(ClientId, ClientSecret, Scopes),
64 | GrantTypes = [
65 | {<<"password">>,
66 | [{<<"username">>, Username},
67 | {<<"password">>, Password},
68 | {<<"scope">>, Scope}],
69 | []}
70 | ,{<<"client_credentials">>,
71 | [{<<"scope">>, Scope}],
72 | [{"Authorization", "Basic " ++ ClientAuth}]}
73 | ,{<<"authorization_code">>,
74 | [{<<"client_id">>, ClientId},
75 | {<<"redirect_uri">>, RedirectUri},
76 | {<<"code">>, AuthCode}],
77 | []}
78 | ],
79 | lists:foreach(
80 | fun({CodeGrant, Params, Headers}) ->
81 | {ok, {200, Result, _}} = http_post([{<<"grant_type">>, CodeGrant}
82 | | Params], Headers),
83 | {struct, ResultData} = mochijson2:decode(Result),
84 | {<<"access_token">>, AccessToken} = proplists:lookup(<<"access_token">>, ResultData),
85 | {<<"expires_in">>, Expiry} = proplists:lookup(<<"expires_in">>, ResultData),
86 | {<<"scope">>, Scope} = proplists:lookup(<<"scope">>, ResultData),
87 | {<<"token_type">>, <<"bearer">>} = proplists:lookup(<<"token_type">>, ResultData),
88 | ok = access_token(AccessToken, Scopes)
89 | end,
90 | GrantTypes).
91 |
92 | test_resp_type() ->
93 | Scope = <<"/_q_configure_foo /_ex_configure_bar">>,
94 | Scopes = binary:split(Scope, <<" ">>, [global]),
95 | ClientId = <<"foo">>,
96 | ClientSecret = <<"bar">>,
97 | RedirectUri = <<"https://example.com">>,
98 | Username = <<"guest">>,
99 | Password = <<"guest">>,
100 | State = <<"Some state">>,
101 |
102 | RespTypes = [<<"token">>, <<"authorization_code">>],
103 | Params = [{<<"client_id">>, ClientId},
104 | {<<"redirect_uri">>, RedirectUri},
105 | {<<"username">>, Username},
106 | {<<"password">>, Password},
107 | {<<"state">>, State},
108 | {<<"scope">>, Scope}],
109 | lists:foreach(
110 | fun(RespType) ->
111 | {ok, {302, _, Headers}} = http_post([{<<"response_type">>, RespType}
112 | | Params], []),
113 | {"location", Location} = proplists:lookup("location", Headers),
114 | [Url, Qs] = string:tokens(Location, "#"),
115 | RedirectUri = list_to_binary(Url),
116 | ResultData = cow_qs:parse_qs(list_to_binary(Qs)),
117 | case RespType of
118 | <<"token">> ->
119 | {<<"access_token">>, AccessToken} = proplists:lookup(<<"access_token">>, ResultData),
120 | {<<"expires_in">>, Expiry} = proplists:lookup(<<"expires_in">>, ResultData),
121 | {<<"scope">>, Scope} = proplists:lookup(<<"scope">>, ResultData),
122 | {<<"token_type">>, <<"bearer">>} = proplists:lookup(<<"token_type">>, ResultData),
123 | ok = access_token(AccessToken, Scopes);
124 | <<"authorization_code">> ->
125 | {<<"access_code">>, AccessCode} = proplists:lookup(<<"access_code">>, ResultData),
126 | {<<"expires_in">>, Expiry} = proplists:lookup(<<"expires_in">>, ResultData),
127 | {<<"scope">>, Scope} = proplists:lookup(<<"scope">>, ResultData),
128 | {<<"token_type">>, <<"bearer">>} = proplists:lookup(<<"token_type">>, ResultData),
129 | ok = access_code(AccessCode, ClientId, ClientSecret, RedirectUri)
130 | end
131 | end,
132 | RespTypes).
133 |
134 | access_token(AccessToken, Scope) ->
135 | {ok, {n, Ctx}} = oauth2:verify_access_token(AccessToken, n),
136 | Scope = proplists:get_value(<<"scope">>, Ctx),
137 | ok.
138 |
139 | access_code(AccessCode, ClientId, ClientSecret, RedirectUri) ->
140 | {ok, {n, _}} = oauth2:authorize_code_grant({ClientId, ClientSecret},
141 | AccessCode, RedirectUri, n),
142 | ok.
143 |
144 | create_client(ClientId, ClientSecret, RedirectUri, Scope) ->
145 | rabbit_oauth2_storage:save_client(ClientId, ClientSecret, RedirectUri, Scope).
146 |
147 | create_code(ClientId, Secret, Scope) ->
148 | {ok, {n, Auth}} = oauth2:authorize_client_credentials({ClientId, Secret}, Scope, n),
149 | {ok, {n, CodeResp}} = oauth2:issue_code(Auth, n),
150 | {ok, Code} = oauth2_response:access_code(CodeResp).
151 |
152 | http_get(Params) ->
153 | Qs = cow_qs:qs(Params),
154 | Url = <<"http://localhost:15672/oauth?", Qs/binary>>,
155 | {ok, {{_, Status, _}, _, Body}} = httpc:request(binary_to_list(Url)),
156 | {ok, {Status, list_to_binary(Body)}}.
157 |
158 | http_post(Params, Headers) ->
159 | Qs = cow_qs:qs(Params),
160 | Url = "http://localhost:15672/oauth",
161 | {ok, {{_, Status, _}, RespHeaders, Body}} = httpc:request(post, {Url, Headers, "application/x-www-form-urlencoded", Qs}, [], []),
162 | {ok, {Status, list_to_binary(Body), RespHeaders}}.
163 |
--------------------------------------------------------------------------------
/src/rabbit_oauth2_auth.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_auth).
2 |
3 | -export([
4 | init/3
5 | ,rest_init/2
6 | ,allowed_methods/2
7 | ]).
8 |
9 | -export([
10 | content_types_provided/2
11 | ,content_types_accepted/2
12 | ]).
13 |
14 | -export([
15 | process_post/2
16 | ,process_get/2
17 | ]).
18 |
19 | -export([binary_join/2]).
20 |
21 | %%%===================================================================
22 | %%% Cowboy callbacks
23 | %%%===================================================================
24 |
25 | init(_Transport, _Req, _Opts) ->
26 | %% Compile the DTL template used for the authentication
27 | %% form in the implicit grant flow.
28 | {upgrade, protocol, cowboy_rest}.
29 |
30 | rest_init(Req, _Opts) ->
31 | {ok, Req, undefined_state}.
32 |
33 | content_types_provided(Req, State) ->
34 | {[{{<<"text">>, <<"html">>, []}, process_get}], Req, State}.
35 |
36 | content_types_accepted(Req, State) ->
37 | {[{{<<"application">>, <<"json">>, []}, process_post},
38 | {{<<"application">>, <<"x-www-form-urlencoded">>, []}, process_post}],
39 | Req, State}.
40 |
41 | allowed_methods(Req, State) ->
42 | {[<<"POST">>, <<"GET">>], Req, State}.
43 |
44 | process_post(Req, State) ->
45 | {ok, Params, Req2} = cowboy_req:body_qs(Req),
46 | {ok, Reply} =
47 | case proplists:get_value(<<"grant_type">>, Params) of
48 | <<"password">> ->
49 | process_password_grant(Req2, Params);
50 | <<"client_credentials">> ->
51 | process_client_credentials_grant(Req2, Params);
52 | <<"authorization_code">> ->
53 | process_authorization_token_grant(Req2, Params);
54 | undefined ->
55 | case proplists:get_value(<<"response_type">>, Params) of
56 | RT when RT == <<"token">>; RT == <<"authorization_code">> ->
57 | process_authorization_grant(Req2, RT, Params);
58 | _ ->
59 | cowboy_req:reply(400, [], <<"Bad Request.">>, Req2)
60 | end;
61 | _ ->
62 | cowboy_req:reply(400, [], <<"Bad Request.">>, Req2)
63 | end,
64 | {halt, Reply, State}.
65 |
66 | process_get(Req, State) ->
67 | {QsVals, Req2} = cowboy_req:qs_vals(Req),
68 | ResponseType = proplists:get_value(<<"response_type">>, QsVals),
69 | {ok, Reply} =
70 | case ResponseType of
71 | RT when RT == <<"token">>; RT == <<"authorization_code">> ->
72 | Params = lists:filter(
73 | fun({K, _V}) ->
74 | lists:member(K, [<<"client_id">>,
75 | <<"redirect_uri">>,
76 | <<"scope">>,
77 | <<"state">>])
78 | end,
79 | QsVals),
80 | show_authorisation_form(Req2, ResponseType, Params);
81 | _ ->
82 | JSON = mochijson2:encode({struct, [{error, <<"unsupported_response_type">>}]}),
83 | cowboy_req:reply(400, [], JSON, Req2)
84 | end,
85 | {halt, Reply, State}.
86 |
87 | %%%===================================================================
88 | %%% Grant type handlers
89 | %%%===================================================================
90 |
91 | process_password_grant(Req, Params) ->
92 | Username = proplists:get_value(<<"username">>, Params),
93 | Password = proplists:get_value(<<"password">>, Params),
94 | Scope = get_scope(Params),
95 | AuthResult = oauth2:authorize_password({Username, Password}, Scope, []),
96 | Response = issue_token(AuthResult),
97 | reply(Response, Req).
98 |
99 | process_client_credentials_grant(Req, Params) ->
100 | case cowboy_req:header(<<"authorization">>, Req) of
101 | {<<"Basic ", Credentials/binary>>, Req2} ->
102 | [Id, Secret] = binary:split(base64:decode(Credentials), <<":">>),
103 | Scope = get_scope(Params),
104 | AuthResult = oauth2:authorize_client_credentials({Id, Secret}, Scope, []),
105 | Response = issue_token(AuthResult),
106 | reply(Response, Req2);
107 | _ -> cowboy_req:reply(401, Req)
108 | end.
109 |
110 | show_authorisation_form(Req, ResponseType, Params) ->
111 | State = proplists:get_value(<<"state">>, Params),
112 | Scope = get_scope(Params),
113 | ClientId = proplists:get_value(<<"client_id">>, Params),
114 | RedirectUri = proplists:get_value(<<"redirect_uri">>, Params),
115 | %% Pass the scope, state and redirect URI to the browser
116 | %% as hidden form parameters, allowing them to "propagate"
117 | %% to the next stage.
118 | case ClientId == undefined of
119 | true ->
120 | cowboy_req:reply(400, [], <<"Bad Request.">>, Req);
121 | false ->
122 | {ok, Html} = auth_form_dtl:render([{redirect_uri, RedirectUri},
123 | {client_id, ClientId},
124 | {state, State},
125 | {scope, binary_join(Scope, <<" ">>)},
126 | {response_type, ResponseType}]),
127 | cowboy_req:reply(200, [], Html, Req)
128 | end.
129 |
130 | % Process response from auth form.
131 | % ResponseType coud be <<"token">> for implicit grant
132 | % or <<"authorization_code">> for authorisation code grant
133 | process_authorization_grant(Req, ResponseType, Params) ->
134 | ClientId = proplists:get_value(<<"client_id">>, Params),
135 | RedirectUri = proplists:get_value(<<"redirect_uri">>, Params),
136 | Username = proplists:get_value(<<"username">>, Params),
137 | Password = proplists:get_value(<<"password">>, Params),
138 | State = proplists:get_value(<<"state">>, Params),
139 | Scope = get_scope(Params),
140 |
141 | ExtraParams = [{<<"state">>, State}],
142 | AuthResult = oauth2:authorize_code_request({Username, Password},
143 | ClientId,
144 | RedirectUri, Scope, []),
145 | Response = case ResponseType of
146 | <<"token">> -> issue_token(AuthResult);
147 | <<"authorization_code">> -> issue_code(AuthResult)
148 | end,
149 | redirect(RedirectUri, Response, ExtraParams, Req).
150 |
151 | process_authorization_token_grant(Req, Params) ->
152 | ClientId = proplists:get_value(<<"client_id">>, Params),
153 | RedirectUri = proplists:get_value(<<"redirect_uri">>, Params),
154 | Code = proplists:get_value(<<"code">>, Params),
155 | AuthResult = oauth2:authorize_code_grant(ClientId, Code, RedirectUri, []),
156 | Response = issue_token(AuthResult),
157 | reply(Response, Req).
158 |
159 |
160 | %%%===================================================================
161 | %%% Internal functions
162 | %%%===================================================================
163 |
164 | refresh_token_grant() ->
165 | application:get_env(rabbitmq_auth_backend_oauth,
166 | grant_refresh_token,
167 | false).
168 |
169 | issue_token({ok, {_, Auth}}) ->
170 | TokenResponse = case refresh_token_grant() of
171 | true -> oauth2:issue_token_and_refresh(Auth, []);
172 | false -> oauth2:issue_token(Auth, [])
173 | end,
174 | case TokenResponse of
175 | {ok, {_, TokenResp}} -> {ok, TokenResp};
176 | {error, _} = Err -> Err
177 | end;
178 | issue_token({error, Err}) ->
179 | {error, Err}.
180 |
181 | issue_code({ok, {_, Auth}}) ->
182 | case oauth2:issue_code(Auth, []) of
183 | {ok, {_, CodeResp}} -> {ok, CodeResp};
184 | {error, _} = Err -> Err
185 | end;
186 | issue_code({error, Err}) ->
187 | {error, Err}.
188 |
189 | reply({ok, Response}, Req) ->
190 | Proplist = lists:keydelete(<<"resource_owner">>, 1,
191 | oauth2_response:to_proplist(Response)),
192 | cowboy_req:reply(200, [], mochijson2:encode({struct, Proplist}), Req);
193 | reply({error, Err}, Req) ->
194 | cowboy_req:reply(400, [],
195 | mochijson2:encode({struct, [{<<"error">>, Err}]}),
196 | Req).
197 |
198 | redirect(RedirectUri, {ok, Response}, Extra, Req) ->
199 | Params = oauth2_response:to_proplist(Response) ++ Extra,
200 | redirect(RedirectUri, Params, Req);
201 | redirect(RedirectUri, {error, Err}, Extra, Req) ->
202 | Params = [{<<"error">>, Err} | Extra],
203 | redirect(RedirectUri, Params, Req).
204 |
205 | redirect(RedirectUri, Params, Req) when is_list(Params) ->
206 | BinParams = lists:map(
207 | fun ({K,V}) when is_integer(V) -> {K, integer_to_binary(V)};
208 | ({K,V}) when is_atom(V) -> {K, atom_to_binary(V, utf8)};
209 | ({K,V}) when is_binary(V) -> {K,V}
210 | end,
211 | Params),
212 | Frag = cow_qs:qs(BinParams),
213 | Req1 = cowboy_req:set_resp_header(<<"location">>,
214 | <>,
215 | Req),
216 | cowboy_req:reply(302, [], <<>>, Req1).
217 |
218 |
219 | get_scope(Params) ->
220 | binary:split(proplists:get_value(<<"scope">>, Params, <<>>),
221 | <<" ">>,
222 | [global]).
223 |
224 | binary_join([], _) -> <<>>;
225 | binary_join([H], _) -> H;
226 | binary_join([H1, H2 | T], Sep) ->
227 | binary_join([<> | T], Sep).
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
--------------------------------------------------------------------------------
/test/src/rabbit_oauth2_backend_test.erl:
--------------------------------------------------------------------------------
1 | -module(rabbit_oauth2_backend_test).
2 |
3 | -compile(export_all).
4 | -include_lib("eunit/include/eunit.hrl").
5 | -include_lib("rabbit_common/include/rabbit.hrl").
6 |
7 | standalone_tests() ->
8 | parse_scope_test(),
9 | scope_permissions_test(),
10 | passed.
11 |
12 | broker_tests() ->
13 | save_load_token_test(),
14 | revoke_token_test(),
15 | expire_token_test(),
16 | token_permission_test(),
17 | client_auth_grant_test(),
18 | access_code_grant_test(),
19 | passed.
20 |
21 | parse_scope_test() ->
22 | Scopes = [
23 | % VHost_Kind_Permission_Name
24 | {<<"vhost_q_configure_foo">>, {<<"vhost">>, queue, <<"foo">>, configure}},
25 | {<<"vhost_q_write_foo">>, {<<"vhost">>, queue, <<"foo">>, write}},
26 | {<<"vhost_q_read_foo">>, {<<"vhost">>, queue, <<"foo">>, read}},
27 | {<<"vhost_ex_configure_foo">>, {<<"vhost">>, exchange, <<"foo">>, configure}},
28 | {<<"vhost_ex_write_foo">>, {<<"vhost">>, exchange, <<"foo">>, write}},
29 | {<<"vhost_ex_read_foo">>, {<<"vhost">>, exchange, <<"foo">>, read}},
30 | {<<"vhost_t_write_foo">>, {<<"vhost">>, topic, <<"foo">>, write}},
31 | % Name can contain '_'
32 | {<<"vhost_q_configure_foo_bar_baz">>, {<<"vhost">>, queue, <<"foo_bar_baz">>, configure}},
33 | % Vhost cannot contain '_'
34 | {<<"vhost_foo_q_configure_foo_bar">>, ignore},
35 | % Vhost and name can contain different characters
36 | {<<"vhost.com/foo_q_configure_foo.bar,baz">>, {<<"vhost.com/foo">>, queue, <<"foo.bar,baz">>, configure}},
37 | % Kind and Permission should be valid
38 | {<<"vhost_qu_configure_name">>, ignore},
39 | {<<"vhost_q_noconfigure_name">>, ignore},
40 | % There should be all parts
41 | {<<"vhost_q_conf">>, ignore},
42 | {<<"vhost_q_name">>, ignore},
43 | {<<"q_configure_name">>, ignore},
44 | % '/' for default host
45 | {<<"/_q_configure_foo">>, {<<"/">>, queue, <<"foo">>, configure}},
46 | % Utf?
47 | {<<"/_q_configure_ПиуПиу"/utf8>>, {<<"/">>, queue, <<"ПиуПиу"/utf8>>, configure}}
48 | ],
49 | lists:foreach(
50 | fun({Scope, Result}) ->
51 | case rabbit_oauth2_backend:parse_scope_el(Scope) of
52 | ignore ->
53 | Result = ignore;
54 | {#resource{ virtual_host = VHost, kind = Kind, name = Name },
55 | Permission, Scope} ->
56 | Result = {VHost, Kind, Name, Permission}
57 | end
58 | end,
59 | Scopes).
60 | save_load_token_test() ->
61 | {error, not_found} = rabbit_oauth2_storage:lookup_access_token(<<"token">>),
62 | {ok, []} = rabbit_oauth2_backend:associate_access_token(<<"token1">>, [{<<"scope">>, [<<"FOO">>]}], []),
63 | {ok, {<<"token1">>, [{<<"scope">>, [<<"FOO">>]}]}} = rabbit_oauth2_storage:lookup_access_token(<<"token1">>),
64 | {error, not_found} = rabbit_oauth2_storage:lookup_access_token(<<"token">>),
65 | TimeSec = time_compat:os_system_time(seconds),
66 | ok = rabbit_oauth2_backend:add_access_token(<<"token2">>, [<<"foo">>, <<"bar">>], 100, TimeSec),
67 | Context = [{<<"scope">>, [<<"foo">>, <<"bar">>]}, {<<"expiry_time">>, 100 + TimeSec}],
68 | {ok, {[], Context}} = rabbit_oauth2_backend:resolve_access_token(<<"token2">>, []).
69 |
70 | revoke_token_test() ->
71 | TimeSec = time_compat:os_system_time(seconds),
72 | ok = rabbit_oauth2_backend:add_access_token(<<"token3">>, [<<"foo">>, <<"bar">>], 100, TimeSec),
73 | {ok, foo} = rabbit_oauth2_backend:revoke_access_token(<<"token3">>, foo),
74 | {error, access_denied} = oauth2:verify_access_token(<<"token3">>, []).
75 |
76 | expire_token_test() ->
77 | TimeSec = time_compat:os_system_time(seconds),
78 | ok = rabbit_oauth2_backend:add_access_token(<<"token3">>, [<<"foo">>, <<"bar">>], 1, TimeSec),
79 | timer:sleep(1500),
80 | {error, access_denied} = oauth2:verify_access_token(<<"token3">>, []).
81 |
82 | scope_permissions_test() ->
83 | Examples = [
84 | % VHost_Kind_Permission_Name
85 | {<<"vhost_q_configure_foo">>, {<<"vhost">>, queue, <<"foo">>, configure}},
86 | {<<"vhost_q_write_foo">>, {<<"vhost">>, queue, <<"foo">>, write}},
87 | {<<"vhost_q_read_foo">>, {<<"vhost">>, queue, <<"foo">>, read}},
88 | {<<"vhost_ex_configure_foo">>, {<<"vhost">>, exchange, <<"foo">>, configure}},
89 | {<<"vhost_ex_write_foo">>, {<<"vhost">>, exchange, <<"foo">>, write}},
90 | {<<"vhost_ex_read_foo">>, {<<"vhost">>, exchange, <<"foo">>, read}},
91 | {<<"vhost_t_write_foo">>, {<<"vhost">>, topic, <<"foo">>, write}},
92 | {<<"vhost_q_configure_foo_bar_baz">>, {<<"vhost">>, queue, <<"foo_bar_baz">>, configure}},
93 | {<<"vhost.com/foo_q_configure_foo.bar,baz">>, {<<"vhost.com/foo">>, queue, <<"foo.bar,baz">>, configure}},
94 | {<<"/_q_configure_foo">>, {<<"/">>, queue, <<"foo">>, configure}},
95 | {<<"/_q_configure_ПиуПиу"/utf8>>, {<<"/">>, queue, <<"ПиуПиу"/utf8>>, configure}}
96 | ],
97 | lists:foreach(
98 | fun(Example) ->
99 | {Scope, {Vhost, Kind, Name, Permission}} = Example,
100 | Resource = #resource{ virtual_host = Vhost, kind = Kind, name = Name},
101 | Context = [{<<"scope">>, [Scope]}],
102 | true = rabbit_oauth2_backend:vhost_access(Vhost, Context),
103 | true = rabbit_oauth2_backend:resource_access(Resource, Permission, Context)
104 | end,
105 | Examples).
106 |
107 |
108 | token_permission_test() ->
109 | TimeSec = time_compat:os_system_time(seconds),
110 | ok = rabbit_oauth2_backend:add_access_token(<<"token4">>, [<<"/_q_configure_foo">>], 1000, TimeSec),
111 | {refused, _, _} = rabbit_auth_backend_oauth:user_login_authentication(<<"token3">>, []),
112 | {ok, #auth_user{ username = <<"token4">> } = AuthUser} =
113 | rabbit_auth_backend_oauth:user_login_authentication(<<"token4">>, []),
114 | {ok, none, []} = rabbit_auth_backend_oauth:user_login_authorization(<<"token4">>),
115 | true = rabbit_auth_backend_oauth:check_vhost_access(AuthUser, <<"/">>, none),
116 | false = rabbit_auth_backend_oauth:check_vhost_access(AuthUser, <<"other">>, none),
117 | true = rabbit_auth_backend_oauth:check_resource_access(
118 | AuthUser,
119 | #resource{ virtual_host = <<"/">>, kind = queue, name = <<"foo">>},
120 | configure),
121 | false = rabbit_auth_backend_oauth:check_resource_access(
122 | AuthUser,
123 | #resource{ virtual_host = <<"other">>, kind = queue, name = <<"foo">>},
124 | configure),
125 | false = rabbit_auth_backend_oauth:check_resource_access(
126 | AuthUser,
127 | #resource{ virtual_host = <<"/">>, kind = queue, name = <<"foo1">>},
128 | configure),
129 | false = rabbit_auth_backend_oauth:check_resource_access(
130 | AuthUser,
131 | #resource{ virtual_host = <<"/">>, kind = exchange, name = <<"foo">>},
132 | configure),
133 | false = rabbit_auth_backend_oauth:check_resource_access(
134 | AuthUser,
135 | #resource{ virtual_host = <<"/">>, kind = queue, name = <<"foo">>},
136 | write).
137 |
138 |
139 | client_auth_grant_test() ->
140 | ClientId = <<"foo">>,
141 | Secret = <<"bar">>,
142 | RedirUrl = <<"localhost">>,
143 | Scope = [<<"/_q_configure_foo">>],
144 | ok = rabbit_oauth2_storage:save_client(ClientId, Secret,
145 | RedirUrl, Scope),
146 | {ok, {n, Auth}} = oauth2:authorize_client_credentials({ClientId, Secret},
147 | Scope, n),
148 | {ok, {n, CodeResp}} = oauth2:issue_code(Auth, n),
149 | {ok, Code} = oauth2_response:access_code(CodeResp),
150 | {ok, {n, Auth1}} = oauth2:authorize_code_grant({ClientId, Secret},
151 | Code, RedirUrl, n),
152 | {ok, {n, TokenResp}} = oauth2:issue_token(Auth1, n),
153 | {error, invalid_authorization} = oauth2:issue_token_and_refresh(Auth1, n),
154 | {ok, AuthToken} = oauth2_response:access_token(TokenResp),
155 | {ok, {n, Ctx}} = oauth2:verify_access_token(AuthToken, n),
156 | Scope = proplists:get_value(<<"scope">>, Ctx).
157 |
158 | access_code_grant_test() ->
159 | ClientId = <<"foo1">>,
160 | Secret = <<"bar1">>,
161 | RedirUrl = <<"localhost">>,
162 | Scope = [<<"/_q_configure_foo">>],
163 | Username = <<"Derp">>,
164 | Password = <<"Pass">>,
165 | ok = rabbit_auth_backend_internal:add_user(Username, Password),
166 | ok = rabbit_auth_backend_internal:set_permissions(Username, <<"/">>,
167 | <<"fo.*">>,
168 | <<"fo.*">>,
169 | <<"fo.*">>),
170 | ok = rabbit_oauth2_storage:save_client(ClientId, Secret, RedirUrl, Scope),
171 | {ok, {n, Auth}} = oauth2:authorize_password({Username, Password},
172 | {ClientId, Secret},
173 | RedirUrl, Scope, n),
174 | {ok, {n, CodeResp}} = oauth2:issue_code(Auth, n),
175 | {ok, Code} = oauth2_response:access_code(CodeResp),
176 |
177 | {ok, {n, Auth1}} = oauth2:authorize_code_grant({ClientId, Secret},
178 | Code, RedirUrl, n),
179 | {ok, {n, TokenResp}} = oauth2:issue_token(Auth1, n),
180 | {ok, {n, RefreshTokenResp}} = oauth2:issue_token_and_refresh(Auth1, n),
181 | {ok, RefreshToken} = oauth2_response:refresh_token(RefreshTokenResp),
182 | {ok, {n, TokenResp1}} = oauth2:refresh_access_token({ClientId, Secret},
183 | RefreshToken, Scope, n).
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
--------------------------------------------------------------------------------
/rabbitmq-components.mk:
--------------------------------------------------------------------------------
1 | ifeq ($(.DEFAULT_GOAL),)
2 | # Define default goal to `all` because this file defines some targets
3 | # before the inclusion of erlang.mk leading to the wrong target becoming
4 | # the default.
5 | .DEFAULT_GOAL = all
6 | endif
7 |
8 | # Automatically add rabbitmq-common to the dependencies, at least for
9 | # the Makefiles.
10 | ifneq ($(PROJECT),rabbit_common)
11 | ifneq ($(PROJECT),rabbitmq_public_umbrella)
12 | ifeq ($(filter rabbit_common,$(DEPS)),)
13 | DEPS += rabbit_common
14 | endif
15 | endif
16 | endif
17 |
18 | # --------------------------------------------------------------------
19 | # RabbitMQ components.
20 | # --------------------------------------------------------------------
21 |
22 | # For RabbitMQ repositories, we want to checkout branches which match
23 | # the parent project. For instance, if the parent project is on a
24 | # release tag, dependencies must be on the same release tag. If the
25 | # parent project is on a topic branch, dependencies must be on the same
26 | # topic branch or fallback to `stable` or `master` whichever was the
27 | # base of the topic branch.
28 |
29 | dep_amqp_client = git_rmq rabbitmq-erlang-client $(current_rmq_ref) $(base_rmq_ref) master
30 | dep_rabbit = git_rmq rabbitmq-server $(current_rmq_ref) $(base_rmq_ref) master
31 | dep_rabbit_common = git_rmq rabbitmq-common $(current_rmq_ref) $(base_rmq_ref) master
32 | dep_rabbitmq_amqp1_0 = git_rmq rabbitmq-amqp1.0 $(current_rmq_ref) $(base_rmq_ref) master
33 | dep_rabbitmq_auth_backend_amqp = git_rmq rabbitmq-auth-backend-amqp $(current_rmq_ref) $(base_rmq_ref) master
34 | dep_rabbitmq_auth_backend_http = git_rmq rabbitmq-auth-backend-http $(current_rmq_ref) $(base_rmq_ref) master
35 | dep_rabbitmq_auth_backend_ldap = git_rmq rabbitmq-auth-backend-ldap $(current_rmq_ref) $(base_rmq_ref) master
36 | dep_rabbitmq_auth_mechanism_ssl = git_rmq rabbitmq-auth-mechanism-ssl $(current_rmq_ref) $(base_rmq_ref) master
37 | dep_rabbitmq_boot_steps_visualiser = git_rmq rabbitmq-boot-steps-visualiser $(current_rmq_ref) $(base_rmq_ref) master
38 | dep_rabbitmq_clusterer = git_rmq rabbitmq-clusterer $(current_rmq_ref) $(base_rmq_ref) master
39 | dep_rabbitmq_codegen = git_rmq rabbitmq-codegen $(current_rmq_ref) $(base_rmq_ref) master
40 | dep_rabbitmq_consistent_hash_exchange = git_rmq rabbitmq-consistent-hash-exchange $(current_rmq_ref) $(base_rmq_ref) master
41 | dep_rabbitmq_delayed_message_exchange = git_rmq rabbitmq-delayed-message-exchange $(current_rmq_ref) $(base_rmq_ref) master
42 | dep_rabbitmq_dotnet_client = git_rmq rabbitmq-dotnet-client $(current_rmq_ref) $(base_rmq_ref) master
43 | dep_rabbitmq_event_exchange = git_rmq rabbitmq-event-exchange $(current_rmq_ref) $(base_rmq_ref) master
44 | dep_rabbitmq_federation = git_rmq rabbitmq-federation $(current_rmq_ref) $(base_rmq_ref) master
45 | dep_rabbitmq_federation_management = git_rmq rabbitmq-federation-management $(current_rmq_ref) $(base_rmq_ref) master
46 | dep_rabbitmq_java_client = git_rmq rabbitmq-java-client $(current_rmq_ref) $(base_rmq_ref) master
47 | dep_rabbitmq_lvc = git_rmq rabbitmq-lvc-plugin $(current_rmq_ref) $(base_rmq_ref) master
48 | dep_rabbitmq_management = git_rmq rabbitmq-management $(current_rmq_ref) $(base_rmq_ref) master
49 | dep_rabbitmq_management_agent = git_rmq rabbitmq-management-agent $(current_rmq_ref) $(base_rmq_ref) master
50 | dep_rabbitmq_management_exchange = git_rmq rabbitmq-management-exchange $(current_rmq_ref) $(base_rmq_ref) master
51 | dep_rabbitmq_management_themes = git_rmq rabbitmq-management-themes $(current_rmq_ref) $(base_rmq_ref) master
52 | dep_rabbitmq_management_visualiser = git_rmq rabbitmq-management-visualiser $(current_rmq_ref) $(base_rmq_ref) master
53 | dep_rabbitmq_message_timestamp = git_rmq rabbitmq-message-timestamp $(current_rmq_ref) $(base_rmq_ref) master
54 | dep_rabbitmq_metronome = git_rmq rabbitmq-metronome $(current_rmq_ref) $(base_rmq_ref) master
55 | dep_rabbitmq_mqtt = git_rmq rabbitmq-mqtt $(current_rmq_ref) $(base_rmq_ref) master
56 | dep_rabbitmq_recent_history_exchange = git_rmq rabbitmq-recent-history-exchange $(current_rmq_ref) $(base_rmq_ref) master
57 | dep_rabbitmq_rtopic_exchange = git_rmq rabbitmq-rtopic-exchange $(current_rmq_ref) $(base_rmq_ref) master
58 | dep_rabbitmq_sharding = git_rmq rabbitmq-sharding $(current_rmq_ref) $(base_rmq_ref) master
59 | dep_rabbitmq_shovel = git_rmq rabbitmq-shovel $(current_rmq_ref) $(base_rmq_ref) master
60 | dep_rabbitmq_shovel_management = git_rmq rabbitmq-shovel-management $(current_rmq_ref) $(base_rmq_ref) master
61 | dep_rabbitmq_stomp = git_rmq rabbitmq-stomp $(current_rmq_ref) $(base_rmq_ref) master
62 | dep_rabbitmq_toke = git_rmq rabbitmq-toke $(current_rmq_ref) $(base_rmq_ref) master
63 | dep_rabbitmq_top = git_rmq rabbitmq-top $(current_rmq_ref) $(base_rmq_ref) master
64 | dep_rabbitmq_tracing = git_rmq rabbitmq-tracing $(current_rmq_ref) $(base_rmq_ref) master
65 | dep_rabbitmq_test = git_rmq rabbitmq-test $(current_rmq_ref) $(base_rmq_ref) master
66 | dep_rabbitmq_web_dispatch = git_rmq rabbitmq-web-dispatch $(current_rmq_ref) $(base_rmq_ref) master
67 | dep_rabbitmq_web_stomp = git_rmq rabbitmq-web-stomp $(current_rmq_ref) $(base_rmq_ref) master
68 | dep_rabbitmq_web_stomp_examples = git_rmq rabbitmq-web-stomp-examples $(current_rmq_ref) $(base_rmq_ref) master
69 | dep_rabbitmq_website = git_rmq rabbitmq-website $(current_rmq_ref) $(base_rmq_ref) live master
70 | dep_sockjs = git_rmq sockjs-erlang $(current_rmq_ref) $(base_rmq_ref) master
71 | dep_toke = git_rmq toke $(current_rmq_ref) $(base_rmq_ref) master
72 |
73 | dep_rabbitmq_public_umbrella = git_rmq rabbitmq-public-umbrella $(current_rmq_ref) $(base_rmq_ref) master
74 |
75 | # FIXME: As of 2015-11-20, we depend on Ranch 1.2.1, but erlang.mk
76 | # defaults to Ranch 1.1.0. All projects depending indirectly on Ranch
77 | # needs to add "ranch" as a BUILD_DEPS. The list of projects needing
78 | # this workaround are:
79 | # o rabbitmq-web-stomp
80 | dep_ranch = git https://github.com/ninenines/ranch 1.2.1
81 |
82 | RABBITMQ_COMPONENTS = amqp_client \
83 | rabbit \
84 | rabbit_common \
85 | rabbitmq_amqp1_0 \
86 | rabbitmq_auth_backend_amqp \
87 | rabbitmq_auth_backend_http \
88 | rabbitmq_auth_backend_ldap \
89 | rabbitmq_auth_mechanism_ssl \
90 | rabbitmq_boot_steps_visualiser \
91 | rabbitmq_clusterer \
92 | rabbitmq_codegen \
93 | rabbitmq_consistent_hash_exchange \
94 | rabbitmq_delayed_message_exchange \
95 | rabbitmq_dotnet_client \
96 | rabbitmq_event_exchange \
97 | rabbitmq_federation \
98 | rabbitmq_federation_management \
99 | rabbitmq_java_client \
100 | rabbitmq_lvc \
101 | rabbitmq_management \
102 | rabbitmq_management_agent \
103 | rabbitmq_management_exchange \
104 | rabbitmq_management_themes \
105 | rabbitmq_management_visualiser \
106 | rabbitmq_message_timestamp \
107 | rabbitmq_metronome \
108 | rabbitmq_mqtt \
109 | rabbitmq_recent_history_exchange \
110 | rabbitmq_rtopic_exchange \
111 | rabbitmq_sharding \
112 | rabbitmq_shovel \
113 | rabbitmq_shovel_management \
114 | rabbitmq_stomp \
115 | rabbitmq_test \
116 | rabbitmq_toke \
117 | rabbitmq_top \
118 | rabbitmq_tracing \
119 | rabbitmq_web_dispatch \
120 | rabbitmq_web_stomp \
121 | rabbitmq_web_stomp_examples \
122 | rabbitmq_website
123 |
124 | # Several components have a custom erlang.mk/build.config, mainly
125 | # to disable eunit. Therefore, we can't use the top-level project's
126 | # erlang.mk copy.
127 | NO_AUTOPATCH += $(RABBITMQ_COMPONENTS)
128 |
129 | ifeq ($(origin current_rmq_ref),undefined)
130 | ifneq ($(wildcard .git),)
131 | current_rmq_ref := $(shell (\
132 | ref=$$(git branch --list | awk '/^\* \(.*detached / {ref=$$0; sub(/.*detached [^ ]+ /, "", ref); sub(/\)$$/, "", ref); print ref; exit;} /^\* / {ref=$$0; sub(/^\* /, "", ref); print ref; exit}');\
133 | if test "$$(git rev-parse --short HEAD)" != "$$ref"; then echo "$$ref"; fi))
134 | else
135 | current_rmq_ref := master
136 | endif
137 | endif
138 | export current_rmq_ref
139 |
140 | ifeq ($(origin base_rmq_ref),undefined)
141 | ifneq ($(wildcard .git),)
142 | base_rmq_ref := $(shell \
143 | (git rev-parse --verify -q stable >/dev/null && \
144 | git merge-base --is-ancestor $$(git merge-base master HEAD) stable && \
145 | echo stable) || \
146 | echo master)
147 | else
148 | base_rmq_ref := master
149 | endif
150 | endif
151 | export base_rmq_ref
152 |
153 | # Repository URL selection.
154 | #
155 | # First, we infer other components' location from the current project
156 | # repository URL, if it's a Git repository:
157 | # - We take the "origin" remote URL as the base
158 | # - The current project name and repository name is replaced by the
159 | # target's properties:
160 | # eg. rabbitmq-common is replaced by rabbitmq-codegen
161 | # eg. rabbit_common is replaced by rabbitmq_codegen
162 | #
163 | # If cloning from this computed location fails, we fallback to RabbitMQ
164 | # upstream which is GitHub.
165 |
166 | # Maccro to transform eg. "rabbit_common" to "rabbitmq-common".
167 | rmq_cmp_repo_name = $(word 2,$(dep_$(1)))
168 |
169 | # Upstream URL for the current project.
170 | RABBITMQ_COMPONENT_REPO_NAME := $(call rmq_cmp_repo_name,$(PROJECT))
171 | RABBITMQ_UPSTREAM_FETCH_URL ?= https://github.com/rabbitmq/$(RABBITMQ_COMPONENT_REPO_NAME).git
172 | RABBITMQ_UPSTREAM_PUSH_URL ?= git@github.com:rabbitmq/$(RABBITMQ_COMPONENT_REPO_NAME).git
173 |
174 | # Current URL for the current project. If this is not a Git clone,
175 | # default to the upstream Git repository.
176 | ifneq ($(wildcard .git),)
177 | git_origin_fetch_url := $(shell git config remote.origin.url)
178 | git_origin_push_url := $(shell git config remote.origin.pushurl || git config remote.origin.url)
179 | RABBITMQ_CURRENT_FETCH_URL ?= $(git_origin_fetch_url)
180 | RABBITMQ_CURRENT_PUSH_URL ?= $(git_origin_push_url)
181 | else
182 | RABBITMQ_CURRENT_FETCH_URL ?= $(RABBITMQ_UPSTREAM_FETCH_URL)
183 | RABBITMQ_CURRENT_PUSH_URL ?= $(RABBITMQ_UPSTREAM_PUSH_URL)
184 | endif
185 |
186 | # Macro to replace the following pattern:
187 | # 1. /foo.git -> /bar.git
188 | # 2. /foo -> /bar
189 | # 3. /foo/ -> /bar/
190 | subst_repo_name = $(patsubst %/$(1)/%,%/$(2)/%,$(patsubst %/$(1),%/$(2),$(patsubst %/$(1).git,%/$(2).git,$(3))))
191 |
192 | # Macro to replace both the project's name (eg. "rabbit_common") and
193 | # repository name (eg. "rabbitmq-common") by the target's equivalent.
194 | #
195 | # This macro is kept on one line because we don't want whitespaces in
196 | # the returned value, as it's used in $(dep_fetch_git_rmq) in a shell
197 | # single-quoted string.
198 | dep_rmq_repo = $(if $(dep_$(2)),$(call subst_repo_name,$(PROJECT),$(2),$(call subst_repo_name,$(RABBITMQ_COMPONENT_REPO_NAME),$(call rmq_cmp_repo_name,$(2)),$(1))),$(pkg_$(1)_repo))
199 |
200 | dep_rmq_commits = $(if $(dep_$(1)), \
201 | $(wordlist 3,$(words $(dep_$(1))),$(dep_$(1))), \
202 | $(pkg_$(1)_commit))
203 |
204 | define dep_fetch_git_rmq
205 | fetch_url1='$(call dep_rmq_repo,$(RABBITMQ_CURRENT_FETCH_URL),$(1))'; \
206 | fetch_url2='$(call dep_rmq_repo,$(RABBITMQ_UPSTREAM_FETCH_URL),$(1))'; \
207 | if test "$$$$fetch_url1" != '$(RABBITMQ_CURRENT_FETCH_URL)' && \
208 | git clone -q -n -- "$$$$fetch_url1" $(DEPS_DIR)/$(call dep_name,$(1)); then \
209 | fetch_url="$$$$fetch_url1"; \
210 | push_url='$(call dep_rmq_repo,$(RABBITMQ_CURRENT_PUSH_URL),$(1))'; \
211 | elif git clone -q -n -- "$$$$fetch_url2" $(DEPS_DIR)/$(call dep_name,$(1)); then \
212 | fetch_url="$$$$fetch_url2"; \
213 | push_url='$(call dep_rmq_repo,$(RABBITMQ_UPSTREAM_PUSH_URL),$(1))'; \
214 | fi; \
215 | cd $(DEPS_DIR)/$(call dep_name,$(1)) && ( \
216 | $(foreach ref,$(call dep_rmq_commits,$(1)), \
217 | git checkout -q $(ref) >/dev/null 2>&1 || \
218 | ) \
219 | (echo "error: no valid pathspec among: $(call dep_rmq_commits,$(1))" \
220 | 1>&2 && false) ) && \
221 | (test "$$$$fetch_url" = "$$$$push_url" || \
222 | git remote set-url --push origin "$$$$push_url")
223 | endef
224 |
225 | # --------------------------------------------------------------------
226 | # Component distribution.
227 | # --------------------------------------------------------------------
228 |
229 | list-dist-deps::
230 | @:
231 |
232 | prepare-dist::
233 | @:
234 |
235 | # --------------------------------------------------------------------
236 | # Run a RabbitMQ node (moved from rabbitmq-run.mk as a workaround).
237 | # --------------------------------------------------------------------
238 |
239 | # Add "rabbit" to the build dependencies when the user wants to start
240 | # a broker or to the test dependencies when the user wants to test a
241 | # project.
242 | #
243 | # NOTE: This should belong to rabbitmq-run.mk. Unfortunately, it is
244 | # loaded *after* erlang.mk which is too late to add a dependency. That's
245 | # why rabbitmq-components.mk knows the list of targets which start a
246 | # broker and add "rabbit" to the dependencies in this case.
247 |
248 | ifneq ($(PROJECT),rabbit)
249 | ifeq ($(filter rabbit,$(DEPS) $(BUILD_DEPS)),)
250 | RUN_RMQ_TARGETS = run-broker \
251 | run-background-broker \
252 | run-node \
253 | run-background-node \
254 | start-background-node
255 |
256 | ifneq ($(filter $(RUN_RMQ_TARGETS),$(MAKECMDGOALS)),)
257 | BUILD_DEPS += rabbit
258 | endif
259 | endif
260 |
261 | ifeq ($(filter rabbit,$(DEPS) $(BUILD_DEPS) $(TEST_DEPS)),)
262 | ifneq ($(filter check tests tests-with-broker test,$(MAKECMDGOALS)),)
263 | TEST_DEPS += rabbit
264 | endif
265 | endif
266 | endif
267 |
268 | ifeq ($(filter rabbit_public_umbrella amqp_client rabbit_common rabbitmq_test,$(PROJECT)),)
269 | ifeq ($(filter rabbitmq_test,$(DEPS) $(BUILD_DEPS) $(TEST_DEPS)),)
270 | TEST_DEPS += rabbitmq_test
271 | endif
272 | endif
273 |
274 | # --------------------------------------------------------------------
275 | # rabbitmq-components.mk checks.
276 | # --------------------------------------------------------------------
277 |
278 | ifeq ($(PROJECT),rabbit_common)
279 | else ifdef SKIP_RMQCOMP_CHECK
280 | else ifeq ($(IS_DEP),1)
281 | else ifneq ($(filter co up,$(MAKECMDGOALS)),)
282 | else
283 | # In all other cases, rabbitmq-components.mk must be in sync.
284 | deps:: check-rabbitmq-components.mk
285 | fetch-deps: check-rabbitmq-components.mk
286 | endif
287 |
288 | # If this project is under the Umbrella project, we override $(DEPS_DIR)
289 | # to point to the Umbrella's one. We also disable `make distclean` so
290 | # $(DEPS_DIR) is not accidentally removed.
291 |
292 | ifneq ($(wildcard ../../UMBRELLA.md),)
293 | UNDER_UMBRELLA = 1
294 | else ifneq ($(wildcard UMBRELLA.md),)
295 | UNDER_UMBRELLA = 1
296 | endif
297 |
298 | ifeq ($(UNDER_UMBRELLA),1)
299 | ifneq ($(PROJECT),rabbitmq_public_umbrella)
300 | DEPS_DIR ?= $(abspath ..)
301 |
302 | distclean:: distclean-components
303 | @:
304 |
305 | distclean-components:
306 | endif
307 |
308 | ifneq ($(filter distclean distclean-deps,$(MAKECMDGOALS)),)
309 | SKIP_DEPS = 1
310 | endif
311 | endif
312 |
313 | UPSTREAM_RMQ_COMPONENTS_MK = $(DEPS_DIR)/rabbit_common/mk/rabbitmq-components.mk
314 |
315 | check-rabbitmq-components.mk:
316 | $(verbose) cmp -s rabbitmq-components.mk \
317 | $(UPSTREAM_RMQ_COMPONENTS_MK) || \
318 | (echo "error: rabbitmq-components.mk must be updated!" 1>&2; \
319 | false)
320 |
321 | ifeq ($(PROJECT),rabbit_common)
322 | rabbitmq-components-mk:
323 | @:
324 | else
325 | rabbitmq-components-mk:
326 | $(gen_verbose) cp -a $(UPSTREAM_RMQ_COMPONENTS_MK) .
327 | ifeq ($(DO_COMMIT),yes)
328 | $(verbose) git diff --quiet rabbitmq-components.mk \
329 | || git commit -m 'Update rabbitmq-components.mk' rabbitmq-components.mk
330 | endif
331 | endif
332 |
--------------------------------------------------------------------------------