diff --git a/client/src/components/Entities.scss b/client/src/components/Entities.scss index d587f930..42654f4d 100644 --- a/client/src/components/Entities.scss +++ b/client/src/components/Entities.scss @@ -29,6 +29,10 @@ button { margin-left: 25px; + @media (max-width: $medium) { + margin-left: 0; + margin-top: 15px + } } h2 { diff --git a/client/src/utils/UserRole.js b/client/src/utils/UserRole.js index 8def2a8c..186a000f 100644 --- a/client/src/utils/UserRole.js +++ b/client/src/utils/UserRole.js @@ -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 } diff --git a/server/src/main/java/access/model/User.java b/server/src/main/java/access/model/User.java index c25a1cdc..49694c31 100644 --- a/server/src/main/java/access/model/User.java +++ b/server/src/main/java/access/model/User.java @@ -15,6 +15,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static access.security.InstitutionAdmin.*; + @Entity(name = "users") @NoArgsConstructor @Getter @@ -87,8 +89,12 @@ public User(boolean superUser, Map 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>) 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)) { @@ -169,6 +175,9 @@ public void updateAttributes(Map 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>) attributes.getOrDefault(APPLICATIONS, Collections.emptyList()); this.lastActivity = Instant.now(); } diff --git a/server/src/main/java/access/security/CustomOidcUserService.java b/server/src/main/java/access/security/CustomOidcUserService.java new file mode 100644 index 00000000..5c6639c6 --- /dev/null +++ b/server/src/main/java/access/security/CustomOidcUserService.java @@ -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 { + 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 claims = oidcUser.getUserInfo().getClaims(); + Map 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> applications = manage.providersByInstitutionalGUID(organizationGuid); + newClaims.put(APPLICATIONS, applications); + } + OidcUserInfo oidcUserInfo = new OidcUserInfo(newClaims); + oidcUser = new DefaultOidcUser(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUserInfo); + return oidcUser; + + } +} diff --git a/server/src/main/java/access/security/InstitutionAdmin.java b/server/src/main/java/access/security/InstitutionAdmin.java index 20090e1f..46f8063d 100644 --- a/server/src/main/java/access/security/InstitutionAdmin.java +++ b/server/src/main/java/access/security/InstitutionAdmin.java @@ -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 attributes, String requiredEntitlement) { + if (attributes.containsKey("eduperson_entitlement")) { + List entitlements = (List) attributes.get("eduperson_entitlement"); + return entitlements.stream().anyMatch(entitlement -> entitlement.equalsIgnoreCase(requiredEntitlement)); + } + return false; + } + + public static Optional getOrganizationGuid(Map attributes, String organizationGuidPrefix) { + if (attributes.containsKey("eduperson_entitlement")) { + List entitlements = (List) 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(); + } +} \ No newline at end of file diff --git a/server/src/main/java/access/security/SecurityConfig.java b/server/src/main/java/access/security/SecurityConfig.java index 48e8d2da..d4a80def 100644 --- a/server/src/main/java/access/security/SecurityConfig.java +++ b/server/src/main/java/access/security/SecurityConfig.java @@ -1,6 +1,5 @@ package access.security; -import access.config.UserHandlerMethodArgumentResolver; import access.exception.ExtendedErrorAttributes; import access.manage.Manage; import access.model.Invitation; @@ -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; @@ -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 @@ -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 argumentResolvers) { - argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin, institutionAdmin, manage)); + argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin)); } @Override @@ -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") @@ -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 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 = diff --git a/server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java b/server/src/main/java/access/security/UserHandlerMethodArgumentResolver.java similarity index 60% rename from server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java rename to server/src/main/java/access/security/UserHandlerMethodArgumentResolver.java index 6c7fca62..a441a16e 100755 --- a/server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java +++ b/server/src/main/java/access/security/UserHandlerMethodArgumentResolver.java @@ -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; @@ -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) { @@ -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); @@ -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 attributes) { - if (attributes.containsKey("eduperson_entitlement")) { - List entitlements = ((List) 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 attributes) { - if (attributes.containsKey("eduperson_entitlement")) { - List entitlements = ((List) 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; - } } \ No newline at end of file