Skip to content

Commit

Permalink
Use Spring Session to store the Manage applications for institutionAdmin
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Sep 26, 2023
1 parent 12983b7 commit 9b324a8
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 80 deletions.
4 changes: 4 additions & 0 deletions client/src/components/Entities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@

button {
margin-left: 25px;
@media (max-width: $medium) {
margin-left: 0;
margin-top: 15px
}
}

h2 {
Expand Down
11 changes: 8 additions & 3 deletions client/src/utils/UserRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,21 @@ export const allowedToRenewUserRole = (user, userRole) => {
if (user.superUser) {
return true;
}
const allowedByApplication = user.institutionAdmin && (user.applications || [])
.some(application => application.id === userRole.role.manageId);
switch (userRole.authority) {
case AUTHORITIES.SUPER_USER:
case AUTHORITIES.MANAGER:
return false;
case AUTHORITIES.INSTITUTION_ADMIN:
return false;
case AUTHORITIES.MANAGER:
return allowedByApplication;
case AUTHORITIES.INVITER :
return isUserAllowed(AUTHORITIES.MANAGER, user) &&
user.userRoles.some(ur => userRole.role.manageId === ur.role.manageId || userRole.role.id === ur.role.id);
(user.userRoles.some(ur => userRole.role.manageId === ur.role.manageId || userRole.role.id === ur.role.id) || allowedByApplication) ;
case AUTHORITIES.GUEST:
return isUserAllowed(AUTHORITIES.INVITER, user) &&
user.userRoles.some(ur => userRole.role.id === ur.role.id);
(user.userRoles.some(ur => userRole.role.id === ur.role.id) || allowedByApplication);
default:
return false
}
Expand Down
9 changes: 9 additions & 0 deletions server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static access.security.InstitutionAdmin.*;

@Entity(name = "users")
@NoArgsConstructor
@Getter
Expand Down Expand Up @@ -87,8 +89,12 @@ public User(boolean superUser, Map<String, Object> attributes) {
this.email = (String) attributes.get("email");
this.givenName = (String) attributes.get("given_name");
this.familyName = (String) attributes.get("family_name");
this.institutionAdmin = (boolean) attributes.get(INSTITUTION_ADMIN);
this.organizationGUID = (String) attributes.get(ORGANIZATION_GUID);
this.applications = (List<Map<String, Object>>) attributes.getOrDefault(APPLICATIONS, Collections.emptyList());
this.createdAt = Instant.now();
this.lastActivity = this.createdAt;

String name = (String) attributes.get("name");
String preferredUsername = (String) attributes.get("preferred_username");
if (StringUtils.hasText(name)) {
Expand Down Expand Up @@ -169,6 +175,9 @@ public void updateAttributes(Map<String, Object> attributes) {
this.givenName = (String) attributes.get("given_name");
this.familyName = (String) attributes.get("family_name");
this.email = (String) attributes.get("email");
this.institutionAdmin = (boolean) attributes.get(INSTITUTION_ADMIN);
this.organizationGUID = (String) attributes.get(ORGANIZATION_GUID);
this.applications = (List<Map<String, Object>>) attributes.getOrDefault(APPLICATIONS, Collections.emptyList());
this.lastActivity = Instant.now();
}

Expand Down
54 changes: 54 additions & 0 deletions server/src/main/java/access/security/CustomOidcUserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package access.security;

import access.manage.Manage;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static access.security.InstitutionAdmin.*;

public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final Manage manage;
private final String entitlement;
private final String organizationGuidPrefix;
private final OidcUserService delegate;

public CustomOidcUserService(Manage manage, String entitlement, String organizationGuidPrefix) {
this.manage = manage;
this.entitlement = entitlement;
this.organizationGuidPrefix = organizationGuidPrefix;
delegate = new OidcUserService();
}

@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
Map<String, Object> claims = oidcUser.getUserInfo().getClaims();
Map<String, Object> newClaims = new HashMap<>(claims);

boolean institutionAdmin = InstitutionAdmin.isInstitutionAdmin(claims, entitlement);
newClaims.put(INSTITUTION_ADMIN, institutionAdmin);

String organizationGuid = InstitutionAdmin.getOrganizationGuid(claims, organizationGuidPrefix).orElse(null);
newClaims.put(ORGANIZATION_GUID, organizationGuid);

if (institutionAdmin && StringUtils.hasText(organizationGuid)) {
List<Map<String, Object>> applications = manage.providersByInstitutionalGUID(organizationGuid);
newClaims.put(APPLICATIONS, applications);
}
OidcUserInfo oidcUserInfo = new OidcUserInfo(newClaims);
oidcUser = new DefaultOidcUser(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUserInfo);
return oidcUser;

}
}
31 changes: 26 additions & 5 deletions server/src/main/java/access/security/InstitutionAdmin.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@ConfigurationProperties(prefix = "institution-admin")
@Getter
@Setter
@SuppressWarnings("unchecked")
public class InstitutionAdmin {

private String entitlement;
private String organizationGuidPrefix;
public static final String INSTITUTION_ADMIN = "INSTITUTION_ADMIN";
public static final String ORGANIZATION_GUID = "ORGANIZATION_GUID";
public static final String APPLICATIONS = "APPLICATIONS";

}
public static boolean isInstitutionAdmin(Map<String, Object> attributes, String requiredEntitlement) {
if (attributes.containsKey("eduperson_entitlement")) {
List<String> entitlements = (List<String>) attributes.get("eduperson_entitlement");
return entitlements.stream().anyMatch(entitlement -> entitlement.equalsIgnoreCase(requiredEntitlement));
}
return false;
}

public static Optional<String> getOrganizationGuid(Map<String, Object> attributes, String organizationGuidPrefix) {
if (attributes.containsKey("eduperson_entitlement")) {
List<String> entitlements = (List<String>) attributes.get("eduperson_entitlement");
final String organizationGuidPrefixLower = organizationGuidPrefix.toLowerCase();
return entitlements.stream()
.filter(entitlement -> entitlement.toLowerCase().startsWith(organizationGuidPrefixLower))
.map(entitlement -> entitlement.substring(organizationGuidPrefix.length()))
.findFirst();
}
return Optional.empty();
}
}
37 changes: 15 additions & 22 deletions server/src/main/java/access/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package access.security;

import access.config.UserHandlerMethodArgumentResolver;
import access.exception.ExtendedErrorAttributes;
import access.manage.Manage;
import access.model.Invitation;
Expand Down Expand Up @@ -29,10 +28,13 @@
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
Expand All @@ -42,11 +44,11 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.function.Consumer;

import static access.security.InstitutionAdmin.*;

@EnableWebSecurity
@EnableScheduling
@Configuration
Expand Down Expand Up @@ -94,25 +96,21 @@ public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository,
}

