initial commit
This commit is contained in:
		
							
								
								
									
										32
									
								
								connector/connector.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								connector/connector.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // Package connector defines interfaces for federated identity strategies. | ||||
| package connector | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/coreos/poke/storage" | ||||
| ) | ||||
|  | ||||
| // Connector is a mechanism for federating login to a remote identity service. | ||||
| // | ||||
| // Implementations are expected to implement either the PasswordConnector or | ||||
| // CallbackConnector interface. | ||||
| type Connector interface { | ||||
| 	Close() error | ||||
| } | ||||
|  | ||||
| // PasswordConnector is an optional interface for password based connectors. | ||||
| type PasswordConnector interface { | ||||
| 	Login(username, password string) (identity storage.Identity, validPassword bool, err error) | ||||
| } | ||||
|  | ||||
| // CallbackConnector is an optional interface for callback based connectors. | ||||
| type CallbackConnector interface { | ||||
| 	LoginURL(callbackURL, state string) (string, error) | ||||
| 	HandleCallback(r *http.Request) (identity storage.Identity, state string, err error) | ||||
| } | ||||
|  | ||||
| // GroupsConnector is an optional interface for connectors which can map a user to groups. | ||||
| type GroupsConnector interface { | ||||
| 	Groups(identity storage.Identity) ([]string, error) | ||||
| } | ||||
							
								
								
									
										177
									
								
								connector/github/github.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								connector/github/github.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| // Package github provides authentication strategies using GitHub. | ||||
