storage: add connector object to backend storage.

This commit is contained in:
rithu john 2017-03-23 09:59:33 -07:00
parent 6e50c18458
commit bc55b86d0d
7 changed files with 363 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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