Merge branch 'master' into ldap

This commit is contained in:
Torkel Ödegaard
2015-06-03 14:54:48 +02:00
124 changed files with 3339 additions and 1427 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) {
for _, key := range section.Keys() {
keyName := key.Name()
value := key.Value()
if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") {
if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) {
value = "************"
}
-56
View File
@@ -9,36 +9,6 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func AdminSearchUsers(c *middleware.Context) {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch users", err)
return
}
c.JSON(200, query.Result)
}
func AdminGetUser(c *middleware.Context) {
userId := c.ParamsInt64(":id")
query := m.GetUserByIdQuery{Id: userId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch user", err)
return
}
result := dtos.AdminUserListItem{
Name: query.Result.Name,
Email: query.Result.Email,
Login: query.Result.Login,
IsGrafanaAdmin: query.Result.IsAdmin,
}
c.JSON(200, result)
}
func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{
Login: form.Login,
@@ -70,32 +40,6 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
c.JsonOK("User created")
}
func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) {
userId := c.ParamsInt64(":id")
cmd := m.UpdateUserCommand{
UserId: userId,
Login: form.Login,
Email: form.Email,
Name: form.Name,
}
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
c.JsonApiErr(400, "Validation error, need specify either username or email", nil)
return
}
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "failed to update user", err)
return
}
c.JsonOK("User updated")
}
func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) {
userId := c.ParamsInt64(":id")
+51 -31
View File
@@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
reqAccountAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
bind := binding.Bind
// not logged in views
@@ -53,48 +53,71 @@ func Register(r *macaron.Macaron) {
// authed api
r.Group("/api", func() {
// user
// user (signed in)
r.Group("/user", func() {
r.Get("/", GetUser)
r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser)
r.Post("/using/:id", UserSetUsingOrg)
r.Get("/orgs", GetUserOrgList)
r.Post("/stars/dashboard/:id", StarDashboard)
r.Delete("/stars/dashboard/:id", UnstarDashboard)
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword)
r.Get("/", wrap(GetSignedInUser))
r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
r.Post("/using/:id", wrap(UserSetUsingOrg))
r.Get("/orgs", wrap(GetSignedInUserOrgList))
r.Post("/stars/dashboard/:id", wrap(StarDashboard))
r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
})
// account
// users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
}, reqGrafanaAdmin)
// current org
r.Group("/org", func() {
r.Get("/", GetOrg)
r.Post("/", bind(m.CreateOrgCommand{}), CreateOrg)
r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg)
r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser)
r.Get("/users", GetOrgUsers)
r.Patch("/users/:id", bind(m.UpdateOrgUserCommand{}), UpdateOrgUser)
r.Delete("/users/:id", RemoveOrgUser)
}, reqAccountAdmin)
r.Get("/", wrap(GetOrgCurrent))
r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrgCurrent))
r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
}, regOrgAdmin)
// create new org
r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg))
// search all orgs
r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
// orgs (admin routes)
r.Group("/orgs/:orgId", func() {
r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg))
r.Get("/users", wrap(GetOrgUsers))
r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
r.Delete("/users/:userId", wrap(RemoveOrgUser))
}, reqGrafanaAdmin)
// auth api keys
r.Group("/auth/keys", func() {
r.Get("/", GetApiKeys)
r.Post("/", bind(m.AddApiKeyCommand{}), AddApiKey)
r.Delete("/:id", DeleteApiKey)
}, reqAccountAdmin)
r.Get("/", wrap(GetApiKeys))
r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
r.Delete("/:id", wrap(DeleteApiKey))
}, regOrgAdmin)
// Data sources
r.Group("/datasources", func() {
r.Combo("/").
Get(GetDataSources).
Put(bind(m.AddDataSourceCommand{}), AddDataSource).
Post(bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
r.Get("/", GetDataSources)
r.Post("/", bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
r.Delete("/:id", DeleteDataSource)
r.Get("/:id", GetDataSourceById)
r.Get("/plugins", GetDataSourcePlugins)
}, reqAccountAdmin)
}, regOrgAdmin)
r.Get("/frontend/settings/", GetFrontendSettings)
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)
// Dashboard
r.Group("/dashboards", func() {
@@ -115,10 +138,7 @@ func Register(r *macaron.Macaron) {
// admin api
r.Group("/api/admin", func() {
r.Get("/settings", AdminGetSettings)
r.Get("/users", AdminSearchUsers)
r.Get("/users/:id", AdminGetUser)
r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser)
r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
r.Delete("/users/:id", AdminDeleteUser)
@@ -127,5 +147,5 @@ func Register(r *macaron.Macaron) {
// rendering
r.Get("/render/*", reqSignedIn, RenderToPng)
r.NotFound(NotFound)
r.NotFound(NotFoundHandler)
}
+12 -16
View File
@@ -8,12 +8,11 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func GetApiKeys(c *middleware.Context) {
func GetApiKeys(c *middleware.Context) Response {
query := m.GetApiKeysQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to list api keys", err)
return
return ApiError(500, "Failed to list api keys", err)
}
result := make([]*m.ApiKeyDTO, len(query.Result))
@@ -24,27 +23,26 @@ func GetApiKeys(c *middleware.Context) {
Role: t.Role,
}
}
c.JSON(200, result)
return Json(200, result)
}
func DeleteApiKey(c *middleware.Context) {
func DeleteApiKey(c *middleware.Context) Response {
id := c.ParamsInt64(":id")
cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
err := bus.Dispatch(cmd)
if err != nil {
c.JsonApiErr(500, "Failed to delete API key", err)
return
return ApiError(500, "Failed to delete API key", err)
}
c.JsonOK("API key deleted")
return ApiSuccess("API key deleted")
}
func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response {
if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil)
return
return ApiError(400, "Invalid role specified", nil)
}
cmd.OrgId = c.OrgId
@@ -53,14 +51,12 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
cmd.Key = newKeyInfo.HashedKey
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to add API key", err)
return
return ApiError(500, "Failed to add API key", err)
}
result := &dtos.NewApiKeyResult{
Name: cmd.Result.Name,
Key: newKeyInfo.ClientSecret,
}
Key: newKeyInfo.ClientSecret}
c.JSON(200, result)
return Json(200, result)
}
+122
View File
@@ -0,0 +1,122 @@
package api
import (
"encoding/json"
"net/http"
"github.com/Unknwon/macaron"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
)
var (
NotFound = ApiError(404, "Not found", nil)
ServerError = ApiError(500, "Server error", nil)
)
type Response interface {
WriteTo(out http.ResponseWriter)
}
type NormalResponse struct {
status int
body []byte
header http.Header
}
func wrap(action interface{}) macaron.Handler {
return func(c *middleware.Context) {
var res Response
val, err := c.Invoke(action)
if err == nil && val != nil && len(val) > 0 {
res = val[0].Interface().(Response)
} else {
res = ServerError
}
res.WriteTo(c.Resp)
}
}
func (r *NormalResponse) WriteTo(out http.ResponseWriter) {
header := out.Header()
for k, v := range r.header {
header[k] = v
}
out.WriteHeader(r.status)
out.Write(r.body)
}
func (r *NormalResponse) Cache(ttl string) *NormalResponse {
return r.Header("Cache-Control", "public,max-age="+ttl)
}
func (r *NormalResponse) Header(key, value string) *NormalResponse {
r.header.Set(key, value)
return r
}
// functions to create responses
func Empty(status int) *NormalResponse {
return Respond(status, nil)
}
func Json(status int, body interface{}) *NormalResponse {
return Respond(status, body).Header("Content-Type", "application/json")
}
func ApiSuccess(message string) *NormalResponse {
resp := make(map[string]interface{})
resp["message"] = message
return Respond(200, resp)
}
func ApiError(status int, message string, err error) *NormalResponse {
resp := make(map[string]interface{})
if err != nil {
log.Error(4, "%s: %v", message, err)
if setting.Env != setting.PROD {
resp["error"] = err.Error()
}
}
switch status {
case 404:
resp["message"] = "Not Found"
metrics.M_Api_Status_500.Inc(1)
case 500:
metrics.M_Api_Status_404.Inc(1)
resp["message"] = "Internal Server Error"
}
if message != "" {
resp["message"] = message
}
return Json(status, resp)
}
func Respond(status int, body interface{}) *NormalResponse {
var b []byte
var err error
switch t := body.(type) {
case []byte:
b = t
case string:
b = []byte(t)
default:
if b, err = json.Marshal(body); err != nil {
return ApiError(500, "body json marshal", err)
}
}
return &NormalResponse{
body: b,
status: status,
header: make(http.Header),
}
}
+1
View File
@@ -55,6 +55,7 @@ func GetDashboard(c *middleware.Context) {
Type: m.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_READ_ONLY_EDITOR,
},
}
+14
View File
@@ -1,9 +1,12 @@
package api
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
@@ -11,6 +14,16 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var dataProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy {
target, _ := url.Parse(ds.Url)
@@ -56,5 +69,6 @@ func ProxyDataSourceRequest(c *middleware.Context) {
proxyPath := c.Params("*")
proxy := NewReverseProxy(&query.Result, proxyPath)
proxy.Transport = dataProxyTransport
proxy.ServeHTTP(c.RW(), c.Req.Request)
}
+3 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
)
func GetDataSources(c *middleware.Context) {
@@ -94,11 +95,12 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
return
}
c.JsonOK("Datasource added")
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
}
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
err := bus.Dispatch(&cmd)
if err != nil {
+1
View File
@@ -34,6 +34,7 @@ type DashboardMeta struct {
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
+5
View File
@@ -54,6 +54,10 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
defaultDatasource = ds.Name
}
if len(ds.JsonData) > 0 {
dsMap["jsonData"] = ds.JsonData
}
if ds.Access == m.DS_ACCESS_DIRECT {
if ds.BasicAuth {
dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
@@ -95,6 +99,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"appSubUrl": setting.AppSubUrl,
"viewerRoleMode": setting.ViewerRoleMode,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
+1 -1
View File
@@ -59,7 +59,7 @@ func Index(c *middleware.Context) {
c.HTML(200, "index")
}
func NotFound(c *middleware.Context) {
func NotFoundHandler(c *middleware.Context) {
if c.IsApiRequest() {
c.JsonApiErr(404, "Not found", nil)
return
+2
View File
@@ -48,6 +48,8 @@ func OAuthLogin(ctx *middleware.Context) {
if err != nil {
if err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
} else if err == social.ErrMissingOrganizationMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
+48 -17
View File
@@ -8,17 +8,25 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func GetOrg(c *middleware.Context) {
query := m.GetOrgByIdQuery{Id: c.OrgId}
// GET /api/org
func GetOrgCurrent(c *middleware.Context) Response {
return getOrgHelper(c.OrgId)
}
// GET /api/orgs/:orgId
func GetOrgById(c *middleware.Context) Response {
return getOrgHelper(c.ParamsInt64(":orgId"))
}
func getOrgHelper(orgId int64) Response {
query := m.GetOrgByIdQuery{Id: orgId}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrOrgNotFound {
c.JsonApiErr(404, "Organization not found", err)
return
return ApiError(404, "Organization not found", err)
}
c.JsonApiErr(500, "Failed to get organization", err)
return
return ApiError(500, "Failed to get organization", err)
}
org := m.OrgDTO{
@@ -26,33 +34,56 @@ func GetOrg(c *middleware.Context) {
Name: query.Result.Name,
}
c.JSON(200, &org)
return Json(200, &org)
}
func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) {
// POST /api/orgs
func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
if !setting.AllowUserOrgCreate && !c.IsGrafanaAdmin {
c.JsonApiErr(401, "Access denied", nil)
return
return ApiError(401, "Access denied", nil)
}
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to create organization", err)
return
return ApiError(500, "Failed to create organization", err)
}
metrics.M_Api_Org_Create.Inc(1)
c.JsonOK("Organization created")
return ApiSuccess("Organization created")
}
func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) {
// PUT /api/org
func UpdateOrgCurrent(c *middleware.Context, cmd m.UpdateOrgCommand) Response {
cmd.OrgId = c.OrgId
return updateOrgHelper(cmd)
}
// PUT /api/orgs/:orgId
func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return updateOrgHelper(cmd)
}
func updateOrgHelper(cmd m.UpdateOrgCommand) Response {
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to update organization", err)
return
return ApiError(500, "Failed to update organization", err)
}
c.JsonOK("Organization updated")
return ApiSuccess("Organization updated")
}
func SearchOrgs(c *middleware.Context) Response {
query := m.SearchOrgsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: 0,
Limit: 1000,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search orgs", err)
}
return Json(200, query.Result)
}
+77 -39
View File
@@ -6,77 +6,115 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) {
// POST /api/org/users
func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.OrgId
return addOrgUserHelper(cmd)
}
// POST /api/orgs/:orgId/users
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return addOrgUserHelper(cmd)
}
func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil)
return
return ApiError(400, "Invalid role specified", nil)
}
userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
err := bus.Dispatch(&userQuery)
if err != nil {
c.JsonApiErr(404, "User not found", nil)
return
return ApiError(404, "User not found", nil)
}
userToAdd := userQuery.Result
if userToAdd.Id == c.UserId {
c.JsonApiErr(400, "Cannot add yourself as user", nil)
return
}
// if userToAdd.Id == c.UserId {
// return ApiError(400, "Cannot add yourself as user", nil)
// }
cmd.OrgId = c.OrgId
cmd.UserId = userToAdd.Id
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Could not add user to organization", err)
return
return ApiError(500, "Could not add user to organization", err)
}
c.JsonOK("User added to organization")
return ApiSuccess("User added to organization")
}
func GetOrgUsers(c *middleware.Context) {
query := m.GetOrgUsersQuery{OrgId: c.OrgId}
// GET /api/org/users
func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
return getOrgUsersHelper(c.OrgId)
}
// GET /api/orgs/:orgId/users
func GetOrgUsers(c *middleware.Context) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId"))
}
func getOrgUsersHelper(orgId int64) Response {
query := m.GetOrgUsersQuery{OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get account user", err)
return
return ApiError(500, "Failed to get account user", err)
}
c.JSON(200, query.Result)
return Json(200, query.Result)
}
func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) {
if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil)
return
}
cmd.UserId = c.ParamsInt64(":id")
// PATCH /api/org/users/:userId
func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed update org user", err)
return
}
c.JsonOK("Organization user updated")
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
func RemoveOrgUser(c *middleware.Context) {
userId := c.ParamsInt64(":id")
// PATCH /api/orgs/:orgId/users/:userId
func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
cmd := m.RemoveOrgUserCommand{OrgId: c.OrgId, UserId: userId}
func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
if !cmd.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
c.JsonApiErr(400, "Cannot remove last organization admin", nil)
return
return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
}
c.JsonApiErr(500, "Failed to remove user from organization", err)
return ApiError(500, "Failed update org user", err)
}
c.JsonOK("User removed from organization")
return ApiSuccess("Organization user updated")
}
// DELETE /api/org/users/:userId
func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
return removeOrgUserHelper(c.OrgId, userId)
}
// DELETE /api/orgs/:orgId/users/:userId
func RemoveOrgUser(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
orgId := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgId, userId)
}
func removeOrgUserHelper(orgId int64, userId int64) Response {
cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
return ApiError(400, "Cannot remove last organization admin", nil)
}
return ApiError(500, "Failed to remove user from organization", err)
}
return ApiSuccess("User removed from organization")
}
+3 -3
View File
@@ -8,17 +8,17 @@ import (
func Search(c *middleware.Context) {
query := c.Query("query")
tag := c.Query("tag")
tags := c.QueryStrings("tag")
starred := c.Query("starred")
limit := c.QueryInt("limit")
if limit == 0 {
limit = 200
limit = 1000
}
searchQuery := search.Query{
Title: query,
Tag: tag,
Tags: tags,
UserId: c.UserId,
Limit: limit,
IsStarred: starred == "true",
+12 -22
View File
@@ -6,45 +6,35 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func StarDashboard(c *middleware.Context) {
func StarDashboard(c *middleware.Context) Response {
if !c.IsSignedIn {
c.JsonApiErr(412, "You need to sign in to star dashboards", nil)
return
return ApiError(412, "You need to sign in to star dashboards", nil)
}
var cmd = m.StarDashboardCommand{
UserId: c.UserId,
DashboardId: c.ParamsInt64(":id"),
}
cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 {
c.JsonApiErr(400, "Missing dashboard id", nil)
return
return ApiError(400, "Missing dashboard id", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to star dashboard", err)
return
return ApiError(500, "Failed to star dashboard", err)
}
c.JsonOK("Dashboard starred!")
return ApiSuccess("Dashboard starred!")
}
func UnstarDashboard(c *middleware.Context) {
var cmd = m.UnstarDashboardCommand{
UserId: c.UserId,
DashboardId: c.ParamsInt64(":id"),
}
func UnstarDashboard(c *middleware.Context) Response {
cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 {
c.JsonApiErr(400, "Missing dashboard id", nil)
return
return ApiError(400, "Missing dashboard id", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to unstar dashboard", err)
return
return ApiError(500, "Failed to unstar dashboard", err)
}
c.JsonOK("Dashboard unstarred")
return ApiSuccess("Dashboard unstarred")
}
+78 -49
View File
@@ -7,44 +7,71 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetUser(c *middleware.Context) {
query := m.GetUserProfileQuery{UserId: c.UserId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get user", err)
return
}
c.JSON(200, query.Result)
// GET /api/user (current authenticated user)
func GetSignedInUser(c *middleware.Context) Response {
return getUserUserProfile(c.UserId)
}
func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) {
// GET /api/user/:id
func GetUserById(c *middleware.Context) Response {
return getUserUserProfile(c.ParamsInt64(":id"))
}
func getUserUserProfile(userId int64) Response {
query := m.GetUserProfileQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get user", err)
}
return Json(200, query.Result)
}
// POST /api/user
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(400, "Failed to update user", err)
return
}
c.JsonOK("User updated")
return handleUpdateUser(cmd)
}
func GetUserOrgList(c *middleware.Context) {
query := m.GetUserOrgListQuery{UserId: c.UserId}
// POST /api/users/:id
func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.ParamsInt64(":id")
return handleUpdateUser(cmd)
}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get user organizations", err)
return
}
for _, ac := range query.Result {
if ac.OrgId == c.OrgId {
ac.IsUsing = true
break
func handleUpdateUser(cmd m.UpdateUserCommand) Response {
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
return ApiError(400, "Validation error, need specify either username or email", nil)
}
}
c.JSON(200, query.Result)
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "failed to update user", err)
}
return ApiSuccess("User updated")
}
// GET /api/user/orgs
func GetSignedInUserOrgList(c *middleware.Context) Response {
return getUserOrgList(c.UserId)
}
// GET /api/user/:id/orgs
func GetUserOrgList(c *middleware.Context) Response {
return getUserOrgList(c.ParamsInt64(":id"))
}
func getUserOrgList(userId int64) Response {
query := m.GetUserOrgListQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Faile to get user organziations", err)
}
return Json(200, query.Result)
}
func validateUsingOrg(userId int64, orgId int64) bool {
@@ -65,53 +92,55 @@ func validateUsingOrg(userId int64, orgId int64) bool {
return valid
}
func UserSetUsingOrg(c *middleware.Context) {
// POST /api/user/using/:id
func UserSetUsingOrg(c *middleware.Context) Response {
orgId := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
c.JsonApiErr(401, "Not a valid organization", nil)
return
return ApiError(401, "Not a valid organization", nil)
}
cmd := m.SetUsingOrgCommand{
UserId: c.UserId,
OrgId: orgId,
}
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed change active organization", err)
return
return ApiError(500, "Failed change active organization", err)
}
c.JsonOK("Active organization changed")
return ApiSuccess("Active organization changed")
}
func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) {
func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response {
userQuery := m.GetUserByIdQuery{Id: c.UserId}
if err := bus.Dispatch(&userQuery); err != nil {
c.JsonApiErr(500, "Could not read user from database", err)
return
return ApiError(500, "Could not read user from database", err)
}
passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
if passwordHashed != userQuery.Result.Password {
c.JsonApiErr(401, "Invalid old password", nil)
return
return ApiError(401, "Invalid old password", nil)
}
if len(cmd.NewPassword) < 4 {
c.JsonApiErr(400, "New password too short", nil)
return
return ApiError(400, "New password too short", nil)
}
cmd.UserId = c.UserId
cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to change user password", err)
return
return ApiError(500, "Failed to change user password", err)
}
c.JsonOK("User password changed")
return ApiSuccess("User password changed")
}
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
}
+1 -1
View File
@@ -26,7 +26,7 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png"
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width,
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", "--ssl-protocol=any", scriptPath, "url="+params.Url, "width="+params.Width,
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
"domain="+setting.Domain, "sessionid="+params.SessionId)
stdout, err := cmd.StdoutPipe()
+1 -1
View File
@@ -69,7 +69,6 @@ type AddDataSourceCommand struct {
// Also acts as api DTO
type UpdateDataSourceCommand struct {
Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"`
@@ -84,6 +83,7 @@ type UpdateDataSourceCommand struct {
JsonData map[string]interface{} `json:"jsonData"`
OrgId int64 `json:"-"`
Id int64 `json:"-"`
}
type DeleteDataSourceCommand struct {
+10 -6
View File
@@ -48,8 +48,13 @@ type GetOrgByNameQuery struct {
Result *Org
}
type GetOrgListQuery struct {
Result []*Org
type SearchOrgsQuery struct {
Query string
Name string
Limit int
Page int
Result []*OrgDTO
}
type OrgDTO struct {
@@ -58,8 +63,7 @@ type OrgDTO struct {
}
type UserOrgDTO struct {
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Role RoleType `json:"role"`
IsUsing bool `json:"isUsing"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Role RoleType `json:"role"`
}
+5 -4
View File
@@ -15,13 +15,14 @@ var (
type RoleType string
const (
ROLE_VIEWER RoleType = "Viewer"
ROLE_EDITOR RoleType = "Editor"
ROLE_ADMIN RoleType = "Admin"
ROLE_VIEWER RoleType = "Viewer"
ROLE_EDITOR RoleType = "Editor"
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
ROLE_ADMIN RoleType = "Admin"
)
func (r RoleType) IsValid() bool {
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
}
type OrgUser struct {
+1
View File
@@ -133,6 +133,7 @@ type UserProfileDTO struct {
Name string `json:"name"`
Login string `json:"login"`
Theme string `json:"theme"`
OrgId int64 `json:"orgId"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
}
+41 -2
View File
@@ -33,9 +33,7 @@ func searchHandler(query *Query) error {
dashQuery := FindPersistedDashboardsQuery{
Title: query.Title,
Tag: query.Tag,
UserId: query.UserId,
Limit: query.Limit,
IsStarred: query.IsStarred,
OrgId: query.OrgId,
}
@@ -55,8 +53,30 @@ func searchHandler(query *Query) error {
hits = append(hits, jsonHits...)
}
// filter out results with tag filter
if len(query.Tags) > 0 {
filtered := HitList{}
for _, hit := range hits {
if hasRequiredTags(query.Tags, hit.Tags) {
filtered = append(filtered, hit)
}
}
hits = filtered
}
// sort main result array
sort.Sort(hits)
if len(hits) > query.Limit {
hits = hits[0:query.Limit]
}
// sort tags
for _, hit := range hits {
sort.Strings(hit.Tags)
}
// add isStarred info
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
return err
}
@@ -65,6 +85,25 @@ func searchHandler(query *Query) error {
return nil
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func hasRequiredTags(queryTags, hitTags []string) bool {
for _, queryTag := range queryTags {
if !stringInSlice(queryTag, hitTags) {
return false
}
}
return true
}
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
query := m.GetUserStarsQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
+61
View File
@@ -0,0 +1,61 @@
package search
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestSearch(t *testing.T) {
Convey("Given search query", t, func() {
jsonDashIndex = NewJsonDashIndex("../../public/dashboards/")
query := Query{Limit: 2000}
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
query.Result = HitList{
&Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}},
}
return nil
})
bus.AddHandler("test", func(query *m.GetUserStarsQuery) error {
query.Result = map[int64]bool{10: true, 12: true}
return nil
})
Convey("That is empty", func() {
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return sorted results", func() {
So(query.Result[0].Title, ShouldEqual, "AABB")
So(query.Result[1].Title, ShouldEqual, "BBAA")
So(query.Result[2].Title, ShouldEqual, "CCAA")
})
Convey("should return sorted tags", func() {
So(query.Result[1].Tags[0], ShouldEqual, "AA")
So(query.Result[1].Tags[1], ShouldEqual, "BB")
So(query.Result[1].Tags[2], ShouldEqual, "EE")
})
})
Convey("That filters by tag", func() {
query.Tags = []string{"BB", "AA"}
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return correct results", func() {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Title, ShouldEqual, "BBAA")
So(query.Result[1].Title, ShouldEqual, "CCAA")
})
})
})
}
+4 -7
View File
@@ -47,18 +47,15 @@ func (index *JsonDashIndex) updateLoop() {
func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
results := make([]*Hit, 0)
if query.IsStarred {
return results, nil
}
for _, item := range index.items {
if len(results) > query.Limit {
break
}
// filter out results with tag filter
if query.Tag != "" {
if !strings.Contains(item.TagsCsv, query.Tag) {
continue
}
}
// add results with matchig title filter
if strings.Contains(item.TitleLower, query.Title) {
results = append(results, &Hit{
+9 -2
View File
@@ -17,19 +17,26 @@ func TestJsonDashIndex(t *testing.T) {
})
Convey("Should be able to search index", func() {
res, err := index.Search(&Query{Title: "", Tag: "", Limit: 20})
res, err := index.Search(&Query{Title: "", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
})
Convey("Should be able to search index by title", func() {
res, err := index.Search(&Query{Title: "home", Tag: "", Limit: 20})
res, err := index.Search(&Query{Title: "home", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 1)
So(res[0].Title, ShouldEqual, "Home")
})
Convey("Should not return when starred is filtered", func() {
res, err := index.Search(&Query{Title: "", IsStarred: true})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 0)
})
})
}
+1 -3
View File
@@ -26,7 +26,7 @@ func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
type Query struct {
Title string
Tag string
Tags []string
OrgId int64
UserId int64
Limit int
@@ -37,10 +37,8 @@ type Query struct {
type FindPersistedDashboardsQuery struct {
Title string
Tag string
OrgId int64
UserId int64
Limit int
IsStarred bool
Result HitList
+14 -13
View File
@@ -109,25 +109,26 @@ func Setup() error {
}
func publish(routingKey string, msgString []byte) {
err := channel.Publish(
exchange, //exchange
routingKey, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: msgString,
},
)
if err != nil {
for {
err := channel.Publish(
exchange, //exchange
routingKey, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: msgString,
},
)
if err == nil {
return
}
// failures are most likely because the connection was lost.
// the connection will be re-established, so just keep
// retrying every 2seconds until we successfully publish.
time.Sleep(2 * time.Second)
fmt.Println("publish failed, retrying.")
publish(routingKey, msgString)
}
return
}
func eventListener(event interface{}) error {
+1 -10
View File
@@ -150,16 +150,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
params = append(params, "%"+query.Title+"%")
}
if len(query.Tag) > 0 {
sql.WriteString(" AND dashboard_tag.term=?")
params = append(params, query.Tag)
}
if query.Limit == 0 || query.Limit > 10000 {
query.Limit = 300
}
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit))
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res)
-12
View File
@@ -99,18 +99,6 @@ func TestDashboardDataAccess(t *testing.T) {
So(len(hit.Tags), ShouldEqual, 2)
})
Convey("Should be able to search for dashboards using tags", func() {
query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1}
query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1}
err := SearchDashboards(&query1)
err = SearchDashboards(&query2)
So(err, ShouldBeNil)
So(len(query1.Result), ShouldEqual, 1)
So(len(query2.Result), ShouldEqual, 0)
})
Convey("Should not be able to save dashboard with same name", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
+14 -3
View File
@@ -14,12 +14,23 @@ func init() {
bus.AddHandler("sql", CreateOrg)
bus.AddHandler("sql", UpdateOrg)
bus.AddHandler("sql", GetOrgByName)
bus.AddHandler("sql", GetOrgList)
bus.AddHandler("sql", SearchOrgs)
bus.AddHandler("sql", DeleteOrg)
}
func GetOrgList(query *m.GetOrgListQuery) error {
return x.Find(&query.Result)
func SearchOrgs(query *m.SearchOrgsQuery) error {
query.Result = make([]*m.OrgDTO, 0)
sess := x.Table("org")
if query.Query != "" {
sess.Where("name LIKE ?", query.Query+"%")
}
if query.Name != "" {
sess.Where("name=?", query.Name)
}
sess.Limit(query.Limit, query.Limit*query.Page)
sess.Cols("id", "name")
err := sess.Find(&query.Result)
return err
}
func GetOrgById(query *m.GetOrgByIdQuery) error {
+8 -1
View File
@@ -142,11 +142,18 @@ func TestAccountDataAccess(t *testing.T) {
})
})
Convey("Cannot delete last admin account user", func() {
Convey("Cannot delete last admin org user", func() {
cmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id}
err := RemoveOrgUser(&cmd)
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
Convey("Cannot update role so no one is admin user", func() {
cmd := m.UpdateOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id, Role: m.ROLE_VIEWER}
err := UpdateOrgUser(&cmd)
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
})
})
})
+20 -12
View File
@@ -48,7 +48,11 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
orgUser.Role = cmd.Role
orgUser.Updated = time.Now()
_, err = sess.Id(orgUser.Id).Update(&orgUser)
return err
if err != nil {
return err
}
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
})
}
@@ -72,16 +76,20 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
return err
}
// validate that there is an admin user left
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", cmd.OrgId)
if err != nil {
return err
}
if len(res) == 0 {
return m.ErrLastOrgAdmin
}
return err
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
})
}
func validateOneAdminLeftInOrg(orgId int64, sess *xorm.Session) error {
// validate that there is an admin user left
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", orgId)
if err != nil {
return err
}
if len(res) == 0 {
return m.ErrLastOrgAdmin
}
return err
}
+11 -4
View File
@@ -231,10 +231,12 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
}
query.Result = m.UserProfileDTO{
Name: user.Name,
Email: user.Email,
Login: user.Login,
Theme: user.Theme,
Name: user.Name,
Email: user.Email,
Login: user.Login,
Theme: user.Theme,
IsGrafanaAdmin: user.IsAdmin,
OrgId: user.OrgId,
}
return err
@@ -282,6 +284,11 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
return m.ErrUserNotFound
}
if user.OrgRole == "" {
user.OrgId = -1
user.OrgName = "Org missing"
}
query.Result = &user
return err
}
+2
View File
@@ -79,6 +79,7 @@ var (
AllowUserOrgCreate bool
AutoAssignOrg bool
AutoAssignOrgRole string
ViewerRoleMode string
// Http auth
AdminUser string
@@ -383,6 +384,7 @@ func NewConfigContext(args *CommandLineArgs) {
AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
ViewerRoleMode = users.Key("viewer_role_mode").In("default", []string{"default", "strinct"})
// anonymous access
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
+145 -28
View File
@@ -78,12 +78,14 @@ func NewOAuthService() {
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,
Config: &config,
allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
teamIds: teamIds,
allowedOrganizations: allowedOrganizations,
}
}
@@ -115,16 +117,21 @@ func isEmailAllowed(email string, allowedDomains []string) bool {
type SocialGithub struct {
*oauth2.Config
allowedDomains []string
apiUrl string
allowSignup bool
teamIds []int
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)
}
@@ -137,26 +144,131 @@ func (s *SocialGithub) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool {
var data struct {
Url string `json:"url"`
State string `json:"state"`
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
if len(s.teamIds) == 0 {
return true
}
membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username)
r, err := client.Get(membershipUrl)
teamMemberships, err := s.FetchTeamMemberships(client)
if err != nil {
return false
}
defer r.Body.Close()
for _, teamId := range s.teamIds {
for _, membershipId := range teamMemberships {
if teamId == membershipId {
return true
}
}
}
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
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
}
active := data.State == "active"
return active
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("https://api.github.com/user/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("https://api.github.com/user/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("https://api.github.com/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 *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
@@ -185,17 +297,22 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
Email: data.Email,
}
if len(s.teamIds) > 0 {
for _, teamId := range s.teamIds {
if s.IsTeamMember(client, data.Name, teamId) {
return userInfo, nil
}
}
if !s.IsTeamMember(client) {
return nil, ErrMissingTeamMembership
} else {
return userInfo, nil
}
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
}
// ________ .__