Browse Source

enhance quota support.

now includes:
- perOrg (users, dashboards, datasources, api_keys)
- perUser (orgs)
- global (users, orgs, dashboards, datasources, api_keys, sessions)
woodsaj 10 years ago
parent
commit
6488324cf1

+ 32 - 3
conf/defaults.ini

@@ -87,6 +87,7 @@ cookie_secure = false
 
 
 # Session life time, default is 86400
 # Session life time, default is 86400
 session_life_time = 86400
 session_life_time = 86400
+gc_interval_time = 86400
 
 
 #################################### Analytics ####################################
 #################################### Analytics ####################################
 [analytics]
 [analytics]
@@ -253,9 +254,37 @@ exchange = grafana_events
 enabled = false
 enabled = false
 path = /var/lib/grafana/dashboards
 path = /var/lib/grafana/dashboards
 
 
+#################################### Usage Quotas ##########################
 [quota]
 [quota]
 enabled = false
 enabled = false
-user = 10
-dashboard = 100
-data_source = 10
 
 
+#### set quotas to -1 to make unlimited. ####
+# limit number of users per Org.
+org_user = 10
+
+# limit number of dashboards per Org.
+org_dashboard = 100
+
+# limit number of data_sources per Org.
+org_data_source = 10
+
+# limit number of api_keys per Org.
+org_api_key = 10
+
+# limit number of orgs a user can create.
+user_org = 10
+
+# Global limit of users.
+global_user = -1
+
+# global limit of orgs.
+global_org = -1
+
+# global limit of dashboards
+global_dashboard = -1
+
+# global limit of api_keys
+global_api_key = -1
+
+# global limit on number of logged in users.
+global_session = -1

+ 15 - 12
pkg/api/api.go

@@ -14,14 +14,14 @@ func Register(r *macaron.Macaron) {
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
 	regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
 	regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
-	limitQuota := middleware.LimitQuota
+	quota := middleware.Quota
 	bind := binding.Bind
 	bind := binding.Bind
 
 
 	// not logged in views
 	// not logged in views
 	r.Get("/", reqSignedIn, Index)
 	r.Get("/", reqSignedIn, Index)
 	r.Get("/logout", Logout)
 	r.Get("/logout", Logout)
-	r.Post("/login", bind(dtos.LoginCommand{}), wrap(LoginPost))
-	r.Get("/login/:name", OAuthLogin)
+	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), wrap(LoginPost))
+	r.Get("/login/:name", quota("session"), OAuthLogin)
 	r.Get("/login", LoginView)
 	r.Get("/login", LoginView)
 	r.Get("/invite/:code", Index)
 	r.Get("/invite/:code", Index)
 
 
@@ -45,7 +45,7 @@ func Register(r *macaron.Macaron) {
 	// sign up
 	// sign up
 	r.Get("/signup", Index)
 	r.Get("/signup", Index)
 	r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
 	r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
-	r.Post("/api/user/signup", bind(dtos.SignUpForm{}), wrap(SignUp))
+	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), wrap(SignUp))
 	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), wrap(SignUpStep2))
 	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), wrap(SignUpStep2))
 
 
 	// invited
 	// invited
