feat(commands): user opaque identifiers commands (#3144)
Add commands for handling user opaque identifiers.pull/3153/head
parent
e7112bfbd6
commit
5a0a15f377
4
go.mod
4
go.mod
|
@ -34,7 +34,7 @@ require (
|
||||||
github.com/valyala/fasthttp v1.35.0
|
github.com/valyala/fasthttp v1.35.0
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.3.7
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -102,7 +102,7 @@ require (
|
||||||
google.golang.org/grpc v1.42.0 // indirect
|
google.golang.org/grpc v1.42.0 // indirect
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/v4/internal/logging"
|
"github.com/authelia/authelia/v4/internal/logging"
|
||||||
|
|
|
@ -111,3 +111,7 @@ const (
|
||||||
var (
|
var (
|
||||||
errNoStorageProvider = errors.New("no storage provider configured")
|
errNoStorageProvider = errors.New("no storage provider configured")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
identifierServiceOpenIDConnect = "openid_connect"
|
||||||
|
)
|
||||||
|
|
|
@ -42,12 +42,80 @@ func NewStorageCmd() (cmd *cobra.Command) {
|
||||||
newStorageMigrateCmd(),
|
newStorageMigrateCmd(),
|
||||||
newStorageSchemaInfoCmd(),
|
newStorageSchemaInfoCmd(),
|
||||||
newStorageEncryptionCmd(),
|
newStorageEncryptionCmd(),
|
||||||
|
newStorageUserCmd(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageUserCmd() (cmd *cobra.Command) {
|
||||||
|
cmd = &cobra.Command{
|
||||||
|
Use: "user",
|
||||||
|
Short: "Manages user settings",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newStorageUserIdentifiersCmd(),
|
||||||
newStorageTOTPCmd(),
|
newStorageTOTPCmd(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newStorageUserIdentifiersCmd() (cmd *cobra.Command) {
|
||||||
|
cmd = &cobra.Command{
|
||||||
|
Use: "identifiers",
|
||||||
|
Short: "Manages user opaque identifiers",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newStorageUserIdentifiersExportCmd(),
|
||||||
|
newStorageUserIdentifiersImportCmd(),
|
||||||
|
newStorageUserIdentifiersAddCmd(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageUserIdentifiersExportCmd() (cmd *cobra.Command) {
|
||||||
|
cmd = &cobra.Command{
|
||||||
|
Use: "export",
|
||||||
|
Short: "Export the identifiers to a YAML file",
|
||||||
|
RunE: storageUserIdentifiersExport,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("file", "f", "user-opaque-identifiers.yml", "The file name for the YAML export")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageUserIdentifiersImportCmd() (cmd *cobra.Command) {
|
||||||
|
cmd = &cobra.Command{
|
||||||
|
Use: "import",
|
||||||
|
Short: "Import the identifiers from a YAML file",
|
||||||
|
RunE: storageUserIdentifiersImport,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("file", "f", "user-opaque-identifiers.yml", "The file name for the YAML import")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageUserIdentifiersAddCmd() (cmd *cobra.Command) {
|
||||||
|
cmd = &cobra.Command{
|
||||||
|
Use: "add [username]",
|
||||||
|
Short: "Add an identifiers to the database",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: storageUserIdentifiersAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().String("identifier", "", "The optional version 4 UUID to use, if not set a random one will be used")
|
||||||
|
cmd.Flags().String("service", identifierServiceOpenIDConnect, "The service to add the identifier for, valid values are: openid_connect")
|
||||||
|
cmd.Flags().String("sector", "", "The sector identifier to use (should usually be blank)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func newStorageEncryptionCmd() (cmd *cobra.Command) {
|
func newStorageEncryptionCmd() (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "encryption",
|
Use: "encryption",
|
||||||
|
@ -103,7 +171,7 @@ func newStorageTOTPCmd() (cmd *cobra.Command) {
|
||||||
|
|
||||||
func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
|
func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "generate username",
|
Use: "generate [username]",
|
||||||
Short: "Generate a TOTP configuration for a user",
|
Short: "Generate a TOTP configuration for a user",
|
||||||
RunE: storageTOTPGenerateRunE,
|
RunE: storageTOTPGenerateRunE,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
|
|
@ -11,8 +11,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/configuration"
|
"github.com/authelia/authelia/v4/internal/configuration"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
|
@ -653,3 +655,166 @@ func checkStorageSchemaUpToDate(ctx context.Context, provider storage.Provider)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func storageUserIdentifiersExport(cmd *cobra.Command, _ []string) (err error) {
|
||||||
|
var (
|
||||||
|
provider storage.Provider
|
||||||
|
|
||||||
|
ctx = context.Background()
|
||||||
|
|
||||||
|
file string
|
||||||
|
)
|
||||||
|
|
||||||
|
if file, err = cmd.Flags().GetString("file"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(file)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return fmt.Errorf("must specify a file that doesn't exist but '%s' exists", file)
|
||||||
|
case !os.IsNotExist(err):
|
||||||
|
return fmt.Errorf("error occurred opening '%s': %w", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = getStorageProvider()
|
||||||
|
|
||||||
|
var (
|
||||||
|
export model.UserOpaqueIdentifiersExport
|
||||||
|
|
||||||
|
data []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if export.Identifiers, err = provider.LoadUserOpaqueIdentifiers(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(export.Identifiers) == 0 {
|
||||||
|
return fmt.Errorf("no data to export")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, err = yaml.Marshal(&export); err != nil {
|
||||||
|
return fmt.Errorf("error occurred marshalling data to YAML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.WriteFile(file, data, 0600); err != nil {
|
||||||
|
return fmt.Errorf("error occurred writing to file '%s': %w", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Exported %d User Opaque Identifiers to %s\n", len(export.Identifiers), file)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageUserIdentifiersImport(cmd *cobra.Command, _ []string) (err error) {
|
||||||
|
var (
|
||||||
|
provider storage.Provider
|
||||||
|
|
||||||
|
ctx = context.Background()
|
||||||
|
|
||||||
|
file string
|
||||||
|
stat os.FileInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if file, err = cmd.Flags().GetString("file"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat, err = os.Stat(file); err != nil {
|
||||||
|
return fmt.Errorf("must specify a file that exists but '%s' had an error opening it: %w", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.IsDir() {
|
||||||
|
return fmt.Errorf("must specify a file that exists but '%s' is a directory", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data []byte
|
||||||
|
export model.UserOpaqueIdentifiersExport
|
||||||
|
)
|
||||||
|
|
||||||
|
if data, err = os.ReadFile(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = yaml.Unmarshal(data, &export); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(export.Identifiers) == 0 {
|
||||||
|
return fmt.Errorf("can't import a file with no data")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = getStorageProvider()
|
||||||
|
|
||||||
|
for _, opaqueID := range export.Identifiers {
|
||||||
|
if err = provider.SaveUserOpaqueIdentifier(ctx, opaqueID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Imported User Opaque Identifiers from %s\n", file)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageUserIdentifiersAdd(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
var (
|
||||||
|
provider storage.Provider
|
||||||
|
|
||||||
|
ctx = context.Background()
|
||||||
|
|
||||||
|
service, sector string
|
||||||
|
)
|
||||||
|
|
||||||
|
if service, err = cmd.Flags().GetString("service"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if service == "" {
|
||||||
|
service = identifierServiceOpenIDConnect
|
||||||
|
} else if service != identifierServiceOpenIDConnect {
|
||||||
|
return fmt.Errorf("the service name '%s' is invalid, the valid values are: 'openid_connect'", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sector, err = cmd.Flags().GetString("sector"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opaqueID := model.UserOpaqueIdentifier{
|
||||||
|
Service: service,
|
||||||
|
Username: args[0],
|
||||||
|
SectorID: sector,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("identifier") {
|
||||||
|
var identifierStr string
|
||||||
|
|
||||||
|
if identifierStr, err = cmd.Flags().GetString("identifier"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opaqueID.Identifier, err = uuid.Parse(identifierStr); err != nil {
|
||||||
|
return fmt.Errorf("the identifier provided '%s' is invalid as it must be a version 4 UUID but parsing it had an error: %w", identifierStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opaqueID.Identifier.Version() != 4 {
|
||||||
|
return fmt.Errorf("the identifier providerd '%s' is a version %d UUID but only version 4 UUID's accepted as identifiers", identifierStr, opaqueID.Identifier.Version())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if opaqueID.Identifier, err = uuid.NewRandom(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = getStorageProvider()
|
||||||
|
|
||||||
|
if err = provider.SaveUserOpaqueIdentifier(ctx, opaqueID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Added User Opaque Identifier:\n\tService: %s\n\tSector: %s\n\tUsername: %s\n\tIdentifier: %s\n\n", opaqueID.Service, opaqueID.SectorID, opaqueID.Username, opaqueID.Identifier)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -361,6 +361,21 @@ func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifierBySignature(arg0, arg
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifierBySignature", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifierBySignature), arg0, arg1, arg2, arg3)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifierBySignature", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifierBySignature), arg0, arg1, arg2, arg3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadUserOpaqueIdentifiers mocks base method.
|
||||||
|
func (m *MockStorage) LoadUserOpaqueIdentifiers(arg0 context.Context) ([]model.UserOpaqueIdentifier, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "LoadUserOpaqueIdentifiers", arg0)
|
||||||
|
ret0, _ := ret[0].([]model.UserOpaqueIdentifier)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadUserOpaqueIdentifiers indicates an expected call of LoadUserOpaqueIdentifiers.
|
||||||
|
func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifiers(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifiers", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifiers), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// LoadWebauthnDevices mocks base method.
|
// LoadWebauthnDevices mocks base method.
|
||||||
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) {
|
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -24,10 +24,15 @@ func NewUserOpaqueIdentifier(service, sectorID, username string) (id *UserOpaque
|
||||||
|
|
||||||
// UserOpaqueIdentifier represents an opaque identifier for a user. Commonly used with OAuth 2.0 and OpenID Connect.
|
// UserOpaqueIdentifier represents an opaque identifier for a user. Commonly used with OAuth 2.0 and OpenID Connect.
|
||||||
type UserOpaqueIdentifier struct {
|
type UserOpaqueIdentifier struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id" yaml:"id"`
|
||||||
Service string `db:"service"`
|
Service string `db:"service" yaml:"service"`
|
||||||
SectorID string `db:"sector_id"`
|
SectorID string `db:"sector_id" yaml:"sector_id"`
|
||||||
Username string `db:"username"`
|
Username string `db:"username" yaml:"username"`
|
||||||
|
|
||||||
Identifier uuid.UUID `db:"identifier"`
|
Identifier uuid.UUID `db:"identifier" yaml:"identifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserOpaqueIdentifiersExport represents a UserOpaqueIdentifier export file.
|
||||||
|
type UserOpaqueIdentifiersExport struct {
|
||||||
|
Identifiers []UserOpaqueIdentifier `yaml:"identifiers"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ type Provider interface {
|
||||||
|
|
||||||
SaveUserOpaqueIdentifier(ctx context.Context, subject model.UserOpaqueIdentifier) (err error)
|
SaveUserOpaqueIdentifier(ctx context.Context, subject model.UserOpaqueIdentifier) (err error)
|
||||||
LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID uuid.UUID) (subject *model.UserOpaqueIdentifier, err error)
|
LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID uuid.UUID) (subject *model.UserOpaqueIdentifier, err error)
|
||||||
|
LoadUserOpaqueIdentifiers(ctx context.Context) (opaqueIDs []model.UserOpaqueIdentifier, err error)
|
||||||
LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (subject *model.UserOpaqueIdentifier, err error)
|
LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (subject *model.UserOpaqueIdentifier, err error)
|
||||||
|
|
||||||
SaveIdentityVerification(ctx context.Context, verification model.IdentityVerification) (err error)
|
SaveIdentityVerification(ctx context.Context, verification model.IdentityVerification) (err error)
|
||||||
|
|
|
@ -66,6 +66,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
|
||||||
|
|
||||||
sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier),
|
sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier),
|
||||||
sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier),
|
sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier),
|
||||||
|
sqlSelectUserOpaqueIdentifiers: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifiers, tableUserOpaqueIdentifier),
|
||||||
sqlSelectUserOpaqueIdentifierBySignature: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifierBySignature, tableUserOpaqueIdentifier),
|
sqlSelectUserOpaqueIdentifierBySignature: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifierBySignature, tableUserOpaqueIdentifier),
|
||||||
|
|
||||||
sqlInsertOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession),
|
sqlInsertOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession),
|
||||||
|
@ -180,6 +181,7 @@ type SQLProvider struct {
|
||||||
// Table: user_opaque_identifier.
|
// Table: user_opaque_identifier.
|
||||||
sqlInsertUserOpaqueIdentifier string
|
sqlInsertUserOpaqueIdentifier string
|
||||||
sqlSelectUserOpaqueIdentifier string
|
sqlSelectUserOpaqueIdentifier string
|
||||||
|
sqlSelectUserOpaqueIdentifiers string
|
||||||
sqlSelectUserOpaqueIdentifierBySignature string
|
sqlSelectUserOpaqueIdentifierBySignature string
|
||||||
|
|
||||||
// Table: migrations.
|
// Table: migrations.
|
||||||
|
@ -349,6 +351,29 @@ func (p *SQLProvider) LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID u
|
||||||
return opaqueID, nil
|
return opaqueID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadUserOpaqueIdentifiers selects an opaque user identifiers from the database.
|
||||||
|
func (p *SQLProvider) LoadUserOpaqueIdentifiers(ctx context.Context) (opaqueIDs []model.UserOpaqueIdentifier, err error) {
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
|
||||||
|
if rows, err = p.db.QueryxContext(ctx, p.sqlSelectUserOpaqueIdentifiers); err != nil {
|
||||||
|
return nil, fmt.Errorf("error selecting user opaque identifiers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var opaqueID *model.UserOpaqueIdentifier
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
opaqueID = &model.UserOpaqueIdentifier{}
|
||||||
|
|
||||||
|
if err = rows.StructScan(opaqueID); err != nil {
|
||||||
|
return nil, fmt.Errorf("error selecting user opaque identifiers: error scanning row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opaqueIDs = append(opaqueIDs, *opaqueID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opaqueIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadUserOpaqueIdentifierBySignature selects an opaque user identifier from the database given a service name, sector id, and username.
|
// LoadUserOpaqueIdentifierBySignature selects an opaque user identifier from the database given a service name, sector id, and username.
|
||||||
func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) {
|
func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) {
|
||||||
opaqueID = &model.UserOpaqueIdentifier{}
|
opaqueID = &model.UserOpaqueIdentifier{}
|
||||||
|
|
|
@ -305,7 +305,7 @@ const (
|
||||||
VALUES(?, ?, ?, ?);`
|
VALUES(?, ?, ?, ?);`
|
||||||
|
|
||||||
queryFmtSelectUserOpaqueIdentifier = `
|
queryFmtSelectUserOpaqueIdentifier = `
|
||||||
SELECT id, sector_id, username, identifier
|
SELECT id, service, sector_id, username, identifier
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE identifier = ?;`
|
WHERE identifier = ?;`
|
||||||
|
|
||||||
|
@ -313,4 +313,8 @@ const (
|
||||||
SELECT id, service, sector_id, username, identifier
|
SELECT id, service, sector_id, username, identifier
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE service = ? AND sector_id = ? AND username = ?;`
|
WHERE service = ? AND sector_id = ? AND username = ?;`
|
||||||
|
|
||||||
|
queryFmtSelectUserOpaqueIdentifiers = `
|
||||||
|
SELECT id, service, sector_id, username, identifier
|
||||||
|
FROM %s;`
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,6 +39,7 @@ func init() {
|
||||||
_ = os.Remove("/tmp/db.sqlite3")
|
_ = os.Remove("/tmp/db.sqlite3")
|
||||||
_ = os.Remove("/tmp/db.sqlite")
|
_ = os.Remove("/tmp/db.sqlite")
|
||||||
_ = os.RemoveAll("/tmp/qr/")
|
_ = os.RemoveAll("/tmp/qr/")
|
||||||
|
_ = os.RemoveAll("/tmp/out/")
|
||||||
_ = os.Remove("/tmp/qr.png")
|
_ = os.Remove("/tmp/qr.png")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/model"
|
"github.com/authelia/authelia/v4/internal/model"
|
||||||
"github.com/authelia/authelia/v4/internal/storage"
|
"github.com/authelia/authelia/v4/internal/storage"
|
||||||
|
@ -192,7 +193,7 @@ func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
|
||||||
|
|
||||||
patternOutdated := regexp.MustCompile(`Error: schema is version \d+ which is outdated please migrate to version \d+ in order to use this command or use an older binary`)
|
patternOutdated := regexp.MustCompile(`Error: schema is version \d+ which is outdated please migrate to version \d+ in order to use this command or use an older binary`)
|
||||||
|
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "export", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().EqualError(err, "exit status 1")
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
s.Assert().Regexp(patternOutdated, output)
|
s.Assert().Regexp(patternOutdated, output)
|
||||||
|
|
||||||
|
@ -342,7 +343,7 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
if testCase.png {
|
if testCase.png {
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", testCase.config.Username, "--period", strconv.Itoa(int(testCase.config.Period)), "--algorithm", testCase.config.Algorithm, "--digits", strconv.Itoa(int(testCase.config.Digits)), "--path=/tmp/qr.png", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "generate", testCase.config.Username, "--period", strconv.Itoa(int(testCase.config.Period)), "--algorithm", testCase.config.Algorithm, "--digits", strconv.Itoa(int(testCase.config.Digits)), "--path=/tmp/qr.png", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
s.Assert().Contains(output, " and saved it as a PNG image at the path '/tmp/qr.png'")
|
s.Assert().Contains(output, " and saved it as a PNG image at the path '/tmp/qr.png'")
|
||||||
|
|
||||||
|
@ -352,7 +353,7 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
|
||||||
s.Assert().False(fileInfo.IsDir())
|
s.Assert().False(fileInfo.IsDir())
|
||||||
s.Assert().Greater(fileInfo.Size(), int64(1000))
|
s.Assert().Greater(fileInfo.Size(), int64(1000))
|
||||||
} else {
|
} else {
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", testCase.config.Username, "--period", strconv.Itoa(int(testCase.config.Period)), "--algorithm", testCase.config.Algorithm, "--digits", strconv.Itoa(int(testCase.config.Digits)), "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "generate", testCase.config.Username, "--period", strconv.Itoa(int(testCase.config.Period)), "--algorithm", testCase.config.Algorithm, "--digits", strconv.Itoa(int(testCase.config.Digits)), "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,25 +366,25 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
|
||||||
expectedLines = append(expectedLines, config.URI())
|
expectedLines = append(expectedLines, config.URI())
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=uri", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "export", "--format=uri", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
|
|
||||||
for _, expectedLine := range expectedLines {
|
for _, expectedLine := range expectedLines {
|
||||||
s.Assert().Contains(output, expectedLine)
|
s.Assert().Contains(output, expectedLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=csv", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "export", "--format=csv", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
|
|
||||||
for _, expectedLine := range expectedLinesCSV {
|
for _, expectedLine := range expectedLinesCSV {
|
||||||
s.Assert().Contains(output, expectedLine)
|
s.Assert().Contains(output, expectedLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=wrong", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "export", "--format=wrong", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().EqualError(err, "exit status 1")
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
s.Assert().Contains(output, "Error: format must be csv, uri, or png")
|
s.Assert().Contains(output, "Error: format must be csv, uri, or png")
|
||||||
|
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=png", "--dir=/tmp/qr", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "export", "--format=png", "--dir=/tmp/qr", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
s.Assert().Contains(output, "Exported TOTP QR codes in PNG format in the '/tmp/qr' directory")
|
s.Assert().Contains(output, "Exported TOTP QR codes in PNG format in the '/tmp/qr' directory")
|
||||||
|
|
||||||
|
@ -397,12 +398,108 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
|
||||||
s.Assert().Greater(fileInfo.Size(), int64(1000))
|
s.Assert().Greater(fileInfo.Size(), int64(1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", "test", "--period=30", "--algorithm=SHA1", "--digits=6", "--path=/tmp/qr.png", "--config=/config/configuration.storage.yml"})
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "totp", "generate", "test", "--period=30", "--algorithm=SHA1", "--digits=6", "--path=/tmp/qr.png", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().EqualError(err, "exit status 1")
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
s.Assert().Contains(output, "Error: image output filepath already exists")
|
s.Assert().Contains(output, "Error: image output filepath already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
|
func (s *CLISuite) TestStorage04ShouldManageUniqueID() {
|
||||||
|
_ = os.Mkdir("/tmp/out", 0777)
|
||||||
|
|
||||||
|
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "export", "--file=out.yml", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: no data to export")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=webauthn", "--sector=''", "--identifier=1097c8f8-83f2-4506-8138-5f40e83a1285", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: the service name 'webauthn' is invalid, the valid values are: 'openid_connect'")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector=''", "--identifier=1097c8f8-83f2-4506-8138-5f40e83a1285", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().NoError(err)
|
||||||
|
s.Assert().Contains(output, "Added User Opaque Identifier:\n\tService: openid_connect\n\tSector: \n\tUsername: john\n\tIdentifier: 1097c8f8-83f2-4506-8138-5f40e83a1285\n\n")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "export", "--file=/a/no/path/fileout.yml", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: error occurred writing to file '/a/no/path/fileout.yml': open /a/no/path/fileout.yml: no such file or directory")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "export", "--file=out.yml", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: error occurred writing to file 'out.yml': open out.yml: permission denied")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "export", "--file=/tmp/out/1.yml", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().NoError(err)
|
||||||
|
s.Assert().Contains(output, "Exported 1 User Opaque Identifiers to /tmp/out/1.yml")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "export", "--file=/tmp/out/1.yml", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: must specify a file that doesn't exist but '/tmp/out/1.yml' exists")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector=''", "--identifier=1097c8f8-83f2-4506-8138-5f40e83a1285", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: error inserting user opaque id for user 'john' with opaque id '1097c8f8-83f2-4506-8138-5f40e83a1285':")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector=''", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: error inserting user opaque id for user 'john' with opaque id")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector='openidconnect.com'", "--identifier=1097c8f8-83f2-4506-8138-5f40e83a1285", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: error inserting user opaque id for user 'john' with opaque id '1097c8f8-83f2-4506-8138-5f40e83a1285':")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector='openidconnect.net'", "--identifier=b0e17f48-933c-4cba-8509-ee9bfadf8ce5", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().NoError(err)
|
||||||
|
s.Assert().Contains(output, "Added User Opaque Identifier:\n\tService: openid_connect\n\tSector: openidconnect.net\n\tUsername: john\n\tIdentifier: b0e17f48-933c-4cba-8509-ee9bfadf8ce5\n\n")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector='bad-uuid.com'", "--identifier=d49564dc-b7a1-11ec-8429-fcaa147128ea", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: the identifier providerd 'd49564dc-b7a1-11ec-8429-fcaa147128ea' is a version 1 UUID but only version 4 UUID's accepted as identifiers")
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "add", "john", "--service=openid_connect", "--sector='bad-uuid.com'", "--identifier=asdmklasdm", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().EqualError(err, "exit status 1")
|
||||||
|
s.Assert().Contains(output, "Error: the identifier provided 'asdmklasdm' is invalid as it must be a version 4 UUID but parsing it had an error: invalid UUID length: 10")
|
||||||
|
|
||||||
|
data, err := os.ReadFile("/tmp/out/1.yml")
|
||||||
|
s.Assert().NoError(err)
|
||||||
|
|
||||||
|
var export model.UserOpaqueIdentifiersExport
|
||||||
|
|
||||||
|
s.Assert().NoError(yaml.Unmarshal(data, &export))
|
||||||
|
|
||||||
|
s.Require().Len(export.Identifiers, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal(1, export.Identifiers[0].ID)
|
||||||
|
s.Assert().Equal("1097c8f8-83f2-4506-8138-5f40e83a1285", export.Identifiers[0].Identifier.String())
|
||||||
|
s.Assert().Equal("john", export.Identifiers[0].Username)
|
||||||
|
s.Assert().Equal("", export.Identifiers[0].SectorID)
|
||||||
|
s.Assert().Equal("openid_connect", export.Identifiers[0].Service)
|
||||||
|
|
||||||
|
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "user", "identifiers", "export", "--file=/tmp/out/2.yml", "--config=/config/configuration.storage.yml"})
|
||||||
|
s.Assert().NoError(err)
|
||||||
|
s.Assert().Contains(output, "Exported 2 User Opaque Identifiers to /tmp/out/2.yml")
|
||||||
|
|
||||||
|
export = model.UserOpaqueIdentifiersExport{}
|
||||||
|
|
||||||
|
data, err = os.ReadFile("/tmp/out/2.yml")
|
||||||
|
s.Assert().NoError(err)
|
||||||
|
|
||||||
|
s.Assert().NoError(yaml.Unmarshal(data, &export))
|
||||||
|
|
||||||
|
s.Require().Len(export.Identifiers, 2)
|
||||||
|
|
||||||
|
s.Assert().Equal(1, export.Identifiers[0].ID)
|
||||||
|
s.Assert().Equal("1097c8f8-83f2-4506-8138-5f40e83a1285", export.Identifiers[0].Identifier.String())
|
||||||
|
s.Assert().Equal("john", export.Identifiers[0].Username)
|
||||||
|
s.Assert().Equal("", export.Identifiers[0].SectorID)
|
||||||
|
s.Assert().Equal("openid_connect", export.Identifiers[0].Service)
|
||||||
|
|
||||||
|
s.Assert().Equal(2, export.Identifiers[1].ID)
|
||||||
|
s.Assert().Equal("b0e17f48-933c-4cba-8509-ee9bfadf8ce5", export.Identifiers[1].Identifier.String())
|
||||||
|
s.Assert().Equal("john", export.Identifiers[1].Username)
|
||||||
|
s.Assert().Equal("openidconnect.net", export.Identifiers[1].SectorID)
|
||||||
|
s.Assert().Equal("openid_connect", export.Identifiers[1].Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CLISuite) TestStorage05ShouldChangeEncryptionKey() {
|
||||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
|
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
|
|
||||||
|
@ -454,7 +551,7 @@ func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
|
||||||
s.Assert().Contains(output, "Error: the new encryption key must be at least 20 characters\n")
|
s.Assert().Contains(output, "Error: the new encryption key must be at least 20 characters\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CLISuite) TestStorage05ShouldMigrateDown() {
|
func (s *CLISuite) TestStorage06ShouldMigrateDown() {
|
||||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target=0", "--destroy-data", "--config=/config/configuration.storage.yml"})
|
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target=0", "--destroy-data", "--config=/config/configuration.storage.yml"})
|
||||||
s.Assert().NoError(err)
|
s.Assert().NoError(err)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue