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

Commit713b954

Browse files
committed
fix: only show editable orgs on deployment page
Also make sure the redirect from /organizations goes to an org that theuser can edit, rather than always the default org.
1 parent3b53f5a commit713b954

File tree

8 files changed

+493
-154
lines changed

8 files changed

+493
-154
lines changed

‎site/src/api/queries/organizations.ts

Lines changed: 99 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
importtype{QueryClient}from"react-query";
22
import{API}from"api/api";
33
importtype{
4+
AuthorizationCheck,
5+
AuthorizationResponse,
46
CreateOrganizationRequest,
7+
Organization,
58
UpdateOrganizationRequest,
69
}from"api/typesGenerated";
710
import{meKey}from"./users";
@@ -121,6 +124,53 @@ export const provisionerDaemons = (organization: string) => {
121124
};
122125
};
123126

127+
constorgChecks=(
128+
organizationId:string,
129+
):Record<string,AuthorizationCheck>=>({
130+
viewMembers:{
131+
object:{
132+
resource_type:"organization_member",
133+
organization_id:organizationId,
134+
},
135+
action:"read",
136+
},
137+
editMembers:{
138+
object:{
139+
resource_type:"organization_member",
140+
organization_id:organizationId,
141+
},
142+
action:"update",
143+
},
144+
createGroup:{
145+
object:{
146+
resource_type:"group",
147+
organization_id:organizationId,
148+
},
149+
action:"create",
150+
},
151+
viewGroups:{
152+
object:{
153+
resource_type:"group",
154+
organization_id:organizationId,
155+
},
156+
action:"read",
157+
},
158+
editOrganization:{
159+
object:{
160+
resource_type:"organization",
161+
organization_id:organizationId,
162+
},
163+
action:"update",
164+
},
165+
auditOrganization:{
166+
object:{
167+
resource_type:"audit_log",
168+
organization_id:organizationId,
169+
},
170+
action:"read",
171+
},
172+
});
173+
124174
/**
125175
* Fetch permissions for a single organization.
126176
*
@@ -133,51 +183,55 @@ export const organizationPermissions = (organizationId: string | undefined) => {
133183
return{
134184
queryKey:["organization",organizationId,"permissions"],
135185
queryFn:()=>
136-
API.checkAuthorization({
137-
checks:{
138-
viewMembers:{
139-
object:{
140-
resource_type:"organization_member",
141-
organization_id:organizationId,
142-
},
143-
action:"read",
144-
},
145-
editMembers:{
146-
object:{
147-
resource_type:"organization_member",
148-
organization_id:organizationId,
149-
},
150-
action:"update",
151-
},
152-
createGroup:{
153-
object:{
154-
resource_type:"group",
155-
organization_id:organizationId,
156-
},
157-
action:"create",
158-
},
159-
viewGroups:{
160-
object:{
161-
resource_type:"group",
162-
organization_id:organizationId,
163-
},
164-
action:"read",
165-
},
166-
editOrganization:{
167-
object:{
168-
resource_type:"organization",
169-
organization_id:organizationId,
170-
},
171-
action:"update",
172-
},
173-
auditOrganization:{
174-
object:{
175-
resource_type:"audit_log",
176-
organization_id:organizationId,
177-
},
178-
action:"read",
179-
},
186+
API.checkAuthorization({checks:orgChecks(organizationId)}),
187+
};
188+
};
189+
190+
/**
191+
* Fetch permissions for all provided organizations.
192+
*
193+
* If organizations are undefined, return a disabled query.
194+
*/
195+
exportconstorganizationsPermissions=(
196+
organizations:Organization[]|undefined,
197+
)=>{
198+
if(!organizations){
199+
return{enabled:false};
200+
}
201+
202+
return{
203+
queryKey:["organizations","permissions"],
204+
queryFn:async()=>{
205+
// The endpoint takes a flat array, so to avoid collisions prepend each
206+
// check with the org ID (the key can be anything we want).
207+
constchecks=organizations
208+
.map((org)=>
209+
Object.entries(orgChecks(org.id)).map(([key,val])=>[
210+
`${org.id}.${key}`,
211+
val,
212+
]),
213+
)
214+
.flat();
215+
216+
constresponse=awaitAPI.checkAuthorization({
217+
checks:Object.fromEntries(checks),
218+
});
219+
220+
// Now we can unflatten by parsing out the org ID from each check.
221+
returnObject.entries(response).reduce(
222+
(acc,[key,value])=>{
223+
constindex=key.indexOf(".");
224+
constorgId=key.substring(0,index);
225+
constperm=key.substring(index+1);
226+
if(!acc[orgId]){
227+
acc[orgId]={[perm]:value};
228+
}else{
229+
acc[orgId][perm]=value;
230+
}
231+
returnacc;
180232
},
181-
}),
233+
{}asRecord<string,AuthorizationResponse>,
234+
);
235+
},
182236
};
183237
};

‎site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type FC, Suspense } from "react";
22
import{useQuery}from"react-query";
33
import{Outlet}from"react-router-dom";
44
import{deploymentConfig}from"api/queries/deployment";
5-
importtype{Organization}from"api/typesGenerated";
5+
importtype{AuthorizationResponse,Organization}from"api/typesGenerated";
66
import{Loader}from"components/Loader/Loader";
77
import{Margins}from"components/Margins/Margins";
88
import{Stack}from"components/Stack/Stack";
@@ -21,6 +21,20 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => {
2121
return{ organizations};
2222
};
2323

24+
/**
25+
* Return true if the user can edit the organization settings or its members.
26+
*/
27+
exportconstcanEditOrganization=(
28+
permissions:AuthorizationResponse|undefined,
29+
)=>{
30+
return(
31+
permissions&&
32+
(permissions.editOrganization||
33+
permissions.viewMembers||
34+
permissions.viewGroups)
35+
);
36+
};
37+
2438
/**
2539
* A multi-org capable settings page layout.
2640
*
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import{screen,within}from"@testing-library/react";
2+
import{HttpResponse,http}from"msw";
3+
import{
4+
MockDefaultOrganization,
5+
MockOrganization2,
6+
}from"testHelpers/entities";
7+
import{
8+
renderWithManagementSettingsLayout,
9+
waitForLoaderToBeRemoved,
10+
}from"testHelpers/renderHelpers";
11+
import{server}from"testHelpers/server";
12+
importOrganizationSettingsPagefrom"./OrganizationSettingsPage";
13+
14+
jest.spyOn(console,"error").mockImplementation(()=>{});
15+
16+
constrenderRootPage=async()=>{
17+
renderWithManagementSettingsLayout(<OrganizationSettingsPage/>,{
18+
route:"/organizations",
19+
path:"/organizations",
20+
extraRoutes:[
21+
{
22+
path:"/organizations/:organization",
23+
element:<OrganizationSettingsPage/>,
24+
},
25+
],
26+
});
27+
awaitwaitForLoaderToBeRemoved();
28+
};
29+
30+
constrenderPage=async(orgName:string)=>{
31+
renderWithManagementSettingsLayout(<OrganizationSettingsPage/>,{
32+
route:`/organizations/${orgName}`,
33+
path:"/organizations/:organization",
34+
});
35+
awaitwaitForLoaderToBeRemoved();
36+
};
37+
38+
describe("OrganizationSettingsPage",()=>{
39+
it("has no organizations",async()=>{
40+
server.use(
41+
http.get("/api/v2/organizations",()=>{
42+
returnHttpResponse.json([]);
43+
}),
44+
http.post("/api/v2/authcheck",async()=>{
45+
returnHttpResponse.json({
46+
[`${MockDefaultOrganization.id}.editOrganization`]:true,
47+
viewDeploymentValues:true,
48+
});
49+
}),
50+
);
51+
awaitrenderRootPage();
52+
awaitscreen.findByText("No organizations found");
53+
});
54+
55+
it("has no editable organizations",async()=>{
56+
server.use(
57+
http.get("/api/v2/organizations",()=>{
58+
returnHttpResponse.json([MockDefaultOrganization,MockOrganization2]);
59+
}),
60+
http.post("/api/v2/authcheck",async()=>{
61+
returnHttpResponse.json({
62+
viewDeploymentValues:true,
63+
});
64+
}),
65+
);
66+
awaitrenderRootPage();
67+
awaitscreen.findByText("No organizations found");
68+
});
69+
70+
it("redirects to default organization",async()=>{
71+
server.use(
72+
http.get("/api/v2/organizations",()=>{
73+
// Default always preferred regardless of order.
74+
returnHttpResponse.json([MockOrganization2,MockDefaultOrganization]);
75+
}),
76+
http.post("/api/v2/authcheck",async()=>{
77+
returnHttpResponse.json({
78+
[`${MockDefaultOrganization.id}.editOrganization`]:true,
79+
[`${MockOrganization2.id}.editOrganization`]:true,
80+
viewDeploymentValues:true,
81+
});
82+
}),
83+
);
84+
awaitrenderRootPage();
85+
constform=screen.getByTestId("org-settings-form");
86+
expect(within(form).getByRole("textbox",{name:"Name"})).toHaveValue(
87+
MockDefaultOrganization.name,
88+
);
89+
});
90+
91+
it("redirects to non-default organization",async()=>{
92+
server.use(
93+
http.get("/api/v2/organizations",()=>{
94+
returnHttpResponse.json([MockDefaultOrganization,MockOrganization2]);
95+
}),
96+
http.post("/api/v2/authcheck",async()=>{
97+
returnHttpResponse.json({
98+
[`${MockOrganization2.id}.editOrganization`]:true,
99+
viewDeploymentValues:true,
100+
});
101+
}),
102+
);
103+
awaitrenderRootPage();
104+
constform=screen.getByTestId("org-settings-form");
105+
expect(within(form).getByRole("textbox",{name:"Name"})).toHaveValue(
106+
MockOrganization2.name,
107+
);
108+
});
109+
110+
it("cannot find organization",async()=>{
111+
server.use(
112+
http.get("/api/v2/organizations",()=>{
113+
returnHttpResponse.json([MockDefaultOrganization,MockOrganization2]);
114+
}),
115+
http.post("/api/v2/authcheck",async()=>{
116+
returnHttpResponse.json({
117+
[`${MockOrganization2.id}.editOrganization`]:true,
118+
viewDeploymentValues:true,
119+
});
120+
}),
121+
);
122+
awaitrenderPage("the-endless-void");
123+
awaitscreen.findByText("Organization not found");
124+
});
125+
});

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp