Skip to content

Commit d076702

Browse files
committed
WIP for #650
1 parent a292038 commit d076702

File tree

11 files changed

+153
-47
lines changed

11 files changed

+153
-47
lines changed

server/src/main/java/invite/api/InvitationController.java

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ public ResponseEntity<Map<String, Object>> accept(@Validated @RequestBody Accept
374374
}
375375
}
376376
user.setInternalPlaceholderIdentifier(invitation.getInternalPlaceholderIdentifier());
377+
if (StringUtils.hasText(invitation.getCrmContactId())) {
378+
user.setCrmContactId(invitation.getCrmContactId());
379+
user.setCrmOrganisationId(invitation.getCrmOrganisationId());
380+
}
377381
userRepository.save(user);
378382
AccessLogger.user(LOG, Event.Created, user);
379383
newUserRoles.forEach(userRole -> userRoleAuditService.logAction(userRole, UserRoleAudit.ActionType.ADD));
@@ -553,22 +557,17 @@ private void checkEmailEquality(User user, Invitation invitation) {
553557
}
554558

555559
private void checkCrmUniqueOrganisation(User user, Invitation invitation) {
556-
if (StringUtils.hasText(invitation.getCrmContactId()) && user.getUserRoles().stream()
557-
.map(userRole -> userRole.getRole().getCrmOrganisationId())
558-
.anyMatch(crmOrganisationId -> StringUtils.hasText(crmOrganisationId) &&
559-
invitation.getRoles().stream()
560-
.map(invitationRole -> invitationRole.getRole())
561-
.anyMatch(role -> StringUtils.hasText(role.getCrmOrganisationId()) && !role.getCrmOrganisationId().equals(crmOrganisationId)))) {
560+
if (StringUtils.hasText(invitation.getCrmContactId()) &&
561+
StringUtils.hasText(user.getCrmContactId()) &&
562+
user.getCrmContactId().equals(invitation.getCrmContactId()) &&
563+
!user.getCrmOrganisationId().equals(invitation.getCrmOrganisationId())) {
562564
throw new InvitationUniqueCrmOrganisationException(
563565
String.format("User %s is not allowed to accept an invitation from Organisation %s, because it already has roles for Organisation %s",
564566
user.getEmail(),
565-
user.getUserRoles().stream()
566-
.map(userRole -> userRole.getRole().getCrmOrganisationId())
567-
.toList(),
568-
invitation.getRoles().stream()
569-
.map(invitationRole -> invitationRole.getRole().getCrmOrganisationId())
570-
.toList()));
567+
invitation.getCrmOrganisationId(),
568+
user.getCrmOrganisationId()
569+
));
570+
571571
}
572572
}
573-
574573
}

server/src/main/java/invite/crm/CRMController.java

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import invite.provision.ProvisioningService;
2525
import invite.provision.scim.OperationType;
2626
import invite.repository.ApplicationRepository;
27+
import invite.repository.InvitationRepository;
2728
import invite.repository.RoleRepository;
2829
import invite.repository.UserRepository;
2930
import org.apache.commons.logging.Log;
@@ -68,6 +69,7 @@ public class CRMController {
6869
private final Map<String, CrmConfigEntry> crmConfig;
6970
private final UserRoleAuditService userRoleAuditService;
7071
private final Provisionable provisionable = () -> "SURF CRM";
72+
private final InvitationRepository invitationRepository;
7173

7274

7375
@SuppressWarnings("unchecked")
@@ -79,7 +81,8 @@ public CRMController(@Value("${crm.collab-person-prefix}") String collabPersonPr
7981
ProvisioningService provisioningService,
8082
ObjectMapper objectMapper,
8183
MailBox mailBox, Manage manage,
82-
UserRoleAuditService userRoleAuditService) throws IOException {
84+
UserRoleAuditService userRoleAuditService,
85+
InvitationRepository invitationRepository) throws IOException {
8386
this.userRepository = userRepository;
8487
this.collabPersonPrefix = collabPersonPrefix;
8588
this.roleRepository = roleRepository;
@@ -100,7 +103,8 @@ public CRMController(@Value("${crm.collab-person-prefix}") String collabPersonPr
100103
crmConfigEntry -> crmConfigEntry.code(),
101104
crmConfigEntry -> crmConfigEntry
102105
));
103-
106+
LOG.info(String.format("Parsed %s entries from %s", this.crmConfig.size(), crmConfigResource.getDescription()));
107+
this.invitationRepository = invitationRepository;
104108
}
105109

