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"
"crypto/subtle"
"errors"
"fmt"
"net"
"net/http"
"time"
@ -30,8 +31,9 @@ import (
// Config defines configuration for debug server.
type Config struct {
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:""`
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:""`
AllowedOauthHost string `help:"the oauth host allowed to bypass token authentication."`
AuthorizationToken string `internal:"true"`
}
@ -87,7 +89,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
root := mux.NewRouter()
api := root.PathPrefix("/api/").Subrouter()
api.Use(allowedAuthorization(config.AuthorizationToken))
api.Use(allowedAuthorization(log, config))
// When adding new options, also update README.md
api.HandleFunc("/users", server.addUser).Methods("POST")
@ -159,24 +161,36 @@ func (server *Server) Close() error {
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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token == "" {
sendJSONError(w, "Authorization not enabled.",
"", http.StatusForbidden)
return
if r.Host != config.AllowedOauthHost {
// not behind the proxy; use old authentication method.
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(
[]byte(r.Header.Get("Authorization")),
[]byte(token),
log.Info(
"admin action",
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")
next.ServeHTTP(w, r)

View File

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

View File

@ -408,7 +408,7 @@ Blank fields will not be updated.`,
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;
}
@ -419,9 +419,11 @@ Blank fields will not be updated.`,
data?: Record<string, unknown>
): Promise<Record<string, unknown> | null> {
const url = this.apiURL(path, query);
const headers = new window.Headers({
Authorization: this.authToken
});
const headers = new window.Headers();
if (this.authToken) {
headers.set('Authorization', this.authToken);
}
let body: string;
if (data) {

View File

@ -1,6 +1,9 @@
# admin peer http listening 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
# admin.static-dir: ""