From 0774a89066f1ea0f73b81bbfecea1255298a3145 Mon Sep 17 00:00:00 2001 From: knangia Date: Mon, 26 Nov 2018 15:51:03 +0100 Subject: [PATCH 1/4] keystone: squashed changes from knangia/dex --- Dockerfile | 9 +-- connector/connector.go | 1 + connector/keystone/keystone.go | 120 +++++++++++++++++++++++++++++++++ examples/config-keystone.yaml | 44 ++++++++++++ server/server.go | 2 + storage/static.go | 1 - 6 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 connector/keystone/keystone.go create mode 100644 examples/config-keystone.yaml diff --git a/Dockerfile b/Dockerfile index dbc0dd38..d6ce6a9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,15 +11,12 @@ 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 - -COPY --from=0 /go/bin/dex /usr/local/bin/dex +RUN apk add --update ca-certificates openssl bash # Import frontend assets and set the correct CWD directory so the assets # are in the default path. COPY web /web WORKDIR / -ENTRYPOINT ["dex"] - -CMD ["version"] +EXPOSE 5500-5600 +CMD ["bash"] diff --git a/connector/connector.go b/connector/connector.go index c442c54a..0335ea94 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -35,6 +35,7 @@ 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 new file mode 100644 index 00000000..43a03a22 --- /dev/null +++ b/connector/keystone/keystone.go @@ -0,0 +1,120 @@ +// Package keystone provides authentication strategy using Keystone. +package keystone + +import ( + "context" + "fmt" + "github.com/dexidp/dex/connector" + "github.com/sirupsen/logrus" + "encoding/json" + "net/http" + "bytes" + "io/ioutil" + "log" +) + +type KeystoneConnector struct { + domain string + keystoneURI string + Logger logrus.FieldLogger +} + +var ( + _ connector.PasswordConnector = &KeystoneConnector{} +) + +// Config holds the configuration parameters for Keystone connector. +// An example config: +// connectors: +// type: ksconfig +// id: keystone +// name: Keystone +// config: +// keystoneURI: http://example:5000/v3/auth/tokens +// domain: default + +type Config struct { + Domain string `json:"domain"` + KeystoneURI string `json:"keystoneURI"` +} + +// Open returns an authentication strategy using Keystone. +func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { + return &KeystoneConnector{c.Domain,c.KeystoneURI,logger}, nil +} + +func (p KeystoneConnector) Close() error { return nil } + +// Declare KeystoneJson struct to get a token +type KeystoneJson struct { + Auth `json:"auth"` +} + +type Auth struct { + Identity `json:"identity"` +} + +type Identity struct { + Methods []string `json:"methods"` + Password `json:"password"` +} + +type Password struct { + User `json:"user"` +} + +type User struct { + Name string `json:"name"` + Domain `json:"domain"` + Password string `json:"password"` +} + +type Domain struct { + ID string `json:"id"` +} + +func (p KeystoneConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (identity connector.Identity, validPassword bool, err error) { + // Instantiate KeystoneJson struct type to get a token + jsonData := KeystoneJson{ + Auth: Auth{ + Identity: Identity{ + Methods:[]string{"password"}, + Password: Password{ + User: User{ + Name: username, + Domain: Domain{ID:p.domain}, + Password: password, + }, + }, + }, + }, + } + + // Marshal jsonData + jsonValue, _ := json.Marshal(jsonData) + + // Make an http post request to Keystone URI + response, err := http.Post(p.keystoneURI, "application/json", bytes.NewBuffer(jsonValue)) + + // Providing wrong password or wrong keystone URI throws error + if err == nil && response.StatusCode == 201 { + data, _ := ioutil.ReadAll(response.Body) + fmt.Println(string(data)) + identity.Username = username + return identity, true, nil + + } else if err != nil { + log.Fatal(err) + return identity, false, err + + } else { + fmt.Printf("The HTTP request failed with error %v\n", response.StatusCode) + data, _ := ioutil.ReadAll(response.Body) + fmt.Println(string(data)) + return identity, false, err + + } + return identity, false, nil +} + +func (p KeystoneConnector) Prompt() string { return "username" } diff --git a/examples/config-keystone.yaml b/examples/config-keystone.yaml new file mode 100644 index 00000000..22a00b08 --- /dev/null +++ b/examples/config-keystone.yaml @@ -0,0 +1,44 @@ +# 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: + http: 0.0.0.0:5556 + +# 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 +connectors: +- type: ksconfig + id: keystone + name: Keystone + config: + keystoneURI: http://example:5000/v3/auth/tokens + domain: default + +# Let dex keep a list of passwords which can be used to login to dex. +enablePasswordDB: true \ No newline at end of file diff --git a/server/server.go b/server/server.go index cf9f7b47..06869ebf 100644 --- a/server/server.go +++ b/server/server.go @@ -34,6 +34,7 @@ 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 @@ -433,6 +434,7 @@ type ConnectorConfig interface { // ConnectorsConfig variable provides an easy way to return a config struct // depending on the connector type. var ConnectorsConfig = map[string]func() ConnectorConfig{ + "ksconfig": func() ConnectorConfig { return new(keystone.Config) }, "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, "ldap": func() ConnectorConfig { return new(ldap.Config) }, diff --git a/storage/static.go b/storage/static.go index 5ae4f783..abf0ab7f 100644 --- a/storage/static.go +++ b/storage/static.go @@ -3,7 +3,6 @@ package storage import ( "errors" "strings" - "github.com/sirupsen/logrus" ) From a965365a2b6716d8486251d0f3d8ebe50de4a221 Mon Sep 17 00:00:00 2001 From: Krzysztof Balka Date: Tue, 27 Nov 2018 11:28:46 +0100 Subject: [PATCH 2/4] keystone: refresh token and groups --- connector/keystone/keystone.go | 200 ++++++++++++-------- connector/keystone/keystone_test.go | 275 ++++++++++++++++++++++++++++ connector/keystone/types.go | 136 ++++++++++++++ examples/config-keystone.yaml | 19 +- server/handlers.go | 6 +- server/server.go | 4 +- 6 files changed, 554 insertions(+), 86 deletions(-) create mode 100644 connector/keystone/keystone_test.go create mode 100644 connector/keystone/types.go diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go index 43a03a22..d62aa179 100644 --- a/connector/keystone/keystone.go +++ b/connector/keystone/keystone.go @@ -10,111 +10,155 @@ import ( "net/http" "bytes" "io/ioutil" - "log" ) -type KeystoneConnector struct { - domain string - keystoneURI string - Logger logrus.FieldLogger -} - var ( - _ connector.PasswordConnector = &KeystoneConnector{} + _ connector.PasswordConnector = &Connector{} + _ connector.RefreshConnector = &Connector{} ) -// Config holds the configuration parameters for Keystone connector. -// An example config: -// connectors: -// type: ksconfig -// id: keystone -// name: Keystone -// config: -// keystoneURI: http://example:5000/v3/auth/tokens -// domain: default - -type Config struct { - Domain string `json:"domain"` - KeystoneURI string `json:"keystoneURI"` -} - // Open returns an authentication strategy using Keystone. func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { - return &KeystoneConnector{c.Domain,c.KeystoneURI,logger}, nil + return &Connector{c.Domain, c.KeystoneHost, + c.KeystoneUsername, c.KeystonePassword, logger}, nil } -func (p KeystoneConnector) Close() error { return nil } +func (p Connector) Close() error { return nil } -// Declare KeystoneJson struct to get a token -type KeystoneJson struct { - Auth `json:"auth"` +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) + + // 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) + + var tokenResponse = new(TokenResponse) + err := json.Unmarshal(data, &tokenResponse) + + 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 + 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 } -type Auth struct { - Identity `json:"identity"` +func (p Connector) Prompt() string { return "username" } + +func (p Connector) 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() + + if err != nil { + fmt.Printf("keystone: failed to obtain admin token") + 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) + return identity, nil } -type Identity struct { - Methods []string `json:"methods"` - Password `json:"password"` -} -type Password struct { - User `json:"user"` -} - -type User struct { - Name string `json:"name"` - Domain `json:"domain"` - Password string `json:"password"` -} - -type Domain struct { - ID string `json:"id"` -} - -func (p KeystoneConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (identity connector.Identity, validPassword bool, err error) { - // Instantiate KeystoneJson struct type to get a token - jsonData := KeystoneJson{ +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}, + Domain: Domain{ID:p.Domain}, Password: password, }, }, }, }, } - - // Marshal jsonData jsonValue, _ := json.Marshal(jsonData) - - // Make an http post request to Keystone URI - response, err := http.Post(p.keystoneURI, "application/json", bytes.NewBuffer(jsonValue)) - - // Providing wrong password or wrong keystone URI throws error - if err == nil && response.StatusCode == 201 { - data, _ := ioutil.ReadAll(response.Body) - fmt.Println(string(data)) - identity.Username = username - return identity, true, nil - - } else if err != nil { - log.Fatal(err) - return identity, false, err - - } else { - fmt.Printf("The HTTP request failed with error %v\n", response.StatusCode) - data, _ := ioutil.ReadAll(response.Body) - fmt.Println(string(data)) - return identity, false, err - - } - return identity, false, nil + loginURI := p.KeystoneHost + "/v3/auth/tokens" + return http.Post(loginURI, "application/json", bytes.NewBuffer(jsonValue)) } -func (p KeystoneConnector) Prompt() string { return "username" } +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 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 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) + + 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 +} diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go new file mode 100644 index 00000000..b8bba0fe --- /dev/null +++ b/connector/keystone/keystone_test.go @@ -0,0 +1,275 @@ +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" + "encoding/json" + "io/ioutil" +) + +const dockerCliVersion = "1.37" + +const exposedKeystonePort = "5000" +const exposedKeystonePortAdmin = "35357" + +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/" + +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 +} + +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) + } +} + +func getAdminToken(admin_name, admin_pass string) (token string) { + 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, + }, + }, + }, + }, + } + + body, _ := json.Marshal(jsonData) + + req, _ := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(body)) + + req.Header.Set("Content-Type", "application/json") + resp, _ := client.Do(req) + + token = resp.Header["X-Subject-Token"][0] + return token +} + +func createUser(token, user_name, user_email, user_pass string) (string){ + client := &http.Client{} + + createUserData := CreateUserRequest{ + CreateUser: CreateUserForm{ + Name: user_name, + Email: user_email, + Enabled: true, + Password: user_pass, + 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) + if err != nil { + fmt.Println(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.Header.Set("X-Auth-Token", token) + resp, _ := client.Do(req) + fmt.Println(resp) +} + +func createGroup(token, description, name string) string{ + client := &http.Client{} + + createGroupData := CreateGroup{ + CreateGroupForm{ + Description: description, + Name: name, + }, + } + + 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) + if err != nil { + fmt.Println(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.Header.Set("X-Auth-Token", token) + resp, _ := client.Do(req) + fmt.Println(resp) +} + +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 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) + + 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) +} + +func TestUseRefreshToken(t *testing.T) { + t.Fatal("Not implemented") +} + +func TestUseRefreshTokenUserDeleted(t *testing.T){ + t.Fatal("Not implemented") +} + +func TestUseRefreshTokenGroupsChanged(t *testing.T){ + t.Fatal("Not implemented") +} + +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 + m.Run() +} diff --git a/connector/keystone/types.go b/connector/keystone/types.go new file mode 100644 index 00000000..9868a815 --- /dev/null +++ b/connector/keystone/types.go @@ -0,0 +1,136 @@ +package keystone + +import ( + "github.com/sirupsen/logrus" +) + +type Connector struct { + Domain string + KeystoneHost string + KeystoneUsername string + KeystonePassword string + Logger logrus.FieldLogger +} + +type ConnectorData struct { + AccessToken string `json:"accessToken"` +} + +type KeystoneUser struct { + Domain KeystoneDomain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +type KeystoneDomain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type Config struct { + Domain string `json:"domain"` + KeystoneHost string `json:"keystoneHost"` + KeystoneUsername string `json:"keystoneUsername"` + KeystonePassword string `json:"keystonePassword"` +} + +type LoginRequestData struct { + Auth `json:"auth"` +} + +type Auth struct { + Identity `json:"identity"` +} + +type Identity struct { + Methods []string `json:"methods"` + Password `json:"password"` +} + +type Password struct { + 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 { + Name string `json:"name"` + Email string `json:"email"` + Enabled bool `json:"enabled"` + 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 { + ID string `json:"id"` +} + +type Links struct { + Self string `json:"self"` + Previous string `json:"previous"` + Next string `json:"next"` +} + +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 GroupsResponse struct { + Links Links `json:"links"` + Groups []Group `json:"groups"` +} diff --git a/examples/config-keystone.yaml b/examples/config-keystone.yaml index 22a00b08..9d5dfdf7 100644 --- a/examples/config-keystone.yaml +++ b/examples/config-keystone.yaml @@ -14,7 +14,11 @@ storage: # Configuration for the HTTP endpoints. web: - http: 0.0.0.0:5556 + 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: @@ -32,13 +36,20 @@ staticClients: secret: ZXhhbXBsZS1hcHAtc2VjcmV0 #Provide Keystone connector and its config here +# /v3/auth/tokens connectors: -- type: ksconfig +- type: keystone id: keystone name: Keystone config: - keystoneURI: http://example:5000/v3/auth/tokens + 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 \ No newline at end of file +enablePasswordDB: true + +oauth2: + skipApprovalScreen: true + diff --git a/server/handlers.go b/server/handlers.go index 5bdf39f0..100f5a38 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -211,6 +211,7 @@ 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 { @@ -345,7 +346,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("Failed to get auth request: %v", err) + s.logger.Errorf("2Failed to get auth request: %v", err) s.renderError(w, http.StatusInternalServerError, "Database error.") return } @@ -357,6 +358,7 @@ 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.") @@ -435,7 +437,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("Failed to get auth request: %v", err) + s.logger.Errorf("3Failed 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 06869ebf..518200cd 100644 --- a/server/server.go +++ b/server/server.go @@ -434,7 +434,7 @@ type ConnectorConfig interface { // ConnectorsConfig variable provides an easy way to return a config struct // depending on the connector type. var ConnectorsConfig = map[string]func() ConnectorConfig{ - "ksconfig": func() ConnectorConfig { return new(keystone.Config) }, + "keystone": func() ConnectorConfig { return new(keystone.Config) }, "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, "ldap": func() ConnectorConfig { return new(ldap.Config) }, @@ -456,7 +456,7 @@ func openConnector(logger logrus.FieldLogger, conn storage.Connector) (connector f, ok := ConnectorsConfig[conn.Type] if !ok { - return c, fmt.Errorf("unknown connector type %q", conn.Type) + return c, fmt.Errorf("xunknown connector type %q", conn.Type) } connConfig := f() From 88d1e2b041a24ea85e9d3fcf7e7b7c19c908083e Mon Sep 17 00:00:00 2001 From: joannano Date: Thu, 13 Dec 2018 12:22:53 +0100 Subject: [PATCH 3/4] 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" ) From e8ba848907a656d32b856c7a2d892dc96de0fca1 Mon Sep 17 00:00:00 2001 From: Krzysztof Balka Date: Thu, 20 Dec 2018 17:25:22 +0100 Subject: [PATCH 4/4] keystone: fetching groups only if requested, refactoring. --- .travis.yml | 10 +- connector/keystone/keystone.go | 182 ++++++++++++++++++++-------- connector/keystone/keystone_test.go | 98 +++++++++++---- connector/keystone/types.go | 87 ------------- 4 files changed, 212 insertions(+), 165 deletions(-) delete mode 100644 connector/keystone/types.go diff --git a/.travis.yml b/.travis.yml index 07b941bb..c5272a0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,18 @@ 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_KEYSTONE_URL=http://localhost:5000 DEX_KEYSTONE_ADMIN_URL=http://localhost:35357 + - 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 DEX_KEYSTONE_ADMIN_USER=demo DEX_KEYSTONE_ADMIN_PASS=DEMO_PASS 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 + - docker run -d -p 0.0.0.0:5000:5000 -p 0.0.0.0:35357:35357 openio/openstack-keystone:pike + - | + until curl --fail http://localhost:5000/v3; do + echo 'Waiting for keystone...' + sleep 1; + done; script: - make testall diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go index 8abff2b4..a86e957d 100644 --- a/connector/keystone/keystone.go +++ b/connector/keystone/keystone.go @@ -14,65 +14,148 @@ import ( "github.com/dexidp/dex/connector" ) +type conn struct { + Domain string + Host string + AdminUsername string + AdminPassword string + Logger logrus.FieldLogger +} + +type userKeystone struct { + Domain domainKeystone `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +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"` + Host string `json:"keystoneHost"` + AdminUsername string `json:"keystoneUsername"` + AdminPassword string `json:"keystonePassword"` +} + +type loginRequestData struct { + auth `json:"auth"` +} + +type auth struct { + Identity identity `json:"identity"` +} + +type identity struct { + Methods []string `json:"methods"` + Password password `json:"password"` +} + +type password struct { + User user `json:"user"` +} + +type user struct { + Name string `json:"name"` + Domain domain `json:"domain"` + Password string `json:"password"` +} + +type domain struct { + ID string `json:"id"` +} + +type token struct { + User userKeystone `json:"user"` +} + +type tokenResponse struct { + Token token `json:"token"` +} + +type group struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type groupsResponse struct { + Groups []group `json:"groups"` +} + var ( - _ connector.PasswordConnector = &keystoneConnector{} - _ connector.RefreshConnector = &keystoneConnector{} + _ connector.PasswordConnector = &conn{} + _ connector.RefreshConnector = &conn{} ) // Open returns an authentication strategy using Keystone. func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) { - return &keystoneConnector{c.Domain, c.KeystoneHost, - c.KeystoneUsername, c.KeystonePassword, logger}, nil + return &conn{ + c.Domain, + c.Host, + c.AdminUsername, + c.AdminPassword, + logger}, nil } -func (p *keystoneConnector) Close() error { return nil } +func (p *conn) Close() error { return nil } -func (p *keystoneConnector) Login(ctx context.Context, s connector.Scopes, username, password string) ( - identity connector.Identity, validPassword bool, err error) { +func (p *conn) Login(ctx context.Context, scopes 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 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 tokenResp = new(tokenResponse) - err = json.Unmarshal(data, &tokenResp) - if err != nil { - return identity, false, fmt.Errorf("keystone: invalid token response: %v", err) - } + if resp.StatusCode/100 != 2 { + return identity, false, fmt.Errorf("keystone login: error %v", resp.StatusCode) + } + if resp.StatusCode != 201 { + return identity, false, nil + } + 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 tokenResp = new(tokenResponse) + err = json.Unmarshal(data, &tokenResp) + if err != nil { + return identity, false, fmt.Errorf("keystone: invalid token response: %v", err) + } + if scopes.Groups { groups, err := p.getUserGroups(ctx, tokenResp.Token.User.ID, token) if err != nil { return identity, false, err } - - identity.Username = username - identity.UserID = tokenResp.Token.User.ID identity.Groups = groups - return identity, true, nil - } - - return identity, false, nil + identity.Username = username + identity.UserID = tokenResp.Token.User.ID + return identity, true, nil } -func (p *keystoneConnector) Prompt() string { return "username" } +func (p *conn) Prompt() string { return "username" } -func (p *keystoneConnector) Refresh( - ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) { +func (p *conn) Refresh( + ctx context.Context, scopes connector.Scopes, identity connector.Identity) (connector.Identity, error) { token, err := p.getAdminToken(ctx) if err != nil { return identity, fmt.Errorf("keystone: failed to obtain admin token: %v", err) } - ok, err := p.checkIfUserExists(ctx, identity.UserID, token) if err != nil { return identity, err @@ -80,17 +163,17 @@ func (p *keystoneConnector) Refresh( if !ok { return identity, fmt.Errorf("keystone: user %q does not exist", identity.UserID) } - - groups, err := p.getUserGroups(ctx, identity.UserID, token) - if err != nil { - return identity, err + if scopes.Groups { + groups, err := p.getUserGroups(ctx, identity.UserID, token) + if err != nil { + return identity, err + } + identity.Groups = groups } - - identity.Groups = groups return identity, nil } -func (p *keystoneConnector) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) { +func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) { client := &http.Client{} jsonData := loginRequestData{ auth: auth{ @@ -110,8 +193,8 @@ func (p *keystoneConnector) getTokenResponse(ctx context.Context, username, pass if err != nil { return nil, err } - - authTokenURL := p.KeystoneHost + "/v3/auth/tokens/" + // https://developer.openstack.org/api-ref/identity/v3/#password-authentication-with-unscoped-authorization + authTokenURL := p.Host + "/v3/auth/tokens/" req, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(jsonValue)) if err != nil { return nil, err @@ -123,8 +206,8 @@ func (p *keystoneConnector) getTokenResponse(ctx context.Context, username, pass return client.Do(req) } -func (p *keystoneConnector) getAdminToken(ctx context.Context) (string, error) { - resp, err := p.getTokenResponse(ctx, p.KeystoneUsername, p.KeystonePassword) +func (p *conn) getAdminToken(ctx context.Context) (string, error) { + resp, err := p.getTokenResponse(ctx, p.AdminUsername, p.AdminPassword) if err != nil { return "", err } @@ -132,8 +215,9 @@ func (p *keystoneConnector) getAdminToken(ctx context.Context) (string, error) { return token, nil } -func (p *keystoneConnector) checkIfUserExists(ctx context.Context, userID string, token string) (bool, error) { - userURL := p.KeystoneHost + "/v3/users/" + userID +func (p *conn) checkIfUserExists(ctx context.Context, userID string, token string) (bool, error) { + // https://developer.openstack.org/api-ref/identity/v3/#show-user-details + userURL := p.Host + "/v3/users/" + userID client := &http.Client{} req, err := http.NewRequest("GET", userURL, nil) if err != nil { @@ -153,10 +237,10 @@ func (p *keystoneConnector) checkIfUserExists(ctx context.Context, userID string return false, err } -func (p *keystoneConnector) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) { +func (p *conn) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) { client := &http.Client{} - groupsURL := p.KeystoneHost + "/v3/users/" + userID + "/groups" - + // https://developer.openstack.org/api-ref/identity/v3/#list-groups-to-which-a-user-belongs + groupsURL := p.Host + "/v3/users/" + userID + "/groups" req, err := http.NewRequest("GET", groupsURL, nil) req.Header.Set("X-Auth-Token", token) req = req.WithContext(ctx) diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go index 0c40888e..d5d65ef1 100644 --- a/connector/keystone/keystone_test.go +++ b/connector/keystone/keystone_test.go @@ -16,8 +16,6 @@ import ( ) const ( - adminUser = "demo" - adminPass = "DEMO_PASS" invalidPass = "WRONG_PASS" testUser = "test_user" @@ -30,6 +28,8 @@ const ( var ( keystoneURL = "" keystoneAdminURL = "" + adminUser = "" + adminPass = "" authTokenURL = "" usersURL = "" groupsURL = "" @@ -213,24 +213,31 @@ func addUserToGroup(t *testing.T, token, groupID, userID string) error { } func TestIncorrectCredentialsLogin(t *testing.T) { - c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} + setupVariables(t) + c := conn{Host: keystoneURL, Domain: testDomain, + AdminUsername: adminUser, AdminPassword: 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() + 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) userID := createUser(t, token, testUser, testEmail, testPass) - c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} + c := conn{Host: keystoneURL, Domain: testDomain, + AdminUsername: adminUser, AdminPassword: adminPass} s := connector.Scopes{OfflineAccess: true, Groups: true} identity, validPW, err := c.Login(context.Background(), s, testUser, testPass) if err != nil { @@ -239,18 +246,19 @@ func TestValidUserLogin(t *testing.T) { t.Log(identity) if !validPW { - t.Fail() + t.Fatal("Valid password was not accepted") } delete(t, token, userID, usersURL) } 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) - c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} + c := conn{Host: keystoneURL, Domain: testDomain, + AdminUsername: adminUser, AdminPassword: adminPass} s := connector.Scopes{OfflineAccess: true, Groups: true} identityLogin, _, err := c.Login(context.Background(), s, adminUser, adminPass) @@ -270,11 +278,12 @@ func TestUseRefreshToken(t *testing.T) { } func TestUseRefreshTokenUserDeleted(t *testing.T) { + setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) userID := createUser(t, token, testUser, testEmail, testPass) - c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} + c := conn{Host: keystoneURL, Domain: testDomain, + AdminUsername: adminUser, AdminPassword: adminPass} s := connector.Scopes{OfflineAccess: true, Groups: true} identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass) @@ -296,11 +305,12 @@ func TestUseRefreshTokenUserDeleted(t *testing.T) { } func TestUseRefreshTokenGroupsChanged(t *testing.T) { + setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) userID := createUser(t, token, testUser, testEmail, testPass) - c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain, - KeystoneUsername: adminUser, KeystonePassword: adminPass} + c := conn{Host: keystoneURL, Domain: testDomain, + AdminUsername: adminUser, AdminPassword: adminPass} s := connector.Scopes{OfflineAccess: true, Groups: true} identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass) @@ -315,7 +325,7 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) { expectEquals(t, 0, len(identityRefresh.Groups)) - groupID := createGroup(t, token, "Test group description", testGroup) + groupID := createGroup(t, token, "Test group", testGroup) addUserToGroup(t, token, groupID, userID) identityRefresh, err = c.Refresh(context.Background(), s, identityLogin) @@ -329,26 +339,62 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) { expectEquals(t, 1, len(identityRefresh.Groups)) } -func TestMain(m *testing.M) { +func TestNoGroupsInScope(t *testing.T) { + setupVariables(t) + token, _ := getAdminToken(t, adminUser, adminPass) + userID := createUser(t, token, testUser, testEmail, testPass) + + c := conn{Host: keystoneURL, Domain: testDomain, + AdminUsername: adminUser, AdminPassword: adminPass} + s := connector.Scopes{OfflineAccess: true, Groups: false} + + groupID := createGroup(t, token, "Test group", testGroup) + addUserToGroup(t, token, groupID, userID) + + 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)) + + delete(t, token, groupID, groupsURL) + delete(t, token, userID, usersURL) +} + +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 == "" { - fmt.Printf("variable %q not set, skipping keystone connector tests\n", keystoneURLEnv) + t.Skip(fmt.Sprintf("variable %q not set, skipping keystone connector tests\n", keystoneURLEnv)) return } - keystoneAdminURL := os.Getenv(keystoneAdminURLEnv) + keystoneAdminURL = os.Getenv(keystoneAdminURLEnv) if keystoneAdminURL == "" { - fmt.Printf("variable %q not set, skipping keystone connector tests\n", keystoneAdminURLEnv) + t.Skip(fmt.Sprintf("variable %q not set, skipping keystone connector tests\n", keystoneAdminURLEnv)) + return + } + adminUser = os.Getenv(keystoneAdminUserEnv) + if adminUser == "" { + t.Skip(fmt.Sprintf("variable %q not set, skipping keystone connector tests\n", keystoneAdminUserEnv)) + return + } + adminPass = os.Getenv(keystoneAdminPassEnv) + if adminPass == "" { + t.Skip(fmt.Sprintf("variable %q not set, skipping keystone connector tests\n", keystoneAdminPassEnv)) 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{}) { diff --git a/connector/keystone/types.go b/connector/keystone/types.go deleted file mode 100644 index fe6b67ae..00000000 --- a/connector/keystone/types.go +++ /dev/null @@ -1,87 +0,0 @@ -package keystone - -import ( - "github.com/sirupsen/logrus" -) - -type keystoneConnector struct { - Domain string - KeystoneHost string - KeystoneUsername string - KeystonePassword string - Logger logrus.FieldLogger -} - -type userKeystone struct { - Domain domainKeystone `json:"domain"` - ID string `json:"id"` - Name string `json:"name"` -} - -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"` - KeystoneUsername string `json:"keystoneUsername"` - KeystonePassword string `json:"keystonePassword"` -} - -type loginRequestData struct { - auth `json:"auth"` -} - -type auth struct { - Identity identity `json:"identity"` -} - -type identity struct { - Methods []string `json:"methods"` - Password password `json:"password"` -} - -type password struct { - User user `json:"user"` -} - -type user struct { - Name string `json:"name"` - Domain domain `json:"domain"` - Password string `json:"password"` -} - -type domain struct { - ID string `json:"id"` -} - -type token struct { - User userKeystone `json:"user"` -} - -type tokenResponse struct { - Token token `json:"token"` -} - -type group struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type groupsResponse struct { - Groups []group `json:"groups"` -}