diff --git a/conf/defaults.ini b/conf/defaults.ini index 750502fb663..9fbecb6a1ea 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -9,7 +9,7 @@ app_mode = production # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty instance_name = ${HOSTNAME} -#################################### Paths #################################### +#################################### Paths ############################### [paths] # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) # @@ -23,7 +23,7 @@ logs = data/log # plugins = data/plugins -#################################### Server #################################### +#################################### Server ############################## [server] # Protocol (http or https) protocol = http @@ -57,7 +57,7 @@ enable_gzip = false cert_file = cert_key = -#################################### Database #################################### +#################################### Database ############################ [database] # 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. @@ -84,7 +84,7 @@ server_cert_name = # For "sqlite3" only, path relative to data_path setting path = grafana.db -#################################### Session #################################### +#################################### Session ############################# [session] # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" provider = file @@ -112,7 +112,7 @@ cookie_secure = false session_life_time = 86400 gc_interval_time = 86400 -#################################### Analytics #################################### +#################################### Analytics ########################### [analytics] # Server reporting, sends usage counters to stats.grafana.org every 24 hours. # No ip addresses are being tracked, only simple counters to track @@ -133,7 +133,7 @@ google_analytics_ua_id = # Google Tag Manager ID, only enabled if you specify an id here google_tag_manager_id = -#################################### Security #################################### +#################################### Security ############################ [security] # default admin user, created on startup admin_user = admin @@ -193,7 +193,7 @@ default_theme = dark # Allow users to sign in using username and password allow_user_pass_login = true -#################################### Anonymous Auth ########################## +#################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access enabled = false @@ -204,7 +204,7 @@ org_name = Main Org. # specify role for unauthenticated users org_role = Viewer -#################################### Github Auth ########################## +#################################### Github Auth ######################### [auth.github] enabled = false allow_sign_up = false @@ -217,7 +217,7 @@ api_url = https://api.github.com/user team_ids = allowed_organizations = -#################################### Google Auth ########################## +#################################### Google Auth ######################### [auth.google] enabled = false allow_sign_up = false @@ -229,7 +229,16 @@ token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = -#################################### Generic OAuth ########################## +#################################### Grafana.net Auth #################### +[auth.grafananet] +enabled = false +allow_sign_up = false +client_id = some_id +client_secret = some_secret +scopes = user:email +allowed_organizations = + +#################################### Generic OAuth ####################### [auth.generic_oauth] enabled = false allow_sign_up = false @@ -253,12 +262,12 @@ header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true -#################################### Auth LDAP ########################## +#################################### Auth LDAP ########################### [auth.ldap] enabled = false config_file = /etc/grafana/ldap.toml -#################################### SMTP / Emailing ########################## +#################################### SMTP / Emailing ##################### [smtp] enabled = false host = localhost:25 @@ -273,9 +282,6 @@ from_address = admin@grafana.localhost welcome_email_on_sign_up = false templates_pattern = emails/*.html -[tmp.files] -rendered_image_ttl_days = 14 - #################################### Logging ########################## [log] # Either "console", "file", "syslog". Default is console and file @@ -331,18 +337,18 @@ facility = tag = -#################################### AMQP Event Publisher ########################## +#################################### AMQP Event Publisher ################ [event_publisher] enabled = false rabbitmq_url = amqp://localhost/ exchange = grafana_events -#################################### Dashboard JSON files ########################## +#################################### Dashboard JSON files ################ [dashboards.json] enabled = false path = /var/lib/grafana/dashboards -#################################### Usage Quotas ########################## +#################################### Usage Quotas ######################## [quota] enabled = false @@ -377,7 +383,7 @@ global_api_key = -1 # global limit on number of logged in users. global_session = -1 -#################################### Alerting ###################################### +#################################### Alerting ############################ # docs about alerting can be found in /docs/sources/alerting/ # __.-/| # \`o_O' @@ -396,7 +402,7 @@ global_session = -1 [alerting] enabled = true -#################################### Internal Grafana Metrics ########################## +#################################### Internal Grafana Metrics ############ # Metrics available at HTTP API Url /api/metrics [metrics] enabled = true @@ -411,7 +417,7 @@ prefix = prod.grafana.%(instance_name)s. [grafana_net] url = https://grafana.net -#################################### External image storage ########################## +#################################### External Image Storage ############## [external_image_storage] # You can choose between (s3, webdav) provider = s3 diff --git a/conf/sample.ini b/conf/sample.ini index d6e33153919..27f64ee8066 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -116,7 +116,7 @@ # in some UI views to notify that grafana or plugin update exists # This option does not cause any auto updates, nor send any information # only a GET request to http://grafana.net to get latest versions -check_for_updates = true +;check_for_updates = true # Google Analytics universal tracking code, only enabled if you specify an id here ;google_analytics_ua_id = @@ -224,6 +224,15 @@ check_for_updates = true ;team_ids = ;allowed_organizations = +#################################### Grafana.net Auth #################### +[auth.grafananet] +;enabled = false +;allow_sign_up = false +;client_id = some_id +;client_secret = some_secret +;scopes = user:email +;allowed_organizations = + #################################### Auth Proxy ########################## [auth.proxy] ;enabled = false diff --git a/pkg/api/login.go b/pkg/api/login.go index 789765ee01e..c3dea616abf 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -25,10 +25,12 @@ func LoginView(c *middleware.Context) { return } - viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google - viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub - viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic - viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName + enabledOAuths := make(map[string]interface{}) + for key, oauth := range setting.OAuthService.OAuthInfos { + enabledOAuths[key] = map[string]string{"name": oauth.Name} + } + + viewData.Settings["oauth"] = enabledOAuths viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp viewData.Settings["loginHint"] = setting.LoginHint viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 83c342937c1..bc222361b25 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -82,10 +82,11 @@ func OAuthLogin(ctx *middleware.Context) { return } cmd := m.CreateUserCommand{ - Login: userInfo.Email, - Email: userInfo.Email, - Name: userInfo.Name, - Company: userInfo.Company, + Login: userInfo.Email, + Email: userInfo.Email, + Name: userInfo.Name, + Company: userInfo.Company, + DefaultOrgRole: userInfo.Role, } if err = bus.Dispatch(&cmd); err != nil { diff --git a/pkg/models/models.go b/pkg/models/models.go index 5a53cfdabb3..3f4b27ed6ab 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -7,4 +7,5 @@ const ( GOOGLE TWITTER GENERIC + GRAFANANET ) diff --git a/pkg/models/user.go b/pkg/models/user.go index a231156b7b0..d2dcdf0a5c9 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -44,15 +44,16 @@ func (u *User) NameOrFallback() string { // COMMANDS type CreateUserCommand struct { - Email string - Login string - Name string - Company string - OrgName string - Password string - EmailVerified bool - IsAdmin bool - SkipOrgSetup bool + Email string + Login string + Name string + Company string + OrgName string + Password string + EmailVerified bool + IsAdmin bool + SkipOrgSetup bool + DefaultOrgRole string Result User } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 3dc685cd7e5..bbf21296519 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -128,7 +128,11 @@ func CreateUser(cmd *m.CreateUserCommand) error { } if setting.AutoAssignOrg && !user.IsAdmin { - orgUser.Role = m.RoleType(setting.AutoAssignOrgRole) + if len(cmd.DefaultOrgRole) > 0 { + orgUser.Role = m.RoleType(cmd.DefaultOrgRole) + } else { + orgUser.Role = m.RoleType(setting.AutoAssignOrgRole) + } } if _, err = sess.Insert(&orgUser); err != nil { diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index 71c4ade1468..8d51343e635 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -8,12 +8,11 @@ type OAuthInfo struct { AllowedDomains []string ApiUrl string AllowSignup bool + Name string } type OAuther struct { - GitHub, Google, Twitter, Generic bool - OAuthInfos map[string]*OAuthInfo - OAuthProviderName string + OAuthInfos map[string]*OAuthInfo } var OAuthService *OAuther diff --git a/pkg/social/grafananet_oauth.go b/pkg/social/grafananet_oauth.go new file mode 100644 index 00000000000..80c1aaedb45 --- /dev/null +++ b/pkg/social/grafananet_oauth.go @@ -0,0 +1,114 @@ +package social + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGrafanaNet struct { + *oauth2.Config + url string + allowedOrganizations []string + allowSignup bool +} + +func (s *SocialGrafanaNet) Type() int { + return int(models.GRAFANANET) +} + +func (s *SocialGrafanaNet) IsEmailAllowed(email string) bool { + return true +} + +func (s *SocialGrafanaNet) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGrafanaNet) 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 *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) { + var data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + Role string `json:"role"` + } + + var err error + client := s.Client(oauth2.NoContext, token) + r, err := client.Get(s.url + "/api/oauth2/user") + 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, + Role: data.Role, + } + + if !s.IsOrganizationMember(client) { + return nil, ErrMissingOrganizationMembership + } + + return userInfo, nil +} diff --git a/pkg/social/social.go b/pkg/social/social.go index 66d0f5fa778..4dbc70d71a9 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -15,6 +15,7 @@ type BasicUserInfo struct { Email string Login string Company string + Role string } type SocialConnector interface { @@ -36,7 +37,7 @@ func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "google", "generic_oauth"} + allOauthes := []string{"github", "google", "generic_oauth", "grafananet"} for _, name := range allOauthes { sec := setting.Cfg.Section("auth." + name) @@ -50,6 +51,7 @@ func NewOAuthService() { Enabled: sec.Key("enabled").MustBool(), AllowedDomains: sec.Key("allowed_domains").Strings(" "), AllowSignup: sec.Key("allow_sign_up").MustBool(), + Name: sec.Key("name").MustString(name), } if !info.Enabled { @@ -70,22 +72,18 @@ func NewOAuthService() { // GitHub. if name == "github" { - setting.OAuthService.GitHub = true - teamIds := sec.Key("team_ids").Ints(",") - allowedOrganizations := sec.Key("allowed_organizations").Strings(" ") SocialMap["github"] = &SocialGithub{ Config: &config, allowedDomains: info.AllowedDomains, apiUrl: info.ApiUrl, allowSignup: info.AllowSignup, - teamIds: teamIds, - allowedOrganizations: allowedOrganizations, + teamIds: sec.Key("team_ids").Ints(","), + allowedOrganizations: sec.Key("allowed_organizations").Strings(" "), } } // Google. if name == "google" { - setting.OAuthService.Google = true SocialMap["google"] = &SocialGoogle{ Config: &config, allowedDomains: info.AllowedDomains, apiUrl: info.ApiUrl, @@ -95,17 +93,33 @@ func NewOAuthService() { // 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, + teamIds: sec.Key("team_ids").Ints(","), + allowedOrganizations: sec.Key("allowed_organizations").Strings(" "), + } + } + + if name == "grafananet" { + config := oauth2.Config{ + ClientID: info.ClientId, + ClientSecret: info.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize", + TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token", + }, + RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name, + Scopes: info.Scopes, + } + + SocialMap["grafananet"] = &SocialGrafanaNet{ + Config: &config, + url: setting.GrafanaNetUrl, + allowSignup: info.AllowSignup, + allowedOrganizations: sec.Key("allowed_organizations").Strings(" "), } } } diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 8067202d511..8a336c16086 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -1,14 +1,15 @@ define([ 'angular', + 'lodash', '../core_module', 'app/core/config', ], -function (angular, coreModule, config) { +function (angular, _, coreModule, config) { 'use strict'; var failCodes = { - "1000": "Required Github team membership not fulfilled", - "1001": "Required Github organization membership not fulfilled", + "1000": "Required team membership not fulfilled", + "1001": "Required organization membership not fulfilled", "1002": "Required email domain not fulfilled", }; @@ -21,12 +22,10 @@ function (angular, coreModule, config) { contextSrv.sidemenu = false; - $scope.googleAuthEnabled = config.googleAuthEnabled; - $scope.githubAuthEnabled = config.githubAuthEnabled; - $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled; + $scope.oauth = config.oauth; + $scope.oauthEnabled = _.keys(config.oauth).length > 0; + $scope.allowUserPassLogin = config.allowUserPassLogin; - $scope.genericOAuthEnabled = config.genericOAuthEnabled; - $scope.oauthProviderName = config.oauthProviderName; $scope.disableUserSignUp = config.disableUserSignUp; $scope.loginHint = config.loginHint; diff --git a/public/app/partials/login.html b/public/app/partials/login.html index f4f5fb26d7e..648632fe1c4 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -51,18 +51,21 @@