@@ -67,7 +67,7 @@ func Register(r *macaron.Macaron) {
 	r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
 	r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
 
 
 	// api renew session based on remember cookie
 	// api renew session based on remember cookie
-	r.Get("/api/login/ping", LoginApiPing)
+	r.Get("/api/login/ping", quota("session"), LoginApiPing)
 
 
 	// authed api
 	// authed api
 	r.Group("/api", func() {
 	r.Group("/api", func() {
@@ -81,6 +81,7 @@ func Register(r *macaron.Macaron) {
 			r.Post("/stars/dashboard/:id", wrap(StarDashboard))
 			r.Post("/stars/dashboard/:id", wrap(StarDashboard))
 			r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
 			r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
 			r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
 			r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
+			r.Get("/quotas", wrap(GetUserQuotas))
 		})
 		})
 
 
 		// users (admin permission required)
 		// users (admin permission required)
@@ -94,26 +95,26 @@ func Register(r *macaron.Macaron) {
 		// org information available to all users.
 		// org information available to all users.
 		r.Group("/org", func() {
 		r.Group("/org", func() {
 			r.Get("/", wrap(GetOrgCurrent))
 			r.Get("/", wrap(GetOrgCurrent))
-			r.Get("/quotas", wrap(GetQuotas))
+			r.Get("/quotas", wrap(GetOrgQuotas))
 		})
 		})
 
 
 		// current org
 		// current org
 		r.Group("/org", func() {
 		r.Group("/org", func() {
 			r.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
 			r.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
 			r.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
 			r.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
-			r.Post("/users", limitQuota(m.QUOTA_USER), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
+			r.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
 			r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
 			r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
 			r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
 			r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
 			r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
 			r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
 
 
 			// invites
 			// invites
 			r.Get("/invites", wrap(GetPendingOrgInvites))
 			r.Get("/invites", wrap(GetPendingOrgInvites))
-			r.Post("/invites", bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
+			r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
 		}, regOrgAdmin)
 		}, regOrgAdmin)
 
 
 		// create new org
 		// create new org
-		r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg))
+		r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
 
 
 		// search all orgs
 		// search all orgs
 		r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
 		r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
@@ -129,20 +130,20 @@ func Register(r *macaron.Macaron) {
 			r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
 			r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
 			r.Delete("/users/:userId", wrap(RemoveOrgUser))
 			r.Delete("/users/:userId", wrap(RemoveOrgUser))
 			r.Get("/quotas", wrap(GetOrgQuotas))
 			r.Get("/quotas", wrap(GetOrgQuotas))
-			r.Put("/quotas/:target", bind(m.UpdateQuotaCmd{}), wrap(UpdateOrgQuota))
+			r.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), wrap(UpdateOrgQuota))
 		}, reqGrafanaAdmin)
 		}, reqGrafanaAdmin)
 
 
 		// auth api keys
 		// auth api keys
 		r.Group("/auth/keys", func() {
 		r.Group("/auth/keys", func() {
 			r.Get("/", wrap(GetApiKeys))
 			r.Get("/", wrap(GetApiKeys))
-			r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
+			r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
 			r.Delete("/:id", wrap(DeleteApiKey))
 			r.Delete("/:id", wrap(DeleteApiKey))
 		}, regOrgAdmin)
 		}, regOrgAdmin)
 
 
 		// Data sources
 		// Data sources
 		r.Group("/datasources", func() {
 		r.Group("/datasources", func() {
 			r.Get("/", GetDataSources)
 			r.Get("/", GetDataSources)
-			r.Post("/", limitQuota(m.QUOTA_DATASOURCE), bind(m.AddDataSourceCommand{}), AddDataSource)
+			r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
 			r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
 			r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
 			r.Delete("/:id", DeleteDataSource)
 			r.Delete("/:id", DeleteDataSource)
 			r.Get("/:id", GetDataSourceById)
 			r.Get("/:id", GetDataSourceById)
@@ -177,6 +178,8 @@ func Register(r *macaron.Macaron) {
 		r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
 		r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
 		r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
 		r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
 		r.Delete("/users/:id", AdminDeleteUser)
 		r.Delete("/users/:id", AdminDeleteUser)
+		r.Get("/users/:id/quotas", wrap(GetUserQuotas))
+		r.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota))
 	}, reqGrafanaAdmin)
 	}, reqGrafanaAdmin)
 
 
 	// rendering
 	// rendering

+ 1 - 1
pkg/api/dashboard.go

