mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
chore: refactor entitlements to be a safe object to use (#14406)
* chore: refactor entitlements to be passable as an argument Previously, all usage of entitlements requires mutex usage on the api struct directly. This prevents passing the entitlements to a sub package. It also creates the possibility for misuse.
This commit is contained in:
@ -15,6 +15,7 @@ import (
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/entitlements"
|
||||
agplportsharing "github.com/coder/coder/v2/coderd/portsharing"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/portsharing"
|
||||
@ -103,19 +104,26 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
}
|
||||
return nil, xerrors.Errorf("init database encryption: %w", err)
|
||||
}
|
||||
|
||||
entitlementsSet := entitlements.New()
|
||||
options.Database = cryptDB
|
||||
api := &API{
|
||||
ctx: ctx,
|
||||
cancel: cancelFunc,
|
||||
Options: options,
|
||||
ctx: ctx,
|
||||
cancel: cancelFunc,
|
||||
Options: options,
|
||||
entitlements: entitlementsSet,
|
||||
provisionerDaemonAuth: &provisionerDaemonAuth{
|
||||
psk: options.ProvisionerDaemonPSK,
|
||||
authorizer: options.Authorizer,
|
||||
db: options.Database,
|
||||
},
|
||||
licenseMetricsCollector: &license.MetricsCollector{
|
||||
Entitlements: entitlementsSet,
|
||||
},
|
||||
}
|
||||
// This must happen before coderd initialization!
|
||||
options.PostAuthAdditionalHeadersFunc = api.writeEntitlementWarningsHeader
|
||||
options.Options.Entitlements = api.entitlements
|
||||
api.AGPL = coderd.New(options.Options)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
@ -493,7 +501,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
}
|
||||
api.AGPL.WorkspaceProxiesFetchUpdater.Store(&fetchUpdater)
|
||||
|
||||
err = api.PrometheusRegistry.Register(&api.licenseMetricsCollector)
|
||||
err = api.PrometheusRegistry.Register(api.licenseMetricsCollector)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("unable to register license metrics collector")
|
||||
}
|
||||
@ -553,13 +561,11 @@ type API struct {
|
||||
// ProxyHealth checks the reachability of all workspace proxies.
|
||||
ProxyHealth *proxyhealth.ProxyHealth
|
||||
|
||||
entitlementsUpdateMu sync.Mutex
|
||||
entitlementsMu sync.RWMutex
|
||||
entitlements codersdk.Entitlements
|
||||
entitlements *entitlements.Set
|
||||
|
||||
provisionerDaemonAuth *provisionerDaemonAuth
|
||||
|
||||
licenseMetricsCollector license.MetricsCollector
|
||||
licenseMetricsCollector *license.MetricsCollector
|
||||
tailnetService *tailnet.ClientService
|
||||
}
|
||||
|
||||
@ -588,11 +594,8 @@ func (api *API) writeEntitlementWarningsHeader(a rbac.Subject, header http.Heade
|
||||
// has no roles. This is a normal user!
|
||||
return
|
||||
}
|
||||
api.entitlementsMu.RLock()
|
||||
defer api.entitlementsMu.RUnlock()
|
||||
for _, warning := range api.entitlements.Warnings {
|
||||
header.Add(codersdk.EntitlementsWarningHeader, warning)
|
||||
}
|
||||
|
||||
api.entitlements.WriteEntitlementWarningHeaders(header)
|
||||
}
|
||||
|
||||
func (api *API) Close() error {
|
||||
@ -614,9 +617,6 @@ func (api *API) Close() error {
|
||||
}
|
||||
|
||||
func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
api.entitlementsUpdateMu.Lock()
|
||||
defer api.entitlementsUpdateMu.Unlock()
|
||||
|
||||
replicas := api.replicaManager.AllPrimary()
|
||||
agedReplicas := make([]database.Replica, 0, len(replicas))
|
||||
for _, replica := range replicas {
|
||||
@ -632,7 +632,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
agedReplicas = append(agedReplicas, replica)
|
||||
}
|
||||
|
||||
entitlements, err := license.Entitlements(
|
||||
reloadedEntitlements, err := license.Entitlements(
|
||||
ctx, api.Database,
|
||||
len(agedReplicas), len(api.ExternalAuthConfigs), api.LicenseKeys, map[codersdk.FeatureName]bool{
|
||||
codersdk.FeatureAuditLog: api.AuditLogging,
|
||||
@ -652,29 +652,24 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if entitlements.RequireTelemetry && !api.DeploymentValues.Telemetry.Enable.Value() {
|
||||
if reloadedEntitlements.RequireTelemetry && !api.DeploymentValues.Telemetry.Enable.Value() {
|
||||
// We can't fail because then the user couldn't remove the offending
|
||||
// license w/o a restart.
|
||||
//
|
||||
// We don't simply append to entitlement.Errors since we don't want any
|
||||
// enterprise features enabled.
|
||||
api.entitlements.Errors = []string{
|
||||
"License requires telemetry but telemetry is disabled",
|
||||
}
|
||||
api.entitlements.Update(func(entitlements *codersdk.Entitlements) {
|
||||
entitlements.Errors = []string{
|
||||
"License requires telemetry but telemetry is disabled",
|
||||
}
|
||||
})
|
||||
|
||||
api.Logger.Error(ctx, "license requires telemetry enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
featureChanged := func(featureName codersdk.FeatureName) (initial, changed, enabled bool) {
|
||||
if api.entitlements.Features == nil {
|
||||
return true, false, entitlements.Features[featureName].Enabled
|
||||
}
|
||||
oldFeature := api.entitlements.Features[featureName]
|
||||
newFeature := entitlements.Features[featureName]
|
||||
if oldFeature.Enabled != newFeature.Enabled {
|
||||
return false, true, newFeature.Enabled
|
||||
}
|
||||
return false, false, newFeature.Enabled
|
||||
return api.entitlements.FeatureChanged(featureName, reloadedEntitlements.Features[featureName])
|
||||
}
|
||||
|
||||
shouldUpdate := func(initial, changed, enabled bool) bool {
|
||||
@ -831,20 +826,16 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// External token encryption is soft-enforced
|
||||
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
||||
featureExternalTokenEncryption := reloadedEntitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
||||
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
|
||||
if featureExternalTokenEncryption.Enabled && featureExternalTokenEncryption.Entitlement != codersdk.EntitlementEntitled {
|
||||
msg := fmt.Sprintf("%s is enabled (due to setting external token encryption keys) but your license is not entitled to this feature.", codersdk.FeatureExternalTokenEncryption.Humanize())
|
||||
api.Logger.Warn(ctx, msg)
|
||||
entitlements.Warnings = append(entitlements.Warnings, msg)
|
||||
reloadedEntitlements.Warnings = append(reloadedEntitlements.Warnings, msg)
|
||||
}
|
||||
entitlements.Features[codersdk.FeatureExternalTokenEncryption] = featureExternalTokenEncryption
|
||||
reloadedEntitlements.Features[codersdk.FeatureExternalTokenEncryption] = featureExternalTokenEncryption
|
||||
|
||||
api.entitlementsMu.Lock()
|
||||
defer api.entitlementsMu.Unlock()
|
||||
api.entitlements = entitlements
|
||||
api.licenseMetricsCollector.Entitlements.Store(&entitlements)
|
||||
api.AGPL.SiteHandler.Entitlements.Store(&entitlements)
|
||||
api.entitlements.Replace(reloadedEntitlements)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1024,10 +1015,7 @@ func derpMapper(logger slog.Logger, proxyHealth *proxyhealth.ProxyHealth) func(*
|
||||
// @Router /entitlements [get]
|
||||
func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
api.entitlementsMu.RLock()
|
||||
entitlements := api.entitlements
|
||||
api.entitlementsMu.RUnlock()
|
||||
httpapi.Write(ctx, rw, http.StatusOK, entitlements)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, api.entitlements.AsJSON())
|
||||
}
|
||||
|
||||
func (api *API) runEntitlementsLoop(ctx context.Context) {
|
||||
|
Reference in New Issue
Block a user