mirror of
https://github.com/valitydev/bouncer.git
synced 2024-11-06 02:15:18 +00:00
MSPF-629: Add decisions (#12)
* MSPF-629: Add decisions * MSPF-629: Fix compile * MSPF-629: Review fix * MSPF-629: Update bouncer_proto * MSPF-629: Fix lint and move from_thrift_struct/4 to bouncer_thrift * MSPF-629: Fix merge * MSPF-629: Remove `jsx:encode/1` for restrictions * MSPF-629: Rename decision -> judgement * MSPF-629: Change README.md * MSPF-629: Fix tests * MSPF-629: Add bundle.tar.gz to gitignore * MSPF-629: Fix test * MSPF-629: Fix last test * MSPF-629: Fix lint * MSPF-629: Simplify regex * MSPF-629: Alternative solution to atomization * MSPF-629: Review fix * MSPF-629: Review fix
This commit is contained in:
parent
2850275de0
commit
530f8e646f
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@ Dockerfile
|
|||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
rebar3.crashdump
|
rebar3.crashdump
|
||||||
/_checkouts/
|
/_checkouts/
|
||||||
|
/test/policies/bundle.tar.gz
|
@ -4,6 +4,6 @@
|
|||||||
|
|
||||||
Primary [Arbiter](https://github.com/rbkmoney/bouncer-proto/blob/97dcad6f/proto/decisions.thrift#L42) thrift service implementation.
|
Primary [Arbiter](https://github.com/rbkmoney/bouncer-proto/blob/97dcad6f/proto/decisions.thrift#L42) thrift service implementation.
|
||||||
|
|
||||||
In a nutshell this service maps incoming contexts into [OPA input documents](https://www.openpolicyagent.org/docs/latest/philosophy/#the-opa-document-model) and asks OPA to compute a set of assertions allowing or forbidding actions under given input context.
|
In a nutshell this service maps incoming contexts into [OPA input documents](https://www.openpolicyagent.org/docs/latest/philosophy/#the-opa-document-model) and asks OPA to compute a judgement allowing, restricting or forbidding actions under given input context.
|
||||||
|
|
||||||
From the service's point of view a **ruleset id** is a path to OPA document that define a subdocument named `assertions` with a rudimentary schema. See https://github.com/rbkmoney/bouncer-policies#authoring for more detailed information.
|
From the service's point of view a **ruleset id** is a path to OPA document that define a subdocument named `judgement` with a rudimentary schema. See https://github.com/rbkmoney/bouncer-policies#authoring for more detailed information.
|
||||||
|
@ -106,7 +106,7 @@
|
|||||||
rules => [
|
rules => [
|
||||||
{elvis_style, invalid_dynamic_call, #{ignore => [
|
{elvis_style, invalid_dynamic_call, #{ignore => [
|
||||||
% Uses thrift reflection through `struct_info/1`.
|
% Uses thrift reflection through `struct_info/1`.
|
||||||
bouncer_context_v1
|
bouncer_thrift
|
||||||
]}}
|
]}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
[{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},2},
|
[{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},2},
|
||||||
{<<"bouncer_proto">>,
|
{<<"bouncer_proto">>,
|
||||||
{git,"git@github.com:rbkmoney/bouncer-proto.git",
|
{git,"git@github.com:rbkmoney/bouncer-proto.git",
|
||||||
{ref,"aab9db7ecc0ff436acc81f3358ae52239559ce7b"}},
|
{ref,"7e00d96d6d9cd183dff50c07e547c99f51acee7e"}},
|
||||||
0},
|
0},
|
||||||
{<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1},
|
{<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1},
|
||||||
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},2},
|
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},2},
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
-type ruleset_id() :: iodata().
|
-type ruleset_id() :: iodata().
|
||||||
|
|
||||||
-type judgement() :: {resolution(), [assertion()]}.
|
-type judgement() :: {resolution(), [assertion()]}.
|
||||||
-type resolution() :: allowed | forbidden.
|
-type resolution() :: allowed | forbidden | {restricted, map()}.
|
||||||
-type assertion() :: {_Code :: binary(), _Details :: #{binary() => _}}.
|
-type assertion() :: {_Code :: binary(), _Details :: #{binary() => _}}.
|
||||||
|
|
||||||
-export_type([judgement/0]).
|
-export_type([judgement/0]).
|
||||||
@ -35,7 +35,7 @@
|
|||||||
judge(RulesetID, Context) ->
|
judge(RulesetID, Context) ->
|
||||||
case mk_opa_client() of
|
case mk_opa_client() of
|
||||||
{ok, Client} ->
|
{ok, Client} ->
|
||||||
Location = join_path(RulesetID, <<"/assertions">>),
|
Location = join_path(RulesetID, <<"/judgement">>),
|
||||||
case request_opa_document(Location, Context, Client) of
|
case request_opa_document(Location, Context, Client) of
|
||||||
{ok, Document} ->
|
{ok, Document} ->
|
||||||
infer_judgement(Document);
|
infer_judgement(Document);
|
||||||
@ -53,19 +53,20 @@ judge(RulesetID, Context) ->
|
|||||||
infer_judgement(Document) ->
|
infer_judgement(Document) ->
|
||||||
case jesse:validate_with_schema(get_judgement_schema(), Document) of
|
case jesse:validate_with_schema(get_judgement_schema(), Document) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
Allowed = maps:get(<<"allowed">>, Document, []),
|
{ok, parse_judgement(Document)};
|
||||||
Forbidden = maps:get(<<"forbidden">>, Document, []),
|
|
||||||
{ok, infer_judgement(Forbidden, Allowed)};
|
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, {ruleset_invalid, Reason}}
|
{error, {ruleset_invalid, Reason}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
infer_judgement(Forbidden = [_ | _], _Allowed) ->
|
parse_judgement(#{<<"resolution">> := [<<"forbidden">>, Assertions]}) ->
|
||||||
{forbidden, extract_assertions(Forbidden)};
|
{forbidden, extract_assertions(Assertions)};
|
||||||
infer_judgement(_Forbidden = [], Allowed = [_ | _]) ->
|
parse_judgement(#{<<"resolution">> := [<<"allowed">>, Assertions]}) ->
|
||||||
{allowed, extract_assertions(Allowed)};
|
{allowed, extract_assertions(Assertions)};
|
||||||
infer_judgement(_Forbidden = [], _Allowed = []) ->
|
parse_judgement(#{
|
||||||
{forbidden, []}.
|
<<"resolution">> := [<<"restricted">>, Assertions],
|
||||||
|
<<"restrictions">> := Restrictions
|
||||||
|
}) ->
|
||||||
|
{{restricted, Restrictions}, extract_assertions(Assertions)}.
|
||||||
|
|
||||||
extract_assertions(Assertions) ->
|
extract_assertions(Assertions) ->
|
||||||
[extract_assertion(E) || E <- Assertions].
|
[extract_assertion(E) || E <- Assertions].
|
||||||
@ -91,14 +92,29 @@ get_judgement_schema() ->
|
|||||||
]}
|
]}
|
||||||
]}
|
]}
|
||||||
],
|
],
|
||||||
|
ResolutionSchema = [
|
||||||
|
{<<"type">>, <<"array">>},
|
||||||
|
{<<"items">>, [
|
||||||
|
[
|
||||||
|
{<<"type">>, <<"string">>},
|
||||||
|
{<<"pattern">>, <<"allowed|forbidden|restricted">>}
|
||||||
|
],
|
||||||
|
AssertionsSchema
|
||||||
|
]},
|
||||||
|
{<<"minItems">>, 2},
|
||||||
|
{<<"additionalItems">>, false}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{<<"$schema">>, <<"http://json-schema.org/draft-04/schema#">>},
|
{<<"$schema">>, <<"http://json-schema.org/draft-04/schema#">>},
|
||||||
{<<"type">>, <<"object">>},
|
{<<"type">>, <<"object">>},
|
||||||
{<<"properties">>, [
|
{<<"properties">>, [
|
||||||
{<<"allowed">>, AssertionsSchema},
|
{<<"resolution">>, ResolutionSchema},
|
||||||
{<<"forbidden">>, AssertionsSchema}
|
{<<"restrictions">>, [
|
||||||
|
{<<"type">>, <<"object">>}
|
||||||
|
]}
|
||||||
]},
|
]},
|
||||||
{<<"additionalProperties">>, false}
|
{<<"additionalProperties">>, false},
|
||||||
|
{<<"required">>, [<<"resolution">>]}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%
|
%%
|
||||||
|
@ -81,19 +81,21 @@ handle_network_error({unknown, Reason} = Error, St) ->
|
|||||||
thrift_judgement().
|
thrift_judgement().
|
||||||
encode_judgement({Resolution, _Assertions}) ->
|
encode_judgement({Resolution, _Assertions}) ->
|
||||||
#bdcs_Judgement{
|
#bdcs_Judgement{
|
||||||
resolution_legacy = encode_resolution_legacy(Resolution),
|
|
||||||
resolution = encode_resolution(Resolution)
|
resolution = encode_resolution(Resolution)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
encode_resolution_legacy(allowed) ->
|
|
||||||
allowed;
|
|
||||||
encode_resolution_legacy(forbidden) ->
|
|
||||||
forbidden.
|
|
||||||
|
|
||||||
encode_resolution(allowed) ->
|
encode_resolution(allowed) ->
|
||||||
{allowed, #bdcs_ResolutionAllowed{}};
|
{allowed, #bdcs_ResolutionAllowed{}};
|
||||||
encode_resolution(forbidden) ->
|
encode_resolution(forbidden) ->
|
||||||
{forbidden, #bdcs_ResolutionForbidden{}}.
|
{forbidden, #bdcs_ResolutionForbidden{}};
|
||||||
|
encode_resolution({restricted, Restrictions}) ->
|
||||||
|
{restricted, #bdcs_ResolutionRestricted{
|
||||||
|
restrictions = encode_restrictions(Restrictions)
|
||||||
|
}}.
|
||||||
|
|
||||||
|
encode_restrictions(Restrictions) ->
|
||||||
|
{struct, _, StructDef} = bouncer_restriction_thrift:struct_info('Restrictions'),
|
||||||
|
bouncer_thrift:json_to_thrift_struct(StructDef, Restrictions, #brstn_Restrictions{}).
|
||||||
|
|
||||||
-spec decode_context(thrift_context(), st()) ->
|
-spec decode_context(thrift_context(), st()) ->
|
||||||
{bouncer_context:ctx(), st()}.
|
{bouncer_context:ctx(), st()}.
|
||||||
|
@ -217,7 +217,8 @@ get_beat_metadata({judgement, Event}) ->
|
|||||||
#{
|
#{
|
||||||
event => completed,
|
event => completed,
|
||||||
resolution => encode_resolution(Resolution),
|
resolution => encode_resolution(Resolution),
|
||||||
assertions => lists:map(fun encode_assertion/1, Assertions)
|
assertions => lists:map(fun encode_assertion/1, Assertions),
|
||||||
|
restrictions => encode_restrictions(Resolution)
|
||||||
};
|
};
|
||||||
{failed, Error} ->
|
{failed, Error} ->
|
||||||
#{
|
#{
|
||||||
@ -228,7 +229,15 @@ get_beat_metadata({judgement, Event}) ->
|
|||||||
}.
|
}.
|
||||||
|
|
||||||
encode_resolution(allowed) -> <<"allowed">>;
|
encode_resolution(allowed) -> <<"allowed">>;
|
||||||
encode_resolution(forbidden) -> <<"forbidden">>.
|
encode_resolution(forbidden) -> <<"forbidden">>;
|
||||||
|
encode_resolution({restricted, _Restrictions}) -> <<"restricted">>.
|
||||||
|
|
||||||
|
%% NOTE
|
||||||
|
%% I judged adding restrictions parsing to be not worth it for audit log
|
||||||
|
encode_restrictions({restricted, Restrictions}) ->
|
||||||
|
Restrictions;
|
||||||
|
encode_restrictions(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
encode_assertion({Code, Details}) ->
|
encode_assertion({Code, Details}) ->
|
||||||
#{code => Code, details => Details}.
|
#{code => Code, details => Details}.
|
||||||
|
@ -58,39 +58,7 @@ from_thrift_context(Ctx) ->
|
|||||||
bouncer_context_v1_thrift:struct_info('ContextFragment'),
|
bouncer_context_v1_thrift:struct_info('ContextFragment'),
|
||||||
% NOTE
|
% NOTE
|
||||||
% This 3 refers to the first data field in a ContextFragment, after version field.
|
% This 3 refers to the first data field in a ContextFragment, after version field.
|
||||||
from_thrift_struct(StructDef, Ctx, 3, #{}).
|
bouncer_thrift:from_thrift_struct(StructDef, Ctx, 3, #{}).
|
||||||
|
|
||||||
from_thrift_struct(StructDef, Struct) ->
|
|
||||||
% NOTE
|
|
||||||
% This 2 refers to the first field in a record tuple.
|
|
||||||
from_thrift_struct(StructDef, Struct, 2, #{}).
|
|
||||||
|
|
||||||
from_thrift_struct([{_, _Req, Type, Name, _Default} | Rest], Struct, Idx, Acc) ->
|
|
||||||
Acc1 = case element(Idx, Struct) of
|
|
||||||
V when V /= undefined ->
|
|
||||||
Acc#{Name => from_thrift_value(Type, V)};
|
|
||||||
undefined ->
|
|
||||||
Acc
|
|
||||||
end,
|
|
||||||
from_thrift_struct(Rest, Struct, Idx + 1, Acc1);
|
|
||||||
from_thrift_struct([], _Struct, _, Acc) ->
|
|
||||||
Acc.
|
|
||||||
|
|
||||||
from_thrift_value({struct, struct, {Mod, Name}}, V) ->
|
|
||||||
{struct, _, StructDef} = Mod:struct_info(Name),
|
|
||||||
from_thrift_struct(StructDef, V);
|
|
||||||
from_thrift_value({set, Type}, Vs) ->
|
|
||||||
ordsets:from_list([from_thrift_value(Type, V) || V <- ordsets:to_list(Vs)]);
|
|
||||||
from_thrift_value(string, V) ->
|
|
||||||
V;
|
|
||||||
from_thrift_value(i64, V) ->
|
|
||||||
V;
|
|
||||||
from_thrift_value(i32, V) ->
|
|
||||||
V;
|
|
||||||
from_thrift_value(i16, V) ->
|
|
||||||
V;
|
|
||||||
from_thrift_value(byte, V) ->
|
|
||||||
V.
|
|
||||||
|
|
||||||
-spec try_upgrade(thrift_ctx_fragment()) ->
|
-spec try_upgrade(thrift_ctx_fragment()) ->
|
||||||
thrift_ctx_fragment().
|
thrift_ctx_fragment().
|
||||||
@ -103,61 +71,16 @@ try_upgrade(#bctx_v1_ContextFragment{vsn = ?BCTX_V1_HEAD} = Ctx) ->
|
|||||||
{ok, _Content} | {error, _}.
|
{ok, _Content} | {error, _}.
|
||||||
encode(thrift, Context) ->
|
encode(thrift, Context) ->
|
||||||
Codec = thrift_strict_binary_codec:new(),
|
Codec = thrift_strict_binary_codec:new(),
|
||||||
try to_thrift(Context) of
|
CtxThrift = to_thrift(Context),
|
||||||
CtxThrift ->
|
case thrift_strict_binary_codec:write(Codec, ?THRIFT_TYPE, CtxThrift) of
|
||||||
case thrift_strict_binary_codec:write(Codec, ?THRIFT_TYPE, CtxThrift) of
|
{ok, Codec1} ->
|
||||||
{ok, Codec1} ->
|
{ok, thrift_strict_binary_codec:close(Codec1)};
|
||||||
{ok, thrift_strict_binary_codec:close(Codec1)};
|
{error, _} = Error ->
|
||||||
{error, _} = Error ->
|
Error
|
||||||
Error
|
|
||||||
end
|
|
||||||
catch throw:{?MODULE, Reason} ->
|
|
||||||
{error, Reason}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec to_thrift(bouncer_context:ctx()) ->
|
-spec to_thrift(bouncer_context:ctx()) ->
|
||||||
thrift_ctx_fragment() | no_return().
|
thrift_ctx_fragment() | no_return().
|
||||||
to_thrift(Context) ->
|
to_thrift(Context) ->
|
||||||
{struct, _, StructDef} = bouncer_context_v1_thrift:struct_info('ContextFragment'),
|
{struct, _, StructDef} = bouncer_context_v1_thrift:struct_info('ContextFragment'),
|
||||||
to_thrift_struct(StructDef, Context, #bctx_v1_ContextFragment{}).
|
bouncer_thrift:to_thrift_struct(StructDef, Context, #bctx_v1_ContextFragment{}).
|
||||||
|
|
||||||
to_thrift_struct(StructDef, Map, Acc) ->
|
|
||||||
% NOTE
|
|
||||||
% This 2 refers to the first field in a record tuple.
|
|
||||||
to_thrift_struct(StructDef, Map, 2, Acc).
|
|
||||||
|
|
||||||
to_thrift_struct([{_Tag, _Req, Type, Name, Default} | Rest], Map, Idx, Acc) ->
|
|
||||||
case maps:take(Name, Map) of
|
|
||||||
{V, MapLeft} ->
|
|
||||||
Acc1 = erlang:setelement(Idx, Acc, to_thrift_value(Type, V)),
|
|
||||||
to_thrift_struct(Rest, MapLeft, Idx + 1, Acc1);
|
|
||||||
error when Default /= undefined ->
|
|
||||||
Acc1 = erlang:setelement(Idx, Acc, Default),
|
|
||||||
to_thrift_struct(Rest, Map, Idx + 1, Acc1);
|
|
||||||
error ->
|
|
||||||
to_thrift_struct(Rest, Map, Idx + 1, Acc)
|
|
||||||
end;
|
|
||||||
to_thrift_struct([], MapLeft, _Idx, Acc) ->
|
|
||||||
case map_size(MapLeft) of
|
|
||||||
0 ->
|
|
||||||
Acc;
|
|
||||||
_ ->
|
|
||||||
throw({?MODULE, {excess_context_data, MapLeft}})
|
|
||||||
end.
|
|
||||||
|
|
||||||
to_thrift_value({struct, struct, {Mod, Name}}, V = #{}) ->
|
|
||||||
{struct, _, StructDef} = Mod:struct_info(Name),
|
|
||||||
Acc = erlang:make_tuple(length(StructDef) + 1, undefined, [{1, Mod:record_name(Name)}]),
|
|
||||||
to_thrift_struct(StructDef, V, Acc);
|
|
||||||
to_thrift_value({set, Type}, Vs) ->
|
|
||||||
ordsets:from_list([to_thrift_value(Type, V) || V <- ordsets:to_list(Vs)]);
|
|
||||||
to_thrift_value(string, V) ->
|
|
||||||
V;
|
|
||||||
to_thrift_value(i64, V) ->
|
|
||||||
V;
|
|
||||||
to_thrift_value(i32, V) ->
|
|
||||||
V;
|
|
||||||
to_thrift_value(i16, V) ->
|
|
||||||
V;
|
|
||||||
to_thrift_value(byte, V) ->
|
|
||||||
V.
|
|
||||||
|
112
src/bouncer_thrift.erl
Normal file
112
src/bouncer_thrift.erl
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
-module(bouncer_thrift).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([json_to_thrift_struct/3]).
|
||||||
|
-export([to_thrift_struct/3]).
|
||||||
|
-export([from_thrift_struct/4]).
|
||||||
|
|
||||||
|
-type struct_flavour() :: struct | exception | union.
|
||||||
|
-type field_num() :: pos_integer().
|
||||||
|
-type field_name() :: atom().
|
||||||
|
-type field_req() :: required | optional | undefined.
|
||||||
|
|
||||||
|
-type type_ref() :: {module(), atom()}.
|
||||||
|
-type field_type() ::
|
||||||
|
bool | byte | i16 | i32 | i64 | string | double |
|
||||||
|
{enum, type_ref()} |
|
||||||
|
{struct, struct_flavour(), type_ref()} |
|
||||||
|
{list, field_type()} |
|
||||||
|
{set, field_type()} |
|
||||||
|
{map, field_type(), field_type()}.
|
||||||
|
|
||||||
|
-type struct_field_info() ::
|
||||||
|
{field_num(), field_req(), field_type(), field_name(), any()}.
|
||||||
|
|
||||||
|
-spec json_to_thrift_struct([struct_field_info()], map(), tuple()) -> tuple().
|
||||||
|
json_to_thrift_struct(StructDef, Map, Acc) ->
|
||||||
|
% NOTE
|
||||||
|
% This 2 refers to the first field in a record tuple.
|
||||||
|
to_thrift_struct(StructDef, Map, 2, Acc, fun genlib:to_binary/1).
|
||||||
|
|
||||||
|
-spec to_thrift_struct([struct_field_info()], map(), tuple()) -> tuple().
|
||||||
|
to_thrift_struct(StructDef, Map, Acc) ->
|
||||||
|
% NOTE
|
||||||
|
% This 2 refers to the first field in a record tuple.
|
||||||
|
to_thrift_struct(StructDef, Map, 2, Acc, fun identity/1).
|
||||||
|
|
||||||
|
to_thrift_struct([{_Tag, _Req, Type, Name, Default} | Rest], Map, Idx, Acc, NameFun) ->
|
||||||
|
TransformedName = NameFun(Name),
|
||||||
|
case maps:take(TransformedName, Map) of
|
||||||
|
{V, MapLeft} ->
|
||||||
|
Acc1 = erlang:setelement(Idx, Acc, to_thrift_value(Type, V, NameFun)),
|
||||||
|
to_thrift_struct(Rest, MapLeft, Idx + 1, Acc1, NameFun);
|
||||||
|
error when Default /= undefined ->
|
||||||
|
Acc1 = erlang:setelement(Idx, Acc, Default),
|
||||||
|
to_thrift_struct(Rest, Map, Idx + 1, Acc1, NameFun);
|
||||||
|
error ->
|
||||||
|
to_thrift_struct(Rest, Map, Idx + 1, Acc, NameFun)
|
||||||
|
end;
|
||||||
|
to_thrift_struct([], MapLeft, _Idx, Acc, _NameFun) ->
|
||||||
|
case map_size(MapLeft) of
|
||||||
|
0 ->
|
||||||
|
Acc;
|
||||||
|
_ ->
|
||||||
|
%% Some part of map was left after converting to thrift,
|
||||||
|
%% indicating that either thrift structure doesn't have
|
||||||
|
%% enough fields or there's error in map creation
|
||||||
|
error({excess_data, MapLeft})
|
||||||
|
end.
|
||||||
|
|
||||||
|
to_thrift_value({struct, struct, {Mod, Name}}, V = #{}, NameFun) ->
|
||||||
|
{struct, _, StructDef} = Mod:struct_info(Name),
|
||||||
|
Acc = erlang:make_tuple(length(StructDef) + 1, undefined, [{1, Mod:record_name(Name)}]),
|
||||||
|
to_thrift_struct(StructDef, V, 2, Acc, NameFun);
|
||||||
|
to_thrift_value({set, Type}, Vs, NameFun) ->
|
||||||
|
ordsets:from_list([to_thrift_value(Type, V, NameFun) || V <- ordsets:to_list(Vs)]);
|
||||||
|
to_thrift_value(string, V, _NameFun) ->
|
||||||
|
V;
|
||||||
|
to_thrift_value(i64, V, _NameFun) ->
|
||||||
|
V;
|
||||||
|
to_thrift_value(i32, V, _NameFun) ->
|
||||||
|
V;
|
||||||
|
to_thrift_value(i16, V, _NameFun) ->
|
||||||
|
V;
|
||||||
|
to_thrift_value(byte, V, _NameFun) ->
|
||||||
|
V.
|
||||||
|
|
||||||
|
from_thrift_struct(StructDef, Struct) ->
|
||||||
|
% NOTE
|
||||||
|
% This 2 refers to the first field in a record tuple.
|
||||||
|
from_thrift_struct(StructDef, Struct, 2, #{}).
|
||||||
|
|
||||||
|
-spec from_thrift_struct([struct_field_info()], tuple(), number(), map()) -> map().
|
||||||
|
from_thrift_struct([{_, _Req, Type, Name, _Default} | Rest], Struct, Idx, Acc) ->
|
||||||
|
Acc1 =
|
||||||
|
case element(Idx, Struct) of
|
||||||
|
V when V /= undefined ->
|
||||||
|
Acc#{Name => from_thrift_value(Type, V)};
|
||||||
|
undefined ->
|
||||||
|
Acc
|
||||||
|
end,
|
||||||
|
from_thrift_struct(Rest, Struct, Idx + 1, Acc1);
|
||||||
|
from_thrift_struct([], _Struct, _, Acc) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
|
from_thrift_value({struct, struct, {Mod, Name}}, V) ->
|
||||||
|
{struct, _, StructDef} = Mod:struct_info(Name),
|
||||||
|
from_thrift_struct(StructDef, V);
|
||||||
|
from_thrift_value({set, Type}, Vs) ->
|
||||||
|
ordsets:from_list([from_thrift_value(Type, V) || V <- ordsets:to_list(Vs)]);
|
||||||
|
from_thrift_value(string, V) ->
|
||||||
|
V;
|
||||||
|
from_thrift_value(i64, V) ->
|
||||||
|
V;
|
||||||
|
from_thrift_value(i32, V) ->
|
||||||
|
V;
|
||||||
|
from_thrift_value(i16, V) ->
|
||||||
|
V;
|
||||||
|
from_thrift_value(byte, V) ->
|
||||||
|
V.
|
||||||
|
|
||||||
|
identity(V) ->
|
||||||
|
V.
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
-define(OPA_HOST, "opa").
|
-define(OPA_HOST, "opa").
|
||||||
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
||||||
-define(API_RULESET_ID, "authz/api").
|
-define(API_RULESET_ID, "service/authz/api").
|
||||||
|
|
||||||
-spec all() ->
|
-spec all() ->
|
||||||
[atom()].
|
[atom()].
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
-define(OPA_HOST, "opa").
|
-define(OPA_HOST, "opa").
|
||||||
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
||||||
-define(API_RULESET_ID, "authz/api").
|
-define(API_RULESET_ID, "service/authz/api").
|
||||||
|
|
||||||
-spec all() ->
|
-spec all() ->
|
||||||
[atom()].
|
[atom()].
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
-export([conflicting_context_invalid/1]).
|
-export([conflicting_context_invalid/1]).
|
||||||
-export([distinct_sets_context_valid/1]).
|
-export([distinct_sets_context_valid/1]).
|
||||||
|
|
||||||
-export([allowed_create_invoice_shop_manager/1]).
|
-export([restricted_search_invoices_shop_manager/1]).
|
||||||
-export([forbidden_w_empty_context/1]).
|
-export([forbidden_w_empty_context/1]).
|
||||||
-export([forbidden_expired/1]).
|
-export([forbidden_expired/1]).
|
||||||
-export([forbidden_blacklisted_ip/1]).
|
-export([forbidden_blacklisted_ip/1]).
|
||||||
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
-define(OPA_HOST, "opa").
|
-define(OPA_HOST, "opa").
|
||||||
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
||||||
-define(API_RULESET_ID, "authz/api").
|
-define(API_RULESET_ID, "service/authz/api").
|
||||||
|
|
||||||
-spec all() ->
|
-spec all() ->
|
||||||
[atom()].
|
[atom()].
|
||||||
@ -72,7 +72,7 @@ groups() ->
|
|||||||
distinct_sets_context_valid
|
distinct_sets_context_valid
|
||||||
]},
|
]},
|
||||||
{rules_authz_api, [parallel], [
|
{rules_authz_api, [parallel], [
|
||||||
allowed_create_invoice_shop_manager,
|
restricted_search_invoices_shop_manager,
|
||||||
forbidden_expired,
|
forbidden_expired,
|
||||||
forbidden_blacklisted_ip,
|
forbidden_blacklisted_ip,
|
||||||
forbidden_w_empty_context
|
forbidden_w_empty_context
|
||||||
@ -167,8 +167,8 @@ end_per_testcase(_Name, _C) ->
|
|||||||
%%
|
%%
|
||||||
|
|
||||||
-define(CONTEXT(Fragments), #bdcs_Context{fragments = Fragments}).
|
-define(CONTEXT(Fragments), #bdcs_Context{fragments = Fragments}).
|
||||||
-define(JUDGEMENT(Resolution, ResolutionLegacy),
|
-define(JUDGEMENT(Resolution),
|
||||||
#bdcs_Judgement{resolution = Resolution, resolution_legacy = ResolutionLegacy}).
|
#bdcs_Judgement{resolution = Resolution}).
|
||||||
|
|
||||||
-spec missing_ruleset_notfound(config()) -> ok.
|
-spec missing_ruleset_notfound(config()) -> ok.
|
||||||
-spec incorrect_ruleset_invalid1(config()) -> ok.
|
-spec incorrect_ruleset_invalid1(config()) -> ok.
|
||||||
@ -199,7 +199,7 @@ incorrect_ruleset_invalid1(C) ->
|
|||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{judgement, {failed, {ruleset_invalid, [
|
{judgement, {failed, {ruleset_invalid, [
|
||||||
{data_invalid, _, no_extra_properties_allowed, _, [<<"fordibben">>]}
|
{data_invalid, _, wrong_size, _, [<<"resolution">>]}
|
||||||
]}}},
|
]}}},
|
||||||
lists:last(flush_beats(Client, C))
|
lists:last(flush_beats(Client, C))
|
||||||
).
|
).
|
||||||
@ -212,7 +212,7 @@ incorrect_ruleset_invalid2(C) ->
|
|||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{judgement, {failed, {ruleset_invalid, [
|
{judgement, {failed, {ruleset_invalid, [
|
||||||
{data_invalid, _, wrong_type, _, [<<"allowed">>]}
|
{data_invalid, _, wrong_type, _, [<<"resolution">>, _]}
|
||||||
]}}},
|
]}}},
|
||||||
lists:last(flush_beats(Client, C))
|
lists:last(flush_beats(Client, C))
|
||||||
).
|
).
|
||||||
@ -225,7 +225,7 @@ incorrect_ruleset_invalid3(C) ->
|
|||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{judgement, {failed, {ruleset_invalid, [
|
{judgement, {failed, {ruleset_invalid, [
|
||||||
{data_invalid, _, missing_required_property, <<"code">>, _}
|
{data_invalid, _, no_extra_items_allowed, [<<"forbidden">>, [#{}], #{}], _}
|
||||||
]}}},
|
]}}},
|
||||||
lists:last(flush_beats(Client, C))
|
lists:last(flush_beats(Client, C))
|
||||||
).
|
).
|
||||||
@ -343,17 +343,17 @@ distinct_sets_context_valid(C) ->
|
|||||||
|
|
||||||
%%
|
%%
|
||||||
|
|
||||||
-spec allowed_create_invoice_shop_manager(config()) -> ok.
|
-spec restricted_search_invoices_shop_manager(config()) -> ok.
|
||||||
-spec forbidden_expired(config()) -> ok.
|
-spec forbidden_expired(config()) -> ok.
|
||||||
-spec forbidden_blacklisted_ip(config()) -> ok.
|
-spec forbidden_blacklisted_ip(config()) -> ok.
|
||||||
-spec forbidden_w_empty_context(config()) -> ok.
|
-spec forbidden_w_empty_context(config()) -> ok.
|
||||||
|
|
||||||
allowed_create_invoice_shop_manager(C) ->
|
restricted_search_invoices_shop_manager(C) ->
|
||||||
Client = mk_client(C),
|
Client = mk_client(C),
|
||||||
Fragment = lists:foldl(fun maps:merge/2, #{}, [
|
Fragment = lists:foldl(fun maps:merge/2, #{}, [
|
||||||
mk_auth_session_token(),
|
mk_auth_session_token(),
|
||||||
mk_env(),
|
mk_env(),
|
||||||
mk_op_create_invoice(<<"BLARG">>, <<"SHOP">>, <<"PARTY">>),
|
mk_op_search_invoices(mk_ordset([#{id => <<"SHOP">>}]), <<"PARTY">>),
|
||||||
mk_user(<<"USER">>, mk_ordset([
|
mk_user(<<"USER">>, mk_ordset([
|
||||||
mk_user_org(<<"PARTY">>, <<"OWNER">>, mk_ordset([
|
mk_user_org(<<"PARTY">>, <<"OWNER">>, mk_ordset([
|
||||||
mk_role(<<"Manager">>, <<"SHOP">>)
|
mk_role(<<"Manager">>, <<"SHOP">>)
|
||||||
@ -362,11 +362,11 @@ allowed_create_invoice_shop_manager(C) ->
|
|||||||
]),
|
]),
|
||||||
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
?JUDGEMENT({allowed, #bdcs_ResolutionAllowed{}}, allowed),
|
?JUDGEMENT({restricted, #bdcs_ResolutionRestricted{}}),
|
||||||
call_judge(?API_RULESET_ID, Context, Client)
|
call_judge(?API_RULESET_ID, Context, Client)
|
||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{judgement, {completed, {allowed, [{<<"user_has_role">>, _}]}}},
|
{judgement, {completed, {{restricted, _}, [{<<"org_role_allows_operation">>, _}]}}},
|
||||||
lists:last(flush_beats(Client, C))
|
lists:last(flush_beats(Client, C))
|
||||||
).
|
).
|
||||||
|
|
||||||
@ -381,7 +381,7 @@ forbidden_expired(C) ->
|
|||||||
}),
|
}),
|
||||||
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}, forbidden),
|
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}),
|
||||||
call_judge(?API_RULESET_ID, Context, Client)
|
call_judge(?API_RULESET_ID, Context, Client)
|
||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
@ -399,7 +399,7 @@ forbidden_blacklisted_ip(C) ->
|
|||||||
]),
|
]),
|
||||||
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}, forbidden),
|
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}),
|
||||||
call_judge(?API_RULESET_ID, Context, Client)
|
call_judge(?API_RULESET_ID, Context, Client)
|
||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
@ -411,7 +411,7 @@ forbidden_w_empty_context(C) ->
|
|||||||
Client1 = mk_client(C),
|
Client1 = mk_client(C),
|
||||||
EmptyFragment = mk_ctx_v1_fragment(#{}),
|
EmptyFragment = mk_ctx_v1_fragment(#{}),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}, forbidden),
|
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}),
|
||||||
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client1)
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client1)
|
||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
@ -420,7 +420,7 @@ forbidden_w_empty_context(C) ->
|
|||||||
),
|
),
|
||||||
Client2 = mk_client(C),
|
Client2 = mk_client(C),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}, forbidden),
|
?JUDGEMENT({forbidden, #bdcs_ResolutionForbidden{}}),
|
||||||
call_judge(?API_RULESET_ID, ?CONTEXT(#{<<"empty">> => EmptyFragment}), Client2)
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{<<"empty">> => EmptyFragment}), Client2)
|
||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
@ -453,12 +453,11 @@ mk_auth_session_token(ExpiresAt) ->
|
|||||||
expiration => format_ts(ExpiresAt, second)
|
expiration => format_ts(ExpiresAt, second)
|
||||||
}}.
|
}}.
|
||||||
|
|
||||||
mk_op_create_invoice(InvoiceID, ShopID, PartyID) ->
|
mk_op_search_invoices(Shops, PartyID) ->
|
||||||
#{capi => #{
|
#{anapi => #{
|
||||||
op => #{
|
op => #{
|
||||||
id => <<"CreateInvoice">>,
|
id => <<"SearchInvoices">>,
|
||||||
invoice => #{id => InvoiceID},
|
shops => Shops,
|
||||||
shop => #{id => ShopID},
|
|
||||||
party => #{id => PartyID}
|
party => #{id => PartyID}
|
||||||
}
|
}
|
||||||
}}.
|
}}.
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
package authz.api
|
|
||||||
|
|
||||||
import data.authz.blacklists
|
|
||||||
|
|
||||||
assertions := {
|
|
||||||
"forbidden" : { why | forbidden[why] },
|
|
||||||
"allowed" : { why | allowed[why] }
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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 an object of the following form:
|
|
||||||
# ```
|
|
||||||
# {"code": "...", ...}
|
|
||||||
# ```
|
|
||||||
forbidden[why] {
|
|
||||||
input
|
|
||||||
not input.auth.method
|
|
||||||
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 := {
|
|
||||||
"code": "auth_expired",
|
|
||||||
"description": sprintf("Authorization is expired at: %s", [input.auth.expiration])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
forbidden[why] {
|
|
||||||
ip := input.requester.ip
|
|
||||||
blacklist := blacklists["source-ip-range"]
|
|
||||||
matches := net.cidr_contains_matches(blacklist, ip)
|
|
||||||
ranges := [ range | matches[_][0] = i; range := blacklist[i] ]
|
|
||||||
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 an object of the following form:
|
|
||||||
# ```
|
|
||||||
# {"code": "...", ...}
|
|
||||||
# ```
|
|
||||||
allowed[why] {
|
|
||||||
input.auth.method == "SessionToken"
|
|
||||||
input.user
|
|
||||||
org_allowed[why]
|
|
||||||
}
|
|
||||||
|
|
||||||
org_allowed[why] {
|
|
||||||
org := org_by_operation
|
|
||||||
org.owner == input.user.id
|
|
||||||
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 := {
|
|
||||||
"code": "user_has_role",
|
|
||||||
"description": sprintf("User has role %s in scope %v", [rolename, scopename])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scopename_by_role[i] = sprintf("shop:%s", [shop]) {
|
|
||||||
role := org_by_operation.roles[i]
|
|
||||||
shop := role.scope.shop.id
|
|
||||||
shop == input.capi.op.shop.id
|
|
||||||
}
|
|
||||||
|
|
||||||
scopename_by_role[i] = "*" {
|
|
||||||
role := org_by_operation.roles[i]
|
|
||||||
not role.scope
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set of roles at least one of which is required to perform the operation in context.
|
|
||||||
role_by_operation["Manager"]
|
|
||||||
{ input.capi.op.id == "CreateInvoice" }
|
|
||||||
{ input.capi.op.id == "GetInvoiceByID" }
|
|
||||||
{ input.capi.op.id == "GetInvoiceEvents" }
|
|
||||||
{ input.capi.op.id == "FulfillInvoice" }
|
|
||||||
{ input.capi.op.id == "RescindInvoice" }
|
|
||||||
{ input.capi.op.id == "GetPayments" }
|
|
||||||
{ input.capi.op.id == "GetPaymentByID" }
|
|
||||||
{ input.capi.op.id == "CancelPayment" }
|
|
||||||
{ input.capi.op.id == "CapturePayment" }
|
|
||||||
{ input.capi.op.id == "GetRefunds" }
|
|
||||||
{ input.capi.op.id == "GetRefundByID" }
|
|
||||||
{ input.capi.op.id == "CreateRefund" }
|
|
||||||
{ input.capi.op.id == "CreateInvoiceTemplate" }
|
|
||||||
{ input.capi.op.id == "GetInvoiceTemplateByID" }
|
|
||||||
{ input.capi.op.id == "UpdateInvoiceTemplate" }
|
|
||||||
{ input.capi.op.id == "DeleteInvoiceTemplate" }
|
|
||||||
|
|
||||||
role_by_operation["Administrator"]
|
|
||||||
{ input.orgmgmt.op.id == "ListInvitations" }
|
|
||||||
{ input.orgmgmt.op.id == "CreateInvitation" }
|
|
||||||
{ input.orgmgmt.op.id == "GetInvitation" }
|
|
||||||
{ input.orgmgmt.op.id == "RevokeInvitation" }
|
|
||||||
|
|
||||||
# Context of an organisation which is being operated upon.
|
|
||||||
org_by_operation = org_by_id[id]
|
|
||||||
{ id = input.capi.op.party.id }
|
|
||||||
{ id = input.orgmgmt.op.organization.id }
|
|
||||||
|
|
||||||
# A mapping of org ids to organizations.
|
|
||||||
org_by_id := { org.id: org | org := input.user.orgs[_] }
|
|
111
test/policies/service/authz/api.rego
Normal file
111
test/policies/service/authz/api.rego
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package service.authz.api
|
||||||
|
|
||||||
|
import data.service.authz.api.invoice_access_token
|
||||||
|
import data.service.authz.api.url_shortener
|
||||||
|
import data.service.authz.api.binapi
|
||||||
|
import data.service.authz.api.anapi
|
||||||
|
import data.service.authz.blacklists
|
||||||
|
import data.service.authz.whitelists
|
||||||
|
import data.service.authz.roles
|
||||||
|
import data.service.authz.org
|
||||||
|
import data.service.authz.judgement
|
||||||
|
|
||||||
|
assertions := {
|
||||||
|
"forbidden" : { why | forbidden[why] },
|
||||||
|
"allowed" : { why | allowed[why] },
|
||||||
|
"restrictions": { what.type: what.restrictions[what.type] | restrictions[what] }
|
||||||
|
}
|
||||||
|
|
||||||
|
judgement := judgement.judge(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 an object of the following form:
|
||||||
|
# ```
|
||||||
|
# {"code": "auth_expired", "description": "..."}
|
||||||
|
# ```
|
||||||
|
forbidden[why] {
|
||||||
|
input
|
||||||
|
not input.auth.method
|
||||||
|
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 := {
|
||||||
|
"code": "auth_expired",
|
||||||
|
"description": sprintf("Authorization is expired at: %s", [input.auth.expiration])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden[why] {
|
||||||
|
ip := input.requester.ip
|
||||||
|
blacklist := blacklists.source_ip_range
|
||||||
|
matches := net.cidr_contains_matches(blacklist, ip)
|
||||||
|
matches[_]
|
||||||
|
ranges := [ range | matches[_][0] = i; range := blacklist[i] ]
|
||||||
|
why := {
|
||||||
|
"code": "ip_range_blacklisted",
|
||||||
|
"description": sprintf(
|
||||||
|
"Requester IP address is blacklisted with ranges: %v",
|
||||||
|
[concat(", ", ranges)]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden[why] {
|
||||||
|
input.anapi
|
||||||
|
anapi.forbidden[why]
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings[why] {
|
||||||
|
not blacklists.source_ip_range
|
||||||
|
why := "Blacklist 'source_ip_range' is not defined, blacklisting by IP will NOT WORK."
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings[why] {
|
||||||
|
not whitelists.bin_lookup_allowed_party_ids
|
||||||
|
why := "Whitelist 'bin_lookup_allowed_party_ids' is not defined, whitelisting by partyID will NOT WORK."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 an object of the following form:
|
||||||
|
# ```
|
||||||
|
# {"code": "auth_expired", "description": "..."}
|
||||||
|
# ```
|
||||||
|
allowed[why] {
|
||||||
|
input.shortener
|
||||||
|
url_shortener.allowed[why]
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed[why] {
|
||||||
|
input.binapi
|
||||||
|
binapi.allowed[why]
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed[why] {
|
||||||
|
input.auth.method == "InvoiceAccessToken"
|
||||||
|
invoice_access_token.allowed[why]
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed[why] {
|
||||||
|
input.anapi
|
||||||
|
anapi.allowed[why]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restrictions
|
||||||
|
|
||||||
|
restrictions[what] {
|
||||||
|
input.anapi
|
||||||
|
rstns := anapi.restrictions[_]
|
||||||
|
what := {
|
||||||
|
"type": "anapi",
|
||||||
|
"restrictions": rstns
|
||||||
|
}
|
||||||
|
}
|
80
test/policies/service/authz/api/anapi.rego
Normal file
80
test/policies/service/authz/api/anapi.rego
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package service.authz.api.anapi
|
||||||
|
|
||||||
|
import input.anapi.op
|
||||||
|
import data.service.authz.roles
|
||||||
|
|
||||||
|
api_name := "AnalyticsAPI"
|
||||||
|
|
||||||
|
# Set of assertions which tell why operation under the input context is forbidden.
|
||||||
|
# Each element must be an object of the following form:
|
||||||
|
# ```
|
||||||
|
# {"code": "auth_expired", "description": "..."}
|
||||||
|
# ```
|
||||||
|
forbidden[why] {
|
||||||
|
input.auth.method != "SessionToken"
|
||||||
|
why := {
|
||||||
|
"code": "unknown_auth_method_forbids_operation",
|
||||||
|
"description": sprintf("Unknown auth method for this operation: %v", [input.auth.method])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restrictions
|
||||||
|
|
||||||
|
restrictions[what] {
|
||||||
|
not user_is_owner
|
||||||
|
user_has_any_role_for_op
|
||||||
|
what := {
|
||||||
|
"anapi": {
|
||||||
|
"op": {
|
||||||
|
"shops": [shop | shop := op_shop_in_scope[_]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
op_shop_in_scope[shop] {
|
||||||
|
some i
|
||||||
|
op.shops[i].id == user_roles_by_operation[_].scope.shop.id
|
||||||
|
shop := op.shops[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set of assertions which tell why operation under the input context is allowed.
|
||||||
|
# Each element must be an object of the following form:
|
||||||
|
# ```
|
||||||
|
# {"code": "auth_expired", "description": "..."}
|
||||||
|
# ```
|
||||||
|
|
||||||
|
allowed[why] {
|
||||||
|
user_is_owner
|
||||||
|
why := {
|
||||||
|
"code": "org_ownership_allows_operation",
|
||||||
|
"description": "User is owner of organization that is subject of this operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed[why] {
|
||||||
|
user_role_id := user_roles_by_operation[_].id
|
||||||
|
why := {
|
||||||
|
"code": "org_role_allows_operation",
|
||||||
|
"description": sprintf("User has role that permits this operation: %v", [user_role_id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_is_owner {
|
||||||
|
organization := org_by_operation
|
||||||
|
input.user.id == organization.owner.id
|
||||||
|
}
|
||||||
|
|
||||||
|
user_has_any_role_for_op {
|
||||||
|
user_roles_by_operation[_]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_roles_by_operation[user_role] {
|
||||||
|
user_role := org_by_operation.roles[_]
|
||||||
|
op.id == roles.roles[user_role.id].apis[api_name].operations[_]
|
||||||
|
}
|
||||||
|
|
||||||
|
org_by_operation = org {
|
||||||
|
org := input.user.orgs[_]
|
||||||
|
org.id == op.party.id
|
||||||
|
}
|
35
test/policies/service/authz/judgement.rego
Normal file
35
test/policies/service/authz/judgement.rego
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package service.authz.judgement
|
||||||
|
|
||||||
|
judge(assertions) = jm {
|
||||||
|
count(assertions.forbidden) > 0
|
||||||
|
jm := {
|
||||||
|
"resolution": ["forbidden", assertions.forbidden]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
judge(assertions) = jm {
|
||||||
|
count(assertions.forbidden) == 0
|
||||||
|
assertions.restrictions != {}
|
||||||
|
count(assertions.allowed) > 0
|
||||||
|
jm := {
|
||||||
|
"resolution": ["restricted", assertions.allowed],
|
||||||
|
"restrictions": assertions.restrictions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
judge(assertions) = jm {
|
||||||
|
count(assertions.forbidden) == 0
|
||||||
|
assertions.restrictions == {}
|
||||||
|
count(assertions.allowed) > 0
|
||||||
|
jm := {
|
||||||
|
"resolution": ["allowed", assertions.allowed]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
judge(assertions) = jm {
|
||||||
|
count(assertions.forbidden) == 0
|
||||||
|
count(assertions.allowed) == 0
|
||||||
|
jm := {
|
||||||
|
"resolution": ["forbidden", []]
|
||||||
|
}
|
||||||
|
}
|
45
test/policies/service/authz/roles/data.json
Normal file
45
test/policies/service/authz/roles/data.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"roles": {
|
||||||
|
"Manager" : {
|
||||||
|
"apis": {
|
||||||
|
"AnalyticsAPI" : {
|
||||||
|
"operations": [
|
||||||
|
"SearchInvoices",
|
||||||
|
"SearchPayments",
|
||||||
|
"SearchRefunds",
|
||||||
|
"SearchChargebacks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Administrator" : {
|
||||||
|
"apis": {
|
||||||
|
"AnalyticsAPI" : {
|
||||||
|
"operations": [
|
||||||
|
"SearchInvoices",
|
||||||
|
"SearchPayments",
|
||||||
|
"SearchRefunds",
|
||||||
|
"SearchPayouts",
|
||||||
|
"SearchReports",
|
||||||
|
"SearchChargebacks",
|
||||||
|
"GetReport",
|
||||||
|
"CreateReport",
|
||||||
|
"CancelReport",
|
||||||
|
"DownloadFile",
|
||||||
|
"GetPaymentsToolDistribution",
|
||||||
|
"GetPaymentsAmount",
|
||||||
|
"GetAveragePayment",
|
||||||
|
"GetPaymentsCount",
|
||||||
|
"GetPaymentsErrorDistribution",
|
||||||
|
"GetPaymentsSplitAmount",
|
||||||
|
"GetPaymentsSplitCount",
|
||||||
|
"GetRefundsAmount",
|
||||||
|
"GetCurrentBalances",
|
||||||
|
"GetPaymentsSubErrorDistribution",
|
||||||
|
"GetCurrentBalancesGroupByShop"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
package trivial.incorrect1
|
package trivial.incorrect1
|
||||||
|
|
||||||
assertions := {
|
judgement := {
|
||||||
"fordibben" : { }
|
"resolution" : []
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package trivial.incorrect2
|
package trivial.incorrect2
|
||||||
|
|
||||||
assertions := {
|
judgement := {
|
||||||
"forbidden" : forbidden,
|
"resolution": ["forbidden", forbidden]
|
||||||
"allowed" : allowed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
forbidden[why] = description {
|
forbidden[why] = description {
|
||||||
@ -11,10 +10,3 @@ forbidden[why] = description {
|
|||||||
why := "auth_required"
|
why := "auth_required"
|
||||||
description := "Authorization is required"
|
description := "Authorization is required"
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed[why] = description {
|
|
||||||
input.auth.method == "SessionToken"
|
|
||||||
input.user
|
|
||||||
why := "its_a_user"
|
|
||||||
description := "Then why not?"
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package trivial.incorrect3
|
package trivial.incorrect3
|
||||||
|
|
||||||
assertions := {
|
judgement := {
|
||||||
"forbidden" : { why | forbidden[why] }
|
"resolution": ["forbidden", forbidden, allowed]
|
||||||
}
|
}
|
||||||
|
|
||||||
forbidden[why] {
|
forbidden[why] {
|
||||||
@ -9,3 +9,10 @@ forbidden[why] {
|
|||||||
not input.auth.method
|
not input.auth.method
|
||||||
why := {}
|
why := {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowed[why] = description {
|
||||||
|
input.auth.method == "SessionToken"
|
||||||
|
input.user
|
||||||
|
why := "its_a_user"
|
||||||
|
description := "Then why not?"
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user