satellite/{admin,ui}: implement changes for oauth2 proxy

We want to put the OAuth2 proxy, https://github.com/oauth2-proxy/oauth2-proxy, in front of the satellite admin ui.
This change implements the changes required/necessary for this to work.

Issue: https://github.com/storj/storj/issues/5072

Change-Id: I6da0df090cc6f0c18f1bf41e48ae082493f53f20
This commit is contained in:
Wilfred Asomani 2022-11-10 13:19:42 +00:00 committed by Storj Robot
parent 9a0c2dd878
commit c1ed5c06e8
4 changed files with 73 additions and 58 deletions

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"crypto/subtle" "crypto/subtle"
"errors" "errors"
"fmt"
"net" "net"
"net/http" "net/http"
"time" "time"
@ -30,8 +31,9 @@ import (
// Config defines configuration for debug server. // Config defines configuration for debug server.
type Config struct { type Config struct {
Address string `help:"admin peer http listening address" releaseDefault:"" devDefault:""` Address string `help:"admin peer http listening address" releaseDefault:"" devDefault:""`
StaticDir string `help:"an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets" releaseDefault:"" devDefault:""` StaticDir string `help:"an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets" releaseDefault:"" devDefault:""`
AllowedOauthHost string `help:"the oauth host allowed to bypass token authentication."`
AuthorizationToken string `internal:"true"` AuthorizationToken string `internal:"true"`
} }
@ -87,7 +89,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
root := mux.NewRouter() root := mux.NewRouter()
api := root.PathPrefix("/api/").Subrouter() api := root.PathPrefix("/api/").Subrouter()
api.Use(allowedAuthorization(config.AuthorizationToken)) api.Use(allowedAuthorization(log, config))
// When adding new options, also update README.md // When adding new options, also update README.md
api.HandleFunc("/users", server.addUser).Methods("POST") api.HandleFunc("/users", server.addUser).Methods("POST")
@ -159,24 +161,36 @@ func (server *Server) Close() error {
return Error.Wrap(server.server.Close()) return Error.Wrap(server.server.Close())
} }
func allowedAuthorization(token string) func(next http.Handler) http.Handler { func allowedAuthorization(log *zap.Logger, config Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token == "" {
sendJSONError(w, "Authorization not enabled.", if r.Host != config.AllowedOauthHost {
"", http.StatusForbidden) // not behind the proxy; use old authentication method.
return if config.AuthorizationToken == "" {
sendJSONError(w, "Authorization not enabled.",
"", http.StatusForbidden)
return
}
equality := subtle.ConstantTimeCompare(
[]byte(r.Header.Get("Authorization")),
[]byte(config.AuthorizationToken),
)
if equality != 1 {
sendJSONError(w, "Forbidden",
"", http.StatusForbidden)
return
}
} }
equality := subtle.ConstantTimeCompare( log.Info(
[]byte(r.Header.Get("Authorization")), "admin action",
[]byte(token), zap.String("host", r.Host),
zap.String("user", r.Header.Get("X-Forwarded-Email")),
zap.String("action", fmt.Sprintf("%s-%s", r.Method, r.RequestURI)),
zap.String("queries", r.URL.Query().Encode()),
) )
if equality != 1 {
sendJSONError(w, "Forbidden",
"", http.StatusForbidden)
return
}
r.Header.Set("Cache-Control", "must-revalidate") r.Header.Set("Cache-Control", "must-revalidate")
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@ -16,7 +16,7 @@ wants to perform.
import UIGen from '$lib/UIGenerator.svelte'; import UIGen from '$lib/UIGenerator.svelte';
const baseURL = `${window.location.protocol}//${window.location.host}/api`; const baseURL = `${window.location.protocol}//${window.location.host}/api`;
let api: Admin; let api: Admin = new Admin(baseURL);
let selectedGroupOp: Operation[]; let selectedGroupOp: Operation[];
let selectedOp: Operation; let selectedOp: Operation;
let authToken: string; let authToken: string;
@ -25,21 +25,19 @@ wants to perform.
if (authToken) { if (authToken) {
api = new Admin(baseURL, authToken); api = new Admin(baseURL, authToken);
} else { } else {
api = null; api = new Admin(baseURL);
} }
} }
</script> </script>
<p> <p>
In order to use the API you have to set the authentication token in the input box and press enter If you did not log in using Oauth (e.g. with Google), you have to set the authentication token in
or click the "confirm" button. the input box and press enter or click the "confirm" button.
</p> </p>
<p> <p>
Token: <input Token: <input
bind:value={authToken} bind:value={authToken}
on:focus={() => { on:focus={() => {
api = null;
// This allows to select the empty item of the second select. // This allows to select the empty item of the second select.
selectedOp = null; selectedOp = null;
}} }}
@ -52,38 +50,36 @@ wants to perform.
<button on:click={confirmAuthToken}>confirm</button> <button on:click={confirmAuthToken}>confirm</button>
</p> </p>
{#if api} <p>
<p> Operation:
Operation: <select
<select bind:value={selectedGroupOp}
bind:value={selectedGroupOp} on:change={() => {
on:change={() => { // This allows hiding the UIGen component when this select change until
// This allows hiding the UIGen component when this select change until // a new operations is selected in the following select element and also
// a new operations is selected in the following select element and also // selecting the empty item of the select.
// selecting the empty item of the select. selectedOp = null;
selectedOp = null; }}
}} >
> <option selected />
{#each Object.keys(api.operations) as group}
<option value={api.operations[group]}>{group}</option>
{/each}
</select>
{#if selectedGroupOp}
<select bind:value={selectedOp}>
<option selected /> <option selected />
{#each Object.keys(api.operations) as group} {#each selectedGroupOp as op (op)}
<option value={api.operations[group]}>{group}</option> <option value={op}>{op.name}</option>
{/each} {/each}
</select> </select>
{#if selectedGroupOp} {/if}
<select bind:value={selectedOp}> </p>
<option selected /> <hr />
{#each selectedGroupOp as op (op)} <p>
<option value={op}>{op.name}</option> {#if selectedOp}
{/each} {#key selectedOp}
</select> <UIGen operation={selectedOp} />
{/if} {/key}
</p> {/if}
<hr /> </p>
<p>
{#if selectedOp}
{#key selectedOp}
<UIGen operation={selectedOp} />
{/key}
{/if}
</p>
{/if}

View File

@ -408,7 +408,7 @@ Blank fields will not be updated.`,
private readonly baseURL: string; private readonly baseURL: string;
constructor(baseURL: string, private readonly authToken: string) { constructor(baseURL: string, private readonly authToken: string = '') {
this.baseURL = baseURL.endsWith('/') ? baseURL.substring(0, baseURL.length - 1) : baseURL; this.baseURL = baseURL.endsWith('/') ? baseURL.substring(0, baseURL.length - 1) : baseURL;
} }
@ -419,9 +419,11 @@ Blank fields will not be updated.`,
data?: Record<string, unknown> data?: Record<string, unknown>
): Promise<Record<string, unknown> | null> { ): Promise<Record<string, unknown> | null> {
const url = this.apiURL(path, query); const url = this.apiURL(path, query);
const headers = new window.Headers({ const headers = new window.Headers();
Authorization: this.authToken
}); if (this.authToken) {
headers.set('Authorization', this.authToken);
}
let body: string; let body: string;
if (data) { if (data) {

View File

@ -1,6 +1,9 @@
# admin peer http listening address # admin peer http listening address
# admin.address: "" # admin.address: ""
# the oauth host allowed to bypass token authentication.
# admin.allowed-oauth-host: ""
# an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets # an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets
# admin.static-dir: "" # admin.static-dir: ""