Skip to content

Commit b57fc2f

Browse files
Dimfacionjohanah29
andauthored
[backend] feat(sso): add group mapping (#4832)
Signed-off-by: johanah29 <johanah.lekeu@filigran.io> Signed-off-by: Gael Leblan <gael.leblan@filigran.io> Co-authored-by: johanah29 <johanah.lekeu@filigran.io>
1 parent 133860d commit b57fc2f

10 files changed

Lines changed: 574 additions & 91 deletions

File tree

openaev-api/src/main/java/io/openaev/config/AppSecurityConfig.java

Lines changed: 13 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,26 @@
11
package io.openaev.config;
22

3-
import static io.openaev.config.security.SecurityService.OPENAEV_PROVIDER_PATH_PREFIX;
43
import static io.openaev.config.security.SecurityService.REGISTRATION_ID;
54
import static org.apache.commons.lang3.StringUtils.isBlank;
65
import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
76

8-
import com.fasterxml.jackson.core.type.TypeReference;
9-
import com.fasterxml.jackson.databind.JsonNode;
107
import com.fasterxml.jackson.databind.ObjectMapper;
11-
import com.fasterxml.jackson.databind.ObjectReader;
128
import io.openaev.config.security.OpenSamlConfig;
139
import io.openaev.config.security.SecurityService;
1410
import io.openaev.database.model.User;
1511
import io.openaev.security.SsoRefererAuthenticationFailureHandler;
1612
import io.openaev.security.SsoRefererAuthenticationSuccessHandler;
1713
import io.openaev.security.TokenAuthenticationFilter;
14+
import io.openaev.service.UserMappingService;
1815
import io.openaev.service.user_events.UserEventService;
1916
import jakarta.annotation.Resource;
2017
import jakarta.servlet.http.HttpServletRequest;
21-
import java.io.IOException;
22-
import java.util.ArrayList;
23-
import java.util.Base64;
2418
import java.util.List;
25-
import java.util.stream.Stream;
2619
import lombok.RequiredArgsConstructor;
2720
import lombok.extern.slf4j.Slf4j;
2821
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2922
import org.springframework.context.annotation.Bean;
3023
import org.springframework.context.annotation.Configuration;
31-
import org.springframework.core.env.Environment;
3224
import org.springframework.http.HttpStatus;
3325
import org.springframework.security.config.Customizer;
3426
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -43,7 +35,6 @@
4335
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
4436
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
4537
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
46-
import org.springframework.security.oauth2.core.OAuth2AccessToken;
4738
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
4839
import org.springframework.security.oauth2.core.OAuth2Error;
4940
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
@@ -59,11 +50,11 @@
5950
@Slf4j
6051
public class AppSecurityConfig {
6152

62-
private final Environment env;
6353
private final OpenAEVConfig openAEVConfig;
6454
private final OpenSamlConfig openSamlConfig;
6555
private final SecurityService securityService;
6656
private final UserEventService userEventService;
57+
private final UserMappingService userMappingService;
6758

6859
@Resource protected ObjectMapper mapper;
6960

@@ -140,43 +131,11 @@ public TokenAuthenticationFilter tokenAuthenticationFilter() {
140131
return new TokenAuthenticationFilter();
141132
}
142133

143-
private List<String> extractRolesFromToken(OAuth2AccessToken accessToken, String registrationId) {
144-
ObjectReader listReader = mapper.readerFor(new TypeReference<List<String>>() {});
145-
if (accessToken != null) {
146-
String rolesPathConfig = OPENAEV_PROVIDER_PATH_PREFIX + registrationId + ".roles_path";
147-
//noinspection unchecked
148-
List<String> rolesPath =
149-
env.getProperty(rolesPathConfig, List.class, new ArrayList<String>());
150-
try {
151-
String[] chunks = accessToken.getTokenValue().split("\\.");
152-
Base64.Decoder decoder = Base64.getUrlDecoder();
153-
String payload = new String(decoder.decode(chunks[1]));
154-
JsonNode jsonNode = mapper.readTree(payload);
155-
return rolesPath.stream()
156-
.map(path -> "/" + path.replaceAll("\\.", "/"))
157-
.flatMap(
158-
path -> {
159-
JsonNode arrayRoles = jsonNode.at(path);
160-
try {
161-
List<String> roles = listReader.readValue(arrayRoles);
162-
return roles.stream();
163-
} catch (IOException e) {
164-
return Stream.empty();
165-
}
166-
})
167-
.toList();
168-
} catch (Exception e) {
169-
log.error(e.getMessage(), e);
170-
}
171-
}
172-
return new ArrayList<>();
173-
}
174-
175-
public User userOauth2Management(
176-
OAuth2AccessToken accessToken, ClientRegistration clientRegistration, OAuth2User user) {
134+
public User userOauth2Management(ClientRegistration clientRegistration, OAuth2User user) {
177135
String emailAttribute = user.getAttribute("email");
178136
String registrationId = clientRegistration.getRegistrationId();
179-
List<String> rolesFromToken = extractRolesFromToken(accessToken, registrationId);
137+
List<String> rolesFromUser = userMappingService.extractRolesFromUser(user, registrationId);
138+
List<String> groupsFromUser = userMappingService.extractGroupsFromUser(user, registrationId);
180139
if (isBlank(emailAttribute)) {
181140
OAuth2Error authError =
182141
new OAuth2Error(
@@ -189,7 +148,8 @@ public User userOauth2Management(
189148
this.securityService.userManagement(
190149
emailAttribute,
191150
registrationId,
192-
rolesFromToken,
151+
rolesFromUser,
152+
groupsFromUser,
193153
user.getAttribute("given_name"),
194154
user.getAttribute("family_name"));
195155

@@ -201,32 +161,28 @@ public User userOauth2Management(
201161
throw new OAuth2AuthenticationException(authError);
202162
}
203163

204-
public OidcUser oidcUserManagement(
205-
OAuth2AccessToken accessToken, ClientRegistration clientRegistration, OAuth2User user) {
206-
User loginUser = userOauth2Management(accessToken, clientRegistration, user);
164+
public OidcUser oidcUserManagement(ClientRegistration clientRegistration, OAuth2User user) {
165+
User loginUser = userOauth2Management(clientRegistration, user);
207166
return new OpenAEVOidcUser(loginUser);
208167
}
209168

210-
public OAuth2User oAuth2UserManagement(
211-
OAuth2AccessToken accessToken, ClientRegistration clientRegistration, OAuth2User user) {
212-
User loginUser = userOauth2Management(accessToken, clientRegistration, user);
169+
public OAuth2User oAuth2UserManagement(ClientRegistration clientRegistration, OAuth2User user) {
170+
User loginUser = userOauth2Management(clientRegistration, user);
213171
return new OpenAEVOAuth2User(loginUser);
214172
}
215173

216174
@Bean
217175
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
218176
OidcUserService delegate = new OidcUserService();
219177
return request ->
220-
oidcUserManagement(
221-
request.getAccessToken(), request.getClientRegistration(), delegate.loadUser(request));
178+
oidcUserManagement(request.getClientRegistration(), delegate.loadUser(request));
222179
}
223180

224181
@Bean
225182
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
226183
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
227184
return request ->
228-
oAuth2UserManagement(
229-
request.getAccessToken(), request.getClientRegistration(), delegate.loadUser(request));
185+
oAuth2UserManagement(request.getClientRegistration(), delegate.loadUser(request));
230186
}
231187

232188
@Bean

openaev-api/src/main/java/io/openaev/config/security/OpenSamlConfig.java

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import io.openaev.config.OpenAEVSaml2User;
99
import io.openaev.database.model.User;
1010
import io.openaev.security.SsoRefererAuthenticationSuccessHandler;
11+
import io.openaev.service.UserMappingService;
1112
import io.openaev.service.user_events.UserEventService;
12-
import jakarta.validation.constraints.NotBlank;
1313
import java.util.ArrayList;
1414
import java.util.List;
1515
import lombok.RequiredArgsConstructor;
@@ -37,12 +37,12 @@
3737
@RequiredArgsConstructor
3838
public class OpenSamlConfig {
3939

40-
public static final String ROLES_PATH_SUFFIX = ".roles_path";
4140
public static final String FIRSTNAME_ATTRIBUTE_PATH_SUFFIX = ".firstname_attribute_key";
4241
public static final String LASTNAME_ATTRIBUTE_PATH_SUFFIX = ".lastname_attribute_key";
4342

4443
private final Environment env;
4544
private final SecurityService securityService;
45+
private final UserMappingService userMappingService;
4646

4747
@Autowired(required = false)
4848
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@@ -101,7 +101,8 @@ private Saml2Authentication saml2UserManagement(
101101
private User userSaml2Management(@NotNull final Saml2AuthenticatedPrincipal user) {
102102
String emailAttribute = user.getName();
103103
String registrationId = user.getRelyingPartyRegistrationId();
104-
List<String> rolesFromUser = extractRolesFromUser(user, registrationId);
104+
List<String> rolesFromUser = userMappingService.extractRolesFromUser(user, registrationId);
105+
List<String> groupsFromUser = userMappingService.extractGroupsFromUser(user, registrationId);
105106

106107
String firstname =
107108
user.getFirstAttribute(
@@ -119,7 +120,7 @@ private User userSaml2Management(@NotNull final Saml2AuthenticatedPrincipal user
119120
try {
120121
User userLogin =
121122
securityService.userManagement(
122-
emailAttribute, registrationId, rolesFromUser, firstname, lastname);
123+
emailAttribute, registrationId, rolesFromUser, groupsFromUser, firstname, lastname);
123124

124125
if (userLogin != null) {
125126
return userLogin;
@@ -131,24 +132,4 @@ private User userSaml2Management(@NotNull final Saml2AuthenticatedPrincipal user
131132
Saml2Error authError = new Saml2Error("invalid_token", "User conversion fail");
132133
throw new Saml2AuthenticationException(authError);
133134
}
134-
135-
private List<String> extractRolesFromUser(
136-
@NotNull final Saml2AuthenticatedPrincipal user, @NotBlank final String registrationId) {
137-
List<String> rolesPath = getRoles(registrationId);
138-
List<String> extractedRoles = new ArrayList<>();
139-
140-
for (String path : rolesPath) {
141-
List<String> roles = user.getAttribute(path);
142-
if (roles != null) {
143-
extractedRoles.addAll(roles);
144-
}
145-
}
146-
return extractedRoles;
147-
}
148-
149-
private List<String> getRoles(@NotBlank final String registrationId) {
150-
String rolesPathConfig = OPENAEV_PROVIDER_PATH_PREFIX + registrationId + ROLES_PATH_SUFFIX;
151-
//noinspection unchecked
152-
return env.getProperty(rolesPathConfig, List.class, new ArrayList<String>());
153-
}
154135
}

openaev-api/src/main/java/io/openaev/config/security/SecurityService.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.openaev.database.model.User;
77
import io.openaev.database.repository.UserRepository;
88
import io.openaev.rest.user.form.user.CreateUserInput;
9+
import io.openaev.service.UserMappingService;
910
import io.openaev.service.UserService;
1011
import io.openaev.service.user_events.UserEventService;
1112
import jakarta.validation.constraints.NotBlank;
@@ -22,25 +23,28 @@ public class SecurityService {
2223

2324
public static final String OPENAEV_PROVIDER_PATH_PREFIX = "openaev.provider.";
2425
public static final String ROLES_ADMIN_PATH_SUFFIX = ".roles_admin";
26+
public static final String GROUPS_MANAGEMENT_SUFFIX = ".groups_management";
2527
public static final String ALL_ADMIN_PATH_SUFFIX = ".all_admin";
2628
public static final String AUDIENCE_PATH = ".audience";
2729
public static final String REGISTRATION_ID = "registration_id";
2830

2931
private final UserRepository userRepository;
3032
private final UserService userService;
33+
private final UserMappingService userMappingService;
3134
private final Environment env;
3235
private final UserEventService userEventService;
3336

3437
public User userManagement(
3538
String emailAttribute,
3639
String registrationId,
37-
List<String> rolesFromToken,
40+
List<String> roles,
41+
List<String> groups,
3842
String firstName,
3943
String lastName) {
4044
String email = ofNullable(emailAttribute).orElseThrow();
4145
List<String> adminRoles = getAdminRoles(registrationId);
4246
boolean allAdmin = isAllAdmin(registrationId);
43-
boolean isAdmin = allAdmin || adminRoles.stream().anyMatch(rolesFromToken::contains);
47+
boolean isAdmin = allAdmin || adminRoles.stream().anyMatch(roles::contains);
4448
if (hasLength(email)) {
4549
Optional<User> optionalUser = userRepository.findByEmailIgnoreCase(email);
4650
// If user not exists, create it
@@ -55,7 +59,13 @@ public User userManagement(
5559
User user = this.userService.createUser(createUserInput, 0);
5660
this.userEventService.createUserCreatedEvent(user, registrationId);
5761
userEventService.createLoginSuccessEvent(user);
58-
return user;
62+
String groupsManagementObject =
63+
env.getProperty(
64+
OPENAEV_PROVIDER_PATH_PREFIX + registrationId + GROUPS_MANAGEMENT_SUFFIX,
65+
String.class,
66+
"");
67+
userMappingService.mapCurrentUserWithGroup(groupsManagementObject, user, groups);
68+
return this.userService.updateUser(user);
5969
} else {
6070
// If user exists, update it
6171
User currentUser = optionalUser.get();
@@ -65,6 +75,12 @@ public User userManagement(
6575
currentUser.setAdmin(isAdmin);
6676
}
6777
userEventService.createLoginSuccessEvent(currentUser);
78+
String groupsManagementObject =
79+
env.getProperty(
80+
OPENAEV_PROVIDER_PATH_PREFIX + registrationId + GROUPS_MANAGEMENT_SUFFIX,
81+
String.class,
82+
"");
83+
userMappingService.mapCurrentUserWithGroup(groupsManagementObject, currentUser, groups);
6884
return this.userService.updateUser(currentUser);
6985
}
7086
}

openaev-api/src/main/java/io/openaev/rest/domain/DomainApi.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,20 @@ public class DomainApi extends RestBehavior {
2929
@LogExecutionTime
3030
@Operation(summary = "Search Domains")
3131
@GetMapping(DOMAIN_URI)
32-
@RBAC(actionPerformed = Action.READ, resourceType = ResourceType.PAYLOAD)
32+
@RBAC(actionPerformed = Action.READ, resourceType = ResourceType.DOMAIN)
3333
public List<Domain> domains() {
3434
return domainService.searchDomains();
3535
}
3636

3737
@Operation(summary = "Get a Domain by ID", description = "Fetches detailed Domain info by ID")
3838
@GetMapping(DOMAIN_URI + "/{domainId}")
39-
@RBAC(
40-
resourceId = "#domainId",
41-
actionPerformed = Action.READ,
42-
resourceType = ResourceType.PAYLOAD)
39+
@RBAC(resourceId = "#domainId", actionPerformed = Action.READ, resourceType = ResourceType.DOMAIN)
4340
public Domain getDomain(@PathVariable String domainId) {
4441
return domainService.findById(domainId);
4542
}
4643

4744
@PostMapping(DOMAIN_URI + "/{domainId}/upsert")
48-
@RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.PAYLOAD)
45+
@RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.DOMAIN)
4946
@Transactional(rollbackOn = Exception.class)
5047
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The upserted domain")})
5148
@Operation(description = "Upsert a domain", summary = "Upsert domain")

0 commit comments

Comments
 (0)