diff --git a/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema b/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema index 5379f87560de..188487654d2d 100644 --- a/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema +++ b/deps/rabbitmq_auth_backend_oauth2/priv/schema/rabbitmq_auth_backend_oauth2.schema @@ -73,6 +73,26 @@ list_to_binary(cuttlefish:conf_get("auth_oauth2.additional_scopes_key", Conf)) end}. +{mapping, + "auth_oauth2.scope_aliases.$alias", + "rabbitmq_auth_backend_oauth2.scope_aliases", + [{datatype, string}]}. + +{mapping, + "auth_oauth2.scope_aliases.$index.alias", + "rabbitmq_auth_backend_oauth2.scope_aliases", + [{datatype, string}]}. + +{mapping, + "auth_oauth2.scope_aliases.$index.scope", + "rabbitmq_auth_backend_oauth2.scope_aliases", + [{datatype, string}]}. + +{translation, + "rabbitmq_auth_backend_oauth2.scope_aliases", + fun(Conf) -> + rabbit_oauth2_schema:translate_scope_aliases(Conf) + end}. %% Configure the plugin to skip validation of the aud field %% @@ -355,6 +375,21 @@ [{datatype, string}] }. +{mapping, + "auth_oauth2.resource_servers.$name.scope_aliases.$alias", + "rabbitmq_auth_backend_oauth2.resource_servers", + [{datatype, string}]}. + +{mapping, + "auth_oauth2.resource_servers.$name.scope_aliases.$index.alias", + "rabbitmq_auth_backend_oauth2.resource_servers", + [{datatype, string}]}. + +{mapping, + "auth_oauth2.resource_servers.$name.scope_aliases.$index.scope", + "rabbitmq_auth_backend_oauth2.resource_servers", + [{datatype, string}]}. + {mapping, "auth_oauth2.resource_servers.$name.oauth_provider_id", "rabbitmq_auth_backend_oauth2.resource_servers", diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl index 72642a43dc1e..aa6aec1df49b 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_oauth2_schema.erl @@ -7,24 +7,131 @@ -module(rabbit_oauth2_schema). +-define(AUTH_OAUTH2, "auth_oauth2"). +-define(SCOPE_ALIASES, "scope_aliases"). +-define(RESOURCE_SERVERS, "resource_servers"). +-define(OAUTH_PROVIDERS, "oauth_providers"). +-define(SIGNING_KEYS, "signing_keys"). +-define(AUTH_OAUTH2_SCOPE_ALIASES, ?AUTH_OAUTH2 ++ "." ++ ?SCOPE_ALIASES). +-define(AUTH_OAUTH2_RESOURCE_SERVERS, ?AUTH_OAUTH2 ++ "." ++ ?RESOURCE_SERVERS). +-define(AUTH_OAUTH2_OAUTH_PROVIDERS, ?AUTH_OAUTH2 ++ "." ++ ?OAUTH_PROVIDERS). +-define(AUTH_OAUTH2_SIGNING_KEYS, ?AUTH_OAUTH2 ++ "." ++ ?SIGNING_KEYS). -export([ translate_oauth_providers/1, translate_resource_servers/1, translate_signing_keys/1, - translate_endpoint_params/2 + translate_endpoint_params/2, + translate_scope_aliases/1 ]). extract_key_as_binary({Name,_}) -> list_to_binary(Name). extract_value({_Name,V}) -> V. +-spec translate_scope_aliases([{list(), binary()}]) -> map(). +translate_scope_aliases(Conf) -> + Settings = cuttlefish_variable:filter_by_prefix( + ?AUTH_OAUTH2_SCOPE_ALIASES, Conf), + maps:merge(extract_scope_alias_as_map(Settings), + extract_scope_aliases_as_list_of_alias_scope_props(Settings)). + +convert_space_separated_string_to_list_of_binaries(String) -> + [ list_to_binary(V) || V <- string:tokens(String, " ")]. + +extract_scope_alias_as_map(Settings) -> + maps:from_list([{ + list_to_binary(Alias), + convert_space_separated_string_to_list_of_binaries(Scope) + } + || {[?AUTH_OAUTH2, ?SCOPE_ALIASES, Alias], Scope} <- Settings ]). + +extract_scope_aliases_as_list_of_alias_scope_props(Settings) -> + KeyFun = fun extract_key_as_binary/1, + ValueFun = fun extract_value/1, + + List0 = [{Index, {list_to_atom(Attr), V}} + || {[?AUTH_OAUTH2, ?SCOPE_ALIASES, Index, Attr], V} <- Settings ], + List1 = maps:to_list(maps:groups_from_list(KeyFun, ValueFun, List0)), + List2 = [extract_scope_alias_mapping(Proplist) || {_, Proplist} <- List1], + maps:from_list([ V || V <- List2, V =/= {}]). + +extract_scope_alias_mapping(Proplist) -> + Alias = + case proplists:get_value(alias, Proplist) of + undefined -> {error, missing_alias_attribute}; + A -> list_to_binary(A) + end, + Scope = + case proplists:get_value(scope, Proplist) of + undefined -> {error, missing_scope_attribute}; + S -> convert_space_separated_string_to_list_of_binaries(S) + end, + case {Alias, Scope} of + {{error, _}, _} -> + cuttlefish:warn( + "Skipped scope_aliases due to missing alias attribute"), + {}; + {_, {error, _}} -> + cuttlefish:warn( + "Skipped scope_aliases due to missing scope attribute"), + {}; + _ = V -> V + end. + +extract_resource_server_scope_aliases_as_list_of_props(Settings) -> + KeyFun = fun extract_key_as_binary/1, + ValueFun = fun extract_value/1, + + List0 = [ + { + Name, + {Index, {list_to_atom(Attr), V}} + } || + {[ + ?AUTH_OAUTH2, ?RESOURCE_SERVERS, Name, ?SCOPE_ALIASES, + Index, Attr + ], V + } <- Settings ], + Map0 = maps:groups_from_list(KeyFun, ValueFun, List0), + + Map4 = maps:map(fun (_, L) -> + Map2 = maps:map(fun (_, L2) -> extract_scope_alias_mapping(L2) end, + maps:groups_from_list(KeyFun, ValueFun, L)), + Map3 = maps:filter(fun (_,V) -> V =/= {} end, Map2), + [{scope_aliases, maps:from_list([ V || {_, V} <- maps:to_list(Map3)])}] + end, Map0), + + Map4. + +extract_resource_server_scope_aliases_as_map(Settings) -> + KeyFun = fun extract_key_as_binary/1, + ValueFun = fun extract_value/1, + + List0 = [ + { + Name, + { + list_to_binary(Alias), + convert_space_separated_string_to_list_of_binaries(Scope) + } + } || + {[ + ?AUTH_OAUTH2, ?RESOURCE_SERVERS, Name, ?SCOPE_ALIASES, + Alias + ], Scope + } <- Settings ], + Map0 = maps:groups_from_list(KeyFun, ValueFun, List0), + maps:map(fun (_, L) -> [{scope_aliases, maps:from_list(L)}] end, Map0). + -spec translate_resource_servers([{list(), binary()}]) -> map(). translate_resource_servers(Conf) -> - Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.resource_servers", - Conf), + Settings = cuttlefish_variable:filter_by_prefix( + ?AUTH_OAUTH2_RESOURCE_SERVERS, Conf), Map = merge_list_of_maps([ extract_resource_server_properties(Settings), - extract_resource_server_preferred_username_claims(Settings) + extract_resource_server_preferred_username_claims(Settings), + extract_resource_server_scope_aliases_as_list_of_props(Settings), + extract_resource_server_scope_aliases_as_map(Settings) ]), Map0 = maps:map(fun(K,V) -> case proplists:get_value(id, V) of @@ -32,14 +139,13 @@ translate_resource_servers(Conf) -> _ -> V end end, Map), ResourceServers = maps:values(Map0), - lists:foldl(fun(Elem,AccMap) -> - maps:put(proplists:get_value(id, Elem), Elem, AccMap) end, #{}, - ResourceServers). + lists:foldl(fun(Elem,AccMap)-> maps:put(proplists:get_value(id, Elem), + Elem, AccMap) end, #{}, ResourceServers). -spec translate_oauth_providers([{list(), binary()}]) -> map(). translate_oauth_providers(Conf) -> - Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.oauth_providers", - Conf), + Settings = cuttlefish_variable:filter_by_prefix( + ?AUTH_OAUTH2_OAUTH_PROVIDERS, Conf), merge_list_of_maps([ extract_oauth_providers_properties(Settings), @@ -52,8 +158,8 @@ translate_oauth_providers(Conf) -> -spec translate_signing_keys([{list(), binary()}]) -> map(). translate_signing_keys(Conf) -> - Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.signing_keys", - Conf), + Settings = cuttlefish_variable:filter_by_prefix( + ?AUTH_OAUTH2_SIGNING_KEYS, Conf), ListOfKidPath = lists:map(fun({Id, Path}) -> { list_to_binary(lists:last(Id)), Path} end, Settings), translate_list_of_signing_keys(ListOfKidPath). @@ -66,9 +172,9 @@ translate_list_of_signing_keys(ListOfKidPath) -> {ok, Bin} -> string:trim(Bin, trailing, "\n"); _Error -> - %% this throws and makes Cuttlefish treak the key as invalid - cuttlefish:invalid("file does not exist or cannot be " ++ - "read by the node") + cuttlefish:invalid(io_lib:format( + "File ~p does not exist or cannot be read by the node", + [Path])) end end, maps:map(fun(_K, Path) -> {pem, TryReadingFileFun(Path)} end, @@ -87,7 +193,6 @@ validator_file_exists(Attr, Filename) -> {ok, _} -> Filename; _Error -> - %% this throws and makes Cuttlefish treak the key as invalid cuttlefish:invalid(io_lib:format( "Invalid attribute (~p) value: file ~p does not exist or " ++ "cannot be read by the node", [Attr, Filename])) @@ -111,21 +216,23 @@ validator_https_uri(Attr, Uri) when is_list(Uri) -> true -> Uri; false -> cuttlefish:invalid(io_lib:format( - "Invalid attribute (~p) value: uri ~p must be a valid https uri", - [Attr, Uri])) + "Invalid attribute (~p) value: uri ~p must be a valid " ++ + "https uri", [Attr, Uri])) end. merge_list_of_maps(ListOfMaps) -> - lists:foldl(fun(Elem, AccIn) -> maps:merge_with(fun(_K,V1,V2) -> V1 ++ V2 end, - Elem, AccIn) end, #{}, ListOfMaps). + lists:foldl(fun(Elem, AccIn) -> maps:merge_with( + fun(_K,V1,V2) -> V1 ++ V2 end, Elem, AccIn) end, #{}, ListOfMaps). extract_oauth_providers_properties(Settings) -> KeyFun = fun extract_key_as_binary/1, ValueFun = fun extract_value/1, - OAuthProviders = [ - {Name, mapOauthProviderProperty({list_to_atom(Key), list_to_binary(V)})} - || {["auth_oauth2", "oauth_providers", Name, Key], V} <- Settings], + OAuthProviders = [{Name, mapOauthProviderProperty( + { + list_to_atom(Key), + list_to_binary(V)}) + } || {[?AUTH_OAUTH2, ?OAUTH_PROVIDERS, Name, Key], V} <- Settings ], maps:groups_from_list(KeyFun, ValueFun, OAuthProviders). @@ -134,7 +241,7 @@ extract_resource_server_properties(Settings) -> ValueFun = fun extract_value/1, OAuthProviders = [{Name, {list_to_atom(Key), list_to_binary(V)}} - || {["auth_oauth2","resource_servers", Name, Key], V} <- Settings ], + || {[?AUTH_OAUTH2, ?RESOURCE_SERVERS, Name, Key], V} <- Settings ], maps:groups_from_list(KeyFun, ValueFun, OAuthProviders). mapOauthProviderProperty({Key, Value}) -> @@ -156,7 +263,7 @@ extract_oauth_providers_https(Settings) -> ExtractProviderNameFun = fun extract_key_as_binary/1, AttributesPerProvider = [{Name, mapHttpProperty({list_to_atom(Key), V})} || - {["auth_oauth2","oauth_providers", Name, "https", Key], V} <- Settings ], + {[?AUTH_OAUTH2, ?OAUTH_PROVIDERS, Name, "https", Key], V} <- Settings ], maps:map(fun(_K,V)-> [{https, V}] end, maps:groups_from_list(ExtractProviderNameFun, fun({_, V}) -> V end, @@ -172,7 +279,7 @@ extract_oauth_providers_algorithm(Settings) -> KeyFun = fun extract_key_as_binary/1, IndexedAlgorithms = [{Name, {Index, list_to_binary(V)}} || - {["auth_oauth2","oauth_providers", Name, "algorithms", Index], V} + {[?AUTH_OAUTH2, ?OAUTH_PROVIDERS, Name, "algorithms", Index], V} <- Settings ], SortedAlgorithms = lists:sort(fun({_,{AI,_}},{_,{BI,_}}) -> AI < BI end, IndexedAlgorithms), @@ -184,7 +291,7 @@ extract_resource_server_preferred_username_claims(Settings) -> KeyFun = fun extract_key_as_binary/1, IndexedClaims = [{Name, {Index, list_to_binary(V)}} || - {["auth_oauth2","resource_servers", Name, "preferred_username_claims", + {[?AUTH_OAUTH2, ?RESOURCE_SERVERS, Name, "preferred_username_claims", Index], V} <- Settings ], SortedClaims = lists:sort(fun({_,{AI,_}},{_,{BI,_}}) -> AI < BI end, IndexedClaims), @@ -205,7 +312,7 @@ extract_oauth_providers_signing_keys(Settings) -> KeyFun = fun extract_key_as_binary/1, IndexedSigningKeys = [{Name, {list_to_binary(Kid), list_to_binary(V)}} || - {["auth_oauth2","oauth_providers", Name, "signing_keys", Kid], V} + {[?AUTH_OAUTH2, ?OAUTH_PROVIDERS, Name, ?SIGNING_KEYS, Kid], V} <- Settings ], maps:map(fun(_K,V)-> [{signing_keys, translate_list_of_signing_keys(V)}] end, maps:groups_from_list(KeyFun, fun({_, V}) -> V end, IndexedSigningKeys)). 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 4638312ecb52..27064f9700f2 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 @@ -196,5 +196,121 @@ {scope_prefix,<<>>} ]} ],[] + }, + {scope_aliases_1, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.scope_aliases.admin = rabbitmq.tag:administrator + auth_oauth2.scope_aliases.developer = rabbitmq.tag:management rabbitmq.read:*/*", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id,<<"new_resource_server_id">>}, + {scope_aliases, #{ + <<"admin">> => [ + <<"rabbitmq.tag:administrator">> + ], + <<"developer">> => [ + <<"rabbitmq.tag:management">>, + <<"rabbitmq.read:*/*">> + ] + }} + ]} + ], [] + }, + {scope_aliases_2, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.scope_aliases.1.alias = admin + auth_oauth2.scope_aliases.1.scope = rabbitmq.tag:administrator + auth_oauth2.scope_aliases.2.alias = developer + auth_oauth2.scope_aliases.2.scope = rabbitmq.tag:management rabbitmq.read:*/*", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id,<<"new_resource_server_id">>}, + {scope_aliases, #{ + <<"admin">> => [ + <<"rabbitmq.tag:administrator">> + ], + <<"developer">> => [ + <<"rabbitmq.tag:management">>, + <<"rabbitmq.read:*/*">> + ] + }} + ]} + ], [] + }, + {scope_aliases_3, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.resource_servers.a.scope_aliases.admin = rabbitmq.tag:administrator + auth_oauth2.resource_servers.a.scope_aliases.developer = rabbitmq.tag:management rabbitmq.read:*/* + auth_oauth2.resource_servers.b.scope_aliases.admin_b = rabbitmq.tag:administrator + auth_oauth2.resource_servers.b.scope_aliases.developer_b = rabbitmq.tag:management rabbitmq.read:*/*", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id,<<"new_resource_server_id">>}, + {resource_servers, #{ + <<"a">> => [ + {scope_aliases, #{ + <<"admin">> => [ + <<"rabbitmq.tag:administrator">> + ], + <<"developer">> => [ + <<"rabbitmq.tag:management">>, + <<"rabbitmq.read:*/*">> + ] + }}, + {id, <<"a">>} + ], + <<"b">> => [ + {scope_aliases, #{ + <<"admin_b">> => [ + <<"rabbitmq.tag:administrator">> + ], + <<"developer_b">> => [ + <<"rabbitmq.tag:management">>, + <<"rabbitmq.read:*/*">> + ] + }}, + {id, <<"b">>} + ] + } + } + ]} + ], [] + }, + {scope_aliases_4, + "auth_oauth2.resource_server_id = new_resource_server_id + auth_oauth2.resource_servers.b.scope_aliases.1.alias = admin_b + auth_oauth2.resource_servers.b.scope_aliases.1.scope = rabbitmq.tag:administrator + auth_oauth2.resource_servers.a.scope_aliases.1.alias = admin + auth_oauth2.resource_servers.a.scope_aliases.1.scope = rabbitmq.tag:administrator + auth_oauth2.resource_servers.a.scope_aliases.2.alias = developer + auth_oauth2.resource_servers.a.scope_aliases.2.scope = rabbitmq.tag:management rabbitmq.read:*/*", + [ + {rabbitmq_auth_backend_oauth2, [ + {resource_server_id,<<"new_resource_server_id">>}, + {resource_servers, #{ + <<"a">> => [ + {scope_aliases, #{ + <<"admin">> => [ + <<"rabbitmq.tag:administrator">> + ], + <<"developer">> => [ + <<"rabbitmq.tag:management">>, + <<"rabbitmq.read:*/*">> + ] + }}, + {id, <<"a">>} + ], + <<"b">> => [ + {scope_aliases, #{ + <<"admin_b">> => [ + <<"rabbitmq.tag:administrator">> + ] + }}, + {id, <<"b">>} + ] + } + } + ]} + ], [] } ]. diff --git a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl index ccf1b3a0f6ac..34c28e730284 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_oauth2_schema_SUITE.erl @@ -15,7 +15,8 @@ -import(rabbit_oauth2_schema, [ translate_endpoint_params/2, translate_oauth_providers/1, - translate_resource_servers/1 + translate_resource_servers/1, + translate_scope_aliases/1 ]). all() -> @@ -37,8 +38,10 @@ all() -> test_with_many_resource_servers, test_resource_servers_attributes, test_invalid_oauth_providers_endpoint_params, - test_without_oauth_providers_with_endpoint_params - + test_without_oauth_providers_with_endpoint_params, + test_scope_aliases_configured_as_list_of_properties, + test_scope_aliases_configured_as_map, + test_scope_aliases_configured_as_list_of_missing_properties ]. @@ -97,9 +100,11 @@ test_without_oauth_providers_with_endpoint_params(_) -> test_with_one_oauth_provider(_) -> Conf = [ - {["auth_oauth2","oauth_providers","keycloak","issuer"],"https://rabbit"} + {["auth_oauth2","oauth_providers","keycloak","issuer"], + "https://rabbit"} ], - #{<<"keycloak">> := [{issuer, "https://rabbit"}] + #{<<"keycloak">> := [ + {issuer, "https://rabbit"}] } = translate_oauth_providers(Conf). test_with_one_resource_server(_) -> @@ -128,8 +133,10 @@ test_with_many_oauth_providers(_) -> test_with_many_resource_servers(_) -> Conf = [ - {["auth_oauth2","resource_servers","rabbitmq1","id"], "rabbitmq1"}, - {["auth_oauth2","resource_servers","rabbitmq2","id"], "rabbitmq2"} + {["auth_oauth2","resource_servers","rabbitmq1","id"], + "rabbitmq1"}, + {["auth_oauth2","resource_servers","rabbitmq2","id"], + "rabbitmq2"} ], #{<<"rabbitmq1">> := [{id, <<"rabbitmq1">>} ], @@ -277,6 +284,49 @@ test_oauth_providers_signing_keys(Conf) -> <<"2">> := {pem, <<"I'm not a certificate">>} } = SigningKeys. +test_scope_aliases_configured_as_list_of_properties(_) -> + CuttlefishConf = [ + {["auth_oauth2","scope_aliases","1","alias"], + "admin"}, + {["auth_oauth2","scope_aliases","1","scope"], + "rabbitmq.tag:administrator"}, + {["auth_oauth2","scope_aliases","2","alias"], + "developer"}, + {["auth_oauth2","scope_aliases","2","scope"], + "rabbitmq.tag:management rabbitmq.read:*/*"} + ], + #{ + <<"admin">> := [<<"rabbitmq.tag:administrator">>], + <<"developer">> := [<<"rabbitmq.tag:management">>, <<"rabbitmq.read:*/*">>] + } = translate_scope_aliases(CuttlefishConf). + +test_scope_aliases_configured_as_list_of_missing_properties(_) -> + CuttlefishConf = [ + {["auth_oauth2","scope_aliases","1","alias"], + "admin"} + ], + #{} = rabbit_oauth2_schema:translate_scope_aliases(CuttlefishConf), + + CuttlefishConf2 = [ + {["auth_oauth2","scope_aliases","1","scope"], + "rabbitmq.tag:management rabbitmq.read:*/*"} + ], + #{} = rabbit_oauth2_schema:translate_scope_aliases(CuttlefishConf2). + + +test_scope_aliases_configured_as_map(_) -> + CuttlefishConf = [ + {["auth_oauth2","scope_aliases","admin"], + "rabbitmq.tag:administrator"}, + {["auth_oauth2","scope_aliases","developer"], + "rabbitmq.tag:management rabbitmq.read:*/*"} + ], + #{ + <<"admin">> := [<<"rabbitmq.tag:administrator">>], + <<"developer">> := [<<"rabbitmq.tag:management">>, <<"rabbitmq.read:*/*">>] + } = rabbit_oauth2_schema:translate_scope_aliases(CuttlefishConf). + + cert_filename(Conf) -> string:concat(?config(data_dir, Conf), "certs/cert.pem").