mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
cb6b5e8fbd
Just moved `rbac.Action` -> `policy.Action`. This is for the stacked PR to not have circular dependencies when doing autogen. Without this, the autogen can produce broken golang code, which prevents the autogen from compiling. So just avoiding circular dependencies. Doing this in it's own PR to reduce LoC diffs in the primary PR, since this has 0 functional changes.
229 lines
7.1 KiB
Go
229 lines
7.1 KiB
Go
package coderd
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"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)
|
|
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects)
|
|
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)
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("prepare filter: %w", err)
|
|
}
|
|
|
|
return prepared, nil
|
|
}
|
|
|
|
// 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 /authcheck [post]
|
|
func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
auth := httpmw.UserAuthorization(r)
|
|
|
|
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
|
|
}
|
|
|
|
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: v.Object.ResourceType.String(),
|
|
}
|
|
if obj.Owner == "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 v.Object.ResourceType.String() {
|
|
case rbac.ResourceWorkspaceExecution.Type:
|
|
workSpace, err := api.Database.GetWorkspaceByID(ctx, id)
|
|
if err == nil {
|
|
dbObj = workSpace.ExecutionRBAC()
|
|
}
|
|
dbErr = err
|
|
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()
|
|
}
|
|
|
|
err := api.Authorizer.Authorize(ctx, auth, policy.Action(v.Action), obj)
|
|
response[k] = err == nil
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, response)
|
|
}
|