forked from coder/coder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdbauthz.go
4076 lines (3519 loc) · 157 KB
/
dbauthz.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package dbauthz
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"github.com/google/uuid"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/open-policy-agent/opa/topdown"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/rbac/rolestore"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/provisionersdk"
)
var _ database.Store = (*querier)(nil)
const wrapname = "dbauthz.querier"
// NoActorError wraps ErrNoRows for the api to return a 404. This is the correct
// response when the user is not authorized.
var NoActorError = xerrors.Errorf("no authorization actor in context: %w", sql.ErrNoRows)
// NotAuthorizedError is a sentinel error that unwraps to sql.ErrNoRows.
// This allows the internal error to be read by the caller if needed. Otherwise
// it will be handled as a 404.
type NotAuthorizedError struct {
Err error
}
// Ensure we implement the IsUnauthorized interface.
var _ httpapiconstraints.IsUnauthorizedError = (*NotAuthorizedError)(nil)
func (e NotAuthorizedError) Error() string {
return fmt.Sprintf("unauthorized: %s", e.Err.Error())
}
// IsUnauthorized implements the IsUnauthorized interface.
func (NotAuthorizedError) IsUnauthorized() bool {
return true
}
// Unwrap will always unwrap to a sql.ErrNoRows so the API returns a 404.
// So 'errors.Is(err, sql.ErrNoRows)' will always be true.
func (e NotAuthorizedError) Unwrap() error {
return e.Err
}
func IsNotAuthorizedError(err error) bool {
if err == nil {
return false
}
if xerrors.Is(err, NoActorError) {
return true
}
return xerrors.As(err, &NotAuthorizedError{})
}
func logNotAuthorizedError(ctx context.Context, logger slog.Logger, err error) error {
// Only log the errors if it is an UnauthorizedError error.
internalError := new(rbac.UnauthorizedError)
if err != nil && xerrors.As(err, &internalError) {
e := new(topdown.Error)
if xerrors.As(err, &e) || e.Code == topdown.CancelErr {
// For some reason rego changes a canceled context to a topdown.CancelErr. We
// expect to check for canceled context errors if the user cancels the request,
// so we should change the error to a context.Canceled error.
//
// NotAuthorizedError is == to sql.ErrNoRows, which is not correct
// if it's actually a canceled context.
contextError := *internalError
contextError.SetInternal(context.Canceled)
return &contextError
}
logger.Debug(ctx, "unauthorized",
slog.F("internal_error", internalError.Internal()),
slog.F("input", internalError.Input()),
slog.Error(err),
)
}
return NotAuthorizedError{
Err: err,
}
}
// querier is a wrapper around the database store that performs authorization
// checks before returning data. All querier methods expect an authorization
// subject present in the context. If no subject is present, most methods will
// fail.
//
// Use WithAuthorizeContext to set the authorization subject in the context for
// the common user case.
type querier struct {
db database.Store
auth rbac.Authorizer
log slog.Logger
acs *atomic.Pointer[AccessControlStore]
}
func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger, acs *atomic.Pointer[AccessControlStore]) database.Store {
// If the underlying db store is already a querier, return it.
// Do not double wrap.
if slices.Contains(db.Wrappers(), wrapname) {
return db
}
return &querier{
db: db,
auth: authorizer,
log: logger,
acs: acs,
}
}
func (q *querier) Wrappers() []string {
return append(q.db.Wrappers(), wrapname)
}
// authorizeContext is a helper function to authorize an action on an object.
func (q *querier) authorizeContext(ctx context.Context, action policy.Action, object rbac.Objecter) error {
act, ok := ActorFromContext(ctx)
if !ok {
return NoActorError
}
err := q.auth.Authorize(ctx, act, action, object.RBACObject())
if err != nil {
return logNotAuthorizedError(ctx, q.log, err)
}
return nil
}
type authContextKey struct{}
// ActorFromContext returns the authorization subject from the context.
// All authentication flows should set the authorization subject in the context.
// If no actor is present, the function returns false.
func ActorFromContext(ctx context.Context) (rbac.Subject, bool) {
a, ok := ctx.Value(authContextKey{}).(rbac.Subject)
return a, ok
}
var (
subjectProvisionerd = rbac.Subject{
FriendlyName: "Provisioner Daemon",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "provisionerd"},
DisplayName: "Provisioner Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
// TODO: Add ProvisionerJob resource type.
rbac.ResourceFile.Type: {policy.ActionRead},
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
// Unsure why provisionerd needs update and read personal
rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop},
rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},
rbac.ResourceApiKey.Type: {policy.WildcardSymbol},
// When org scoped provisioner credentials are implemented,
// this can be reduced to read a specific org.
rbac.ResourceOrganization.Type: {policy.ActionRead},
rbac.ResourceGroup.Type: {policy.ActionRead},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
subjectAutostart = rbac.Subject{
FriendlyName: "Autostart",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "autostart"},
DisplayName: "Autostart Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop},
rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},
rbac.ResourceUser.Type: {policy.ActionRead},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
// See unhanger package.
subjectHangDetector = rbac.Subject{
FriendlyName: "Hang Detector",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "hangdetector"},
DisplayName: "Hang Detector Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceTemplate.Type: {policy.ActionRead},
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
subjectSystemRestricted = rbac.Subject{
FriendlyName: "System",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "system"},
DisplayName: "Coder",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceWildcard.Type: {policy.ActionRead},
rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(),
rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate},
rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(),
rbac.ResourceAssignOrgRole.Type: rbac.ResourceAssignOrgRole.AvailableActions(),
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead},
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH},
rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
)
// AsProvisionerd returns a context with an actor that has permissions required
// for provisionerd to function.
func AsProvisionerd(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, subjectProvisionerd)
}
// AsAutostart returns a context with an actor that has permissions required
// for autostart to function.
func AsAutostart(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, subjectAutostart)
}
// AsHangDetector returns a context with an actor that has permissions required
// for unhanger.Detector to function.
func AsHangDetector(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, subjectHangDetector)
}
// AsSystemRestricted returns a context with an actor that has permissions
// required for various system operations (login, logout, metrics cache).
func AsSystemRestricted(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted)
}
var AsRemoveActor = rbac.Subject{
ID: "remove-actor",
}
// As returns a context with the given actor stored in the context.
// This is used for cases where the actor touching the database is not the
// actor stored in the context.
// When you use this function, be sure to add a //nolint comment
// explaining why it is necessary.
func As(ctx context.Context, actor rbac.Subject) context.Context {
if actor.Equal(AsRemoveActor) {
// AsRemoveActor is a special case that is used to indicate that the actor
// should be removed from the context.
return context.WithValue(ctx, authContextKey{}, nil)
}
return context.WithValue(ctx, authContextKey{}, actor)
}
//
// Generic functions used to implement the database.Store methods.
//
// insert runs an policy.ActionCreate on the rbac object argument before
// running the insertFunc. The insertFunc is expected to return the object that
// was inserted.
func insert[
ObjectType any,
ArgumentType any,
Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error),
](
logger slog.Logger,
authorizer rbac.Authorizer,
object rbac.Objecter,
insertFunc Insert,
) Insert {
return insertWithAction(logger, authorizer, object, policy.ActionCreate, insertFunc)
}
func insertWithAction[
ObjectType any,
ArgumentType any,
Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error),
](
logger slog.Logger,
authorizer rbac.Authorizer,
object rbac.Objecter,
action policy.Action,
insertFunc Insert,
) Insert {
return func(ctx context.Context, arg ArgumentType) (empty ObjectType, err error) {
// Fetch the rbac subject
act, ok := ActorFromContext(ctx)
if !ok {
return empty, NoActorError
}
// Authorize the action
err = authorizer.Authorize(ctx, act, action, object.RBACObject())
if err != nil {
return empty, logNotAuthorizedError(ctx, logger, err)
}
// Insert the database object
return insertFunc(ctx, arg)
}
}
func deleteQ[
ObjectType rbac.Objecter,
ArgumentType any,
Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error),
Delete func(ctx context.Context, arg ArgumentType) error,
](
logger slog.Logger,
authorizer rbac.Authorizer,
fetchFunc Fetch,
deleteFunc Delete,
) Delete {
return fetchAndExec(logger, authorizer,
policy.ActionDelete, fetchFunc, deleteFunc)
}
func updateWithReturn[
ObjectType rbac.Objecter,
ArgumentType any,
Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error),
UpdateQuery func(ctx context.Context, arg ArgumentType) (ObjectType, error),
](
logger slog.Logger,
authorizer rbac.Authorizer,
fetchFunc Fetch,
updateQuery UpdateQuery,
) UpdateQuery {
return fetchAndQuery(logger, authorizer, policy.ActionUpdate, fetchFunc, updateQuery)
}
func update[
ObjectType rbac.Objecter,
ArgumentType any,
Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error),
Exec func(ctx context.Context, arg ArgumentType) error,
](
logger slog.Logger,
authorizer rbac.Authorizer,
fetchFunc Fetch,
updateExec Exec,
) Exec {
return fetchAndExec(logger, authorizer, policy.ActionUpdate, fetchFunc, updateExec)
}
// fetch is a generic function that wraps a database
// query function (returns an object and an error) with authorization. The
// returned function has the same arguments as the database function.
//
// The database query function will **ALWAYS** hit the database, even if the
// user cannot read the resource. This is because the resource details are
// required to run a proper authorization check.
func fetchWithAction[
ArgumentType any,
ObjectType rbac.Objecter,
DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error),
](
logger slog.Logger,
authorizer rbac.Authorizer,
action policy.Action,
f DatabaseFunc,
) DatabaseFunc {
return func(ctx context.Context, arg ArgumentType) (empty ObjectType, err error) {
// Fetch the rbac subject
act, ok := ActorFromContext(ctx)
if !ok {
return empty, NoActorError
}
// Fetch the database object
object, err := f(ctx, arg)
if err != nil {
return empty, xerrors.Errorf("fetch object: %w", err)
}
// Authorize the action
err = authorizer.Authorize(ctx, act, action, object.RBACObject())
if err != nil {
return empty, logNotAuthorizedError(ctx, logger, err)
}
return object, nil
}
}
func fetch[
ArgumentType any,
ObjectType rbac.Objecter,
DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error),
](
logger slog.Logger,
authorizer rbac.Authorizer,
f DatabaseFunc,
) DatabaseFunc {
return fetchWithAction(logger, authorizer, policy.ActionRead, f)
}
// fetchAndExec uses fetchAndQuery but only returns the error. The naming comes
// from SQL 'exec' functions which only return an error.
// See fetchAndQuery for more information.
func fetchAndExec[
ObjectType rbac.Objecter,
ArgumentType any,
Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error),
Exec func(ctx context.Context, arg ArgumentType) error,
](
logger slog.Logger,
authorizer rbac.Authorizer,
action policy.Action,
fetchFunc Fetch,
execFunc Exec,
) Exec {
f := fetchAndQuery(logger, authorizer, action, fetchFunc, func(ctx context.Context, arg ArgumentType) (empty ObjectType, err error) {
return empty, execFunc(ctx, arg)
})
return func(ctx context.Context, arg ArgumentType) error {
_, err := f(ctx, arg)
return err
}
}
// fetchAndQuery is a generic function that wraps a database fetch and query.
// A query has potential side effects in the database (update, delete, etc).
// The fetch is used to know which rbac object the action should be asserted on
// **before** the query runs. The returns from the fetch are only used to
// assert rbac. The final return of this function comes from the Query function.
func fetchAndQuery[
ObjectType rbac.Objecter,
ArgumentType any,
Fetch func(ctx context.Context, arg ArgumentType) (ObjectType, error),
Query func(ctx context.Context, arg ArgumentType) (ObjectType, error),
](
logger slog.Logger,
authorizer rbac.Authorizer,
action policy.Action,
fetchFunc Fetch,
queryFunc Query,
) Query {
return func(ctx context.Context, arg ArgumentType) (empty ObjectType, err error) {
// Fetch the rbac subject
act, ok := ActorFromContext(ctx)
if !ok {
return empty, NoActorError
}
// Fetch the database object
object, err := fetchFunc(ctx, arg)
if err != nil {
return empty, xerrors.Errorf("fetch object: %w", err)
}
// Authorize the action
err = authorizer.Authorize(ctx, act, action, object.RBACObject())
if err != nil {
return empty, logNotAuthorizedError(ctx, logger, err)
}
return queryFunc(ctx, arg)
}
}
// fetchWithPostFilter is like fetch, but works with lists of objects.
// SQL filters are much more optimal.
func fetchWithPostFilter[
ArgumentType any,
ObjectType rbac.Objecter,
DatabaseFunc func(ctx context.Context, arg ArgumentType) ([]ObjectType, error),
](
authorizer rbac.Authorizer,
action policy.Action,
f DatabaseFunc,
) DatabaseFunc {
return func(ctx context.Context, arg ArgumentType) (empty []ObjectType, err error) {
// Fetch the rbac subject
act, ok := ActorFromContext(ctx)
if !ok {
return empty, NoActorError
}
// Fetch the database object
objects, err := f(ctx, arg)
if err != nil {
return nil, xerrors.Errorf("fetch object: %w", err)
}
// Authorize the action
return rbac.Filter(ctx, authorizer, act, action, objects)
}
}
// prepareSQLFilter is a helper function that prepares a SQL filter using the
// given authorization context.
func prepareSQLFilter(ctx context.Context, authorizer rbac.Authorizer, action policy.Action, resourceType string) (rbac.PreparedAuthorized, error) {
act, ok := ActorFromContext(ctx)
if !ok {
return nil, NoActorError
}
return authorizer.Prepare(ctx, act, action, resourceType)
}
func (q *querier) Ping(ctx context.Context) (time.Duration, error) {
return q.db.Ping(ctx)
}
// InTx runs the given function in a transaction.
func (q *querier) InTx(function func(querier database.Store) error, txOpts *sql.TxOptions) error {
return q.db.InTx(func(tx database.Store) error {
// Wrap the transaction store in a querier.
wrapped := New(tx, q.auth, q.log, q.acs)
return function(wrapped)
}, txOpts)
}
// authorizeReadFile is a hotfix for the fact that file permissions are
// independent of template permissions. This function checks if the user has
// update access to any of the file's templates.
func (q *querier) authorizeUpdateFileTemplate(ctx context.Context, file database.File) error {
tpls, err := q.db.GetFileTemplates(ctx, file.ID)
if err != nil {
return err
}
// There __should__ only be 1 template per file, but there can be more than
// 1, so check them all.
for _, tpl := range tpls {
// If the user has update access to any template, they have read access to the file.
if err := q.authorizeContext(ctx, policy.ActionUpdate, tpl); err == nil {
return nil
}
}
return NotAuthorizedError{
Err: xerrors.Errorf("not authorized to read file %s", file.ID),
}
}
// convertToOrganizationRoles converts a set of scoped role names to their unique
// scoped names. The database stores roles as an array of strings, and needs to be
// converted.
// TODO: Maybe make `[]rbac.RoleIdentifier` a custom type that implements a sql scanner
// to remove the need for these converters?
func (*querier) convertToOrganizationRoles(organizationID uuid.UUID, names []string) ([]rbac.RoleIdentifier, error) {
uniques := make([]rbac.RoleIdentifier, 0, len(names))
for _, name := range names {
// This check is a developer safety check. Old code might try to invoke this code path with
// organization id suffixes. Catch this and return a nice error so it can be fixed.
if strings.Contains(name, ":") {
return nil, xerrors.Errorf("attempt to assign a role %q, remove the ':<organization_id> suffix", name)
}
uniques = append(uniques, rbac.RoleIdentifier{Name: name, OrganizationID: organizationID})
}
return uniques, nil
}
// convertToDeploymentRoles converts string role names into deployment wide roles.
func (*querier) convertToDeploymentRoles(names []string) []rbac.RoleIdentifier {
uniques := make([]rbac.RoleIdentifier, 0, len(names))
for _, name := range names {
uniques = append(uniques, rbac.RoleIdentifier{Name: name})
}
return uniques
}
// canAssignRoles handles assigning built in and custom roles.
func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, removed []rbac.RoleIdentifier) error {
actor, ok := ActorFromContext(ctx)
if !ok {
return NoActorError
}
roleAssign := rbac.ResourceAssignRole
shouldBeOrgRoles := false
if orgID != nil {
roleAssign = rbac.ResourceAssignOrgRole.InOrg(*orgID)
shouldBeOrgRoles = true
}
grantedRoles := append(added, removed...)
customRoles := make([]rbac.RoleIdentifier, 0)
// Validate that the roles being assigned are valid.
for _, r := range grantedRoles {
isOrgRole := r.OrganizationID != uuid.Nil
if shouldBeOrgRoles && !isOrgRole {
return xerrors.Errorf("Must only update org roles")
}
if !shouldBeOrgRoles && isOrgRole {
return xerrors.Errorf("Must only update site wide roles")
}
if shouldBeOrgRoles {
if orgID == nil {
return xerrors.Errorf("should never happen, orgID is nil, but trying to assign an organization role")
}
if r.OrganizationID != *orgID {
return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, orgID.String())
}
}
// All roles should be valid roles
if _, err := rbac.RoleByName(r); err != nil {
customRoles = append(customRoles, r)
}
}
customRolesMap := make(map[rbac.RoleIdentifier]struct{}, len(customRoles))
for _, r := range customRoles {
customRolesMap[r] = struct{}{}
}
if len(customRoles) > 0 {
// Leverage any custom role cache that might exist.
expandedCustomRoles, err := rolestore.Expand(ctx, q.db, customRoles)
if err != nil {
return xerrors.Errorf("fetching custom roles: %w", err)
}
// If the lists are not identical, then have a problem, as some roles
// provided do no exist.
if len(customRoles) != len(expandedCustomRoles) {
for _, role := range customRoles {
// Stop at the first one found. We could make a better error that
// returns them all, but then someone could pass in a large list to make us do
// a lot of loop iterations.
if !slices.ContainsFunc(expandedCustomRoles, func(customRole rbac.Role) bool {
return strings.EqualFold(customRole.Identifier.Name, role.Name) && customRole.Identifier.OrganizationID == role.OrganizationID
}) {
return xerrors.Errorf("%q is not a supported role", role)
}
}
}
}
if len(added) > 0 {
if err := q.authorizeContext(ctx, policy.ActionAssign, roleAssign); err != nil {
return err
}
}
if len(removed) > 0 {
if err := q.authorizeContext(ctx, policy.ActionDelete, roleAssign); err != nil {
return err
}
}
for _, roleName := range grantedRoles {
if _, isCustom := customRolesMap[roleName]; isCustom {
// To support a dynamic mapping of what roles can assign what, we need
// to store this in the database. For now, just use a static role so
// owners and org admins can assign roles.
if roleName.IsOrgRole() {
roleName = rbac.CustomOrganizationRole(roleName.OrganizationID)
} else {
roleName = rbac.CustomSiteRole()
}
}
if !rbac.CanAssignRole(actor.Roles, roleName) {
return xerrors.Errorf("not authorized to assign role %q", roleName)
}
}
return nil
}
func (q *querier) SoftDeleteTemplateByID(ctx context.Context, id uuid.UUID) error {
deleteF := func(ctx context.Context, id uuid.UUID) error {
return q.db.UpdateTemplateDeletedByID(ctx, database.UpdateTemplateDeletedByIDParams{
ID: id,
Deleted: true,
UpdatedAt: dbtime.Now(),
})
}
return deleteQ(q.log, q.auth, q.db.GetTemplateByID, deleteF)(ctx, id)
}
func (q *querier) SoftDeleteWorkspaceByID(ctx context.Context, id uuid.UUID) error {
return deleteQ(q.log, q.auth, q.db.GetWorkspaceByID, func(ctx context.Context, id uuid.UUID) error {
return q.db.UpdateWorkspaceDeletedByID(ctx, database.UpdateWorkspaceDeletedByIDParams{
ID: id,
Deleted: true,
})
})(ctx, id)
}
func authorizedTemplateVersionFromJob(ctx context.Context, q *querier, job database.ProvisionerJob) (database.TemplateVersion, error) {
switch job.Type {
case database.ProvisionerJobTypeTemplateVersionDryRun:
// TODO: This is really unfortunate that we need to inspect the json
// payload. We should fix this.
tmp := struct {
TemplateVersionID uuid.UUID `json:"template_version_id"`
}{}
err := json.Unmarshal(job.Input, &tmp)
if err != nil {
return database.TemplateVersion{}, xerrors.Errorf("dry-run unmarshal: %w", err)
}
// Authorized call to get template version.
tv, err := q.GetTemplateVersionByID(ctx, tmp.TemplateVersionID)
if err != nil {
return database.TemplateVersion{}, err
}
return tv, nil
case database.ProvisionerJobTypeTemplateVersionImport:
// Authorized call to get template version.
tv, err := q.GetTemplateVersionByJobID(ctx, job.ID)
if err != nil {
return database.TemplateVersion{}, err
}
return tv, nil
default:
return database.TemplateVersion{}, xerrors.Errorf("unknown job type: %q", job.Type)
}
}
func (q *querier) authorizeTemplateInsights(ctx context.Context, templateIDs []uuid.UUID) error {
// Abort early if can read all template insights, aka admins.
// TODO: If we know the org, that would allow org admins to abort early too.
if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil {
for _, templateID := range templateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil {
return err
}
}
if len(templateIDs) == 0 {
if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil {
return err
}
}
}
return nil
}
// customRoleEscalationCheck checks to make sure the caller has every permission they are adding
// to a custom role. This prevents permission escalation.
func (q *querier) customRoleEscalationCheck(ctx context.Context, actor rbac.Subject, perm rbac.Permission, object rbac.Object) error {
if perm.Negate {
// Users do not need negative permissions. We can include it later if required.
return xerrors.Errorf("invalid permission for action=%q type=%q, no negative permissions", perm.Action, perm.ResourceType)
}
if perm.Action == policy.WildcardSymbol || perm.ResourceType == policy.WildcardSymbol {
// It is possible to check for supersets with wildcards, but wildcards can also
// include resources and actions that do not exist today. Custom roles should only be allowed
// to include permissions for existing resources.
return xerrors.Errorf("invalid permission for action=%q type=%q, no wildcard symbols", perm.Action, perm.ResourceType)
}
object.Type = perm.ResourceType
if err := q.auth.Authorize(ctx, actor, perm.Action, object); err != nil {
// This is a forbidden error, but we can provide more context. Since the user can create a role, just not
// with this perm.
return xerrors.Errorf("invalid permission for action=%q type=%q, not allowed to grant this permission", perm.Action, perm.ResourceType)
}
return nil
}
// customRoleCheck will validate a custom role for inserting or updating.
// If the role is not valid, an error will be returned.
// - Check custom roles are valid for their resource types + actions
// - Check the actor can create the custom role
// - Check the custom role does not grant perms the actor does not have
// - Prevent negative perms
// - Prevent roles with site and org permissions.
func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) error {
act, ok := ActorFromContext(ctx)
if !ok {
return NoActorError
}
// Org permissions require an org role
if role.OrganizationID.UUID == uuid.Nil && len(role.OrgPermissions) > 0 {
return xerrors.Errorf("organization permissions require specifying an organization id")
}
// Org roles can only specify org permissions
if role.OrganizationID.UUID != uuid.Nil && (len(role.SitePermissions) > 0 || len(role.UserPermissions) > 0) {
return xerrors.Errorf("organization roles specify site or user permissions")
}
// The rbac.Role has a 'Valid()' function on it that will do a lot
// of checks.
rbacRole, err := rolestore.ConvertDBRole(database.CustomRole{
Name: role.Name,
DisplayName: role.DisplayName,
SitePermissions: role.SitePermissions,
OrgPermissions: role.OrgPermissions,
UserPermissions: role.UserPermissions,
OrganizationID: role.OrganizationID,
})
if err != nil {
return xerrors.Errorf("invalid args: %w", err)
}
err = rbacRole.Valid()
if err != nil {
return xerrors.Errorf("invalid role: %w", err)
}
if len(rbacRole.Org) > 0 && len(rbacRole.Site) > 0 {
// This is a choice to keep roles simple. If we allow mixing site and org scoped perms, then knowing who can
// do what gets more complicated.
return xerrors.Errorf("invalid custom role, cannot assign both org and site permissions at the same time")
}
if len(rbacRole.Org) > 1 {
// Again to avoid more complexity in our roles
return xerrors.Errorf("invalid custom role, cannot assign permissions to more than 1 org at a time")
}
// Prevent escalation
for _, sitePerm := range rbacRole.Site {
err := q.customRoleEscalationCheck(ctx, act, sitePerm, rbac.Object{Type: sitePerm.ResourceType})
if err != nil {
return xerrors.Errorf("site permission: %w", err)
}
}
for orgID, perms := range rbacRole.Org {
for _, orgPerm := range perms {
err := q.customRoleEscalationCheck(ctx, act, orgPerm, rbac.Object{OrgID: orgID, Type: orgPerm.ResourceType})
if err != nil {
return xerrors.Errorf("org=%q: %w", orgID, err)
}
}
}
for _, userPerm := range rbacRole.User {
err := q.customRoleEscalationCheck(ctx, act, userPerm, rbac.Object{Type: userPerm.ResourceType, Owner: act.ID})
if err != nil {
return xerrors.Errorf("user permission: %w", err)
}
}
return nil
}
func (q *querier) AcquireLock(ctx context.Context, id int64) error {
return q.db.AcquireLock(ctx, id)
}
func (q *querier) AcquireNotificationMessages(ctx context.Context, arg database.AcquireNotificationMessagesParams) ([]database.AcquireNotificationMessagesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.AcquireNotificationMessages(ctx, arg)
}
// TODO: We need to create a ProvisionerJob resource type
func (q *querier) AcquireProvisionerJob(ctx context.Context, arg database.AcquireProvisionerJobParams) (database.ProvisionerJob, error) {
// if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
// return database.ProvisionerJob{}, err
// }
return q.db.AcquireProvisionerJob(ctx, arg)
}
func (q *querier) ActivityBumpWorkspace(ctx context.Context, arg database.ActivityBumpWorkspaceParams) error {
fetch := func(ctx context.Context, arg database.ActivityBumpWorkspaceParams) (database.Workspace, error) {
return q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
}
return update(q.log, q.auth, fetch, q.db.ActivityBumpWorkspace)(ctx, arg)
}
func (q *querier) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) {
// Although this technically only reads users, only system-related functions should be
// allowed to call this.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.AllUserIDs(ctx)
}
func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
tpl, err := q.db.GetTemplateByID(ctx, arg.TemplateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, tpl); err != nil {
return nil, err
}
return q.db.ArchiveUnusedTemplateVersions(ctx, arg)
}
func (q *querier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg database.BatchUpdateWorkspaceLastUsedAtParams) error {
// Could be any workspace and checking auth to each workspace is overkill for the purpose
// of this function.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil {
return err
}
return q.db.BatchUpdateWorkspaceLastUsedAt(ctx, arg)
}
func (q *querier) BulkMarkNotificationMessagesFailed(ctx context.Context, arg database.BulkMarkNotificationMessagesFailedParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return 0, err
}
return q.db.BulkMarkNotificationMessagesFailed(ctx, arg)
}
func (q *querier) BulkMarkNotificationMessagesSent(ctx context.Context, arg database.BulkMarkNotificationMessagesSentParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return 0, err
}
return q.db.BulkMarkNotificationMessagesSent(ctx, arg)
}
func (q *querier) CleanTailnetCoordinators(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return err
}
return q.db.CleanTailnetCoordinators(ctx)
}
func (q *querier) CleanTailnetLostPeers(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return err
}
return q.db.CleanTailnetLostPeers(ctx)
}
func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return err
}
return q.db.CleanTailnetTunnels(ctx)
}
// TODO: Handle org scoped lookups
func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAssignRole); err != nil {
return nil, err
}
return q.db.CustomRoles(ctx, arg)
}
func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error {
return deleteQ(q.log, q.auth, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByID)(ctx, id)