diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index 0d41ba2a..50e705ba 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -48,6 +48,7 @@ func RunTests(t *testing.T, newStorage func() storage.Storage) { {"PasswordCRUD", testPasswordCRUD}, {"KeysCRUD", testKeysCRUD}, {"OfflineSessionCRUD", testOfflineSessionCRUD}, + {"ConnectorCRUD", testConnectorCRUD}, {"GarbageCollection", testGC}, {"TimezoneSupport", testTimezones}, }) @@ -571,6 +572,74 @@ func testOfflineSessionCRUD(t *testing.T, s storage.Storage) { mustBeErrNotFound(t, "offline session", err) } +func testConnectorCRUD(t *testing.T, s storage.Storage) { + id1 := storage.NewID() + config1 := []byte(`{"issuer": "https://accounts.google.com"}`) + c1 := storage.Connector{ + ID: id1, + Type: "Default", + Name: "Default", + ResourceVersion: "1", + Config: config1, + } + + if err := s.CreateConnector(c1); err != nil { + t.Fatalf("create connector with ID = %s: %v", c1.ID, err) + } + + // Attempt to create same Connector twice. + err := s.CreateConnector(c1) + mustBeErrAlreadyExists(t, "connector", err) + + id2 := storage.NewID() + config2 := []byte(`{"redirectURIi": "http://127.0.0.1:5556/dex/callback"}`) + c2 := storage.Connector{ + ID: id2, + Type: "Mock", + Name: "Mock", + ResourceVersion: "2", + Config: config2, + } + + if err := s.CreateConnector(c2); err != nil { + t.Fatalf("create connector with ID = %s: %v", c2.ID, err) + } + + getAndCompare := func(id string, want storage.Connector) { + gr, err := s.GetConnector(id) + if err != nil { + t.Errorf("get connector: %v", err) + return + } + if diff := pretty.Compare(want, gr); diff != "" { + t.Errorf("connector retrieved from storage did not match: %s", diff) + } + } + + getAndCompare(id1, c1) + + if err := s.UpdateConnector(c1.ID, func(old storage.Connector) (storage.Connector, error) { + old.Type = "oidc" + return old, nil + }); err != nil { + t.Fatalf("failed to update Connector: %v", err) + } + + c1.Type = "oidc" + getAndCompare(id1, c1) + + if err := s.DeleteConnector(c1.ID); err != nil { + t.Fatalf("failed to delete connector: %v", err) + } + + if err := s.DeleteConnector(c2.ID); err != nil { + t.Fatalf("failed to delete connector: %v", err) + } + + _, err = s.GetConnector(c1.ID) + mustBeErrNotFound(t, "connector", err) +} + func testKeysCRUD(t *testing.T, s storage.Storage) { updateAndCompare := func(k storage.Keys) { err := s.UpdateKeys(func(oldKeys storage.Keys) (storage.Keys, error) { diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go index 10738915..ce7c58fe 100644 --- a/storage/kubernetes/storage.go +++ b/storage/kubernetes/storage.go @@ -20,6 +20,7 @@ const ( kindKeys = "SigningKey" kindPassword = "Password" kindOfflineSessions = "OfflineSessions" + kindConnector = "Connector" ) const ( @@ -30,6 +31,7 @@ const ( resourceKeys = "signingkeies" // Kubernetes attempts to pluralize. resourcePassword = "passwords" resourceOfflineSessions = "offlinesessionses" // Again attempts to pluralize. + resourceConnector = "connectors" ) // Config values for the Kubernetes storage type. @@ -173,6 +175,10 @@ func (cli *client) CreateOfflineSessions(o storage.OfflineSessions) error { return cli.post(resourceOfflineSessions, cli.fromStorageOfflineSessions(o)) } +func (cli *client) CreateConnector(c storage.Connector) error { + return cli.post(resourceConnector, cli.fromStorageConnector(c)) +} + func (cli *client) GetAuthRequest(id string) (storage.AuthRequest, error) { var req AuthRequest if err := cli.get(resourceAuthRequest, id, &req); err != nil { @@ -271,6 +277,14 @@ func (cli *client) getOfflineSessions(userID string, connID string) (o OfflineSe return o, nil } +func (cli *client) GetConnector(id string) (storage.Connector, error) { + var c Connector + if err := cli.get(resourceConnector, id, &c); err != nil { + return storage.Connector{}, err + } + return toStorageConnector(c), nil +} + func (cli *client) ListClients() ([]storage.Client, error) { return nil, errors.New("not implemented") } @@ -298,6 +312,20 @@ func (cli *client) ListPasswords() (passwords []storage.Password, err error) { return } +func (cli *client) ListConnectors() (connectors []storage.Connector, err error) { + var connectorList ConnectorList + if err = cli.list(resourceConnector, &connectorList); err != nil { + return connectors, fmt.Errorf("failed to list connectors: %v", err) + } + + connectors = make([]storage.Connector, len(connectorList.Connectors)) + for i, connector := range connectorList.Connectors { + connectors[i] = toStorageConnector(connector) + } + + return +} + func (cli *client) DeleteAuthRequest(id string) error { return cli.delete(resourceAuthRequest, id) } @@ -337,6 +365,10 @@ func (cli *client) DeleteOfflineSessions(userID string, connID string) error { return cli.delete(resourceOfflineSessions, o.ObjectMeta.Name) } +func (cli *client) DeleteConnector(id string) error { + return cli.delete(resourceConnector, id) +} + func (cli *client) UpdateRefreshToken(id string, updater func(old storage.RefreshToken) (storage.RefreshToken, error)) error { r, err := cli.getRefreshToken(id) if err != nil { @@ -446,6 +478,23 @@ func (cli *client) UpdateAuthRequest(id string, updater func(a storage.AuthReque return cli.put(resourceAuthRequest, id, newReq) } +func (cli *client) UpdateConnector(id string, updater func(a storage.Connector) (storage.Connector, error)) error { + var c Connector + err := cli.get(resourceConnector, id, &c) + if err != nil { + return err + } + + updated, err := updater(toStorageConnector(c)) + if err != nil { + return err + } + + newConn := cli.fromStorageConnector(updated) + newConn.ObjectMeta = c.ObjectMeta + return cli.put(resourceConnector, id, newConn) +} + func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err error) { var authRequests AuthRequestList if err := cli.list(resourceAuthRequest, &authRequests); err != nil { diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index c362452f..8dd16eb2 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -74,6 +74,14 @@ var thirdPartyResources = []k8sapi.ThirdPartyResource{ Description: "User sessions with an active refresh token.", Versions: []k8sapi.APIVersion{{Name: "v1"}}, }, + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "connector.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "Connectors available for login", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, } // There will only ever be a single keys resource. Maintain this by setting a @@ -513,3 +521,52 @@ func toStorageOfflineSessions(o OfflineSessions) storage.OfflineSessions { } return s } + +// Connector is a mirrored struct from storage with JSON struct tags and Kubernetes +// type metadata. +type Connector struct { + k8sapi.TypeMeta `json:",inline"` + k8sapi.ObjectMeta `json:"metadata,omitempty"` + + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty"` + // Config holds connector specific configuration information + Config []byte `json:"config,omitempty"` +} + +func (cli *client) fromStorageConnector(c storage.Connector) Connector { + return Connector{ + TypeMeta: k8sapi.TypeMeta{ + Kind: kindConnector, + APIVersion: cli.apiVersion, + }, + ObjectMeta: k8sapi.ObjectMeta{ + Name: c.ID, + Namespace: cli.namespace, + }, + ID: c.ID, + Type: c.Type, + Name: c.Name, + ResourceVersion: c.ResourceVersion, + Config: c.Config, + } +} + +func toStorageConnector(c Connector) storage.Connector { + return storage.Connector{ + ID: c.ID, + Type: c.Type, + Name: c.Name, + ResourceVersion: c.ResourceVersion, + Config: c.Config, + } +} + +// ConnectorList is a list of Connectors. +type ConnectorList struct { + k8sapi.TypeMeta `json:",inline"` + k8sapi.ListMeta `json:"metadata,omitempty"` + Connectors []Connector `json:"items"` +} diff --git a/storage/memory/memory.go b/storage/memory/memory.go index ac0b1d4e..97940c0e 100644 --- a/storage/memory/memory.go +++ b/storage/memory/memory.go @@ -19,6 +19,7 @@ func New(logger logrus.FieldLogger) storage.Storage { authReqs: make(map[string]storage.AuthRequest), passwords: make(map[string]storage.Password), offlineSessions: make(map[offlineSessionID]storage.OfflineSessions), + connectors: make(map[string]storage.Connector), logger: logger, } } @@ -44,6 +45,7 @@ type memStorage struct { authReqs map[string]storage.AuthRequest passwords map[string]storage.Password offlineSessions map[offlineSessionID]storage.OfflineSessions + connectors map[string]storage.Connector keys storage.Keys @@ -152,6 +154,17 @@ func (s *memStorage) CreateOfflineSessions(o storage.OfflineSessions) (err error return } +func (s *memStorage) CreateConnector(connector storage.Connector) (err error) { + s.tx(func() { + if _, ok := s.connectors[connector.ID]; ok { + err = storage.ErrAlreadyExists + } else { + s.connectors[connector.ID] = connector + } + }) + return +} + func (s *memStorage) GetAuthCode(id string) (c storage.AuthCode, err error) { s.tx(func() { var ok bool @@ -226,6 +239,16 @@ func (s *memStorage) GetOfflineSessions(userID string, connID string) (o storage return } +func (s *memStorage) GetConnector(id string) (connector storage.Connector, err error) { + s.tx(func() { + var ok bool + if connector, ok = s.connectors[id]; !ok { + err = storage.ErrNotFound + } + }) + return +} + func (s *memStorage) ListClients() (clients []storage.Client, err error) { s.tx(func() { for _, client := range s.clients { @@ -253,6 +276,15 @@ func (s *memStorage) ListPasswords() (passwords []storage.Password, err error) { return } +func (s *memStorage) ListConnectors() (conns []storage.Connector, err error) { + s.tx(func() { + for _, c := range s.connectors { + conns = append(conns, c) + } + }) + return +} + func (s *memStorage) DeletePassword(email string) (err error) { email = strings.ToLower(email) s.tx(func() { @@ -324,6 +356,17 @@ func (s *memStorage) DeleteOfflineSessions(userID string, connID string) (err er return } +func (s *memStorage) DeleteConnector(id string) (err error) { + s.tx(func() { + if _, ok := s.connectors[id]; !ok { + err = storage.ErrNotFound + return + } + delete(s.connectors, id) + }) + return +} + func (s *memStorage) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) (err error) { s.tx(func() { client, ok := s.clients[id] @@ -408,3 +451,17 @@ func (s *memStorage) UpdateOfflineSessions(userID string, connID string, updater }) return } + +func (s *memStorage) UpdateConnector(id string, updater func(c storage.Connector) (storage.Connector, error)) (err error) { + s.tx(func() { + r, ok := s.connectors[id] + if !ok { + err = storage.ErrNotFound + return + } + if r, err = updater(r); err == nil { + s.connectors[id] = r + } + }) + return +} diff --git a/storage/sql/crud.go b/storage/sql/crud.go index f8b941d1..17886b91 100644 --- a/storage/sql/crud.go +++ b/storage/sql/crud.go @@ -717,6 +717,104 @@ func scanOfflineSessions(s scanner) (o storage.OfflineSessions, err error) { return o, nil } +func (c *conn) CreateConnector(connector storage.Connector) error { + _, err := c.Exec(` + insert into connector ( + id, type, name, resource_version, config + ) + values ( + $1, $2, $3, $4, $5 + ); + `, + connector.ID, connector.Type, connector.Name, connector.ResourceVersion, connector.Config, + ) + if err != nil { + if c.alreadyExistsCheck(err) { + return storage.ErrAlreadyExists + } + return fmt.Errorf("insert connector: %v", err) + } + return nil +} + +func (c *conn) UpdateConnector(id string, updater func(s storage.Connector) (storage.Connector, error)) error { + return c.ExecTx(func(tx *trans) error { + connector, err := getConnector(tx, id) + if err != nil { + return err + } + + newConn, err := updater(connector) + if err != nil { + return err + } + _, err = tx.Exec(` + update connector + set + type = $1, + name = $2, + resource_version = $3, + config = $4 + where id = $5; + `, + newConn.Type, newConn.Name, newConn.ResourceVersion, newConn.Config, connector.ID, + ) + if err != nil { + return fmt.Errorf("update connector: %v", err) + } + return nil + }) +} + +func (c *conn) GetConnector(id string) (storage.Connector, error) { + return getConnector(c, id) +} + +func getConnector(q querier, id string) (storage.Connector, error) { + return scanConnector(q.QueryRow(` + select + id, type, name, resource_version, config + from connector + where id = $1; + `, id)) +} + +func scanConnector(s scanner) (c storage.Connector, err error) { + err = s.Scan( + &c.ID, &c.Type, &c.Name, &c.ResourceVersion, &c.Config, + ) + if err != nil { + if err == sql.ErrNoRows { + return c, storage.ErrNotFound + } + return c, fmt.Errorf("select connector: %v", err) + } + return c, nil +} + +func (c *conn) ListConnectors() ([]storage.Connector, error) { + rows, err := c.Query(` + select + id, type, name, resource_version, config + from connector; + `) + if err != nil { + return nil, err + } + var connectors []storage.Connector + for rows.Next() { + conn, err := scanConnector(rows) + if err != nil { + return nil, err + } + connectors = append(connectors, conn) + } + if err := rows.Err(); err != nil { + return nil, err + } + return connectors, nil +} + func (c *conn) DeleteAuthRequest(id string) error { return c.delete("auth_request", "id", id) } func (c *conn) DeleteAuthCode(id string) error { return c.delete("auth_code", "id", id) } func (c *conn) DeleteClient(id string) error { return c.delete("client", "id", id) } @@ -724,6 +822,7 @@ func (c *conn) DeleteRefresh(id string) error { return c.delete("refresh_tok func (c *conn) DeletePassword(email string) error { return c.delete("password", "email", strings.ToLower(email)) } +func (c *conn) DeleteConnector(id string) error { return c.delete("connector", "id", id) } func (c *conn) DeleteOfflineSessions(userID string, connID string) error { result, err := c.Exec(`delete from offline_session where user_id = $1 AND conn_id = $2`, userID, connID) diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go index 07ba4a22..6341037a 100644 --- a/storage/sql/migrate.go +++ b/storage/sql/migrate.go @@ -176,4 +176,15 @@ var migrations = []migration{ ); `, }, + { + stmt: ` + create table connector ( + id text not null primary key, + type text not null, + name text not null, + resource_version text not null, + config bytea + ); + `, + }, } diff --git a/storage/storage.go b/storage/storage.go index 869b7066..92ac2ee2 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -53,6 +53,7 @@ type Storage interface { CreateRefresh(r RefreshToken) error CreatePassword(p Password) error CreateOfflineSessions(s OfflineSessions) error + CreateConnector(c Connector) error // TODO(ericchiang): return (T, bool, error) so we can indicate not found // requests that way instead of using ErrNotFound. @@ -63,10 +64,12 @@ type Storage interface { GetRefresh(id string) (RefreshToken, error) GetPassword(email string) (Password, error) GetOfflineSessions(userID string, connID string) (OfflineSessions, error) + GetConnector(id string) (Connector, error) ListClients() ([]Client, error) ListRefreshTokens() ([]RefreshToken, error) ListPasswords() ([]Password, error) + ListConnectors() ([]Connector, error) // Delete methods MUST be atomic. DeleteAuthRequest(id string) error @@ -75,6 +78,7 @@ type Storage interface { DeleteRefresh(id string) error DeletePassword(email string) error DeleteOfflineSessions(userID string, connID string) error + DeleteConnector(id string) error // Update methods take a function for updating an object then performs that update within // a transaction. "updater" functions may be called multiple times by a single update call. @@ -96,6 +100,7 @@ type Storage interface { UpdateRefreshToken(id string, updater func(r RefreshToken) (RefreshToken, error)) error UpdatePassword(email string, updater func(p Password) (Password, error)) error UpdateOfflineSessions(userID string, connID string, updater func(s OfflineSessions) (OfflineSessions, error)) error + UpdateConnector(id string, updater func(c Connector) (Connector, error)) error // GarbageCollect deletes all expired AuthCodes and AuthRequests. GarbageCollect(now time.Time) (GCResult, error) @@ -290,6 +295,22 @@ type Password struct { UserID string `json:"userID"` } +// Connector is an object that contains the metadata about connectors used to login to Dex. +type Connector struct { + // ID that will uniquely identify the connector object. + ID string + // The Type of the connector. E.g. 'oidc' or 'ldap' + Type string + // The Name of the connector that is used when displaying it to the end user. + Name string + // ResourceVersion is the static versioning used to keep track of dynamic configuration + // changes to the connector object made by the API calls. + ResourceVersion string + // Config holds all the configuration information specific to the connector type. Since there + // no generic struct we can use for this purpose, it is stored as a byte stream. + Config []byte +} + // VerificationKey is a rotated signing key which can still be used to verify // signatures. type VerificationKey struct {