Explorar o código

Oauth2 Updates (#6226)

* break out go and js build commands

* support oauth providers that return errors via redirect

* remove extra call to get grafana.net org membership

* removed GitHub specifics from generic OAuth

* readded ability to name generic source

* revert to a backward-compatible state, refactor and clean up

* streamline oauth user creation, make generic oauth support more generic
Dan Cech %!s(int64=9) %!d(string=hai) anos
pai
achega
6b16fcea52

+ 15 - 3
Makefile

@@ -1,16 +1,28 @@
 all: deps build
 
-deps:
+deps-go:
 	go run build.go setup
+
+deps-js:
 	npm install
 
-build:
+deps: deps-go deps-js
+
+build-go:
 	go run build.go build
+
+build-js:
 	npm run build
 
-test:
+build: build-go build-js
+
+test-go:
 	go test -v ./pkg/...
+
+test-js:
 	npm test
 
+test: test-go test-js
+
 run:
 	./bin/grafana-server

+ 57 - 8
pkg/api/login_oauth.go

@@ -1,11 +1,17 @@
 package api
 
 import (
-	"errors"
-	"fmt"
 	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/base64"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
 
+	"golang.org/x/net/context"
 	"golang.org/x/oauth2"
 
 	"github.com/grafana/grafana/pkg/bus"
@@ -17,9 +23,9 @@ import (
 )
 
 func GenStateString() string {
-        rnd := make([]byte, 32)
-        rand.Read(rnd)
-        return base64.StdEncoding.EncodeToString(rnd)
+	rnd := make([]byte, 32)
+	rand.Read(rnd)
+	return base64.StdEncoding.EncodeToString(rnd)
 }
 
 func OAuthLogin(ctx *middleware.Context) {
@@ -35,6 +41,14 @@ func OAuthLogin(ctx *middleware.Context) {
 		return
 	}
 
+	error := ctx.Query("error")
+	if error != "" {
+		errorDesc := ctx.Query("error_description")
+		ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc)
+		ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003")
+		return
+	}
+
 	code := ctx.Query("code")
 	if code == "" {
 		state := GenStateString()
@@ -52,7 +66,38 @@ func OAuthLogin(ctx *middleware.Context) {
 	}
 
 	// handle call back
-	token, err := connect.Exchange(oauth2.NoContext, code)
+
+	// initialize oauth2 context
+	oauthCtx := oauth2.NoContext
+	if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" {
+		cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		// Load CA cert
+		caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
+		if err != nil {
+			log.Fatal(err)
+		}
+		caCertPool := x509.NewCertPool()
+		caCertPool.AppendCertsFromPEM(caCert)
+
+		tr := &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+				Certificates: []tls.Certificate{cert},
+				RootCAs: caCertPool,
+			},
+		}
+		sslcli := &http.Client{Transport: tr}
+
+		oauthCtx = context.TODO()
+		oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
+	}
+
+	// get token from provider
+	token, err := connect.Exchange(oauthCtx, code)
 	if err != nil {
 		ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
 		return
@@ -60,7 +105,11 @@ func OAuthLogin(ctx *middleware.Context) {
 
 	ctx.Logger.Debug("OAuthLogin Got token")
 
-	userInfo, err := connect.UserInfo(token)
+	// set up oauth2 client
+	client := connect.Client(oauthCtx, token)
+
+	// get user info
+	userInfo, err := connect.UserInfo(client)
 	if err != nil {
 		if err == social.ErrMissingTeamMembership {
 			ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
@@ -100,7 +149,7 @@ func OAuthLogin(ctx *middleware.Context) {
 			return
 		}
 		cmd := m.CreateUserCommand{
-			Login:          userInfo.Email,
+			Login:          userInfo.Login,
 			Email:          userInfo.Email,
 			Name:           userInfo.Name,
 			Company:        userInfo.Company,

+ 3 - 0
pkg/setting/setting_oauth.go

@@ -9,6 +9,9 @@ type OAuthInfo struct {
 	ApiUrl                 string
 	AllowSignup            bool
 	Name                   string
+	TlsClientCert          string
+	TlsClientKey           string
+	TlsClientCa            string
 }
 
 type OAuther struct {

+ 25 - 13
pkg/social/generic_oauth.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"strconv"
 
 	"github.com/grafana/grafana/pkg/models"
 
@@ -160,15 +159,16 @@ func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error)
 	return logins, nil
 }
 
-func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 	var data struct {
-		Id    int    `json:"id"`
-		Name  string `json:"login"`
-		Email string `json:"email"`
+		Name       string              `json:"name"`
+		Login      string              `json:"login"`
+		Username   string              `json:"username"`
+		Email      string              `json:"email"`
+		Attributes map[string][]string `json:"attributes"`
 	}
 
 	var err error
-	client := s.Client(oauth2.NoContext, token)
 	r, err := client.Get(s.apiUrl)
 	if err != nil {
 		return nil, err
@@ -181,17 +181,13 @@ func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 	}
 
 	userInfo := &BasicUserInfo{
-		Identity: strconv.Itoa(data.Id),
 		Name:     data.Name,
+		Login:    data.Login,
 		Email:    data.Email,
 	}
 
-	if !s.IsTeamMember(client) {
-		return nil, errors.New("User not a member of one of the required teams")
-	}
-
-	if !s.IsOrganizationMember(client) {
-		return nil, errors.New("User not a member of one of the required organizations")
+	if (userInfo.Email == "" && data.Attributes["email:primary"] != nil) {
+		userInfo.Email = data.Attributes["email:primary"][0]
 	}
 
 	if userInfo.Email == "" {
@@ -201,5 +197,21 @@ func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 		}
 	}
 
+	if (userInfo.Login == "" && data.Username != "") {
+		userInfo.Login = data.Username
+	}
+
+	if (userInfo.Login == "") {
+		userInfo.Login = data.Email
+	}
+
+	if !s.IsTeamMember(client) {
+		return nil, errors.New("User not a member of one of the required teams")
+	}
+
+	if !s.IsOrganizationMember(client) {
+		return nil, errors.New("User not a member of one of the required organizations")
+	}
+
 	return userInfo, nil
 }

+ 4 - 6
pkg/social/github_oauth.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"strconv"
 
 	"github.com/grafana/grafana/pkg/models"
 
@@ -168,15 +167,14 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error)
 	return logins, nil
 }
 
-func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 	var data struct {
 		Id    int    `json:"id"`
-		Name  string `json:"login"`
+		Login string `json:"login"`
 		Email string `json:"email"`
 	}
 
 	var err error
-	client := s.Client(oauth2.NoContext, token)
 	r, err := client.Get(s.apiUrl)
 	if err != nil {
 		return nil, err
@@ -189,8 +187,8 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 	}
 
 	userInfo := &BasicUserInfo{
-		Identity: strconv.Itoa(data.Id),
-		Name:     data.Name,
+		Name:     data.Login,
+		Login:    data.Login,
 		Email:    data.Email,
 	}
 

+ 2 - 4
pkg/social/google_oauth.go

@@ -2,6 +2,7 @@ package social
 
 import (
 	"encoding/json"
+	"net/http"
 
 	"github.com/grafana/grafana/pkg/models"
 
@@ -27,15 +28,13 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
 	return s.allowSignup
 }
 
-func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 	var data struct {
-		Id    string `json:"id"`
 		Name  string `json:"name"`
 		Email string `json:"email"`
 	}
 	var err error
 
-	client := s.Client(oauth2.NoContext, token)
 	r, err := client.Get(s.apiUrl)
 	if err != nil {
 		return nil, err
@@ -45,7 +44,6 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 		return nil, err
 	}
 	return &BasicUserInfo{
-		Identity: data.Id,
 		Name:     data.Name,
 		Email:    data.Email,
 	}, nil

+ 12 - 42
pkg/social/grafananet_oauth.go

@@ -2,9 +2,7 @@ package social
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/http"
-	"strconv"
 
 	"github.com/grafana/grafana/pkg/models"
 
@@ -18,6 +16,10 @@ type SocialGrafanaNet struct {
 	allowSignup          bool
 }
 
+type OrgRecord struct {
+	Login string `json:"login"`
+}
+
 func (s *SocialGrafanaNet) Type() int {
 	return int(models.GRAFANANET)
 }
@@ -30,19 +32,14 @@ func (s *SocialGrafanaNet) IsSignupAllowed() bool {
 	return s.allowSignup
 }
 
-func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool {
+func (s *SocialGrafanaNet) IsOrganizationMember(organizations []OrgRecord) bool {
 	if len(s.allowedOrganizations) == 0 {
 		return true
 	}
 
-	organizations, err := s.FetchOrganizations(client)
-	if err != nil {
-		return false
-	}
-
 	for _, allowedOrganization := range s.allowedOrganizations {
 		for _, organization := range organizations {
-			if organization == allowedOrganization {
+			if organization.Login == allowedOrganization {
 				return true
 			}
 		}
@@ -51,43 +48,16 @@ func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool {
 	return false
 }
 
-func (s *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) {
-	type Record struct {
-		Login string `json:"login"`
-	}
-
-	url := fmt.Sprintf(s.url + "/api/oauth2/user/orgs")
-	r, err := client.Get(url)
-	if err != nil {
-		return nil, err
-	}
-
-	defer r.Body.Close()
-
-	var records []Record
-
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
-	}
-
-	var logins = make([]string, len(records))
-	for i, record := range records {
-		logins[i] = record.Login
-	}
-
-	return logins, nil
-}
-
-func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+func (s *SocialGrafanaNet) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 	var data struct {
-		Id    int    `json:"id"`
-		Name  string `json:"login"`
+		Name  string `json:"name"`
+		Login string `json:"username"`
 		Email string `json:"email"`
 		Role  string `json:"role"`
+		Orgs  []OrgRecord `json:"orgs"`
 	}
 
 	var err error
-	client := s.Client(oauth2.NoContext, token)
 	r, err := client.Get(s.url + "/api/oauth2/user")
 	if err != nil {
 		return nil, err
@@ -100,13 +70,13 @@ func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error)
 	}
 
 	userInfo := &BasicUserInfo{
-		Identity: strconv.Itoa(data.Id),
 		Name:     data.Name,
+		Login:    data.Login,
 		Email:    data.Email,
 		Role:     data.Role,
 	}
 
-	if !s.IsOrganizationMember(client) {
+	if !s.IsOrganizationMember(data.Orgs) {
 		return nil, ErrMissingOrganizationMembership
 	}
 

+ 19 - 13
pkg/social/social.go

@@ -1,16 +1,16 @@
 package social
 
 import (
+	"net/http"
 	"strings"
 
-	"github.com/grafana/grafana/pkg/setting"
 	"golang.org/x/net/context"
-
 	"golang.org/x/oauth2"
+
+	"github.com/grafana/grafana/pkg/setting"
 )
 
 type BasicUserInfo struct {
-	Identity string
 	Name     string
 	Email    string
 	Login    string
@@ -20,12 +20,13 @@ type BasicUserInfo struct {
 
 type SocialConnector interface {
 	Type() int
-	UserInfo(token *oauth2.Token) (*BasicUserInfo, error)
+	UserInfo(client *http.Client) (*BasicUserInfo, error)
 	IsEmailAllowed(email string) bool
 	IsSignupAllowed() bool
 
 	AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
 	Exchange(ctx context.Context, code string) (*oauth2.Token, error)
+	Client(ctx context.Context, t *oauth2.Token) *http.Client
 }
 
 var (
@@ -52,6 +53,9 @@ func NewOAuthService() {
 			AllowedDomains: sec.Key("allowed_domains").Strings(" "),
 			AllowSignup:    sec.Key("allow_sign_up").MustBool(),
 			Name:           sec.Key("name").MustString(name),
+			TlsClientCert:  sec.Key("tls_client_cert").String(),
+			TlsClientKey:   sec.Key("tls_client_key").String(),
+			TlsClientCa:    sec.Key("tls_client_ca").String(),
 		}
 
 		if !info.Enabled {
@@ -59,6 +63,7 @@ func NewOAuthService() {
 		}
 
 		setting.OAuthService.OAuthInfos[name] = info
+
 		config := oauth2.Config{
 			ClientID:     info.ClientId,
 			ClientSecret: info.ClientSecret,
@@ -85,9 +90,10 @@ func NewOAuthService() {
 		// Google.
 		if name == "google" {
 			SocialMap["google"] = &SocialGoogle{
-				Config: &config, allowedDomains: info.AllowedDomains,
-				apiUrl:      info.ApiUrl,
-				allowSignup: info.AllowSignup,
+				Config:               &config,
+				allowedDomains:       info.AllowedDomains,
+				apiUrl:               info.ApiUrl,
+				allowSignup:          info.AllowSignup,
 			}
 		}
 
@@ -104,15 +110,15 @@ func NewOAuthService() {
 		}
 
 		if name == "grafananet" {
-			config := oauth2.Config{
+			config = oauth2.Config{
 				ClientID:     info.ClientId,
 				ClientSecret: info.ClientSecret,
-				Endpoint: oauth2.Endpoint{
-					AuthURL:  setting.GrafanaNetUrl + "/oauth2/authorize",
-					TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token",
+				Endpoint:     oauth2.Endpoint{
+					AuthURL:      setting.GrafanaNetUrl + "/oauth2/authorize",
+					TokenURL:     setting.GrafanaNetUrl + "/api/oauth2/token",
 				},
-				RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
-				Scopes:      info.Scopes,
+				RedirectURL:  strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
+				Scopes:       info.Scopes,
 			}
 
 			SocialMap["grafananet"] = &SocialGrafanaNet{

+ 1 - 0
public/app/core/controllers/login_ctrl.js

@@ -11,6 +11,7 @@ function (angular, _, coreModule, config) {
     "1000": "Required team membership not fulfilled",
     "1001": "Required organization membership not fulfilled",
     "1002": "Required email domain not fulfilled",
+    "1003": "Login provider denied login request",
   };
 
   coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {