Skip to content

Commit 91ec0f1

Browse files
geokatEmyrkaslilac
authored
feat: add service_accounts workspace sharing mode (coder#23093)
Introduce a three-way workspace sharing setting (none, everyone, service_accounts) replacing the boolean workspace_sharing_disabled. In service_accounts mode, only service account-owned workspaces can be shared while regular members' share permissions are removed. Adds a new organization-service-account system role with per-org permissions reconciled alongside the existing organization-member system role. Related to: https://linear.app/codercom/issue/PLAT-28/feat-service-accounts-sharing-mode-and-rbac-role --------- Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com> Co-authored-by: Kayla はな <mckayla@hey.com>
1 parent 6b76e30 commit 91ec0f1

38 files changed

Lines changed: 1438 additions & 422 deletions

cli/organizationroles.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (r *RootCmd) createOrganizationRole(orgContext *OrganizationContext) *serpe
214214
} else {
215215
updated, err = client.CreateOrganizationRole(ctx, customRole)
216216
if err != nil {
217-
return xerrors.Errorf("patch role: %w", err)
217+
return xerrors.Errorf("create role: %w", err)
218218
}
219219
}
220220

coderd/apidoc/docs.go

Lines changed: 43 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 31 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderdtest/coderdtest.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,15 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI
879879
m(&req)
880880
}
881881

882+
// Service accounts cannot have a password or email and must
883+
// use login_type=none. Enforce this after mutators so callers
884+
// only need to set ServiceAccount=true.
885+
if req.ServiceAccount {
886+
req.Password = ""
887+
req.Email = ""
888+
req.UserLoginType = codersdk.LoginTypeNone
889+
}
890+
882891
user, err := client.CreateUserWithOrgs(context.Background(), req)
883892
var apiError *codersdk.Error
884893
// If the user already exists by username or email conflict, try again up to "retries" times.

coderd/database/dbauthz/dbauthz.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID uuid.UUID, added, re
12641264
// System roles are stored in the database but have a fixed, code-defined
12651265
// meaning. Do not rewrite the name for them so the static "who can assign
12661266
// what" mapping applies.
1267-
if !rbac.SystemRoleName(roleName.Name) {
1267+
if !rolestore.IsSystemRoleName(roleName.Name) {
12681268
// To support a dynamic mapping of what roles can assign what, we need
12691269
// to store this in the database. For now, just use a static role so
12701270
// owners and org admins can assign roles.
@@ -2145,12 +2145,12 @@ func (q *querier) DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) erro
21452145
return fetchAndExec(q.log, q.auth, policy.ActionShare, fetch, q.db.DeleteWorkspaceACLByID)(ctx, id)
21462146
}
21472147

2148-
func (q *querier) DeleteWorkspaceACLsByOrganization(ctx context.Context, organizationID uuid.UUID) error {
2148+
func (q *querier) DeleteWorkspaceACLsByOrganization(ctx context.Context, params database.DeleteWorkspaceACLsByOrganizationParams) error {
21492149
// This is a system-only function.
21502150
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
21512151
return err
21522152
}
2153-
return q.db.DeleteWorkspaceACLsByOrganization(ctx, organizationID)
2153+
return q.db.DeleteWorkspaceACLsByOrganization(ctx, params)
21542154
}
21552155

21562156
func (q *querier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,7 +1485,7 @@ func (s *MethodTestSuite) TestOrganization() {
14851485
org := testutil.Fake(s.T(), faker, database.Organization{})
14861486
arg := database.UpdateOrganizationWorkspaceSharingSettingsParams{
14871487
ID: org.ID,
1488-
WorkspaceSharingDisabled: true,
1488+
ShareableWorkspaceOwners: database.ShareableWorkspaceOwnersNone,
14891489
}
14901490
dbm.EXPECT().GetOrganizationByID(gomock.Any(), org.ID).Return(org, nil).AnyTimes()
14911491
dbm.EXPECT().UpdateOrganizationWorkspaceSharingSettings(gomock.Any(), arg).Return(org, nil).AnyTimes()
@@ -2404,9 +2404,12 @@ func (s *MethodTestSuite) TestWorkspace() {
24042404
check.Args(w.ID).Asserts(w, policy.ActionShare)
24052405
}))
24062406
s.Run("DeleteWorkspaceACLsByOrganization", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
2407-
orgID := uuid.New()
2408-
dbm.EXPECT().DeleteWorkspaceACLsByOrganization(gomock.Any(), orgID).Return(nil).AnyTimes()
2409-
check.Args(orgID).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
2407+
arg := database.DeleteWorkspaceACLsByOrganizationParams{
2408+
OrganizationID: uuid.New(),
2409+
ExcludeServiceAccounts: false,
2410+
}
2411+
dbm.EXPECT().DeleteWorkspaceACLsByOrganization(gomock.Any(), arg).Return(nil).AnyTimes()
2412+
check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
24102413
}))
24112414
s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
24122415
w := testutil.Fake(s.T(), faker, database.Workspace{})

