diff --git a/cmd/dex/config.go b/cmd/dex/config.go index aa49a18188..c76ff03041 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -95,19 +95,23 @@ type password storage.Password func (p *password) UnmarshalJSON(b []byte) error { var data struct { - Email string `json:"email"` - Username string `json:"username"` - UserID string `json:"userID"` - Hash string `json:"hash"` - HashFromEnv string `json:"hashFromEnv"` + Email string `json:"email"` + Username string `json:"username"` + PreferredUsername string `json:"preferredUsername"` + UserID string `json:"userID"` + Hash string `json:"hash"` + HashFromEnv string `json:"hashFromEnv"` + Groups []string `json:"groups"` } if err := json.Unmarshal(b, &data); err != nil { return err } *p = password(storage.Password{ - Email: data.Email, - Username: data.Username, - UserID: data.UserID, + Email: data.Email, + Username: data.Username, + PreferredUsername: data.PreferredUsername, + UserID: data.UserID, + Groups: data.Groups, }) if len(data.Hash) == 0 && len(data.HashFromEnv) > 0 { data.Hash = os.Getenv(data.HashFromEnv) @@ -275,12 +279,12 @@ var ( _ StorageConfig = (*ent.MySQL)(nil) ) -func getORMBasedSQLStorage(normal, entBased StorageConfig) func() StorageConfig { +func getORMBasedSQLStorage(normal, entBased func() StorageConfig) func() StorageConfig { return func() StorageConfig { if featureflags.EntEnabled.Enabled() { - return entBased + return entBased() } - return normal + return normal() } } @@ -309,9 +313,9 @@ var storages = map[string]func() StorageConfig{ "etcd": func() StorageConfig { return new(etcd.Etcd) }, "kubernetes": func() StorageConfig { return new(kubernetes.Config) }, "memory": func() StorageConfig { return new(memory.Config) }, - "sqlite3": getORMBasedSQLStorage(&sql.SQLite3{}, &ent.SQLite3{}), - "postgres": getORMBasedSQLStorage(&sql.Postgres{}, &ent.Postgres{}), - "mysql": getORMBasedSQLStorage(&sql.MySQL{}, &ent.MySQL{}), + "sqlite3": getORMBasedSQLStorage(func() StorageConfig { return new(sql.SQLite3) }, func() StorageConfig { return new(ent.SQLite3) }), + "postgres": getORMBasedSQLStorage(func() StorageConfig { return new(sql.Postgres) }, func() StorageConfig { return new(ent.Postgres) }), + "mysql": getORMBasedSQLStorage(func() StorageConfig { return new(sql.MySQL) }, func() StorageConfig { return new(ent.MySQL) }), } // UnmarshalJSON allows Storage to implement the unmarshaler interface to diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 68abe1f793..30d50c6cb7 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -116,6 +116,10 @@ staticPasswords: # bcrypt hash of the string "password" hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy" username: "admin" + preferredUsername: "admin-public" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" - email: "foo@example.com" # base64'd value of the same bcrypt hash above. We want to be able to parse both of these @@ -206,10 +210,15 @@ additionalFeatures: [ EnablePasswordDB: true, StaticPasswords: []password{ { - Email: "admin@example.com", - Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"), - Username: "admin", - UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466", + Email: "admin@example.com", + Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"), + Username: "admin", + PreferredUsername: "admin-public", + UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466", + Groups: []string{ + "team-a", + "team-a/admins", + }, }, { Email: "foo@example.com", diff --git a/config.dev.yaml b/config.dev.yaml index dda65e08f7..b0dc959c58 100644 --- a/config.dev.yaml +++ b/config.dev.yaml @@ -32,4 +32,8 @@ staticPasswords: - email: "admin@example.com" hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" + preferredUsername: "admin" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/config.yaml.dist b/config.yaml.dist index b7e1410ffc..c187ca3ce6 100644 --- a/config.yaml.dist +++ b/config.yaml.dist @@ -135,4 +135,15 @@ enablePasswordDB: true # A static list of passwords for the password connector. # # Alternatively, passwords my be added/updated through the gRPC API. -# staticPasswords: [] +# staticPasswords: +# - email: "user@example.com" +# # bcrypt hash of the string "password" +# hash: "$2a$10$examplehash..." +# username: "user-login" +# # Optional. Maps to OIDC "preferred_username" claim. +# preferredUsername: "user-public" +# # Optional. Maps to OIDC "groups" claim (when 'groups' scope is requested). +# groups: +# - "team-a" +# - "team-a/admins" +# userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml index 147597a265..0fdf350cab 100644 --- a/examples/config-dev.yaml +++ b/examples/config-dev.yaml @@ -164,4 +164,8 @@ staticPasswords: # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" + preferredUsername: "admin" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/examples/k8s/dex.yaml b/examples/k8s/dex.yaml index c8d8394401..41156af07f 100644 --- a/examples/k8s/dex.yaml +++ b/examples/k8s/dex.yaml @@ -106,6 +106,10 @@ data: # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" + preferredUsername: "admin" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" --- apiVersion: v1 diff --git a/server/handlers_test.go b/server/handlers_test.go index 114712ba79..184080c81c 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -18,6 +18,7 @@ import ( "github.com/AppsFlyer/go-sundheit/checks" "github.com/coreos/go-oidc/v3/oidc" "github.com/stretchr/testify/require" + "golang.org/x/crypto/bcrypt" "golang.org/x/oauth2" "github.com/dexidp/dex/storage" @@ -402,6 +403,85 @@ func TestHandlePassword(t *testing.T) { } } +func TestHandlePassword_LocalPasswordDBClaims(t *testing.T) { + ctx := t.Context() + + // Setup a dex server. + httpServer, s := newTestServer(t, func(c *Config) { + c.PasswordConnector = "local" + }) + defer httpServer.Close() + + // Client credentials for password grant. + client := storage.Client{ + ID: "test", + Secret: "barfoo", + RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"}, + } + require.NoError(t, s.storage.CreateClient(ctx, client)) + + // Enable local connector. + localConn := storage.Connector{ + ID: "local", + Type: LocalConnector, + Name: "Email", + ResourceVersion: "1", + } + require.NoError(t, s.storage.CreateConnector(ctx, localConn)) + _, err := s.OpenConnector(localConn) + require.NoError(t, err) + + // Create a user in the password DB with groups and preferred_username. + pw := "secret" + hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) + require.NoError(t, err) + require.NoError(t, s.storage.CreatePassword(ctx, storage.Password{ + Email: "user@example.com", + Username: "user-login", + PreferredUsername: "user-public", + UserID: "user-id", + Groups: []string{"team-a", "team-a/admins"}, + Hash: hash, + })) + + u, err := url.Parse(s.issuerURL.String()) + require.NoError(t, err) + u.Path = path.Join(u.Path, "/token") + + v := url.Values{} + v.Add("scope", "openid profile email groups") + v.Add("grant_type", "password") + v.Add("username", "user@example.com") + v.Add("password", pw) + + req, _ := http.NewRequest("POST", u.String(), bytes.NewBufferString(v.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth("test", "barfoo") + + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + require.Equal(t, http.StatusOK, rr.Code) + + var tokenResponse struct { + IDToken string `json:"id_token"` + } + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &tokenResponse)) + require.NotEmpty(t, tokenResponse.IDToken) + + p, err := oidc.NewProvider(ctx, httpServer.URL) + require.NoError(t, err) + idToken, err := p.Verifier(&oidc.Config{SkipClientIDCheck: true}).Verify(ctx, tokenResponse.IDToken) + require.NoError(t, err) + + var claims struct { + PreferredUsername string `json:"preferred_username"` + Groups []string `json:"groups"` + } + require.NoError(t, idToken.Claims(&claims)) + require.Equal(t, "user-public", claims.PreferredUsername) + require.Equal(t, []string{"team-a", "team-a/admins"}, claims.Groups) +} + func TestHandlePasswordLoginWithSkipApproval(t *testing.T) { ctx := t.Context() diff --git a/server/server.go b/server/server.go index 70e8ae755f..d81a0f71a6 100644 --- a/server/server.go +++ b/server/server.go @@ -565,10 +565,12 @@ func (db passwordDB) Login(ctx context.Context, s connector.Scopes, email, passw return connector.Identity{}, false, nil } return connector.Identity{ - UserID: p.UserID, - Username: p.Username, - Email: p.Email, - EmailVerified: true, + UserID: p.UserID, + Username: p.Username, + PreferredUsername: p.PreferredUsername, + Email: p.Email, + EmailVerified: true, + Groups: p.Groups, }, true, nil } @@ -591,8 +593,10 @@ func (db passwordDB) Refresh(ctx context.Context, s connector.Scopes, identity c // refreshed token. // // No other fields are expected to be refreshable as email is effectively used - // as an ID and this implementation doesn't deal with groups. + // as an ID. identity.Username = p.Username + identity.PreferredUsername = p.PreferredUsername + identity.Groups = p.Groups return identity, nil } diff --git a/server/server_test.go b/server/server_test.go index a922aa751c..312c0de974 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1279,10 +1279,12 @@ func TestPasswordDB(t *testing.T) { } s.CreatePassword(ctx, storage.Password{ - Email: "jane@example.com", - Username: "jane", - UserID: "foobar", - Hash: h, + Email: "jane@example.com", + Username: "jane", + PreferredUsername: "jane-public", + UserID: "foobar", + Groups: []string{"team-a", "team-a/admins"}, + Hash: h, }) tests := []struct { @@ -1298,10 +1300,12 @@ func TestPasswordDB(t *testing.T) { username: "jane@example.com", password: pw, wantIdentity: connector.Identity{ - Email: "jane@example.com", - Username: "jane", - UserID: "foobar", - EmailVerified: true, + Email: "jane@example.com", + Username: "jane", + PreferredUsername: "jane-public", + UserID: "foobar", + EmailVerified: true, + Groups: []string{"team-a", "team-a/admins"}, }, }, { diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index f9d219619d..333561d905 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -456,10 +456,12 @@ func testPasswordCRUD(t *testing.T, s storage.Storage) { } password1 := storage.Password{ - Email: "jane@example.com", - Hash: passwordHash1, - Username: "jane", - UserID: "foobar", + Email: "jane@example.com", + Hash: passwordHash1, + Username: "jane", + PreferredUsername: "jane-public", + UserID: "foobar", + Groups: []string{"team-a", "team-a/admins"}, } if err := s.CreatePassword(ctx, password1); err != nil { t.Fatalf("create password token: %v", err) @@ -475,10 +477,12 @@ func testPasswordCRUD(t *testing.T, s storage.Storage) { } password2 := storage.Password{ - Email: "john@example.com", - Hash: passwordHash2, - Username: "john", - UserID: "barfoo", + Email: "john@example.com", + Hash: passwordHash2, + Username: "john", + PreferredUsername: "john-public", + UserID: "barfoo", + Groups: []string{"team-b"}, } if err := s.CreatePassword(ctx, password2); err != nil { t.Fatalf("create password token: %v", err) diff --git a/storage/ent/client/password.go b/storage/ent/client/password.go index 2845fa8f76..c2dc07ca03 100644 --- a/storage/ent/client/password.go +++ b/storage/ent/client/password.go @@ -14,7 +14,9 @@ func (d *Database) CreatePassword(ctx context.Context, password storage.Password SetEmail(password.Email). SetHash(password.Hash). SetUsername(password.Username). + SetPreferredUsername(password.PreferredUsername). SetUserID(password.UserID). + SetGroups(password.Groups). Save(ctx) if err != nil { return convertDBError("create password: %w", err) @@ -86,7 +88,9 @@ func (d *Database) UpdatePassword(ctx context.Context, email string, updater fun SetEmail(newPassword.Email). SetHash(newPassword.Hash). SetUsername(newPassword.Username). + SetPreferredUsername(newPassword.PreferredUsername). SetUserID(newPassword.UserID). + SetGroups(newPassword.Groups). Save(ctx) if err != nil { return rollback(tx, "update password uploading: %w", err) diff --git a/storage/ent/client/types.go b/storage/ent/client/types.go index 397d4d30a2..27f20cfa8a 100644 --- a/storage/ent/client/types.go +++ b/storage/ent/client/types.go @@ -139,10 +139,12 @@ func toStorageRefreshToken(r *db.RefreshToken) storage.RefreshToken { func toStoragePassword(p *db.Password) storage.Password { return storage.Password{ - Email: p.Email, - Hash: p.Hash, - Username: p.Username, - UserID: p.UserID, + Email: p.Email, + Hash: p.Hash, + Username: p.Username, + PreferredUsername: p.PreferredUsername, + UserID: p.UserID, + Groups: p.Groups, } } diff --git a/storage/ent/db/migrate/schema.go b/storage/ent/db/migrate/schema.go index d3295a0c79..3fee7bd5e2 100644 --- a/storage/ent/db/migrate/schema.go +++ b/storage/ent/db/migrate/schema.go @@ -161,7 +161,9 @@ var ( {Name: "email", Type: field.TypeString, Unique: true, Size: 2147483647, SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}}, {Name: "hash", Type: field.TypeBytes}, {Name: "username", Type: field.TypeString, Size: 2147483647, SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}}, + {Name: "preferred_username", Type: field.TypeString, Size: 2147483647, Default: "", SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}}, {Name: "user_id", Type: field.TypeString, Size: 2147483647, SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}}, + {Name: "groups", Type: field.TypeJSON, Nullable: true}, } // PasswordsTable holds the schema information for the "passwords" table. PasswordsTable = &schema.Table{ diff --git a/storage/ent/db/mutation.go b/storage/ent/db/mutation.go index 71203574e6..e0c1cc48b2 100644 --- a/storage/ent/db/mutation.go +++ b/storage/ent/db/mutation.go @@ -6314,17 +6314,20 @@ func (m *OfflineSessionMutation) ResetEdge(name string) error { // PasswordMutation represents an operation that mutates the Password nodes in the graph. type PasswordMutation struct { config - op Op - typ string - id *int - email *string - hash *[]byte - username *string - user_id *string - clearedFields map[string]struct{} - done bool - oldValue func(context.Context) (*Password, error) - predicates []predicate.Password + op Op + typ string + id *int + email *string + hash *[]byte + username *string + preferred_username *string + user_id *string + groups *[]string + appendgroups []string + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Password, error) + predicates []predicate.Password } var _ ent.Mutation = (*PasswordMutation)(nil) @@ -6533,6 +6536,42 @@ func (m *PasswordMutation) ResetUsername() { m.username = nil } +// SetPreferredUsername sets the "preferred_username" field. +func (m *PasswordMutation) SetPreferredUsername(s string) { + m.preferred_username = &s +} + +// PreferredUsername returns the value of the "preferred_username" field in the mutation. +func (m *PasswordMutation) PreferredUsername() (r string, exists bool) { + v := m.preferred_username + if v == nil { + return + } + return *v, true +} + +// OldPreferredUsername returns the old "preferred_username" field's value of the Password entity. +// If the Password object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PasswordMutation) OldPreferredUsername(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPreferredUsername is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPreferredUsername requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPreferredUsername: %w", err) + } + return oldValue.PreferredUsername, nil +} + +// ResetPreferredUsername resets all changes to the "preferred_username" field. +func (m *PasswordMutation) ResetPreferredUsername() { + m.preferred_username = nil +} + // SetUserID sets the "user_id" field. func (m *PasswordMutation) SetUserID(s string) { m.user_id = &s @@ -6569,6 +6608,71 @@ func (m *PasswordMutation) ResetUserID() { m.user_id = nil } +// SetGroups sets the "groups" field. +func (m *PasswordMutation) SetGroups(s []string) { + m.groups = &s + m.appendgroups = nil +} + +// Groups returns the value of the "groups" field in the mutation. +func (m *PasswordMutation) Groups() (r []string, exists bool) { + v := m.groups + if v == nil { + return + } + return *v, true +} + +// OldGroups returns the old "groups" field's value of the Password entity. +// If the Password object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PasswordMutation) OldGroups(ctx context.Context) (v []string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGroups is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGroups requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGroups: %w", err) + } + return oldValue.Groups, nil +} + +// AppendGroups adds s to the "groups" field. +func (m *PasswordMutation) AppendGroups(s []string) { + m.appendgroups = append(m.appendgroups, s...) +} + +// AppendedGroups returns the list of values that were appended to the "groups" field in this mutation. +func (m *PasswordMutation) AppendedGroups() ([]string, bool) { + if len(m.appendgroups) == 0 { + return nil, false + } + return m.appendgroups, true +} + +// ClearGroups clears the value of the "groups" field. +func (m *PasswordMutation) ClearGroups() { + m.groups = nil + m.appendgroups = nil + m.clearedFields[password.FieldGroups] = struct{}{} +} + +// GroupsCleared returns if the "groups" field was cleared in this mutation. +func (m *PasswordMutation) GroupsCleared() bool { + _, ok := m.clearedFields[password.FieldGroups] + return ok +} + +// ResetGroups resets all changes to the "groups" field. +func (m *PasswordMutation) ResetGroups() { + m.groups = nil + m.appendgroups = nil + delete(m.clearedFields, password.FieldGroups) +} + // Where appends a list predicates to the PasswordMutation builder. func (m *PasswordMutation) Where(ps ...predicate.Password) { m.predicates = append(m.predicates, ps...) @@ -6603,7 +6707,7 @@ func (m *PasswordMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *PasswordMutation) Fields() []string { - fields := make([]string, 0, 4) + fields := make([]string, 0, 6) if m.email != nil { fields = append(fields, password.FieldEmail) } @@ -6613,9 +6717,15 @@ func (m *PasswordMutation) Fields() []string { if m.username != nil { fields = append(fields, password.FieldUsername) } + if m.preferred_username != nil { + fields = append(fields, password.FieldPreferredUsername) + } if m.user_id != nil { fields = append(fields, password.FieldUserID) } + if m.groups != nil { + fields = append(fields, password.FieldGroups) + } return fields } @@ -6630,8 +6740,12 @@ func (m *PasswordMutation) Field(name string) (ent.Value, bool) { return m.Hash() case password.FieldUsername: return m.Username() + case password.FieldPreferredUsername: + return m.PreferredUsername() case password.FieldUserID: return m.UserID() + case password.FieldGroups: + return m.Groups() } return nil, false } @@ -6647,8 +6761,12 @@ func (m *PasswordMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldHash(ctx) case password.FieldUsername: return m.OldUsername(ctx) + case password.FieldPreferredUsername: + return m.OldPreferredUsername(ctx) case password.FieldUserID: return m.OldUserID(ctx) + case password.FieldGroups: + return m.OldGroups(ctx) } return nil, fmt.Errorf("unknown Password field %s", name) } @@ -6679,6 +6797,13 @@ func (m *PasswordMutation) SetField(name string, value ent.Value) error { } m.SetUsername(v) return nil + case password.FieldPreferredUsername: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPreferredUsername(v) + return nil case password.FieldUserID: v, ok := value.(string) if !ok { @@ -6686,6 +6811,13 @@ func (m *PasswordMutation) SetField(name string, value ent.Value) error { } m.SetUserID(v) return nil + case password.FieldGroups: + v, ok := value.([]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGroups(v) + return nil } return fmt.Errorf("unknown Password field %s", name) } @@ -6715,7 +6847,11 @@ func (m *PasswordMutation) AddField(name string, value ent.Value) error { // ClearedFields returns all nullable fields that were cleared during this // mutation. func (m *PasswordMutation) ClearedFields() []string { - return nil + var fields []string + if m.FieldCleared(password.FieldGroups) { + fields = append(fields, password.FieldGroups) + } + return fields } // FieldCleared returns a boolean indicating if a field with the given name was @@ -6728,6 +6864,11 @@ func (m *PasswordMutation) FieldCleared(name string) bool { // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. func (m *PasswordMutation) ClearField(name string) error { + switch name { + case password.FieldGroups: + m.ClearGroups() + return nil + } return fmt.Errorf("unknown Password nullable field %s", name) } @@ -6744,9 +6885,15 @@ func (m *PasswordMutation) ResetField(name string) error { case password.FieldUsername: m.ResetUsername() return nil + case password.FieldPreferredUsername: + m.ResetPreferredUsername() + return nil case password.FieldUserID: m.ResetUserID() return nil + case password.FieldGroups: + m.ResetGroups() + return nil } return fmt.Errorf("unknown Password field %s", name) } diff --git a/storage/ent/db/password.go b/storage/ent/db/password.go index e2ceec8f22..26f4089b04 100644 --- a/storage/ent/db/password.go +++ b/storage/ent/db/password.go @@ -3,6 +3,7 @@ package db import ( + "encoding/json" "fmt" "strings" @@ -22,8 +23,12 @@ type Password struct { Hash []byte `json:"hash,omitempty"` // Username holds the value of the "username" field. Username string `json:"username,omitempty"` + // PreferredUsername holds the value of the "preferred_username" field. + PreferredUsername string `json:"preferred_username,omitempty"` // UserID holds the value of the "user_id" field. - UserID string `json:"user_id,omitempty"` + UserID string `json:"user_id,omitempty"` + // Groups holds the value of the "groups" field. + Groups []string `json:"groups,omitempty"` selectValues sql.SelectValues } @@ -32,11 +37,11 @@ func (*Password) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case password.FieldHash: + case password.FieldHash, password.FieldGroups: values[i] = new([]byte) case password.FieldID: values[i] = new(sql.NullInt64) - case password.FieldEmail, password.FieldUsername, password.FieldUserID: + case password.FieldEmail, password.FieldUsername, password.FieldPreferredUsername, password.FieldUserID: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -77,12 +82,26 @@ func (_m *Password) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Username = value.String } + case password.FieldPreferredUsername: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field preferred_username", values[i]) + } else if value.Valid { + _m.PreferredUsername = value.String + } case password.FieldUserID: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field user_id", values[i]) } else if value.Valid { _m.UserID = value.String } + case password.FieldGroups: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field groups", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Groups); err != nil { + return fmt.Errorf("unmarshal field groups: %w", err) + } + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -128,8 +147,14 @@ func (_m *Password) String() string { builder.WriteString("username=") builder.WriteString(_m.Username) builder.WriteString(", ") + builder.WriteString("preferred_username=") + builder.WriteString(_m.PreferredUsername) + builder.WriteString(", ") builder.WriteString("user_id=") builder.WriteString(_m.UserID) + builder.WriteString(", ") + builder.WriteString("groups=") + builder.WriteString(fmt.Sprintf("%v", _m.Groups)) builder.WriteByte(')') return builder.String() } diff --git a/storage/ent/db/password/password.go b/storage/ent/db/password/password.go index 37ab1e49a0..4502db306c 100644 --- a/storage/ent/db/password/password.go +++ b/storage/ent/db/password/password.go @@ -17,8 +17,12 @@ const ( FieldHash = "hash" // FieldUsername holds the string denoting the username field in the database. FieldUsername = "username" + // FieldPreferredUsername holds the string denoting the preferred_username field in the database. + FieldPreferredUsername = "preferred_username" // FieldUserID holds the string denoting the user_id field in the database. FieldUserID = "user_id" + // FieldGroups holds the string denoting the groups field in the database. + FieldGroups = "groups" // Table holds the table name of the password in the database. Table = "passwords" ) @@ -29,7 +33,9 @@ var Columns = []string{ FieldEmail, FieldHash, FieldUsername, + FieldPreferredUsername, FieldUserID, + FieldGroups, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -47,6 +53,8 @@ var ( EmailValidator func(string) error // UsernameValidator is a validator for the "username" field. It is called by the builders before save. UsernameValidator func(string) error + // DefaultPreferredUsername holds the default value on creation for the "preferred_username" field. + DefaultPreferredUsername string // UserIDValidator is a validator for the "user_id" field. It is called by the builders before save. UserIDValidator func(string) error ) @@ -69,6 +77,11 @@ func ByUsername(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUsername, opts...).ToFunc() } +// ByPreferredUsername orders the results by the preferred_username field. +func ByPreferredUsername(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPreferredUsername, opts...).ToFunc() +} + // ByUserID orders the results by the user_id field. func ByUserID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUserID, opts...).ToFunc() diff --git a/storage/ent/db/password/where.go b/storage/ent/db/password/where.go index 105a8d4fc2..f710e24825 100644 --- a/storage/ent/db/password/where.go +++ b/storage/ent/db/password/where.go @@ -67,6 +67,11 @@ func Username(v string) predicate.Password { return predicate.Password(sql.FieldEQ(FieldUsername, v)) } +// PreferredUsername applies equality check predicate on the "preferred_username" field. It's identical to PreferredUsernameEQ. +func PreferredUsername(v string) predicate.Password { + return predicate.Password(sql.FieldEQ(FieldPreferredUsername, v)) +} + // UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ. func UserID(v string) predicate.Password { return predicate.Password(sql.FieldEQ(FieldUserID, v)) @@ -242,6 +247,71 @@ func UsernameContainsFold(v string) predicate.Password { return predicate.Password(sql.FieldContainsFold(FieldUsername, v)) } +// PreferredUsernameEQ applies the EQ predicate on the "preferred_username" field. +func PreferredUsernameEQ(v string) predicate.Password { + return predicate.Password(sql.FieldEQ(FieldPreferredUsername, v)) +} + +// PreferredUsernameNEQ applies the NEQ predicate on the "preferred_username" field. +func PreferredUsernameNEQ(v string) predicate.Password { + return predicate.Password(sql.FieldNEQ(FieldPreferredUsername, v)) +} + +// PreferredUsernameIn applies the In predicate on the "preferred_username" field. +func PreferredUsernameIn(vs ...string) predicate.Password { + return predicate.Password(sql.FieldIn(FieldPreferredUsername, vs...)) +} + +// PreferredUsernameNotIn applies the NotIn predicate on the "preferred_username" field. +func PreferredUsernameNotIn(vs ...string) predicate.Password { + return predicate.Password(sql.FieldNotIn(FieldPreferredUsername, vs...)) +} + +// PreferredUsernameGT applies the GT predicate on the "preferred_username" field. +func PreferredUsernameGT(v string) predicate.Password { + return predicate.Password(sql.FieldGT(FieldPreferredUsername, v)) +} + +// PreferredUsernameGTE applies the GTE predicate on the "preferred_username" field. +func PreferredUsernameGTE(v string) predicate.Password { + return predicate.Password(sql.FieldGTE(FieldPreferredUsername, v)) +} + +// PreferredUsernameLT applies the LT predicate on the "preferred_username" field. +func PreferredUsernameLT(v string) predicate.Password { + return predicate.Password(sql.FieldLT(FieldPreferredUsername, v)) +} + +// PreferredUsernameLTE applies the LTE predicate on the "preferred_username" field. +func PreferredUsernameLTE(v string) predicate.Password { + return predicate.Password(sql.FieldLTE(FieldPreferredUsername, v)) +} + +// PreferredUsernameContains applies the Contains predicate on the "preferred_username" field. +func PreferredUsernameContains(v string) predicate.Password { + return predicate.Password(sql.FieldContains(FieldPreferredUsername, v)) +} + +// PreferredUsernameHasPrefix applies the HasPrefix predicate on the "preferred_username" field. +func PreferredUsernameHasPrefix(v string) predicate.Password { + return predicate.Password(sql.FieldHasPrefix(FieldPreferredUsername, v)) +} + +// PreferredUsernameHasSuffix applies the HasSuffix predicate on the "preferred_username" field. +func PreferredUsernameHasSuffix(v string) predicate.Password { + return predicate.Password(sql.FieldHasSuffix(FieldPreferredUsername, v)) +} + +// PreferredUsernameEqualFold applies the EqualFold predicate on the "preferred_username" field. +func PreferredUsernameEqualFold(v string) predicate.Password { + return predicate.Password(sql.FieldEqualFold(FieldPreferredUsername, v)) +} + +// PreferredUsernameContainsFold applies the ContainsFold predicate on the "preferred_username" field. +func PreferredUsernameContainsFold(v string) predicate.Password { + return predicate.Password(sql.FieldContainsFold(FieldPreferredUsername, v)) +} + // UserIDEQ applies the EQ predicate on the "user_id" field. func UserIDEQ(v string) predicate.Password { return predicate.Password(sql.FieldEQ(FieldUserID, v)) @@ -307,6 +377,16 @@ func UserIDContainsFold(v string) predicate.Password { return predicate.Password(sql.FieldContainsFold(FieldUserID, v)) } +// GroupsIsNil applies the IsNil predicate on the "groups" field. +func GroupsIsNil() predicate.Password { + return predicate.Password(sql.FieldIsNull(FieldGroups)) +} + +// GroupsNotNil applies the NotNil predicate on the "groups" field. +func GroupsNotNil() predicate.Password { + return predicate.Password(sql.FieldNotNull(FieldGroups)) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Password) predicate.Password { return predicate.Password(sql.AndPredicates(predicates...)) diff --git a/storage/ent/db/password_create.go b/storage/ent/db/password_create.go index 4d68de8f38..b5cbf32553 100644 --- a/storage/ent/db/password_create.go +++ b/storage/ent/db/password_create.go @@ -37,12 +37,32 @@ func (_c *PasswordCreate) SetUsername(v string) *PasswordCreate { return _c } +// SetPreferredUsername sets the "preferred_username" field. +func (_c *PasswordCreate) SetPreferredUsername(v string) *PasswordCreate { + _c.mutation.SetPreferredUsername(v) + return _c +} + +// SetNillablePreferredUsername sets the "preferred_username" field if the given value is not nil. +func (_c *PasswordCreate) SetNillablePreferredUsername(v *string) *PasswordCreate { + if v != nil { + _c.SetPreferredUsername(*v) + } + return _c +} + // SetUserID sets the "user_id" field. func (_c *PasswordCreate) SetUserID(v string) *PasswordCreate { _c.mutation.SetUserID(v) return _c } +// SetGroups sets the "groups" field. +func (_c *PasswordCreate) SetGroups(v []string) *PasswordCreate { + _c.mutation.SetGroups(v) + return _c +} + // Mutation returns the PasswordMutation object of the builder. func (_c *PasswordCreate) Mutation() *PasswordMutation { return _c.mutation @@ -50,6 +70,7 @@ func (_c *PasswordCreate) Mutation() *PasswordMutation { // Save creates the Password in the database. func (_c *PasswordCreate) Save(ctx context.Context) (*Password, error) { + _c.defaults() return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) } @@ -75,6 +96,14 @@ func (_c *PasswordCreate) ExecX(ctx context.Context) { } } +// defaults sets the default values of the builder before save. +func (_c *PasswordCreate) defaults() { + if _, ok := _c.mutation.PreferredUsername(); !ok { + v := password.DefaultPreferredUsername + _c.mutation.SetPreferredUsername(v) + } +} + // check runs all checks and user-defined validators on the builder. func (_c *PasswordCreate) check() error { if _, ok := _c.mutation.Email(); !ok { @@ -96,6 +125,9 @@ func (_c *PasswordCreate) check() error { return &ValidationError{Name: "username", err: fmt.Errorf(`db: validator failed for field "Password.username": %w`, err)} } } + if _, ok := _c.mutation.PreferredUsername(); !ok { + return &ValidationError{Name: "preferred_username", err: errors.New(`db: missing required field "Password.preferred_username"`)} + } if _, ok := _c.mutation.UserID(); !ok { return &ValidationError{Name: "user_id", err: errors.New(`db: missing required field "Password.user_id"`)} } @@ -142,10 +174,18 @@ func (_c *PasswordCreate) createSpec() (*Password, *sqlgraph.CreateSpec) { _spec.SetField(password.FieldUsername, field.TypeString, value) _node.Username = value } + if value, ok := _c.mutation.PreferredUsername(); ok { + _spec.SetField(password.FieldPreferredUsername, field.TypeString, value) + _node.PreferredUsername = value + } if value, ok := _c.mutation.UserID(); ok { _spec.SetField(password.FieldUserID, field.TypeString, value) _node.UserID = value } + if value, ok := _c.mutation.Groups(); ok { + _spec.SetField(password.FieldGroups, field.TypeJSON, value) + _node.Groups = value + } return _node, _spec } @@ -167,6 +207,7 @@ func (_c *PasswordCreateBulk) Save(ctx context.Context) ([]*Password, error) { for i := range _c.builders { func(i int, root context.Context) { builder := _c.builders[i] + builder.defaults() var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { mutation, ok := m.(*PasswordMutation) if !ok { diff --git a/storage/ent/db/password_update.go b/storage/ent/db/password_update.go index 75394a78a2..3c5f7f05a8 100644 --- a/storage/ent/db/password_update.go +++ b/storage/ent/db/password_update.go @@ -9,6 +9,7 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/dialect/sql/sqljson" "entgo.io/ent/schema/field" "github.com/dexidp/dex/storage/ent/db/password" "github.com/dexidp/dex/storage/ent/db/predicate" @@ -61,6 +62,20 @@ func (_u *PasswordUpdate) SetNillableUsername(v *string) *PasswordUpdate { return _u } +// SetPreferredUsername sets the "preferred_username" field. +func (_u *PasswordUpdate) SetPreferredUsername(v string) *PasswordUpdate { + _u.mutation.SetPreferredUsername(v) + return _u +} + +// SetNillablePreferredUsername sets the "preferred_username" field if the given value is not nil. +func (_u *PasswordUpdate) SetNillablePreferredUsername(v *string) *PasswordUpdate { + if v != nil { + _u.SetPreferredUsername(*v) + } + return _u +} + // SetUserID sets the "user_id" field. func (_u *PasswordUpdate) SetUserID(v string) *PasswordUpdate { _u.mutation.SetUserID(v) @@ -75,6 +90,24 @@ func (_u *PasswordUpdate) SetNillableUserID(v *string) *PasswordUpdate { return _u } +// SetGroups sets the "groups" field. +func (_u *PasswordUpdate) SetGroups(v []string) *PasswordUpdate { + _u.mutation.SetGroups(v) + return _u +} + +// AppendGroups appends value to the "groups" field. +func (_u *PasswordUpdate) AppendGroups(v []string) *PasswordUpdate { + _u.mutation.AppendGroups(v) + return _u +} + +// ClearGroups clears the value of the "groups" field. +func (_u *PasswordUpdate) ClearGroups() *PasswordUpdate { + _u.mutation.ClearGroups() + return _u +} + // Mutation returns the PasswordMutation object of the builder. func (_u *PasswordUpdate) Mutation() *PasswordMutation { return _u.mutation @@ -148,9 +181,23 @@ func (_u *PasswordUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.Username(); ok { _spec.SetField(password.FieldUsername, field.TypeString, value) } + if value, ok := _u.mutation.PreferredUsername(); ok { + _spec.SetField(password.FieldPreferredUsername, field.TypeString, value) + } if value, ok := _u.mutation.UserID(); ok { _spec.SetField(password.FieldUserID, field.TypeString, value) } + if value, ok := _u.mutation.Groups(); ok { + _spec.SetField(password.FieldGroups, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedGroups(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, password.FieldGroups, value) + }) + } + if _u.mutation.GroupsCleared() { + _spec.ClearField(password.FieldGroups, field.TypeJSON) + } if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{password.Label} @@ -205,6 +252,20 @@ func (_u *PasswordUpdateOne) SetNillableUsername(v *string) *PasswordUpdateOne { return _u } +// SetPreferredUsername sets the "preferred_username" field. +func (_u *PasswordUpdateOne) SetPreferredUsername(v string) *PasswordUpdateOne { + _u.mutation.SetPreferredUsername(v) + return _u +} + +// SetNillablePreferredUsername sets the "preferred_username" field if the given value is not nil. +func (_u *PasswordUpdateOne) SetNillablePreferredUsername(v *string) *PasswordUpdateOne { + if v != nil { + _u.SetPreferredUsername(*v) + } + return _u +} + // SetUserID sets the "user_id" field. func (_u *PasswordUpdateOne) SetUserID(v string) *PasswordUpdateOne { _u.mutation.SetUserID(v) @@ -219,6 +280,24 @@ func (_u *PasswordUpdateOne) SetNillableUserID(v *string) *PasswordUpdateOne { return _u } +// SetGroups sets the "groups" field. +func (_u *PasswordUpdateOne) SetGroups(v []string) *PasswordUpdateOne { + _u.mutation.SetGroups(v) + return _u +} + +// AppendGroups appends value to the "groups" field. +func (_u *PasswordUpdateOne) AppendGroups(v []string) *PasswordUpdateOne { + _u.mutation.AppendGroups(v) + return _u +} + +// ClearGroups clears the value of the "groups" field. +func (_u *PasswordUpdateOne) ClearGroups() *PasswordUpdateOne { + _u.mutation.ClearGroups() + return _u +} + // Mutation returns the PasswordMutation object of the builder. func (_u *PasswordUpdateOne) Mutation() *PasswordMutation { return _u.mutation @@ -322,9 +401,23 @@ func (_u *PasswordUpdateOne) sqlSave(ctx context.Context) (_node *Password, err if value, ok := _u.mutation.Username(); ok { _spec.SetField(password.FieldUsername, field.TypeString, value) } + if value, ok := _u.mutation.PreferredUsername(); ok { + _spec.SetField(password.FieldPreferredUsername, field.TypeString, value) + } if value, ok := _u.mutation.UserID(); ok { _spec.SetField(password.FieldUserID, field.TypeString, value) } + if value, ok := _u.mutation.Groups(); ok { + _spec.SetField(password.FieldGroups, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedGroups(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, password.FieldGroups, value) + }) + } + if _u.mutation.GroupsCleared() { + _spec.ClearField(password.FieldGroups, field.TypeJSON) + } _node = &Password{config: _u.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/storage/ent/db/runtime.go b/storage/ent/db/runtime.go index 797c97613b..055a721715 100644 --- a/storage/ent/db/runtime.go +++ b/storage/ent/db/runtime.go @@ -212,8 +212,12 @@ func init() { passwordDescUsername := passwordFields[2].Descriptor() // password.UsernameValidator is a validator for the "username" field. It is called by the builders before save. password.UsernameValidator = passwordDescUsername.Validators[0].(func(string) error) + // passwordDescPreferredUsername is the schema descriptor for preferred_username field. + passwordDescPreferredUsername := passwordFields[3].Descriptor() + // password.DefaultPreferredUsername holds the default value on creation for the preferred_username field. + password.DefaultPreferredUsername = passwordDescPreferredUsername.Default.(string) // passwordDescUserID is the schema descriptor for user_id field. - passwordDescUserID := passwordFields[3].Descriptor() + passwordDescUserID := passwordFields[4].Descriptor() // password.UserIDValidator is a validator for the "user_id" field. It is called by the builders before save. password.UserIDValidator = passwordDescUserID.Validators[0].(func(string) error) refreshtokenFields := schema.RefreshToken{}.Fields() diff --git a/storage/ent/schema/password.go b/storage/ent/schema/password.go index cbc72fc52e..af7513c158 100644 --- a/storage/ent/schema/password.go +++ b/storage/ent/schema/password.go @@ -32,9 +32,14 @@ func (Password) Fields() []ent.Field { field.Text("username"). SchemaType(textSchema). NotEmpty(), + field.Text("preferred_username"). + SchemaType(textSchema). + Default(""), field.Text("user_id"). SchemaType(textSchema). NotEmpty(), + field.JSON("groups", []string{}). + Optional(), } } diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go index eae5b7a6de..a53e25549f 100644 --- a/storage/kubernetes/storage.go +++ b/storage/kubernetes/storage.go @@ -383,13 +383,7 @@ func (cli *client) ListPasswords(ctx context.Context) (passwords []storage.Passw } for _, password := range passwordList.Passwords { - p := storage.Password{ - Email: password.Email, - Hash: password.Hash, - Username: password.Username, - UserID: password.UserID, - } - passwords = append(passwords, p) + passwords = append(passwords, toStoragePassword(password)) } return diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index c126ddc087..a9806add80 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -431,9 +431,11 @@ type Password struct { // This field is IMMUTABLE. Do not change. Email string `json:"email,omitempty"` - Hash []byte `json:"hash,omitempty"` - Username string `json:"username,omitempty"` - UserID string `json:"userID,omitempty"` + Hash []byte `json:"hash,omitempty"` + Username string `json:"username,omitempty"` + PreferredUsername string `json:"preferredUsername,omitempty"` + UserID string `json:"userID,omitempty"` + Groups []string `json:"groups,omitempty"` } // PasswordList is a list of Passwords. @@ -454,19 +456,23 @@ func (cli *client) fromStoragePassword(p storage.Password) Password { Name: cli.idToName(email), Namespace: cli.namespace, }, - Email: email, - Hash: p.Hash, - Username: p.Username, - UserID: p.UserID, + Email: email, + Hash: p.Hash, + Username: p.Username, + PreferredUsername: p.PreferredUsername, + UserID: p.UserID, + Groups: p.Groups, } } func toStoragePassword(p Password) storage.Password { return storage.Password{ - Email: p.Email, - Hash: p.Hash, - Username: p.Username, - UserID: p.UserID, + Email: p.Email, + Hash: p.Hash, + Username: p.Username, + PreferredUsername: p.PreferredUsername, + UserID: p.UserID, + Groups: p.Groups, } } diff --git a/storage/sql/crud.go b/storage/sql/crud.go index a9ca38167d..b7b3265053 100644 --- a/storage/sql/crud.go +++ b/storage/sql/crud.go @@ -598,13 +598,13 @@ func (c *conn) CreatePassword(ctx context.Context, p storage.Password) error { p.Email = strings.ToLower(p.Email) _, err := c.Exec(` insert into password ( - email, hash, username, user_id + email, hash, username, preferred_username, user_id, groups ) values ( - $1, $2, $3, $4 + $1, $2, $3, $4, $5, $6 ); `, - p.Email, p.Hash, p.Username, p.UserID, + p.Email, p.Hash, p.Username, p.PreferredUsername, p.UserID, encoder(p.Groups), ) if err != nil { if c.alreadyExistsCheck(err) { @@ -629,10 +629,10 @@ func (c *conn) UpdatePassword(ctx context.Context, email string, updater func(p _, err = tx.Exec(` update password set - hash = $1, username = $2, user_id = $3 - where email = $4; + hash = $1, username = $2, preferred_username = $3, user_id = $4, groups = $5 + where email = $6; `, - np.Hash, np.Username, np.UserID, p.Email, + np.Hash, np.Username, np.PreferredUsername, np.UserID, encoder(np.Groups), p.Email, ) if err != nil { return fmt.Errorf("update password: %v", err) @@ -648,7 +648,7 @@ func (c *conn) GetPassword(ctx context.Context, email string) (storage.Password, func getPassword(ctx context.Context, q querier, email string) (p storage.Password, err error) { return scanPassword(q.QueryRow(` select - email, hash, username, user_id + email, hash, username, preferred_username, user_id, groups from password where email = $1; `, strings.ToLower(email))) } @@ -656,7 +656,7 @@ func getPassword(ctx context.Context, q querier, email string) (p storage.Passwo func (c *conn) ListPasswords(ctx context.Context) ([]storage.Password, error) { rows, err := c.Query(` select - email, hash, username, user_id + email, hash, username, preferred_username, user_id, groups from password; `) if err != nil { @@ -680,7 +680,7 @@ func (c *conn) ListPasswords(ctx context.Context) ([]storage.Password, error) { func scanPassword(s scanner) (p storage.Password, err error) { err = s.Scan( - &p.Email, &p.Hash, &p.Username, &p.UserID, + &p.Email, &p.Hash, &p.Username, &p.PreferredUsername, &p.UserID, decoder(&p.Groups), ) if err != nil { if err == sql.ErrNoRows { diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go index 83e9c20d94..dc8eee05e1 100644 --- a/storage/sql/migrate.go +++ b/storage/sql/migrate.go @@ -298,4 +298,44 @@ var migrations = []migration{ add column hmac_key bytea;`, }, }, + { + stmts: []string{ + ` + alter table password + add column preferred_username text not null default '';`, + ` + alter table password + add column groups bytea not null default convert_to('[]', 'UTF8');`, + }, + flavor: &flavorPostgres, + }, + { + stmts: []string{ + ` + alter table password + add column preferred_username text not null default '';`, + ` + alter table password + add column groups bytea not null default '[]';`, + }, + flavor: &flavorSQLite3, + }, + { + stmts: []string{ + ` + alter table password + add column preferred_username text not null default '';`, + ` + alter table password + add column groups bytea;`, + ` + update password + set groups = '[]' + where groups is null;`, + ` + alter table password + modify column groups bytea not null;`, + }, + flavor: &flavorMySQL, + }, } diff --git a/storage/storage.go b/storage/storage.go index 574b0a5a5e..79a2fca373 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -352,8 +352,14 @@ type Password struct { // Optional username to display. NOT used during login. Username string `json:"username"` + // Optional preferred username for OIDC "preferred_username" claim. + PreferredUsername string `json:"preferredUsername"` + // Randomly generated user ID. This is NOT the primary ID of the Password object. UserID string `json:"userID"` + + // Groups assigned to the user + Groups []string `json:"groups"` } // Connector is an object that contains the metadata about connectors used to login to Dex.