Merge branch 'kfitzpatrick-custom-github-style-oauth'

This commit is contained in:
bergquist
2016-09-07 11:07:30 +02:00
13 changed files with 562 additions and 273 deletions
+14 -1
View File
@@ -59,7 +59,7 @@ cert_key =
#################################### Database ####################################
[database]
# You can configure the database connection by specifying type, host, name, user and password
# You can configure the database connection by specifying type, host, name, user and password
# as seperate properties or as on string using the url propertie.
# Either "mysql", "postgres" or "sqlite3", it's your choice
@@ -223,6 +223,19 @@ token_url = https://accounts.google.com/o/oauth2/token
api_url = https://www.googleapis.com/oauth2/v1/userinfo
allowed_domains =
#################################### Generic OAuth ##########################
[auth.generic_oauth]
enabled = false
allow_sign_up = false
client_id = some_id
client_secret = some_secret
scopes = user:email
auth_url =
token_url =
api_url =
team_ids =
allowed_organizations =
#################################### Basic Auth ##########################
[auth.basic]
enabled = true
+14 -1
View File
@@ -61,7 +61,7 @@
#################################### Database ####################################
[database]
# You can configure the database connection by specifying type, host, name, user and password
# You can configure the database connection by specifying type, host, name, user and password
# as seperate properties or as on string using the url propertie.
# Either "mysql", "postgres" or "sqlite3", it's your choice
@@ -205,6 +205,19 @@ check_for_updates = true
;api_url = https://www.googleapis.com/oauth2/v1/userinfo
;allowed_domains =
#################################### Generic OAuth ##########################
[auth.generic_oauth]
;enabled = false
;allow_sign_up = false
;client_id = some_id
;client_secret = some_secret
;scopes = user:email,read:org
;auth_url = https://foo.bar/login/oauth/authorize
;token_url = https://foo.bar/login/oauth/access_token
;api_url = https://foo.bar/user
;team_ids =
;allowed_organizations =
#################################### Auth Proxy ##########################
[auth.proxy]
;enabled = false
@@ -341,6 +341,23 @@ You may allow users to sign-up via Google authentication by setting the
user successfully authenticating via Google authentication will be
automatically signed up.
## [auth.generic_oauth]
This option could be used if have your own oauth service.
This callback URL must match the full HTTP address that you use in your
browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
[auth.generic_oauth]
enabled = true
client_id = YOUR_APP_CLIENT_ID
client_secret = YOUR_APP_CLIENT_SECRET
scopes =
auth_url =
token_url =
allowed_domains = mycompany.com mycompany.org
allow_sign_up = false
<hr>
## [auth.basic]
+2
View File
@@ -27,6 +27,8 @@ func LoginView(c *middleware.Context) {
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
+1
View File
@@ -6,4 +6,5 @@ const (
GITHUB OAuthType = iota + 1
GOOGLE
TWITTER
GENERIC
)
+3 -2
View File
@@ -11,8 +11,9 @@ type OAuthInfo struct {
}
type OAuther struct {
GitHub, Google, Twitter bool
OAuthInfos map[string]*OAuthInfo
GitHub, Google, Twitter, Generic bool
OAuthInfos map[string]*OAuthInfo
OAuthProviderName string
}
var OAuthService *OAuther
+20
View File
@@ -0,0 +1,20 @@
package social
import (
"fmt"
"strings"
)
func isEmailAllowed(email string, allowedDomains []string) bool {
if len(allowedDomains) == 0 {
return true
}
valid := false
for _, domain := range allowedDomains {
emailSuffix := fmt.Sprintf("@%s", domain)
valid = valid || strings.HasSuffix(email, emailSuffix)
}
return valid
}
+205
View File
@@ -0,0 +1,205 @@
package social
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/models"
"golang.org/x/oauth2"
)
type GenericOAuth struct {
*oauth2.Config
allowedDomains []string
allowedOrganizations []string
apiUrl string
allowSignup bool
teamIds []int
}
func (s *GenericOAuth) Type() int {
return int(models.GENERIC)
}
func (s *GenericOAuth) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.allowedDomains)
}
func (s *GenericOAuth) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
if len(s.teamIds) == 0 {
return true
}
teamMemberships, err := s.FetchTeamMemberships(client)
if err != nil {
return false
}
for _, teamId := range s.teamIds {
for _, membershipId := range teamMemberships {
if teamId == membershipId {
return true
}
}
}
return false
}
func (s *GenericOAuth) IsOrganizationMember(client *http.Client) 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 {
return true
}
}
}
return false
}
func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
type Record struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
r, err := client.Get(emailsUrl)
if err != nil {
return "", err
}
defer r.Body.Close()
var records []Record
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
return "", err
}
var email = ""
for _, record := range records {
if record.Primary {
email = record.Email
}
}
return email, nil
}
func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
type Record struct {
Id int `json:"id"`
}
membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
r, err := client.Get(membershipUrl)
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 ids = make([]int, len(records))
for i, record := range records {
ids[i] = record.Id
}
return ids, nil
}
func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
type Record struct {
Login string `json:"login"`
}
url := fmt.Sprintf(s.apiUrl + "/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 *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
Name 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
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return nil, err
}
userInfo := &BasicUserInfo{
Identity: strconv.Itoa(data.Id),
Name: data.Name,
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 == "" {
userInfo.Email, err = s.FetchPrivateEmail(client)
if err != nil {
return nil, err
}
}
return userInfo, nil
}
+213
View File
@@ -0,0 +1,213 @@
package social
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/models"
"golang.org/x/oauth2"
)
type SocialGithub struct {
*oauth2.Config
allowedDomains []string
allowedOrganizations []string
apiUrl string
allowSignup bool
teamIds []int
}
var (
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
)
var (
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
)
func (s *SocialGithub) Type() int {
return int(models.GITHUB)
}
func (s *SocialGithub) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.allowedDomains)
}
func (s *SocialGithub) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
if len(s.teamIds) == 0 {
return true
}
teamMemberships, err := s.FetchTeamMemberships(client)
if err != nil {
return false
}
for _, teamId := range s.teamIds {
for _, membershipId := range teamMemberships {
if teamId == membershipId {
return true
}
}
}
return false
}
func (s *SocialGithub) IsOrganizationMember(client *http.Client) 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 {
return true
}
}
}
return false
}
func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
type Record struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
r, err := client.Get(emailsUrl)
if err != nil {
return "", err
}
defer r.Body.Close()
var records []Record
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
return "", err
}
var email = ""
for _, record := range records {
if record.Primary {
email = record.Email
}
}
return email, nil
}
func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
type Record struct {
Id int `json:"id"`
}
membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
r, err := client.Get(membershipUrl)
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 ids = make([]int, len(records))
for i, record := range records {
ids[i] = record.Id
}
return ids, nil
}
func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
type Record struct {
Login string `json:"login"`
}
url := fmt.Sprintf(s.apiUrl + "/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 *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
Name 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
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return nil, err
}
userInfo := &BasicUserInfo{
Identity: strconv.Itoa(data.Id),
Name: data.Name,
Email: data.Email,
}
if !s.IsTeamMember(client) {
return nil, ErrMissingTeamMembership
}
if !s.IsOrganizationMember(client) {
return nil, ErrMissingOrganizationMembership
}
if userInfo.Email == "" {
userInfo.Email, err = s.FetchPrivateEmail(client)
if err != nil {
return nil, err
}
}
return userInfo, nil
}
+52
View File
@@ -0,0 +1,52 @@
package social
import (
"encoding/json"
"github.com/grafana/grafana/pkg/models"
"golang.org/x/oauth2"
)
type SocialGoogle struct {
*oauth2.Config
allowedDomains []string
apiUrl string
allowSignup bool
}
func (s *SocialGoogle) Type() int {
return int(models.GOOGLE)
}
func (s *SocialGoogle) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.allowedDomains)
}
func (s *SocialGoogle) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*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
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return nil, err
}
return &BasicUserInfo{
Identity: data.Id,
Name: data.Name,
Email: data.Email,
}, nil
}
+14 -268
View File
@@ -1,14 +1,8 @@
package social
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/net/context"
@@ -42,7 +36,7 @@ func NewOAuthService() {
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
allOauthes := []string{"github", "google"}
allOauthes := []string{"github", "google", "generic_oauth"}
for _, name := range allOauthes {
sec := setting.Cfg.Section("auth." + name)
@@ -98,269 +92,21 @@ func NewOAuthService() {
allowSignup: info.AllowSignup,
}
}
}
}
func isEmailAllowed(email string, allowedDomains []string) bool {
if len(allowedDomains) == 0 {
return true
}
valid := false
for _, domain := range allowedDomains {
emailSuffix := fmt.Sprintf("@%s", domain)
valid = valid || strings.HasSuffix(email, emailSuffix)
}
return valid
}
type SocialGithub struct {
*oauth2.Config
allowedDomains []string
allowedOrganizations []string
apiUrl string
allowSignup bool
teamIds []int
}
var (
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
)
var (
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
)
func (s *SocialGithub) Type() int {
return int(models.GITHUB)
}
func (s *SocialGithub) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.allowedDomains)
}
func (s *SocialGithub) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
if len(s.teamIds) == 0 {
return true
}
teamMemberships, err := s.FetchTeamMemberships(client)
if err != nil {
return false
}
for _, teamId := range s.teamIds {
for _, membershipId := range teamMemberships {
if teamId == membershipId {
return true
// Generic - Uses the same scheme as Github.
if name == "generic_oauth" {
setting.OAuthService.Generic = true
setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
teamIds := sec.Key("team_ids").Ints(",")
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
SocialMap["generic_oauth"] = &GenericOAuth{
Config: &config,
allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
teamIds: teamIds,
allowedOrganizations: allowedOrganizations,
}
}
}
return false
}
func (s *SocialGithub) IsOrganizationMember(client *http.Client) 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 {
return true
}
}
}
return false
}
func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
type Record struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
r, err := client.Get(emailsUrl)
if err != nil {
return "", err
}
defer r.Body.Close()
var records []Record
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
return "", err
}
var email = ""
for _, record := range records {
if record.Primary {
email = record.Email
}
}
return email, nil
}
func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
type Record struct {
Id int `json:"id"`
}
membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
r, err := client.Get(membershipUrl)
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 ids = make([]int, len(records))
for i, record := range records {
ids[i] = record.Id
}
return ids, nil
}
func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
type Record struct {
Login string `json:"login"`
}
url := fmt.Sprintf(s.apiUrl + "/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 *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
Name 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
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return nil, err
}
userInfo := &BasicUserInfo{
Identity: strconv.Itoa(data.Id),
Name: data.Name,
Email: data.Email,
}
if !s.IsTeamMember(client) {
return nil, ErrMissingTeamMembership
}
if !s.IsOrganizationMember(client) {
return nil, ErrMissingOrganizationMembership
}
if userInfo.Email == "" {
userInfo.Email, err = s.FetchPrivateEmail(client)
if err != nil {
return nil, err
}
}
return userInfo, nil
}
// ________ .__
// / _____/ ____ ____ ____ | | ____
// / \ ___ / _ \ / _ \ / ___\| | _/ __ \
// \ \_\ ( <_> | <_> ) /_/ > |_\ ___/
// \______ /\____/ \____/\___ /|____/\___ >
// \/ /_____/ \/
type SocialGoogle struct {
*oauth2.Config
allowedDomains []string
apiUrl string
allowSignup bool
}
func (s *SocialGoogle) Type() int {
return int(models.GOOGLE)
}
func (s *SocialGoogle) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.allowedDomains)
}
func (s *SocialGoogle) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*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
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return nil, err
}
return &BasicUserInfo{
Identity: data.Id,
Name: data.Name,
Email: data.Email,
}, nil
}
+3 -1
View File
@@ -17,8 +17,10 @@ function (angular, coreModule, config) {
$scope.googleAuthEnabled = config.googleAuthEnabled;
$scope.githubAuthEnabled = config.githubAuthEnabled;
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
$scope.allowUserPassLogin = config.allowUserPassLogin;
$scope.genericOAuthEnabled = config.genericOAuthEnabled;
$scope.oauthProviderName = config.oauthProviderName;
$scope.disableUserSignUp = config.disableUserSignUp;
$scope.loginHint = config.loginHint;
+4
View File
@@ -59,6 +59,10 @@
<i class="fa fa-github"></i>
with Github
</a>
<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="genericOAuthEnabled">
<i class="fa fa-gear"></i>
with {{oauthProviderName || "OAuth 2"}}
</a>
</div>
</div>