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