chore: break down dbauthz.System into smaller roles (#6218)

- rbac: export rbac.Permissions
- dbauthz: move GetDeploymentDAUs, GetTemplateDAUs,
  GetTemplateAverageBuildTime from querier.go to system.go
  and removes auth checks
- dbauthz: remove AsSystem(), add individual roles for
  autostart, provisionerd, add restricted system role for 
  everything else
This commit is contained in:
Cian Johnston 2023-02-15 16:14:37 +00:00 committed by GitHub
parent 84da6056b2
commit f0f39b4892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 180 additions and 141 deletions

View File

@ -83,7 +83,7 @@ func activityBumpWorkspace(ctx context.Context, log slog.Logger, db database.Sto
}, nil)
if err != nil {
if !xerrors.Is(err, context.Canceled) {
// Bump will fail if the context is cancelled, but this is ok.
// Bump will fail if the context is canceled, but this is ok.
log.Error(ctx, "bump failed", slog.Error(err),
slog.F("workspace_id", workspaceID),
)

View File

@ -34,8 +34,8 @@ type Stats struct {
// New returns a new autobuild executor.
func New(ctx context.Context, db database.Store, log slog.Logger, tick <-chan time.Time) *Executor {
le := &Executor{
//nolint:gocritic // TODO: make an autostart role instead of using System
ctx: dbauthz.AsSystem(ctx),
//nolint:gocritic // Autostart has a limited set of permissions.
ctx: dbauthz.AsAutostart(ctx),
db: db,
tick: tick,
log: log,

View File

@ -46,12 +46,12 @@ func logNotAuthorizedError(ctx context.Context, logger slog.Logger, err error) e
if err != nil && xerrors.As(err, &internalError) {
e := new(topdown.Error)
if xerrors.As(err, &e) || e.Code == topdown.CancelErr {
// For some reason rego changes a cancelled context to a topdown.CancelErr. We
// expect to check for cancelled context errors if the user cancels the request,
// 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 cancelled context.
// if it's actually a canceled context.
internalError.SetInternal(context.Canceled)
return internalError
}
@ -117,29 +117,73 @@ func ActorFromContext(ctx context.Context) (rbac.Subject, bool) {
return a, ok
}
// AsSystem returns a context with a system actor. This is used for internal
// system operations that are not tied to any particular actor.
// When you use this function, be sure to add a //nolint comment
// explaining why it is necessary.
//
// We trust you have received the usual lecture from the local System
// Administrator. It usually boils down to these three things:
// #1) Respect the privacy of others.
// #2) Think before you type.
// #3) With great power comes great responsibility.
func AsSystem(ctx context.Context) context.Context {
// AsProvisionerd returns a context with an actor that has permissions required
// for provisionerd to function.
func AsProvisionerd(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, rbac.Subject{
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Name: "provisionerd",
DisplayName: "Provisioner Daemon",
Site: rbac.Permissions(map[string][]rbac.Action{
rbac.ResourceFile.Type: {rbac.ActionRead},
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceUser.Type: {rbac.ActionRead},
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
},
)
}
// AsAutostart returns a context with an actor that has permissions required
// for autostart to function.
func AsAutostart(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, rbac.Subject{
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Name: "autostart",
DisplayName: "Autostart Daemon",
Site: rbac.Permissions(map[string][]rbac.Action{
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
},
)
}
// 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 context.WithValue(ctx, authContextKey{}, rbac.Subject{
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Name: "system",
DisplayName: "System",
Site: []rbac.Permission{
{
ResourceType: rbac.ResourceWildcard.Type,
Action: rbac.WildcardSymbol,
},
},
DisplayName: "Coder",
Site: rbac.Permissions(map[string][]rbac.Action{
rbac.ResourceWildcard.Type: {rbac.ActionRead},
rbac.ResourceAPIKey.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
rbac.ResourceGroup.Type: {rbac.ActionCreate, rbac.ActionUpdate},
rbac.ResourceRoleAssignment.Type: {rbac.ActionCreate},
rbac.ResourceOrganization.Type: {rbac.ActionCreate},
rbac.ResourceOrganizationMember.Type: {rbac.ActionCreate},
rbac.ResourceOrgRoleAssignment.Type: {rbac.ActionCreate},
rbac.ResourceUser.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
rbac.ResourceUserData.Type: {rbac.ActionCreate, rbac.ActionUpdate},
rbac.ResourceWorkspace.Type: {rbac.ActionUpdate},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},

View File

@ -327,13 +327,6 @@ func (q *querier) GetProvisionerDaemons(ctx context.Context) ([]database.Provisi
return fetchWithPostFilter(q.auth, fetch)(ctx, nil)
}
func (q *querier) GetDeploymentDAUs(ctx context.Context) ([]database.GetDeploymentDAUsRow, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceUser.All()); err != nil {
return nil, err
}
return q.db.GetDeploymentDAUs(ctx)
}
func (q *querier) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.Group, error) {
return fetchWithPostFilter(q.auth, q.db.GetGroupsByOrganizationID)(ctx, organizationID)
}
@ -622,16 +615,6 @@ func (q *querier) GetPreviousTemplateVersion(ctx context.Context, arg database.G
return q.db.GetPreviousTemplateVersion(ctx, arg)
}
func (q *querier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
// An actor can read the average build time if they can read the related template.
// It doesn't make any sense to get the average build time for a template that doesn't
// exist, so omitting this check here.
if _, err := q.GetTemplateByID(ctx, arg.TemplateID.UUID); 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)
}
@ -640,15 +623,6 @@ func (q *querier) GetTemplateByOrganizationAndName(ctx context.Context, arg data
return fetch(q.log, q.auth, q.db.GetTemplateByOrganizationAndName)(ctx, arg)
}
func (q *querier) GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]database.GetTemplateDAUsRow, error) {
// An actor can read the DAUs if they can read the related template.
// Again, it doesn't make sense to get DAUs for a template that doesn't exist.
if _, err := q.GetTemplateByID(ctx, templateID); err != nil {
return nil, err
}
return q.db.GetTemplateDAUs(ctx, templateID)
}
func (q *querier) GetTemplateVersionByID(ctx context.Context, tvid uuid.UUID) (database.TemplateVersion, error) {
tv, err := q.db.GetTemplateVersionByID(ctx, tvid)
if err != nil {

View File

@ -540,12 +540,6 @@ func (s *MethodTestSuite) TestTemplate() {
TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true},
}).Asserts(t1, rbac.ActionRead).Returns(b)
}))
s.Run("GetTemplateAverageBuildTime", s.Subtest(func(db database.Store, check *expects) {
t1 := dbgen.Template(s.T(), db, database.Template{})
check.Args(database.GetTemplateAverageBuildTimeParams{
TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true},
}).Asserts(t1, rbac.ActionRead)
}))
s.Run("GetTemplateByID", s.Subtest(func(db database.Store, check *expects) {
t1 := dbgen.Template(s.T(), db, database.Template{})
check.Args(t1.ID).Asserts(t1, rbac.ActionRead).Returns(t1)
@ -560,10 +554,6 @@ func (s *MethodTestSuite) TestTemplate() {
OrganizationID: o1.ID,
}).Asserts(t1, rbac.ActionRead).Returns(t1)
}))
s.Run("GetTemplateDAUs", s.Subtest(func(db database.Store, check *expects) {
t1 := dbgen.Template(s.T(), db, database.Template{})
check.Args(t1.ID).Asserts(t1, rbac.ActionRead)
}))
s.Run("GetTemplateVersionByJobID", s.Subtest(func(db database.Store, check *expects) {
t1 := dbgen.Template(s.T(), db, database.Template{})
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
@ -1220,7 +1210,4 @@ func (s *MethodTestSuite) TestExtraMethods() {
s.NoError(err, "insert provisioner daemon")
check.Args().Asserts(d, rbac.ActionRead)
}))
s.Run("GetDeploymentDAUs", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts(rbac.ResourceUser.All(), rbac.ActionRead)
}))
}

