diff options
author | dcorbacho <dparracorbacho@piotal.io> | 2020-11-18 14:27:41 +0000 |
---|---|---|
committer | dcorbacho <dparracorbacho@piotal.io> | 2020-11-18 14:27:41 +0000 |
commit | f23a51261d9502ec39df0f8db47ba6b22aa7659f (patch) | |
tree | 53dcdf46e7dc2c14e81ee960bce8793879b488d3 /deps/rabbitmq_auth_backend_oauth2/test | |
parent | afa2c2bf6c7e0e9b63f4fb53dc931c70388e1c82 (diff) | |
parent | 9f6d64ec4a4b1eeac24d7846c5c64fd96798d892 (diff) | |
download | rabbitmq-server-git-stream-timestamp-offset.tar.gz |
Merge remote-tracking branch 'origin/master' into stream-timestamp-offsetstream-timestamp-offset
Diffstat (limited to 'deps/rabbitmq_auth_backend_oauth2/test')
6 files changed, 1544 insertions, 0 deletions
diff --git a/deps/rabbitmq_auth_backend_oauth2/test/add_uaa_key_command_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/add_uaa_key_command_SUITE.erl new file mode 100644 index 0000000000..ba46715db1 --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/add_uaa_key_command_SUITE.erl @@ -0,0 +1,75 @@ +%% 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-2020 VMware, Inc. or its affiliates. All rights reserved. +%% +-module(add_uaa_key_command_SUITE). + +-compile(export_all). + +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(COMMAND, 'Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand'). + +all() -> + [validate_arguments, + validate_json_key, + validate_pem_key, + validate_pem_file_key + ]. + + +init_per_suite(Config) -> + rabbit_ct_helpers:run_setup_steps(Config, []). + +end_per_suite(Config) -> + rabbit_ct_helpers:run_teardown_steps(Config, []). + + +validate_arguments(_) -> + {validation_failure, too_many_args} = + ?COMMAND:validate([<<"one">>, <<"two">>], #{json => <<"{}">>}), + {validation_failure, not_enough_args} = + ?COMMAND:validate([], #{json => <<"{}">>}), + {validation_failure, {bad_argument, <<"No key specified">>}} = + ?COMMAND:validate([<<"foo">>], #{}), + {validation_failure, {bad_argument, <<"There can be only one key type">>}} = + ?COMMAND:validate([<<"foo">>], #{json => <<"{}">>, pem => <<"pem">>}), + {validation_failure, {bad_argument, <<"There can be only one key type">>}} = + ?COMMAND:validate([<<"foo">>], #{json => <<"{}">>, pem_file => <<"/tmp/key.pem">>}), + {validation_failure, {bad_argument, <<"There can be only one key type">>}} = + ?COMMAND:validate([<<"foo">>], #{pem => <<"pem">>, pem_file => <<"/tmp/key.pem">>}). + +validate_json_key(_) -> + {validation_failure, {bad_argument, <<"Invalid JSON">>}} = + ?COMMAND:validate([<<"foo">>], #{json => <<"foobar">>}), + {validation_failure, {bad_argument, <<"Json key should contain \"kty\" field">>}} = + ?COMMAND:validate([<<"foo">>], #{json => <<"{}">>}), + {validation_failure, {bad_argument, _}} = + ?COMMAND:validate([<<"foo">>], #{json => <<"{\"kty\": \"oct\"}">>}), + ValidJson = <<"{\"alg\":\"HS256\",\"k\":\"dG9rZW5rZXk\",\"kid\":\"token-key\",\"kty\":\"oct\",\"use\":\"sig\",\"value\":\"tokenkey\"}">>, + ok = ?COMMAND:validate([<<"foo">>], #{json => ValidJson}). + +validate_pem_key(Config) -> + {validation_failure, <<"Unable to read a key from the PEM string">>} = + ?COMMAND:validate([<<"foo">>], #{pem => <<"not a key">>}), + CertsDir = ?config(rmq_certsdir, Config), + Keyfile = filename:join([CertsDir, <<"client">>, <<"key.pem">>]), + {ok, Key} = file:read_file(Keyfile), + ok = ?COMMAND:validate([<<"foo">>], #{pem => Key}). + +validate_pem_file_key(Config) -> + {validation_failure, {bad_argument, <<"PEM file not found">>}} = + ?COMMAND:validate([<<"foo">>], #{pem_file => <<"non_existent_file">>}), + file:write_file("empty.pem", <<"">>), + {validation_failure, <<"Unable to read a key from the PEM file">>} = + ?COMMAND:validate([<<"foo">>], #{pem_file => <<"empty.pem">>}), + file:write_file("not_pem.pem", <<"">>), + {validation_failure, _} = + ?COMMAND:validate([<<"foo">>], #{pem_file => <<"not_pem.pem">>}), + CertsDir = ?config(rmq_certsdir, Config), + Keyfile = filename:join([CertsDir, <<"client">>, <<"key.pem">>]), + ok = ?COMMAND:validate([<<"foo">>], #{pem_file => Keyfile}). + 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 new file mode 100644 index 0000000000..5b8ed5f837 --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/rabbit_auth_backend_oauth2_test_util.erl @@ -0,0 +1,99 @@ +%% 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-2020 VMware, Inc. or its affiliates. All rights reserved. +%% +-module(rabbit_auth_backend_oauth2_test_util). + +-compile(export_all). + +-define(DEFAULT_EXPIRATION_IN_SECONDS, 2). + +%% +%% API +%% + +sign_token_hs(Token, #{<<"kid">> := TokenKey} = Jwk) -> + sign_token_hs(Token, Jwk, TokenKey). + +sign_token_hs(Token, Jwk, TokenKey) -> + Jws = #{ + <<"alg">> => <<"HS256">>, + <<"kid">> => TokenKey + }, + sign_token(Token, Jwk, Jws). + +sign_token_rsa(Token, Jwk, TokenKey) -> + Jws = #{ + <<"alg">> => <<"RS256">>, + <<"kid">> => TokenKey + }, + sign_token(Token, Jwk, Jws). + +sign_token_no_kid(Token, Jwk) -> + Signed = jose_jwt:sign(Jwk, Token), + jose_jws:compact(Signed). + +sign_token(Token, Jwk, Jws) -> + Signed = jose_jwt:sign(Jwk, Jws, Token), + jose_jws:compact(Signed). + +fixture_jwk() -> + #{<<"alg">> => <<"HS256">>, + <<"k">> => <<"dG9rZW5rZXk">>, + <<"kid">> => <<"token-key">>, + <<"kty">> => <<"oct">>, + <<"use">> => <<"sig">>, + <<"value">> => <<"tokenkey">>}. + +full_permission_scopes() -> + [<<"rabbitmq.configure:*/*">>, + <<"rabbitmq.write:*/*">>, + <<"rabbitmq.read:*/*">>]. + +expirable_token() -> + expirable_token(?DEFAULT_EXPIRATION_IN_SECONDS). + +expirable_token(Seconds) -> + TokenPayload = fixture_token(), + %% expiration is a timestamp with precision in seconds + TokenPayload#{<<"exp">> := os:system_time(seconds) + Seconds}. + +wait_for_token_to_expire() -> + timer:sleep(timer:seconds(?DEFAULT_EXPIRATION_IN_SECONDS)). + +wait_for_token_to_expire(DurationInMs) -> + timer:sleep(DurationInMs). + +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). + +fixture_token_with_scopes(Scopes) -> + token_with_scopes_and_expiration(Scopes, os:system_time(seconds) + 10). + +token_with_scopes_and_expiration(Scopes, Expiration) -> + %% expiration is a timestamp with precision in seconds + #{<<"exp">> => Expiration, + <<"kid">> => <<"token-key">>, + <<"iss">> => <<"unit_test">>, + <<"foo">> => <<"bar">>, + <<"aud">> => [<<"rabbitmq">>], + <<"scope">> => Scopes}. + +fixture_token() -> + fixture_token([]). + +fixture_token(ExtraScopes) -> + Scopes = [<<"rabbitmq.configure:vhost/foo">>, + <<"rabbitmq.write:vhost/foo">>, + <<"rabbitmq.read:vhost/foo">>, + <<"rabbitmq.read:vhost/bar">>, + <<"rabbitmq.read:vhost/bar/%23%2Ffoo">>] ++ ExtraScopes, + fixture_token_with_scopes(Scopes). + +fixture_token_with_full_permissions() -> + fixture_token_with_scopes(full_permission_scopes()). diff --git a/deps/rabbitmq_auth_backend_oauth2/test/scope_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/scope_SUITE.erl new file mode 100644 index 0000000000..1338f28f50 --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/scope_SUITE.erl @@ -0,0 +1,341 @@ +%% 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-2020 VMware, Inc. or its affiliates. All rights reserved. +%% +-module(scope_SUITE). + +-compile(export_all). + +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [ + permission_all, + permission_vhost, + permission_resource, + permission_topic + ]. + +permission_all(_Config) -> + WildcardScopeWrite = <<"write:*/*">>, + WildcardScopeWriteTopic = <<"write:*/*/*">>, + WildcardScopeRead = <<"read:*/*">>, + WildcardScopeReadTopic = <<"read:*/*/*">>, + WildcardScopeConfigure = <<"configure:*/*">>, + WildcardScopeConfigureTopic = <<"configure:*/*/*">>, + + ReadScopes = [WildcardScopeRead, WildcardScopeReadTopic], + WriteScopes = [WildcardScopeWrite, WildcardScopeWriteTopic], + ConfigureScopes = [WildcardScopeConfigure, WildcardScopeConfigureTopic], + + ExampleVhosts = [<<"/">>, <<"foo">>, <<"*">>, <<"foo/bar">>, <<"юникод"/utf8>>], + ExampleResources = [<<"foo">>, <<"foo/bar">>, <<"*">>, <<"*/*">>, <<"юникод"/utf8>>], + + + [ vhost_allowed(<<"/">>, Scope) || + Scope <- ReadScopes ++ WriteScopes ++ ConfigureScopes ], + [ vhost_allowed(<<"foo">>, Scope) || + Scope <- ReadScopes ++ WriteScopes ++ ConfigureScopes ], + [ vhost_allowed(<<"*">>, Scope) || + Scope <- ReadScopes ++ WriteScopes ++ ConfigureScopes ], + [ vhost_allowed(<<"foo/bar">>, Scope) || + Scope <- ReadScopes ++ WriteScopes ++ ConfigureScopes ], + [ vhost_allowed(<<"юникод"/utf8>>, Scope) || + Scope <- ReadScopes ++ WriteScopes ++ ConfigureScopes ], + + [ read_allowed(Vhost, Resource, Scope) || + Scope <- ReadScopes, + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ write_allowed(Vhost, Resource, Scope) || + Scope <- WriteScopes, + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ configure_allowed(Vhost, Resource, Scope) || + Scope <- ConfigureScopes, + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ read_refused(Vhost, Resource, Scope) || + Scope <- WriteScopes ++ ConfigureScopes, + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ write_refused(Vhost, Resource, Scope) || + Scope <- ReadScopes ++ ConfigureScopes, + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ configure_refused(Vhost, Resource, Scope) || + Scope <- WriteScopes ++ ReadScopes, + Vhost <- ExampleVhosts, + Resource <- ExampleResources ]. + +permission_vhost(_Config) -> + FooScopeWrite = <<"write:foo/*">>, + FooScopeWriteTopic = <<"write:foo/*/*">>, + FooScopeRead = <<"read:foo/*">>, + FooScopeReadTopic = <<"read:foo/*/*">>, + FooScopeConfigure = <<"configure:foo/*">>, + FooScopeConfigureTopic = <<"configure:foo/*/*">>, + + ComplexVHost = <<"foo/bar/*/">>, + EncodedVhost = cow_qs:urlencode(ComplexVHost), + + EncodedScopeWrite = <<"write:", EncodedVhost/binary, "/*">>, + EncodedScopeWriteTopic = <<"write:", EncodedVhost/binary, "/*/*">>, + EncodedScopeRead = <<"read:", EncodedVhost/binary, "/*">>, + EncodedScopeReadTopic = <<"read:", EncodedVhost/binary, "/*/*">>, + EncodedScopeConfigure = <<"configure:", EncodedVhost/binary, "/*">>, + EncodedScopeConfigureTopic = <<"configure:", EncodedVhost/binary, "/*/*">>, + + FooReadScopes = [FooScopeRead, FooScopeReadTopic], + EncodedReadScopes = [EncodedScopeRead, EncodedScopeReadTopic], + + FooWriteScopes = [FooScopeWrite, FooScopeWriteTopic], + EncodedWriteScopes = [EncodedScopeWrite, EncodedScopeWriteTopic], + + FooConfigureScopes = [FooScopeConfigure, FooScopeConfigureTopic], + EncodedConfigureScopes = [EncodedScopeConfigure, EncodedScopeConfigureTopic], + + ExampleResources = [<<"foo">>, <<"foo/bar">>, <<"*">>, <<"*/*">>, <<"юникод"/utf8>>], + + Tags = [<<"tag:management">>, <<"tag:policymaker">>], + + [ vhost_allowed(<<"foo">>, Scope) || + Scope <- FooReadScopes ++ FooWriteScopes ++ FooConfigureScopes ], + [ vhost_allowed(ComplexVHost, [Scope] ++ Tags) || + Scope <- EncodedReadScopes ++ EncodedWriteScopes ++ EncodedConfigureScopes ], + + [ read_allowed(<<"foo">>, Resource, [Scope] ++ Tags) || + Scope <- FooReadScopes, + Resource <- ExampleResources ], + + [ write_allowed(<<"foo">>, Resource, [Scope] ++ Tags) || + Scope <- FooWriteScopes, + Resource <- ExampleResources ], + + [ configure_allowed(<<"foo">>, Resource, [Scope] ++ Tags) || + Scope <- FooConfigureScopes, + Resource <- ExampleResources ], + + [ read_refused(<<"foo">>, Resource, [Scope] ++ Tags) || + Scope <- FooWriteScopes ++ FooConfigureScopes ++ + EncodedWriteScopes ++ EncodedConfigureScopes, + Resource <- ExampleResources ], + + [ write_refused(<<"foo">>, Resource, [Scope] ++ Tags) || + Scope <- FooReadScopes ++ FooConfigureScopes ++ + EncodedReadScopes ++ EncodedConfigureScopes, + Resource <- ExampleResources ], + + [ configure_refused(<<"foo">>, Resource, [Scope] ++ Tags) || + Scope <- FooWriteScopes ++ FooReadScopes ++ + EncodedWriteScopes ++ EncodedReadScopes, + Resource <- ExampleResources ], + + [ read_allowed(ComplexVHost, Resource, Scope) || + Scope <- EncodedReadScopes, + Resource <- ExampleResources ], + + [ write_allowed(ComplexVHost, Resource, Scope) || + Scope <- EncodedWriteScopes, + Resource <- ExampleResources ], + + [ configure_allowed(ComplexVHost, Resource, Scope) || + Scope <- EncodedConfigureScopes, + Resource <- ExampleResources ], + + [ read_refused(ComplexVHost, Resource, Scope) || + Scope <- EncodedWriteScopes ++ EncodedConfigureScopes ++ + FooWriteScopes ++ FooConfigureScopes, + Resource <- ExampleResources ], + + [ write_refused(ComplexVHost, Resource, Scope) || + Scope <- EncodedReadScopes ++ EncodedConfigureScopes ++ + FooReadScopes ++ FooConfigureScopes, + Resource <- ExampleResources ], + + [ configure_refused(ComplexVHost, Resource, Scope) || + Scope <- EncodedWriteScopes ++ EncodedReadScopes ++ + FooWriteScopes ++ FooReadScopes, + Resource <- ExampleResources ]. + +permission_resource(_Config) -> + ComplexResource = <<"bar*/baz">>, + EncodedResource = cow_qs:urlencode(ComplexResource), + + ScopeWrite = <<"write:*/", EncodedResource/binary>>, + ScopeWriteTopic = <<"write:*/", EncodedResource/binary, "/*">>, + ScopeRead = <<"read:*/", EncodedResource/binary>>, + ScopeReadTopic = <<"read:*/", EncodedResource/binary, "/*">>, + ScopeConfigure = <<"configure:*/", EncodedResource/binary>>, + ScopeConfigureTopic = <<"configure:*/", EncodedResource/binary, "/*">>, + + ExampleVhosts = [<<"/">>, <<"foo">>, <<"*">>, <<"foo/bar">>, <<"юникод"/utf8>>], + ExampleResources = [<<"foo">>, <<"foo/bar">>, <<"*">>, <<"*/*">>, <<"юникод"/utf8>>], + + %% Resource access is allowed for complex resource with any vhost + [ read_allowed(Vhost, ComplexResource, Scope) || + Scope <- [ScopeRead, ScopeReadTopic], + Vhost <- ExampleVhosts ], + [ write_allowed(Vhost, ComplexResource, Scope) || + Scope <- [ScopeWrite, ScopeWriteTopic], + Vhost <- ExampleVhosts ], + [ configure_allowed(Vhost, ComplexResource, Scope) || + Scope <- [ScopeConfigure, ScopeConfigureTopic], + Vhost <- ExampleVhosts ], + + %% Resource access is refused for any other resource + [ read_refused(Vhost, Resource, Scope) || + Scope <- [ScopeWrite, ScopeWriteTopic, + ScopeRead, ScopeReadTopic, + ScopeConfigure, ScopeConfigureTopic], + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ write_refused(Vhost, Resource, Scope) || + Scope <- [ScopeWrite, ScopeWriteTopic, + ScopeRead, ScopeReadTopic, + ScopeConfigure, ScopeConfigureTopic], + Vhost <- ExampleVhosts, + Resource <- ExampleResources ], + + [ configure_refused(Vhost, Resource, Scope) || + Scope <- [ScopeWrite, ScopeWriteTopic, + ScopeRead, ScopeReadTopic, + ScopeConfigure, ScopeConfigureTopic], + Vhost <- ExampleVhosts, + Resource <- ExampleResources ]. + +permission_topic(_Config) -> + TopicWildcardRead = <<"read:*/*/*">>, + TopicVhostRead = <<"read:vhost/*/*">>, + TopicResourceRead = <<"read:*/exchange/*">>, + TopicRoutingKeyRead = <<"read:*/*/rout">>, + TopicRoutingSuffixKeyRead = <<"read:*/*/*rout">>, + + ExampleVhosts = [<<"/">>, <<"foo">>, <<"*">>, <<"foo/bar">>, <<"юникод"/utf8>>], + ExampleResources = [<<"foo">>, <<"foo/bar">>, <<"*">>, <<"*/*">>, <<"юникод"/utf8>>], + ExampleRoutingKeys = [<<"rout">>, <<"norout">>, <<"some_other">>], + + [ topic_read_allowed(VHost, Resource, RoutingKey, TopicWildcardRead) || + VHost <- ExampleVhosts, + Resource <- ExampleResources, + RoutingKey <- ExampleRoutingKeys], + + [ topic_read_allowed(<<"vhost">>, Resource, RoutingKey, TopicVhostRead) || + Resource <- ExampleResources, + RoutingKey <- ExampleRoutingKeys], + + [ topic_read_allowed(VHost, <<"exchange">>, RoutingKey, TopicResourceRead) || + VHost <- ExampleVhosts, + RoutingKey <- ExampleRoutingKeys], + + [ topic_read_allowed(VHost, Resource, <<"rout">>, TopicRoutingKeyRead) || + VHost <- ExampleVhosts, + Resource <- ExampleResources], + + [ topic_read_allowed(VHost, Resource, RoutingKey, TopicRoutingSuffixKeyRead) || + VHost <- ExampleVhosts, + Resource <- ExampleResources, + RoutingKey <- [<<"rout">>, <<"norout">>, <<"sprout">>]], + + [ topic_read_refused(VHost, Resource, RoutingKey, TopicVhostRead) || + VHost <- ExampleVhosts, + Resource <- ExampleResources, + RoutingKey <- ExampleRoutingKeys], + + [ topic_read_refused(VHost, Resource, RoutingKey, TopicResourceRead) || + VHost <- ExampleVhosts, + Resource <- ExampleResources, + RoutingKey <- ExampleRoutingKeys], + + [ topic_read_refused(VHost, Resource, RoutingKey, TopicRoutingKeyRead) || + VHost <- ExampleVhosts, + Resource <- ExampleResources, + RoutingKey <- [<<"foo">>, <<"bar">>]]. + +vhost_allowed(Vhost, Scopes) when is_list(Scopes) -> + ?assertEqual(true, rabbit_oauth2_scope:vhost_access(Vhost, Scopes)); + +vhost_allowed(Vhost, Scope) -> + vhost_allowed(Vhost, [Scope]). + +read_allowed(Vhost, Resource, Scopes) when is_list(Scopes) -> + resource_perm(Vhost, Resource, Scopes, read, true); + +read_allowed(Vhost, Resource, Scope) -> + resource_perm(Vhost, Resource, Scope, read, true). + +read_refused(Vhost, Resource, Scopes) when is_list(Scopes) -> + resource_perm(Vhost, Resource, Scopes, read, false); + +read_refused(Vhost, Resource, Scope) -> + resource_perm(Vhost, Resource, Scope, read, false). + +write_allowed(Vhost, Resource, Scopes) when is_list(Scopes) -> + resource_perm(Vhost, Resource, Scopes, write, true); + +write_allowed(Vhost, Resource, Scope) -> + resource_perm(Vhost, Resource, Scope, write, true). + +write_refused(Vhost, Resource, Scopes) when is_list(Scopes) -> + resource_perm(Vhost, Resource, Scopes, write, false); + +write_refused(Vhost, Resource, Scope) -> + resource_perm(Vhost, Resource, Scope, write, false). + +configure_allowed(Vhost, Resource, Scopes) when is_list(Scopes) -> + resource_perm(Vhost, Resource, Scopes, configure, true); + +configure_allowed(Vhost, Resource, Scope) -> + resource_perm(Vhost, Resource, Scope, configure, true). + +configure_refused(Vhost, Resource, Scopes) when is_list(Scopes) -> + resource_perm(Vhost, Resource, Scopes, configure, false); + +configure_refused(Vhost, Resource, Scope) -> + resource_perm(Vhost, Resource, Scope, configure, false). + + +resource_perm(Vhost, Resource, Scopes, Permission, Result) when is_list(Scopes) -> + [ ?assertEqual(Result, rabbit_oauth2_scope:resource_access( + #resource{virtual_host = Vhost, + kind = Kind, + name = Resource}, + Permission, + Scopes)) || Kind <- [queue, exchange] ]; + +resource_perm(Vhost, Resource, Scope, Permission, Result) -> + resource_perm(Vhost, Resource, [Scope], Permission, Result). + +topic_read_allowed(Vhost, Resource, RoutingKey, Scopes) when is_list(Scopes) -> + topic_perm(Vhost, Resource, RoutingKey, Scopes, read, true); + +topic_read_allowed(Vhost, Resource, RoutingKey, Scope) -> + topic_perm(Vhost, Resource, RoutingKey, Scope, read, true). + +topic_read_refused(Vhost, Resource, RoutingKey, Scopes) when is_list(Scopes) -> + topic_perm(Vhost, Resource, RoutingKey, Scopes, read, false); + +topic_read_refused(Vhost, Resource, RoutingKey, Scope) -> + topic_perm(Vhost, Resource, RoutingKey, Scope, read, false). + +topic_perm(Vhost, Resource, RoutingKey, Scopes, Permission, Result) when is_list(Scopes) -> + ?assertEqual(Result, rabbit_oauth2_scope:topic_access( + #resource{virtual_host = Vhost, + kind = topic, + name = Resource}, + Permission, + #{routing_key => RoutingKey}, + Scopes)); + +topic_perm(Vhost, Resource, RoutingKey, Scope, Permission, Result) -> + topic_perm(Vhost, Resource, RoutingKey, [Scope], Permission, Result). diff --git a/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl new file mode 100644 index 0000000000..bb98b469a3 --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/system_SUITE.erl @@ -0,0 +1,373 @@ +%% 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-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(system_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-import(rabbit_ct_client_helpers, [close_connection/1, close_channel/1, + open_unmanaged_connection/4, open_unmanaged_connection/5, + close_connection_and_channel/2]). +-import(rabbit_mgmt_test_util, [amqp_port/1]). + +all() -> + [ + {group, happy_path}, + {group, unhappy_path} + ]. + +groups() -> + [ + {happy_path, [], [ + test_successful_connection_with_a_full_permission_token_and_all_defaults, + test_successful_connection_with_a_full_permission_token_and_explicitly_configured_vhost, + test_successful_connection_with_simple_strings_for_aud_and_scope, + test_successful_connection_with_complex_claim_as_a_map, + test_successful_connection_with_complex_claim_as_a_list, + test_successful_connection_with_complex_claim_as_a_binary, + test_successful_connection_with_keycloak_token, + test_successful_token_refresh + ]}, + {unhappy_path, [], [ + test_failed_connection_with_expired_token, + test_failed_connection_with_a_non_token, + test_failed_connection_with_a_token_with_insufficient_vhost_permission, + test_failed_connection_with_a_token_with_insufficient_resource_permission, + test_failed_token_refresh_case1, + test_failed_token_refresh_case2 + ]} + ]. + +%% +%% Setup and Teardown +%% + +-define(UTIL_MOD, rabbit_auth_backend_oauth2_test_util). +-define(RESOURCE_SERVER_ID, <<"rabbitmq">>). +-define(EXTRA_SCOPES_SOURCE, <<"additional_rabbitmq_scopes">>). + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + rabbit_ct_helpers:run_setup_steps(Config, + rabbit_ct_broker_helpers:setup_steps() ++ [ + fun preconfigure_node/1, + fun preconfigure_token/1 + ]). + +end_per_suite(Config) -> + rabbit_ct_helpers:run_teardown_steps(Config, rabbit_ct_broker_helpers:teardown_steps()). + + +init_per_group(_Group, Config) -> + %% The broker is managed by {init,end}_per_testcase(). + lists:foreach(fun(Value) -> + rabbit_ct_broker_helpers:add_vhost(Config, Value) + end, + [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]), + Config. + +end_per_group(_Group, Config) -> + %% The broker is managed by {init,end}_per_testcase(). + lists:foreach(fun(Value) -> + rabbit_ct_broker_helpers:delete_vhost(Config, Value) + end, + [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]), + Config. + + +init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_a_full_permission_token_and_explicitly_configured_vhost orelse + Testcase =:= test_successful_token_refresh -> + rabbit_ct_broker_helpers:add_vhost(Config, <<"vhost1">>), + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config; + +init_per_testcase(Testcase, Config) when Testcase =:= test_failed_token_refresh_case1 orelse + Testcase =:= test_failed_token_refresh_case2 -> + rabbit_ct_broker_helpers:add_vhost(Config, <<"vhost4">>), + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config; + +init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_complex_claim_as_a_map orelse + Testcase =:= test_successful_connection_with_complex_claim_as_a_list orelse + Testcase =:= test_successful_connection_with_complex_claim_as_a_binary -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, extra_scopes_source, ?EXTRA_SCOPES_SOURCE]), + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config; + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config. + +end_per_testcase(Testcase, Config) when Testcase =:= test_failed_token_refresh_case1 orelse + Testcase =:= test_failed_token_refresh_case2 -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost4">>), + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config; + +end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_complex_claim_as_a_map orelse + Testcase =:= test_successful_connection_with_complex_claim_as_a_list orelse + Testcase =:= test_successful_connection_with_complex_claim_as_a_binary -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, extra_scopes_source, undefined]), + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config; + +end_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), + rabbit_ct_helpers:testcase_finished(Config, Testcase), + Config. + +preconfigure_node(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbit, auth_backends, [rabbit_auth_backend_oauth2]]), + Jwk = ?UTIL_MOD:fixture_jwk(), + KeyConfig = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}], + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, key_config, KeyConfig]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_auth_backend_oauth2, resource_server_id, ?RESOURCE_SERVER_ID]), + + rabbit_ct_helpers:set_config(Config, {fixture_jwk, Jwk}). + +generate_valid_token(Config) -> + generate_valid_token(Config, ?UTIL_MOD:full_permission_scopes()). + +generate_valid_token(Config, Scopes) -> + generate_valid_token(Config, Scopes, undefined). + +generate_valid_token(Config, Scopes, Audience) -> + Jwk = case rabbit_ct_helpers:get_config(Config, fixture_jwk) of + undefined -> ?UTIL_MOD:fixture_jwk(); + Value -> Value + end, + Token = case Audience of + undefined -> ?UTIL_MOD:fixture_token_with_scopes(Scopes); + DefinedAudience -> maps:put(<<"aud">>, DefinedAudience, ?UTIL_MOD:fixture_token_with_scopes(Scopes)) + end, + ?UTIL_MOD:sign_token_hs(Token, Jwk). + +generate_valid_token_with_extra_fields(Config, ExtraFields) -> + Jwk = case rabbit_ct_helpers:get_config(Config, fixture_jwk) of + undefined -> ?UTIL_MOD:fixture_jwk(); + Value -> Value + end, + Token = maps:merge(?UTIL_MOD:fixture_token_with_scopes([]), ExtraFields), + ?UTIL_MOD:sign_token_hs(Token, Jwk). + +generate_expired_token(Config) -> + generate_expired_token(Config, ?UTIL_MOD:full_permission_scopes()). + +generate_expired_token(Config, Scopes) -> + Jwk = case rabbit_ct_helpers:get_config(Config, fixture_jwk) of + undefined -> ?UTIL_MOD:fixture_jwk(); + Value -> Value + end, + ?UTIL_MOD:sign_token_hs(?UTIL_MOD:expired_token_with_scopes(Scopes), Jwk). + +generate_expirable_token(Config, Seconds) -> + generate_expirable_token(Config, ?UTIL_MOD:full_permission_scopes(), Seconds). + +generate_expirable_token(Config, Scopes, Seconds) -> + Jwk = case rabbit_ct_helpers:get_config(Config, fixture_jwk) of + undefined -> ?UTIL_MOD:fixture_jwk(); + Value -> Value + end, + Expiration = os:system_time(seconds) + Seconds, + ?UTIL_MOD:sign_token_hs(?UTIL_MOD:token_with_scopes_and_expiration(Scopes, Expiration), Jwk). + +preconfigure_token(Config) -> + Token = generate_valid_token(Config), + rabbit_ct_helpers:set_config(Config, {fixture_jwt, Token}). + +%% +%% Test Cases +%% + +test_successful_connection_with_a_full_permission_token_and_all_defaults(Config) -> + {_Algo, Token} = rabbit_ct_helpers:get_config(Config, fixture_jwt), + Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_connection_with_a_full_permission_token_and_explicitly_configured_vhost(Config) -> + {_Algo, Token} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost1/*">>, + <<"rabbitmq.write:vhost1/*">>, + <<"rabbitmq.read:vhost1/*">>]), + Conn = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_connection_with_simple_strings_for_aud_and_scope(Config) -> + {_Algo, Token} = generate_valid_token( + Config, + <<"rabbitmq.configure:*/* rabbitmq.write:*/* rabbitmq.read:*/*">>, + <<"hare rabbitmq">> + ), + Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_connection_with_complex_claim_as_a_map(Config) -> + {_Algo, Token} = generate_valid_token_with_extra_fields( + Config, + #{<<"additional_rabbitmq_scopes">> => #{<<"rabbitmq">> => [<<"configure:*/*">>, <<"read:*/*">>, <<"write:*/*">>]}} + ), + Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_connection_with_complex_claim_as_a_list(Config) -> + {_Algo, Token} = generate_valid_token_with_extra_fields( + Config, + #{<<"additional_rabbitmq_scopes">> => [<<"rabbitmq.configure:*/*">>, <<"rabbitmq.read:*/*">>, <<"rabbitmq.write:*/*">>]} + ), + Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_connection_with_complex_claim_as_a_binary(Config) -> + {_Algo, Token} = generate_valid_token_with_extra_fields( + Config, + #{<<"additional_rabbitmq_scopes">> => <<"rabbitmq.configure:*/* rabbitmq.read:*/*" "rabbitmq.write:*/*">>} + ), + Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_connection_with_keycloak_token(Config) -> + {_Algo, Token} = generate_valid_token_with_extra_fields( + Config, + #{<<"authorization">> => #{<<"permissions">> => + [#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>, + <<"rsname">> => <<"allvhost">>, + <<"scopes">> => [<<"rabbitmq.configure:*/*">>]}, + #{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>, + <<"rsname">> => <<"vhost1">>, + <<"scopes">> => [<<"rabbitmq.write:*/*">>]}, + #{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>, + <<"rsname">> => <<"Default Resource">>, + <<"scopes">> => [<<"rabbitmq.read:*/*">>]}, + %% this one won't be used because of the resource id + #{<<"rsid">> => <<"bee8fac6-c3ec-11e9-aa8c-2a2ae2dbcce4">>, + <<"rsname">> => <<"Default Resource">>, + <<"scopes">> => [<<"rabbitmq-resource-read">>]}]}} + ), + Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + close_connection_and_channel(Conn, Ch). + +test_successful_token_refresh(Config) -> + Duration = 5, + {_Algo, Token} = generate_expirable_token(Config, [<<"rabbitmq.configure:vhost1/*">>, + <<"rabbitmq.write:vhost1/*">>, + <<"rabbitmq.read:vhost1/*">>], + Duration), + Conn = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + + {_Algo, Token2} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost1/*">>, + <<"rabbitmq.write:vhost1/*">>, + <<"rabbitmq.read:vhost1/*">>]), + ?UTIL_MOD:wait_for_token_to_expire(timer:seconds(Duration)), + ?assertEqual(ok, amqp_connection:update_secret(Conn, Token2, <<"token refresh">>)), + + {ok, Ch2} = amqp_connection:open_channel(Conn), + + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch2, #'queue.declare'{exclusive = true}), + + amqp_channel:close(Ch2), + close_connection_and_channel(Conn, Ch). + + +test_failed_connection_with_expired_token(Config) -> + {_Algo, Token} = generate_expired_token(Config, [<<"rabbitmq.configure:vhost1/*">>, + <<"rabbitmq.write:vhost1/*">>, + <<"rabbitmq.read:vhost1/*">>]), + ?assertMatch({error, {auth_failure, _}}, + open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, Token)). + +test_failed_connection_with_a_non_token(Config) -> + ?assertMatch({error, {auth_failure, _}}, + open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, <<"a-non-token-value">>)). + +test_failed_connection_with_a_token_with_insufficient_vhost_permission(Config) -> + {_Algo, Token} = generate_valid_token(Config, [<<"rabbitmq.configure:alt-vhost/*">>, + <<"rabbitmq.write:alt-vhost/*">>, + <<"rabbitmq.read:alt-vhost/*">>]), + ?assertEqual({error, not_allowed}, + open_unmanaged_connection(Config, 0, <<"off-limits-vhost">>, <<"username">>, Token)). + +test_failed_connection_with_a_token_with_insufficient_resource_permission(Config) -> + {_Algo, Token} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost2/jwt*">>, + <<"rabbitmq.write:vhost2/jwt*">>, + <<"rabbitmq.read:vhost2/jwt*">>]), + Conn = open_unmanaged_connection(Config, 0, <<"vhost2">>, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + ?assertExit({{shutdown, {server_initiated_close, 403, _}}, _}, + amqp_channel:call(Ch, #'queue.declare'{queue = <<"alt-prefix.eq.1">>, exclusive = true})), + close_connection(Conn). + +test_failed_token_refresh_case1(Config) -> + {_Algo, Token} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost4/*">>, + <<"rabbitmq.write:vhost4/*">>, + <<"rabbitmq.read:vhost4/*">>]), + Conn = open_unmanaged_connection(Config, 0, <<"vhost4">>, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + + {_Algo, Token2} = generate_expired_token(Config, [<<"rabbitmq.configure:vhost4/*">>, + <<"rabbitmq.write:vhost4/*">>, + <<"rabbitmq.read:vhost4/*">>]), + %% the error is communicated asynchronously via a connection-level error + ?assertEqual(ok, amqp_connection:update_secret(Conn, Token2, <<"token refresh">>)), + + {ok, Ch2} = amqp_connection:open_channel(Conn), + ?assertExit({{shutdown, {server_initiated_close, 403, _}}, _}, + amqp_channel:call(Ch2, #'queue.declare'{queue = <<"a.q">>, exclusive = true})), + + close_connection(Conn). + +test_failed_token_refresh_case2(Config) -> + {_Algo, Token} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost4/*">>, + <<"rabbitmq.write:vhost4/*">>, + <<"rabbitmq.read:vhost4/*">>]), + Conn = open_unmanaged_connection(Config, 0, <<"vhost4">>, <<"username">>, Token), + {ok, Ch} = amqp_connection:open_channel(Conn), + #'queue.declare_ok'{queue = _} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + + %% the error is communicated asynchronously via a connection-level error + ?assertEqual(ok, amqp_connection:update_secret(Conn, <<"not-a-token-^^^^5%">>, <<"token refresh">>)), + + ?assertExit({{shutdown, {connection_closing, {server_initiated_close, 530, _}}}, _}, + amqp_connection:open_channel(Conn)), + + close_connection(Conn). diff --git a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl new file mode 100644 index 0000000000..f1ed34fabf --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl @@ -0,0 +1,551 @@ +%% 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-2020 VMware, Inc. or its affiliates. All rights reserved. +%% +-module(unit_SUITE). + +-compile(export_all). + +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [ + test_own_scope, + test_validate_payload_resource_server_id_mismatch, + test_validate_payload, + test_successful_access_with_a_token, + test_successful_access_with_a_token_that_has_tag_scopes, + test_unsuccessful_access_with_a_bogus_token, + test_restricted_vhost_access_with_a_valid_token, + test_insufficient_permissions_in_a_valid_token, + test_command_json, + test_command_pem, + test_command_pem_no_kid, + test_token_expiration, + test_incorrect_kid, + test_post_process_token_payload, + test_post_process_token_payload_keycloak, + test_post_process_token_payload_complex_claims + ]. + +init_per_suite(Config) -> + application:load(rabbitmq_auth_backend_oauth2), + Env = application:get_all_env(rabbitmq_auth_backend_oauth2), + Config1 = rabbit_ct_helpers:set_config(Config, {env, Env}), + rabbit_ct_helpers:run_setup_steps(Config1, []). + +end_per_suite(Config) -> + Env = ?config(env, Config), + lists:foreach( + fun({K, V}) -> + application:set_env(rabbitmq_auth_backend_oauth2, K, V) + end, + Env), + rabbit_ct_helpers:run_teardown_steps(Config). + +init_per_testcase(test_post_process_token_payload_complex_claims, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, extra_scopes_source, <<"additional_rabbitmq_scopes">>), + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + Config; + +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(test_post_process_token_payload_complex_claims, Config) -> + application:set_env(rabbitmq_auth_backend_oauth2, extra_scopes_source, undefined), + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, undefined), + Config; +end_per_testcase(_, Config) -> + Config. + +%% +%% Test Cases +%% + +-define(UTIL_MOD, rabbit_auth_backend_oauth2_test_util). +-define(RESOURCE_SERVER_ID, <<"rabbitmq">>). + +test_post_process_token_payload(_) -> + ArgumentsExpections = [ + {{[<<"rabbitmq">>, <<"hare">>], [<<"read">>, <<"write">>, <<"configure">>]}, + {[<<"rabbitmq">>, <<"hare">>], [<<"read">>, <<"write">>, <<"configure">>]}}, + {{<<"rabbitmq hare">>, <<"read write configure">>}, + {[<<"rabbitmq">>, <<"hare">>], [<<"read">>, <<"write">>, <<"configure">>]}}, + {{<<"rabbitmq">>, <<"read">>}, + {[<<"rabbitmq">>], [<<"read">>]}} + ], + lists:foreach( + fun({{Aud, Scope}, {ExpectedAud, ExpectedScope}}) -> + Payload = post_process_token_payload(Aud, Scope), + ?assertEqual(ExpectedAud, maps:get(<<"aud">>, Payload)), + ?assertEqual(ExpectedScope, maps:get(<<"scope">>, Payload)) + end, ArgumentsExpections). + +post_process_token_payload(Audience, Scopes) -> + Jwk = ?UTIL_MOD:fixture_jwk(), + Token = maps:put(<<"aud">>, Audience, ?UTIL_MOD:fixture_token_with_scopes(Scopes)), + {_, EncodedToken} = ?UTIL_MOD:sign_token_hs(Token, Jwk), + {true, Payload} = uaa_jwt_jwt:decode_and_verify(Jwk, EncodedToken), + rabbit_auth_backend_oauth2:post_process_payload(Payload). + +test_post_process_token_payload_keycloak(_) -> + Pairs = [ + %% common case + { + #{<<"permissions">> => + [#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>, + <<"rsname">> => <<"allvhost">>, + <<"scopes">> => [<<"rabbitmq-resource.read:*/*">>]}, + #{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>, + <<"rsname">> => <<"vhost1">>, + <<"scopes">> => [<<"rabbitmq-resource-read">>]}, + #{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>, + <<"rsname">> => <<"Default Resource">>}]}, + [<<"rabbitmq-resource.read:*/*">>, <<"rabbitmq-resource-read">>] + }, + + %% one scopes field with a string instead of an array + { + #{<<"permissions">> => + [#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>, + <<"rsname">> => <<"allvhost">>, + <<"scopes">> => <<"rabbitmq-resource.read:*/*">>}, + #{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>, + <<"rsname">> => <<"vhost1">>, + <<"scopes">> => [<<"rabbitmq-resource-read">>]}, + #{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>, + <<"rsname">> => <<"Default Resource">>}]}, + [<<"rabbitmq-resource.read:*/*">>, <<"rabbitmq-resource-read">>] + }, + + %% no scopes field in permissions + { + #{<<"permissions">> => + [#{<<"rsid">> => <<"2c390fe4-02ad-41c7-98a2-cebb8c60ccf1">>, + <<"rsname">> => <<"allvhost">>}, + #{<<"rsid">> => <<"e7f12e94-4c34-43d8-b2b1-c516af644cee">>, + <<"rsname">> => <<"vhost1">>}, + #{<<"rsid">> => <<"12ac3d1c-28c2-4521-8e33-0952eff10bd9">>, + <<"rsname">> => <<"Default Resource">>}]}, + [] + }, + + %% no permissions + { + #{<<"permissions">> => []}, + [] + }, + %% missing permissions key + {#{}, []} + ], + lists:foreach( + fun({Authorization, ExpectedScope}) -> + Payload = post_process_payload_with_keycloak_authorization(Authorization), + ?assertEqual(ExpectedScope, maps:get(<<"scope">>, Payload)) + end, Pairs). + +post_process_payload_with_keycloak_authorization(Authorization) -> + Jwk = ?UTIL_MOD:fixture_jwk(), + Token = maps:put(<<"authorization">>, Authorization, ?UTIL_MOD:fixture_token_with_scopes([])), + {_, EncodedToken} = ?UTIL_MOD:sign_token_hs(Token, Jwk), + {true, Payload} = uaa_jwt_jwt:decode_and_verify(Jwk, EncodedToken), + rabbit_auth_backend_oauth2:post_process_payload(Payload). + + + +test_post_process_token_payload_complex_claims(_) -> + Pairs = [ + %% claims in form of binary + { + <<"rabbitmq.rabbitmq-resource.read:*/* rabbitmq.rabbitmq-resource-read">>, + [<<"rabbitmq.rabbitmq-resource.read:*/*">>, <<"rabbitmq.rabbitmq-resource-read">>] + }, + %% claims in form of binary - empty result + {<<>>, []}, + %% claims in form of list + { + [<<"rabbitmq.rabbitmq-resource.read:*/*">>, + <<"rabbitmq2.rabbitmq-resource-read">>], + [<<"rabbitmq.rabbitmq-resource.read:*/*">>, <<"rabbitmq2.rabbitmq-resource-read">>] + }, + %% claims in form of list - empty result + {[], []}, + %% claims are map with list content + { + #{<<"rabbitmq">> => + [<<"rabbitmq-resource.read:*/*">>, + <<"rabbitmq-resource-read">>], + <<"rabbitmq3">> => + [<<"rabbitmq-resource.write:*/*">>, + <<"rabbitmq-resource-write">>]}, + [<<"rabbitmq.rabbitmq-resource.read:*/*">>, <<"rabbitmq.rabbitmq-resource-read">>] + }, + %% claims are map with list content - empty result + { + #{<<"rabbitmq2">> => + [<<"rabbitmq-resource.read:*/*">>, + <<"rabbitmq-resource-read">>]}, + [] + }, + %% claims are map with binary content + { + #{<<"rabbitmq">> => <<"rabbitmq-resource.read:*/* rabbitmq-resource-read">>, + <<"rabbitmq3">> => <<"rabbitmq-resource.write:*/* rabbitmq-resource-write">>}, + [<<"rabbitmq.rabbitmq-resource.read:*/*">>, <<"rabbitmq.rabbitmq-resource-read">>] + }, + %% claims are map with binary content - empty result + { + #{<<"rabbitmq2">> => <<"rabbitmq-resource.read:*/* rabbitmq-resource-read">>}, [] + }, + %% claims are map with empty binary content - empty result + { + #{<<"rabbitmq">> => <<>>}, [] + }, + %% claims are map with empty list content - empty result + { + #{<<"rabbitmq">> => []}, [] + }, + %% no extra claims provided + {[], []}, + %% no extra claims provided + {#{}, []} + ], + lists:foreach( + fun({Authorization, ExpectedScope}) -> + Payload = post_process_payload_with_complex_claim_authorization(Authorization), + ?assertEqual(ExpectedScope, maps:get(<<"scope">>, Payload)) + end, Pairs). + +post_process_payload_with_complex_claim_authorization(Authorization) -> + Jwk = ?UTIL_MOD:fixture_jwk(), + Token = maps:put(<<"additional_rabbitmq_scopes">>, Authorization, ?UTIL_MOD:fixture_token_with_scopes([])), + {_, EncodedToken} = ?UTIL_MOD:sign_token_hs(Token, Jwk), + {true, Payload} = uaa_jwt_jwt:decode_and_verify(Jwk, EncodedToken), + rabbit_auth_backend_oauth2:post_process_payload(Payload). + +test_successful_access_with_a_token(_) -> + %% Generate a token with JOSE + %% Check authorization with the token + %% Check user access granted by token + 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">>), + Username = <<"username">>, + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:fixture_token(), Jwk), + + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token}), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"vhost">>, none)), + ?assertEqual(true, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = queue, + name = <<"foo">>}, + configure, + #{})), + ?assertEqual(true, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = exchange, + name = <<"foo">>}, + write, + #{})), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = custom, + name = <<"bar">>}, + read, + #{})), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_topic_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = topic, + name = <<"bar">>}, + read, + #{routing_key => <<"#/foo">>})). + +test_successful_access_with_a_token_that_has_tag_scopes(_) -> + 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">>), + Username = <<"username">>, + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:fixture_token([<<"rabbitmq.tag:management">>, + <<"rabbitmq.tag:policymaker">>]), Jwk), + + {ok, #auth_user{username = Username, tags = [management, policymaker]}} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]). + +test_unsuccessful_access_with_a_bogus_token(_) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + + Jwk0 = ?UTIL_MOD:fixture_jwk(), + Jwk = Jwk0#{<<"k">> => <<"bm90b2tlbmtleQ">>}, + UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}], + application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv), + + ?assertMatch({refused, _, _}, + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, <<"not a token">>}])). + +test_restricted_vhost_access_with_a_valid_token(_) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + + Jwk = ?UTIL_MOD:fixture_jwk(), + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:fixture_token(), Jwk), + UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}], + application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv), + + %% this user can authenticate successfully and access certain vhosts + {ok, #auth_user{username = Username, tags = []} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]), + + %% access to a different vhost + ?assertEqual(false, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"different vhost">>, none)). + +test_insufficient_permissions_in_a_valid_token(_) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + + Jwk = ?UTIL_MOD:fixture_jwk(), + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:fixture_token(), Jwk), + UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}], + application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv), + + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]), + + %% access to these resources is not granted + ?assertEqual(false, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = queue, + name = <<"foo1">>}, + configure, + #{})), + ?assertEqual(false, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = custom, + name = <<"bar">>}, + write, + #{})), + ?assertEqual(false, rabbit_auth_backend_oauth2:check_topic_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = topic, + name = <<"bar">>}, + read, + #{routing_key => <<"foo/#">>})). + +test_token_expiration(_) -> + Username = <<"username">>, + 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">>), + TokenData = ?UTIL_MOD:expirable_token(), + Username = <<"username">>, + Token = ?UTIL_MOD:sign_token_hs(TokenData, Jwk), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}]), + ?assertEqual(true, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = queue, + name = <<"foo">>}, + configure, + #{})), + ?assertEqual(true, rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = exchange, + name = <<"foo">>}, + write, + #{})), + + ?UTIL_MOD:wait_for_token_to_expire(), + #{<<"exp">> := Exp} = TokenData, + ExpectedError = "Provided JWT token has expired at timestamp " ++ integer_to_list(Exp) ++ " (validated at " ++ integer_to_list(Exp) ++ ")", + ?assertEqual({error, ExpectedError}, + rabbit_auth_backend_oauth2:check_resource_access( + User, + #resource{virtual_host = <<"vhost">>, + kind = queue, + name = <<"foo">>}, + configure, + #{})), + + ?assertMatch({refused, _, _}, + rabbit_auth_backend_oauth2:user_login_authentication(Username, [{password, Token}])). + +test_incorrect_kid(_) -> + AltKid = <<"other-token-key">>, + Username = <<"username">>, + Jwk = ?UTIL_MOD:fixture_jwk(), + Jwk1 = Jwk#{<<"kid">> := AltKid}, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:fixture_token(), Jwk1), + + ?assertMatch({refused, "Authentication using an OAuth 2/JWT token failed: ~p", [{error,key_not_found}]}, + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token})). + +test_command_json(_) -> + Username = <<"username">>, + Jwk = ?UTIL_MOD:fixture_jwk(), + Json = rabbit_json:encode(Jwk), + 'Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand':run( + [<<"token-key">>], + #{node => node(), json => Json}), + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:fixture_token(), Jwk), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token}), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"vhost">>, none)). + +test_command_pem_file(Config) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + CertsDir = ?config(rmq_certsdir, Config), + Keyfile = filename:join([CertsDir, "client", "key.pem"]), + Jwk = jose_jwk:from_pem_file(Keyfile), + + PublicJwk = jose_jwk:to_public(Jwk), + PublicKeyFile = filename:join([CertsDir, "client", "public.pem"]), + jose_jwk:to_pem_file(PublicKeyFile, PublicJwk), + + 'Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand':run( + [<<"token-key">>], + #{node => node(), pem_file => PublicKeyFile}), + + Token = ?UTIL_MOD:sign_token_rsa(?UTIL_MOD:fixture_token(), Jwk, <<"token-key">>), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token}), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"vhost">>, none)). + + +test_command_pem_file_no_kid(Config) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + CertsDir = ?config(rmq_certsdir, Config), + Keyfile = filename:join([CertsDir, "client", "key.pem"]), + Jwk = jose_jwk:from_pem_file(Keyfile), + + PublicJwk = jose_jwk:to_public(Jwk), + PublicKeyFile = filename:join([CertsDir, "client", "public.pem"]), + jose_jwk:to_pem_file(PublicKeyFile, PublicJwk), + + 'Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand':run( + [<<"token-key">>], + #{node => node(), pem_file => PublicKeyFile}), + + %% Set default key + {ok, UaaEnv0} = application:get_env(rabbitmq_auth_backend_oauth2, key_config), + UaaEnv1 = proplists:delete(default_key, UaaEnv0), + UaaEnv2 = [{default_key, <<"token-key">>} | UaaEnv1], + application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv2), + + Token = ?UTIL_MOD:sign_token_no_kid(?UTIL_MOD:fixture_token(), Jwk), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token}), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"vhost">>, none)). + +test_command_pem(Config) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + CertsDir = ?config(rmq_certsdir, Config), + Keyfile = filename:join([CertsDir, "client", "key.pem"]), + Jwk = jose_jwk:from_pem_file(Keyfile), + + Pem = jose_jwk:to_pem(jose_jwk:to_public(Jwk)), + + 'Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand':run( + [<<"token-key">>], + #{node => node(), pem => Pem}), + + Token = ?UTIL_MOD:sign_token_rsa(?UTIL_MOD:fixture_token(), Jwk, <<"token-key">>), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token}), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"vhost">>, none)). + + +test_command_pem_no_kid(Config) -> + Username = <<"username">>, + application:set_env(rabbitmq_auth_backend_oauth2, resource_server_id, <<"rabbitmq">>), + CertsDir = ?config(rmq_certsdir, Config), + Keyfile = filename:join([CertsDir, "client", "key.pem"]), + Jwk = jose_jwk:from_pem_file(Keyfile), + + Pem = jose_jwk:to_pem(jose_jwk:to_public(Jwk)), + + 'Elixir.RabbitMQ.CLI.Ctl.Commands.AddUaaKeyCommand':run( + [<<"token-key">>], + #{node => node(), pem => Pem}), + + %% This is the default key + {ok, UaaEnv0} = application:get_env(rabbitmq_auth_backend_oauth2, key_config), + UaaEnv1 = proplists:delete(default_key, UaaEnv0), + UaaEnv2 = [{default_key, <<"token-key">>} | UaaEnv1], + application:set_env(rabbitmq_auth_backend_oauth2, key_config, UaaEnv2), + + Token = ?UTIL_MOD:sign_token_no_kid(?UTIL_MOD:fixture_token(), Jwk), + {ok, #auth_user{username = Username} = User} = + rabbit_auth_backend_oauth2:user_login_authentication(Username, #{password => Token}), + + ?assertEqual(true, rabbit_auth_backend_oauth2:check_vhost_access(User, <<"vhost">>, none)). + + +test_own_scope(_) -> + Examples = [ + {<<"foo">>, [<<"foo">>, <<"foo.bar">>, <<"bar.foo">>, + <<"one.two">>, <<"foobar">>, <<"foo.other.third">>], + [<<"bar">>, <<"other.third">>]}, + {<<"foo">>, [], []}, + {<<"foo">>, [<<"foo">>, <<"other.foo.bar">>], []}, + {<<"">>, [<<"foo">>, <<"bar">>], [<<"foo">>, <<"bar">>]} + ], + lists:map( + fun({ResId, Src, Dest}) -> + Dest = rabbit_auth_backend_oauth2:filter_scopes(Src, ResId) + end, + Examples). + +test_validate_payload_resource_server_id_mismatch(_) -> + NoKnownResourceServerId = #{<<"aud">> => [<<"foo">>, <<"bar">>], + <<"scope">> => [<<"foo">>, <<"foo.bar">>, + <<"bar.foo">>, <<"one.two">>, + <<"foobar">>, <<"foo.other.third">>]}, + EmptyAud = #{<<"aud">> => [], + <<"scope">> => [<<"foo.bar">>, <<"bar.foo">>]}, + + ?assertEqual({refused, {invalid_aud, {resource_id_not_found_in_aud, ?RESOURCE_SERVER_ID, + [<<"foo">>,<<"bar">>]}}}, + rabbit_auth_backend_oauth2:validate_payload(NoKnownResourceServerId, ?RESOURCE_SERVER_ID)), + + ?assertEqual({refused, {invalid_aud, {resource_id_not_found_in_aud, ?RESOURCE_SERVER_ID, []}}}, + rabbit_auth_backend_oauth2:validate_payload(EmptyAud, ?RESOURCE_SERVER_ID)). + +test_validate_payload(_) -> + KnownResourceServerId = #{<<"aud">> => [?RESOURCE_SERVER_ID], + <<"scope">> => [<<"foo">>, <<"rabbitmq.bar">>, + <<"bar.foo">>, <<"one.two">>, + <<"foobar">>, <<"rabbitmq.other.third">>]}, + ?assertEqual({ok, #{<<"aud">> => [?RESOURCE_SERVER_ID], + <<"scope">> => [<<"bar">>, <<"other.third">>]}}, + rabbit_auth_backend_oauth2:validate_payload(KnownResourceServerId, ?RESOURCE_SERVER_ID)). diff --git a/deps/rabbitmq_auth_backend_oauth2/test/wildcard_match_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/wildcard_match_SUITE.erl new file mode 100644 index 0000000000..6996a95f80 --- /dev/null +++ b/deps/rabbitmq_auth_backend_oauth2/test/wildcard_match_SUITE.erl @@ -0,0 +1,105 @@ +%% 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-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(wildcard_match_SUITE). + +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [ + exact_match, + prefix_match, + suffix_match, + mixed_match + ]. + +exact_match(_Config) -> + ?assertEqual(true, wildcard:match(<<"value">>, <<"value">>)), + ?assertEqual(true, wildcard:match(<<"string with / special % characters">>, + <<"string%20with%20%2F%20special%20%25%20characters">>)), + ?assertEqual(true, wildcard:match(<<"pattern with plus spaces">>, + <<"pattern+with++plus++spaces">>)), + ?assertEqual(true, wildcard:match(<<"pattern with plus spaces and * wildcard encoded">>, + <<"pattern+with+plus+spaces+and+%2A+wildcard+encoded">>)), + ?assertEqual(true, wildcard:match(<<"case with * special / characters">>, + <<"case+with+%2a+special+%2f+characters">>)), + + ?assertEqual(false, wildcard:match(<<"casesensitive">>, <<"CaseSensitive">>)), + + ?assertEqual(false, wildcard:match(<<"foo">>, <<"fo">>)), + ?assertEqual(false, wildcard:match(<<"foo">>, <<"fooo">>)), + + %% Special characters and spaces should be %-encoded. + ?assertEqual(false, wildcard:match(<<"string with unescaped % character">>, + <<"string with unescaped % character">>)), + + %% Here a wildcard is matched by another wildcard, so to match exactly the '*' character + %% we need to %-encode it + ?assertEqual(true, wildcard:match(<<"wildcard * is matched by wildcard">>, + <<"wildcard * is matched by wildcard">>)), + + ?assertEqual(true, wildcard:match(<<"wildcard * and anything else is matched by wildcard">>, + <<"wildcard * is matched by wildcard">>)), + + ?assertEqual(true, wildcard:match(<<"wildcard * is matched by urlencoded">>, + <<"wildcard+%2a+is+matched+by+urlencoded">>)), + ?assertEqual(false, wildcard:match(<<"wildcard * and extra content is not matched by urlencoded">>, + <<"wildcard+%2a+is+not+matched+by+urlencoded">>)), + + %% Spaces do not interfere with parsing + ?assertEqual(true, wildcard:match(<<"pattern with spaces">>, + <<"pattern with spaces">>)). + +suffix_match(_Config) -> + ?assertEqual(true, wildcard:match(<<"foo">>, <<"*oo">>)), + %% Empty prefix + ?assertEqual(true, wildcard:match(<<"foo">>, <<"*foo">>)), + %% Anything goes + ?assertEqual(true, wildcard:match(<<"foo">>, <<"*">>)), + ?assertEqual(true, wildcard:match(<<"line * with * special * characters">>, <<"*+characters">>)), + ?assertEqual(true, wildcard:match(<<"line * with * special * characters">>, <<"*special+%2A+characters">>)), + + ?assertEqual(false, wildcard:match(<<"foo">>, <<"*r">>)), + ?assertEqual(false, wildcard:match(<<"foo">>, <<"*foobar">>)). + +prefix_match(_Config) -> + ?assertEqual(true, wildcard:match(<<"foo">>, <<"fo*">>)), + %% Empty suffix + ?assertEqual(true, wildcard:match(<<"foo">>, <<"foo*">>)), + ?assertEqual(true, wildcard:match(<<"line * with * special * characters">>, <<"line+*">>)), + ?assertEqual(true, wildcard:match(<<"line * with * special * characters">>, <<"line+%2a*">>)), + + %% Wildcard matches '*' character + ?assertEqual(true, wildcard:match(<<"string with unescaped *">>, + <<"string+with+unescaped+*">>)), + + ?assertEqual(false, wildcard:match(<<"foo">>, <<"b*">>)), + ?assertEqual(false, wildcard:match(<<"foo">>, <<"barfoo*">>)). + +mixed_match(_Config) -> + %% Empty wildcards + ?assertEqual(true, wildcard:match(<<"string">>, <<"*str*ing*">>)), + ?assertEqual(true, wildcard:match(<<"str*ing">>, <<"*str*ing*">>)), + + ?assertEqual(true, wildcard:match(<<"some long string">>, <<"some*string">>)), + ?assertEqual(true, wildcard:match(<<"some long string">>, <<"some*long*string">>)), + ?assertEqual(true, wildcard:match(<<"some long string">>, <<"*some*string*">>)), + ?assertEqual(true, wildcard:match(<<"some long string">>, <<"*long*">>)), + %% Matches two spaces (3 or more words) + ?assertEqual(true, wildcard:match(<<"some long string">>, <<"*+*+*">>)), + + ?assertEqual(false, wildcard:match(<<"some string">>, <<"*+*+*">>)), + ?assertEqual(false, wildcard:match(<<"some long string">>, <<"some*other*string">>)), + + ?assertEqual(true, wildcard:match(<<"some long string">>, <<"s*e*str*">>)), + %% The z doesn't appear in the subject + ?assertEqual(false, wildcard:match(<<"some long string">>, <<"s*z*str*">>)), + + ?assertEqual(false, wildcard:match(<<"string">>, <<"*some*">>)), + ?assertEqual(false, wildcard:match(<<"string">>, <<"*some*string*">>)). |