mirror of
https://github.com/valitydev/url-shortener.git
synced 2024-11-06 01:55:19 +00:00
MSPF-605: Add bouncer integration (#35)
This commit is contained in:
parent
bf8673d8ed
commit
71b63ffb38
@ -14,6 +14,8 @@
|
||||
cowboy_access_log,
|
||||
swag_server,
|
||||
mg_proto,
|
||||
bouncer_proto,
|
||||
bouncer_client,
|
||||
woody,
|
||||
woody_user_identity,
|
||||
erl_health,
|
||||
|
@ -1,7 +1,7 @@
|
||||
-module(shortener_auth).
|
||||
|
||||
-export([authorize_api_key/2]).
|
||||
-export([authorize_operation/3]).
|
||||
-export([authorize_operation/4]).
|
||||
|
||||
-type context() :: shortener_authorizer_jwt:t().
|
||||
-type claims() :: shortener_authorizer_jwt:claims().
|
||||
@ -42,17 +42,38 @@ parse_api_key(ApiKey) ->
|
||||
authorize_api_key(_OperationID, bearer, Token) ->
|
||||
shortener_authorizer_jwt:verify(Token).
|
||||
|
||||
-spec authorize_operation(OperationID, Slug, Context) -> ok | {error, forbidden} when
|
||||
-spec authorize_operation(OperationID, Slug, ReqContext, WoodyCtx) -> ok | {error, forbidden} when
|
||||
OperationID :: swag_server:operation_id(),
|
||||
Slug :: shortener_slug:slug() | no_slug,
|
||||
Context :: context().
|
||||
authorize_operation(OperationID, Slug, {{SubjectID, ACL}, _Claims}) ->
|
||||
ReqContext :: swag_server:request_context(),
|
||||
WoodyCtx :: woody_context:ctx().
|
||||
authorize_operation(OperationID, Slug, ReqContext, WoodyCtx) ->
|
||||
{{SubjectID, _ACL, ExpiresAt}, Claims} = get_auth_context(ReqContext),
|
||||
IpAddress = get_peer(ReqContext),
|
||||
Owner = get_slug_owner(Slug),
|
||||
Permissions = shortener_acl:match(['shortened-urls'], ACL),
|
||||
case is_operation_permitted(OperationID, SubjectID, Owner, Permissions) of
|
||||
true ->
|
||||
ID = get_slug_id(Slug),
|
||||
Email = maps:get(<<"email">>, Claims, undefined),
|
||||
JudgeContext = #{
|
||||
fragments => #{
|
||||
<<"env">> => bouncer_context_helpers:make_default_env_context_fragment(),
|
||||
<<"auth">> => bouncer_context_helpers:make_auth_context_fragment(#{
|
||||
method => <<"SessionToken">>,
|
||||
expiration => genlib_rfc3339:format(ExpiresAt, second)
|
||||
}),
|
||||
<<"user">> => bouncer_context_helpers:make_user_context_fragment(#{id => SubjectID, email => Email}),
|
||||
<<"requester">> => bouncer_context_helpers:make_requester_context_fragment(#{ip => IpAddress}),
|
||||
<<"shortener">> => shortener_bouncer_client:make_shortener_context_fragment(
|
||||
genlib:to_binary(OperationID),
|
||||
ID,
|
||||
Owner
|
||||
)
|
||||
}
|
||||
},
|
||||
{ok, RulesetID} = application:get_env(shortener, bouncer_ruleset_id),
|
||||
case bouncer_client:judge(RulesetID, JudgeContext, WoodyCtx) of
|
||||
allowed ->
|
||||
ok;
|
||||
false ->
|
||||
forbidden ->
|
||||
{error, forbidden}
|
||||
end.
|
||||
|
||||
@ -62,13 +83,21 @@ get_slug_owner(no_slug) ->
|
||||
get_slug_owner(#{owner := Owner}) ->
|
||||
Owner.
|
||||
|
||||
is_operation_permitted('ShortenUrl', _SubjectID, undefined, Ps) ->
|
||||
lists:member(write, Ps);
|
||||
is_operation_permitted('DeleteShortenedUrl', _SubjectID, undefined, Ps) ->
|
||||
lists:member(write, Ps);
|
||||
is_operation_permitted('DeleteShortenedUrl', SubjectID, Owner, Ps) ->
|
||||
(SubjectID == Owner) and lists:member(write, Ps);
|
||||
is_operation_permitted('GetShortenedUrl', _SubjectID, undefined, Ps) ->
|
||||
lists:member(read, Ps);
|
||||
is_operation_permitted('GetShortenedUrl', SubjectID, Owner, Ps) ->
|
||||
(SubjectID == Owner) and lists:member(read, Ps).
|
||||
-spec get_slug_id(shortener_slug:slug() | no_slug) -> shortener_slug:id() | undefined.
|
||||
get_slug_id(no_slug) ->
|
||||
undefined;
|
||||
get_slug_id(#{id := ID}) ->
|
||||
ID.
|
||||
|
||||
get_auth_context(#{auth_context := AuthContext}) ->
|
||||
AuthContext.
|
||||
|
||||
get_peer(#{peer := Peer}) ->
|
||||
case maps:get(ip_address, Peer, undefined) of
|
||||
undefined ->
|
||||
undefined;
|
||||
IP ->
|
||||
inet:ntoa(IP)
|
||||
end;
|
||||
get_peer(_) ->
|
||||
undefined.
|
||||
|
@ -24,7 +24,8 @@
|
||||
-type key() :: #jose_jwk{}.
|
||||
-type token() :: binary().
|
||||
-type claims() :: #{binary() => term()}.
|
||||
-type subject() :: {subject_id(), shortener_acl:t()}.
|
||||
%% Added expiration to subject tuple as part of token service claims
|
||||
-type subject() :: {subject_id(), shortener_acl:t(), pos_integer() | undefined}.
|
||||
-type subject_id() :: binary().
|
||||
-type t() :: {subject(), claims()}.
|
||||
-type expiration() ::
|
||||
@ -262,8 +263,9 @@ verify(KID, Alg, ExpandedToken) ->
|
||||
verify(JWK, ExpandedToken) ->
|
||||
case jose_jwt:verify(JWK, ExpandedToken) of
|
||||
{true, #jose_jwt{fields = Claims}, _JWS} ->
|
||||
{#{subject_id := SubjectID}, Claims1} = validate_claims(Claims),
|
||||
get_result(SubjectID, decode_roles(Claims1));
|
||||
{Data = #{subject_id := SubjectID}, Claims1} = validate_claims(Claims),
|
||||
ExpiresAt = maps:get(expires_at, Data, undefined),
|
||||
get_result({SubjectID, ExpiresAt}, decode_roles(Claims1));
|
||||
{false, _JWT, _JWS} ->
|
||||
{error, invalid_signature}
|
||||
end.
|
||||
@ -277,9 +279,9 @@ validate_claims(Claims, [{Name, Claim, Validator} | Rest], Acc) ->
|
||||
validate_claims(Claims, [], Acc) ->
|
||||
{Acc, Claims}.
|
||||
|
||||
get_result(SubjectID, {Roles, Claims}) ->
|
||||
get_result({SubjectID, ExpiresAt}, {Roles, Claims}) ->
|
||||
try
|
||||
Subject = {SubjectID, shortener_acl:decode(Roles)},
|
||||
Subject = {SubjectID, shortener_acl:decode(Roles), ExpiresAt},
|
||||
{ok, {Subject, Claims}}
|
||||
catch
|
||||
error:{badarg, _} = Reason ->
|
||||
|
30
apps/shortener/src/shortener_bouncer_client.erl
Normal file
30
apps/shortener/src/shortener_bouncer_client.erl
Normal file
@ -0,0 +1,30 @@
|
||||
-module(shortener_bouncer_client).
|
||||
|
||||
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
|
||||
|
||||
%% API
|
||||
|
||||
-export([make_shortener_context_fragment/3]).
|
||||
|
||||
%%
|
||||
|
||||
-type operation_id() :: binary().
|
||||
-type id() :: shortener_slug:id().
|
||||
-type owner() :: shortener_slug:owner().
|
||||
|
||||
-spec make_shortener_context_fragment(operation_id(), id() | undefined, owner() | undefined) ->
|
||||
bouncer_client:context_fragment().
|
||||
make_shortener_context_fragment(OperationID, ID, OwnerID) ->
|
||||
{fragment, #bctx_v1_ContextFragment{
|
||||
shortener = #bctx_v1_ContextUrlShortener{
|
||||
op = #bctx_v1_UrlShortenerOperation{
|
||||
id = OperationID,
|
||||
shortened_url = #bctx_v1_ShortenedUrl{
|
||||
id = ID,
|
||||
owner = #bctx_v1_Entity{
|
||||
id = OwnerID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}.
|
@ -40,7 +40,7 @@ handle_request(OperationID, Req, Context, _Opts) ->
|
||||
AuthContext = get_auth_context(Context),
|
||||
WoodyCtx = create_woody_ctx(Req, AuthContext),
|
||||
Slug = prefetch_slug(Req, WoodyCtx),
|
||||
case shortener_auth:authorize_operation(OperationID, Slug, AuthContext) of
|
||||
case shortener_auth:authorize_operation(OperationID, Slug, Context, WoodyCtx) of
|
||||
ok ->
|
||||
SubjectID = get_subject_id(AuthContext),
|
||||
process_request(OperationID, Req, Slug, SubjectID, WoodyCtx);
|
||||
@ -96,7 +96,7 @@ collect_user_identity(AuthContext) ->
|
||||
username => get_claim(<<"name">>, AuthContext, undefined)
|
||||
}).
|
||||
|
||||
get_subject_id({{SubjectID, _ACL}, _}) ->
|
||||
get_subject_id({{SubjectID, _ACL, _ExpiresAt}, _}) ->
|
||||
SubjectID.
|
||||
|
||||
get_claim(ClaimName, {_Subject, Claims}, Default) ->
|
||||
|
@ -35,6 +35,7 @@
|
||||
}.
|
||||
|
||||
-export_type([slug/0]).
|
||||
-export_type([id/0]).
|
||||
-export_type([owner/0]).
|
||||
|
||||
-type ctx() :: woody_context:ctx().
|
||||
|
360
apps/shortener/test/shortener_auth_SUITE.erl
Normal file
360
apps/shortener/test/shortener_auth_SUITE.erl
Normal file
@ -0,0 +1,360 @@
|
||||
-module(shortener_auth_SUITE).
|
||||
|
||||
-include_lib("bouncer_proto/include/bouncer_decisions_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
-export([groups/0]).
|
||||
-export([init_per_suite/1]).
|
||||
-export([end_per_suite/1]).
|
||||
-export([init_per_group/2]).
|
||||
-export([end_per_group/2]).
|
||||
-export([init_per_testcase/2]).
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([failed_authorization/1]).
|
||||
-export([insufficient_permissions/1]).
|
||||
-export([readonly_permissions/1]).
|
||||
-export([other_subject_delete/1]).
|
||||
-export([other_subject_read/1]).
|
||||
|
||||
%% tests descriptions
|
||||
|
||||
-type config() :: [{atom(), term()}].
|
||||
-type test_case_name() :: atom().
|
||||
|
||||
-define(config(Key, C), (element(2, lists:keyfind(Key, 1, C)))).
|
||||
|
||||
-spec all() -> [test_case_name()].
|
||||
all() ->
|
||||
[
|
||||
{group, general}
|
||||
].
|
||||
|
||||
-spec groups() -> [{atom(), list(), [test_case_name()]}].
|
||||
groups() ->
|
||||
[
|
||||
{general, [], [
|
||||
failed_authorization,
|
||||
insufficient_permissions,
|
||||
readonly_permissions,
|
||||
other_subject_delete,
|
||||
other_subject_read
|
||||
]}
|
||||
].
|
||||
|
||||
-spec init_per_suite(config()) -> config().
|
||||
init_per_suite(C) ->
|
||||
% _ = dbg:tracer(),
|
||||
% _ = dbg:p(all, c),
|
||||
% _ = dbg:tpl({shortener_swagger_server, '_', '_'}, x),
|
||||
Host = "url-shortener",
|
||||
Port = 8080,
|
||||
Netloc = Host ++ ":" ++ genlib:to_list(Port),
|
||||
Apps =
|
||||
genlib_app:start_application_with(scoper, [
|
||||
{storage, scoper_storage_logger}
|
||||
]) ++
|
||||
genlib_app:start_application_with(bouncer_client, shortener_ct_helper:get_bouncer_client_app_config()),
|
||||
[
|
||||
{suite_apps, Apps},
|
||||
{api_endpoint, "http://" ++ Netloc},
|
||||
{host, Host},
|
||||
{port, Port},
|
||||
{netloc, Netloc}
|
||||
] ++ C.
|
||||
|
||||
-spec init_per_group(atom(), config()) -> config().
|
||||
init_per_group(_Group, C) ->
|
||||
ShortenerApp =
|
||||
genlib_app:start_application_with(
|
||||
shortener,
|
||||
shortener_ct_helper:get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C)
|
||||
)
|
||||
),
|
||||
[
|
||||
{shortener_app, ShortenerApp}
|
||||
] ++ C.
|
||||
|
||||
-spec end_per_group(atom(), config()) -> _.
|
||||
end_per_group(_Group, C) ->
|
||||
genlib_app:stop_unload_applications(?config(shortener_app, C)).
|
||||
|
||||
get_keysource(Key, C) ->
|
||||
filename:join(?config(data_dir, C), Key).
|
||||
|
||||
-spec end_per_suite(config()) -> term().
|
||||
end_per_suite(C) ->
|
||||
genlib_app:stop_unload_applications(?config(suite_apps, C)).
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
init_per_testcase(_Name, C) ->
|
||||
shortener_ct_helper:with_test_sup(C).
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
end_per_testcase(_Name, C) ->
|
||||
shortener_ct_helper:stop_test_sup(C),
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec failed_authorization(config()) -> _.
|
||||
-spec insufficient_permissions(config()) -> _.
|
||||
-spec readonly_permissions(config()) -> _.
|
||||
-spec other_subject_delete(config()) -> _.
|
||||
-spec other_subject_read(config()) -> _.
|
||||
|
||||
failed_authorization(C) ->
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
C1 = clean_api_auth_token(C),
|
||||
{ok, 401, _, _} = shorten_url(Params, C1),
|
||||
{ok, 401, _, _} = delete_shortened_url(<<"42">>, C1),
|
||||
{ok, 401, _, _} = get_shortened_url(<<"42">>, C1).
|
||||
|
||||
insufficient_permissions(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = forbidden}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(insufficient_permissions, C),
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
{ok, 403, _, _} = shorten_url(Params, C1),
|
||||
{ok, 403, _, _} = delete_shortened_url(<<"42">>, C1),
|
||||
{ok, 403, _, _} = get_shortened_url(<<"42">>, C1).
|
||||
|
||||
readonly_permissions(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_operation_id(Fragments) of
|
||||
<<"ShortenUrl">> ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
<<"GetShortenedUrl">> ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
<<"DeleteShortenedUrl">> ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(readonly_permissions, C),
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
{ok, 201, _, #{<<"id">> := ID}} = shorten_url(Params, C1),
|
||||
{ok, 200, _, #{<<"id">> := ID}} = get_shortened_url(ID, C1),
|
||||
{ok, 403, _, _} = delete_shortened_url(ID, C1).
|
||||
|
||||
other_subject_delete(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_operation_id(Fragments) of
|
||||
<<"ShortenUrl">> ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
<<"GetShortenedUrl">> ->
|
||||
case get_owner_info(Fragments) of
|
||||
{ID, ID} ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end;
|
||||
<<"DeleteShortenedUrl">> ->
|
||||
case get_owner_info(Fragments) of
|
||||
{ID, ID} ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(other_subject_delete_first, C),
|
||||
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
C2 = set_api_auth_token(other_subject_delete_second, C1),
|
||||
{ok, 403, _, _} = delete_shortened_url(ID, C2),
|
||||
{ok, 301, Headers, _} = hackney:request(get, ShortUrl),
|
||||
{<<"location">>, SourceUrl} = lists:keyfind(<<"location">>, 1, Headers).
|
||||
|
||||
other_subject_read(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_operation_id(Fragments) of
|
||||
<<"ShortenUrl">> ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
<<"GetShortenedUrl">> ->
|
||||
case get_owner_info(Fragments) of
|
||||
{ID, ID} ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end;
|
||||
<<"DeleteShortenedUrl">> ->
|
||||
case get_owner_info(Fragments) of
|
||||
{ID, ID} ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
C1 = set_api_auth_token(other_subject_read_first, C),
|
||||
{ok, 201, _, #{<<"id">> := ID}} = shorten_url(Params, C1),
|
||||
C2 = set_api_auth_token(other_subject_read_second, C1),
|
||||
{ok, 403, _, _} = get_shortened_url(ID, C2).
|
||||
|
||||
%%
|
||||
|
||||
construct_params(SourceUrl) ->
|
||||
construct_params(SourceUrl, 3600).
|
||||
|
||||
construct_params(SourceUrl, Lifetime) ->
|
||||
#{
|
||||
<<"sourceUrl">> => SourceUrl,
|
||||
<<"expiresAt">> => format_ts(genlib_time:unow() + Lifetime)
|
||||
}.
|
||||
|
||||
set_api_auth_token(Name, C) ->
|
||||
UserID = genlib:to_binary(Name),
|
||||
ACL = construct_shortener_acl([]),
|
||||
{ok, T} = shortener_authorizer_jwt:issue({{UserID, shortener_acl:from_list(ACL)}, #{}}, unlimited),
|
||||
lists:keystore(api_auth_token, 1, C, {api_auth_token, T}).
|
||||
|
||||
clean_api_auth_token(C) ->
|
||||
lists:keydelete(api_auth_token, 1, C).
|
||||
|
||||
construct_shortener_acl(Permissions) ->
|
||||
lists:map(fun(P) -> {['shortened-urls'], P} end, Permissions).
|
||||
|
||||
%%
|
||||
|
||||
shorten_url(ShortenedUrlParams, C) ->
|
||||
swag_client_shortener_api:shorten_url(
|
||||
?config(api_endpoint, C),
|
||||
append_common_params(#{body => ShortenedUrlParams}, C)
|
||||
).
|
||||
|
||||
delete_shortened_url(ID, C) ->
|
||||
swag_client_shortener_api:delete_shortened_url(
|
||||
?config(api_endpoint, C),
|
||||
append_common_params(#{binding => #{<<"shortenedUrlID">> => ID}}, C)
|
||||
).
|
||||
|
||||
get_shortened_url(ID, C) ->
|
||||
swag_client_shortener_api:get_shortened_url(
|
||||
?config(api_endpoint, C),
|
||||
append_common_params(#{binding => #{<<"shortenedUrlID">> => ID}}, C)
|
||||
).
|
||||
|
||||
append_common_params(Params, C) ->
|
||||
append_media_type(
|
||||
append_auth(
|
||||
append_request_id(
|
||||
maps:merge(#{binding => #{}, qs_val => #{}, header => #{}, body => #{}}, Params)
|
||||
),
|
||||
C
|
||||
)
|
||||
).
|
||||
|
||||
append_media_type(Params = #{header := Headers}) ->
|
||||
Params#{
|
||||
header => Headers#{
|
||||
<<"Accept">> => <<"application/json">>,
|
||||
<<"Content-Type">> => <<"application/json; charset=utf-8">>
|
||||
}
|
||||
}.
|
||||
|
||||
append_auth(Params = #{header := Headers}, C) ->
|
||||
case lists:keyfind(api_auth_token, 1, C) of
|
||||
{api_auth_token, AuthToken} ->
|
||||
Params#{header => Headers#{<<"Authorization">> => <<"Bearer ", AuthToken/binary>>}};
|
||||
_ ->
|
||||
Params
|
||||
end.
|
||||
|
||||
append_request_id(Params = #{header := Headers}) ->
|
||||
Params#{header => Headers#{<<"X-Request-ID">> => woody_context:new_req_id()}}.
|
||||
|
||||
format_ts(Ts) ->
|
||||
genlib_rfc3339:format(Ts, second).
|
||||
|
||||
get_operation_id(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"shortener">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{
|
||||
shortener = #bctx_v1_ContextUrlShortener{op = #bctx_v1_UrlShortenerOperation{id = OperationID}}
|
||||
} ->
|
||||
OperationID
|
||||
end.
|
||||
|
||||
get_owner_info(Context) ->
|
||||
{get_owner_id(Context), get_user_id(Context)}.
|
||||
|
||||
get_owner_id(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"shortener">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{
|
||||
shortener = #bctx_v1_ContextUrlShortener{op = #bctx_v1_UrlShortenerOperation{shortened_url = Url}}
|
||||
} ->
|
||||
#bctx_v1_ShortenedUrl{owner = #bctx_v1_Entity{id = OwnerID}} = Url,
|
||||
OwnerID
|
||||
end.
|
||||
|
||||
get_user_id(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"user">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{user = #bctx_v1_User{id = UserID}} ->
|
||||
UserID
|
||||
end.
|
||||
|
||||
decode(Content) ->
|
||||
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
|
||||
Codec = thrift_strict_binary_codec:new(Content),
|
||||
case thrift_strict_binary_codec:read(Codec, Type) of
|
||||
{ok, CtxThrift, Codec1} ->
|
||||
case thrift_strict_binary_codec:close(Codec1) of
|
||||
<<>> ->
|
||||
CtxThrift;
|
||||
Leftovers ->
|
||||
{error, {excess_binary_data, Leftovers}}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
@ -0,0 +1,9 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
|
||||
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
|
||||
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
|
||||
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
|
||||
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
|
||||
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
|
||||
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
|
||||
-----END RSA PRIVATE KEY-----
|
@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg
|
||||
7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
184
apps/shortener/test/shortener_ct_helper.erl
Normal file
184
apps/shortener/test/shortener_ct_helper.erl
Normal file
@ -0,0 +1,184 @@
|
||||
-module(shortener_ct_helper).
|
||||
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-export([with_test_sup/1]).
|
||||
-export([stop_test_sup/1]).
|
||||
-export([mock_services/2]).
|
||||
-export([get_app_config/3]).
|
||||
-export([get_app_config/4]).
|
||||
-export([get_bouncer_client_app_config/0]).
|
||||
|
||||
-type config() :: [{atom(), any()}].
|
||||
|
||||
-define(SHORTENER_IP, "::").
|
||||
-define(SHORTENER_PORT, 8080).
|
||||
-define(SHORTENER_HOST_NAME, "localhost").
|
||||
-define(SHORTENER_URL, ?SHORTENER_HOST_NAME ++ ":" ++ integer_to_list(?SHORTENER_PORT)).
|
||||
|
||||
-spec with_test_sup(config()) -> config().
|
||||
with_test_sup(C) ->
|
||||
{ok, SupPid} = genlib_adhoc_supervisor:start_link(#{}, []),
|
||||
_ = unlink(SupPid),
|
||||
[{test_sup, SupPid} | C].
|
||||
|
||||
-spec stop_test_sup(config()) -> _.
|
||||
stop_test_sup(C) ->
|
||||
exit(?config(test_sup, C), shutdown).
|
||||
|
||||
-spec mock_services(list(), config()) -> _.
|
||||
mock_services(Services, SupOrConfig) ->
|
||||
maps:map(fun set_cfg/2, mock_services_(Services, SupOrConfig)).
|
||||
|
||||
set_cfg(Service, Url) when Service =:= bouncer orelse Service =:= org_management ->
|
||||
{ok, Clients} = application:get_env(bouncer_client, service_clients),
|
||||
#{Service := Cfg} = Clients,
|
||||
ok = application:set_env(
|
||||
bouncer_client,
|
||||
service_clients,
|
||||
Clients#{Service => Cfg#{url => Url}}
|
||||
);
|
||||
set_cfg(Service, Url) ->
|
||||
{ok, Clients} = application:get_env(shortener, service_clients),
|
||||
#{Service := Cfg} = Clients,
|
||||
ok = application:set_env(
|
||||
shortener,
|
||||
service_clients,
|
||||
Clients#{Service => Cfg#{url => Url}}
|
||||
).
|
||||
|
||||
mock_services_(Services, Config) when is_list(Config) ->
|
||||
mock_services_(Services, ?config(test_sup, Config));
|
||||
mock_services_(Services, SupPid) when is_pid(SupPid) ->
|
||||
Name = lists:map(fun get_service_name/1, Services),
|
||||
|
||||
Port = get_random_port(),
|
||||
{ok, IP} = inet:parse_address(?SHORTENER_IP),
|
||||
ChildSpec = woody_server:child_spec(
|
||||
{dummy, Name},
|
||||
#{
|
||||
ip => IP,
|
||||
port => Port,
|
||||
event_handler => scoper_woody_event_handler,
|
||||
handlers => lists:map(fun mock_service_handler/1, Services)
|
||||
}
|
||||
),
|
||||
{ok, _} = supervisor:start_child(SupPid, ChildSpec),
|
||||
|
||||
lists:foldl(
|
||||
fun(Service, Acc) ->
|
||||
ServiceName = get_service_name(Service),
|
||||
Acc#{ServiceName => make_url(ServiceName, Port)}
|
||||
end,
|
||||
#{},
|
||||
Services
|
||||
).
|
||||
|
||||
get_service_name({ServiceName, _Fun}) ->
|
||||
ServiceName;
|
||||
get_service_name({ServiceName, _WoodyService, _Fun}) ->
|
||||
ServiceName.
|
||||
|
||||
mock_service_handler({ServiceName, Fun}) ->
|
||||
mock_service_handler(ServiceName, get_service_modname(ServiceName), Fun);
|
||||
mock_service_handler({ServiceName, WoodyService, Fun}) ->
|
||||
mock_service_handler(ServiceName, WoodyService, Fun).
|
||||
|
||||
mock_service_handler(ServiceName, WoodyService, Fun) ->
|
||||
{make_path(ServiceName), {WoodyService, {shortener_mock_service, #{function => Fun}}}}.
|
||||
|
||||
get_service_modname(bouncer) ->
|
||||
{bouncer_decisions_thrift, 'Arbiter'}.
|
||||
|
||||
% TODO not so failproof, ideally we need to bind socket first and then give to a ranch listener
|
||||
get_random_port() ->
|
||||
rand:uniform(32768) + 32767.
|
||||
|
||||
make_url(ServiceName, Port) ->
|
||||
iolist_to_binary(["http://", ?SHORTENER_HOST_NAME, ":", integer_to_list(Port), make_path(ServiceName)]).
|
||||
|
||||
make_path(ServiceName) ->
|
||||
"/" ++ atom_to_list(ServiceName).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_app_config(_, _, _) -> _.
|
||||
get_app_config(Port, Netloc, PemFile) ->
|
||||
get_app_config(Port, Netloc, PemFile, <<"http://machinegun:8022/v1/automaton">>).
|
||||
|
||||
-spec get_app_config(_, _, _, _) -> _.
|
||||
get_app_config(Port, Netloc, PemFile, AutomatonUrl) ->
|
||||
[
|
||||
{bouncer_ruleset_id, <<"service/authz/api">>},
|
||||
{space_size, 8},
|
||||
{hash_algorithm, sha256},
|
||||
{api, #{
|
||||
ip => "::",
|
||||
port => Port,
|
||||
authorizer => #{
|
||||
signee => local,
|
||||
keyset => #{
|
||||
local => {pem_file, PemFile}
|
||||
}
|
||||
},
|
||||
source_url_whitelist => [
|
||||
"https://*",
|
||||
"ftp://*",
|
||||
"http://localhost/*"
|
||||
],
|
||||
short_url_template => #{
|
||||
scheme => http,
|
||||
netloc => Netloc,
|
||||
path => "/r/e/d/i/r/"
|
||||
}
|
||||
}},
|
||||
{processor, #{
|
||||
ip => "::",
|
||||
port => 8022
|
||||
}},
|
||||
{health_check, #{
|
||||
service => {erl_health, service, [<<"shortener">>]}
|
||||
}},
|
||||
{service_clients, #{
|
||||
automaton => #{
|
||||
url => AutomatonUrl,
|
||||
retries => #{
|
||||
% function => retry strategy
|
||||
% '_' work as "any"
|
||||
% default value is 'finish'
|
||||
% for more info look genlib_retry :: strategy()
|
||||
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
|
||||
'Start' => {linear, 3, 1000},
|
||||
'GetMachine' => {linear, 3, 1000},
|
||||
'Remove' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
}
|
||||
}}
|
||||
].
|
||||
|
||||
-spec get_bouncer_client_app_config() -> _.
|
||||
get_bouncer_client_app_config() ->
|
||||
[
|
||||
{service_clients, #{
|
||||
bouncer => #{
|
||||
url => <<"http://bouncer:8022/">>,
|
||||
retries => #{
|
||||
'Judge' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
},
|
||||
org_management => #{
|
||||
url => <<"http://org_management:8022/">>,
|
||||
retries => #{
|
||||
% function => retry strategy
|
||||
% '_' work as "any"
|
||||
% default value is 'finish'
|
||||
% for more info look genlib_retry :: strategy()
|
||||
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
|
||||
'GetUserContext' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
}
|
||||
}}
|
||||
].
|
@ -1,5 +1,8 @@
|
||||
-module(shortener_general_SUITE).
|
||||
|
||||
-include_lib("bouncer_proto/include/bouncer_decisions_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
-export([groups/0]).
|
||||
-export([init_per_suite/1]).
|
||||
@ -9,13 +12,8 @@
|
||||
-export([init_per_testcase/2]).
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([failed_authorization/1]).
|
||||
-export([insufficient_permissions/1]).
|
||||
-export([readonly_permissions/1]).
|
||||
-export([successful_redirect/1]).
|
||||
-export([successful_delete/1]).
|
||||
-export([other_subject_delete/1]).
|
||||
-export([other_subject_read/1]).
|
||||
-export([fordidden_source_url/1]).
|
||||
-export([url_expired/1]).
|
||||
-export([always_unique_url/1]).
|
||||
@ -49,14 +47,8 @@ all() ->
|
||||
groups() ->
|
||||
[
|
||||
{general, [], [
|
||||
failed_authorization,
|
||||
insufficient_permissions,
|
||||
readonly_permissions,
|
||||
|
||||
successful_redirect,
|
||||
successful_delete,
|
||||
other_subject_delete,
|
||||
other_subject_read,
|
||||
fordidden_source_url,
|
||||
url_expired,
|
||||
always_unique_url
|
||||
@ -80,7 +72,8 @@ init_per_suite(C) ->
|
||||
Apps =
|
||||
genlib_app:start_application_with(scoper, [
|
||||
{storage, scoper_storage_logger}
|
||||
]),
|
||||
]) ++
|
||||
genlib_app:start_application_with(bouncer_client, shortener_ct_helper:get_bouncer_client_app_config()),
|
||||
[
|
||||
{suite_apps, Apps},
|
||||
{api_endpoint, "http://" ++ Netloc},
|
||||
@ -94,7 +87,7 @@ init_per_group(_Group, C) ->
|
||||
ShortenerApp =
|
||||
genlib_app:start_application_with(
|
||||
shortener,
|
||||
get_app_config(
|
||||
shortener_ct_helper:get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C)
|
||||
@ -117,50 +110,29 @@ end_per_suite(C) ->
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
init_per_testcase(_Name, C) ->
|
||||
C.
|
||||
shortener_ct_helper:with_test_sup(C).
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
end_per_testcase(_Name, _C) ->
|
||||
end_per_testcase(_Name, C) ->
|
||||
shortener_ct_helper:stop_test_sup(C),
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec failed_authorization(config()) -> _.
|
||||
-spec insufficient_permissions(config()) -> _.
|
||||
-spec readonly_permissions(config()) -> _.
|
||||
|
||||
-spec successful_redirect(config()) -> _.
|
||||
-spec successful_delete(config()) -> _.
|
||||
-spec other_subject_delete(config()) -> _.
|
||||
-spec other_subject_read(config()) -> _.
|
||||
-spec fordidden_source_url(config()) -> _.
|
||||
-spec url_expired(config()) -> _.
|
||||
-spec always_unique_url(config()) -> _.
|
||||
|
||||
failed_authorization(C) ->
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
C1 = clean_api_auth_token(C),
|
||||
{ok, 401, _, _} = shorten_url(Params, C1),
|
||||
{ok, 401, _, _} = delete_shortened_url(<<"42">>, C1),
|
||||
{ok, 401, _, _} = get_shortened_url(<<"42">>, C1).
|
||||
|
||||
insufficient_permissions(C) ->
|
||||
C1 = set_api_auth_token(insufficient_permissions, [], C),
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
{ok, 403, _, _} = shorten_url(Params, C1),
|
||||
{ok, 403, _, _} = delete_shortened_url(<<"42">>, C1),
|
||||
{ok, 403, _, _} = get_shortened_url(<<"42">>, C1).
|
||||
|
||||
readonly_permissions(C) ->
|
||||
C1 = set_api_auth_token(readonly_permissions, [read, write], C),
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
{ok, 201, _, #{<<"id">> := ID}} = shorten_url(Params, C1),
|
||||
C2 = set_api_auth_token(readonly_permissions, [read], C1),
|
||||
{ok, 200, _, #{<<"id">> := ID}} = get_shortened_url(ID, C2),
|
||||
{ok, 403, _, _} = delete_shortened_url(ID, C2).
|
||||
|
||||
successful_redirect(C) ->
|
||||
C1 = set_api_auth_token(successful_redirect, [read, write], C),
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(successful_redirect, C),
|
||||
SourceUrl = <<"https://example.com/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
@ -169,32 +141,27 @@ successful_redirect(C) ->
|
||||
{<<"location">>, SourceUrl} = lists:keyfind(<<"location">>, 1, Headers).
|
||||
|
||||
successful_delete(C) ->
|
||||
C1 = set_api_auth_token(successful_delete, [read, write], C),
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(successful_delete, C),
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
{ok, 204, _, _} = delete_shortened_url(ID, C1),
|
||||
{ok, 404, _, _} = get_shortened_url(ID, C1),
|
||||
{ok, 404, _, _} = hackney:request(get, ShortUrl).
|
||||
|
||||
other_subject_delete(C) ->
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(other_subject_delete_first, [read, write], C),
|
||||
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
C2 = set_api_auth_token(other_subject_delete_second, [read, write], C1),
|
||||
{ok, 403, _, _} = delete_shortened_url(ID, C2),
|
||||
{ok, 301, Headers, _} = hackney:request(get, ShortUrl),
|
||||
{<<"location">>, SourceUrl} = lists:keyfind(<<"location">>, 1, Headers).
|
||||
|
||||
other_subject_read(C) ->
|
||||
Params = construct_params(<<"https://oops.io/">>),
|
||||
C1 = set_api_auth_token(other_subject_read_first, [read, write], C),
|
||||
{ok, 201, _, #{<<"id">> := ID}} = shorten_url(Params, C1),
|
||||
C2 = set_api_auth_token(other_subject_read_second, [read, write], C1),
|
||||
{ok, 403, _, _} = get_shortened_url(ID, C2).
|
||||
|
||||
fordidden_source_url(C) ->
|
||||
C1 = set_api_auth_token(fordidden_source_url, [read, write], C),
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(fordidden_source_url, C),
|
||||
{ok, 201, _, #{}} = shorten_url(construct_params(<<"http://localhost/hack?id=42">>), C1),
|
||||
{ok, 201, _, #{}} = shorten_url(construct_params(<<"https://localhost/hack?id=42">>), C1),
|
||||
{ok, 400, _, #{}} = shorten_url(construct_params(<<"http://example.io/">>), C1),
|
||||
@ -202,7 +169,13 @@ fordidden_source_url(C) ->
|
||||
{ok, 201, _, #{}} = shorten_url(construct_params(<<"ftp://ftp.hp.com/pub/hpcp/newsletter_july2003">>), C1).
|
||||
|
||||
url_expired(C) ->
|
||||
C1 = set_api_auth_token(url_expired, [read, write], C),
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(url_expired, C),
|
||||
Params = construct_params(<<"https://oops.io/">>, 1),
|
||||
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
{ok, 200, _, #{<<"shortenedUrl">> := ShortUrl}} = get_shortened_url(ID, C1),
|
||||
@ -211,7 +184,13 @@ url_expired(C) ->
|
||||
{ok, 404, _, _} = hackney:request(get, ShortUrl).
|
||||
|
||||
always_unique_url(C) ->
|
||||
C1 = set_api_auth_token(always_unique_url, [read, write], C),
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C1 = set_api_auth_token(always_unique_url, C),
|
||||
N = 42,
|
||||
Params = construct_params(<<"https://oops.io/">>, 3600),
|
||||
{IDs, ShortUrls} = lists:unzip([
|
||||
@ -229,18 +208,30 @@ always_unique_url(C) ->
|
||||
-spec supported_cors_header(config()) -> _.
|
||||
|
||||
unsupported_cors_method(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(unsupported_cors_method, [read, write], C),
|
||||
C1 = set_api_auth_token(unsupported_cors_method, C),
|
||||
{ok, 201, _, #{<<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
ReqHeaders = [{<<"origin">>, <<"localhost">>}, {<<"access-control-request-method">>, <<"PATCH">>}],
|
||||
{ok, 200, Headers, _} = hackney:request(options, ShortUrl, ReqHeaders),
|
||||
false = lists:member(<<"access-control-allow-methods">>, Headers).
|
||||
|
||||
supported_cors_method(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(supported_cors_method, [read, write], C),
|
||||
C1 = set_api_auth_token(supported_cors_method, C),
|
||||
{ok, 201, _, #{<<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
ReqHeaders = [{<<"origin">>, <<"localhost">>}, {<<"access-control-request-method">>, <<"GET">>}],
|
||||
{ok, 200, Headers, _} = hackney:request(options, ShortUrl, ReqHeaders),
|
||||
@ -249,9 +240,15 @@ supported_cors_method(C) ->
|
||||
Allowed = binary:split(Returned, <<",">>, [global]).
|
||||
|
||||
supported_cors_header(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(supported_cors_header, [read, write], C),
|
||||
C1 = set_api_auth_token(supported_cors_header, C),
|
||||
{ok, 201, _, #{<<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
ReqHeaders = [
|
||||
{<<"origin">>, <<"localhost">>},
|
||||
@ -265,9 +262,15 @@ supported_cors_header(C) ->
|
||||
[_ | Allowed] = binary:split(Returned, <<",">>, [global]).
|
||||
|
||||
unsupported_cors_header(C) ->
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(unsupported_cors_header, [read, write], C),
|
||||
C1 = set_api_auth_token(unsupported_cors_header, C),
|
||||
{ok, 201, _, #{<<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
ReqHeaders = [
|
||||
{<<"origin">>, <<"localhost">>},
|
||||
@ -294,14 +297,20 @@ construct_params(SourceUrl, Lifetime) ->
|
||||
woody_timeout_test(C) ->
|
||||
Apps = genlib_app:start_application_with(
|
||||
shortener,
|
||||
get_app_config(
|
||||
shortener_ct_helper:get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C),
|
||||
<<"http://invalid_url:8022/v1/automaton">>
|
||||
)
|
||||
),
|
||||
C2 = set_api_auth_token(woody_timeout_test, [read, write], C),
|
||||
shortener_ct_helper:mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
C2 = set_api_auth_token(woody_timeout_test, C),
|
||||
SourceUrl = <<"https://example.com/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
{Time, {error, {invalid_response_code, 503}}} =
|
||||
@ -316,7 +325,7 @@ woody_timeout_test(C) ->
|
||||
health_check_passing(C) ->
|
||||
Apps = genlib_app:start_application_with(
|
||||
shortener,
|
||||
get_app_config(
|
||||
shortener_ct_helper:get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C)
|
||||
@ -328,15 +337,12 @@ health_check_passing(C) ->
|
||||
genlib_app:stop_unload_applications(Apps).
|
||||
|
||||
%%
|
||||
set_api_auth_token(Name, Permissions, C) ->
|
||||
set_api_auth_token(Name, C) ->
|
||||
UserID = genlib:to_binary(Name),
|
||||
ACL = construct_shortener_acl(Permissions),
|
||||
ACL = construct_shortener_acl([]),
|
||||
{ok, T} = shortener_authorizer_jwt:issue({{UserID, shortener_acl:from_list(ACL)}, #{}}, unlimited),
|
||||
lists:keystore(api_auth_token, 1, C, {api_auth_token, T}).
|
||||
|
||||
clean_api_auth_token(C) ->
|
||||
lists:keydelete(api_auth_token, 1, C).
|
||||
|
||||
construct_shortener_acl(Permissions) ->
|
||||
lists:map(fun(P) -> {['shortened-urls'], P} end, Permissions).
|
||||
|
||||
@ -391,57 +397,3 @@ append_request_id(Params = #{header := Headers}) ->
|
||||
|
||||
format_ts(Ts) ->
|
||||
genlib_rfc3339:format(Ts, second).
|
||||
|
||||
%%
|
||||
|
||||
get_app_config(Port, Netloc, PemFile) ->
|
||||
get_app_config(Port, Netloc, PemFile, <<"http://machinegun:8022/v1/automaton">>).
|
||||
|
||||
get_app_config(Port, Netloc, PemFile, AutomatonUrl) ->
|
||||
[
|
||||
{space_size, 8},
|
||||
{hash_algorithm, sha256},
|
||||
{api, #{
|
||||
ip => "::",
|
||||
port => Port,
|
||||
authorizer => #{
|
||||
signee => local,
|
||||
keyset => #{
|
||||
local => {pem_file, PemFile}
|
||||
}
|
||||
},
|
||||
source_url_whitelist => [
|
||||
"https://*",
|
||||
"ftp://*",
|
||||
"http://localhost/*"
|
||||
],
|
||||
short_url_template => #{
|
||||
scheme => http,
|
||||
netloc => Netloc,
|
||||
path => "/r/e/d/i/r/"
|
||||
}
|
||||
}},
|
||||
{processor, #{
|
||||
ip => "::",
|
||||
port => 8022
|
||||
}},
|
||||
{health_check, #{
|
||||
service => {erl_health, service, [<<"shortener">>]}
|
||||
}},
|
||||
{service_clients, #{
|
||||
automaton => #{
|
||||
url => AutomatonUrl,
|
||||
retries => #{
|
||||
% function => retry strategy
|
||||
% '_' work as "any"
|
||||
% default value is 'finish'
|
||||
% for more info look genlib_retry :: strategy()
|
||||
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
|
||||
'Start' => {linear, 3, 1000},
|
||||
'GetMachine' => {linear, 3, 1000},
|
||||
'Remove' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
}
|
||||
}}
|
||||
].
|
||||
|
9
apps/shortener/test/shortener_mock_service.erl
Normal file
9
apps/shortener/test/shortener_mock_service.erl
Normal file
@ -0,0 +1,9 @@
|
||||
-module(shortener_mock_service).
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
|
||||
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), #{}) -> {ok, term()}.
|
||||
handle_function(FunName, Args, _, #{function := Fun}) ->
|
||||
Fun(FunName, Args).
|
@ -4,6 +4,7 @@
|
||||
]},
|
||||
|
||||
{shortener, [
|
||||
{bouncer_ruleset_id , <<"service/authz/api">>},
|
||||
{space_size , 8},
|
||||
{hash_algorithm , sha256},
|
||||
{api, #{
|
||||
@ -42,6 +43,18 @@
|
||||
'Remove' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
},
|
||||
bouncer => #{
|
||||
url => <<"http://bouncer:8022/">>,
|
||||
retries => #{
|
||||
% function => retry strategy
|
||||
% '_' work as "any"
|
||||
% default value is 'finish'
|
||||
% for more info look genlib_retry :: strategy()
|
||||
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
|
||||
'Judge' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
}
|
||||
}},
|
||||
{health_checkers, [
|
||||
|
10
rebar.config
10
rebar.config
@ -67,6 +67,16 @@
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{bouncer_proto,
|
||||
{git, "https://github.com/rbkmoney/bouncer-proto.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{bouncer_client,
|
||||
{git, "https://github.com/rbkmoney/bouncer_client_erlang.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{erl_health,
|
||||
{git, "https://github.com/rbkmoney/erlang-health.git",
|
||||
{branch, "master"}
|
||||
|
12
rebar.lock
12
rebar.lock
@ -2,6 +2,14 @@
|
||||
[{<<"accept">>,{pkg,<<"accept">>,<<"0.3.5">>},2},
|
||||
{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},1},
|
||||
{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},3},
|
||||
{<<"bouncer_client">>,
|
||||
{git,"git@github.com:rbkmoney/bouncer_client_erlang.git",
|
||||
{ref,"d053efc9a3d9cbf97b2ca9c7a7302b4dd6097d18"}},
|
||||
0},
|
||||
{<<"bouncer_proto">>,
|
||||
{git,"git@github.com:rbkmoney/bouncer-proto.git",
|
||||
{ref,"298356b934e097393593785560c04bfa152ea0b5"}},
|
||||
0},
|
||||
{<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1},
|
||||
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.3.1">>},1},
|
||||
{<<"cg_mon">>,
|
||||
@ -63,6 +71,10 @@
|
||||
{ref,"d814d6948d4ff13f6f41d12c6613f59c805750b2"}},
|
||||
0},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},1},
|
||||
{<<"org_management_proto">>,
|
||||
{git,"git@github.com:rbkmoney/org-management-proto.git",
|
||||
{ref,"06c5c8430e445cb7874e54358e457cbb5697fc32"}},
|
||||
1},
|
||||
{<<"parse_trans">>,
|
||||
{git,"https://github.com/rbkmoney/parse_trans.git",
|
||||
{ref,"5ee45f5bfa6c04329bea3281977b774f04c89f11"}},
|
||||
|
Loading…
Reference in New Issue
Block a user