mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): gate org-member workspace elevation behind experiment (#26027)
Gates the workspace-ops elevation on `organization-member` and `organization-service-account` behind the `minimum-implicit-member` experiment.
This commit is contained in:
+9
-7
@@ -348,11 +348,16 @@ func New(options *Options) *API {
|
||||
panic("developer error: options.PrometheusRegistry is nil and not running a unit test")
|
||||
}
|
||||
|
||||
if options.DeploymentValues.DisableOwnerWorkspaceExec || options.DeploymentValues.DisableWorkspaceSharing || options.DeploymentValues.DisableChatSharing {
|
||||
experiments := ReadExperiments(
|
||||
options.Logger, options.DeploymentValues.Experiments.Value(),
|
||||
)
|
||||
|
||||
if bool(options.DeploymentValues.DisableOwnerWorkspaceExec) || bool(options.DeploymentValues.DisableWorkspaceSharing) || bool(options.DeploymentValues.DisableChatSharing) || experiments.Enabled(codersdk.ExperimentMinimumImplicitMember) {
|
||||
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
|
||||
NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
|
||||
NoWorkspaceSharing: bool(options.DeploymentValues.DisableWorkspaceSharing),
|
||||
NoChatSharing: bool(options.DeploymentValues.DisableChatSharing),
|
||||
NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
|
||||
NoWorkspaceSharing: bool(options.DeploymentValues.DisableWorkspaceSharing),
|
||||
NoChatSharing: bool(options.DeploymentValues.DisableChatSharing),
|
||||
MinimumImplicitMember: experiments.Enabled(codersdk.ExperimentMinimumImplicitMember),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -391,9 +396,6 @@ func New(options *Options) *API {
|
||||
options.IDPSync = idpsync.NewAGPLSync(options.Logger, options.RuntimeConfig, idpsync.FromDeploymentValues(options.DeploymentValues))
|
||||
}
|
||||
|
||||
experiments := ReadExperiments(
|
||||
options.Logger, options.DeploymentValues.Experiments.Value(),
|
||||
)
|
||||
if options.AppHostname != "" && options.AppHostnameRegex == nil || options.AppHostname == "" && options.AppHostnameRegex != nil {
|
||||
panic("coderd: both AppHostname and AppHostnameRegex must be set or unset")
|
||||
}
|
||||
|
||||
@@ -267,3 +267,16 @@ func SetChatACLDisabled(v bool) {
|
||||
func ChatACLDisabled() bool {
|
||||
return chatACLDisabled.Load()
|
||||
}
|
||||
|
||||
// minimumImplicitMember mirrors RoleOptions.MinimumImplicitMember.
|
||||
// Stored as a global because OrgMemberPermissions and
|
||||
// OrgServiceAccountPermissions are called from rolestore without
|
||||
// access to api instance state.
|
||||
var minimumImplicitMember atomic.Bool
|
||||
|
||||
// MinimumImplicitMember reports whether the workspace-ops elevation
|
||||
// has been stripped from organization-member and
|
||||
// organization-service-account. See RoleOptions.MinimumImplicitMember.
|
||||
func MinimumImplicitMember() bool {
|
||||
return minimumImplicitMember.Load()
|
||||
}
|
||||
|
||||
+24
-7
@@ -320,6 +320,14 @@ type RoleOptions struct {
|
||||
NoOwnerWorkspaceExec bool
|
||||
NoWorkspaceSharing bool
|
||||
NoChatSharing bool
|
||||
|
||||
// MinimumImplicitMember removes the workspace-ops elevation
|
||||
// (OrgWorkspaceAccessMemberPerms) from organization-member and
|
||||
// organization-service-account. With it set, those two roles carry
|
||||
// only the floor, and the elevation must be granted explicitly via
|
||||
// the organization-workspace-access role (typically attached
|
||||
// through default_org_member_roles).
|
||||
MinimumImplicitMember bool
|
||||
}
|
||||
|
||||
// ReservedRoleName exists because the database should only allow unique role
|
||||
@@ -341,6 +349,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
opts = &RoleOptions{}
|
||||
}
|
||||
|
||||
minimumImplicitMember.Store(opts.MinimumImplicitMember)
|
||||
|
||||
denyPermissions := []Permission{}
|
||||
if opts.NoWorkspaceSharing {
|
||||
denyPermissions = append(denyPermissions, Permission{
|
||||
@@ -1171,12 +1181,16 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
|
||||
ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(),
|
||||
})
|
||||
|
||||
// Workspace-ops elevation. Today bundled into organization-member;
|
||||
// the minimum-implicit-member experiment will move the binding
|
||||
// exclusively onto organization-workspace-access so a user without
|
||||
// that role has only the floor. See OrgWorkspaceAccessMemberPerms
|
||||
// for the perm set and the "Intentionally omitted" rationale.
|
||||
elevation := OrgWorkspaceAccessMemberPerms()
|
||||
// Workspace-ops elevation. When MinimumImplicitMember is off, the
|
||||
// elevation is bundled into organization-member here. When on, the
|
||||
// elevation lives exclusively on organization-workspace-access; a
|
||||
// user without that role then has only the floor. See
|
||||
// OrgWorkspaceAccessMemberPerms for the perm set and the
|
||||
// "Intentionally omitted" rationale.
|
||||
var elevation []Permission
|
||||
if !MinimumImplicitMember() {
|
||||
elevation = OrgWorkspaceAccessMemberPerms()
|
||||
}
|
||||
|
||||
memberPerms := slices.Concat(elevation, floor)
|
||||
|
||||
@@ -1249,7 +1263,10 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
|
||||
ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(),
|
||||
})
|
||||
|
||||
elevation := OrgWorkspaceAccessMemberPerms()
|
||||
var elevation []Permission
|
||||
if !MinimumImplicitMember() {
|
||||
elevation = OrgWorkspaceAccessMemberPerms()
|
||||
}
|
||||
|
||||
memberPerms := slices.Concat(elevation, floor)
|
||||
|
||||
|
||||
@@ -203,6 +203,62 @@ func TestOwnerExec(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestMinimumImplicitMember verifies the floor/elevation gate on
|
||||
// organization-member and organization-service-account. When the option
|
||||
// is off (default), both roles carry the workspace-ops elevation. When
|
||||
// on, both roles carry only the floor and the elevation must be
|
||||
// granted explicitly via organization-workspace-access.
|
||||
//
|
||||
//nolint:tparallel,paralleltest
|
||||
func TestMinimumImplicitMember(t *testing.T) {
|
||||
orgSettings := rbac.OrgSettings{
|
||||
ShareableWorkspaceOwners: rbac.ShareableWorkspaceOwnersEveryone,
|
||||
}
|
||||
|
||||
hasResource := func(perms []rbac.Permission, resource string) bool {
|
||||
for _, p := range perms {
|
||||
if p.ResourceType == resource && !p.Negate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResourceWorkspace is granted by the elevation
|
||||
// (OrgWorkspaceAccessMemberPerms) and not by the floor, so it acts as
|
||||
// a witness for whether the elevation is bundled in.
|
||||
elevationWitness := rbac.ResourceWorkspace.Type
|
||||
// ResourceOrganizationMember is part of the floor; floor must remain
|
||||
// regardless of the option.
|
||||
floorWitness := rbac.ResourceOrganizationMember.Type
|
||||
|
||||
t.Run("Off", func(t *testing.T) {
|
||||
rbac.ReloadBuiltinRoles(nil)
|
||||
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
|
||||
|
||||
member := rbac.OrgMemberPermissions(orgSettings).Member
|
||||
require.True(t, hasResource(member, elevationWitness), "organization-member should include the elevation when MinimumImplicitMember is off")
|
||||
require.True(t, hasResource(member, floorWitness), "organization-member should include the floor")
|
||||
|
||||
sa := rbac.OrgServiceAccountPermissions(orgSettings).Member
|
||||
require.True(t, hasResource(sa, elevationWitness), "organization-service-account should include the elevation when MinimumImplicitMember is off")
|
||||
require.True(t, hasResource(sa, floorWitness), "organization-service-account should include the floor")
|
||||
})
|
||||
|
||||
t.Run("On", func(t *testing.T) {
|
||||
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{MinimumImplicitMember: true})
|
||||
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
|
||||
|
||||
member := rbac.OrgMemberPermissions(orgSettings).Member
|
||||
require.False(t, hasResource(member, elevationWitness), "organization-member should drop the elevation when MinimumImplicitMember is on")
|
||||
require.True(t, hasResource(member, floorWitness), "organization-member should still include the floor")
|
||||
|
||||
sa := rbac.OrgServiceAccountPermissions(orgSettings).Member
|
||||
require.False(t, hasResource(sa, elevationWitness), "organization-service-account should drop the elevation when MinimumImplicitMember is on")
|
||||
require.True(t, hasResource(sa, floorWitness), "organization-service-account should still include the floor")
|
||||
})
|
||||
}
|
||||
|
||||
// These were "pared down" in https://github.com/coder/coder/pull/21359 to avoid
|
||||
// using the now DB-backed organization-member role. As a result, they no longer
|
||||
// model real-world org-scoped users (who also have organization-member).
|
||||
|
||||
Reference in New Issue
Block a user