Skip to content

Commit

Permalink
feat: Introduce additionally configurable UNIX groups' mappings to X-…
Browse files Browse the repository at this point in the history
…Road user role (#1729)

Refs: XRDDEV-1997
  • Loading branch information
andresrosenthal authored Jul 6, 2023
1 parent 3c2761b commit f325677
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 188 deletions.
39 changes: 21 additions & 18 deletions doc/Manuals/ug-syspar_x-road_v6_system_parameters.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@
import org.niis.xroad.restapi.config.ApiCachingConfiguration;
import org.niis.xroad.restapi.config.IdentifierValidationConfiguration;
import org.niis.xroad.restapi.config.LimitRequestSizesFilter;
import org.niis.xroad.restapi.config.UserRoleConfig;
import org.niis.xroad.restapi.domain.Role;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;

import java.util.EnumMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;
import static org.niis.xroad.restapi.domain.Role.XROAD_REGISTRATION_OFFICER;
import static org.niis.xroad.restapi.domain.Role.XROAD_SECURITY_OFFICER;
import static org.niis.xroad.restapi.domain.Role.XROAD_SYSTEM_ADMINISTRATOR;

/**
* Admin service configuration properties.
Expand All @@ -59,7 +68,8 @@ public class AdminServiceProperties implements IpThrottlingFilterConfig,
ApiCachingConfiguration.Config,
LimitRequestSizesFilter.Config,
IdentifierValidationConfiguration.Config,
AllowedFilesConfig {
AllowedFilesConfig,
UserRoleConfig {

/**
* Controls the rate of global configuration generation in seconds.
Expand Down Expand Up @@ -145,4 +155,24 @@ public class AdminServiceProperties implements IpThrottlingFilterConfig,

/** Configures Api file upload request size limit. */
private DataSize requestSizeLimitBinaryUpload;

/**
* Configures additional UNIX groups mapped to X-Road user roles.
*/
private EnumMap<Role, List<String>> complementaryUserRoleMappings;

@Override
public EnumMap<Role, List<String>> getUserRoleMappings() {
EnumMap<Role, List<String>> userRoleMappings = new EnumMap<>(Role.class);
userRoleMappings.put(XROAD_SECURITY_OFFICER, List.of("xroad-security-officer"));
userRoleMappings.put(XROAD_REGISTRATION_OFFICER, List.of("xroad-registration-officer"));
userRoleMappings.put(XROAD_SYSTEM_ADMINISTRATOR, List.of("xroad-system-administrator"));

if (complementaryUserRoleMappings != null) {
complementaryUserRoleMappings.forEach((role, groups) -> userRoleMappings.merge(role, groups,
(a, b) -> Stream.concat(a.stream(), b.stream()).collect(toList())));
}

return userRoleMappings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import org.niis.xroad.restapi.domain.PersistentApiKeyType;
import org.niis.xroad.restapi.domain.Role;
import org.niis.xroad.restapi.service.ApiKeyService;
import org.niis.xroad.restapi.util.SecurityHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -57,21 +56,19 @@ public class ApiKeyAuthenticationManager implements AuthenticationManager {
private final GrantedAuthorityMapper permissionMapper;
private final AuthenticationIpWhitelist authenticationIpWhitelist;
private final AuditEventLoggingFacade auditEventLoggingFacade;
private final SecurityHelper securityHelper;

@Autowired
public ApiKeyAuthenticationManager(ApiKeyAuthenticationHelper apiKeyAuthenticationHelper,
AuthenticationHeaderDecoder authenticationHeaderDecoder,
GrantedAuthorityMapper permissionMapper,
@Qualifier(AuthenticationIpWhitelist.REGULAR_API_WHITELIST)
AuthenticationIpWhitelist authenticationIpWhitelist,
AuditEventLoggingFacade auditEventLoggingFacade, SecurityHelper securityHelper) {
AuditEventLoggingFacade auditEventLoggingFacade) {
this.apiKeyAuthenticationHelper = apiKeyAuthenticationHelper;
this.authenticationHeaderDecoder = authenticationHeaderDecoder;
this.permissionMapper = permissionMapper;
this.authenticationIpWhitelist = authenticationIpWhitelist;
this.auditEventLoggingFacade = auditEventLoggingFacade;
this.securityHelper = securityHelper;
}

@Override
Expand All @@ -90,11 +87,8 @@ public Authentication authenticate(Authentication authentication) throws Authent
throw new BadCredentialsException("Unknown problem when getting API key", e);
}
Set<Role> roles = key.getRoles();
PreAuthenticatedAuthenticationToken authenticationWithGrants =
new PreAuthenticatedAuthenticationToken(createPrincipal(key),
authentication.getCredentials(),
permissionMapper.getAuthorities(roles));
return authenticationWithGrants;
return new PreAuthenticatedAuthenticationToken(
createPrincipal(key), authentication.getCredentials(), permissionMapper.getAuthorities(roles));
} catch (Exception e) {
auditEventLoggingFacade.auditLogFail(RestApiAuditEvent.API_KEY_AUTHENTICATION, e);
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@
*/
package org.niis.xroad.restapi.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jvnet.libpam.PAM;
import org.jvnet.libpam.PAMException;
import org.jvnet.libpam.UnixUser;
import org.niis.xroad.restapi.config.audit.AuditEventLoggingFacade;
import org.niis.xroad.restapi.config.audit.RestApiAuditEvent;
import org.niis.xroad.restapi.domain.Role;
import org.niis.xroad.restapi.util.SecurityHelper;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
Expand All @@ -41,11 +41,14 @@
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toSet;

/**
* PAM authentication provider.
Expand All @@ -56,6 +59,7 @@
* Authentication is limited with an IP whitelist.
*/
@Slf4j
@RequiredArgsConstructor
public class PamAuthenticationProvider implements AuthenticationProvider {

// from PAMLoginModule
Expand All @@ -66,52 +70,20 @@ public class PamAuthenticationProvider implements AuthenticationProvider {

private final AuthenticationIpWhitelist authenticationIpWhitelist;
private final GrantedAuthorityMapper grantedAuthorityMapper;
private final EnumMap<Role, List<String>> userRoleMappings;
private final RestApiAuditEvent loginEvent; // login event to audit log
private final AuditEventLoggingFacade auditEventLoggingFacade;
private final SecurityHelper securityHelper;

/**
* constructor
* @param authenticationIpWhitelist whitelist that limits the authentication
*/
public PamAuthenticationProvider(AuthenticationIpWhitelist authenticationIpWhitelist,
GrantedAuthorityMapper grantedAuthorityMapper, RestApiAuditEvent loginEvent,
AuditEventLoggingFacade auditEventLoggingFacade, SecurityHelper securityHelper) {
this.authenticationIpWhitelist = authenticationIpWhitelist;
this.grantedAuthorityMapper = grantedAuthorityMapper;
this.loginEvent = loginEvent;
this.auditEventLoggingFacade = auditEventLoggingFacade;
this.securityHelper = securityHelper;
}

/**
* users with these groups are allowed access
*/
private static final Set<String> ALLOWED_GROUP_NAMES = Collections.unmodifiableSet(
Arrays.stream(Role.values())
.map(Role::getLinuxGroupName)
.collect(Collectors.toSet()));

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
boolean success = false;
String username = "unknown user";
Exception caughException = null;

String username = String.valueOf(authentication.getPrincipal());
try {
username = String.valueOf(authentication.getPrincipal());
Authentication result = doAuthenticateInternal(authentication, username);
success = true;
auditEventLoggingFacade.auditLogSuccess(loginEvent, username);
return result;
} catch (Exception e) {
caughException = e;
auditEventLoggingFacade.auditLogFail(loginEvent, e, username);
throw e;
} finally {
if (success) {
auditEventLoggingFacade.auditLogSuccess(loginEvent, username);
} else {
auditEventLoggingFacade.auditLogFail(loginEvent, caughException, username);
}
}
}

Expand All @@ -127,15 +99,13 @@ private Authentication doAuthenticateInternal(Authentication authentication, Str
try {
UnixUser user = pam.authenticate(username, password);
Set<String> groups = user.getGroups();
Set<String> matchingGroups = groups.stream()
.filter(ALLOWED_GROUP_NAMES::contains)
.collect(Collectors.toSet());
if (matchingGroups.isEmpty()) {
Collection<Role> xroadRoles = userRoleMappings.entrySet().stream()
.filter(roleGroupMapping -> !Collections.disjoint(roleGroupMapping.getValue(), groups))
.map(Map.Entry::getKey)
.collect(toSet());
if (xroadRoles.isEmpty()) {
throw new AuthenticationServiceException("user hasn't got any required groups");
}
Collection<Role> xroadRoles = matchingGroups.stream()
.map(groupName -> Role.getForGroupName(groupName).get())
.collect(Collectors.toSet());
Set<GrantedAuthority> grants = grantedAuthorityMapper.getAuthorities(xroadRoles);
return new UsernamePasswordAuthenticationToken(user.getUserName(), authentication.getCredentials(), grants);
} catch (PAMException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
*/
package org.niis.xroad.restapi.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.niis.xroad.restapi.config.UserRoleConfig;
import org.niis.xroad.restapi.config.audit.AuditEventLoggingFacade;
import org.niis.xroad.restapi.config.audit.RestApiAuditEvent;
import org.niis.xroad.restapi.util.SecurityHelper;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -45,6 +46,7 @@
@Slf4j
@Configuration
@Profile("!devtools-test-auth")
@RequiredArgsConstructor
public class PamAuthenticationProviderConfig {

public static final String KEY_MANAGEMENT_PAM_AUTHENTICATION = "keyManagementPam";
Expand All @@ -56,17 +58,7 @@ public class PamAuthenticationProviderConfig {

private final GrantedAuthorityMapper grantedAuthorityMapper;
private final AuditEventLoggingFacade auditEventLoggingFacade;
private final SecurityHelper securityHelper;

/**
* constructor
*/
public PamAuthenticationProviderConfig(GrantedAuthorityMapper grantedAuthorityMapper,
AuditEventLoggingFacade auditEventLoggingFacade, SecurityHelper securityHelper) {
this.grantedAuthorityMapper = grantedAuthorityMapper;
this.auditEventLoggingFacade = auditEventLoggingFacade;
this.securityHelper = securityHelper;
}
private final UserRoleConfig userRoleConfig;

/**
* PAM authentication for form login, with corresponding IP whitelist
Expand All @@ -76,8 +68,9 @@ public PamAuthenticationProviderConfig(GrantedAuthorityMapper grantedAuthorityMa
public PamAuthenticationProvider formLoginPamAuthentication() {
AuthenticationIpWhitelist formLoginWhitelist = new AuthenticationIpWhitelist();
formLoginWhitelist.setWhitelistEntries(FORM_LOGIN_IP_WHITELIST);
return new PamAuthenticationProvider(formLoginWhitelist, grantedAuthorityMapper, RestApiAuditEvent.FORM_LOGIN,
auditEventLoggingFacade, securityHelper);
return new PamAuthenticationProvider(
formLoginWhitelist, grantedAuthorityMapper, userRoleConfig.getUserRoleMappings(), RestApiAuditEvent.FORM_LOGIN,
auditEventLoggingFacade);
}

/**
Expand All @@ -87,8 +80,8 @@ public PamAuthenticationProvider formLoginPamAuthentication() {
@Bean(KEY_MANAGEMENT_PAM_AUTHENTICATION)
public PamAuthenticationProvider keyManagementWhitelist(
@Qualifier(KEY_MANAGEMENT_API_WHITELIST) AuthenticationIpWhitelist keyManagementWhitelist) {
return new PamAuthenticationProvider(keyManagementWhitelist, grantedAuthorityMapper,
RestApiAuditEvent.KEY_MANAGEMENT_PAM_LOGIN, auditEventLoggingFacade, securityHelper);
return new PamAuthenticationProvider(keyManagementWhitelist, grantedAuthorityMapper, userRoleConfig.getUserRoleMappings(),
RestApiAuditEvent.KEY_MANAGEMENT_PAM_LOGIN, auditEventLoggingFacade);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* The MIT License
* Copyright (c) 2019- Nordic Institute for Interoperability Solutions (NIIS)
* Copyright (c) 2018 Estonian Information System Authority (RIA),
* Nordic Institute for Interoperability Solutions (NIIS), Population Register Centre (VRK)
* Copyright (c) 2015-2017 Estonian Information System Authority (RIA), Population Register Centre (VRK)
* <p>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* <p>
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* <p>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.niis.xroad.restapi.config;

import org.niis.xroad.restapi.domain.Role;

import java.util.EnumMap;
import java.util.List;

public interface UserRoleConfig {

/**
* Configures UNIX groups mapped to X-Road user roles.
*/
EnumMap<Role, List<String>> getUserRoleMappings();

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ public class InvalidRoleNameException extends Exception {
public InvalidRoleNameException(String s) {
super(s);
}

public InvalidRoleNameException(String s, Throwable cause) {
super(s, cause);
}
}
Loading

0 comments on commit f325677

Please sign in to comment.