Current section
Files
Jump to
Current section
Files
src/aarondb@auth.erl
-module(aarondb@auth).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aarondb/auth.gleam").
-export([authorize/2, decode_token/1]).
-export_type([action/0, resource/0, capability/0, token/0, raw_cap/0, raw_token/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type action() :: read | write | admin.
-type resource() :: all | {shard, integer()} | {database, binary()}.
-type capability() :: {capability, action(), resource()}.
-type token() :: {token, binary(), list(capability()), binary()}.
-type raw_cap() :: {raw_cap, binary(), binary()}.
-type raw_token() :: {raw_token, binary(), list(raw_cap()), binary()}.
-file("src/aarondb/auth.gleam", 63).
-spec subsumes_resource(resource(), resource()) -> boolean().
subsumes_resource(Provided, Required) ->
case {Provided, Required} of
{all, _} ->
true;
{{database, P}, {database, R}} when P =:= R ->
true;
{{database, _}, {shard, _}} ->
true;
{{shard, P@1}, {shard, R@1}} when P@1 =:= R@1 ->
true;
{_, _} ->
false
end.
-file("src/aarondb/auth.gleam", 51).
?DOC(
" Rich Hickey 🧙🏾♂️:\n"
" Subsumption is a logical property. A broader capability subsumes a narrower one.\n"
" e.g. Read(All) subsumes Read(Shard(1)).\n"
).
-spec subsumes(capability(), capability()) -> boolean().
subsumes(Provided, Required) ->
case {erlang:element(2, Provided), erlang:element(2, Required)} of
{admin, _} ->
subsumes_resource(
erlang:element(3, Provided),
erlang:element(3, Required)
);
{P_act, R_act} when P_act =:= R_act ->
subsumes_resource(
erlang:element(3, Provided),
erlang:element(3, Required)
);
{_, _} ->
false
end.
-file("src/aarondb/auth.gleam", 32).
?DOC(
" Verifies if a given Token satisfies a list of required capabilities.\n"
" Time Complexity: O(R * P) where R is required caps, P is provided caps.\n"
" Space Complexity: O(1) auxiliary space.\n"
).
-spec authorize(token(), list(capability())) -> {ok, nil} | {error, binary()}.
authorize(Token, Required) ->
Is_authorized = gleam@list:all(
Required,
fun(Req) ->
gleam@list:any(
erlang:element(3, Token),
fun(Prov) -> subsumes(Prov, Req) end
)
end
),
case Is_authorized of
true ->
{ok, nil};
false ->
{error,
<<"Unauthorized: Insufficient capabilities in token."/utf8>>}
end.
-file("src/aarondb/auth.gleam", 87).
?DOC(
" Example simple parser from a JSON-like token format.\n"
" In a real UCAN, you'd verify proper base64 + EdDSA signatures here.\n"
).
-spec decode_token(binary()) -> {ok, token()} |
{error, gleam@json:decode_error()}.
decode_token(Payload) ->
Cap_decoder = begin
gleam@dynamic@decode:field(
<<"op"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Op) ->
gleam@dynamic@decode:field(
<<"res"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Res) ->
gleam@dynamic@decode:success({raw_cap, Op, Res})
end
)
end
)
end,
Token_decoder = begin
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) ->
gleam@dynamic@decode:field(
<<"caps"/utf8>>,
gleam@dynamic@decode:list(Cap_decoder),
fun(Caps) ->
gleam@dynamic@decode:field(
<<"iss"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Iss) ->
gleam@dynamic@decode:success(
{raw_token, Id, Caps, Iss}
)
end
)
end
)
end
)
end,
case gleam@json:parse(Payload, Token_decoder) of
{ok, Raw} ->
Caps@1 = gleam@list:filter_map(
erlang:element(3, Raw),
fun(C) ->
Action = case erlang:element(2, C) of
<<"read"/utf8>> ->
{ok, read};
<<"write"/utf8>> ->
{ok, write};
<<"admin"/utf8>> ->
{ok, admin};
_ ->
{error, nil}
end,
Resource = case erlang:element(3, C) of
<<"all"/utf8>> ->
{ok, all};
S ->
case gleam_stdlib:string_starts_with(
S,
<<"shard:"/utf8>>
) of
true ->
Num_str = gleam@string:replace(
S,
<<"shard:"/utf8>>,
<<""/utf8>>
),
case gleam_stdlib:parse_int(Num_str) of
{ok, N} ->
{ok, {shard, N}};
{error, _} ->
{error, nil}
end;
false ->
case gleam_stdlib:string_starts_with(
S,
<<"db:"/utf8>>
) of
true ->
{ok,
{database,
gleam@string:replace(
S,
<<"db:"/utf8>>,
<<""/utf8>>
)}};
false ->
{error, nil}
end
end
end,
case {Action, Resource} of
{{ok, A}, {ok, R}} ->
{ok, {capability, A, R}};
{_, _} ->
{error, nil}
end
end
),
{ok,
{token, erlang:element(2, Raw), Caps@1, erlang:element(4, Raw)}};
{error, E} ->
{error, E}
end.