Newer
Older
package keystone
import (
"bytes"
"net/http"
"os"
"reflect"
"strings"
"testing"
"github.com/dexidp/dex/connector"
)
const (
invalidPass = "WRONG_PASS"
testUser = "test_user"
testPass = "test_pass"
testEmail = "test@example.com"
testGroup = "test_group"
testDomainAltName = "altdomain"
testDomainID = "default"
testDomainName = "Default"
var (
keystoneURL = ""
keystoneAdminURL = ""
adminUser = ""
adminPass = ""
authTokenURL = ""
usersURL = ""
groupsURL = ""
domainsURL = ""
type userReq struct {
Name string `json:"name"`
Email string `json:"email"`
Enabled bool `json:"enabled"`
Password string `json:"password"`
Roles []string `json:"roles"`
DomainID string `json:"domain_id,omitempty"`
}
type domainResponse struct {
Domain domainKeystone `json:"domain"`
}
type domainsResponse struct {
Domains []domainKeystone `json:"domains"`
}
type groupResponse struct {
Group struct {
ID string `json:"id"`
} `json:"group"`
func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) {
t.Helper()
jsonData := loginRequestData{
auth: auth{
Identity: identity{
Methods: []string{"password"},
Password: password{
User: user{
Name: adminName,
Domain: domainKeystone{ID: testDomainID},
body, err := json.Marshal(jsonData)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(body))
if err != nil {
t.Fatalf("keystone: failed to obtain admin token: %v\n", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
token = resp.Header.Get("X-Subject-Token")
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
err = json.Unmarshal(data, &tokenResp)
if err != nil {
t.Fatal(err)
}
return token, tokenResp.Token.User.ID
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
func getOrCreateDomain(t *testing.T, token, domainName string) string {
t.Helper()
domainSearchURL := domainsURL + "?name=" + domainName
reqGet, err := http.NewRequest("GET", domainSearchURL, nil)
if err != nil {
t.Fatal(err)
}
reqGet.Header.Set("X-Auth-Token", token)
reqGet.Header.Add("Content-Type", "application/json")
respGet, err := http.DefaultClient.Do(reqGet)
if err != nil {
t.Fatal(err)
}
dataGet, err := io.ReadAll(respGet.Body)
if err != nil {
t.Fatal(err)
}
defer respGet.Body.Close()
domainsResp := new(domainsResponse)
err = json.Unmarshal(dataGet, &domainsResp)
if err != nil {
t.Fatal(err)
}
if len(domainsResp.Domains) >= 1 {
return domainsResp.Domains[0].ID
}
createDomainData := map[string]interface{}{
"domain": map[string]interface{}{
"name": domainName,
"enabled": true,
},
}
body, err := json.Marshal(createDomainData)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", domainsURL, bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-Auth-Token", token)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 201 {
t.Fatalf("failed to create domain %s", domainName)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
domainResp := new(domainResponse)
err = json.Unmarshal(data, &domainResp)
if err != nil {
t.Fatal(err)
}
return domainResp.Domain.ID
}
func createUser(t *testing.T, token, domainID, userName, userEmail, userPass string) string {
createUserData := map[string]interface{}{
"user": userReq{
DomainID: domainID,
Name: userName,
Email: userEmail,
Enabled: true,
Password: userPass,
Roles: []string{"admin"},
body, err := json.Marshal(createUserData)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", usersURL, bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-Auth-Token", token)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
data, err := io.ReadAll(resp.Body)
err = json.Unmarshal(data, &userResp)
if err != nil {
t.Fatal(err)
}
func deleteResource(t *testing.T, token, id, uri string) {
deleteURI := uri + id
req, err := http.NewRequest("DELETE", deleteURI, nil)
if err != nil {
t.Fatalf("error: %v", err)
}
req.Header.Set("X-Auth-Token", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("error: %v", err)
}
defer resp.Body.Close()
func createGroup(t *testing.T, token, description, name string) string {
t.Helper()
createGroupData := map[string]interface{}{
"group": map[string]interface{}{
"name": name,
"description": description,
body, err := json.Marshal(createGroupData)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", groupsURL, bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-Auth-Token", token)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
err = json.Unmarshal(data, &groupResp)
func addUserToGroup(t *testing.T, token, groupID, userID string) error {
t.Helper()
uri := groupsURL + groupID + "/users/" + userID
req, err := http.NewRequest("PUT", uri, nil)
if err != nil {
return err
}
req.Header.Set("X-Auth-Token", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("error: %v", err)
}
defer resp.Body.Close()
}
func TestIncorrectCredentialsLogin(t *testing.T) {
setupVariables(t)
client: http.DefaultClient,
Host: keystoneURL, Domain: domainKeystone{ID: testDomainID},
s := connector.Scopes{OfflineAccess: true, Groups: true}
_, validPW, err := c.Login(context.Background(), s, adminUser, invalidPass)
if validPW {
t.Fatal("Incorrect password check")
}
if err == nil {
t.Fatal("Error should be returned when invalid password is provided")
}
if !strings.Contains(err.Error(), "401") {
t.Fatal("Unrecognized error, expecting 401")
}
func TestValidUserLogin(t *testing.T) {
setupVariables(t)
token, _ := getAdminToken(t, adminUser, adminPass)
createDomain bool
domain domainKeystone
username string
email string
password string
}
type expect struct {
username string
email string
verifiedEmail bool
}
name string
input tUser
expected expect
}{
{
name: "test with email address",
input: tUser{
createDomain: false,
domain: domainKeystone{ID: testDomainID},
username: testUser,
email: testEmail,
password: testPass,
},
expected: expect{
username: testUser,
email: testEmail,
verifiedEmail: true,
},
},
{
name: "test without email address",
input: tUser{
createDomain: false,
domain: domainKeystone{ID: testDomainID},
username: testUser,
email: "",
password: testPass,
},
expected: expect{
username: testUser,
email: "",
verifiedEmail: false,
},
},
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
{
name: "test with default domain Name",
input: tUser{
createDomain: false,
domain: domainKeystone{Name: testDomainName},
username: testUser,
email: testEmail,
password: testPass,
},
expected: expect{
username: testUser,
email: testEmail,
verifiedEmail: true,
},
},
{
name: "test with custom domain Name",
input: tUser{
createDomain: true,
domain: domainKeystone{Name: testDomainAltName},
username: testUser,
email: testEmail,
password: testPass,
},
expected: expect{
username: testUser,
email: testEmail,
verifiedEmail: true,
},
},
{
name: "test with custom domain ID",
input: tUser{
createDomain: true,
domain: domainKeystone{},
username: testUser,
email: testEmail,
password: testPass,
},
expected: expect{
username: testUser,
email: testEmail,
verifiedEmail: true,
},
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
domainID := ""
if tt.input.createDomain == true {
domainID = getOrCreateDomain(t, token, testDomainAltName)
t.Logf("getOrCreateDomain ID: %s\n", domainID)
// if there was nothing set then use the dynamically generated domain ID
if tt.input.domain.ID == "" && tt.input.domain.Name == "" {
tt.input.domain.ID = domainID
}
}
userID := createUser(t, token, domainID, tt.input.username, tt.input.email, tt.input.password)
defer deleteResource(t, token, userID, usersURL)
client: http.DefaultClient,
Host: keystoneURL, Domain: tt.input.domain,
s := connector.Scopes{OfflineAccess: true, Groups: true}
identity, validPW, err := c.Login(context.Background(), s, tt.input.username, tt.input.password)
if err != nil {
t.Fatalf("Login failed for user %s: %v", tt.input.username, err.Error())
}
t.Log(identity)
if identity.Username != tt.expected.username {
t.Fatalf("Invalid user. Got: %v. Wanted: %v", identity.Username, tt.expected.username)
}
if identity.UserID == "" {
t.Fatalf("Didn't get any UserID back")
}
if identity.Email != tt.expected.email {
t.Fatalf("Invalid email. Got: %v. Wanted: %v", identity.Email, tt.expected.email)
}
if identity.EmailVerified != tt.expected.verifiedEmail {
t.Fatalf("Invalid verifiedEmail. Got: %v. Wanted: %v", identity.EmailVerified, tt.expected.verifiedEmail)
}
if !validPW {
t.Fatal("Valid password was not accepted")
}
})
}
func TestUseRefreshToken(t *testing.T) {
setupVariables(t)
token, adminID := getAdminToken(t, adminUser, adminPass)
groupID := createGroup(t, token, "Test group description", testGroup)
addUserToGroup(t, token, groupID, adminID)
defer deleteResource(t, token, groupID, groupsURL)
client: http.DefaultClient,
Host: keystoneURL, Domain: domainKeystone{ID: testDomainID},
s := connector.Scopes{OfflineAccess: true, Groups: true}
identityLogin, _, err := c.Login(context.Background(), s, adminUser, adminPass)
if err != nil {
t.Fatal(err.Error())
}
identityRefresh, err := c.Refresh(context.Background(), s, identityLogin)
if err != nil {
t.Fatal(err.Error())
}
expectEquals(t, 1, len(identityRefresh.Groups))
func TestUseRefreshTokenUserDeleted(t *testing.T) {
setupVariables(t)
token, _ := getAdminToken(t, adminUser, adminPass)
userID := createUser(t, token, "", testUser, testEmail, testPass)
client: http.DefaultClient,
Host: keystoneURL, Domain: domainKeystone{ID: testDomainID},
s := connector.Scopes{OfflineAccess: true, Groups: true}
identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass)
if err != nil {
t.Fatal(err.Error())
}
_, err = c.Refresh(context.Background(), s, identityLogin)
if err != nil {
t.Fatal(err.Error())
}
deleteResource(t, token, userID, usersURL)
_, err = c.Refresh(context.Background(), s, identityLogin)
if !strings.Contains(err.Error(), "does not exist") {
t.Errorf("unexpected error: %s", err.Error())
}
func TestUseRefreshTokenGroupsChanged(t *testing.T) {
setupVariables(t)
token, _ := getAdminToken(t, adminUser, adminPass)
userID := createUser(t, token, "", testUser, testEmail, testPass)
defer deleteResource(t, token, userID, usersURL)
client: http.DefaultClient,
Host: keystoneURL, Domain: domainKeystone{ID: testDomainID},
s := connector.Scopes{OfflineAccess: true, Groups: true}
identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass)
if err != nil {
t.Fatal(err.Error())
}
identityRefresh, err := c.Refresh(context.Background(), s, identityLogin)
if err != nil {
t.Fatal(err.Error())
}
expectEquals(t, 0, len(identityRefresh.Groups))
groupID := createGroup(t, token, "Test group", testGroup)
addUserToGroup(t, token, groupID, userID)
defer deleteResource(t, token, groupID, groupsURL)
identityRefresh, err = c.Refresh(context.Background(), s, identityLogin)
if err != nil {
t.Fatal(err.Error())
}
expectEquals(t, 1, len(identityRefresh.Groups))
func TestNoGroupsInScope(t *testing.T) {
setupVariables(t)
token, _ := getAdminToken(t, adminUser, adminPass)
userID := createUser(t, token, "", testUser, testEmail, testPass)
defer deleteResource(t, token, userID, usersURL)
client: http.DefaultClient,
Host: keystoneURL, Domain: domainKeystone{ID: testDomainID},
s := connector.Scopes{OfflineAccess: true, Groups: false}
groupID := createGroup(t, token, "Test group", testGroup)
addUserToGroup(t, token, groupID, userID)
defer deleteResource(t, token, groupID, groupsURL)
identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass)
if err != nil {
t.Fatal(err.Error())
}
expectEquals(t, 0, len(identityLogin.Groups))
identityRefresh, err := c.Refresh(context.Background(), s, identityLogin)
if err != nil {
t.Fatal(err.Error())
}
expectEquals(t, 0, len(identityRefresh.Groups))
}
func setupVariables(t *testing.T) {
keystoneURLEnv := "DEX_KEYSTONE_URL"
keystoneAdminURLEnv := "DEX_KEYSTONE_ADMIN_URL"
keystoneAdminUserEnv := "DEX_KEYSTONE_ADMIN_USER"
keystoneAdminPassEnv := "DEX_KEYSTONE_ADMIN_PASS"
keystoneURL = os.Getenv(keystoneURLEnv)
if keystoneURL == "" {
t.Skipf("variable %q not set, skipping keystone connector tests\n", keystoneURLEnv)
keystoneAdminURL = os.Getenv(keystoneAdminURLEnv)
t.Skipf("variable %q not set, skipping keystone connector tests\n", keystoneAdminURLEnv)
return
}
adminUser = os.Getenv(keystoneAdminUserEnv)
if adminUser == "" {
t.Skipf("variable %q not set, skipping keystone connector tests\n", keystoneAdminUserEnv)
return
}
adminPass = os.Getenv(keystoneAdminPassEnv)
if adminPass == "" {
t.Skipf("variable %q not set, skipping keystone connector tests\n", keystoneAdminPassEnv)
return
}
authTokenURL = keystoneURL + "/v3/auth/tokens/"
usersURL = keystoneAdminURL + "/v3/users/"
groupsURL = keystoneAdminURL + "/v3/groups/"
domainsURL = keystoneAdminURL + "/v3/domains/"