// Package license defines the Vantage licence payload and its offline // verification. // // A licence is a signed blob (ECDSA P-384 with SHA-256). The server checks a // signature, an expiry, a deployment mode and an instance ID, and asks nobody's // permission. That buys air-gapped self-hosting and means no instance depends // on the licensing service being reachable. // // It costs revocation: once issued, a licence is valid until it expires // whatever the billing system later says. Self Hosted is sold annually only so // that window is bounded. package license import "time" const ( TierFree = "free" TierProfessional = "professional" TierEnterprise = "enterprise" // TierSelfHosted is LEGACY and no new licence carries it. // // It was a tier when self-hosting was a tier rather than a deployment. Blobs // already signed with it exist and cannot be rewritten, so it stays a // recognised value that NormaliseTier maps forward. Never put it in a plan // row and never offer it in a UI. TierSelfHosted = "self_hosted" DeploymentCloud = "cloud" DeploymentSelfHosted = "self_hosted" FeatureConsole = "console" // browser SSH/RDP/VNC FeatureOIDC = "oidc" // per-instance single sign-on ) // Support levels. Carried for display and enforced by nothing — there is no code // path anywhere that branches on these, and there must not be one. They are here // so an air-gapped install can tell its operator who to call without reaching // Vantage HQ. const ( SupportCommunity = "community" SupportEmail24x5 = "email_24_5" SupportEmailCall24x7 = "email_call_24_7" ) // Unlimited is the sentinel for "no cap" in every Limits field. const Unlimited = -1 // Limits are the countable caps a licence grants. // // Every field is a plain int with Unlimited as the sentinel. AuditRetentionDays // is the odd one out: it bounds a duration rather than a count, and Unlimited // there means "never trim" rather than "no cap". type Limits struct { MaxServers int `json:"max_servers"` MaxMonitors int `json:"max_monitors"` MaxSecretGroups int `json:"max_secret_groups"` MaxChannels int `json:"max_channels"` AuditRetentionDays int `json:"audit_retention_days"` } // FillUnset replaces any zero field with the same field from base. // // This exists for one reason: a licence signed before a field existed decodes it // as 0, and 0 would read as the most restrictive possible value — no monitors, // and an audit log trimmed to nothing. A blob we cannot re-sign must not be // allowed to mean that. // // The cost is that 0 stops being expressible as a real allowance. No plan grants // zero of anything, so nothing is lost today; a plan that genuinely means zero // must use a negative-free sentinel of its own rather than reintroducing 0 here. func (l Limits) FillUnset(base Limits) Limits { if l.MaxServers == 0 { l.MaxServers = base.MaxServers } if l.MaxMonitors == 0 { l.MaxMonitors = base.MaxMonitors } if l.MaxSecretGroups == 0 { l.MaxSecretGroups = base.MaxSecretGroups } if l.MaxChannels == 0 { l.MaxChannels = base.MaxChannels } if l.AuditRetentionDays == 0 { l.AuditRetentionDays = base.AuditRetentionDays } return l } // License is the signed payload. // // InstanceID is always populated: the self-hosted purchase flow links the // instance UUID before the licence is signed, so there is no unbound licence // and no claim protocol. type License struct { ID string `json:"id"` // uuid, for support and audit InstanceID string `json:"instance_id"` // the instance this licence is bound to AccountID string `json:"account_id"` // admin-side customer, informational InstanceName string `json:"instance_name"` // display only Tier string `json:"tier"` Deployment string `json:"deployment"` SupportLevel string `json:"support_level,omitempty"` // display only IssuedAt time.Time `json:"issued_at"` ExpiresAt time.Time `json:"expires_at"` Limits Limits `json:"limits"` Features []string `json:"features"` } // HasFeature reports whether the licence grants a named feature. // // Callers must use this rather than switching on Tier. Adding a tier, or // changing what a tier includes, must never require a server release. func (l License) HasFeature(name string) bool { for _, f := range l.Features { if f == name { return true } } return false } // WithinLimit reports whether one more of something is allowed. // A max of Unlimited always allows. func WithinLimit(current, max int) bool { if max == Unlimited { return true } return current < max }