From 84291746af5fc863f90bcf7ae9ba5a2d3ca26cdd Mon Sep 17 00:00:00 2001 From: Gareth George Date: Sat, 23 Mar 2024 17:48:10 +0000 Subject: [PATCH] feat: allow disabling authentication --- gen/go/v1/config.pb.go | 36 ++++++++++++-------- internal/auth/auth.go | 40 ++++++++++++++--------- internal/auth/middleware.go | 10 ++++++ proto/v1/config.proto | 1 + webui/gen/ts/v1/authentication_connect.ts | 2 +- webui/gen/ts/v1/config_pb.ts | 8 +++++ webui/gen/ts/v1/service_connect.ts | 2 +- webui/src/index.tsx | 6 ++-- webui/src/views/App.tsx | 7 ++-- webui/src/views/SettingsModal.tsx | 31 +++++++++--------- 10 files changed, 92 insertions(+), 51 deletions(-) diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 050d59c7..d2d92990 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -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 ( diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 3d4307ca..cc8cb911 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -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 } diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go index 22ce652b..8452c4ed 100644 --- a/internal/auth/middleware.go +++ b/internal/auth/middleware.go @@ -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 { diff --git a/proto/v1/config.proto b/proto/v1/config.proto index e2b0ce11..cd752fce 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -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. } diff --git a/webui/gen/ts/v1/authentication_connect.ts b/webui/gen/ts/v1/authentication_connect.ts index 3b3e83be..bc813d13 100644 --- a/webui/gen/ts/v1/authentication_connect.ts +++ b/webui/gen/ts/v1/authentication_connect.ts @@ -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 diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index c8788f60..18c37df4 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -876,6 +876,13 @@ export class Hook_Slack extends Message { * @generated from message v1.Auth */ export class Auth extends Message { + /** + * 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 { 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 }, ]); diff --git a/webui/gen/ts/v1/service_connect.ts b/webui/gen/ts/v1/service_connect.ts index 4816f668..4def3948 100644 --- a/webui/gen/ts/v1/service_connect.ts +++ b/webui/gen/ts/v1/service_connect.ts @@ -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 diff --git a/webui/src/index.tsx b/webui/src/index.tsx index d694d33e..7656d7cd 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -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 && diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 55f8c0e5..fe371398 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -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(); + if (!config.auth || (!config.auth.disabled && config.auth.users.length === 0)) { + import("./SettingsModal").then(({ SettingsModal }) => { + showModal(); + }); } else { showModal(null); } diff --git a/webui/src/views/SettingsModal.tsx b/webui/src/views/SettingsModal.tsx index 9b9c5412..70ddecc0 100644 --- a/webui/src/views/SettingsModal.tsx +++ b/webui/src/views/SettingsModal.tsx @@ -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 = () => {

)} + + + { - 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 }) => ( <>