enhance quota support.

now includes:
- perOrg (users, dashboards, datasources, api_keys)
- perUser (orgs)
- global (users, orgs, dashboards, datasources, api_keys, sessions)
This commit is contained in:
woodsaj
2015-09-11 23:17:10 +08:00
parent 47bf1bd21a
commit 6488324cf1
10 changed files with 485 additions and 101 deletions

View File

@@ -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

View File

@@ -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.Post("/login", quota("session"), bind(dtos.LoginCommand{}), wrap(LoginPost))
r.Get("/login/:name", OAuthLogin) 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

View File

@@ -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

View File

@@ -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,

View File

@@ -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 UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response {
func GetQuotas(c *middleware.Context) Response {
if !setting.Quota.Enabled {
return ApiError(404, "Quotas not enabled", nil)
}
query := m.GetQuotasQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get quotas", err)
}
return Json(200, query.Result)
}
func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateQuotaCmd) 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.OrgId = c.ParamsInt64(":orgId")
cmd.Target = m.QuotaTarget(c.Params(":target")) cmd.Target = c.Params(":target")
if !cmd.Target.IsValid() { 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 {
return ApiError(500, "Failed to get org quotas", err)
}
return Json(200, query.Result)
}
func UpdateUserQuota(c *middleware.Context, cmd m.UpdateUserQuotaCmd) Response {
if !setting.Quota.Enabled {
return ApiError(404, "Quotas not enabled", nil)
}
cmd.UserId = c.ParamsInt64(":id")
cmd.Target = c.Params(":target")
if _, ok := m.QuotaToMap(setting.Quota.User)[cmd.Target]; !ok {
return ApiError(404, "Invalid quota target", nil) return ApiError(404, "Invalid quota target", nil)
} }

View File

@@ -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 // 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
} }
query := m.GetQuotaByTargetQuery{OrgId: org_id, Target: target} log.Info(fmt.Sprintf("checking quota for %s in scopes %v", target, scopes))
if err := bus.Dispatch(&query); err != nil {
return true, err for _, scope := range scopes {
} log.Info(fmt.Sprintf("checking scope %s", scope.Name))
if query.Result.Used >= query.Result.Limit { switch scope.Name {
return true, nil 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
} }

View File

@@ -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 { type QuotaScope struct {
OrgId int64 `json:"org_id"` Name string
Target QuotaTarget `json:"target"` Target string
Limit int64 `json:"limit"` DefaultLimit int64
Used int64 `json:"used"`
} }
type GetQuotaByTargetQuery struct { type OrgQuotaDTO struct {
Target QuotaTarget 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 GetOrgQuotasQuery struct {
OrgId int64 OrgId int64
Result *QuotaDTO Result []*OrgQuotaDTO
} }
type GetQuotasQuery struct { type GetUserQuotaByTargetQuery struct {
OrgId int64 Target string
Result []*QuotaDTO UserId int64
Default int64
Result *UserQuotaDTO
} }
type UpdateQuotaCmd struct { type GetUserQuotasQuery struct {
Target QuotaTarget `json:"target"` UserId int64
Limit int64 `json:"limit"` Result []*UserQuotaDTO
OrgId int64 `json:"-"` }
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
}
}
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
} }

View File

@@ -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))

View File

@@ -8,16 +8,20 @@ import (
) )
func init() { func init() {
bus.AddHandler("sql", GetQuotaByTarget) bus.AddHandler("sql", GetOrgQuotaByTarget)
bus.AddHandler("sql", GetQuotas) bus.AddHandler("sql", GetOrgQuotas)
bus.AddHandler("sql", UpdateQuota) 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 { for t, v := range defaultQuotas {
if _, ok := seenTargets[m.QuotaTarget(t)]; !ok { 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
}

View File

@@ -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) // per ORG Limits
DefaultQuotas["dashboard"] = quota.Key("dashboard").MustInt64(10) Quota.Org = &OrgQuota{
Quota.Default = DefaultQuotas 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),
}
} }