coderd/database/dbauthz/setup_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/coder/coder/v2/coderd/rbac"
3030
"github.com/coder/coder/v2/coderd/rbac/policy"
3131
"github.com/coder/coder/v2/coderd/rbac/regosql"
32+
"github.com/coder/coder/v2/coderd/rbac/rolestore"
3233
"github.com/coder/coder/v2/coderd/util/slice"
3334
)
3435

@@ -143,7 +144,7 @@ func (s *MethodTestSuite) Mocked(testCaseF func(dmb *dbmock.MockStore, faker *go
143144
UUID: pair.OrganizationID,
144145
Valid: pair.OrganizationID != uuid.Nil,
145146
},
146-
IsSystem: rbac.SystemRoleName(pair.Name),
147+
IsSystem: rolestore.IsSystemRoleName(pair.Name),
147148
ID: uuid.New(),
148149
})
149150
}

coderd/database/dbgen/dbgen.go

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -650,34 +650,26 @@ func Organization(t testing.TB, db database.Store, orig database.Organization) d
650650
})
651651
require.NoError(t, err, "insert organization")
652652

653-
// Populate the placeholder organization-member system role (created by
654-
// DB trigger/migration) so org members have expected permissions.
655-
//nolint:gocritic // ReconcileOrgMemberRole needs the system:update
653+
// Populate the placeholder system roles (created by DB
654+
// trigger/migration) so org members have expected permissions.
655+
//nolint:gocritic // ReconcileSystemRole needs the system:update
656656
// permission that `genCtx` does not have.
657657
sysCtx := dbauthz.AsSystemRestricted(genCtx)
658-
_, _, err = rolestore.ReconcileOrgMemberRole(sysCtx, db, database.CustomRole{
659-
Name: rbac.RoleOrgMember(),
660-
OrganizationID: uuid.NullUUID{
661-
UUID: org.ID,
662-
Valid: true,
663-
},
664-
}, org.WorkspaceSharingDisabled)
665-
666-
if errors.Is(err, sql.ErrNoRows) {
667-
// The trigger that creates the placeholder role didn't run (e.g.,
668-
// triggers were disabled in the test). Create the role manually.
669-
err = rolestore.CreateOrgMemberRole(sysCtx, db, org)
670-
require.NoError(t, err, "create organization-member role")
671-
672-
_, _, err = rolestore.ReconcileOrgMemberRole(sysCtx, db, database.CustomRole{
673-
Name: rbac.RoleOrgMember(),
674-
OrganizationID: uuid.NullUUID{
675-
UUID: org.ID,
676-
Valid: true,
677-
},
678-
}, org.WorkspaceSharingDisabled)
658+
for roleName := range rolestore.SystemRoleNames {
659+
role := database.CustomRole{
660+
Name: roleName,
661+
OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true},
662+
}
663+
_, _, err = rolestore.ReconcileSystemRole(sysCtx, db, role, org)
664+
if errors.Is(err, sql.ErrNoRows) {
665+
// The trigger that creates the placeholder role didn't run (e.g.,
666+
// triggers were disabled in the test). Create the role manually.
667+
err = rolestore.CreateSystemRole(sysCtx, db, org, roleName)
668+
require.NoError(t, err, "create role "+roleName)
669+
_, _, err = rolestore.ReconcileSystemRole(sysCtx, db, role, org)
670+
}
671+
require.NoError(t, err, "reconcile role "+roleName)
679672
}
680-
require.NoError(t, err, "reconcile organization-member role")
681673

682674
return org
683675
}

coderd/database/dbmetrics/querymetrics.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)