diff --git a/go.mod b/go.mod index 427da6d9f..007cbdc64 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/valyala/fasthttp v1.35.0 golang.org/x/text v0.3.7 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 ( @@ -102,7 +102,7 @@ require ( google.golang.org/grpc v1.42.0 // indirect google.golang.org/protobuf v1.27.1 // 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 ( diff --git a/internal/authentication/file_user_provider.go b/internal/authentication/file_user_provider.go index b70fb53db..c4ea7af4e 100644 --- a/internal/authentication/file_user_provider.go +++ b/internal/authentication/file_user_provider.go @@ -8,7 +8,7 @@ import ( "sync" "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/logging" diff --git a/internal/commands/const.go b/internal/commands/const.go index eb1f1a6cb..3bcb27a87 100644 --- a/internal/commands/const.go +++ b/internal/commands/const.go @@ -111,3 +111,7 @@ const ( var ( errNoStorageProvider = errors.New("no storage provider configured") ) + +const ( + identifierServiceOpenIDConnect = "openid_connect" +) diff --git a/internal/commands/storage.go b/internal/commands/storage.go index 1aa44813f..debfdc6bf 100644 --- a/internal/commands/storage.go +++ b/internal/commands/storage.go @@ -42,12 +42,80 @@ func NewStorageCmd() (cmd *cobra.Command) { newStorageMigrateCmd(), newStorageSchemaInfoCmd(), newStorageEncryptionCmd(), + newStorageUserCmd(), + ) + + return cmd +} + +func newStorageUserCmd() (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "user", + Short: "Manages user settings", + } + + cmd.AddCommand( + newStorageUserIdentifiersCmd(), newStorageTOTPCmd(), ) 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) { cmd = &cobra.Command{ Use: "encryption", @@ -103,7 +171,7 @@ func newStorageTOTPCmd() (cmd *cobra.Command) { func newStorageTOTPGenerateCmd() (cmd *cobra.Command) { cmd = &cobra.Command{ - Use: "generate username", + Use: "generate [username]", Short: "Generate a TOTP configuration for a user", RunE: storageTOTPGenerateRunE, Args: cobra.ExactArgs(1), diff --git a/internal/commands/storage_run.go b/internal/commands/storage_run.go index 38ffb85a3..318728842 100644 --- a/internal/commands/storage_run.go +++ b/internal/commands/storage_run.go @@ -11,8 +11,10 @@ import ( "path/filepath" "strings" + "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/pflag" + "gopkg.in/yaml.v3" "github.com/authelia/authelia/v4/internal/configuration" "github.com/authelia/authelia/v4/internal/configuration/schema" @@ -653,3 +655,166 @@ func checkStorageSchemaUpToDate(ctx context.Context, provider storage.Provider) 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 +} diff --git a/internal/mocks/storage.go b/internal/mocks/storage.go index 4bcea9add..9371fbe2d 100644 --- a/internal/mocks/storage.go +++ b/internal/mocks/storage.go @@ -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) } +// 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. func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) { m.ctrl.T.Helper() diff --git a/internal/model/user_opaque_identifier.go b/internal/model/user_opaque_identifier.go index c89b80380..557c1370d 100644 --- a/internal/model/user_opaque_identifier.go +++ b/internal/model/user_opaque_identifier.go @@ -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. type UserOpaqueIdentifier struct { - ID int `db:"id"` - Service string `db:"service"` - SectorID string `db:"sector_id"` - Username string `db:"username"` + ID int `db:"id" yaml:"id"` + Service string `db:"service" yaml:"service"` + SectorID string `db:"sector_id" yaml:"sector_id"` + 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"` } diff --git a/internal/storage/provider.go b/internal/storage/provider.go index 742b36002..1d2a8698e 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -24,6 +24,7 @@ type Provider interface { SaveUserOpaqueIdentifier(ctx context.Context, 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) SaveIdentityVerification(ctx context.Context, verification model.IdentityVerification) (err error) diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index 94b9c8392..6872022d8 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -66,6 +66,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier), sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier), + sqlSelectUserOpaqueIdentifiers: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifiers, tableUserOpaqueIdentifier), sqlSelectUserOpaqueIdentifierBySignature: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifierBySignature, tableUserOpaqueIdentifier), sqlInsertOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession), @@ -180,6 +181,7 @@ type SQLProvider struct { // Table: user_opaque_identifier. sqlInsertUserOpaqueIdentifier string sqlSelectUserOpaqueIdentifier string + sqlSelectUserOpaqueIdentifiers string sqlSelectUserOpaqueIdentifierBySignature string // Table: migrations. @@ -349,6 +351,29 @@ func (p *SQLProvider) LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID u 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. func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) { opaqueID = &model.UserOpaqueIdentifier{} diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index 57ecbc66c..3fbbfeb0a 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -305,7 +305,7 @@ const ( VALUES(?, ?, ?, ?);` queryFmtSelectUserOpaqueIdentifier = ` - SELECT id, sector_id, username, identifier + SELECT id, service, sector_id, username, identifier FROM %s WHERE identifier = ?;` @@ -313,4 +313,8 @@ const ( SELECT id, service, sector_id, username, identifier FROM %s WHERE service = ? AND sector_id = ? AND username = ?;` + + queryFmtSelectUserOpaqueIdentifiers = ` + SELECT id, service, sector_id, username, identifier + FROM %s;` ) diff --git a/internal/suites/suite_cli.go b/internal/suites/suite_cli.go index 64a544702..873a57f8e 100644 --- a/internal/suites/suite_cli.go +++ b/internal/suites/suite_cli.go @@ -39,6 +39,7 @@ func init() { _ = os.Remove("/tmp/db.sqlite3") _ = os.Remove("/tmp/db.sqlite") _ = os.RemoveAll("/tmp/qr/") + _ = os.RemoveAll("/tmp/out/") _ = os.Remove("/tmp/qr.png") return err diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go index 846ce23fe..a3aef5900 100644 --- a/internal/suites/suite_cli_test.go +++ b/internal/suites/suite_cli_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v3" "github.com/authelia/authelia/v4/internal/model" "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`) - 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().Regexp(patternOutdated, output) @@ -342,7 +343,7 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() { for _, testCase := range testCases { 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().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().Greater(fileInfo.Size(), int64(1000)) } 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) } @@ -365,25 +366,25 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() { 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) for _, expectedLine := range expectedLines { 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) for _, expectedLine := range expectedLinesCSV { 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().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().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)) } - 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().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"}) 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") } -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"}) s.Assert().NoError(err)