feat(server): enforce licence limits on servers, secret groups and channels
This commit is contained in:
1 parent
1d3fcebb28
commit
855537c535
4 files changed
+120
No files matched your search
@@ -72,6 +72,9 @@ func validateChannelIDs(instanceID string, channelIDs []string) error {
|
||||
}
|
||||
|
||||
func CreateChannel(instanceID string, ch *models.NotificationChannel) (*models.NotificationChannel, error) {
|
||||
if err := CheckChannelLimit(instanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
ch.InstanceID = instanceID
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/db"
|
||||
"github.com/mrhid6/vantage/shared/license"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// LimitError is returned when a licence cap would be exceeded. The API maps it
|
||||
// to 403 with a machine-readable body.
|
||||
type LimitError struct {
|
||||
Limit string
|
||||
Current int
|
||||
Max int
|
||||
}
|
||||
|
||||
func (e *LimitError) Error() string {
|
||||
return fmt.Sprintf("licence limit reached: %s (%d of %d)", e.Limit, e.Current, e.Max)
|
||||
}
|
||||
|
||||
func limitCtx() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), 5*time.Second)
|
||||
}
|
||||
|
||||
// CheckServerLimit refuses a new server when the instance is at its cap.
|
||||
//
|
||||
// Counts live rows only. An instance already over its cap keeps every server it
|
||||
// has — nothing is truncated — it simply cannot add another.
|
||||
func CheckServerLimit(instanceID string) error {
|
||||
st := GetLicenseState(instanceID)
|
||||
ctx, cancel := limitCtx()
|
||||
defer cancel()
|
||||
|
||||
n, err := db.Col("servers").CountDocuments(ctx, bson.M{"instance_id": instanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !license.WithinLimit(int(n), st.Limits.MaxServers) {
|
||||
return &LimitError{Limit: "max_servers", Current: int(n), Max: st.Limits.MaxServers}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckSecretGroupLimit refuses a NEW group at the cap. Writing to a group that
|
||||
// already exists is always allowed, so a capped customer can still rotate the
|
||||
// secrets they have.
|
||||
func CheckSecretGroupLimit(instanceID, group string) error {
|
||||
st := GetLicenseState(instanceID)
|
||||
ctx, cancel := limitCtx()
|
||||
defer cancel()
|
||||
|
||||
existing, err := db.Col("secrets").CountDocuments(ctx,
|
||||
bson.M{"instance_id": instanceID, "group": group})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if err := db.Col("secrets").Distinct(ctx, "group",
|
||||
bson.M{"instance_id": instanceID}).Decode(&groups); err != nil {
|
||||
return err
|
||||
}
|
||||
if !license.WithinLimit(len(groups), st.Limits.MaxSecretGroups) {
|
||||
return &LimitError{Limit: "max_secret_groups", Current: len(groups), Max: st.Limits.MaxSecretGroups}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckChannelLimit refuses a new notification channel at the cap.
|
||||
func CheckChannelLimit(instanceID string) error {
|
||||
st := GetLicenseState(instanceID)
|
||||
ctx, cancel := limitCtx()
|
||||
defer cancel()
|
||||
|
||||
n, err := db.Col("notification_channels").CountDocuments(ctx, bson.M{"instance_id": instanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !license.WithinLimit(int(n), st.Limits.MaxChannels) {
|
||||
return &LimitError{Limit: "max_channels", Current: int(n), Max: st.Limits.MaxChannels}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LicenseUsage reports current counts, so the UI can say "12 of 3 servers"
|
||||
// honestly when an instance is over its cap rather than pretending.
|
||||
func LicenseUsage(instanceID string) (servers, secretGroups, channels int) {
|
||||
ctx, cancel := limitCtx()
|
||||
defer cancel()
|
||||
|
||||
if n, err := db.Col("servers").CountDocuments(ctx, bson.M{"instance_id": instanceID}); err == nil {
|
||||
servers = int(n)
|
||||
}
|
||||
var groups []string
|
||||
if err := db.Col("secrets").Distinct(ctx, "group",
|
||||
bson.M{"instance_id": instanceID}).Decode(&groups); err == nil {
|
||||
secretGroups = len(groups)
|
||||
}
|
||||
if n, err := db.Col("notification_channels").CountDocuments(ctx,
|
||||
bson.M{"instance_id": instanceID}); err == nil {
|
||||
channels = int(n)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -128,6 +128,9 @@ func RevealSecret(instanceID, group, key string) (string, error) {
|
||||
}
|
||||
|
||||
func UpsertSecrets(instanceID, group string, values map[string]string) error {
|
||||
if err := CheckSecretGroupLimit(instanceID, group); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ func HashToken(token string) string {
|
||||
}
|
||||
|
||||
func CreateServer(instanceID string) (*models.Server, string, error) {
|
||||
if err := CheckServerLimit(instanceID); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
token, err := generateToken(32)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
Reference in new issue
Block a user