Enable roles for authority lookup / permission management

This commit is contained in:
AFaust
2020-02-16 15:01:59 +01:00
parent 0f974c9f1d
commit 146f91f011
19 changed files with 526 additions and 42 deletions

15
pom.xml
View File

@@ -204,6 +204,21 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.repo</artifactId>
<version>${acosix.utility.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.repo</artifactId>
<version>${acosix.utility.version}</version>
<classifier>installable</classifier>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.orderofthebee.support-tools</groupId> <groupId>org.orderofthebee.support-tools</groupId>
<artifactId>support-tools-repo</artifactId> <artifactId>support-tools-repo</artifactId>

View File

@@ -75,6 +75,11 @@
<constructor-arg value="cache.${moduleId}.ticketTokenCache" /> <constructor-arg value="cache.${moduleId}.ticketTokenCache" />
</bean> </bean>
<bean id="${moduleId}.ScriptRoleService" parent="baseJavaScriptExtension" class="${project.artifactId}.roles.ScriptRoleService">
<property name="extensionName" value="keycloakRoles" />
<property name="roleService" ref="${moduleId}.RoleService" />
</bean>
<bean id="webscript.de.acosix.keycloak.roles.get" class="${project.artifactId}.web.scripts.RolesGet" parent="webscript"> <bean id="webscript.de.acosix.keycloak.roles.get" class="${project.artifactId}.web.scripts.RolesGet" parent="webscript">
<property name="roleService" ref="${moduleId}.RoleService" /> <property name="roleService" ref="${moduleId}.RoleService" />
</bean> </bean>

View File

@@ -0,0 +1,18 @@
<?xml version='1.0' encoding='UTF-8'?>
<extension>
<modules>
<module>
<id>${moduleId} - Web Script Extension</id>
<description>${project.name}</description>
<version>${noSnapshotVersion}</version>
<auto-deploy>true</auto-deploy>
<customizations>
<customization>
<targetPackageRoot>org.alfresco</targetPackageRoot>
<sourcePackageRoot>de.acosix.keycloak.customisations</sourcePackageRoot>
</customization>
</customizations>
</module>
</modules>
</extension>

View File

@@ -256,10 +256,6 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
public RefreshableAccessTokenHolder checkAndRefreshTicketToken(final RefreshableAccessTokenHolder ticketToken) public RefreshableAccessTokenHolder checkAndRefreshTicketToken(final RefreshableAccessTokenHolder ticketToken)
throws AuthenticationException throws AuthenticationException
{ {
if (this.failExpiredTicketTokens && ticketToken.isExpired())
{
throw new AuthenticationException("Keycloak access token has expired - authentication ticket is no longer valid");
}
RefreshableAccessTokenHolder result = null; RefreshableAccessTokenHolder result = null;
if (ticketToken.canRefresh() && ticketToken.shouldRefresh(this.deployment.getTokenMinimumTimeToLive())) if (ticketToken.canRefresh() && ticketToken.shouldRefresh(this.deployment.getTokenMinimumTimeToLive()))
@@ -289,6 +285,10 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
throw new AuthenticationException("Failed to refresh Keycloak authentication", ioex); throw new AuthenticationException("Failed to refresh Keycloak authentication", ioex);
} }
} }
else if (this.failExpiredTicketTokens && ticketToken.isExpired())
{
throw new AuthenticationException("Keycloak access token has expired - authentication ticket is no longer valid");
}
if (result != null || !ticketToken.isExpired()) if (result != null || !ticketToken.isExpired())
{ {

View File

@@ -27,6 +27,16 @@ import java.util.List;
public class NoOpRoleServiceImpl implements RoleService public class NoOpRoleServiceImpl implements RoleService
{ {
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> listRoles()
{
return Collections.emptyList();
}
/** /**
* *
* {@inheritDoc} * {@inheritDoc}
@@ -37,6 +47,16 @@ public class NoOpRoleServiceImpl implements RoleService
return Collections.emptyList(); return Collections.emptyList();
} }
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> listRoles(final boolean realmOnly)
{
return Collections.emptyList();
}
/** /**
* *
* {@inheritDoc} * {@inheritDoc}
@@ -47,6 +67,16 @@ public class NoOpRoleServiceImpl implements RoleService
return Collections.emptyList(); return Collections.emptyList();
} }
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> listRoles(final String resourceName)
{
return Collections.emptyList();
}
/** /**
* *
* {@inheritDoc} * {@inheritDoc}

View File

@@ -25,6 +25,13 @@ import java.util.List;
public interface RoleService public interface RoleService
{ {
/**
* Retrieves roles in the main realm and/or resource scopes (as far as possible based on configuration).
*
* @return the list of roles
*/
List<Role> listRoles();
/** /**
* Finds matching roles in the main realm and/or resource scopes (as far as possible based on configuration). * Finds matching roles in the main realm and/or resource scopes (as far as possible based on configuration).
* *
@@ -35,6 +42,17 @@ public interface RoleService
*/ */
List<Role> findRoles(String shortNameFilter); List<Role> findRoles(String shortNameFilter);
/**
* Retrieves roles in the main realm and/or resource scopes (as far as possible based on configuration).
*
* @param realmOnly
* {@code true} if the list operation should only consider the main realm, or {@code false} if both realm and resource
* scopes are allowed to be listed
*
* @return the list of roles
*/
List<Role> listRoles(boolean realmOnly);
/** /**
* Finds matching roles in the main realm and/or resource scopes (as far as possible based on configuration). * Finds matching roles in the main realm and/or resource scopes (as far as possible based on configuration).
* *
@@ -48,6 +66,16 @@ public interface RoleService
*/ */
List<Role> findRoles(String shortNameFilter, boolean realmOnly); List<Role> findRoles(String shortNameFilter, boolean realmOnly);
/**
* Retrieves roles in a specific resource scope (as far as possible based on configuration).
*
* @param resourceName
* the name of the resource for which to retrieve roles
*
* @return the list of roles
*/
List<Role> listRoles(String resourceName);
/** /**
* Finds matching roles in a specific resource scope (as far as possible based on configuration). * Finds matching roles in a specific resource scope (as far as possible based on configuration).
* *

View File

@@ -215,6 +215,16 @@ public class RoleServiceImpl implements InitializingBean, RoleService
this.hiddenMappedRoles = hiddenMappedRoles; this.hiddenMappedRoles = hiddenMappedRoles;
} }
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> listRoles()
{
return this.doFindRoles(null, false);
}
/** /**
* *
* {@inheritDoc} * {@inheritDoc}
@@ -222,7 +232,18 @@ public class RoleServiceImpl implements InitializingBean, RoleService
@Override @Override
public List<Role> findRoles(final String shortNameFilter) public List<Role> findRoles(final String shortNameFilter)
{ {
return this.findRoles(shortNameFilter, false); ParameterCheck.mandatoryString("shortNameFilter", shortNameFilter);
return this.doFindRoles(shortNameFilter, false);
}
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> listRoles(final boolean realmOnly)
{
return this.doFindRoles(null, realmOnly);
} }
/** /**
@@ -231,6 +252,35 @@ public class RoleServiceImpl implements InitializingBean, RoleService
*/ */
@Override @Override
public List<Role> findRoles(final String shortNameFilter, final boolean realmOnly) public List<Role> findRoles(final String shortNameFilter, final boolean realmOnly)
{
ParameterCheck.mandatoryString("shortNameFilter", shortNameFilter);
return this.doFindRoles(shortNameFilter, realmOnly);
}
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> listRoles(final String resourceName)
{
ParameterCheck.mandatory("resourceName", resourceName);
return this.doFindRoles(resourceName, null);
}
/**
*
* {@inheritDoc}
*/
@Override
public List<Role> findRoles(final String resourceName, final String shortNameFilter)
{
ParameterCheck.mandatory("resourceName", resourceName);
ParameterCheck.mandatoryString("shortNameFilter", shortNameFilter);
return this.doFindRoles(resourceName, shortNameFilter);
}
protected List<Role> doFindRoles(final String shortNameFilter, final boolean realmOnly)
{ {
final List<Role> roles; final List<Role> roles;
@@ -254,7 +304,7 @@ public class RoleServiceImpl implements InitializingBean, RoleService
if (!realmOnly && this.processResourceRoles) if (!realmOnly && this.processResourceRoles)
{ {
this.resourceRoleNameMapper.keySet().stream().forEach(resourceName -> { this.resourceRoleNameMapper.keySet().stream().forEach(resourceName -> {
final List<Role> resourceRoles = this.findRoles(resourceName, shortNameFilter); final List<Role> resourceRoles = this.doFindRoles(resourceName, shortNameFilter);
roles.addAll(resourceRoles); roles.addAll(resourceRoles);
}); });
} }
@@ -275,15 +325,8 @@ public class RoleServiceImpl implements InitializingBean, RoleService
return roles; return roles;
} }
/** protected List<Role> doFindRoles(final String resourceName, final String shortNameFilter)
*
* {@inheritDoc}
*/
@Override
public List<Role> findRoles(final String resourceName, final String shortNameFilter)
{ {
ParameterCheck.mandatory("resourceName", resourceName);
List<Role> roles; List<Role> roles;
if (this.enabled && !this.processResourceRoles) if (this.enabled && !this.processResourceRoles)

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2019 - 2020 Acosix GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.acosix.alfresco.keycloak.repo.roles;
import java.util.List;
import org.alfresco.repo.jscript.BaseScopableProcessorExtension;
import org.alfresco.util.PropertyCheck;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.springframework.beans.factory.InitializingBean;
/**
* This service exposes mapped Keycloak roles to scripts running within the Repository-tier script processor, e.g. web script controllers.
*
* @author Axel Faust
*/
public class ScriptRoleService extends BaseScopableProcessorExtension implements InitializingBean
{
protected RoleService roleService;
/**
*
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "roleService", this.roleService);
}
/**
* @param roleService
* the roleService to set
*/
public void setRoleService(final RoleService roleService)
{
this.roleService = roleService;
}
/**
* Retrieves roles in the main realm and/or resource scopes (as far as possible based on configuration).
*
* @return the list of roles
*/
public Scriptable listRoles()
{
final List<Role> roles = this.roleService.listRoles();
final Scriptable roleArray = this.makeRoleArray(roles);
return roleArray;
}
/**
* Finds matching roles in the main realm and/or resource scopes (as far as possible based on configuration).
*
* @param shortNameFilter
* name pattern on which to filter groups - the filter will be applied on both the original Keycloak and the mapped Alfresco
* role name, and a match in either will result the role to be considered a match
* @return the list of matching roles
*/
public Scriptable findRoles(final String shortNameFilter)
{
final List<Role> roles = this.roleService.findRoles(shortNameFilter);
final Scriptable roleArray = this.makeRoleArray(roles);
return roleArray;
}
/**
* Retrieves roles in the main realm and/or resource scopes (as far as possible based on configuration).
*
* @param realmOnly
* {@code true} if the list operation should only consider the main realm, or {@code false} if both realm and resource
* scopes are allowed to be listed
*
* @return the list of roles
*/
public Scriptable listRoles(final boolean realmOnly)
{
final List<Role> roles = this.roleService.listRoles(realmOnly);
final Scriptable roleArray = this.makeRoleArray(roles);
return roleArray;
}
/**
* Finds matching roles in the main realm and/or resource scopes (as far as possible based on configuration).
*
* @param shortNameFilter
* name pattern on which to filter groups - the filter will be applied on both the original Keycloak and the mapped Alfresco
* role name, and a match in either will result the role to be considered a match
* @param realmOnly
* {@code true} if the search operation should only consider the main realm, or {@code false} if both realm and resource
* scopes are allowed to be searched
* @return the list of matching roles
*/
public Scriptable findRoles(final String shortNameFilter, final boolean realmOnly)
{
final List<Role> roles = this.roleService.findRoles(shortNameFilter, realmOnly);
final Scriptable roleArray = this.makeRoleArray(roles);
return roleArray;
}
/**
* Retrieves roles in a specific resource scope (as far as possible based on configuration).
*
* @param resourceName
* the name of the resource for which to retrieve roles
*
* @return the list of roles
*/
public Scriptable listRoles(final String resourceName)
{
final List<Role> roles = this.roleService.listRoles(resourceName);
final Scriptable roleArray = this.makeRoleArray(roles);
return roleArray;
}
/**
* Finds matching roles in a specific resource scope (as far as possible based on configuration).
*
* @param resourceName
* the name of the resource for which to retrieve roles
* @param shortNameFilter
* name pattern on which to filter groups - the filter will be applied on both the original Keycloak and the mapped Alfresco
* role name, and a match in either will result the role to be considered a match
* @return the list of matching roles
*/
public Scriptable findRoles(final String resourceName, final String shortNameFilter)
{
final List<Role> roles = this.roleService.findRoles(resourceName, shortNameFilter);
final Scriptable roleArray = this.makeRoleArray(roles);
return roleArray;
}
protected Scriptable makeRoleArray(final List<Role> roles)
{
final Scriptable sitesArray = Context.getCurrentContext().newArray(this.getScope(), roles.toArray(new Object[0]));
return sitesArray;
}
}

View File

@@ -103,7 +103,9 @@ public class RolesGet extends DeclarativeWebScript implements InitializingBean
maxItems = Integer.parseInt(maxItemsParam); maxItems = Integer.parseInt(maxItemsParam);
} }
final List<Role> roles = this.roleService.findRoles(shortNameFilterParam); final List<Role> roles = shortNameFilterParam != null && !shortNameFilterParam.trim().isEmpty()
? this.roleService.findRoles(shortNameFilterParam)
: this.roleService.listRoles();
if (roles.isEmpty()) if (roles.isEmpty())
{ {

View File

@@ -0,0 +1,57 @@
/* global keycloakRoles: false */
var keycloakRolesHash;
function process(permissions)
{
var idx, permissionObj, authority, keycloakRolesArr, jdx, role;
for (idx = 0; idx < permissions.length; idx++)
{
permissionObj = permissions[idx];
authority = permissionObj.authority.name;
if (authority && /^ROLE_.+$/.test(authority))
{
// lazy init
if (!keycloakRolesHash)
{
keycloakRolesArr = keycloakRoles.listRoles();
keycloakRolesHash = {};
for (jdx = 0; jdx < keycloakRolesArr.length; jdx++)
{
keycloakRolesHash[keycloakRolesArr[jdx].name] = keycloakRolesArr[jdx];
}
}
// only process if role mapped from Keycloak
if (keycloakRolesHash.hasOwnProperty(authority))
{
role = keycloakRolesHash[authority];
if (role)
{
// enhance permissionObj.authority to at least add displayName
// may/will still look like a user in UI which only differentiates groups / users
permissionObj.authority = {
name : authority,
fullName : authority,
shortName : authority.substring(5),
displayName : role.description || role.keycloakName
};
}
}
}
}
}
function main()
{
var permissions = model.data;
process(permissions.direct);
process(permissions.inherited);
model.data = permissions;
}
main();

View File

@@ -0,0 +1,44 @@
/* global keycloakRoles: false */
function main()
{
var nodeRef, node, permissions, idx, permissionObj, add, authority, permission, keycloakRolesHash, keycloakRolesArr, jdx;
nodeRef = url.templateArgs.store_type + '://' + url.templateArgs.store_id + '/' + url.templateArgs.id;
// normally not a fan of Alfresco utils object, but needed here for consistency with base script (there via parse-args.lib.js import)
node = utils.resolveNodeReference(nodeRef);
permissions = json.getJSONArray('permissions');
for (idx = 0; idx < permissions.length(); idx++)
{
permissionObj = permissions.getJSONObject(idx);
add = !permissionObj.has('remove') || !permissionObj.getBoolean('remove');
if (add)
{
authority = permissionObj.getString('authority');
permission = permissionObj.getString('role');
if (/^ROLE_.+$/.test(authority))
{
// lazy init
if (!keycloakRolesHash)
{
keycloakRolesArr = keycloakRoles.listRoles();
keycloakRolesHash = {};
for (jdx = 0; jdx < keycloakRolesArr.length; jdx++)
{
keycloakRolesHash[keycloakRolesArr[jdx].name] = true;
}
}
// only process if role mapped from Keycloak
if (keycloakRolesHash.hasOwnProperty(authority))
{
node.setPermission(permission, authority);
}
}
}
}
}
main();

View File

@@ -88,6 +88,12 @@
<classifier>installable</classifier> <classifier>installable</classifier>
</dependency> </dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.repo</artifactId>
<classifier>installable</classifier>
</dependency>
<dependency> <dependency>
<groupId>de.acosix.alfresco.utility</groupId> <groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.share</artifactId> <artifactId>de.acosix.alfresco.utility.core.share</artifactId>

View File

@@ -392,6 +392,10 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.continueFilterChain(context, request, response, chain); this.continueFilterChain(context, request, response, chain);
} }
else if (res.isCommitted())
{
LOGGER.debug("Response has already been committed by skip condition-check - not processing it any further");
}
else else
{ {
this.processKeycloakAuthenticationAndActions(context, req, res, chain); this.processKeycloakAuthenticationAndActions(context, req, res, chain);
@@ -819,34 +823,34 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
if (!this.externalAuthEnabled || !this.filterEnabled) if (!this.externalAuthEnabled || !this.filterEnabled)
{ {
LOGGER.debug("Skipping doFilter as filter and/or external authentication are not enabled"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as filter and/or external authentication are not enabled");
skip = true; skip = true;
} }
else if (this.keycloakDeployment == null) else if (this.keycloakDeployment == null)
{ {
LOGGER.debug("Skipping doFilter as Keycloak adapter was not properly initialised"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as Keycloak adapter was not properly initialised");
skip = true; skip = true;
} }
else if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN)) else if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
{ {
LOGGER.debug("Explicitly not skipping doFilter as Keycloak action URL is being called"); LOGGER.debug("Explicitly not skipping processKeycloakAuthenticationAndActions as Keycloak action URL is being called");
} }
else if (req.getParameter("state") != null && req.getParameter("code") != null && this.hasStateCookie(req)) else if (req.getParameter("state") != null && req.getParameter("code") != null && this.hasStateCookie(req))
{ {
LOGGER.debug( LOGGER.debug(
"Explicitly not skipping doFilter as state and code query parameters of OAuth2 redirect as well as state cookie are present"); "Explicitly not skipping processKeycloakAuthenticationAndActions as state and code query parameters of OAuth2 redirect as well as state cookie are present");
} }
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer ")) else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
{ {
LOGGER.debug("Explicitly not skipping doFilter as Bearer authorization header is present"); LOGGER.debug("Explicitly not skipping processKeycloakAuthenticationAndActions as Bearer authorization header is present");
} }
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic ")) else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
{ {
LOGGER.debug("Explicitly not skipping doFilter as Basic authorization header is present"); LOGGER.debug("Explicitly not skipping processKeycloakAuthenticationAndActions as Basic authorization header is present");
} }
else if (authHeader != null) else if (authHeader != null)
{ {
LOGGER.debug("Skipping doFilter as non-OIDC / non-Basic authorization header is present"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as non-OIDC / non-Basic authorization header is present");
skip = true; skip = true;
} }
else if (currentSession != null && AuthenticationUtil.isAuthenticated(req)) else if (currentSession != null && AuthenticationUtil.isAuthenticated(req))
@@ -858,7 +862,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
} }
else else
{ {
LOGGER.debug("Skipping doFilter as non-Keycloak-authenticated session is already established"); // TODO Validate via custom /touch to check if session is still valid
// custom => handle potential 302 instead of 401 response from Keycloak-enabled backend
// custom => deal with redirect host being unknown (similar to our auth-server-url vs. directAuthHost case)
LOGGER.debug(
"Skipping processKeycloakAuthenticationAndActions as non-Keycloak-authenticated session is already established");
skip = true; skip = true;
} }
} }
@@ -868,26 +876,26 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final String noauth = proxyMatcher.group(2); final String noauth = proxyMatcher.group(2);
if (noauth != null && !noauth.trim().isEmpty()) if (noauth != null && !noauth.trim().isEmpty())
{ {
LOGGER.debug("Skipping doFilter as proxy servlet to noauth endpoint {} is being called"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as proxy servlet to noauth endpoint {} is being called");
skip = true; skip = true;
} }
else if (!endpoint.equals(this.primaryEndpoint) else if (!endpoint.equals(this.primaryEndpoint)
&& (this.secondaryEndpoints == null || !this.secondaryEndpoints.contains(endpoint))) && (this.secondaryEndpoints == null || !this.secondaryEndpoints.contains(endpoint)))
{ {
LOGGER.debug( LOGGER.debug(
"Skipping doFilter on proxy servlet call as endpoint {} has not been configured as a primary / secondary endpoint to handle"); "Skipping processKeycloakAuthenticationAndActions on proxy servlet call as endpoint {} has not been configured as a primary / secondary endpoint to handle");
skip = true; skip = true;
} }
} }
else if (PAGE_SERVLET_PATH.equals(servletPath) && (LOGIN_PATH_INFORMATION.equals(pathInfo) else if (PAGE_SERVLET_PATH.equals(servletPath) && (LOGIN_PATH_INFORMATION.equals(pathInfo)
|| (pathInfo == null && LOGIN_PAGE_TYPE_PARAMETER_VALUE.equals(req.getParameter(PAGE_TYPE_PARAMETER_NAME))))) || (pathInfo == null && LOGIN_PAGE_TYPE_PARAMETER_VALUE.equals(req.getParameter(PAGE_TYPE_PARAMETER_NAME)))))
{ {
LOGGER.debug("Skipping doFilter as login page was explicitly requested"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as login page was explicitly requested");
skip = true; skip = true;
} }
else if (this.isNoAuthPage(req)) else if (this.isNoAuthPage(req))
{ {
LOGGER.debug("Skipping doFilter as requested page does not require authentication"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as requested page does not require authentication");
skip = true; skip = true;
} }
@@ -928,7 +936,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
boolean skip = false; boolean skip = false;
if (currentSession != null) if (currentSession != null)
{ {
LOGGER.debug("Skipping doFilter as Keycloak-authentication session is still valid"); LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as Keycloak-authentication session is still valid");
skip = true; skip = true;
if (keycloakAccount instanceof OidcKeycloakAccount) if (keycloakAccount instanceof OidcKeycloakAccount)

View File

@@ -0,0 +1,3 @@
<@markup id="keycloak-js" target="js" action="after">
<@script src="${url.context}/res/acosix/keycloak/components/manage-permissions/manage-permissions.js" group="manage-permissions"/>
</@>

View File

@@ -0,0 +1,45 @@
function main()
{
var requestedAuthorityType, filter, maxResults, url, response, responseObj, authorities, idx;
requestedAuthorityType = args.authorityType ? String(args.authorityType).toLowerCase() : 'all';
filter = args.filter ? String(args.filter) : null;
maxResults = args.maxResults ? parseInt(String(args.maxResults)) : 0;
if (requestedAuthorityType === 'all')
{
url = '/acosix/api/keycloak/roles';
if (maxResults > 0)
{
url += '?maxItems=' + maxResults;
}
if (filter)
{
url += url.indexOf('?') === -1 ? '?' : '&';
url += 'shortNameFilter=' + encodeURIComponent(filter);
}
response = remote.call(url);
if (response.status.code === 200)
{
responseObj = JSON.parse(response.text);
authorities = model.authorities;
for (idx = 0; idx < responseObj.data.length; idx++)
{
// UI likely cannot handle authorityType ROLE, which would be semantically correct
authorities.push({
authorityType : 'GROUP',
shortName : responseObj.data[idx].shortName,
fullName : responseObj.data[idx].fullName,
displayName : responseObj.data[idx].displayName,
description : responseObj.data[idx].fullName,
metadata : {}
});
}
model.authorities = authorities;
}
}
}
main();

View File

@@ -0,0 +1,35 @@
(function()
{
var Dom = YAHOO.util.Dom;
if (Alfresco.component.ManagePermissions)
{
Alfresco.component.ManagePermissions.prototype.fnRenderCellAuthorityIcon = function Acosix_Keycloak_ManagePermissions_fnRenderCellAuthorityIcon()
{
return function Acosix_Keycloak_ManagePermissions_renderCellAuthorityIcon(elCell, oRecord, oColumn)
{
var authority, isGroupLike, iconUrl;
Dom.setStyle(elCell, 'width', oColumn.width + 'px');
Dom.setStyle(elCell.parentNode, 'width', oColumn.width + 'px');
authority = oRecord.getData('authority');
// main modification - treat ROLE just like a group, because any number of users can belong to a role
isGroupLike = /^(GROUP|ROLE)_.*/.test(authority.name);
// end main modification
iconUrl = Alfresco.constants.URL_RESCONTEXT + 'components/images/' + (isGroupLike ? 'group' : 'no-user-photo') + '-64.png';
if (authority.avatar && authority.avatar.length !== 0)
{
iconUrl = Alfresco.constants.PROXY_URI + authority.avatar + '?c=queue&ph=true';
}
else if (authority.iconUrl)
{
// As passed-back from the Authority Finder component
iconUrl = authority.iconUrl;
}
elCell.innerHTML = '<img class="icon32" src="' + iconUrl + '" alt="icon" />';
};
};
}
}());

View File

@@ -21,18 +21,10 @@
<connector> <connector>
<id>alfrescoCookie</id> <id>alfrescoCookie</id>
<name>Alfresco Connector</name> <name>Alfresco Connector</name>
<description>Connects to an Alfresco instance using cookie-based authentication</description> <description>Connects to an Alfresco instance using cookie-based authentication and awareness of OIDC bearer tokens</description>
<class>de.acosix.alfresco.keycloak.share.remote.BearerTokenAwareSlingshotAlfrescoConnector</class> <class>de.acosix.alfresco.keycloak.share.remote.BearerTokenAwareSlingshotAlfrescoConnector</class>
</connector> </connector>
<connector>
<id>alfrescoHeader</id>
<name>Alfresco Connector</name>
<description>Connects to an Alfresco instance using header and cookie-based authentication</description>
<class>de.acosix.alfresco.keycloak.share.remote.BearerTokenAwareSlingshotAlfrescoConnector</class>
<userHeader>SsoUserHeader</userHeader>
</connector>
<endpoint> <endpoint>
<id>alfresco</id> <id>alfresco</id>
<name>Alfresco - user access</name> <name>Alfresco - user access</name>
@@ -47,7 +39,7 @@
<id>alfresco-feed</id> <id>alfresco-feed</id>
<name>Alfresco Feed</name> <name>Alfresco Feed</name>
<description>Alfresco Feed - supports basic HTTP authentication via the EndPointProxyServlet</description> <description>Alfresco Feed - supports basic HTTP authentication via the EndPointProxyServlet</description>
<connector-id>alfrescoHeader</connector-id> <connector-id>alfrescoCookie</connector-id>
<endpoint-url>http://repository:8080/alfresco/wcs</endpoint-url> <endpoint-url>http://repository:8080/alfresco/wcs</endpoint-url>
<basic-auth>true</basic-auth> <basic-auth>true</basic-auth>
<identity>user</identity> <identity>user</identity>
@@ -58,10 +50,8 @@
<id>alfresco-api</id> <id>alfresco-api</id>
<parent-id>alfresco</parent-id> <parent-id>alfresco</parent-id>
<name>Alfresco Public API - user access</name> <name>Alfresco Public API - user access</name>
<description>Access to Alfresco Repository Public API that require user authentication. <description>Access to Alfresco Repository Public API that require user authentication. This makes use of the authentication that is provided by parent 'alfresco' endpoint. </description>
This makes use of the authentication that is provided by parent 'alfresco' endpoint. <connector-id>alfrescoCookie</connector-id>
</description>
<connector-id>alfrescoHeader</connector-id>
<endpoint-url>http://repository:8080/alfresco/api</endpoint-url> <endpoint-url>http://repository:8080/alfresco/api</endpoint-url>
<identity>user</identity> <identity>user</identity>
<external-auth>true</external-auth> <external-auth>true</external-auth>

View File

@@ -70,6 +70,8 @@
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include> <include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo.deps:*</include> <include>${project.groupId}:de.acosix.alfresco.keycloak.repo.deps:*</include>
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:*</include> <include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:*</include>
<!-- full acosix-utility repo module required for extension to repository-tier permissions.post.json.js to take effect -->
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:*</include>
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*</include> <include>${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*</include>
</includes> </includes>
<scope>test</scope> <scope>test</scope>