Skip to content

Commit

Permalink
WIP for institution admin provisioning
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Sep 25, 2023
1 parent 6a8f7cc commit 1fc9794
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 37 deletions.
12 changes: 8 additions & 4 deletions client/src/utils/UserRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@ export const INVITATION_STATUS = {

export const AUTHORITIES = {
SUPER_USER: "SUPER_USER",
INSTITUTION_ADMIN: "INSTITUTION_ADMIN",
MANAGER: "MANAGER",
INVITER: "INVITER",
GUEST: "GUEST"
}

const AUTHORITIES_HIERARCHY = {
[AUTHORITIES.SUPER_USER]: 1,
[AUTHORITIES.MANAGER]: 2,
[AUTHORITIES.INVITER]: 3,
[AUTHORITIES.GUEST]: 4
[AUTHORITIES.INSTITUTION_ADMIN]: 2,
[AUTHORITIES.MANAGER]: 3,
[AUTHORITIES.INVITER]: 4,
[AUTHORITIES.GUEST]: 5
}

export const highestAuthority = user => {
if (user.superUser) {
return AUTHORITIES.SUPER_USER;
}
if (user.institutionAdmin) {
return AUTHORITIES.INSTITUTION_ADMIN;
}
return (user.userRoles || []).reduce((acc, u) => {
if (AUTHORITIES_HIERARCHY[acc] > AUTHORITIES_HIERARCHY[AUTHORITIES[u.authority]]) {
return u.authority
Expand Down Expand Up @@ -130,7 +135,6 @@ export const allowedAuthoritiesForInvitation = (user, selectedRoles) => {
}
//Return only the AUTHORITIES where the user has the correct authority per selectedRole
const userRolesForSelectedRoles = selectedRoles
//TODO Remove this hack and require only really roles
.map(role => role.isUserRole ? role.role : role)
.map(role => user.userRoles.find(userRole => userRole.role.manageId === role.manageId || userRole.role.id === role.id))
.filter(userRole => !isEmpty(userRole));
Expand Down
4 changes: 4 additions & 0 deletions server/src/main/java/access/api/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ public ResponseEntity<User> me(@Parameter(hidden = true) User user) {
LOG.debug("/me");
List<Role> roles = user.getUserRoles().stream().map(UserRole::getRole).toList();
manage.deriveRemoteApplications(roles);
if (user.isInstitutionAdmin() && StringUtils.hasText(user.getOrganizationGUID())) {
List<Map<String, Object>> applications = manage.providersByInstitutionalGUID(user.getOrganizationGUID());
user.setApplications(applications);
}
return ResponseEntity.ok(user);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import access.exception.UserRestrictionException;
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;
Expand All @@ -15,18 +16,20 @@
import org.springframework.web.method.support.ModelAndViewContainer;

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

public class UserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

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

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

public boolean supportsParameter(MethodParameter methodParameter) {
Expand Down Expand Up @@ -54,7 +57,17 @@ public User resolveArgument(MethodParameter methodParameter,
superAdmin.getUsers().stream().filter(adminSub -> adminSub.equals(sub))
.findFirst()
.map(adminSub -> userRepository.save(new User(true, attributes)))
).map(user -> {
)
.or(() -> {
if (this.isInstitutionAdmin(attributes)) {
User user = new User(attributes);
userRepository.save(user);
return Optional.of(user);
} else {
return Optional.empty();
}
})
.map(user -> {
String impersonateId = webRequest.getHeader("X-IMPERSONATE-ID");
if (StringUtils.hasText(impersonateId) && user.isSuperUser()) {
return userRepository.findById(Long.valueOf(impersonateId))
Expand All @@ -69,10 +82,47 @@ public User resolveArgument(MethodParameter methodParameter,
return optionalUser.map(user -> {
if (user.getId() != null) {
user.updateAttributes(attributes);
this.updateUser(user, attributes);
userRepository.save(user);
}
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;
}
}
18 changes: 16 additions & 2 deletions server/src/main/java/access/manage/LocalManage.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -30,9 +31,11 @@ private List<Map<String, Object>> initialize(ObjectMapper objectMapper, EntityTy
}

@Override
public List<Map<String, Object>> providers(EntityType entityType) {
public List<Map<String, Object>> providers(EntityType... entityTypes) {
//Ensure it is immutable
return addIdentifierAlias(this.allProviders.get(entityType).stream().toList());
return addIdentifierAlias(Stream.of(entityTypes).map(entityType -> this.allProviders.get(entityType).stream().toList())
.flatMap(List::stream)
.toList());
}

@Override
Expand Down Expand Up @@ -82,4 +85,15 @@ public List<Map<String, Object>> allowedEntries(EntityType entityType, String id
})
.collect(Collectors.toList()));
}

@Override
public List<Map<String, Object>> providersByInstitutionalGUID(String organisationGUID) {
List<Map<String, Object>> providers = providers(EntityType.SAML20_SP, EntityType.OIDC10_RP);
return providers
.stream()
.filter(provider -> Objects.equals(((Map<String, Object>) ((Map<String, Object>) provider.get("data"))
.get("metaDataFields"))
.get("coin:institution_guid"), organisationGUID))
.toList();
}
}
4 changes: 3 additions & 1 deletion server/src/main/java/access/manage/Manage.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

public interface Manage {

List<Map<String, Object>> providers(EntityType entityType);
List<Map<String, Object>> providers(EntityType... entityTypes);

Map<String, Object> providerById(EntityType entityType, String id);

Expand All @@ -20,6 +20,8 @@ public interface Manage {

List<Map<String, Object>> allowedEntries(EntityType entityType, String id);

List<Map<String, Object>> providersByInstitutionalGUID(String organisationGUID);

//Due to the different API's we are using, the result sometimes contains an "_id" and sometimes an "id"
default List<Map<String, Object>> addIdentifierAlias(List<Map<String, Object>> providers) {
providers.forEach(this::addIdentifierAlias);
Expand Down
29 changes: 22 additions & 7 deletions server/src/main/java/access/manage/RemoteManage.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.web.client.RestTemplate;
Expand All @@ -14,6 +13,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@SuppressWarnings("unchecked")
public class RemoteManage implements Manage {
Expand All @@ -30,8 +30,10 @@ public RemoteManage(String url, String user, String password, ObjectMapper objec
}

@Override
public List<Map<String, Object>> providers(EntityType entityType) {
return getRemoteMetaData(entityType.collectionName());
public List<Map<String, Object>> providers(EntityType... entityTypes) {
return addIdentifierAlias(Stream.of(entityTypes).map(entityType -> this.getRemoteMetaData(entityType.collectionName()))
.flatMap(List::stream)
.toList());
}

@Override
Expand All @@ -46,7 +48,7 @@ public List<Map<String, Object>> providersByIdIn(EntityType entityType, List<Str
public Optional<Map<String, Object>> providerByEntityID(EntityType entityType, String entityID) {
String query = URLEncoder.encode(String.format("{\"data.entityid\":\"%s\"}", entityID), Charset.defaultCharset());
String queryUrl = String.format("%s/manage/api/internal/rawSearch/%s?query=%s", url, entityType.collectionName(), query);
List<Map<String, Object>> providers = addIdentifierAlias(restTemplate.getForEntity(queryUrl, List.class).getBody());
List<Map<String, Object>> providers = addIdentifierAlias(restTemplate.getForEntity(queryUrl, List.class).getBody());
return providers.isEmpty() ? Optional.empty() : Optional.of(providers.get(0));
}

Expand All @@ -57,7 +59,6 @@ public Map<String, Object> providerById(EntityType entityType, String id) {
}



@Override
public List<Map<String, Object>> provisioning(List<String> ids) {
String queryUrl = String.format("%s/manage/api/internal/provisioning", url);
Expand All @@ -66,14 +67,28 @@ public List<Map<String, Object>> provisioning(List<String> ids) {

@Override
public List<Map<String, Object>> allowedEntries(EntityType entityType, String id) {
String queryUrl = String.format("%s/manage/api/internal/allowedEntities/%s/%s", url, entityType, id);
String queryUrl = String.format("%s/manage/api/internal/allowedEntities/%s/%s", url, entityType.collectionName(), id);
return addIdentifierAlias(restTemplate.getForEntity(queryUrl, List.class).getBody());
}

@Override
public List<Map<String, Object>> providersByInstitutionalGUID(String organisationGUID) {
Map<String, Object> baseQuery = (Map<String, Object>) this.queries.get("base_query");
baseQuery.put("coin:institution_guid", organisationGUID);
List serviceProviders = restTemplate.postForObject(
String.format("%s/manage/api/internal/search/%s", this.url, EntityType.SAML20_SP.collectionName()),
baseQuery, List.class);
List relyingParties = restTemplate.postForObject(
String.format("%s/manage/api/internal/search/%s", this.url, EntityType.OIDC10_RP.collectionName()),
baseQuery, List.class);
serviceProviders.addAll(relyingParties);
return addIdentifierAlias(serviceProviders);
}

private List<Map<String, Object>> getRemoteMetaData(String type) {
Object baseQuery = this.queries.get("base_query");
String url = String.format("%s/manage/api/internal/search/%s", this.url, type);
return addIdentifierAlias(restTemplate.postForObject(url, baseQuery, List.class));

}

}
4 changes: 3 additions & 1 deletion server/src/main/java/access/model/Authority.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

public enum Authority {

SUPER_USER(3), MANAGER(2), INVITER(1), GUEST(0);
SUPER_USER(4), INSTITUTION_ADMIN(3), MANAGER(2), INVITER(1), GUEST(0);

private Map<String, Map<String, String>> translations = Map.of(
"SUPER_USER", Map.of("en", "Super user", "nl", "Super user"),
"INSTITUTION_ADMIN", Map.of("en", "Institution admin", "nl", "Instellings-admin"),
"MANAGER", Map.of("en", "Manager", "nl", "Beheerder"),
"INVITER", Map.of("en", "Inviter", "nl", "Uitnodiger"),
"GUEST", Map.of("en", "Guest", "nl", "Gast")
Expand All @@ -31,6 +32,7 @@ public Authority nextAuthorityInHierarchy() {
return switch (this.rights) {
case 0 -> INVITER;
case 1 -> MANAGER;
case 2 -> INSTITUTION_ADMIN;
default -> SUPER_USER;
};
}
Expand Down
15 changes: 11 additions & 4 deletions server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@

import java.io.Serializable;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -52,6 +49,13 @@ public class User implements Serializable, Provisionable {
@Column(name = "schac_home_organization")
private String schacHomeOrganization;

@Column(name = "organization_guid")
private String organizationGUID;

@Column(name = "institution_admin")
@NotNull
private boolean institutionAdmin;

@Column
private String email;

Expand All @@ -68,6 +72,9 @@ public class User implements Serializable, Provisionable {
@JsonIgnore
private Set<RemoteProvisionedUser> remoteProvisionedUsers = new HashSet<>();

@Transient
private List<Map<String, Object>> applications = Collections.emptyList();

public User(Map<String, Object> attributes) {
this(false, attributes);
}
Expand Down
17 changes: 17 additions & 0 deletions server/src/main/java/access/security/InstitutionAdmin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package access.security;

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

import java.util.List;

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

private String entitlement;
private String organizationGuidPrefix;

}
8 changes: 5 additions & 3 deletions server/src/main/java/access/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,23 @@ public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository,
}

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

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

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

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

@Override
Expand Down
4 changes: 4 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ super-admin:
users:
- "urn:collab:person:example.com:admin"

institution-admin:
entitlement: "urn:mace:surfnet.nl:surfnet.nl:sab:role:SURFconextverantwoordelijke"
organization-guid-prefix: "urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:"

config:
client-url: "http://localhost:3000"
welcome-url: "http://localhost:4000"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE `users`
add `institution_admin` bool DEFAULT 0;
ALTER TABLE `users`
add `organization_guid` varchar(255) DEFAULT NULL;
Loading

0 comments on commit 1fc9794

Please sign in to comment.