Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand Down
35 changes: 0 additions & 35 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -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">>,
Expand Down Expand Up @@ -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">>}]}
].
2 changes: 0 additions & 2 deletions src/triagebot.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
public_key,
telemetry,
nova,
nova_auth,
nova_auth_oidc,
datastar,
gakudan,
gakudan_tickets,
Expand Down
17 changes: 0 additions & 17 deletions src/triagebot_app.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand All @@ -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.
23 changes: 0 additions & 23 deletions src/triagebot_auth_controller.erl

This file was deleted.

108 changes: 43 additions & 65 deletions src/triagebot_dashboard_auth.erl
Original file line number Diff line number Diff line change
@@ -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 =
~"""
<!doctype html><meta charset="utf-8"><title>Not allowed</title>
<body style="font-family:system-ui;background:#14110d;color:#e8dfce;display:grid;place-items:center;height:100vh;margin:0">
<main style="text-align:center"><h1 style="font-weight:400">Not allowed</h1>
<p>Your account is not permitted on this dashboard.</p></main>
""",
{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)"}.
40 changes: 0 additions & 40 deletions src/triagebot_oidc_config.erl

This file was deleted.

10 changes: 0 additions & 10 deletions src/triagebot_router.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading