Skip to content

Commit d0a819f

Browse files
committed
WIP for IdP policies
1 parent 6b9aab9 commit d0a819f

File tree

8 files changed

+243
-70
lines changed

8 files changed

+243
-70
lines changed

client/src/locale/en.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,9 @@ const en = {
11411141
title: "Whoops, error!!!",
11421142
unexpected: "An unexpected error occurred. Please contact <a href='mailto:support@surfconext.nl'>support@surfconext.nl</a> for support.",
11431143
jiraDown: "Momentarily the backend could not process your request. Please contact <a href='mailto:support@surfconext.nl'>support@surfconext.nl</a> for support."
1144+
},
1145+
policies: {
1146+
title: "Policies for {{name}}"
11441147
}
11451148

11461149
}

client/src/pages/ApplicationDetail.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "./ApplicationDetail.scss";
2+
import "../styles/access_card.scss";
23
import React, {useEffect, useState} from "react";
34
import {
45
connectServiceProviderToIdentityProvider,

client/src/pages/ApplicationDetail.scss

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -337,56 +337,6 @@ div.application-detail-container {
337337
}
338338
}
339339

340-
.access-card {
341-
background: white;
342-
margin: 10px 0;
343-
border: 1px solid var(--sl-color-secondary-400);
344-
border-radius: 10px;
345-
display: flex;
346-
align-items: center;
347-
padding: 8px;
348-
width: 100%;
349-
350-
&.grey {
351-
background-color: var(--sds--color--gray--background);
352-
font-style: italic;
353-
}
354-
355-
&.border {
356-
border: 1px solid var(--sl-color-neutral-400);
357-
358-
}
359-
360-
&.large {
361-
font-size: 18px;
362-
}
363-
364-
h4 {
365-
font-size: 18px;
366-
}
367-
368-
img, svg {
369-
margin-left: auto;
370-
width: 42px;
371-
height: 42px;
372-
}
373-
374-
.chip {
375-
margin-left: auto;
376-
border-radius: 38px;
377-
padding: 2px 12px;
378-
font-size: 16px;
379-
background-color: var(--sl-color-neutral-100);
380-
color: var(--sl-color-neutral-500);
381-
min-width: 100px;
382-
text-align: center;
383-
384-
&.blue {
385-
background-color: var(--sds--color--blue--400);
386-
color: white;
387-
}
388-
}
389-
}
390340
}
391341

392342
}

client/src/pages/Policies.jsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React, {useEffect, useMemo, useState} from "react";
2+
import {useAppStore} from "../stores/AppStore";
3+
import {Navigate, useNavigate, useParams} from "react-router-dom";
4+
import {getPolicyByIdentityProvider} from "../api/index.js";
5+
import {isEmpty} from "../utils/Utils.js";
6+
import "./Policies.scss";
7+
import I18n from "../locale/I18n";
8+
import DOMPurify from "dompurify";
9+
import {authorities} from "../utils/Permissions.js";
10+
import {Loader} from "@surfnet/sds";
11+
import {useShallow} from "zustand/react/shallow";
12+
import {groupByValues, policyTemplate} from "../utils/Policy.js";
13+
import {PolicyForm} from "../policies/PolicyForm.jsx";
14+
import {PolicyOverview} from "../policies/PolicyOverview.jsx";
15+
16+
17+
const Policies = () => {
18+
19+
const {user, currentOrganization} = useAppStore(useShallow(state => ({
20+
user: state.user,
21+
currentOrganization: state.currentOrganization
22+
})));
23+
const {page, policyId} = useParams();
24+
25+
const [loading, setLoading] = useState(true);
26+
const [policies, setPolicies] = useState({});
27+
const [showPolicyOverview, setShowPolicyOverview] = useState(true);
28+
const [showPolicyDetails, setShowPolicyDetails] = useState(false);
29+
const [currentPolicy, setCurrentPolicy] = useState(null);
30+
31+
const navigate = useNavigate();
32+
33+
const adminUser = useMemo(() => {
34+
return user.superUser || (user.organizationMemberships
35+
.some(om => om.authority === authorities.ADMIN && om.organization.id === currentOrganization.id)
36+
&& !isEmpty(currentOrganization.manageIdentifier));
37+
}, [user, currentOrganization]);
38+
39+
const toPolicyDetail = (policyIdentifier, allPolicies = policies) => {
40+
setShowPolicyOverview(false);
41+
let newCurrentPolicy;
42+
if (policyIdentifier === "new") {
43+
newCurrentPolicy = policyTemplate(user.identityProvider.data.entityid);
44+
} else {
45+
newCurrentPolicy = allPolicies.find(policy => policy.id === policyIdentifier);
46+
if (isEmpty(newCurrentPolicy)) {
47+
navigate("/404");
48+
return;
49+
}
50+
newCurrentPolicy.data.attributes = groupByValues([...newCurrentPolicy.data.attributes]);
51+
}
52+
window.scrollTo({top: 0, behavior: "smooth"});
53+
setCurrentPolicy(newCurrentPolicy);
54+
setShowPolicyDetails(true);
55+
navigate(`/policies/details/${policyIdentifier}`);
56+
}
57+
58+
useEffect(() => {
59+
getPolicyByIdentityProvider()
60+
.then(res => {
61+
setPolicies(res);
62+
if (page === "details" && !isEmpty(policyId)) {
63+
toPolicyDetail(policyId, res);
64+
}
65+
setLoading(false);
66+
}).catch(() => {
67+
navigate("/home")
68+
});
69+
70+
}, []);// eslint-disable-line react-hooks/exhaustive-deps
71+
72+
if (!adminUser) {
73+
return <Navigate to={"/404"} replace/>;
74+
}
75+
76+
if (loading) {
77+
return <Loader/>
78+
}
79+
80+
const refreshPolicies = () => {
81+
setLoading(true);
82+
getPolicyByIdentityProvider()
83+
.then(res => {
84+
setPolicies(res);
85+
navigate("/policies/overview");
86+
setShowPolicyOverview(true);
87+
setShowPolicyDetails(false);
88+
setLoading(false);
89+
});
90+
}
91+
92+
return (
93+
<div
94+
className="policies-outer-container">
95+
<div className="policies-header-container">
96+
<div className="top-header">
97+
<h1>{I18n.t("policies.title", {name: currentOrganization.name})}</h1>
98+
</div>
99+
<p dangerouslySetInnerHTML={{
100+
__html: DOMPurify.sanitize(I18n.t("myOrganization.info"),
101+
{ADD_ATTR: ['target'], ADD_TAGS: ['rel']})
102+
}}/>
103+
</div>
104+
<div className="policies">
105+
<div className="app-policies">
106+
{showPolicyDetails &&
107+
<PolicyForm policy={currentPolicy}
108+
setPolicy={setCurrentPolicy}
109+
isExistingPolicy={!isEmpty(currentPolicy.id)}
110+
originalName={currentPolicy.originalName}
111+
refreshPolicies={refreshPolicies}
112+
/>
113+
}
114+
{showPolicyOverview &&
115+
<PolicyOverview
116+
policies={policies}
117+
policyDetails={toPolicyDetail}
118+
refreshPolicies={refreshPolicies}
119+
/>
120+
}
121+
122+
</div>
123+
124+
</div>
125+
</div>
126+
127+
)
128+
129+
};
130+
export default Policies;

