Skip to content

Commit b24c302

Browse files
committed
WIP for #595
1 parent 8ef80b5 commit b24c302

File tree

7 files changed

+118
-16
lines changed

7 files changed

+118
-16
lines changed

client/src/App.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ const App = () => {
8080
setIsAuthenticated(res[0].authenticated);
8181
if (res[0].authenticated) {
8282
me().then(user => {
83-
const currentOrganization = user.organizationMemberships.map(om => om.organization)[0] || {name: ""};
83+
//If there are multiple organization memberships, we default to the one which is used to login
84+
const identityProviderId = (user.identityProvider || {}).id;
85+
const orgMembership = (user.organizationMemberships || [])
86+
.find(organizationMembership => !isEmpty(identityProviderId) &&
87+
organizationMembership.organization.manageIdentifier === identityProviderId);
88+
const currentOrganization = orgMembership?.organization || user.organizationMemberships.map(om => om.organization)[0] || {name: ""};
8489
const newMenuItems = menuItemsForUser(user, currentOrganization);
8590
useAppStore.setState(() => ({
8691
user: user,

client/src/api/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export function me() {
7878
return fetchJson("/api/v1/users/me");
7979
}
8080

81+
export function organizationSwitch(organization) {
82+
return fetchJson(`/api/v1/users/organization-switch/${organization.id}`);
83+
}
84+
8185
export function deleteUser() {
8286
return fetchDelete("/api/v1/users");
8387
}

client/src/components/UserMenu.jsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import CheckPlain from "../icons/check-plain.svg";
99
import CaretDown from "../icons/caret_down.svg";
1010
import {mainMenuItems, menuItemsForUser} from "../utils/MenuItems.js";
1111
import {useLogout} from '../hooks/UseLogout.jsx';
12+
import {organizationSwitch} from "../api/index.js";
1213

1314
export const UserMenu = ({setIsAuthenticated}) => {
1415

@@ -23,13 +24,17 @@ export const UserMenu = ({setIsAuthenticated}) => {
2324
const logoutUser = useLogout();
2425

2526
const switchOrganization = organization => {
26-
const newMenuItems = menuItemsForUser(user, organization);
27-
useAppStore.setState(() => ({
28-
currentOrganization: organization,
29-
menuItems: newMenuItems,
30-
activeMenuItem: mainMenuItems.home
31-
}))
32-
navigate("/home");
27+
organizationSwitch(organization)
28+
.then(newUser => {
29+
const newMenuItems = menuItemsForUser(newUser, organization);
30+
useAppStore.setState(() => ({
31+
currentOrganization: organization,
32+
menuItems: newMenuItems,
33+
user: newUser,
34+
activeMenuItem: mainMenuItems.home,
35+
}));
36+
navigate("/home");
37+
});
3338
}
3439

3540
const renderOrganizationSwitch = () => {
@@ -48,7 +53,9 @@ export const UserMenu = ({setIsAuthenticated}) => {
4853
{isSwitchOrganizationOpen &&
4954
<section className="organization-switch-section sds--user-info--dropdown">
5055
{organizations.map((org, index) =>
51-
<div key={index} className="organization-option" onClick={() => switchOrganization(org)}>
56+
<div key={index}
57+
className="organization-option"
58+
onClick={() => switchOrganization(org)}>
5259
<span className={`${currentOrganization.id === org.id ? "active" : ""}`}>
5360
{org.name}
5461
</span>

client/src/utils/MenuItems.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const doMenuItemsForUser = (user, currentOrganization, feedbackWidgetEnabled = u
5757
if (isMember || isAdmin) {
5858
newMenuItems.push(mainMenuItems.users);
5959
}
60-
if (!user.externalUser) {
60+
if (!isEmpty(currentOrganization.manageIdentifier)) {
6161
newMenuItems.push(mainMenuItems.accessibleApps, mainMenuItems.idp, mainMenuItems.invite, mainMenuItems.sram);
6262
}
6363
if ((isAdmin || user.superUser) && !isEmpty(currentOrganization.manageIdentifier)) {

server/src/main/java/access/api/ConnectionController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ private Connection productionReadyChangeRequests(Connection connection, User use
323323
//And therefore we don't create a new change request, but update the existing one
324324
Map<String, Object> existingChangeRequest = existingChangeRequests.getFirst();
325325
ChangeRequest changeRequest = changeRequestOptional.get();
326-
existingChangeRequest.get("pathUpdates")
326+
existingChangeRequest.get("pathUpdates");
327327
//TODO , add / override all new pathUpdates, except if the value is a List, then sort out the difference
328328

329329

server/src/main/java/access/api/UserController.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import access.config.Config;
44
import access.exception.NotAllowedException;
55
import access.exception.NotFoundException;
6+
import access.exception.UserRestrictionException;
67
import access.mail.MailBox;
78
import access.manage.Manage;
89
import access.model.*;
@@ -134,6 +135,34 @@ public ResponseEntity<User> me(@Parameter(hidden = true) User user, Authenticati
134135
return ResponseEntity.ok(userFromDB);
135136
}
136137

138+
@GetMapping("/organization-switch/{organizationId}")
139+
public ResponseEntity<User> organizationSwitch(@Parameter(hidden = true) User user,
140+
@PathVariable("organizationId") Long organizationId,
141+
Authentication authentication) {
142+
LOG.debug(String.format("/organization-switch for user %s", user.getEduPersonPrincipalName()));
143+
144+
User userFromDB = userRepository.findDetailsById(user.getId())
145+
.orElseThrow(() -> new NotFoundException("User not found"));
146+
147+
Organization organization = userFromDB.getOrganizationMemberships().stream()
148+
.filter(organizationMembership -> organizationMembership.getOrganization().getId().equals(organizationId))
149+
.map(organizationMembership -> organizationMembership.getOrganization())
150+
.findFirst()
151+
.orElseThrow(() -> new UserRestrictionException((String.format("User %s is not a member of organization %s",
152+
user.getEmail(), organizationId))));
153+
154+
boolean isInternalUserFromOrganization = StringUtils.hasText(organization.getManageIdentifier());
155+
userFromDB.setExternalUser(!isInternalUserFromOrganization);
156+
if (isInternalUserFromOrganization) {
157+
Map<String, Object> identityProvider = manage.providerByManageIdentifier(
158+
EntityType.saml20_idp, organization.getManageIdentifier(), Environment.PROD);
159+
userFromDB.setIdentityProvider(identityProvider);
160+
List<Map<String, Object>> changeRequests = manage.getChangeRequestsIdentityProvider(identityProvider);
161+
userFromDB.setChangeRequests(changeRequests);
162+
}
163+
return ResponseEntity.ok(userFromDB);
164+
}
165+
137166
@GetMapping("/other/{id}")
138167
public ResponseEntity<User> details(@PathVariable("id") Long id, @Parameter(hidden = true) User user) {
139168
LOG.debug(String.format("/other/%s for user %s", id, user.getEduPersonPrincipalName()));

server/src/test/java/access/api/UserControllerTest.java

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@
33
import access.AbstractTest;
44
import access.AccessCookieFilter;
55
import access.UserInfoEnhancer;
6-
import access.model.*;
6+
import access.model.Authority;
7+
import access.model.EntityType;
8+
import access.model.Environment;
9+
import access.model.Institution;
10+
import access.model.Organization;
11+
import access.model.OrganizationMembership;
12+
import access.model.User;
713
import io.restassured.common.mapper.TypeRef;
814
import io.restassured.http.ContentType;
9-
import io.restassured.http.Headers;
10-
import lombok.SneakyThrows;
1115
import org.junit.jupiter.api.Test;
1216
import org.springframework.data.domain.Sort;
1317
import org.springframework.http.HttpStatus;
1418

15-
import java.lang.reflect.Type;
1619
import java.util.List;
1720
import java.util.Map;
1821
import java.util.Optional;
1922

20-
import static com.github.tomakehurst.wiremock.client.WireMock.*;
2123
import static io.restassured.RestAssured.given;
2224
import static org.junit.jupiter.api.Assertions.*;
2325

@@ -112,6 +114,61 @@ void meManagerWithMockLogin() {
112114
assertEquals("ShareLogics", organization.getName());
113115
}
114116

117+
@Test
118+
void swichOrganizationManagerWithMockLogin() {
119+
AccessCookieFilter accessCookieFilter = mockLoginFlow(MANAGE_SUB);
120+
121+
super.stubForGetProvider(EntityType.saml20_idp, "7", Environment.PROD);
122+
super.stubForGetChangeRequests(getChangeRequests());
123+
124+
User user = given()
125+
.when()
126+
.filter(accessCookieFilter.cookieFilter())
127+
.accept(ContentType.JSON)
128+
.contentType(ContentType.JSON)
129+
.pathParam("organizationId", seedIdentifiers.get(SHARE_LOGICS))
130+
.get("/api/v1/users/organization-switch/{organizationId}")
131+
.as(User.class);
132+
assertEquals(1, user.getOrganizationMemberships().size());
133+
134+
Organization organization = user.getOrganizationMemberships().stream().findFirst().get().getOrganization();
135+
assertEquals("ShareLogics", organization.getName());
136+
137+
assertEquals("7", user.getIdentityProvider().get("id"));
138+
}
139+
140+
@Test
141+
void swichOrganizationGuestWithoutIdP() {
142+
AccessCookieFilter accessCookieFilter = mockLoginFlow(GUEST_SUB);
143+
144+
User user = given()
145+
.when()
146+
.filter(accessCookieFilter.cookieFilter())
147+
.accept(ContentType.JSON)
148+
.contentType(ContentType.JSON)
149+
.pathParam("organizationId", seedIdentifiers.get(FAR_WIND))
150+
.get("/api/v1/users/organization-switch/{organizationId}")
151+
.as(User.class);
152+
assertEquals(1, user.getOrganizationMemberships().size());
153+
154+
assertNull(user.getIdentityProvider());
155+
}
156+
157+
@Test
158+
void swichOrganizationGuestNoAccess() {
159+
AccessCookieFilter accessCookieFilter = mockLoginFlow(GUEST_SUB);
160+
161+
given()
162+
.when()
163+
.filter(accessCookieFilter.cookieFilter())
164+
.accept(ContentType.JSON)
165+
.contentType(ContentType.JSON)
166+
.pathParam("organizationId", -1L)
167+
.get("/api/v1/users/organization-switch/{organizationId}")
168+
.then()
169+
.statusCode(HttpStatus.FORBIDDEN.value());
170+
}
171+
115172
@Test
116173
void meMissingAttributes() throws Exception {
117174
this.stubForStats();

0 commit comments

Comments
 (0)