@Configuration
@EnableConfigurationProperties({SuperAdmin.class, InstitutionAdmin.class})
@EnableConfigurationProperties({SuperAdmin.class})
public static class MvcConfig implements WebMvcConfigurer {

private final UserRepository userRepository;
private final SuperAdmin superAdmin;
private final InstitutionAdmin institutionAdmin;
private final Manage manage;

@Autowired
public MvcConfig(UserRepository userRepository, SuperAdmin superAdmin, InstitutionAdmin institutionAdmin, Manage manage) {
public MvcConfig(UserRepository userRepository, SuperAdmin superAdmin) {
this.userRepository = userRepository;
this.superAdmin = superAdmin;
this.institutionAdmin = institutionAdmin;
this.manage = manage;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin, institutionAdmin, manage));
argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin));
}

@Override
Expand All @@ -125,7 +123,10 @@ public void addCorsMappings(CorsRegistry registry) {

@Bean
@Order(1)
SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http) throws Exception {
SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http,
Manage manage,
@Value("${institution-admin.entitlement}") String entitlement,
@Value("${institution-admin.organization-guid-prefix}") String organizationGuidPrefix) throws Exception {
http
.csrf(c -> c
.ignoringRequestMatchers("/login/oauth2/code/oidcng")
Expand All @@ -149,21 +150,13 @@ SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http) throws Excepti
.authorizationRequestResolver(
authorizationRequestResolver(this.clientRegistrationRepository)
)
).userInfoEndpoint(userInfo -> userInfo.oidcUserService(this.oidcUserService()))
).userInfoEndpoint(userInfo -> userInfo.oidcUserService(
new CustomOidcUserService(manage, entitlement, organizationGuidPrefix)))
);

