diff --git a/conf/defaults.ini b/conf/defaults.ini index e1c06a0223c..d640820fe65 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -87,6 +87,7 @@ cookie_secure = false # Session life time, default is 86400 session_life_time = 86400 +gc_interval_time = 86400 #################################### Analytics #################################### [analytics] @@ -253,9 +254,37 @@ exchange = grafana_events enabled = false path = /var/lib/grafana/dashboards +#################################### Usage Quotas ########################## [quota] 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 diff --git a/pkg/api/api.go b/pkg/api/api.go index 90118d93912..27eb3c749db 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,14 +14,14 @@ func Register(r *macaron.Macaron) { reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) - limitQuota := middleware.LimitQuota + quota := middleware.Quota bind := binding.Bind // not logged in views r.Get("/", reqSignedIn, Index) 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("/invite/:code", Index) @@ -45,7 +45,7 @@ func Register(r *macaron.Macaron) { // sign up r.Get("/signup", Index) 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)) // invited @@ -67,7 +67,7 @@ func Register(r *macaron.Macaron) { r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot) // api renew session based on remember cookie - r.Get("/api/login/ping", LoginApiPing) + r.Get("/api/login/ping", quota("session"), LoginApiPing) // authed api r.Group("/api", func() { @@ -81,6 +81,7 @@ func Register(r *macaron.Macaron) { r.Post("/stars/dashboard/:id", wrap(StarDashboard)) r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) + r.Get("/quotas", wrap(GetUserQuotas)) }) // users (admin permission required) @@ -94,26 +95,26 @@ func Register(r *macaron.Macaron) { // org information available to all users. r.Group("/org", func() { r.Get("/", wrap(GetOrgCurrent)) - r.Get("/quotas", wrap(GetQuotas)) + r.Get("/quotas", wrap(GetOrgQuotas)) }) // current org r.Group("/org", func() { r.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent)) 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.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg)) r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg)) // invites 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)) }, regOrgAdmin) // create new org - r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg)) + r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg)) // search all orgs 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.Delete("/users/:userId", wrap(RemoveOrgUser)) r.Get("/quotas", wrap(GetOrgQuotas)) - r.Put("/quotas/:target", bind(m.UpdateQuotaCmd{}), wrap(UpdateOrgQuota)) + r.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), wrap(UpdateOrgQuota)) }, reqGrafanaAdmin) // auth api keys r.Group("/auth/keys", func() { 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)) }, regOrgAdmin) // Data sources r.Group("/datasources", func() { 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.Delete("/:id", DeleteDataSource) 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/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions) r.Delete("/users/:id", AdminDeleteUser) + r.Get("/users/:id/quotas", wrap(GetUserQuotas)) + r.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota)) }, reqGrafanaAdmin) // rendering diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index cc6ccd29064..d7f9f2cd740 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -88,7 +88,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { dash := cmd.GetDashboardModel() if dash.Id == 0 { - limitReached, err := middleware.QuotaReached(cmd.OrgId, m.QUOTA_DASHBOARD) + limitReached, err := middleware.QuotaReached(c, "dashboard") if err != nil { c.JsonApiErr(500, "failed to get quota", err) return diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 796599df864..4244feef664 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -74,7 +74,15 @@ func OAuthLogin(ctx *middleware.Context) { ctx.Redirect(setting.AppSubUrl + "/login") 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{ Login: userInfo.Email, Email: userInfo.Email, diff --git a/pkg/api/quota.go b/pkg/api/quota.go index 797ebd1661e..490a8e90729 100644 --- a/pkg/api/quota.go +++ b/pkg/api/quota.go @@ -11,7 +11,7 @@ func GetOrgQuotas(c *middleware.Context) Response { if !setting.Quota.Enabled { 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 { return ApiError(500, "Failed to get org quotas", err) @@ -20,28 +20,44 @@ func GetOrgQuotas(c *middleware.Context) Response { return Json(200, query.Result) } -// allow users to query the quotas of their own org. -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 { +func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response { if !setting.Quota.Enabled { return ApiError(404, "Quotas not enabled", nil) } 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) } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 1b218a6eedc..8a61bdb3a0b 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,6 +1,7 @@ package middleware import ( + "fmt" "strconv" "strings" @@ -254,33 +255,95 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) { ctx.JSON(status, resp) } -func LimitQuota(target m.QuotaTarget) macaron.Handler { +func Quota(target string) macaron.Handler { return func(c *Context) { - limitReached, err := QuotaReached(c.OrgId, target) + limitReached, err := QuotaReached(c, target) if err != nil { c.JsonApiErr(500, "failed to get quota", err) return } if limitReached { - c.JsonApiErr(403, "Quota reached", nil) + c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil) return } } } -func QuotaReached(org_id int64, target m.QuotaTarget) (bool, error) { +func QuotaReached(c *Context, target string) (bool, error) { if !setting.Quota.Enabled { 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} - if err := bus.Dispatch(&query); err != nil { - return true, 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 } diff --git a/pkg/models/quotas.go b/pkg/models/quotas.go index 75df5cd3765..ba17b848403 100644 --- a/pkg/models/quotas.go +++ b/pkg/models/quotas.go @@ -3,53 +3,152 @@ package models import ( "errors" "github.com/grafana/grafana/pkg/setting" + "reflect" "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") -func (q QuotaTarget) IsValid() bool { - _, ok := setting.Quota.Default[string(q)] - return ok -} - type Quota struct { Id int64 OrgId int64 - Target QuotaTarget + UserId int64 + Target string Limit int64 Created 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 +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 GetOrgQuotasQuery struct { OrgId int64 - Result *QuotaDTO + Result []*OrgQuotaDTO } -type GetQuotasQuery struct { - OrgId int64 - Result []*QuotaDTO +type GetUserQuotaByTargetQuery struct { + Target string + UserId int64 + Default int64 + Result *UserQuotaDTO } -type UpdateQuotaCmd struct { - Target QuotaTarget `json:"target"` - Limit int64 `json:"limit"` - OrgId int64 `json:"-"` +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 + } +} + +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 } diff --git a/pkg/services/sqlstore/migrations/quota_mig.go b/pkg/services/sqlstore/migrations/quota_mig.go index d0b3b14fd5a..d877bb4c3c1 100644 --- a/pkg/services/sqlstore/migrations/quota_mig.go +++ b/pkg/services/sqlstore/migrations/quota_mig.go @@ -10,14 +10,15 @@ func addQuotaMigration(mg *Migrator) { Name: "quota", Columns: []*Column{ {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: "limit", Type: DB_BigInt, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, 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)) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index b54f6a83a57..23bea264e87 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -8,16 +8,20 @@ import ( ) 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 { Count int64 } -func GetQuotaByTarget(query *m.GetQuotaByTargetQuery) error { +func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error { quota := m.Quota{ Target: query.Target, OrgId: query.OrgId, @@ -26,17 +30,17 @@ func GetQuotaByTarget(query *m.GetQuotaByTargetQuery) error { if err != nil { return err } else if has == false { - quota.Limit = setting.Quota.Default[string(query.Target)] + quota.Limit = query.Default } //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) if err := x.Sql(rawSql, query.OrgId).Find(&resp); err != nil { return err } - query.Result = &m.QuotaDTO{ + query.Result = &m.OrgQuotaDTO{ Target: query.Target, Limit: quota.Limit, OrgId: query.OrgId, @@ -46,36 +50,39 @@ func GetQuotaByTarget(query *m.GetQuotaByTargetQuery) error { return nil } -func GetQuotas(query *m.GetQuotasQuery) error { +func GetOrgQuotas(query *m.GetOrgQuotasQuery) error { quotas := make([]*m.Quota, 0) sess := x.Table("quota") - if err := sess.Where("org_id=?", query.OrgId).Find("as); err != nil { + if err := sess.Where("org_id=? AND user_id=0", query.OrgId).Find("as); err != nil { return err } - seenTargets := make(map[m.QuotaTarget]bool) + defaultQuotas := m.QuotaToMap(setting.Quota.Org) + + seenTargets := make(map[string]bool) for _, q := range quotas { 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{ OrgId: query.OrgId, - Target: m.QuotaTarget(t), + Target: t, Limit: v, }) } } - result := make([]*m.QuotaDTO, len(quotas)) + + result := make([]*m.OrgQuotaDTO, len(quotas)) for i, q := range quotas { //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) if err := x.Sql(rawSql, q.OrgId).Find(&resp); err != nil { return err } - result[i] = &m.QuotaDTO{ + result[i] = &m.OrgQuotaDTO{ Target: q.Target, Limit: q.Limit, OrgId: q.OrgId, @@ -86,7 +93,7 @@ func GetQuotas(query *m.GetQuotasQuery) error { return nil } -func UpdateQuota(cmd *m.UpdateQuotaCmd) error { +func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { return inTransaction2(func(sess *session) error { //Check if quota is already defined in the DB quota := m.Quota{ @@ -113,3 +120,120 @@ func UpdateQuota(cmd *m.UpdateQuotaCmd) error { 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("as); 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("a); err != nil { + return err + } + } else { + //update existing quota entry in the DB. + if _, err := sess.Id(quota.Id).Update("a); 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 +} diff --git a/pkg/setting/setting_quota.go b/pkg/setting/setting_quota.go index d6ca0cd7ba8..df412fe23e4 100644 --- a/pkg/setting/setting_quota.go +++ b/pkg/setting/setting_quota.go @@ -1,17 +1,58 @@ 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 { Enabled bool - Default map[string]int64 + Org *OrgQuota + User *UserQuota + Global *GlobalQuota } func readQuotaSettings() { // set global defaults. - DefaultQuotas := make(map[string]int64) quota := Cfg.Section("quota") 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), + } + }