*: add password resource to kubernetes storage implementation
This commit is contained in:
		| @@ -46,3 +46,12 @@ kind: ThirdPartyResource | |||||||
| description: "Refresh tokens for clients to continuously act on behalf of an end user." | description: "Refresh tokens for clients to continuously act on behalf of an end user." | ||||||
| versions: | versions: | ||||||
| - name: v1 | - name: v1 | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | metadata: | ||||||
|  |   name: password.passwords.oidc.coreos.com | ||||||
|  | apiVersion: extensions/v1beta1 | ||||||
|  | kind: ThirdPartyResource | ||||||
|  | description: "Passwords managed by the OIDC server." | ||||||
|  | versions: | ||||||
|  | - name: v1 | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
|  | 	"encoding/base64" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @@ -165,6 +166,26 @@ func (c *client) delete(resource, name string) error { | |||||||
| 	return checkHTTPErr(resp, http.StatusOK) | 	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 { | func (c *client) put(resource, name string, v interface{}) error { | ||||||
| 	body, err := json.Marshal(v) | 	body, err := json.Marshal(v) | ||||||
| 	if err != nil { | 	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) { | func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (*client, error) { | ||||||
| 	tlsConfig := cryptopasta.DefaultTLSConfig() | 	tlsConfig := cryptopasta.DefaultTLSConfig() | ||||||
| 	data := func(b []byte, file string) ([]byte, error) { | 	data := func(b string, file string) ([]byte, error) { | ||||||
| 		if b != nil { | 		if b != "" { | ||||||
| 			return b, nil | 			return base64.StdEncoding.DecodeString(b) | ||||||
| 		} | 		} | ||||||
| 		if file == "" { | 		if file == "" { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
|   | |||||||
| @@ -62,7 +62,9 @@ type Cluster struct { | |||||||
| 	// CertificateAuthority is the path to a cert file for the certificate authority. | 	// CertificateAuthority is the path to a cert file for the certificate authority. | ||||||
| 	CertificateAuthority string `yaml:"certificate-authority,omitempty"` | 	CertificateAuthority string `yaml:"certificate-authority,omitempty"` | ||||||
| 	// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority | 	// 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 holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields | ||||||
| 	Extensions []NamedExtension `yaml:"extensions,omitempty"` | 	Extensions []NamedExtension `yaml:"extensions,omitempty"` | ||||||
| } | } | ||||||
| @@ -72,11 +74,15 @@ type AuthInfo struct { | |||||||
| 	// ClientCertificate is the path to a client cert file for TLS. | 	// ClientCertificate is the path to a client cert file for TLS. | ||||||
| 	ClientCertificate string `yaml:"client-certificate,omitempty"` | 	ClientCertificate string `yaml:"client-certificate,omitempty"` | ||||||
| 	// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate | 	// 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 is the path to a client key file for TLS. | ||||||
| 	ClientKey string `yaml:"client-key,omitempty"` | 	ClientKey string `yaml:"client-key,omitempty"` | ||||||
| 	// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey | 	// 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 is the bearer token for authentication to the kubernetes cluster. | ||||||
| 	Token string `yaml:"token,omitempty"` | 	Token string `yaml:"token,omitempty"` | ||||||
| 	// Impersonate is the username to imperonate.  The name matches the flag. | 	// Impersonate is the username to imperonate.  The name matches the flag. | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ const ( | |||||||
| 	kindClient       = "OAuth2Client" | 	kindClient       = "OAuth2Client" | ||||||
| 	kindRefreshToken = "RefreshToken" | 	kindRefreshToken = "RefreshToken" | ||||||
| 	kindKeys         = "SigningKey" | 	kindKeys         = "SigningKey" | ||||||
|  | 	kindPassword     = "Password" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -28,6 +29,7 @@ const ( | |||||||
| 	resourceClient       = "oauth2clients" | 	resourceClient       = "oauth2clients" | ||||||
| 	resourceRefreshToken = "refreshtokens" | 	resourceRefreshToken = "refreshtokens" | ||||||
| 	resourceKeys         = "signingkeies" // Kubernetes attempts to pluralize. | 	resourceKeys         = "signingkeies" // Kubernetes attempts to pluralize. | ||||||
|  | 	resourcePassword     = "passwords" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Config values for the Kubernetes storage type. | // 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)) | 	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 { | func (cli *client) CreateRefresh(r storage.RefreshToken) error { | ||||||
| 	refresh := RefreshToken{ | 	refresh := RefreshToken{ | ||||||
| 		TypeMeta: k8sapi.TypeMeta{ | 		TypeMeta: k8sapi.TypeMeta{ | ||||||
| @@ -152,6 +158,14 @@ func (cli *client) GetClient(id string) (storage.Client, error) { | |||||||
| 	return toStorageClient(c), nil | 	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) { | func (cli *client) GetKeys() (storage.Keys, error) { | ||||||
| 	var keys Keys | 	var keys Keys | ||||||
| 	if err := cli.get(resourceKeys, keysName, &keys); err != nil { | 	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) | 	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 { | func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error { | ||||||
| 	var c Client | 	var c Client | ||||||
| 	if err := cli.get(resourceClient, id, &c); err != nil { | 	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) | 	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 { | func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, error)) error { | ||||||
| 	firstUpdate := false | 	firstUpdate := false | ||||||
| 	var keys Keys | 	var keys Keys | ||||||
|   | |||||||
| @@ -75,7 +75,18 @@ func TestURLFor(t *testing.T) { | |||||||
| func TestStorage(t *testing.T) { | func TestStorage(t *testing.T) { | ||||||
| 	client := loadClient(t) | 	client := loadClient(t) | ||||||
| 	conformance.RunTestSuite(t, func() storage.Storage { | 	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 | 		return client | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| package kubernetes | package kubernetes | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/base32" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	jose "gopkg.in/square/go-jose.v2" | 	jose "gopkg.in/square/go-jose.v2" | ||||||
| @@ -182,6 +184,60 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest { | |||||||
| 	return req | 	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 | // AuthCode is a mirrored struct from storage with JSON struct tags and | ||||||
| // Kubernetes type metadata. | // Kubernetes type metadata. | ||||||
| type AuthCode struct { | type AuthCode struct { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user