| package github | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"golang.org/x/net/context" | ||||
| 	"golang.org/x/oauth2" | ||||
| 	"golang.org/x/oauth2/github" | ||||
|  | ||||
| 	"github.com/coreos/poke/connector" | ||||
| 	"github.com/coreos/poke/storage" | ||||
| ) | ||||
|  | ||||
| const baseURL = "https://api.github.com" | ||||
|  | ||||
| // Config holds configuration options for github logins. | ||||
| type Config struct { | ||||
| 	ClientID     string `yaml:"clientID"` | ||||
| 	ClientSecret string `yaml:"clientSecret"` | ||||
| 	RedirectURI  string `yaml:"redirectURI"` | ||||
| 	Org          string `yaml:"org"` | ||||
| } | ||||
|  | ||||
| // Open returns a strategy for logging in through GitHub. | ||||
| func (c *Config) Open() (connector.Connector, error) { | ||||
| 	return &githubConnector{ | ||||
| 		redirectURI: c.RedirectURI, | ||||
| 		org:         c.Org, | ||||
| 		oauth2Config: &oauth2.Config{ | ||||
| 			ClientID:     os.ExpandEnv(c.ClientID), | ||||
| 			ClientSecret: os.ExpandEnv(c.ClientSecret), | ||||
| 			Endpoint:     github.Endpoint, | ||||
| 			Scopes: []string{ | ||||
| 				"user:email", // View user's email | ||||
| 				"read:org",   // View user's org teams. | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| type connectorData struct { | ||||
| 	// GitHub's OAuth2 tokens never expire. We don't need a refresh token. | ||||
| 	AccessToken string `json:"accessToken"` | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	_ connector.CallbackConnector = (*githubConnector)(nil) | ||||
| 	_ connector.GroupsConnector   = (*githubConnector)(nil) | ||||
| ) | ||||
|  | ||||
| type githubConnector struct { | ||||
| 	redirectURI  string | ||||
| 	org          string | ||||
| 	oauth2Config *oauth2.Config | ||||
| 	ctx          context.Context | ||||
| 	cancel       context.CancelFunc | ||||
| } | ||||
|  | ||||
| func (c *githubConnector) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *githubConnector) LoginURL(callbackURL, state string) (string, error) { | ||||
| 	if c.redirectURI != callbackURL { | ||||
| 		return "", fmt.Errorf("expected callback URL did not match the URL in the config") | ||||
| 	} | ||||
| 	return c.oauth2Config.AuthCodeURL(state), nil | ||||
| } | ||||
|  | ||||
| type oauth2Error struct { | ||||
| 	error            string | ||||
| 	errorDescription string | ||||
| } | ||||
|  | ||||
| func (e *oauth2Error) Error() string { | ||||
| 	if e.errorDescription == "" { | ||||
| 		return e.error | ||||
| 	} | ||||
| 	return e.error + ": " + e.errorDescription | ||||
| } | ||||
|  | ||||
| func (c *githubConnector) HandleCallback(r *http.Request) (identity storage.Identity, state string, err error) { | ||||
| 	q := r.URL.Query() | ||||
| 	if errType := q.Get("error"); errType != "" { | ||||
| 		return identity, "", &oauth2Error{errType, q.Get("error_description")} | ||||
| 	} | ||||
| 	token, err := c.oauth2Config.Exchange(c.ctx, q.Get("code")) | ||||
| 	if err != nil { | ||||
| 		return identity, "", fmt.Errorf("github: failed to get token: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := c.oauth2Config.Client(c.ctx, token).Get(baseURL + "/user") | ||||
| 	if err != nil { | ||||
| 		return identity, "", fmt.Errorf("github: get URL %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		body, err := ioutil.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			return identity, "", fmt.Errorf("github: read body: %v", err) | ||||
| 		} | ||||
| 		return identity, "", fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
| 	var user struct { | ||||
| 		Name  string `json:"name"` | ||||
| 		Login string `json:"login"` | ||||
| 		ID    int    `json:"id"` | ||||
| 		Email string `json:"email"` | ||||
| 	} | ||||
| 	if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { | ||||
| 		return identity, "", fmt.Errorf("failed to decode response: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	data := connectorData{AccessToken: token.AccessToken} | ||||
| 	connData, err := json.Marshal(data) | ||||
| 	if err != nil { | ||||
| 		return identity, "", fmt.Errorf("marshal connector data: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	username := user.Name | ||||
| 	if username == "" { | ||||
| 		username = user.Login | ||||
| 	} | ||||
| 	identity = storage.Identity{ | ||||
| 		UserID:        strconv.Itoa(user.ID), | ||||
| 		Username:      username, | ||||
| 		Email:         user.Email, | ||||
| 		EmailVerified: true, | ||||
| 		ConnectorData: connData, | ||||
| 	} | ||||
| 	return identity, q.Get("state"), nil | ||||
| } | ||||
|  | ||||
| func (c *githubConnector) Groups(identity storage.Identity) ([]string, error) { | ||||
| 	var data connectorData | ||||
| 	if err := json.Unmarshal(identity.ConnectorData, &data); err != nil { | ||||
| 		return nil, fmt.Errorf("decode connector data: %v", err) | ||||
| 	} | ||||
| 	token := &oauth2.Token{AccessToken: data.AccessToken} | ||||
| 	resp, err := c.oauth2Config.Client(c.ctx, token).Get(baseURL + "/user/teams") | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("github: get teams: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		body, err := ioutil.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("github: read body: %v", err) | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
|  | ||||
| 	// https://developer.github.com/v3/orgs/teams/#response-12 | ||||
| 	var teams []struct { | ||||
| 		Name string `json:"name"` | ||||
| 		Org  struct { | ||||
| 			Login string `json:"login"` | ||||
| 		} `json:"organization"` | ||||
| 	} | ||||
| 	if err := json.NewDecoder(resp.Body).Decode(&teams); err != nil { | ||||
| 		return nil, fmt.Errorf("github: unmarshal groups: %v", err) | ||||
| 	} | ||||
| 	groups := []string{} | ||||
| 	for _, team := range teams { | ||||
| 		if team.Org.Login == c.org { | ||||
| 			groups = append(groups, team.Name) | ||||
| 		} | ||||
| 	} | ||||
| 	return groups, nil | ||||
| } | ||||
							
								
								
									
										59
									
								
								connector/ldap/ldap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								connector/ldap/ldap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| // Package ldap implements strategies for authenticating using the LDAP protocol. | ||||
| package ldap | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"gopkg.in/ldap.v2" | ||||
|  | ||||
| 	"github.com/coreos/poke/connector" | ||||
| 	"github.com/coreos/poke/storage" | ||||
| ) | ||||
|  | ||||
| // Config holds the configuration parameters for the LDAP connector. | ||||
| type Config struct { | ||||
| 	Host   string `yaml:"host"` | ||||
| 	BindDN string `yaml:"bindDN"` | ||||
| } | ||||
|  | ||||
| // Open returns an authentication strategy using LDAP. | ||||
| func (c *Config) Open() (connector.Connector, error) { | ||||
| 	if c.Host == "" { | ||||
| 		return nil, errors.New("missing host parameter") | ||||
| 	} | ||||
| 	if c.BindDN == "" { | ||||
| 		return nil, errors.New("missing bindDN paramater") | ||||
| 	} | ||||
| 	return &ldapConnector{*c}, nil | ||||
| } | ||||
|  | ||||
| type ldapConnector struct { | ||||
| 	Config | ||||
| } | ||||
|  | ||||
| func (c *ldapConnector) do(f func(c *ldap.Conn) error) error { | ||||
| 	// TODO(ericchiang): Connection pooling. | ||||
| 	conn, err := ldap.Dial("tcp", c.Host) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to connect: %v", err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
|  | ||||
| 	return f(conn) | ||||
| } | ||||
|  | ||||
| func (c *ldapConnector) Login(username, password string) (storage.Identity, error) { | ||||
| 	err := c.do(func(conn *ldap.Conn) error { | ||||
| 		return conn.Bind(fmt.Sprintf("uid=%s,%s", username, c.BindDN), password) | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return storage.Identity{}, err | ||||
| 	} | ||||
|  | ||||
| 	return storage.Identity{Username: username}, nil | ||||
| } | ||||
|  | ||||
| func (c *ldapConnector) Close() error { | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										53
									
								
								connector/mock/connectortest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								connector/mock/connectortest.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // Package mock implements a mock connector which requires no user interaction. | ||||
| package mock | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/coreos/poke/connector" | ||||
| 	"github.com/coreos/poke/storage" | ||||
| ) | ||||
|  | ||||
| // New returns a mock connector which requires no user interaction. It always returns | ||||
| // the same (fake) identity. | ||||
| func New() connector.Connector { | ||||
| 	return mockConnector{} | ||||
| } | ||||
|  | ||||
| type mockConnector struct{} | ||||
|  | ||||
| func (m mockConnector) Close() error { return nil } | ||||
|  | ||||
| func (m mockConnector) LoginURL(callbackURL, state string) (string, error) { | ||||
| 	u, err := url.Parse(callbackURL) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) | ||||
| 	} | ||||
| 	v := u.Query() | ||||
| 	v.Set("state", state) | ||||
| 	u.RawQuery = v.Encode() | ||||
| 	return u.String(), nil | ||||
| } | ||||
|  | ||||
| func (m mockConnector) HandleCallback(r *http.Request) (storage.Identity, string, error) { | ||||
| 	return storage.Identity{ | ||||
| 		UserID:        "0-385-28089-0", | ||||
| 		Username:      "Kilgore Trout", | ||||
| 		Email:         "kilgore@kilgore.trout", | ||||
| 		EmailVerified: true, | ||||
| 	}, r.URL.Query().Get("state"), nil | ||||
| } | ||||
|  | ||||
| func (m mockConnector) Groups(identity storage.Identity) ([]string, error) { | ||||
| 	return []string{"authors"}, nil | ||||
| } | ||||
|  | ||||
| // Config holds the configuration parameters for the mock connector. | ||||
| type Config struct{} | ||||
|  | ||||
| // Open returns an authentication strategy which requires no user interaction. | ||||
| func (c *Config) Open() (connector.Connector, error) { | ||||
| 	return New(), nil | ||||
| } | ||||
							
								
								
									
										2
									
								
								connector/oidc/oidc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								connector/oidc/oidc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| // Package oidc implements logging in through OpenID Connect providers. | ||||
| package oidc | ||||
		Reference in New Issue
	
	Block a user