@@ -88,7 +88,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
 
 
 	dash := cmd.GetDashboardModel()
 	dash := cmd.GetDashboardModel()
 	if dash.Id == 0 {
 	if dash.Id == 0 {
-		limitReached, err := middleware.QuotaReached(cmd.OrgId, m.QUOTA_DASHBOARD)
+		limitReached, err := middleware.QuotaReached(c, "dashboard")
 		if err != nil {
 		if err != nil {
 			c.JsonApiErr(500, "failed to get quota", err)
 			c.JsonApiErr(500, "failed to get quota", err)
 			return
 			return

+ 9 - 1
pkg/api/login_oauth.go

@@ -74,7 +74,15 @@ func OAuthLogin(ctx *middleware.Context) {
 			ctx.Redirect(setting.AppSubUrl + "/login")
 			ctx.Redirect(setting.AppSubUrl + "/login")
 			return
 			return
 		}
 		}
-
+		limitReached, err := middleware.QuotaReached(ctx, "user")
+		if err != nil {
+			ctx.Handle(500, "Failed to get user quota", err)
+			return
+		}
+		if limitReached {
+			ctx.Redirect(setting.AppSubUrl + "/login")
+			return
+		}
 		cmd := m.CreateUserCommand{
 		cmd := m.CreateUserCommand{
 			Login:   userInfo.Email,
 			Login:   userInfo.Email,
 			Email:   userInfo.Email,
 			Email:   userInfo.Email,

+ 25 - 9
pkg/api/quota.go

@@ -11,7 +11,7 @@ func GetOrgQuotas(c *middleware.Context) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
 		return ApiError(404, "Quotas not enabled", nil)
 		return ApiError(404, "Quotas not enabled", nil)
 	}
 	}
-	query := m.GetQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
+	query := m.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return ApiError(500, "Failed to get org quotas", err)
 		return ApiError(500, "Failed to get org quotas", err)
@@ -20,28 +20,44 @@ func GetOrgQuotas(c *middleware.Context) Response {
 	return Json(200, query.Result)
 	return Json(200, query.Result)
 }
 }
 
 
-// allow users to query the quotas of their own org.
-func GetQuotas(c *middleware.Context) Response {
+func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
 		return ApiError(404, "Quotas not enabled", nil)
 		return ApiError(404, "Quotas not enabled", nil)
 	}
 	}
-	query := m.GetQuotasQuery{OrgId: c.OrgId}
+	cmd.OrgId = c.ParamsInt64(":orgId")
+	cmd.Target = c.Params(":target")
+
+	if _, ok := m.QuotaToMap(setting.Quota.Org)[cmd.Target]; !ok {
+		return ApiError(404, "Invalid quota target", nil)
+	}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to update org quotas", err)
+	}
+	return ApiSuccess("Organization quota updated")
+}
+
+func GetUserQuotas(c *middleware.Context) Response {
+	if !setting.Quota.Enabled {
+		return ApiError(404, "Quotas not enabled", nil)
+	}
+	query := m.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to get quotas", err)
+		return ApiError(500, "Failed to get org quotas", err)
 	}
 	}
 
 
 	return Json(200, query.Result)
 	return Json(200, query.Result)
 }
 }
 
 
-func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateQuotaCmd) Response {
+func UpdateUserQuota(c *middleware.Context, cmd m.UpdateUserQuotaCmd) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
 		return ApiError(404, "Quotas not enabled", nil)
 		return ApiError(404, "Quotas not enabled", nil)
 	}
 	}
-	cmd.OrgId = c.ParamsInt64(":orgId")
-	cmd.Target = m.QuotaTarget(c.Params(":target"))
+	cmd.UserId = c.ParamsInt64(":id")
+	cmd.Target = c.Params(":target")
 
 
-	if !cmd.Target.IsValid() {
+	if _, ok := m.QuotaToMap(setting.Quota.User)[cmd.Target]; !ok {
 		return ApiError(404, "Invalid quota target", nil)
 		return ApiError(404, "Invalid quota target", nil)
 	}
 	}
 
 

+ 75 - 12
pkg/middleware/middleware.go

@@ -1,6 +1,7 @@
 package middleware
 package middleware
 
 
 import (
 import (
+	"fmt"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -254,33 +255,95 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
 	ctx.JSON(status, resp)
 	ctx.JSON(status, resp)
 }
 }
 
 
-func LimitQuota(target m.QuotaTarget) macaron.Handler {
+func Quota(target string) macaron.Handler {
 	return func(c *Context) {
 	return func(c *Context) {
-		limitReached, err := QuotaReached(c.OrgId, target)
+		limitReached, err := QuotaReached(c, target)
 		if err != nil {
 		if err != nil {
 			c.JsonApiErr(500, "failed to get quota", err)
 			c.JsonApiErr(500, "failed to get quota", err)
 			return
 			return
 		}
 		}
 		if limitReached {
 		if limitReached {
-			c.JsonApiErr(403, "Quota reached", nil)
+			c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
 			return
 			return
 		}
 		}
 	}
 	}
 }
 }
 
 
