Not allowed
-Your account is not permitted on this dashboard.
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 = - ~""" -
Your account is not permitted on this dashboard.