return http.build();
}

private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();

return (userRequest) -> {
// Delegate to the default implementation for loading a user
return delegate.loadUser(userRequest);
};
}

private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package access.config;
package access.security;

import access.exception.UserRestrictionException;
import access.manage.Manage;
import access.model.User;
import access.repository.UserRepository;
import access.security.InstitutionAdmin;
import access.security.SuperAdmin;
import org.springframework.core.MethodParameter;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
Expand All @@ -17,22 +14,20 @@
import org.springframework.web.method.support.ModelAndViewContainer;

import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static access.security.InstitutionAdmin.INSTITUTION_ADMIN;

public class UserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

private final UserRepository userRepository;
private final SuperAdmin superAdmin;
private final InstitutionAdmin institutionAdmin;
private final Manage manage;

public UserHandlerMethodArgumentResolver(UserRepository userRepository, SuperAdmin superAdmin, InstitutionAdmin institutionAdmin, Manage manage) {

public UserHandlerMethodArgumentResolver(UserRepository userRepository, SuperAdmin superAdmin) {
this.userRepository = userRepository;
this.superAdmin = superAdmin;
this.institutionAdmin = institutionAdmin;
this.manage = manage;
}

public boolean supportsParameter(MethodParameter methodParameter) {
Expand Down Expand Up @@ -62,7 +57,7 @@ public User resolveArgument(MethodParameter methodParameter,
.map(adminSub -> userRepository.save(new User(true, attributes)))
)
.or(() -> {
if (this.isInstitutionAdmin(attributes)) {
if ((boolean) attributes.get(INSTITUTION_ADMIN)) {
User user = new User(attributes);
userRepository.save(user);
return Optional.of(user);
Expand All @@ -85,50 +80,11 @@ public User resolveArgument(MethodParameter methodParameter,
return optionalUser.map(user -> {
if (user.getId() != null) {
user.updateAttributes(attributes);
this.updateUser(user, attributes);
userRepository.save(user);
}
if (user.isInstitutionAdmin() && StringUtils.hasText(user.getOrganizationGUID())) {
user.setApplications(manage.providersByInstitutionalGUID(user.getOrganizationGUID()));
}
return user;
}).orElseThrow(UserRestrictionException::new);

}

private boolean isInstitutionAdmin(Map<String, Object> attributes) {
if (attributes.containsKey("eduperson_entitlement")) {
List<String> entitlements = ((List<String>) attributes.get("eduperson_entitlement"))
.stream().map(String::toLowerCase).toList();
if (entitlements.contains(this.institutionAdmin.getEntitlement().toLowerCase())) {
return true;
}
}
return false;
}

private User updateUser(User user, Map<String, Object> attributes) {
if (attributes.containsKey("eduperson_entitlement")) {
List<String> entitlements = ((List<String>) attributes.get("eduperson_entitlement"))
.stream().map(String::toLowerCase).toList();
user.setInstitutionAdmin(entitlements.contains(this.institutionAdmin.getEntitlement().toLowerCase()));
String organizationGUIPrefix = this.institutionAdmin.getOrganizationGuidPrefix().toLowerCase();
boolean hasOrganizationPrefix = false;
//lambda requires final variables
for (String entitlement : entitlements) {
if (entitlement.startsWith(organizationGUIPrefix)) {
user.setOrganizationGUID(entitlement.substring(this.institutionAdmin.getOrganizationGuidPrefix().length()));
hasOrganizationPrefix = true;
break;
}
}
if (!hasOrganizationPrefix) {
user.setOrganizationGUID(null);
}
} else {
user.setInstitutionAdmin(false);
user.setOrganizationGUID(null);
}
return user;
}
}

0 comments on commit 9b324a8

Please sign in to comment.