Skip to content

Commit 07f2f14

Browse files
committed
MB-62604: Add cb-on-behalf extras
cb-on-behalf-extras (optional) header populates auth context for SAML sessions. checkPermission, getUserBuckets endpoints are modified to accept an additional context parameter. on-behalf-of requests parse the context if present to determine extra groups and roles associated with the SAML session. Corresponding cbauth change: https://review.couchbase.org/c/cbauth/+/214565 Change-Id: I13481b35f037cef78c305dbd7ebfbe3aa947e83c Reviewed-on: https://review.couchbase.org/c/ns_server/+/214426 Well-Formed: Restriction Checker Well-Formed: Build Bot <[email protected]> Tested-by: Neelima Premsankar <[email protected]> Reviewed-by: Timofey Barmin <[email protected]>
1 parent d9608b5 commit 07f2f14

File tree

6 files changed

+133
-36
lines changed

6 files changed

+133
-36
lines changed

src/goxdcr_rest.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ headers_for_proxy(MochiReq, AuthnRes) ->
4040
{true, {convert_header_name(Name), Value}}
4141
end
4242
end, HeadersList),
43-
[menelaus_rest:on_behalf_header(AuthnRes),
44-
menelaus_rest:special_auth_header() | Headers].
43+
menelaus_rest:on_behalf_headers(AuthnRes) ++
44+
[menelaus_rest:special_auth_header() | Headers].
4545

4646
send(MochiReq, Method, Path, Headers, Body) ->
4747
Params = mochiweb_request:parse_qs(MochiReq),

src/menelaus_auth.erl

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
new_session_id/0,
4343
get_resp_headers/1,
4444
acting_on_behalf/1,
45-
init_auth/1]).
45+
init_auth/1,
46+
on_behalf_context/1,
47+
get_authn_res_from_on_behalf_of/3]).
4648

