mirror of
https://github.com/valitydev/api-key-mgmt-v2.git
synced 2024-11-06 02:15:19 +00:00
TD-651: Implement RevokeApiKey (#8)
* TD-651: Implement RevokeApiKey * Fix specs and checks * Format * Fix tests * Add revoke * Fix lint * Add test * TD-651: fix formatting * TD-651: fix dialyzer checks * TD-651: fix style * TD-651: fix build * TD-651: fix typo * TD-651: fix formatting * TD-651: implement revoke tokens * TD-651: add party_id checking to sql query * TD-651:fix sql migrations --------- Co-authored-by: anatoliy.losev <losto@nix>
This commit is contained in:
parent
51702c69a0
commit
6e02444fbd
2
.env
2
.env
@ -1,5 +1,5 @@
|
|||||||
SERVICE_NAME=api-key-mgmt-v2
|
SERVICE_NAME=api-key-mgmt-v2
|
||||||
OTP_VERSION=24.2.0
|
OTP_VERSION=25.3
|
||||||
REBAR_VERSION=3.18
|
REBAR_VERSION=3.18
|
||||||
THRIFT_VERSION=0.14.2.3
|
THRIFT_VERSION=0.14.2.3
|
||||||
DATABASE_URL=postgresql://postgres:postgres@db/apikeymgmtv2
|
DATABASE_URL=postgresql://postgres:postgres@db/apikeymgmtv2
|
@ -26,7 +26,9 @@
|
|||||||
eql,
|
eql,
|
||||||
swag_server_apikeys,
|
swag_server_apikeys,
|
||||||
snowflake,
|
snowflake,
|
||||||
woody_user_identity
|
woody_user_identity,
|
||||||
|
erlydtl,
|
||||||
|
gen_smtp
|
||||||
]},
|
]},
|
||||||
{env, []}
|
{env, []}
|
||||||
]}.
|
]}.
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
-export_type([headers/0]).
|
-export_type([headers/0]).
|
||||||
-export_type([response_data/0]).
|
-export_type([response_data/0]).
|
||||||
-export_type([request_context/0]).
|
-export_type([request_context/0]).
|
||||||
|
-export_type([auth_context/0]).
|
||||||
-export_type([operation_id/0]).
|
-export_type([operation_id/0]).
|
||||||
-export_type([handler_context/0]).
|
-export_type([handler_context/0]).
|
||||||
-export_type([swag_server_get_schema_fun/0]).
|
-export_type([swag_server_get_schema_fun/0]).
|
||||||
@ -83,7 +84,7 @@ prepare(OperationID = 'GetApiKey', #{'partyId' := PartyID, 'apiKeyId' := ApiKeyI
|
|||||||
{ok, Resolution}
|
{ok, Resolution}
|
||||||
end,
|
end,
|
||||||
Process = fun() ->
|
Process = fun() ->
|
||||||
case akm_apikeys_processing:get_api_key(ApiKeyId) of
|
case akm_apikeys_processing:get_api_key(ApiKeyId, PartyID) of
|
||||||
{ok, ApiKey} ->
|
{ok, ApiKey} ->
|
||||||
akm_handler_utils:reply_ok(200, ApiKey);
|
akm_handler_utils:reply_ok(200, ApiKey);
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
@ -91,18 +92,67 @@ prepare(OperationID = 'GetApiKey', #{'partyId' := PartyID, 'apiKeyId' := ApiKeyI
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
{ok, #{authorize => Authorize, process => Process}};
|
{ok, #{authorize => Authorize, process => Process}};
|
||||||
prepare(OperationID = 'ListApiKeys', #{'partyId' := PartyID, 'limit' := Limit, 'status' := Status0,
|
prepare(
|
||||||
continuationToken := ContinuationToken0}, Context, _Opts) ->
|
OperationID = 'ListApiKeys',
|
||||||
|
#{
|
||||||
|
'partyId' := PartyID,
|
||||||
|
'limit' := Limit,
|
||||||
|
'status' := Status0,
|
||||||
|
continuationToken := ContinuationToken0
|
||||||
|
},
|
||||||
|
Context,
|
||||||
|
_Opts
|
||||||
|
) ->
|
||||||
Authorize = fun() ->
|
Authorize = fun() ->
|
||||||
Prototypes = [{operation, #{id => OperationID, party => PartyID}}],
|
Prototypes = [{operation, #{id => OperationID, party => PartyID}}],
|
||||||
Resolution = akm_auth:authorize_operation(Prototypes, Context),
|
Resolution = akm_auth:authorize_operation(Prototypes, Context),
|
||||||
{ok, Resolution}
|
{ok, Resolution}
|
||||||
end,
|
end,
|
||||||
Status = genlib:define(Status0, <<"active">>),
|
Status = genlib:define(Status0, <<"active">>),
|
||||||
ContinuationToken = erlang:binary_to_integer(genlib:define(ContinuationToken0, <<"0">>)),
|
ContinuationToken = erlang:binary_to_integer(genlib:define(ContinuationToken0, <<"0">>)),
|
||||||
Process = fun() ->
|
Process = fun() ->
|
||||||
{ok, Response} = akm_apikeys_processing:list_api_keys(PartyID, Status, Limit, ContinuationToken),
|
{ok, Response} = akm_apikeys_processing:list_api_keys(PartyID, Status, Limit, ContinuationToken),
|
||||||
akm_handler_utils:reply_ok(200, Response)
|
akm_handler_utils:reply_ok(200, Response)
|
||||||
end,
|
end,
|
||||||
{ok, #{authorize => Authorize, process => Process}}
|
{ok, #{authorize => Authorize, process => Process}};
|
||||||
.
|
prepare(OperationID = 'RequestRevokeApiKey', Params, Context, _Opts) ->
|
||||||
|
#{
|
||||||
|
'partyId' := PartyID,
|
||||||
|
'apiKeyId' := ApiKeyId,
|
||||||
|
'RequestRevoke' := #{<<"status">> := Status}
|
||||||
|
} = Params,
|
||||||
|
Authorize = fun() ->
|
||||||
|
Prototypes = [{operation, #{id => OperationID, party => PartyID}}],
|
||||||
|
Resolution = akm_auth:authorize_operation(Prototypes, Context),
|
||||||
|
{ok, Resolution}
|
||||||
|
end,
|
||||||
|
Process = fun() ->
|
||||||
|
Email = akm_auth:get_user_email(akm_auth:extract_auth_context(Context)),
|
||||||
|
case akm_apikeys_processing:request_revoke(Email, PartyID, ApiKeyId, Status) of
|
||||||
|
{ok, revoke_email_sent} ->
|
||||||
|
akm_handler_utils:reply_ok(204);
|
||||||
|
{error, not_found} ->
|
||||||
|
akm_handler_utils:reply_error(404)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{ok, #{authorize => Authorize, process => Process}};
|
||||||
|
prepare(
|
||||||
|
OperationID = 'RevokeApiKey',
|
||||||
|
#{'partyId' := PartyID, 'apiKeyId' := ApiKeyId, 'apiKeyRevokeToken' := Token},
|
||||||
|
Context,
|
||||||
|
_Opts
|
||||||
|
) ->
|
||||||
|
Authorize = fun() ->
|
||||||
|
Prototypes = [{operation, #{id => OperationID, party => PartyID}}],
|
||||||
|
Resolution = akm_auth:authorize_operation(Prototypes, Context),
|
||||||
|
{ok, Resolution}
|
||||||
|
end,
|
||||||
|
Process = fun() ->
|
||||||
|
case akm_apikeys_processing:revoke(PartyID, ApiKeyId, Token) of
|
||||||
|
ok ->
|
||||||
|
akm_handler_utils:reply_ok(204);
|
||||||
|
{error, not_found} ->
|
||||||
|
akm_handler_utils:reply_error(404)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{ok, #{authorize => Authorize, process => Process}}.
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
-include_lib("epgsql/include/epgsql.hrl").
|
-include_lib("epgsql/include/epgsql.hrl").
|
||||||
|
|
||||||
-export([issue_api_key/3]).
|
-export([issue_api_key/3]).
|
||||||
-export([get_api_key/1]).
|
-export([get_api_key/2]).
|
||||||
-export([list_api_keys/4]).
|
-export([list_api_keys/4]).
|
||||||
|
-export([request_revoke/4]).
|
||||||
|
-export([revoke/3]).
|
||||||
|
|
||||||
-type list_keys_response() :: #{
|
-type list_keys_response() :: #{
|
||||||
results => [map()],
|
results => [map()],
|
||||||
@ -16,7 +18,7 @@
|
|||||||
-spec issue_api_key(_, _, _) -> _.
|
-spec issue_api_key(_, _, _) -> _.
|
||||||
issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) ->
|
issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) ->
|
||||||
Metadata0 = maps:get(<<"metadata">>, ApiKey0, #{}),
|
Metadata0 = maps:get(<<"metadata">>, ApiKey0, #{}),
|
||||||
%% REWORK ненормальный ID, переработать
|
%% REWORK ненормальный ID, переработать
|
||||||
ID = akm_id:generate_snowflake_id(),
|
ID = akm_id:generate_snowflake_id(),
|
||||||
ContextV1Fragment = bouncer_context_helpers:make_auth_fragment(#{
|
ContextV1Fragment = bouncer_context_helpers:make_auth_fragment(#{
|
||||||
method => <<"IssueApiKey">>,
|
method => <<"IssueApiKey">>,
|
||||||
@ -34,11 +36,11 @@ issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) ->
|
|||||||
{ok, #{token := Token}} ->
|
{ok, #{token := Token}} ->
|
||||||
{ok, _, Columns, Rows} = epgsql_pool:query(
|
{ok, _, Columns, Rows} = epgsql_pool:query(
|
||||||
main_pool,
|
main_pool,
|
||||||
"INSERT INTO apikeys (id, name, party_id, status, metadata)"
|
"INSERT INTO apikeys (id, name, party_id, status, pending_status, metadata)"
|
||||||
"VALUES ($1, $2, $3, $4, $5) RETURNING id, name, status, metadata, created_at",
|
"VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, status, metadata, created_at",
|
||||||
[ID, Name, PartyID, Status, jsx:encode(Metadata)]
|
[ID, Name, PartyID, Status, Status, jsx:encode(Metadata)]
|
||||||
),
|
),
|
||||||
[ApiKey | _] = to_maps(Columns, Rows),
|
[ApiKey | _] = to_marshalled_maps(Columns, Rows),
|
||||||
Resp = #{
|
Resp = #{
|
||||||
<<"AccessToken">> => marshall_access_token(Token),
|
<<"AccessToken">> => marshall_access_token(Token),
|
||||||
<<"ApiKey">> => ApiKey
|
<<"ApiKey">> => ApiKey
|
||||||
@ -48,18 +50,18 @@ issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) ->
|
|||||||
{error, already_exists}
|
{error, already_exists}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_api_key(binary()) -> {ok, map()} | {error, not_found}.
|
-spec get_api_key(binary(), binary()) -> {ok, map()} | {error, not_found}.
|
||||||
get_api_key(ApiKeyId) ->
|
get_api_key(ApiKeyId, PartyId) ->
|
||||||
Result = epgsql_pool:query(
|
Result = epgsql_pool:query(
|
||||||
main_pool,
|
main_pool,
|
||||||
"SELECT id, name, status, metadata, created_at FROM apikeys where id = $1",
|
"SELECT id, name, status, metadata, created_at FROM apikeys WHERE id = $1 AND party_id = $2",
|
||||||
[ApiKeyId]
|
[ApiKeyId, PartyId]
|
||||||
),
|
),
|
||||||
case Result of
|
case Result of
|
||||||
{ok, _Columns, []} ->
|
{ok, _Columns, []} ->
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
[ApiKey | _ ] = to_maps(Columns, Rows),
|
[ApiKey | _] = to_marshalled_maps(Columns, Rows),
|
||||||
{ok, ApiKey}
|
{ok, ApiKey}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@ -74,9 +76,59 @@ list_api_keys(PartyId, Status, Limit, Offset) ->
|
|||||||
case erlang:length(Rows) < Limit of
|
case erlang:length(Rows) < Limit of
|
||||||
true ->
|
true ->
|
||||||
% last piece of data
|
% last piece of data
|
||||||
{ok, #{results => to_maps(Columns, Rows)}};
|
{ok, #{results => to_marshalled_maps(Columns, Rows)}};
|
||||||
false ->
|
false ->
|
||||||
{ok, #{results => to_maps(Columns, Rows), continuationToken => erlang:integer_to_binary(Offset + Limit)}}
|
{ok, #{
|
||||||
|
results => to_marshalled_maps(Columns, Rows),
|
||||||
|
continuationToken => erlang:integer_to_binary(Offset + Limit)
|
||||||
|
}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec request_revoke(binary(), binary(), binary(), binary()) ->
|
||||||
|
{ok, revoke_email_sent} | {error, not_found}.
|
||||||
|
request_revoke(Email, PartyID, ApiKeyId, Status) ->
|
||||||
|
case get_full_api_key(ApiKeyId, PartyID) of
|
||||||
|
{error, not_found} ->
|
||||||
|
{error, not_found};
|
||||||
|
{ok, _ApiKey} ->
|
||||||
|
Token = akm_id:generate_snowflake_id(),
|
||||||
|
try
|
||||||
|
epgsql_pool:transaction(
|
||||||
|
main_pool,
|
||||||
|
fun(Worker) ->
|
||||||
|
ok = akm_mailer:send_revoke_mail(Email, PartyID, ApiKeyId, Token),
|
||||||
|
epgsql_pool:query(
|
||||||
|
Worker,
|
||||||
|
"UPDATE apikeys SET pending_status = $1, revoke_token = $2 "
|
||||||
|
"WHERE id = $3 AND party_id = $4",
|
||||||
|
[Status, Token, ApiKeyId, PartyID]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, 1} ->
|
||||||
|
{ok, revoke_email_sent}
|
||||||
|
catch
|
||||||
|
_Ex:_Er ->
|
||||||
|
error(failed_to_send_email)
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec revoke(binary(), binary(), binary()) -> ok | {error, not_found}.
|
||||||
|
revoke(PartyId, ApiKeyId, RevokeToken) ->
|
||||||
|
case get_full_api_key(ApiKeyId, PartyId) of
|
||||||
|
{ok, #{
|
||||||
|
<<"pending_status">> := PendingStatus,
|
||||||
|
<<"revoke_token">> := RevokeToken
|
||||||
|
}} ->
|
||||||
|
{ok, 1} = epgsql_pool:query(
|
||||||
|
main_pool,
|
||||||
|
"UPDATE apikeys SET status = $1, revoke_token = null WHERE id = $2 AND party_id = $3",
|
||||||
|
[PendingStatus, ApiKeyId, PartyId]
|
||||||
|
),
|
||||||
|
ok;
|
||||||
|
_ ->
|
||||||
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
@ -84,38 +136,45 @@ list_api_keys(PartyId, Status, Limit, Offset) ->
|
|||||||
get_authority_id() ->
|
get_authority_id() ->
|
||||||
application:get_env(akm, authority_id).
|
application:get_env(akm, authority_id).
|
||||||
|
|
||||||
%% Marshalling
|
get_full_api_key(ApiKeyId, PartyId) ->
|
||||||
|
Result = epgsql_pool:query(
|
||||||
|
main_pool,
|
||||||
|
"SELECT * FROM apikeys WHERE id = $1 AND party_id = $2",
|
||||||
|
[ApiKeyId, PartyId]
|
||||||
|
),
|
||||||
|
case Result of
|
||||||
|
{ok, _Columns, []} ->
|
||||||
|
{error, not_found};
|
||||||
|
{ok, Columns, Rows} ->
|
||||||
|
[ApiKey | _] = to_maps(Columns, Rows),
|
||||||
|
{ok, ApiKey}
|
||||||
|
end.
|
||||||
|
|
||||||
marshall_api_key(#{
|
%% Encode/Decode
|
||||||
<<"id">> := ID,
|
|
||||||
<<"created_at">> := DateTime,
|
|
||||||
<<"name">> := Name,
|
|
||||||
<<"status">> := Status,
|
|
||||||
<<"metadata">> := Metadata
|
|
||||||
}) ->
|
|
||||||
#{
|
|
||||||
<<"id">> => ID,
|
|
||||||
<<"createdAt">> => DateTime,
|
|
||||||
<<"name">> => Name,
|
|
||||||
<<"status">> => Status,
|
|
||||||
<<"metadata">> => decode_json(Metadata)
|
|
||||||
}.
|
|
||||||
|
|
||||||
marshall_access_token(Token) ->
|
to_marshalled_maps(Columns, Rows) ->
|
||||||
#{
|
to_maps(Columns, Rows, fun marshall_api_key/1).
|
||||||
<<"accessToken">> => Token
|
|
||||||
}.
|
|
||||||
|
|
||||||
to_maps(Columns, Rows) ->
|
to_maps(Columns, Rows) ->
|
||||||
|
to_maps(Columns, Rows, fun(V) -> V end).
|
||||||
|
|
||||||
|
to_maps(Columns, Rows, TransformRowFun) ->
|
||||||
ColNumbers = erlang:length(Columns),
|
ColNumbers = erlang:length(Columns),
|
||||||
Seq = lists:seq(1, ColNumbers),
|
Seq = lists:seq(1, ColNumbers),
|
||||||
lists:map(fun(Row) ->
|
lists:map(
|
||||||
Data = lists:foldl(fun(Pos, Acc) ->
|
fun(Row) ->
|
||||||
#column{name = Field, type = Type} = lists:nth(Pos, Columns),
|
Data = lists:foldl(
|
||||||
Acc#{Field => convert(Type, erlang:element(Pos, Row))}
|
fun(Pos, Acc) ->
|
||||||
end, #{}, Seq),
|
#column{name = Field, type = Type} = lists:nth(Pos, Columns),
|
||||||
marshall_api_key(Data)
|
Acc#{Field => convert(Type, erlang:element(Pos, Row))}
|
||||||
end, Rows).
|
end,
|
||||||
|
#{},
|
||||||
|
Seq
|
||||||
|
),
|
||||||
|
TransformRowFun(Data)
|
||||||
|
end,
|
||||||
|
Rows
|
||||||
|
).
|
||||||
|
|
||||||
%% for reference https://github.com/epgsql/epgsql#data-representation
|
%% for reference https://github.com/epgsql/epgsql#data-representation
|
||||||
convert(timestamp, Value) ->
|
convert(timestamp, Value) ->
|
||||||
@ -133,3 +192,25 @@ datetime_to_binary(DateTime) ->
|
|||||||
|
|
||||||
decode_json(null) -> #{};
|
decode_json(null) -> #{};
|
||||||
decode_json(Value) -> jsx:decode(Value, [return_maps]).
|
decode_json(Value) -> jsx:decode(Value, [return_maps]).
|
||||||
|
|
||||||
|
%% Marshalling
|
||||||
|
|
||||||
|
marshall_api_key(#{
|
||||||
|
<<"id">> := ID,
|
||||||
|
<<"created_at">> := DateTime,
|
||||||
|
<<"name">> := Name,
|
||||||
|
<<"status">> := Status,
|
||||||
|
<<"metadata">> := Metadata
|
||||||
|
}) ->
|
||||||
|
#{
|
||||||
|
<<"id">> => ID,
|
||||||
|
<<"createdAt">> => DateTime,
|
||||||
|
<<"name">> => Name,
|
||||||
|
<<"status">> => Status,
|
||||||
|
<<"metadata">> => decode_json(Metadata)
|
||||||
|
}.
|
||||||
|
|
||||||
|
marshall_access_token(Token) ->
|
||||||
|
#{
|
||||||
|
<<"accessToken">> => Token
|
||||||
|
}.
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
-export([get_party_id/1]).
|
-export([get_party_id/1]).
|
||||||
-export([get_user_id/1]).
|
-export([get_user_id/1]).
|
||||||
-export([get_user_email/1]).
|
-export([get_user_email/1]).
|
||||||
|
-export([extract_auth_context/1]).
|
||||||
|
|
||||||
-export([preauthorize_api_key/1]).
|
-export([preauthorize_api_key/1]).
|
||||||
-export([authorize_api_key/3]).
|
-export([authorize_api_key/3]).
|
||||||
@ -101,6 +102,8 @@ authorize_operation(Prototypes, Context) ->
|
|||||||
get_token_keeper_fragment(?AUTHORIZED(#{context := Context})) ->
|
get_token_keeper_fragment(?AUTHORIZED(#{context := Context})) ->
|
||||||
Context.
|
Context.
|
||||||
|
|
||||||
|
-spec extract_auth_context(akm_apikeys_handler:handler_context()) ->
|
||||||
|
akm_apikeys_handler:auth_context().
|
||||||
extract_auth_context(#{swagger_context := #{auth_context := AuthContext}}) ->
|
extract_auth_context(#{swagger_context := #{auth_context := AuthContext}}) ->
|
||||||
AuthContext.
|
AuthContext.
|
||||||
|
|
||||||
|
@ -33,11 +33,11 @@ handle_command({ok, {Args, ["new", Name]}}) ->
|
|||||||
"-- :down\n",
|
"-- :down\n",
|
||||||
"-- Down migration\n"
|
"-- Down migration\n"
|
||||||
],
|
],
|
||||||
Result = case file:write_file(Filename, list_to_binary(C), [exclusive]) of
|
Result =
|
||||||
ok -> {ok, "Created migration: ~s~n", [Filename]};
|
case file:write_file(Filename, list_to_binary(C), [exclusive]) of
|
||||||
{error, Reason} -> {error,
|
ok -> {ok, "Created migration: ~s~n", [Filename]};
|
||||||
"Migration can not be written to file ~s: ~s~n", [Filename, Reason]}
|
{error, Reason} -> {error, "Migration can not be written to file ~s: ~s~n", [Filename, Reason]}
|
||||||
end,
|
end,
|
||||||
handle_command_result(Result);
|
handle_command_result(Result);
|
||||||
handle_command({ok, {Args, ["run"]}}) ->
|
handle_command({ok, {Args, ["run"]}}) ->
|
||||||
Available = available_migrations(Args),
|
Available = available_migrations(Args),
|
||||||
@ -128,10 +128,10 @@ handle_command({ok, {_, _}}) ->
|
|||||||
%% Utils
|
%% Utils
|
||||||
|
|
||||||
-type command_result() ::
|
-type command_result() ::
|
||||||
ok
|
ok
|
||||||
| {ok, io:format(), [term()]}
|
| {ok, io:format(), [term()]}
|
||||||
| {error, string()}
|
| {error, string()}
|
||||||
| {error, io:format(), [term()]}.
|
| {error, io:format(), [term()]}.
|
||||||
|
|
||||||
-spec handle_command_result(command_result()) -> ok | {error, term()}.
|
-spec handle_command_result(command_result()) -> ok | {error, term()}.
|
||||||
handle_command_result(ok) ->
|
handle_command_result(ok) ->
|
||||||
|
79
apps/akm/src/akm_mailer.erl
Normal file
79
apps/akm/src/akm_mailer.erl
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
-module(akm_mailer).
|
||||||
|
|
||||||
|
-include_lib("bouncer_proto/include/bouncer_ctx_v1_thrift.hrl").
|
||||||
|
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
|
||||||
|
-include_lib("epgsql/include/epgsql.hrl").
|
||||||
|
|
||||||
|
-define(TEMPLATE_FILE, "request_revoke.dtl").
|
||||||
|
|
||||||
|
-export([send_revoke_mail/4]).
|
||||||
|
|
||||||
|
-spec send_revoke_mail(string(), binary(), binary(), binary()) ->
|
||||||
|
ok | {error, {failed_to_send, term()}}.
|
||||||
|
send_revoke_mail(Email, PartyID, ApiKeyID, Token) ->
|
||||||
|
{ok, Mod} = compile_template(),
|
||||||
|
{ok, Body} = Mod:render([
|
||||||
|
{url, url()},
|
||||||
|
{party_id, PartyID},
|
||||||
|
{api_key_id, ApiKeyID},
|
||||||
|
{revoke_token, Token}
|
||||||
|
]),
|
||||||
|
BinaryBody = erlang:iolist_to_binary(Body),
|
||||||
|
Pid = self(),
|
||||||
|
case
|
||||||
|
gen_smtp_client:send(
|
||||||
|
{from_email(), [Email], BinaryBody},
|
||||||
|
[{relay, relay()}, {username, username()}, {password, password()}],
|
||||||
|
fun(Result) -> erlang:send(Pid, {sending_result, Result}) end
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, {failed_to_send, Reason}};
|
||||||
|
{ok, _SenderPid} ->
|
||||||
|
wait_result()
|
||||||
|
end.
|
||||||
|
|
||||||
|
url() ->
|
||||||
|
#{url := URL} = get_env(),
|
||||||
|
URL.
|
||||||
|
|
||||||
|
from_email() ->
|
||||||
|
#{from_email := From} = get_env(),
|
||||||
|
From.
|
||||||
|
|
||||||
|
relay() ->
|
||||||
|
#{relay := Relay} = get_env(),
|
||||||
|
Relay.
|
||||||
|
|
||||||
|
username() ->
|
||||||
|
#{username := Username} = get_env(),
|
||||||
|
Username.
|
||||||
|
|
||||||
|
password() ->
|
||||||
|
#{password := Password} = get_env(),
|
||||||
|
Password.
|
||||||
|
|
||||||
|
get_env() ->
|
||||||
|
genlib_app:env(akm, mailer, #{
|
||||||
|
url => "vality.dev",
|
||||||
|
from_email => "example@example.com",
|
||||||
|
relay => "smtp.gmail.com",
|
||||||
|
username => "username",
|
||||||
|
%% NOTICE: for gmail need to generate password for application in https://myaccount.google.com/apppasswords
|
||||||
|
password => "password"
|
||||||
|
}).
|
||||||
|
|
||||||
|
compile_template() ->
|
||||||
|
WorkDir = akm_utils:get_env_var("WORK_DIR"),
|
||||||
|
File = filename:join([WorkDir, "priv", "mails", ?TEMPLATE_FILE]),
|
||||||
|
erlydtl:compile(File, akm_mail_request_revoke).
|
||||||
|
|
||||||
|
wait_result() ->
|
||||||
|
receive
|
||||||
|
{sending_result, {ok, _Receipt}} ->
|
||||||
|
ok;
|
||||||
|
{sending_result, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
after 3000 ->
|
||||||
|
{error, {failed_to_send, sending_email_timeout}}
|
||||||
|
end.
|
@ -30,7 +30,7 @@ init([]) ->
|
|||||||
ok = start_epgsql_pooler(),
|
ok = start_epgsql_pooler(),
|
||||||
{ok, {
|
{ok, {
|
||||||
{one_for_all, 0, 1},
|
{one_for_all, 0, 1},
|
||||||
LogicHandlerSpecs ++ [SwaggerSpec]
|
LogicHandlerSpecs ++ [SwaggerSpec]
|
||||||
}}.
|
}}.
|
||||||
|
|
||||||
-spec get_logic_handler_info() -> {akm_swagger_server:logic_handlers(), [supervisor:child_spec()]}.
|
-spec get_logic_handler_info() -> {akm_swagger_server:logic_handlers(), [supervisor:child_spec()]}.
|
||||||
|
@ -29,6 +29,9 @@
|
|||||||
-export([get_unique_id/0]).
|
-export([get_unique_id/0]).
|
||||||
-export([get_random_id/0]).
|
-export([get_random_id/0]).
|
||||||
|
|
||||||
|
-export([get_env_var/1]).
|
||||||
|
-export([get_env_var/2]).
|
||||||
|
|
||||||
-type binding_value() :: binary().
|
-type binding_value() :: binary().
|
||||||
-type url() :: binary().
|
-type url() :: binary().
|
||||||
-type path() :: binary().
|
-type path() :: binary().
|
||||||
@ -172,6 +175,17 @@ parse_deadline(DeadlineStr) ->
|
|||||||
],
|
],
|
||||||
try_parse_deadline(DeadlineStr, Parsers).
|
try_parse_deadline(DeadlineStr, Parsers).
|
||||||
|
|
||||||
|
-spec get_env_var(string()) -> term().
|
||||||
|
get_env_var(Name) ->
|
||||||
|
case os:getenv(Name) of
|
||||||
|
false -> throw({os_env_required, Name});
|
||||||
|
V -> V
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec get_env_var(string(), term()) -> term().
|
||||||
|
get_env_var(Name, Default) ->
|
||||||
|
os:getenv(Name, Default).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%% Internals
|
%% Internals
|
||||||
%%
|
%%
|
||||||
|
@ -4,34 +4,87 @@
|
|||||||
-export([
|
-export([
|
||||||
init_per_suite/1,
|
init_per_suite/1,
|
||||||
end_per_suite/1,
|
end_per_suite/1,
|
||||||
all/0
|
init_per_testcase/2,
|
||||||
|
end_per_testcase/2,
|
||||||
|
all/0,
|
||||||
|
groups/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([issue_get_key_success_test/1]).
|
-export([issue_get_key_success_test/1]).
|
||||||
-export([get_unknown_key_test/1]).
|
-export([get_unknown_key_test/1]).
|
||||||
-export([list_keys_test/1]).
|
-export([list_keys_test/1]).
|
||||||
|
-export([revoke_key_w_email_error_test/1]).
|
||||||
|
-export([revoke_key_test/1]).
|
||||||
|
|
||||||
%% also defined in ct hook module akm_cth.erl
|
%% also defined in ct hook module akm_cth.erl
|
||||||
-define(ACCESS_TOKEN, <<"some.access.token">>).
|
-define(ACCESS_TOKEN, <<"some.access.token">>).
|
||||||
|
|
||||||
|
-type config() :: akm_cth:config().
|
||||||
|
-type test_case_name() :: akm_cth:test_case_name().
|
||||||
|
-type group_name() :: akm_cth:group_name().
|
||||||
|
-type test_result() :: any() | no_return().
|
||||||
|
|
||||||
-spec init_per_suite(_) -> _.
|
-spec init_per_suite(_) -> _.
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
-spec end_per_suite(_) -> _.
|
-spec end_per_suite(_) -> _.
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
|
ok = akm_ct_utils:cleanup_db(),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
-spec all() -> list().
|
-spec all() -> [{group, test_case_name()}].
|
||||||
all() -> [
|
all() ->
|
||||||
issue_get_key_success_test,
|
[{group, basic_operations}].
|
||||||
get_unknown_key_test,
|
|
||||||
list_keys_test
|
|
||||||
].
|
|
||||||
|
|
||||||
-spec issue_get_key_success_test(_) -> _.
|
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
|
||||||
|
groups() ->
|
||||||
|
[
|
||||||
|
{basic_operations, [], [
|
||||||
|
issue_get_key_success_test,
|
||||||
|
get_unknown_key_test,
|
||||||
|
list_keys_test,
|
||||||
|
revoke_key_w_email_error_test,
|
||||||
|
revoke_key_test
|
||||||
|
]}
|
||||||
|
].
|
||||||
|
|
||||||
|
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||||
|
init_per_testcase(revoke_key_w_email_error_test, C) ->
|
||||||
|
meck:expect(
|
||||||
|
gen_smtp_client,
|
||||||
|
send,
|
||||||
|
fun({_, _, _Msg}, _, CallbackFun) ->
|
||||||
|
P = spawn(fun() -> CallbackFun({error, {failed_to_send, sending_email_timeout}}) end),
|
||||||
|
{ok, P}
|
||||||
|
end
|
||||||
|
),
|
||||||
|
C;
|
||||||
|
init_per_testcase(revoke_key_test, C) ->
|
||||||
|
meck:expect(
|
||||||
|
gen_smtp_client,
|
||||||
|
send,
|
||||||
|
fun({_, _, Msg}, _, CallbackFun) ->
|
||||||
|
application:set_env(akm, email_msg, Msg),
|
||||||
|
P = spawn(fun() -> CallbackFun({ok, <<"success">>}) end),
|
||||||
|
{ok, P}
|
||||||
|
end
|
||||||
|
),
|
||||||
|
C;
|
||||||
|
init_per_testcase(_Name, C) ->
|
||||||
|
C.
|
||||||
|
|
||||||
|
-spec end_per_testcase(test_case_name(), config()) -> _.
|
||||||
|
end_per_testcase(Name, C) when
|
||||||
|
Name =:= revoke_key_w_email_error_test;
|
||||||
|
Name =:= revoke_key_test
|
||||||
|
->
|
||||||
|
meck:unload(gen_smtp_client),
|
||||||
|
C;
|
||||||
|
end_per_testcase(_Name, C) ->
|
||||||
|
C.
|
||||||
|
|
||||||
|
-spec issue_get_key_success_test(config()) -> test_result().
|
||||||
issue_get_key_success_test(Config) ->
|
issue_get_key_success_test(Config) ->
|
||||||
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
||||||
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
||||||
@ -59,14 +112,14 @@ issue_get_key_success_test(Config) ->
|
|||||||
%% check getApiKey
|
%% check getApiKey
|
||||||
ExpectedApiKey = akm_client:get_key(Host, Port, PartyId, ApiKeyId).
|
ExpectedApiKey = akm_client:get_key(Host, Port, PartyId, ApiKeyId).
|
||||||
|
|
||||||
-spec get_unknown_key_test(_) -> _.
|
-spec get_unknown_key_test(config()) -> test_result().
|
||||||
get_unknown_key_test(Config) ->
|
get_unknown_key_test(Config) ->
|
||||||
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
||||||
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
||||||
PartyId = <<"unknown_key_test_party">>,
|
PartyId = <<"unknown_key_test_party">>,
|
||||||
not_found = akm_client:get_key(Host, Port, PartyId, <<"UnknownKeyId">>).
|
not_found = akm_client:get_key(Host, Port, PartyId, <<"UnknownKeyId">>).
|
||||||
|
|
||||||
-spec list_keys_test(_) -> _.
|
-spec list_keys_test(config()) -> test_result().
|
||||||
list_keys_test(Config) ->
|
list_keys_test(Config) ->
|
||||||
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
||||||
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
||||||
@ -75,11 +128,19 @@ list_keys_test(Config) ->
|
|||||||
%% check empty list
|
%% check empty list
|
||||||
#{<<"results">> := []} = akm_client:list_keys(Host, Port, PartyId),
|
#{<<"results">> := []} = akm_client:list_keys(Host, Port, PartyId),
|
||||||
|
|
||||||
ListKeys = lists:foldl(fun(Num, Acc) ->
|
ListKeys = lists:foldl(
|
||||||
#{<<"ApiKey">> := ApiKey} = akm_client:issue_key(Host, Port, PartyId,
|
fun(Num, Acc) ->
|
||||||
#{name => <<(erlang:integer_to_binary(Num))/binary, "list_keys_success">>}),
|
#{<<"ApiKey">> := ApiKey} = akm_client:issue_key(
|
||||||
[ApiKey | Acc]
|
Host,
|
||||||
end, [], lists:seq(1, 10)),
|
Port,
|
||||||
|
PartyId,
|
||||||
|
#{name => <<(erlang:integer_to_binary(Num))/binary, "list_keys_success">>}
|
||||||
|
),
|
||||||
|
[ApiKey | Acc]
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
lists:seq(1, 10)
|
||||||
|
),
|
||||||
ExpectedList = lists:reverse(ListKeys),
|
ExpectedList = lists:reverse(ListKeys),
|
||||||
|
|
||||||
%% check one batch
|
%% check one batch
|
||||||
@ -89,17 +150,85 @@ list_keys_test(Config) ->
|
|||||||
|
|
||||||
%% check continuation when limit multiple of the count keys
|
%% check continuation when limit multiple of the count keys
|
||||||
MultLimit = <<"1">>,
|
MultLimit = <<"1">>,
|
||||||
ExpectedList = get_list_keys(Host, Port, PartyId, MultLimit,
|
ExpectedList = get_list_keys(
|
||||||
akm_client:list_keys(Host, Port, PartyId, [{<<"limit">>, MultLimit}]), []),
|
Host,
|
||||||
|
Port,
|
||||||
|
PartyId,
|
||||||
|
MultLimit,
|
||||||
|
akm_client:list_keys(Host, Port, PartyId, [{<<"limit">>, MultLimit}]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
|
||||||
%% check continuation when limit NOT multiple of the count keys
|
%% check continuation when limit NOT multiple of the count keys
|
||||||
NoMultLimit = <<"3">>,
|
NoMultLimit = <<"3">>,
|
||||||
ExpectedList = get_list_keys(Host, Port, PartyId, NoMultLimit,
|
ExpectedList = get_list_keys(
|
||||||
akm_client:list_keys(Host, Port, PartyId, [{<<"limit">>, NoMultLimit}]), []).
|
Host,
|
||||||
|
Port,
|
||||||
|
PartyId,
|
||||||
|
NoMultLimit,
|
||||||
|
akm_client:list_keys(Host, Port, PartyId, [{<<"limit">>, NoMultLimit}]),
|
||||||
|
[]
|
||||||
|
).
|
||||||
|
|
||||||
|
-spec revoke_key_w_email_error_test(config()) -> test_result().
|
||||||
|
revoke_key_w_email_error_test(Config) ->
|
||||||
|
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
||||||
|
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
||||||
|
PartyId = <<"revoke_party">>,
|
||||||
|
|
||||||
|
#{
|
||||||
|
<<"ApiKey">> := #{
|
||||||
|
<<"id">> := ApiKeyId
|
||||||
|
}
|
||||||
|
} = akm_client:issue_key(Host, Port, PartyId, #{name => <<"live-site-integration">>}),
|
||||||
|
|
||||||
|
{500, _, _} = akm_client:request_revoke_key(Host, Port, PartyId, ApiKeyId).
|
||||||
|
|
||||||
|
-spec revoke_key_test(config()) -> test_result().
|
||||||
|
revoke_key_test(Config) ->
|
||||||
|
Host = akm_ct_utils:lookup_config(akm_host, Config),
|
||||||
|
Port = akm_ct_utils:lookup_config(akm_port, Config),
|
||||||
|
PartyId = <<"revoke_party">>,
|
||||||
|
|
||||||
|
#{
|
||||||
|
<<"ApiKey">> := #{
|
||||||
|
<<"id">> := ApiKeyId
|
||||||
|
}
|
||||||
|
} = akm_client:issue_key(Host, Port, PartyId, #{name => <<"live-site-integration">>}),
|
||||||
|
|
||||||
|
%% check request with unknown ApiKeyId
|
||||||
|
not_found = akm_client:request_revoke_key(Host, Port, PartyId, <<"BadID">>),
|
||||||
|
|
||||||
|
%% check success request revoke
|
||||||
|
{204, _, _} = akm_client:request_revoke_key(Host, Port, PartyId, ApiKeyId),
|
||||||
|
|
||||||
|
RevokePath = extract_revoke_path(),
|
||||||
|
RevokeWithBadApiKeyId = break_api_key_id(RevokePath, ApiKeyId),
|
||||||
|
RevokeWithBadRevokeToken = break_revoke_token(RevokePath),
|
||||||
|
|
||||||
|
%% check revoke with unknown ApiKey
|
||||||
|
not_found = akm_client:revoke_key(Host, Port, RevokeWithBadApiKeyId),
|
||||||
|
|
||||||
|
%% check revoke with unknown RevokeToken
|
||||||
|
not_found = akm_client:revoke_key(Host, Port, RevokeWithBadRevokeToken),
|
||||||
|
|
||||||
|
%% check success revoke
|
||||||
|
{204, _, _} = akm_client:revoke_key(Host, Port, RevokePath).
|
||||||
|
|
||||||
get_list_keys(Host, Port, PartyId, Limit, #{<<"results">> := ListKeys, <<"continuationToken">> := Cont}, Acc) ->
|
get_list_keys(Host, Port, PartyId, Limit, #{<<"results">> := ListKeys, <<"continuationToken">> := Cont}, Acc) ->
|
||||||
Params = [{<<"limit">>, Limit}, {<<"continuationToken">>, Cont}],
|
Params = [{<<"limit">>, Limit}, {<<"continuationToken">>, Cont}],
|
||||||
get_list_keys(Host, Port, PartyId, Limit, akm_client:list_keys(Host, Port, PartyId, Params), Acc ++ ListKeys);
|
get_list_keys(Host, Port, PartyId, Limit, akm_client:list_keys(Host, Port, PartyId, Params), Acc ++ ListKeys);
|
||||||
get_list_keys(_Host, _Port, _PartyId, _Limit, #{<<"results">> := ListKeys}, Acc) ->
|
get_list_keys(_Host, _Port, _PartyId, _Limit, #{<<"results">> := ListKeys}, Acc) ->
|
||||||
Acc ++ ListKeys.
|
Acc ++ ListKeys.
|
||||||
|
|
||||||
|
extract_revoke_path() ->
|
||||||
|
{ok, Msg} = application:get_env(akm, email_msg),
|
||||||
|
[_, Path] = binary:split(Msg, <<".dev">>),
|
||||||
|
Path.
|
||||||
|
|
||||||
|
break_api_key_id(Path, ApiKeyId) ->
|
||||||
|
binary:replace(Path, ApiKeyId, <<"BadID">>).
|
||||||
|
|
||||||
|
break_revoke_token(Path) ->
|
||||||
|
[Head | _] = binary:split(Path, <<"=">>),
|
||||||
|
<<Head/binary, "=BadRevokeToken">>.
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
issue_key/4,
|
issue_key/4,
|
||||||
get_key/4,
|
get_key/4,
|
||||||
list_keys/4,
|
list_keys/4,
|
||||||
list_keys/3
|
list_keys/3,
|
||||||
|
request_revoke_key/4,
|
||||||
|
revoke_key/3
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-spec issue_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), map()) -> any().
|
-spec issue_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), map()) -> any().
|
||||||
@ -53,6 +55,32 @@ list_keys(Host, Port, PartyId, QsList) ->
|
|||||||
disconnect(ConnPid),
|
disconnect(ConnPid),
|
||||||
parse(Answer).
|
parse(Answer).
|
||||||
|
|
||||||
|
-spec request_revoke_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), binary()) -> any().
|
||||||
|
request_revoke_key(Host, Port, PartyId, ApiKeyId) ->
|
||||||
|
Path = <<"/apikeys/v2/orgs/", PartyId/binary, "/api-keys/", ApiKeyId/binary, "/status">>,
|
||||||
|
Headers = [
|
||||||
|
{<<"X-Request-ID">>, <<"request_revoke">>},
|
||||||
|
{<<"content-type">>, <<"application/json; charset=utf-8">>},
|
||||||
|
{<<"Authorization">>, <<"Bearer sffsdfsfsdfsdfs">>}
|
||||||
|
],
|
||||||
|
Body = jsx:encode(#{<<"status">> => <<"revoked">>}),
|
||||||
|
ConnPid = connect(Host, Port),
|
||||||
|
Answer = put(ConnPid, Path, Headers, Body),
|
||||||
|
disconnect(ConnPid),
|
||||||
|
parse(Answer).
|
||||||
|
|
||||||
|
-spec revoke_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary()) -> any().
|
||||||
|
revoke_key(Host, Port, PathWithQuery) ->
|
||||||
|
Headers = [
|
||||||
|
{<<"X-Request-ID">>, <<"revoke_key">>},
|
||||||
|
{<<"content-type">>, <<"application/json; charset=utf-8">>},
|
||||||
|
{<<"Authorization">>, <<"Bearer sffsdfsfsdfsdfs">>}
|
||||||
|
],
|
||||||
|
ConnPid = connect(Host, Port),
|
||||||
|
Answer = get(ConnPid, PathWithQuery, Headers),
|
||||||
|
disconnect(ConnPid),
|
||||||
|
parse(Answer).
|
||||||
|
|
||||||
% Internal functions
|
% Internal functions
|
||||||
|
|
||||||
-spec connect(inet:hostname() | inet:ip_address(), inet:port_number()) -> any().
|
-spec connect(inet:hostname() | inet:ip_address(), inet:port_number()) -> any().
|
||||||
@ -76,6 +104,10 @@ post(ConnPid, Path, Headers, Body) ->
|
|||||||
StreamRef = gun:post(ConnPid, Path, Headers, Body),
|
StreamRef = gun:post(ConnPid, Path, Headers, Body),
|
||||||
get_response(ConnPid, StreamRef).
|
get_response(ConnPid, StreamRef).
|
||||||
|
|
||||||
|
put(ConnPid, Path, Headers, Body) ->
|
||||||
|
StreamRef = gun:put(ConnPid, Path, Headers, Body),
|
||||||
|
get_response(ConnPid, StreamRef).
|
||||||
|
|
||||||
get_response(ConnPid, StreamRef) ->
|
get_response(ConnPid, StreamRef) ->
|
||||||
case gun:await(ConnPid, StreamRef) of
|
case gun:await(ConnPid, StreamRef) of
|
||||||
{response, fin, Status, Headers} ->
|
{response, fin, Status, Headers} ->
|
||||||
@ -85,7 +117,8 @@ get_response(ConnPid, StreamRef) ->
|
|||||||
{Status, Headers, Body}
|
{Status, Headers, Body}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
maybe_query(Path, []) -> Path;
|
maybe_query(Path, []) ->
|
||||||
|
Path;
|
||||||
maybe_query(Path, QsList) ->
|
maybe_query(Path, QsList) ->
|
||||||
QS = uri_string:compose_query(QsList),
|
QS = uri_string:compose_query(QsList),
|
||||||
<<Path/binary, "?", QS/binary>>.
|
<<Path/binary, "?", QS/binary>>.
|
@ -3,7 +3,8 @@
|
|||||||
%% API
|
%% API
|
||||||
-export([
|
-export([
|
||||||
lookup_config/2,
|
lookup_config/2,
|
||||||
lookup_config/3
|
lookup_config/3,
|
||||||
|
cleanup_db/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-spec lookup_config(_, _) -> _.
|
-spec lookup_config(_, _) -> _.
|
||||||
@ -19,3 +20,8 @@ lookup_config(Key, Config, Default) ->
|
|||||||
false -> Default;
|
false -> Default;
|
||||||
{_, Value} -> Value
|
{_, Value} -> Value
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec cleanup_db() -> ok.
|
||||||
|
cleanup_db() ->
|
||||||
|
{ok, _, _} = epgsql_pool:query(main_pool, "TRUNCATE apikeys"),
|
||||||
|
ok.
|
@ -3,6 +3,14 @@
|
|||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
|
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
|
||||||
|
|
||||||
|
-type config() :: [{atom(), term()}].
|
||||||
|
-type test_case_name() :: atom().
|
||||||
|
-type group_name() :: atom().
|
||||||
|
|
||||||
|
-export_type([config/0]).
|
||||||
|
-export_type([test_case_name/0]).
|
||||||
|
-export_type([group_name/0]).
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([
|
-export([
|
||||||
init/2,
|
init/2,
|
||||||
@ -52,12 +60,18 @@ pipe(Funs, State) ->
|
|||||||
|
|
||||||
set_environment(State) ->
|
set_environment(State) ->
|
||||||
{_, SysConfig} = lookup_key(sys_config, State),
|
{_, SysConfig} = lookup_key(sys_config, State),
|
||||||
lists:foreach(fun({Application, Config}) ->
|
lists:foreach(
|
||||||
ok = application:load(Application),
|
fun({Application, Config}) ->
|
||||||
lists:foreach(fun({Param, Value}) ->
|
ok = application:load(Application),
|
||||||
application:set_env(Application, Param, Value)
|
lists:foreach(
|
||||||
end, Config)
|
fun({Param, Value}) ->
|
||||||
end, SysConfig),
|
application:set_env(Application, Param, Value)
|
||||||
|
end,
|
||||||
|
Config
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
SysConfig
|
||||||
|
),
|
||||||
State.
|
State.
|
||||||
|
|
||||||
prepare_config(State) ->
|
prepare_config(State) ->
|
||||||
@ -82,6 +96,14 @@ prepare_config(State) ->
|
|||||||
user_id => <<"dev.vality.user.id">>,
|
user_id => <<"dev.vality.user.id">>,
|
||||||
user_email => <<"dev.vality.user.email">>
|
user_email => <<"dev.vality.user.email">>
|
||||||
}
|
}
|
||||||
|
}},
|
||||||
|
|
||||||
|
{mailer, #{
|
||||||
|
url => "http://vality.dev",
|
||||||
|
from_email => "example@example.com",
|
||||||
|
relay => "smtp4dev",
|
||||||
|
username => "username",
|
||||||
|
password => "password"
|
||||||
}}
|
}}
|
||||||
]}
|
]}
|
||||||
],
|
],
|
25
compose.yaml
25
compose.yaml
@ -59,3 +59,28 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 1s
|
timeout: 1s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
|
smtp4dev:
|
||||||
|
image: rnwood/smtp4dev:v3
|
||||||
|
ports:
|
||||||
|
# Change the number before : to the port the web interface should be accessible on
|
||||||
|
- '5000:80'
|
||||||
|
# Change the number before : to the port the SMTP server should be accessible on
|
||||||
|
- '25:25'
|
||||||
|
# Change the number before : to the port the IMAP server should be accessible on
|
||||||
|
- '143:143'
|
||||||
|
volumes:
|
||||||
|
# This is where smtp4dev stores the database..
|
||||||
|
- smtp4dev-data:/smtp4dev
|
||||||
|
environment:
|
||||||
|
#Specifies the server hostname. Used in auto-generated TLS certificate if enabled.
|
||||||
|
- ServerOptions__HostName=smtp4dev
|
||||||
|
|
||||||
|
#The username for the SMTP server used to relay messages. If "" no authentication is attempted.
|
||||||
|
- RelayOptions__Login=username
|
||||||
|
|
||||||
|
#The password for the SMTP server used to relay messages
|
||||||
|
- RelayOptions__Password=password
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
smtp4dev-data:
|
||||||
|
@ -75,6 +75,14 @@
|
|||||||
username => "postgres",
|
username => "postgres",
|
||||||
password => "postgres",
|
password => "postgres",
|
||||||
database => "apikeymgmtv2"
|
database => "apikeymgmtv2"
|
||||||
|
}},
|
||||||
|
|
||||||
|
{mailer, #{
|
||||||
|
url => "vality.dev",
|
||||||
|
from_email => "example@example.com",
|
||||||
|
relay => "smtp.gmail.com",
|
||||||
|
username => "username",
|
||||||
|
password => "password"
|
||||||
}}
|
}}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
@ -25,6 +25,11 @@
|
|||||||
akm_apikeys_handler,
|
akm_apikeys_handler,
|
||||||
akm_apikeys_processing
|
akm_apikeys_processing
|
||||||
]
|
]
|
||||||
|
}},
|
||||||
|
{elvis_style, invalid_dynamic_call, #{
|
||||||
|
ignore => [
|
||||||
|
akm_mailer
|
||||||
|
]
|
||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -4,13 +4,13 @@
|
|||||||
CREATE TYPE apikeys_status AS ENUM ('active', 'revoked');
|
CREATE TYPE apikeys_status AS ENUM ('active', 'revoked');
|
||||||
|
|
||||||
CREATE TABLE apikeys (
|
CREATE TABLE apikeys (
|
||||||
id TEXT,
|
id TEXT,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
party_id TEXT,
|
party_id TEXT,
|
||||||
status apikeys_status,
|
status apikeys_status,
|
||||||
revoke_token TEXT,
|
revoke_token TEXT,
|
||||||
metadata TEXT,
|
metadata TEXT,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
);
|
);
|
||||||
-- :down
|
-- :down
|
||||||
-- Down migration
|
-- Down migration
|
||||||
|
8
migrations/1689828848-TD-651-add-pending-status.sql
Normal file
8
migrations/1689828848-TD-651-add-pending-status.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- migrations/1689828848-TD-651-add-pending-status.sql
|
||||||
|
-- :up
|
||||||
|
-- Up migration
|
||||||
|
ALTER TABLE apikeys ADD COLUMN pending_status apikeys_status;
|
||||||
|
|
||||||
|
-- :down
|
||||||
|
-- Down migration
|
||||||
|
ALTER TABLE apikeys DROP COLUMN pending_status;
|
1
priv/mails/request_revoke.dtl
Normal file
1
priv/mails/request_revoke.dtl
Normal file
@ -0,0 +1 @@
|
|||||||
|
To revoke key, go to link: {{ url }}/apikeys/v2/orgs/{{ party_id }}/revoke-api-key/{{ api_key_id }}?apiKeyRevokeToken={{ revoke_token }}
|
20
rebar.config
20
rebar.config
@ -27,6 +27,8 @@
|
|||||||
% Common project dependencies.
|
% Common project dependencies.
|
||||||
{deps, [
|
{deps, [
|
||||||
{gun, "2.0.1"},
|
{gun, "2.0.1"},
|
||||||
|
{gen_smtp, "1.2.0"},
|
||||||
|
{erlydtl, "0.14.0"},
|
||||||
{genlib, {git, "https://github.com/valitydev/genlib.git", {branch, "master"}}},
|
{genlib, {git, "https://github.com/valitydev/genlib.git", {branch, "master"}}},
|
||||||
{cowboy_draining_server, {git, "https://github.com/valitydev/cowboy_draining_server.git", {branch, "master"}}},
|
{cowboy_draining_server, {git, "https://github.com/valitydev/cowboy_draining_server.git", {branch, "master"}}},
|
||||||
{uuid, {git, "https://github.com/okeuday/uuid.git", {branch, "master"}}},
|
{uuid, {git, "https://github.com/okeuday/uuid.git", {branch, "master"}}},
|
||||||
@ -109,13 +111,12 @@
|
|||||||
{sys_config, "./config/sys.config"},
|
{sys_config, "./config/sys.config"},
|
||||||
{vm_args, "./config/vm.args"},
|
{vm_args, "./config/vm.args"},
|
||||||
{mode, minimal},
|
{mode, minimal},
|
||||||
{extended_start_script, true},
|
{extended_start_script, true}
|
||||||
%% api-key-mgmt-v2
|
%% api-key-mgmt-v2
|
||||||
{overlay, [
|
%{overlay, [
|
||||||
{mkdir, "var/keys/api-key-mgmt-v2"},
|
% {mkdir, "var/keys/akm"},
|
||||||
{copy, "apps/api-key-mgmt-v2/var/keys/api-key-mgmt-v2/private.pem",
|
% {copy, "apps/akm/var/keys/akm/private.pem", "var/keys/akm/private.pem"}
|
||||||
"var/keys/api-key-mgmt-v2/private.pem"}
|
%]}
|
||||||
]}
|
|
||||||
]}
|
]}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
@ -152,7 +153,7 @@
|
|||||||
{erlfmt, [
|
{erlfmt, [
|
||||||
{print_width, 120},
|
{print_width, 120},
|
||||||
{files, [
|
{files, [
|
||||||
"apps/api-key-mgmt*/{src,include,test}/*.{hrl,erl,app.src}",
|
"apps/akm*/{src,include,test}/*.{hrl,erl,app.src}",
|
||||||
"rebar.config",
|
"rebar.config",
|
||||||
"elvis.config",
|
"elvis.config",
|
||||||
"config/sys.config",
|
"config/sys.config",
|
||||||
@ -170,3 +171,8 @@
|
|||||||
{ct_opts, [
|
{ct_opts, [
|
||||||
{ct_hooks, [akm_cth]}
|
{ct_hooks, [akm_cth]}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{shell, [
|
||||||
|
{apps, [akm]},
|
||||||
|
{config, "config/sys.config"}
|
||||||
|
]}.
|
||||||
|
16
rebar.lock
16
rebar.lock
@ -65,6 +65,7 @@
|
|||||||
{git,"https://github.com/valitydev/erlang-health.git",
|
{git,"https://github.com/valitydev/erlang-health.git",
|
||||||
{ref,"5958e2f35cd4d09f40685762b82b82f89b4d9333"}},
|
{ref,"5958e2f35cd4d09f40685762b82b82f89b4d9333"}},
|
||||||
0},
|
0},
|
||||||
|
{<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},0},
|
||||||
{<<"file_storage_proto">>,
|
{<<"file_storage_proto">>,
|
||||||
{git,"https://github.com/valitydev/file-storage-proto.git",
|
{git,"https://github.com/valitydev/file-storage-proto.git",
|
||||||
{ref,"1dbc0067db68780660b4f691ea6ca6d5f68d56aa"}},
|
{ref,"1dbc0067db68780660b4f691ea6ca6d5f68d56aa"}},
|
||||||
@ -77,6 +78,7 @@
|
|||||||
{git,"https://github.com/valitydev/fistful-reporter-proto.git",
|
{git,"https://github.com/valitydev/fistful-reporter-proto.git",
|
||||||
{ref,"69565e48f036ded9b5ecc337b4f631d0e2fa6f8d"}},
|
{ref,"69565e48f036ded9b5ecc337b4f631d0e2fa6f8d"}},
|
||||||
0},
|
0},
|
||||||
|
{<<"gen_smtp">>,{pkg,<<"gen_smtp">>,<<"1.2.0">>},0},
|
||||||
{<<"genlib">>,
|
{<<"genlib">>,
|
||||||
{git,"https://github.com/valitydev/genlib.git",
|
{git,"https://github.com/valitydev/genlib.git",
|
||||||
{ref,"b08ef4d61e0dde98995ec3d2f69a4447255e79ef"}},
|
{ref,"b08ef4d61e0dde98995ec3d2f69a4447255e79ef"}},
|
||||||
@ -119,7 +121,7 @@
|
|||||||
{git,"https://github.com/okeuday/quickrand.git",
|
{git,"https://github.com/okeuday/quickrand.git",
|
||||||
{ref,"7fe89e9cfcc1378b7164e9dac4e7f02119110b68"}},
|
{ref,"7fe89e9cfcc1378b7164e9dac4e7f02119110b68"}},
|
||||||
1},
|
1},
|
||||||
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},2},
|
{<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},1},
|
||||||
{<<"scoper">>,
|
{<<"scoper">>,
|
||||||
{git,"https://github.com/valitydev/scoper.git",
|
{git,"https://github.com/valitydev/scoper.git",
|
||||||
{ref,"87110f5bd72c0e39ba9b7d6eca88fea91b8cd357"}},
|
{ref,"87110f5bd72c0e39ba9b7d6eca88fea91b8cd357"}},
|
||||||
@ -131,11 +133,11 @@
|
|||||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2},
|
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2},
|
||||||
{<<"swag_client_apikeys">>,
|
{<<"swag_client_apikeys">>,
|
||||||
{git,"https://github.com/valitydev/swag-api-keys-v2.git",
|
{git,"https://github.com/valitydev/swag-api-keys-v2.git",
|
||||||
{ref,"fb6c31fb680897e2073a29c795b61acebabec2e7"}},
|
{ref,"de86e82de67071276030186f4de806f1a7ff0431"}},
|
||||||
0},
|
0},
|
||||||
{<<"swag_server_apikeys">>,
|
{<<"swag_server_apikeys">>,
|
||||||
{git,"https://github.com/valitydev/swag-api-keys-v2.git",
|
{git,"https://github.com/valitydev/swag-api-keys-v2.git",
|
||||||
{ref,"4ce0440be9fef7786d86317247e53c1722d8c430"}},
|
{ref,"5e27ca5e3aa6f4b44b9677e870e5c8d557fee773"}},
|
||||||
0},
|
0},
|
||||||
{<<"tds_proto">>,
|
{<<"tds_proto">>,
|
||||||
{git,"https://github.com/valitydev/tds-proto.git",
|
{git,"https://github.com/valitydev/tds-proto.git",
|
||||||
@ -174,6 +176,8 @@
|
|||||||
{<<"cowlib">>, <<"A9FA9A625F1D2025FE6B462CB865881329B5CAFF8F1854D1CBC9F9533F00E1E1">>},
|
{<<"cowlib">>, <<"A9FA9A625F1D2025FE6B462CB865881329B5CAFF8F1854D1CBC9F9533F00E1E1">>},
|
||||||
{<<"email_validator">>, <<"7E09A862E9AA99AE2CA6FD2A718D2B94360E32940A1339B53DFEE6B774BCDB03">>},
|
{<<"email_validator">>, <<"7E09A862E9AA99AE2CA6FD2A718D2B94360E32940A1339B53DFEE6B774BCDB03">>},
|
||||||
{<<"eql">>, <<"598ABC19A1CF6AFB8EF89FFEA869F43BAEBB1CEC3260DD5065112FEE7D8CE3E2">>},
|
{<<"eql">>, <<"598ABC19A1CF6AFB8EF89FFEA869F43BAEBB1CEC3260DD5065112FEE7D8CE3E2">>},
|
||||||
|
{<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>},
|
||||||
|
{<<"gen_smtp">>, <<"9CFC75C72A8821588B9B9FE947AE5AB2AED95A052B81237E0928633A13276FD3">>},
|
||||||
{<<"getopt">>, <<"33D9B44289FE7AD08627DDFE1D798E30B2DA0033B51DA1B3A2D64E72CD581D02">>},
|
{<<"getopt">>, <<"33D9B44289FE7AD08627DDFE1D798E30B2DA0033B51DA1B3A2D64E72CD581D02">>},
|
||||||
{<<"gproc">>, <<"853CCB7805E9ADA25D227A157BA966F7B34508F386A3E7E21992B1B484230699">>},
|
{<<"gproc">>, <<"853CCB7805E9ADA25D227A157BA966F7B34508F386A3E7E21992B1B484230699">>},
|
||||||
{<<"gun">>, <<"160A9A5394800FCBA41BC7E6D421295CF9A7894C2252C0678244948E3336AD73">>},
|
{<<"gun">>, <<"160A9A5394800FCBA41BC7E6D421295CF9A7894C2252C0678244948E3336AD73">>},
|
||||||
@ -184,7 +188,7 @@
|
|||||||
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
|
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
|
||||||
{<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>},
|
{<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>},
|
||||||
{<<"pooler">>, <<"898CD1FA301FC42D4A8ED598CE139B71CA85B54C16AB161152B5CC5FBDCFA1A8">>},
|
{<<"pooler">>, <<"898CD1FA301FC42D4A8ED598CE139B71CA85B54C16AB161152B5CC5FBDCFA1A8">>},
|
||||||
{<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>},
|
{<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>},
|
||||||
{<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>},
|
{<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>},
|
||||||
{<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]},
|
{<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]},
|
||||||
{pkg_hash_ext,[
|
{pkg_hash_ext,[
|
||||||
@ -194,6 +198,8 @@
|
|||||||
{<<"cowlib">>, <<"163B73F6367A7341B33C794C4E88E7DBFE6498AC42DCD69EF44C5BC5507C8DB0">>},
|
{<<"cowlib">>, <<"163B73F6367A7341B33C794C4E88E7DBFE6498AC42DCD69EF44C5BC5507C8DB0">>},
|
||||||
{<<"email_validator">>, <<"2B1E6DF7BB14155C8D7D131F1C95CF4676200BC056EEBA82123396833FF94DA2">>},
|
{<<"email_validator">>, <<"2B1E6DF7BB14155C8D7D131F1C95CF4676200BC056EEBA82123396833FF94DA2">>},
|
||||||
{<<"eql">>, <<"513BE6B36EE86E8292B2B7475C257ABB66CED5AAD40CBF7AD21E233D0A3BF51D">>},
|
{<<"eql">>, <<"513BE6B36EE86E8292B2B7475C257ABB66CED5AAD40CBF7AD21E233D0A3BF51D">>},
|
||||||
|
{<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>},
|
||||||
|
{<<"gen_smtp">>, <<"5EE0375680BCA8F20C4D85F58C2894441443A743355430FF33A783FE03296779">>},
|
||||||
{<<"getopt">>, <<"A0029AEA4322FB82A61F6876A6D9C66DC9878B6CB61FAA13DF3187384FD4EA26">>},
|
{<<"getopt">>, <<"A0029AEA4322FB82A61F6876A6D9C66DC9878B6CB61FAA13DF3187384FD4EA26">>},
|
||||||
{<<"gproc">>, <<"587E8AF698CCD3504CF4BA8D90F893EDE2B0F58CABB8A916E2BF9321DE3CF10B">>},
|
{<<"gproc">>, <<"587E8AF698CCD3504CF4BA8D90F893EDE2B0F58CABB8A916E2BF9321DE3CF10B">>},
|
||||||
{<<"gun">>, <<"A10BC8D6096B9502205022334F719CC9A08D9ADCFBFC0DBEE9EF31B56274A20B">>},
|
{<<"gun">>, <<"A10BC8D6096B9502205022334F719CC9A08D9ADCFBFC0DBEE9EF31B56274A20B">>},
|
||||||
@ -204,7 +210,7 @@
|
|||||||
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
|
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
|
||||||
{<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>},
|
{<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>},
|
||||||
{<<"pooler">>, <<"058D85C5081289B90E97E4DDDBC3BB5A3B4A19A728AB3BC88C689EFCC36A07C7">>},
|
{<<"pooler">>, <<"058D85C5081289B90E97E4DDDBC3BB5A3B4A19A728AB3BC88C689EFCC36A07C7">>},
|
||||||
{<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>},
|
{<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>},
|
||||||
{<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>},
|
{<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>},
|
||||||
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]}
|
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]}
|
||||||
].
|
].
|
||||||
|
Loading…
Reference in New Issue
Block a user