Skip to content

Commit

Permalink
feat: allow disabling authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Mar 23, 2024
1 parent 75776fa commit 8429174
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 51 deletions.
36 changes: 23 additions & 13 deletions gen/go/v1/config.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 25 additions & 15 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,42 @@ 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,
}
}

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
}
Expand All @@ -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
})
Expand All @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions internal/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions proto/v1/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand Down
2 changes: 1 addition & 1 deletion webui/gen/ts/v1/authentication_connect.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions webui/gen/ts/v1/config_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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 },
]);

Expand Down
2 changes: 1 addition & 1 deletion webui/gen/ts/v1/service_connect.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions webui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ 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 &&
createRoot(el).render(
<AntdConfigProvider
theme={{
algorithm: [
darkThemeMq.matches ? theme.darkAlgorithm : theme.defaultAlgorithm,
darkTheme ? theme.darkAlgorithm : theme.defaultAlgorithm,
theme.compactAlgorithm,
],
}}
>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={createTheme({
palette: {
mode: darkThemeMq ? "dark" : "light"
mode: darkTheme ? "dark" : "light"
},
})}>
<Root>
Expand Down
7 changes: 4 additions & 3 deletions webui/src/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
}
Expand Down
31 changes: 16 additions & 15 deletions webui/src/views/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Card,
Col,
Collapse,
Checkbox,
} from "antd";
import React, { useEffect, useState } from "react";
import { useShowModal } from "../components/ModalManager";
Expand Down Expand Up @@ -48,18 +49,24 @@ 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;
}
}
}

// Update configuration
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(() => {
Expand Down Expand Up @@ -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 }) => (
<>
Expand Down

0 comments on commit 8429174

Please sign in to comment.