From bc55b86d0d607079f125e99a6248b9966746d694 Mon Sep 17 00:00:00 2001
From: rithu john <rithujohn191@gmail.com>
Date: Thu, 23 Mar 2017 09:59:33 -0700
Subject: [PATCH] storage: add connector object to backend storage.

---
 storage/conformance/conformance.go | 69 +++++++++++++++++++++
 storage/kubernetes/storage.go      | 49 +++++++++++++++
 storage/kubernetes/types.go        | 57 +++++++++++++++++
 storage/memory/memory.go           | 57 +++++++++++++++++
 storage/sql/crud.go                | 99 ++++++++++++++++++++++++++++++
 storage/sql/migrate.go             | 11 ++++
 storage/storage.go                 | 21 +++++++
 7 files changed, 363 insertions(+)

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 {
-- 
GitLab