diff --git a/rebar.config b/rebar.config index 71d77f9..90a306b 100644 --- a/rebar.config +++ b/rebar.config @@ -18,16 +18,7 @@ {datastar, {git, "https://github.com/Taure/datastar.git", {ref, "fcc11f399294b305620a7c99ed633b898ce3889b"}}}, - {telemetry, "1.4.2"}, - {nova_auth_oidc, {git, "https://github.com/novaframework/nova_auth_oidc.git", {tag, "v0.1.0"}}}, - %% nova_auth (Hex 0.1.1, and novaframework main) still ships the - %% nova_auth_oidc/_jwt modules extracted into nova_auth_oidc, so both apps - %% define them -> duplicate-module errors (relx + dialyzer). This fork drops - %% the stale copies; pinned to a SHA. Upstream fix: novaframework/nova_auth#11 - %% - swap back to Hex once it merges + releases. - {nova_auth, - {git, "https://github.com/Taure/nova_auth.git", - {ref, "1e3423f44a936f3060d863f3241352d58d95b46b"}}} + {telemetry, "1.4.2"} ]}. {relx, [ diff --git a/rebar.lock b/rebar.lock index efc3005..ef4e822 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,6 +1,5 @@ {"1.2.0", [{<<"accept">>,{pkg,<<"accept">>,<<"0.3.7">>},3}, - {<<"backoff">>,{pkg,<<"backoff">>,<<"1.1.6">>},3}, {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.15.0">>},1}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.1">>},2}, {<<"datastar">>, @@ -30,78 +29,44 @@ {ref,"d17c09246b612328121946197ced9df3caf3a31a"}}, 0}, {<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.11.2">>},1}, - {<<"jose">>,{pkg,<<"jose">>,<<"1.11.12">>},2}, - {<<"kura">>,{pkg,<<"kura">>,<<"1.14.1">>},1}, {<<"nova">>, {git,"https://github.com/Taure/nova.git", {ref,"aad5296d579e34ed71ac6203c03f60eb19e8ca1e"}}, 0}, - {<<"nova_auth">>, - {git,"https://github.com/Taure/nova_auth.git", - {ref,"1e3423f44a936f3060d863f3241352d58d95b46b"}}, - 0}, - {<<"nova_auth_oidc">>, - {git,"https://github.com/novaframework/nova_auth_oidc.git", - {ref,"ec020c9f55cfc0c73a8f0e0345e286f1f9d7cfd9"}}, - 0}, - {<<"oidcc">>,{pkg,<<"oidcc">>,<<"3.7.2">>},1}, - {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.5.0">>},3}, - {<<"pg_types">>,{pkg,<<"pg_types">>,<<"0.6.0">>},3}, - {<<"pgo">>,{pkg,<<"pgo">>,<<"0.20.0">>},2}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"6.1.2">>},1}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.2.0">>},1}, {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.15">>},2}, {<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},2}, {<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.11">>},1}, - {<<"seki">>,{pkg,<<"seki">>,<<"0.4.3">>},1}, {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.4.2">>},0}, - {<<"telemetry_registry">>,{pkg,<<"telemetry_registry">>,<<"0.3.2">>},2}, {<<"thoas">>,{pkg,<<"thoas">>,<<"1.2.1">>},1}]}. [ {pkg_hash,[ {<<"accept">>, <<"CD6E34A2D7E28CA38B2D3CB233734CA0C221EFBC1F171F91FEC5F162CC2D18DA">>}, - {<<"backoff">>, <<"83B72ED2108BA1EE8F7D1C22E0B4A00CFE3593A67DBC792799E8CCE9F42F796B">>}, {<<"cowboy">>, <<"9CFE86ED7117BF045E10ADBEDB0170AF7BE57F2A3637E7BE143433D8DD267396">>}, {<<"cowlib">>, <<"318D385D55F657E9A5005838C4E426E13DCD724A691438384B6165A69687E531">>}, {<<"ddskerl">>, <<"BB97B90EEF1B5906520CEBDD38DA29B18A770B5567F6297927F53566EAE9BEBB">>}, {<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>}, {<<"jhn_stdlib">>, <<"785074F3CA368EAA8E9AF1592BC19AE9EF1F7AF30B2CD6456A6083173A8F5CCB">>}, - {<<"jose">>, <<"06E62B467B61D3726CBC19E9B5489F7549C37993DE846DFB3EE8259F9ED208B3">>}, - {<<"kura">>, <<"081A4B13169CE75427B85A04FC2434B2C3228A8774F6B5B2052014A0475CB697">>}, - {<<"oidcc">>, <<"2047949832CA7984D6D9C218CC5F23E8096BF50EBB809124D3A01673EE2BFE12">>}, - {<<"opentelemetry_api">>, <<"1A676F3E3340CAB81C763E939A42E11A70C22863F645AA06AAFEFC689B5550CF">>}, - {<<"pg_types">>, <<"B530C56330A59288BE49CDD27739EC025B6E877A475AE70BF95B04F1E3A817C4">>}, - {<<"pgo">>, <<"4F4A1FCB0A4894311BE238195BDAD4DF80312AB091DB87FE5348FAFA4DA75F87">>}, {<<"prometheus">>, <<"E5C3C567CD8B0994425920763405635211183C15BDC47D0CD524807DDE20CA1D">>}, {<<"prometheus_cowboy">>, <<"526F75D9850A9125496F78BCEECCA0F237BC7B403C976D44508543AE5967DAD9">>}, {<<"prometheus_httpd">>, <<"8F767D819A5D36275EAB9264AFF40D87279151646776069BF69FBDBBD562BD75">>}, {<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>}, {<<"routing_tree">>, <<"72ACEF2095F0EC804F7AFD07EF781DDE5009425A1CA0A28F0706B1DB334A4812">>}, - {<<"seki">>, <<"3A9209AD6154DA08084E8409C8F97586C79855450A748D989EDF23483B90B8B8">>}, {<<"telemetry">>, <<"A0CB522801DFFB1C49FE6E30561BADFFC7B6D0E180DB1300DF759FAA22062855">>}, - {<<"telemetry_registry">>, <<"701576890320BE6428189BFF963E865E8F23E0FF3615EADE8F78662BE0FC003C">>}, {<<"thoas">>, <<"19A25F31177A17E74004D4840F66D791D4298C5738790FA2CC73731EB911F195">>}]}, {pkg_hash_ext,[ {<<"accept">>, <<"CA69388943F5DAD2E7232A5478F16086E3C872F48E32B88B378E1885A59F5649">>}, - {<<"backoff">>, <<"CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39">>}, {<<"cowboy">>, <<"179FB65140FB440A17B767AD53B755081506F9596C4DB5C49C0396D8C8643668">>}, {<<"cowlib">>, <<"58F1E425A9E04176F1D30E20116F57C4E90EF0E187552E9741C465BDF4044F70">>}, {<<"ddskerl">>, <<"4E0F6047C6A002CE38F6EA155276DD918CD635FD0A5EDB9E0B46AEB6F7AFF2C2">>}, {<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>}, {<<"jhn_stdlib">>, <<"2329CD16DEE46704AAB6184D09508E59DBA31C4D3255271DBB7D34D115ECA508">>}, - {<<"jose">>, <<"31E92B653E9210B696765CDD885437457DE1ADD2A9011D92F8CF63E4641BAB7B">>}, - {<<"kura">>, <<"D0769BCCB80897E42815E874F3AD12BE21FEB381FA853B9BA576E05E92818F98">>}, - {<<"oidcc">>, <<"E3F1ED91509FDEB31EC8B9DE4ECDA0E80CB68B463A9F5B7A9EE1EE40E521E445">>}, - {<<"opentelemetry_api">>, <<"F53EC8A1337AE4A487D43AC89DA4BD3A3C99DDF576655D071DEED8B56A2D5DDA">>}, - {<<"pg_types">>, <<"9949A4849DD13408FA249AB7B745E0D2DFDB9532AEE2B9722326E33CD082A778">>}, - {<<"pgo">>, <<"2F11E6649CEB38E569EF56B16BE1D04874AE5B11A02867080A2817CE423C683B">>}, {<<"prometheus">>, <<"109662001328112B860DE112D49B575310BE8FF33FE04BF0482EEC4B1E5A1278">>}, {<<"prometheus_cowboy">>, <<"2C7EB12F4B970D91E3B47BAAD0F138F6ADC34E53EEB0AE18068FF0AFAB441B24">>}, {<<"prometheus_httpd">>, <<"67736D000745184D5013C58A63E947821AB90CB9320BC2E6AE5D3061C6FFE039">>}, {<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>}, {<<"routing_tree">>, <<"85982C7AC502892C5179CD2A591331003BACD2D2A71723640BA7D23F45408E6E">>}, - {<<"seki">>, <<"5493BAC387329DE722AC5BCB967364CACECC104AE3E6ED143863453263DC6DB4">>}, {<<"telemetry">>, <<"928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1">>}, - {<<"telemetry_registry">>, <<"E7ED191EB1D115A3034AF8E1E35E4E63D5348851D556646D46CA3D1B4E16BAB9">>}, {<<"thoas">>, <<"E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A">>}]} ]. diff --git a/src/triagebot.app.src b/src/triagebot.app.src index e75316a..fbc9249 100644 --- a/src/triagebot.app.src +++ b/src/triagebot.app.src @@ -13,8 +13,6 @@ public_key, telemetry, nova, - nova_auth, - nova_auth_oidc, datastar, gakudan, gakudan_tickets, diff --git a/src/triagebot_app.erl b/src/triagebot_app.erl index fff39ee..a932e60 100644 --- a/src/triagebot_app.erl +++ b/src/triagebot_app.erl @@ -3,14 +3,11 @@ -behaviour(application). --include_lib("kernel/include/logger.hrl"). - -export([start/2, stop/1]). start(_StartType, _StartArgs) -> ok = triagebot_config:setup(), maybe_start_metrics_listener(), - ok = maybe_start_oidc(), triagebot_sup:start_link(). stop(_State) -> @@ -24,17 +21,3 @@ maybe_start_metrics_listener() -> {ok, _} = gakudan_metrics:start_listener(Port), ok end. - -maybe_start_oidc() -> - case os:getenv("TRIAGEBOT_OIDC_CLIENT_ID") of - Empty when Empty =:= false; Empty =:= "" -> - ?LOG_WARNING(#{ - event => dashboard_oidc_disabled, - detail => - ~"TRIAGEBOT_OIDC_CLIENT_ID not set; the live dashboard is mounted but login will not work" - }), - ok; - _ -> - _ = nova_auth_oidc:ensure_providers(triagebot_oidc_config), - ok - end. diff --git a/src/triagebot_auth_controller.erl b/src/triagebot_auth_controller.erl deleted file mode 100644 index 85aa803..0000000 --- a/src/triagebot_auth_controller.erl +++ /dev/null @@ -1,23 +0,0 @@ --module(triagebot_auth_controller). --moduledoc """ -Adapter in front of `nova_auth_oidc_controller`. - -This Nova build keys route bindings by the path-segment binary (`~"provider"`), -but `nova_auth_oidc_controller` pattern-matches an atom `provider` binding and -reads `auth_mod` from the request. We normalise the binding to an atom and pin -the OIDC config module, then delegate. Remove once nova_auth_oidc accepts -binary-keyed bindings upstream. -""". - --export([login/1, callback/1, adapt/1]). - -login(Req) -> - nova_auth_oidc_controller:login(adapt(Req)). - -callback(Req) -> - nova_auth_oidc_controller:callback(adapt(Req)). - -adapt(Req) -> - Bindings = maps:get(bindings, Req, #{}), - ProviderBin = maps:get(~"provider", Bindings, ~"google"), - Req#{bindings => Bindings#{provider => ProviderBin}, auth_mod => triagebot_oidc_config}. diff --git a/src/triagebot_dashboard_auth.erl b/src/triagebot_dashboard_auth.erl index 701c674..7cfc575 100644 --- a/src/triagebot_dashboard_auth.erl +++ b/src/triagebot_dashboard_auth.erl @@ -1,80 +1,58 @@ -module(triagebot_dashboard_auth). -moduledoc """ -Route security for the triagebot live dashboard. +HTTP Basic auth gate for the triagebot live dashboard. -Gates the `gakudan_liveboard` pages behind an authenticated session and an -email-domain allowlist, so "not everyone can log in": +Credentials come from `TRIAGEBOT_DASHBOARD_USER` / `TRIAGEBOT_DASHBOARD_PASSWORD`. -- no session -> 302 redirect to the OIDC login, -- authenticated but email domain not allowed -> 403, -- authenticated and allowed -> `{true, Actor}`. - -The allowlist is `TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS` (comma-separated, e.g. -`taure.se,example.com`). Empty/unset allows any authenticated user, which is -the right default for a single-org Google Workspace where the OIDC provider is -already the gate. +Closed by default: if either is unset the dashboard denies everything (503), so +it is never accidentally exposed. The username and password are compared in +constant time. Gates both the view routes and the HITL controls. """. --export([check/1, authorize/1]). +-export([check/1]). -spec check(cowboy_req:req()) -> - {true, nova_auth:actor()} | {false, integer(), map(), binary()}. + {true, map()} | {false, 401 | 503, map(), binary()}. check(Req) -> - authorize(nova_auth_actor:fetch(Req)). - --spec authorize({ok, nova_auth:actor()} | {error, not_found}) -> - {true, nova_auth:actor()} | {false, integer(), map(), binary()}. -authorize({ok, Actor}) -> - case allowed(Actor) of - true -> {true, Actor}; - false -> forbidden() - end; -authorize({error, not_found}) -> - login_redirect(). - -allowed(#{email := Email}) when is_binary(Email) -> - case allowed_domains() of - [] -> true; - Domains -> lists:member(domain_of(Email), Domains) - end; -allowed(_Actor) -> - allowed_domains() =:= []. - -domain_of(Email) -> - case binary:split(Email, ~"@") of - [_, Domain] -> string:lowercase(Domain); - _ -> ~"" + case credentials() of + {ok, User, Pass} -> verify(Req, User, Pass); + error -> unconfigured() end. -allowed_domains() -> - case os:getenv("TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS") of - false -> - []; - "" -> - []; - Raw -> - [ - string:lowercase(list_to_binary(string:trim(D))) - || D <- string:split(Raw, ",", all), D =/= "" - ] +credentials() -> + case {os:getenv("TRIAGEBOT_DASHBOARD_USER"), os:getenv("TRIAGEBOT_DASHBOARD_PASSWORD")} of + {User, Pass} when is_list(User), is_list(Pass), User =/= "", Pass =/= "" -> + {ok, list_to_binary(User), list_to_binary(Pass)}; + _ -> + error end. -login_redirect() -> - {false, 302, #{~"location" => login_url()}, ~""}. - -login_url() -> - case os:getenv("TRIAGEBOT_OIDC_PROVIDER") of - false -> ~"/auth/google/login"; - "" -> ~"/auth/google/login"; - Provider -> iolist_to_binary([~"/auth/", Provider, ~"/login"]) +verify(Req, User, Pass) -> + case cowboy_req:parse_header(~"authorization", Req) of + {basic, GivenUser, GivenPass} -> + UserOk = secure_equal(GivenUser, User), + PassOk = secure_equal(GivenPass, Pass), + case UserOk andalso PassOk of + true -> {true, #{user => User}}; + false -> challenge() + end; + _ -> + challenge() end. -forbidden() -> - Body = - ~""" - Not allowed - -

