mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-05-29 16:00:57 +00:00
feat: allow disabling authentication
This commit is contained in:
+23
-13
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 }) => (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user