Ctrl K
ComunidadGuías…

coderd/authorize.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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, &params) {
		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)
}