/
role_assignments.go
274 lines (250 loc) · 9.01 KB
/
role_assignments.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
package api
import (
"context"
"encoding/json"
"github.com/brigadecore/brigade/v2/apiserver/internal/meta"
"github.com/pkg/errors"
)
const (
// RoleAssignmentKind represents the canonical RoleAssignment kind string
RoleAssignmentKind = "RoleAssignment"
// RoleAssignmentListKind represents the canonical RoleAssignmentList kind
// string
RoleAssignmentListKind = "RoleAssignmentList"
)
// RoleAssignment represents the assignment of a Role to a principal such as a
// User or ServiceAccount.
type RoleAssignment struct {
// Role assigns a Role to the specified principal.
Role Role `json:"role" bson:"role"`
// Principal specifies the principal to whom the Role is assigned.
Principal PrincipalReference `json:"principal" bson:"principal"`
// Scope qualifies the scope of the Role. The value is opaque and has meaning
// only in relation to a specific Role.
Scope string `json:"scope,omitempty" bson:"scope,omitempty"`
}
// MarshalJSON amends RoleAssignment instances with type metadata.
func (r RoleAssignment) MarshalJSON() ([]byte, error) {
type Alias RoleAssignment
return json.Marshal(
struct {
meta.TypeMeta `json:",inline"`
Alias `json:",inline"`
}{
TypeMeta: meta.TypeMeta{
APIVersion: meta.APIVersion,
Kind: RoleAssignmentKind,
},
Alias: (Alias)(r),
},
)
}
// Matches determines if this RoleAssignment matches the role and scope
// arguments.
func (r RoleAssignment) Matches(role Role, scope string) bool {
return r.Role == role &&
(r.Scope == scope || r.Scope == RoleScopeGlobal)
}
// RoleAssignmentsSelector represents useful filter criteria when selecting
// multiple RoleAssignments for API group operations like list.
type RoleAssignmentsSelector struct {
// Principal specifies that only RoleAssignments for the specified principal
// should be selected.
Principal *PrincipalReference
// Role specifies that only RoleAssignments for the specified Role should be
// selected.
Role Role
}
// RoleAssignmentsService is the specialized interface for managing
// RoleAssignments. It's decoupled from underlying technology choices (e.g. data
// store, message bus, etc.) to keep business logic reusable and consistent
// while the underlying tech stack remains free to change.
type RoleAssignmentsService interface {
// Grant grants the Role specified by the RoleAssignment to the principal also
// specified by the RoleAssignment. If the specified principal does not exist,
// implementations must return a *meta.ErrNotFound error.
Grant(ctx context.Context, roleAssignment RoleAssignment) error
// List returns a RoleAssignmentsList, with its Items (RoleAssignments)
// ordered by principal type, principalID, role, and scope. Criteria for which
// RoleAssignments should be retrieved can be specified using the
// RoleAssignmentsSelector parameter.
List(
context.Context,
RoleAssignmentsSelector,
meta.ListOptions,
) (meta.List[RoleAssignment], error)
// Revoke revokes the Role specified by the RoleAssignment for the principal
// also specified by the RoleAssignment. If the specified principal does not
// exist, implementations must return a *meta.ErrNotFound error.
Revoke(ctx context.Context, roleAssignment RoleAssignment) error
}
type roleAssignmentsService struct {
authorize AuthorizeFn
usersStore UsersStore
serviceAccountsStore ServiceAccountsStore
roleAssignmentsStore RoleAssignmentsStore
}
// NewRoleAssignmentsService returns a specialized interface for managing
// RoleAssignments.
func NewRoleAssignmentsService(
authorizeFn AuthorizeFn,
usersStore UsersStore,
serviceAccountsStore ServiceAccountsStore,
roleAssignmentsStore RoleAssignmentsStore,
) RoleAssignmentsService {
return &roleAssignmentsService{
authorize: authorizeFn,
usersStore: usersStore,
serviceAccountsStore: serviceAccountsStore,
roleAssignmentsStore: roleAssignmentsStore,
}
}
func (r *roleAssignmentsService) Grant(
ctx context.Context,
roleAssignment RoleAssignment,
) error {
if err := r.authorize(ctx, RoleAdmin, ""); err != nil {
return err
}
switch roleAssignment.Principal.Type {
case PrincipalTypeUser:
// Make sure the User exists
user, err := r.usersStore.Get(ctx, roleAssignment.Principal.ID)
if err != nil {
return errors.Wrapf(
err,
"error retrieving user %q from store",
roleAssignment.Principal.ID,
)
}
// From an end-user's perspective, User IDs are case insensitive, but when
// creating a role assignment, we'd like to respect case. So we DON'T use
// the ID from the inbound RoleAssignment-- which may have incorrect case.
// Instead we replace it with the ID (with correct case) from the User we
// found.
roleAssignment.Principal.ID = user.ID
case PrincipalTypeServiceAccount:
// Make sure the ServiceAccount exists
if _, err :=
r.serviceAccountsStore.Get(ctx, roleAssignment.Principal.ID); err != nil {
return errors.Wrapf(
err,
"error retrieving service account %q from store",
roleAssignment.Principal.ID,
)
}
default:
return nil
}
// Give them the Role
if err := r.roleAssignmentsStore.Grant(ctx, roleAssignment); err != nil {
return errors.Wrapf(
err,
"error granting role %q with scope %q to %s %q in store",
roleAssignment.Role,
roleAssignment.Scope,
roleAssignment.Principal.Type,
roleAssignment.Principal.ID,
)
}
return nil
}
func (r *roleAssignmentsService) List(
ctx context.Context,
selector RoleAssignmentsSelector,
opts meta.ListOptions,
) (meta.List[RoleAssignment], error) {
if err := r.authorize(ctx, RoleReader, ""); err != nil {
return meta.List[RoleAssignment]{}, err
}
if opts.Limit == 0 {
opts.Limit = 20
}
roleAssignments, err := r.roleAssignmentsStore.List(ctx, selector, opts)
return roleAssignments,
errors.Wrap(err, "error retrieving role assignments from store")
}
func (r *roleAssignmentsService) Revoke(
ctx context.Context,
roleAssignment RoleAssignment,
) error {
if err := r.authorize(ctx, RoleAdmin, ""); err != nil {
return err
}
switch roleAssignment.Principal.Type {
case PrincipalTypeUser:
// Make sure the User exists
user, err := r.usersStore.Get(ctx, roleAssignment.Principal.ID)
if err != nil {
return errors.Wrapf(
err,
"error retrieving user %q from store",
roleAssignment.Principal.ID,
)
}
// From an end-user's perspective, User IDs are case insensitive, but when
// creating a role assignment, we'd like to respect case. So we DON'T use
// the ID from the inbound RoleAssignment-- which may have incorrect case.
// Instead we replace it with the ID (with correct case) from the User we
// found.
roleAssignment.Principal.ID = user.ID
case PrincipalTypeServiceAccount:
// Make sure the ServiceAccount exists
if _, err :=
r.serviceAccountsStore.Get(ctx, roleAssignment.Principal.ID); err != nil {
return errors.Wrapf(
err,
"error retrieving service account %q from store",
roleAssignment.Principal.ID,
)
}
default:
return nil
}
// Revoke the Role
if err := r.roleAssignmentsStore.Revoke(ctx, roleAssignment); err != nil {
return errors.Wrapf(
err,
"error revoking role %q with scope %q for %s %q in store",
roleAssignment.Role,
roleAssignment.Scope,
roleAssignment.Principal.Type,
roleAssignment.Principal.ID,
)
}
return nil
}
// RoleAssignmentsStore is an interface for components that implement
// RoleAssignment persistence concerns.
type RoleAssignmentsStore interface {
// Grant the role specified by the RoleAssignment to the principal specified
// by the RoleAssignment.
Grant(context.Context, RoleAssignment) error
// List returns a RoleAssignmentsList, with its Items (system-level
// RoleAssignments) ordered by principal type, principalID, role name, and
// scope. Criteria for which RoleAssignments should be retrieved can be
// specified using the RoleAssignmentsSelector parameter.
List(
context.Context,
RoleAssignmentsSelector,
meta.ListOptions,
) (meta.List[RoleAssignment], error)
// Revoke the role specified by the RoleAssignment for the principal specified
// by the RoleAssignment.
Revoke(context.Context, RoleAssignment) error
// RevokeByPrincipal revokes all roles for the principal specified by the
// PrincipalReference.
RevokeByPrincipal(context.Context, PrincipalReference) error
// Exists returns a bool indicating whether the specified RoleAssignment
// exists within the store. Implementations MUST also return true if a
// RoleAssignment exists in the store that logically "overlaps" the specified
// RoleAssignment. For instance, when seeking to determine whether a
// RoleAssignment exists that endows some principal P with Role X having scope
// Y, and such a RoleAssignment does not exist, but one does that endows that
// principal P with Role X having GLOBAL SCOPE (*), then true MUST be
// returned. Implementations MUST also return an error if and only if anything
// goes wrong. i.e. Errors are never used to communicate that the specified
// RoleAssignment does not exist in the store. They are only used to convey an
// actual failure.
Exists(context.Context, RoleAssignment) (bool, error)
}