Merge pull request #1515 from flant/atlassian-crowd-connector
new connector for Atlassian Crowd
This commit is contained in:
commit
b7cf701032
6 changed files with 650 additions and 0 deletions
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 a new issue