@@ -3,7 +3,6 @@ package coderd
33import (
44 "context"
55 "crypto/ed25519"
6- "fmt"
76 "net/http"
87 "sync"
98 "time"
@@ -15,11 +14,14 @@ import (
1514
1615 "cdr.dev/slog"
1716 "github.com/coder/coder/coderd"
17+ agplaudit "github.com/coder/coder/coderd/audit"
1818 "github.com/coder/coder/coderd/httpapi"
1919 "github.com/coder/coder/coderd/httpmw"
20+ "github.com/coder/coder/coderd/workspacequota"
2021 "github.com/coder/coder/codersdk"
2122 "github.com/coder/coder/enterprise/audit"
2223 "github.com/coder/coder/enterprise/audit/backends"
24+ "github.com/coder/coder/enterprise/coderd/license"
2325)
2426
2527// New constructs an Enterprise coderd API instance.
@@ -34,19 +36,8 @@ func New(ctx context.Context, options *Options) (*API, error) {
3436 }
3537 ctx , cancelFunc := context .WithCancel (ctx )
3638 api := & API {
37- AGPL : coderd .New (options .Options ),
38- Options : options ,
39-
40- entitlements : entitlements {
41- activeUsers : codersdk.Feature {
42- Entitlement : codersdk .EntitlementNotEntitled ,
43- Enabled : false ,
44- },
45- auditLogs : codersdk .EntitlementNotEntitled ,
46- browserOnly : codersdk .EntitlementNotEntitled ,
47- scim : codersdk .EntitlementNotEntitled ,
48- workspaceQuota : codersdk .EntitlementNotEntitled ,
49- },
39+ AGPL : coderd .New (options .Options ),
40+ Options : options ,
5041 cancelEntitlementsLoop : cancelFunc ,
5142 }
5243 oauthConfigs := & httpmw.OAuth2Configs {
@@ -117,16 +108,7 @@ type API struct {
117108
118109 cancelEntitlementsLoop func ()
119110 entitlementsMu sync.RWMutex
120- entitlements entitlements
121- }
122-
123- type entitlements struct {
124- hasLicense bool
125- activeUsers codersdk.Feature
126- auditLogs codersdk.Entitlement
127- browserOnly codersdk.Entitlement
128- scim codersdk.Entitlement
129- workspaceQuota codersdk.Entitlement
111+ entitlements codersdk.Entitlements
130112}
131113
132114func (api * API ) Close () error {
@@ -135,94 +117,57 @@ func (api *API) Close() error {
135117}
136118
137119func (api * API ) updateEntitlements (ctx context.Context ) error {
138- licenses , err := api .Database .GetUnexpiredLicenses (ctx )
139- if err != nil {
140- return err
141- }
142120 api .entitlementsMu .Lock ()
143121 defer api .entitlementsMu .Unlock ()
144- now := time .Now ()
145122
146- // Default all entitlements to be disabled.
147- entitlements := entitlements {
148- hasLicense : false ,
149- activeUsers : codersdk.Feature {
150- Enabled : false ,
151- Entitlement : codersdk .EntitlementNotEntitled ,
152- },
153- auditLogs : codersdk .EntitlementNotEntitled ,
154- scim : codersdk .EntitlementNotEntitled ,
155- browserOnly : codersdk .EntitlementNotEntitled ,
156- workspaceQuota : codersdk .EntitlementNotEntitled ,
123+ entitlements , err := license .Entitlements (ctx , api .Database , api .Logger , api .Keys , map [string ]bool {
124+ codersdk .FeatureAuditLog : api .AuditLogging ,
125+ codersdk .FeatureBrowserOnly : api .BrowserOnly ,
126+ codersdk .FeatureSCIM : len (api .SCIMAPIKey ) != 0 ,
127+ codersdk .FeatureWorkspaceQuota : api .UserWorkspaceQuota != 0 ,
128+ })
129+ if err != nil {
130+ return err
157131 }
158132
159- // Here we loop through licenses to detect enabled features.
160- for _ , l := range licenses {
161- claims , err := validateDBLicense (l , api .Keys )
162- if err != nil {
163- api .Logger .Debug (ctx , "skipping invalid license" ,
164- slog .F ("id" , l .ID ), slog .Error (err ))
165- continue
133+ featureChanged := func (featureName string ) (changed bool , enabled bool ) {
134+ if api .entitlements .Features == nil {
135+ return true , entitlements .Features [featureName ].Enabled
166136 }
167- entitlements .hasLicense = true
168- entitlement := codersdk .EntitlementEntitled
169- if now .After (claims .LicenseExpires .Time ) {
170- // if the grace period were over, the validation fails, so if we are after
171- // LicenseExpires we must be in grace period.
172- entitlement = codersdk .EntitlementGracePeriod
173- }
174- if claims .Features .UserLimit > 0 {
175- entitlements .activeUsers = codersdk.Feature {
176- Enabled : true ,
177- Entitlement : entitlement ,
178- }
179- currentLimit := int64 (0 )
180- if entitlements .activeUsers .Limit != nil {
181- currentLimit = * entitlements .activeUsers .Limit
182- }
183- limit := max (currentLimit , claims .Features .UserLimit )
184- entitlements .activeUsers .Limit = & limit
185- }
186- if claims .Features .AuditLog > 0 {
187- entitlements .auditLogs = entitlement
188- }
189- if claims .Features .BrowserOnly > 0 {
190- entitlements .browserOnly = entitlement
191- }
192- if claims .Features .SCIM > 0 {
193- entitlements .scim = entitlement
194- }
195- if claims .Features .WorkspaceQuota > 0 {
196- entitlements .workspaceQuota = entitlement
137+ oldFeature := api .entitlements .Features [featureName ]
138+ newFeature := entitlements .Features [featureName ]
139+ if oldFeature .Enabled != newFeature .Enabled {
140+ return true , newFeature .Enabled
197141 }
142+ return false , newFeature .Enabled
198143 }
199144
200- if entitlements .auditLogs != api .entitlements .auditLogs {
201- // A flag could be added to the options that would allow disabling
202- // enhanced audit logging here!
203- if entitlements .auditLogs != codersdk .EntitlementNotEntitled && api .AuditLogging {
204- auditor := audit .NewAuditor (
145+ if changed , enabled := featureChanged (codersdk .FeatureAuditLog ); changed {
146+ auditor := agplaudit .NewNop ()
147+ if enabled {
148+ auditor = audit .NewAuditor (
205149 audit .DefaultFilter ,
206150 backends .NewPostgres (api .Database , true ),
207151 backends .NewSlog (api .Logger ),
208152 )
209- api .AGPL .Auditor .Store (& auditor )
210153 }
154+ api .AGPL .Auditor .Store (& auditor )
211155 }
212156
213- if entitlements . browserOnly != api . entitlements . browserOnly {
157+ if changed , enabled := featureChanged ( codersdk . FeatureBrowserOnly ); changed {
214158 var handler func (rw http.ResponseWriter ) bool
215- if entitlements . browserOnly != codersdk . EntitlementNotEntitled && api . BrowserOnly {
159+ if enabled {
216160 handler = api .shouldBlockNonBrowserConnections
217161 }
218162 api .AGPL .WorkspaceClientCoordinateOverride .Store (& handler )
219163 }
220164
221- if entitlements . workspaceQuota != api . entitlements . workspaceQuota {
222- if entitlements . workspaceQuota != codersdk . EntitlementNotEntitled && api . UserWorkspaceQuota > 0 {
223- enforcer := NewEnforcer ( api . Options . UserWorkspaceQuota )
224- api .AGPL . WorkspaceQuotaEnforcer . Store ( & enforcer )
165+ if changed , enabled := featureChanged ( codersdk . FeatureWorkspaceQuota ); changed {
166+ enforcer := workspacequota . NewNop ()
167+ if enabled {
168+ enforcer = NewEnforcer ( api .Options . UserWorkspaceQuota )
225169 }
170+ api .AGPL .WorkspaceQuotaEnforcer .Store (& enforcer )
226171 }
227172
228173 api .entitlements = entitlements
@@ -235,82 +180,7 @@ func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) {
235180 api .entitlementsMu .RLock ()
236181 entitlements := api .entitlements
237182 api .entitlementsMu .RUnlock ()
238-
239- resp := codersdk.Entitlements {
240- Features : make (map [string ]codersdk.Feature ),
241- Warnings : make ([]string , 0 ),
242- HasLicense : entitlements .hasLicense ,
243- Experimental : api .Experimental ,
244- }
245-
246- if entitlements .activeUsers .Limit != nil {
247- activeUserCount , err := api .Database .GetActiveUserCount (ctx )
248- if err != nil {
249- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
250- Message : "Unable to query database" ,
251- Detail : err .Error (),
252- })
253- return
254- }
255- entitlements .activeUsers .Actual = & activeUserCount
256- if activeUserCount > * entitlements .activeUsers .Limit {
257- resp .Warnings = append (resp .Warnings ,
258- fmt .Sprintf (
259- "Your deployment has %d active users but is only licensed for %d." ,
260- activeUserCount , * entitlements .activeUsers .Limit ))
261- }
262- }
263- resp .Features [codersdk .FeatureUserLimit ] = entitlements .activeUsers
264-
265- // Audit logs
266- resp .Features [codersdk .FeatureAuditLog ] = codersdk.Feature {
267- Entitlement : entitlements .auditLogs ,
268- Enabled : api .AuditLogging ,
269- }
270- // Audit logging is enabled by default. We don't want to display
271- // a warning if they don't have a license.
272- if entitlements .hasLicense && api .AuditLogging {
273- if entitlements .auditLogs == codersdk .EntitlementNotEntitled {
274- resp .Warnings = append (resp .Warnings ,
275- "Audit logging is enabled but your license is not entitled to this feature." )
276- }
277- if entitlements .auditLogs == codersdk .EntitlementGracePeriod {
278- resp .Warnings = append (resp .Warnings ,
279- "Audit logging is enabled but your license for this feature is expired." )
280- }
281- }
282-
283- resp .Features [codersdk .FeatureBrowserOnly ] = codersdk.Feature {
284- Entitlement : entitlements .browserOnly ,
285- Enabled : api .BrowserOnly ,
286- }
287- if api .BrowserOnly {
288- if entitlements .browserOnly == codersdk .EntitlementNotEntitled {
289- resp .Warnings = append (resp .Warnings ,
290- "Browser only connections are enabled but your license is not entitled to this feature." )
291- }
292- if entitlements .browserOnly == codersdk .EntitlementGracePeriod {
293- resp .Warnings = append (resp .Warnings ,
294- "Browser only connections are enabled but your license for this feature is expired." )
295- }
296- }
297-
298- resp .Features [codersdk .FeatureWorkspaceQuota ] = codersdk.Feature {
299- Entitlement : entitlements .workspaceQuota ,
300- Enabled : api .UserWorkspaceQuota > 0 ,
301- }
302- if api .UserWorkspaceQuota > 0 {
303- if entitlements .workspaceQuota == codersdk .EntitlementNotEntitled {
304- resp .Warnings = append (resp .Warnings ,
305- "Workspace quotas are enabled but your license is not entitled to this feature." )
306- }
307- if entitlements .workspaceQuota == codersdk .EntitlementGracePeriod {
308- resp .Warnings = append (resp .Warnings ,
309- "Workspace quotas are enabled but your license for this feature is expired." )
310- }
311- }
312-
313- httpapi .Write (ctx , rw , http .StatusOK , resp )
183+ httpapi .Write (ctx , rw , http .StatusOK , entitlements )
314184}
315185
316186func (api * API ) runEntitlementsLoop (ctx context.Context ) {
@@ -374,10 +244,3 @@ func (api *API) runEntitlementsLoop(ctx context.Context) {
374244 }
375245 }
376246}
377-
378- func max (a , b int64 ) int64 {
379- if a > b {
380- return a
381- }
382- return b
383- }
0 commit comments