From 88d1e2b041a24ea85e9d3fcf7e7b7c19c908083e Mon Sep 17 00:00:00 2001 From: joannano Date: Thu, 13 Dec 2018 12:22:53 +0100 Subject: [PATCH] keystone: test cases, refactoring and cleanup --- .travis.yml | 5 +- Dockerfile | 9 +- connector/connector.go | 1 - connector/keystone/keystone.go | 253 +++++++------- connector/keystone/keystone_test.go | 495 ++++++++++++++++------------ connector/keystone/types.go | 141 +++----- examples/config-keystone.yaml | 55 ---- server/handlers.go | 6 +- server/server.go | 4 +- storage/static.go | 1 + 10 files changed, 487 insertions(+), 483 deletions(-) delete mode 100644 examples/config-keystone.yaml diff --git a/.travis.yml b/.travis.yml index 934e32e1..07b941bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,13 +13,14 @@ services: - docker env: - - DEX_POSTGRES_DATABASE=postgres DEX_POSTGRES_USER=postgres DEX_POSTGRES_HOST="localhost" DEX_ETCD_ENDPOINTS=http://localhost:2379 DEX_LDAP_TESTS=1 DEBIAN_FRONTEND=noninteractive + - DEX_POSTGRES_DATABASE=postgres DEX_POSTGRES_USER=postgres DEX_POSTGRES_HOST="localhost" DEX_ETCD_ENDPOINTS=http://localhost:2379 DEX_LDAP_TESTS=1 DEBIAN_FRONTEND=noninteractive DEX_KEYSTONE_URL=http://localhost:5000 DEX_KEYSTONE_ADMIN_URL=http://localhost:35357 install: - sudo -E apt-get install -y --force-yes slapd time ldap-utils - sudo /etc/init.d/slapd stop - docker run -d --net=host gcr.io/etcd-development/etcd:v3.2.9 - + - docker run -d -p 0.0.0.0:5000:5000 -p 0.0.0.0:35357:35357 openio/openstack-keystone + - sleep 60s script: - make testall diff --git a/Dockerfile b/Dockerfile index d6ce6a9c..dbc0dd38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,12 +11,15 @@ FROM alpine:3.8 # experience when this doesn't work out of the box. # # OpenSSL is required so wget can query HTTPS endpoints for health checking. -RUN apk add --update ca-certificates openssl bash +RUN apk add --update ca-certificates openssl + +COPY --from=0 /go/bin/dex /usr/local/bin/dex # Import frontend assets and set the correct CWD directory so the assets # are in the default path. COPY web /web WORKDIR / -EXPOSE 5500-5600 -CMD ["bash"] +ENTRYPOINT ["dex"] + +CMD ["version"] diff --git a/connector/connector.go b/connector/connector.go index 0335ea94..c442c54a 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -35,7 +35,6 @@ type Identity struct { // // This data is never shared with end users, OAuth clients, or through the API. ConnectorData []byte - Password string } // PasswordConnector is an interface implemented by connectors which take a diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go index d62aa179..8abff2b4 100644 --- a/connector/keystone/keystone.go +++ b/connector/keystone/keystone.go @@ -2,163 +2,186 @@ package keystone import ( - "context" - "fmt" - "github.com/dexidp/dex/connector" - "github.com/sirupsen/logrus" - "encoding/json" - "net/http" "bytes" + "context" + "encoding/json" + "fmt" "io/ioutil" + "net/http" + + "github.com/sirupsen/logrus" + + "github.com/dexidp/dex/connector" ) var ( - _ connector.PasswordConnector = &Connector{} - _ connector.RefreshConnector = &Connector{} + _ connector.PasswordConnector = &keystoneConnector{} + _ connector.RefreshConnector = &keystoneConnector{} ) // Open returns an authentication strategy using Keystone. func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { - return &Connector{c.Domain, c.KeystoneHost, - c.KeystoneUsername, c.KeystonePassword, logger}, nil + return &keystoneConnector{c.Domain, c.KeystoneHost, + c.KeystoneUsername, c.KeystonePassword, logger}, nil } -func (p Connector) Close() error { return nil } +func (p *keystoneConnector) Close() error { return nil } -func (p Connector) Login(ctx context.Context, s connector.Scopes, username, password string) ( - identity connector.Identity, validPassword bool, err error) { - response, err := p.getTokenResponse(username, password) +func (p *keystoneConnector) Login(ctx context.Context, s connector.Scopes, username, password string) ( + identity connector.Identity, validPassword bool, err error) { + resp, err := p.getTokenResponse(ctx, username, password) + if err != nil { + return identity, false, fmt.Errorf("keystone: error %v", err) + } // Providing wrong password or wrong keystone URI throws error - if err == nil && response.StatusCode == 201 { - token := response.Header["X-Subject-Token"][0] - data, _ := ioutil.ReadAll(response.Body) + if resp.StatusCode == 201 { + token := resp.Header.Get("X-Subject-Token") + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return identity, false, err + } + defer resp.Body.Close() - var tokenResponse = new(TokenResponse) - err := json.Unmarshal(data, &tokenResponse) + var tokenResp = new(tokenResponse) + err = json.Unmarshal(data, &tokenResp) + if err != nil { + return identity, false, fmt.Errorf("keystone: invalid token response: %v", err) + } + groups, err := p.getUserGroups(ctx, tokenResp.Token.User.ID, token) + if err != nil { + return identity, false, err + } - if err != nil { - fmt.Printf("keystone: invalid token response: %v", err) - return identity, false, err - } - groups, err := p.getUserGroups(tokenResponse.Token.User.ID, token) - - if err != nil { - return identity, false, err - } - - identity.Username = username - identity.UserID = tokenResponse.Token.User.ID - identity.Groups = groups + identity.Username = username + identity.UserID = tokenResp.Token.User.ID + identity.Groups = groups return identity, true, nil - } else if err != nil { - fmt.Printf("keystone: error %v", err) - return identity, false, err - - } else { - data, _ := ioutil.ReadAll(response.Body) - fmt.Println(string(data)) - return identity, false, err } + return identity, false, nil } -func (p Connector) Prompt() string { return "username" } +func (p *keystoneConnector) Prompt() string { return "username" } -func (p Connector) Refresh( +func (p *keystoneConnector) Refresh( ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) { - if len(identity.ConnectorData) == 0 { - return identity, nil + token, err := p.getAdminToken(ctx) + if err != nil { + return identity, fmt.Errorf("keystone: failed to obtain admin token: %v", err) } - token, err := p.getAdminToken() + ok, err := p.checkIfUserExists(ctx, identity.UserID, token) + if err != nil { + return identity, err + } + if !ok { + return identity, fmt.Errorf("keystone: user %q does not exist", identity.UserID) + } - if err != nil { - fmt.Printf("keystone: failed to obtain admin token") - return identity, err - } + groups, err := p.getUserGroups(ctx, identity.UserID, token) + if err != nil { + return identity, err + } - ok := p.checkIfUserExists(identity.UserID, token) - if !ok { - fmt.Printf("keystone: user %q does not exist\n", identity.UserID) - return identity, fmt.Errorf("keystone: user %q does not exist", identity.UserID) - } - - groups, err := p.getUserGroups(identity.UserID, token) - if err != nil { - fmt.Printf("keystone: Failed to fetch user %q groups", identity.UserID) - return identity, fmt.Errorf("keystone: failed to fetch user %q groups", identity.UserID) - } - - identity.Groups = groups - fmt.Printf("Identity data after use of refresh token: %v", identity) + identity.Groups = groups return identity, nil } - -func (p Connector) getTokenResponse(username, password string) (response *http.Response, err error) { - jsonData := LoginRequestData{ - Auth: Auth{ - Identity: Identity{ - Methods:[]string{"password"}, - Password: Password{ - User: User{ - Name: username, - Domain: Domain{ID:p.Domain}, - Password: password, +func (p *keystoneConnector) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) { + client := &http.Client{} + jsonData := loginRequestData{ + auth: auth{ + Identity: identity{ + Methods: []string{"password"}, + Password: password{ + User: user{ + Name: username, + Domain: domain{ID: p.Domain}, + Password: pass, }, }, }, }, } - jsonValue, _ := json.Marshal(jsonData) - loginURI := p.KeystoneHost + "/v3/auth/tokens" - return http.Post(loginURI, "application/json", bytes.NewBuffer(jsonValue)) + jsonValue, err := json.Marshal(jsonData) + if err != nil { + return nil, err + } + + authTokenURL := p.KeystoneHost + "/v3/auth/tokens/" + req, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(jsonValue)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req = req.WithContext(ctx) + + return client.Do(req) } -func (p Connector) getAdminToken()(string, error) { - response, err := p.getTokenResponse(p.KeystoneUsername, p.KeystonePassword) - if err!= nil { - return "", err - } - token := response.Header["X-Subject-Token"][0] - return token, nil +func (p *keystoneConnector) getAdminToken(ctx context.Context) (string, error) { + resp, err := p.getTokenResponse(ctx, p.KeystoneUsername, p.KeystonePassword) + if err != nil { + return "", err + } + token := resp.Header.Get("X-Subject-Token") + return token, nil } -func (p Connector) checkIfUserExists(userID string, token string) (bool) { - groupsURI := p.KeystoneHost + "/v3/users/" + userID - client := &http.Client{} - req, _ := http.NewRequest("GET", groupsURI, nil) - req.Header.Set("X-Auth-Token", token) - response, err := client.Do(req) - if err == nil && response.StatusCode == 200 { - return true - } - return false +func (p *keystoneConnector) checkIfUserExists(ctx context.Context, userID string, token string) (bool, error) { + userURL := p.KeystoneHost + "/v3/users/" + userID + client := &http.Client{} + req, err := http.NewRequest("GET", userURL, nil) + if err != nil { + return false, err + } + + req.Header.Set("X-Auth-Token", token) + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return false, err + } + + if resp.StatusCode == 200 { + return true, nil + } + return false, err } -func (p Connector) getUserGroups(userID string, token string) ([]string, error) { - groupsURI := p.KeystoneHost + "/v3/users/" + userID + "/groups" - client := &http.Client{} - req, _ := http.NewRequest("GET", groupsURI, nil) - req.Header.Set("X-Auth-Token", token) - response, err := client.Do(req) +func (p *keystoneConnector) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) { + client := &http.Client{} + groupsURL := p.KeystoneHost + "/v3/users/" + userID + "/groups" - if err != nil { - fmt.Printf("keystone: error while fetching user %q groups\n", userID) - return nil, err - } - data, _ := ioutil.ReadAll(response.Body) - var groupsResponse = new(GroupsResponse) - err = json.Unmarshal(data, &groupsResponse) - if err != nil { - return nil, err - } - groups := []string{} - for _, group := range groupsResponse.Groups { - groups = append(groups, group.Name) - } - return groups, nil + req, err := http.NewRequest("GET", groupsURL, nil) + req.Header.Set("X-Auth-Token", token) + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID) + return nil, err + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var groupsResp = new(groupsResponse) + + err = json.Unmarshal(data, &groupsResp) + if err != nil { + return nil, err + } + + groups := make([]string, len(groupsResp.Groups)) + for i, group := range groupsResp.Groups { + groups[i] = group.Name + } + return groups, nil } diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go index b8bba0fe..0c40888e 100644 --- a/connector/keystone/keystone_test.go +++ b/connector/keystone/keystone_test.go @@ -1,275 +1,358 @@ package keystone import ( - "testing" - "github.com/dexidp/dex/connector" - - "fmt" - "io" - "os" - "time" - "net/http" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - networktypes "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" - "golang.org/x/net/context" "bytes" + "context" "encoding/json" + "fmt" "io/ioutil" + "net/http" + "os" + "reflect" + "strings" + "testing" + + "github.com/dexidp/dex/connector" ) -const dockerCliVersion = "1.37" +const ( + adminUser = "demo" + adminPass = "DEMO_PASS" + invalidPass = "WRONG_PASS" -const exposedKeystonePort = "5000" -const exposedKeystonePortAdmin = "35357" + testUser = "test_user" + testPass = "test_pass" + testEmail = "test@example.com" + testGroup = "test_group" + testDomain = "default" +) -const keystoneHost = "http://localhost" -const keystoneURL = keystoneHost + ":" + exposedKeystonePort -const keystoneAdminURL = keystoneHost + ":" + exposedKeystonePortAdmin -const authTokenURL = keystoneURL + "/v3/auth/tokens/" -const userURL = keystoneAdminURL + "/v3/users/" -const groupURL = keystoneAdminURL + "/v3/groups/" +var ( + keystoneURL = "" + keystoneAdminURL = "" + authTokenURL = "" + usersURL = "" + groupsURL = "" +) -func startKeystoneContainer() string { - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.WithVersion(dockerCliVersion)) - - if err != nil { - fmt.Printf("Error %v", err) - return "" - } - - imageName := "openio/openstack-keystone" - out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{}) - if err != nil { - fmt.Printf("Error %v", err) - return "" - } - io.Copy(os.Stdout, out) - - resp, err := cli.ContainerCreate(ctx, &container.Config{ - Image: imageName, - }, &container.HostConfig{ - PortBindings: nat.PortMap{ - "5000/tcp": []nat.PortBinding{ - { - HostIP: "0.0.0.0", - HostPort: exposedKeystonePort, - }, - }, - "35357/tcp": []nat.PortBinding{ - { - HostIP: "0.0.0.0", - HostPort: exposedKeystonePortAdmin, - }, - }, - }, - }, &networktypes.NetworkingConfig{}, "dex_keystone_test") - - if err != nil { - fmt.Printf("Error %v", err) - return "" - } - - if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { - panic(err) - } - - fmt.Println(resp.ID) - return resp.ID +type userResponse struct { + User struct { + ID string `json:"id"` + } `json:"user"` } -func cleanKeystoneContainer(ID string) { - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.WithVersion(dockerCliVersion)) - if err != nil { - fmt.Printf("Error %v", err) - return - } - duration := time.Duration(1) - if err:= cli.ContainerStop(ctx, ID, &duration); err != nil { - fmt.Printf("Error %v", err) - return - } - if err:= cli.ContainerRemove(ctx, ID, types.ContainerRemoveOptions{}); err != nil { - fmt.Printf("Error %v", err) - } +type groupResponse struct { + Group struct { + ID string `json:"id"` + } `json:"group"` } -func getAdminToken(admin_name, admin_pass string) (token string) { +func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) { + t.Helper() client := &http.Client{} - jsonData := LoginRequestData{ - Auth: Auth{ - Identity: Identity{ - Methods:[]string{"password"}, - Password: Password{ - User: User{ - Name: admin_name, - Domain: Domain{ID: "default"}, - Password: admin_pass, + jsonData := loginRequestData{ + auth: auth{ + Identity: identity{ + Methods: []string{"password"}, + Password: password{ + User: user{ + Name: adminName, + Domain: domain{ID: testDomain}, + Password: adminPass, }, }, }, }, } - body, _ := json.Marshal(jsonData) + body, err := json.Marshal(jsonData) + if err != nil { + t.Fatal(err) + } - req, _ := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(body)) + 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, _ := client.Do(req) + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } - token = resp.Header["X-Subject-Token"][0] - return token + token = resp.Header.Get("X-Subject-Token") + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + var tokenResp = new(tokenResponse) + err = json.Unmarshal(data, &tokenResp) + if err != nil { + t.Fatal(err) + } + return token, tokenResp.Token.User.ID } -func createUser(token, user_name, user_email, user_pass string) (string){ +func createUser(t *testing.T, token, userName, userEmail, userPass string) string { + t.Helper() client := &http.Client{} - createUserData := CreateUserRequest{ - CreateUser: CreateUserForm{ - Name: user_name, - Email: user_email, - Enabled: true, - Password: user_pass, - Roles: []string{"admin"}, + createUserData := map[string]interface{}{ + "user": map[string]interface{}{ + "name": userName, + "email": userEmail, + "enabled": true, + "password": userPass, + "roles": []string{"admin"}, }, } - body, _ := json.Marshal(createUserData) - - req, _ := http.NewRequest("POST", userURL, bytes.NewBuffer(body)) - req.Header.Set("X-Auth-Token", token) - req.Header.Add("Content-Type", "application/json") - resp, _ := client.Do(req) - - data, _ := ioutil.ReadAll(resp.Body) - var userResponse = new(UserResponse) - err := json.Unmarshal(data, &userResponse) + body, err := json.Marshal(createUserData) if err != nil { - fmt.Println(err) + t.Fatal(err) } - fmt.Println(userResponse.User.ID) - return userResponse.User.ID - -} - -func deleteUser(token, id string) { - client := &http.Client{} - - deleteUserURI := userURL + id - fmt.Println(deleteUserURI) - req, _ := http.NewRequest("DELETE", deleteUserURI, nil) + req, err := http.NewRequest("POST", usersURL, bytes.NewBuffer(body)) + if err != nil { + t.Fatal(err) + } req.Header.Set("X-Auth-Token", token) - resp, _ := client.Do(req) - fmt.Println(resp) + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + var userResp = new(userResponse) + err = json.Unmarshal(data, &userResp) + if err != nil { + t.Fatal(err) + } + + return userResp.User.ID } -func createGroup(token, description, name string) string{ +// delete group or user +func delete(t *testing.T, token, id, uri string) { + t.Helper() client := &http.Client{} - createGroupData := CreateGroup{ - CreateGroupForm{ - Description: description, - Name: name, + 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) + client.Do(req) +} + +func createGroup(t *testing.T, token, description, name string) string { + t.Helper() + client := &http.Client{} + + createGroupData := map[string]interface{}{ + "group": map[string]interface{}{ + "name": name, + "description": description, }, } - body, _ := json.Marshal(createGroupData) - - req, _ := http.NewRequest("POST", groupURL, bytes.NewBuffer(body)) - req.Header.Set("X-Auth-Token", token) - req.Header.Add("Content-Type", "application/json") - resp, _ := client.Do(req) - data, _ := ioutil.ReadAll(resp.Body) - - var groupResponse = new(GroupID) - err := json.Unmarshal(data, &groupResponse) + body, err := json.Marshal(createGroupData) if err != nil { - fmt.Println(err) + t.Fatal(err) } - return groupResponse.Group.ID -} - -func addUserToGroup(token, groupId, userId string) { - uri := groupURL + groupId + "/users/" + userId - client := &http.Client{} - req, _ := http.NewRequest("PUT", uri, nil) + req, err := http.NewRequest("POST", groupsURL, bytes.NewBuffer(body)) + if err != nil { + t.Fatal(err) + } req.Header.Set("X-Auth-Token", token) - resp, _ := client.Do(req) - fmt.Println(resp) + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + var groupResp = new(groupResponse) + err = json.Unmarshal(data, &groupResp) + if err != nil { + t.Fatal(err) + } + + return groupResp.Group.ID } -const adminUser = "demo" -const adminPass = "DEMO_PASS" -const invalidPass = "WRONG_PASS" - -const testUser = "test_user" -const testPass = "test_pass" -const testEmail = "test@example.com" - -const domain = "default" +func addUserToGroup(t *testing.T, token, groupID, userID string) error { + t.Helper() + uri := groupsURL + groupID + "/users/" + userID + client := &http.Client{} + req, err := http.NewRequest("PUT", uri, nil) + if err != nil { + return err + } + req.Header.Set("X-Auth-Token", token) + client.Do(req) + return nil +} func TestIncorrectCredentialsLogin(t *testing.T) { - c := Connector{KeystoneHost: keystoneURL, Domain: domain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} - s := connector.Scopes{OfflineAccess: true, Groups: true} - _, validPW, _ := c.Login(context.Background(), s, adminUser, invalidPass) + c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, + KeystoneUsername: adminUser, KeystonePassword: adminPass} + s := connector.Scopes{OfflineAccess: true, Groups: true} + _, validPW, err := c.Login(context.Background(), s, adminUser, invalidPass) + if err != nil { + t.Fatal(err.Error()) + } - if validPW { - t.Fail() - } + if validPW { + t.Fail() + } } func TestValidUserLogin(t *testing.T) { - token := getAdminToken(adminUser, adminPass) - userID := createUser(token, testUser, testEmail, testPass) - c := Connector{KeystoneHost: keystoneURL, Domain: domain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} - s := connector.Scopes{OfflineAccess: true, Groups: true} - _, validPW, _ := c.Login(context.Background(), s, testUser, testPass) - if !validPW { - t.Fail() - } - deleteUser(token, userID) + token, _ := getAdminToken(t, adminUser, adminPass) + userID := createUser(t, token, testUser, testEmail, testPass) + c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, + KeystoneUsername: adminUser, KeystonePassword: adminPass} + s := connector.Scopes{OfflineAccess: true, Groups: true} + identity, validPW, err := c.Login(context.Background(), s, testUser, testPass) + if err != nil { + t.Fatal(err.Error()) + } + t.Log(identity) + + if !validPW { + t.Fail() + } + delete(t, token, userID, usersURL) } func TestUseRefreshToken(t *testing.T) { - t.Fatal("Not implemented") + token, adminID := getAdminToken(t, adminUser, adminPass) + groupID := createGroup(t, token, "Test group description", testGroup) + addUserToGroup(t, token, groupID, adminID) + + c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, + KeystoneUsername: adminUser, KeystonePassword: adminPass} + 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()) + } + + delete(t, token, groupID, groupsURL) + + expectEquals(t, 1, len(identityRefresh.Groups)) + expectEquals(t, testGroup, string(identityRefresh.Groups[0])) } -func TestUseRefreshTokenUserDeleted(t *testing.T){ - t.Fatal("Not implemented") +func TestUseRefreshTokenUserDeleted(t *testing.T) { + token, _ := getAdminToken(t, adminUser, adminPass) + userID := createUser(t, token, testUser, testEmail, testPass) + + c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, + KeystoneUsername: adminUser, KeystonePassword: adminPass} + 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()) + } + + delete(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){ - t.Fatal("Not implemented") +func TestUseRefreshTokenGroupsChanged(t *testing.T) { + token, _ := getAdminToken(t, adminUser, adminPass) + userID := createUser(t, token, testUser, testEmail, testPass) + + c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, + KeystoneUsername: adminUser, KeystonePassword: adminPass} + 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 description", testGroup) + addUserToGroup(t, token, groupID, userID) + + identityRefresh, err = c.Refresh(context.Background(), s, identityLogin) + if err != nil { + t.Fatal(err.Error()) + } + + delete(t, token, groupID, groupsURL) + delete(t, token, userID, usersURL) + + expectEquals(t, 1, len(identityRefresh.Groups)) } func TestMain(m *testing.M) { - dockerID := startKeystoneContainer() - repeats := 10 - running := false - for i := 0; i < repeats; i++ { - _, err := http.Get(keystoneURL) - if err == nil { - running = true - break - } - time.Sleep(10 * time.Second) - } - if !running { - fmt.Printf("Failed to start keystone container") - os.Exit(1) - } - defer cleanKeystoneContainer(dockerID) - // run all tests + keystoneURLEnv := "DEX_KEYSTONE_URL" + keystoneAdminURLEnv := "DEX_KEYSTONE_ADMIN_URL" + keystoneURL = os.Getenv(keystoneURLEnv) + if keystoneURL == "" { + fmt.Printf("variable %q not set, skipping keystone connector tests\n", keystoneURLEnv) + return + } + keystoneAdminURL := os.Getenv(keystoneAdminURLEnv) + if keystoneAdminURL == "" { + fmt.Printf("variable %q not set, skipping keystone connector tests\n", keystoneAdminURLEnv) + return + } + authTokenURL = keystoneURL + "/v3/auth/tokens/" + fmt.Printf("Auth token url %q\n", authTokenURL) + fmt.Printf("Keystone URL %q\n", keystoneURL) + usersURL = keystoneAdminURL + "/v3/users/" + groupsURL = keystoneAdminURL + "/v3/groups/" + // run all tests m.Run() } + +func expectEquals(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(a, b) { + t.Errorf("Expected %v to be equal %v", a, b) + } +} diff --git a/connector/keystone/types.go b/connector/keystone/types.go index 9868a815..fe6b67ae 100644 --- a/connector/keystone/types.go +++ b/connector/keystone/types.go @@ -4,133 +4,84 @@ import ( "github.com/sirupsen/logrus" ) -type Connector struct { - Domain string - KeystoneHost string +type keystoneConnector struct { + Domain string + KeystoneHost string KeystoneUsername string KeystonePassword string - Logger logrus.FieldLogger + Logger logrus.FieldLogger } -type ConnectorData struct { - AccessToken string `json:"accessToken"` +type userKeystone struct { + Domain domainKeystone `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` } -type KeystoneUser struct { - Domain KeystoneDomain `json:"domain"` - ID string `json:"id"` - Name string `json:"name"` -} - -type KeystoneDomain struct { - ID string `json:"id"` +type domainKeystone struct { + ID string `json:"id"` Name string `json:"name"` } +// Config holds the configuration parameters for Keystone connector. +// Keystone should expose API v3 +// An example config: +// connectors: +// type: keystone +// id: keystone +// name: Keystone +// config: +// keystoneHost: http://example:5000 +// domain: default +// keystoneUsername: demo +// keystonePassword: DEMO_PASS type Config struct { - Domain string `json:"domain"` - KeystoneHost string `json:"keystoneHost"` + Domain string `json:"domain"` + KeystoneHost string `json:"keystoneHost"` KeystoneUsername string `json:"keystoneUsername"` KeystonePassword string `json:"keystonePassword"` } -type LoginRequestData struct { - Auth `json:"auth"` +type loginRequestData struct { + auth `json:"auth"` } -type Auth struct { - Identity `json:"identity"` +type auth struct { + Identity identity `json:"identity"` } -type Identity struct { +type identity struct { Methods []string `json:"methods"` - Password `json:"password"` + Password password `json:"password"` } -type Password struct { - User `json:"user"` +type password struct { + User user `json:"user"` } -type User struct { - Name string `json:"name"` - Domain `json:"domain"` - Password string `json:"password"` -} - -type Domain struct { - ID string `json:"id"` -} - -type Token struct { - IssuedAt string `json:"issued_at"` - Extras map[string]interface{} `json:"extras"` - Methods []string `json:"methods"` - ExpiresAt string `json:"expires_at"` - User KeystoneUser `json:"user"` -} - -type TokenResponse struct { - Token Token `json:"token"` -} - -type CreateUserRequest struct { - CreateUser CreateUserForm `json:"user"` -} - -type CreateUserForm struct { +type user struct { Name string `json:"name"` - Email string `json:"email"` - Enabled bool `json:"enabled"` + Domain domain `json:"domain"` Password string `json:"password"` - Roles []string `json:"roles"` } -type UserResponse struct { - User CreateUserResponse `json:"user"` -} - -type CreateUserResponse struct { - Username string `json:"username"` - Name string `json:"name"` - Roles []string `json:"roles"` - Enabled bool `json:"enabled"` - Options string `json:"options"` - ID string `json:"id"` - Email string `json:"email"` -} - -type CreateGroup struct { - Group CreateGroupForm `json:"group"` -} - -type CreateGroupForm struct { - Description string `json:"description"` - Name string `json:"name"` -} - -type GroupID struct { - Group GroupIDForm `json:"group"` -} - -type GroupIDForm struct { +type domain struct { ID string `json:"id"` } -type Links struct { - Self string `json:"self"` - Previous string `json:"previous"` - Next string `json:"next"` +type token struct { + User userKeystone `json:"user"` } -type Group struct { - DomainID string `json:"domain_id` - Description string `json:"description"` - ID string `json:"id"` - Links Links `json:"links"` - Name string `json:"name"` +type tokenResponse struct { + Token token `json:"token"` } -type GroupsResponse struct { - Links Links `json:"links"` - Groups []Group `json:"groups"` +type group struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type groupsResponse struct { + Groups []group `json:"groups"` } diff --git a/examples/config-keystone.yaml b/examples/config-keystone.yaml deleted file mode 100644 index 9d5dfdf7..00000000 --- a/examples/config-keystone.yaml +++ /dev/null @@ -1,55 +0,0 @@ -# The base path of dex and the external name of the OpenID Connect service. -# This is the canonical URL that all clients MUST use to refer to dex. If a -# path is provided, dex's HTTP service will listen at a non-root URL. -issuer: http://0.0.0.0:5556/dex - -# The storage configuration determines where dex stores its state. Supported -# options include SQL flavors and Kubernetes third party resources. -# -# See the storage document at Documentation/storage.md for further information. -storage: - type: sqlite3 - config: - file: examples/dex.db #be in the dex directory, else change path here - -# Configuration for the HTTP endpoints. -web: - https: 0.0.0.0:5556 - # Uncomment for HTTPS options. - # https: 127.0.0.1:5554 - tlsCert: ./ssl/dex.crt - tlsKey: ./ssl/dex.key - -# Configuration for telemetry -telemetry: - http: 0.0.0.0:5558 - -oauth2: - responseTypes: ["id_token"] - -# Instead of reading from an external storage, use this list of clients. -staticClients: -- id: example-app - redirectURIs: - - 'http://127.0.0.1:5555/callback' - name: 'Example App' - secret: ZXhhbXBsZS1hcHAtc2VjcmV0 - -#Provide Keystone connector and its config here -# /v3/auth/tokens -connectors: -- type: keystone - id: keystone - name: Keystone - config: - keystoneHost: http://localhost:5000 - domain: default - keystoneUsername: demo - keystonePassword: DEMO_PASS - -# Let dex keep a list of passwords which can be used to login to dex. -enablePasswordDB: true - -oauth2: - skipApprovalScreen: true - diff --git a/server/handlers.go b/server/handlers.go index 100f5a38..5bdf39f0 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -211,7 +211,6 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { } authReqID := r.FormValue("req") - s.logger.Errorf("Auth req id %v", authReqID) authReq, err := s.storage.GetAuthRequest(authReqID) if err != nil { @@ -346,7 +345,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") return } - s.logger.Errorf("2Failed to get auth request: %v", err) + s.logger.Errorf("Failed to get auth request: %v", err) s.renderError(w, http.StatusInternalServerError, "Database error.") return } @@ -358,7 +357,6 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) } conn, err := s.getConnector(authReq.ConnectorID) - s.logger.Errorf("X Connector %v", conn) if err != nil { s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err) s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") @@ -437,7 +435,7 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) { authReq, err := s.storage.GetAuthRequest(r.FormValue("req")) if err != nil { - s.logger.Errorf("3Failed to get auth request: %v", err) + s.logger.Errorf("Failed to get auth request: %v", err) s.renderError(w, http.StatusInternalServerError, "Database error.") return } diff --git a/server/server.go b/server/server.go index 518200cd..ee3355b5 100644 --- a/server/server.go +++ b/server/server.go @@ -27,6 +27,7 @@ import ( "github.com/dexidp/dex/connector/bitbucketcloud" "github.com/dexidp/dex/connector/github" "github.com/dexidp/dex/connector/gitlab" + "github.com/dexidp/dex/connector/keystone" "github.com/dexidp/dex/connector/ldap" "github.com/dexidp/dex/connector/linkedin" "github.com/dexidp/dex/connector/microsoft" @@ -34,7 +35,6 @@ import ( "github.com/dexidp/dex/connector/oidc" "github.com/dexidp/dex/connector/saml" "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/connector/keystone" ) // LocalConnector is the local passwordDB connector which is an internal @@ -456,7 +456,7 @@ func openConnector(logger logrus.FieldLogger, conn storage.Connector) (connector f, ok := ConnectorsConfig[conn.Type] if !ok { - return c, fmt.Errorf("xunknown connector type %q", conn.Type) + return c, fmt.Errorf("unknown connector type %q", conn.Type) } connConfig := f() diff --git a/storage/static.go b/storage/static.go index abf0ab7f..5ae4f783 100644 --- a/storage/static.go +++ b/storage/static.go @@ -3,6 +3,7 @@ package storage import ( "errors" "strings" + "github.com/sirupsen/logrus" )