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:
parent
9a0c2dd878
commit
c1ed5c06e8
@ -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)
|
||||||
|
@ -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}
|
|
||||||
|
@ -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) {
|
||||||
|
3
scripts/testdata/satellite-config.yaml.lock
vendored
3
scripts/testdata/satellite-config.yaml.lock
vendored
@ -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: ""
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user