-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathauthorize.go
More file actions
339 lines (311 loc) · 12 KB
/
Copy pathauthorize.go
File metadata and controls
339 lines (311 loc) · 12 KB
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
package coderd
import (
"context"
"fmt"
"net/http"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
)
// AuthorizeFilter takes a list of objects and returns the filtered list of
// objects that the user is authorized to perform the given action on.
// This is faster than calling Authorize() on each object.
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action policy.Action, objects []O) ([]O, error) {
roles := httpmw.UserAuthorization(r.Context())
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects, rbac.DefaultFilterThreshold)
if err != nil {
// Log the error as Filter should not be erroring.
h.Logger.Error(r.Context(), "authorization filter failed",
slog.Error(err),
slog.F("user_id", roles.ID),
slog.F("username", roles),
slog.F("roles", roles.SafeRoleNames()),
slog.F("scope", roles.SafeScopeName()),
slog.F("route", r.URL.Path),
slog.F("action", action),
)
return nil, err
}
return objects, nil
}
type HTTPAuthorizer struct {
Authorizer rbac.Authorizer
Logger slog.Logger
}
// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
// Eg:
//
// if !api.Authorize(...) {
// httpapi.Forbidden(rw)
// return
// }
func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
return api.HTTPAuth.Authorize(r, action, object)
}
// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
// Eg:
//
// if !h.Authorize(...) {
// httpapi.Forbidden(rw)
// return
// }
func (h *HTTPAuthorizer) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
roles := httpmw.UserAuthorization(r.Context())
err := h.Authorizer.Authorize(r.Context(), roles, action, object.RBACObject())
if err != nil {
// Log the errors for debugging
internalError := new(rbac.UnauthorizedError)
logger := h.Logger
if xerrors.As(err, internalError) {
logger = h.Logger.With(slog.F("internal_error", internalError.Internal()))
}
// Log information for debugging. This will be very helpful
// in the early days
logger.Warn(r.Context(), "requester is not authorized to access the object",
slog.F("roles", roles.SafeRoleNames()),
slog.F("actor_id", roles.ID),
slog.F("actor_name", roles),
slog.F("scope", roles.SafeScopeName()),
slog.F("route", r.URL.Path),
slog.F("action", action),
slog.F("object", object),
)
return false
}
return true
}
// AuthorizeContext checks whether the RBAC subject on the context
// is authorized to perform the given action. The subject must have
// been set via dbauthz.As or the ExtractAPIKey middleware. Returns
// false if the subject is missing or unauthorized.
func (h *HTTPAuthorizer) AuthorizeContext(ctx context.Context, action policy.Action, object rbac.Objecter) bool {
roles, ok := dbauthz.ActorFromContext(ctx)
if !ok {
h.Logger.Error(ctx, "no authorization actor in context")
return false
}
err := h.Authorizer.Authorize(ctx, roles, action, object.RBACObject())
if err != nil {
internalError := new(rbac.UnauthorizedError)
logger := h.Logger
if xerrors.As(err, internalError) {
logger = h.Logger.With(slog.F("internal_error", internalError.Internal()))
}
logger.Warn(ctx, "requester is not authorized to access the object",
slog.F("roles", roles.SafeRoleNames()),
slog.F("actor_id", roles.ID),
slog.F("actor_name", roles),
slog.F("scope", roles.SafeScopeName()),
slog.F("action", action),
slog.F("object", object),
)
return false
}
return true
}
// AuthorizeSQLFilter returns an authorization filter that can used in a
// SQL 'WHERE' clause. If the filter is used, the resulting rows returned
// from postgres are already authorized, and the caller does not need to
// call 'Authorize()' on the returned objects.
// Note the authorization is only for the given action and object type.
func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
roles := httpmw.UserAuthorization(r.Context())
prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType)
if err != nil {
return nil, xerrors.Errorf("prepare filter: %w", err)
}
return prepared, nil
}
// AuthorizeSQLFilterContext is like AuthorizeSQLFilter but reads the
// RBAC subject from the context directly rather than from an
// *http.Request. The subject must have been set via dbauthz.As.
func (h *HTTPAuthorizer) AuthorizeSQLFilterContext(ctx context.Context, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
roles, ok := dbauthz.ActorFromContext(ctx)
if !ok {
return nil, xerrors.New("no authorization actor in context")
}
prepared, err := h.Authorizer.Prepare(ctx, roles, action, objectType)
if err != nil {
return nil, xerrors.Errorf("prepare filter: %w", err)
}
return prepared, nil
}
// authcheckFilterThreshold is the per-(action, resource type) group size at or
// above which checkAuthorization lets rbac.Filter switch to a single partial
// evaluation. For a subject in many organizations, a group can hold one object
// per organization while the subject also carries one role per organization, so
// Prepare cost grows with the group size. The measured crossover where batching
// beats per-object evaluation is ~35 (DEVEX-608), so this sits above it:
// subjects with few objects of a given type keep the per-object path and cannot
// regress, and only large groups pay for and benefit from partial evaluation.
// It is higher than rbac.DefaultFilterThreshold because that default assumes a
// Prepare cost independent of the input size.
const authcheckFilterThreshold = 50
// authorizeCheck carries an authorization check's response key alongside its
// resolved RBAC object. It implements rbac.Objecter so a batch of same-typed
// checks can be run through rbac.Filter; because the key travels with the
// object, the filtered subset maps back to keys by reading the field, without
// relying on element identity.
type authorizeCheck struct {
key string
object rbac.Object
}
func (c authorizeCheck) RBACObject() rbac.Object { return c.object }
// checkAuthorization returns if the current API key can use the given
// permissions, factoring in the current user's roles and the API key scopes.
//
// @Summary Check authorization
// @ID check-authorization
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Authorization
// @Param request body codersdk.AuthorizationRequest true "Authorization request"
// @Success 200 {object} codersdk.AuthorizationResponse
// @Router /api/v2/authcheck [post]
func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
auth := httpmw.UserAuthorization(r.Context())
var params codersdk.AuthorizationRequest
if !httpapi.Read(ctx, rw, r, ¶ms) {
return
}
api.Logger.Debug(ctx, "check-auth",
slog.F("my_id", httpmw.APIKey(r).UserID),
slog.F("got_id", auth.ID),
slog.F("name", auth),
slog.F("roles", auth.SafeRoleNames()),
slog.F("scope", auth.SafeScopeName()),
)
response := make(codersdk.AuthorizationResponse)
// Prevent using too many resources by ID. This prevents database abuse
// from this endpoint. This also prevents misuse of this endpoint, as
// resource_id should be used for single objects, not for a list of them.
var (
idFetch int
maxFetch = 10
)
for _, v := range params.Checks {
if v.Object.ResourceID != "" {
idFetch++
}
}
if idFetch > maxFetch {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf(
"Endpoint only supports using \"resource_id\" field %d times, found %d usages. Remove %d objects with this field set.",
maxFetch, idFetch, idFetch-maxFetch,
),
})
return
}
// Group the checks by (action, resource type) and authorize each group with
// rbac.Filter, which amortizes a single partial evaluation across the group
// once it is large enough. Each check carries its response key so the
// filtered subset maps back without relying on element identity.
type checkGroup struct {
action policy.Action
objectType string
}
groups := make(map[checkGroup][]authorizeCheck)
for k, v := range params.Checks {
if v.Object.ResourceType == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Object's \"resource_type\" field must be defined for key %q.", k),
})
return
}
obj := rbac.Object{
Owner: v.Object.OwnerID,
OrgID: v.Object.OrganizationID,
Type: string(v.Object.ResourceType),
AnyOrgOwner: v.Object.AnyOrgOwner,
}
if obj.Owner == codersdk.Me {
obj.Owner = auth.ID
}
// If a resource ID is specified, fetch that specific resource.
if v.Object.ResourceID != "" {
id, err := uuid.Parse(v.Object.ResourceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Object %q id is not a valid uuid.", v.Object.ResourceID),
Validations: []codersdk.ValidationError{{Field: "resource_id", Detail: err.Error()}},
})
return
}
var dbObj rbac.Objecter
var dbErr error
// Only support referencing some resources by ID.
switch string(v.Object.ResourceType) {
case rbac.ResourceWorkspace.Type:
dbObj, dbErr = api.Database.GetWorkspaceByID(ctx, id)
case rbac.ResourceTemplate.Type:
dbObj, dbErr = api.Database.GetTemplateByID(ctx, id)
case rbac.ResourceUser.Type:
dbObj, dbErr = api.Database.GetUserByID(ctx, id)
case rbac.ResourceGroup.Type:
dbObj, dbErr = api.Database.GetGroupByID(ctx, id)
default:
msg := fmt.Sprintf("Object type %q does not support \"resource_id\" field.", v.Object.ResourceType)
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: msg,
Validations: []codersdk.ValidationError{{Field: "resource_type", Detail: msg}},
})
return
}
if dbErr != nil {
// 404 or unauthorized is false
response[k] = false
continue
}
obj = dbObj.RBACObject()
}
// AnyOrgOwner objects have no verified semantics under partial
// evaluation: Filter's prepared path can deny objects that a full
// evaluation allows (authz_internal_test.go skips the full-vs-partial
// equivalence assertion for them). Authorize them per-object with a full
// evaluation instead of grouping them into the batched Filter path.
if obj.AnyOrgOwner {
err := api.Authorizer.Authorize(ctx, auth, policy.Action(v.Action), obj)
response[k] = err == nil
continue
}
group := checkGroup{action: policy.Action(v.Action), objectType: obj.Type}
groups[group] = append(groups[group], authorizeCheck{key: k, object: obj})
}
for group, checks := range groups {
allowed, err := rbac.Filter(ctx, api.Authorizer, auth, group.action, checks, authcheckFilterThreshold)
if err != nil {
// A Filter error is never a per-object denial: per-object rejections
// are filtered out inside Filter, so only Prepare failures and context
// errors reach here. Reporting the group as denied would hide an
// evaluation failure behind a "not permitted" answer, so surface it.
if ctx.Err() != nil {
// The client went away or the request was canceled; there is no
// useful response to write and nothing worth logging.
return
}
httpapi.InternalServerError(rw, xerrors.Errorf("authorize %q %q: %w", group.action, group.objectType, err))
return
}
// Default to denied, then mark the checks Filter allowed.
for _, c := range checks {
response[c.key] = false
}
for _, c := range allowed {
response[c.key] = true
}
}
httpapi.Write(ctx, rw, http.StatusOK, response)
}