package dbauthz import ( "context" "database/sql" "fmt" "github.com/google/uuid" "golang.org/x/xerrors" "github.com/open-policy-agent/opa/topdown" "cdr.dev/slog" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/rbac" ) var _ database.Store = (*querier)(nil) var ( // NoActorError wraps ErrNoRows for the api to return a 404. This is the correct // response when the user is not authorized. 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 } func (e NotAuthorizedError) Error() string { return fmt.Sprintf("unauthorized: %s", e.Err.Error()) } // 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 (NotAuthorizedError) Unwrap() error { return sql.ErrNoRows } 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 cancelled context to a topdown.CancelErr. We // expect to check for cancelled 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 cancelled context. internalError.SetInternal(context.Canceled) return internalError } logger.Debug(ctx, "unauthorized", slog.F("internal", 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 } func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) database.Store { // If the underlying db store is already a querier, return it. // Do not double wrap. if _, ok := db.(*querier); ok { return db } return &querier{ db: db, auth: authorizer, log: logger, } } // authorizeContext is a helper function to authorize an action on an object. func (q *querier) authorizeContext(ctx context.Context, action rbac.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 } // AsSystem returns a context with a system actor. This is used for internal // system operations that are not tied to any particular actor. // When you use this function, be sure to add a //nolint comment // explaining why it is necessary. // // We trust you have received the usual lecture from the local System // Administrator. It usually boils down to these three things: // #1) Respect the privacy of others. // #2) Think before you type. // #3) With great power comes great responsibility. func AsSystem(ctx context.Context) context.Context { return context.WithValue(ctx, authContextKey{}, rbac.Subject{ ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Name: "system", DisplayName: "System", Site: []rbac.Permission{ { ResourceType: rbac.ResourceWildcard.Type, Action: rbac.WildcardSymbol, }, }, Org: map[string][]rbac.Permission{}, User: []rbac.Permission{}, }, }), Scope: rbac.ScopeAll, }, ) } 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 rbac.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 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, rbac.ActionCreate, 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, rbac.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, rbac.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, rbac.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 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 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, rbac.ActionRead, object.RBACObject()) if err != nil { return empty, logNotAuthorizedError(ctx, logger, err) } return object, nil } } // 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 rbac.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 rbac.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, 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, rbac.ActionRead, 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 rbac.Action, resourceType string) (rbac.PreparedAuthorized, error) { act, ok := ActorFromContext(ctx) if !ok { return nil, NoActorError } return authorizer.Prepare(ctx, act, action, resourceType) }