package dbauthz import ( "context" "database/sql" "encoding/json" "errors" "slices" "strings" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/open-policy-agent/opa/topdown" "golang.org/x/xerrors" slog "cdr.dev/slog/v3" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/rbac/rolestore" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/provisionersdk" ) var _ database.Store = (*querier)(nil) const wrapname = "dbauthz.querier" // ErrNoActor is returned if no actor is present in the context. var ErrNoActor = xerrors.Errorf("no authorization actor in context") // 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 } // Ensure we implement the IsUnauthorized interface. var _ httpapiconstraints.IsUnauthorizedError = (*NotAuthorizedError)(nil) func (e NotAuthorizedError) Error() string { var detail string if e.Err != nil { detail = ": " + e.Err.Error() } return "unauthorized" + detail } // IsUnauthorized implements the IsUnauthorized interface. func (NotAuthorizedError) IsUnauthorized() bool { return true } // 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 (e NotAuthorizedError) Unwrap() error { return e.Err } func IsNotAuthorizedError(err error) bool { if err == nil { return false } if xerrors.Is(err, ErrNoActor) { return true } return xerrors.As(err, &NotAuthorizedError{}) } 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 canceled context to a topdown.CancelErr. We // expect to check for canceled 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 canceled context. contextError := *internalError contextError.SetInternal(context.Canceled) return &contextError } logger.Debug(ctx, "unauthorized", slog.F("internal_error", 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 acs *atomic.Pointer[AccessControlStore] } func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger, acs *atomic.Pointer[AccessControlStore]) database.Store { // If the underlying db store is already a querier, return it. // Do not double wrap. if slices.Contains(db.Wrappers(), wrapname) { return db } return &querier{ db: db, auth: authorizer, log: logger, acs: acs, } } func (q *querier) Wrappers() []string { return append(q.db.Wrappers(), wrapname) } // authorizeContext is a helper function to authorize an action on an object. func (q *querier) authorizeContext(ctx context.Context, action policy.Action, object rbac.Objecter) error { act, ok := ActorFromContext(ctx) if !ok { return ErrNoActor } err := q.auth.Authorize(ctx, act, action, object.RBACObject()) if err != nil { return logNotAuthorizedError(ctx, q.log, err) } return nil } // authorizePrebuiltWorkspace handles authorization for workspace resource types. // prebuilt_workspaces are a subset of workspaces, currently limited to // supporting delete operations. This function first attempts normal workspace // authorization. If that fails, the action is delete or update and the workspace // is a prebuild, a prebuilt-specific authorization is attempted. // Note: Delete operations of workspaces requires both update and delete // permissions. func (q *querier) authorizePrebuiltWorkspace(ctx context.Context, action policy.Action, workspace database.Workspace) error { // Try default workspace authorization first var workspaceErr error if workspaceErr = q.authorizeContext(ctx, action, workspace); workspaceErr == nil { return nil } // Special handling for prebuilt workspace deletion if (action == policy.ActionUpdate || action == policy.ActionDelete) && workspace.IsPrebuild() { var prebuiltErr error if prebuiltErr = q.authorizeContext(ctx, action, workspace.AsPrebuild()); prebuiltErr == nil { return nil } return xerrors.Errorf("authorize context failed for workspace (%v) and prebuilt (%w)", workspaceErr, prebuiltErr) } return xerrors.Errorf("authorize context: %w", workspaceErr) } func workspaceTransitionAction(transition database.WorkspaceTransition) (policy.Action, error) { switch transition { case database.WorkspaceTransitionStart: return policy.ActionWorkspaceStart, nil case database.WorkspaceTransitionStop: return policy.ActionWorkspaceStop, nil case database.WorkspaceTransitionDelete: return policy.ActionDelete, nil default: return "", xerrors.Errorf("unsupported workspace transition %q", transition) } } // authorizeAIBridgeInterceptionAction validates that the context's actor matches the initiator of the AIBridgeInterception. // This is used by all of the sub-resources which fall under the [ResourceAibridgeInterception] umbrella. func (q *querier) authorizeAIBridgeInterceptionAction(ctx context.Context, action policy.Action, interceptionID uuid.UUID) error { inter, err := q.db.GetAIBridgeInterceptionByID(ctx, interceptionID) if err != nil { return xerrors.Errorf("fetch aibridge interception %q: %w", interceptionID, err) } err = q.authorizeContext(ctx, action, inter.RBACObject()) if err != nil { return 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 } var ( subjectProvisionerd = rbac.Subject{ Type: rbac.SubjectTypeProvisionerd, FriendlyName: "Provisioner Daemon", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "provisionerd"}, DisplayName: "Provisioner Daemon", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate}, rbac.ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, // Unsure why provisionerd needs update and read personal rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, // Provisionerd needs to read, update, and delete tasks associated with workspaces. rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, // When org scoped provisioner credentials are implemented, // this can be reduced to read a specific org. rbac.ResourceOrganization.Type: {policy.ActionRead}, rbac.ResourceGroup.Type: {policy.ActionRead}, // Provisionerd creates notification messages rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead}, // Provisionerd creates workspaces resources monitor rbac.ResourceWorkspaceAgentResourceMonitor.Type: {policy.ActionCreate}, rbac.ResourceWorkspaceAgentDevcontainers.Type: {policy.ActionCreate}, // Provisionerd creates usage events rbac.ResourceUsageEvent.Type: {policy.ActionCreate}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectAutostart = rbac.Subject{ Type: rbac.SubjectTypeAutostart, FriendlyName: "Autostart", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "autostart"}, DisplayName: "Autostart Daemon", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceOrganizationMember.Type: {policy.ActionRead}, rbac.ResourceFile.Type: {policy.ActionRead}, // Required to read terraform files rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceUser.Type: {policy.ActionRead}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() // See reaper package. subjectJobReaper = rbac.Subject{ Type: rbac.SubjectTypeJobReaper, FriendlyName: "Job Reaper", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "jobreaper"}, DisplayName: "Job Reaper Daemon", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() // See cryptokeys package. subjectCryptoKeyRotator = rbac.Subject{ Type: rbac.SubjectTypeCryptoKeyRotator, FriendlyName: "Crypto Key Rotator", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "keyrotator"}, DisplayName: "Key Rotator", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceCryptoKey.Type: {policy.WildcardSymbol}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() // See cryptokeys package. subjectCryptoKeyReader = rbac.Subject{ Type: rbac.SubjectTypeCryptoKeyReader, FriendlyName: "Crypto Key Reader", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "keyrotator"}, DisplayName: "Key Rotator", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceCryptoKey.Type: {policy.WildcardSymbol}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectConnectionLogger = rbac.Subject{ Type: rbac.SubjectTypeConnectionLogger, FriendlyName: "Connection Logger", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "connectionlogger"}, DisplayName: "Connection Logger", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceConnectionLog.Type: {policy.ActionUpdate, policy.ActionRead}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectNotifier = rbac.Subject{ Type: rbac.SubjectTypeNotifier, FriendlyName: "Notifier", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "notifier"}, DisplayName: "Notifier", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceInboxNotification.Type: {policy.ActionCreate}, rbac.ResourceWebpushSubscription.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceDeploymentConfig.Type: {policy.ActionRead, policy.ActionUpdate}, // To read and upsert VAPID keys }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectResourceMonitor = rbac.Subject{ Type: rbac.SubjectTypeResourceMonitor, FriendlyName: "Resource Monitor", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "resourcemonitor"}, DisplayName: "Resource Monitor", Site: rbac.Permissions(map[string][]policy.Action{ // The workspace monitor needs to be able to update monitors rbac.ResourceWorkspaceAgentResourceMonitor.Type: {policy.ActionUpdate}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectSubAgentAPI = func(userID uuid.UUID, orgID uuid.UUID) rbac.Subject { return rbac.Subject{ Type: rbac.SubjectTypeSubAgentAPI, FriendlyName: "Sub Agent API", ID: userID.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "subagentapi"}, DisplayName: "Sub Agent API", Site: []rbac.Permission{}, User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{ orgID.String(): { Member: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, }), }, }, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() } subjectSystemRestricted = rbac.Subject{ Type: rbac.SubjectTypeSystemRestricted, FriendlyName: "System", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "system"}, DisplayName: "Coder", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceWildcard.Type: {policy.ActionRead}, rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(), rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate}, rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(), rbac.ResourceAssignOrgRole.Type: rbac.ResourceAssignOrgRole.AvailableActions(), rbac.ResourceSystem.Type: {policy.WildcardSymbol}, rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead}, rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(), rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop}, rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceCryptoKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate}, rbac.ResourceOauth2App.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectSystemOAuth2 = rbac.Subject{ Type: rbac.SubjectTypeSystemOAuth, FriendlyName: "System OAuth2", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "system-oauth2"}, DisplayName: "System OAuth2", Site: rbac.Permissions(map[string][]policy.Action{ // OAuth2 resources - full CRUD permissions rbac.ResourceOauth2App.Type: rbac.ResourceOauth2App.AvailableActions(), rbac.ResourceOauth2AppSecret.Type: rbac.ResourceOauth2AppSecret.AvailableActions(), rbac.ResourceOauth2AppCodeToken.Type: rbac.ResourceOauth2AppCodeToken.AvailableActions(), // API key permissions needed for OAuth2 token revocation rbac.ResourceApiKey.Type: {policy.ActionRead, policy.ActionDelete}, // Minimal read permissions that might be needed for OAuth2 operations rbac.ResourceUser.Type: {policy.ActionRead}, rbac.ResourceOrganization.Type: {policy.ActionRead}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectSystemReadProvisionerDaemons = rbac.Subject{ Type: rbac.SubjectTypeSystemReadProvisionerDaemons, FriendlyName: "Provisioner Daemons Reader", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "system-read-provisioner-daemons"}, DisplayName: "Coder", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceProvisionerDaemon.Type: {policy.ActionRead}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectPrebuildsOrchestrator = rbac.Subject{ Type: rbac.SubjectTypePrebuildsOrchestrator, FriendlyName: "Prebuilds Orchestrator", ID: database.PrebuildsSystemUserID.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "prebuilds-orchestrator"}, DisplayName: "Coder", Site: rbac.Permissions(map[string][]policy.Action{ // May use template, read template-related info, & insert template-related resources (preset prebuilds). rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionUse, policy.ActionViewInsights}, // May CRUD workspaces, and start/stop them. rbac.ResourceWorkspace.Type: { policy.ActionCreate, policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, }, // PrebuiltWorkspaces are a subset of Workspaces. // Explicitly setting PrebuiltWorkspace permissions for clarity. // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. rbac.ResourcePrebuiltWorkspace.Type: { policy.ActionUpdate, policy.ActionDelete, }, // Should be able to add the prebuilds system user as a member to any organization that needs prebuilds. rbac.ResourceOrganizationMember.Type: { policy.ActionRead, policy.ActionCreate, }, // Needs to be able to assign roles to the system user in order to make it a member of an organization. rbac.ResourceAssignOrgRole.Type: { policy.ActionAssign, }, // Needs to be able to read users to determine which organizations the prebuild system user is a member of. rbac.ResourceUser.Type: { policy.ActionRead, }, rbac.ResourceOrganization.Type: { policy.ActionRead, }, // Required to read the terraform files of a template rbac.ResourceFile.Type: { policy.ActionRead, }, // Needs to be able to add the prebuilds system user to the "prebuilds" group in each organization that needs prebuilt workspaces // so that prebuilt workspaces can be scheduled and owned in those organizations. rbac.ResourceGroup.Type: { policy.ActionRead, policy.ActionCreate, policy.ActionUpdate, }, rbac.ResourceGroupMember.Type: { policy.ActionRead, }, }), }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectFileReader = rbac.Subject{ Type: rbac.SubjectTypeFileReader, FriendlyName: "Can Read All Files", // Arbitrary uuid to have a unique ID for this subject. ID: rbac.SubjectTypeFileReaderID, Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "file-reader"}, DisplayName: "FileReader", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceFile.Type: {policy.ActionRead}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectUsagePublisher = rbac.Subject{ Type: rbac.SubjectTypeUsagePublisher, FriendlyName: "Usage Publisher", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "usage-publisher"}, DisplayName: "Usage Publisher", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceLicense.Type: {policy.ActionRead}, // The usage publisher doesn't create events, just // reads/processes them. rbac.ResourceUsageEvent.Type: {policy.ActionRead, policy.ActionUpdate}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() // See aibridged package. subjectAibridged = rbac.Subject{ Type: rbac.SubjectAibridged, FriendlyName: "AI Bridge Daemon", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "aibridged"}, DisplayName: "AI Bridge Daemon", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceUser.Type: { policy.ActionRead, // Required to validate API key owner is active. policy.ActionReadPersonal, // Required to read users' external auth links. // TODO: this is too broad; reduce scope to just external_auth_links by creating separate resource. }, rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys. rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectDBPurge = rbac.Subject{ Type: rbac.SubjectTypeDBPurge, FriendlyName: "DB Purge", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "dbpurge"}, DisplayName: "DB Purge Daemon", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceSystem.Type: {policy.ActionDelete}, rbac.ResourceNotificationMessage.Type: {policy.ActionDelete}, rbac.ResourceApiKey.Type: {policy.ActionDelete}, rbac.ResourceAibridgeInterception.Type: {policy.ActionDelete}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() // Used by the boundary usage tracker to record telemetry statistics. subjectBoundaryUsageTracker = rbac.Subject{ Type: rbac.SubjectTypeBoundaryUsageTracker, FriendlyName: "Boundary Usage Tracker", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "boundary-usage-tracker"}, DisplayName: "Boundary Usage Tracker", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceBoundaryUsage.Type: rbac.ResourceBoundaryUsage.AvailableActions(), }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectWorkspaceBuilder = rbac.Subject{ Type: rbac.SubjectTypeWorkspaceBuilder, FriendlyName: "Workspace Builder", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "workspace-builder"}, DisplayName: "Workspace Builder", Site: rbac.Permissions(map[string][]policy.Action{ // Reading provisioner daemons to check eligibility. rbac.ResourceProvisionerDaemon.Type: {policy.ActionRead}, // Updating provisioner jobs (e.g. marking prebuild // jobs complete). rbac.ResourceProvisionerJobs.Type: {policy.ActionUpdate}, // Reading provisioner state requires template update // permission. rbac.ResourceTemplate.Type: {policy.ActionUpdate}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() subjectChatd = rbac.Subject{ Type: rbac.SubjectTypeChatd, FriendlyName: "Chatd", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "chatd"}, DisplayName: "Chat Daemon", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceWorkspace.Type: {policy.ActionRead}, rbac.ResourceDeploymentConfig.Type: {policy.ActionRead}, rbac.ResourceUser.Type: {policy.ActionReadPersonal}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, }, }), Scope: rbac.ScopeAll, }.WithCachedASTValue() ) // AsProvisionerd returns a context with an actor that has permissions required // for provisionerd to function. func AsProvisionerd(ctx context.Context) context.Context { return As(ctx, subjectProvisionerd) } // AsAutostart returns a context with an actor that has permissions required // for autostart to function. func AsAutostart(ctx context.Context) context.Context { return As(ctx, subjectAutostart) } // AsJobReaper returns a context with an actor that has permissions required // for reaper.Detector to function. func AsJobReaper(ctx context.Context) context.Context { return As(ctx, subjectJobReaper) } // AsKeyRotator returns a context with an actor that has permissions required for rotating crypto keys. func AsKeyRotator(ctx context.Context) context.Context { return As(ctx, subjectCryptoKeyRotator) } // AsKeyReader returns a context with an actor that has permissions required for reading crypto keys. func AsKeyReader(ctx context.Context) context.Context { return As(ctx, subjectCryptoKeyReader) } func AsConnectionLogger(ctx context.Context) context.Context { return As(ctx, subjectConnectionLogger) } // AsNotifier returns a context with an actor that has permissions required for // creating/reading/updating/deleting notifications. func AsNotifier(ctx context.Context) context.Context { return As(ctx, subjectNotifier) } // AsResourceMonitor returns a context with an actor that has permissions required for // updating resource monitors. func AsResourceMonitor(ctx context.Context) context.Context { return As(ctx, subjectResourceMonitor) } // AsSubAgentAPI returns a context with an actor that has permissions required for // handling the lifecycle of sub agents. func AsSubAgentAPI(ctx context.Context, orgID uuid.UUID, userID uuid.UUID) context.Context { return As(ctx, subjectSubAgentAPI(userID, orgID)) } // AsSystemRestricted returns a context with an actor that has permissions // required for various system operations (login, logout, metrics cache). func AsSystemRestricted(ctx context.Context) context.Context { return As(ctx, subjectSystemRestricted) } // AsSystemOAuth2 returns a context with an actor that has permissions // required for OAuth2 provider operations (token revocation, device codes, registration). func AsSystemOAuth2(ctx context.Context) context.Context { return As(ctx, subjectSystemOAuth2) } // AsSystemReadProvisionerDaemons returns a context with an actor that has permissions // to read provisioner daemons. func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context { return As(ctx, subjectSystemReadProvisionerDaemons) } // AsPrebuildsOrchestrator returns a context with an actor that has permissions // to read orchestrator workspace prebuilds. func AsPrebuildsOrchestrator(ctx context.Context) context.Context { return As(ctx, subjectPrebuildsOrchestrator) } // AsFileReader returns a context with an actor that has permissions required // for reading all files. func AsFileReader(ctx context.Context) context.Context { return As(ctx, subjectFileReader) } // AsUsagePublisher returns a context with an actor that has permissions // required for creating, reading, and updating usage events. func AsUsagePublisher(ctx context.Context) context.Context { return As(ctx, subjectUsagePublisher) } // AsAIBridged returns a context with an actor that has permissions // required for creating, reading, and updating aibridge-related resources. func AsAIBridged(ctx context.Context) context.Context { return As(ctx, subjectAibridged) } // AsDBPurge returns a context with an actor that has permissions required // for dbpurge to delete old database records. func AsDBPurge(ctx context.Context) context.Context { return As(ctx, subjectDBPurge) } // AsBoundaryUsageTracker returns a context with an actor that has permissions // required for the boundary usage tracker to record telemetry statistics. func AsBoundaryUsageTracker(ctx context.Context) context.Context { return As(ctx, subjectBoundaryUsageTracker) } // AsWorkspaceBuilder returns a context with an actor that has permissions // required for the workspace builder to prepare workspace builds. This // includes reading provisioner daemons, updating provisioner jobs, and // reading provisioner state (which requires template update permission). func AsWorkspaceBuilder(ctx context.Context) context.Context { return As(ctx, subjectWorkspaceBuilder) } // AsChatd returns a context with an actor scoped to the chat // daemon's background worker. It can manage chats and read // workspaces and deployment config, but nothing else. func AsChatd(ctx context.Context) context.Context { return As(ctx, subjectChatd) } 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) } if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { rlogger.WithAuthContext(actor) } return context.WithValue(ctx, authContextKey{}, actor) } // // Generic functions used to implement the database.Store methods. // // insert runs an policy.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 insertWithAction(logger, authorizer, object, policy.ActionCreate, insertFunc) } func insertWithAction[ ObjectType any, ArgumentType any, Insert func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, object rbac.Objecter, action policy.Action, 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, ErrNoActor } // Authorize the action err = authorizer.Authorize(ctx, act, action, 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, policy.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, policy.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, policy.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 fetchWithAction[ ArgumentType any, ObjectType rbac.Objecter, DatabaseFunc func(ctx context.Context, arg ArgumentType) (ObjectType, error), ]( logger slog.Logger, authorizer rbac.Authorizer, action policy.Action, 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, ErrNoActor } // 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, action, object.RBACObject()) if err != nil { return empty, logNotAuthorizedError(ctx, logger, err) } return object, nil } } 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 fetchWithAction(logger, authorizer, policy.ActionRead, f) } // 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 policy.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 policy.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, ErrNoActor } // 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, action policy.Action, 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, ErrNoActor } // 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, action, 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 policy.Action, resourceType string) (rbac.PreparedAuthorized, error) { act, ok := ActorFromContext(ctx) if !ok { return nil, ErrNoActor } return authorizer.Prepare(ctx, act, action, resourceType) } func (q *querier) Ping(ctx context.Context) (time.Duration, error) { return q.db.Ping(ctx) } func (q *querier) PGLocks(ctx context.Context) (database.PGLocks, error) { return q.db.PGLocks(ctx) } // InTx runs the given function in a transaction. func (q *querier) InTx(function func(querier database.Store) error, txOpts *database.TxOptions) error { return q.db.InTx(func(tx database.Store) error { // Wrap the transaction store in a querier. wrapped := New(tx, q.auth, q.log, q.acs) return function(wrapped) }, txOpts) } // authorizeReadFile is a hotfix for the fact that file permissions are // independent of template permissions. This function checks if the user has // update access to any of the file's templates. func (q *querier) authorizeUpdateFileTemplate(ctx context.Context, file database.File) error { tpls, err := q.db.GetFileTemplates(ctx, file.ID) if err != nil { return err } // There __should__ only be 1 template per file, but there can be more than // 1, so check them all. for _, tpl := range tpls { // If the user has update access to any template, they have read access to the file. if err := q.authorizeContext(ctx, policy.ActionUpdate, tpl); err == nil { return nil } } return NotAuthorizedError{ Err: xerrors.Errorf("not authorized to read file %s", file.ID), } } // convertToOrganizationRoles converts a set of scoped role names to their unique // scoped names. The database stores roles as an array of strings, and needs to be // converted. // TODO: Maybe make `[]rbac.RoleIdentifier` a custom type that implements a sql scanner // to remove the need for these converters? func (*querier) convertToOrganizationRoles(organizationID uuid.UUID, names []string) ([]rbac.RoleIdentifier, error) { uniques := make([]rbac.RoleIdentifier, 0, len(names)) for _, name := range names { // This check is a developer safety check. Old code might try to invoke this code path with // organization id suffixes. Catch this and return a nice error so it can be fixed. if strings.Contains(name, ":") { return nil, xerrors.Errorf("attempt to assign a role %q, remove the ': suffix", name) } uniques = append(uniques, rbac.RoleIdentifier{Name: name, OrganizationID: organizationID}) } return uniques, nil } // convertToDeploymentRoles converts string role names into deployment wide roles. func (*querier) convertToDeploymentRoles(names []string) []rbac.RoleIdentifier { uniques := make([]rbac.RoleIdentifier, 0, len(names)) for _, name := range names { uniques = append(uniques, rbac.RoleIdentifier{Name: name}) } return uniques } // canAssignRoles handles assigning built in and custom roles. func (q *querier) canAssignRoles(ctx context.Context, orgID uuid.UUID, added, removed []rbac.RoleIdentifier) error { actor, ok := ActorFromContext(ctx) if !ok { return ErrNoActor } roleAssign := rbac.ResourceAssignRole shouldBeOrgRoles := false if orgID != uuid.Nil { roleAssign = rbac.ResourceAssignOrgRole.InOrg(orgID) shouldBeOrgRoles = true } grantedRoles := make([]rbac.RoleIdentifier, 0, len(added)+len(removed)) grantedRoles = append(grantedRoles, added...) grantedRoles = append(grantedRoles, removed...) customRoles := make([]rbac.RoleIdentifier, 0) // Validate that the roles being assigned are valid. for _, r := range grantedRoles { isOrgRole := r.OrganizationID != uuid.Nil if shouldBeOrgRoles && !isOrgRole { return xerrors.Errorf("Must only update org roles") } if !shouldBeOrgRoles && isOrgRole { return xerrors.Errorf("Must only update site wide roles") } if shouldBeOrgRoles { if orgID == uuid.Nil { return xerrors.Errorf("should never happen, orgID is nil, but trying to assign an organization role") } if r.OrganizationID != orgID { return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, orgID.String()) } } // All roles should be valid roles if _, err := rbac.RoleByName(r); err != nil { customRoles = append(customRoles, r) } } customRolesMap := make(map[rbac.RoleIdentifier]struct{}, len(customRoles)) for _, r := range customRoles { customRolesMap[r] = struct{}{} } if len(customRoles) > 0 { // Leverage any custom role cache that might exist. expandedCustomRoles, err := rolestore.Expand(ctx, q.db, customRoles) if err != nil { return xerrors.Errorf("fetching custom roles: %w", err) } // If the lists are not identical, then have a problem, as some roles // provided do no exist. if len(customRoles) != len(expandedCustomRoles) { for _, role := range customRoles { // Stop at the first one found. We could make a better error that // returns them all, but then someone could pass in a large list to make us do // a lot of loop iterations. if !slices.ContainsFunc(expandedCustomRoles, func(customRole rbac.Role) bool { return strings.EqualFold(customRole.Identifier.Name, role.Name) && customRole.Identifier.OrganizationID == role.OrganizationID }) { return xerrors.Errorf("%q is not a supported role", role) } } } } if len(added) > 0 { if err := q.authorizeContext(ctx, policy.ActionAssign, roleAssign); err != nil { return err } } if len(removed) > 0 { if err := q.authorizeContext(ctx, policy.ActionUnassign, roleAssign); err != nil { return err } } for _, roleName := range grantedRoles { if _, isCustom := customRolesMap[roleName]; isCustom { // System roles are stored in the database but have a fixed, code-defined // meaning. Do not rewrite the name for them so the static "who can assign // what" mapping applies. if !rbac.SystemRoleName(roleName.Name) { // To support a dynamic mapping of what roles can assign what, we need // to store this in the database. For now, just use a static role so // owners and org admins can assign roles. if roleName.IsOrgRole() { roleName = rbac.CustomOrganizationRole(roleName.OrganizationID) } else { roleName = rbac.CustomSiteRole() } } } if !rbac.CanAssignRole(actor.Roles, roleName) { return xerrors.Errorf("not authorized to assign role %q", roleName) } } return nil } func (q *querier) SoftDeleteTemplateByID(ctx context.Context, id uuid.UUID) error { deleteF := func(ctx context.Context, id uuid.UUID) error { return q.db.UpdateTemplateDeletedByID(ctx, database.UpdateTemplateDeletedByIDParams{ ID: id, Deleted: true, UpdatedAt: dbtime.Now(), }) } return deleteQ(q.log, q.auth, q.db.GetTemplateByID, deleteF)(ctx, id) } func (q *querier) SoftDeleteWorkspaceByID(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetWorkspaceByID, func(ctx context.Context, id uuid.UUID) error { return q.db.UpdateWorkspaceDeletedByID(ctx, database.UpdateWorkspaceDeletedByIDParams{ ID: id, Deleted: true, }) })(ctx, id) } func authorizedTemplateVersionFromJob(ctx context.Context, q *querier, job database.ProvisionerJob) (database.TemplateVersion, error) { switch job.Type { case database.ProvisionerJobTypeTemplateVersionDryRun: // TODO: This is really unfortunate that we need to inspect the json // payload. We should fix this. tmp := struct { TemplateVersionID uuid.UUID `json:"template_version_id"` }{} err := json.Unmarshal(job.Input, &tmp) if err != nil { return database.TemplateVersion{}, xerrors.Errorf("dry-run unmarshal: %w", err) } // Authorized call to get template version. tv, err := q.GetTemplateVersionByID(ctx, tmp.TemplateVersionID) if err != nil { return database.TemplateVersion{}, err } return tv, nil case database.ProvisionerJobTypeTemplateVersionImport: // Authorized call to get template version. tv, err := q.GetTemplateVersionByJobID(ctx, job.ID) if err != nil { return database.TemplateVersion{}, err } return tv, nil default: return database.TemplateVersion{}, xerrors.Errorf("unknown job type: %q", job.Type) } } func (q *querier) authorizeTemplateInsights(ctx context.Context, templateIDs []uuid.UUID) error { // Abort early if can read all template insights, aka admins. // TODO: If we know the org, that would allow org admins to abort early too. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { for _, templateID := range templateIDs { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { return err } } if len(templateIDs) == 0 { if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return err } } } return nil } // customRoleEscalationCheck checks to make sure the caller has every permission they are adding // to a custom role. This prevents permission escalation. func (q *querier) customRoleEscalationCheck(ctx context.Context, actor rbac.Subject, perm rbac.Permission, object rbac.Object) error { if perm.Negate { // Users do not need negative permissions. We can include it later if required. return xerrors.Errorf("invalid permission for action=%q type=%q, no negative permissions", perm.Action, perm.ResourceType) } if perm.Action == policy.WildcardSymbol || perm.ResourceType == policy.WildcardSymbol { // It is possible to check for supersets with wildcards, but wildcards can also // include resources and actions that do not exist today. Custom roles should only be allowed // to include permissions for existing resources. return xerrors.Errorf("invalid permission for action=%q type=%q, no wildcard symbols", perm.Action, perm.ResourceType) } object.Type = perm.ResourceType if err := q.auth.Authorize(ctx, actor, perm.Action, object); err != nil { // This is a forbidden error, but we can provide more context. Since the user can create a role, just not // with this perm. return xerrors.Errorf("invalid permission for action=%q type=%q, not allowed to grant this permission", perm.Action, perm.ResourceType) } return nil } // customRoleCheck will validate a custom role for inserting or updating. // If the role is not valid, an error will be returned. // - Check custom roles are valid for their resource types + actions // - Check the actor can create the custom role // - Check the custom role does not grant perms the actor does not have // - Prevent negative perms for non-system roles // - Prevent roles that have both organization scoped and non-organization scoped permissions func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole, action policy.Action) error { act, ok := ActorFromContext(ctx) if !ok { return ErrNoActor } // Org and org member permissions require an org role. if role.OrganizationID.UUID == uuid.Nil && (len(role.OrgPermissions) > 0 || len(role.MemberPermissions) > 0) { return xerrors.Errorf("organization and member permissions require specifying an organization id") } // Org roles can only specify org permissions; system roles can also specify orgMember ones. if role.OrganizationID.UUID != uuid.Nil && (len(role.SitePermissions) > 0 || len(role.UserPermissions) > 0) { return xerrors.Errorf("organization roles specify site or user permissions") } // For now only system roles can specify orgMember permissions. if !role.IsSystem && len(role.MemberPermissions) > 0 { return xerrors.Errorf("non-system roles specify member permissions") } // The rbac.Role has a 'Valid()' function on it that will do a lot // of checks. rbacRole, err := rolestore.ConvertDBRole(database.CustomRole{ Name: role.Name, DisplayName: role.DisplayName, SitePermissions: role.SitePermissions, OrgPermissions: role.OrgPermissions, UserPermissions: role.UserPermissions, MemberPermissions: role.MemberPermissions, OrganizationID: role.OrganizationID, }) if err != nil { return xerrors.Errorf("invalid args: %w", err) } err = rbacRole.Valid() if err != nil { return xerrors.Errorf("invalid role: %w", err) } if len(rbacRole.ByOrgID) > 0 && (len(rbacRole.Site) > 0 || len(rbacRole.User) > 0) { // This is a choice to keep roles simple. If we allow mixing site and org // scoped perms, then knowing who can do what gets more complicated. Roles // should either be entirely org-scoped or entirely unrelated to // organizations. return xerrors.Errorf("invalid custom role, cannot assign both org-scoped and site/user permissions at the same time") } if len(rbacRole.ByOrgID) > 1 { // Again to avoid more complexity in our roles. Roles are limited to one // organization. return xerrors.Errorf("invalid custom role, cannot assign permissions to more than 1 org at a time") } // System roles are managed internally and may include permissions // (including negative ones) that user-facing custom role APIs // should reject. Still validate that the role shape and perms // are internally consistent via rbacRole.Valid() above. if role.IsSystem { // Defensive programming: the caller should have checked that // the action is authorized, but we double-check. return q.authorizeContext(ctx, action, rbac.ResourceSystem) } // Prevent escalation for _, sitePerm := range rbacRole.Site { err := q.customRoleEscalationCheck(ctx, act, sitePerm, rbac.Object{Type: sitePerm.ResourceType}) if err != nil { return xerrors.Errorf("site permission: %w", err) } } for orgID, perms := range rbacRole.ByOrgID { for _, orgPerm := range perms.Org { err := q.customRoleEscalationCheck(ctx, act, orgPerm, rbac.Object{OrgID: orgID, Type: orgPerm.ResourceType}) if err != nil { return xerrors.Errorf("org=%q: org: %w", orgID, err) } } for _, memberPerm := range perms.Member { // The person giving the permission should still be required to have // the permissions throughout the org in order to give individuals the // same permission among their own resources, since the role can be given // to anyone. The `Owner` is intentionally omitted from the `Object` to // enforce this. err := q.customRoleEscalationCheck(ctx, act, memberPerm, rbac.Object{OrgID: orgID, Type: memberPerm.ResourceType}) if err != nil { return xerrors.Errorf("org=%q: member: %w", orgID, err) } } } for _, userPerm := range rbacRole.User { err := q.customRoleEscalationCheck(ctx, act, userPerm, rbac.Object{Type: userPerm.ResourceType, Owner: act.ID}) if err != nil { return xerrors.Errorf("user permission: %w", err) } } return nil } func (q *querier) authorizeProvisionerJob(ctx context.Context, job database.ProvisionerJob) error { switch job.Type { case database.ProvisionerJobTypeWorkspaceBuild: // Authorized call to get workspace build. If we can read the build, we can // read the job. _, err := q.GetWorkspaceBuildByJobID(ctx, job.ID) if err != nil { return xerrors.Errorf("fetch related workspace build: %w", err) } case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport: // Authorized call to get template version. _, err := authorizedTemplateVersionFromJob(ctx, q, job) if err != nil { return xerrors.Errorf("fetch related template version: %w", err) } default: return xerrors.Errorf("unknown job type: %q", job.Type) } return nil } func (q *querier) AcquireChats(ctx context.Context, arg database.AcquireChatsParams) ([]database.Chat, error) { // AcquireChats is a system-level operation used by the chat processor. // Authorization is done at the system level, not per-user. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil { return nil, err } return q.db.AcquireChats(ctx, arg) } func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } func (q *querier) AcquireNotificationMessages(ctx context.Context, arg database.AcquireNotificationMessagesParams) ([]database.AcquireNotificationMessagesRow, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil { return nil, err } return q.db.AcquireNotificationMessages(ctx, arg) } func (q *querier) AcquireProvisionerJob(ctx context.Context, arg database.AcquireProvisionerJobParams) (database.ProvisionerJob, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return database.ProvisionerJob{}, err } return q.db.AcquireProvisionerJob(ctx, arg) } func (q *querier) AcquireStaleChatDiffStatuses(ctx context.Context, limitVal int32) ([]database.AcquireStaleChatDiffStatusesRow, error) { // This is a system-level batch operation used by the gitsync // background worker. Per-object authorization is impractical // for a SKIP LOCKED acquisition query; callers must use // AsChatd context. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil { return nil, err } return q.db.AcquireStaleChatDiffStatuses(ctx, limitVal) } func (q *querier) ActivityBumpWorkspace(ctx context.Context, arg database.ActivityBumpWorkspaceParams) error { fetch := func(ctx context.Context, arg database.ActivityBumpWorkspaceParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) } return update(q.log, q.auth, fetch, q.db.ActivityBumpWorkspace)(ctx, arg) } func (q *querier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) { // Although this technically only reads users, only system-related functions // should be allowed to call this. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.AllUserIDs(ctx, includeSystem) } func (q *querier) ArchiveChatByID(ctx context.Context, id uuid.UUID) error { chat, err := q.db.GetChatByID(ctx, id) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return err } return q.db.ArchiveChatByID(ctx, id) } func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) { tpl, err := q.db.GetTemplateByID(ctx, arg.TemplateID) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, tpl); err != nil { return nil, err } return q.db.ArchiveUnusedTemplateVersions(ctx, arg) } func (q *querier) BackoffChatDiffStatus(ctx context.Context, arg database.BackoffChatDiffStatusParams) error { // This is a system-level operation used by the gitsync // background worker to reschedule failed refreshes. Same // authorization pattern as AcquireStaleChatDiffStatuses. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil { return err } return q.db.BackoffChatDiffStatus(ctx, arg) } func (q *querier) BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg database.BatchUpdateWorkspaceAgentMetadataParams) error { // Could be any workspace agent and checking auth to each workspace agent is overkill for // the purpose of this function. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil { return err } return q.db.BatchUpdateWorkspaceAgentMetadata(ctx, arg) } func (q *querier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg database.BatchUpdateWorkspaceLastUsedAtParams) error { // Could be any workspace and checking auth to each workspace is overkill for // the purpose of this function. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil { return err } return q.db.BatchUpdateWorkspaceLastUsedAt(ctx, arg) } func (q *querier) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg database.BatchUpdateWorkspaceNextStartAtParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil { return err } return q.db.BatchUpdateWorkspaceNextStartAt(ctx, arg) } func (q *querier) BulkMarkNotificationMessagesFailed(ctx context.Context, arg database.BulkMarkNotificationMessagesFailedParams) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil { return 0, err } return q.db.BulkMarkNotificationMessagesFailed(ctx, arg) } func (q *querier) BulkMarkNotificationMessagesSent(ctx context.Context, arg database.BulkMarkNotificationMessagesSentParams) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil { return 0, err } return q.db.BulkMarkNotificationMessagesSent(ctx, arg) } func (q *querier) CalculateAIBridgeInterceptionsTelemetrySummary(ctx context.Context, arg database.CalculateAIBridgeInterceptionsTelemetrySummaryParams) (database.CalculateAIBridgeInterceptionsTelemetrySummaryRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil { return database.CalculateAIBridgeInterceptionsTelemetrySummaryRow{}, err } return q.db.CalculateAIBridgeInterceptionsTelemetrySummary(ctx, arg) } func (q *querier) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) { empty := database.ClaimPrebuiltWorkspaceRow{} preset, err := q.db.GetPresetByID(ctx, arg.PresetID) if err != nil { return empty, err } workspaceObject := rbac.ResourceWorkspace.WithOwner(arg.NewUserID.String()).InOrg(preset.OrganizationID) err = q.authorizeContext(ctx, policy.ActionCreate, workspaceObject.RBACObject()) if err != nil { return empty, err } tpl, err := q.GetTemplateByID(ctx, preset.TemplateID.UUID) if err != nil { return empty, xerrors.Errorf("verify template by id: %w", err) } if err := q.authorizeContext(ctx, policy.ActionUse, tpl); err != nil { return empty, xerrors.Errorf("use template for workspace: %w", err) } return q.db.ClaimPrebuiltWorkspace(ctx, arg) } func (q *querier) CleanTailnetCoordinators(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return err } return q.db.CleanTailnetCoordinators(ctx) } func (q *querier) CleanTailnetLostPeers(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return err } return q.db.CleanTailnetLostPeers(ctx) } func (q *querier) CleanTailnetTunnels(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return err } return q.db.CleanTailnetTunnels(ctx) } func (q *querier) CountAIBridgeInterceptions(ctx context.Context, arg database.CountAIBridgeInterceptionsParams) (int64, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type) if err != nil { return 0, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.CountAuthorizedAIBridgeInterceptions(ctx, arg, prep) } func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLogsParams) (int64, error) { // Shortcut if the user is an owner. The SQL filter is noticeable, // and this is an easy win for owners. Which is the common case. err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog) if err == nil { return q.db.CountAuditLogs(ctx, arg) } prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAuditLog.Type) if err != nil { return 0, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.CountAuthorizedAuditLogs(ctx, arg, prep) } func (q *querier) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) { // Just like the actual query, shortcut if the user is an owner. err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog) if err == nil { return q.db.CountConnectionLogs(ctx, arg) } prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceConnectionLog.Type) if err != nil { return 0, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.CountAuthorizedConnectionLogs(ctx, arg, prep) } func (q *querier) CountInProgressPrebuilds(ctx context.Context) ([]database.CountInProgressPrebuildsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil { return nil, err } return q.db.CountInProgressPrebuilds(ctx) } func (q *querier) CountPendingNonActivePrebuilds(ctx context.Context) ([]database.CountPendingNonActivePrebuildsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil { return nil, err } return q.db.CountPendingNonActivePrebuilds(ctx) } func (q *querier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceInboxNotification.WithOwner(userID.String())); err != nil { return 0, err } return q.db.CountUnreadInboxNotificationsByUserID(ctx, userID) } func (q *querier) CreateUserSecret(ctx context.Context, arg database.CreateUserSecretParams) (database.UserSecret, error) { obj := rbac.ResourceUserSecret.WithOwner(arg.UserID.String()) if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil { return database.UserSecret{}, err } return q.db.CreateUserSecret(ctx, arg) } // TODO: Handle org scoped lookups func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) { roleObject := rbac.ResourceAssignRole if arg.OrganizationID != uuid.Nil { roleObject = rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID) } if err := q.authorizeContext(ctx, policy.ActionRead, roleObject); err != nil { return nil, err } return q.db.CustomRoles(ctx, arg) } func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error { return deleteQ(q.log, q.auth, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByID)(ctx, id) } func (q *querier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error { // TODO: This is not 100% correct because it omits apikey IDs. err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceApiKey.WithOwner(userID.String())) if err != nil { return err } return q.db.DeleteAPIKeysByUserID(ctx, userID) } func (q *querier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error { chat, err := q.db.GetChatByID(ctx, chatID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return err } return q.db.DeleteAllChatQueuedMessages(ctx, chatID) } func (q *querier) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return err } return q.db.DeleteAllTailnetTunnels(ctx, arg) } func (q *querier) DeleteAllWebpushSubscriptions(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription); err != nil { return err } return q.db.DeleteAllWebpushSubscriptions(ctx) } func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error { // TODO: This is not 100% correct because it omits apikey IDs. err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceApiKey.WithOwner(userID.String())) if err != nil { return err } return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID) } func (q *querier) DeleteChatMessagesAfterID(ctx context.Context, arg database.DeleteChatMessagesAfterIDParams) error { // Authorize update on the parent chat. chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return err } return q.db.DeleteChatMessagesAfterID(ctx, arg) } func (q *querier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.DeleteChatModelConfigByID(ctx, id) } func (q *querier) DeleteChatProviderByID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.DeleteChatProviderByID(ctx, id) } func (q *querier) DeleteChatQueuedMessage(ctx context.Context, arg database.DeleteChatQueuedMessageParams) error { chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return err } return q.db.DeleteChatQueuedMessage(ctx, arg) } func (q *querier) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err } return q.db.DeleteCryptoKey(ctx, arg) } func (q *querier) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error { if !arg.OrganizationID.Valid || arg.OrganizationID.UUID == uuid.Nil { return NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")} } if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { return err } return q.db.DeleteCustomRole(ctx, arg) } func (q *querier) DeleteExpiredAPIKeys(ctx context.Context, arg database.DeleteExpiredAPIKeysParams) (int64, error) { // Requires DELETE across all API keys. if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceApiKey); err != nil { return 0, err } return q.db.DeleteExpiredAPIKeys(ctx, arg) } func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error { return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) { //nolint:gosimple return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID}) }, q.db.DeleteExternalAuthLink)(ctx, arg) } func (q *querier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetGroupByID, q.db.DeleteGroupByID)(ctx, id) } func (q *querier) DeleteGroupMemberFromGroup(ctx context.Context, arg database.DeleteGroupMemberFromGroupParams) error { // Deleting a group member counts as updating a group. fetch := func(ctx context.Context, arg database.DeleteGroupMemberFromGroupParams) (database.Group, error) { return q.db.GetGroupByID(ctx, arg.GroupID) } return update(q.log, q.auth, fetch, q.db.DeleteGroupMemberFromGroup)(ctx, arg) } func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) { err := deleteQ(q.log, q.auth, q.db.GetLicenseByID, func(ctx context.Context, id int32) error { _, err := q.db.DeleteLicense(ctx, id) return err })(ctx, id) if err != nil { return -1, err } return id, nil } func (q *querier) DeleteOAuth2ProviderAppByClientID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2App); err != nil { return err } return q.db.DeleteOAuth2ProviderAppByClientID(ctx, id) } func (q *querier) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2App); err != nil { return err } return q.db.DeleteOAuth2ProviderAppByID(ctx, id) } func (q *querier) DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error { code, err := q.db.GetOAuth2ProviderAppCodeByID(ctx, id) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionDelete, code); err != nil { return err } return q.db.DeleteOAuth2ProviderAppCodeByID(ctx, id) } func (q *querier) DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return err } return q.db.DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx, arg) } func (q *querier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2AppSecret); err != nil { return err } return q.db.DeleteOAuth2ProviderAppSecretByID(ctx, id) } func (q *querier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg database.DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return err } return q.db.DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx, arg) } func (q *querier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAibridgeInterception); err != nil { return -1, err } return q.db.DeleteOldAIBridgeRecords(ctx, beforeTime) } func (q *querier) DeleteOldAuditLogConnectionEvents(ctx context.Context, threshold database.DeleteOldAuditLogConnectionEventsParams) error { // `ResourceSystem` is deprecated, but it doesn't make sense to add // `policy.ActionDelete` to `ResourceAuditLog`, since this is the one and // only time we'll be deleting from the audit log. if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteOldAuditLogConnectionEvents(ctx, threshold) } func (q *querier) DeleteOldAuditLogs(ctx context.Context, arg database.DeleteOldAuditLogsParams) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return 0, err } return q.db.DeleteOldAuditLogs(ctx, arg) } func (q *querier) DeleteOldConnectionLogs(ctx context.Context, arg database.DeleteOldConnectionLogsParams) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return 0, err } return q.db.DeleteOldConnectionLogs(ctx, arg) } func (q *querier) DeleteOldNotificationMessages(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceNotificationMessage); err != nil { return err } return q.db.DeleteOldNotificationMessages(ctx) } func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteOldProvisionerDaemons(ctx) } func (q *querier) DeleteOldTelemetryLocks(ctx context.Context, beforeTime time.Time) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteOldTelemetryLocks(ctx, beforeTime) } func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return 0, err } return q.db.DeleteOldWorkspaceAgentLogs(ctx, threshold) } func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteOldWorkspaceAgentStats(ctx) } func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error { return deleteQ[database.OrganizationMember](q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) { member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: arg.OrganizationID, UserID: arg.UserID, IncludeSystem: false, GithubUserID: 0, })) if err != nil { return database.OrganizationMember{}, err } return member.OrganizationMember, nil }, q.db.DeleteOrganizationMember)(ctx, arg) } func (q *querier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetProvisionerKeyByID, q.db.DeleteProvisionerKey)(ctx, id) } func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteReplicasUpdatedBefore(ctx, updatedAt) } func (q *querier) DeleteRuntimeConfig(ctx context.Context, key string) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteRuntimeConfig(ctx, key) } func (q *querier) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return database.DeleteTailnetPeerRow{}, err } return q.db.DeleteTailnetPeer(ctx, arg) } func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTailnetTunnelParams) (database.DeleteTailnetTunnelRow, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return database.DeleteTailnetTunnelRow{}, err } return q.db.DeleteTailnetTunnel(ctx, arg) } func (q *querier) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (uuid.UUID, error) { task, err := q.db.GetTaskByID(ctx, arg.ID) if err != nil { return uuid.UUID{}, err } if err := q.authorizeContext(ctx, policy.ActionDelete, task.RBACObject()); err != nil { return uuid.UUID{}, err } return q.db.DeleteTask(ctx, arg) } func (q *querier) DeleteUserSecret(ctx context.Context, id uuid.UUID) error { // First get the secret to check ownership secret, err := q.GetUserSecret(ctx, id) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionDelete, secret); err != nil { return err } return q.db.DeleteUserSecret(ctx, id) } func (q *querier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil { return err } return q.db.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, arg) } func (q *querier) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteWebpushSubscriptions(ctx, ids) } func (q *querier) DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) error { fetch := func(ctx context.Context, id uuid.UUID) (database.WorkspaceTable, error) { w, err := q.db.GetWorkspaceByID(ctx, id) if err != nil { return database.WorkspaceTable{}, err } return w.WorkspaceTable(), nil } return fetchAndExec(q.log, q.auth, policy.ActionShare, fetch, q.db.DeleteWorkspaceACLByID)(ctx, id) } func (q *querier) DeleteWorkspaceACLsByOrganization(ctx context.Context, organizationID uuid.UUID) error { // This is a system-only function. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.DeleteWorkspaceACLsByOrganization(ctx, organizationID) } func (q *querier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error { w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { return err } // deleting a workspace port share is more akin to just updating the workspace. if err = q.authorizeContext(ctx, policy.ActionUpdate, w.RBACObject()); err != nil { return xerrors.Errorf("authorize context: %w", err) } return q.db.DeleteWorkspaceAgentPortShare(ctx, arg) } func (q *querier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return err } return q.db.DeleteWorkspaceAgentPortSharesByTemplate(ctx, templateID) } func (q *querier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error { workspace, err := q.db.GetWorkspaceByAgentID(ctx, id) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionDeleteAgent, workspace); err != nil { return err } return q.db.DeleteWorkspaceSubAgentByID(ctx, id) } func (q *querier) DisableForeignKeysAndTriggers(ctx context.Context) error { if !testing.Testing() { return xerrors.Errorf("DisableForeignKeysAndTriggers is only allowed in tests") } return q.db.DisableForeignKeysAndTriggers(ctx) } func (q *querier) EnqueueNotificationMessage(ctx context.Context, arg database.EnqueueNotificationMessageParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceNotificationMessage); err != nil { return err } return q.db.EnqueueNotificationMessage(ctx, arg) } func (q *querier) ExpirePrebuildsAPIKeys(ctx context.Context, now time.Time) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceApiKey); err != nil { return err } return q.db.ExpirePrebuildsAPIKeys(ctx, now) } func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error { fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, id) } return update(q.log, q.auth, fetch, q.db.FavoriteWorkspace)(ctx, id) } func (q *querier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (database.WorkspaceAgentMemoryResourceMonitor, error) { workspace, err := q.db.GetWorkspaceByAgentID(ctx, agentID) if err != nil { return database.WorkspaceAgentMemoryResourceMonitor{}, err } err = q.authorizeContext(ctx, policy.ActionRead, workspace) if err != nil { return database.WorkspaceAgentMemoryResourceMonitor{}, err } return q.db.FetchMemoryResourceMonitorsByAgentID(ctx, agentID) } func (q *querier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) { // Ideally, we would return a list of monitors that the user has access to. However, that check would need to // be implemented similarly to GetWorkspaces, which is more complex than what we're doing here. Since this query // was introduced for telemetry, we perform a simpler check. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return nil, err } return q.db.FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt) } func (q *querier) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil { return database.FetchNewMessageMetadataRow{}, err } return q.db.FetchNewMessageMetadata(ctx, arg) } func (q *querier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { workspace, err := q.db.GetWorkspaceByAgentID(ctx, agentID) if err != nil { return nil, err } err = q.authorizeContext(ctx, policy.ActionRead, workspace) if err != nil { return nil, err } return q.db.FetchVolumesResourceMonitorsByAgentID(ctx, agentID) } func (q *querier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { // Ideally, we would return a list of monitors that the user has access to. However, that check would need to // be implemented similarly to GetWorkspaces, which is more complex than what we're doing here. Since this query // was introduced for telemetry, we perform a simpler check. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return nil, err } return q.db.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt) } func (q *querier) FindMatchingPresetID(ctx context.Context, arg database.FindMatchingPresetIDParams) (uuid.UUID, error) { _, err := q.GetTemplateVersionByID(ctx, arg.TemplateVersionID) if err != nil { return uuid.Nil, err } return q.db.FindMatchingPresetID(ctx, arg) } func (q *querier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (database.AIBridgeInterception, error) { return fetch(q.log, q.auth, q.db.GetAIBridgeInterceptionByID)(ctx, id) } func (q *querier) GetAIBridgeInterceptionLineageByToolCallID(ctx context.Context, toolCallID string) (database.GetAIBridgeInterceptionLineageByToolCallIDRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil { return database.GetAIBridgeInterceptionLineageByToolCallIDRow{}, err } return q.db.GetAIBridgeInterceptionLineageByToolCallID(ctx, toolCallID) } func (q *querier) GetAIBridgeInterceptions(ctx context.Context) ([]database.AIBridgeInterception, error) { fetch := func(ctx context.Context, _ any) ([]database.AIBridgeInterception, error) { return q.db.GetAIBridgeInterceptions(ctx) } return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]database.AIBridgeTokenUsage, error) { // All aibridge_token_usages records belong to the initiator of their associated interception. if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionRead, interceptionID); err != nil { return nil, err } return q.db.GetAIBridgeTokenUsagesByInterceptionID(ctx, interceptionID) } func (q *querier) GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]database.AIBridgeToolUsage, error) { // All aibridge_token_usages records belong to the initiator of their associated interception. if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionRead, interceptionID); err != nil { return nil, err } return q.db.GetAIBridgeToolUsagesByInterceptionID(ctx, interceptionID) } func (q *querier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]database.AIBridgeUserPrompt, error) { // All aibridge_token_usages records belong to the initiator of their associated interception. if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionRead, interceptionID); err != nil { return nil, err } return q.db.GetAIBridgeUserPromptsByInterceptionID(ctx, interceptionID) } func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) { return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id) } func (q *querier) GetAPIKeyByName(ctx context.Context, arg database.GetAPIKeyByNameParams) (database.APIKey, error) { return fetch(q.log, q.auth, q.db.GetAPIKeyByName)(ctx, arg) } func (q *querier) GetAPIKeysByLoginType(ctx context.Context, loginType database.GetAPIKeysByLoginTypeParams) ([]database.APIKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysByLoginType)(ctx, loginType) } func (q *querier) GetAPIKeysByUserID(ctx context.Context, params database.GetAPIKeysByUserIDParams) ([]database.APIKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysByUserID)(ctx, params) } func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]database.APIKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysLastUsedAfter)(ctx, lastUsed) } func (q *querier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil { return nil, err } return q.db.GetActivePresetPrebuildSchedules(ctx) } func (q *querier) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return 0, err } return q.db.GetActiveUserCount(ctx, includeSystem) } func (q *querier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceBuild, error) { // This is a system-only function. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return []database.WorkspaceBuild{}, err } return q.db.GetActiveWorkspaceBuildsByTemplateID(ctx, templateID) } func (q *querier) GetAllTailnetCoordinators(ctx context.Context) ([]database.TailnetCoordinator, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err } return q.db.GetAllTailnetCoordinators(ctx) } func (q *querier) GetAllTailnetPeers(ctx context.Context) ([]database.TailnetPeer, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err } return q.db.GetAllTailnetPeers(ctx) } func (q *querier) GetAllTailnetTunnels(ctx context.Context) ([]database.TailnetTunnel, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err } return q.db.GetAllTailnetTunnels(ctx) } func (q *querier) GetAndResetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (database.GetAndResetBoundaryUsageSummaryRow, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceBoundaryUsage); err != nil { return database.GetAndResetBoundaryUsageSummaryRow{}, err } return q.db.GetAndResetBoundaryUsageSummary(ctx, maxStalenessMs) } func (q *querier) GetAnnouncementBanners(ctx context.Context) (string, error) { // No authz checks return q.db.GetAnnouncementBanners(ctx) } func (q *querier) GetApplicationName(ctx context.Context) (string, error) { // No authz checks return q.db.GetApplicationName(ctx) } func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) { // Shortcut if the user is an owner. The SQL filter is noticeable, // and this is an easy win for owners. Which is the common case. err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog) if err == nil { return q.db.GetAuditLogsOffset(ctx, arg) } prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAuditLog.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.GetAuthorizedAuditLogsOffset(ctx, arg, prep) } func (q *querier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow, error) { // This is a system function. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow{}, err } return q.db.GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx, authToken) } func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (database.GetAuthorizationUserRolesRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.GetAuthorizationUserRolesRow{}, err } return q.db.GetAuthorizationUserRoles(ctx, userID) } func (q *querier) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) { return fetch(q.log, q.auth, q.db.GetChatByID)(ctx, id) } func (q *querier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (database.Chat, error) { return fetch(q.log, q.auth, q.db.GetChatByIDForUpdate)(ctx, id) } func (q *querier) GetChatCostPerChat(ctx context.Context, arg database.GetChatCostPerChatParams) ([]database.GetChatCostPerChatRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.OwnerID.String())); err != nil { return nil, err } return q.db.GetChatCostPerChat(ctx, arg) } func (q *querier) GetChatCostPerModel(ctx context.Context, arg database.GetChatCostPerModelParams) ([]database.GetChatCostPerModelRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.OwnerID.String())); err != nil { return nil, err } return q.db.GetChatCostPerModel(ctx, arg) } func (q *querier) GetChatCostPerUser(ctx context.Context, arg database.GetChatCostPerUserParams) ([]database.GetChatCostPerUserRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil { return nil, err } return q.db.GetChatCostPerUser(ctx, arg) } func (q *querier) GetChatCostSummary(ctx context.Context, arg database.GetChatCostSummaryParams) (database.GetChatCostSummaryRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.OwnerID.String())); err != nil { return database.GetChatCostSummaryRow{}, err } return q.db.GetChatCostSummary(ctx, arg) } func (q *querier) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) { // Authorize read on the parent chat. _, err := q.GetChatByID(ctx, chatID) if err != nil { return database.ChatDiffStatus{}, err } return q.db.GetChatDiffStatusByChatID(ctx, chatID) } func (q *querier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIDs []uuid.UUID) ([]database.ChatDiffStatus, error) { if len(chatIDs) == 0 { return []database.ChatDiffStatus{}, nil } actor, ok := ActorFromContext(ctx) if ok && actor.Type == rbac.SubjectTypeSystemRestricted { return q.db.GetChatDiffStatusesByChatIDs(ctx, chatIDs) } for _, chatID := range chatIDs { // Authorize read on each parent chat. _, err := q.GetChatByID(ctx, chatID) if err != nil { return nil, err } } return q.db.GetChatDiffStatusesByChatIDs(ctx, chatIDs) } func (q *querier) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.ChatFile, error) { file, err := q.db.GetChatFileByID(ctx, id) if err != nil { return database.ChatFile{}, err } if err := q.authorizeContext(ctx, policy.ActionRead, file); err != nil { return database.ChatFile{}, err } return file, nil } func (q *querier) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ChatFile, error) { files, err := q.db.GetChatFilesByIDs(ctx, ids) if err != nil { return nil, err } for _, f := range files { if err := q.authorizeContext(ctx, policy.ActionRead, f); err != nil { return nil, err } } return files, nil } func (q *querier) GetChatMessageByID(ctx context.Context, id int64) (database.ChatMessage, error) { // ChatMessages are authorized through their parent Chat. // We need to fetch the message first to get its chat_id. msg, err := q.db.GetChatMessageByID(ctx, id) if err != nil { return database.ChatMessage{}, err } // Authorize read on the parent chat. _, err = q.GetChatByID(ctx, msg.ChatID) if err != nil { return database.ChatMessage{}, err } return msg, nil } func (q *querier) GetChatMessagesByChatID(ctx context.Context, arg database.GetChatMessagesByChatIDParams) ([]database.ChatMessage, error) { // Authorize read on the parent chat. _, err := q.GetChatByID(ctx, arg.ChatID) if err != nil { return nil, err } return q.db.GetChatMessagesByChatID(ctx, arg) } func (q *querier) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) { // Authorize read on the parent chat. _, err := q.GetChatByID(ctx, chatID) if err != nil { return nil, err } return q.db.GetChatMessagesForPromptByChatID(ctx, chatID) } func (q *querier) GetChatModelConfigByID(ctx context.Context, id uuid.UUID) (database.ChatModelConfig, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return database.ChatModelConfig{}, err } return q.db.GetChatModelConfigByID(ctx, id) } func (q *querier) GetChatModelConfigs(ctx context.Context) ([]database.ChatModelConfig, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return nil, err } return q.db.GetChatModelConfigs(ctx) } func (q *querier) GetChatProviderByID(ctx context.Context, id uuid.UUID) (database.ChatProvider, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return database.ChatProvider{}, err } return q.db.GetChatProviderByID(ctx, id) } func (q *querier) GetChatProviderByProvider(ctx context.Context, provider string) (database.ChatProvider, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return database.ChatProvider{}, err } return q.db.GetChatProviderByProvider(ctx, provider) } func (q *querier) GetChatProviders(ctx context.Context) ([]database.ChatProvider, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return nil, err } return q.db.GetChatProviders(ctx) } func (q *querier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) { _, err := q.GetChatByID(ctx, chatID) if err != nil { return nil, err } return q.db.GetChatQueuedMessages(ctx, chatID) } func (q *querier) GetChatSystemPrompt(ctx context.Context) (string, error) { // The system prompt is a deployment-wide setting read during chat // creation by every authenticated user, so no RBAC policy check // is needed. We still verify that a valid actor exists in the // context to ensure this is never callable by an unauthenticated // or system-internal path without an explicit actor. if _, ok := ActorFromContext(ctx); !ok { return "", ErrNoActor } return q.db.GetChatSystemPrompt(ctx) } func (q *querier) GetChatsByOwnerID(ctx context.Context, ownerID database.GetChatsByOwnerIDParams) ([]database.Chat, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByOwnerID)(ctx, ownerID) } func (q *querier) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) { // Just like with the audit logs query, shortcut if the user is an owner. err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog) if err == nil { return q.db.GetConnectionLogsOffset(ctx, arg) } prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceConnectionLog.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.GetAuthorizedConnectionLogsOffset(ctx, arg, prep) } func (q *querier) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err } return q.db.GetCryptoKeyByFeatureAndSequence(ctx, arg) } func (q *querier) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil { return nil, err } return q.db.GetCryptoKeys(ctx) } func (q *querier) GetCryptoKeysByFeature(ctx context.Context, feature database.CryptoKeyFeature) ([]database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil { return nil, err } return q.db.GetCryptoKeysByFeature(ctx, feature) } func (q *querier) GetDBCryptKeys(ctx context.Context) ([]database.DBCryptKey, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetDBCryptKeys(ctx) } func (q *querier) GetDERPMeshKey(ctx context.Context) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err } return q.db.GetDERPMeshKey(ctx) } func (q *querier) GetDefaultChatModelConfig(ctx context.Context) (database.ChatModelConfig, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return database.ChatModelConfig{}, err } return q.db.GetDefaultChatModelConfig(ctx) } func (q *querier) GetDefaultOrganization(ctx context.Context) (database.Organization, error) { return fetch(q.log, q.auth, func(ctx context.Context, _ any) (database.Organization, error) { return q.db.GetDefaultOrganization(ctx) })(ctx, nil) } func (q *querier) GetDefaultProxyConfig(ctx context.Context) (database.GetDefaultProxyConfigRow, error) { // No authz checks return q.db.GetDefaultProxyConfig(ctx) } func (q *querier) GetDeploymentID(ctx context.Context) (string, error) { // No authz checks return q.db.GetDeploymentID(ctx) } func (q *querier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAfter time.Time) (database.GetDeploymentWorkspaceAgentStatsRow, error) { return q.db.GetDeploymentWorkspaceAgentStats(ctx, createdAfter) } func (q *querier) GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (database.GetDeploymentWorkspaceAgentUsageStatsRow, error) { return q.db.GetDeploymentWorkspaceAgentUsageStats(ctx, createdAt) } func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.GetDeploymentWorkspaceStatsRow, error) { return q.db.GetDeploymentWorkspaceStats(ctx) } func (q *querier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIDs []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetEligibleProvisionerDaemonsByProvisionerJobIDs)(ctx, provisionerJobIDs) } func (q *querier) GetEnabledChatModelConfigs(ctx context.Context) ([]database.ChatModelConfig, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return nil, err } return q.db.GetEnabledChatModelConfigs(ctx) } func (q *querier) GetEnabledChatProviders(ctx context.Context) ([]database.ChatProvider, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return nil, err } return q.db.GetEnabledChatProviders(ctx) } func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) { return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLink)(ctx, arg) } func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]database.ExternalAuthLink, error) { return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLinksByUserID)(ctx, userID) } func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetFailedWorkspaceBuildsByTemplateID(ctx, arg) } func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { file, err := q.db.GetFileByHashAndCreator(ctx, arg) if err != nil { return database.File{}, err } err = q.authorizeContext(ctx, policy.ActionRead, file) if err != nil { // Check the user's access to the file's templates. if q.authorizeUpdateFileTemplate(ctx, file) != nil { return database.File{}, err } } return file, nil } func (q *querier) GetFileByID(ctx context.Context, id uuid.UUID) (database.File, error) { file, err := q.db.GetFileByID(ctx, id) if err != nil { return database.File{}, err } err = q.authorizeContext(ctx, policy.ActionRead, file) if err != nil { // Check the user's access to the file's templates. if q.authorizeUpdateFileTemplate(ctx, file) != nil { return database.File{}, err } } return file, nil } func (q *querier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]database.GetFileTemplatesRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetFileTemplates(ctx, fileID) } func (q *querier) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg database.GetFilteredInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetFilteredInboxNotificationsByUserID)(ctx, arg) } func (q *querier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) { return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetGitSSHKey)(ctx, userID) } func (q *querier) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) { return fetch(q.log, q.auth, q.db.GetGroupByID)(ctx, id) } func (q *querier) GetGroupByOrgAndName(ctx context.Context, arg database.GetGroupByOrgAndNameParams) (database.Group, error) { return fetch(q.log, q.auth, q.db.GetGroupByOrgAndName)(ctx, arg) } func (q *querier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]database.GroupMember, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetGroupMembers(ctx, includeSystem) } func (q *querier) GetGroupMembersByGroupID(ctx context.Context, arg database.GetGroupMembersByGroupIDParams) ([]database.GroupMember, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupMembersByGroupID)(ctx, arg) } func (q *querier) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) { if _, err := q.GetGroupByID(ctx, arg.GroupID); err != nil { // AuthZ check return 0, err } memberCount, err := q.db.GetGroupMembersCountByGroupID(ctx, arg) if err != nil { return 0, err } return memberCount, nil } func (q *querier) GetGroups(ctx context.Context, arg database.GetGroupsParams) ([]database.GetGroupsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err == nil { // Optimize this query for system users as it is used in telemetry. // Calling authz on all groups in a deployment for telemetry jobs is // excessive. Most user calls should have some filtering applied to reduce // the size of the set. return q.db.GetGroups(ctx, arg) } return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroups)(ctx, arg) } func (q *querier) GetHealthSettings(ctx context.Context) (string, error) { // No authz checks return q.db.GetHealthSettings(ctx) } func (q *querier) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (database.InboxNotification, error) { return fetchWithAction(q.log, q.auth, policy.ActionRead, q.db.GetInboxNotificationByID)(ctx, id) } func (q *querier) GetInboxNotificationsByUserID(ctx context.Context, userID database.GetInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetInboxNotificationsByUserID)(ctx, userID) } func (q *querier) GetLastChatMessageByRole(ctx context.Context, arg database.GetLastChatMessageByRoleParams) (database.ChatMessage, error) { // Authorize read on the parent chat. _, err := q.GetChatByID(ctx, arg.ChatID) if err != nil { return database.ChatMessage{}, err } return q.db.GetLastChatMessageByRole(ctx, arg) } func (q *querier) GetLastUpdateCheck(ctx context.Context) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err } return q.db.GetLastUpdateCheck(ctx) } func (q *querier) GetLatestCryptoKeyByFeature(ctx context.Context, feature database.CryptoKeyFeature) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err } return q.db.GetLatestCryptoKeyByFeature(ctx, feature) } func (q *querier) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (database.WorkspaceAppStatus, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.WorkspaceAppStatus{}, err } return q.db.GetLatestWorkspaceAppStatusByAppID(ctx, appID) } func (q *querier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx, ids) } func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { // Fast path: Check if we have a workspace RBAC object in context. if rbacObj, ok := WorkspaceRBACFromContext(ctx); ok { // Errors here will result in falling back to GetWorkspaceByAgentID, // in case the cached data is stale. if err := q.authorizeContext(ctx, policy.ActionRead, rbacObj); err == nil { return q.db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID) } q.log.Debug(ctx, "fast path authorization failed for GetLatestWorkspaceBuildByWorkspaceID, using slow path", slog.F("workspace_id", workspaceID)) } if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil { return database.WorkspaceBuild{}, err } return q.db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID) } func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { // This function is a system function until we implement a join for workspace builds. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, ids) } func (q *querier) GetLicenseByID(ctx context.Context, id int32) (database.License, error) { return fetch(q.log, q.auth, q.db.GetLicenseByID)(ctx, id) } func (q *querier) GetLicenses(ctx context.Context) ([]database.License, error) { fetch := func(ctx context.Context, _ interface{}) ([]database.License, error) { return q.db.GetLicenses(ctx) } return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetLogoURL(ctx context.Context) (string, error) { // No authz checks return q.db.GetLogoURL(ctx) } func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg database.GetNotificationMessagesByStatusParams) ([]database.NotificationMessage, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil { return nil, err } return q.db.GetNotificationMessagesByStatus(ctx, arg) } func (q *querier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.NotificationReportGeneratorLog{}, err } return q.db.GetNotificationReportGeneratorLogByTemplate(ctx, arg) } func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil { return database.NotificationTemplate{}, err } return q.db.GetNotificationTemplateByID(ctx, id) } func (q *querier) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) { // Anyone can read the 'system' and 'custom' notification templates. if kind == database.NotificationTemplateKindSystem || kind == database.NotificationTemplateKindCustom { return q.db.GetNotificationTemplatesByKind(ctx, kind) } // TODO(dannyk): handle template ownership when we support user-default notification templates. return nil, sql.ErrNoRows } func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) { // No authz checks return q.db.GetNotificationsSettings(ctx) } func (q *querier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return false, err } return q.db.GetOAuth2GithubDefaultEligible(ctx) } func (q *querier) GetOAuth2ProviderAppByClientID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.GetOAuth2ProviderAppByClientID(ctx, id) } func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.GetOAuth2ProviderAppByID(ctx, id) } func (q *querier) GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderAppCode, error) { return fetch(q.log, q.auth, q.db.GetOAuth2ProviderAppCodeByID)(ctx, id) } func (q *querier) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (database.OAuth2ProviderAppCode, error) { return fetch(q.log, q.auth, q.db.GetOAuth2ProviderAppCodeByPrefix)(ctx, secretPrefix) } func (q *querier) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderAppSecret, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppSecret); err != nil { return database.OAuth2ProviderAppSecret{}, err } return q.db.GetOAuth2ProviderAppSecretByID(ctx, id) } func (q *querier) GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (database.OAuth2ProviderAppSecret, error) { return fetch(q.log, q.auth, q.db.GetOAuth2ProviderAppSecretByPrefix)(ctx, secretPrefix) } func (q *querier) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]database.OAuth2ProviderAppSecret, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppSecret); err != nil { return []database.OAuth2ProviderAppSecret{}, err } return q.db.GetOAuth2ProviderAppSecretsByAppID(ctx, appID) } func (q *querier) GetOAuth2ProviderAppTokenByAPIKeyID(ctx context.Context, apiKeyID string) (database.OAuth2ProviderAppToken, error) { token, err := q.db.GetOAuth2ProviderAppTokenByAPIKeyID(ctx, apiKeyID) if err != nil { return database.OAuth2ProviderAppToken{}, err } if err := q.authorizeContext(ctx, policy.ActionRead, token.RBACObject()); err != nil { return database.OAuth2ProviderAppToken{}, err } return token, nil } func (q *querier) GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (database.OAuth2ProviderAppToken, error) { token, err := q.db.GetOAuth2ProviderAppTokenByPrefix(ctx, hashPrefix) if err != nil { return database.OAuth2ProviderAppToken{}, err } if err := q.authorizeContext(ctx, policy.ActionRead, token.RBACObject()); err != nil { return database.OAuth2ProviderAppToken{}, err } return token, nil } func (q *querier) GetOAuth2ProviderApps(ctx context.Context) ([]database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return []database.OAuth2ProviderApp{}, err } return q.db.GetOAuth2ProviderApps(ctx) } func (q *querier) GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]database.GetOAuth2ProviderAppsByUserIDRow, error) { // This authz check is to make sure the caller can read all their own tokens. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppCodeToken.WithOwner(userID.String())); err != nil { return []database.GetOAuth2ProviderAppsByUserIDRow{}, err } return q.db.GetOAuth2ProviderAppsByUserID(ctx, userID) } func (q *querier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (database.Organization, error) { return fetch(q.log, q.auth, q.db.GetOrganizationByID)(ctx, id) } func (q *querier) GetOrganizationByName(ctx context.Context, name database.GetOrganizationByNameParams) (database.Organization, error) { return fetch(q.log, q.auth, q.db.GetOrganizationByName)(ctx, name) } func (q *querier) GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]database.GetOrganizationIDsByMemberIDsRow, error) { // TODO: This should be rewritten to return a list of database.OrganizationMember for consistent RBAC objects. // Currently this row returns a list of org ids per user, which is challenging to check against the RBAC system. return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationIDsByMemberIDs)(ctx, ids) } func (q *querier) GetOrganizationResourceCountByID(ctx context.Context, organizationID uuid.UUID) (database.GetOrganizationResourceCountByIDRow, error) { // Can read org members if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOrganizationMember.InOrg(organizationID)); err != nil { return database.GetOrganizationResourceCountByIDRow{}, err } // Can read org workspaces if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.InOrg(organizationID)); err != nil { return database.GetOrganizationResourceCountByIDRow{}, err } // Can read org groups if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceGroup.InOrg(organizationID)); err != nil { return database.GetOrganizationResourceCountByIDRow{}, err } // Can read org templates if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.InOrg(organizationID)); err != nil { return database.GetOrganizationResourceCountByIDRow{}, err } // Can read org provisioner daemons if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerDaemon.InOrg(organizationID)); err != nil { return database.GetOrganizationResourceCountByIDRow{}, err } return q.db.GetOrganizationResourceCountByID(ctx, organizationID) } func (q *querier) GetOrganizations(ctx context.Context, args database.GetOrganizationsParams) ([]database.Organization, error) { fetch := func(ctx context.Context, _ interface{}) ([]database.Organization, error) { return q.db.GetOrganizations(ctx, args) } return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetOrganizationsByUserID(ctx context.Context, userID database.GetOrganizationsByUserIDParams) ([]database.Organization, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationsByUserID)(ctx, userID) } func (q *querier) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg database.GetOrganizationsWithPrebuildStatusParams) ([]database.GetOrganizationsWithPrebuildStatusRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOrganization.All()); err != nil { return nil, err } return q.db.GetOrganizationsWithPrebuildStatus(ctx, arg) } func (q *querier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { version, err := q.db.GetTemplateVersionByJobID(ctx, jobID) if err != nil { return nil, err } object := version.RBACObjectNoTemplate() if version.TemplateID.Valid { tpl, err := q.db.GetTemplateByID(ctx, version.TemplateID.UUID) if err != nil { return nil, err } object = version.RBACObject(tpl) } err = q.authorizeContext(ctx, policy.ActionRead, object) if err != nil { return nil, err } return q.db.GetParameterSchemasByJobID(ctx, jobID) } func (q *querier) GetPrebuildMetrics(ctx context.Context) ([]database.GetPrebuildMetricsRow, error) { // GetPrebuildMetrics returns metrics related to prebuilt workspaces, // such as the number of created and failed prebuilt workspaces. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil { return nil, err } return q.db.GetPrebuildMetrics(ctx) } func (q *querier) GetPrebuildsSettings(ctx context.Context) (string, error) { return q.db.GetPrebuildsSettings(ctx) } func (q *querier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) { empty := database.GetPresetByIDRow{} preset, err := q.db.GetPresetByID(ctx, presetID) if err != nil { return empty, err } _, err = q.GetTemplateByID(ctx, preset.TemplateID.UUID) if err != nil { return empty, err } return preset, nil } func (q *querier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceID uuid.UUID) (database.TemplateVersionPreset, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate); err != nil { return database.TemplateVersionPreset{}, err } return q.db.GetPresetByWorkspaceBuildID(ctx, workspaceID) } func (q *querier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { // An actor can read template version presets if they can read the related template version. _, err := q.GetPresetByID(ctx, presetID) if err != nil { return nil, err } return q.db.GetPresetParametersByPresetID(ctx, presetID) } func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, args uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { // An actor can read template version presets if they can read the related template version. _, err := q.GetTemplateVersionByID(ctx, args) if err != nil { return nil, err } return q.db.GetPresetParametersByTemplateVersionID(ctx, args) } func (q *querier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]database.GetPresetsAtFailureLimitRow, error) { // GetPresetsAtFailureLimit returns a list of template version presets that have reached the hard failure limit. // Request the same authorization permissions as GetPresetsBackoff, since the methods are similar. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return nil, err } return q.db.GetPresetsAtFailureLimit(ctx, hardLimit) } func (q *querier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) { // GetPresetsBackoff returns a list of template version presets along with metadata such as the number of failed prebuilds. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return nil, err } return q.db.GetPresetsBackoff(ctx, lookback) } func (q *querier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) { // An actor can read template version presets if they can read the related template version. _, err := q.GetTemplateVersionByID(ctx, templateVersionID) if err != nil { return nil, err } return q.db.GetPresetsByTemplateVersionID(ctx, templateVersionID) } func (q *querier) GetPreviousTemplateVersion(ctx context.Context, arg database.GetPreviousTemplateVersionParams) (database.TemplateVersion, error) { // An actor can read the previous template version if they can read the related template. // If no linked template exists, we check if the actor can read *a* template. if !arg.TemplateID.Valid { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.InOrg(arg.OrganizationID)); err != nil { return database.TemplateVersion{}, err } } if _, err := q.GetTemplateByID(ctx, arg.TemplateID.UUID); err != nil { return database.TemplateVersion{}, err } return q.db.GetPreviousTemplateVersion(ctx, arg) } func (q *querier) GetProvisionerDaemons(ctx context.Context) ([]database.ProvisionerDaemon, error) { fetch := func(ctx context.Context, _ interface{}) ([]database.ProvisionerDaemon, error) { return q.db.GetProvisionerDaemons(ctx) } return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } func (q *querier) GetProvisionerDaemonsByOrganization(ctx context.Context, organizationID database.GetProvisionerDaemonsByOrganizationParams) ([]database.ProvisionerDaemon, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsByOrganization)(ctx, organizationID) } func (q *querier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsWithStatusByOrganization)(ctx, arg) } func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { job, err := q.db.GetProvisionerJobByID(ctx, id) if err != nil { return database.ProvisionerJob{}, err } if err := q.authorizeProvisionerJob(ctx, job); err != nil { return database.ProvisionerJob{}, err } return job, nil } func (q *querier) GetProvisionerJobByIDForUpdate(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { job, err := q.db.GetProvisionerJobByIDForUpdate(ctx, id) if err != nil { return database.ProvisionerJob{}, err } if err := q.authorizeProvisionerJob(ctx, job); err != nil { return database.ProvisionerJob{}, err } return job, nil } func (q *querier) GetProvisionerJobByIDWithLock(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { job, err := q.db.GetProvisionerJobByIDWithLock(ctx, id) if err != nil { return database.ProvisionerJob{}, err } if err := q.authorizeProvisionerJob(ctx, job); err != nil { return database.ProvisionerJob{}, err } return job, nil } func (q *querier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ProvisionerJobTiming, error) { _, err := q.GetProvisionerJobByID(ctx, jobID) if err != nil { return nil, err } return q.db.GetProvisionerJobTimingsByJobID(ctx, jobID) } func (q *querier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids database.GetProvisionerJobsByIDsWithQueuePositionParams) ([]database.GetProvisionerJobsByIDsWithQueuePositionRow, error) { // TODO: Remove this once we have a proper rbac check for provisioner jobs. // Details in https://github.com/coder/coder/issues/16160 return q.db.GetProvisionerJobsByIDsWithQueuePosition(ctx, ids) } func (q *querier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { // TODO: Remove this once we have a proper rbac check for provisioner jobs. // Details in https://github.com/coder/coder/issues/16160 return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner)(ctx, arg) } func (q *querier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.ProvisionerJob, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerJobs); err != nil { return nil, err } return q.db.GetProvisionerJobsCreatedAfter(ctx, createdAt) } func (q *querier) GetProvisionerJobsToBeReaped(ctx context.Context, arg database.GetProvisionerJobsToBeReapedParams) ([]database.ProvisionerJob, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerJobs); err != nil { return nil, err } return q.db.GetProvisionerJobsToBeReaped(ctx, arg) } func (q *querier) GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (database.ProvisionerKey, error) { return fetch(q.log, q.auth, q.db.GetProvisionerKeyByHashedSecret)(ctx, hashedSecret) } func (q *querier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (database.ProvisionerKey, error) { return fetch(q.log, q.auth, q.db.GetProvisionerKeyByID)(ctx, id) } func (q *querier) GetProvisionerKeyByName(ctx context.Context, name database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { return fetch(q.log, q.auth, q.db.GetProvisionerKeyByName)(ctx, name) } func (q *querier) GetProvisionerLogsAfterID(ctx context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { // Authorized read on job lets the actor also read the logs. _, err := q.GetProvisionerJobByID(ctx, arg.JobID) if err != nil { return nil, err } return q.db.GetProvisionerLogsAfterID(ctx, arg) } func (q *querier) GetQuotaAllowanceForUser(ctx context.Context, params database.GetQuotaAllowanceForUserParams) (int64, error) { err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(params.UserID)) if err != nil { return -1, err } return q.db.GetQuotaAllowanceForUser(ctx, params) } func (q *querier) GetQuotaConsumedForUser(ctx context.Context, params database.GetQuotaConsumedForUserParams) (int64, error) { err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(params.OwnerID)) if err != nil { return -1, err } return q.db.GetQuotaConsumedForUser(ctx, params) } func (q *querier) GetRegularWorkspaceCreateMetrics(ctx context.Context) ([]database.GetRegularWorkspaceCreateMetricsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil { return nil, err } return q.db.GetRegularWorkspaceCreateMetrics(ctx) } func (q *querier) GetReplicaByID(ctx context.Context, id uuid.UUID) (database.Replica, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.Replica{}, err } return q.db.GetReplicaByID(ctx, id) } func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.Replica, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetReplicasUpdatedAfter(ctx, updatedAt) } func (q *querier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]database.GetRunningPrebuiltWorkspacesRow, error) { // This query returns only prebuilt workspaces, but we decided to require permissions for all workspaces. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil { return nil, err } return q.db.GetRunningPrebuiltWorkspaces(ctx) } func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err } return q.db.GetRuntimeConfig(ctx, key) } func (q *querier) GetStaleChats(ctx context.Context, staleThreshold time.Time) ([]database.Chat, error) { // GetStaleChats is a system-level operation used by the chat processor for recovery. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil { return nil, err } return q.db.GetStaleChats(ctx, staleThreshold) } func (q *querier) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]database.TailnetPeer, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err } return q.db.GetTailnetPeers(ctx, id) } func (q *querier) GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]database.GetTailnetTunnelPeerBindingsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err } return q.db.GetTailnetTunnelPeerBindings(ctx, srcID) } func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]database.GetTailnetTunnelPeerIDsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err } return q.db.GetTailnetTunnelPeerIDs(ctx, srcID) } func (q *querier) GetTaskByID(ctx context.Context, id uuid.UUID) (database.Task, error) { return fetch(q.log, q.auth, q.db.GetTaskByID)(ctx, id) } func (q *querier) GetTaskByOwnerIDAndName(ctx context.Context, arg database.GetTaskByOwnerIDAndNameParams) (database.Task, error) { return fetch(q.log, q.auth, q.db.GetTaskByOwnerIDAndName)(ctx, arg) } func (q *querier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.Task, error) { return fetch(q.log, q.auth, q.db.GetTaskByWorkspaceID)(ctx, workspaceID) } func (q *querier) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (database.TaskSnapshot, error) { // Fetch task to build RBAC object for authorization. task, err := q.GetTaskByID(ctx, taskID) if err != nil { return database.TaskSnapshot{}, err } if err := q.authorizeContext(ctx, policy.ActionRead, task.RBACObject()); err != nil { return database.TaskSnapshot{}, err } return q.db.GetTaskSnapshot(ctx, taskID) } func (q *querier) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.TelemetryItem{}, err } return q.db.GetTelemetryItem(ctx, key) } func (q *querier) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetTelemetryItems(ctx) } func (q *querier) GetTelemetryTaskEvents(ctx context.Context, arg database.GetTelemetryTaskEventsParams) ([]database.GetTelemetryTaskEventsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTask.All()); err != nil { return nil, err } return q.db.GetTelemetryTaskEvents(ctx, arg) } func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err } return q.db.GetTemplateAppInsights(ctx, arg) } func (q *querier) GetTemplateAppInsightsByTemplate(ctx context.Context, arg database.GetTemplateAppInsightsByTemplateParams) ([]database.GetTemplateAppInsightsByTemplateRow, error) { // Only used by prometheus metrics, so we don't strictly need to check update template perms. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { return nil, err } return q.db.GetTemplateAppInsightsByTemplate(ctx, arg) } // Only used by metrics cache. func (q *querier) GetTemplateAverageBuildTime(ctx context.Context, arg uuid.NullUUID) (database.GetTemplateAverageBuildTimeRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.GetTemplateAverageBuildTimeRow{}, err } return q.db.GetTemplateAverageBuildTime(ctx, arg) } func (q *querier) GetTemplateByID(ctx context.Context, id uuid.UUID) (database.Template, error) { return fetch(q.log, q.auth, q.db.GetTemplateByID)(ctx, id) } func (q *querier) GetTemplateByOrganizationAndName(ctx context.Context, arg database.GetTemplateByOrganizationAndNameParams) (database.Template, error) { return fetch(q.log, q.auth, q.db.GetTemplateByOrganizationAndName)(ctx, arg) } func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return database.GetTemplateInsightsRow{}, err } return q.db.GetTemplateInsights(ctx, arg) } func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err } return q.db.GetTemplateInsightsByInterval(ctx, arg) } func (q *querier) GetTemplateInsightsByTemplate(ctx context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) { // Only used by prometheus metrics collector. No need to check update template perms. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { return nil, err } return q.db.GetTemplateInsightsByTemplate(ctx, arg) } func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err } return q.db.GetTemplateParameterInsights(ctx, arg) } func (q *querier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]database.GetTemplatePresetsWithPrebuildsRow, error) { // GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds. // Presets and prebuilds are part of the template, so if you can access templates - you can access them as well. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil { return nil, err } return q.db.GetTemplatePresetsWithPrebuilds(ctx, templateID) } func (q *querier) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err } return q.db.GetTemplateUsageStats(ctx, arg) } func (q *querier) GetTemplateVersionByID(ctx context.Context, tvid uuid.UUID) (database.TemplateVersion, error) { tv, err := q.db.GetTemplateVersionByID(ctx, tvid) if err != nil { return database.TemplateVersion{}, err } if !tv.TemplateID.Valid { // If no linked template exists, check if the actor can read a template in the organization. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.InOrg(tv.OrganizationID)); err != nil { return database.TemplateVersion{}, err } } else if _, err := q.GetTemplateByID(ctx, tv.TemplateID.UUID); err != nil { // An actor can read the template version if they can read the related template. return database.TemplateVersion{}, err } return tv, nil } func (q *querier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (database.TemplateVersion, error) { tv, err := q.db.GetTemplateVersionByJobID(ctx, jobID) if err != nil { return database.TemplateVersion{}, err } if !tv.TemplateID.Valid { // If no linked template exists, check if the actor can read a template in the organization. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.InOrg(tv.OrganizationID)); err != nil { return database.TemplateVersion{}, err } } else if _, err := q.GetTemplateByID(ctx, tv.TemplateID.UUID); err != nil { // An actor can read the template version if they can read the related template. return database.TemplateVersion{}, err } return tv, nil } func (q *querier) GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg database.GetTemplateVersionByTemplateIDAndNameParams) (database.TemplateVersion, error) { tv, err := q.db.GetTemplateVersionByTemplateIDAndName(ctx, arg) if err != nil { return database.TemplateVersion{}, err } if !tv.TemplateID.Valid { // If no linked template exists, check if the actor can read a template in the organization. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.InOrg(tv.OrganizationID)); err != nil { return database.TemplateVersion{}, err } } else if _, err := q.GetTemplateByID(ctx, tv.TemplateID.UUID); err != nil { // An actor can read the template version if they can read the related template. return database.TemplateVersion{}, err } return tv, nil } func (q *querier) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionParameter, error) { // An actor can read template version parameters if they can read the related template. tv, err := q.db.GetTemplateVersionByID(ctx, templateVersionID) if err != nil { return nil, err } var object rbac.Objecter template, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return nil, err } object = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { object = tv.RBACObject(template) } if err := q.authorizeContext(ctx, policy.ActionRead, object); err != nil { return nil, err } return q.db.GetTemplateVersionParameters(ctx, templateVersionID) } func (q *querier) GetTemplateVersionTerraformValues(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersionTerraformValue, error) { // The template_version_terraform_values table should follow the same access // control as the template_version table. Rather than reimplement the checks, // we just defer to existing implementation. (plus we'd need to use this query // to reimplement the proper checks anyway) _, err := q.GetTemplateVersionByID(ctx, templateVersionID) if err != nil { return database.TemplateVersionTerraformValue{}, err } return q.db.GetTemplateVersionTerraformValues(ctx, templateVersionID) } func (q *querier) GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionVariable, error) { tv, err := q.db.GetTemplateVersionByID(ctx, templateVersionID) if err != nil { return nil, err } var object rbac.Objecter template, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return nil, err } object = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { object = tv.RBACObject(template) } if err := q.authorizeContext(ctx, policy.ActionRead, object); err != nil { return nil, err } return q.db.GetTemplateVersionVariables(ctx, templateVersionID) } func (q *querier) GetTemplateVersionWorkspaceTags(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionWorkspaceTag, error) { tv, err := q.db.GetTemplateVersionByID(ctx, templateVersionID) if err != nil { return nil, err } var object rbac.Objecter template, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return nil, err } object = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { object = tv.RBACObject(template) } if err := q.authorizeContext(ctx, policy.ActionRead, object); err != nil { return nil, err } return q.db.GetTemplateVersionWorkspaceTags(ctx, templateVersionID) } // GetTemplateVersionsByIDs is only used for workspace build data. // The workspace is already fetched. func (q *querier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.TemplateVersion, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetTemplateVersionsByIDs(ctx, ids) } func (q *querier) GetTemplateVersionsByTemplateID(ctx context.Context, arg database.GetTemplateVersionsByTemplateIDParams) ([]database.TemplateVersion, error) { // An actor can read template versions if they can read the related template. template, err := q.db.GetTemplateByID(ctx, arg.TemplateID) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionRead, template); err != nil { return nil, err } return q.db.GetTemplateVersionsByTemplateID(ctx, arg) } func (q *querier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.TemplateVersion, error) { // An actor can read execute this query if they can read all templates. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil { return nil, err } return q.db.GetTemplateVersionsCreatedAfter(ctx, createdAt) } func (q *querier) GetTemplates(ctx context.Context) ([]database.Template, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetTemplates(ctx) } func (q *querier) GetTemplatesWithFilter(ctx context.Context, arg database.GetTemplatesWithFilterParams) ([]database.Template, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceTemplate.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.GetAuthorizedTemplates(ctx, arg, prep) } func (q *querier) GetTotalUsageDCManagedAgentsV1(ctx context.Context, arg database.GetTotalUsageDCManagedAgentsV1Params) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUsageEvent); err != nil { return 0, err } return q.db.GetTotalUsageDCManagedAgentsV1(ctx, arg) } func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceLicense); err != nil { return nil, err } return q.db.GetUnexpiredLicenses(ctx) } func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { // Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { for _, templateID := range arg.TemplateIDs { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { return nil, err } } if len(arg.TemplateIDs) == 0 { if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return nil, err } } } return q.db.GetUserActivityInsights(ctx, arg) } func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { return fetch(q.log, q.auth, q.db.GetUserByEmailOrUsername)(ctx, arg) } func (q *querier) GetUserByID(ctx context.Context, id uuid.UUID) (database.User, error) { return fetch(q.log, q.auth, q.db.GetUserByID)(ctx, id) } func (q *querier) GetUserChatCustomPrompt(ctx context.Context, userID uuid.UUID) (string, error) { u, err := q.db.GetUserByID(ctx, userID) if err != nil { return "", err } if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { return "", err } return q.db.GetUserChatCustomPrompt(ctx, userID) } func (q *querier) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return 0, err } return q.db.GetUserCount(ctx, includeSystem) } func (q *querier) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) { // Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms. if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil { for _, templateID := range arg.TemplateIDs { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionViewInsights, template); err != nil { return nil, err } } if len(arg.TemplateIDs) == 0 { if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil { return nil, err } } } return q.db.GetUserLatencyInsights(ctx, arg) } func (q *querier) GetUserLinkByLinkedID(ctx context.Context, linkedID string) (database.UserLink, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.UserLink{}, err } return q.db.GetUserLinkByLinkedID(ctx, linkedID) } func (q *querier) GetUserLinkByUserIDLoginType(ctx context.Context, arg database.GetUserLinkByUserIDLoginTypeParams) (database.UserLink, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.UserLink{}, err } return q.db.GetUserLinkByUserIDLoginType(ctx, arg) } func (q *querier) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]database.UserLink, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetUserLinksByUserID(ctx, userID) } func (q *querier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationPreference.WithOwner(userID.String())); err != nil { return nil, err } return q.db.GetUserNotificationPreferences(ctx, userID) } func (q *querier) GetUserSecret(ctx context.Context, id uuid.UUID) (database.UserSecret, error) { // First get the secret to check ownership secret, err := q.db.GetUserSecret(ctx, id) if err != nil { return database.UserSecret{}, err } if err := q.authorizeContext(ctx, policy.ActionRead, secret); err != nil { return database.UserSecret{}, err } return secret, nil } func (q *querier) GetUserSecretByUserIDAndName(ctx context.Context, arg database.GetUserSecretByUserIDAndNameParams) (database.UserSecret, error) { obj := rbac.ResourceUserSecret.WithOwner(arg.UserID.String()) if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil { return database.UserSecret{}, err } return q.db.GetUserSecretByUserIDAndName(ctx, arg) } func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil { return nil, err } return q.db.GetUserStatusCounts(ctx, arg) } func (q *querier) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) { user, err := q.db.GetUserByID(ctx, userID) if err != nil { return false, err } if err := q.authorizeContext(ctx, policy.ActionReadPersonal, user); err != nil { return false, err } return q.db.GetUserTaskNotificationAlertDismissed(ctx, userID) } func (q *querier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { u, err := q.db.GetUserByID(ctx, userID) if err != nil { return "", err } if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { return "", err } return q.db.GetUserTerminalFont(ctx, userID) } func (q *querier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { u, err := q.db.GetUserByID(ctx, userID) if err != nil { return "", err } if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { return "", err } return q.db.GetUserThemePreference(ctx, userID) } func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { u, err := q.db.GetUserByID(ctx, params.OwnerID) if err != nil { return nil, err } // This permission is a bit strange. Reading workspace build params should be a permission // on the workspace. However, this use case is to autofill a user's last input // to some parameter. So this is kind of a "user setting". For now, this will // be lumped in with user personal data. Subject to change. if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { return nil, err } return q.db.GetUserWorkspaceBuildParameters(ctx, params) } func (q *querier) GetUsers(ctx context.Context, arg database.GetUsersParams) ([]database.GetUsersRow, error) { // This does the filtering in SQL. prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceUser.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.GetAuthorizedUsers(ctx, arg, prep) } // GetUsersByIDs is only used for usernames on workspace return data. // This function should be replaced by joining this data to the workspace query // itself. func (q *querier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]database.User, error) { for _, uid := range ids { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(uid)); err != nil { return nil, err } } return q.db.GetUsersByIDs(ctx, ids) } func (q *querier) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]database.WebpushSubscription, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWebpushSubscription.WithOwner(userID.String())); err != nil { return nil, err } return q.db.GetWebpushSubscriptionsByUserID(ctx, userID) } func (q *querier) GetWebpushVAPIDKeys(ctx context.Context) (database.GetWebpushVAPIDKeysRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return database.GetWebpushVAPIDKeysRow{}, err } return q.db.GetWebpushVAPIDKeys(ctx) } func (q *querier) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceACLByIDRow, error) { workspace, err := q.db.GetWorkspaceByID(ctx, id) if err != nil { return database.GetWorkspaceACLByIDRow{}, err } if err := q.authorizeContext(ctx, policy.ActionRead, workspace); err != nil { return database.GetWorkspaceACLByIDRow{}, err } return q.db.GetWorkspaceACLByID(ctx, id) } func (q *querier) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentAndWorkspaceByIDRow, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceAgentAndWorkspaceByID)(ctx, id) } func (q *querier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { // Fast path: Check if we have a workspace RBAC object in context. // In the agent API this is set at agent connection time to avoid the expensive // GetWorkspaceByAgentID query for every agent operation. // NOTE: The cached RBAC object is refreshed every 5 minutes in agentapi/api.go. if rbacObj, ok := WorkspaceRBACFromContext(ctx); ok { // Errors here will result in falling back to GetWorkspaceByAgentID, // in case the cached data is stale. if err := q.authorizeContext(ctx, policy.ActionRead, rbacObj); err == nil { return q.db.GetWorkspaceAgentByID(ctx, id) } q.log.Debug(ctx, "fast path authorization failed for GetWorkspaceAgentByID, using slow path", slog.F("agent_id", id)) } // Slow path: Fallback to fetching the workspace for authorization if _, err := q.GetWorkspaceByAgentID(ctx, id); err != nil { return database.WorkspaceAgent{}, err } return q.db.GetWorkspaceAgentByID(ctx, id) } // GetWorkspaceAgentByInstanceID might want to be a system call? Unsure exactly, // but this will fail. Need to figure out what AuthInstanceID is, and if it // is essentially an auth token. But the caller using this function is not // an authenticated user. So this authz check will fail. func (q *querier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (database.WorkspaceAgent, error) { agent, err := q.db.GetWorkspaceAgentByInstanceID(ctx, authInstanceID) if err != nil { return database.WorkspaceAgent{}, err } _, err = q.GetWorkspaceByAgentID(ctx, agent.ID) if err != nil { return database.WorkspaceAgent{}, err } return agent, nil } func (q *querier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentDevcontainer, error) { _, err := q.GetWorkspaceAgentByID(ctx, workspaceAgentID) if err != nil { return nil, err } return q.db.GetWorkspaceAgentDevcontainersByAgentID(ctx, workspaceAgentID) } func (q *querier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentLifecycleStateByIDRow, error) { _, err := q.GetWorkspaceAgentByID(ctx, id) if err != nil { return database.GetWorkspaceAgentLifecycleStateByIDRow{}, err } return q.db.GetWorkspaceAgentLifecycleStateByID(ctx, id) } func (q *querier) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentLogSource, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAgentLogSourcesByAgentIDs(ctx, ids) } func (q *querier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) { _, err := q.GetWorkspaceAgentByID(ctx, arg.AgentID) if err != nil { return nil, err } return q.db.GetWorkspaceAgentLogsAfter(ctx, arg) } func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, arg database.GetWorkspaceAgentMetadataParams) ([]database.WorkspaceAgentMetadatum, error) { workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.WorkspaceAgentID) if err != nil { return nil, err } err = q.authorizeContext(ctx, policy.ActionRead, workspace) if err != nil { return nil, err } return q.db.GetWorkspaceAgentMetadata(ctx, arg) } func (q *querier) GetWorkspaceAgentPortShare(ctx context.Context, arg database.GetWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { return database.WorkspaceAgentPortShare{}, err } // reading a workspace port share is more akin to just reading the workspace. if err = q.authorizeContext(ctx, policy.ActionRead, w.RBACObject()); err != nil { return database.WorkspaceAgentPortShare{}, xerrors.Errorf("authorize context: %w", err) } return q.db.GetWorkspaceAgentPortShare(ctx, arg) } func (q *querier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAgentScriptTimingsByBuildID(ctx, id) } func (q *querier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids) } func (q *querier) GetWorkspaceAgentStats(ctx context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) { return q.db.GetWorkspaceAgentStats(ctx, createdAfter) } func (q *querier) GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsAndLabelsRow, error) { return q.db.GetWorkspaceAgentStatsAndLabels(ctx, createdAfter) } func (q *querier) GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]database.GetWorkspaceAgentUsageStatsRow, error) { return q.db.GetWorkspaceAgentUsageStats(ctx, createdAt) } func (q *querier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]database.GetWorkspaceAgentUsageStatsAndLabelsRow, error) { return q.db.GetWorkspaceAgentUsageStatsAndLabels(ctx, createdAt) } func (q *querier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) { workspace, err := q.db.GetWorkspaceByAgentID(ctx, parentID) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionRead, workspace); err != nil { return nil, err } return q.db.GetWorkspaceAgentsByParentID(ctx, parentID) } // GetWorkspaceAgentsByResourceIDs // The workspace/job is already fetched. func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAgentsByResourceIDs(ctx, ids) } func (q *querier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { return nil, err } return q.db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) } func (q *querier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceAgent, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAgentsCreatedAfter(ctx, createdAt) } func (q *querier) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]database.GetWorkspaceAgentsForMetricsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace); err != nil { return nil, err } return q.db.GetWorkspaceAgentsForMetrics(ctx) } func (q *querier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgent, error) { workspace, err := q.GetWorkspaceByID(ctx, workspaceID) if err != nil { return nil, err } return q.db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) } func (q *querier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) { // If we can fetch the workspace, we can fetch the apps. Use the authorized call. if _, err := q.GetWorkspaceByAgentID(ctx, arg.AgentID); err != nil { return database.WorkspaceApp{}, err } return q.db.GetWorkspaceAppByAgentIDAndSlug(ctx, arg) } func (q *querier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAppStatusesByAppIDs(ctx, ids) } func (q *querier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceApp, error) { if _, err := q.GetWorkspaceByAgentID(ctx, agentID); err != nil { return nil, err } return q.db.GetWorkspaceAppsByAgentID(ctx, agentID) } // GetWorkspaceAppsByAgentIDs // The workspace/job is already fetched. func (q *querier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAppsByAgentIDs(ctx, ids) } func (q *querier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceAppsCreatedAfter(ctx, createdAt) } func (q *querier) GetWorkspaceBuildByID(ctx context.Context, buildID uuid.UUID) (database.WorkspaceBuild, error) { build, err := q.db.GetWorkspaceBuildByID(ctx, buildID) if err != nil { return database.WorkspaceBuild{}, err } if _, err := q.GetWorkspaceByID(ctx, build.WorkspaceID); err != nil { return database.WorkspaceBuild{}, err } return build, nil } func (q *querier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { build, err := q.db.GetWorkspaceBuildByJobID(ctx, jobID) if err != nil { return database.WorkspaceBuild{}, err } // Authorized fetch _, err = q.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { return database.WorkspaceBuild{}, err } return build, nil } func (q *querier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil { return database.WorkspaceBuild{}, err } return q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, arg) } func (q *querier) GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceBuildMetricsByResourceIDRow, error) { // Verify access to the resource first. if _, err := q.GetWorkspaceResourceByID(ctx, id); err != nil { return database.GetWorkspaceBuildMetricsByResourceIDRow{}, err } return q.db.GetWorkspaceBuildMetricsByResourceID(ctx, id) } func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { // Authorized call to get the workspace build. If we can read the build, // we can read the params. _, err := q.GetWorkspaceBuildByID(ctx, workspaceBuildID) if err != nil { return nil, err } return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID) } func (q *querier) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, buildID uuid.UUID) (database.GetWorkspaceBuildProvisionerStateByIDRow, error) { // Fetching the provisioner state requires Update permission on the template. return fetchWithAction(q.log, q.auth, policy.ActionUpdate, q.db.GetWorkspaceBuildProvisionerStateByID)(ctx, buildID) } func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceBuildStatsByTemplates(ctx, since) } func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil { return nil, err } return q.db.GetWorkspaceBuildsByWorkspaceID(ctx, arg) } // Telemetry related functions. These functions are system functions for returning // telemetry data. Never called by a user. func (q *querier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceBuild, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceBuildsCreatedAfter(ctx, createdAt) } func (q *querier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (database.Workspace, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceByAgentID)(ctx, agentID) } func (q *querier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (database.Workspace, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceByID)(ctx, id) } func (q *querier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg database.GetWorkspaceByOwnerIDAndNameParams) (database.Workspace, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceByOwnerIDAndName)(ctx, arg) } func (q *querier) GetWorkspaceByResourceID(ctx context.Context, resourceID uuid.UUID) (database.Workspace, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceByResourceID)(ctx, resourceID) } func (q *querier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (database.Workspace, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceByWorkspaceAppID)(ctx, workspaceAppID) } func (q *querier) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceModule, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceModulesByJobID(ctx, jobID) } func (q *querier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceModule, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceModulesCreatedAfter(ctx, createdAt) } func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxies(ctx) })(ctx, nil) } func (q *querier) GetWorkspaceProxyByHostname(ctx context.Context, params database.GetWorkspaceProxyByHostnameParams) (database.WorkspaceProxy, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.WorkspaceProxy{}, err } return q.db.GetWorkspaceProxyByHostname(ctx, params) } func (q *querier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (database.WorkspaceProxy, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByID)(ctx, id) } func (q *querier) GetWorkspaceProxyByName(ctx context.Context, name string) (database.WorkspaceProxy, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByName)(ctx, name) } func (q *querier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (database.WorkspaceResource, error) { // TODO: Optimize this resource, err := q.db.GetWorkspaceResourceByID(ctx, id) if err != nil { return database.WorkspaceResource{}, err } _, err = q.GetProvisionerJobByID(ctx, resource.JobID) if err != nil { return database.WorkspaceResource{}, err } return resource, nil } // GetWorkspaceResourceMetadataByResourceIDs is only used for build data. // The workspace/job is already fetched. func (q *querier) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceResourceMetadataByResourceIDs(ctx, ids) } func (q *querier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceResourceMetadatum, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceResourceMetadataCreatedAfter(ctx, createdAt) } func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { job, err := q.db.GetProvisionerJobByID(ctx, jobID) if err != nil { return nil, err } var obj rbac.Objecter switch job.Type { case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport: // We don't need to do an authorized check, but this helper function // handles the job type for us. // TODO: Do not duplicate auth checks. tv, err := authorizedTemplateVersionFromJob(ctx, q, job) if err != nil { return nil, err } if !tv.TemplateID.Valid { // Orphaned template version obj = tv.RBACObjectNoTemplate() } else { template, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { return nil, err } obj = template.RBACObject() } case database.ProvisionerJobTypeWorkspaceBuild: build, err := q.db.GetWorkspaceBuildByJobID(ctx, jobID) if err != nil { return nil, err } workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { return nil, err } obj = workspace default: return nil, xerrors.Errorf("unknown job type: %s", job.Type) } if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil { return nil, err } return q.db.GetWorkspaceResourcesByJobID(ctx, jobID) } // GetWorkspaceResourcesByJobIDs is only used for workspace build data. // The workspace is already fetched. // TODO: Find a way to replace this with proper authz. func (q *querier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceResource, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceResourcesByJobIDs(ctx, ids) } func (q *querier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceResource, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceResourcesCreatedAfter(ctx, createdAt) } func (q *querier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIDs []uuid.UUID) ([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, templateIDs) } func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceWorkspace.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.GetAuthorizedWorkspaces(ctx, arg, prep) } func (q *querier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceWorkspace.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, ownerID, prep) } func (q *querier) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceTable, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.GetWorkspacesByTemplateID(ctx, templateID) } func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.GetWorkspacesEligibleForTransitionRow, error) { return q.db.GetWorkspacesEligibleForTransition(ctx, now) } func (q *querier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]database.GetWorkspacesForWorkspaceMetricsRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace); err != nil { return nil, err } return q.db.GetWorkspacesForWorkspaceMetrics(ctx) } func (q *querier) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) { return insert(q.log, q.auth, rbac.ResourceAibridgeInterception.WithOwner(arg.InitiatorID.String()), q.db.InsertAIBridgeInterception)(ctx, arg) } func (q *querier) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) (database.AIBridgeTokenUsage, error) { // All aibridge_token_usages records belong to the initiator of their associated interception. if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, arg.InterceptionID); err != nil { return database.AIBridgeTokenUsage{}, err } return q.db.InsertAIBridgeTokenUsage(ctx, arg) } func (q *querier) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) (database.AIBridgeToolUsage, error) { // All aibridge_tool_usages records belong to the initiator of their associated interception. if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, arg.InterceptionID); err != nil { return database.AIBridgeToolUsage{}, err } return q.db.InsertAIBridgeToolUsage(ctx, arg) } func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) (database.AIBridgeUserPrompt, error) { // All aibridge_user_prompts records belong to the initiator of their associated interception. if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, arg.InterceptionID); err != nil { return database.AIBridgeUserPrompt{}, err } return q.db.InsertAIBridgeUserPrompt(ctx, arg) } func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { // TODO(Cian): ideally this would be encoded in the policy, but system users are just members and we // don't currently have a capability to conditionally deny creating resources by owner ID in a role. // We also need to enrich rbac.Actor with IsSystem so that we can distinguish all system users. // For now, there is only one system user (prebuilds). if act, ok := ActorFromContext(ctx); ok && act.ID == database.PrebuildsSystemUserID.String() { return database.APIKey{}, logNotAuthorizedError(ctx, q.log, NotAuthorizedError{Err: xerrors.Errorf("prebuild user may not create api keys")}) } return insert(q.log, q.auth, rbac.ResourceApiKey.WithOwner(arg.UserID.String()), q.db.InsertAPIKey)(ctx, arg) } func (q *querier) InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (database.Group, error) { // This method creates a new group. return insert(q.log, q.auth, rbac.ResourceGroup.InOrg(organizationID), q.db.InsertAllUsersGroup)(ctx, organizationID) } func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLogParams) (database.AuditLog, error) { return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg) } func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) { return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg) } func (q *querier) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) { // Authorize create on chat resource scoped to the owner and org. return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChatFile)(ctx, arg) } func (q *querier) InsertChatMessage(ctx context.Context, arg database.InsertChatMessageParams) (database.ChatMessage, error) { // Authorize create on the parent chat (using update permission). chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { return database.ChatMessage{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.ChatMessage{}, err } return q.db.InsertChatMessage(ctx, arg) } func (q *querier) InsertChatModelConfig(ctx context.Context, arg database.InsertChatModelConfigParams) (database.ChatModelConfig, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return database.ChatModelConfig{}, err } return q.db.InsertChatModelConfig(ctx, arg) } func (q *querier) InsertChatProvider(ctx context.Context, arg database.InsertChatProviderParams) (database.ChatProvider, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return database.ChatProvider{}, err } return q.db.InsertChatProvider(ctx, arg) } func (q *querier) InsertChatQueuedMessage(ctx context.Context, arg database.InsertChatQueuedMessageParams) (database.ChatQueuedMessage, error) { chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { return database.ChatQueuedMessage{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.ChatQueuedMessage{}, err } return q.db.InsertChatQueuedMessage(ctx, arg) } func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err } return q.db.InsertCryptoKey(ctx, arg) } func (q *querier) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) { // Org and site role upsert share the same query. So switch the assertion based on the org uuid. if !arg.OrganizationID.Valid || arg.OrganizationID.UUID == uuid.Nil { return database.CustomRole{}, NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")} } rbacObj := rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID) if err := q.authorizeContext(ctx, policy.ActionCreate, rbacObj); err != nil { return database.CustomRole{}, err } if arg.IsSystem { err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem) if err != nil { return database.CustomRole{}, err } } if err := q.customRoleCheck(ctx, database.CustomRole{ Name: arg.Name, DisplayName: arg.DisplayName, SitePermissions: arg.SitePermissions, OrgPermissions: arg.OrgPermissions, UserPermissions: arg.UserPermissions, MemberPermissions: arg.MemberPermissions, CreatedAt: time.Now(), UpdatedAt: time.Now(), OrganizationID: arg.OrganizationID, ID: uuid.New(), IsSystem: arg.IsSystem, }, policy.ActionCreate); err != nil { return database.CustomRole{}, err } return q.db.InsertCustomRole(ctx, arg) } func (q *querier) InsertDBCryptKey(ctx context.Context, arg database.InsertDBCryptKeyParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertDBCryptKey(ctx, arg) } func (q *querier) InsertDERPMeshKey(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertDERPMeshKey(ctx, value) } func (q *querier) InsertDeploymentID(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertDeploymentID(ctx, value) } func (q *querier) InsertExternalAuthLink(ctx context.Context, arg database.InsertExternalAuthLinkParams) (database.ExternalAuthLink, error) { return insertWithAction(q.log, q.auth, rbac.ResourceUser.WithID(arg.UserID).WithOwner(arg.UserID.String()), policy.ActionUpdatePersonal, q.db.InsertExternalAuthLink)(ctx, arg) } func (q *querier) InsertFile(ctx context.Context, arg database.InsertFileParams) (database.File, error) { return insert(q.log, q.auth, rbac.ResourceFile.WithOwner(arg.CreatedBy.String()), q.db.InsertFile)(ctx, arg) } func (q *querier) InsertGitSSHKey(ctx context.Context, arg database.InsertGitSSHKeyParams) (database.GitSSHKey, error) { return insertWithAction(q.log, q.auth, rbac.ResourceUser.WithOwner(arg.UserID.String()).WithID(arg.UserID), policy.ActionUpdatePersonal, q.db.InsertGitSSHKey)(ctx, arg) } func (q *querier) InsertGroup(ctx context.Context, arg database.InsertGroupParams) (database.Group, error) { return insert(q.log, q.auth, rbac.ResourceGroup.InOrg(arg.OrganizationID), q.db.InsertGroup)(ctx, arg) } func (q *querier) InsertGroupMember(ctx context.Context, arg database.InsertGroupMemberParams) error { fetch := func(ctx context.Context, arg database.InsertGroupMemberParams) (database.Group, error) { return q.db.GetGroupByID(ctx, arg.GroupID) } return update(q.log, q.auth, fetch, q.db.InsertGroupMember)(ctx, arg) } func (q *querier) InsertInboxNotification(ctx context.Context, arg database.InsertInboxNotificationParams) (database.InboxNotification, error) { return insert(q.log, q.auth, rbac.ResourceInboxNotification.WithOwner(arg.UserID.String()), q.db.InsertInboxNotification)(ctx, arg) } func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseParams) (database.License, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceLicense); err != nil { return database.License{}, err } return q.db.InsertLicense(ctx, arg) } func (q *querier) InsertMemoryResourceMonitor(ctx context.Context, arg database.InsertMemoryResourceMonitorParams) (database.WorkspaceAgentMemoryResourceMonitor, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return database.WorkspaceAgentMemoryResourceMonitor{}, err } return q.db.InsertMemoryResourceMonitor(ctx, arg) } func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return nil, err } return q.db.InsertMissingGroups(ctx, arg) } func (q *querier) InsertOAuth2ProviderApp(ctx context.Context, arg database.InsertOAuth2ProviderAppParams) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.InsertOAuth2ProviderApp(ctx, arg) } func (q *querier) InsertOAuth2ProviderAppCode(ctx context.Context, arg database.InsertOAuth2ProviderAppCodeParams) (database.OAuth2ProviderAppCode, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return database.OAuth2ProviderAppCode{}, err } return q.db.InsertOAuth2ProviderAppCode(ctx, arg) } func (q *querier) InsertOAuth2ProviderAppSecret(ctx context.Context, arg database.InsertOAuth2ProviderAppSecretParams) (database.OAuth2ProviderAppSecret, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppSecret); err != nil { return database.OAuth2ProviderAppSecret{}, err } return q.db.InsertOAuth2ProviderAppSecret(ctx, arg) } func (q *querier) InsertOAuth2ProviderAppToken(ctx context.Context, arg database.InsertOAuth2ProviderAppTokenParams) (database.OAuth2ProviderAppToken, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil { return database.OAuth2ProviderAppToken{}, err } return q.db.InsertOAuth2ProviderAppToken(ctx, arg) } func (q *querier) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) { return insert(q.log, q.auth, rbac.ResourceOrganization, q.db.InsertOrganization)(ctx, arg) } func (q *querier) InsertOrganizationMember(ctx context.Context, arg database.InsertOrganizationMemberParams) (database.OrganizationMember, error) { orgRoles, err := q.convertToOrganizationRoles(arg.OrganizationID, arg.Roles) if err != nil { return database.OrganizationMember{}, xerrors.Errorf("converting to organization roles: %w", err) } // All roles are added roles. Org member is always implied. //nolint:gocritic addedRoles := append(orgRoles, rbac.ScopedRoleOrgMember(arg.OrganizationID)) err = q.canAssignRoles(ctx, arg.OrganizationID, addedRoles, []rbac.RoleIdentifier{}) if err != nil { return database.OrganizationMember{}, err } obj := rbac.ResourceOrganizationMember.InOrg(arg.OrganizationID).WithID(arg.UserID) return insert(q.log, q.auth, obj, q.db.InsertOrganizationMember)(ctx, arg) } func (q *querier) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) { err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate) if err != nil { return database.TemplateVersionPreset{}, err } return q.db.InsertPreset(ctx, arg) } func (q *querier) InsertPresetParameters(ctx context.Context, arg database.InsertPresetParametersParams) ([]database.TemplateVersionPresetParameter, error) { err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate) if err != nil { return nil, err } return q.db.InsertPresetParameters(ctx, arg) } func (q *querier) InsertPresetPrebuildSchedule(ctx context.Context, arg database.InsertPresetPrebuildScheduleParams) (database.TemplateVersionPresetPrebuildSchedule, error) { err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate) if err != nil { return database.TemplateVersionPresetPrebuildSchedule{}, err } return q.db.InsertPresetPrebuildSchedule(ctx, arg) } func (q *querier) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { // TODO: Remove this once we have a proper rbac check for provisioner jobs. // Details in https://github.com/coder/coder/issues/16160 return q.db.InsertProvisionerJob(ctx, arg) } func (q *querier) InsertProvisionerJobLogs(ctx context.Context, arg database.InsertProvisionerJobLogsParams) ([]database.ProvisionerJobLog, error) { // TODO: Remove this once we have a proper rbac check for provisioner jobs. // Details in https://github.com/coder/coder/issues/16160 return q.db.InsertProvisionerJobLogs(ctx, arg) } func (q *querier) InsertProvisionerJobTimings(ctx context.Context, arg database.InsertProvisionerJobTimingsParams) ([]database.ProvisionerJobTiming, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return nil, err } return q.db.InsertProvisionerJobTimings(ctx, arg) } func (q *querier) InsertProvisionerKey(ctx context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { return insert(q.log, q.auth, rbac.ResourceProvisionerDaemon.InOrg(arg.OrganizationID).WithID(arg.ID), q.db.InsertProvisionerKey)(ctx, arg) } func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.Replica{}, err } return q.db.InsertReplica(ctx, arg) } func (q *querier) InsertTask(ctx context.Context, arg database.InsertTaskParams) (database.TaskTable, error) { // Ensure the actor can access the specified template version (and thus its template). if _, err := q.GetTemplateVersionByID(ctx, arg.TemplateVersionID); err != nil { return database.TaskTable{}, err } obj := rbac.ResourceTask.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID) return insert(q.log, q.auth, obj, q.db.InsertTask)(ctx, arg) } func (q *querier) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertTelemetryItemIfNotExists(ctx, arg) } func (q *querier) InsertTelemetryLock(ctx context.Context, arg database.InsertTelemetryLockParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertTelemetryLock(ctx, arg) } func (q *querier) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { obj := rbac.ResourceTemplate.InOrg(arg.OrganizationID) if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil { return err } return q.db.InsertTemplate(ctx, arg) } func (q *querier) InsertTemplateVersion(ctx context.Context, arg database.InsertTemplateVersionParams) error { if !arg.TemplateID.Valid { // Making a new template version is the same permission as creating a new template. err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceTemplate.InOrg(arg.OrganizationID)) if err != nil { return err } } else { // Must do an authorized fetch to prevent leaking template ids this way. tpl, err := q.GetTemplateByID(ctx, arg.TemplateID.UUID) if err != nil { return err } // Check the create permission on the template. err = q.authorizeContext(ctx, policy.ActionCreate, tpl) if err != nil { return err } } return q.db.InsertTemplateVersion(ctx, arg) } func (q *querier) InsertTemplateVersionParameter(ctx context.Context, arg database.InsertTemplateVersionParameterParams) (database.TemplateVersionParameter, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.TemplateVersionParameter{}, err } return q.db.InsertTemplateVersionParameter(ctx, arg) } func (q *querier) InsertTemplateVersionTerraformValuesByJobID(ctx context.Context, arg database.InsertTemplateVersionTerraformValuesByJobIDParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertTemplateVersionTerraformValuesByJobID(ctx, arg) } func (q *querier) InsertTemplateVersionVariable(ctx context.Context, arg database.InsertTemplateVersionVariableParams) (database.TemplateVersionVariable, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.TemplateVersionVariable{}, err } return q.db.InsertTemplateVersionVariable(ctx, arg) } func (q *querier) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg database.InsertTemplateVersionWorkspaceTagParams) (database.TemplateVersionWorkspaceTag, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.TemplateVersionWorkspaceTag{}, err } return q.db.InsertTemplateVersionWorkspaceTag(ctx, arg) } func (q *querier) InsertUsageEvent(ctx context.Context, arg database.InsertUsageEventParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceUsageEvent); err != nil { return err } return q.db.InsertUsageEvent(ctx, arg) } func (q *querier) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) { // Always check if the assigned roles can actually be assigned by this actor. impliedRoles := append([]rbac.RoleIdentifier{rbac.RoleMember()}, q.convertToDeploymentRoles(arg.RBACRoles)...) err := q.canAssignRoles(ctx, uuid.Nil, impliedRoles, []rbac.RoleIdentifier{}) if err != nil { return database.User{}, err } obj := rbac.ResourceUser return insert(q.log, q.auth, obj, q.db.InsertUser)(ctx, arg) } func (q *querier) InsertUserGroupsByID(ctx context.Context, arg database.InsertUserGroupsByIDParams) ([]uuid.UUID, error) { // This is used by OIDC sync. So only used by a system user. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return nil, err } return q.db.InsertUserGroupsByID(ctx, arg) } // TODO: Should this be in system.go? func (q *querier) InsertUserLink(ctx context.Context, arg database.InsertUserLinkParams) (database.UserLink, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceUserObject(arg.UserID)); err != nil { return database.UserLink{}, err } return q.db.InsertUserLink(ctx, arg) } func (q *querier) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return database.WorkspaceAgentVolumeResourceMonitor{}, err } return q.db.InsertVolumeResourceMonitor(ctx, arg) } func (q *querier) InsertWebpushSubscription(ctx context.Context, arg database.InsertWebpushSubscriptionParams) (database.WebpushSubscription, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil { return database.WebpushSubscription{}, err } return q.db.InsertWebpushSubscription(ctx, arg) } func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) { obj := rbac.ResourceWorkspace.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID) tpl, err := q.GetTemplateByID(ctx, arg.TemplateID) if err != nil { return database.WorkspaceTable{}, xerrors.Errorf("verify template by id: %w", err) } if err := q.authorizeContext(ctx, policy.ActionUse, tpl); err != nil { return database.WorkspaceTable{}, xerrors.Errorf("use template for workspace: %w", err) } return insert(q.log, q.auth, obj, q.db.InsertWorkspace)(ctx, arg) } func (q *querier) InsertWorkspaceAgent(ctx context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) { // NOTE(DanielleMaywood): // Currently, the only way to link a Resource back to a Workspace is by following this chain: // // WorkspaceResource -> WorkspaceBuild -> Workspace // // It is possible for this function to be called without there existing // a `WorkspaceBuild` to link back to. This means that we want to allow // execution to continue if there isn't a workspace found to allow this // behavior to continue. workspace, err := q.db.GetWorkspaceByResourceID(ctx, arg.ResourceID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return database.WorkspaceAgent{}, err } if err := q.authorizeContext(ctx, policy.ActionCreateAgent, workspace); err != nil { return database.WorkspaceAgent{}, err } return q.db.InsertWorkspaceAgent(ctx, arg) } func (q *querier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg database.InsertWorkspaceAgentDevcontainersParams) ([]database.WorkspaceAgentDevcontainer, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentDevcontainers); err != nil { return nil, err } return q.db.InsertWorkspaceAgentDevcontainers(ctx, arg) } func (q *querier) InsertWorkspaceAgentLogSources(ctx context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) { // TODO: This is used by the agent, should we have an rbac check here? return q.db.InsertWorkspaceAgentLogSources(ctx, arg) } func (q *querier) InsertWorkspaceAgentLogs(ctx context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) { // TODO: This is used by the agent, should we have an rbac check here? return q.db.InsertWorkspaceAgentLogs(ctx, arg) } func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database.InsertWorkspaceAgentMetadataParams) error { // We don't check for workspace ownership here since the agent metadata may // be associated with an orphaned agent used by a dry run build. if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertWorkspaceAgentMetadata(ctx, arg) } func (q *querier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) (database.WorkspaceAgentScriptTiming, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.WorkspaceAgentScriptTiming{}, err } return q.db.InsertWorkspaceAgentScriptTimings(ctx, arg) } func (q *querier) InsertWorkspaceAgentScripts(ctx context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return []database.WorkspaceAgentScript{}, err } return q.db.InsertWorkspaceAgentScripts(ctx, arg) } func (q *querier) InsertWorkspaceAgentStats(ctx context.Context, arg database.InsertWorkspaceAgentStatsParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertWorkspaceAgentStats(ctx, arg) } func (q *querier) InsertWorkspaceAppStats(ctx context.Context, arg database.InsertWorkspaceAppStatsParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.InsertWorkspaceAppStats(ctx, arg) } func (q *querier) InsertWorkspaceAppStatus(ctx context.Context, arg database.InsertWorkspaceAppStatusParams) (database.WorkspaceAppStatus, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.WorkspaceAppStatus{}, err } return q.db.InsertWorkspaceAppStatus(ctx, arg) } func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error { w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { return xerrors.Errorf("get workspace by id: %w", err) } action, err := workspaceTransitionAction(arg.Transition) if err != nil { return err } // Special handling for prebuilt workspace deletion if err := q.authorizePrebuiltWorkspace(ctx, action, w); err != nil { return err } // If we're starting a workspace we need to check the template. if arg.Transition == database.WorkspaceTransitionStart { t, err := q.db.GetTemplateByID(ctx, w.TemplateID) if err != nil { return xerrors.Errorf("get template by id: %w", err) } accessControl := (*q.acs.Load()).GetTemplateAccessControl(t) // If the template requires the active version we need to check if // the user is a template admin. If they aren't and are attempting // to use a non-active version then we must fail the request. if accessControl.RequireActiveVersion { if arg.TemplateVersionID != t.ActiveVersionID { if err = q.authorizeContext(ctx, policy.ActionUpdate, t); err != nil { return xerrors.Errorf("cannot use non-active version: %w", err) } } } } return q.db.InsertWorkspaceBuild(ctx, arg) } func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg database.InsertWorkspaceBuildParametersParams) error { // TODO: Optimize this. We always have the workspace and build already fetched. build, err := q.db.GetWorkspaceBuildByID(ctx, arg.WorkspaceBuildID) if err != nil { return err } workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { return err } action, err := workspaceTransitionAction(build.Transition) if err != nil { return err } // Special handling for prebuilt workspace deletion if err := q.authorizePrebuiltWorkspace(ctx, action, workspace); err != nil { return err } return q.db.InsertWorkspaceBuildParameters(ctx, arg) } func (q *querier) InsertWorkspaceModule(ctx context.Context, arg database.InsertWorkspaceModuleParams) (database.WorkspaceModule, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.WorkspaceModule{}, err } return q.db.InsertWorkspaceModule(ctx, arg) } func (q *querier) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { return insert(q.log, q.auth, rbac.ResourceWorkspaceProxy, q.db.InsertWorkspaceProxy)(ctx, arg) } func (q *querier) InsertWorkspaceResource(ctx context.Context, arg database.InsertWorkspaceResourceParams) (database.WorkspaceResource, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.WorkspaceResource{}, err } return q.db.InsertWorkspaceResource(ctx, arg) } func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg database.InsertWorkspaceResourceMetadataParams) ([]database.WorkspaceResourceMetadatum, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return nil, err } return q.db.InsertWorkspaceResourceMetadata(ctx, arg) } func (q *querier) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.ListAuthorizedAIBridgeInterceptions(ctx, arg, prep) } func (q *querier) ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Context, arg database.ListAIBridgeInterceptionsTelemetrySummariesParams) ([]database.ListAIBridgeInterceptionsTelemetrySummariesRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil { return nil, err } return q.db.ListAIBridgeInterceptionsTelemetrySummaries(ctx, arg) } func (q *querier) ListAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams) ([]string, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) } return q.db.ListAuthorizedAIBridgeModels(ctx, arg, prep) } func (q *querier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeTokenUsage, error) { // This function is a system function until we implement a join for aibridge interceptions. // Matches the behavior of the workspaces listing endpoint. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.ListAIBridgeTokenUsagesByInterceptionIDs(ctx, interceptionIDs) } func (q *querier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeToolUsage, error) { // This function is a system function until we implement a join for aibridge interceptions. // Matches the behavior of the workspaces listing endpoint. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.ListAIBridgeToolUsagesByInterceptionIDs(ctx, interceptionIDs) } func (q *querier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeUserPrompt, error) { // This function is a system function until we implement a join for aibridge interceptions. // Matches the behavior of the workspaces listing endpoint. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } return q.db.ListAIBridgeUserPromptsByInterceptionIDs(ctx, interceptionIDs) } func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID) } func (q *querier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganizationExcludeReserved)(ctx, organizationID) } func (q *querier) ListTasks(ctx context.Context, arg database.ListTasksParams) ([]database.Task, error) { // TODO(Cian): replace this with a sql filter for improved performance. https://github.com/coder/internal/issues/1061 return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListTasks)(ctx, arg) } func (q *querier) ListUserSecrets(ctx context.Context, userID uuid.UUID) ([]database.UserSecret, error) { obj := rbac.ResourceUserSecret.WithOwner(userID.String()) if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil { return nil, err } return q.db.ListUserSecrets(ctx, userID) } func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID) if err != nil { return nil, err } // listing port shares is more akin to reading the workspace. if err := q.authorizeContext(ctx, policy.ActionRead, workspace); err != nil { return nil, err } return q.db.ListWorkspaceAgentPortShares(ctx, workspaceID) } func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { resource := rbac.ResourceInboxNotification.WithOwner(arg.UserID.String()) if err := q.authorizeContext(ctx, policy.ActionUpdate, resource); err != nil { return err } return q.db.MarkAllInboxNotificationsAsRead(ctx, arg) } func (q *querier) OIDCClaimFieldValues(ctx context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) { resource := rbac.ResourceIdpsyncSettings if args.OrganizationID != uuid.Nil { resource = resource.InOrg(args.OrganizationID) } if err := q.authorizeContext(ctx, policy.ActionRead, resource); err != nil { return nil, err } return q.db.OIDCClaimFieldValues(ctx, args) } func (q *querier) OIDCClaimFields(ctx context.Context, organizationID uuid.UUID) ([]string, error) { resource := rbac.ResourceIdpsyncSettings if organizationID != uuid.Nil { resource = resource.InOrg(organizationID) } if err := q.authorizeContext(ctx, policy.ActionRead, resource); err != nil { return nil, err } return q.db.OIDCClaimFields(ctx, organizationID) } func (q *querier) OrganizationMembers(ctx context.Context, arg database.OrganizationMembersParams) ([]database.OrganizationMembersRow, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.OrganizationMembers)(ctx, arg) } func (q *querier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { // Required to have permission to read all members in the organization if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOrganizationMember.InOrg(arg.OrganizationID)); err != nil { return nil, err } return q.db.PaginatedOrganizationMembers(ctx, arg) } func (q *querier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) { chat, err := q.db.GetChatByID(ctx, chatID) if err != nil { return database.ChatQueuedMessage{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.ChatQueuedMessage{}, err } return q.db.PopNextQueuedMessage(ctx, chatID) } func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return err } return q.db.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID) } func (q *querier) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { fetch := func(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxyByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.RegisterWorkspaceProxy)(ctx, arg) } func (q *querier) RemoveUserFromGroups(ctx context.Context, arg database.RemoveUserFromGroupsParams) ([]uuid.UUID, error) { // This is a system function to clear user groups in group sync. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return nil, err } return q.db.RemoveUserFromGroups(ctx, arg) } func (q *querier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.RevokeDBCryptKey(ctx, activeKeyDigest) } func (q *querier) SelectUsageEventsForPublishing(ctx context.Context, arg time.Time) ([]database.UsageEvent, error) { // ActionUpdate because we're updating the publish_started_at column. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceUsageEvent); err != nil { return nil, err } return q.db.SelectUsageEventsForPublishing(ctx, arg) } func (q *querier) TryAcquireLock(ctx context.Context, id int64) (bool, error) { return q.db.TryAcquireLock(ctx, id) } func (q *querier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) error { chat, err := q.db.GetChatByID(ctx, id) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return err } return q.db.UnarchiveChatByID(ctx, id) } func (q *querier) UnarchiveTemplateVersion(ctx context.Context, arg database.UnarchiveTemplateVersionParams) error { v, err := q.db.GetTemplateVersionByID(ctx, arg.TemplateVersionID) if err != nil { return err } tpl, err := q.db.GetTemplateByID(ctx, v.TemplateID.UUID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, tpl); err != nil { return err } return q.db.UnarchiveTemplateVersion(ctx, arg) } func (q *querier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error { fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, id) } return update(q.log, q.auth, fetch, q.db.UnfavoriteWorkspace)(ctx, id) } func (q *querier) UnsetDefaultChatModelConfigs(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UnsetDefaultChatModelConfigs(ctx) } func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params database.UpdateAIBridgeInterceptionEndedParams) (database.AIBridgeInterception, error) { if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, params.ID); err != nil { return database.AIBridgeInterception{}, err } return q.db.UpdateAIBridgeInterceptionEnded(ctx, params) } func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateAPIKeyByIDParams) (database.APIKey, error) { return q.db.GetAPIKeyByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg) } func (q *querier) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) { chat, err := q.db.GetChatByID(ctx, arg.ID) if err != nil { return database.Chat{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.Chat{}, err } return q.db.UpdateChatByID(ctx, arg) } func (q *querier) UpdateChatHeartbeat(ctx context.Context, arg database.UpdateChatHeartbeatParams) (int64, error) { chat, err := q.db.GetChatByID(ctx, arg.ID) if err != nil { return 0, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return 0, err } return q.db.UpdateChatHeartbeat(ctx, arg) } func (q *querier) UpdateChatMessageByID(ctx context.Context, arg database.UpdateChatMessageByIDParams) (database.ChatMessage, error) { // Authorize update on the parent chat of the edited message. msg, err := q.db.GetChatMessageByID(ctx, arg.ID) if err != nil { return database.ChatMessage{}, err } chat, err := q.db.GetChatByID(ctx, msg.ChatID) if err != nil { return database.ChatMessage{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.ChatMessage{}, err } return q.db.UpdateChatMessageByID(ctx, arg) } func (q *querier) UpdateChatModelConfig(ctx context.Context, arg database.UpdateChatModelConfigParams) (database.ChatModelConfig, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return database.ChatModelConfig{}, err } return q.db.UpdateChatModelConfig(ctx, arg) } func (q *querier) UpdateChatProvider(ctx context.Context, arg database.UpdateChatProviderParams) (database.ChatProvider, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return database.ChatProvider{}, err } return q.db.UpdateChatProvider(ctx, arg) } func (q *querier) UpdateChatStatus(ctx context.Context, arg database.UpdateChatStatusParams) (database.Chat, error) { // UpdateChatStatus is used by the chat processor to change chat status. // It should be called with system context. chat, err := q.db.GetChatByID(ctx, arg.ID) if err != nil { return database.Chat{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.Chat{}, err } return q.db.UpdateChatStatus(ctx, arg) } func (q *querier) UpdateChatWorkspace(ctx context.Context, arg database.UpdateChatWorkspaceParams) (database.Chat, error) { chat, err := q.db.GetChatByID(ctx, arg.ID) if err != nil { return database.Chat{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.Chat{}, err } // UpdateChatWorkspace is manually implemented for chat tables and may not be // present on every wrapped store interface yet. chatWorkspaceUpdater, ok := q.db.(interface { UpdateChatWorkspace(context.Context, database.UpdateChatWorkspaceParams) (database.Chat, error) }) if !ok { return database.Chat{}, xerrors.New("update chat workspace is not implemented by wrapped store") } return chatWorkspaceUpdater.UpdateChatWorkspace(ctx, arg) } func (q *querier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err } return q.db.UpdateCryptoKeyDeletesAt(ctx, arg) } func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) { if !arg.OrganizationID.Valid || arg.OrganizationID.UUID == uuid.Nil { return database.CustomRole{}, NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")} } rbacObj := rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID) if err := q.authorizeContext(ctx, policy.ActionUpdate, rbacObj); err != nil { return database.CustomRole{}, err } existing, err := database.ExpectOne(q.db.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: []database.NameOrganizationPair{ { Name: arg.Name, OrganizationID: arg.OrganizationID.UUID, }, }, ExcludeOrgRoles: false, OrganizationID: uuid.Nil, IncludeSystemRoles: true, })) if err != nil { return database.CustomRole{}, err } if existing.IsSystem { err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem) if err != nil { return database.CustomRole{}, err } } if err := q.customRoleCheck(ctx, database.CustomRole{ Name: arg.Name, DisplayName: arg.DisplayName, SitePermissions: arg.SitePermissions, OrgPermissions: arg.OrgPermissions, UserPermissions: arg.UserPermissions, MemberPermissions: arg.MemberPermissions, CreatedAt: time.Now(), UpdatedAt: time.Now(), OrganizationID: arg.OrganizationID, ID: uuid.New(), IsSystem: existing.IsSystem, }, policy.ActionUpdate); err != nil { return database.CustomRole{}, err } return q.db.UpdateCustomRole(ctx, arg) } func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) { fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) { return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID}) } return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateExternalAuthLink)(ctx, arg) } func (q *querier) UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg database.UpdateExternalAuthLinkRefreshTokenParams) error { fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkRefreshTokenParams) (database.ExternalAuthLink, error) { return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID}) } return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateExternalAuthLinkRefreshToken)(ctx, arg) } func (q *querier) UpdateGitSSHKey(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) { fetch := func(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) { return q.db.GetGitSSHKey(ctx, arg.UserID) } return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateGitSSHKey)(ctx, arg) } func (q *querier) UpdateGroupByID(ctx context.Context, arg database.UpdateGroupByIDParams) (database.Group, error) { fetch := func(ctx context.Context, arg database.UpdateGroupByIDParams) (database.Group, error) { return q.db.GetGroupByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateGroupByID)(ctx, arg) } func (q *querier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return nil, err } return q.db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter) } func (q *querier) UpdateInboxNotificationReadStatus(ctx context.Context, args database.UpdateInboxNotificationReadStatusParams) error { fetchFunc := func(ctx context.Context, args database.UpdateInboxNotificationReadStatusParams) (database.InboxNotification, error) { return q.db.GetInboxNotificationByID(ctx, args.ID) } return update(q.log, q.auth, fetchFunc, q.db.UpdateInboxNotificationReadStatus)(ctx, args) } func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) { // Authorized fetch will check that the actor has read access to the org member since the org member is returned. member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: arg.OrgID, UserID: arg.UserID, IncludeSystem: false, GithubUserID: 0, })) if err != nil { return database.OrganizationMember{}, err } originalRoles, err := q.convertToOrganizationRoles(member.OrganizationMember.OrganizationID, member.OrganizationMember.Roles) if err != nil { return database.OrganizationMember{}, xerrors.Errorf("convert original roles: %w", err) } // The 'rbac' package expects role names to be scoped. // Convert the argument roles for validation. scopedGranted, err := q.convertToOrganizationRoles(arg.OrgID, arg.GrantedRoles) if err != nil { return database.OrganizationMember{}, err } // The org member role is always implied. //nolint:gocritic impliedTypes := append(scopedGranted, rbac.ScopedRoleOrgMember(arg.OrgID)) added, removed := rbac.ChangeRoleSet(originalRoles, impliedTypes) err = q.canAssignRoles(ctx, arg.OrgID, added, removed) if err != nil { return database.OrganizationMember{}, err } return q.db.UpdateMemberRoles(ctx, arg) } func (q *querier) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return err } return q.db.UpdateMemoryResourceMonitor(ctx, arg) } func (q *querier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationTemplate); err != nil { return database.NotificationTemplate{}, err } return q.db.UpdateNotificationTemplateMethodByID(ctx, arg) } func (q *querier) UpdateOAuth2ProviderAppByClientID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByClientIDParams) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.UpdateOAuth2ProviderAppByClientID(ctx, arg) } func (q *querier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err } return q.db.UpdateOAuth2ProviderAppByID(ctx, arg) } func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { fetch := func(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { return q.db.GetOrganizationByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOrganization)(ctx, arg) } func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg database.UpdateOrganizationDeletedByIDParams) error { deleteF := func(ctx context.Context, id uuid.UUID) error { return q.db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ ID: id, UpdatedAt: dbtime.Now(), }) } return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID) } func (q *querier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg database.UpdateOrganizationWorkspaceSharingSettingsParams) (database.Organization, error) { fetch := func(ctx context.Context, arg database.UpdateOrganizationWorkspaceSharingSettingsParams) (database.Organization, error) { return q.db.GetOrganizationByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOrganizationWorkspaceSharingSettings)(ctx, arg) } func (q *querier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]database.UpdatePrebuildProvisionerJobWithCancelRow, error) { // Prebuild operation for canceling pending prebuild jobs from non-active template versions if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourcePrebuiltWorkspace); err != nil { return []database.UpdatePrebuildProvisionerJobWithCancelRow{}, err } return q.db.UpdatePrebuildProvisionerJobWithCancel(ctx, arg) } func (q *querier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.UpdatePresetPrebuildStatusParams) error { preset, err := q.db.GetPresetByID(ctx, arg.PresetID) if err != nil { return err } // TODO: This does not check the acl list on the template. Should it? object := rbac.ResourceTemplate. WithID(preset.TemplateID.UUID). InOrg(preset.OrganizationID) err = q.authorizeContext(ctx, policy.ActionUpdate, object) if err != nil { return err } return q.db.UpdatePresetPrebuildStatus(ctx, arg) } func (q *querier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) { // Fetch template to check authorization template, err := q.db.GetTemplateByID(ctx, arg.TemplateID) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return nil, err } return q.db.UpdatePresetsLastInvalidatedAt(ctx, arg) } func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil { return err } return q.db.UpdateProvisionerDaemonLastSeenAt(ctx, arg) } func (q *querier) UpdateProvisionerJobByID(ctx context.Context, arg database.UpdateProvisionerJobByIDParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return err } return q.db.UpdateProvisionerJobByID(ctx, arg) } func (q *querier) UpdateProvisionerJobLogsLength(ctx context.Context, arg database.UpdateProvisionerJobLogsLengthParams) error { // Not sure what the rbac should be here, going with this for now if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return err } return q.db.UpdateProvisionerJobLogsLength(ctx, arg) } func (q *querier) UpdateProvisionerJobLogsOverflowed(ctx context.Context, arg database.UpdateProvisionerJobLogsOverflowedParams) error { // Not sure what the rbac should be here, going with this for now if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return err } return q.db.UpdateProvisionerJobLogsOverflowed(ctx, arg) } func (q *querier) UpdateProvisionerJobWithCancelByID(ctx context.Context, arg database.UpdateProvisionerJobWithCancelByIDParams) error { // TODO: Remove this once we have a proper rbac check for provisioner jobs. // Details in https://github.com/coder/coder/issues/16160 job, err := q.db.GetProvisionerJobByID(ctx, arg.ID) if err != nil { return err } switch job.Type { case database.ProvisionerJobTypeWorkspaceBuild: build, err := q.db.GetWorkspaceBuildByJobID(ctx, arg.ID) if err != nil { return err } workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { return err } template, err := q.db.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { return err } // Template can specify if cancels are allowed. // Would be nice to have a way in the rbac rego to do this. if !template.AllowUserCancelWorkspaceJobs { // Only owners can cancel workspace builds actor, ok := ActorFromContext(ctx) if !ok { return ErrNoActor } if !slice.Contains(actor.Roles.Names(), rbac.RoleOwner()) { return xerrors.Errorf("only owners can cancel workspace builds") } } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace) if err != nil { return err } case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport: // Authorized call to get template version. templateVersion, err := authorizedTemplateVersionFromJob(ctx, q, job) if err != nil { return err } if templateVersion.TemplateID.Valid { template, err := q.db.GetTemplateByID(ctx, templateVersion.TemplateID.UUID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdate, templateVersion.RBACObject(template)) if err != nil { return err } } else { err = q.authorizeContext(ctx, policy.ActionUpdate, templateVersion.RBACObjectNoTemplate()) if err != nil { return err } } default: return xerrors.Errorf("unknown job type: %q", job.Type) } return q.db.UpdateProvisionerJobWithCancelByID(ctx, arg) } func (q *querier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg database.UpdateProvisionerJobWithCompleteByIDParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return err } return q.db.UpdateProvisionerJobWithCompleteByID(ctx, arg) } func (q *querier) UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg database.UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerJobs); err != nil { return err } return q.db.UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx, arg) } func (q *querier) UpdateReplica(ctx context.Context, arg database.UpdateReplicaParams) (database.Replica, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return database.Replica{}, err } return q.db.UpdateReplica(ctx, arg) } func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil { return err } return q.db.UpdateTailnetPeerStatusByCoordinator(ctx, arg) } func (q *querier) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) { // An actor is allowed to update the prompt of a task if they have // permission to update the task (same as UpdateTaskWorkspaceID). task, err := q.db.GetTaskByID(ctx, arg.ID) if err != nil { return database.TaskTable{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil { return database.TaskTable{}, err } return q.db.UpdateTaskPrompt(ctx, arg) } func (q *querier) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) { // An actor is allowed to update the workspace ID of a task if they are the // owner of the task and workspace or have the appropriate permissions. task, err := q.db.GetTaskByID(ctx, arg.ID) if err != nil { return database.TaskTable{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil { return database.TaskTable{}, err } ws, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID.UUID) if err != nil { return database.TaskTable{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, ws.RBACObject()); err != nil { return database.TaskTable{}, err } return q.db.UpdateTaskWorkspaceID(ctx, arg) } func (q *querier) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } // UpdateTemplateACL uses the ActionCreate action. Only users that can create the template // may update the ACL. return fetchAndExec(q.log, q.auth, policy.ActionCreate, fetch, q.db.UpdateTemplateACLByID)(ctx, arg) } func (q *querier) UpdateTemplateAccessControlByID(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateTemplateAccessControlByID)(ctx, arg) } func (q *querier) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateTemplateActiveVersionByID)(ctx, arg) } // Deprecated: use SoftDeleteTemplateByID instead. func (q *querier) UpdateTemplateDeletedByID(ctx context.Context, arg database.UpdateTemplateDeletedByIDParams) error { return q.SoftDeleteTemplateByID(ctx, arg.ID) } func (q *querier) UpdateTemplateMetaByID(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateTemplateMetaByID)(ctx, arg) } func (q *querier) UpdateTemplateScheduleByID(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateTemplateScheduleByID)(ctx, arg) } func (q *querier) UpdateTemplateVersionByID(ctx context.Context, arg database.UpdateTemplateVersionByIDParams) error { // An actor is allowed to update the template version if they are authorized to update the template. tv, err := q.db.GetTemplateVersionByID(ctx, arg.ID) if err != nil { return err } var obj rbac.Objecter if !tv.TemplateID.Valid { obj = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { tpl, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { return err } obj = tpl } if err := q.authorizeContext(ctx, policy.ActionUpdate, obj); err != nil { return err } return q.db.UpdateTemplateVersionByID(ctx, arg) } func (q *querier) UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg database.UpdateTemplateVersionDescriptionByJobIDParams) error { // An actor is allowed to update the template version description if they are authorized to update the template. tv, err := q.db.GetTemplateVersionByJobID(ctx, arg.JobID) if err != nil { return err } var obj rbac.Objecter if !tv.TemplateID.Valid { obj = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { tpl, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { return err } obj = tpl } if err := q.authorizeContext(ctx, policy.ActionUpdate, obj); err != nil { return err } return q.db.UpdateTemplateVersionDescriptionByJobID(ctx, arg) } func (q *querier) UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg database.UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error { // An actor is allowed to update the template version external auth providers if they are authorized to update the template. tv, err := q.db.GetTemplateVersionByJobID(ctx, arg.JobID) if err != nil { return err } var obj rbac.Objecter if !tv.TemplateID.Valid { obj = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { tpl, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { return err } obj = tpl } if err := q.authorizeContext(ctx, policy.ActionUpdate, obj); err != nil { return err } return q.db.UpdateTemplateVersionExternalAuthProvidersByJobID(ctx, arg) } func (q *querier) UpdateTemplateVersionFlagsByJobID(ctx context.Context, arg database.UpdateTemplateVersionFlagsByJobIDParams) error { // An actor is allowed to update the template version ai task and external agent flag if they are authorized to update the template. tv, err := q.db.GetTemplateVersionByJobID(ctx, arg.JobID) if err != nil { return err } var obj rbac.Objecter if !tv.TemplateID.Valid { obj = rbac.ResourceTemplate.InOrg(tv.OrganizationID) } else { tpl, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID) if err != nil { return err } obj = tpl } if err := q.authorizeContext(ctx, policy.ActionUpdate, obj); err != nil { return err } return q.db.UpdateTemplateVersionFlagsByJobID(ctx, arg) } func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.TemplateID) } return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg) } func (q *querier) UpdateUsageEventsPostPublish(ctx context.Context, arg database.UpdateUsageEventsPostPublishParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceUsageEvent); err != nil { return err } return q.db.UpdateUsageEventsPostPublish(ctx, arg) } func (q *querier) UpdateUserChatCustomPrompt(ctx context.Context, arg database.UpdateUserChatCustomPromptParams) (database.UserConfig, error) { u, err := q.db.GetUserByID(ctx, arg.UserID) if err != nil { return database.UserConfig{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.UserConfig{}, err } return q.db.UpdateUserChatCustomPrompt(ctx, arg) } func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id) } func (q *querier) UpdateUserGithubComUserID(ctx context.Context, arg database.UpdateUserGithubComUserIDParams) error { user, err := q.db.GetUserByID(ctx, arg.ID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdatePersonal, user) if err != nil { // System user can also update err = q.authorizeContext(ctx, policy.ActionUpdate, user) if err != nil { return err } } return q.db.UpdateUserGithubComUserID(ctx, arg) } func (q *querier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg database.UpdateUserHashedOneTimePasscodeParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpdateUserHashedOneTimePasscode(ctx, arg) } func (q *querier) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error { user, err := q.db.GetUserByID(ctx, arg.ID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdatePersonal, user) if err != nil { // Admins can update passwords for other users. err = q.authorizeContext(ctx, policy.ActionUpdate, user) if err != nil { return err } } return q.db.UpdateUserHashedPassword(ctx, arg) } func (q *querier) UpdateUserLastSeenAt(ctx context.Context, arg database.UpdateUserLastSeenAtParams) (database.User, error) { fetch := func(ctx context.Context, arg database.UpdateUserLastSeenAtParams) (database.User, error) { return q.db.GetUserByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserLastSeenAt)(ctx, arg) } func (q *querier) UpdateUserLink(ctx context.Context, arg database.UpdateUserLinkParams) (database.UserLink, error) { fetch := func(ctx context.Context, arg database.UpdateUserLinkParams) (database.UserLink, error) { return q.db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{ UserID: arg.UserID, LoginType: arg.LoginType, }) } return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateUserLink)(ctx, arg) } func (q *querier) UpdateUserLoginType(ctx context.Context, arg database.UpdateUserLoginTypeParams) (database.User, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return database.User{}, err } return q.db.UpdateUserLoginType(ctx, arg) } func (q *querier) UpdateUserNotificationPreferences(ctx context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationPreference.WithOwner(arg.UserID.String())); err != nil { return -1, err } return q.db.UpdateUserNotificationPreferences(ctx, arg) } func (q *querier) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) { u, err := q.db.GetUserByID(ctx, arg.ID) if err != nil { return database.User{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.User{}, err } return q.db.UpdateUserProfile(ctx, arg) } func (q *querier) UpdateUserQuietHoursSchedule(ctx context.Context, arg database.UpdateUserQuietHoursScheduleParams) (database.User, error) { u, err := q.db.GetUserByID(ctx, arg.ID) if err != nil { return database.User{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.User{}, err } return q.db.UpdateUserQuietHoursSchedule(ctx, arg) } // UpdateUserRoles updates the site roles of a user. The validation for this function include more than // just a basic RBAC check. func (q *querier) UpdateUserRoles(ctx context.Context, arg database.UpdateUserRolesParams) (database.User, error) { // We need to fetch the user being updated to identify the change in roles. // This requires read access on the user in question, since the user is // returned from this function. user, err := fetch(q.log, q.auth, q.db.GetUserByID)(ctx, arg.ID) if err != nil { return database.User{}, err } // The member role is always implied. impliedTypes := append(q.convertToDeploymentRoles(arg.GrantedRoles), rbac.RoleMember()) // If the changeset is nothing, less rbac checks need to be done. added, removed := rbac.ChangeRoleSet(q.convertToDeploymentRoles(user.RBACRoles), impliedTypes) err = q.canAssignRoles(ctx, uuid.Nil, added, removed) if err != nil { return database.User{}, err } return q.db.UpdateUserRoles(ctx, arg) } func (q *querier) UpdateUserSecret(ctx context.Context, arg database.UpdateUserSecretParams) (database.UserSecret, error) { // First get the secret to check ownership secret, err := q.db.GetUserSecret(ctx, arg.ID) if err != nil { return database.UserSecret{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, secret); err != nil { return database.UserSecret{}, err } return q.db.UpdateUserSecret(ctx, arg) } func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) { fetch := func(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) { return q.db.GetUserByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserStatus)(ctx, arg) } func (q *querier) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg database.UpdateUserTaskNotificationAlertDismissedParams) (bool, error) { user, err := q.db.GetUserByID(ctx, arg.UserID) if err != nil { return false, err } if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, user); err != nil { return false, err } return q.db.UpdateUserTaskNotificationAlertDismissed(ctx, arg) } func (q *querier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { u, err := q.db.GetUserByID(ctx, arg.UserID) if err != nil { return database.UserConfig{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.UserConfig{}, err } return q.db.UpdateUserTerminalFont(ctx, arg) } func (q *querier) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { u, err := q.db.GetUserByID(ctx, arg.UserID) if err != nil { return database.UserConfig{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { return database.UserConfig{}, err } return q.db.UpdateUserThemePreference(ctx, arg) } func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return err } return q.db.UpdateVolumeResourceMonitor(ctx, arg) } func (q *querier) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.ID) if err != nil { return database.WorkspaceTable{}, err } return w.WorkspaceTable(), nil } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspace)(ctx, arg) } func (q *querier) UpdateWorkspaceACLByID(ctx context.Context, arg database.UpdateWorkspaceACLByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceACLByIDParams) (database.WorkspaceTable, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.ID) if err != nil { return database.WorkspaceTable{}, err } return w.WorkspaceTable(), nil } return fetchAndExec(q.log, q.auth, policy.ActionShare, fetch, q.db.UpdateWorkspaceACLByID)(ctx, arg) } func (q *querier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpdateWorkspaceAgentConnectionByID(ctx, arg) } func (q *querier) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg database.UpdateWorkspaceAgentDisplayAppsByIDParams) error { workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.ID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdateAgent, workspace); err != nil { return err } return q.db.UpdateWorkspaceAgentDisplayAppsByID(ctx, arg) } func (q *querier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error { workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.ID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, workspace); err != nil { return err } return q.db.UpdateWorkspaceAgentLifecycleStateByID(ctx, arg) } func (q *querier) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg database.UpdateWorkspaceAgentLogOverflowByIDParams) error { agent, err := q.db.GetWorkspaceAgentByID(ctx, arg.ID) if err != nil { return err } workspace, err := q.db.GetWorkspaceByAgentID(ctx, agent.ID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, workspace); err != nil { return err } return q.db.UpdateWorkspaceAgentLogOverflowByID(ctx, arg) } func (q *querier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error { // Fast path: Check if we have an RBAC object in context. // This is set by the workspace agent RPC handler to avoid the expensive // GetWorkspaceByAgentID query for every metadata update. // NOTE: The cached RBAC object is refreshed every 5 minutes in agentapi/api.go. if rbacObj, ok := WorkspaceRBACFromContext(ctx); ok { // Errors here will result in falling back to the GetWorkspaceAgentByID query, skipping // the cache in case the cached data is stale. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbacObj); err == nil { return q.db.UpdateWorkspaceAgentMetadata(ctx, arg) } q.log.Debug(ctx, "fast path authorization failed, using slow path", slog.F("agent_id", arg.WorkspaceAgentID)) } // Slow path: Fallback to fetching the workspace for authorization if the RBAC object is not present (or is invalid) // in the request context. workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.WorkspaceAgentID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace) if err != nil { return err } return q.db.UpdateWorkspaceAgentMetadata(ctx, arg) } func (q *querier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg database.UpdateWorkspaceAgentStartupByIDParams) error { agent, err := q.db.GetWorkspaceAgentByID(ctx, arg.ID) if err != nil { return err } workspace, err := q.db.GetWorkspaceByAgentID(ctx, agent.ID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, workspace); err != nil { return err } return q.db.UpdateWorkspaceAgentStartupByID(ctx, arg) } func (q *querier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error { // TODO: This is a workspace agent operation. Should users be able to query this? workspace, err := q.db.GetWorkspaceByWorkspaceAppID(ctx, arg.ID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace.RBACObject()) if err != nil { return err } return q.db.UpdateWorkspaceAppHealthByID(ctx, arg) } func (q *querier) UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg database.UpdateWorkspaceAutomaticUpdatesParams) error { workspace, err := q.db.GetWorkspaceByID(ctx, arg.ID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace.RBACObject()) if err != nil { return err } return q.db.UpdateWorkspaceAutomaticUpdates(ctx, arg) } func (q *querier) UpdateWorkspaceAutostart(ctx context.Context, arg database.UpdateWorkspaceAutostartParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceAutostartParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceAutostart)(ctx, arg) } // UpdateWorkspaceBuildCostByID is used by the provisioning system to update the cost of a workspace build. func (q *querier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpdateWorkspaceBuildCostByID(ctx, arg) } func (q *querier) UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg database.UpdateWorkspaceBuildDeadlineByIDParams) error { build, err := q.db.GetWorkspaceBuildByID(ctx, arg.ID) if err != nil { return err } workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace.RBACObject()) if err != nil { return err } return q.db.UpdateWorkspaceBuildDeadlineByID(ctx, arg) } func (q *querier) UpdateWorkspaceBuildFlagsByID(ctx context.Context, arg database.UpdateWorkspaceBuildFlagsByIDParams) error { build, err := q.db.GetWorkspaceBuildByID(ctx, arg.ID) if err != nil { return err } workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { return err } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace.RBACObject()) if err != nil { return err } return q.db.UpdateWorkspaceBuildFlagsByID(ctx, arg) } func (q *querier) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg database.UpdateWorkspaceBuildProvisionerStateByIDParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpdateWorkspaceBuildProvisionerStateByID(ctx, arg) } // Deprecated: Use SoftDeleteWorkspaceByID func (q *querier) UpdateWorkspaceDeletedByID(ctx context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { // TODO deleteQ me, placeholder for database.Store fetch := func(ctx context.Context, arg database.UpdateWorkspaceDeletedByIDParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } // This function is always used to deleteQ. return deleteQ(q.log, q.auth, fetch, q.db.UpdateWorkspaceDeletedByID)(ctx, arg) } func (q *querier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.WorkspaceTable, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.WorkspaceTable, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.ID) if err != nil { return database.WorkspaceTable{}, err } return w.WorkspaceTable(), nil } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceDormantDeletingAt)(ctx, arg) } func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) } func (q *querier) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceNextStartAt)(ctx, arg) } func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxyByID(ctx, arg.ID) } return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceProxy)(ctx, arg) } func (q *querier) UpdateWorkspaceProxyDeleted(ctx context.Context, arg database.UpdateWorkspaceProxyDeletedParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceProxyDeletedParams) (database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxyByID(ctx, arg.ID) } return deleteQ(q.log, q.auth, fetch, q.db.UpdateWorkspaceProxyDeleted)(ctx, arg) } func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWorkspaceTTLParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceTTLParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceTTL)(ctx, arg) } func (q *querier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]database.WorkspaceTable, error) { template, err := q.db.GetTemplateByID(ctx, arg.TemplateID) if err != nil { return nil, xerrors.Errorf("get template by id: %w", err) } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return nil, err } return q.db.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, arg) } func (q *querier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg database.UpdateWorkspacesTTLByTemplateIDParams) error { template, err := q.db.GetTemplateByID(ctx, arg.TemplateID) if err != nil { return xerrors.Errorf("get template by id: %w", err) } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return err } return q.db.UpdateWorkspacesTTLByTemplateID(ctx, arg) } func (q *querier) UpsertAnnouncementBanners(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertAnnouncementBanners(ctx, value) } func (q *querier) UpsertApplicationName(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertApplicationName(ctx, value) } func (q *querier) UpsertBoundaryUsageStats(ctx context.Context, arg database.UpsertBoundaryUsageStatsParams) (bool, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceBoundaryUsage); err != nil { return false, err } return q.db.UpsertBoundaryUsageStats(ctx, arg) } func (q *querier) UpsertChatDiffStatus(ctx context.Context, arg database.UpsertChatDiffStatusParams) (database.ChatDiffStatus, error) { // Authorize update on the parent chat. chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { return database.ChatDiffStatus{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.ChatDiffStatus{}, err } return q.db.UpsertChatDiffStatus(ctx, arg) } func (q *querier) UpsertChatDiffStatusReference(ctx context.Context, arg database.UpsertChatDiffStatusReferenceParams) (database.ChatDiffStatus, error) { // Authorize update on the parent chat. chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { return database.ChatDiffStatus{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { return database.ChatDiffStatus{}, err } return q.db.UpsertChatDiffStatusReference(ctx, arg) } func (q *querier) UpsertChatSystemPrompt(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertChatSystemPrompt(ctx, value) } func (q *querier) UpsertConnectionLog(ctx context.Context, arg database.UpsertConnectionLogParams) (database.ConnectionLog, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceConnectionLog); err != nil { return database.ConnectionLog{}, err } return q.db.UpsertConnectionLog(ctx, arg) } func (q *querier) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDefaultProxyParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertDefaultProxy(ctx, arg) } func (q *querier) UpsertHealthSettings(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertHealthSettings(ctx, value) } func (q *querier) UpsertLastUpdateCheck(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertLastUpdateCheck(ctx, value) } func (q *querier) UpsertLogoURL(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertLogoURL(ctx, value) } func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertNotificationReportGeneratorLog(ctx, arg) } func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertNotificationsSettings(ctx, value) } func (q *querier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertOAuth2GithubDefaultEligible(ctx, eligible) } func (q *querier) UpsertPrebuildsSettings(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertPrebuildsSettings(ctx, value) } func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { res := rbac.ResourceProvisionerDaemon.InOrg(arg.OrganizationID) if arg.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser { res.Owner = arg.Tags[provisionersdk.TagOwner] } if err := q.authorizeContext(ctx, policy.ActionCreate, res); err != nil { return database.ProvisionerDaemon{}, err } return q.db.UpsertProvisionerDaemon(ctx, arg) } func (q *querier) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertRuntimeConfig(ctx, arg) } func (q *querier) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (database.TailnetCoordinator, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil { return database.TailnetCoordinator{}, err } return q.db.UpsertTailnetCoordinator(ctx, id) } func (q *querier) UpsertTailnetPeer(ctx context.Context, arg database.UpsertTailnetPeerParams) (database.TailnetPeer, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceTailnetCoordinator); err != nil { return database.TailnetPeer{}, err } return q.db.UpsertTailnetPeer(ctx, arg) } func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTailnetTunnelParams) (database.TailnetTunnel, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceTailnetCoordinator); err != nil { return database.TailnetTunnel{}, err } return q.db.UpsertTailnetTunnel(ctx, arg) } func (q *querier) UpsertTaskSnapshot(ctx context.Context, arg database.UpsertTaskSnapshotParams) error { // Fetch task to build RBAC object for authorization. task, err := q.GetTaskByID(ctx, arg.TaskID) if err != nil { return err } if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil { return err } return q.db.UpsertTaskSnapshot(ctx, arg) } func (q *querier) UpsertTaskWorkspaceApp(ctx context.Context, arg database.UpsertTaskWorkspaceAppParams) (database.TaskWorkspaceApp, error) { // Fetch the task to derive the RBAC object and authorize update on it. task, err := q.db.GetTaskByID(ctx, arg.TaskID) if err != nil { return database.TaskWorkspaceApp{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, task); err != nil { return database.TaskWorkspaceApp{}, err } return q.db.UpsertTaskWorkspaceApp(ctx, arg) } func (q *querier) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertTelemetryItem(ctx, arg) } func (q *querier) UpsertTemplateUsageStats(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertTemplateUsageStats(ctx) } func (q *querier) UpsertWebpushVAPIDKeys(ctx context.Context, arg database.UpsertWebpushVAPIDKeysParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err } return q.db.UpsertWebpushVAPIDKeys(ctx, arg) } func (q *querier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) { workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { return database.WorkspaceAgentPortShare{}, err } err = q.authorizeContext(ctx, policy.ActionUpdate, workspace) if err != nil { return database.WorkspaceAgentPortShare{}, err } return q.db.UpsertWorkspaceAgentPortShare(ctx, arg) } func (q *querier) UpsertWorkspaceApp(ctx context.Context, arg database.UpsertWorkspaceAppParams) (database.WorkspaceApp, error) { // NOTE(DanielleMaywood): // It is possible for there to exist an agent without a workspace. // This means that we want to allow execution to continue if // there isn't a workspace found to allow this behavior to continue. workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.AgentID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return database.WorkspaceApp{}, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, workspace); err != nil { return database.WorkspaceApp{}, err } return q.db.UpsertWorkspaceApp(ctx, arg) } func (q *querier) UpsertWorkspaceAppAuditSession(ctx context.Context, arg database.UpsertWorkspaceAppAuditSessionParams) (bool, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return false, err } return q.db.UpsertWorkspaceAppAuditSession(ctx, arg) } func (q *querier) ValidateGroupIDs(ctx context.Context, groupIDs []uuid.UUID) (database.ValidateGroupIDsRow, error) { // This check is probably overly restrictive, but the "correct" check isn't // necessarily obvious. It's only used as a verification check for ACLs right // now, which are performed as system. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.ValidateGroupIDsRow{}, err } return q.db.ValidateGroupIDs(ctx, groupIDs) } func (q *querier) ValidateUserIDs(ctx context.Context, userIDs []uuid.UUID) (database.ValidateUserIDsRow, error) { // This check is probably overly restrictive, but the "correct" check isn't // necessarily obvious. It's only used as a verification check for ACLs right // now, which are performed as system. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.ValidateUserIDsRow{}, err } return q.db.ValidateUserIDs(ctx, userIDs) } func (q *querier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, _ rbac.PreparedAuthorized) ([]database.Template, error) { // TODO Delete this function, all GetTemplates should be authorized. For now just call getTemplates on the authz querier. return q.GetTemplatesWithFilter(ctx, arg) } func (q *querier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([]database.TemplateGroup, error) { // An actor is authorized to read template group roles if they are authorized to update the template. template, err := q.db.GetTemplateByID(ctx, id) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return nil, err } return q.db.GetTemplateGroupRoles(ctx, id) } func (q *querier) GetTemplateUserRoles(ctx context.Context, id uuid.UUID) ([]database.TemplateUser, error) { // An actor is authorized to query template user roles if they are authorized to update the template. template, err := q.db.GetTemplateByID(ctx, id) if err != nil { return nil, err } if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil { return nil, err } return q.db.GetTemplateUserRoles(ctx, id) } func (q *querier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, _ rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) { // TODO Delete this function, all GetWorkspaces should be authorized. For now just call GetWorkspaces on the authz querier. return q.GetWorkspaces(ctx, arg) } func (q *querier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, _ rbac.PreparedAuthorized) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { return q.GetWorkspacesAndAgentsByOwnerID(ctx, ownerID) } // GetAuthorizedUsers is not required for dbauthz since GetUsers is already // authenticated. func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, _ rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { // GetUsers is authenticated. return q.GetUsers(ctx, arg) } func (q *querier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams, _ rbac.PreparedAuthorized) ([]database.GetAuditLogsOffsetRow, error) { return q.GetAuditLogsOffset(ctx, arg) } func (q *querier) CountAuthorizedAuditLogs(ctx context.Context, arg database.CountAuditLogsParams, _ rbac.PreparedAuthorized) (int64, error) { return q.CountAuditLogs(ctx, arg) } func (q *querier) GetAuthorizedConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams, _ rbac.PreparedAuthorized) ([]database.GetConnectionLogsOffsetRow, error) { return q.GetConnectionLogsOffset(ctx, arg) } func (q *querier) CountAuthorizedConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams, _ rbac.PreparedAuthorized) (int64, error) { return q.CountConnectionLogs(ctx, arg) } func (q *querier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeInterceptionsRow, error) { return q.db.ListAuthorizedAIBridgeInterceptions(ctx, arg, prepared) } func (q *querier) CountAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.CountAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) (int64, error) { return q.db.CountAuthorizedAIBridgeInterceptions(ctx, arg, prepared) } func (q *querier) ListAuthorizedAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams, _ rbac.PreparedAuthorized) ([]string, error) { // TODO: Delete this function, all ListAIBridgeModels should be authorized. For now just call ListAIBridgeModels on the authz querier. // This cannot be deleted for now because it's included in the // database.Store interface, so dbauthz needs to implement it. return q.ListAIBridgeModels(ctx, arg) }