dcca427592
this failed on my machine due to the unexported 'loc' field of the time structure - it was nil in one and set to a ton of tiemzone data in the other. instead let's just compare the unix timestamp value and zero it out for the struct comparison.
878 lines
24 KiB
Go
878 lines
24 KiB
Go
// +build go1.7
|
|
|
|
// Package conformance provides conformance tests for storage implementations.
|
|
package conformance
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
jose "gopkg.in/square/go-jose.v2"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/dexidp/dex/storage"
|
|
|
|
"github.com/kylelemons/godebug/pretty"
|
|
)
|
|
|
|
// ensure that values being tested on never expire.
|
|
var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100)
|
|
|
|
type subTest struct {
|
|
name string
|
|
run func(t *testing.T, s storage.Storage)
|
|
}
|
|
|
|
func runTests(t *testing.T, newStorage func() storage.Storage, tests []subTest) {
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
s := newStorage()
|
|
test.run(t, s)
|
|
s.Close()
|
|
})
|
|
}
|
|
}
|
|
|
|
// RunTests runs a set of conformance tests against a storage. newStorage should
|
|
// return an initialized but empty storage. The storage will be closed at the
|
|
// end of each test run.
|
|
func RunTests(t *testing.T, newStorage func() storage.Storage) {
|
|
runTests(t, newStorage, []subTest{
|
|
{"AuthCodeCRUD", testAuthCodeCRUD},
|
|
{"AuthRequestCRUD", testAuthRequestCRUD},
|
|
{"ClientCRUD", testClientCRUD},
|
|
{"RefreshTokenCRUD", testRefreshTokenCRUD},
|
|
{"PasswordCRUD", testPasswordCRUD},
|
|
{"KeysCRUD", testKeysCRUD},
|
|
{"OfflineSessionCRUD", testOfflineSessionCRUD},
|
|
{"ConnectorCRUD", testConnectorCRUD},
|
|
{"GarbageCollection", testGC},
|
|
{"TimezoneSupport", testTimezones},
|
|
})
|
|
}
|
|
|
|
func mustLoadJWK(b string) *jose.JSONWebKey {
|
|
var jwt jose.JSONWebKey
|
|
if err := jwt.UnmarshalJSON([]byte(b)); err != nil {
|
|
panic(err)
|
|
}
|
|
return &jwt
|
|
}
|
|
|
|
func mustBeErrNotFound(t *testing.T, kind string, err error) {
|
|
switch {
|
|
case err == nil:
|
|
t.Errorf("deleting non-existent %s should return an error", kind)
|
|
case err != storage.ErrNotFound:
|
|
t.Errorf("deleting %s expected storage.ErrNotFound, got %v", kind, err)
|
|
}
|
|
}
|
|
|
|
func mustBeErrAlreadyExists(t *testing.T, kind string, err error) {
|
|
switch {
|
|
case err == nil:
|
|
t.Errorf("attempting to create an existing %s should return an error", kind)
|
|
case err != storage.ErrAlreadyExists:
|
|
t.Errorf("creating an existing %s expected storage.ErrAlreadyExists, got %v", kind, err)
|
|
}
|
|
}
|
|
|
|
func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
|
|
a1 := storage.AuthRequest{
|
|
ID: storage.NewID(),
|
|
ClientID: "client1",
|
|
ResponseTypes: []string{"code"},
|
|
Scopes: []string{"openid", "email"},
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "foo",
|
|
State: "bar",
|
|
ForceApprovalPrompt: true,
|
|
LoggedIn: true,
|
|
Expiry: neverExpire,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "1",
|
|
Username: "jane",
|
|
Email: "jane.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
}
|
|
|
|
identity := storage.Claims{Email: "foobar"}
|
|
|
|
if err := s.CreateAuthRequest(a1); err != nil {
|
|
t.Fatalf("failed creating auth request: %v", err)
|
|
}
|
|
|
|
// Attempt to create same AuthRequest twice.
|
|
err := s.CreateAuthRequest(a1)
|
|
mustBeErrAlreadyExists(t, "auth request", err)
|
|
|
|
a2 := storage.AuthRequest{
|
|
ID: storage.NewID(),
|
|
ClientID: "client2",
|
|
ResponseTypes: []string{"code"},
|
|
Scopes: []string{"openid", "email"},
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "bar",
|
|
State: "foo",
|
|
ForceApprovalPrompt: true,
|
|
LoggedIn: true,
|
|
Expiry: neverExpire,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "2",
|
|
Username: "john",
|
|
Email: "john.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a"},
|
|
},
|
|
}
|
|
|
|
if err := s.CreateAuthRequest(a2); err != nil {
|
|
t.Fatalf("failed creating auth request: %v", err)
|
|
}
|
|
|
|
if err := s.UpdateAuthRequest(a1.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) {
|
|
old.Claims = identity
|
|
old.ConnectorID = "connID"
|
|
return old, nil
|
|
}); err != nil {
|
|
t.Fatalf("failed to update auth request: %v", err)
|
|
}
|
|
|
|
got, err := s.GetAuthRequest(a1.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get auth req: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got.Claims, identity) {
|
|
t.Fatalf("update failed, wanted identity=%#v got %#v", identity, got.Claims)
|
|
}
|
|
|
|
if err := s.DeleteAuthRequest(a1.ID); err != nil {
|
|
t.Fatalf("failed to delete auth request: %v", err)
|
|
}
|
|
|
|
if err := s.DeleteAuthRequest(a2.ID); err != nil {
|
|
t.Fatalf("failed to delete auth request: %v", err)
|
|
}
|
|
|
|
}
|
|
|
|
func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
|
|
a1 := storage.AuthCode{
|
|
ID: storage.NewID(),
|
|
ClientID: "client1",
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "foobar",
|
|
Scopes: []string{"openid", "email"},
|
|
Expiry: neverExpire,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "1",
|
|
Username: "jane",
|
|
Email: "jane.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
}
|
|
|
|
if err := s.CreateAuthCode(a1); err != nil {
|
|
t.Fatalf("failed creating auth code: %v", err)
|
|
}
|
|
|
|
a2 := storage.AuthCode{
|
|
ID: storage.NewID(),
|
|
ClientID: "client2",
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "foobar",
|
|
Scopes: []string{"openid", "email"},
|
|
Expiry: neverExpire,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "2",
|
|
Username: "john",
|
|
Email: "john.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a"},
|
|
},
|
|
}
|
|
|
|
// Attempt to create same AuthCode twice.
|
|
err := s.CreateAuthCode(a1)
|
|
mustBeErrAlreadyExists(t, "auth code", err)
|
|
|
|
if err := s.CreateAuthCode(a2); err != nil {
|
|
t.Fatalf("failed creating auth code: %v", err)
|
|
}
|
|
|
|
got, err := s.GetAuthCode(a1.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get auth req: %v", err)
|
|
}
|
|
if a1.Expiry.Unix() != got.Expiry.Unix() {
|
|
t.Errorf("auth code expiry did not match want=%s vs got=%s", a1.Expiry, got.Expiry)
|
|
}
|
|
got.Expiry = a1.Expiry // time fields do not compare well
|
|
if diff := pretty.Compare(a1, got); diff != "" {
|
|
t.Errorf("auth code retrieved from storage did not match: %s", diff)
|
|
}
|
|
|
|
if err := s.DeleteAuthCode(a1.ID); err != nil {
|
|
t.Fatalf("delete auth code: %v", err)
|
|
}
|
|
|
|
if err := s.DeleteAuthCode(a2.ID); err != nil {
|
|
t.Fatalf("delete auth code: %v", err)
|
|
}
|
|
|
|
_, err = s.GetAuthCode(a1.ID)
|
|
mustBeErrNotFound(t, "auth code", err)
|
|
}
|
|
|
|
func testClientCRUD(t *testing.T, s storage.Storage) {
|
|
id1 := storage.NewID()
|
|
c1 := storage.Client{
|
|
ID: id1,
|
|
Secret: "foobar",
|
|
RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"},
|
|
Name: "dex client",
|
|
LogoURL: "https://goo.gl/JIyzIC",
|
|
}
|
|
err := s.DeleteClient(id1)
|
|
mustBeErrNotFound(t, "client", err)
|
|
|
|
if err := s.CreateClient(c1); err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
|
|
// Attempt to create same Client twice.
|
|
err = s.CreateClient(c1)
|
|
mustBeErrAlreadyExists(t, "client", err)
|
|
|
|
id2 := storage.NewID()
|
|
c2 := storage.Client{
|
|
ID: id2,
|
|
Secret: "barfoo",
|
|
RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"},
|
|
Name: "dex client",
|
|
LogoURL: "https://goo.gl/JIyzIC",
|
|
}
|
|
|
|
if err := s.CreateClient(c2); err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
|
|
getAndCompare := func(id string, want storage.Client) {
|
|
gc, err := s.GetClient(id1)
|
|
if err != nil {
|
|
t.Errorf("get client: %v", err)
|
|
return
|
|
}
|
|
if diff := pretty.Compare(want, gc); diff != "" {
|
|
t.Errorf("client retrieved from storage did not match: %s", diff)
|
|
}
|
|
}
|
|
|
|
getAndCompare(id1, c1)
|
|
|
|
newSecret := "barfoo"
|
|
err = s.UpdateClient(id1, func(old storage.Client) (storage.Client, error) {
|
|
old.Secret = newSecret
|
|
return old, nil
|
|
})
|
|
if err != nil {
|
|
t.Errorf("update client: %v", err)
|
|
}
|
|
c1.Secret = newSecret
|
|
getAndCompare(id1, c1)
|
|
|
|
if err := s.DeleteClient(id1); err != nil {
|
|
t.Fatalf("delete client: %v", err)
|
|
}
|
|
|
|
if err := s.DeleteClient(id2); err != nil {
|
|
t.Fatalf("delete client: %v", err)
|
|
}
|
|
|
|
_, err = s.GetClient(id1)
|
|
mustBeErrNotFound(t, "client", err)
|
|
}
|
|
|
|
func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
|
|
id := storage.NewID()
|
|
refresh := storage.RefreshToken{
|
|
ID: id,
|
|
Token: "bar",
|
|
Nonce: "foo",
|
|
ClientID: "client_id",
|
|
ConnectorID: "client_secret",
|
|
Scopes: []string{"openid", "email", "profile"},
|
|
CreatedAt: time.Now().UTC().Round(time.Millisecond),
|
|
LastUsed: time.Now().UTC().Round(time.Millisecond),
|
|
Claims: storage.Claims{
|
|
UserID: "1",
|
|
Username: "jane",
|
|
Email: "jane.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
}
|
|
if err := s.CreateRefresh(refresh); err != nil {
|
|
t.Fatalf("create refresh token: %v", err)
|
|
}
|
|
|
|
// Attempt to create same Refresh Token twice.
|
|
err := s.CreateRefresh(refresh)
|
|
mustBeErrAlreadyExists(t, "refresh token", err)
|
|
|
|
getAndCompare := func(id string, want storage.RefreshToken) {
|
|
gr, err := s.GetRefresh(id)
|
|
if err != nil {
|
|
t.Errorf("get refresh: %v", err)
|
|
return
|
|
}
|
|
|
|
if diff := pretty.Compare(gr.CreatedAt.UnixNano(), gr.CreatedAt.UnixNano()); diff != "" {
|
|
t.Errorf("refresh token created timestamp retrieved from storage did not match: %s", diff)
|
|
}
|
|
|
|
if diff := pretty.Compare(gr.LastUsed.UnixNano(), gr.LastUsed.UnixNano()); diff != "" {
|
|
t.Errorf("refresh token last used timestamp retrieved from storage did not match: %s", diff)
|
|
}
|
|
|
|
gr.CreatedAt = time.Time{}
|
|
gr.LastUsed = time.Time{}
|
|
want.CreatedAt = time.Time{}
|
|
want.LastUsed = time.Time{}
|
|
|
|
if diff := pretty.Compare(want, gr); diff != "" {
|
|
t.Errorf("refresh token retrieved from storage did not match: %s", diff)
|
|
}
|
|
}
|
|
|
|
getAndCompare(id, refresh)
|
|
|
|
id2 := storage.NewID()
|
|
refresh2 := storage.RefreshToken{
|
|
ID: id2,
|
|
Token: "bar_2",
|
|
Nonce: "foo_2",
|
|
ClientID: "client_id_2",
|
|
ConnectorID: "client_secret",
|
|
Scopes: []string{"openid", "email", "profile"},
|
|
CreatedAt: time.Now().UTC().Round(time.Millisecond),
|
|
LastUsed: time.Now().UTC().Round(time.Millisecond),
|
|
Claims: storage.Claims{
|
|
UserID: "2",
|
|
Username: "john",
|
|
Email: "john.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
}
|
|
|
|
if err := s.CreateRefresh(refresh2); err != nil {
|
|
t.Fatalf("create second refresh token: %v", err)
|
|
}
|
|
|
|
getAndCompare(id2, refresh2)
|
|
|
|
updatedAt := time.Now().UTC().Round(time.Millisecond)
|
|
|
|
updater := func(r storage.RefreshToken) (storage.RefreshToken, error) {
|
|
r.Token = "spam"
|
|
r.LastUsed = updatedAt
|
|
return r, nil
|
|
}
|
|
if err := s.UpdateRefreshToken(id, updater); err != nil {
|
|
t.Errorf("failed to udpate refresh token: %v", err)
|
|
}
|
|
refresh.Token = "spam"
|
|
refresh.LastUsed = updatedAt
|
|
getAndCompare(id, refresh)
|
|
|
|
// Ensure that updating the first token doesn't impact the second. Issue #847.
|
|
getAndCompare(id2, refresh2)
|
|
|
|
if err := s.DeleteRefresh(id); err != nil {
|
|
t.Fatalf("failed to delete refresh request: %v", err)
|
|
}
|
|
|
|
if err := s.DeleteRefresh(id2); err != nil {
|
|
t.Fatalf("failed to delete refresh request: %v", err)
|
|
}
|
|
|
|
_, err = s.GetRefresh(id)
|
|
mustBeErrNotFound(t, "refresh token", err)
|
|
}
|
|
|
|
type byEmail []storage.Password
|
|
|
|
func (n byEmail) Len() int { return len(n) }
|
|
func (n byEmail) Less(i, j int) bool { return n[i].Email < n[j].Email }
|
|
func (n byEmail) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
|
|
|
func testPasswordCRUD(t *testing.T, s storage.Storage) {
|
|
// Use bcrypt.MinCost to keep the tests short.
|
|
passwordHash1, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
password1 := storage.Password{
|
|
Email: "jane@example.com",
|
|
Hash: passwordHash1,
|
|
Username: "jane",
|
|
UserID: "foobar",
|
|
}
|
|
if err := s.CreatePassword(password1); err != nil {
|
|
t.Fatalf("create password token: %v", err)
|
|
}
|
|
|
|
// Attempt to create same Password twice.
|
|
err = s.CreatePassword(password1)
|
|
mustBeErrAlreadyExists(t, "password", err)
|
|
|
|
passwordHash2, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
password2 := storage.Password{
|
|
Email: "john@example.com",
|
|
Hash: passwordHash2,
|
|
Username: "john",
|
|
UserID: "barfoo",
|
|
}
|
|
if err := s.CreatePassword(password2); 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", password1)
|
|
getAndCompare("JANE@example.com", password1) // Emails should be case insensitive
|
|
|
|
if err := s.UpdatePassword(password1.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)
|
|
}
|
|
|
|
password1.Username = "jane doe"
|
|
getAndCompare("jane@example.com", password1)
|
|
|
|
var passwordList []storage.Password
|
|
passwordList = append(passwordList, password1, password2)
|
|
|
|
listAndCompare := func(want []storage.Password) {
|
|
passwords, err := s.ListPasswords()
|
|
if err != nil {
|
|
t.Errorf("list password: %v", err)
|
|
return
|
|
}
|
|
sort.Sort(byEmail(want))
|
|
sort.Sort(byEmail(passwords))
|
|
if diff := pretty.Compare(want, passwords); diff != "" {
|
|
t.Errorf("password list retrieved from storage did not match: %s", diff)
|
|
}
|
|
}
|
|
|
|
listAndCompare(passwordList)
|
|
|
|
if err := s.DeletePassword(password1.Email); err != nil {
|
|
t.Fatalf("failed to delete password: %v", err)
|
|
}
|
|
|
|
if err := s.DeletePassword(password2.Email); err != nil {
|
|
t.Fatalf("failed to delete password: %v", err)
|
|
}
|
|
|
|
_, err = s.GetPassword(password1.Email)
|
|
mustBeErrNotFound(t, "password", err)
|
|
|
|
}
|
|
|
|
func testOfflineSessionCRUD(t *testing.T, s storage.Storage) {
|
|
userID1 := storage.NewID()
|
|
session1 := storage.OfflineSessions{
|
|
UserID: userID1,
|
|
ConnID: "Conn1",
|
|
Refresh: make(map[string]*storage.RefreshTokenRef),
|
|
}
|
|
|
|
// Creating an OfflineSession with an empty Refresh list to ensure that
|
|
// an empty map is translated as expected by the storage.
|
|
if err := s.CreateOfflineSessions(session1); err != nil {
|
|
t.Fatalf("create offline session with UserID = %s: %v", session1.UserID, err)
|
|
}
|
|
|
|
// Attempt to create same OfflineSession twice.
|
|
err := s.CreateOfflineSessions(session1)
|
|
mustBeErrAlreadyExists(t, "offline session", err)
|
|
|
|
userID2 := storage.NewID()
|
|
session2 := storage.OfflineSessions{
|
|
UserID: userID2,
|
|
ConnID: "Conn2",
|
|
Refresh: make(map[string]*storage.RefreshTokenRef),
|
|
}
|
|
|
|
if err := s.CreateOfflineSessions(session2); err != nil {
|
|
t.Fatalf("create offline session with UserID = %s: %v", session2.UserID, err)
|
|
}
|
|
|
|
getAndCompare := func(userID string, connID string, want storage.OfflineSessions) {
|
|
gr, err := s.GetOfflineSessions(userID, connID)
|
|
if err != nil {
|
|
t.Errorf("get offline session: %v", err)
|
|
return
|
|
}
|
|
if diff := pretty.Compare(want, gr); diff != "" {
|
|
t.Errorf("offline session retrieved from storage did not match: %s", diff)
|
|
}
|
|
}
|
|
|
|
getAndCompare(userID1, "Conn1", session1)
|
|
|
|
id := storage.NewID()
|
|
tokenRef := storage.RefreshTokenRef{
|
|
ID: id,
|
|
ClientID: "client_id",
|
|
CreatedAt: time.Now().UTC().Round(time.Millisecond),
|
|
LastUsed: time.Now().UTC().Round(time.Millisecond),
|
|
}
|
|
session1.Refresh[tokenRef.ClientID] = &tokenRef
|
|
|
|
if err := s.UpdateOfflineSessions(session1.UserID, session1.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
|
|
old.Refresh[tokenRef.ClientID] = &tokenRef
|
|
return old, nil
|
|
}); err != nil {
|
|
t.Fatalf("failed to update offline session: %v", err)
|
|
}
|
|
|
|
getAndCompare(userID1, "Conn1", session1)
|
|
|
|
if err := s.DeleteOfflineSessions(session1.UserID, session1.ConnID); err != nil {
|
|
t.Fatalf("failed to delete offline session: %v", err)
|
|
}
|
|
|
|
if err := s.DeleteOfflineSessions(session2.UserID, session2.ConnID); err != nil {
|
|
t.Fatalf("failed to delete offline session: %v", err)
|
|
}
|
|
|
|
_, err = s.GetOfflineSessions(session1.UserID, session1.ConnID)
|
|
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)
|
|
|
|
connectorList := []storage.Connector{c1, c2}
|
|
listAndCompare := func(want []storage.Connector) {
|
|
connectors, err := s.ListConnectors()
|
|
if err != nil {
|
|
t.Errorf("list connectors: %v", err)
|
|
return
|
|
}
|
|
sort.Slice(connectors, func(i, j int) bool {
|
|
return connectors[i].Name < connectors[j].Name
|
|
})
|
|
if diff := pretty.Compare(want, connectors); diff != "" {
|
|
t.Errorf("password list retrieved from storage did not match: %s", diff)
|
|
}
|
|
}
|
|
listAndCompare(connectorList)
|
|
|
|
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) {
|
|
return k, nil
|
|
})
|
|
if err != nil {
|
|
t.Errorf("failed to update keys: %v", err)
|
|
return
|
|
}
|
|
|
|
if got, err := s.GetKeys(); err != nil {
|
|
t.Errorf("failed to get keys: %v", err)
|
|
} else {
|
|
got.NextRotation = got.NextRotation.UTC()
|
|
if diff := pretty.Compare(k, got); diff != "" {
|
|
t.Errorf("got keys did not equal expected: %s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Postgres isn't as accurate with nano seconds as we'd like
|
|
n := time.Now().UTC().Round(time.Second)
|
|
|
|
keys1 := storage.Keys{
|
|
SigningKey: jsonWebKeys[0].Private,
|
|
SigningKeyPub: jsonWebKeys[0].Public,
|
|
NextRotation: n,
|
|
}
|
|
|
|
keys2 := storage.Keys{
|
|
SigningKey: jsonWebKeys[2].Private,
|
|
SigningKeyPub: jsonWebKeys[2].Public,
|
|
NextRotation: n.Add(time.Hour),
|
|
VerificationKeys: []storage.VerificationKey{
|
|
{
|
|
PublicKey: jsonWebKeys[0].Public,
|
|
Expiry: n.Add(time.Hour),
|
|
},
|
|
{
|
|
PublicKey: jsonWebKeys[1].Public,
|
|
Expiry: n.Add(time.Hour * 2),
|
|
},
|
|
},
|
|
}
|
|
|
|
updateAndCompare(keys1)
|
|
updateAndCompare(keys2)
|
|
}
|
|
|
|
func testGC(t *testing.T, s storage.Storage) {
|
|
est, err := time.LoadLocation("America/New_York")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pst, err := time.LoadLocation("America/Los_Angeles")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expiry := time.Now().In(est)
|
|
c := storage.AuthCode{
|
|
ID: storage.NewID(),
|
|
ClientID: "foobar",
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "foobar",
|
|
Scopes: []string{"openid", "email"},
|
|
Expiry: expiry,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "1",
|
|
Username: "jane",
|
|
Email: "jane.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
}
|
|
|
|
if err := s.CreateAuthCode(c); err != nil {
|
|
t.Fatalf("failed creating auth code: %v", err)
|
|
}
|
|
|
|
for _, tz := range []*time.Location{time.UTC, est, pst} {
|
|
result, err := s.GarbageCollect(expiry.Add(-time.Hour).In(tz))
|
|
if err != nil {
|
|
t.Errorf("garbage collection failed: %v", err)
|
|
} else {
|
|
if result.AuthCodes != 0 || result.AuthRequests != 0 {
|
|
t.Errorf("expected no garbage collection results, got %#v", result)
|
|
}
|
|
}
|
|
if _, err := s.GetAuthCode(c.ID); err != nil {
|
|
t.Errorf("expected to be able to get auth code after GC: %v", err)
|
|
}
|
|
}
|
|
|
|
if r, err := s.GarbageCollect(expiry.Add(time.Hour)); err != nil {
|
|
t.Errorf("garbage collection failed: %v", err)
|
|
} else if r.AuthCodes != 1 {
|
|
t.Errorf("expected to garbage collect 1 objects, got %d", r.AuthCodes)
|
|
}
|
|
|
|
if _, err := s.GetAuthCode(c.ID); err == nil {
|
|
t.Errorf("expected auth code to be GC'd")
|
|
} else if err != storage.ErrNotFound {
|
|
t.Errorf("expected storage.ErrNotFound, got %v", err)
|
|
}
|
|
|
|
a := storage.AuthRequest{
|
|
ID: storage.NewID(),
|
|
ClientID: "foobar",
|
|
ResponseTypes: []string{"code"},
|
|
Scopes: []string{"openid", "email"},
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "foo",
|
|
State: "bar",
|
|
ForceApprovalPrompt: true,
|
|
LoggedIn: true,
|
|
Expiry: expiry,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "1",
|
|
Username: "jane",
|
|
Email: "jane.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
}
|
|
|
|
if err := s.CreateAuthRequest(a); err != nil {
|
|
t.Fatalf("failed creating auth request: %v", err)
|
|
}
|
|
|
|
for _, tz := range []*time.Location{time.UTC, est, pst} {
|
|
result, err := s.GarbageCollect(expiry.Add(-time.Hour).In(tz))
|
|
if err != nil {
|
|
t.Errorf("garbage collection failed: %v", err)
|
|
} else {
|
|
if result.AuthCodes != 0 || result.AuthRequests != 0 {
|
|
t.Errorf("expected no garbage collection results, got %#v", result)
|
|
}
|
|
}
|
|
if _, err := s.GetAuthRequest(a.ID); err != nil {
|
|
t.Errorf("expected to be able to get auth code after GC: %v", err)
|
|
}
|
|
}
|
|
|
|
if r, err := s.GarbageCollect(expiry.Add(time.Hour)); err != nil {
|
|
t.Errorf("garbage collection failed: %v", err)
|
|
} else if r.AuthRequests != 1 {
|
|
t.Errorf("expected to garbage collect 1 objects, got %d", r.AuthRequests)
|
|
}
|
|
|
|
if _, err := s.GetAuthRequest(a.ID); err == nil {
|
|
t.Errorf("expected auth code to be GC'd")
|
|
} else if err != storage.ErrNotFound {
|
|
t.Errorf("expected storage.ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
// testTimezones tests that backends either fully support timezones or
|
|
// do the correct standardization.
|
|
func testTimezones(t *testing.T, s storage.Storage) {
|
|
est, err := time.LoadLocation("America/New_York")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Create an expiry with timezone info. Only expect backends to be
|
|
// accurate to the millisecond
|
|
expiry := time.Now().In(est).Round(time.Millisecond)
|
|
|
|
c := storage.AuthCode{
|
|
ID: storage.NewID(),
|
|
ClientID: "foobar",
|
|
RedirectURI: "https://localhost:80/callback",
|
|
Nonce: "foobar",
|
|
Scopes: []string{"openid", "email"},
|
|
Expiry: expiry,
|
|
ConnectorID: "ldap",
|
|
ConnectorData: []byte(`{"some":"data"}`),
|
|
Claims: storage.Claims{
|
|
UserID: "1",
|
|
Username: "jane",
|
|
Email: "jane.doe@example.com",
|
|
EmailVerified: true,
|
|
Groups: []string{"a", "b"},
|
|
},
|
|
}
|
|
if err := s.CreateAuthCode(c); err != nil {
|
|
t.Fatalf("failed creating auth code: %v", err)
|
|
}
|
|
got, err := s.GetAuthCode(c.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get auth code: %v", err)
|
|
}
|
|
|
|
// Ensure that if the resulting time is converted to the same
|
|
// timezone, it's the same value. We DO NOT expect timezones
|
|
// to be preserved.
|
|
gotTime := got.Expiry.In(est)
|
|
wantTime := expiry
|
|
if !gotTime.Equal(wantTime) {
|
|
t.Fatalf("expected expiry %v got %v", wantTime, gotTime)
|
|
}
|
|
}
|