refactor: adjust settings components

pull/4806/head
James Elliott 2022-12-31 18:27:43 +11:00
parent 4239db6171
commit dd781ffc51
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
11 changed files with 107 additions and 65 deletions

View File

@ -3,11 +3,13 @@ package handlers
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/storage"
)
@ -30,11 +32,11 @@ func getWebauthnDeviceIDFromContext(ctx *middlewares.AutheliaCtx) (int, error) {
return deviceID, nil
}
// WebauthnDevicesGet returns all devices registered for the current user.
func WebauthnDevicesGet(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
// WebauthnDevicesGET returns all devices registered for the current user.
func WebauthnDevicesGET(ctx *middlewares.AutheliaCtx) {
s := ctx.GetSession()
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, s.Username)
if err != nil && err != storage.ErrNoWebauthnDevice {
ctx.Error(err, messageOperationFailed)
@ -47,19 +49,20 @@ func WebauthnDevicesGet(ctx *middlewares.AutheliaCtx) {
}
}
// WebauthnDeviceUpdate updates the description for a specific device for the current user.
func WebauthnDeviceUpdate(ctx *middlewares.AutheliaCtx) {
type requestPostData struct {
Description string `json:"description"`
}
// WebauthnDevicePUT updates the description for a specific device for the current user.
func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
var (
bodyJSON bodyEditWebauthnDeviceRequest
var postData *requestPostData
id int
device *model.WebauthnDevice
err error
)
userSession := ctx.GetSession()
s := ctx.GetSession()
err := json.Unmarshal(ctx.PostBody(), &postData)
if err != nil {
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, s.Username, err)
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.Error(err, messageOperationFailed)
@ -67,28 +70,54 @@ func WebauthnDeviceUpdate(ctx *middlewares.AutheliaCtx) {
return
}
deviceID, err := getWebauthnDeviceIDFromContext(ctx)
if err != nil {
if id, err = getWebauthnDeviceIDFromContext(ctx); err != nil {
return
}
if err := ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, userSession.Username, deviceID, postData.Description); err != nil {
if device, err = ctx.Providers.StorageProvider.LoadWebauthnDeviceByID(ctx, id); err != nil {
ctx.Error(err, messageOperationFailed)
return
}
if device.Username != s.Username {
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", s.Username, device.ID, device.Username), messageOperationFailed)
return
}
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, s.Username, id, bodyJSON.Description); err != nil {
ctx.Error(err, messageOperationFailed)
return
}
}
// WebauthnDeviceDelete deletes a specific device for the current user.
func WebauthnDeviceDelete(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
// WebauthnDeviceDELETE deletes a specific device for the current user.
func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
var (
id int
device *model.WebauthnDevice
err error
)
deviceID, err := getWebauthnDeviceIDFromContext(ctx)
if err != nil {
if id, err = getWebauthnDeviceIDFromContext(ctx); err != nil {
return
}
if err := ctx.Providers.StorageProvider.DeleteWebauthnDeviceByUsernameAndID(ctx, userSession.Username, deviceID); err != nil {
if device, err = ctx.Providers.StorageProvider.LoadWebauthnDeviceByID(ctx, id); err != nil {
ctx.Error(err, messageOperationFailed)
return
}
s := ctx.GetSession()
if device.Username != s.Username {
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", s.Username, device.ID, device.Username), messageOperationFailed)
return
}
if err = ctx.Providers.StorageProvider.DeleteWebauthnDevice(ctx, device.KID.String()); err != nil {
ctx.Error(err, messageOperationFailed)
return
}
ctx.ReplyOK()
}

View File

@ -39,6 +39,10 @@ type bodySignWebauthnRequest struct {
WorkflowID string `json:"workflowID"`
}
type bodyEditWebauthnDeviceRequest struct {
Description string `json:"description"`
}
// bodySignDuoRequest is the model of the request body of Duo 2FA authentication endpoint.
type bodySignDuoRequest struct {
TargetURL string `json:"targetURL"`

View File

@ -194,20 +194,6 @@ func (mr *MockStorageMockRecorder) DeleteWebauthnDeviceByUsername(arg0, arg1, ar
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDeviceByUsername", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDeviceByUsername), arg0, arg1, arg2)
}
// DeleteWebauthnDeviceByUsernameAndID mocks base method.
func (m *MockStorage) DeleteWebauthnDeviceByUsernameAndID(arg0 context.Context, arg1 string, arg2 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebauthnDeviceByUsernameAndID", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteWebauthnDeviceByUsernameAndID indicates an expected call of DeleteWebauthnDeviceByUsernameAndID.
func (mr *MockStorageMockRecorder) DeleteWebauthnDeviceByUsernameAndID(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDeviceByUsernameAndID", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDeviceByUsernameAndID), arg0, arg1, arg2)
}
// FindIdentityVerification mocks base method.
func (m *MockStorage) FindIdentityVerification(arg0 context.Context, arg1 string) (bool, error) {
m.ctrl.T.Helper()
@ -418,6 +404,21 @@ func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifiers(arg0 interface{}) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifiers", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifiers), arg0)
}
// LoadWebauthnDeviceByID mocks base method.
func (m *MockStorage) LoadWebauthnDeviceByID(arg0 context.Context, arg1 int) (*model.WebauthnDevice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDeviceByID", arg0, arg1)
ret0, _ := ret[0].(*model.WebauthnDevice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadWebauthnDeviceByID indicates an expected call of LoadWebauthnDeviceByID.
func (mr *MockStorageMockRecorder) LoadWebauthnDeviceByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDeviceByID", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDeviceByID), arg0, arg1)
}
// LoadWebauthnDevices mocks base method.
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) {
m.ctrl.T.Helper()

View File

@ -200,9 +200,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionPOST))
// Management of the webauthn devices.
r.GET("/api/secondfactor/webauthn/devices", middleware1FA(handlers.WebauthnDevicesGet))
r.PUT("/api/secondfactor/webauthn/devices/{deviceID}", middleware2FA(handlers.WebauthnDeviceUpdate))
r.DELETE("/api/secondfactor/webauthn/devices/{deviceID}", middleware2FA(handlers.WebauthnDeviceDelete))
r.GET("/api/secondfactor/webauthn/devices", middleware1FA(handlers.WebauthnDevicesGET))
r.PUT("/api/secondfactor/webauthn/device/{deviceID}", middleware2FA(handlers.WebauthnDevicePUT))
r.DELETE("/api/secondfactor/webauthn/device/{deviceID}", middleware2FA(handlers.WebauthnDeviceDELETE))
}
// Configure DUO api endpoint only if configuration exists.

View File

@ -43,9 +43,9 @@ type Provider interface {
UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error)
DeleteWebauthnDevice(ctx context.Context, kid string) (err error)
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error)
DeleteWebauthnDeviceByUsernameAndID(ctx context.Context, username string, deviceID int) (err error)
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error)
LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebauthnDevice, err error)
LoadWebauthnDeviceByID(ctx context.Context, id int) (device *model.WebauthnDevice, err error)
SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error)
DeletePreferredDuoDevice(ctx context.Context, username string) (err error)

