Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • perun/perun-proxyidp/v1/OpenID-Connect-Java-Spring-Server
1 result
Show changes
Commits on Source (8)
Showing
with 1880 additions and 1561 deletions
# [18.5.0](https://gitlab.ics.muni.cz/perun/perun-proxyidp/v1/OpenID-Connect-Java-Spring-Server/compare/v18.4.0...v18.5.0) (2024-05-15)
### Features
* 🎸 filtering of assigned resource groups in entitlements ([7f1cf4c](https://gitlab.ics.muni.cz/perun/perun-proxyidp/v1/OpenID-Connect-Java-Spring-Server/commit/7f1cf4c531fe5b7dd5069bcb4645f35dbc8662f4))
* 🎸 filtering of groups in access control filter ([76ce690](https://gitlab.ics.muni.cz/perun/perun-proxyidp/v1/OpenID-Connect-Java-Spring-Server/commit/76ce690363dbe25097836f609ab6f6aca2197b5b))
# [18.4.0](https://gitlab.ics.muni.cz/perun/perun-proxyidp/v1/OpenID-Connect-Java-Spring-Server/compare/v18.3.1...v18.4.0) (2024-04-23)
......
# MITREid Connect
![maintenance status: end of life](https://img.shields.io/maintenance/end%20of%20life/2023)
![maintenance status: end of life](https://img.shields.io/maintenance/end%20of%20life/2024)
This project has reached end of life, which means no new features will be added. Security patches and important bug fixes will end as of 2023. Check out [SATOSA](https://github.com/IdentityPython/SATOSA) and its [OIDC frontend](https://github.com/UniversitaDellaCalabria/SATOSA-oidcop) instead.
This project has reached end of life, which means no new features will be added. Security patches and important bug fixes will end as of 2024. Check out [Apereo CAS](https://apereo.github.io/cas/) instead.
## Description
......
......@@ -21,7 +21,7 @@
<parent>
<groupId>cz.muni.ics</groupId>
<artifactId>perun-oidc-parent</artifactId>
<version>18.4.0</version>
<version>18.5.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
......
......@@ -22,7 +22,7 @@
<parent>
<groupId>cz.muni.ics</groupId>
<artifactId>perun-oidc-parent</artifactId>
<version>18.4.0</version>
<version>18.5.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
......
......@@ -12,8 +12,8 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_POST_LOGOUT_REDIRECT_URI;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_STATE;
import static cz.muni.ics.openid.connect.web.endpoint.EndSessionEndpoint.PARAM_POST_LOGOUT_REDIRECT_URI;
@Slf4j
public class PerunOidcLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
......
......@@ -86,11 +86,12 @@ public interface PerunAdapterMethods {
* Perform check if user can access service based on his/her membership
* in groups assigned to facility resources
*
* @param facility Facility object
* @param userId ID of user
* @param facility Facility object
* @param userId ID of user
* @param accessControlDisabledAttr
* @return TRUE if user can access, FALSE otherwise
*/
boolean canUserAccessBasedOnMembership(Facility facility, Long userId);
boolean canUserAccessBasedOnMembership(Facility facility, Long userId, String accessControlDisabledAttr);
/**
* Fetch facility attribute values
......@@ -156,11 +157,16 @@ public interface PerunAdapterMethods {
/**
* Get groups where user is active (also in VO in which group exists) and are assigned to the resources of facility.
* Fill the uniqueGroupName for groups as well.
* @param facilityId Id of Facility
* @param userId Id of User
*
* @param facilityId Id of Facility
* @param userId Id of User
* @param resourceGroupEntitlementDisabledAttribute Attribute to check if groups assigned to resource should be
* considered. Pass null to ignore the setting.
* @return Set of groups (filled or empty)
*/
Set<Group> getGroupsWhereUserIsActiveWithUniqueNames(Long facilityId, Long userId);
Set<Group> getGroupsWhereUserIsActiveWithUniqueNames(Long facilityId,
Long userId,
String resourceGroupEntitlementDisabledAttribute);
/**
* Fetch VO attribute values
......
......@@ -62,12 +62,12 @@ public class PerunAdapterImpl extends PerunAdapter {
}
@Override
public boolean canUserAccessBasedOnMembership(Facility facility, Long userId) {
public boolean canUserAccessBasedOnMembership(Facility facility, Long userId, String accessControlDisabledAttr) {
try {
return this.getAdapterPrimary().canUserAccessBasedOnMembership(facility, userId);
return this.getAdapterPrimary().canUserAccessBasedOnMembership(facility, userId, accessControlDisabledAttr);
} catch (UnsupportedOperationException e) {
if (this.isCallFallback()) {
return this.getAdapterFallback().canUserAccessBasedOnMembership(facility, userId);
return this.getAdapterFallback().canUserAccessBasedOnMembership(facility, userId, accessControlDisabledAttr);
} else {
throw e;
}
......@@ -334,12 +334,17 @@ public class PerunAdapterImpl extends PerunAdapter {
}
@Override
public Set<Group> getGroupsWhereUserIsActiveWithUniqueNames(Long facilityId, Long userId) {
public Set<Group> getGroupsWhereUserIsActiveWithUniqueNames(Long facilityId,
Long userId,
String resourceGroupEntitlementDisabledAttribute)
{
try {
return this.getAdapterPrimary().getGroupsWhereUserIsActiveWithUniqueNames(facilityId, userId);
return this.getAdapterPrimary().getGroupsWhereUserIsActiveWithUniqueNames(
facilityId, userId, resourceGroupEntitlementDisabledAttribute);
} catch (UnsupportedOperationException e) {
if (this.isCallFallback()) {
return this.getAdapterFallback().getGroupsWhereUserIsActiveWithUniqueNames(facilityId, userId);
return this.getAdapterFallback().getGroupsWhereUserIsActiveWithUniqueNames(
facilityId, userId, resourceGroupEntitlementDisabledAttribute);
} else {
throw e;
}
......
......@@ -142,14 +142,14 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
}
@Override
public boolean canUserAccessBasedOnMembership(Facility facility, Long userId) {
Set<Long> groupsWithAccessIds = getGroupIdsWithAccessToFacility(facility.getId());
public boolean canUserAccessBasedOnMembership(Facility facility, Long userId, String accessControlDisabledAttr) {
Set<Long> groupsWithAccessIds = getGroupIdsAssignedToFacility(facility.getId(), accessControlDisabledAttr);
if (groupsWithAccessIds == null || groupsWithAccessIds.isEmpty()) {
return false;
}
Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, null);
if (userGroupIds == null || userGroupIds.isEmpty()) {
if (userGroupIds.isEmpty()) {
return false;
}
......@@ -212,7 +212,7 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
public List<String> getGroupsAssignedToResourcesWithUniqueNames(Facility facility) {
List<String> res = new ArrayList<>();
Set<Long> groupIds = getGroupIdsWithAccessToFacility(facility.getId());
Set<Long> groupIds = getGroupIdsAssignedToFacility(facility.getId(), null);
if (groupIds == null || groupIds.isEmpty()) {
return res;
}
......@@ -415,9 +415,13 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
}
@Override
public Set<Group> getGroupsWhereUserIsActiveWithUniqueNames(Long facilityId, Long userId) {
Set<Long> userGroups = this.getGroupIdsWhereUserIsMember(userId, null);
Set<Long> facilityGroups = this.getGroupIdsWithAccessToFacility(facilityId);
public Set<Group> getGroupsWhereUserIsActiveWithUniqueNames(Long facilityId,
Long userId,
String resourceGroupEntitlementDisabledAttribute)
{
Set<Long> userGroups = getGroupIdsWhereUserIsMember(userId, null);
Set<Long> facilityGroups = getGroupIdsAssignedToFacility(
facilityId, resourceGroupEntitlementDisabledAttribute);
Set<Long> groupIds = userGroups.stream()
.filter(facilityGroups::contains)
.collect(Collectors.toSet());
......@@ -659,15 +663,50 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE;
}
private Set<Long> getGroupIdsWithAccessToFacility(Long facilityId) {
private Set<Long> getGroupIdsAssignedToFacility(Long facilityId, String ignoreGroupsAttribute) {
String ldapResourceGroupEntitlementDisabledAttrName = null;
if (ignoreGroupsAttribute != null) {
AttributeMapping mapping = getMappingForAttrName(PerunEntityType.RESOURCE, ignoreGroupsAttribute);
if (mapping == null) {
log.warn("No Attribute mapping found for '{}'", ignoreGroupsAttribute);
} else {
ldapResourceGroupEntitlementDisabledAttrName = mapping.getLdapName();
}
}
final String ignoreResourceAttr = ldapResourceGroupEntitlementDisabledAttrName;
FilterBuilder filter = and(equal(OBJECT_CLASS, PERUN_RESOURCE), equal(PERUN_FACILITY_ID, String.valueOf(facilityId)));
String[] attributes = new String[] { ASSIGNED_GROUP_ID };
Set<String> attributesToFetch = new HashSet<>();
attributesToFetch.add(ASSIGNED_GROUP_ID);
final String[] mandatoryAttributes = attributesToFetch.toArray(new String[] {});
if (StringUtils.hasText(ignoreResourceAttr)) {
attributesToFetch.add(ignoreResourceAttr);
}
final String[] attributes = attributesToFetch.toArray(new String[] {});
EntryMapper<Set<Long>> mapper = e -> {
Set<Long> ids = new HashSet<>();
if (checkHasAttributes(e, attributes)) {
if (checkHasAttributes(e, mandatoryAttributes)) {
boolean ignore = false;
if (StringUtils.hasText(ignoreResourceAttr)) {
Attribute ignoreAttr = e.get(ignoreResourceAttr);
if (ignoreAttr != null && "true".equalsIgnoreCase(ignoreAttr.getString())) {
ignore = true;
}
}
Attribute a = e.get(ASSIGNED_GROUP_ID);
if (a != null) {
a.iterator().forEachRemaining(id -> ids.add(Long.valueOf(id.getString())));
final Set<Long> groupIds = new HashSet<>();
a.iterator().forEachRemaining(id -> groupIds.add(Long.valueOf(id.getString())));
if (!ignore) {
log.debug("Including groups '{}' due to '{}' not set on a resource", groupIds, ignoreGroupsAttribute);
ids.addAll(groupIds);
} else {
log.debug("Ignoring groups '{}' due to '{}' set on a resource", groupIds, ignoreGroupsAttribute);
}
}
}
......@@ -844,6 +883,33 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
return mappings;
}
private AttributeMapping getMappingForAttrName(PerunEntityType entity, String attrToFetch) {
AttributeMapping mapping;
switch (entity) {
case USER:
mapping = getUserAttributesMappingService().getMappingByIdentifier(attrToFetch);
break;
case FACILITY:
mapping = getFacilityAttributesMappingService().getMappingByIdentifier(attrToFetch);
break;
case VO:
mapping = getVoAttributesMappingService().getMappingByIdentifier(attrToFetch);
break;
case GROUP:
mapping = getGroupAttributesMappingService().getMappingByIdentifier(attrToFetch);
break;
case RESOURCE:
mapping = getResourceAttributesMappingService().getMappingByIdentifier(attrToFetch);
break;
default:
log.error("Unknown ENTITY {}", entity);
mapping = null;
break;
}
return mapping;
}
private String[] getAttributesFromMappings(Set<AttributeMapping> mappings) {
return mappings.stream()
.map(AttributeMapping::getLdapName)
......
......@@ -96,14 +96,18 @@ public class ClaimUtils {
return res;
}
public static Set<Group> getUserGroupsOnFacility(Facility facility, Long userId,
PerunAdapter perunAdapter, String claimName)
public static Set<Group> getUserGroupsOnFacility(Facility facility,
Long userId,
PerunAdapter perunAdapter,
String resourceGroupEntitlementDisabledAttribute,
String claimName)
{
Set<Group> userGroups = new HashSet<>();
if (facility == null) {
log.warn("{} - no facility provided when searching for user groups, will return empty set", claimName);
} else {
userGroups = perunAdapter.getGroupsWhereUserIsActiveWithUniqueNames(facility.getId(), userId);
userGroups = perunAdapter.getGroupsWhereUserIsActiveWithUniqueNames(
facility.getId(), userId, resourceGroupEntitlementDisabledAttribute);
}
log.trace("{} - found user groups: '{}'", claimName, userGroups);
return userGroups;
......
......@@ -45,7 +45,8 @@ public class EntitlementExtendedClaimSource extends EntitlementSource {
}
private Set<String> produceEntitlementsExtended(Facility facility, Long userId, PerunAdapter perunAdapter) {
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(facility, userId, perunAdapter, getClaimName());
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(
facility, userId, perunAdapter, getClaimName(), getGroupEntitlementDisabledAttr());
Map<Long, String> groupIdToNameMap = super.getGroupIdToNameMap(userGroups, false);
Set<String> entitlements = new TreeSet<>();
this.fillUuidEntitlements(userGroups, entitlements);
......
......@@ -9,6 +9,7 @@ import cz.muni.ics.oidc.server.adapters.PerunAdapter;
import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
import cz.muni.ics.oidc.server.claims.ClaimUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
......@@ -30,6 +31,10 @@ import java.util.TreeSet;
* </li>
* <li><b>custom.claim.[claimName].source.resourceCapabilities</b> - resource capabilities attribute name for resources</li>
* <li><b>custom.claim.[claimName].source.facilityCapabilities</b> - resource capabilities attribute name for facility</li>
* <li>
* <b>custom.claim.[claimName].source.groupEntitlementDisabledAttribute</b> - resource attribute which triggers
* if resource assigned groups should not be used for generating group entitlements.
* When not specified, all groups will be used</li>
* <li><b>custom.claim.[claimName].source.prefix</b> - string to be prepended to the value,</li>
* <li>
* <b>custom.claim.[claimName].source.authority</b> - string to be appended to the value, represents authority
......@@ -48,12 +53,17 @@ public class EntitlementSource extends GroupNamesSource {
protected static final String FORWARDED_ENTITLEMENTS = "forwardedEntitlements";
protected static final String RESOURCE_CAPABILITIES = "resourceCapabilities";
protected static final String FACILITY_CAPABILITIES = "facilityCapabilities";
protected static final String GROUP_ENTITLEMENT_DISABLED_ATTR = "groupEntitlementDisabledAttribute";
protected static final String PREFIX = "prefix";
protected static final String AUTHORITY = "authority";
private final String forwardedEntitlements;
private final String resourceCapabilities;
private final String facilityCapabilities;
@Getter
private final String groupEntitlementDisabledAttr;
private final String prefix;
private final String authority;
......@@ -63,6 +73,7 @@ public class EntitlementSource extends GroupNamesSource {
this.forwardedEntitlements = ClaimUtils.fillStringPropertyOrDefaultVal(FORWARDED_ENTITLEMENTS, ctx, null);
this.resourceCapabilities = ClaimUtils.fillStringPropertyOrDefaultVal(RESOURCE_CAPABILITIES, ctx, null);
this.facilityCapabilities = ClaimUtils.fillStringPropertyOrDefaultVal(FACILITY_CAPABILITIES, ctx, null);
this.groupEntitlementDisabledAttr = ClaimUtils.fillStringPropertyOrDefaultVal(GROUP_ENTITLEMENT_DISABLED_ATTR, ctx, null);
this.prefix = ClaimUtils.fillStringMandatoryProperty(PREFIX, ctx, getClaimName());
this.authority = ClaimUtils.fillStringMandatoryProperty(AUTHORITY, ctx, getClaimName());
......@@ -86,7 +97,7 @@ public class EntitlementSource extends GroupNamesSource {
PerunAdapter perunAdapter = pctx.getPerunAdapter();
Long userId = pctx.getPerunUserId();
Facility facility = pctx.getFacility();
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(facility, userId, perunAdapter, getClaimName());
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(facility, userId, perunAdapter, groupEntitlementDisabledAttr, getClaimName());
Set<String> entitlements = produceEntitlements(facility, userGroups, userId, perunAdapter);
JsonNode result = ClaimUtils.convertResultStringsToJsonArray(entitlements);
......
......@@ -49,7 +49,7 @@ public class GroupNamesSource extends ClaimSource {
log.trace("{} - produce group names with trimming 'members' part of the group names", getClaimName());
Facility facility = pctx.getFacility();
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(facility, pctx.getPerunUserId(),
pctx.getPerunAdapter(), getClaimName());
pctx.getPerunAdapter(), getClaimName(), null);
return getGroupIdToNameMap(userGroups, true);
}
......
......@@ -58,7 +58,7 @@ public class ResourceAssignedActiveMemberGroupsClaimSource extends ClaimSource {
Long userId = pctx.getPerunUserId();
Facility facility = pctx.getFacility();
PerunAdapter perunAdapter = pctx.getPerunAdapter();
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(facility, userId, perunAdapter, getClaimName());
Set<Group> userGroups = ClaimUtils.getUserGroupsOnFacility(facility, userId, perunAdapter, getClaimName(), null);
return getValuesFromAttribute(userGroups, perunAdapter);
}
......
......@@ -37,182 +37,232 @@ import java.util.Map;
@Slf4j
public class PerunConnectorRpc implements InitializingBean {
public static final String ATTRIBUTES_MANAGER = "attributesManager";
public static final String FACILITIES_MANAGER = "facilitiesManager";
public static final String GROUPS_MANAGER = "groupsManager";
public static final String MEMBERS_MANAGER = "membersManager";
public static final String REGISTRAR_MANAGER = "registrarManager";
public static final String SEARCHER = "searcher";
public static final String USERS_MANAGER = "usersManager";
public static final String VOS_MANAGER = "vosManager";
public static final String RESOURCES_MANAGER = "resourcesManager";
private String perunUrl;
private String perunUser;
private String perunPassword;
private boolean isEnabled;
private String serializer;
private int connectionRequestTimeout = 30000;
private int connectionTimeout = 30000;
private int responseTimeout = 60000;
private RestTemplate restTemplate;
public PerunConnectorRpc(String url,
String username,
String password,
String enabled,
String serializer,
int connectionRequestTimeout,
int connectionTimeout,
int responseTimeout)
{
this.isEnabled = Boolean.parseBoolean(enabled);
this.setPerunUrl(url);
this.setPerunUser(username);
this.setPerunPassword(password);
this.setSerializer(serializer);
this.setConnectionRequestTimeout(connectionRequestTimeout);
this.setConnectionTimeout(connectionTimeout);
this.setResponseTimeout(responseTimeout);
}
private void setEnabled(String enabled) {
this.isEnabled = Boolean.parseBoolean(enabled);
}
public boolean isEnabled() {
return isEnabled;
}
private void setPerunUrl(String perunUrl) {
if (!StringUtils.hasText(perunUrl)) {
throw new IllegalArgumentException("Perun URL cannot be null or empty");
} else if (perunUrl.endsWith("/")) {
perunUrl = perunUrl.substring(0, perunUrl.length() - 1);
}
this.perunUrl = perunUrl;
}
private void setPerunUser(String perunUser) {
if (!StringUtils.hasText(perunUser)) {
throw new IllegalArgumentException("Perun USER cannot be null or empty");
}
this.perunUser = perunUser;
}
private void setPerunPassword(String perunPassword) {
if (!StringUtils.hasText(perunPassword)) {
throw new IllegalArgumentException("Perun PASSWORD cannot be null or empty");
}
this.perunPassword = perunPassword;
}
private void setSerializer(String serializer) {
if (!StringUtils.hasText(serializer)) {
serializer = "json";
}
this.serializer = serializer;
}
private void setConnectionRequestTimeout(int connectionRequestTimeout) {
if (0 >= connectionRequestTimeout) {
throw new IllegalArgumentException("Connection request timeout must be greater than 0ms");
}
this.connectionRequestTimeout = connectionRequestTimeout;
}
private void setConnectionTimeout(int connectionTimeout) {
if (0 >= connectionTimeout) {
throw new IllegalArgumentException("Connection timeout must be greater than 0ms");
}
this.connectionTimeout = connectionTimeout;
}
private void setResponseTimeout(int responseTimeout) {
if (0 >= responseTimeout) {
throw new IllegalArgumentException("Response timeout must be greater than 0ms");
}
this.responseTimeout = responseTimeout;
}
@Override
public void afterPropertiesSet() {
restTemplate = new RestTemplate();
//HTTP connection pooling, see https://howtodoinjava.com/spring-restful/resttemplate-httpclient-java-config/
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(this.connectionRequestTimeout) // The timeout when requesting a connection from the connection manager
.setConnectTimeout(this.connectionTimeout) // Determines the timeout in milliseconds until a connection is established
.setSocketTimeout(this.responseTimeout) // The timeout for waiting for data
.build();
PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager();
poolingConnectionManager.setMaxTotal(20); // maximum connections total
poolingConnectionManager.setDefaultMaxPerRoute(18);
ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (response, context) -> {
HeaderElementIterator it = new BasicHeaderElementIterator
(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return 20000L;
};
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(poolingConnectionManager)
.setKeepAliveStrategy(connectionKeepAliveStrategy)
.build();
HttpComponentsClientHttpRequestFactory poolingRequestFactory = new HttpComponentsClientHttpRequestFactory();
poolingRequestFactory.setHttpClient(httpClient);
//basic authentication
List<ClientHttpRequestInterceptor> interceptors =
Collections.singletonList(new BasicAuthorizationInterceptor(perunUser, perunPassword));
InterceptingClientHttpRequestFactory authenticatingRequestFactory = new InterceptingClientHttpRequestFactory(poolingRequestFactory, interceptors);
restTemplate.setRequestFactory(authenticatingRequestFactory);
}
/**
* Make post call to Perun RPC
* @param manager String value representing manager to be called. Use constants from this class.
* @param method Method to be called (i.e. getUserById)
* @param map Map of parameters to be passed as request body
* @return Response from Perun
*/
@LogTimes
public JsonNode post(String manager, String method, Map<String, Object> map) {
if (!this.isEnabled) {
return JsonNodeFactory.instance.nullNode();
}
String actionUrl = perunUrl + '/' + serializer + '/' + manager + '/' + method;
//make the call
try {
log.debug("calling {} with {}", actionUrl, map);
return restTemplate.postForObject(actionUrl, map, JsonNode.class);
} catch (HttpClientErrorException ex) {
MediaType contentType = ex.getResponseHeaders().getContentType();
String body = ex.getResponseBodyAsString();
log.error("HTTP ERROR " + ex.getRawStatusCode() + " URL " + actionUrl + " Content-Type: " + contentType);
if ("json".equals(contentType.getSubtype())) {
try {
log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText());
} catch (IOException e) {
log.error("cannot parse error message from JSON", e);
}
} else {
log.error(ex.getMessage());
}
throw new RuntimeException("cannot connect to Perun RPC", ex);
}
}
public static final String ATTRIBUTES_MANAGER = "attributesManager";
public static final String FACILITIES_MANAGER = "facilitiesManager";
public static final String GROUPS_MANAGER = "groupsManager";
public static final String MEMBERS_MANAGER = "membersManager";
public static final String REGISTRAR_MANAGER = "registrarManager";
public static final String SEARCHER = "searcher";
public static final String USERS_MANAGER = "usersManager";
public static final String VOS_MANAGER = "vosManager";
public static final String RESOURCES_MANAGER = "resourcesManager";
public static final String PARAM_ID = "id";
public static final String PARAM_RESOURCE = "resource";
public static final String PARAM_FACILITY = "facility";
public static final String PARAM_GROUP = "group";
public static final String PARAM_VO = "vo";
public static final String PARAM_USER = "user";
public static final String PARAM_MEMBER = "member";
public static final String PARAM_USER_EXT_SOURCE = "userExtSource";
public static final String PARAM_ATTRIBUTE = "attribute";
public static final String PARAM_ATTRIBUTE_NAME = "attributeName";
public static final String PARAM_ATTRIBUTE_VALUE = "attributeValue";
public static final String PARAM_ATTRIBUTE_DEFINITION = "attributeDefinition";
public static final String PARAM_ATTR_NAMES = "attrNames";
public static final String PARAM_ATTR_NAME = "attrName";
public static final String PARAM_EXT_LOGIN = "extLogin";
public static final String PARAM_EXT_SOURCE_NAME = "extSourceName";
public static final String PARAM_SHORT_NAME = "shortName";
// METHODS
public static final String METHOD_GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN =
"getUserByExtSourceNameAndExtLogin";
public static final String METHOD_GET_FACILITIES_BY_ATTRIBUTE = "getFacilitiesByAttribute";
public static final String METHOD_GET_ATTRIBUTE = "getAttribute";
public static final String METHOD_GET_GROUP_BY_ID = "getGroupById";
public static final String METHOD_GET_MEMBER_BY_USER = "getMemberByUser";
public static final String METHOD_IS_GROUP_MEMBER = "isGroupMember";
public static final String METHOD_SET_ATTRIBUTE = "setAttribute";
public static final String EXT_SOURCE_IDP = "cz.metacentrum.perun.core.impl.ExtSourceIdp";
public static final String METHOD_GET_ENTITYLESS_ATTRIBUTES = "getEntitylessAttributes";
public static final String METHOD_GET_ENTITYLESS_KEYS = "getEntitylessKeys";
public static final String METHOD_GET_VO_BY_SHORT_NAME = "getVoByShortName";
public static final String METHOD_GET_USER_BY_ID = "getUserById";
public static final String METHOD_GET_GROUPS_WHERE_USER_IS_ACTIVE = "getGroupsWhereUserIsActive";
public static final String METHOD_GET_ALLOWED_RESOURCES = "getAllowedResources";
public static final String METHOD_GET_VO_BY_ID = "getVoById";
public static final String METHOD_GET_ASSIGNED_GROUPS = "getAssignedGroups";
public static final String METHOD_GET_ATTRIBUTES = "getAttributes";
public static final String METHOD_GET_MEMBER_GROUPS = "getMemberGroups";
public static final String METHOD_GET_MEMBERS_BY_USER = "getMembersByUser";
public static final String METHOD_GET_ALLOWED_GROUPS = "getAllowedGroups";
public static final String METHOD_GET_APPLICATION_FORM = "getApplicationForm";
public static final String METHOD_GET_ASSIGNED_RESOURCES = "getAssignedResources";
public static final String METHOD_GET_ASSIGNED_RICH_RESOURCES = "getAssignedRichResources";
public static final String METHOD_GET_RICH_GROUPS_ASSIGNED_TO_RESOURCE_WITH_ATTRIBUTES_BY_NAMES =
"getRichGroupsAssignedToResourceWithAttributesByNames";
public static final String METHOD_GET_USER_EXT_SOURCES = "getUserExtSources";
// VARIABLES
private String perunUrl;
private String perunUser;
private String perunPassword;
private boolean isEnabled;
private String serializer;
private int connectionRequestTimeout = 30000;
private int connectionTimeout = 30000;
private int responseTimeout = 60000;
private RestTemplate restTemplate;
public PerunConnectorRpc(String url,
String username,
String password,
String enabled,
String serializer,
int connectionRequestTimeout,
int connectionTimeout,
int responseTimeout) {
this.isEnabled = Boolean.parseBoolean(enabled);
this.setPerunUrl(url);
this.setPerunUser(username);
this.setPerunPassword(password);
this.setSerializer(serializer);
this.setConnectionRequestTimeout(connectionRequestTimeout);
this.setConnectionTimeout(connectionTimeout);
this.setResponseTimeout(responseTimeout);
}
private void setEnabled(String enabled) {
this.isEnabled = Boolean.parseBoolean(enabled);
}
public boolean isEnabled() {
return isEnabled;
}
private void setPerunUrl(String perunUrl) {
if (!StringUtils.hasText(perunUrl)) {
throw new IllegalArgumentException("Perun URL cannot be null or empty");
} else if (perunUrl.endsWith("/")) {
perunUrl = perunUrl.substring(0, perunUrl.length() - 1);
}
this.perunUrl = perunUrl;
}
private void setPerunUser(String perunUser) {
if (!StringUtils.hasText(perunUser)) {
throw new IllegalArgumentException("Perun USER cannot be null or empty");
}
this.perunUser = perunUser;
}
private void setPerunPassword(String perunPassword) {
if (!StringUtils.hasText(perunPassword)) {
throw new IllegalArgumentException("Perun PASSWORD cannot be null or empty");
}
this.perunPassword = perunPassword;
}
private void setSerializer(String serializer) {
if (!StringUtils.hasText(serializer)) {
serializer = "json";
}
this.serializer = serializer;
}
private void setConnectionRequestTimeout(int connectionRequestTimeout) {
if (0 >= connectionRequestTimeout) {
throw new IllegalArgumentException("Connection request timeout must be greater than 0ms");
}
this.connectionRequestTimeout = connectionRequestTimeout;
}
private void setConnectionTimeout(int connectionTimeout) {
if (0 >= connectionTimeout) {
throw new IllegalArgumentException("Connection timeout must be greater than 0ms");
}
this.connectionTimeout = connectionTimeout;
}
private void setResponseTimeout(int responseTimeout) {
if (0 >= responseTimeout) {
throw new IllegalArgumentException("Response timeout must be greater than 0ms");
}
this.responseTimeout = responseTimeout;
}
@Override
public void afterPropertiesSet() {
restTemplate = new RestTemplate();
//HTTP connection pooling, see https://howtodoinjava.com/spring-restful/resttemplate-httpclient-java-config/
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(this.connectionRequestTimeout) // The timeout when requesting a connection from the connection manager
.setConnectTimeout(this.connectionTimeout) // Determines the timeout in milliseconds until a connection is established
.setSocketTimeout(this.responseTimeout) // The timeout for waiting for data
.build();
PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager();
poolingConnectionManager.setMaxTotal(20); // maximum connections total
poolingConnectionManager.setDefaultMaxPerRoute(18);
ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (response, context) -> {
HeaderElementIterator it = new BasicHeaderElementIterator
(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return 20000L;
};
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(poolingConnectionManager)
.setKeepAliveStrategy(connectionKeepAliveStrategy)
.build();
HttpComponentsClientHttpRequestFactory poolingRequestFactory = new HttpComponentsClientHttpRequestFactory();
poolingRequestFactory.setHttpClient(httpClient);
//basic authentication
List<ClientHttpRequestInterceptor> interceptors =
Collections.singletonList(new BasicAuthorizationInterceptor(perunUser, perunPassword));
InterceptingClientHttpRequestFactory authenticatingRequestFactory = new InterceptingClientHttpRequestFactory(poolingRequestFactory, interceptors);
restTemplate.setRequestFactory(authenticatingRequestFactory);
}
/**
* Make post call to Perun RPC
*
* @param manager String value representing manager to be called. Use constants from this class.
* @param method Method to be called (i.e. getUserById)
* @param map Map of parameters to be passed as request body
* @return Response from Perun
*/
@LogTimes
public JsonNode post(String manager, String method, Map<String, Object> map) {
if (!this.isEnabled) {
return JsonNodeFactory.instance.nullNode();
}
String actionUrl = perunUrl + '/' + serializer + '/' + manager + '/' + method;
//make the call
try {
log.debug("calling {} with {}", actionUrl, map);
return restTemplate.postForObject(actionUrl, map, JsonNode.class);
} catch (HttpClientErrorException ex) {
MediaType contentType = ex.getResponseHeaders().getContentType();
String body = ex.getResponseBodyAsString();
log.error("HTTP ERROR " + ex.getRawStatusCode() + " URL " + actionUrl + " Content-Type: " + contentType);
if ("json".equals(contentType.getSubtype())) {
try {
log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText());
} catch (IOException e) {
log.error("cannot parse error message from JSON", e);
}
} else {
log.error(ex.getMessage());
}
throw new RuntimeException("cannot connect to Perun RPC", ex);
}
}
}
......@@ -23,7 +23,6 @@ public interface AuthProcFilterConstants {
String PARAM_ACCEPTED = "accepted";
String PARAM_ACR_VALUES = "acr_values";
String PARAM_MAX_AGE = "max_age";
String PARAM_POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri";
String PARAM_STATE = "state";
String CLIENT_ID_PREFIX = "urn:cesnet:proxyidp:client_id:";
String AARC_IDP_HINT = "aarc_idp_hint";
......
......@@ -31,6 +31,11 @@ import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.STATE;
*
* Configuration:
* - based on the configuration of bean "facilityAttrsConfig"
* Configuration of filter (replace [name] part with the name defined for the filter):
* <ul>
* <li><b>filter.[name].accessControlDisabledAttr</b> - resource attribute which triggers if resource assigned
* groups should not be used for controlling access. When not specified, all groups will be used.</li>
* </ul>
* @see FacilityAttrsConfig
* @see cz.muni.ics.oidc.server.filters.AuthProcFilter (basic configuration options)
*
......@@ -39,15 +44,19 @@ import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.STATE;
@Slf4j
public class PerunAuthorizationFilter extends AuthProcFilter {
protected static final String ACCESS_CONTROL_DISABLED_ATTR = "accessControlDisabledAttr";
private final PerunAdapter perunAdapter;
private final FacilityAttrsConfig facilityAttrsConfig;
private final PerunOidcConfig config;
private final String accessControlDisabledAttr;
public PerunAuthorizationFilter(AuthProcFilterInitContext ctx) throws ConfigurationException {
super(ctx);
this.perunAdapter = ctx.getPerunAdapterBean();
this.config = ctx.getPerunOidcConfigBean();
this.facilityAttrsConfig = ctx.getBeanUtil().getBean(FacilityAttrsConfig.class);
this.accessControlDisabledAttr = ctx.getProperty(ACCESS_CONTROL_DISABLED_ATTR, null);
}
@Override
......@@ -65,7 +74,7 @@ public class PerunAuthorizationFilter extends AuthProcFilter {
}
return this.decideAccess(facility, user, req, res, params.getClientIdentifier(),
perunAdapter, facilityAttrsConfig);
perunAdapter, facilityAttrsConfig, accessControlDisabledAttr);
}
@Override
......@@ -73,9 +82,14 @@ public class PerunAuthorizationFilter extends AuthProcFilter {
return false;
}
private boolean decideAccess(Facility facility, PerunUser user, HttpServletRequest req,
HttpServletResponse response, String clientIdentifier, PerunAdapter perunAdapter,
FacilityAttrsConfig facilityAttrsConfig)
private boolean decideAccess(Facility facility,
PerunUser user,
HttpServletRequest req,
HttpServletResponse response,
String clientIdentifier,
PerunAdapter perunAdapter,
FacilityAttrsConfig facilityAttrsConfig,
String accessControlDisabledAttr)
{
Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(
facility, facilityAttrsConfig.getMembershipAttrNames());
......@@ -85,7 +99,7 @@ public class PerunAuthorizationFilter extends AuthProcFilter {
return true;
}
if (perunAdapter.canUserAccessBasedOnMembership(facility, user.getId())) {
if (perunAdapter.canUserAccessBasedOnMembership(facility, user.getId(), accessControlDisabledAttr)) {
log.info("{} - user allowed to access the service", getFilterName());
return true;
} else {
......
......@@ -31,11 +31,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.saml.SAMLLogoutFilter;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
......@@ -45,8 +46,6 @@ import javax.servlet.http.HttpSession;
import java.text.ParseException;
import java.util.Map;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_POST_LOGOUT_REDIRECT_URI;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_STATE;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_TARGET;
/**
......@@ -69,9 +68,23 @@ public class EndSessionEndpoint {
public static final String URL = "endsession";
private static final String CLIENT_KEY = "client";
private static final String STATE_KEY = "state";
private static final String REDIRECT_URI_KEY = "redirectUri";
public static final String PARAM_POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri";
public static final String PARAM_STATE = "state";
public static final String PARAM_CLIENT_ID = "client_id";
public static final String PARAM_ID_TOKEN_HINT = "id_token_hint";
private static final String SESSION_KEY_CLIENT = "client";
private static final String SESSION_KEY_STATE = "state";
private static final String SESSION_KEY_REDIRECT_URI = "redirect_uri";
private static final String MODEL_CLIENT_KEY = "client";
private static final String PREFIX_REDIRECT = "redirect:";
private final SelfAssertionValidator validator;
private final PerunOidcConfig perunOidcConfig;
......@@ -90,73 +103,67 @@ public class EndSessionEndpoint {
this.htmlClasses = htmlClasses;
}
@RequestMapping(value = "/" + URL, method = RequestMethod.GET)
public String endSession(@RequestParam(value = "id_token_hint", required = false) String idTokenHint,
@GetMapping(value = "/" + URL)
public String endSession(@RequestParam(value = PARAM_ID_TOKEN_HINT, required = false) String idTokenHint,
@RequestParam(value = PARAM_POST_LOGOUT_REDIRECT_URI, required = false) String postLogoutRedirectUri,
@RequestParam(value = STATE_KEY, required = false) String state,
@RequestParam(value = PARAM_STATE, required = false) String state,
@RequestParam(value = PARAM_CLIENT_ID, required = false) String clientId,
HttpServletRequest request,
HttpSession session,
Authentication auth, Map<String, Object> model)
Authentication auth,
Map<String, Object> model)
{
JWTClaimsSet idTokenClaims = null; // pulled from the parsed and validated ID token
ClientDetailsEntity client = null; // pulled from ID token's audience field
if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) {
session.setAttribute(REDIRECT_URI_KEY, postLogoutRedirectUri);
session.setAttribute(SESSION_KEY_REDIRECT_URI, postLogoutRedirectUri);
}
if (!Strings.isNullOrEmpty(state)) {
session.setAttribute(STATE_KEY, state);
session.setAttribute(SESSION_KEY_STATE, state);
}
// parse the ID token hint to see if it's valid
if (!Strings.isNullOrEmpty(idTokenHint)) {
try {
JWT idToken = JWTParser.parse(idTokenHint);
if (validator.isValid(idToken)) {
// we issued this ID token, figure out who it's for
idTokenClaims = idToken.getJWTClaimsSet();
String clientId = Iterables.getOnlyElement(idTokenClaims.getAudience());
client = clientService.loadClientByClientId(clientId);
clientId = resolveClientIdFromTokenAndParameter(idTokenHint, clientId);
}
// save a reference in the session for us to pick up later
//session.setAttribute("endSession_idTokenHint_claims", idTokenClaims);
session.setAttribute(CLIENT_KEY, client);
}
} catch (ParseException e) {
// it's not a valid ID token, ignore it
log.debug("Invalid id token hint", e);
} catch (InvalidClientException e) {
if (StringUtils.hasText(clientId)) {
try {
client = clientService.loadClientByClientId(clientId);
session.setAttribute(SESSION_KEY_CLIENT, client);
} catch (InvalidClientException e) {
// couldn't find the client, ignore it
log.debug("Invalid client", e);
throw new InvalidRequestException(
"Client requesting the logout cannot be found. Is someone doing something nasty?"
);
}
}
// are we logged in or not?
if (auth == null || !request.isUserInRole("ROLE_USER")) {
// we're not logged in anyway, process the final redirect bits if needed
return processLogout(null, null, session);
// We're not logged in, anyway. Process the final redirect bits if needed.
return processLogout(null, null, request, session, model);
} else {
log.info("Logout confirmating for user {} from client {}", auth.getName(), client != null ? client.getClientName() : "unknown");
log.info("Display logout confirm prompt for user {} from client {}",
auth.getName(), client != null ? client.getClientName() : "unknown"
);
// we are logged in, need to prompt the user before we log out
model.put("client", client);
model.put("idToken", idTokenClaims);
model.put(MODEL_CLIENT_KEY, client);
ControllerUtils.setPageOptions(model, request, htmlClasses, perunOidcConfig);
return "logout";
}
}
@RequestMapping(value = "/" + URL, method = RequestMethod.POST)
@PostMapping(value = "/" + URL)
public String processLogout(@RequestParam(value = "approve", required = false) String approved,
@RequestParam(value = "deny", required = false) String deny,
HttpSession session)
HttpServletRequest request,
HttpSession session,
Map<String, Object> model)
{
String redirectUri = (String) session.getAttribute(REDIRECT_URI_KEY);
String state = (String) session.getAttribute(STATE_KEY);
ClientDetailsEntity client = (ClientDetailsEntity) session.getAttribute(CLIENT_KEY);
String redirectUri = (String) session.getAttribute(SESSION_KEY_REDIRECT_URI);
String state = (String) session.getAttribute(SESSION_KEY_STATE);
ClientDetailsEntity client = (ClientDetailsEntity) session.getAttribute(SESSION_KEY_CLIENT);
String redirectURL = null;
// if we have a client AND the client has post-logout redirect URIs
......@@ -164,10 +171,9 @@ public class EndSessionEndpoint {
if (isUriValid(redirectUri, client)) {
UriComponentsBuilder uri = UriComponentsBuilder.fromHttpUrl(redirectUri);
if (StringUtils.hasText(state)) {
uri = uri.queryParam("state", state);
uri = uri.queryParam(PARAM_STATE, state);
}
UriComponents uriComponents = uri.build();
log.trace("redirect URL: {}", uriComponents);
redirectURL = uriComponents.toString();
}
......@@ -175,22 +181,52 @@ public class EndSessionEndpoint {
String target = getRedirectUrl(redirectUri, state);
if (StringUtils.hasText(approved)) {
target = getLogoutUrl(target);
log.trace("redirecting to logout SAML and then {}", target);
return "redirect:" + target;
log.trace("Endsession - redirecting to SAML logout, then to {}", target);
return PREFIX_REDIRECT + target;
} else {
log.trace("redirecting to {}", target);
return "redirect:" + redirectURL;
log.trace("Endsession - redirecting to {}", target);
return PREFIX_REDIRECT + redirectURL;
}
} else {
if (StringUtils.hasText(approved)) {
log.trace("redirecting to logout SAML only");
return "redirect:" + getLogoutUrl(null);
log.trace("Endsession - redirecting to SAML logout only");
return PREFIX_REDIRECT + getLogoutUrl(null);
} else {
ControllerUtils.setPageOptions(model, request, htmlClasses, perunOidcConfig);
log.trace("Endsession - user denied the logout and we have no redirect, display logout denied page");
return "logout_denied";
}
}
}
private String resolveClientIdFromTokenAndParameter(String idTokenHint, String clientId) {
JWTClaimsSet idTokenClaims = null;
try {
JWT idToken = JWTParser.parse(idTokenHint);
if (validator.isValid(idToken)) {
idTokenClaims = idToken.getJWTClaimsSet();
}
} catch (ParseException e) {
// it's not a valid ID token, ignore it
log.debug("Invalid id token hint", e);
}
if (idTokenClaims != null) {
String clientIdFromToken = Iterables.getOnlyElement(idTokenClaims.getAudience());
if (StringUtils.hasText(clientId)) {
if (StringUtils.hasText(clientIdFromToken) && !clientIdFromToken.equals(clientId)) {
throw new InvalidRequestException(
"Client ID and client for which the ID token has been issued do not match. Is someone doing something nasty?"
);
}
} else {
clientId = clientIdFromToken;
}
}
return clientId;
}
private boolean isUriValid(String redirectUri, ClientDetailsEntity client) {
return StringUtils.hasText(redirectUri)
&& client != null
......
......@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>cz.muni.ics</groupId>
<artifactId>perun-oidc-parent</artifactId>
<version>18.4.0</version>
<version>18.5.0</version>
<packaging>pom</packaging>
<modules>
......