client/src/pages/Policies.scss

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@use "../index";
2+
@use "../styles/vars" as *;
3+
4+
div.policies-outer-container {
5+
width: 100%;
6+
display: flex;
7+
flex-direction: column;
8+
height: 100%;
9+
10+
.policies-header-container {
11+
background-color: var(--sds--color--gray--background);
12+
padding: 25px 50px 25px 50px;
13+
14+
.top-header {
15+
margin-bottom: 15px;
16+
}
17+
}
18+
19+
div.policies {
20+
padding: 25px 50px;
21+
background-color: white;
22+
}
23+
24+
.app-policies {
25+
display: flex;
26+
width: 75%;
27+
gap: 25px;
28+
@media (max-width: $medium) {
29+
width: 100%;
30+
flex-direction: column;
31+
}
32+
33+
}
34+
35+
36+
}

client/src/policies/PolicyOverview.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "./PolicyOverview.scss";
2+
import "../styles/access_card.scss";
23
import React, {useState} from "react";
34
import {Button, ButtonType, Chip, Tooltip} from "@surfnet/sds";
45
import I18n from "../locale/I18n.js";
@@ -78,7 +79,7 @@ export const PolicyOverview = ({policies, backToAccess, policyDetails, refreshPo
7879
question={question}
7980
/>}
8081
<div className="policy-overview">
81-
<a href="/#" onClick={e => backToAccess(e)}>{I18n.t("appAccess.backToAccess")}</a>
82+
{backToAccess && <a href="/#" onClick={e => backToAccess(e)}>{I18n.t("appAccess.backToAccess")}</a>}
8283
<div className="grouped">
8384
<h2>{I18n.t("appAccess.authorizationRules")}</h2>
8485
<Button type={ButtonType.Primary}

client/src/styles/access_card.scss

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.access-card {
2+
background: white;
3+
margin: 10px 0;
4+
border: 1px solid var(--sl-color-secondary-400);
5+
border-radius: 10px;
6+
display: flex;
7+
align-items: center;
8+
padding: 8px;
9+
width: 100%;
10+
11+
&.grey {
12+
background-color: var(--sds--color--gray--background);
13+
font-style: italic;
14+
}
15+
16+
&.border {
17+
border: 1px solid var(--sl-color-neutral-400);
18+
19+
}
20+
21+
&.large {
22+
font-size: 18px;
23+
}
24+
25+
h4 {
26+
font-size: 18px;
27+
}
28+
29+
img, svg {
30+
margin-left: auto;
31+
width: 42px;
32+
height: 42px;
33+
}
34+
35+
.chip {
36+
margin-left: auto;
37+
border-radius: 38px;
38+
padding: 2px 12px;
39+
font-size: 16px;
40+
background-color: var(--sl-color-neutral-100);
41+
color: var(--sl-color-neutral-500);
42+
min-width: 100px;
43+
text-align: center;
44+
45+
&.blue {
46+
background-color: var(--sds--color--blue--400);
47+
color: white;
48+
}
49+
}
50+
}

server/src/main/java/access/manage/RemoteManage.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.web.client.RestTemplate;
2525

2626
import java.io.IOException;
27+
import java.lang.reflect.Type;
2728
import java.net.URI;
2829
import java.util.HashMap;
2930
import java.util.List;
@@ -37,6 +38,8 @@
3738
public class RemoteManage implements Manage {
3839

3940
private static final Log LOG = LogFactory.getLog(RemoteManage.class);
41+
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_TYPE_REFERENCE = new ParameterizedTypeReference<>() {
42+
};
4043

4144
//Because of the custom error handling, we need to use Buffering
4245
private final Map<Environment, RestTemplate> restTemplates;
@@ -113,14 +116,9 @@ public Map<String, Object> saveIdentityProvider(Organization organization) {
113116

114117
RestTemplate restTemplate = environmentRestTemplate(Environment.PROD);
115118
String url = environmentUrl(Environment.PROD);
116-
ResponseEntity<Map> responseEntity = restTemplate.exchange(String.format("%s/manage/api/internal/metadata", url),
117-
HttpMethod.PUT, new HttpEntity<>(provider), Map.class);
118-
Map body = responseEntity.getBody();
119-
if (ResilientErrorHandler.ignoreError(body)) {
120-
//See ResilientErrorHandler#handleError. Any no-data-changed error is thrown there
121-
return provider;
122-
}
123-
return body;
119+
ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(String.format("%s/manage/api/internal/metadata", url),
120+
HttpMethod.PUT, new HttpEntity<>(provider), PARAMETERIZED_TYPE_REFERENCE);
121+
return checkNoChangeResponse(responseEntity, provider);
124122
}
125123

126124
@SneakyThrows
@@ -136,14 +134,9 @@ public Map<String, Object> saveProvider(Connection connection) {
136134
RestTemplate restTemplate = environmentRestTemplate(connection.getEnvironment());
137135
String url = environmentUrl(connection.getEnvironment());
138136
HttpMethod httpMethod = StringUtils.hasText(connection.getManageIdentifier()) ? HttpMethod.PUT : HttpMethod.POST;
139-
ResponseEntity<Map> responseEntity = restTemplate.exchange(String.format("%s/manage/api/internal/metadata", url),
140-
httpMethod, new HttpEntity<>(provider), Map.class);
141-
Map body = responseEntity.getBody();
142-
if (ResilientErrorHandler.ignoreError(body)) {
143-
//See ResilientErrorHandler#handleError. Any no-data-changed error is thrown there
144-
return provider;
145-
}
146-
return body;
137+
ResponseEntity<Map<String ,Object>> responseEntity = restTemplate.exchange(String.format("%s/manage/api/internal/metadata", url),
138+
httpMethod, new HttpEntity<>(provider), PARAMETERIZED_TYPE_REFERENCE);
139+
return checkNoChangeResponse(responseEntity, provider);
147140
}
148141

149142
@Override
@@ -179,8 +172,7 @@ public Map<String, Object> createChangeRequest(Environment environment, ChangeRe
179172
String url = String.format("%s/manage/api/internal/change-requests", environmentUrl(environment));
180173
HttpEntity<ChangeRequest> requestEntity = new HttpEntity<>(changeRequest);
181174
ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity,
182-
new ParameterizedTypeReference<>() {
183-
});
175+
PARAMETERIZED_TYPE_REFERENCE);
184176
return responseEntity.getBody();
185177
}
186178

@@ -399,7 +391,8 @@ public Map<String, Object> updatePolicy(Map<String, Object> policy) {
399391
RestTemplate restTemplate = environmentRestTemplate(Environment.PROD);
400392
String url = String.format("%s/manage/api/internal/metadata",
401393
environmentUrl(Environment.PROD));
402-
return restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(policy), Map.class).getBody();
394+
ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(policy), PARAMETERIZED_TYPE_REFERENCE);
395+
return checkNoChangeResponse(responseEntity, policy);
403396
}
404397

405398
@Override
@@ -466,5 +459,14 @@ private RestTemplate environmentRestTemplate(Environment environment) {
466459
return restTemplates.get(environment);
467460
}
468461

462+
private Map<String, Object> checkNoChangeResponse(ResponseEntity<Map<String, Object>> responseEntity, Map<String, Object> provider) {
463+
Map<String, Object> body = responseEntity.getBody();
464+
if (body == null || ResilientErrorHandler.ignoreError(body)) {
465+
//See ResilientErrorHandler#handleError. Any no-data-changed error is thrown there
466+
return provider;
467+
}
468+
return body;
469+
470+
}
469471

470472
}

0 commit comments

Comments
 (0)