| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.security.authentication.token; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.Principal; |
| import java.security.SecureRandom; |
| import java.util.Arrays; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.UUID; |
| import javax.jcr.AccessDeniedException; |
| import javax.jcr.Credentials; |
| import javax.jcr.RepositoryException; |
| |
| import com.google.common.collect.ImmutableMap; |
| import org.apache.jackrabbit.JcrConstants; |
| import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; |
| import org.apache.jackrabbit.api.security.user.Authorizable; |
| import org.apache.jackrabbit.api.security.user.User; |
| import org.apache.jackrabbit.api.security.user.UserManager; |
| import org.apache.jackrabbit.oak.api.CommitFailedException; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Root; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.namepath.NamePathMapper; |
| import org.apache.jackrabbit.oak.namepath.PathMapper; |
| import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; |
| import org.apache.jackrabbit.oak.plugins.tree.TreeAware; |
| import org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants; |
| import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; |
| import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials; |
| import org.apache.jackrabbit.oak.spi.security.authentication.credentials.CredentialsSupport; |
| import org.apache.jackrabbit.oak.spi.security.authentication.credentials.SimpleCredentialsSupport; |
| import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenConstants; |
| import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; |
| import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; |
| import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; |
| import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil; |
| import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; |
| import org.apache.jackrabbit.util.ISO8601; |
| import org.apache.jackrabbit.util.Text; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static org.apache.jackrabbit.oak.api.Type.DATE; |
| import static org.apache.jackrabbit.oak.api.Type.STRING; |
| import static org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager.getIdentifier; |
| |
| /** |
| * Default implementation of the {@code TokenProvider} interface that keeps login |
| * tokens in the content repository. As a precondition the configured the user |
| * management implementation must provide paths for all |
| * {@link org.apache.jackrabbit.api.security.user.User users} that refer to |
| * a valid {@link Tree} in the content repository. |
| * <p> |
| * <h3>Backwards compatibility with Jackrabbit 2.x</h3> |
| * For security reasons the nodes storing the token information now have a |
| * dedicated node type (rep:Token) which has the following definition: |
| * <pre> |
| * [rep:Token] > mix:referenceable |
| * - rep:token.key (STRING) protected mandatory |
| * - rep:token.exp (DATE) protected mandatory |
| * - * (UNDEFINED) protected |
| * - * (UNDEFINED) multiple protected |
| * </pre> |
| * Consequently the hash of the token and the expiration time of tokens generated |
| * by this provider can no longer be manipulated using regular JCR item |
| * modifications.<p> |
| * <p> |
| * Existing login tokens generated by Jackrabbit 2.x which are migrated to |
| * OAK will still be valid (unless they expire) due to the fact that |
| * {@link #getTokenInfo(String)} and the implementation of the {@link TokenInfo} |
| * interface will not validate the node type of the token node associated with |
| * a given token. |
| */ |
| class TokenProviderImpl implements TokenProvider, TokenConstants { |
| |
| private static final Logger log = LoggerFactory.getLogger(TokenProviderImpl.class); |
| |
| /** |
| * Optional configuration parameter to define the number of token nodes that |
| * when exceeded will trigger a cleanup of expired tokens upon creation. |
| */ |
| static final String PARAM_TOKEN_CLEANUP_THRESHOLD = "tokenCleanupThreshold"; |
| |
| /** |
| * Default value indicating that tokens should never be cleaned up (i.e. |
| * backwards compatible behavior). |
| */ |
| static final long NO_TOKEN_CLEANUP = 0; |
| |
| /** |
| * Default expiration time in ms for login tokens is 2 hours. |
| */ |
| static final long DEFAULT_TOKEN_EXPIRATION = 2 * 3600 * 1000; |
| static final int DEFAULT_KEY_SIZE = 8; |
| |
| private static final char DELIM = '_'; |
| |
| private final Root root; |
| private final ConfigurationParameters options; |
| private final CredentialsSupport credentialsSupport; |
| |
| private final long tokenExpiration; |
| private final UserManager userManager; |
| private final IdentifierManager identifierManager; |
| private final long cleanupThreshold; |
| |
| TokenProviderImpl(@NotNull Root root, @NotNull ConfigurationParameters options, @NotNull UserConfiguration userConfiguration) { |
| this(root, options, userConfiguration, SimpleCredentialsSupport.getInstance()); |
| } |
| |
| TokenProviderImpl(@NotNull Root root, @NotNull ConfigurationParameters options, @NotNull UserConfiguration userConfiguration, @NotNull CredentialsSupport credentialsSupport) { |
| this.root = root; |
| this.options = options; |
| this.credentialsSupport = credentialsSupport; |
| |
| this.tokenExpiration = options.getConfigValue(PARAM_TOKEN_EXPIRATION, DEFAULT_TOKEN_EXPIRATION); |
| this.userManager = userConfiguration.getUserManager(root, NamePathMapper.DEFAULT); |
| this.identifierManager = new IdentifierManager(root); |
| this.cleanupThreshold = options.getConfigValue(PARAM_TOKEN_CLEANUP_THRESHOLD, NO_TOKEN_CLEANUP); |
| } |
| |
| //------------------------------------------------------< TokenProvider >--- |
| |
| /** |
| * Returns {@code true} if {@code SimpleCredentials} can be extracted from |
| * the specified credentials object and that simple credentials object has |
| * a {@link #TOKEN_ATTRIBUTE} attribute with an empty value. |
| * |
| * @param credentials The current credentials. |
| * @return {@code true} if the specified credentials or those extracted from |
| * {@link ImpersonationCredentials} are supported and and if the (extracted) |
| * credentials object contain a {@link #TOKEN_ATTRIBUTE} attribute with an |
| * empty value; {@code false} otherwise. |
| */ |
| @Override |
| public boolean doCreateToken(@NotNull Credentials credentials) { |
| Credentials creds = extractCredentials(credentials); |
| if (creds == null) { |
| return false; |
| } else { |
| Object attr = credentialsSupport.getAttributes(creds).get(TOKEN_ATTRIBUTE); |
| return (attr != null && attr.toString().isEmpty()); |
| } |
| } |
| |
| /** |
| * Create a separate token node underneath a dedicated token store within |
| * the user home node. That token node contains the hashed token, the |
| * expiration time and additional mandatory attributes that will be verified |
| * during login. |
| * |
| * @param credentials The current credentials. |
| * @return A new {@code TokenInfo} or {@code null} if the token could not |
| * be created. |
| */ |
| @Nullable |
| @Override |
| public TokenInfo createToken(@NotNull Credentials credentials) { |
| Credentials creds = extractCredentials(credentials); |
| String uid = (creds != null) ? credentialsSupport.getUserId(creds) : null; |
| |
| TokenInfo tokenInfo = null; |
| if (uid != null) { |
| Map<String, ?> attributes = credentialsSupport.getAttributes(creds); |
| tokenInfo = createToken(uid, attributes); |
| if (tokenInfo != null) { |
| // also set the new token to the credentials. |
| if (!credentialsSupport.setAttributes(creds, ImmutableMap.of(TOKEN_ATTRIBUTE, tokenInfo.getToken()))) { |
| log.debug("Cannot set token attribute to {}", creds); |
| } |
| } |
| } |
| |
| return tokenInfo; |
| } |
| |
| /** |
| * Create a separate token node underneath a dedicated token store within |
| * the user home node. That token node contains the hashed token, the |
| * expiration time and additional mandatory attributes that will be verified |
| * during login. |
| * |
| * @param userId The identifier of the user for which a new token should |
| * be created. |
| * @param attributes The attributes associated with the new token. |
| * @return A new {@code TokenInfo} or {@code null} if the token could not |
| * be created. |
| */ |
| @Nullable |
| @Override |
| public TokenInfo createToken(@NotNull String userId, @NotNull Map<String, ?> attributes) { |
| String error = "Failed to create login token. {}"; |
| User user = getUser(userId); |
| Tree tokenParent = (user == null) ? null : getTokenParent(user); |
| if (tokenParent != null) { |
| try { |
| String id = user.getID(); |
| long creationTime = System.currentTimeMillis(); |
| long exp; |
| if (attributes.containsKey(PARAM_TOKEN_EXPIRATION)) { |
| exp = Long.parseLong(attributes.get(PARAM_TOKEN_EXPIRATION).toString()); |
| } else { |
| exp = tokenExpiration; |
| } |
| long expTime = createExpirationTime(creationTime, exp); |
| String uuid = UUID.randomUUID().toString(); |
| |
| TokenInfo tokenInfo; |
| try { |
| String tokenName = uuid; |
| tokenInfo = createTokenNode(tokenParent, tokenName, expTime, uuid, id, attributes); |
| root.commit(CommitMarker.asCommitAttributes()); |
| } catch (CommitFailedException e) { |
| // conflict while creating token node -> retry |
| log.debug("Failed to create token node. Using random name as fallback."); |
| root.refresh(); |
| tokenInfo = createTokenNode(tokenParent, UUID.randomUUID().toString(), expTime, uuid, id, attributes); |
| root.commit(CommitMarker.asCommitAttributes()); |
| } |
| cleanupExpired(userId, tokenParent, creationTime, tokenInfo.getToken()); |
| return tokenInfo; |
| } catch (NoSuchAlgorithmException | UnsupportedEncodingException | CommitFailedException | RepositoryException e) { |
| // error while generating login token or while committing changes |
| log.error(error, e.getMessage()); |
| } |
| } else { |
| log.error("Unable to get/create token store for user {}.", userId); |
| } |
| return null; |
| } |
| |
| /** |
| * Retrieves the token information associated with the specified login |
| * token. If no accessible {@code Tree} exists for the given token or if |
| * the token is not associated with a valid user this method returns {@code null}. |
| * |
| * @param token A valid login token. |
| * @return The {@code TokenInfo} associated with the specified token or |
| * {@code null} of the corresponding information does not exist or is not |
| * associated with a valid user. |
| */ |
| @Nullable |
| @Override |
| public TokenInfo getTokenInfo(@NotNull String token) { |
| int pos = token.indexOf(DELIM); |
| String nodeId = (pos == -1) ? token : token.substring(0, pos); |
| Tree tokenTree = identifierManager.getTree(nodeId); |
| if (isValidTokenTree(tokenTree)) { |
| try { |
| User user = getUser(tokenTree); |
| if (user != null) { |
| return new TokenInfoImpl(tokenTree, token, user.getID(), user.getPrincipal()); |
| } |
| } catch (RepositoryException e) { |
| log.debug("Cannot determine userID/principal from token: {}", e.getMessage()); |
| } |
| } |
| // invalid token tree or failed to extract user or it's id/principal |
| return null; |
| } |
| |
| //-------------------------------------------------------------------------- |
| private static long createExpirationTime(long creationTime, long tokenExpiration) { |
| return creationTime + tokenExpiration; |
| } |
| |
| private static long getExpirationTime(@NotNull Tree tokenTree, long defaultValue) { |
| return TreeUtil.getLong(tokenTree, TOKEN_ATTRIBUTE_EXPIRY, defaultValue); |
| } |
| |
| private static boolean isExpired(long expirationTime, long loginTime) { |
| return expirationTime < loginTime; |
| } |
| |
| private static void setExpirationTime(@NotNull Tree tree, long time) { |
| Calendar calendar = Calendar.getInstance(); |
| calendar.setTimeInMillis(time); |
| tree.setProperty(TOKEN_ATTRIBUTE_EXPIRY, ISO8601.format(calendar), DATE); |
| } |
| |
| @Nullable |
| private Credentials extractCredentials(@NotNull Credentials credentials) { |
| Credentials creds = credentials; |
| if (credentials instanceof ImpersonationCredentials) { |
| creds = ((ImpersonationCredentials) credentials).getBaseCredentials(); |
| } |
| |
| if (credentialsSupport.getCredentialClasses().contains(creds.getClass())) { |
| return creds; |
| } else { |
| return null; |
| } |
| } |
| |
| @NotNull |
| private static String generateKey(int size) { |
| SecureRandom random = new SecureRandom(); |
| byte key[] = new byte[size]; |
| random.nextBytes(key); |
| |
| StringBuilder res = new StringBuilder(key.length * 2); |
| for (byte b : key) { |
| res.append(Text.hexTable[(b >> 4) & 15]); |
| res.append(Text.hexTable[b & 15]); |
| } |
| return res.toString(); |
| } |
| |
| @NotNull |
| private static String getKeyValue(@NotNull String key, @NotNull String userId) { |
| return key + userId; |
| } |
| |
| private static boolean isValidTokenTree(@Nullable Tree tokenTree) { |
| if (tokenTree == null || !tokenTree.exists()) { |
| return false; |
| } else { |
| return TOKENS_NODE_NAME.equals(tokenTree.getParent().getName()) && |
| TOKEN_NT_NAME.equals(TreeUtil.getPrimaryTypeName(tokenTree)); |
| } |
| } |
| |
| @NotNull |
| private Tree getTokenTree(@NotNull TokenInfoImpl tokenInfo) { |
| return root.getTree(tokenInfo.tokenPath); |
| } |
| |
| @Nullable |
| private User getUser(@NotNull Tree tokenTree) throws RepositoryException { |
| String userPath = Text.getRelativeParent(tokenTree.getPath(), 2); |
| Authorizable authorizable = userManager.getAuthorizableByPath(userPath); |
| if (authorizable != null && !authorizable.isGroup() && !((User) authorizable).isDisabled()) { |
| return (User) authorizable; |
| } else { |
| return null; |
| } |
| } |
| |
| @Nullable |
| private User getUser(@NotNull String userId) { |
| try { |
| Authorizable user = userManager.getAuthorizable(userId); |
| if (user != null && !user.isGroup()) { |
| return (User) user; |
| } else { |
| log.debug("Cannot create login token: No corresponding node for User {}.", userId); |
| } |
| } catch (RepositoryException e) { |
| // error while accessing user. |
| log.debug("Error while accessing user " + userId + '.', e); |
| } |
| return null; |
| } |
| |
| @Nullable |
| private Tree getTokenParent(@NotNull User user) { |
| Tree tokenParent = null; |
| String parentPath = null; |
| try { |
| String userPath = user.getPath(); |
| parentPath = userPath + '/' + TOKENS_NODE_NAME; |
| |
| Tree userNode = getTree(user, userPath); |
| tokenParent = TreeUtil.getOrAddChild(userNode, TOKENS_NODE_NAME, TOKENS_NT_NAME); |
| |
| root.commit(); |
| } catch (RepositoryException e) { |
| // error while creating token node. |
| log.debug("Error while creating token node {}", e.getMessage()); |
| } catch (CommitFailedException e) { |
| // conflict while creating token store for this user -> refresh and |
| // try to get the tree from the updated root. |
| log.debug("Conflict while creating token store -> retrying {}", e.getMessage()); |
| root.refresh(); |
| Tree parentTree = root.getTree(parentPath); |
| if (parentTree.exists()) { |
| tokenParent = parentTree; |
| } else { |
| tokenParent = null; |
| } |
| } |
| return tokenParent; |
| } |
| |
| public @NotNull Tree getTree(@NotNull User user, @NotNull String userPath) { |
| if (user instanceof TreeAware) { |
| return ((TreeAware) user).getTree(); |
| } else { |
| return root.getTree(userPath); |
| } |
| } |
| |
| /** |
| * Create a new token node below the specified {@code parent}. |
| * |
| * @param parent The parent node. |
| * @param expTime The expiration time of the new token. |
| * @param uuid The uuid of the token node. |
| * @param id The id of the user that issues the token. |
| * @param attributes The additional attributes of the token to be created. |
| * @return The new token info |
| * @throws AccessDeniedException If the editing session cannot access the |
| * new token node. |
| * |
| */ |
| @NotNull |
| private TokenInfo createTokenNode(@NotNull Tree parent, @NotNull String tokenName, |
| long expTime, @NotNull String uuid, |
| @NotNull String id, Map<String, ?> attributes) |
| throws AccessDeniedException, UnsupportedEncodingException, NoSuchAlgorithmException { |
| |
| Tree tokenNode = TreeUtil.addChild(parent, tokenName, TOKEN_NT_NAME); |
| tokenNode.setProperty(JcrConstants.JCR_UUID, uuid); |
| |
| String key = generateKey(options.getConfigValue(PARAM_TOKEN_LENGTH, DEFAULT_KEY_SIZE)); |
| String nodeId = getIdentifier(tokenNode); |
| String token = nodeId + DELIM + key; |
| |
| String keyHash = PasswordUtil.buildPasswordHash(getKeyValue(key, id), options); |
| tokenNode.setProperty(TOKEN_ATTRIBUTE_KEY, keyHash); |
| setExpirationTime(tokenNode, expTime); |
| |
| attributes.forEach((name, value) -> { |
| if (!RESERVED_ATTRIBUTES.contains(name)) { |
| tokenNode.setProperty(name, value.toString()); |
| } |
| }); |
| return new TokenInfoImpl(tokenNode, token, id, null); |
| } |
| |
| /** |
| * Remove expired token nodes if the configured threshold (i.e. number of |
| * token nodes) is matched/exceeded. By default (i.e. unless configured with |
| * a value bigger than {@link #NO_TOKEN_CLEANUP}) no cleanup is performed |
| * and this method returns without looking at the token nodes; this makes |
| * this addition optional and will not affect existing configurations. |
| * |
| * @param parent |
| * The token parent node. |
| * @param currentTime |
| * The time to be used for analysing expiration of existing |
| * tokens. |
| * @param token |
| * The token info used as random data to skip cleanup. |
| */ |
| private void cleanupExpired(@NotNull String userId, @NotNull Tree parent, long currentTime, @NotNull String token) { |
| if (cleanupThreshold > NO_TOKEN_CLEANUP && shouldRunCleanup(token)) { |
| long start = System.currentTimeMillis(); |
| long active = 0; |
| long expired = 0; |
| try { |
| if (parent.getChildrenCount(cleanupThreshold) >= cleanupThreshold) { |
| for (Tree child : parent.getChildren()) { |
| if (isExpired(getExpirationTime(child, Long.MIN_VALUE), currentTime)) { |
| expired++; |
| child.remove(); |
| } else { |
| active++; |
| } |
| } |
| } |
| if (root.hasPendingChanges()) { |
| root.commit(CommitMarker.asCommitAttributes()); |
| } |
| } catch (CommitFailedException e) { |
| log.debug("Failed to cleanup expired token nodes", e); |
| root.refresh(); |
| } finally { |
| if (active + expired > 0) { |
| log.debug("Token cleanup completed in {} ms: removed {}/{} tokens for {}.", System.currentTimeMillis() - start, expired, active + expired, userId); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Method that determines if the cleanup should run or not based on the |
| * randomly generated token's first char, this decreases the chances to 1/8. |
| * |
| * @param token The target token |
| * @return true if the cleanup should run |
| */ |
| static boolean shouldRunCleanup(@NotNull String token) { |
| return token.charAt(0) < '2'; |
| } |
| |
| // -------------------------------------------------------------------------- |
| |
| /** |
| * TokenInfo |
| */ |
| final class TokenInfoImpl implements TokenInfo { |
| |
| private final String token; |
| private final String tokenPath; |
| private final String userId; |
| private final Principal principal; |
| |
| private final long expirationTime; |
| private final String key; |
| |
| private final Map<String, String> mandatoryAttributes; |
| private final Map<String, String> publicAttributes; |
| |
| private TokenInfoImpl(@NotNull Tree tokenTree, @NotNull String token, @NotNull String userId, @Nullable Principal principal) { |
| this.token = token; |
| this.tokenPath = tokenTree.getPath(); |
| this.userId = userId; |
| this.principal = principal; |
| |
| expirationTime = getExpirationTime(tokenTree, Long.MIN_VALUE); |
| key = TreeUtil.getString(tokenTree, TOKEN_ATTRIBUTE_KEY); |
| |
| mandatoryAttributes = new HashMap<>(); |
| publicAttributes = new HashMap<>(); |
| for (PropertyState propertyState : tokenTree.getProperties()) { |
| String name = propertyState.getName(); |
| String value = propertyState.getValue(STRING); |
| if (RESERVED_ATTRIBUTES.contains(name)) { |
| continue; |
| } |
| if (isMandatoryAttribute(name)) { |
| mandatoryAttributes.put(name, value); |
| } else if (isInfoAttribute(name)) { |
| // info attribute |
| publicAttributes.put(name, value); |
| } // else: jcr specific property |
| } |
| } |
| |
| @Nullable |
| Principal getPrincipal() { |
| return principal; |
| } |
| |
| //------------------------------------------------------< TokenInfo >--- |
| |
| @NotNull |
| @Override |
| public String getUserId() { |
| return userId; |
| } |
| |
| @NotNull |
| @Override |
| public String getToken() { |
| return token; |
| } |
| |
| @Override |
| public boolean isExpired(long loginTime) { |
| return TokenProviderImpl.isExpired(expirationTime, loginTime); |
| } |
| |
| @Override |
| public boolean resetExpiration(long loginTime) { |
| // for backwards compatibility use true as default value for the 'tokenRefresh' configuration |
| if (options.getConfigValue(PARAM_TOKEN_REFRESH, true)) { |
| Tree tokenTree = getTokenTree(this); |
| if (tokenTree.exists()) { |
| if (isExpired(loginTime)) { |
| log.debug("Attempt to reset an expired token."); |
| return false; |
| } |
| |
| if (expirationTime - loginTime <= tokenExpiration / 2) { |
| try { |
| long expTime = createExpirationTime(loginTime, tokenExpiration); |
| setExpirationTime(tokenTree, expTime); |
| root.commit(CommitMarker.asCommitAttributes()); |
| log.debug("Successfully reset token expiration time."); |
| return true; |
| } catch (CommitFailedException e) { |
| log.debug("Failed to reset token expiration {}", e.getMessage()); |
| root.refresh(); |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean remove() { |
| Tree tokenTree = getTokenTree(this); |
| if (tokenTree.exists()) { |
| try { |
| if (tokenTree.remove()) { |
| root.commit(CommitMarker.asCommitAttributes()); |
| return true; |
| } |
| } catch (CommitFailedException e) { |
| log.debug("Error while removing expired token {}", e.getMessage()); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean matches(@NotNull TokenCredentials tokenCredentials) { |
| String tk = tokenCredentials.getToken(); |
| int pos = tk.lastIndexOf(DELIM); |
| if (pos > -1) { |
| tk = tk.substring(pos + 1); |
| } |
| if (!PasswordUtil.isSame(key, getKeyValue(tk, userId))) { |
| return false; |
| } |
| |
| for (Map.Entry<String,String> mandatory : mandatoryAttributes.entrySet()) { |
| String name = mandatory.getKey(); |
| String expectedValue = mandatory.getValue(); |
| if (!expectedValue.equals(tokenCredentials.getAttribute(name))) { |
| return false; |
| } |
| } |
| |
| // update set of informative attributes on the credentials |
| // based on the properties present on the token node. |
| Collection<String> attrNames = Arrays.asList(tokenCredentials.getAttributeNames()); |
| publicAttributes.forEach((name, value) -> { |
| if (!attrNames.contains(name)) { |
| tokenCredentials.setAttribute(name, value); |
| |
| } |
| }); |
| return true; |
| } |
| |
| @NotNull |
| @Override |
| public Map<String, String> getPrivateAttributes() { |
| return Collections.unmodifiableMap(mandatoryAttributes); |
| } |
| |
| @NotNull |
| @Override |
| public Map<String, String> getPublicAttributes() { |
| return Collections.unmodifiableMap(publicAttributes); |
| } |
| |
| /** |
| * Returns {@code true} if the specified {@code attributeName} |
| * starts with or equals {@link #TOKEN_ATTRIBUTE}. |
| * |
| * @param attributeName The attribute name. |
| * @return {@code true} if the specified {@code attributeName} |
| * starts with or equals {@link #TOKEN_ATTRIBUTE}. |
| */ |
| private boolean isMandatoryAttribute(@NotNull String attributeName) { |
| return attributeName.startsWith(TOKEN_ATTRIBUTE); |
| } |
| |
| /** |
| * Returns {@code false} if the specified attribute name doesn't have |
| * a 'jcr' or 'rep' namespace prefix; {@code true} otherwise. This is |
| * a lazy evaluation in order to avoid testing the defining node type of |
| * the associated jcr property. |
| * |
| * @param attributeName The attribute name. |
| * @return {@code true} if the specified property name doesn't seem |
| * to represent repository internal information. |
| */ |
| private boolean isInfoAttribute(@NotNull String attributeName) { |
| String prefix = Text.getNamespacePrefix(attributeName); |
| return !NamespaceConstants.RESERVED_PREFIXES.contains(prefix); |
| } |
| } |
| } |