Merge pull request #1515 from flant/atlassian-crowd-connector
new connector for Atlassian Crowd
This commit is contained in:
		
							
								
								
									
										39
									
								
								Documentation/connectors/atlassian-crowd.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Documentation/connectors/atlassian-crowd.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
|  Authentication through Atlassian Crowd | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| Atlassian Crowd is a centralized identity management solution providing single sign-on and user identity. | ||||
|  | ||||
| Current connector uses request to [Crowd REST API](https://developer.atlassian.com/server/crowd/json-requests-and-responses/) endpoints: | ||||
| * `/user` - to get user-info | ||||
| * `/session` - to authenticate the user | ||||
|  | ||||
| Offline Access scope support provided with a new request to user authentication and user info endpoints.  | ||||
|  | ||||
| ## Configuration | ||||
| To start using the Atlassian Crowd connector, firstly you need to register an application in your Crowd like specified in the [docs](https://confluence.atlassian.com/crowd/adding-an-application-18579591.html). | ||||
|  | ||||
| The following is an example of a configuration for dex `examples/config-dev.yaml`: | ||||
|  | ||||
| ```yaml | ||||
| connectors: | ||||
| - type: atlassian-crowd | ||||
|   # Required field for connector id. | ||||
|   id: crowd | ||||
|   # Required field for connector name. | ||||
|   name: Crowd | ||||
|   config: | ||||
|     # Required field to connect to Crowd. | ||||
|     baseURL: https://crowd.example.com/crowd | ||||
|     # Credentials can be string literals or pulled from the environment. | ||||
|     clientID: $ATLASSIAN_CROWD_APPLICATION_ID | ||||
|     clientSecret: $ATLASSIAN_CROWD_CLIENT_SECRET | ||||
|     # Optional groups whitelist, communicated through the "groups" scope. | ||||
|     # If `groups` is omitted, all of the user's Crowd groups are returned when the groups scope is present. | ||||
|     # If `groups` is provided, this acts as a whitelist - only the user's Crowd groups that are in the configured `groups` below will go into the groups claim.   | ||||
|     # Conversely, if the user is not in any of the configured `groups`, the user will not be authenticated. | ||||
|     groups: | ||||
|     - my-group | ||||
|     # Prompt for username field. | ||||
|     usernamePrompt: Login | ||||
| ``` | ||||
							
								
								
									
										437
									
								
								connector/atlassiancrowd/atlassiancrowd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										437
									
								
								connector/atlassiancrowd/atlassiancrowd.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,437 @@ | ||||
| // Package atlassiancrowd provides authentication strategies using Atlassian Crowd. | ||||
| package atlassiancrowd | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/dexidp/dex/connector" | ||||
| 	"github.com/dexidp/dex/pkg/groups" | ||||
| 	"github.com/dexidp/dex/pkg/log" | ||||
| ) | ||||
|  | ||||
| // Config holds configuration options for Atlassian Crowd connector. | ||||
| // Crowd connectors require executing two queries, the first to find | ||||
| // the user based on the username and password given to the connector. | ||||
| // The second to use the user entry to search for groups. | ||||
| // | ||||
| // An example config: | ||||
| // | ||||
| //     type: atlassian-crowd | ||||
| //     config: | ||||
| //       baseURL: https://crowd.example.com/context | ||||
| //       clientID: applogin | ||||
| //       clientSecret: appP4$$w0rd | ||||
| //       # users can be restricted by a list of groups | ||||
| //       groups: | ||||
| //       - admin | ||||
| //       # Prompt for username field | ||||
| //       usernamePrompt: Login | ||||
| // | ||||
| type Config struct { | ||||
| 	BaseURL      string   `json:"baseURL"` | ||||
| 	ClientID     string   `json:"clientID"` | ||||
| 	ClientSecret string   `json:"clientSecret"` | ||||
| 	Groups       []string `json:"groups"` | ||||
|  | ||||
| 	// UsernamePrompt allows users to override the username attribute (displayed | ||||
| 	// in the username/password prompt). If unset, the handler will use. | ||||
| 	// "Username". | ||||
| 	UsernamePrompt string `json:"usernamePrompt"` | ||||
| } | ||||
|  | ||||
| type crowdUser struct { | ||||
| 	Key    string | ||||
| 	Name   string | ||||
| 	Active bool | ||||
| 	Email  string | ||||
| } | ||||
|  | ||||
| type crowdGroups struct { | ||||
| 	Groups []struct { | ||||
| 		Name string | ||||
| 	} `json:"groups"` | ||||
| } | ||||
|  | ||||
| type crowdAuthentication struct { | ||||
| 	Token string | ||||
| 	User  struct { | ||||
| 		Name string | ||||
| 	} `json:"user"` | ||||
| 	CreatedDate uint64 `json:"created-date"` | ||||
| 	ExpiryDate  uint64 `json:"expiry-date"` | ||||
| } | ||||
|  | ||||
| type crowdAuthenticationError struct { | ||||
| 	Reason  string | ||||
| 	Message string | ||||
| } | ||||
|  | ||||
| // Open returns a strategy for logging in through Atlassian Crowd | ||||
| func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) { | ||||
| 	if c.BaseURL == "" { | ||||
| 		return nil, fmt.Errorf("crowd: no baseURL provided for crowd connector") | ||||
| 	} | ||||
| 	return &crowdConnector{Config: *c, logger: logger}, nil | ||||
| } | ||||
|  | ||||
| type crowdConnector struct { | ||||
| 	Config | ||||
| 	logger log.Logger | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	_ connector.PasswordConnector = (*crowdConnector)(nil) | ||||
| 	_ connector.RefreshConnector  = (*crowdConnector)(nil) | ||||
| ) | ||||
|  | ||||
| type refreshData struct { | ||||
| 	Username string `json:"username"` | ||||
| } | ||||
|  | ||||
| func (c *crowdConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) { | ||||
| 	// make this check to avoid empty passwords. | ||||
| 	if password == "" { | ||||
| 		return connector.Identity{}, false, nil | ||||
| 	} | ||||
|  | ||||
| 	// We want to return a different error if the user's password is incorrect vs | ||||
| 	// if there was an error. | ||||
| 	incorrectPass := false | ||||
| 	var user crowdUser | ||||
|  | ||||
| 	client := c.crowdAPIClient() | ||||
|  | ||||
| 	if incorrectPass, err = c.authenticateWithPassword(ctx, client, username, password); err != nil { | ||||
| 		return connector.Identity{}, false, err | ||||
| 	} | ||||
|  | ||||
| 	if incorrectPass { | ||||
| 		return connector.Identity{}, false, nil | ||||
| 	} | ||||
|  | ||||
| 	if user, err = c.user(ctx, client, username); err != nil { | ||||
| 		return connector.Identity{}, false, err | ||||
| 	} | ||||
|  | ||||
| 	if ident, err = c.identityFromCrowdUser(user); err != nil { | ||||
| 		return connector.Identity{}, false, err | ||||
| 	} | ||||
|  | ||||
| 	if s.Groups { | ||||
| 		userGroups, err := c.getGroups(ctx, client, s.Groups, ident.Username) | ||||
| 		if err != nil { | ||||
| 			return connector.Identity{}, false, fmt.Errorf("crowd: failed to query groups: %v", err) | ||||
| 		} | ||||
| 		ident.Groups = userGroups | ||||
| 	} | ||||
|  | ||||
| 	if s.OfflineAccess { | ||||
| 		refresh := refreshData{Username: username} | ||||
| 		// Encode entry for following up requests such as the groups query and refresh attempts. | ||||
| 		if ident.ConnectorData, err = json.Marshal(refresh); err != nil { | ||||
| 			return connector.Identity{}, false, fmt.Errorf("crowd: marshal refresh data: %v", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return ident, true, nil | ||||
| } | ||||
|  | ||||
| func (c *crowdConnector) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) { | ||||
| 	var data refreshData | ||||
| 	if err := json.Unmarshal(ident.ConnectorData, &data); err != nil { | ||||
| 		return ident, fmt.Errorf("crowd: failed to unmarshal internal data: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	var user crowdUser | ||||
| 	client := c.crowdAPIClient() | ||||
|  | ||||
| 	user, err := c.user(ctx, client, data.Username) | ||||
| 	if err != nil { | ||||
| 		return ident, fmt.Errorf("crowd: get user %q: %v", data.Username, err) | ||||
| 	} | ||||
|  | ||||
| 	newIdent, err := c.identityFromCrowdUser(user) | ||||
| 	if err != nil { | ||||
| 		return ident, err | ||||
| 	} | ||||
| 	newIdent.ConnectorData = ident.ConnectorData | ||||
|  | ||||
| 	// If user exists, authenticate it to prolong sso session. | ||||
| 	err = c.authenticateUser(ctx, client, data.Username) | ||||
| 	if err != nil { | ||||
| 		return ident, fmt.Errorf("crowd: authenticate user: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if s.Groups { | ||||
| 		userGroups, err := c.getGroups(ctx, client, s.Groups, newIdent.Username) | ||||
| 		if err != nil { | ||||
| 			return connector.Identity{}, fmt.Errorf("crowd: failed to query groups: %v", err) | ||||
| 		} | ||||
| 		newIdent.Groups = userGroups | ||||
| 	} | ||||
| 	return newIdent, nil | ||||
| } | ||||
|  | ||||
| func (c *crowdConnector) Prompt() string { | ||||
| 	return c.UsernamePrompt | ||||
| } | ||||
|  | ||||
| func (c *crowdConnector) crowdAPIClient() *http.Client { | ||||
| 	return &http.Client{ | ||||
| 		Transport: &http.Transport{ | ||||
| 			Proxy: http.ProxyFromEnvironment, | ||||
| 			DialContext: (&net.Dialer{ | ||||
| 				Timeout:   30 * time.Second, | ||||
| 				KeepAlive: 30 * time.Second, | ||||
| 			}).DialContext, | ||||
| 			MaxIdleConns:          100, | ||||
| 			IdleConnTimeout:       90 * time.Second, | ||||
| 			TLSHandshakeTimeout:   10 * time.Second, | ||||
| 			ExpectContinueTimeout: 1 * time.Second, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // authenticateWithPassword creates a new session for user and validates a password with Crowd API | ||||
| func (c *crowdConnector) authenticateWithPassword(ctx context.Context, client *http.Client, username string, password string) (invalidPass bool, err error) { | ||||
| 	req, err := c.crowdUserManagementRequest(ctx, | ||||
| 		"POST", | ||||
| 		"/session", | ||||
| 		struct { | ||||
| 			Username string `json:"username"` | ||||
| 			Password string `json:"password"` | ||||
| 		}{Username: username, Password: password}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return false, fmt.Errorf("crowd: new auth pass api request %v", err) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return false, fmt.Errorf("crowd: api request %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	body, err := c.validateCrowdResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusCreated { | ||||
| 		var authError crowdAuthenticationError | ||||
| 		if err := json.Unmarshal(body, &authError); err != nil { | ||||
| 			return false, fmt.Errorf("unmarshal auth pass response: %d %v %q", resp.StatusCode, err, string(body)) | ||||
| 		} | ||||
|  | ||||
| 		if authError.Reason == "INVALID_USER_AUTHENTICATION" { | ||||
| 			return true, nil | ||||
| 		} | ||||
|  | ||||
| 		return false, fmt.Errorf("%s: %s", resp.Status, authError.Message) | ||||
| 	} | ||||
|  | ||||
| 	var authResponse crowdAuthentication | ||||
|  | ||||
| 	if err := json.Unmarshal(body, &authResponse); err != nil { | ||||
| 		return false, fmt.Errorf("decode auth response: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| // authenticateUser creates a new session for user without password validations with Crowd API | ||||
| func (c *crowdConnector) authenticateUser(ctx context.Context, client *http.Client, username string) error { | ||||
| 	req, err := c.crowdUserManagementRequest(ctx, | ||||
| 		"POST", | ||||
| 		"/session?validate-password=false", | ||||
| 		struct { | ||||
| 			Username string `json:"username"` | ||||
| 		}{Username: username}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("crowd: new auth api request %v", err) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("crowd: api request %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	body, err := c.validateCrowdResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusCreated { | ||||
| 		return fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
|  | ||||
| 	var authResponse crowdAuthentication | ||||
|  | ||||
| 	if err := json.Unmarshal(body, &authResponse); err != nil { | ||||
| 		return fmt.Errorf("decode auth response: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // user retrieves user info from Crowd API | ||||
| func (c *crowdConnector) user(ctx context.Context, client *http.Client, username string) (crowdUser, error) { | ||||
| 	var user crowdUser | ||||
|  | ||||
| 	req, err := c.crowdUserManagementRequest(ctx, | ||||
| 		"GET", | ||||
| 		fmt.Sprintf("/user?username=%s", username), | ||||
| 		nil, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return user, fmt.Errorf("crowd: new user api request %v", err) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return user, fmt.Errorf("crowd: api request %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	body, err := c.validateCrowdResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return user, err | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return user, fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
|  | ||||
| 	if err := json.Unmarshal(body, &user); err != nil { | ||||
| 		return user, fmt.Errorf("failed to decode response: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return user, nil | ||||
| } | ||||
|  | ||||
| // groups retrieves groups from Crowd API | ||||
| func (c *crowdConnector) groups(ctx context.Context, client *http.Client, username string) (userGroups []string, err error) { | ||||
| 	var crowdGroups crowdGroups | ||||
|  | ||||
| 	req, err := c.crowdUserManagementRequest(ctx, | ||||
| 		"GET", | ||||
| 		fmt.Sprintf("/user/group/nested?username=%s", username), | ||||
| 		nil, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("crowd: new groups api request %v", err) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("crowd: api request %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	body, err := c.validateCrowdResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
|  | ||||
| 	if err := json.Unmarshal(body, &crowdGroups); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to decode response: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	for _, group := range crowdGroups.Groups { | ||||
| 		userGroups = append(userGroups, group.Name) | ||||
| 	} | ||||
|  | ||||
| 	return userGroups, nil | ||||
| } | ||||
|  | ||||
| // identityFromCrowdUser converts crowdUser to Identity | ||||
| func (c *crowdConnector) identityFromCrowdUser(user crowdUser) (connector.Identity, error) { | ||||
| 	identity := connector.Identity{ | ||||
| 		Username:      user.Name, | ||||
| 		UserID:        user.Key, | ||||
| 		Email:         user.Email, | ||||
| 		EmailVerified: true, | ||||
| 	} | ||||
|  | ||||
| 	return identity, nil | ||||
| } | ||||
|  | ||||
| // getGroups retrieves a list of user's groups and filters it | ||||
| func (c *crowdConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string) ([]string, error) { | ||||
| 	crowdGroups, err := c.groups(ctx, client, userLogin) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if len(c.Groups) > 0 { | ||||
| 		filteredGroups := groups.Filter(crowdGroups, c.Groups) | ||||
| 		if len(filteredGroups) == 0 { | ||||
| 			return nil, fmt.Errorf("crowd: user %q is not in any of the required groups", userLogin) | ||||
| 		} | ||||
| 		return filteredGroups, nil | ||||
| 	} else if groupScope { | ||||
| 		return crowdGroups, nil | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // crowdUserManagementRequest create a http.Request with basic auth, json payload and Accept header | ||||
| func (c *crowdConnector) crowdUserManagementRequest(ctx context.Context, method string, apiURL string, jsonPayload interface{}) (*http.Request, error) { | ||||
| 	var body io.Reader | ||||
| 	if jsonPayload != nil { | ||||
| 		jsonData, err := json.Marshal(jsonPayload) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("crowd: marshal API json payload: %v", err) | ||||
| 		} | ||||
| 		body = bytes.NewReader(jsonData) | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest(method, fmt.Sprintf("%s/rest/usermanagement/1%s", c.BaseURL, apiURL), body) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("new API req: %v", err) | ||||
| 	} | ||||
| 	req = req.WithContext(ctx) | ||||
|  | ||||
| 	// Crowd API requires a basic auth | ||||
| 	req.SetBasicAuth(c.ClientID, c.ClientSecret) | ||||
| 	req.Header.Set("Accept", "application/json") | ||||
| 	if jsonPayload != nil { | ||||
| 		req.Header.Set("Content-type", "application/json") | ||||
| 	} | ||||
| 	return req, nil | ||||
| } | ||||
|  | ||||
| // validateCrowdResponse validates unique not JSON responses from API | ||||
| func (c *crowdConnector) validateCrowdResponse(resp *http.Response) ([]byte, error) { | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("crowd: read user body: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode == http.StatusForbidden && strings.Contains(string(body), "The server understood the request but refuses to authorize it.") { | ||||
| 		c.logger.Debugf("crowd response validation failed: %s", string(body)) | ||||
| 		return nil, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", c.BaseURL) | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode == http.StatusUnauthorized && string(body) == "Application failed to authenticate" { | ||||
| 		c.logger.Debugf("crowd response validation failed: %s", string(body)) | ||||
| 		return nil, fmt.Errorf("dex failed to authenticate Crowd Application with ID %q", c.ClientID) | ||||
| 	} | ||||
| 	return body, nil | ||||
| } | ||||
							
								
								
									
										150
									
								
								connector/atlassiancrowd/atlassiancrowd_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								connector/atlassiancrowd/atlassiancrowd_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| // Package atlassiancrowd provides authentication strategies using Atlassian Crowd. | ||||
| package atlassiancrowd | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func TestUserGroups(t *testing.T) { | ||||
| 	s := newTestServer(map[string]TestServerResponse{ | ||||
| 		"/rest/usermanagement/1/user/group/nested?username=testuser": { | ||||
| 			Body: crowdGroups{Groups: []struct{ Name string }{{Name: "group1"}, {Name: "group2"}}}, | ||||
| 			Code: 200, | ||||
| 		}, | ||||
| 	}) | ||||
| 	defer s.Close() | ||||
|  | ||||
| 	c := newTestCrowdConnector(s.URL) | ||||
| 	groups, err := c.getGroups(context.Background(), newClient(), true, "testuser") | ||||
|  | ||||
| 	expectNil(t, err) | ||||
| 	expectEquals(t, groups, []string{"group1", "group2"}) | ||||
| } | ||||
|  | ||||
| func TestUserGroupsWithFiltering(t *testing.T) { | ||||
| 	s := newTestServer(map[string]TestServerResponse{ | ||||
| 		"/rest/usermanagement/1/user/group/nested?username=testuser": { | ||||
| 			Body: crowdGroups{Groups: []struct{ Name string }{{Name: "group1"}, {Name: "group2"}}}, | ||||
| 			Code: 200, | ||||
| 		}, | ||||
| 	}) | ||||
| 	defer s.Close() | ||||
|  | ||||
| 	c := newTestCrowdConnector(s.URL) | ||||
| 	c.Groups = []string{"group1"} | ||||
| 	groups, err := c.getGroups(context.Background(), newClient(), true, "testuser") | ||||
|  | ||||
| 	expectNil(t, err) | ||||
| 	expectEquals(t, groups, []string{"group1"}) | ||||
| } | ||||
|  | ||||
| func TestUserLoginFlow(t *testing.T) { | ||||
| 	s := newTestServer(map[string]TestServerResponse{ | ||||
| 		"/rest/usermanagement/1/session?validate-password=false": { | ||||
| 			Body: crowdAuthentication{}, | ||||
| 			Code: 201, | ||||
| 		}, | ||||
| 		"/rest/usermanagement/1/user?username=testuser": { | ||||
| 			Body: crowdUser{Active: true, Name: "testuser", Email: "testuser@example.com"}, | ||||
| 			Code: 200, | ||||
| 		}, | ||||
| 		"/rest/usermanagement/1/user?username=testuser2": { | ||||
| 			Body: `<html>The server understood the request but refuses to authorize it.</html>`, | ||||
| 			Code: 403, | ||||
| 		}, | ||||
| 	}) | ||||
| 	defer s.Close() | ||||
|  | ||||
| 	c := newTestCrowdConnector(s.URL) | ||||
| 	user, err := c.user(context.Background(), newClient(), "testuser") | ||||
| 	expectNil(t, err) | ||||
| 	expectEquals(t, user.Name, "testuser") | ||||
| 	expectEquals(t, user.Email, "testuser@example.com") | ||||
|  | ||||
| 	_, err = c.identityFromCrowdUser(user) | ||||
| 	expectNil(t, err) | ||||
|  | ||||
| 	err = c.authenticateUser(context.Background(), newClient(), "testuser") | ||||
| 	expectNil(t, err) | ||||
|  | ||||
| 	_, err = c.user(context.Background(), newClient(), "testuser2") | ||||
| 	expectEquals(t, err, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", s.URL)) | ||||
| } | ||||
|  | ||||
| func TestUserPassword(t *testing.T) { | ||||
| 	s := newTestServer(map[string]TestServerResponse{ | ||||
| 		"/rest/usermanagement/1/session": { | ||||
| 			Body: crowdAuthenticationError{Reason: "INVALID_USER_AUTHENTICATION", Message: "test"}, | ||||
| 			Code: 401, | ||||
| 		}, | ||||
| 		"/rest/usermanagement/1/session?validate-password=false": { | ||||
| 			Body: crowdAuthentication{}, | ||||
| 			Code: 201, | ||||
| 		}, | ||||
| 	}) | ||||
| 	defer s.Close() | ||||
|  | ||||
| 	c := newTestCrowdConnector(s.URL) | ||||
| 	invalidPassword, err := c.authenticateWithPassword(context.Background(), newClient(), "testuser", "testpassword") | ||||
|  | ||||
| 	expectNil(t, err) | ||||
| 	expectEquals(t, invalidPassword, true) | ||||
|  | ||||
| 	err = c.authenticateUser(context.Background(), newClient(), "testuser") | ||||
| 	expectNil(t, err) | ||||
| } | ||||
|  | ||||
| type TestServerResponse struct { | ||||
| 	Body interface{} | ||||
| 	Code int | ||||
| } | ||||
|  | ||||
| func newTestCrowdConnector(baseURL string) crowdConnector { | ||||
| 	connector := crowdConnector{} | ||||
| 	connector.BaseURL = baseURL | ||||
| 	connector.logger = &logrus.Logger{ | ||||
| 		Out:       ioutil.Discard, | ||||
| 		Level:     logrus.DebugLevel, | ||||
| 		Formatter: &logrus.TextFormatter{DisableColors: true}, | ||||
| 	} | ||||
| 	return connector | ||||
| } | ||||
|  | ||||
| func newTestServer(responses map[string]TestServerResponse) *httptest.Server { | ||||
| 	s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		response := responses[r.RequestURI] | ||||
| 		w.Header().Add("Content-Type", "application/json") | ||||
| 		w.WriteHeader(response.Code) | ||||
| 		json.NewEncoder(w).Encode(response.Body) | ||||
| 	})) | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| func newClient() *http.Client { | ||||
| 	tr := &http.Transport{ | ||||
| 		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | ||||
| 	} | ||||
| 	return &http.Client{Transport: tr} | ||||
| } | ||||
|  | ||||
| func expectNil(t *testing.T, a interface{}) { | ||||
| 	if a != nil { | ||||
| 		t.Errorf("Expected %+v to equal nil", a) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func expectEquals(t *testing.T, a interface{}, b interface{}) { | ||||
| 	if !reflect.DeepEqual(a, b) { | ||||
| 		t.Errorf("Expected %+v to equal %+v", a, b) | ||||
| 	} | ||||
| } | ||||
| @@ -21,6 +21,7 @@ import ( | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
|  | ||||
| 	"github.com/dexidp/dex/connector" | ||||
| 	"github.com/dexidp/dex/connector/atlassiancrowd" | ||||
| 	"github.com/dexidp/dex/connector/authproxy" | ||||
| 	"github.com/dexidp/dex/connector/bitbucketcloud" | ||||
| 	"github.com/dexidp/dex/connector/github" | ||||
| @@ -477,6 +478,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{ | ||||
| 	"microsoft":       func() ConnectorConfig { return new(microsoft.Config) }, | ||||
| 	"bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) }, | ||||
| 	"openshift":       func() ConnectorConfig { return new(openshift.Config) }, | ||||
| 	"atlassian-crowd": func() ConnectorConfig { return new(atlassiancrowd.Config) }, | ||||
| 	// Keep around for backwards compatibility. | ||||
| 	"samlExperimental": func() ConnectorConfig { return new(saml.Config) }, | ||||
| } | ||||
|   | ||||
							
								
								
									
										17
									
								
								web/static/img/atlassian-crowd-icon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/static/img/atlassian-crowd-icon.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| <?xml version="1.0" standalone="no"?> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" | ||||
|  "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> | ||||
| <svg version="1.0" xmlns="http://www.w3.org/2000/svg" | ||||
|  width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000" | ||||
|  preserveAspectRatio="xMidYMid meet"> | ||||
|  | ||||
| <g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)" | ||||
| fill="#4169E1" stroke="none"> | ||||
| <path d="M580 1422 l-315 -117 3 -214 c4 -298 25 -400 113 -548 73 -122 257 | ||||
| -285 302 -267 11 4 157 326 157 347 0 3 -16 11 -35 17 -54 18 -122 92 -140 | ||||
| 152 -19 65 -19 93 0 149 56 165 256 222 386 110 77 -65 107 -169 74 -260 -22 | ||||
| -64 -52 -99 -111 -132 -30 -17 -54 -37 -54 -45 0 -26 133 -336 147 -341 25 | ||||
| -10 58 6 128 62 122 97 210 219 255 355 32 96 39 150 46 391 l6 219 -33 14 | ||||
| c-48 20 -606 226 -610 226 -2 -1 -146 -54 -319 -118z"/> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 861 B | 
| @@ -73,6 +73,11 @@ body { | ||||
|   background-image: url(../static/img/bitbucket-icon.svg); | ||||
| } | ||||
|  | ||||
| .dex-btn-icon--atlassian-crowd { | ||||
|   background-color: #CFDCEA; | ||||
|   background-image: url(../static/img/atlassian-crowd-icon.svg); | ||||
| } | ||||
|  | ||||
| .dex-btn-icon--ldap { | ||||
|   background-color: #84B6EF; | ||||
|   background-image: url(../static/img/ldap-icon.svg); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user