mirror of
https://github.com/valitydev/bouncer.git
synced 2024-11-06 02:15:18 +00:00
Futureproof policy contract (#3)
In line w/ rbkmoney/bouncer-policies#1. * Fix stop order in tests for robustness * Add another testcase for invalid ruleset * Enforce draft-04 JSON schema validation
This commit is contained in:
parent
9004e3fe50
commit
902f06949a
@ -8,15 +8,15 @@
|
||||
%% This must be a path to some document with the subdocument of the following form:
|
||||
%% ```
|
||||
%% "assertions": {
|
||||
%% "allowed": [["code", "description"], ...] // 0 or more, may be unset
|
||||
%% "forbidden": [["code", "description"], ...] // 0 or more, may be unset
|
||||
%% "allowed": [{"code": "...", ...}, ...] // 0 or more, may be unset
|
||||
%% "forbidden": [{"code": "...", ...}, ...] // 0 or more, may be unset
|
||||
%% }
|
||||
%% ```
|
||||
-type ruleset_id() :: iodata().
|
||||
|
||||
-type judgement() :: {resolution(), [assertion()]}.
|
||||
-type resolution() :: allowed | forbidden.
|
||||
-type assertion() :: {_Code :: binary(), _Description :: binary() | undefined}.
|
||||
-type assertion() :: {_Code :: binary(), _Details :: #{binary() => _}}.
|
||||
|
||||
-export_type([judgement/0]).
|
||||
-export_type([resolution/0]).
|
||||
@ -70,30 +70,29 @@ infer_judgement(_Forbidden = [], _Allowed = []) ->
|
||||
extract_assertions(Assertions) ->
|
||||
[extract_assertion(E) || E <- Assertions].
|
||||
|
||||
extract_assertion([Code, Description]) ->
|
||||
{Code, Description};
|
||||
extract_assertion(Code) when is_binary(Code) ->
|
||||
{Code, undefined}.
|
||||
extract_assertion(Assertion = #{<<"code">> := Code}) ->
|
||||
{Code, maps:without([<<"code">>], Assertion)}.
|
||||
|
||||
-spec get_judgement_schema() ->
|
||||
jesse:schema().
|
||||
get_judgement_schema() ->
|
||||
% TODO
|
||||
% Worth declaring in a separate file? Should be helpful w/ CI-like activities.
|
||||
CodeSchema = [{<<"type">>, <<"string">>}, {<<"minLength">>, 1}],
|
||||
AssertionsSchema = [
|
||||
{<<"type">>, <<"array">>},
|
||||
{<<"items">>, [{<<"oneOf">>, [
|
||||
CodeSchema,
|
||||
[
|
||||
{<<"type">>, <<"array">>},
|
||||
{<<"items">>, CodeSchema},
|
||||
{<<"minItems">>, 1},
|
||||
{<<"maxItems">>, 2}
|
||||
]
|
||||
]}]}
|
||||
{<<"items">>, [
|
||||
{<<"type">>, <<"object">>},
|
||||
{<<"required">>, [<<"code">>]},
|
||||
{<<"properties">>, [
|
||||
{<<"code">>, [
|
||||
{<<"type">>, <<"string">>},
|
||||
{<<"minLength">>, 1}
|
||||
]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
[
|
||||
{<<"$schema">>, <<"http://json-schema.org/draft-04/schema#">>},
|
||||
{<<"type">>, <<"object">>},
|
||||
{<<"properties">>, [
|
||||
{<<"allowed">>, AssertionsSchema},
|
||||
|
@ -13,7 +13,9 @@
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([missing_ruleset_notfound/1]).
|
||||
-export([incorrect_ruleset_invalid/1]).
|
||||
-export([incorrect_ruleset_invalid1/1]).
|
||||
-export([incorrect_ruleset_invalid2/1]).
|
||||
-export([incorrect_ruleset_invalid3/1]).
|
||||
-export([missing_content_invalid_context/1]).
|
||||
-export([junk_content_invalid_context/1]).
|
||||
-export([conflicting_context_invalid/1]).
|
||||
@ -61,7 +63,9 @@ groups() ->
|
||||
[
|
||||
{general, [parallel], [
|
||||
missing_ruleset_notfound,
|
||||
incorrect_ruleset_invalid,
|
||||
incorrect_ruleset_invalid1,
|
||||
incorrect_ruleset_invalid2,
|
||||
incorrect_ruleset_invalid3,
|
||||
missing_content_invalid_context,
|
||||
junk_content_invalid_context,
|
||||
conflicting_context_invalid,
|
||||
@ -143,8 +147,8 @@ end_per_group(_Name, C) ->
|
||||
stop_bouncer(C).
|
||||
|
||||
stop_bouncer(C) ->
|
||||
with_config(stash, C, fun (Pid) -> ?assertEqual(ok, ct_stash:destroy(Pid)) end),
|
||||
with_config(group_apps, C, fun (Apps) -> genlib_app:stop_unload_applications(Apps) end).
|
||||
with_config(group_apps, C, fun (Apps) -> genlib_app:stop_unload_applications(Apps) end),
|
||||
with_config(stash, C, fun (Pid) -> ?assertEqual(ok, ct_stash:destroy(Pid)) end).
|
||||
|
||||
-spec init_per_testcase(atom(), config()) ->
|
||||
config().
|
||||
@ -164,7 +168,9 @@ end_per_testcase(_Name, _C) ->
|
||||
-define(JUDGEMENT(Resolution), #bdcs_Judgement{resolution = Resolution}).
|
||||
|
||||
-spec missing_ruleset_notfound(config()) -> ok.
|
||||
-spec incorrect_ruleset_invalid(config()) -> ok.
|
||||
-spec incorrect_ruleset_invalid1(config()) -> ok.
|
||||
-spec incorrect_ruleset_invalid2(config()) -> ok.
|
||||
-spec incorrect_ruleset_invalid3(config()) -> ok.
|
||||
-spec missing_content_invalid_context(config()) -> ok.
|
||||
-spec junk_content_invalid_context(config()) -> ok.
|
||||
-spec conflicting_context_invalid(config()) -> ok.
|
||||
@ -182,28 +188,43 @@ missing_ruleset_notfound(C) ->
|
||||
lists:last(flush_beats(Client, C))
|
||||
).
|
||||
|
||||
incorrect_ruleset_invalid(C) ->
|
||||
Client1 = mk_client(C),
|
||||
incorrect_ruleset_invalid1(C) ->
|
||||
Client = mk_client(C),
|
||||
?assertThrow(
|
||||
#bdcs_InvalidRuleset{},
|
||||
call_judge("trivial/incorrect1", ?CONTEXT(#{}), Client1)
|
||||
call_judge("trivial/incorrect1", ?CONTEXT(#{}), Client)
|
||||
),
|
||||
?assertMatch(
|
||||
{judgement, {failed, {ruleset_invalid, [
|
||||
{data_invalid, _, no_extra_properties_allowed, _, [<<"fordibben">>]}
|
||||
]}}},
|
||||
lists:last(flush_beats(Client1, C))
|
||||
),
|
||||
Client2 = mk_client(C),
|
||||
lists:last(flush_beats(Client, C))
|
||||
).
|
||||
|
||||
incorrect_ruleset_invalid2(C) ->
|
||||
Client = mk_client(C),
|
||||
?assertThrow(
|
||||
#bdcs_InvalidRuleset{},
|
||||
call_judge("trivial/incorrect2", ?CONTEXT(#{}), Client2)
|
||||
call_judge("trivial/incorrect2", ?CONTEXT(#{}), Client)
|
||||
),
|
||||
?assertMatch(
|
||||
{judgement, {failed, {ruleset_invalid, [
|
||||
{data_invalid, _, wrong_type, _, [<<"allowed">>]}
|
||||
]}}},
|
||||
lists:last(flush_beats(Client2, C))
|
||||
lists:last(flush_beats(Client, C))
|
||||
).
|
||||
|
||||
incorrect_ruleset_invalid3(C) ->
|
||||
Client = mk_client(C),
|
||||
?assertThrow(
|
||||
#bdcs_InvalidRuleset{},
|
||||
call_judge("trivial/incorrect3", ?CONTEXT(#{}), Client)
|
||||
),
|
||||
?assertMatch(
|
||||
{judgement, {failed, {ruleset_invalid, [
|
||||
{data_invalid, _, missing_required_property, <<"code">>, _}
|
||||
]}}},
|
||||
lists:last(flush_beats(Client, C))
|
||||
).
|
||||
|
||||
missing_content_invalid_context(C) ->
|
||||
|
@ -9,27 +9,27 @@ assertions := {
|
||||
|
||||
# Set of assertions which tell why operation under the input context is forbidden.
|
||||
# When the set is empty operation is not explicitly forbidden.
|
||||
# Each element must be a 2-item array of the following form:
|
||||
# Each element must be an object of the following form:
|
||||
# ```
|
||||
# ["code", "description"]
|
||||
# {"code": "...", ...}
|
||||
# ```
|
||||
forbidden[why] {
|
||||
input
|
||||
not input.auth.method
|
||||
why := [
|
||||
"auth_required",
|
||||
"Authorization is required"
|
||||
]
|
||||
why := {
|
||||
"code": "auth_required",
|
||||
"description": "Authorization is required"
|
||||
}
|
||||
}
|
||||
|
||||
forbidden[why] {
|
||||
exp := time.parse_rfc3339_ns(input.auth.expiration)
|
||||
now := time.parse_rfc3339_ns(input.env.now)
|
||||
now > exp
|
||||
why := [
|
||||
"auth_expired",
|
||||
sprintf("Authorization is expired at: %s", [input.auth.expiration])
|
||||
]
|
||||
why := {
|
||||
"code": "auth_expired",
|
||||
"description": sprintf("Authorization is expired at: %s", [input.auth.expiration])
|
||||
}
|
||||
}
|
||||
|
||||
forbidden[why] {
|
||||
@ -37,17 +37,17 @@ forbidden[why] {
|
||||
blacklist := blacklists["source-ip-range"]
|
||||
matches := net.cidr_contains_matches(blacklist, ip)
|
||||
ranges := [ range | matches[_][0] = i; range := blacklist[i] ]
|
||||
why := [
|
||||
"ip_range_blacklisted",
|
||||
sprintf("Requester IP address is blacklisted with ranges: %v", [concat(", ", ranges)])
|
||||
]
|
||||
why := {
|
||||
"code": "ip_range_blacklisted",
|
||||
"description": sprintf("Requester IP address is blacklisted with ranges: %v", [concat(", ", ranges)])
|
||||
}
|
||||
}
|
||||
|
||||
# Set of assertions which tell why operation under the input context is allowed.
|
||||
# When the set is empty operation is not explicitly allowed.
|
||||
# Each element must be a 2-item array of the following form:
|
||||
# Each element must be an object of the following form:
|
||||
# ```
|
||||
# ["code", "description"]
|
||||
# {"code": "...", ...}
|
||||
# ```
|
||||
allowed[why] {
|
||||
input.auth.method == "SessionToken"
|
||||
@ -58,20 +58,19 @@ allowed[why] {
|
||||
org_allowed[why] {
|
||||
org := org_by_operation
|
||||
org.owner == input.user.id
|
||||
why := [
|
||||
"user_is_owner",
|
||||
"User is the organisation owner itself"
|
||||
]
|
||||
why := {
|
||||
"code": "user_is_owner"
|
||||
}
|
||||
}
|
||||
|
||||
org_allowed[why] {
|
||||
rolename := role_by_operation[_]
|
||||
org_by_operation.roles[i].id == rolename
|
||||
scopename := scopename_by_role[i]
|
||||
why := [
|
||||
"user_has_role",
|
||||
sprintf("User has role %s in scope %v", [rolename, scopename])
|
||||
]
|
||||
why := {
|
||||
"code": "user_has_role",
|
||||
"description": sprintf("User has role %s in scope %v", [rolename, scopename])
|
||||
}
|
||||
}
|
||||
|
||||
scopename_by_role[i] = sprintf("shop:%s", [shop]) {
|
||||
|
11
test/policies/trivial/incorrect3.rego
Normal file
11
test/policies/trivial/incorrect3.rego
Normal file
@ -0,0 +1,11 @@
|
||||
package trivial.incorrect3
|
||||
|
||||
assertions := {
|
||||
"forbidden" : { why | forbidden[why] }
|
||||
}
|
||||
|
||||
forbidden[why] {
|
||||
input
|
||||
not input.auth.method
|
||||
why := {}
|
||||
}
|
Loading…
Reference in New Issue
Block a user