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 8358bc374c..7b7cbdf16f 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 @@ -37,7 +37,7 @@ %% a term used by the IdentityServer community -define(COMPLEX_CLAIM_APP_ENV_KEY, extra_scopes_source). %% scope aliases map "role names" to a set of scopes --define(SCOPE_ALIASES_APP_ENV_KEY, scope_aliases). +-define(SCOPE_MAPPINGS_APP_ENV_KEY, scope_aliases). %% %% Key JWT fields @@ -201,7 +201,7 @@ post_process_payload(Payload, AppEnv) when is_map(Payload) -> -spec has_configured_scope_aliases(AppEnv :: app_env()) -> boolean(). has_configured_scope_aliases(AppEnv) -> Map = maps:from_list(AppEnv), - maps:is_key(scope_mappings, Map). + maps:is_key(?SCOPE_MAPPINGS_APP_ENV_KEY, Map). -spec post_process_payload_with_scope_aliases(Payload :: map(), AppEnv :: app_env()) -> map(). @@ -222,7 +222,7 @@ post_process_payload_with_scope_aliases(Payload, AppEnv) -> AppEnv :: app_env()) -> map(). %% First attempt: use the value in the 'scope' field for alias post_process_payload_with_scope_alias_in_scope_field(Payload, AppEnv) -> - ScopeMappings = proplists:get_value(?SCOPE_ALIASES_APP_ENV_KEY, AppEnv, #{}), + ScopeMappings = proplists:get_value(?SCOPE_MAPPINGS_APP_ENV_KEY, AppEnv, #{}), post_process_payload_with_scope_alias_field_named(Payload, ?SCOPE_JWT_FIELD, ScopeMappings). @@ -232,32 +232,44 @@ post_process_payload_with_scope_alias_in_scope_field(Payload, AppEnv) -> post_process_payload_with_scope_alias_in_extra_scopes_source(Payload, AppEnv) -> ExtraScopesField = proplists:get_value(?COMPLEX_CLAIM_APP_ENV_KEY, AppEnv, undefined), case ExtraScopesField of - %% nothing ot inject + %% nothing to inject undefined -> Payload; _ -> - ScopeMappings = proplists:get_value(?SCOPE_ALIASES_APP_ENV_KEY, AppEnv, #{}), + ScopeMappings = proplists:get_value(?SCOPE_MAPPINGS_APP_ENV_KEY, AppEnv, #{}), post_process_payload_with_scope_alias_field_named(Payload, ExtraScopesField, ScopeMappings) end. -spec post_process_payload_with_scope_alias_field_named(Payload :: map(), Field :: binary(), - Scopes :: map()) -> map(). -post_process_payload_with_scope_alias_field_named(Payload, undefined, _AppEnv) -> + ScopeAliasMapping :: map()) -> map(). +post_process_payload_with_scope_alias_field_named(Payload, undefined, _ScopeAliasMapping) -> Payload; -post_process_payload_with_scope_alias_field_named(Payload, ScopeAlias, AppEnv) -> - AdditionalScopes = case ScopeAlias of +post_process_payload_with_scope_alias_field_named(Payload, FieldName, ScopeAliasMapping) -> + ExistingScopes = maps:get(?SCOPE_JWT_FIELD, Payload, []), + AdditionalScopes = case FieldName of undefined -> []; - _ -> - ScopeMappings = proplists:get_value(<<"scope_mappings">>, AppEnv, #{}), - maps:get(ScopeAlias, ScopeMappings, []) + [] -> []; + _Value -> + ScopeAlias = maps:get(FieldName, Payload, undefined), + case ScopeAlias of + undefined -> []; + [] -> []; + [Value1] -> + rabbit_data_coercion:to_list(maps:get(Value1, ScopeAliasMapping, [])); + Value2 when is_binary(Value2) -> + maps:get(Value2, ScopeAliasMapping, []); + Value3 when is_list(Value3) -> + maps:get(list_to_binary(Value3), ScopeAliasMapping, []) + end end, case AdditionalScopes of - [] -> Payload; - _ -> - ExistingScopes = maps:get(?SCOPE_JWT_FIELD, Payload, []), - maps:put(?SCOPE_JWT_FIELD, AdditionalScopes ++ ExistingScopes, Payload) + [] -> Payload; + List when is_list(List) -> + maps:put(?SCOPE_JWT_FIELD, AdditionalScopes ++ ExistingScopes, Payload); + Bin when is_binary(Bin) -> + maps:put(?SCOPE_JWT_FIELD, [Bin | ExistingScopes], Payload) end. diff --git a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_auth_backend_oauth2_test_util.erl b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_auth_backend_oauth2_test_util.erl index b367a3a606..4374339f1c 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_auth_backend_oauth2_test_util.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_auth_backend_oauth2_test_util.erl @@ -70,10 +70,10 @@ expired_token() -> expired_token_with_scopes(full_permission_scopes()). expired_token_with_scopes(Scopes) -> - token_with_scopes_and_expiration(Scopes, os:system_time(seconds) - 10). + token_with_scopes_and_expiration(Scopes, seconds_in_the_past(10)). fixture_token_with_scopes(Scopes) -> - token_with_scopes_and_expiration(Scopes, os:system_time(seconds) + 30). + token_with_scopes_and_expiration(Scopes, default_expiration_moment()). token_with_scopes_and_expiration(Scopes, Expiration) -> %% expiration is a timestamp with precision in seconds @@ -97,3 +97,37 @@ fixture_token(ExtraScopes) -> fixture_token_with_full_permissions() -> fixture_token_with_scopes(full_permission_scopes()). + +token_with_scope_alias_in_scope_field(Alias) -> + %% expiration is a timestamp with precision in seconds + #{<<"exp">> => default_expiration_moment(), + <<"kid">> => <<"token-key">>, + <<"iss">> => <<"unit_test">>, + <<"foo">> => <<"bar">>, + <<"aud">> => [<<"rabbitmq">>], + <<"scope">> => Alias}. + +token_with_scope_alias_in_claim_field(Alias, Scopes) -> + %% expiration is a timestamp with precision in seconds + #{<<"exp">> => default_expiration_moment(), + <<"kid">> => <<"token-key">>, + <<"iss">> => <<"unit_test">>, + <<"foo">> => <<"bar">>, + <<"aud">> => [<<"rabbitmq">>], + <<"scope">> => Scopes, + <<"claims">> => Alias}. + +seconds_in_the_future() -> + seconds_in_the_future(30). + +seconds_in_the_future(N) -> + os:system_time(seconds) + N. + +seconds_in_the_past() -> + seconds_in_the_past(10). + +seconds_in_the_past(N) -> + os:system_time(seconds) - N. + +default_expiration_moment() -> + seconds_in_the_future(30). diff --git a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl index 91e8e6fe7a..2b48e3d8ab 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl @@ -29,7 +29,8 @@ all() -> test_incorrect_kid, test_post_process_token_payload, test_post_process_token_payload_keycloak, - test_post_process_token_payload_complex_claims + test_post_process_token_payload_complex_claims, + test_successful_access_with_a_token_that_uses_scope_alias_in_scope_field ]. init_per_suite(Config) -> @@ -287,6 +288,42 @@ test_successful_access_with_a_token_that_has_tag_scopes(_) -> {ok, #auth_user{username = Username, tags = [management, policymaker]}} = rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]). +test_successful_access_with_a_token_that_uses_scope_alias_in_scope_field(_) -> + Jwk = ?UTIL_MOD:fixture_jwk(), + UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}], + application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv), + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + Alias = <<"client-alias-1">>, + application:set_env(rabbitmq_auth_backend_oauth2, scope_aliases, #{ + Alias => [ + <<"rabbitmq.configure:vhost/one">>, + <<"rabbitmq.write:vhost/two">>, + <<"rabbitmq.read:vhost/one">>, + <<"rabbitmq.read:vhost/two">>, + <<"rabbitmq.read:vhost/two/abc">> + ] + }), + + VHost = <<"vhost">>, + Username = <<"username">>, + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:token_with_scope_alias_in_scope_field(Alias), Jwk), + + {ok, AuthUser} = rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]), + assert_vhost_access_granted(AuthUser, VHost), + assert_vhost_access_denied(AuthUser, <<"some-other-vhost">>), + + assert_resource_access_granted(AuthUser, VHost, <<"one">>, configure), + assert_resource_access_granted(AuthUser, VHost, <<"one">>, read), + assert_resource_access_granted(AuthUser, VHost, <<"two">>, read), + assert_resource_access_granted(AuthUser, VHost, <<"two">>, write), + assert_resource_access_denied(AuthUser, VHost, <<"three">>, configure), + assert_resource_access_denied(AuthUser, VHost, <<"three">>, read), + assert_resource_access_denied(AuthUser, VHost, <<"three">>, write), + + application:unset_env(rabbitmq_auth_backend_oauth2, scope_aliases), + application:unset_env(rabbitmq_auth_backend_oauth2, key_config), + application:unset_env(rabbitmq_auth_backend_oauth2, resource_server_id). + test_unsuccessful_access_with_a_bogus_token(_) -> Username = <<"username">>, application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), @@ -549,3 +586,31 @@ test_validate_payload(_) -> ?assertEqual({ok, #{<<"aud">> => [?RESOURCE_SERVER_ID], <<"scope">> => [<<"bar">>, <<"other.third">>]}}, rabbit_auth_backend_oauth2:validate_payload(KnownResourceServerId, ?RESOURCE_SERVER_ID)). + + +%% +%% Helpers +%% + +assert_vhost_access_granted(AuthUser, VHost) -> + assert_vhost_access_response(true, AuthUser, VHost). + +assert_vhost_access_denied(AuthUser, VHost) -> + assert_vhost_access_response(false, AuthUser, VHost). + +assert_vhost_access_response(ExpectedResult, AuthUser, VHost) -> + ?assertEqual(ExpectedResult, + rabbit_auth_backend_oauth2:check_vhost_access(AuthUser, VHost, none)). + +assert_resource_access_granted(AuthUser, VHost, ResourceName, PermissionKind) -> + assert_resource_access_response(true, AuthUser, VHost, ResourceName, PermissionKind). + +assert_resource_access_denied(AuthUser, VHost, ResourceName, PermissionKind) -> + assert_resource_access_response(false, AuthUser, VHost, ResourceName, PermissionKind). + +assert_resource_access_response(ExpectedResult, AuthUser, VHost, ResourceName, PermissionKind) -> + ?assertEqual(ExpectedResult, + rabbit_auth_backend_oauth2:check_resource_access( + AuthUser, + rabbit_misc:r(VHost, queue, ResourceName), + PermissionKind, #{})).