Skip to content

Commit

Permalink
Finished invite for institution admin
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Dec 9, 2023
1 parent 1752ceb commit 7af4f5b
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 96 deletions.
17 changes: 13 additions & 4 deletions server/src/main/java/access/api/HasManage.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
import access.model.GroupedProviders;
import access.model.Role;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.stream.Collectors;

import static access.security.InstitutionAdmin.*;

public interface HasManage {

Manage getManage() ;
Expand All @@ -32,7 +31,17 @@ default List<GroupedProviders> getGroupedProviders(List<Role> requestedRoles) {
.anyMatch(application -> application.getManageId().equals(id))).toList(), UUID.randomUUID().toString());
})
.toList();
}

default Map<String, Object> enrichInstitutionAdmin(String organizationGUID) {
Map<String, Object> claims = new HashMap<>();
claims.put(INSTITUTION_ADMIN, true);
claims.put(ORGANIZATION_GUID, organizationGUID);
List<Map<String, Object>> applications = getManage().providersByInstitutionalGUID(organizationGUID);
claims.put(APPLICATIONS, applications);
Optional<Map<String, Object>> identityProvider = getManage().identityProviderByInstitutionalGUID(organizationGUID);
claims.put(INSTITUTION, identityProvider.orElse(null));
return claims;
}

}
56 changes: 39 additions & 17 deletions server/src/main/java/access/api/InvitationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import access.validation.EmailFormatValidator;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand All @@ -27,8 +29,9 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -59,23 +62,26 @@ public class InvitationController implements HasManage {
private final InvitationRepository invitationRepository;
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final EmailFormatValidator emailFormatValidator = new EmailFormatValidator();
private final ProvisioningService provisioningService;
private final SecurityContextRepository securityContextRepository;
private final SuperAdmin superAdmin;
private final EmailFormatValidator emailFormatValidator = new EmailFormatValidator();

public InvitationController(MailBox mailBox,
Manage manage,
InvitationRepository invitationRepository,
UserRepository userRepository,
RoleRepository roleRepository,
ProvisioningService provisioningService,
SecurityContextRepository securityContextRepository,
SuperAdmin superAdmin) {
this.mailBox = mailBox;
this.manage = manage;
this.invitationRepository = invitationRepository;
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.provisioningService = provisioningService;
this.securityContextRepository = securityContextRepository;
this.superAdmin = superAdmin;
}

Expand Down Expand Up @@ -191,7 +197,9 @@ public ResponseEntity<List<Invitation>> all(@Parameter(hidden = true) User user)

@PostMapping("accept")
public ResponseEntity<Map<String, String>> accept(@Validated @RequestBody AcceptInvitation acceptInvitation,
Authentication authentication) {
Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
Invitation invitation = invitationRepository.findByHash(acceptInvitation.hash()).orElseThrow(NotFoundException::new);
if (!invitation.getId().equals(acceptInvitation.invitationId())) {
throw new NotFoundException();
Expand Down Expand Up @@ -255,21 +263,9 @@ public ResponseEntity<Map<String, String>> accept(@Validated @RequestBody Accept
if (invitation.getIntendedAuthority().equals(Authority.INSTITUTION_ADMIN)) {
user.setInstitutionAdmin(true);
user.setOrganizationGUID(inviter.getOrganizationGUID());
//Corner case - a new institution admin has logged in, but was not enriched by the CustomOidcUserService
//Rare case - a new institution admin has logged in, but was not yet enriched by the CustomOidcUserService
if (optionalUser.isEmpty()) {
OAuth2AuthenticationToken existingToken = (OAuth2AuthenticationToken) authentication;
DefaultOidcUser existingTokenPrincipal = (DefaultOidcUser) existingToken.getPrincipal();
existingTokenPrincipal.getClaims()
existingTokenPrincipal.getAttributes()
DefaultOidcUser oidcUser = new DefaultOidcUser(

)
OAuth2AuthenticationToken newToken = new OAuth2AuthenticationToken(
existingToken.getPrincipal(),
existingToken.getAuthorities(),
existingToken.getAuthorizedClientRegistrationId()
);
SecurityContextHolder.getContext().setAuthentication(newToken);
saveOAuth2AuthenticationToken(authentication, user, servletRequest, servletResponse);
}
}
userRepository.save(user);
Expand All @@ -288,6 +284,32 @@ public ResponseEntity<Map<String, String>> accept(@Validated @RequestBody Accept
return ResponseEntity.status(HttpStatus.CREATED).body(body);
}

private void saveOAuth2AuthenticationToken(Authentication authentication,
User user,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthenticationToken existingToken = (OAuth2AuthenticationToken) authentication;
DefaultOidcUser existingTokenPrincipal = (DefaultOidcUser) existingToken.getPrincipal();
//claims of the tokenPricipal are immutable
Map<String, Object> claims = new HashMap<>(existingTokenPrincipal.getClaims());
claims.putAll(enrichInstitutionAdmin(user.getOrganizationGUID()));
DefaultOidcUser oidcUser = new DefaultOidcUser(
existingToken.getAuthorities(),
existingTokenPrincipal.getIdToken(),
new OidcUserInfo(claims)
);
OAuth2AuthenticationToken newToken = new OAuth2AuthenticationToken(
oidcUser,
existingToken.getAuthorities(),
existingToken.getAuthorizedClientRegistrationId()
);
SecurityContextHolder.getContext().setAuthentication(newToken);
//New in Spring security 6.x
securityContextRepository.saveContext(SecurityContextHolder.getContext(), servletRequest, servletResponse);


}

@GetMapping("roles/{roleId}")
public ResponseEntity<List<Invitation>> byRole(@PathVariable("roleId") Long roleId, @Parameter(hidden = true) User user) {
LOG.debug("/me");
Expand Down
30 changes: 0 additions & 30 deletions server/src/main/java/access/config/ApplicationConverter.java

This file was deleted.

8 changes: 4 additions & 4 deletions server/src/main/java/access/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,22 @@ public Config(Config base) {
}

public Config withAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
this.setAuthenticated(authenticated);
return this;
}

public Config withName(String name) {
this.name = name;
this.setName(name);
return this;
}

public Config withMissingAttributes(List<String> missingAttributes) {
this.missingAttributes = missingAttributes;
this.setMissingAttributes(missingAttributes);
return this;
}

public Config withGroupUrnPrefix(String groupUrnPrefix) {
this.groupUrnPrefix = groupUrnPrefix;
this.setGroupUrnPrefix(groupUrnPrefix);
return this;
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/main/java/access/model/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import lombok.*;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Entity(name = "applications")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = {"manageId", "manageType"})
Expand All @@ -36,7 +36,7 @@ public class Application implements Serializable {

@ManyToMany(mappedBy = "applications")
@JsonIgnore
private Set<Role> roles;
private Set<Role> roles = new HashSet<>();

public Application(String manageId, EntityType manageType) {
this.manageId = manageId;
Expand Down
4 changes: 0 additions & 4 deletions server/src/main/java/access/model/Role.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package access.model;


import access.config.ApplicationConverter;
import access.provision.scim.GroupURN;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import org.hibernate.annotations.Formula;

import java.io.Serializable;
Expand Down
25 changes: 0 additions & 25 deletions server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,29 +220,4 @@ public Optional<UserRole> latestUserRole() {
return this.userRoles.stream().max(Comparator.comparing(UserRole::getCreatedAt));
}

@JsonIgnore
private static String resolveSub(UserRoleProvisioning userRoleProvisioning) {
if (StringUtils.hasText(userRoleProvisioning.sub)) {
return userRoleProvisioning.sub;
}
String schacHome = null;
String uid = null;
if (StringUtils.hasText(userRoleProvisioning.schacHomeOrganization)) {
schacHome = userRoleProvisioning.schacHomeOrganization;
}
String eppn = userRoleProvisioning.eduPersonPrincipalName;
if (StringUtils.hasText(eppn) && eppn.contains("@")) {
uid = eppn.substring(0, eppn.indexOf("@"));
schacHome = schacHome != null ? schacHome : eppn.substring(eppn.indexOf("@") + 1);
}
String mail = userRoleProvisioning.email;
if (StringUtils.hasText(mail)) {
uid = uid != null ? uid : mail.substring(0, mail.indexOf("@"));
schacHome = schacHome != null ? schacHome : mail.substring(mail.indexOf("@") + 1);
}
if (schacHome == null || uid == null) {
throw new IllegalArgumentException("Can't resolve sub from " + userRoleProvisioning);
}
return String.format("urn:collab:person:%s:%s", schacHome, uid);
}
}
15 changes: 10 additions & 5 deletions server/src/main/java/access/security/CustomOidcUserService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package access.security;

import access.api.HasManage;
import access.manage.Manage;
import access.model.User;
import access.repository.UserRepository;
Expand All @@ -19,7 +20,7 @@

import static access.security.InstitutionAdmin.*;

public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser>, HasManage {

private final Manage manage;
private final UserRepository userRepository;
Expand Down Expand Up @@ -53,10 +54,9 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
newClaims.put(ORGANIZATION_GUID, organizationGuid);

if (institutionAdmin && StringUtils.hasText(organizationGuid)) {
List<Map<String, Object>> applications = manage.providersByInstitutionalGUID(organizationGuid);
newClaims.put(APPLICATIONS, applications);
Optional<Map<String, Object>> identityProvider = manage.identityProviderByInstitutionalGUID(organizationGuid);
newClaims.put(INSTITUTION, identityProvider.orElse(null));
Map<String, Object> manageClaims = enrichInstitutionAdmin(organizationGuid);
newClaims.put(APPLICATIONS, manageClaims.get(APPLICATIONS));
newClaims.put(INSTITUTION, manageClaims.get(INSTITUTION));
}
optionalUser.ifPresent(user -> {
user.updateAttributes(newClaims);
Expand All @@ -67,4 +67,9 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
return oidcUser;

}

@Override
public Manage getManage() {
return manage;
}
}
20 changes: 17 additions & 3 deletions server/src/main/java/access/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
Expand All @@ -26,6 +25,10 @@
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
Expand All @@ -35,7 +38,8 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

import java.util.*;
import java.util.List;
import java.util.Locale;

@EnableWebSecurity
@EnableScheduling
Expand Down Expand Up @@ -162,11 +166,21 @@ SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http,
)
).userInfoEndpoint(userInfo -> userInfo.oidcUserService(
new CustomOidcUserService(manage, userRepository, entitlement, organizationGuidPrefix)))
);
)
//We need a reference to the securityContextRepository to update the authentication after an InstitutionAdmin invitation accept
.securityContext(securityContext -> securityContext.securityContextRepository(this.securityContextRepository()));

return http.build();
}

@Bean
public SecurityContextRepository securityContextRepository() {
return new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
);
}

private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import access.model.User;
import access.repository.APITokenRepository;
import access.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
Expand All @@ -26,6 +27,7 @@

import static access.security.InstitutionAdmin.INSTITUTION_ADMIN;
import static access.security.SecurityConfig.API_TOKEN_HEADER;
import static org.springframework.security.web.context.RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME;

public class UserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

Expand Down Expand Up @@ -115,10 +117,12 @@ public User resolveArgument(MethodParameter methodParameter,
}
return user;
});
String requestURI = ((ServletWebRequest) webRequest).getRequest().getRequestURI();
HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
String requestURI = request.getRequestURI();
if (optionalUser.isEmpty() && requestURI.equals("/api/v1/users/config")) {
return new User(attributes);
}
Object attribute = request.getAttribute(DEFAULT_REQUEST_ATTR_NAME);
return optionalUser.map(user -> {
if (user.isInstitutionAdmin() && StringUtils.hasText(user.getOrganizationGUID())) {
String organizationGUID = user.getOrganizationGUID();
Expand Down
Loading

0 comments on commit 7af4f5b

Please sign in to comment.