4749
%% rpc from ns_couchdb node
4850
-export([authenticate/1,
@@ -501,7 +503,7 @@ verify_rest_auth(Req, Permission) ->
501503
{ok, #authn_res{} = AuthnRes,
502504
RespHeaders} ->
503505
Req2 = append_resp_headers(RespHeaders, Req),
504-
case apply_on_behalf_of_identity(AuthnRes, Req2) of
506+
case apply_on_behalf_of_authn_res(AuthnRes, Req2) of
505507
error ->
506508
Req3 = maybe_store_rejected_user(
507509
get_rejected_user(Auth), Req2),
@@ -526,15 +528,41 @@ verify_rest_auth(Req, Permission) ->
526528
{auth_failure, Req2}
527529
end.
528530

529-
-spec apply_on_behalf_of_identity(#authn_res{}, mochiweb_request()) ->
530-
error | #authn_res{}.
531-
apply_on_behalf_of_identity(AuthnRes, Req) ->
532-
case extract_on_behalf_of_identity(Req) of
531+
%% Specify authentication context for SAML (and later JWT) in on-behalf-of
532+
%% extras. {User, Domain} aren't sufficient to determine the full set of
533+
%% privileges available to the user.
534+
-spec on_behalf_context(#authn_res{}) -> {string(), boolean()}.
535+
on_behalf_context(#authn_res{session_id = Id}) when is_binary(Id) ->
536+
{"context:ui=" ++ binary_to_list(Id), true};
537+
on_behalf_context(_) -> {"", false}.
538+
539+
-spec get_authn_res_from_on_behalf_of(User :: rbac_user_id(),
540+
Domain :: rbac_identity_type(),
541+
Context :: string() | undefined) ->
542+
#authn_res{}.
543+
get_authn_res_from_on_behalf_of(User, Domain, Context) ->
544+
AuthnRes0 = #authn_res{identity = {User, Domain}},
545+
case Context of
546+
undefined -> AuthnRes0;
547+
"ui=" ++ Id ->
548+
UiAuthnRes = menelaus_ui_auth:get_authn_res_from_ui_session(Id),
549+
case UiAuthnRes of
550+
undefined -> AuthnRes0;
551+
#authn_res{identity = {User0, Domain0}}
552+
when User0 =:= User, Domain0 =:= Domain -> UiAuthnRes;
553+
_ -> AuthnRes0
554+
end
555+
end.
556+
557+
-spec apply_on_behalf_of_authn_res(#authn_res{}, mochiweb_request()) ->
558+
error | #authn_res{}.
559+
apply_on_behalf_of_authn_res(AuthnRes, Req) ->
560+
case extract_on_behalf_of_authn_res(Req) of
533561
error ->
534562
error;
535563
undefined ->
536564
AuthnRes;
537-
{ok, RealIdentity} ->
565+
{User, Domain, Context} ->
538566
%% The permission is formed the way that it is currently granted
539567
%% to full admins only. We might consider to reformulate it
540568
%% like {[onbehalf], impersonate} or, such in the upcoming
@@ -547,9 +575,7 @@ apply_on_behalf_of_identity(AuthnRes, Req) ->
547575
case menelaus_roles:is_allowed(
548576
{[admin, security, admin], impersonate}, AuthnRes) of
549577
true ->
550-
AuthnRes#authn_res{identity = RealIdentity,
551-
extra_groups = [],
552-
extra_roles = []};
578+
get_authn_res_from_on_behalf_of(User, Domain, Context);
553579
false ->
554580
error
555581
end
@@ -559,17 +585,23 @@ apply_on_behalf_of_identity(AuthnRes, Req) ->
559585
acting_on_behalf(Req) ->
560586
get_authenticated_identity(Req) =/= get_identity(Req).
561587

562-
-spec extract_on_behalf_of_identity(mochiweb_request()) ->
563-
error | undefined
564-
| {ok, rbac_identity()}.
565-
extract_on_behalf_of_identity(Req) ->
588+
-spec extract_on_behalf_of_authn_res(mochiweb_request()) ->
589+
error | undefined |
590+
{rbac_user_id(), rbac_identity_type(), string() | undefined}.
591+
extract_on_behalf_of_authn_res(Req) ->
566592
case read_on_behalf_of_header(Req) of
567593
Header when is_list(Header) ->
568594
case parse_on_behalf_of_header(Header) of
569595
{User, Domain} ->
570-
try
571-
ExistingDomain = list_to_existing_atom(Domain),
572-
{ok, {User, ExistingDomain}}
596+
try list_to_existing_atom(Domain) of
597+
ExistingDomain ->
598+
case parse_on_behalf_of_extras(Req) of
599+
error -> error;
600+
Context when is_list(Context) ->
601+
{User, ExistingDomain, Context};
602+
undefined ->
603+
{User, ExistingDomain, undefined}
604+
end
573605
catch
574606
error:badarg ->
575607
?log_debug("Invalid domain in cb-on-behalf-of: ~s",
@@ -582,12 +614,21 @@ extract_on_behalf_of_identity(Req) ->
582614
error
583615
end;
584616
undefined ->
585-
undefined
617+
case read_on_behalf_of_extras(Req) of
618+
undefined -> undefined;
619+
Hdr ->
620+
?log_debug("Unexpected cb-on-behalf-extras: ~s",
621+
[ns_config_log:tag_user_name(Hdr)]),
622+
undefined
623+
end
586624
end.
587625

588626
read_on_behalf_of_header(Req) ->
589627
mochiweb_request:get_header_value("cb-on-behalf-of", Req).
590628

629+
read_on_behalf_of_extras(Req) ->
630+
mochiweb_request:get_header_value("cb-on-behalf-extras", Req).
631+
591632
parse_on_behalf_of_header(Header) ->
592633
case (catch base64:decode_to_string(Header)) of
593634
UserDomainStr when is_list(UserDomainStr) ->
@@ -602,6 +643,29 @@ parse_on_behalf_of_header(Header) ->
602643
error
603644
end.
604645

646+
parse_on_behalf_of_extras(Req) ->
647+
case read_on_behalf_of_extras(Req) of
648+
undefined -> undefined;
649+
Extras when is_list(Extras) ->
650+
Status =
651+
case (catch base64:decode_to_string(Extras)) of
652+
ContextStr when is_list(ContextStr) ->
653+
case ContextStr of
654+
"context:" ++ X -> X;
655+
_ -> error
656+
end;
657+
_ -> error
658+
end,
659+
case Status of
660+
error ->
661+
?log_debug("Invalid context in cb-on-behalf-extras:~s",
662+
[ns_config_log:tag_user_name(Extras)]),
663+
error;
664+
S -> S
665+
end;
666+
_ -> error
667+
end.
668+
605669
-spec extract_identity_from_cert(binary()) ->
606670
tuple() | auth_failure | temporary_failure.
607671
extract_identity_from_cert(CertDer) ->

src/menelaus_pluggable_ui.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ proxy_req(RestPrefix, Path, PluginsConfig, Req) ->
291291
% this node and the destination node are same.
292292

293293
AuthnRes = menelaus_auth:get_authn_res(Req),
294-
FwdHeader = [menelaus_rest:on_behalf_header(AuthnRes),
295-
menelaus_rest:special_auth_header(Node)],
294+
FwdHeader = menelaus_rest:on_behalf_headers(AuthnRes) ++
295+
[menelaus_rest:special_auth_header(Node)],
296296

297297
Headers = FwdHeader ++
298298
convert_headers(Req, add_filter_headers(HdrFilter)) ++

src/menelaus_rest.erl

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
special_auth_header/1,
2727
is_auth_header/1,
2828
if_none_match_header/1,
29-
on_behalf_header/1]).
29+
on_behalf_headers/1]).
3030

3131
-spec rest_url(string(), string() | integer(), string(), string() | atom()) -> string().
3232
rest_url(Host, Port, Path, Scheme) when is_atom(Scheme) ->
@@ -55,9 +55,17 @@ special_auth_header(Node) when is_atom(Node) ->
5555
basic_auth_header(?HIDE({basic_auth, ns_config_auth:get_user(special),
5656
ns_config_auth:get_password(Node, special)})).
5757

58-
on_behalf_header(#authn_res{identity = {User, Domain}}) ->
59-
{"cb-on-behalf-of",
60-
base64:encode_to_string(User ++ ":" ++ atom_to_list(Domain))}.
58+
%% Allow specifying on behalf of user (User, Domain) in cb-on-behalf-of header
59+
%% and an optional on-behalf-extras header for additional auth context.
60+
%% on-behalf-extras can specify multiple kv pairs separated by ;
61+
%% [key1:v1;key2:v2;key3:v3]
62+
%% Currently only (optional) UI context is specified as
63+
%% context:ui=<session_id>.
64+
on_behalf_headers(#authn_res{identity = {User, Domain}} = AuthnRes) ->
65+
{Value, Present} = menelaus_auth:on_behalf_context(AuthnRes),
66+
[{"cb-on-behalf-of",
67+
base64:encode_to_string(User ++ ":" ++ atom_to_list(Domain))}] ++
68+
[{"cb-on-behalf-extras", base64:encode_to_string(Value)} || Present].
6169

6270
is_auth_header(Header) when is_atom(Header) ->
6371
is_auth_header(atom_to_list(Header));
@@ -68,6 +76,8 @@ is_auth_header_lc("authorization") ->
6876
true;
6977
is_auth_header_lc("cb-on-behalf-of") ->
7078
true;
79+
is_auth_header_lc("cb-on-behalf-extras") ->
80+
true;
7181
is_auth_header_lc("ns-server-auth-token") ->
7282
true;
7383
is_auth_header_lc("ns-server-ui") ->

src/menelaus_ui_auth.erl

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717

1818
-export([start_ui_session/3, maybe_refresh/1,
1919
check/1, reset/0, logout/1, session_type_by_id/1,
20-
logout_by_session_name/2, logout_by_session_type/1]).
20+
logout_by_session_name/2, logout_by_session_type/1,
21+
get_authn_res_from_ui_session/1]).
2122

2223
start_link() ->
2324
token_server:start_link(?MODULE, 1024, ?UI_AUTH_EXPIRATION_SECONDS,
@@ -108,3 +109,20 @@ logout_by_session_name(Type, SessionName) ->
108109
logout_by_session_type(Type) ->
109110
Pattern = #uisession{type = Type, _ = '_'},
110111
token_server:purge(?MODULE, Pattern).
112+
113+
-spec get_authn_res_from_ui_session(Id :: string()) -> #authn_res{} | undefined.
114+
get_authn_res_from_ui_session(Id) ->
115+
case ns_node_disco:couchdb_node() == node() of
116+
false ->
117+
SessionBin = list_to_binary(Id),
118+
AuthPattern = #authn_res{type = ui, session_id = SessionBin,
119+
_ = '_'},
120+
MemoPattern = #uisession{authn_res = AuthPattern, _ = '_'},
121+
case token_server:find_memos(?MODULE, MemoPattern) of
122+
[] -> undefined;
123+
[#uisession{authn_res = UiAuthnRes}] -> UiAuthnRes
124+
end;
125+
true ->
126+
rpc:call(ns_node_disco:ns_server_node(),
127+
?MODULE, get_authn_res_from_ui_session, [Id])
128+
end.

src/menelaus_web_rbac.erl

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,8 +1405,8 @@ check_permissions_url_version(Snapshot) ->
14051405
menelaus_roles:params_version(Snapshot)]),
14061406
base64:encode(crypto:hash(sha, B)).
14071407

1408-
get_accessible_buckets(Identity) ->
1409-
Roles = menelaus_roles:get_compiled_roles(Identity),
1408+
get_accessible_buckets(AuthnRes) ->
1409+
Roles = menelaus_roles:get_compiled_roles(AuthnRes),
14101410
lists:filter(
14111411
fun (Bucket) ->
14121412
lists:any(
@@ -1417,9 +1417,12 @@ get_accessible_buckets(Identity) ->
14171417

14181418
handle_get_user_buckets_for_cbauth(Req) ->
14191419
Params = mochiweb_request:parse_qs(Req),
1420-
Identity = {proplists:get_value("user", Params),
1421-
list_to_existing_atom(proplists:get_value("domain", Params))},
1422-
Buckets = [list_to_binary(B) || B <- get_accessible_buckets(Identity)],
1420+
User = proplists:get_value("user", Params),
1421+
Domain = list_to_existing_atom(proplists:get_value("domain", Params)),
1422+
Context = proplists:get_value("context", Params, undefined),
1423+
AuthnRes = menelaus_auth:get_authn_res_from_on_behalf_of(User, Domain,
1424+
Context),
1425+
Buckets = [list_to_binary(B) || B <- get_accessible_buckets(AuthnRes)],
14231426
menelaus_util:reply_json(Req, Buckets, 200).
14241427

14251428
handle_get_user_uuid_for_cbauth(Req) ->
@@ -1433,13 +1436,15 @@ handle_get_user_uuid_for_cbauth(Req) ->
14331436

14341437
handle_check_permission_for_cbauth(Req) ->
14351438
Params = mochiweb_request:parse_qs(Req),
1436-
Identity = {proplists:get_value("user", Params),
1437-
list_to_existing_atom(proplists:get_value("domain", Params))},
14381439
RawPermission = proplists:get_value("permission", Params),
14391440
Permission = parse_permission(string:trim(RawPermission)),
1441+
User = proplists:get_value("user", Params),
1442+
Domain = list_to_existing_atom(proplists:get_value("domain", Params)),
1443+
Context = proplists:get_value("context", Params, undefined),
1444+
AuthnRes = menelaus_auth:get_authn_res_from_on_behalf_of(User, Domain,
1445+
Context),
14401446

1441-
case menelaus_roles:is_allowed(Permission,
1442-
#authn_res{identity = Identity}) of
1447+
case menelaus_roles:is_allowed(Permission, AuthnRes) of
14431448
true ->
14441449
menelaus_util:reply_text(Req, "", 200);
14451450
false ->

0 commit comments

Comments
 (0)