fix(authn): fix some bugs
This commit is contained in:
parent
a5a596e3ac
commit
2a594b1a73
|
@ -16,338 +16,414 @@
|
|||
|
||||
-module(emqx_authn_api).
|
||||
|
||||
-behavior(minirest_api).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
-export([ create_authenticator/2
|
||||
, delete_authenticator/2
|
||||
, update_authenticator/2
|
||||
, lookup_authenticator/2
|
||||
, list_authenticators/2
|
||||
, move_authenticator/2
|
||||
, import_users/2
|
||||
, add_user/2
|
||||
, delete_user/2
|
||||
, update_user/2
|
||||
, lookup_user/2
|
||||
, list_users/2
|
||||
]).
|
||||
-export([ api_spec/0 ]).
|
||||
|
||||
-rest_api(#{name => create_authenticator,
|
||||
method => 'POST',
|
||||
path => "/authentication/authenticators",
|
||||
func => create_authenticator,
|
||||
descr => "Create authenticator"
|
||||
}).
|
||||
api_spec() ->
|
||||
{[authenticator_api()], definitions()}.
|
||||
|
||||
-rest_api(#{name => delete_authenticator,
|
||||
method => 'DELETE',
|
||||
path => "/authentication/authenticators/:bin:name",
|
||||
func => delete_authenticator,
|
||||
descr => "Delete authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => update_authenticator,
|
||||
method => 'PUT',
|
||||
path => "/authentication/authenticators/:bin:name",
|
||||
func => update_authenticator,
|
||||
descr => "Update authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_authenticator,
|
||||
method => 'GET',
|
||||
path => "/authentication/authenticators/:bin:name",
|
||||
func => lookup_authenticator,
|
||||
descr => "Lookup authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list_authenticators,
|
||||
method => 'GET',
|
||||
path => "/authentication/authenticators",
|
||||
func => list_authenticators,
|
||||
descr => "List authenticators"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => move_authenticator,
|
||||
method => 'POST',
|
||||
path => "/authentication/authenticators/:bin:name/position",
|
||||
func => move_authenticator,
|
||||
descr => "Change the order of authenticators"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => import_users,
|
||||
method => 'POST',
|
||||
path => "/authentication/authenticators/:bin:name/import-users",
|
||||
func => import_users,
|
||||
descr => "Import users"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => add_user,
|
||||
method => 'POST',
|
||||
path => "/authentication/authenticators/:bin:name/users",
|
||||
func => add_user,
|
||||
descr => "Add user"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => delete_user,
|
||||
method => 'DELETE',
|
||||
path => "/authentication/authenticators/:bin:name/users/:bin:user_id",
|
||||
func => delete_user,
|
||||
descr => "Delete user"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => update_user,
|
||||
method => 'PUT',
|
||||
path => "/authentication/authenticators/:bin:name/users/:bin:user_id",
|
||||
func => update_user,
|
||||
descr => "Update user"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_user,
|
||||
method => 'GET',
|
||||
path => "/authentication/authenticators/:bin:name/users/:bin:user_id",
|
||||
func => lookup_user,
|
||||
descr => "Lookup user"
|
||||
}).
|
||||
|
||||
%% TODO: Support pagination
|
||||
-rest_api(#{name => list_users,
|
||||
method => 'GET',
|
||||
path => "/authentication/authenticators/:bin:name/users",
|
||||
func => list_users,
|
||||
descr => "List all users"
|
||||
}).
|
||||
|
||||
create_authenticator(Binding, Params) ->
|
||||
do_create_authenticator(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
do_create_authenticator(_Binding, Authenticator0) ->
|
||||
Config = #{<<"emqx_authn">> => #{
|
||||
<<"authenticators">> => [Authenticator0]
|
||||
}},
|
||||
#{emqx_authn := #{authenticators := [Authenticator1]}}
|
||||
= hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:create_authenticator(?CHAIN, Authenticator1) of
|
||||
{ok, Authenticator2} ->
|
||||
return({ok, Authenticator2});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
delete_authenticator(Binding, Params) ->
|
||||
do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_delete_authenticator(#{name := Name}, _Params) ->
|
||||
case emqx_authn:delete_authenticator(?CHAIN, Name) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
%% TODO: Support incremental update
|
||||
update_authenticator(Binding, Params) ->
|
||||
do_update_authenticator(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
%% TOOD: PUT method supports creation and update
|
||||
do_update_authenticator(#{name := Name}, NewConfig0) ->
|
||||
case emqx_authn:lookup_authenticator(?CHAIN, Name) of
|
||||
{ok, #{mechanism := Mechanism}} ->
|
||||
Authenticator = #{<<"name">> => Name,
|
||||
<<"mechanism">> => Mechanism,
|
||||
<<"config">> => NewConfig0},
|
||||
Config = #{<<"emqx_authn">> => #{
|
||||
<<"authenticators">> => [Authenticator]
|
||||
}},
|
||||
#{
|
||||
emqx_authn := #{
|
||||
authenticators := [#{
|
||||
config := NewConfig1
|
||||
}]
|
||||
authenticator_api() ->
|
||||
Example1 = #{name => <<"example">>,
|
||||
mechanism => <<"password-based">>,
|
||||
config => #{
|
||||
server_type => <<"built-in-example">>,
|
||||
user_id_type => <<"username">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"sha256">>
|
||||
}
|
||||
} = hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:update_authenticator(?CHAIN, Name, NewConfig1) of
|
||||
{ok, NAuthenticator} ->
|
||||
return({ok, NAuthenticator});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
}},
|
||||
Example2 = #{name => <<"example">>,
|
||||
mechanism => <<"password-based">>,
|
||||
config => #{
|
||||
server_type => <<"http-server">>,
|
||||
method => <<"post">>,
|
||||
url => <<"http://localhost:80/login">>,
|
||||
headers => #{
|
||||
<<"content-type">> => <<"application/json">>
|
||||
},
|
||||
form_data => #{
|
||||
<<"username">> => <<"${mqtt-username}">>,
|
||||
<<"password">> => <<"${mqtt-password}">>
|
||||
}
|
||||
}},
|
||||
Example3 = #{name => <<"example">>,
|
||||
mechanism => <<"jwt">>,
|
||||
config => #{
|
||||
use_jwks => false,
|
||||
algorithm => <<"hmac-based">>,
|
||||
secret => <<"mysecret">>,
|
||||
secret_base64_encoded => false,
|
||||
verify_claims => #{
|
||||
<<"username">> => <<"${mqtt-username}">>
|
||||
}
|
||||
}},
|
||||
Metadata = #{
|
||||
post => #{
|
||||
description => "Create authenticator",
|
||||
requestBody => #{
|
||||
content => #{
|
||||
'application/json' => #{
|
||||
schema => minirest:ref(<<"authenticator">>),
|
||||
examples => #{
|
||||
default => #{
|
||||
summary => <<"Default">>,
|
||||
value => emqx_json:encode(Example1)
|
||||
},
|
||||
http => #{
|
||||
summary => <<"Authentication provided by HTTP Server">>,
|
||||
value => emqx_json:encode(Example2)
|
||||
},
|
||||
jwt => #{
|
||||
summary => <<"JWT Authentication">>,
|
||||
value => emqx_json:encode(Example3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses => #{
|
||||
<<"201">> => #{
|
||||
description => <<"Created successfully">>,
|
||||
content => #{}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{"/authentication/authenticators", Metadata, clients}.
|
||||
|
||||
lookup_authenticator(Binding, Params) ->
|
||||
do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)).
|
||||
definitions() ->
|
||||
AuthenticatorDef = #{
|
||||
allOf => [
|
||||
#{
|
||||
type => object,
|
||||
required => [name],
|
||||
properties => #{
|
||||
name => #{
|
||||
type => string,
|
||||
example => "exmaple"
|
||||
}
|
||||
}
|
||||
},
|
||||
#{
|
||||
oneOf => [ minirest:ref(<<"password_based">>)
|
||||
, minirest:ref(<<"jwt">>)
|
||||
, minirest:ref(<<"scram">>)
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
do_lookup_authenticator(#{name := Name}, _Params) ->
|
||||
case emqx_authn:lookup_authenticator(?CHAIN, Name) of
|
||||
{ok, Authenticator} ->
|
||||
return({ok, Authenticator});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
PasswordBasedDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
mechanism => #{
|
||||
type => string,
|
||||
enum => [<<"password-based">>],
|
||||
example => <<"password-based">>
|
||||
},
|
||||
config => #{
|
||||
oneOf => [ minirest:ref(<<"password_based_built_in_database">>)
|
||||
, minirest:ref(<<"password_based_mysql">>)
|
||||
, minirest:ref(<<"password_based_pgsql">>)
|
||||
, minirest:ref(<<"password_based_http_server">>)
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
list_authenticators(Binding, Params) ->
|
||||
do_list_authenticators(uri_decode(Binding), maps:from_list(Params)).
|
||||
JWTDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
mechanism => #{
|
||||
type => string,
|
||||
enum => [<<"jwt">>],
|
||||
example => <<"jwt">>
|
||||
},
|
||||
config => #{
|
||||
type => object,
|
||||
properties => #{
|
||||
use_jwks => #{
|
||||
type => boolean,
|
||||
default => false,
|
||||
example => false
|
||||
},
|
||||
algorithm => #{
|
||||
type => string,
|
||||
enum => [<<"hmac-based">>, <<"public-key">>],
|
||||
default => <<"hmac-based">>,
|
||||
example => <<"hmac-based">>
|
||||
},
|
||||
secret => #{
|
||||
type => string
|
||||
},
|
||||
secret_base64_encoded => #{
|
||||
type => boolean,
|
||||
default => false
|
||||
},
|
||||
certificate => #{
|
||||
type => string
|
||||
},
|
||||
verify_claims => #{
|
||||
type => object,
|
||||
additionalProperties => #{
|
||||
type => string
|
||||
}
|
||||
},
|
||||
ssl => minirest:ref(<<"ssl">>)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
do_list_authenticators(_Binding, _Params) ->
|
||||
case emqx_authn:list_authenticators(?CHAIN) of
|
||||
{ok, Authenticators} ->
|
||||
return({ok, Authenticators});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
SCRAMDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
mechanism => #{
|
||||
type => string,
|
||||
enum => [<<"scram">>],
|
||||
example => <<"scram">>
|
||||
},
|
||||
config => #{
|
||||
type => object,
|
||||
properties => #{
|
||||
server_type => #{
|
||||
type => string,
|
||||
enum => [<<"built-in-database">>],
|
||||
default => <<"built-in-database">>
|
||||
},
|
||||
algorithm => #{
|
||||
type => string,
|
||||
enum => [<<"sha256">>, <<"sha512">>],
|
||||
default => <<"sha256">>
|
||||
},
|
||||
iteration_count => #{
|
||||
type => integer,
|
||||
default => 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
move_authenticator(Binding, Params) ->
|
||||
do_move_authenticator(uri_decode(Binding), maps:from_list(Params)).
|
||||
PasswordBasedBuiltInDatabaseDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
server_type => #{
|
||||
type => string,
|
||||
enum => [<<"built-in-database">>],
|
||||
example => <<"built-in-database">>
|
||||
},
|
||||
user_id_type => #{
|
||||
type => string,
|
||||
enum => [<<"username">>, <<"clientid">>],
|
||||
default => <<"username">>,
|
||||
example => <<"username">>
|
||||
},
|
||||
password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>)
|
||||
}
|
||||
},
|
||||
|
||||
do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the front">>}) ->
|
||||
case emqx_authn:move_authenticator_to_the_front(?CHAIN, Name) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the end">>}) ->
|
||||
case emqx_authn:move_authenticator_to_the_end(?CHAIN, Name) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_move_authenticator(#{name := Name}, #{<<"position">> := N}) when is_number(N) ->
|
||||
case emqx_authn:move_authenticator_to_the_nth(?CHAIN, Name, N) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_move_authenticator(_Binding, _Params) ->
|
||||
return(serialize_error({missing_parameter, <<"position">>})).
|
||||
PasswordBasedMySQLDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
server_type => #{
|
||||
type => string,
|
||||
enum => [<<"mysql">>],
|
||||
example => <<"mysql">>
|
||||
},
|
||||
server => #{
|
||||
type => string,
|
||||
example => <<"localhost:3306">>
|
||||
},
|
||||
database => #{
|
||||
type => string
|
||||
},
|
||||
pool_size => #{
|
||||
type => integer,
|
||||
default => 8
|
||||
},
|
||||
username => #{
|
||||
type => string
|
||||
},
|
||||
password => #{
|
||||
type => string
|
||||
},
|
||||
auto_reconnect => #{
|
||||
type => boolean,
|
||||
default => true
|
||||
},
|
||||
ssl => minirest:ref(<<"ssl">>),
|
||||
password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>),
|
||||
salt_position => #{
|
||||
type => string,
|
||||
enum => [<<"prefix">>, <<"suffix">>],
|
||||
default => <<"prefix">>
|
||||
},
|
||||
query => #{
|
||||
type => string,
|
||||
example => <<"SELECT password_hash FROM mqtt_user WHERE username = ${mqtt-username}">>
|
||||
},
|
||||
query_timeout => #{
|
||||
type => integer,
|
||||
description => <<"Query timeout, Unit: Milliseconds">>,
|
||||
default => 5000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
import_users(Binding, Params) ->
|
||||
do_import_users(uri_decode(Binding), maps:from_list(Params)).
|
||||
PasswordBasedPgSQLDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
server_type => #{
|
||||
type => string,
|
||||
enum => [<<"pgsql">>],
|
||||
example => <<"pgsql">>
|
||||
},
|
||||
server => #{
|
||||
type => string,
|
||||
example => <<"localhost:5432">>
|
||||
},
|
||||
database => #{
|
||||
type => string
|
||||
},
|
||||
pool_size => #{
|
||||
type => integer,
|
||||
default => 8
|
||||
},
|
||||
username => #{
|
||||
type => string
|
||||
},
|
||||
password => #{
|
||||
type => string
|
||||
},
|
||||
auto_reconnect => #{
|
||||
type => boolean,
|
||||
default => true
|
||||
},
|
||||
password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>),
|
||||
salt_position => #{
|
||||
type => string,
|
||||
enum => [<<"prefix">>, <<"suffix">>],
|
||||
default => <<"prefix">>
|
||||
},
|
||||
query => #{
|
||||
type => string,
|
||||
example => <<"SELECT password_hash FROM mqtt_user WHERE username = ${mqtt-username}">>
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
do_import_users(#{name := Name},
|
||||
#{<<"filename">> := Filename}) ->
|
||||
case emqx_authn:import_users(?CHAIN, Name, Filename) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_import_users(_Binding, Params) ->
|
||||
Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]),
|
||||
return(serialize_error({missing_parameter, Missed})).
|
||||
PasswordBasedHTTPServerDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
server_type => #{
|
||||
type => string,
|
||||
enum => [<<"http-server">>],
|
||||
example => <<"http-server">>
|
||||
},
|
||||
method => #{
|
||||
type => string,
|
||||
enum => [<<"get">>, <<"post">>],
|
||||
default => <<"post">>
|
||||
},
|
||||
url => #{
|
||||
type => string,
|
||||
example => <<"http://localhost:80/login">>
|
||||
},
|
||||
headers => #{
|
||||
type => object,
|
||||
additionalProperties => #{
|
||||
type => string
|
||||
}
|
||||
},
|
||||
format_data => #{
|
||||
type => string
|
||||
},
|
||||
connect_timeout => #{
|
||||
type => integer,
|
||||
default => 5000
|
||||
},
|
||||
max_retries => #{
|
||||
type => integer,
|
||||
default => 5
|
||||
},
|
||||
retry_interval => #{
|
||||
type => integer,
|
||||
default => 1000
|
||||
},
|
||||
request_timout => #{
|
||||
type => integer,
|
||||
default => 5000
|
||||
},
|
||||
pool_size => #{
|
||||
type => integer,
|
||||
default => 8
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
add_user(Binding, Params) ->
|
||||
do_add_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
PasswordHashAlgorithmDef = #{
|
||||
type => object,
|
||||
required => [name],
|
||||
properties => #{
|
||||
name => #{
|
||||
type => string,
|
||||
enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>],
|
||||
default => <<"sha256">>
|
||||
},
|
||||
salt_rounds => #{
|
||||
type => integer,
|
||||
default => 10
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
do_add_user(#{name := Name}, UserInfo) ->
|
||||
case emqx_authn:add_user(?CHAIN, Name, UserInfo) of
|
||||
{ok, User} ->
|
||||
return({ok, User});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
SSLDef = #{
|
||||
type => object,
|
||||
properties => #{
|
||||
enable => #{
|
||||
type => boolean,
|
||||
default => false
|
||||
},
|
||||
certfile => #{
|
||||
type => string
|
||||
},
|
||||
keyfile => #{
|
||||
type => string
|
||||
},
|
||||
cacertfile => #{
|
||||
type => string
|
||||
},
|
||||
verify => #{
|
||||
type => boolean,
|
||||
default => true
|
||||
},
|
||||
server_name_indication => #{
|
||||
type => object,
|
||||
properties => #{
|
||||
enable => #{
|
||||
type => boolean,
|
||||
default => false
|
||||
},
|
||||
hostname => #{
|
||||
type => string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
delete_user(Binding, Params) ->
|
||||
do_delete_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_delete_user(#{name := Name,
|
||||
user_id := UserID}, _Params) ->
|
||||
case emqx_authn:delete_user(?CHAIN, Name, UserID) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
update_user(Binding, Params) ->
|
||||
do_update_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_update_user(#{name := Name,
|
||||
user_id := UserID}, NewUserInfo) ->
|
||||
case emqx_authn:update_user(?CHAIN, Name, UserID, NewUserInfo) of
|
||||
{ok, User} ->
|
||||
return({ok, User});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
lookup_user(Binding, Params) ->
|
||||
do_lookup_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_lookup_user(#{name := Name,
|
||||
user_id := UserID}, _Params) ->
|
||||
case emqx_authn:lookup_user(?CHAIN, Name, UserID) of
|
||||
{ok, User} ->
|
||||
return({ok, User});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
list_users(Binding, Params) ->
|
||||
do_list_users(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_list_users(#{name := Name}, _Params) ->
|
||||
case emqx_authn:list_users(?CHAIN, Name) of
|
||||
{ok, Users} ->
|
||||
return({ok, Users});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
uri_decode(Params) ->
|
||||
maps:fold(fun(K, V, Acc) ->
|
||||
Acc#{K => emqx_http_lib:uri_decode(V)}
|
||||
end, #{}, Params).
|
||||
|
||||
lists_to_map(L) ->
|
||||
lists_to_map(L, #{}).
|
||||
|
||||
lists_to_map([], Acc) ->
|
||||
Acc;
|
||||
lists_to_map([{K, V} | More], Acc) when is_list(V) ->
|
||||
NV = lists_to_map(V),
|
||||
lists_to_map(More, Acc#{K => NV});
|
||||
lists_to_map([{K, V} | More], Acc) ->
|
||||
lists_to_map(More, Acc#{K => V});
|
||||
lists_to_map([_ | _] = L, _) ->
|
||||
L.
|
||||
|
||||
serialize_error({already_exists, {Type, ID}}) ->
|
||||
{error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))};
|
||||
serialize_error({not_found, {Type, ID}}) ->
|
||||
{error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))};
|
||||
serialize_error({duplicate, Name}) ->
|
||||
{error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Authenticator name '~s' is duplicated", [Name]))};
|
||||
serialize_error({missing_parameter, Names = [_ | Rest]}) ->
|
||||
Format = ["~s," || _ <- Rest] ++ ["~s"],
|
||||
NFormat = binary_to_list(iolist_to_binary(Format)),
|
||||
{error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))};
|
||||
serialize_error({missing_parameter, Name}) ->
|
||||
{error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))};
|
||||
serialize_error(_) ->
|
||||
{error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}.
|
||||
|
||||
serialize_type(authenticator) ->
|
||||
"Authenticator".
|
||||
|
||||
get_missed_params(Actual, Expected) ->
|
||||
Keys = lists:foldl(fun(Key, Acc) ->
|
||||
case maps:is_key(Key, Actual) of
|
||||
true -> Acc;
|
||||
false -> [Key | Acc]
|
||||
end
|
||||
end, [], Expected),
|
||||
lists:reverse(Keys).
|
||||
|
||||
return(_) ->
|
||||
%% TODO: V5 API
|
||||
ok.
|
||||
[ #{<<"authenticator">> => AuthenticatorDef}
|
||||
, #{<<"password_based">> => PasswordBasedDef}
|
||||
, #{<<"jwt">> => JWTDef}
|
||||
, #{<<"scram">> => SCRAMDef}
|
||||
, #{<<"password_based_built_in_database">> => PasswordBasedBuiltInDatabaseDef}
|
||||
, #{<<"password_based_mysql">> => PasswordBasedMySQLDef}
|
||||
, #{<<"password_based_pgsql">> => PasswordBasedPgSQLDef}
|
||||
, #{<<"password_based_http_server">> => PasswordBasedHTTPServerDef}
|
||||
, #{<<"password_hash_algorithm">> => PasswordHashAlgorithmDef}
|
||||
, #{<<"ssl">> => SSLDef}
|
||||
].
|
|
@ -26,13 +26,6 @@
|
|||
, validations/0
|
||||
]).
|
||||
|
||||
-type accept() :: 'application/json' | 'application/x-www-form-urlencoded'.
|
||||
-type content_type() :: accept().
|
||||
|
||||
-reflect_type([ accept/0
|
||||
, content_type/0
|
||||
]).
|
||||
|
||||
-export([ create/3
|
||||
, update/4
|
||||
, authenticate/2
|
||||
|
@ -53,45 +46,53 @@ fields("") ->
|
|||
|
||||
fields(get) ->
|
||||
[ {method, #{type => get,
|
||||
default => get}}
|
||||
default => post}}
|
||||
, {headers, fun headers_no_content_type/1}
|
||||
] ++ common_fields();
|
||||
|
||||
fields(post) ->
|
||||
[ {method, #{type => post,
|
||||
default => get}}
|
||||
, {content_type, fun content_type/1}
|
||||
default => post}}
|
||||
, {headers, fun headers/1}
|
||||
] ++ common_fields().
|
||||
|
||||
common_fields() ->
|
||||
[ {server_type, {enum, ['http-server']}}
|
||||
, {url, fun url/1}
|
||||
, {accept, fun accept/1}
|
||||
, {headers, fun headers/1}
|
||||
, {form_data, fun form_data/1}
|
||||
, {request_timeout, fun request_timeout/1}
|
||||
] ++ proplists:delete(base_url, emqx_connector_http:fields(config)).
|
||||
] ++ maps:to_list(maps:without([ base_url
|
||||
, pool_type],
|
||||
maps:from_list(emqx_connector_http:fields(config)))).
|
||||
|
||||
validations() ->
|
||||
[ {check_ssl_opts, fun check_ssl_opts/1} ].
|
||||
[ {check_ssl_opts, fun check_ssl_opts/1}
|
||||
, {check_headers, fun check_headers/1}
|
||||
].
|
||||
|
||||
url(type) -> binary();
|
||||
url(nullable) -> false;
|
||||
url(validate) -> [fun check_url/1];
|
||||
url(_) -> undefined.
|
||||
|
||||
accept(type) -> accept();
|
||||
accept(default) -> 'application/json';
|
||||
accept(_) -> undefined.
|
||||
|
||||
content_type(type) -> content_type();
|
||||
content_type(default) -> 'application/json';
|
||||
content_type(_) -> undefined.
|
||||
|
||||
headers(type) -> list();
|
||||
headers(default) -> [];
|
||||
headers(type) -> map();
|
||||
headers(converter) ->
|
||||
fun(Headers) ->
|
||||
maps:merge(default_headers(), transform_header_name(Headers))
|
||||
end;
|
||||
headers(default) -> default_headers();
|
||||
headers(_) -> undefined.
|
||||
|
||||
form_data(type) -> binary();
|
||||
headers_no_content_type(type) -> map();
|
||||
headers_no_content_type(converter) ->
|
||||
fun(Headers) ->
|
||||
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
||||
end;
|
||||
headers_no_content_type(default) -> default_headers_no_content_type();
|
||||
headers_no_content_type(_) -> undefined.
|
||||
|
||||
%% TODO: Using map()
|
||||
form_data(type) -> map();
|
||||
form_data(nullable) -> false;
|
||||
form_data(validate) -> [fun check_form_data/1];
|
||||
form_data(_) -> undefined.
|
||||
|
@ -107,28 +108,17 @@ request_timeout(_) -> undefined.
|
|||
create(ChainID, AuthenticatorName,
|
||||
#{method := Method,
|
||||
url := URL,
|
||||
accept := Accept,
|
||||
headers := Headers,
|
||||
form_data := FormData,
|
||||
request_timeout := RequestTimeout} = Config) ->
|
||||
ContentType = maps:get(content_type, Config, undefined),
|
||||
DefaultHeader0 = case ContentType of
|
||||
undefined -> #{};
|
||||
_ -> #{<<"content-type">> => atom_to_binary(ContentType, utf8)}
|
||||
end,
|
||||
DefaultHeader = DefaultHeader0#{<<"accept">> => atom_to_binary(Accept, utf8)},
|
||||
NHeaders = maps:to_list(maps:merge(DefaultHeader, maps:from_list(Headers))),
|
||||
NFormData = preprocess_form_data(FormData),
|
||||
#{path := Path,
|
||||
query := Query} = URIMap = parse_url(URL),
|
||||
BaseURL = generate_base_url(URIMap),
|
||||
State = #{method => Method,
|
||||
path => Path,
|
||||
base_query => cow_qs:parse_qs(list_to_binary(Query)),
|
||||
accept => Accept,
|
||||
content_type => ContentType,
|
||||
headers => NHeaders,
|
||||
form_data => NFormData,
|
||||
headers => maps:to_list(Headers),
|
||||
form_data => FormData,
|
||||
request_timeout => RequestTimeout},
|
||||
ResourceID = <<ChainID/binary, "/", AuthenticatorName/binary>>,
|
||||
case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url => BaseURL}) of
|
||||
|
@ -182,26 +172,38 @@ check_url(URL) ->
|
|||
end.
|
||||
|
||||
check_form_data(FormData) ->
|
||||
KVs = binary:split(FormData, [<<"&">>], [global]),
|
||||
case false =:= lists:any(fun(T) -> T =:= <<>> end, KVs) of
|
||||
true ->
|
||||
NKVs = [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs],
|
||||
false =:=
|
||||
lists:any(fun({K, V}) ->
|
||||
K =:= <<>> orelse V =:= <<>>;
|
||||
(_) ->
|
||||
true
|
||||
end, NKVs);
|
||||
false ->
|
||||
false
|
||||
end.
|
||||
lists:any(fun({_, V}) ->
|
||||
not is_binary(V)
|
||||
end, maps:to_list(FormData)).
|
||||
|
||||
default_headers() ->
|
||||
maps:put(<<"content-type">>,
|
||||
<<"application/json">>,
|
||||
default_headers_no_content_type()).
|
||||
|
||||
default_headers_no_content_type() ->
|
||||
#{ <<"accept">> => <<"application/json">>
|
||||
, <<"cache-control">> => <<"no-cache">>
|
||||
, <<"connection">> => <<"keep-alive">>
|
||||
, <<"keep-alive">> => <<"timeout=5">>
|
||||
}.
|
||||
|
||||
transform_header_name(Headers) ->
|
||||
maps:fold(fun(K0, V, Acc) ->
|
||||
K = list_to_binary(string:to_lower(binary_to_list(K0))),
|
||||
maps:put(K, V, Acc)
|
||||
end, #{}, Headers).
|
||||
|
||||
check_ssl_opts(Conf) ->
|
||||
emqx_connector_http:check_ssl_opts("url", Conf).
|
||||
|
||||
preprocess_form_data(FormData) ->
|
||||
KVs = binary:split(FormData, [<<"&">>], [global]),
|
||||
[list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs].
|
||||
check_headers(Conf) ->
|
||||
Method = hocon_schema:get_value("method", Conf),
|
||||
Headers = hocon_schema:get_value("headers", Conf),
|
||||
case Method =:= get andalso maps:get(<<"content-type">>, Headers, undefined) =/= undefined of
|
||||
true -> false;
|
||||
false -> true
|
||||
end.
|
||||
|
||||
parse_url(URL) ->
|
||||
{ok, URIMap} = emqx_http_lib:uri_parse(URL),
|
||||
|
@ -220,7 +222,6 @@ generate_base_url(#{scheme := Scheme,
|
|||
generate_request(Credential, #{method := Method,
|
||||
path := Path,
|
||||
base_query := BaseQuery,
|
||||
content_type := ContentType,
|
||||
headers := Headers,
|
||||
form_data := FormData0}) ->
|
||||
FormData = replace_placeholders(FormData0, Credential),
|
||||
|
@ -230,6 +231,7 @@ generate_request(Credential, #{method := Method,
|
|||
{NPath, Headers};
|
||||
post ->
|
||||
NPath = append_query(Path, BaseQuery),
|
||||
ContentType = proplists:get_value(<<"content-type">>, Headers),
|
||||
Body = serialize_body(ContentType, FormData),
|
||||
{NPath, Headers, Body}
|
||||
end.
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
|
||||
-export([ structs/0
|
||||
, fields/1
|
||||
, validations/0
|
||||
]).
|
||||
|
||||
-export([ create/3
|
||||
|
@ -81,19 +80,13 @@ fields(ssl_enable) ->
|
|||
];
|
||||
|
||||
fields(ssl_disable) ->
|
||||
[ {enable, #{type => false}} ];
|
||||
|
||||
fields(claim) ->
|
||||
[ {"$name", fun expected_claim_value/1} ].
|
||||
|
||||
validations() ->
|
||||
[ {check_verify_claims, fun check_verify_claims/1} ].
|
||||
[ {enable, #{type => false}} ].
|
||||
|
||||
secret(type) -> string();
|
||||
secret(_) -> undefined.
|
||||
|
||||
secret_base64_encoded(type) -> boolean();
|
||||
secret_base64_encoded(defualt) -> false;
|
||||
secret_base64_encoded(default) -> false;
|
||||
secret_base64_encoded(_) -> undefined.
|
||||
|
||||
certificate(type) -> string();
|
||||
|
@ -123,13 +116,15 @@ verify(_) -> undefined.
|
|||
server_name_indication(type) -> string();
|
||||
server_name_indication(_) -> undefined.
|
||||
|
||||
verify_claims(type) -> hoconsc:array(hoconsc:ref(claim));
|
||||
verify_claims(default) -> [];
|
||||
verify_claims(type) -> list();
|
||||
verify_claims(default) -> #{};
|
||||
verify_claims(validate) -> [fun check_verify_claims/1];
|
||||
verify_claims(converter) ->
|
||||
fun(VerifyClaims) ->
|
||||
maps:to_list(VerifyClaims)
|
||||
end;
|
||||
verify_claims(_) -> undefined.
|
||||
|
||||
expected_claim_value(type) -> string();
|
||||
expected_claim_value(_) -> undefined.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
|
@ -57,6 +57,10 @@ password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other
|
|||
password_hash_algorithm(default) -> #{<<"name">> => sha256};
|
||||
password_hash_algorithm(_) -> undefined.
|
||||
|
||||
salt_rounds(type) -> integer();
|
||||
salt_rounds(default) -> 10;
|
||||
salt_rounds(_) -> undefined.
|
||||
|
||||
salt_position(type) -> {enum, [prefix, suffix]};
|
||||
salt_position(default) -> prefix;
|
||||
salt_position(_) -> undefined.
|
||||
|
|
Loading…
Reference in New Issue