diff --git a/deps/rabbitmq_auth_backend_oauth2/app.bzl b/deps/rabbitmq_auth_backend_oauth2/app.bzl index 5d18fb9ae2e4..a503e4b3544f 100644 --- a/deps/rabbitmq_auth_backend_oauth2/app.bzl +++ b/deps/rabbitmq_auth_backend_oauth2/app.bzl @@ -13,7 +13,6 @@ def all_beam_files(name = "all_beam_files"): "src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl", "src/rabbit_auth_backend_oauth2.erl", "src/rabbit_auth_backend_oauth2_app.erl", - "src/rabbit_oauth2_keycloak.erl", "src/rabbit_oauth2_provider.erl", "src/rabbit_oauth2_rar.erl", "src/rabbit_oauth2_resource_server.erl", @@ -51,7 +50,6 @@ def all_test_beam_files(name = "all_test_beam_files"): "src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl", "src/rabbit_auth_backend_oauth2.erl", "src/rabbit_auth_backend_oauth2_app.erl", - "src/rabbit_oauth2_keycloak.erl", "src/rabbit_oauth2_provider.erl", "src/rabbit_oauth2_rar.erl", "src/rabbit_oauth2_resource_server.erl", @@ -101,7 +99,6 @@ def all_srcs(name = "all_srcs"): "src/Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand.erl", "src/rabbit_auth_backend_oauth2.erl", "src/rabbit_auth_backend_oauth2_app.erl", - "src/rabbit_oauth2_keycloak.erl", "src/rabbit_oauth2_provider.erl", "src/rabbit_oauth2_rar.erl", "src/rabbit_oauth2_resource_server.erl", diff --git a/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl b/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl index 4652c16ddcd1..e7792e49298b 100644 --- a/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl +++ b/deps/rabbitmq_auth_backend_oauth2/include/oauth2.hrl @@ -22,6 +22,14 @@ %% End of Key JWT fields +%% UMA claim-type returns a RPT which is a token +%% where scopes are located under a map of list of objects which have +%% the scopes in the "scopes" attribute +%% Used by Keycloak, WSO2 and others. +%% https://en.wikipedia.org/wiki/User-Managed_Access#cite_note-docs.wso2.com-19 +-define(SCOPES_LOCATION_IN_REQUESTING_PARTY_TOKEN, <<"authorization.permissions.scopes">>). + + -type raw_jwt_token() :: binary() | #{binary() => any()}. -type decoded_jwt_token() :: #{binary() => any()}. diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl index df5ea8548d04..133a566f177c 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl @@ -28,10 +28,11 @@ get_scope/1, set_scope/2, resolve_resource_server/1]). --import(rabbit_oauth2_keycloak, [has_keycloak_scopes/1, extract_scopes_from_keycloak_format/1]). --import(rabbit_oauth2_rar, [extract_scopes_from_rich_auth_request/2, has_rich_auth_request_scopes/1]). +-import(rabbit_oauth2_rar, [extract_scopes_from_rich_auth_request/2]). --import(rabbit_oauth2_scope, [filter_matching_scope_prefix_and_drop_it/2]). +-import(rabbit_oauth2_scope, [ + filter_matching_scope_prefix/2, + filter_matching_scope_prefix_and_drop_it/2]). -ifdef(TEST). -compile(export_all). @@ -229,98 +230,152 @@ check_token(Token, {ResourceServer, InternalOAuthProvider}) -> {false, _} -> {refused, signature_invalid} end. +extract_scopes_from_scope_claim(Payload) -> + case maps:find(?SCOPE_JWT_FIELD, Payload) of + {ok, Bin} when is_binary(Bin) -> + maps:put(?SCOPE_JWT_FIELD, + binary:split(Bin, <<" ">>, [global, trim_all]), + Payload); + _ -> Payload + end. + -spec normalize_token_scope( ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map(). normalize_token_scope(ResourceServer, Payload) -> - Payload0 = maps:map(fun(K, V) -> - case K of - ?SCOPE_JWT_FIELD when is_binary(V) -> - binary:split(V, <<" ">>, [global, trim_all]); - _ -> V - end - end, Payload), - - Payload1 = case has_additional_scopes_key(ResourceServer, Payload0) of - true -> extract_scopes_from_additional_scopes_key(ResourceServer, Payload0); - false -> Payload0 - end, - - Payload2 = case has_keycloak_scopes(Payload1) of - true -> extract_scopes_from_keycloak_format(Payload1); - false -> Payload1 - end, - - Payload3 = case ResourceServer#resource_server.scope_aliases of - undefined -> Payload2; - ScopeAliases -> extract_scopes_using_scope_aliases(ScopeAliases, Payload2) - end, - - Payload4 = case has_rich_auth_request_scopes(Payload3) of - true -> extract_scopes_from_rich_auth_request(ResourceServer, Payload3); - false -> Payload3 - end, - - FilteredScopes = filter_matching_scope_prefix_and_drop_it( - get_scope(Payload4), ResourceServer#resource_server.scope_prefix), - set_scope(FilteredScopes, Payload4). + filter_duplicates( + filter_matching_scope_prefix(ResourceServer, + extract_scopes_from_rich_auth_request(ResourceServer, + extract_scopes_using_scope_aliases(ResourceServer, + extract_scopes_from_additional_scopes_key(ResourceServer, + extract_scopes_from_requesting_party_token(ResourceServer, + extract_scopes_from_scope_claim(Payload))))))). + +filter_duplicates(#{?SCOPE_JWT_FIELD := Scopes} = Payload) -> + set_scope(lists:usort(Scopes), Payload); +filter_duplicates(Payload) -> Payload. + +-spec extract_scopes_from_requesting_party_token( + ResourceServer :: resource_server(), DecodedToken :: decoded_jwt_token()) -> map(). +extract_scopes_from_requesting_party_token(ResourceServer, Payload) -> + Path = ?SCOPES_LOCATION_IN_REQUESTING_PARTY_TOKEN, + case extract_token_value(ResourceServer, Payload, Path, + fun extract_scope_list_from_token_value/2) of + [] -> + Payload; + AdditionalScopes -> + set_scope(lists:flatten(AdditionalScopes) ++ get_scope(Payload), Payload) + end. -spec extract_scopes_using_scope_aliases( - ScopeAliasMapping :: map(), Payload :: map()) -> map(). -extract_scopes_using_scope_aliases(ScopeAliasMapping, Payload) -> - Scopes0 = get_scope(Payload), - Scopes = rabbit_data_coercion:to_list_of_binaries(Scopes0), - %% for all scopes, look them up in the scope alias map, and if they are - %% present, add the alias to the final scope list. Note that we also preserve - %% the original scopes, it should not hurt. - ExpandedScopes = - lists:foldl(fun(ScopeListItem, Acc) -> - case maps:get(ScopeListItem, ScopeAliasMapping, undefined) of - undefined -> - Acc; - MappedList when is_list(MappedList) -> - Binaries = rabbit_data_coercion:to_list_of_binaries(MappedList), - Acc ++ Binaries; - Value -> - Binaries = rabbit_data_coercion:to_list_of_binaries(Value), - Acc ++ Binaries - end - end, Scopes, Scopes), - set_scope(ExpandedScopes, Payload). - --spec has_additional_scopes_key( - ResourceServer :: resource_server(), Payload :: map()) -> boolean(). -has_additional_scopes_key(ResourceServer, Payload) when is_map(Payload) -> - case ResourceServer#resource_server.additional_scopes_key of - undefined -> false; - ScopeKey -> maps:is_key(ScopeKey, Payload) + ResourceServer :: resource_server(), Payload :: map()) -> map(). +extract_scopes_using_scope_aliases( + #resource_server{scope_aliases = ScopeAliasMapping}, Payload) + when is_map(ScopeAliasMapping) -> + Scopes0 = get_scope(Payload), + Scopes = rabbit_data_coercion:to_list_of_binaries(Scopes0), + %% for all scopes, look them up in the scope alias map, and if they are + %% present, add the alias to the final scope list. Note that we also preserve + %% the original scopes, it should not hurt. + ExpandedScopes = + lists:foldl(fun(ScopeListItem, Acc) -> + case maps:get(ScopeListItem, ScopeAliasMapping, undefined) of + undefined -> + Acc; + MappedList when is_list(MappedList) -> + Binaries = rabbit_data_coercion:to_list_of_binaries(MappedList), + Acc ++ Binaries; + Value -> + Binaries = rabbit_data_coercion:to_list_of_binaries(Value), + Acc ++ Binaries + end + end, Scopes, Scopes), + set_scope(ExpandedScopes, Payload); +extract_scopes_using_scope_aliases(_, Payload) -> Payload. + +%% Path is a binary expression which is a plain word like <<"roles">> +%% or +1 word separated by . like <<"authorization.permissions.scopes">> +%% The Payload is a map. +%% Using the path <<"authorization.permissions.scopes">> as an example +%% 1. lookup the key <<"authorization">> in the Payload +%% 2. if it is found, the next map to use as payload is the value found from the key <<"authorization">> +%% 3. lookup the key <<"permissions">> in the previous map +%% 4. if it is found, it may be a map or a list of maps. +%% 5. if it is a list of maps, iterate each element in the list +%% 6. for each element in the list, which should be a map, find the key <<"scopes">> +%% 7. because there are no more words/keys, return a list of all the values found +%% associated to the word <<"scopes">> +extract_token_value(R, Payload, Path, ValueMapperFun) + when is_map(Payload), is_binary(Path), is_function(ValueMapperFun) -> + extract_token_value_from_map(R, Payload, [], split_path(Path), ValueMapperFun); +extract_token_value(_, _, _, _) -> + []. + +extract_scope_list_from_token_value(_R, List) when is_list(List) -> List; +extract_scope_list_from_token_value(_R, Binary) when is_binary(Binary) -> + binary:split(Binary, <<" ">>, [global, trim_all]); +extract_scope_list_from_token_value(#resource_server{id = ResourceServerId}, Map) when is_map(Map) -> + case maps:get(ResourceServerId, Map, undefined) of + undefined -> []; + Ks when is_list(Ks) -> + [erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- Ks]; + ClaimBin when is_binary(ClaimBin) -> + UnprefixedClaims = binary:split(ClaimBin, <<" ">>, [global, trim_all]), + [erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- UnprefixedClaims]; + _ -> [] + end; +extract_scope_list_from_token_value(_, _) -> []. + +extract_token_value_from_map(_, _Map, Acc, [], _Mapper) -> + Acc; +extract_token_value_from_map(R, Map, Acc, [KeyStr], Mapper) when is_map(Map) -> + case maps:find(KeyStr, Map) of + {ok, Value} -> Acc ++ Mapper(R, Value); + error -> Acc + end; +extract_token_value_from_map(R, Map, Acc, [KeyStr | Rest], Mapper) when is_map(Map) -> + case maps:find(KeyStr, Map) of + {ok, M} when is_map(M) -> extract_token_value_from_map(R, M, Acc, Rest, Mapper); + {ok, L} when is_list(L) -> extract_token_value_from_list(R, L, Acc, Rest, Mapper); + {ok, Value} when Rest =:= [] -> Acc ++ Mapper(R, Value); + _ -> Acc end. +extract_token_value_from_list(_, [], Acc, [], _Mapper) -> + Acc; +extract_token_value_from_list(_, [], Acc, [_KeyStr | _Rest], _Mapper) -> + Acc; +extract_token_value_from_list(R, [H | T], Acc, [KeyStr | Rest] = KeyList, Mapper) when is_map(H) -> + NewAcc = case maps:find(KeyStr, H) of + {ok, Map} when is_map(Map) -> extract_token_value_from_map(R, Map, Acc, Rest, Mapper); + {ok, List} when is_list(List) -> extract_token_value_from_list(R, List, Acc, Rest, Mapper); + {ok, Value} -> Acc++Mapper(R, Value); + _ -> Acc + end, + extract_token_value_from_list(R, T, NewAcc, KeyList, Mapper); + +extract_token_value_from_list(R, [E | T], Acc, [], Mapper) -> + extract_token_value_from_list(R, T, Acc++Mapper(R, E), [], Mapper); +extract_token_value_from_list(R, [E | _T] = L, Acc, KeyList, Mapper) when is_map(E) -> + extract_token_value_from_list(R, L, Acc, KeyList, Mapper); +extract_token_value_from_list(R, [_ | T], Acc, KeyList, Mapper) -> + extract_token_value_from_list(R, T, Acc, KeyList, Mapper). + + +split_path(Path) when is_binary(Path) -> + binary:split(Path, <<".">>, [global, trim_all]). + + -spec extract_scopes_from_additional_scopes_key( ResourceServer :: resource_server(), Payload :: map()) -> map(). -extract_scopes_from_additional_scopes_key(ResourceServer, Payload) -> - Claim = maps:get(ResourceServer#resource_server.additional_scopes_key, Payload), - AdditionalScopes = extract_additional_scopes(ResourceServer, Claim), - set_scope(AdditionalScopes ++ get_scope(Payload), Payload). - -extract_additional_scopes(ResourceServer, ComplexClaim) -> - ResourceServerId = ResourceServer#resource_server.id, - case ComplexClaim of - L when is_list(L) -> L; - M when is_map(M) -> - case maps:get(ResourceServerId, M, undefined) of - undefined -> []; - Ks when is_list(Ks) -> - [erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- Ks]; - ClaimBin when is_binary(ClaimBin) -> - UnprefixedClaims = binary:split(ClaimBin, <<" ">>, [global, trim_all]), - [erlang:iolist_to_binary([ResourceServerId, <<".">>, K]) || K <- UnprefixedClaims]; - _ -> [] - end; - Bin when is_binary(Bin) -> - binary:split(Bin, <<" ">>, [global, trim_all]); - _ -> [] - end. +extract_scopes_from_additional_scopes_key( + #resource_server{additional_scopes_key = Key} = ResourceServer, Payload) + when is_binary(Key) -> + Paths = binary:split(Key, <<" ">>, [global, trim_all]), + AdditionalScopes = [ extract_token_value(ResourceServer, + Payload, Path, fun extract_scope_list_from_token_value/2) || Path <- Paths], + set_scope(lists:flatten(AdditionalScopes) ++ get_scope(Payload), Payload); +extract_scopes_from_additional_scopes_key(_, Payload) -> Payload. %% A token may be present in the password credential or in the rabbit_auth_backend_oauth2 diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_keycloak.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_keycloak.erl deleted file mode 100644 index e75910e48055..000000000000 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_keycloak.erl +++ /dev/null @@ -1,41 +0,0 @@ -%% This Source Code Form is subject to the terms of the Mozilla Public -%% License, v. 2.0. If a copy of the MPL was not distributed with this -%% file, You can obtain one at https://mozilla.org/MPL/2.0/. -%% -%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -%% - --module(rabbit_oauth2_keycloak). - --include("oauth2.hrl"). - --export([extract_scopes_from_keycloak_format/1, has_keycloak_scopes/1]). --import(uaa_jwt, [get_scope/1, set_scope/2]). - --define(AUTHORIZATION_CLAIM, <<"authorization">>). --define(PERMISSIONS_CLAIM, <<"permissions">>). --define(SCOPES_CLAIM, <<"scopes">>). - --spec has_keycloak_scopes(Payload::map()) -> boolean(). -has_keycloak_scopes(Payload) -> - maps:is_key(?AUTHORIZATION_CLAIM, Payload). - --spec extract_scopes_from_keycloak_format(Payload :: map()) -> map(). -%% keycloak token format: https://github.com/rabbitmq/rabbitmq-auth-backend-oauth2/issues/36 -extract_scopes_from_keycloak_format(#{?AUTHORIZATION_CLAIM := Authorization} = Payload) -> - AdditionalScopes = extract_scopes_from_keycloak_permissions([], - maps:get(?PERMISSIONS_CLAIM, Authorization, [])), - set_scope(AdditionalScopes ++ get_scope(Payload), Payload). - -extract_scopes_from_keycloak_permissions(Acc, []) -> - Acc; -extract_scopes_from_keycloak_permissions(Acc, [H | T]) when is_map(H) -> - Scopes = case maps:get(?SCOPES_CLAIM, H, []) of - ScopesAsList when is_list(ScopesAsList) -> - ScopesAsList; - ScopesAsBinary when is_binary(ScopesAsBinary) -> - [ScopesAsBinary] - end, - extract_scopes_from_keycloak_permissions(Acc ++ Scopes, T); -extract_scopes_from_keycloak_permissions(Acc, [_ | T]) -> - extract_scopes_from_keycloak_permissions(Acc, T). diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_rar.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_rar.erl index 9d7a583fa4c1..5e71c81dd7d8 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_rar.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_rar.erl @@ -11,7 +11,7 @@ -include("oauth2.hrl"). -import(uaa_jwt, [get_scope/1, set_scope/2]). --export([extract_scopes_from_rich_auth_request/2, has_rich_auth_request_scopes/1]). +-export([extract_scopes_from_rich_auth_request/2]). -define(AUTHORIZATION_DETAILS_CLAIM, <<"authorization_details">>). -define(RAR_ACTIONS_FIELD, <<"actions">>). @@ -44,15 +44,12 @@ <<"management">>, <<"policymaker">> ]). --spec has_rich_auth_request_scopes(Payload::map()) -> boolean(). -has_rich_auth_request_scopes(Payload) -> - maps:is_key(?AUTHORIZATION_DETAILS_CLAIM, Payload). - -spec extract_scopes_from_rich_auth_request(ResourceServer :: resource_server(), Payload :: map()) -> map(). %% https://oauth.net/2/rich-authorization-requests/ extract_scopes_from_rich_auth_request(ResourceServer, - #{?AUTHORIZATION_DETAILS_CLAIM := Permissions} = Payload) -> + #{?AUTHORIZATION_DETAILS_CLAIM := Permissions} = Payload) + when is_list(Permissions) -> ResourceServerType = ResourceServer#resource_server.resource_server_type, FilteredPermissionsByType = lists:filter(fun(P) -> @@ -61,7 +58,8 @@ extract_scopes_from_rich_auth_request(ResourceServer, ResourceServer#resource_server.id, FilteredPermissionsByType), ExistingScopes = get_scope(Payload), - set_scope(AdditionalScopes ++ ExistingScopes, Payload). + set_scope(AdditionalScopes ++ ExistingScopes, Payload); +extract_scopes_from_rich_auth_request(_, Payload) -> Payload. put_location_attribute(Attribute, Map) -> put_attribute(binary:split(Attribute, <<":">>, [global, trim_all]), Map). diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl index 7391c9e3857b..7e1efd24706f 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_scope.erl @@ -7,10 +7,13 @@ -module(rabbit_oauth2_scope). +-include("oauth2.hrl"). + -export([vhost_access/2, resource_access/3, topic_access/4, concat_scopes/2, + filter_matching_scope_prefix/2, filter_matching_scope_prefix_and_drop_it/2]). -include_lib("rabbit_common/include/rabbit.hrl"). @@ -93,8 +96,17 @@ parse_resource_pattern(Pattern, Permission) -> _Other -> ignore end. +-spec filter_matching_scope_prefix(ResourceServer :: resource_server(), + Payload :: map()) -> map(). +filter_matching_scope_prefix( + #resource_server{scope_prefix = ScopePrefix}, + #{?SCOPE_JWT_FIELD := Scopes} = Payload) -> + Payload#{?SCOPE_JWT_FIELD := + filter_matching_scope_prefix_and_drop_it(Scopes, ScopePrefix)}; +filter_matching_scope_prefix(_, Payload) -> Payload. + -spec filter_matching_scope_prefix_and_drop_it(list(), binary()|list()) -> list(). -filter_matching_scope_prefix_and_drop_it(Scopes, <<"">>) -> Scopes; +filter_matching_scope_prefix_and_drop_it(Scopes, <<>>) -> Scopes; filter_matching_scope_prefix_and_drop_it(Scopes, PrefixPattern) -> PatternLength = byte_size(PrefixPattern), lists:filtermap( diff --git a/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets b/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets index 7b6b148b5944..4db415c113a3 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets +++ b/deps/rabbitmq_auth_backend_oauth2/test/config_schema_SUITE_data/rabbitmq_auth_backend_oauth2.snippets @@ -316,5 +316,15 @@ } ]} ], [] + }, + {additional_scopes_key, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.additional_scopes_key = roles realm.roles", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id,<<"new_resource_server_id">>}, + {extra_scopes_source, <<"roles realm.roles">> } + ]} + ], [] } ]. diff --git a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl index 5dedc8cefc85..d920db3ec05e 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl @@ -17,13 +17,16 @@ user_login_authentication/2, user_login_authorization/2, normalize_token_scope/2, - check_vhost_access/3]). + check_vhost_access/3, + extract_token_value/4, + extract_scope_list_from_token_value/2]). -import(rabbit_oauth2_resource_server, [ new_resource_server/1 ]). all() -> [ + test_extract_scope_from_path_expression, filter_matching_scope_prefix_and_drop_it, normalize_token_scopes_with_scope_prefix, normalize_token_scope_from_space_separated_list_in_scope_claim, @@ -39,13 +42,15 @@ all() -> test_token_expiration, test_invalid_signature, test_incorrect_kid, - normalize_token_scope_with_keycloak_scopes, + normalize_token_scope_using_multiple_scopes_key, + normalize_token_scope_with_requesting_party_token_scopes, normalize_token_scope_with_rich_auth_request, normalize_token_scope_with_rich_auth_request_using_regular_expression_with_cluster, test_unsuccessful_access_with_a_token_that_uses_missing_scope_alias_in_scope_field, test_unsuccessful_access_with_a_token_that_uses_missing_scope_alias_in_extra_scope_source_field, test_username_from, {group, with_rabbitmq_node} + ]. groups() -> [ @@ -116,8 +121,75 @@ end_per_group(_, Config) -> -define(RESOURCE_SERVER_TYPE, <<"rabbitmq-type">>). -define(DEFAULT_SCOPE_PREFIX, <<"rabbitmq.">>). +normalize_token_scope_using_multiple_scopes_key(_) -> + Pairs = [ + %% common case + { + "keycloak format 1, i.e. requesting party token", + #{<<"authorization">> => + #{<<"permissions">> => + [#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>, + <<"rsname">> => <<"allvhost">>, + <<"scopes">> => [<<"rabbitmq-resource.read:*/*">>]}, + #{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>, + <<"rsname">> => <<"vhost1">>, + <<"scopes">> => [<<"rabbitmq-resource.write:vhost1/*">>]}, + #{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>, + <<"rsname">> => <<"Default Resource">>, + <<"scopes">> => [<<"unknown-resource.write:vhost1/*">>]} + ] + } + }, + [<<"read:*/*">>, <<"write:vhost1/*">>] + }, + { + "keycloak format 2 using realm_access", + #{<<"realm_access">> => + #{<<"roles">> => [<<"rabbitmq-resource.read:format2/*">>]} + }, + [<<"read:format2/*">>] + }, + { + "keycloak format 2 using resource_access", + #{<<"resource_access">> => + #{<<"account">> => #{<<"roles">> => [<<"rabbitmq-resource.read:format2bis/*">>]} } + }, + [<<"read:format2bis/*">>] + }, + { + "both formats", + #{<<"authorization">> => + #{<<"permissions">> => + [#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>, + <<"rsname">> => <<"allvhost">>, + <<"scopes">> => [<<"rabbitmq-resource.read:*/*">>]}, + #{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>, + <<"rsname">> => <<"vhost1">>, + <<"scopes">> => [<<"rabbitmq-resource.write:vhost1/*">>]}, + #{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>, + <<"rsname">> => <<"Default Resource">>, + <<"scopes">> => [<<"unknown-resource.write:vhost1/*">>]} + ] + }, + <<"realm_access">> => + #{<<"roles">> => [<<"rabbitmq-resource.read:format2/*">>]}, + <<"resource_access">> => + #{<<"account">> => #{<<"roles">> => [<<"rabbitmq-resource.read:format2bis/*">>]} } + }, + [<<"read:*/*">>, <<"write:vhost1/*">>, <<"read:format2/*">>, <<"read:format2bis/*">>] + } + ], + + lists:foreach(fun({Case, Token0, ExpectedScope}) -> + ResourceServer0 = new_resource_server(<<"rabbitmq-resource">>), + ResourceServer = ResourceServer0#resource_server{ + additional_scopes_key = <<"authorization.permissions.scopes realm_access.roles resource_access.account.roles">> + }, + Token = normalize_token_scope(ResourceServer, Token0), + ?assertEqual(lists:sort(ExpectedScope), lists:sort(uaa_jwt:get_scope(Token)), Case) + end, Pairs). -normalize_token_scope_with_keycloak_scopes(_) -> +normalize_token_scope_with_requesting_party_token_scopes(_) -> Pairs = [ %% common case { @@ -169,9 +241,9 @@ normalize_token_scope_with_keycloak_scopes(_) -> ], lists:foreach(fun({Case, Authorization, ExpectedScope}) -> - ResourceServer = new_resource_server(<<"rabbitmq-resource">>), + ResourceServer0 = new_resource_server(<<"rabbitmq-resource">>), Token0 = #{<<"authorization">> => Authorization}, - Token = normalize_token_scope(ResourceServer, Token0), + Token = normalize_token_scope(ResourceServer0, Token0), ?assertEqual(ExpectedScope, uaa_jwt:get_scope(Token), Case) end, Pairs). @@ -356,7 +428,7 @@ normalize_token_scope_with_rich_auth_request(_) -> } ], [<<"tag:management">>, <<"tag:policymaker">>, - <<"tag:management">>, <<"tag:monitoring">> ] + <<"tag:monitoring">> ] }, { "should produce a scope for every user tag action but only for the clusters that match {resource_server_id}", [ #{<<"type">> => ?RESOURCE_SERVER_TYPE, @@ -1286,6 +1358,77 @@ normalize_token_scope_without_scope_claim(_) -> Token0 = #{ }, ?assertEqual([], uaa_jwt:get_scope(normalize_token_scope(ResourceServer, Token0))). + +test_extract_scope_from_path_expression(_) -> + M = fun rabbit_auth_backend_oauth2:extract_scope_list_from_token_value/2, + R = #resource_server{id = <<"rabbitmq">>}, + + [<<"role1">>] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => <<"role1">> }}, + <<"auth.permission">>, M), + [<<"role1">>,<<"role2">>] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => [<<"role1">>,<<"role2">>] }}, + <<"auth.permission">>, M), + [<<"role1">>,<<"role2">>] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => <<"role1 role2">> }}, + <<"auth.permission">>, M), + [<<"rabbitmq.role1">>,<<"rabbitmq.role2">>] = extract_token_value(R, + #{ <<"auth">> => #{ + <<"rabbitmq">> => [<<"role1">>,<<"role2">>] + }}, + <<"auth">>, M), + [<<"rabbitmq.role1">>,<<"rabbitmq.role2">>] = extract_token_value(R, + #{ <<"auth">> => #{ + <<"rabbitmq">> => <<"role1 role2">> + }}, + <<"auth">>, M), + %% this is the old keycloak format + [<<"role1">>,<<"role2">>] = extract_token_value(R, + #{ <<"auth">> => #{ + <<"permission">> => [ + #{ <<"scopes">> => <<"role1">>}, + #{ <<"scopes">> => <<"role2">>} + ] + }}, + <<"auth.permission.scopes">>, M), + + [<<"role1">>,<<"role2">>] = extract_token_value(R, + #{ <<"auth">> => #{ + <<"permission">> => [ + #{ <<"scopes">> => [<<"role1">>]}, + #{ <<"scopes">> => [<<"role2">>]} + ] + }}, + <<"auth.permission.scopes">>, M), + + [<<"role1">>,<<"role2">>] = extract_token_value(R, + #{ <<"auth">> => [ + #{ <<"permission">> => [ + #{ <<"scopes">> => [<<"role1">>]} + ]}, + #{ <<"permission">> => [ + #{ <<"scopes">> => [<<"role2">>]} + ]} + ]}, + <<"auth.permission.scopes">>, M), + + [<<"role1">>] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }}, + <<"auth.permission">>, M), + + [] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }}, + <<"auth.permission2">>, M), + + [] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }}, + <<"auth2.permission">>, M), + + [] = extract_token_value(R, + #{ <<"auth">> => #{ <<"permission">> => [<<"role1">>] }}, + <<"auth.permission2">>, M). + + %% %% Helpers %% diff --git a/deps/rabbitmq_mqtt/test/auth_SUITE.erl b/deps/rabbitmq_mqtt/test/auth_SUITE.erl index a7a4ea78f1d8..037d161aad4d 100644 --- a/deps/rabbitmq_mqtt/test/auth_SUITE.erl +++ b/deps/rabbitmq_mqtt/test/auth_SUITE.erl @@ -592,7 +592,7 @@ invalid_client_id_from_cert_san_dns(Config) -> MqttClientId = <<"other_client_id">>, {ok, C} = connect_ssl(MqttClientId, Config), unlink(C), - {error, {client_identifier_not_valid, _}} = emqtt:connect(C). + {error, {client_identifier_not_valid, _}} = emqtt:connect(C). ssl_user_vhost_parameter_mapping_success(Config) -> expect_successful_connection(fun connect_ssl/1, Config).