106110
@PostMapping("")
@@ -112,7 +116,7 @@ public ResponseEntity<String> contact(@RequestBody CRMContact crmContact) {
112116
if (crmContact.isSuppressInvitation()) {
113117
if (!StringUtils.hasText(crmContact.getSchacHomeOrganisation()) || !StringUtils.hasText(crmContact.getUid())) {
114118
throw new InvalidInputException(
115-
"Missing schacHomeOrganisation or uid in crmContact with sendInvitation false: " + crmContact);
119+
"Missing schacHomeOrganisation or uid in crmContact with isSuppressInvitation true: " + crmContact);
116120
}
117121
created = provisionUser(crmContact);
118122
} else {
@@ -126,8 +130,16 @@ public ResponseEntity<String> contact(@RequestBody CRMContact crmContact) {
126130
public ResponseEntity<String> delete(@RequestBody CRMContact crmContact) {
127131
LOG.debug("DELETE /api/external/v1/crm: " + crmContact);
128132

129-
List<User> users = userRepository.findByCrmContactId(crmContact.getContactId());
130-
users.forEach(user -> {
133+
List<Invitation> invitations = invitationRepository.findByCrmContactIdAndCrmOrganisationId(
134+
crmContact.getContactId(), crmContact.getOrganisation().getOrganisationId());
135+
invitations.forEach(invitation -> {
136+
LOG.info("Deleting CRM invitation: " + invitation.getEmail());
137+
this.invitationRepository.delete(invitation);
138+
});
139+
140+
Optional<User> userOptional = userRepository.findByCrmContactIdAndCrmOrganisationId(
141+
crmContact.getContactId(), crmContact.getOrganisation().getOrganisationId());
142+
userOptional.ifPresent(user -> {
131143
LOG.info("Deleting CRM user: " + user.getEmail());
132144
this.provisioningService.deleteUserRequest(user);
133145
this.userRepository.delete(user);
@@ -140,6 +152,9 @@ private boolean provisionUser(CRMContact crmContact) {
140152
String sub = constructSub(crmContact);
141153
Optional<User> optionalUser = userRepository.findBySubIgnoreCase(sub);
142154
User user = optionalUser.orElseGet(() -> createUser(crmContact, sub));
155+
user.setCrmContactId(crmContact.getContactId());
156+
user.setCrmOrganisationId(crmContact.getOrganisation().getOrganisationId());
157+
143158
List<CRMRole> newCrmRoles = syncCrmRoles(crmContact, user);
144159

145160
List<Role> roles = convertCrmRolesToInviteRoles(crmContact, newCrmRoles);
@@ -166,7 +181,6 @@ private User createUser(CRMContact crmContact, String sub) {
166181
crmContact.getFirstname(),
167182
StringUtils.hasText(middleName) ? String.format("%s %s", middleName, surName) : surName,
168183
crmContact.getEmail());
169-
unsavedUser.setCrmContactId(crmContact.getContactId());
170184
User user = userRepository.save(unsavedUser);
171185
this.provisioningService.newUserRequest(user);
172186
return user;
@@ -177,6 +191,7 @@ private String constructSub(CRMContact crmContact) {
177191
}
178192

179193
private List<CRMRole> syncCrmRoles(CRMContact crmContact, User user) {
194+
LOG.debug(String.format("Start syncing crmRoles %s for user %s", crmContact.getRoles(), user.getEmail()));
180195
// Removes roles no longer present in CRM
181196
user.getUserRoles().removeIf(userRole -> {
182197
Role role = userRole.getRole();
@@ -192,15 +207,18 @@ private List<CRMRole> syncCrmRoles(CRMContact crmContact, User user) {
192207
.map(userRole -> userRole.getRole())
193208
.toList();
194209
// Return all the new CRM roles
195-
return crmContact.getRoles().stream()
210+
List<CRMRole> crmRoles = crmContact.getRoles().stream()
196211
.filter(crmRole -> currentRoles.stream()
197212
.noneMatch(role -> crmRole.getRoleId().equalsIgnoreCase(role.getCrmRoleId())))
198213
.toList();
214+
LOG.debug(String.format("Finished syncing crmRoles %s for user %s", crmContact.getRoles(), user.getEmail()));
215+
return crmRoles;
199216
}
200217

201218
private boolean sendInvitation(CRMContact crmContact) {
202-
String sub = constructSub(crmContact);
203-
Optional<User> optionalUser = userRepository.findBySubIgnoreCase(sub);
219+
Optional<User> optionalUser =
220+
userRepository.findByCrmContactIdAndCrmOrganisationId(
221+
crmContact.getContactId(), crmContact.getOrganisation().getOrganisationId());
204222
List<CRMRole> newCrmRoles = syncCrmRoles(crmContact, optionalUser.orElse(new User()));
205223
//Only save the user when the user already existed
206224
optionalUser.ifPresent(user -> userRepository.save(user));
@@ -213,7 +231,7 @@ private boolean sendInvitation(CRMContact crmContact) {
213231
.map(role -> new InvitationRole(role))
214232
.collect(Collectors.toSet());
215233
Invitation invitation = createInvitation(crmContact, invitationRoles);
216-
invitation.setOrganizationGUID(crmContact.getOrganisation().getOrganisationId());
234+
217235
Optional<String> idpName = identityProviderName(manage, invitation);
218236
mailBox.sendInviteMail(this.provisionable, invitation, groupedProviders, Language.en, idpName);
219237
}
@@ -257,13 +275,15 @@ private Role createRole(CRMOrganisation crmOrganisation, CRMRole crmRole) {
257275
unsavedRole.setCrmRoleName(crmConfigEntry.name());
258276
unsavedRole.setCrmOrganisationId(crmOrganisation.getOrganisationId());
259277
unsavedRole.setCrmOrganisationCode(crmOrganisation.getAbbrev());
278+
unsavedRole.setOrganizationGUID(crmOrganisation.getOrganisationId());
279+
260280
Role role = roleRepository.save(unsavedRole);
261281
this.provisioningService.newGroupRequest(role);
262282
return role;
263283
}
264284

265285
private Invitation createInvitation(CRMContact crmContact, Set<InvitationRole> invitationRoles) {
266-
return new Invitation(
286+
Invitation invitation = new Invitation(
267287
Authority.GUEST,
268288
HashGenerator.generateRandomHash(),
269289
crmContact.getEmail(),
@@ -279,6 +299,12 @@ private Invitation createInvitation(CRMContact crmContact, Set<InvitationRole> i
279299
invitationRoles,
280300
null
281301
);
302+
String crmOrganisationId = crmContact.getOrganisation().getOrganisationId();
303+
invitation.setOrganizationGUID(crmOrganisationId);
304+
invitation.setCrmOrganisationId(crmOrganisationId);
305+
invitation.setCrmContactId(crmContact.getContactId());
306+
invitationRepository.save(invitation);
307+
return invitation;
282308

283309
}
284310
}

server/src/main/java/invite/model/Invitation.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ public class Invitation implements Serializable {
9292
@Column(name = "crm_contact_id")
9393
private String crmContactId;
9494

95+
@Column(name = "crm_organisation_id")
96+
private String crmOrganisationId;
97+
9598
@OneToMany(mappedBy = "invitation", orphanRemoval = true, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
9699
private Set<InvitationRole> roles = new HashSet<>();
97100

server/src/main/java/invite/model/User.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ public class User implements Serializable, Provisionable {
8787
@Column(name = "crm_contact_id")
8888
private String crmContactId;
8989

90+
@Column(name = "crm_organisation_id")
91+
private String crmOrganisationId;
92+
9093
@OneToMany(mappedBy = "user", orphanRemoval = true, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
9194
private Set<UserRole> userRoles = new HashSet<>();
9295

server/src/main/java/invite/repository/InvitationRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public interface InvitationRepository extends JpaRepository<Invitation, Long>, Q
2424
attributePaths = {"inviter", "roles", "roles.role"})
2525
Optional<Invitation> findByHash(String hash);
2626

27+
List<Invitation> findByCrmContactIdAndCrmOrganisationId(String crmContactId, String crmOrganisationId);
28+
2729
Optional<Invitation> findTopBySubInviteeOrderByCreatedAtDesc(String email);
2830

2931
List<Invitation> findByStatus(Status status);

server/src/main/java/invite/repository/UserRepository.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,14 @@ public interface UserRepository extends JpaRepository<User, Long>, QueryRewriter
1919

2020
Optional<User> findBySubIgnoreCase(String sub);
2121

22-
List<User> findByCrmContactId(String crmContactId);
22+
Optional<User> findByCrmContactIdAndCrmOrganisationId(String crmContactId, String crmOrganisationId);
2323

2424
List<User> findByOrganizationGUIDAndInstitutionAdmin(String organizationGUID, boolean institutionAdmin);
2525

2626
List<User> findBySuperUserTrue();
2727

2828
long countBySuperUserTrue();
2929

30-
long countByInstitutionAdminTrue();
31-
3230
Optional<User> findByEduPersonPrincipalNameIgnoreCase(String eppn);
3331

3432
Optional<User> findByEmailIgnoreCase(String email);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
ALTER TABLE `users`
2+
ADD `crm_organisation_id` varchar(255) DEFAULT NULL;
3+
4+
CREATE INDEX `users_crm_organisation_id_index` ON `users` (`crm_organisation_id`);
5+
6+
ALTER TABLE `invitations`
7+
ADD `crm_organisation_id` varchar(255) DEFAULT NULL;
8+
9+
CREATE INDEX `invitations_crm_organisation_id_index` ON `users` (`crm_organisation_id`);
10+
11+
ALTER TABLE `users`
12+
ADD CONSTRAINT `users_unique_crm_contact_profile`
13+
UNIQUE (`crm_contact_id`, `crm_organisation_id`);

server/src/test/java/invite/AbstractTest.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public abstract class AbstractTest {
111111
public static final String API_TOKEN_INVITER_USER_HASH = HashGenerator.generateToken();
112112
public static final String API_TOKEN_LEGACY_HASH = HashGenerator.generateToken();
113113
public static final String CRM_CONTACT_ID = "5B5A4230-7A67-46E9-9EE1-95C6F5CACA4A";
114+
public static final String CRM_ORGANIZATION_ID = "60507EEF-732D-4B38-B30B-8C7186A61813";
114115

115116

116117
@Value("${manage.staticManageDirectory}")
@@ -579,18 +580,23 @@ protected UnaryOperator<Map<String, Object>> institutionalAdminEntitlementOperat
579580
};
580581
}
581582

582-
protected CRMContact getCrmContact(CRMRole crmRole, String uid, String schacHomeOrganisation, boolean suppressInvitation) {
583+
protected CRMContact createCrmContact(String crmContactId,
584+
String crmOrganizationId,
585+
CRMRole crmRole,
586+
String uid,
587+
String schacHomeOrganisation,
588+
boolean suppressInvitation) {
583589
return new CRMContact(
584590
uid,
585591
schacHomeOrganisation,
586592
suppressInvitation,
587-
"contactId",
593+
crmContactId,
588594
"John",
589595
"from",
590596
"Doe",
591597
"jdoe@example.com",
592598
new CRMOrganisation(
593-
"organisationId",
599+
crmOrganizationId,
594600
"abbrec",
595601
"Inc. Corporated"
596602
),
@@ -636,6 +642,8 @@ private void doSeed() {
636642
User kbUser =
637643
new User(false, KB_USER_SUB, KB_USER_SUB, "kb.nl", "George", "Best", "gb@kb.nl");
638644
kbUser.setCrmContactId(CRM_CONTACT_ID);
645+
kbUser.setCrmOrganisationId(CRM_ORGANIZATION_ID);
646+
639647
doSave(this.userRepository, superUser, institutionAdmin, manager, inviter, wikiInviter, guest, kbUser);
640648

641649
Role wiki =

server/src/test/java/invite/aggregation/AttributeAggregatorControllerTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ void manageDown() {
122122
void attributeAggregationCRMRole() throws JsonProcessingException {
123123
CRMRole crmRoleResearch = new CRMRole("5e17b508-08e4-e811-8100-005056956c1a", "CONBEH", "SURFconextbeheerder");
124124
CRMRole crmRoleCloud = new CRMRole("cf652619-08e4-e811-8100-005056956c1a", "CONVER", "SURFconextverantwoordelijke");
125-
CRMContact crmContact = getCrmContact(crmRoleResearch, "guest", "example.com", true);
125+
CRMContact crmContact = createCrmContact(CRM_CONTACT_ID, CRM_ORGANIZATION_ID, crmRoleResearch,
126+
"guest", "kb.nl", true);
126127
crmContact.setRoles(List.of(crmRoleCloud, crmRoleResearch));
127128
//This application is linked to the 'CONBEH' CRM role
128129
String researchEntityId = "https://research";
@@ -149,7 +150,7 @@ void attributeAggregationCRMRole() throws JsonProcessingException {
149150
.auth().preemptive().basic("aa", "secret")
150151
.accept(ContentType.JSON)
151152
.contentType(ContentType.JSON)
152-
.pathParam("sub", GUEST_SUB)
153+
.pathParam("sub", KB_USER_SUB)
153154
.queryParam("SPentityID", researchEntityId)
154155
.get("/api/external/v1/aa/{sub}")
155156
.as(new TypeRef<>() {
@@ -161,7 +162,7 @@ void attributeAggregationCRMRole() throws JsonProcessingException {
161162
List<String> autorizations = autorisatie.stream().map(m -> m.get("autorisatie")).sorted().toList();
162163
List<String> expected = Stream.of(
163164
"urn:mace:surfnet.nl:surfnet.nl:sab:organizationCode:abbrec",
164-
"urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:organisationId",
165+
"urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:" + CRM_ORGANIZATION_ID,
165166
"urn:mace:surfnet.nl:surfnet.nl:sab:role:SURFconextbeheerder").sorted().toList();
166167
assertEquals(expected, autorizations);
167168
}

server/src/test/java/invite/api/InvitationControllerTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.junit.jupiter.api.Test;
1515
import org.springframework.data.domain.Example;
1616
import org.springframework.data.domain.Sort;
17+
import org.springframework.http.HttpStatus;
1718
import org.springframework.util.MultiValueMap;
1819
import org.springframework.web.util.UriComponentsBuilder;
1920

@@ -299,6 +300,27 @@ void accept() throws Exception {
299300
assertEquals(1, remoteProvisionedUserRepository.count());
300301
}
301302

303+
@Test
304+
void acceptWithCRMConstraint() throws Exception {
305+
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", KB_USER_SUB);
306+
String hash = Authority.GUEST.name();
307+
Invitation invitation = invitationRepository.findByHash(hash).get();
308+
invitation.setCrmOrganisationId(UUID.randomUUID().toString());
309+
invitation.setCrmContactId(CRM_CONTACT_ID);
310+
invitationRepository.save(invitation);
311+
312+
AcceptInvitation acceptInvitation = new AcceptInvitation(hash, invitation.getId());
313+
given()
314+
.when()
315+
.filter(accessCookieFilter.cookieFilter())
316+
.accept(ContentType.JSON)
317+
.header(accessCookieFilter.csrfToken().getHeaderName(), accessCookieFilter.csrfToken().getToken())
318+
.contentType(ContentType.JSON)
319+
.body(acceptInvitation)
320+
.post("/api/v1/invitations/accept")
321+
.then()
322+
.statusCode(HttpStatus.NOT_ACCEPTABLE.value());
323+
}
302324
@Test
303325
void acceptGraph() throws Exception {
304326
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", "graph@new.com");

0 commit comments

Comments
 (0)