e5948ab3ce
Get rid of all outdated "ClientIdentity" terminology.
630 lines
14 KiB
Go
630 lines
14 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"net/url"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coreos/go-oidc/oidc"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/kylelemons/godebug/pretty"
|
|
|
|
"github.com/coreos/dex/client"
|
|
"github.com/coreos/dex/connector"
|
|
"github.com/coreos/dex/db"
|
|
schema "github.com/coreos/dex/schema/workerschema"
|
|
"github.com/coreos/dex/user"
|
|
"github.com/coreos/dex/user/manager"
|
|
)
|
|
|
|
type testEmailer struct {
|
|
cantEmail bool
|
|
lastEmail string
|
|
lastClientID string
|
|
lastRedirectURL url.URL
|
|
lastWasInvite bool
|
|
}
|
|
|
|
// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
|
|
func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
|
return t.sendEmail(email, redirectURL, clientID, false)
|
|
}
|
|
|
|
func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
|
return t.sendEmail(email, redirectURL, clientID, true)
|
|
}
|
|
|
|
func (t *testEmailer) sendEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) {
|
|
t.lastEmail = email
|
|
t.lastRedirectURL = redirectURL
|
|
t.lastClientID = clientID
|
|
t.lastWasInvite = invite
|
|
|
|
var retURL *url.URL
|
|
if t.cantEmail {
|
|
retURL = &resetPasswordURL
|
|
}
|
|
return retURL, nil
|
|
}
|
|
|
|
var (
|
|
clock = clockwork.NewFakeClock()
|
|
|
|
goodCreds = Creds{
|
|
User: user.User{
|
|
ID: "ID-1",
|
|
Admin: true,
|
|
},
|
|
ClientID: "XXX",
|
|
}
|
|
|
|
badCreds = Creds{
|
|
User: user.User{
|
|
ID: "ID-2",
|
|
},
|
|
}
|
|
|
|
disabledCreds = Creds{
|
|
User: user.User{
|
|
ID: "ID-1",
|
|
Admin: true,
|
|
Disabled: true,
|
|
},
|
|
ClientID: "XXX",
|
|
}
|
|
|
|
resetPasswordURL = url.URL{
|
|
Host: "dex.example.com",
|
|
Path: "resetPassword",
|
|
}
|
|
|
|
validRedirURL = url.URL{
|
|
Scheme: "http",
|
|
Host: "client.example.com",
|
|
Path: "/callback",
|
|
}
|
|
)
|
|
|
|
func makeTestFixtures() (*UsersAPI, *testEmailer) {
|
|
dbMap := db.NewMemDB()
|
|
ur := func() user.UserRepo {
|
|
repo, err := db.NewUserRepoFromUsers(dbMap, []user.UserWithRemoteIdentities{
|
|
{
|
|
User: user.User{
|
|
ID: "ID-1",
|
|
Email: "id1@example.com",
|
|
Admin: true,
|
|
CreatedAt: clock.Now(),
|
|
},
|
|
}, {
|
|
User: user.User{
|
|
ID: "ID-2",
|
|
Email: "id2@example.com",
|
|
EmailVerified: true,
|
|
CreatedAt: clock.Now(),
|
|
},
|
|
}, {
|
|
User: user.User{
|
|
ID: "ID-3",
|
|
Email: "id3@example.com",
|
|
CreatedAt: clock.Now(),
|
|
},
|
|
}, {
|
|
User: user.User{
|
|
ID: "ID-4",
|
|
Email: "id4@example.com",
|
|
CreatedAt: clock.Now(),
|
|
Disabled: true,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
panic("Failed to create user repo: " + err.Error())
|
|
}
|
|
return repo
|
|
}()
|
|
|
|
pwr := func() user.PasswordInfoRepo {
|
|
repo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, []user.PasswordInfo{
|
|
{
|
|
UserID: "ID-1",
|
|
Password: []byte("password-1"),
|
|
},
|
|
{
|
|
UserID: "ID-2",
|
|
Password: []byte("password-2"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
panic("Failed to create user repo: " + err.Error())
|
|
}
|
|
return repo
|
|
}()
|
|
|
|
ccr := func() connector.ConnectorConfigRepo {
|
|
repo := db.NewConnectorConfigRepo(dbMap)
|
|
c := []connector.ConnectorConfig{
|
|
&connector.LocalConnectorConfig{ID: "local"},
|
|
}
|
|
if err := repo.Set(c); err != nil {
|
|
panic(err)
|
|
}
|
|
return repo
|
|
}()
|
|
|
|
mgr := manager.NewUserManager(ur, pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{})
|
|
mgr.Clock = clock
|
|
ci := client.Client{
|
|
Credentials: oidc.ClientCredentials{
|
|
ID: "XXX",
|
|
Secret: base64.URLEncoding.EncodeToString([]byte("secrete")),
|
|
},
|
|
Metadata: oidc.ClientMetadata{
|
|
RedirectURIs: []url.URL{
|
|
validRedirURL,
|
|
},
|
|
},
|
|
}
|
|
if _, err := db.NewClientRepoFromClients(dbMap, []client.Client{ci}); err != nil {
|
|
panic("Failed to create client repo: " + err.Error())
|
|
}
|
|
|
|
// Used in TestRevokeRefreshToken test.
|
|
refreshTokens := []struct {
|
|
clientID string
|
|
userID string
|
|
}{
|
|
{"XXX", "ID-1"},
|
|
{"XXX", "ID-2"},
|
|
}
|
|
refreshRepo := db.NewRefreshTokenRepo(dbMap)
|
|
for _, token := range refreshTokens {
|
|
if _, err := refreshRepo.Create(token.userID, token.clientID); err != nil {
|
|
panic("Failed to create refresh token: " + err.Error())
|
|
}
|
|
}
|
|
|
|
emailer := &testEmailer{}
|
|
api := NewUsersAPI(dbMap, mgr, emailer, "local")
|
|
return api, emailer
|
|
|
|
}
|
|
|
|
func TestGetUser(t *testing.T) {
|
|
tests := []struct {
|
|
creds Creds
|
|
id string
|
|
wantErr error
|
|
}{
|
|
{
|
|
creds: goodCreds,
|
|
id: "ID-1",
|
|
},
|
|
{
|
|
creds: badCreds,
|
|
id: "ID-1",
|
|
wantErr: ErrorUnauthorized,
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
id: "NO_ID",
|
|
wantErr: ErrorResourceNotFound,
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
api, _ := makeTestFixtures()
|
|
usr, err := api.GetUser(tt.creds, tt.id)
|
|
if tt.wantErr != nil {
|
|
if err != tt.wantErr {
|
|
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("case %d: want nil err, got: %q ", i, err)
|
|
}
|
|
|
|
if usr.Id != tt.id {
|
|
t.Errorf("case %d: want=%v, got=%v ", i, tt.id, usr.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListUsers(t *testing.T) {
|
|
tests := []struct {
|
|
creds Creds
|
|
filter user.UserFilter
|
|
maxResults int
|
|
pages int
|
|
wantErr error
|
|
wantIDs [][]string
|
|
}{
|
|
{
|
|
creds: goodCreds,
|
|
pages: 3,
|
|
maxResults: 1,
|
|
wantIDs: [][]string{{"ID-1"}, {"ID-2"}, {"ID-3"}},
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
pages: 1,
|
|
maxResults: 3,
|
|
wantIDs: [][]string{{"ID-1", "ID-2", "ID-3"}},
|
|
},
|
|
{
|
|
creds: badCreds,
|
|
pages: 3,
|
|
maxResults: 1,
|
|
wantErr: ErrorUnauthorized,
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
pages: 3,
|
|
maxResults: 10000,
|
|
wantErr: ErrorMaxResultsTooHigh,
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
api, _ := makeTestFixtures()
|
|
|
|
gotIDs := [][]string{}
|
|
var next string
|
|
var err error
|
|
var users []*schema.User
|
|
for x := 0; x < tt.pages; x++ {
|
|
users, next, err = api.ListUsers(tt.creds, tt.maxResults, next)
|
|
if tt.wantErr != nil {
|
|
if err != tt.wantErr {
|
|
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err)
|
|
}
|
|
goto NextTest
|
|
}
|
|
|
|
var ids []string
|
|
for _, usr := range users {
|
|
ids = append(ids, usr.Id)
|
|
}
|
|
gotIDs = append(gotIDs, ids)
|
|
}
|
|
|
|
if diff := pretty.Compare(tt.wantIDs, gotIDs); diff != "" {
|
|
t.Errorf("case %d: Compare(want, got) = %v", i,
|
|
diff)
|
|
}
|
|
NextTest:
|
|
}
|
|
}
|
|
|
|
func TestCreateUser(t *testing.T) {
|
|
tests := []struct {
|
|
creds Creds
|
|
usr schema.User
|
|
redirURL url.URL
|
|
cantEmail bool
|
|
|
|
wantResponse schema.UserCreateResponse
|
|
wantErr error
|
|
}{
|
|
{
|
|
creds: goodCreds,
|
|
usr: schema.User{
|
|
Email: "newuser01@example.com",
|
|
DisplayName: "New User",
|
|
EmailVerified: true,
|
|
Admin: false,
|
|
},
|
|
redirURL: validRedirURL,
|
|
|
|
wantResponse: schema.UserCreateResponse{
|
|
EmailSent: true,
|
|
User: &schema.User{
|
|
Email: "newuser01@example.com",
|
|
DisplayName: "New User",
|
|
EmailVerified: true,
|
|
Admin: false,
|
|
CreatedAt: clock.Now().Format(time.RFC3339),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
usr: schema.User{
|
|
Email: "newuser02@example.com",
|
|
DisplayName: "New User",
|
|
EmailVerified: true,
|
|
Admin: false,
|
|
},
|
|
redirURL: validRedirURL,
|
|
cantEmail: true,
|
|
|
|
wantResponse: schema.UserCreateResponse{
|
|
User: &schema.User{
|
|
Email: "newuser02@example.com",
|
|
DisplayName: "New User",
|
|
EmailVerified: true,
|
|
Admin: false,
|
|
CreatedAt: clock.Now().Format(time.RFC3339),
|
|
},
|
|
ResetPasswordLink: resetPasswordURL.String(),
|
|
},
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
usr: schema.User{
|
|
Email: "newuser03@example.com",
|
|
DisplayName: "New User",
|
|
EmailVerified: true,
|
|
Admin: false,
|
|
},
|
|
redirURL: url.URL{Host: "scammers.com"},
|
|
|
|
wantErr: ErrorInvalidRedirectURL,
|
|
},
|
|
{
|
|
creds: badCreds,
|
|
usr: schema.User{
|
|
Email: "newuser04@example.com",
|
|
DisplayName: "New User",
|
|
EmailVerified: true,
|
|
Admin: false,
|
|
},
|
|
redirURL: validRedirURL,
|
|
|
|
wantErr: ErrorUnauthorized,
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
api, emailer := makeTestFixtures()
|
|
emailer.cantEmail = tt.cantEmail
|
|
|
|
response, err := api.CreateUser(tt.creds, tt.usr, tt.redirURL)
|
|
if tt.wantErr != nil {
|
|
if err != tt.wantErr {
|
|
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err)
|
|
}
|
|
|
|
tok := ""
|
|
for {
|
|
list, tok, err := api.ListUsers(goodCreds, 100, tok)
|
|
if err != nil {
|
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
|
break
|
|
}
|
|
for _, u := range list {
|
|
if u.Email == tt.usr.Email {
|
|
t.Errorf("case %d: got an error but user was still created", i)
|
|
}
|
|
}
|
|
if tok == "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("case %d: want nil err, got: %q ", i, err)
|
|
}
|
|
|
|
newID := response.User.Id
|
|
if newID == "" {
|
|
t.Errorf("case %d: expected non-empty newID", i)
|
|
}
|
|
|
|
tt.wantResponse.User.Id = newID
|
|
if diff := pretty.Compare(tt.wantResponse, response); diff != "" {
|
|
t.Errorf("case %d: Compare(want, got) = %v", i,
|
|
diff)
|
|
}
|
|
|
|
wantEmalier := testEmailer{
|
|
cantEmail: tt.cantEmail,
|
|
lastEmail: tt.usr.Email,
|
|
lastClientID: tt.creds.ClientID,
|
|
lastRedirectURL: tt.redirURL,
|
|
lastWasInvite: true,
|
|
}
|
|
if diff := pretty.Compare(wantEmalier, emailer); diff != "" {
|
|
t.Errorf("case %d: Compare(want, got) = %v", i,
|
|
diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDisableUsers(t *testing.T) {
|
|
tests := []struct {
|
|
id string
|
|
disable bool
|
|
}{
|
|
{
|
|
id: "ID-1",
|
|
disable: true,
|
|
},
|
|
{
|
|
id: "ID-1",
|
|
disable: false,
|
|
},
|
|
{
|
|
id: "ID-4",
|
|
disable: true,
|
|
},
|
|
{
|
|
id: "ID-4",
|
|
disable: false,
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
api, _ := makeTestFixtures()
|
|
_, err := api.DisableUser(goodCreds, tt.id, tt.disable)
|
|
if err != nil {
|
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
|
}
|
|
|
|
usr, err := api.GetUser(goodCreds, tt.id)
|
|
if err != nil {
|
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
|
}
|
|
|
|
if usr.Disabled != tt.disable {
|
|
t.Errorf("case %d: user disable state wrong. wanted: %v got: %v", i, tt.disable, usr.Disabled)
|
|
}
|
|
}
|
|
}
|
|
func TestResendEmailInvitation(t *testing.T) {
|
|
tests := []struct {
|
|
creds Creds
|
|
userID string
|
|
email string
|
|
redirURL url.URL
|
|
cantEmail bool
|
|
|
|
wantResponse schema.ResendEmailInvitationResponse
|
|
wantErr error
|
|
}{
|
|
{
|
|
creds: goodCreds,
|
|
userID: "ID-1",
|
|
email: "id1@example.com",
|
|
redirURL: validRedirURL,
|
|
|
|
wantResponse: schema.ResendEmailInvitationResponse{
|
|
EmailSent: true,
|
|
},
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
userID: "ID-1",
|
|
email: "id1@example.com",
|
|
redirURL: validRedirURL,
|
|
cantEmail: true,
|
|
|
|
wantResponse: schema.ResendEmailInvitationResponse{
|
|
EmailSent: false,
|
|
ResetPasswordLink: resetPasswordURL.String(),
|
|
},
|
|
},
|
|
{
|
|
creds: badCreds,
|
|
userID: "ID-1",
|
|
email: "id1@example.com",
|
|
redirURL: validRedirURL,
|
|
|
|
wantErr: ErrorUnauthorized,
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
userID: "ID-1",
|
|
email: "id1@example.com",
|
|
redirURL: url.URL{Host: "scammers.com"},
|
|
|
|
wantErr: ErrorInvalidRedirectURL,
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
userID: "ID-2",
|
|
email: "id2@example.com",
|
|
redirURL: validRedirURL,
|
|
|
|
wantErr: ErrorVerifiedEmail,
|
|
},
|
|
{
|
|
creds: goodCreds,
|
|
userID: "non-existent",
|
|
email: "non-existent@example.com",
|
|
redirURL: validRedirURL,
|
|
|
|
wantErr: ErrorResourceNotFound,
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
api, emailer := makeTestFixtures()
|
|
emailer.cantEmail = tt.cantEmail
|
|
|
|
response, err := api.ResendEmailInvitation(tt.creds, tt.userID, tt.redirURL)
|
|
if tt.wantErr != nil {
|
|
if err != tt.wantErr {
|
|
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("case %d: want nil err, got: %q ", i, err)
|
|
}
|
|
|
|
if diff := pretty.Compare(tt.wantResponse, response); diff != "" {
|
|
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
|
|
}
|
|
|
|
wantEmailer := testEmailer{
|
|
cantEmail: tt.cantEmail,
|
|
lastEmail: tt.email,
|
|
lastClientID: tt.creds.ClientID,
|
|
lastRedirectURL: tt.redirURL,
|
|
lastWasInvite: true,
|
|
}
|
|
if diff := pretty.Compare(wantEmailer, emailer); diff != "" {
|
|
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRevokeRefreshToken(t *testing.T) {
|
|
tests := []struct {
|
|
userID string
|
|
toRevoke string
|
|
before []string // clientIDs expected before the change.
|
|
after []string // clientIDs expected after the change.
|
|
}{
|
|
{"ID-1", "XXX", []string{"XXX"}, []string{}},
|
|
{"ID-2", "XXX", []string{"XXX"}, []string{}},
|
|
}
|
|
|
|
api, _ := makeTestFixtures()
|
|
|
|
listClientsWithRefreshTokens := func(creds Creds, userID string) ([]string, error) {
|
|
clients, err := api.ListClientsWithRefreshTokens(creds, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientIDs := make([]string, len(clients))
|
|
for i, client := range clients {
|
|
clientIDs[i] = client.ClientID
|
|
}
|
|
sort.Strings(clientIDs)
|
|
return clientIDs, nil
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
creds := Creds{User: user.User{ID: tt.userID}}
|
|
|
|
gotBefore, err := listClientsWithRefreshTokens(creds, tt.userID)
|
|
if err != nil {
|
|
t.Errorf("case %d: list clients failed: %v", i, err)
|
|
} else {
|
|
if diff := pretty.Compare(tt.before, gotBefore); diff != "" {
|
|
t.Errorf("case %d: before exp!=got: %s", i, diff)
|
|
}
|
|
}
|
|
|
|
if err := api.RevokeRefreshTokensForClient(creds, tt.userID, tt.toRevoke); err != nil {
|
|
t.Errorf("case %d: failed to revoke client: %v", i, err)
|
|
continue
|
|
}
|
|
|
|
gotAfter, err := listClientsWithRefreshTokens(creds, tt.userID)
|
|
if err != nil {
|
|
t.Errorf("case %d: list clients failed: %v", i, err)
|
|
} else {
|
|
if diff := pretty.Compare(tt.after, gotAfter); diff != "" {
|
|
t.Errorf("case %d: after exp!=got: %s", i, diff)
|
|
}
|
|
}
|
|
}
|
|
}
|