Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit4633658

Browse files
gcp-cherry-pick-bot[bot]Emyrkjaaydenhaslilac
authored
feat: implement WorkspaceCreationBan org role (cherry-pick#16686) (#16786)
Cherry-picked feat: implement WorkspaceCreationBan org role (#16686)Using negative permissions, this role prevents a user's ability tocreate & delete a workspace within a given organization.Workspaces are uniquely owned by an org and a user, so the org has tosupercede the user permission with a negative permission.# Use caseOrganizations must be able to restrict a member's ability to create aworkspace. This permission is implicitly granted (see#16546 (comment)).To revoke this permission, the solution chosen was to use negativepermissions in a built in role called `WorkspaceCreationBan`.# RationalUsing negative permissions is new territory, and not ideal. However,workspaces are in a unique position.Workspaces have 2 owners. The organization and the user. To preventusers from creating a workspace in another organization, an [impliednegativepermission](https://github.com/coder/coder/blob/36d9f5ddb3d98029fee07d004709e1e51022e979/coderd/rbac/policy.rego#L172-L192)is used. So the truth table looks like: _how to read this table[here](https://github.com/coder/coder/blob/36d9f5ddb3d98029fee07d004709e1e51022e979/coderd/rbac/README.md#roles)_| Role (example) | Site | Org | User | Result ||-----------------|------|------|------|--------|| non-org-member | \_ | N | YN\_ | N || user | \_ | \_ | Y | Y || WorkspaceBan | \_ | N | Y | Y || unauthenticated | \_ | \_ | \_ | N |This new role, `WorkspaceCreationBan` is the same truth table conditionas if the user was not a member of the organization (when doing aworkspace create/delete). So this behavior **is not entirely new**.<details><summary>How to do it without a negative permission</summary>The alternate approach would be to remove the implied permission, andgrant it via and organization role. However this would add new behaviorthat an organizational role has the ability to grant a user permissionson their own resources?It does not make sense for an org role to prevent user from changingtheir profile information for example. So the only option is to create anew truth table column for resources that are owned by both anorganization and a user.| Role (example) | Site | Org |User+Org| User | Result ||-----------------|------|------|--------|------|--------|| non-org-member | \_ | N | \_ | \_ | N || user | \_ | \_ | \_ | \_ | N || WorkspaceAllow | \_ | \_ | Y | \_ | Y || unauthenticated | \_ | \_ | \_ | \_ | N |Now a user has no opinion on if they can create a workspace, which feelsa little wrong. A user should have the authority over what is theres.There is fundamental _philosophical_ question of "Who does a workspacebelong to?". The user has some set of autonomy, yet it is theorganization that controls it's existence. A head scratcher 🤔</details>## Will we need more negative built in roles?There are few resources that have shared ownership. Only`ResourceOrganizationMember` and `ResourceGroupMember`. Since negativepermissions is intended to revoke access to a shared resource, then**no.** **This is the only one we need**.Classic resources like `ResourceTemplate` are entirely controlled by theOrganization permissions. And resources entirely in the user control(like user profile) are only controlled by `User` permissions.![Uploading Screenshot 2025-02-26 at 22.26.52.png…]()---------Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>Co-authored-by: ケイラ <mckayla@hey.com>Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>Co-authored-by: ケイラ <mckayla@hey.com>
1 parent6da3c9d commit4633658

File tree

11 files changed

+261
-65
lines changed

11 files changed

+261
-65
lines changed

‎coderd/httpapi/httpapi.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,13 @@ func ResourceNotFound(rw http.ResponseWriter) {
151151
Write(context.Background(),rw,http.StatusNotFound,ResourceNotFoundResponse)
152152
}
153153

154+
varResourceForbiddenResponse= codersdk.Response{
155+
Message:"Forbidden.",
156+
Detail:"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials.",
157+
}
158+
154159
funcForbidden(rw http.ResponseWriter) {
155-
Write(context.Background(),rw,http.StatusForbidden, codersdk.Response{
156-
Message:"Forbidden.",
157-
Detail:"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials.",
158-
})
160+
Write(context.Background(),rw,http.StatusForbidden,ResourceForbiddenResponse)
159161
}
160162

161163
funcInternalServerError(rw http.ResponseWriter,errerror) {

‎coderd/rbac/roles.go

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ const (
2727
customSiteRolestring="custom-site-role"
2828
customOrganizationRolestring="custom-organization-role"
2929

30-
orgAdminstring="organization-admin"
31-
orgMemberstring="organization-member"
32-
orgAuditorstring="organization-auditor"
33-
orgUserAdminstring="organization-user-admin"
34-
orgTemplateAdminstring="organization-template-admin"
30+
orgAdminstring="organization-admin"
31+
orgMemberstring="organization-member"
32+
orgAuditorstring="organization-auditor"
33+
orgUserAdminstring="organization-user-admin"
34+
orgTemplateAdminstring="organization-template-admin"
35+
orgWorkspaceCreationBanstring="organization-workspace-creation-ban"
3536
)
3637

3738
funcinit() {
@@ -159,6 +160,10 @@ func RoleOrgTemplateAdmin() string {
159160
returnorgTemplateAdmin
160161
}
161162

163+
funcRoleOrgWorkspaceCreationBan()string {
164+
returnorgWorkspaceCreationBan
165+
}
166+
162167
// ScopedRoleOrgAdmin is the org role with the organization ID
163168
funcScopedRoleOrgAdmin(organizationID uuid.UUID)RoleIdentifier {
164169
returnRoleIdentifier{Name:RoleOrgAdmin(),OrganizationID:organizationID}
@@ -181,6 +186,10 @@ func ScopedRoleOrgTemplateAdmin(organizationID uuid.UUID) RoleIdentifier {
181186
returnRoleIdentifier{Name:RoleOrgTemplateAdmin(),OrganizationID:organizationID}
182187
}
183188

189+
funcScopedRoleOrgWorkspaceCreationBan(organizationID uuid.UUID)RoleIdentifier {
190+
returnRoleIdentifier{Name:RoleOrgWorkspaceCreationBan(),OrganizationID:organizationID}
191+
}
192+
184193
funcallPermsExcept(excepts...Objecter) []Permission {
185194
resources:=AllResources()
186195
varperms []Permission
@@ -496,6 +505,31 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
496505
User: []Permission{},
497506
}
498507
},
508+
// orgWorkspaceCreationBan prevents creating & deleting workspaces. This
509+
// overrides any permissions granted by the org or user level. It accomplishes
510+
// this by using negative permissions.
511+
orgWorkspaceCreationBan:func(organizationID uuid.UUID)Role {
512+
returnRole{
513+
Identifier:RoleIdentifier{Name:orgWorkspaceCreationBan,OrganizationID:organizationID},
514+
DisplayName:"Organization Workspace Creation Ban",
515+
Site: []Permission{},
516+
Org:map[string][]Permission{
517+
organizationID.String(): {
518+
{
519+
Negate:true,
520+
ResourceType:ResourceWorkspace.Type,
521+
Action:policy.ActionCreate,
522+
},
523+
{
524+
Negate:true,
525+
ResourceType:ResourceWorkspace.Type,
526+
Action:policy.ActionDelete,
527+
},
528+
},
529+
},
530+
User: []Permission{},
531+
}
532+
},
499533
}
500534
}
501535

@@ -506,44 +540,47 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
506540
//map[actor_role][assign_role]<can_assign>
507541
varassignRoles=map[string]map[string]bool{
508542
"system": {
509-
owner:true,
510-
auditor:true,
511-
member:true,
512-
orgAdmin:true,
513-
orgMember:true,
514-
orgAuditor:true,
515-
orgUserAdmin:true,
516-
orgTemplateAdmin:true,
517-
templateAdmin:true,
518-
userAdmin:true,
519-
customSiteRole:true,
520-
customOrganizationRole:true,
543+
owner:true,
544+
auditor:true,
545+
member:true,
546+
orgAdmin:true,
547+
orgMember:true,
548+
orgAuditor:true,
549+
orgUserAdmin:true,
550+
orgTemplateAdmin:true,
551+
orgWorkspaceCreationBan:true,
552+
templateAdmin:true,
553+
userAdmin:true,
554+
customSiteRole:true,
555+
customOrganizationRole:true,
521556
},
522557
owner: {
523-
owner:true,
524-
auditor:true,
525-
member:true,
526-
orgAdmin:true,
527-
orgMember:true,
528-
orgAuditor:true,
529-
orgUserAdmin:true,
530-
orgTemplateAdmin:true,
531-
templateAdmin:true,
532-
userAdmin:true,
533-
customSiteRole:true,
534-
customOrganizationRole:true,
558+
owner:true,
559+
auditor:true,
560+
member:true,
561+
orgAdmin:true,
562+
orgMember:true,
563+
orgAuditor:true,
564+
orgUserAdmin:true,
565+
orgTemplateAdmin:true,
566+
orgWorkspaceCreationBan:true,
567+
templateAdmin:true,
568+
userAdmin:true,
569+
customSiteRole:true,
570+
customOrganizationRole:true,
535571
},
536572
userAdmin: {
537573
member:true,
538574
orgMember:true,
539575
},
540576
orgAdmin: {
541-
orgAdmin:true,
542-
orgMember:true,
543-
orgAuditor:true,
544-
orgUserAdmin:true,
545-
orgTemplateAdmin:true,
546-
customOrganizationRole:true,
577+
orgAdmin:true,
578+
orgMember:true,
579+
orgAuditor:true,
580+
orgUserAdmin:true,
581+
orgTemplateAdmin:true,
582+
orgWorkspaceCreationBan:true,
583+
customOrganizationRole:true,
547584
},
548585
orgUserAdmin: {
549586
orgMember:true,

‎coderd/rbac/roles_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func TestRolePermissions(t *testing.T) {
112112
// Subjects to user
113113
memberMe:=authSubject{Name:"member_me",Actor: rbac.Subject{ID:currentUser.String(),Roles: rbac.RoleIdentifiers{rbac.RoleMember()}}}
114114
orgMemberMe:=authSubject{Name:"org_member_me",Actor: rbac.Subject{ID:currentUser.String(),Roles: rbac.RoleIdentifiers{rbac.RoleMember(),rbac.ScopedRoleOrgMember(orgID)}}}
115+
orgMemberMeBanWorkspace:=authSubject{Name:"org_member_me_workspace_ban",Actor: rbac.Subject{ID:currentUser.String(),Roles: rbac.RoleIdentifiers{rbac.RoleMember(),rbac.ScopedRoleOrgMember(orgID),rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}}}
115116
groupMemberMe:=authSubject{Name:"group_member_me",Actor: rbac.Subject{ID:currentUser.String(),Roles: rbac.RoleIdentifiers{rbac.RoleMember(),rbac.ScopedRoleOrgMember(orgID)},Groups: []string{groupID.String()}}}
116117

117118
owner:=authSubject{Name:"owner",Actor: rbac.Subject{ID:adminID.String(),Roles: rbac.RoleIdentifiers{rbac.RoleMember(),rbac.RoleOwner()}}}
@@ -181,20 +182,30 @@ func TestRolePermissions(t *testing.T) {
181182
Actions: []policy.Action{policy.ActionRead},
182183
Resource:rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
183184
AuthorizeMap:map[bool][]hasAuthSubjects{
184-
true: {owner,orgMemberMe,orgAdmin,templateAdmin,orgTemplateAdmin},
185+
true: {owner,orgMemberMe,orgAdmin,templateAdmin,orgTemplateAdmin,orgMemberMeBanWorkspace},
185186
false: {setOtherOrg,memberMe,userAdmin,orgAuditor,orgUserAdmin},
186187
},
187188
},
188189
{
189-
Name:"C_RDMyWorkspaceInOrg",
190+
Name:"UpdateMyWorkspaceInOrg",
190191
// When creating the WithID won't be set, but it does not change the result.
191-
Actions: []policy.Action{policy.ActionCreate,policy.ActionUpdate,policy.ActionDelete},
192+
Actions: []policy.Action{policy.ActionUpdate},
192193
Resource:rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
193194
AuthorizeMap:map[bool][]hasAuthSubjects{
194195
true: {owner,orgMemberMe,orgAdmin},
195196
false: {setOtherOrg,memberMe,userAdmin,templateAdmin,orgTemplateAdmin,orgUserAdmin,orgAuditor},
196197
},
197198
},
199+
{
200+
Name:"CreateDeleteMyWorkspaceInOrg",
201+
// When creating the WithID won't be set, but it does not change the result.
202+
Actions: []policy.Action{policy.ActionCreate,policy.ActionDelete},
203+
Resource:rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
204+
AuthorizeMap:map[bool][]hasAuthSubjects{
205+
true: {owner,orgMemberMe,orgAdmin},
206+
false: {setOtherOrg,memberMe,userAdmin,templateAdmin,orgTemplateAdmin,orgUserAdmin,orgAuditor,orgMemberMeBanWorkspace},
207+
},
208+
},
198209
{
199210
Name:"MyWorkspaceInOrgExecution",
200211
// When creating the WithID won't be set, but it does not change the result.
@@ -942,6 +953,7 @@ func TestListRoles(t *testing.T) {
942953
fmt.Sprintf("organization-auditor:%s",orgID.String()),
943954
fmt.Sprintf("organization-user-admin:%s",orgID.String()),
944955
fmt.Sprintf("organization-template-admin:%s",orgID.String()),
956+
fmt.Sprintf("organization-workspace-creation-ban:%s",orgID.String()),
945957
},
946958
orgRoleNames)
947959
}

‎coderd/workspaces_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,54 @@ func TestWorkspace(t *testing.T) {
375375
require.Error(t,err,"create workspace with archived version")
376376
require.ErrorContains(t,err,"Archived template versions cannot")
377377
})
378+
379+
t.Run("WorkspaceBan",func(t*testing.T) {
380+
t.Parallel()
381+
owner,_,_:=coderdtest.NewWithAPI(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
382+
first:=coderdtest.CreateFirstUser(t,owner)
383+
384+
version:=coderdtest.CreateTemplateVersion(t,owner,first.OrganizationID,nil)
385+
coderdtest.AwaitTemplateVersionJobCompleted(t,owner,version.ID)
386+
template:=coderdtest.CreateTemplate(t,owner,first.OrganizationID,version.ID)
387+
388+
goodClient,_:=coderdtest.CreateAnotherUser(t,owner,first.OrganizationID)
389+
390+
// When a user with workspace-creation-ban
391+
client,user:=coderdtest.CreateAnotherUser(t,owner,first.OrganizationID,rbac.ScopedRoleOrgWorkspaceCreationBan(first.OrganizationID))
392+
393+
// Ensure a similar user can create a workspace
394+
coderdtest.CreateWorkspace(t,goodClient,template.ID)
395+
396+
ctx:=testutil.Context(t,testutil.WaitLong)
397+
// Then: Cannot create a workspace
398+
_,err:=client.CreateUserWorkspace(ctx,codersdk.Me, codersdk.CreateWorkspaceRequest{
399+
TemplateID:template.ID,
400+
TemplateVersionID: uuid.UUID{},
401+
Name:"random",
402+
})
403+
require.Error(t,err)
404+
varapiError*codersdk.Error
405+
require.ErrorAs(t,err,&apiError)
406+
require.Equal(t,http.StatusForbidden,apiError.StatusCode())
407+
408+
// When: workspace-ban use has a workspace
409+
wrk,err:=owner.CreateUserWorkspace(ctx,user.ID.String(), codersdk.CreateWorkspaceRequest{
410+
TemplateID:template.ID,
411+
TemplateVersionID: uuid.UUID{},
412+
Name:"random",
413+
})
414+
require.NoError(t,err)
415+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,wrk.LatestBuild.ID)
416+
417+
// Then: They cannot delete said workspace
418+
_,err=client.CreateWorkspaceBuild(ctx,wrk.ID, codersdk.CreateWorkspaceBuildRequest{
419+
Transition:codersdk.WorkspaceTransitionDelete,
420+
ProvisionerState: []byte{},
421+
})
422+
require.Error(t,err)
423+
require.ErrorAs(t,err,&apiError)
424+
require.Equal(t,http.StatusForbidden,apiError.StatusCode())
425+
})
378426
}
379427

380428
funcTestResolveAutostart(t*testing.T) {

‎coderd/wsbuilder/wsbuilder.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,15 @@ func (b *Builder) authorize(authFunc func(action policy.Action, object rbac.Obje
790790
returnBuildError{http.StatusBadRequest,msg,xerrors.New(msg)}
791791
}
792792
if!authFunc(action,b.workspace) {
793+
ifauthFunc(policy.ActionRead,b.workspace) {
794+
// If the user can read the workspace, but not delete/create/update. Show
795+
// a more helpful error. They are allowed to know the workspace exists.
796+
returnBuildError{
797+
Status:http.StatusForbidden,
798+
Message:fmt.Sprintf("You do not have permission to %s this workspace.",action),
799+
Wrapped:xerrors.New(httpapi.ResourceForbiddenResponse.Detail),
800+
}
801+
}
793802
// We use the same wording as the httpapi to avoid leaking the existence of the workspace
794803
returnBuildError{http.StatusNotFound,httpapi.ResourceNotFoundResponse.Message,xerrors.New(httpapi.ResourceNotFoundResponse.Message)}
795804
}

‎codersdk/rbacroles.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ const (
88
RoleUserAdminstring="user-admin"
99
RoleAuditorstring="auditor"
1010

11-
RoleOrganizationAdminstring="organization-admin"
12-
RoleOrganizationMemberstring="organization-member"
13-
RoleOrganizationAuditorstring="organization-auditor"
14-
RoleOrganizationTemplateAdminstring="organization-template-admin"
15-
RoleOrganizationUserAdminstring="organization-user-admin"
11+
RoleOrganizationAdminstring="organization-admin"
12+
RoleOrganizationMemberstring="organization-member"
13+
RoleOrganizationAuditorstring="organization-auditor"
14+
RoleOrganizationTemplateAdminstring="organization-template-admin"
15+
RoleOrganizationUserAdminstring="organization-user-admin"
16+
RoleOrganizationWorkspaceCreationBanstring="organization-workspace-creation-ban"
1617
)

‎enterprise/coderd/roles_test.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -441,10 +441,11 @@ func TestListRoles(t *testing.T) {
441441
returnmember.ListOrganizationRoles(ctx,owner.OrganizationID)
442442
},
443443
ExpectedRoles:convertRoles(map[rbac.RoleIdentifier]bool{
444-
{Name:codersdk.RoleOrganizationAdmin,OrganizationID:owner.OrganizationID}:false,
445-
{Name:codersdk.RoleOrganizationAuditor,OrganizationID:owner.OrganizationID}:false,
446-
{Name:codersdk.RoleOrganizationTemplateAdmin,OrganizationID:owner.OrganizationID}:false,
447-
{Name:codersdk.RoleOrganizationUserAdmin,OrganizationID:owner.OrganizationID}:false,
444+
{Name:codersdk.RoleOrganizationAdmin,OrganizationID:owner.OrganizationID}:false,
445+
{Name:codersdk.RoleOrganizationAuditor,OrganizationID:owner.OrganizationID}:false,
446+
{Name:codersdk.RoleOrganizationTemplateAdmin,OrganizationID:owner.OrganizationID}:false,
447+
{Name:codersdk.RoleOrganizationUserAdmin,OrganizationID:owner.OrganizationID}:false,
448+
{Name:codersdk.RoleOrganizationWorkspaceCreationBan,OrganizationID:owner.OrganizationID}:false,
448449
}),
449450
},
450451
{
@@ -473,10 +474,11 @@ func TestListRoles(t *testing.T) {
473474
returnorgAdmin.ListOrganizationRoles(ctx,owner.OrganizationID)
474475
},
475476
ExpectedRoles:convertRoles(map[rbac.RoleIdentifier]bool{
476-
{Name:codersdk.RoleOrganizationAdmin,OrganizationID:owner.OrganizationID}:true,
477-
{Name:codersdk.RoleOrganizationAuditor,OrganizationID:owner.OrganizationID}:true,
478-
{Name:codersdk.RoleOrganizationTemplateAdmin,OrganizationID:owner.OrganizationID}:true,
479-
{Name:codersdk.RoleOrganizationUserAdmin,OrganizationID:owner.OrganizationID}:true,
477+
{Name:codersdk.RoleOrganizationAdmin,OrganizationID:owner.OrganizationID}:true,
478+
{Name:codersdk.RoleOrganizationAuditor,OrganizationID:owner.OrganizationID}:true,
479+
{Name:codersdk.RoleOrganizationTemplateAdmin,OrganizationID:owner.OrganizationID}:true,
480+
{Name:codersdk.RoleOrganizationUserAdmin,OrganizationID:owner.OrganizationID}:true,
481+
{Name:codersdk.RoleOrganizationWorkspaceCreationBan,OrganizationID:owner.OrganizationID}:true,
480482
}),
481483
},
482484
{
@@ -505,10 +507,11 @@ func TestListRoles(t *testing.T) {
505507
returnclient.ListOrganizationRoles(ctx,owner.OrganizationID)
506508
},
507509
ExpectedRoles:convertRoles(map[rbac.RoleIdentifier]bool{
508-
{Name:codersdk.RoleOrganizationAdmin,OrganizationID:owner.OrganizationID}:true,
509-
{Name:codersdk.RoleOrganizationAuditor,OrganizationID:owner.OrganizationID}:true,
510-
{Name:codersdk.RoleOrganizationTemplateAdmin,OrganizationID:owner.OrganizationID}:true,
511-
{Name:codersdk.RoleOrganizationUserAdmin,OrganizationID:owner.OrganizationID}:true,
510+
{Name:codersdk.RoleOrganizationAdmin,OrganizationID:owner.OrganizationID}:true,
511+
{Name:codersdk.RoleOrganizationAuditor,OrganizationID:owner.OrganizationID}:true,
512+
{Name:codersdk.RoleOrganizationTemplateAdmin,OrganizationID:owner.OrganizationID}:true,
513+
{Name:codersdk.RoleOrganizationUserAdmin,OrganizationID:owner.OrganizationID}:true,
514+
{Name:codersdk.RoleOrganizationWorkspaceCreationBan,OrganizationID:owner.OrganizationID}:true,
512515
}),
513516
},
514517
}

‎site/src/api/typesGenerated.ts

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

‎site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.stories.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
MockOwnerRole,
55
MockSiteRoles,
66
MockUserAdminRole,
7+
MockWorkspaceCreationBanRole,
78
}from"testHelpers/entities";
89
import{withDesktopViewport}from"testHelpers/storybook";
910
import{EditRolesButton}from"./EditRolesButton";
@@ -41,3 +42,14 @@ export const Loading: Story = {
4142
awaituserEvent.click(canvas.getByRole("button"));
4243
},
4344
};
45+
46+
exportconstAdvancedOpen:Story={
47+
args:{
48+
selectedRoleNames:newSet([MockWorkspaceCreationBanRole.name]),
49+
roles:MockSiteRoles,
50+
},
51+
play:async({ canvasElement})=>{
52+
constcanvas=within(canvasElement);
53+
awaituserEvent.click(canvas.getByRole("button"));
54+
},
55+
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp