diff --git a/examples/k8s/thirdpartyresources.yaml b/examples/k8s/thirdpartyresources.yaml index 65e02256..40f03027 100644 --- a/examples/k8s/thirdpartyresources.yaml +++ b/examples/k8s/thirdpartyresources.yaml @@ -46,3 +46,12 @@ kind: ThirdPartyResource description: "Refresh tokens for clients to continuously act on behalf of an end user." versions: - name: v1 +--- + +metadata: + name: password.passwords.oidc.coreos.com +apiVersion: extensions/v1beta1 +kind: ThirdPartyResource +description: "Passwords managed by the OIDC server." +versions: +- name: v1 diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go index f6f97f17..ec703214 100644 --- a/storage/kubernetes/client.go +++ b/storage/kubernetes/client.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -165,6 +166,26 @@ func (c *client) delete(resource, name string) error { return checkHTTPErr(resp, http.StatusOK) } +func (c *client) deleteAll(resource string) error { + var list struct { + k8sapi.TypeMeta `json:",inline"` + k8sapi.ListMeta `json:"metadata,omitempty"` + Items []struct { + k8sapi.TypeMeta `json:",inline"` + k8sapi.ObjectMeta `json:"metadata,omitempty"` + } `json:"items"` + } + if err := c.list(resource, &list); err != nil { + return err + } + for _, item := range list.Items { + if err := c.delete(resource, item.Name); err != nil { + return err + } + } + return nil +} + func (c *client) put(resource, name string, v interface{}) error { body, err := json.Marshal(v) if err != nil { @@ -190,9 +211,9 @@ func (c *client) put(resource, name string, v interface{}) error { func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (*client, error) { tlsConfig := cryptopasta.DefaultTLSConfig() - data := func(b []byte, file string) ([]byte, error) { - if b != nil { - return b, nil + data := func(b string, file string) ([]byte, error) { + if b != "" { + return base64.StdEncoding.DecodeString(b) } if file == "" { return nil, nil diff --git a/storage/kubernetes/k8sapi/client.go b/storage/kubernetes/k8sapi/client.go index c8df7341..d84fa5cc 100644 --- a/storage/kubernetes/k8sapi/client.go +++ b/storage/kubernetes/k8sapi/client.go @@ -62,7 +62,9 @@ type Cluster struct { // CertificateAuthority is the path to a cert file for the certificate authority. CertificateAuthority string `yaml:"certificate-authority,omitempty"` // CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority - CertificateAuthorityData []byte `yaml:"certificate-authority-data,omitempty"` + // + // NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string. + CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"` // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields Extensions []NamedExtension `yaml:"extensions,omitempty"` } @@ -72,11 +74,15 @@ type AuthInfo struct { // ClientCertificate is the path to a client cert file for TLS. ClientCertificate string `yaml:"client-certificate,omitempty"` // ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate - ClientCertificateData []byte `yaml:"client-certificate-data,omitempty"` + // + // NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string. + ClientCertificateData string `yaml:"client-certificate-data,omitempty"` // ClientKey is the path to a client key file for TLS. ClientKey string `yaml:"client-key,omitempty"` // ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey - ClientKeyData []byte `yaml:"client-key-data,omitempty"` + // + // NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string. + ClientKeyData string `yaml:"client-key-data,omitempty"` // Token is the bearer token for authentication to the kubernetes cluster. Token string `yaml:"token,omitempty"` // Impersonate is the username to imperonate. The name matches the flag. diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go index bfb48855..44920f6b 100644 --- a/storage/kubernetes/storage.go +++ b/storage/kubernetes/storage.go @@ -20,6 +20,7 @@ const ( kindClient = "OAuth2Client" kindRefreshToken = "RefreshToken" kindKeys = "SigningKey" + kindPassword = "Password" ) const ( @@ -28,6 +29,7 @@ const ( resourceClient = "oauth2clients" resourceRefreshToken = "refreshtokens" resourceKeys = "signingkeies" // Kubernetes attempts to pluralize. + resourcePassword = "passwords" ) // Config values for the Kubernetes storage type. @@ -109,6 +111,10 @@ func (cli *client) CreateAuthCode(c storage.AuthCode) error { return cli.post(resourceAuthCode, cli.fromStorageAuthCode(c)) } +func (cli *client) CreatePassword(p storage.Password) error { + return cli.post(resourcePassword, cli.fromStoragePassword(p)) +} + func (cli *client) CreateRefresh(r storage.RefreshToken) error { refresh := RefreshToken{ TypeMeta: k8sapi.TypeMeta{ @@ -152,6 +158,14 @@ func (cli *client) GetClient(id string) (storage.Client, error) { return toStorageClient(c), nil } +func (cli *client) GetPassword(email string) (storage.Password, error) { + var p Password + if err := cli.get(resourcePassword, emailToID(email), &p); err != nil { + return storage.Password{}, err + } + return toStoragePassword(p), nil +} + func (cli *client) GetKeys() (storage.Keys, error) { var keys Keys if err := cli.get(resourceKeys, keysName, &keys); err != nil { @@ -199,6 +213,10 @@ func (cli *client) DeleteRefresh(id string) error { return cli.delete(resourceRefreshToken, id) } +func (cli *client) DeletePassword(email string) error { + return cli.delete(resourcePassword, emailToID(email)) +} + func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error { var c Client if err := cli.get(resourceClient, id, &c); err != nil { @@ -214,6 +232,23 @@ func (cli *client) UpdateClient(id string, updater func(old storage.Client) (sto return cli.put(resourceClient, id, newClient) } +func (cli *client) UpdatePassword(email string, updater func(old storage.Password) (storage.Password, error)) error { + id := emailToID(email) + var p Password + if err := cli.get(resourcePassword, id, &p); err != nil { + return err + } + + updated, err := updater(toStoragePassword(p)) + if err != nil { + return err + } + + newPassword := cli.fromStoragePassword(updated) + newPassword.ObjectMeta = p.ObjectMeta + return cli.put(resourcePassword, id, newPassword) +} + func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, error)) error { firstUpdate := false var keys Keys diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go index c0011f39..f41b01b1 100644 --- a/storage/kubernetes/storage_test.go +++ b/storage/kubernetes/storage_test.go @@ -75,7 +75,18 @@ func TestURLFor(t *testing.T) { func TestStorage(t *testing.T) { client := loadClient(t) conformance.RunTestSuite(t, func() storage.Storage { - // TODO(erichiang): Tear down namespaces between each iteration. + for _, resource := range []string{ + resourceAuthCode, + resourceAuthRequest, + resourceClient, + resourceRefreshToken, + resourceKeys, + resourcePassword, + } { + if err := client.deleteAll(resource); err != nil { + t.Fatalf("delete all %q failed: %v", resource, err) + } + } return client }) } diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index 8bc934f2..3c914e84 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -1,6 +1,8 @@ package kubernetes import ( + "encoding/base32" + "strings" "time" jose "gopkg.in/square/go-jose.v2" @@ -182,6 +184,60 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest { return req } +// Password is a mirrored struct from the stroage with JSON struct tags and +// Kubernetes type metadata. +type Password struct { + k8sapi.TypeMeta `json:",inline"` + k8sapi.ObjectMeta `json:"metadata,omitempty"` + + // The Kubernetes name is actually an encoded version of this value. + // + // 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"` +} + +// Kubernetes only allows lower case letters for names. +// +// NOTE(ericchiang): This is currently copied from the storage package's NewID() +// method. Once we refactor those into the storage, just use that instead. +var encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567") + +// Map an arbitrary email to a valid Kuberntes name. +func emailToID(email string) string { + return strings.TrimRight(encoding.EncodeToString([]byte(strings.ToLower(email))), "=") +} + +func (cli *client) fromStoragePassword(p storage.Password) Password { + email := strings.ToLower(p.Email) + return Password{ + TypeMeta: k8sapi.TypeMeta{ + Kind: kindPassword, + APIVersion: cli.apiVersionForResource(resourcePassword), + }, + ObjectMeta: k8sapi.ObjectMeta{ + Name: emailToID(email), + Namespace: cli.namespace, + }, + Email: email, + Hash: p.Hash, + Username: p.Username, + UserID: p.UserID, + } +} + +func toStoragePassword(p Password) storage.Password { + return storage.Password{ + Email: p.Email, + Hash: p.Hash, + Username: p.Username, + UserID: p.UserID, + } +} + // AuthCode is a mirrored struct from storage with JSON struct tags and // Kubernetes type metadata. type AuthCode struct {