View File

@ -226,8 +226,8 @@ func (s *MethodTestSuite) NotAuthorizedErrorTest(ctx context.Context, az *coderd
}
})
s.Run("Cancelled", func() {
// Pass in a cancelled context
s.Run("Canceled", func() {
// Pass in a canceled context
ctx, cancel := context.WithCancel(ctx)
cancel()
az.AlwaysReturn = rbac.ForbiddenWithInternal(&topdown.Error{Code: topdown.CancelErr},

View File

@ -96,6 +96,21 @@ func (q *querier) GetTemplates(ctx context.Context) ([]database.Template, error)
return q.db.GetTemplates(ctx)
}
// Only used by metrics cache.
func (q *querier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
return q.db.GetTemplateAverageBuildTime(ctx, arg)
}
// Only used by metrics cache.
func (q *querier) GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]database.GetTemplateDAUsRow, error) {
return q.db.GetTemplateDAUs(ctx, templateID)
}
// Only used by metrics cache.
func (q *querier) GetDeploymentDAUs(ctx context.Context) ([]database.GetDeploymentDAUsRow, error) {
return q.db.GetDeploymentDAUs(ctx)
}
// 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) (database.WorkspaceBuild, error) {
return q.db.UpdateWorkspaceBuildCostByID(ctx, arg)

View File

@ -161,7 +161,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
}
//nolint:gocritic // System needs to fetch API key to check if it's valid.
key, err := cfg.DB.GetAPIKeyByID(dbauthz.AsSystem(ctx), keyID)
key, err := cfg.DB.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), keyID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
@ -195,7 +195,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
)
if key.LoginType == database.LoginTypeGithub || key.LoginType == database.LoginTypeOIDC {
//nolint:gocritic // System needs to fetch UserLink to check if it's valid.
link, err = cfg.DB.GetUserLinkByUserIDLoginType(dbauthz.AsSystem(ctx), database.GetUserLinkByUserIDLoginTypeParams{
link, err = cfg.DB.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{
UserID: key.UserID,
LoginType: key.LoginType,
})
@ -279,7 +279,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
}
if changed {
//nolint:gocritic // System needs to update API Key LastUsed
err := cfg.DB.UpdateAPIKeyByID(dbauthz.AsSystem(ctx), database.UpdateAPIKeyByIDParams{
err := cfg.DB.UpdateAPIKeyByID(dbauthz.AsSystemRestricted(ctx), database.UpdateAPIKeyByIDParams{
ID: key.ID,
LastUsed: key.LastUsed,
ExpiresAt: key.ExpiresAt,
@ -296,7 +296,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
// then we want to update the relevant oauth fields.
if link.UserID != uuid.Nil {
// nolint:gocritic
link, err = cfg.DB.UpdateUserLink(dbauthz.AsSystem(ctx), database.UpdateUserLinkParams{
link, err = cfg.DB.UpdateUserLink(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLinkParams{
UserID: link.UserID,
LoginType: link.LoginType,
OAuthAccessToken: link.OAuthAccessToken,
@ -316,7 +316,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
// load. We update alongside the UserLink and APIKey since it's
// easier on the DB to colocate writes.
// nolint:gocritic
_, err = cfg.DB.UpdateUserLastSeenAt(dbauthz.AsSystem(ctx), database.UpdateUserLastSeenAtParams{
_, err = cfg.DB.UpdateUserLastSeenAt(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLastSeenAtParams{
ID: key.UserID,
LastSeenAt: database.Now(),
UpdatedAt: database.Now(),
@ -334,7 +334,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
// The roles are used for RBAC authorize checks, and the status
// is to block 'suspended' users from accessing the platform.
// nolint:gocritic
roles, err := cfg.DB.GetAuthorizationUserRoles(dbauthz.AsSystem(ctx), key.UserID)
roles, err := cfg.DB.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), key.UserID)
if err != nil {
write(http.StatusUnauthorized, codersdk.Response{
Message: internalErrorMessage,

View File

@ -28,7 +28,7 @@ func AsAuthzSystem(mws ...func(http.Handler) http.Handler) func(http.Handler) ht
}
// nolint:gocritic // AsAuthzSystem needs to do this.
r = r.WithContext(dbauthz.AsSystem(ctx))
r = r.WithContext(dbauthz.AsSystemRestricted(ctx))
chain.Handler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
r = r.WithContext(dbauthz.As(r.Context(), before))
next.ServeHTTP(rw, r)

View File

@ -96,7 +96,7 @@ func TestHSTS(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
res := httptest.NewRecorder()
got.ServeHTTP(res, req)
require.Equal(t, tt.expectHeader, res.Header().Get("Strict-Transport-Security"), "expected header value")
})
}

View File

@ -70,7 +70,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han
return
}
//nolint:gocritic // System needs to be able to get user from param.
user, err = db.GetUserByID(dbauthz.AsSystem(ctx), apiKey.UserID)
user, err = db.GetUserByID(dbauthz.AsSystemRestricted(ctx), apiKey.UserID)
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(rw)
return
@ -84,7 +84,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han
}
} else if userID, err := uuid.Parse(userQuery); err == nil {
//nolint:gocritic // If the userQuery is a valid uuid
user, err = db.GetUserByID(dbauthz.AsSystem(ctx), userID)
user, err = db.GetUserByID(dbauthz.AsSystemRestricted(ctx), userID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: userErrorMessage,
@ -93,7 +93,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han
}
} else {
// nolint:gocritic // Try as a username last
user, err = db.GetUserByEmailOrUsername(dbauthz.AsSystem(ctx), database.GetUserByEmailOrUsernameParams{
user, err = db.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
Username: userQuery,
})
if err != nil {

View File

@ -48,7 +48,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
return
}
//nolint:gocritic // System needs to be able to get workspace agents.
agent, err := db.GetWorkspaceAgentByAuthToken(dbauthz.AsSystem(ctx), token)
agent, err := db.GetWorkspaceAgentByAuthToken(dbauthz.AsSystemRestricted(ctx), token)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
@ -66,7 +66,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
}
//nolint:gocritic // System needs to be able to get workspace agents.
subject, err := getAgentSubject(dbauthz.AsSystem(ctx), db, agent)
subject, err := getAgentSubject(dbauthz.AsSystemRestricted(ctx), db, agent)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent.",

View File

@ -144,7 +144,7 @@ func countUniqueUsers(rows []database.GetTemplateDAUsRow) int {
func (c *Cache) refresh(ctx context.Context) error {
//nolint:gocritic // This is a system service.
ctx = dbauthz.AsSystem(ctx)
ctx = dbauthz.AsSystemRestricted(ctx)
err := c.database.DeleteOldAgentStats(ctx)
if err != nil {
return xerrors.Errorf("delete old stats: %w", err)

View File

@ -57,8 +57,8 @@ type Server struct {
// AcquireJob queries the database to lock a job.
func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) {
//nolint:gocritic //TODO: make a provisionerd role
ctx = dbauthz.AsSystem(ctx)
//nolint:gocritic // Provisionerd has specific authz rules.
ctx = dbauthz.AsProvisionerd(ctx)
// This prevents loads of provisioner daemons from consistently
// querying the database when no jobs are available.
//
@ -273,8 +273,8 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
}
func (server *Server) CommitQuota(ctx context.Context, request *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) {
//nolint:gocritic //TODO: make a provisionerd role
ctx = dbauthz.AsSystem(ctx)
//nolint:gocritic // Provisionerd has specific authz rules.
ctx = dbauthz.AsProvisionerd(ctx)
jobID, err := uuid.Parse(request.JobId)
if err != nil {
return nil, xerrors.Errorf("parse job id: %w", err)
@ -304,8 +304,8 @@ func (server *Server) CommitQuota(ctx context.Context, request *proto.CommitQuot
}
func (server *Server) UpdateJob(ctx context.Context, request *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) {
//nolint:gocritic //TODO: make a provisionerd role
ctx = dbauthz.AsSystem(ctx)
//nolint:gocritic // Provisionerd has specific authz rules.
ctx = dbauthz.AsProvisionerd(ctx)
parsedID, err := uuid.Parse(request.JobId)
if err != nil {
return nil, xerrors.Errorf("parse job id: %w", err)
@ -352,8 +352,8 @@ func (server *Server) UpdateJob(ctx context.Context, request *proto.UpdateJobReq
slog.F("stage", log.Stage),
slog.F("output", log.Output))
}
//nolint:gocritic //TODO: make a provisionerd role
logs, err := server.Database.InsertProvisionerJobLogs(dbauthz.AsSystem(context.Background()), insertParams)
//nolint:gocritic // Provisionerd has specific authz rules.
logs, err := server.Database.InsertProvisionerJobLogs(dbauthz.AsProvisionerd(context.Background()), insertParams)
if err != nil {
server.Logger.Error(ctx, "failed to insert job logs", slog.F("job_id", parsedID), slog.Error(err))
return nil, xerrors.Errorf("insert job logs: %w", err)
@ -478,8 +478,8 @@ func (server *Server) UpdateJob(ctx context.Context, request *proto.UpdateJobReq
}
func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.Empty, error) {
//nolint:gocritic // TODO: make a provisionerd role
ctx = dbauthz.AsSystem(ctx)
//nolint:gocritic // Provisionerd has specific authz rules.
ctx = dbauthz.AsProvisionerd(ctx)
jobID, err := uuid.Parse(failJob.JobId)
if err != nil {
return nil, xerrors.Errorf("parse job id: %w", err)
@ -606,8 +606,8 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
// CompleteJob is triggered by a provision daemon to mark a provisioner job as completed.
func (server *Server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) {
//nolint:gocritic // TODO: make a provisionerd role
ctx = dbauthz.AsSystem(ctx)
//nolint:gocritic // Provisionerd has specific authz rules.
ctx = dbauthz.AsProvisionerd(ctx)
jobID, err := uuid.Parse(completed.JobId)
if err != nil {
return nil, xerrors.Errorf("parse job id: %w", err)

View File

@ -811,7 +811,7 @@ func TestAuthorizeScope(t *testing.T) {
Role: Role{
Name: "workspace_agent",
DisplayName: "Workspace Agent",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
// Only read access for workspaces.
ResourceWorkspace.Type: {ActionRead},
}),
@ -900,7 +900,7 @@ func TestAuthorizeScope(t *testing.T) {
Role: Role{
Name: "create_workspace",
DisplayName: "Create Workspace",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
// Only read access for workspaces.
ResourceWorkspace.Type: {ActionCreate},
}),

View File

@ -77,7 +77,7 @@ var (
return Role{
Name: owner,
DisplayName: "Owner",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
Org: map[string][]Permission{},
@ -90,7 +90,7 @@ var (
return Role{
Name: member,
DisplayName: "",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
// All users can read all other users and know they exist.
ResourceUser.Type: {ActionRead},
ResourceRoleAssignment.Type: {ActionRead},
@ -98,7 +98,7 @@ var (
ResourceProvisionerDaemon.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: permissions(map[string][]Action{
User: Permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
}
@ -111,7 +111,7 @@ var (
return Role{
Name: auditor,
DisplayName: "Auditor",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate.Type: {ActionRead},
@ -126,7 +126,7 @@ var (
return Role{
Name: templateAdmin,
DisplayName: "Template Admin",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// CRUD all files, even those they did not upload.
ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
@ -145,7 +145,7 @@ var (
return Role{
Name: userAdmin,
DisplayName: "User Admin",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Full perms to manage org members
@ -430,9 +430,9 @@ func roleSplit(role string) (name string, orgID string, err error) {
return arr[0], "", nil
}
// permissions is just a helper function to make building roles that list out resources
// Permissions is just a helper function to make building roles that list out resources
// and actions a bit easier.
func permissions(perms map[string][]Action) []Permission {
func Permissions(perms map[string][]Action) []Permission {
list := make([]Permission, 0, len(perms))
for k, actions := range perms {
for _, act := range actions {

View File

@ -79,7 +79,7 @@ var builtinScopes = map[ScopeName]Scope{
Role: Role{
Name: fmt.Sprintf("Scope_%s", ScopeAll),
DisplayName: "All operations",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
Org: map[string][]Permission{},
@ -92,7 +92,7 @@ var builtinScopes = map[ScopeName]Scope{
Role: Role{
Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect),
DisplayName: "Ability to connect to applications",
Site: permissions(map[string][]Action{
Site: Permissions(map[string][]Action{
ResourceWorkspaceApplicationConnect.Type: {ActionCreate},
}),
Org: map[string][]Permission{},

View File

@ -58,7 +58,7 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // In order to login, we need to get the user first!
user, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystem(ctx), database.GetUserByEmailOrUsernameParams{
user, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
Email: loginWithPassword.Email,
})
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
@ -104,7 +104,7 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // System needs to fetch user roles in order to login user.
roles, err := api.Database.GetAuthorizationUserRoles(dbauthz.AsSystem(ctx), user.ID)
roles, err := api.Database.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error.",
@ -775,7 +775,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
if user.ID == uuid.Nil {
var organizationID uuid.UUID
//nolint:gocritic
organizations, _ := tx.GetOrganizations(dbauthz.AsSystem(ctx))
organizations, _ := tx.GetOrganizations(dbauthz.AsSystemRestricted(ctx))
if len(organizations) > 0 {
// Add the user to the first organization. Once multi-organization
// support is added, we should enable a configuration map of user
@ -784,7 +784,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
}
//nolint:gocritic
_, err := tx.GetUserByEmailOrUsername(dbauthz.AsSystem(ctx), database.GetUserByEmailOrUsernameParams{
_, err := tx.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
Username: params.Username,
})
if err == nil {
@ -798,7 +798,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
params.Username = httpapi.UsernameFrom(alternate)
//nolint:gocritic
_, err := tx.GetUserByEmailOrUsername(dbauthz.AsSystem(ctx), database.GetUserByEmailOrUsernameParams{
_, err := tx.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
Username: params.Username,
})
if xerrors.Is(err, sql.ErrNoRows) {
@ -818,7 +818,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
}
//nolint:gocritic
user, _, err = api.CreateUser(dbauthz.AsSystem(ctx), tx, CreateUserRequest{
user, _, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: params.Email,
Username: params.Username,
@ -833,7 +833,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
if link.UserID == uuid.Nil {
//nolint:gocritic
link, err = tx.InsertUserLink(dbauthz.AsSystem(ctx), database.InsertUserLinkParams{
link, err = tx.InsertUserLink(dbauthz.AsSystemRestricted(ctx), database.InsertUserLinkParams{
UserID: user.ID,
LoginType: params.LoginType,
LinkedID: params.LinkedID,
@ -848,7 +848,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
if link.UserID != uuid.Nil {
//nolint:gocritic
link, err = tx.UpdateUserLink(dbauthz.AsSystem(ctx), database.UpdateUserLinkParams{
link, err = tx.UpdateUserLink(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLinkParams{
UserID: user.ID,
LoginType: params.LoginType,
OAuthAccessToken: params.State.Token.AccessToken,
@ -863,7 +863,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
// Ensure groups are correct.
if len(params.Groups) > 0 {
//nolint:gocritic
err := api.Options.SetUserGroups(dbauthz.AsSystem(ctx), tx, user.ID, params.Groups)
err := api.Options.SetUserGroups(dbauthz.AsSystemRestricted(ctx), tx, user.ID, params.Groups)
if err != nil {
return xerrors.Errorf("set user groups: %w", err)
}
@ -897,7 +897,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
// longer sign in until an administrator finds the offending built-in
// user and changes their username.
//nolint:gocritic
user, err = tx.UpdateUserProfile(dbauthz.AsSystem(ctx), database.UpdateUserProfileParams{
user, err = tx.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{
ID: user.ID,
Email: user.Email,
Username: user.Username,
@ -916,7 +916,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
}
//nolint:gocritic
cookie, key, err := api.createAPIKey(dbauthz.AsSystem(ctx), createAPIKeyParams{
cookie, key, err := api.createAPIKey(dbauthz.AsSystemRestricted(ctx), createAPIKeyParams{
UserID: user.ID,
LoginType: params.LoginType,
RemoteAddr: r.RemoteAddr,

View File

@ -38,8 +38,7 @@ import (
// @Router /users/first [get]
func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
//nolint:gocritic // needed for first user check
userCount, err := api.Database.GetUserCount(dbauthz.AsSystem(ctx))
userCount, err := api.Database.GetUserCount(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user count.",
@ -80,8 +79,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
}
// This should only function for the first user.
//nolint:gocritic // needed to create first user
userCount, err := api.Database.GetUserCount(dbauthz.AsSystem(ctx))
userCount, err := api.Database.GetUserCount(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user count.",
@ -122,7 +120,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed to create first user
user, organizationID, err := api.CreateUser(dbauthz.AsSystem(ctx), api.Database, CreateUserRequest{
user, organizationID, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
@ -152,7 +150,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
// and add some rbac bypass when calling api functions this way??
// Add the admin role to this first user.
//nolint:gocritic // needed to create first user
_, err = api.Database.UpdateUserRoles(dbauthz.AsSystem(ctx), database.UpdateUserRolesParams{
_, err = api.Database.UpdateUserRoles(dbauthz.AsSystemRestricted(ctx), database.UpdateUserRolesParams{
GrantedRoles: []string{rbac.RoleOwner()},
ID: user.ID,
})

View File

@ -632,7 +632,7 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request
// Use a system context as the agent has disconnected and that token
// may no longer be valid.
//nolint:gocritic
ctx, cancel := context.WithTimeout(dbauthz.AsSystem(api.ctx), api.AgentInactiveDisconnectTimeout)
ctx, cancel := context.WithTimeout(dbauthz.AsSystemRestricted(api.ctx), api.AgentInactiveDisconnectTimeout)
defer cancel()
disconnectedAt = sql.NullTime{

View File

@ -332,7 +332,7 @@ func (api *API) handleWorkspaceAppLogout(rw http.ResponseWriter, r *http.Request
// unchecked API key, we validate that the secret matches the secret
// we store in the database.
//nolint:gocritic // needed for workspace app logout
apiKey, err := api.Database.GetAPIKeyByID(dbauthz.AsSystem(ctx), id)
apiKey, err := api.Database.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), id)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to lookup API key.",
@ -352,7 +352,7 @@ func (api *API) handleWorkspaceAppLogout(rw http.ResponseWriter, r *http.Request
return
}
//nolint:gocritic // needed for workspace app logout
err = api.Database.DeleteAPIKeyByID(dbauthz.AsSystem(ctx), id)
err = api.Database.DeleteAPIKeyByID(dbauthz.AsSystemRestricted(ctx), id)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to delete API key.",
@ -412,10 +412,10 @@ func (api *API) handleWorkspaceAppLogout(rw http.ResponseWriter, r *http.Request
// error while looking it up, an HTML error page is returned and false is
// returned so the caller can return early.
func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agentID uuid.UUID, appSlug string) (database.WorkspaceApp, bool) {
// dbauthz.AsSystem is allowed here as the app authz is checked later.
// dbauthz.AsSystemRestricted is allowed here as the app authz is checked later.
// The app authz is determined by the sharing level.
//nolint:gocritic
app, err := api.Database.GetWorkspaceAppByAgentIDAndSlug(dbauthz.AsSystem(r.Context()), database.GetWorkspaceAppByAgentIDAndSlugParams{
app, err := api.Database.GetWorkspaceAppByAgentIDAndSlug(dbauthz.AsSystemRestricted(r.Context()), database.GetWorkspaceAppByAgentIDAndSlugParams{
AgentID: agentID,
Slug: appSlug,
})
@ -1026,7 +1026,7 @@ func decryptAPIKey(ctx context.Context, db database.Store, encryptedAPIKey strin
// Lookup the API key so we can decrypt it.
keyID := object.Header.KeyID
//nolint:gocritic // needed to check API key
key, err := db.GetAPIKeyByID(dbauthz.AsSystem(ctx), keyID)
key, err := db.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), keyID)
if err != nil {
return database.APIKey{}, "", xerrors.Errorf("get API key by key ID: %w", err)
}

View File

@ -128,7 +128,7 @@ func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter,
func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, instanceID string) {
ctx := r.Context()
//nolint:gocritic // needed for auth instance id
agent, err := api.Database.GetWorkspaceAgentByInstanceID(dbauthz.AsSystem(ctx), instanceID)
agent, err := api.Database.GetWorkspaceAgentByInstanceID(dbauthz.AsSystemRestricted(ctx), instanceID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("Instance with id %q not found.", instanceID),
@ -143,7 +143,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in
return
}
//nolint:gocritic // needed for auth instance id
resource, err := api.Database.GetWorkspaceResourceByID(dbauthz.AsSystem(ctx), agent.ResourceID)
resource, err := api.Database.GetWorkspaceResourceByID(dbauthz.AsSystemRestricted(ctx), agent.ResourceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job resource.",
@ -152,7 +152,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in
return
}
//nolint:gocritic // needed for auth instance id
job, err := api.Database.GetProvisionerJobByID(dbauthz.AsSystem(ctx), resource.JobID)
job, err := api.Database.GetProvisionerJobByID(dbauthz.AsSystemRestricted(ctx), resource.JobID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
@ -176,7 +176,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in
return
}
//nolint:gocritic // needed for auth instance id
resourceHistory, err := api.Database.GetWorkspaceBuildByID(dbauthz.AsSystem(ctx), jobData.WorkspaceBuildID)
resourceHistory, err := api.Database.GetWorkspaceBuildByID(dbauthz.AsSystemRestricted(ctx), jobData.WorkspaceBuildID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace build.",
@ -188,7 +188,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in
// for the latest history. If an instance ID is recycled by a cloud,
// we'd hate to leak access to a user's workspace.
//nolint:gocritic // needed for auth instance id
latestHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(dbauthz.AsSystem(ctx), resourceHistory.WorkspaceID)
latestHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(dbauthz.AsSystemRestricted(ctx), resourceHistory.WorkspaceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching the latest workspace build.",

View File

@ -6,7 +6,10 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/rbac"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -103,7 +106,7 @@ func TestEntitlements(t *testing.T) {
require.False(t, entitlements.HasLicense)
coderdtest.CreateFirstUser(t, client)
//nolint:gocritic // unit test
ctx := dbauthz.AsSystem(context.Background())
ctx := testDBAuthzRole(context.Background())
_, err = api.Database.InsertLicense(ctx, database.InsertLicenseParams{
UploadedAt: database.Now(),
Exp: database.Now().AddDate(1, 0, 0),
@ -134,7 +137,7 @@ func TestEntitlements(t *testing.T) {
// Valid
ctx := context.Background()
//nolint:gocritic // unit test
_, err = api.Database.InsertLicense(dbauthz.AsSystem(ctx), database.InsertLicenseParams{
_, err = api.Database.InsertLicense(testDBAuthzRole(ctx), database.InsertLicenseParams{
UploadedAt: database.Now(),
Exp: database.Now().AddDate(1, 0, 0),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
@ -146,7 +149,7 @@ func TestEntitlements(t *testing.T) {
require.NoError(t, err)
// Expired
//nolint:gocritic // unit test
_, err = api.Database.InsertLicense(dbauthz.AsSystem(ctx), database.InsertLicenseParams{
_, err = api.Database.InsertLicense(testDBAuthzRole(ctx), database.InsertLicenseParams{
UploadedAt: database.Now(),
Exp: database.Now().AddDate(-1, 0, 0),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
@ -156,7 +159,7 @@ func TestEntitlements(t *testing.T) {
require.NoError(t, err)
// Invalid
//nolint:gocritic // unit test
_, err = api.Database.InsertLicense(dbauthz.AsSystem(ctx), database.InsertLicenseParams{
_, err = api.Database.InsertLicense(testDBAuthzRole(ctx), database.InsertLicenseParams{
UploadedAt: database.Now(),
Exp: database.Now().AddDate(1, 0, 0),
JWT: "invalid",
@ -201,3 +204,23 @@ func TestAuditLogging(t *testing.T) {
assert.Equal(t, reflect.ValueOf(ea).Type(), reflect.ValueOf(auditor).Type())
})
}
// testDBAuthzRole returns a context with a subject that has a role
// with permissions required for test setup.
func testDBAuthzRole(ctx context.Context) context.Context {
return dbauthz.As(ctx, rbac.Subject{
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Name: "testing",
DisplayName: "Unit Tests",
Site: rbac.Permissions(map[string][]rbac.Action{
rbac.ResourceWildcard.Type: {rbac.WildcardSymbol},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
})
}

View File

@ -157,7 +157,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed for SCIM
user, _, err := api.AGPL.CreateUser(dbauthz.AsSystem(ctx), api.Database, agpl.CreateUserRequest{
user, _, err := api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Username: sUser.UserName,
Email: email,
@ -210,7 +210,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed for SCIM
dbUser, err := api.Database.GetUserByID(dbauthz.AsSystem(ctx), uid)
dbUser, err := api.Database.GetUserByID(dbauthz.AsSystemRestricted(ctx), uid)
if err != nil {
_ = handlerutil.WriteError(rw, err)
return
@ -224,7 +224,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed for SCIM
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystem(r.Context()), database.UpdateUserStatusParams{
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
ID: dbUser.ID,
Status: status,
UpdatedAt: database.Now(),

View File

@ -24,7 +24,6 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/provisionerd/proto"
sdkproto "github.com/coder/coder/provisionersdk/proto"
@ -887,8 +886,7 @@ func (r *Runner) commitQuota(ctx context.Context, resources []*sdkproto.Resource
const stage = "Commit quota"
//nolint:gocritic // TODO: make a provisionerd role
resp, err := r.quotaCommitter.CommitQuota(dbauthz.AsSystem(ctx), &proto.CommitQuotaRequest{
resp, err := r.quotaCommitter.CommitQuota(ctx, &proto.CommitQuotaRequest{
JobId: r.job.JobId,
DailyCost: int32(cost),
})