-func QuotaReached(org_id int64, target m.QuotaTarget) (bool, error) {
+func QuotaReached(c *Context, target string) (bool, error) {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
 		return false, nil
 		return false, nil
 	}
 	}
-	if !target.IsValid() {
-		return true, m.ErrInvalidQuotaTarget
-	}
-	query := m.GetQuotaByTargetQuery{OrgId: org_id, Target: target}
-	if err := bus.Dispatch(&query); err != nil {
-		return true, err
+
+	// get the list of scopes that this target is valid for. Org, User, Global
+	scopes, err := m.GetQuotaScopes(target)
+	if err != nil {
+		return false, err
 	}
 	}
-	if query.Result.Used >= query.Result.Limit {
-		return true, nil
+	log.Info(fmt.Sprintf("checking quota for %s in scopes %v", target, scopes))
+
+	for _, scope := range scopes {
+		log.Info(fmt.Sprintf("checking scope %s", scope.Name))
+		switch scope.Name {
+		case "global":
+			if scope.DefaultLimit < 0 {
+				continue
+			}
+			if scope.DefaultLimit == 0 {
+				return true, nil
+			}
+			if target == "session" {
+				usedSessions := sessionManager.Count()
+				if int64(usedSessions) > scope.DefaultLimit {
+					log.Info(fmt.Sprintf("%d sessions active, limit is %d", usedSessions, scope.DefaultLimit))
+					return true, nil
+				}
+				continue
+			}
+			query := m.GetGlobalQuotaByTargetQuery{Target: scope.Target}
+			if err := bus.Dispatch(&query); err != nil {
+				return true, err
+			}
+			if query.Result.Used >= scope.DefaultLimit {
+				return true, nil
+			}
+		case "org":
+			if !c.IsSignedIn {
+				continue
+			}
+			query := m.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit}
+			if err := bus.Dispatch(&query); err != nil {
+				return true, err
+			}
+			if query.Result.Limit < 0 {
+				continue
+			}
+			if query.Result.Limit == 0 {
+				return true, nil
+			}
+
+			if query.Result.Used >= query.Result.Limit {
+				return true, nil
+			}
+		case "user":
+			if !c.IsSignedIn || c.UserId == 0 {
+				continue
+			}
+			query := m.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit}
+			if err := bus.Dispatch(&query); err != nil {
+				return true, err
+			}
+			if query.Result.Limit < 0 {
+				continue
+			}
+			if query.Result.Limit == 0 {
+				return true, nil
+			}
+
+			if query.Result.Used >= query.Result.Limit {
+				return true, nil
+			}
+		}
 	}
 	}
+
 	return false, nil
 	return false, nil
 }
 }

+ 128 - 29
pkg/models/quotas.go

@@ -3,53 +3,152 @@ package models
 import (
 import (
 	"errors"
 	"errors"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
+	"reflect"
 	"time"
 	"time"
 )
 )
 
 
-type QuotaTarget string
-
-const (
-	QUOTA_USER       QuotaTarget = "user" //SQL table to query. ie. "select count(*) from user where org_id=?"
-	QUOTA_DATASOURCE QuotaTarget = "data_source"
-	QUOTA_DASHBOARD  QuotaTarget = "dashboard"
-)
-
 var ErrInvalidQuotaTarget = errors.New("Invalid quota target")
 var ErrInvalidQuotaTarget = errors.New("Invalid quota target")
 
 
-func (q QuotaTarget) IsValid() bool {
-	_, ok := setting.Quota.Default[string(q)]
-	return ok
-}
-
 type Quota struct {
 type Quota struct {
 	Id      int64
 	Id      int64
 	OrgId   int64
 	OrgId   int64
-	Target  QuotaTarget
+	UserId  int64
+	Target  string
 	Limit   int64
 	Limit   int64
 	Created time.Time
 	Created time.Time
 	Updated time.Time
 	Updated time.Time
 }
 }
 
 
-type QuotaDTO struct {
-	OrgId  int64       `json:"org_id"`
-	Target QuotaTarget `json:"target"`
-	Limit  int64       `json:"limit"`
-	Used   int64       `json:"used"`
+type QuotaScope struct {
+	Name         string
+	Target       string
+	DefaultLimit int64
 }
 }
 
 
