Skip to content

Commit

Permalink
Teams API endpoint for migration
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 27, 2023
1 parent 4038276 commit adea65c
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 9 deletions.
3 changes: 0 additions & 3 deletions server/src/main/java/access/api/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,6 @@ private void verifyMissingAttributes(User user, Config result) {
if (!StringUtils.hasText(user.getSub())) {
missingAttributes.add("sub");
}
if (!StringUtils.hasText(user.getEduPersonPrincipalName())) {
missingAttributes.add("eduPersonPrincipalName");
}
if (!StringUtils.hasText(user.getEmail())) {
missingAttributes.add("email");
}
Expand Down
7 changes: 7 additions & 0 deletions server/src/main/java/access/model/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public class Role implements Serializable, Provisionable {
@Column(name = "landing_page")
private String landingPage;

@Column(name = "urn")
private String urn;

@Column(name = "default_expiry_days")
private Integer defaultExpiryDays;

Expand All @@ -56,6 +59,10 @@ public class Role implements Serializable, Provisionable {
@Column(name = "override_settings_allowed")
private boolean overrideSettingsAllowed;

@Column(name = "teams_origin")
private boolean teamsOrigin;


@Column(name = "identifier")
private String identifier;

Expand Down
1 change: 0 additions & 1 deletion server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public class User implements Serializable, Provisionable {
private boolean superUser;

@Column(name = "eduperson_principal_name")
@NotNull
private String eduPersonPrincipalName;

@Column(name = "given_name")
Expand Down
5 changes: 5 additions & 0 deletions server/src/main/java/access/provision/scim/GroupURN.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ public static String urnFromRole(String groupUrnPrefix, Role role) {
role.getShortName());
}

public static String teamsUrnFromRole(String teamsNameContext, Role role) {
return String.format("%s:%s",
teamsNameContext,
role.getUrn());
}
}
19 changes: 18 additions & 1 deletion server/src/main/java/access/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public class SecurityConfig {
private final String attributeAggregationPassword;
private final String lifeCycleUser;
private final String lifeCyclePassword;
private final String teamsUser;
private final String teamsPassword;

@Autowired
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository,
Expand All @@ -69,6 +71,8 @@ public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository,
@Value("${voot.password}") String vootPassword,
@Value("${lifecyle.user}") String lifeCycleUser,
@Value("${lifecyle.password}") String lifeCyclePassword,
@Value("${teams.user}") String teamsUser,
@Value("${teams.password}") String teamsPassword,
@Value("${attribute-aggregation.user}") String attributeAggregationUser,
@Value("${attribute-aggregation.password}") String attributeAggregationPassword) {
this.clientRegistrationRepository = clientRegistrationRepository;
Expand All @@ -81,6 +85,8 @@ public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository,
this.vootPassword = vootPassword;
this.lifeCycleUser = lifeCycleUser;
this.lifeCyclePassword = lifeCyclePassword;
this.teamsUser = teamsUser;
this.teamsPassword = teamsPassword;
this.attributeAggregationUser = attributeAggregationUser;
this.attributeAggregationPassword = attributeAggregationPassword;
}
Expand Down Expand Up @@ -178,6 +184,8 @@ SecurityFilterChain basicAuthenticationSecurityFilterChain(HttpSecurity http) th
.securityMatcher(
"/api/voot/**",
"/api/external/v1/voot/**",
"/api/teams/**",
"/api/external/v1/teams/**",
"/api/aa/**",
"/api/external/v1/aa/**",
"/api/deprovisioning/**",
Expand Down Expand Up @@ -231,12 +239,21 @@ public InMemoryUserDetailsManager userDetailsService() {
.password("{noop}" + attributeAggregationPassword)
.roles("ATTRIBUTE_AGGREGATION")
.build();
UserDetails teamsUserDetails = User
.withUsername(teamsUser)
.password("{noop}" + teamsPassword)
.roles("TEAMS")
.build();
UserDetails lifeCyleUserDetails = User
.withUsername(lifeCycleUser)
.password("{noop}" + lifeCyclePassword)
.roles("LIFECYCLE")
.build();
return new InMemoryUserDetailsManager(vootUserDetails, attributeAggregationUserDetails, lifeCyleUserDetails);
return new InMemoryUserDetailsManager(
vootUserDetails,
attributeAggregationUserDetails,
lifeCyleUserDetails,
teamsUserDetails);
}

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

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Membership implements Serializable {

private Person person;
private Role role;

}
18 changes: 18 additions & 0 deletions server/src/main/java/access/teams/Person.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package access.teams;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {

private String urn;
private String name;
private String email;
private String schacHomeOrganization;
}
7 changes: 7 additions & 0 deletions server/src/main/java/access/teams/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package access.teams;

public enum Role {

MEMBER, MANAGER, ADMIN, OWNER;

}
23 changes: 23 additions & 0 deletions server/src/main/java/access/teams/Team.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package access.teams;

import access.model.Application;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Team implements Serializable {

private String urn;
private String name;
private String description;
private String landingPage;
private List<Membership> memberships;
private List<Application> applications;

}
121 changes: 121 additions & 0 deletions server/src/main/java/access/teams/TeamsController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package access.teams;

import access.exception.NotFoundException;
import access.exception.RemoteException;
import access.manage.Manage;
import access.model.Role;
import access.model.*;
import access.provision.ProvisioningService;
import access.provision.scim.GroupURN;
import access.provision.scim.OperationType;
import access.repository.RoleRepository;
import access.repository.UserRepository;
import access.repository.UserRoleRepository;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.rmi.Remote;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import static access.SwaggerOpenIdConfig.ATTRIBUTE_AGGREGATION_SCHEME_NAME;

@RestController
@RequestMapping(value = {"/api/teams", "/api/external/v1/teams"}, produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = ATTRIBUTE_AGGREGATION_SCHEME_NAME)
public class TeamsController {

private static final int DEFAULT_EXPIRY_DAYS = 5 * 365;

private final RoleRepository roleRepository;
private final UserRepository userRepository;
private final UserRoleRepository userRoleRepository;
private final Manage manage;
private final ProvisioningService provisioningService;

public TeamsController(RoleRepository roleRepository,
UserRepository userRepository,
UserRoleRepository userRoleRepository,
Manage manage,
ProvisioningService provisioningService) {
this.roleRepository = roleRepository;
this.userRepository = userRepository;
this.userRoleRepository = userRoleRepository;
this.manage = manage;
this.provisioningService = provisioningService;
}

@PostMapping("")
@PreAuthorize("hasRole('TEAMS')")
@Transactional
public ResponseEntity<Void> migrateTeam(@RequestBody Team team) {
Role role = new Role();
role.setName(team.getName());
role.setShortName(GroupURN.sanitizeRoleShortName(role.getName()));
role.setDescription(team.getDescription());
role.setUrn(team.getUrn());
role.setLandingPage(team.getLandingPage());
role.setDefaultExpiryDays(DEFAULT_EXPIRY_DAYS);
role.setIdentifier(UUID.randomUUID().toString());
role.setTeamsOrigin(true);
//Check if the applications exist in Manage
Set<Application> applications = team.getApplications().stream().filter(this::applicationExists).collect(Collectors.toSet());
if (applications.isEmpty()) {
throw new NotFoundException();
}
role.setApplications(applications);
Role savedRole = roleRepository.save(role);

provisioningService.newGroupRequest(savedRole);

List<Membership> memberships = team.getMemberships();
memberships.forEach(membership -> this.provision(savedRole, membership));

return ResponseEntity.status(201).build();
}

private boolean applicationExists(Application application) {
try {
manage.providerById(application.getManageType(), application.getManageId());
return true;
} catch (RuntimeException e) {
return false;
}
}

private void provision(Role role, Membership membership) {
Person person = membership.getPerson();
Optional<User> optionalUser = userRepository.findBySubIgnoreCase(person.getUrn());
User user = optionalUser.orElseGet(() -> {
User newUser = new User();
newUser.setSub(person.getUrn());
newUser.setName(person.getName());
newUser.setEmail(person.getEmail());
newUser.setSchacHomeOrganization(person.getSchacHomeOrganization());
return userRepository.save(newUser);
});
UserRole userRole = new UserRole();
userRole.setInviter("teams_migration");
userRole.setUser(user);
userRole.setRole(role);
Instant now = Instant.now();
userRole.setCreatedAt(now);
userRole.setEndDate(now.plus(DEFAULT_EXPIRY_DAYS, ChronoUnit.DAYS));
userRole.setAuthority(membership.getRole().equals(access.teams.Role.MEMBER) ? Authority.GUEST : Authority.INVITER);
userRole = userRoleRepository.save(userRole);

provisioningService.updateGroupRequest(userRole, OperationType.Add);
}
}
9 changes: 7 additions & 2 deletions server/src/main/java/access/voot/VootController.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ public class VootController {

private final UserRepository userRepository;
private final String groupUrnPrefix;
private final String teamsNameContext;

public VootController(UserRepository userRepository, @Value("${voot.group_urn_domain}") String groupUrnPrefix) {
public VootController(UserRepository userRepository,
@Value("${voot.group_urn_domain}") String groupUrnPrefix,
@Value("${teams.group-name-context}") String teamsNameContext) {
this.userRepository = userRepository;
this.groupUrnPrefix = groupUrnPrefix;
this.teamsNameContext = teamsNameContext;
}

@GetMapping("/{unspecified_id}")
Expand All @@ -51,7 +55,8 @@ public ResponseEntity<List<Map<String, String>>> getGroupMemberships(@PathVariab
private Map<String, String> parseUserRole(UserRole userRole) {
Map<String, String> res = new HashMap<>();
Role role = userRole.getRole();
res.put("urn", GroupURN.urnFromRole(groupUrnPrefix, userRole.getRole()));
String urn = role.isTeamsOrigin() ? GroupURN.teamsUrnFromRole(teamsNameContext, role) : GroupURN.urnFromRole(groupUrnPrefix, userRole.getRole());
res.put("urn", urn);
res.put("name", role.getName());
return res;
}
Expand Down
5 changes: 5 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ voot:
password: secret
group_urn_domain: urn:mace:surf.nl:test.surfaccess.nl

teams:
user: teams
password: secret
group-name-context: "urn:collab:group:test.surfteams.nl:"

attribute-aggregation:
user: aa
password: secret
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `users` MODIFY `eduperson_principal_name` varchar(255) DEFAULT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `roles` DROP INDEX `roles_unique_short_name`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE `roles`
add `urn` varchar(255) DEFAULT NULL;
ALTER TABLE `roles`
add `teams_origin` bool DEFAULT 0;
3 changes: 2 additions & 1 deletion server/src/test/java/access/AbstractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,8 @@ protected void stubForManageProviderByOrganisationGUID(String organisationGUID)

}

protected void stubForManageProviderById(EntityType entityType, String id) throws JsonProcessingException {
@SneakyThrows
protected void stubForManageProviderById(EntityType entityType, String id) {
String path = String.format("/manage/api/internal/metadata/%s/%s", entityType.name().toLowerCase(), id);
String body = objectMapper.writeValueAsString(localManage.providerById(entityType, id));
stubFor(get(urlPathMatching(path)).willReturn(aResponse()
Expand Down
2 changes: 1 addition & 1 deletion server/src/test/java/access/api/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void configMissingAttributes() throws Exception {
.get("/api/v1/users/config")
.as(Map.class);
assertFalse((Boolean) res.get("authenticated"));
assertEquals(2, ((List) res.get("missingAttributes")).size());
assertEquals(1, ((List) res.get("missingAttributes")).size());
}

@Test
Expand Down
Loading

0 comments on commit adea65c

Please sign in to comment.