Merge pull request #590 from ericchiang/dev-add-password-resource
dev branch: add a password resource for local email/password login
This commit is contained in:
commit
182f14fb30
18 changed files with 643 additions and 45 deletions
3
TODO.md
3
TODO.md
|
@ -33,7 +33,7 @@ Documentation
|
||||||
|
|
||||||
Storage
|
Storage
|
||||||
|
|
||||||
- [ ] Add SQL storage implementation
|
- [x] Add SQL storage implementation
|
||||||
- [ ] Utilize fixes for third party resources in Kubernetes 1.4
|
- [ ] Utilize fixes for third party resources in Kubernetes 1.4
|
||||||
|
|
||||||
UX
|
UX
|
||||||
|
@ -48,3 +48,4 @@ Backend
|
||||||
|
|
||||||
- [ ] Improve logging, possibly switch to logrus
|
- [ ] Improve logging, possibly switch to logrus
|
||||||
- [ ] Standardize OAuth2 error handling
|
- [ ] Standardize OAuth2 error handling
|
||||||
|
- [ ] Switch to github.com/ghodss/yaml for []byte to base64 string logic
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/coreos/dex/connector"
|
"github.com/coreos/dex/connector"
|
||||||
|
@ -26,7 +27,46 @@ type Config struct {
|
||||||
|
|
||||||
Templates server.TemplateConfig `yaml:"templates"`
|
Templates server.TemplateConfig `yaml:"templates"`
|
||||||
|
|
||||||
|
// StaticClients cause the server to use this list of clients rather than
|
||||||
|
// querying the storage. Write operations, like creating a client, will fail.
|
||||||
StaticClients []storage.Client `yaml:"staticClients"`
|
StaticClients []storage.Client `yaml:"staticClients"`
|
||||||
|
|
||||||
|
// If enabled, the server will maintain a list of passwords which can be used
|
||||||
|
// to identify a user.
|
||||||
|
EnablePasswordDB bool `yaml:"enablePasswordDB"`
|
||||||
|
|
||||||
|
// StaticPasswords cause the server use this list of passwords rather than
|
||||||
|
// querying the storage. Cannot be specified without enabling a passwords
|
||||||
|
// database.
|
||||||
|
//
|
||||||
|
// The "password" type is identical to the storage.Password type, but does
|
||||||
|
// unmarshaling into []byte correctly.
|
||||||
|
StaticPasswords []password `yaml:"staticPasswords"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type password struct {
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
UserID string `yaml:"userID"`
|
||||||
|
|
||||||
|
// Because our YAML parser doesn't base64, we have to do it ourselves.
|
||||||
|
//
|
||||||
|
// TODO(ericchiang): switch to github.com/ghodss/yaml
|
||||||
|
Hash string `yaml:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode the hash appropriately and convert to the storage passwords.
|
||||||
|
func (p password) toPassword() (storage.Password, error) {
|
||||||
|
hash, err := base64.StdEncoding.DecodeString(p.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return storage.Password{}, fmt.Errorf("decoding hash: %v", err)
|
||||||
|
}
|
||||||
|
return storage.Password{
|
||||||
|
Email: p.Email,
|
||||||
|
Username: p.Username,
|
||||||
|
UserID: p.UserID,
|
||||||
|
Hash: hash,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth2 describes enabled OAuth2 extensions.
|
// OAuth2 describes enabled OAuth2 extensions.
|
||||||
|
|
|
@ -55,7 +55,8 @@ func serve(cmd *cobra.Command, args []string) error {
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
{c.Issuer == "", "no issuer specified in config file"},
|
{c.Issuer == "", "no issuer specified in config file"},
|
||||||
{len(c.Connectors) == 0, "no connectors supplied in config file"},
|
{len(c.Connectors) == 0 && !c.EnablePasswordDB, "no connectors supplied in config file"},
|
||||||
|
{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
|
||||||
{c.Storage.Config == nil, "no storage suppied in config file"},
|
{c.Storage.Config == nil, "no storage suppied in config file"},
|
||||||
{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"},
|
{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"},
|
||||||
{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
|
{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
|
||||||
|
@ -103,6 +104,15 @@ func serve(cmd *cobra.Command, args []string) error {
|
||||||
if len(c.StaticClients) > 0 {
|
if len(c.StaticClients) > 0 {
|
||||||
s = storage.WithStaticClients(s, c.StaticClients)
|
s = storage.WithStaticClients(s, c.StaticClients)
|
||||||
}
|
}
|
||||||
|
if len(c.StaticPasswords) > 0 {
|
||||||
|
p := make([]storage.Password, len(c.StaticPasswords))
|
||||||
|
for i, pw := range c.StaticPasswords {
|
||||||
|
if p[i], err = pw.toPassword(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s = storage.WithStaticPasswords(s, p)
|
||||||
|
}
|
||||||
|
|
||||||
serverConfig := server.Config{
|
serverConfig := server.Config{
|
||||||
SupportedResponseTypes: c.OAuth2.ResponseTypes,
|
SupportedResponseTypes: c.OAuth2.ResponseTypes,
|
||||||
|
@ -110,6 +120,7 @@ func serve(cmd *cobra.Command, args []string) error {
|
||||||
Connectors: connectors,
|
Connectors: connectors,
|
||||||
Storage: s,
|
Storage: s,
|
||||||
TemplateConfig: c.Templates,
|
TemplateConfig: c.Templates,
|
||||||
|
EnablePasswordDB: c.EnablePasswordDB,
|
||||||
}
|
}
|
||||||
|
|
||||||
serv, err := server.NewServer(serverConfig)
|
serv, err := server.NewServer(serverConfig)
|
||||||
|
|
|
@ -11,16 +11,23 @@ connectors:
|
||||||
- type: mockCallback
|
- type: mockCallback
|
||||||
id: mock-callback
|
id: mock-callback
|
||||||
name: Mock
|
name: Mock
|
||||||
- type: mockPassword
|
|
||||||
id: mock-password
|
|
||||||
name: Password
|
|
||||||
config:
|
|
||||||
username: "admin"
|
|
||||||
password: "PASSWORD"
|
|
||||||
|
|
||||||
|
# Instead of reading from an external storage, use this list of clients.
|
||||||
staticClients:
|
staticClients:
|
||||||
- id: example-app
|
- id: example-app
|
||||||
redirectURIs:
|
redirectURIs:
|
||||||
- 'http://127.0.0.1:5555/callback'
|
- 'http://127.0.0.1:5555/callback'
|
||||||
name: 'Example App'
|
name: 'Example App'
|
||||||
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
|
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
|
||||||
|
|
||||||
|
# Let dex keep a list of passwords which can be used to login the user.
|
||||||
|
enablePasswordDB: true
|
||||||
|
|
||||||
|
# A static list of passwords to login the end user. By identifying here, dex
|
||||||
|
# won't look in its undlying storage for passwords.
|
||||||
|
staticPasswords:
|
||||||
|
- email: "admin@example.com"
|
||||||
|
# bcrypt hash of the string "password"
|
||||||
|
hash: "JDJhJDE0JDh4TnlVZ3pzSmVuQm4ySlRPT2QvbmVGcUlnQzF4TEFVRFA3VlpTVzhDNWlkLnFPcmNlYUJX"
|
||||||
|
username: "admin"
|
||||||
|
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -3,12 +3,15 @@ package server
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/coreos/dex/connector"
|
"github.com/coreos/dex/connector"
|
||||||
|
@ -44,6 +47,8 @@ type Config struct {
|
||||||
// If specified, the server will use this function for determining time.
|
// If specified, the server will use this function for determining time.
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
|
|
||||||
|
EnablePasswordDB bool
|
||||||
|
|
||||||
TemplateConfig TemplateConfig
|
TemplateConfig TemplateConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +96,14 @@ func newServer(c Config, rotationStrategy rotationStrategy) (*Server, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("server: can't parse issuer URL")
|
return nil, fmt.Errorf("server: can't parse issuer URL")
|
||||||
}
|
}
|
||||||
|
if c.EnablePasswordDB {
|
||||||
|
c.Connectors = append(c.Connectors, Connector{
|
||||||
|
ID: "local",
|
||||||
|
DisplayName: "Email",
|
||||||
|
Connector: newPasswordDB(c.Storage),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if len(c.Connectors) == 0 {
|
if len(c.Connectors) == 0 {
|
||||||
return nil, errors.New("server: no connectors specified")
|
return nil, errors.New("server: no connectors specified")
|
||||||
}
|
}
|
||||||
|
@ -182,6 +195,38 @@ func (s *Server) absURL(pathItems ...string) string {
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newPasswordDB(s storage.Storage) interface {
|
||||||
|
connector.Connector
|
||||||
|
connector.PasswordConnector
|
||||||
|
} {
|
||||||
|
return passwordDB{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
type passwordDB struct {
|
||||||
|
s storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db passwordDB) Close() error { return nil }
|
||||||
|
|
||||||
|
func (db passwordDB) Login(email, password string) (connector.Identity, bool, error) {
|
||||||
|
p, err := db.s.GetPassword(email)
|
||||||
|
if err != nil {
|
||||||
|
if err != storage.ErrNotFound {
|
||||||
|
log.Printf("get password: %v", err)
|
||||||
|
}
|
||||||
|
return connector.Identity{}, false, err
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword(p.Hash, []byte(password)); err != nil {
|
||||||
|
return connector.Identity{}, false, nil
|
||||||
|
}
|
||||||
|
return connector.Identity{
|
||||||
|
UserID: p.UserID,
|
||||||
|
Username: p.Username,
|
||||||
|
Email: p.Email,
|
||||||
|
EmailVerified: true,
|
||||||
|
}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// newKeyCacher returns a storage which caches keys so long as the next
|
// newKeyCacher returns a storage which caches keys so long as the next
|
||||||
func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage {
|
func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage {
|
||||||
if now == nil {
|
if now == nil {
|
||||||
|
|
|
@ -16,9 +16,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ericchiang/oidc"
|
"github.com/ericchiang/oidc"
|
||||||
|
"github.com/kylelemons/godebug/pretty"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/coreos/dex/connector"
|
||||||
"github.com/coreos/dex/connector/mock"
|
"github.com/coreos/dex/connector/mock"
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
"github.com/coreos/dex/storage/memory"
|
"github.com/coreos/dex/storage/memory"
|
||||||
|
@ -381,6 +384,91 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPasswordDB(t *testing.T) {
|
||||||
|
s := memory.New()
|
||||||
|
conn := newPasswordDB(s)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
pw := "hi"
|
||||||
|
|
||||||
|
h, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.MinCost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.CreatePassword(storage.Password{
|
||||||
|
Email: "jane@example.com",
|
||||||
|
Username: "jane",
|
||||||
|
UserID: "foobar",
|
||||||
|
Hash: h,
|
||||||
|
})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
wantIdentity connector.Identity
|
||||||
|
wantInvalid bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid password",
|
||||||
|
username: "jane@example.com",
|
||||||
|
password: pw,
|
||||||
|
wantIdentity: connector.Identity{
|
||||||
|
Email: "jane@example.com",
|
||||||
|
Username: "jane",
|
||||||
|
UserID: "foobar",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown user",
|
||||||
|
username: "john@example.com",
|
||||||
|
password: pw,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid password",
|
||||||
|
username: "jane@example.com",
|
||||||
|
password: "not the correct password",
|
||||||
|
wantInvalid: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
ident, valid, err := conn.Login(tc.username, tc.password)
|
||||||
|
if err != nil {
|
||||||
|
if !tc.wantErr {
|
||||||
|
t.Errorf("%s: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
t.Errorf("%s: expected error", tc.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
if !tc.wantInvalid {
|
||||||
|
t.Errorf("%s: expected valid password", tc.name)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.wantInvalid {
|
||||||
|
t.Errorf("%s: expected invalid password", tc.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := pretty.Compare(tc.wantIdentity, ident); diff != "" {
|
||||||
|
t.Errorf("%s: %s", tc.name, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
type storageWithKeysTrigger struct {
|
type storageWithKeysTrigger struct {
|
||||||
storage.Storage
|
storage.Storage
|
||||||
f func()
|
f func()
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
|
|
||||||
"github.com/kylelemons/godebug/pretty"
|
"github.com/kylelemons/godebug/pretty"
|
||||||
|
@ -30,6 +32,7 @@ func RunTestSuite(t *testing.T, sf StorageFactory) {
|
||||||
{"AuthRequestCRUD", testAuthRequestCRUD},
|
{"AuthRequestCRUD", testAuthRequestCRUD},
|
||||||
{"ClientCRUD", testClientCRUD},
|
{"ClientCRUD", testClientCRUD},
|
||||||
{"RefreshTokenCRUD", testRefreshTokenCRUD},
|
{"RefreshTokenCRUD", testRefreshTokenCRUD},
|
||||||
|
{"PasswordCRUD", testPasswordCRUD},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
@ -222,5 +225,54 @@ func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
|
||||||
if _, err := s.GetRefresh(id); err != storage.ErrNotFound {
|
if _, err := s.GetRefresh(id); err != storage.ErrNotFound {
|
||||||
t.Errorf("after deleting refresh expected storage.ErrNotFound, got %v", err)
|
t.Errorf("after deleting refresh expected storage.ErrNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPasswordCRUD(t *testing.T, s storage.Storage) {
|
||||||
|
// Use bcrypt.MinCost to keep the tests short.
|
||||||
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password := storage.Password{
|
||||||
|
Email: "jane@example.com",
|
||||||
|
Hash: passwordHash,
|
||||||
|
Username: "jane",
|
||||||
|
UserID: "foobar",
|
||||||
|
}
|
||||||
|
if err := s.CreatePassword(password); err != nil {
|
||||||
|
t.Fatalf("create password token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAndCompare := func(id string, want storage.Password) {
|
||||||
|
gr, err := s.GetPassword(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("get password %q: %v", id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if diff := pretty.Compare(want, gr); diff != "" {
|
||||||
|
t.Errorf("password retrieved from storage did not match: %s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAndCompare("jane@example.com", password)
|
||||||
|
getAndCompare("JANE@example.com", password) // Emails should be case insensitive
|
||||||
|
|
||||||
|
if err := s.UpdatePassword(password.Email, func(old storage.Password) (storage.Password, error) {
|
||||||
|
old.Username = "jane doe"
|
||||||
|
return old, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("failed to update auth request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password.Username = "jane doe"
|
||||||
|
getAndCompare("jane@example.com", password)
|
||||||
|
|
||||||
|
if err := s.DeletePassword(password.Email); err != nil {
|
||||||
|
t.Fatalf("failed to delete password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.GetPassword(password.Email); err != storage.ErrNotFound {
|
||||||
|
t.Errorf("after deleting password expected storage.ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
|
@ -15,6 +15,7 @@ func New() storage.Storage {
|
||||||
authCodes: make(map[string]storage.AuthCode),
|
authCodes: make(map[string]storage.AuthCode),
|
||||||
refreshTokens: make(map[string]storage.RefreshToken),
|
refreshTokens: make(map[string]storage.RefreshToken),
|
||||||
authReqs: make(map[string]storage.AuthRequest),
|
authReqs: make(map[string]storage.AuthRequest),
|
||||||
|
passwords: make(map[string]storage.Password),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ type memStorage struct {
|
||||||
authCodes map[string]storage.AuthCode
|
authCodes map[string]storage.AuthCode
|
||||||
refreshTokens map[string]storage.RefreshToken
|
refreshTokens map[string]storage.RefreshToken
|
||||||
authReqs map[string]storage.AuthRequest
|
authReqs map[string]storage.AuthRequest
|
||||||
|
passwords map[string]storage.Password
|
||||||
|
|
||||||
keys storage.Keys
|
keys storage.Keys
|
||||||
}
|
}
|
||||||
|
@ -47,28 +49,73 @@ func (s *memStorage) tx(f func()) {
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
|
|
||||||
var errAlreadyExists = errors.New("already exists")
|
|
||||||
|
|
||||||
func (s *memStorage) Close() error { return nil }
|
func (s *memStorage) Close() error { return nil }
|
||||||
|
|
||||||
func (s *memStorage) CreateClient(c storage.Client) error {
|
func (s *memStorage) CreateClient(c storage.Client) (err error) {
|
||||||
s.tx(func() { s.clients[c.ID] = c })
|
s.tx(func() {
|
||||||
return nil
|
if _, ok := s.clients[c.ID]; ok {
|
||||||
|
err = storage.ErrAlreadyExists
|
||||||
|
} else {
|
||||||
|
s.clients[c.ID] = c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memStorage) CreateAuthCode(c storage.AuthCode) error {
|
func (s *memStorage) CreateAuthCode(c storage.AuthCode) (err error) {
|
||||||
s.tx(func() { s.authCodes[c.ID] = c })
|
s.tx(func() {
|
||||||
return nil
|
if _, ok := s.authCodes[c.ID]; ok {
|
||||||
|
err = storage.ErrAlreadyExists
|
||||||
|
} else {
|
||||||
|
s.authCodes[c.ID] = c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memStorage) CreateRefresh(r storage.RefreshToken) error {
|
func (s *memStorage) CreateRefresh(r storage.RefreshToken) (err error) {
|
||||||
s.tx(func() { s.refreshTokens[r.RefreshToken] = r })
|
s.tx(func() {
|
||||||
return nil
|
if _, ok := s.refreshTokens[r.RefreshToken]; ok {
|
||||||
|
err = storage.ErrAlreadyExists
|
||||||
|
} else {
|
||||||
|
s.refreshTokens[r.RefreshToken] = r
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memStorage) CreateAuthRequest(a storage.AuthRequest) error {
|
func (s *memStorage) CreateAuthRequest(a storage.AuthRequest) (err error) {
|
||||||
s.tx(func() { s.authReqs[a.ID] = a })
|
s.tx(func() {
|
||||||
return nil
|
if _, ok := s.authReqs[a.ID]; ok {
|
||||||
|
err = storage.ErrAlreadyExists
|
||||||
|
} else {
|
||||||
|
s.authReqs[a.ID] = a
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *memStorage) CreatePassword(p storage.Password) (err error) {
|
||||||
|
p.Email = strings.ToLower(p.Email)
|
||||||
|
s.tx(func() {
|
||||||
|
if _, ok := s.passwords[p.Email]; ok {
|
||||||
|
err = storage.ErrAlreadyExists
|
||||||
|
} else {
|
||||||
|
s.passwords[p.Email] = p
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *memStorage) GetPassword(email string) (p storage.Password, err error) {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
s.tx(func() {
|
||||||
|
var ok bool
|
||||||
|
if p, ok = s.passwords[email]; !ok {
|
||||||
|
err = storage.ErrNotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memStorage) GetClient(id string) (client storage.Client, err error) {
|
func (s *memStorage) GetClient(id string) (client storage.Client, err error) {
|
||||||
|
@ -126,6 +173,18 @@ func (s *memStorage) ListRefreshTokens() (tokens []storage.RefreshToken, err err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *memStorage) DeletePassword(email string) (err error) {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
s.tx(func() {
|
||||||
|
if _, ok := s.passwords[email]; !ok {
|
||||||
|
err = storage.ErrNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(s.passwords, email)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s *memStorage) DeleteClient(id string) (err error) {
|
func (s *memStorage) DeleteClient(id string) (err error) {
|
||||||
s.tx(func() {
|
s.tx(func() {
|
||||||
if _, ok := s.clients[id]; !ok {
|
if _, ok := s.clients[id]; !ok {
|
||||||
|
@ -235,9 +294,24 @@ func (s *memStorage) UpdateAuthRequest(id string, updater func(old storage.AuthR
|
||||||
err = storage.ErrNotFound
|
err = storage.ErrNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req, err := updater(req); err == nil {
|
if req, err = updater(req); err == nil {
|
||||||
s.authReqs[id] = req
|
s.authReqs[id] = req
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *memStorage) UpdatePassword(email string, updater func(p storage.Password) (storage.Password, error)) (err error) {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
s.tx(func() {
|
||||||
|
req, ok := s.passwords[email]
|
||||||
|
if !ok {
|
||||||
|
err = storage.ErrNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req, err = updater(req); err == nil {
|
||||||
|
s.passwords[email] = req
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
)
|
)
|
||||||
|
@ -137,7 +138,7 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest)
|
||||||
a.Claims.UserID, a.Claims.Username, a.Claims.Email, a.Claims.EmailVerified,
|
a.Claims.UserID, a.Claims.Username, a.Claims.Email, a.Claims.EmailVerified,
|
||||||
encoder(a.Claims.Groups),
|
encoder(a.Claims.Groups),
|
||||||
a.ConnectorID, a.ConnectorData,
|
a.ConnectorID, a.ConnectorData,
|
||||||
a.Expiry, a.ID,
|
a.Expiry, r.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update auth request: %v", err)
|
return fmt.Errorf("update auth request: %v", err)
|
||||||
|
@ -462,14 +463,83 @@ func scanClient(s scanner) (cli storage.Client, err error) {
|
||||||
return cli, nil
|
return cli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) DeleteAuthRequest(id string) error { return c.delete("auth_request", id) }
|
func (c *conn) CreatePassword(p storage.Password) error {
|
||||||
func (c *conn) DeleteAuthCode(id string) error { return c.delete("auth_code", id) }
|
p.Email = strings.ToLower(p.Email)
|
||||||
func (c *conn) DeleteClient(id string) error { return c.delete("client", id) }
|
_, err := c.Exec(`
|
||||||
func (c *conn) DeleteRefresh(id string) error { return c.delete("refresh_token", id) }
|
insert into password (
|
||||||
|
email, hash, username, user_id
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
$1, $2, $3, $4
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
p.Email, p.Hash, p.Username, p.UserID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert password: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) UpdatePassword(email string, updater func(p storage.Password) (storage.Password, error)) error {
|
||||||
|
return c.ExecTx(func(tx *trans) error {
|
||||||
|
p, err := getPassword(tx, email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
np, err := updater(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
update password
|
||||||
|
set
|
||||||
|
hash = $1, username = $2, user_id = $3
|
||||||
|
where email = $4;
|
||||||
|
`,
|
||||||
|
np.Hash, np.Username, np.UserID, p.Email,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update password: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) GetPassword(email string) (storage.Password, error) {
|
||||||
|
return getPassword(c, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPassword(q querier, email string) (p storage.Password, err error) {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
err = q.QueryRow(`
|
||||||
|
select
|
||||||
|
email, hash, username, user_id
|
||||||
|
from password where email = $1;
|
||||||
|
`, email).Scan(
|
||||||
|
&p.Email, &p.Hash, &p.Username, &p.UserID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return p, storage.ErrNotFound
|
||||||
|
}
|
||||||
|
return p, fmt.Errorf("select password: %v", err)
|
||||||
|
}
|
||||||
|
return p, 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) }
|
||||||
|
func (c *conn) DeleteRefresh(id string) error { return c.delete("refresh_token", "id", id) }
|
||||||
|
func (c *conn) DeletePassword(email string) error {
|
||||||
|
return c.delete("password", "email", strings.ToLower(email))
|
||||||
|
}
|
||||||
|
|
||||||
// Do NOT call directly. Does not escape table.
|
// Do NOT call directly. Does not escape table.
|
||||||
func (c *conn) delete(table, id string) error {
|
func (c *conn) delete(table, field, id string) error {
|
||||||
result, err := c.Exec(`delete from `+table+` where id = $1`, id)
|
result, err := c.Exec(`delete from `+table+` where `+field+` = $1`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete %s: %v", table, id)
|
return fmt.Errorf("delete %s: %v", table, id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,6 +137,13 @@ var migrations = []migration{
|
||||||
connector_id text not null,
|
connector_id text not null,
|
||||||
connector_data bytea
|
connector_data bytea
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table password (
|
||||||
|
email text not null primary key,
|
||||||
|
hash bytea not null,
|
||||||
|
username text not null,
|
||||||
|
user_id text not null
|
||||||
|
);
|
||||||
|
|
||||||
-- keys is a weird table because we only ever expect there to be a single row
|
-- keys is a weird table because we only ever expect there to be a single row
|
||||||
create table keys (
|
create table keys (
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Tests for this code are in the "memory" package, since this package doesn't
|
// Tests for this code are in the "memory" package, since this package doesn't
|
||||||
// define a concrete storage implementation.
|
// define a concrete storage implementation.
|
||||||
|
@ -53,3 +56,39 @@ func (s staticClientsStorage) DeleteClient(id string) error {
|
||||||
func (s staticClientsStorage) UpdateClient(id string, updater func(old Client) (Client, error)) error {
|
func (s staticClientsStorage) UpdateClient(id string, updater func(old Client) (Client, error)) error {
|
||||||
return errors.New("static clients: read-only cannot update client")
|
return errors.New("static clients: read-only cannot update client")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type staticPasswordsStorage struct {
|
||||||
|
Storage
|
||||||
|
|
||||||
|
passwordsByEmail map[string]Password
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStaticPasswords returns a storage with a read-only set of passwords. Write actions,
|
||||||
|
// such as creating other passwords, will fail.
|
||||||
|
func WithStaticPasswords(s Storage, staticPasswords []Password) Storage {
|
||||||
|
passwordsByEmail := make(map[string]Password, len(staticPasswords))
|
||||||
|
for _, p := range staticPasswords {
|
||||||
|
p.Email = strings.ToLower(p.Email)
|
||||||
|
passwordsByEmail[p.Email] = p
|
||||||
|
}
|
||||||
|
return staticPasswordsStorage{s, passwordsByEmail}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticPasswordsStorage) GetPassword(email string) (Password, error) {
|
||||||
|
if password, ok := s.passwordsByEmail[strings.ToLower(email)]; ok {
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
return Password{}, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticPasswordsStorage) CreatePassword(p Password) error {
|
||||||
|
return errors.New("static passwords: read-only cannot create password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticPasswordsStorage) DeletePassword(id string) error {
|
||||||
|
return errors.New("static passwords: read-only cannot create password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticPasswordsStorage) UpdatePassword(id string, updater func(old Password) (Password, error)) error {
|
||||||
|
return errors.New("static passwords: read-only cannot update password")
|
||||||
|
}
|
|
@ -16,12 +16,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// stubbed out for testing
|
// ErrNotFound is the error returned by storages if a resource cannot be found.
|
||||||
now = time.Now
|
ErrNotFound = errors.New("not found")
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNotFound is the error returned by storages if a resource cannot be found.
|
// ErrAlreadyExists is the error returned by storages if a resource ID is taken during a create.
|
||||||
var ErrNotFound = errors.New("not found")
|
ErrAlreadyExists = errors.New("ID already exists")
|
||||||
|
)
|
||||||
|
|
||||||
// Kubernetes only allows lower case letters for names.
|
// Kubernetes only allows lower case letters for names.
|
||||||
//
|
//
|
||||||
|
@ -51,6 +51,7 @@ type Storage interface {
|
||||||
CreateClient(c Client) error
|
CreateClient(c Client) error
|
||||||
CreateAuthCode(c AuthCode) error
|
CreateAuthCode(c AuthCode) error
|
||||||
CreateRefresh(r RefreshToken) error
|
CreateRefresh(r RefreshToken) error
|
||||||
|
CreatePassword(p Password) error
|
||||||
|
|
||||||
// TODO(ericchiang): return (T, bool, error) so we can indicate not found
|
// TODO(ericchiang): return (T, bool, error) so we can indicate not found
|
||||||
// requests that way instead of using ErrNotFound.
|
// requests that way instead of using ErrNotFound.
|
||||||
|
@ -59,6 +60,7 @@ type Storage interface {
|
||||||
GetClient(id string) (Client, error)
|
GetClient(id string) (Client, error)
|
||||||
GetKeys() (Keys, error)
|
GetKeys() (Keys, error)
|
||||||
GetRefresh(id string) (RefreshToken, error)
|
GetRefresh(id string) (RefreshToken, error)
|
||||||
|
GetPassword(email string) (Password, error)
|
||||||
|
|
||||||
ListClients() ([]Client, error)
|
ListClients() ([]Client, error)
|
||||||
ListRefreshTokens() ([]RefreshToken, error)
|
ListRefreshTokens() ([]RefreshToken, error)
|
||||||
|
@ -68,6 +70,7 @@ type Storage interface {
|
||||||
DeleteAuthCode(code string) error
|
DeleteAuthCode(code string) error
|
||||||
DeleteClient(id string) error
|
DeleteClient(id string) error
|
||||||
DeleteRefresh(id string) error
|
DeleteRefresh(id string) error
|
||||||
|
DeletePassword(email string) error
|
||||||
|
|
||||||
// Update functions are assumed to be a performed within a single object transaction.
|
// Update functions are assumed to be a performed within a single object transaction.
|
||||||
//
|
//
|
||||||
|
@ -75,6 +78,7 @@ type Storage interface {
|
||||||
UpdateClient(id string, updater func(old Client) (Client, error)) error
|
UpdateClient(id string, updater func(old Client) (Client, error)) error
|
||||||
UpdateKeys(updater func(old Keys) (Keys, error)) error
|
UpdateKeys(updater func(old Keys) (Keys, error)) error
|
||||||
UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
|
UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
|
||||||
|
UpdatePassword(email string, updater func(p Password) (Password, error)) error
|
||||||
|
|
||||||
// TODO(ericchiang): Add a GarbageCollect(now time.Time) method so conformance tests
|
// TODO(ericchiang): Add a GarbageCollect(now time.Time) method so conformance tests
|
||||||
// can test implementations.
|
// can test implementations.
|
||||||
|
@ -217,6 +221,28 @@ type RefreshToken struct {
|
||||||
Nonce string
|
Nonce string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password is an email to password mapping managed by the storage.
|
||||||
|
type Password struct {
|
||||||
|
// Email and identifying name of the password. Emails are assumed to be valid and
|
||||||
|
// determining that an end-user controls the address is left to an outside application.
|
||||||
|
//
|
||||||
|
// Emails are case insensitive and should be standardized by the storage.
|
||||||
|
//
|
||||||
|
// Storages that don't support an extended character set for IDs, such as '.' and '@'
|
||||||
|
// (cough cough, kubernetes), must map this value appropriately.
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
|
||||||
|
// Bcrypt encoded hash of the password. This package recommends a cost value of at
|
||||||
|
// least 14.
|
||||||
|
Hash []byte `yaml:"hash"`
|
||||||
|
|
||||||
|
// Optional username to display. NOT used during login.
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
|
||||||
|
// Randomly generated user ID. This is NOT the primary ID of the Password object.
|
||||||
|
UserID string `yaml:"userID"`
|
||||||
|
}
|
||||||
|
|
||||||
// VerificationKey is a rotated signing key which can still be used to verify
|
// VerificationKey is a rotated signing key which can still be used to verify
|
||||||
// signatures.
|
// signatures.
|
||||||
type VerificationKey struct {
|
type VerificationKey struct {
|
||||||
|
|
Reference in a new issue