-type GetQuotaByTargetQuery struct {
-	Target QuotaTarget
-	OrgId  int64
-	Result *QuotaDTO
+type OrgQuotaDTO struct {
+	OrgId  int64  `json:"org_id"`
+	Target string `json:"target"`
+	Limit  int64  `json:"limit"`
+	Used   int64  `json:"used"`
+}
+
+type UserQuotaDTO struct {
+	UserId int64  `json:"user_id"`
+	Target string `json:"target"`
+	Limit  int64  `json:"limit"`
+	Used   int64  `json:"used"`
+}
+
+type GlobalQuotaDTO struct {
+	Target string `json:"target"`
+	Limit  int64  `json:"limit"`
+	Used   int64  `json:"used"`
+}
+
+type GetOrgQuotaByTargetQuery struct {
+	Target  string
+	OrgId   int64
+	Default int64
+	Result  *OrgQuotaDTO
 }
 }
 
 
-type GetQuotasQuery struct {
+type GetOrgQuotasQuery struct {
 	OrgId  int64
 	OrgId  int64
-	Result []*QuotaDTO
+	Result []*OrgQuotaDTO
+}
+
+type GetUserQuotaByTargetQuery struct {
+	Target  string
+	UserId  int64
+	Default int64
+	Result  *UserQuotaDTO
+}
+
+type GetUserQuotasQuery struct {
+	UserId int64
+	Result []*UserQuotaDTO
+}
+
+type GetGlobalQuotaByTargetQuery struct {
+	Target  string
+	Default int64
+	Result  *GlobalQuotaDTO
+}
+
+type UpdateOrgQuotaCmd struct {
+	Target string `json:"target"`
+	Limit  int64  `json:"limit"`
+	OrgId  int64  `json:"-"`
+}
+
+type UpdateUserQuotaCmd struct {
+	Target string `json:"target"`
+	Limit  int64  `json:"limit"`
+	UserId int64  `json:"-"`
+}
+
+func GetQuotaScopes(target string) ([]QuotaScope, error) {
+	scopes := make([]QuotaScope, 0)
+	switch target {
+	case "user":
+		scopes = append(scopes,
+			QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.User},
+			QuotaScope{Name: "org", Target: "org_user", DefaultLimit: setting.Quota.Org.User},
+		)
+		return scopes, nil
+	case "org":
+		scopes = append(scopes,
+			QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.Org},
+			QuotaScope{Name: "user", Target: "org_user", DefaultLimit: setting.Quota.User.Org},
+		)
+		return scopes, nil
+	case "dashboard":
+		scopes = append(scopes,
+			QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.Dashboard},
+			QuotaScope{Name: "org", Target: target, DefaultLimit: setting.Quota.Org.Dashboard},
+		)
+		return scopes, nil
+	case "data_source":
+		scopes = append(scopes,
+			QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.DataSource},
+			QuotaScope{Name: "org", Target: target, DefaultLimit: setting.Quota.Org.DataSource},
+		)
+		return scopes, nil
+	case "api_key":
+		scopes = append(scopes,
+			QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.ApiKey},
+			QuotaScope{Name: "org", Target: target, DefaultLimit: setting.Quota.Org.ApiKey},
+		)
+		return scopes, nil
+	case "session":
+		scopes = append(scopes,
+			QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.Session},
+		)
+		return scopes, nil
+	default:
+		return scopes, ErrInvalidQuotaTarget
+	}
 }
 }
 
 
-type UpdateQuotaCmd struct {
-	Target QuotaTarget `json:"target"`
-	Limit  int64       `json:"limit"`
-	OrgId  int64       `json:"-"`
+func QuotaToMap(q interface{}) map[string]int64 {
+	qMap := make(map[string]int64)
+	typ := reflect.TypeOf(q)
+	val := reflect.ValueOf(q)
+	if typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+		val = val.Elem()
+	}
+	for i := 0; i < typ.NumField(); i++ {
+		field := typ.Field(i)
+		name := field.Tag.Get("target")
+		if name == "" {
+			name = field.Name
+		}
+		if name == "-" {
+			continue
+		}
+		value := val.Field(i)
+		qMap[name] = value.Int()
+	}
+	return qMap
 }
 }