View File

@ -49,6 +49,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpsertWebauthnDevice: fmt.Sprintf(queryFmtUpsertWebauthnDevice, tableWebauthnDevices),
sqlSelectWebauthnDevices: fmt.Sprintf(queryFmtSelectWebauthnDevices, tableWebauthnDevices),
sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices),
sqlSelectWebauthnDeviceByID: fmt.Sprintf(queryFmtSelectWebauthnDeviceByID, tableWebauthnDevices),
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID, tableWebauthnDevices),
sqlUpdateWebauthnDevicePublicKey: fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices),
sqlUpdateWebauthnDevicePublicKeyByUsername: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDevicePublicKeyByUsername, tableWebauthnDevices),
@ -56,7 +57,6 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpdateWebauthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignInByUsername, tableWebauthnDevices),
sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices),
sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices),
sqlDeleteWebauthnDeviceByUsernameAndID: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndID, tableWebauthnDevices),
sqlDeleteWebauthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices),
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
@ -166,6 +166,7 @@ type SQLProvider struct {
sqlUpsertWebauthnDevice string
sqlSelectWebauthnDevices string
sqlSelectWebauthnDevicesByUsername string
sqlSelectWebauthnDeviceByID string
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID string
sqlUpdateWebauthnDevicePublicKey string
@ -175,7 +176,6 @@ type SQLProvider struct {
sqlDeleteWebauthnDevice string
sqlDeleteWebauthnDeviceByUsername string
sqlDeleteWebauthnDeviceByUsernameAndID string
sqlDeleteWebauthnDeviceByUsernameAndDescription string
// Table: duo_devices.
@ -803,7 +803,7 @@ func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username stri
func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error) {
config = &model.TOTPConfiguration{}
if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil {
if err = p.db.GetContext(ctx, config, p.sqlSelectTOTPConfig, username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoTOTPConfiguration
}
@ -903,19 +903,6 @@ func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, userna
return nil
}
// DeleteWebauthnDeviceByUsernameAndID deletes a registered Webauthn device by username and ID.
func (p *SQLProvider) DeleteWebauthnDeviceByUsernameAndID(ctx context.Context, username string, deviceID int) (err error) {
if len(username) == 0 {
return fmt.Errorf("error deleting webauthn device with username '%s' and id '%d': username must not be empty", username, deviceID)
}
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndID, username, deviceID); err != nil {
return fmt.Errorf("error deleting webauthn device with username '%s' and id '%d': %w", username, deviceID, err)
}
return nil
}
// LoadWebauthnDevices loads Webauthn device registrations.
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) {
devices = make([]model.WebauthnDevice, 0, limit)
@ -937,6 +924,21 @@ func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int)
return devices, nil
}
// LoadWebauthnDeviceByID loads a webauthn device registration for a given id.
func (p *SQLProvider) LoadWebauthnDeviceByID(ctx context.Context, id int) (device *model.WebauthnDevice, err error) {
device = &model.WebauthnDevice{}
if err = p.db.GetContext(ctx, device, p.sqlSelectWebauthnDeviceByID, id); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, sql.ErrNoRows
}
return nil, fmt.Errorf("error selecting Webauthn device with id '%d': %w", id, err)
}
return device, nil
}
// LoadWebauthnDevicesByUsername loads all webauthn devices registration for a given username.
func (p *SQLProvider) LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebauthnDevice, err error) {
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByUsername, username); err != nil {

View File

@ -61,6 +61,7 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices)
provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername)
provider.sqlSelectWebauthnDeviceByID = provider.db.Rebind(provider.sqlSelectWebauthnDeviceByID)
provider.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID)
provider.sqlUpdateWebauthnDevicePublicKey = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKey)
provider.sqlUpdateWebauthnDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKeyByUsername)
@ -68,7 +69,6 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
provider.sqlUpdateWebauthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignInByUsername)
provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice)
provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername)
provider.sqlDeleteWebauthnDeviceByUsernameAndID = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndID)
provider.sqlDeleteWebauthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDescription)
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)

