Merge pull request #7939 from savonarola/authz-generalize
feat(authz): add default authn-based authz source
This commit is contained in:
commit
4d661cd67b
|
@ -210,4 +210,15 @@ Authentication will verify that the value of claims in the JWT (taken from the P
|
||||||
zh: """SSL 配置。"""
|
zh: """SSL 配置。"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acl_claim_name {
|
||||||
|
desc {
|
||||||
|
en: """JWT claim name to use for getting ACL rules."""
|
||||||
|
zh: """JWT claim name to use for getting ACL rules."""
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
en: """acl_claim_name"""
|
||||||
|
zh: """acl_claim_name"""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,11 @@ desc(_) ->
|
||||||
common_fields() ->
|
common_fields() ->
|
||||||
[
|
[
|
||||||
{mechanism, emqx_authn_schema:mechanism('jwt')},
|
{mechanism, emqx_authn_schema:mechanism('jwt')},
|
||||||
|
{acl_claim_name, #{
|
||||||
|
type => binary(),
|
||||||
|
default => <<"acl">>,
|
||||||
|
desc => ?DESC(acl_claim_name)
|
||||||
|
}},
|
||||||
{verify_claims, fun verify_claims/1}
|
{verify_claims, fun verify_claims/1}
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
|
@ -231,17 +236,19 @@ authenticate(
|
||||||
Credential = #{password := JWT},
|
Credential = #{password := JWT},
|
||||||
#{
|
#{
|
||||||
verify_claims := VerifyClaims0,
|
verify_claims := VerifyClaims0,
|
||||||
jwk := JWK
|
jwk := JWK,
|
||||||
|
acl_claim_name := AclClaimName
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
JWKs = [JWK],
|
JWKs = [JWK],
|
||||||
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
||||||
verify(JWT, JWKs, VerifyClaims);
|
verify(JWT, JWKs, VerifyClaims, AclClaimName);
|
||||||
authenticate(
|
authenticate(
|
||||||
Credential = #{password := JWT},
|
Credential = #{password := JWT},
|
||||||
#{
|
#{
|
||||||
verify_claims := VerifyClaims0,
|
verify_claims := VerifyClaims0,
|
||||||
jwk_resource := ResourceId
|
jwk_resource := ResourceId,
|
||||||
|
acl_claim_name := AclClaimName
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
case emqx_resource:query(ResourceId, get_jwks) of
|
case emqx_resource:query(ResourceId, get_jwks) of
|
||||||
|
@ -254,7 +261,7 @@ authenticate(
|
||||||
ignore;
|
ignore;
|
||||||
{ok, JWKs} ->
|
{ok, JWKs} ->
|
||||||
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
||||||
verify(JWT, JWKs, VerifyClaims)
|
verify(JWT, JWKs, VerifyClaims, AclClaimName)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{jwk_resource := ResourceId}) ->
|
destroy(#{jwk_resource := ResourceId}) ->
|
||||||
|
@ -272,7 +279,8 @@ create2(#{
|
||||||
algorithm := 'hmac-based',
|
algorithm := 'hmac-based',
|
||||||
secret := Secret0,
|
secret := Secret0,
|
||||||
secret_base64_encoded := Base64Encoded,
|
secret_base64_encoded := Base64Encoded,
|
||||||
verify_claims := VerifyClaims
|
verify_claims := VerifyClaims,
|
||||||
|
acl_claim_name := AclClaimName
|
||||||
}) ->
|
}) ->
|
||||||
case may_decode_secret(Base64Encoded, Secret0) of
|
case may_decode_secret(Base64Encoded, Secret0) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -281,24 +289,28 @@ create2(#{
|
||||||
JWK = jose_jwk:from_oct(Secret),
|
JWK = jose_jwk:from_oct(Secret),
|
||||||
{ok, #{
|
{ok, #{
|
||||||
jwk => JWK,
|
jwk => JWK,
|
||||||
verify_claims => VerifyClaims
|
verify_claims => VerifyClaims,
|
||||||
|
acl_claim_name => AclClaimName
|
||||||
}}
|
}}
|
||||||
end;
|
end;
|
||||||
create2(#{
|
create2(#{
|
||||||
use_jwks := false,
|
use_jwks := false,
|
||||||
algorithm := 'public-key',
|
algorithm := 'public-key',
|
||||||
public_key := PublicKey,
|
public_key := PublicKey,
|
||||||
verify_claims := VerifyClaims
|
verify_claims := VerifyClaims,
|
||||||
|
acl_claim_name := AclClaimName
|
||||||
}) ->
|
}) ->
|
||||||
JWK = create_jwk_from_public_key(PublicKey),
|
JWK = create_jwk_from_public_key(PublicKey),
|
||||||
{ok, #{
|
{ok, #{
|
||||||
jwk => JWK,
|
jwk => JWK,
|
||||||
verify_claims => VerifyClaims
|
verify_claims => VerifyClaims,
|
||||||
|
acl_claim_name => AclClaimName
|
||||||
}};
|
}};
|
||||||
create2(
|
create2(
|
||||||
#{
|
#{
|
||||||
use_jwks := true,
|
use_jwks := true,
|
||||||
verify_claims := VerifyClaims
|
verify_claims := VerifyClaims,
|
||||||
|
acl_claim_name := AclClaimName
|
||||||
} = Config
|
} = Config
|
||||||
) ->
|
) ->
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
|
@ -310,7 +322,8 @@ create2(
|
||||||
),
|
),
|
||||||
{ok, #{
|
{ok, #{
|
||||||
jwk_resource => ResourceId,
|
jwk_resource => ResourceId,
|
||||||
verify_claims => VerifyClaims
|
verify_claims => VerifyClaims,
|
||||||
|
acl_claim_name => AclClaimName
|
||||||
}}.
|
}}.
|
||||||
|
|
||||||
create_jwk_from_public_key(PublicKey) when
|
create_jwk_from_public_key(PublicKey) when
|
||||||
|
@ -352,23 +365,39 @@ replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) ->
|
||||||
replace_placeholder([{Name, Value} | More], Variables, Acc) ->
|
replace_placeholder([{Name, Value} | More], Variables, Acc) ->
|
||||||
replace_placeholder(More, Variables, [{Name, Value} | Acc]).
|
replace_placeholder(More, Variables, [{Name, Value} | Acc]).
|
||||||
|
|
||||||
verify(JWT, JWKs, VerifyClaims) ->
|
verify(JWT, JWKs, VerifyClaims, AclClaimName) ->
|
||||||
case do_verify(JWT, JWKs, VerifyClaims) of
|
case do_verify(JWT, JWKs, VerifyClaims) of
|
||||||
{ok, Extra} -> {ok, Extra};
|
{ok, Extra} -> {ok, acl(Extra, AclClaimName)};
|
||||||
{error, {missing_claim, _}} -> {error, bad_username_or_password};
|
{error, {missing_claim, _}} -> {error, bad_username_or_password};
|
||||||
{error, invalid_signature} -> ignore;
|
{error, invalid_signature} -> ignore;
|
||||||
{error, {claims, _}} -> {error, bad_username_or_password}
|
{error, {claims, _}} -> {error, bad_username_or_password}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
acl(Claims, AclClaimName) ->
|
||||||
|
Acl =
|
||||||
|
case Claims of
|
||||||
|
#{<<"exp">> := Expire, AclClaimName := Rules} ->
|
||||||
|
#{
|
||||||
|
acl => #{
|
||||||
|
rules => Rules,
|
||||||
|
expire => Expire
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_ ->
|
||||||
|
#{}
|
||||||
|
end,
|
||||||
|
maps:merge(emqx_authn_utils:is_superuser(Claims), Acl).
|
||||||
|
|
||||||
do_verify(_JWS, [], _VerifyClaims) ->
|
do_verify(_JWS, [], _VerifyClaims) ->
|
||||||
{error, invalid_signature};
|
{error, invalid_signature};
|
||||||
do_verify(JWS, [JWK | More], VerifyClaims) ->
|
do_verify(JWS, [JWK | More], VerifyClaims) ->
|
||||||
try jose_jws:verify(JWK, JWS) of
|
try jose_jws:verify(JWK, JWS) of
|
||||||
{true, Payload, _JWS} ->
|
{true, Payload, _JWS} ->
|
||||||
Claims = emqx_json:decode(Payload, [return_maps]),
|
Claims0 = emqx_json:decode(Payload, [return_maps]),
|
||||||
|
Claims = try_convert_to_int(Claims0, [<<"exp">>, <<"iat">>, <<"nbf">>]),
|
||||||
case verify_claims(Claims, VerifyClaims) of
|
case verify_claims(Claims, VerifyClaims) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, maps:put(jwt, Claims, emqx_authn_utils:is_superuser(Claims))};
|
{ok, Claims};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
|
@ -384,37 +413,39 @@ verify_claims(Claims, VerifyClaims0) ->
|
||||||
Now = os:system_time(seconds),
|
Now = os:system_time(seconds),
|
||||||
VerifyClaims =
|
VerifyClaims =
|
||||||
[
|
[
|
||||||
{<<"exp">>, required,
|
{<<"exp">>, required, fun(ExpireTime) ->
|
||||||
with_int_value(fun(ExpireTime) ->
|
is_integer(ExpireTime) andalso Now < ExpireTime
|
||||||
Now < ExpireTime
|
end},
|
||||||
end)},
|
{<<"iat">>, optional, fun(IssueAt) ->
|
||||||
{<<"iat">>, optional,
|
is_integer(IssueAt) andalso IssueAt =< Now
|
||||||
with_int_value(fun(IssueAt) ->
|
end},
|
||||||
IssueAt =< Now
|
{<<"nbf">>, optional, fun(NotBefore) ->
|
||||||
end)},
|
is_integer(NotBefore) andalso NotBefore =< Now
|
||||||
{<<"nbf">>, optional,
|
end}
|
||||||
with_int_value(fun(NotBefore) ->
|
|
||||||
NotBefore =< Now
|
|
||||||
end)}
|
|
||||||
] ++ VerifyClaims0,
|
] ++ VerifyClaims0,
|
||||||
do_verify_claims(Claims, VerifyClaims).
|
do_verify_claims(Claims, VerifyClaims).
|
||||||
|
|
||||||
with_int_value(Fun) ->
|
try_convert_to_int(Claims, [Name | Names]) ->
|
||||||
fun(Value) ->
|
case Claims of
|
||||||
case Value of
|
#{Name := Value} ->
|
||||||
Int when is_integer(Int) -> Fun(Int);
|
case Value of
|
||||||
Bin when is_binary(Bin) ->
|
Int when is_integer(Int) ->
|
||||||
case string:to_integer(Bin) of
|
try_convert_to_int(Claims#{Name => Int}, Names);
|
||||||
{Int, <<>>} -> Fun(Int);
|
Bin when is_binary(Bin) ->
|
||||||
_ -> false
|
case string:to_integer(Bin) of
|
||||||
end;
|
{Int, <<>>} ->
|
||||||
Str when is_list(Str) ->
|
try_convert_to_int(Claims#{Name => Int}, Names);
|
||||||
case string:to_integer(Str) of
|
_ ->
|
||||||
{Int, ""} -> Fun(Int);
|
try_convert_to_int(Claims, Names)
|
||||||
_ -> false
|
end;
|
||||||
end
|
_ ->
|
||||||
end
|
try_convert_to_int(Claims, Names)
|
||||||
end.
|
end;
|
||||||
|
_ ->
|
||||||
|
try_convert_to_int(Claims, Names)
|
||||||
|
end;
|
||||||
|
try_convert_to_int(Claims, []) ->
|
||||||
|
Claims.
|
||||||
|
|
||||||
do_verify_claims(_Claims, []) ->
|
do_verify_claims(_Claims, []) ->
|
||||||
ok;
|
ok;
|
||||||
|
|
|
@ -56,6 +56,7 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
Secret = <<"abcdef">>,
|
Secret = <<"abcdef">>,
|
||||||
Config = #{
|
Config = #{
|
||||||
mechanism => jwt,
|
mechanism => jwt,
|
||||||
|
acl_claim_name => <<"acl">>,
|
||||||
use_jwks => false,
|
use_jwks => false,
|
||||||
algorithm => 'hmac-based',
|
algorithm => 'hmac-based',
|
||||||
secret => Secret,
|
secret => Secret,
|
||||||
|
@ -179,6 +180,7 @@ t_jwt_authenticator_public_key(_) ->
|
||||||
PrivateKey = test_rsa_key(private),
|
PrivateKey = test_rsa_key(private),
|
||||||
Config = #{
|
Config = #{
|
||||||
mechanism => jwt,
|
mechanism => jwt,
|
||||||
|
acl_claim_name => <<"acl">>,
|
||||||
use_jwks => false,
|
use_jwks => false,
|
||||||
algorithm => 'public-key',
|
algorithm => 'public-key',
|
||||||
public_key => PublicKey,
|
public_key => PublicKey,
|
||||||
|
@ -214,6 +216,7 @@ t_jwks_renewal(_Config) ->
|
||||||
|
|
||||||
BadConfig0 = #{
|
BadConfig0 = #{
|
||||||
mechanism => jwt,
|
mechanism => jwt,
|
||||||
|
acl_claim_name => <<"acl">>,
|
||||||
algorithm => 'public-key',
|
algorithm => 'public-key',
|
||||||
ssl => #{enable => false},
|
ssl => #{enable => false},
|
||||||
verify_claims => [],
|
verify_claims => [],
|
||||||
|
@ -308,6 +311,7 @@ t_jwt_authenticator_verify_claims(_) ->
|
||||||
Secret = <<"abcdef">>,
|
Secret = <<"abcdef">>,
|
||||||
Config0 = #{
|
Config0 = #{
|
||||||
mechanism => jwt,
|
mechanism => jwt,
|
||||||
|
acl_claim_name => <<"acl">>,
|
||||||
use_jwks => false,
|
use_jwks => false,
|
||||||
algorithm => 'hmac-based',
|
algorithm => 'hmac-based',
|
||||||
secret => Secret,
|
secret => Secret,
|
||||||
|
@ -384,6 +388,7 @@ t_jwt_not_allow_empty_claim_name(_) ->
|
||||||
Request = #{
|
Request = #{
|
||||||
<<"use_jwks">> => false,
|
<<"use_jwks">> => false,
|
||||||
<<"algorithm">> => <<"hmac-based">>,
|
<<"algorithm">> => <<"hmac-based">>,
|
||||||
|
<<"acl_claim_name">> => <<"acl">>,
|
||||||
<<"secret">> => <<"secret">>,
|
<<"secret">> => <<"secret">>,
|
||||||
<<"mechanism">> => <<"jwt">>
|
<<"mechanism">> => <<"jwt">>
|
||||||
},
|
},
|
||||||
|
|
|
@ -348,28 +348,6 @@ Filter supports the following placeholders:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt {
|
|
||||||
desc {
|
|
||||||
en: """Authorization using ACL rules from authentication JWT."""
|
|
||||||
zh: """使用 JWT 登录认证中携带的 ACL 规则来进行发布和订阅的授权。"""
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
en: """jwt"""
|
|
||||||
zh: """jwt"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acl_claim_name {
|
|
||||||
desc {
|
|
||||||
en: """JWT claim name to use for getting ACL rules."""
|
|
||||||
zh: """JWT claim name to use for getting ACL rules."""
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
en: """acl_claim_name"""
|
|
||||||
zh: """acl_claim_name"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd {
|
cmd {
|
||||||
desc {
|
desc {
|
||||||
en: """Database query used to retrieve authorization data."""
|
en: """Database query used to retrieve authorization data."""
|
||||||
|
|
|
@ -95,6 +95,7 @@ register_metrics() ->
|
||||||
|
|
||||||
init() ->
|
init() ->
|
||||||
ok = register_metrics(),
|
ok = register_metrics(),
|
||||||
|
ok = init_metrics(client_info_source()),
|
||||||
emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE),
|
emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE),
|
||||||
Sources = emqx_conf:get(?CONF_KEY_PATH, []),
|
Sources = emqx_conf:get(?CONF_KEY_PATH, []),
|
||||||
ok = check_dup_types(Sources),
|
ok = check_dup_types(Sources),
|
||||||
|
@ -307,7 +308,7 @@ authorize(
|
||||||
DefaultResult,
|
DefaultResult,
|
||||||
Sources
|
Sources
|
||||||
) ->
|
) ->
|
||||||
case do_authorize(Client, PubSub, Topic, Sources) of
|
case do_authorize(Client, PubSub, Topic, sources_with_defaults(Sources)) of
|
||||||
{{matched, allow}, AuthzSource} ->
|
{{matched, allow}, AuthzSource} ->
|
||||||
emqx:run_hook(
|
emqx:run_hook(
|
||||||
'client.check_authz_complete',
|
'client.check_authz_complete',
|
||||||
|
@ -392,6 +393,14 @@ get_enabled_authzs() ->
|
||||||
%% Internal function
|
%% Internal function
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
client_info_source() ->
|
||||||
|
emqx_authz_client_info:create(
|
||||||
|
#{type => client_info, enable => true}
|
||||||
|
).
|
||||||
|
|
||||||
|
sources_with_defaults(Sources) ->
|
||||||
|
[client_info_source() | Sources].
|
||||||
|
|
||||||
take(Type) -> take(Type, lookup()).
|
take(Type) -> take(Type, lookup()).
|
||||||
|
|
||||||
%% Take the source of give type, the sources list is split into two parts
|
%% Take the source of give type, the sources list is split into two parts
|
||||||
|
@ -431,8 +440,8 @@ type(postgresql) -> postgresql;
|
||||||
type(<<"postgresql">>) -> postgresql;
|
type(<<"postgresql">>) -> postgresql;
|
||||||
type(built_in_database) -> built_in_database;
|
type(built_in_database) -> built_in_database;
|
||||||
type(<<"built_in_database">>) -> built_in_database;
|
type(<<"built_in_database">>) -> built_in_database;
|
||||||
type(jwt) -> jwt;
|
type(client_info) -> client_info;
|
||||||
type(<<"jwt">>) -> jwt;
|
type(<<"client_info">>) -> client_info;
|
||||||
%% should never happen if the input is type-checked by hocon schema
|
%% should never happen if the input is type-checked by hocon schema
|
||||||
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
|
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_authz_client_info).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([
|
||||||
|
description/0,
|
||||||
|
create/1,
|
||||||
|
update/1,
|
||||||
|
destroy/1,
|
||||||
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(RULE_NAMES, [
|
||||||
|
{[pub, <<"pub">>], publish},
|
||||||
|
{[sub, <<"sub">>], subscribe},
|
||||||
|
{[all, <<"all">>], all}
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% emqx_authz callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
description() ->
|
||||||
|
"AuthZ with ClientInfo".
|
||||||
|
|
||||||
|
create(Source) ->
|
||||||
|
Source.
|
||||||
|
|
||||||
|
update(Source) ->
|
||||||
|
Source.
|
||||||
|
|
||||||
|
destroy(_Source) -> ok.
|
||||||
|
|
||||||
|
authorize(#{acl := Acl} = Client, PubSub, Topic, _Source) ->
|
||||||
|
case check(Acl) of
|
||||||
|
{ok, Rules} when is_map(Rules) ->
|
||||||
|
do_authorize(Client, PubSub, Topic, Rules);
|
||||||
|
{error, MatchResult} ->
|
||||||
|
MatchResult
|
||||||
|
end;
|
||||||
|
authorize(_Client, _PubSub, _Topic, _Source) ->
|
||||||
|
nomatch.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
check(#{expire := Expire, rules := Rules}) when is_map(Rules) ->
|
||||||
|
Now = erlang:system_time(second),
|
||||||
|
case Expire of
|
||||||
|
N when is_integer(N) andalso N > Now -> {ok, Rules};
|
||||||
|
undefined -> {ok, Rules};
|
||||||
|
_ -> {error, {matched, deny}}
|
||||||
|
end;
|
||||||
|
%% no expire
|
||||||
|
check(#{rules := Rules}) ->
|
||||||
|
{ok, Rules};
|
||||||
|
%% no rules — no match
|
||||||
|
check(#{}) ->
|
||||||
|
{error, nomatch}.
|
||||||
|
|
||||||
|
do_authorize(Client, PubSub, Topic, AclRules) ->
|
||||||
|
do_authorize(Client, PubSub, Topic, AclRules, ?RULE_NAMES).
|
||||||
|
|
||||||
|
do_authorize(_Client, _PubSub, _Topic, _AclRules, []) ->
|
||||||
|
{matched, deny};
|
||||||
|
do_authorize(Client, PubSub, Topic, AclRules, [{Keys, Action} | RuleNames]) ->
|
||||||
|
TopicFilters = get_topic_filters(Keys, AclRules, []),
|
||||||
|
case
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
Client,
|
||||||
|
PubSub,
|
||||||
|
Topic,
|
||||||
|
emqx_authz_rule:compile({allow, all, Action, TopicFilters})
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{matched, Permission} -> {matched, Permission};
|
||||||
|
nomatch -> do_authorize(Client, PubSub, Topic, AclRules, RuleNames)
|
||||||
|
end.
|
||||||
|
|
||||||
|
get_topic_filters([], _Rules, Default) ->
|
||||||
|
Default;
|
||||||
|
get_topic_filters([Key | Keys], Rules, Default) ->
|
||||||
|
case Rules of
|
||||||
|
#{Key := Value} -> Value;
|
||||||
|
#{} -> get_topic_filters(Keys, Rules, Default)
|
||||||
|
end.
|
|
@ -1,142 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
||||||
%%
|
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
%% you may not use this file except in compliance with the License.
|
|
||||||
%% You may obtain a copy of the License at
|
|
||||||
%%
|
|
||||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
%%
|
|
||||||
%% Unless required by applicable law or agreed to in writing, software
|
|
||||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
%% See the License for the specific language governing permissions and
|
|
||||||
%% limitations under the License.
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
-module(emqx_authz_jwt).
|
|
||||||
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
|
||||||
|
|
||||||
-behaviour(emqx_authz).
|
|
||||||
|
|
||||||
-ifdef(TEST).
|
|
||||||
-compile(export_all).
|
|
||||||
-compile(nowarn_export_all).
|
|
||||||
-endif.
|
|
||||||
|
|
||||||
%% APIs
|
|
||||||
-export([
|
|
||||||
description/0,
|
|
||||||
create/1,
|
|
||||||
update/1,
|
|
||||||
destroy/1,
|
|
||||||
authorize/4
|
|
||||||
]).
|
|
||||||
|
|
||||||
-define(JWT_RULE_NAMES, [
|
|
||||||
{<<"pub">>, publish},
|
|
||||||
{<<"sub">>, subscribe},
|
|
||||||
{<<"all">>, all}
|
|
||||||
]).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% emqx_authz callbacks
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
description() ->
|
|
||||||
"AuthZ with JWT".
|
|
||||||
|
|
||||||
create(#{acl_claim_name := _AclClaimName} = Source) ->
|
|
||||||
Source.
|
|
||||||
|
|
||||||
update(#{acl_claim_name := _AclClaimName} = Source) ->
|
|
||||||
Source.
|
|
||||||
|
|
||||||
destroy(_Source) -> ok.
|
|
||||||
|
|
||||||
authorize(#{jwt := JWT} = Client, PubSub, Topic, #{acl_claim_name := AclClaimName}) ->
|
|
||||||
case verify(JWT) of
|
|
||||||
{ok, #{AclClaimName := Rules}} when is_map(Rules) ->
|
|
||||||
do_authorize(Client, PubSub, Topic, Rules);
|
|
||||||
_ ->
|
|
||||||
{matched, deny}
|
|
||||||
end;
|
|
||||||
authorize(_Client, _PubSub, _Topic, _Source) ->
|
|
||||||
nomatch.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Internal functions
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
verify(JWT) ->
|
|
||||||
Now = erlang:system_time(second),
|
|
||||||
VerifyClaims =
|
|
||||||
[
|
|
||||||
{<<"exp">>, required,
|
|
||||||
with_int_value(fun(ExpireTime) ->
|
|
||||||
Now < ExpireTime
|
|
||||||
end)},
|
|
||||||
{<<"iat">>, optional,
|
|
||||||
with_int_value(fun(IssueAt) ->
|
|
||||||
IssueAt =< Now
|
|
||||||
end)},
|
|
||||||
{<<"nbf">>, optional,
|
|
||||||
with_int_value(fun(NotBefore) ->
|
|
||||||
NotBefore =< Now
|
|
||||||
end)}
|
|
||||||
],
|
|
||||||
IsValid = lists:all(
|
|
||||||
fun({ClaimName, Required, Validator}) ->
|
|
||||||
verify_claim(ClaimName, Required, JWT, Validator)
|
|
||||||
end,
|
|
||||||
VerifyClaims
|
|
||||||
),
|
|
||||||
case IsValid of
|
|
||||||
true -> {ok, JWT};
|
|
||||||
false -> error
|
|
||||||
end.
|
|
||||||
|
|
||||||
with_int_value(Fun) ->
|
|
||||||
fun(Value) ->
|
|
||||||
case Value of
|
|
||||||
Int when is_integer(Int) -> Fun(Int);
|
|
||||||
Bin when is_binary(Bin) ->
|
|
||||||
case string:to_integer(Bin) of
|
|
||||||
{Int, <<>>} -> Fun(Int);
|
|
||||||
_ -> false
|
|
||||||
end;
|
|
||||||
Str when is_list(Str) ->
|
|
||||||
case string:to_integer(Str) of
|
|
||||||
{Int, ""} -> Fun(Int);
|
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
verify_claim(ClaimName, Required, JWT, Validator) ->
|
|
||||||
case JWT of
|
|
||||||
#{ClaimName := Value} ->
|
|
||||||
Validator(Value);
|
|
||||||
#{} ->
|
|
||||||
Required =:= optional
|
|
||||||
end.
|
|
||||||
|
|
||||||
do_authorize(Client, PubSub, Topic, AclRules) ->
|
|
||||||
do_authorize(Client, PubSub, Topic, AclRules, ?JWT_RULE_NAMES).
|
|
||||||
|
|
||||||
do_authorize(_Client, _PubSub, _Topic, _AclRules, []) ->
|
|
||||||
{matched, deny};
|
|
||||||
do_authorize(Client, PubSub, Topic, AclRules, [{Key, Action} | JWTRuleNames]) ->
|
|
||||||
TopicFilters = maps:get(Key, AclRules, []),
|
|
||||||
case
|
|
||||||
emqx_authz_rule:match(
|
|
||||||
Client,
|
|
||||||
PubSub,
|
|
||||||
Topic,
|
|
||||||
emqx_authz_rule:compile({allow, all, Action, TopicFilters})
|
|
||||||
)
|
|
||||||
of
|
|
||||||
{matched, Permission} -> {matched, Permission};
|
|
||||||
nomatch -> do_authorize(Client, PubSub, Topic, AclRules, JWTRuleNames)
|
|
||||||
end.
|
|
|
@ -70,8 +70,7 @@ fields("authorization") ->
|
||||||
hoconsc:ref(?MODULE, postgresql),
|
hoconsc:ref(?MODULE, postgresql),
|
||||||
hoconsc:ref(?MODULE, redis_single),
|
hoconsc:ref(?MODULE, redis_single),
|
||||||
hoconsc:ref(?MODULE, redis_sentinel),
|
hoconsc:ref(?MODULE, redis_sentinel),
|
||||||
hoconsc:ref(?MODULE, redis_cluster),
|
hoconsc:ref(?MODULE, redis_cluster)
|
||||||
hoconsc:ref(?MODULE, jwt)
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
default => [],
|
default => [],
|
||||||
|
@ -129,15 +128,6 @@ fields(redis_cluster) ->
|
||||||
authz_common_fields(redis) ++
|
authz_common_fields(redis) ++
|
||||||
connector_fields(redis, cluster) ++
|
connector_fields(redis, cluster) ++
|
||||||
[{cmd, cmd()}];
|
[{cmd, cmd()}];
|
||||||
fields(jwt) ->
|
|
||||||
authz_common_fields(jwt) ++
|
|
||||||
[
|
|
||||||
{acl_claim_name, #{
|
|
||||||
type => binary(),
|
|
||||||
default => <<"acl">>,
|
|
||||||
desc => ?DESC(acl_claim_name)
|
|
||||||
}}
|
|
||||||
];
|
|
||||||
fields("metrics_status_fields") ->
|
fields("metrics_status_fields") ->
|
||||||
[
|
[
|
||||||
{"resource_metrics", mk(ref(?MODULE, "resource_metrics"), #{desc => ?DESC("metrics")})},
|
{"resource_metrics", mk(ref(?MODULE, "resource_metrics"), #{desc => ?DESC("metrics")})},
|
||||||
|
@ -236,8 +226,6 @@ desc(redis_sentinel) ->
|
||||||
?DESC(redis_sentinel);
|
?DESC(redis_sentinel);
|
||||||
desc(redis_cluster) ->
|
desc(redis_cluster) ->
|
||||||
?DESC(redis_cluster);
|
?DESC(redis_cluster);
|
||||||
desc(jwt) ->
|
|
||||||
?DESC(jwt);
|
|
||||||
desc(_) ->
|
desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,6 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
),
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:reset_authorizers(),
|
ok = emqx_authz_test_lib:reset_authorizers(),
|
||||||
{ok, _} = emqx_authz:update(replace, [authz_config()]),
|
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_testcase(_TestCase, _Config) ->
|
end_per_testcase(_TestCase, _Config) ->
|
||||||
|
@ -317,17 +316,12 @@ authn_config() ->
|
||||||
<<"algorithm">> => <<"hmac-based">>,
|
<<"algorithm">> => <<"hmac-based">>,
|
||||||
<<"secret">> => ?SECRET,
|
<<"secret">> => ?SECRET,
|
||||||
<<"secret_base64_encoded">> => <<"false">>,
|
<<"secret_base64_encoded">> => <<"false">>,
|
||||||
|
<<"acl_claim_name">> => <<"acl">>,
|
||||||
<<"verify_claims">> => #{
|
<<"verify_claims">> => #{
|
||||||
<<"username">> => ?PH_USERNAME
|
<<"username">> => ?PH_USERNAME
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
authz_config() ->
|
|
||||||
#{
|
|
||||||
<<"type">> => <<"jwt">>,
|
|
||||||
<<"acl_claim_name">> => <<"acl">>
|
|
||||||
}.
|
|
||||||
|
|
||||||
generate_jws(Payload) ->
|
generate_jws(Payload) ->
|
||||||
JWK = jose_jwk:from_oct(?SECRET),
|
JWK = jose_jwk:from_oct(?SECRET),
|
||||||
Header = #{
|
Header = #{
|
||||||
|
|
Loading…
Reference in New Issue