Not allowed

-

Your account is not permitted on this dashboard.

- """, - {false, 403, #{~"content-type" => ~"text/html; charset=utf-8"}, Body}. +%% Constant-time comparison: hash both sides to a fixed length first so +%% crypto:hash_equals/2 never leaks length and never raises on size mismatch. +secure_equal(A, B) -> + crypto:hash_equals(crypto:hash(sha256, A), crypto:hash(sha256, B)). + +challenge() -> + {false, 401, + #{ + ~"www-authenticate" => ~"Basic realm=\"triagebot dashboard\", charset=\"UTF-8\"", + ~"content-type" => ~"text/plain; charset=utf-8" + }, + ~"Authentication required"}. + +unconfigured() -> + {false, 503, #{~"content-type" => ~"text/plain; charset=utf-8"}, + ~"Dashboard auth not configured (set TRIAGEBOT_DASHBOARD_USER and _PASSWORD)"}. diff --git a/src/triagebot_oidc_config.erl b/src/triagebot_oidc_config.erl deleted file mode 100644 index 9cdf342..0000000 --- a/src/triagebot_oidc_config.erl +++ /dev/null @@ -1,40 +0,0 @@ --module(triagebot_oidc_config). --moduledoc """ -OIDC provider config for the triagebot dashboard (nova_auth_oidc behaviour). - -Defaults to Google; configure via env: - -- `TRIAGEBOT_OIDC_CLIENT_ID` / `TRIAGEBOT_OIDC_CLIENT_SECRET` - OAuth client. -- `TRIAGEBOT_OIDC_ISSUER` - override the issuer (default Google). -- `TRIAGEBOT_BASE_URL` - public base URL (default http://localhost:8080); the - OAuth redirect URI is `/auth/google/callback`. - -"Not everyone can log in" is enforced two ways: the provider only issues -tokens to its users, and `triagebot_dashboard_auth` restricts by email domain -(`TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS`). -""". - --behaviour(nova_auth_oidc). - --export([config/0]). - -config() -> - #{ - providers => #{ - google => #{ - issuer => env(~"TRIAGEBOT_OIDC_ISSUER", ~"https://accounts.google.com"), - client_id => env(~"TRIAGEBOT_OIDC_CLIENT_ID", ~""), - client_secret => env(~"TRIAGEBOT_OIDC_CLIENT_SECRET", ~""), - scopes => [~"openid", ~"email", ~"profile"] - } - }, - base_url => env(~"TRIAGEBOT_BASE_URL", ~"http://localhost:8080"), - claims_mapping => #{~"sub" => id, ~"email" => email} - }. - -env(Name, Default) -> - case os:getenv(binary_to_list(Name)) of - false -> Default; - "" -> Default; - Value -> list_to_binary(Value) - end. diff --git a/src/triagebot_router.erl b/src/triagebot_router.erl index b6fd298..a3aca76 100644 --- a/src/triagebot_router.erl +++ b/src/triagebot_router.erl @@ -18,16 +18,6 @@ routes(_Environment) -> {"/assets/[...]", liveboard_assets_dir()} ] }, - #{ - prefix => "/auth", - security => false, - routes => [ - {"/:provider/login", fun triagebot_auth_controller:login/1, #{methods => [get]}}, - {"/:provider/callback", fun triagebot_auth_controller:callback/1, #{ - methods => [get] - }} - ] - }, #{ prefix => "", security => fun triagebot_dashboard_auth:check/1, diff --git a/test/triagebot_dashboard_auth_SUITE.erl b/test/triagebot_dashboard_auth_SUITE.erl index 4add56e..b196408 100644 --- a/test/triagebot_dashboard_auth_SUITE.erl +++ b/test/triagebot_dashboard_auth_SUITE.erl @@ -4,34 +4,33 @@ -export([all/0, init_per_testcase/2, end_per_testcase/2]). -export([ - unauthenticated_redirects_to_login/1, - login_url_honours_provider_env/1, - no_allowlist_allows_any_authenticated/1, - allowlist_permits_listed_domain/1, - allowlist_is_case_insensitive/1, - allowlist_rejects_other_domain/1, - allowlist_rejects_actor_without_email/1, - adapt_normalises_binary_binding/1, - adapt_defaults_provider_to_google/1, - oidc_config_exposes_google_provider/1 + denied_when_unconfigured/1, + challenges_without_credentials/1, + challenge_sets_www_authenticate/1, + accepts_correct_credentials/1, + rejects_wrong_password/1, + rejects_wrong_username/1 ]). +-define(USER, "conductor"). +-define(PASS, "a-very-long-hard-passphrase"). + all() -> [ - unauthenticated_redirects_to_login, - login_url_honours_provider_env, - no_allowlist_allows_any_authenticated, - allowlist_permits_listed_domain, - allowlist_is_case_insensitive, - allowlist_rejects_other_domain, - allowlist_rejects_actor_without_email, - adapt_normalises_binary_binding, - adapt_defaults_provider_to_google, - oidc_config_exposes_google_provider + denied_when_unconfigured, + challenges_without_credentials, + challenge_sets_www_authenticate, + accepts_correct_credentials, + rejects_wrong_password, + rejects_wrong_username ]. -init_per_testcase(_Case, Config) -> +init_per_testcase(denied_when_unconfigured, Config) -> clear_env(), + Config; +init_per_testcase(_Case, Config) -> + os:putenv("TRIAGEBOT_DASHBOARD_USER", ?USER), + os:putenv("TRIAGEBOT_DASHBOARD_PASSWORD", ?PASS), Config. end_per_testcase(_Case, _Config) -> @@ -39,61 +38,39 @@ end_per_testcase(_Case, _Config) -> ok. clear_env() -> - os:unsetenv("TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS"), - os:unsetenv("TRIAGEBOT_OIDC_PROVIDER"), - os:unsetenv("TRIAGEBOT_OIDC_CLIENT_ID"). - -unauthenticated_redirects_to_login(_Config) -> - ?assertEqual( - {false, 302, #{~"location" => ~"/auth/google/login"}, ~""}, - triagebot_dashboard_auth:authorize({error, not_found}) - ). + os:unsetenv("TRIAGEBOT_DASHBOARD_USER"), + os:unsetenv("TRIAGEBOT_DASHBOARD_PASSWORD"). -login_url_honours_provider_env(_Config) -> - os:putenv("TRIAGEBOT_OIDC_PROVIDER", "authentik"), - {false, 302, Headers, _} = triagebot_dashboard_auth:authorize({error, not_found}), - ?assertEqual(~"/auth/authentik/login", maps:get(~"location", Headers)). +denied_when_unconfigured(_Config) -> + ?assertMatch({false, 503, _, _}, triagebot_dashboard_auth:check(req(undefined))). -no_allowlist_allows_any_authenticated(_Config) -> - Actor = #{email => ~"anyone@whatever.com"}, - ?assertEqual({true, Actor}, triagebot_dashboard_auth:authorize({ok, Actor})). +challenges_without_credentials(_Config) -> + ?assertMatch({false, 401, _, _}, triagebot_dashboard_auth:check(req(undefined))). -allowlist_permits_listed_domain(_Config) -> - os:putenv("TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS", "taure.se, example.com"), - Actor = #{email => ~"daniel@taure.se"}, - ?assertEqual({true, Actor}, triagebot_dashboard_auth:authorize({ok, Actor})). +challenge_sets_www_authenticate(_Config) -> + {false, 401, Headers, _} = triagebot_dashboard_auth:check(req(undefined)), + ?assert(maps:is_key(~"www-authenticate", Headers)). -allowlist_is_case_insensitive(_Config) -> - os:putenv("TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS", "Taure.SE"), - Actor = #{email => ~"Daniel@TAURE.se"}, - ?assertEqual({true, Actor}, triagebot_dashboard_auth:authorize({ok, Actor})). - -allowlist_rejects_other_domain(_Config) -> - os:putenv("TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS", "taure.se"), +accepts_correct_credentials(_Config) -> ?assertMatch( - {false, 403, _, _}, - triagebot_dashboard_auth:authorize({ok, #{email => ~"intruder@evil.com"}}) + {true, #{user := <<"conductor">>}}, + triagebot_dashboard_auth:check(req({?USER, ?PASS})) ). -allowlist_rejects_actor_without_email(_Config) -> - os:putenv("TRIAGEBOT_DASHBOARD_ALLOWED_DOMAINS", "taure.se"), +rejects_wrong_password(_Config) -> ?assertMatch( - {false, 403, _, _}, - triagebot_dashboard_auth:authorize({ok, #{id => ~"u1"}}) + {false, 401, _, _}, + triagebot_dashboard_auth:check(req({?USER, "wrong"})) ). -adapt_normalises_binary_binding(_Config) -> - Adapted = triagebot_auth_controller:adapt(#{bindings => #{~"provider" => ~"authentik"}}), - ?assertEqual(~"authentik", maps:get(provider, maps:get(bindings, Adapted))), - ?assertEqual(triagebot_oidc_config, maps:get(auth_mod, Adapted)). - -adapt_defaults_provider_to_google(_Config) -> - Adapted = triagebot_auth_controller:adapt(#{bindings => #{}}), - ?assertEqual(~"google", maps:get(provider, maps:get(bindings, Adapted))). +rejects_wrong_username(_Config) -> + ?assertMatch( + {false, 401, _, _}, + triagebot_dashboard_auth:check(req({"intruder", ?PASS})) + ). -oidc_config_exposes_google_provider(_Config) -> - os:putenv("TRIAGEBOT_OIDC_CLIENT_ID", "abc.apps.googleusercontent.com"), - Cfg = triagebot_oidc_config:config(), - Google = maps:get(google, maps:get(providers, Cfg)), - ?assertEqual(~"abc.apps.googleusercontent.com", maps:get(client_id, Google)), - ?assertEqual(#{~"sub" => id, ~"email" => email}, maps:get(claims_mapping, Cfg)). +req(undefined) -> + #{headers => #{}}; +req({User, Pass}) -> + Cred = base64:encode(iolist_to_binary([User, ":", Pass])), + #{headers => #{~"authorization" => <<"Basic ", Cred/binary>>}}.