View File

@ -134,6 +134,11 @@ const (
FROM %s
WHERE username = ?;`
queryFmtSelectWebauthnDeviceByID = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
FROM %s
WHERE id = ?;`
queryFmtUpdateWebauthnDevicePublicKey = `
UPDATE %s
SET public_key = ?
@ -181,10 +186,6 @@ const (
DELETE FROM %s
WHERE username = ?;`
queryFmtDeleteWebauthnDeviceByUsernameAndID = `
DELETE FROM %s
WHERE username = ? AND id = ?;`
queryFmtDeleteWebauthnDeviceByUsernameAndDescription = `
DELETE FROM %s
WHERE username = ? AND description = ?;`

View File

@ -18,6 +18,7 @@ export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/at
export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";
export const WebauthnDevicesPath = basePath + "/api/secondfactor/webauthn/devices";
export const WebauthnDevicePath = basePath + "/api/secondfactor/webauthn/device";
export const InitiateDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_devices";
export const CompleteDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_device";

View File

@ -24,7 +24,7 @@ import {
ServiceResponse,
WebauthnAssertionPath,
WebauthnAttestationPath,
WebauthnDevicesPath,
WebauthnDevicePath,
WebauthnIdentityFinishPath,
} from "@services/Api";
import { SignInResponse } from "@services/SignIn";
@ -399,12 +399,12 @@ export async function performAssertionCeremony(
}
export async function deleteDevice(deviceID: string): Promise<number> {
let response = await axios.delete(`${WebauthnDevicesPath}/${deviceID}`);
let response = await axios.delete(`${WebauthnDevicePath}/${deviceID}`);
return response.status;
}
export async function updateDevice(deviceID: string, description: string): Promise<number> {
let response = await axios.put<ServiceResponse<WebauthnDeviceUpdateRequest>>(`${WebauthnDevicesPath}/${deviceID}`, {
let response = await axios.put<ServiceResponse<WebauthnDeviceUpdateRequest>>(`${WebauthnDevicePath}/${deviceID}`, {
description: description,
});
return response.status;

View File

@ -44,6 +44,8 @@ export default function WebauthnDeviceItem(props: Props) {
const status = await updateDevice(props.device.id, name);
console.log("Status was: ", status);
setLoadingEdit(false);
if (status !== 200) {
@ -65,6 +67,8 @@ export default function WebauthnDeviceItem(props: Props) {
const status = await deleteDevice(props.device.id);
console.log("Status was: ", status);
setLoadingDelete(false);
if (status !== 200) {