+ 3 - 2
pkg/services/sqlstore/migrations/quota_mig.go

@@ -10,14 +10,15 @@ func addQuotaMigration(mg *Migrator) {
 		Name: "quota",
 		Name: "quota",
 		Columns: []*Column{
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
-			{Name: "org_id", Type: DB_BigInt, Nullable: false},
+			{Name: "org_id", Type: DB_BigInt, Nullable: true},
+			{Name: "user_id", Type: DB_BigInt, Nullable: true},
 			{Name: "target", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "target", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "limit", Type: DB_BigInt, Nullable: false},
 			{Name: "limit", Type: DB_BigInt, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},
 		},
 		Indices: []*Index{
 		Indices: []*Index{
-			{Cols: []string{"org_id", "target"}, Type: UniqueIndex},
+			{Cols: []string{"org_id", "user_id", "target"}, Type: UniqueIndex},
 		},
 		},
 	}
 	}
 	mg.AddMigration("create quota table v1", NewAddTableMigration(quotaV1))
 	mg.AddMigration("create quota table v1", NewAddTableMigration(quotaV1))

+ 141 - 17
pkg/services/sqlstore/quota.go

@@ -8,16 +8,20 @@ import (
 )
 )
 
 
 func init() {
 func init() {
-	bus.AddHandler("sql", GetQuotaByTarget)
-	bus.AddHandler("sql", GetQuotas)
-	bus.AddHandler("sql", UpdateQuota)
+	bus.AddHandler("sql", GetOrgQuotaByTarget)
+	bus.AddHandler("sql", GetOrgQuotas)
+	bus.AddHandler("sql", UpdateOrgQuota)
+	bus.AddHandler("sql", GetUserQuotaByTarget)
+	bus.AddHandler("sql", GetUserQuotas)
+	bus.AddHandler("sql", UpdateUserQuota)
+	bus.AddHandler("sql", GetGlobalQuotaByTarget)
 }
 }
 
 
 type targetCount struct {
 type targetCount struct {
 	Count int64
 	Count int64
 }
 }
 
 
