feat(commands): user opaque identifiers commands (#3144)

Add commands for handling user opaque identifiers.
pull/3153/head
James Elliott 2022-04-09 17:13:19 +10:00 committed by GitHub
parent e7112bfbd6
commit 5a0a15f377
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 405 additions and 20 deletions

4
go.mod
View File

@ -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 (

View File

@ -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"

View File

@ -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"
)

View File

@ -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),

View File

@ -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
}

View File

@ -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()

View File

@ -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"`
} }

View File

@ -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)

View File

@ -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{}

View File

@ -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;`
) )

View File

@ -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

View File

@ -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)