feat(license): add the licence payload and tier seed table
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// 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"
|
||||
TierSelfHosted = "self_hosted"
|
||||
|
||||
DeploymentCloud = "cloud"
|
||||
DeploymentSelfHosted = "self_hosted"
|
||||
|
||||
FeatureConsole = "console" // browser SSH/RDP/VNC
|
||||
FeatureOIDC = "oidc" // per-instance single sign-on
|
||||
)
|
||||
|
||||
// Unlimited is the sentinel for "no cap" in every Limits field.
|
||||
const Unlimited = -1
|
||||
|
||||
// Limits are the countable caps a licence grants.
|
||||
type Limits struct {
|
||||
MaxServers int `json:"max_servers"`
|
||||
MaxSecretGroups int `json:"max_secret_groups"`
|
||||
MaxChannels int `json:"max_channels"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package license
|
||||
|
||||
// Plan is the contents of a tier at issue time.
|
||||
//
|
||||
// This table is the seed. Once the admin service exists (spec 3) it owns the
|
||||
// authoritative copy in its `plans` collection, and every issued licence
|
||||
// snapshots the plan it was cut from — so editing a plan never rewrites an
|
||||
// existing licence, the same rule as workflow_runs.steps_snapshot.
|
||||
//
|
||||
// lkctl uses this table to issue by hand until then.
|
||||
type Plan struct {
|
||||
Tier string
|
||||
Name string
|
||||
Deployment string
|
||||
Limits Limits
|
||||
Features []string
|
||||
}
|
||||
|
||||
var plans = map[string]Plan{
|
||||
TierFree: {
|
||||
Tier: TierFree,
|
||||
Name: "Free",
|
||||
Deployment: DeploymentCloud, // cloud only, by construction
|
||||
Limits: Limits{MaxServers: 3, MaxSecretGroups: 1, MaxChannels: 1},
|
||||
Features: nil,
|
||||
},
|
||||
TierProfessional: {
|
||||
Tier: TierProfessional,
|
||||
Name: "Professional",
|
||||
Deployment: DeploymentCloud,
|
||||
Limits: Limits{MaxServers: Unlimited, MaxSecretGroups: Unlimited, MaxChannels: Unlimited},
|
||||
Features: []string{FeatureConsole, FeatureOIDC},
|
||||
},
|
||||
TierSelfHosted: {
|
||||
Tier: TierSelfHosted,
|
||||
Name: "Self Hosted",
|
||||
Deployment: DeploymentSelfHosted,
|
||||
Limits: Limits{MaxServers: Unlimited, MaxSecretGroups: Unlimited, MaxChannels: Unlimited},
|
||||
Features: []string{FeatureConsole, FeatureOIDC},
|
||||
},
|
||||
}
|
||||
|
||||
// PlanFor returns the seed plan for a tier.
|
||||
func PlanFor(tier string) (Plan, bool) {
|
||||
p, ok := plans[tier]
|
||||
return p, ok
|
||||
}
|
||||
Reference in New Issue
Block a user