-func GetQuotaByTarget(query *m.GetQuotaByTargetQuery) error {
+func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
 	quota := m.Quota{
 	quota := m.Quota{
 		Target: query.Target,
 		Target: query.Target,
 		OrgId:  query.OrgId,
 		OrgId:  query.OrgId,
@@ -26,17 +30,17 @@ func GetQuotaByTarget(query *m.GetQuotaByTargetQuery) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	} else if has == false {
 	} else if has == false {
-		quota.Limit = setting.Quota.Default[string(query.Target)]
+		quota.Limit = query.Default
 	}
 	}
 
 
 	//get quota used.
 	//get quota used.
-	rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(string(query.Target)))
+	rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(query.Target))
 	resp := make([]*targetCount, 0)
 	resp := make([]*targetCount, 0)
 	if err := x.Sql(rawSql, query.OrgId).Find(&resp); err != nil {
 	if err := x.Sql(rawSql, query.OrgId).Find(&resp); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	query.Result = &m.QuotaDTO{
+	query.Result = &m.OrgQuotaDTO{
 		Target: query.Target,
 		Target: query.Target,
 		Limit:  quota.Limit,
 		Limit:  quota.Limit,
 		OrgId:  query.OrgId,
 		OrgId:  query.OrgId,
@@ -46,36 +50,39 @@ func GetQuotaByTarget(query *m.GetQuotaByTargetQuery) error {
 	return nil
 	return nil
 }
 }
 
 
-func GetQuotas(query *m.GetQuotasQuery) error {
+func GetOrgQuotas(query *m.GetOrgQuotasQuery) error {
 	quotas := make([]*m.Quota, 0)
 	quotas := make([]*m.Quota, 0)
 	sess := x.Table("quota")
 	sess := x.Table("quota")
-	if err := sess.Where("org_id=?", query.OrgId).Find(&quotas); err != nil {
+	if err := sess.Where("org_id=? AND user_id=0", query.OrgId).Find(&quotas); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	seenTargets := make(map[m.QuotaTarget]bool)
+	defaultQuotas := m.QuotaToMap(setting.Quota.Org)
+
+	seenTargets := make(map[string]bool)
 	for _, q := range quotas {
 	for _, q := range quotas {
 		seenTargets[q.Target] = true
 		seenTargets[q.Target] = true
 	}
 	}
 
 
-	for t, v := range setting.Quota.Default {
-		if _, ok := seenTargets[m.QuotaTarget(t)]; !ok {
+	for t, v := range defaultQuotas {
+		if _, ok := seenTargets[t]; !ok {
 			quotas = append(quotas, &m.Quota{
 			quotas = append(quotas, &m.Quota{
 				OrgId:  query.OrgId,
 				OrgId:  query.OrgId,
-				Target: m.QuotaTarget(t),
+				Target: t,
 				Limit:  v,
 				Limit:  v,
 			})
 			})
 		}
 		}
 	}
 	}
-	result := make([]*m.QuotaDTO, len(quotas))
+
+	result := make([]*m.OrgQuotaDTO, len(quotas))
 	for i, q := range quotas {
 	for i, q := range quotas {
 		//get quota used.
 		//get quota used.
-		rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(string(q.Target)))
+		rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target))
 		resp := make([]*targetCount, 0)
 		resp := make([]*targetCount, 0)
 		if err := x.Sql(rawSql, q.OrgId).Find(&resp); err != nil {
 		if err := x.Sql(rawSql, q.OrgId).Find(&resp); err != nil {
 			return err
 			return err
 		}
 		}
-		result[i] = &m.QuotaDTO{
+		result[i] = &m.OrgQuotaDTO{
 			Target: q.Target,
 			Target: q.Target,
 			Limit:  q.Limit,
 			Limit:  q.Limit,
 			OrgId:  q.OrgId,
 			OrgId:  q.OrgId,
@@ -86,7 +93,7 @@ func GetQuotas(query *m.GetQuotasQuery) error {
 	return nil
 	return nil
 }
 }
 
 
-func UpdateQuota(cmd *m.UpdateQuotaCmd) error {
+func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
 	return inTransaction2(func(sess *session) error {
 	return inTransaction2(func(sess *session) error {
 		//Check if quota is already defined in the DB
 		//Check if quota is already defined in the DB
 		quota := m.Quota{
 		quota := m.Quota{
@@ -113,3 +120,120 @@ func UpdateQuota(cmd *m.UpdateQuotaCmd) error {
 		return nil
 		return nil
 	})
 	})
 }
 }
+
+func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
+	quota := m.Quota{
+		Target: query.Target,
+		UserId: query.UserId,
+	}
+	has, err := x.Get(quota)
+	if err != nil {
+		return err
+	} else if has == false {
+		quota.Limit = query.Default
+	}
+
+	//get quota used.
+	rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target))
+	resp := make([]*targetCount, 0)
+	if err := x.Sql(rawSql, query.UserId).Find(&resp); err != nil {
+		return err
+	}
+
+	query.Result = &m.UserQuotaDTO{
+		Target: query.Target,
+		Limit:  quota.Limit,
+		UserId: query.UserId,
+		Used:   resp[0].Count,
+	}
+
+	return nil
+}
+
+func GetUserQuotas(query *m.GetUserQuotasQuery) error {
+	quotas := make([]*m.Quota, 0)
+	sess := x.Table("quota")
+	if err := sess.Where("user_id=? AND org_id=0", query.UserId).Find(&quotas); err != nil {
+		return err
+	}
+
+	defaultQuotas := m.QuotaToMap(setting.Quota.User)
+
+	seenTargets := make(map[string]bool)
+	for _, q := range quotas {
+		seenTargets[q.Target] = true
+	}
+
+	for t, v := range defaultQuotas {
+		if _, ok := seenTargets[t]; !ok {
+			quotas = append(quotas, &m.Quota{
+				UserId: query.UserId,
+				Target: t,
+				Limit:  v,
+			})
+		}
+	}
+
+	result := make([]*m.UserQuotaDTO, len(quotas))
+	for i, q := range quotas {
+		//get quota used.
+		rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target))
+		resp := make([]*targetCount, 0)
+		if err := x.Sql(rawSql, q.UserId).Find(&resp); err != nil {
+			return err
+		}
+		result[i] = &m.UserQuotaDTO{
+			Target: q.Target,
+			Limit:  q.Limit,
+			UserId: q.UserId,
+			Used:   resp[0].Count,
+		}
+	}
+	query.Result = result
+	return nil
+}
+
+func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
+	return inTransaction2(func(sess *session) error {
+		//Check if quota is already defined in the DB
+		quota := m.Quota{
+			Target: cmd.Target,
+			UserId: cmd.UserId,
+		}
+		has, err := sess.Get(quota)
+		if err != nil {
+			return err
+		}
+		quota.Limit = cmd.Limit
+		if has == false {
+			//No quota in the DB for this target, so create a new one.
+			if _, err := sess.Insert(&quota); err != nil {
+				return err
+			}
+		} else {
+			//update existing quota entry in the DB.
+			if _, err := sess.Id(quota.Id).Update(&quota); err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+}
+
+func GetGlobalQuotaByTarget(query *m.GetGlobalQuotaByTargetQuery) error {
+	//get quota used.
+	rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s", dialect.Quote(query.Target))
+	resp := make([]*targetCount, 0)
+	if err := x.Sql(rawSql).Find(&resp); err != nil {
+		return err
+	}
+
+	query.Result = &m.GlobalQuotaDTO{
+		Target: query.Target,
+		Limit:  query.Default,
+		Used:   resp[0].Count,
+	}
+
+	return nil
+}

+ 47 - 6
pkg/setting/setting_quota.go

@@ -1,17 +1,58 @@
 package setting
 package setting
 
 
+type OrgQuota struct {
+	User       int64 `target:"org_user"`
+	DataSource int64 `target:"data_source"`
+	Dashboard  int64 `target:"dashboard"`
+	ApiKey     int64 `target:"api_key"`
+}
+
+type UserQuota struct {
+	Org int64 `target:"org_user"`
+}
+
+type GlobalQuota struct {
+	Org        int64 `target:"org"`
+	User       int64 `target:"user"`
+	DataSource int64 `target:"data_source"`
+	Dashboard  int64 `target:"dashboard"`
+	ApiKey     int64 `target:"api_key"`
+	Session    int64 `target:"-"`
+}
+
 type QuotaSettings struct {
 type QuotaSettings struct {
 	Enabled bool
 	Enabled bool
-	Default map[string]int64
+	Org     *OrgQuota
+	User    *UserQuota
+	Global  *GlobalQuota
 }
 }
 
 
 func readQuotaSettings() {
 func readQuotaSettings() {
 	// set global defaults.
 	// set global defaults.
-	DefaultQuotas := make(map[string]int64)
 	quota := Cfg.Section("quota")
 	quota := Cfg.Section("quota")
 	Quota.Enabled = quota.Key("enabled").MustBool(false)
 	Quota.Enabled = quota.Key("enabled").MustBool(false)
-	DefaultQuotas["user"] = quota.Key("user").MustInt64(10)
-	DefaultQuotas["data_source"] = quota.Key("data_source").MustInt64(10)
-	DefaultQuotas["dashboard"] = quota.Key("dashboard").MustInt64(10)
-	Quota.Default = DefaultQuotas
+
+	// per ORG Limits
+	Quota.Org = &OrgQuota{
+		User:       quota.Key("org_user").MustInt64(10),
+		DataSource: quota.Key("org_data_source").MustInt64(10),
+		Dashboard:  quota.Key("org_dashboard").MustInt64(10),
+		ApiKey:     quota.Key("org_api_key").MustInt64(10),
+	}
+
+	// per User limits
+	Quota.User = &UserQuota{
+		Org: quota.Key("user_org").MustInt64(10),
+	}
+
+	// Global Limits
+	Quota.Global = &GlobalQuota{
+		User:       quota.Key("global_user").MustInt64(-1),
+		Org:        quota.Key("global_org").MustInt64(-1),
+		DataSource: quota.Key("global_data_source").MustInt64(-1),
+		Dashboard:  quota.Key("global_dashboard").MustInt64(-1),
+		ApiKey:     quota.Key("global_api_key").MustInt64(-1),
+		Session:    quota.Key("global_session").MustInt64(-1),
+	}
+
 }
 }