feat: allow disabling authentication

This commit is contained in:
Gareth George
2024-03-23 17:48:10 +00:00
parent 75776fa7d0
commit 84291746af
10 changed files with 92 additions and 51 deletions
+23 -13
View File
@@ -761,7 +761,8 @@ type Auth struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Users []*User `protobuf:"bytes,2,rep,name=users,proto3" json:"users,omitempty"` // users to allow access to the UI.
Disabled bool `protobuf:"varint,1,opt,name=disabled,proto3" json:"disabled,omitempty"` // disable authentication.
Users []*User `protobuf:"bytes,2,rep,name=users,proto3" json:"users,omitempty"` // users to allow access to the UI.
}
func (x *Auth) Reset() {
@@ -796,6 +797,13 @@ func (*Auth) Descriptor() ([]byte, []int) {
return file_v1_config_proto_rawDescGZIP(), []int{6}
}
func (x *Auth) GetDisabled() bool {
if x != nil {
return x.Disabled
}
return false
}
func (x *Auth) GetUsers() []*User {
if x != nil {
return x.Users
@@ -1384,18 +1392,20 @@ var file_v1_config_proto_rawDesc = []byte{
0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e,
0x44, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e,
0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10,
0x04, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x26, 0x0a, 0x04, 0x41,
0x75, 0x74, 0x68, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73,
0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79,
0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73,
0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67,
0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67,
0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x04, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41,
0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12,
0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08,
0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22,
0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61,
0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
+25 -15
View File
@@ -12,21 +12,24 @@ import (
"golang.org/x/crypto/bcrypt"
)
var defaultUsers = []*v1.User{
{
var (
anonymousUser = &v1.User{
Name: "default",
Password: &v1.User_PasswordBcrypt{PasswordBcrypt: "JDJhJDEwJDNCdzJoNFlhaWFZQy9TSDN3ZGxSRHVPZHdzV2lsNmtBSHdFSmtIWHk1dS8wYjZuUWJrMGFx"}, // default password is "password"
},
}
}
defaultUsers = []*v1.User{
anonymousUser,
}
)
type Authenticator struct {
config config.ConfigStore
key []byte
}
func NewAuthenticator(key []byte, configProvider config.ConfigStore) *Authenticator {
func NewAuthenticator(key []byte, config config.ConfigStore) *Authenticator {
return &Authenticator{
config: configProvider,
config: config,
key: key,
}
}
@@ -34,19 +37,17 @@ func NewAuthenticator(key []byte, configProvider config.ConfigStore) *Authentica
var ErrUserNotFound = errors.New("user not found")
var ErrInvalidPassword = errors.New("invalid password")
func (a *Authenticator) users() []*v1.User {
func (a *Authenticator) Login(username, password string) (*v1.User, error) {
config, err := a.config.Get()
if err != nil {
return nil
return nil, fmt.Errorf("get config: %w", err)
}
if len(config.Auth.GetUsers()) != 0 {
return config.Auth.GetUsers()
auth := config.GetAuth()
if auth.GetDisabled() {
return nil, errors.New("authentication is disabled")
}
return defaultUsers
}
func (a *Authenticator) Login(username, password string) (*v1.User, error) {
for _, user := range a.users() {
for _, user := range auth.GetUsers() {
if user.Name != username {
continue
}
@@ -62,6 +63,15 @@ func (a *Authenticator) Login(username, password string) (*v1.User, error) {
}
func (a *Authenticator) VerifyJWT(token string) (*v1.User, error) {
config, err := a.config.Get()
if err != nil {
return nil, fmt.Errorf("get config: %w", err)
}
auth := config.GetAuth()
if auth == nil {
return nil, fmt.Errorf("auth config not set")
}
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return a.key, nil
})
@@ -78,7 +88,7 @@ func (a *Authenticator) VerifyJWT(token string) (*v1.User, error) {
return nil, fmt.Errorf("get subject: %w", err)
}
for _, user := range a.users() {
for _, user := range auth.GetUsers() {
if user.Name == subject {
return user, nil
}
+10
View File
@@ -17,6 +17,16 @@ const UserContextKey contextKey = "user"
func RequireAuthentication(h http.Handler, auth *Authenticator) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
config, err := auth.config.Get()
if err != nil {
zap.S().Errorf("auth middleware failed to get config: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if config.GetAuth() == nil || config.GetAuth().GetDisabled() {
h.ServeHTTP(w, r)
return
}
username, password, usesBasicAuth := r.BasicAuth()
if usesBasicAuth {
+1
View File
@@ -120,6 +120,7 @@ message Hook {
}
message Auth {
bool disabled = 1 [json_name="disabled"]; // disable authentication.
repeated User users = 2 [json_name="users"]; // users to allow access to the UI.
}
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-connect-es v1.3.0 with parameter "target=ts"
// @generated by protoc-gen-connect-es v1.2.0 with parameter "target=ts"
// @generated from file v1/authentication.proto (package v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
+8
View File
@@ -876,6 +876,13 @@ export class Hook_Slack extends Message<Hook_Slack> {
* @generated from message v1.Auth
*/
export class Auth extends Message<Auth> {
/**
* disable authentication.
*
* @generated from field: bool disabled = 1;
*/
disabled = false;
/**
* users to allow access to the UI.
*
@@ -891,6 +898,7 @@ export class Auth extends Message<Auth> {
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "v1.Auth";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "disabled", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
{ no: 2, name: "users", kind: "message", T: User, repeated: true },
]);
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-connect-es v1.3.0 with parameter "target=ts"
// @generated by protoc-gen-connect-es v1.2.0 with parameter "target=ts"
// @generated from file v1/service.proto (package v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
+3 -3
View File
@@ -22,7 +22,7 @@ const Root = ({ children }: { children: React.ReactNode }) => {
);
};
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
const el = document.querySelector("#app");
el &&
@@ -30,7 +30,7 @@ el &&
<AntdConfigProvider
theme={{
algorithm: [
darkThemeMq.matches ? theme.darkAlgorithm : theme.defaultAlgorithm,
darkTheme ? theme.darkAlgorithm : theme.defaultAlgorithm,
theme.compactAlgorithm,
],
}}
@@ -38,7 +38,7 @@ el &&
<StyledEngineProvider injectFirst>
<ThemeProvider theme={createTheme({
palette: {
mode: darkThemeMq ? "dark" : "light"
mode: darkTheme ? "dark" : "light"
},
})}>
<Root>
+4 -3
View File
@@ -21,7 +21,6 @@ import LogoSvg from "url:../../assets/logo.svg";
import _ from "lodash";
import { Code } from "@connectrpc/connect";
import { LoginModal } from "./LoginModal";
import { SettingsModal } from "./SettingsModal";
import { backrestService, setAuthToken } from "../api";
import { MainContentArea, useSetContent } from "./MainContentArea";
import { GettingStartedGuide } from "./GettingStartedGuide";
@@ -44,8 +43,10 @@ export const App: React.FC = () => {
backrestService.getConfig({})
.then((config) => {
setConfig(config);
if (!config.auth || config.auth.users.length === 0) {
showModal(<SettingsModal />);
if (!config.auth || (!config.auth.disabled && config.auth.users.length === 0)) {
import("./SettingsModal").then(({ SettingsModal }) => {
showModal(<SettingsModal />);
});
} else {
showModal(null);
}
+16 -15
View File
@@ -12,6 +12,7 @@ import {
Card,
Col,
Collapse,
Checkbox,
} from "antd";
import React, { useEffect, useState } from "react";
import { useShowModal } from "../components/ModalManager";
@@ -48,11 +49,13 @@ export const SettingsModal = () => {
// Validate form
let formData = await validateForm(form);
for (const user of formData.auth?.users) {
if (user.needsBcrypt) {
const hash = await authenticationService.hashPassword({ value: user.passwordBcrypt });
user.passwordBcrypt = hash.value;
delete user.needsBcrypt;
if (formData.auth?.users) {
for (const user of formData.auth?.users) {
if (user.needsBcrypt) {
const hash = await authenticationService.hashPassword({ value: user.passwordBcrypt });
user.passwordBcrypt = hash.value;
delete user.needsBcrypt;
}
}
}
@@ -60,6 +63,10 @@ export const SettingsModal = () => {
let newConfig = config!.clone();
newConfig.auth = new Auth().fromJson(formData.auth, { ignoreUnknownFields: false });
if (!newConfig.auth?.users && !newConfig.auth?.disabled) {
throw new Error("At least one user must be configured or authentication must be disabled");
}
setConfig(await backrestService.setConfig(newConfig));
alertsApi.success("Settings updated", 5);
setTimeout(() => {
@@ -114,19 +121,13 @@ export const SettingsModal = () => {
</p>
</>
)}
<Form.Item label="Disable Authentication" name={["auth", "disabled"]} valuePropName="checked" initialValue={config.auth?.disabled || false}>
<Checkbox />
</Form.Item>
<Form.Item label="Users" required={true}>
<Form.List
name={["auth", "users"]}
rules={[
{
validator: async (_, users) => {
if (!users || users.length < 1) {
return Promise.reject(new Error("At least one user is required"));
}
},
},
]}
initialValue={configObj.auth?.users || []}
initialValue={config.auth?.users || []}
>
{(fields, { add, remove }) => (
<>