@@ -5,17 +5,23 @@ import (
5
5
"crypto/ed25519"
6
6
"fmt"
7
7
"net/http"
8
+ "reflect"
8
9
"sync"
9
10
"time"
10
11
12
+ "github.com/coder/coder/enterprise/audit/backends"
13
+
11
14
"github.com/cenkalti/backoff/v4"
15
+ "golang.org/x/xerrors"
12
16
13
17
"cdr.dev/slog"
14
18
15
19
agpl "github.com/coder/coder/coderd"
20
+ agplAudit "github.com/coder/coder/coderd/audit"
16
21
"github.com/coder/coder/coderd/database"
17
22
"github.com/coder/coder/coderd/httpapi"
18
23
"github.com/coder/coder/codersdk"
24
+ "github.com/coder/coder/enterprise/audit"
19
25
)
20
26
21
27
type Enablements struct {
@@ -29,6 +35,13 @@ type featuresService struct {
29
35
keys map [string ]ed25519.PublicKey
30
36
enablements Enablements
31
37
resyncInterval time.Duration
38
+ // enabledImplementations includes an "enabled" implementation of every feature. This is
39
+ // initialized at start of day and remains static. The consequence of this is that these things
40
+ // are hanging around using memory even if not licensed or in use, but it greatly simplifies the
41
+ // logic because we don't have to bother creating and destroying them as entitlements change.
42
+ // If we have a particularly memory-hungry feature in future, we might wish to reconsider this
43
+ // choice.
44
+ enabledImplementations agpl.FeatureInterfaces
32
45
33
46
mu sync.RWMutex
34
47
entitlements entitlements
@@ -44,11 +57,18 @@ func newFeaturesService(
44
57
enablements Enablements ,
45
58
) agpl.FeaturesService {
46
59
fs := & featuresService {
47
- logger : logger ,
48
- database : db ,
49
- pubsub : pubsub ,
50
- keys : keys ,
51
- enablements : enablements ,
60
+ logger : logger ,
61
+ database : db ,
62
+ pubsub : pubsub ,
63
+ keys : keys ,
64
+ enablements : enablements ,
65
+ enabledImplementations : agpl.FeatureInterfaces {
66
+ Auditor : audit .NewAuditor (
67
+ audit .DefaultFilter ,
68
+ backends .NewPostgres (db , true ),
69
+ backends .NewSlog (logger ),
70
+ ),
71
+ },
52
72
resyncInterval : 10 * time .Minute ,
53
73
entitlements : entitlements {
54
74
activeUsers : numericalEntitlement {
@@ -259,3 +279,48 @@ func max(a, b int64) int64 {
259
279
}
260
280
return b
261
281
}
282
+
283
+ func (s * featuresService ) Get (ps any ) error {
284
+ if reflect .TypeOf (ps ).Kind () != reflect .Pointer {
285
+ return xerrors .New ("input must be pointer to struct" )
286
+ }
287
+ vs := reflect .ValueOf (ps ).Elem ()
288
+ if vs .Kind () != reflect .Struct {
289
+ return xerrors .New ("input must be pointer to struct" )
290
+ }
291
+ // grab a local copy of entitlements so that we have a consistent set, but aren't keeping it
292
+ // locked from updates while we process.
293
+ s .mu .RLock ()
294
+ ent := s .entitlements
295
+ s .mu .RUnlock ()
296
+
297
+ for i := 0 ; i < vs .NumField (); i ++ {
298
+ vf := vs .Field (i )
299
+ tf := vf .Type ()
300
+ if tf .Kind () != reflect .Interface {
301
+ return xerrors .Errorf ("fields of input struct must be interfaces: %s" , tf .String ())
302
+ }
303
+
304
+ err := s .setImplementation (ent , vf , tf )
305
+ if err != nil {
306
+ return err
307
+ }
308
+ }
309
+ return nil
310
+ }
311
+
312
+ func (s * featuresService ) setImplementation (ent entitlements , vf reflect.Value , tf reflect.Type ) error {
313
+ // c.f. https://stackoverflow.com/questions/7132848/how-to-get-the-reflect-type-of-an-interface
314
+ switch tf {
315
+ case reflect .TypeOf ((* agplAudit .Auditor )(nil )).Elem ():
316
+ // Audit logging
317
+ if ! s .enablements .AuditLogs || ent .auditLogs .state == notEntitled {
318
+ vf .Set (reflect .ValueOf (agpl .DisabledImplementations .Auditor ))
319
+ return nil
320
+ }
321
+ vf .Set (reflect .ValueOf (s .enabledImplementations .Auditor ))
322
+ return nil
323
+ default :
324
+ return xerrors .Errorf ("unable to find implementation of interface %s" , tf .String ())
325
+ }
326
+ }
0 commit comments