com.sun.enterprise.admin.util.CommandSecurityChecker Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2012-2017 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://oss.oracle.com/licenses/CDDL+GPL-1.1
* or LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.enterprise.admin.util;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.security.auth.Subject;
import org.glassfish.api.admin.*;
import org.glassfish.api.admin.AccessRequired.AccessCheck;
import org.glassfish.hk2.api.IterableProvider;
import org.glassfish.hk2.api.PostConstruct;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.internal.api.EmbeddedSystemAdministrator;
import org.glassfish.logging.annotation.LogMessagesResourceBundle;
import org.glassfish.logging.annotation.LoggerInfo;
import org.glassfish.security.services.api.authorization.AuthorizationAdminConstants;
import org.glassfish.security.services.api.authorization.AuthorizationService;
import org.glassfish.security.services.api.authorization.AzAction;
import org.glassfish.security.services.api.authorization.AzResource;
import org.glassfish.security.services.api.authorization.AzResult;
import org.glassfish.security.services.api.authorization.AzSubject;
import org.glassfish.security.services.api.common.Attributes;
import org.glassfish.security.services.api.context.SecurityContextService;
import org.jvnet.hk2.annotations.Service;
import org.jvnet.hk2.config.ConfigBean;
import org.jvnet.hk2.config.ConfigBeanProxy;
/**
* Utility class which checks if the Subject is allowed to execute the specified command.
*
* The processing includes {@link AccessRequired}} annotations, CRUD commands,
* {@code RestEndpoint} annotations, and if the command
* class implements {@link AdminCommandSecurity.AccessCheckProvider} it also invokes the
* corresponding {@code getAccessChecks} method. To succeed the overall authorization
* all access checks - whether inferred from annotations or returned from
* {@code getAccessChecks} - for which {@code isFailureFatal} is true must pass.
*
* @author tjquinn
*/
@Service
@Singleton
public class CommandSecurityChecker implements PostConstruct {
@LoggerInfo(subsystem="ADMSECAUTHZ", description="Admin security authorization")
private static final String ADMSEC_AUTHZ_LOGGER_NAME = "javax.enterprise.system.tools.admin.security.authorization";
@LogMessagesResourceBundle
private static final String LOG_MESSAGES_RB = "com.sun.enterprise.admin.util.LogMessages";
static final Logger ADMSEC_AUTHZ_LOGGER = Logger.getLogger(ADMSEC_AUTHZ_LOGGER_NAME, LOG_MESSAGES_RB);
private static final String RESOURCE_NAME_URL_ENCODING = "UTF-8";
private static final Level PROGRESS_LEVEL = Level.FINE;
private static final String LINE_SEP = System.getProperty("line.separator");
private static final String ADMIN_RESOURCE_SCHEME = "admin";
@Inject
private ServiceLocator locator;
@Inject
private AuthorizationService authService;
@Inject
private NamedResourceManager namedResourceMgr;
@Inject
private IterableProvider authPreprocessors;
@Inject
private ServerEnvironment serverEnv;
@Inject
private SecurityContextService securityContextService;
@Inject
private EmbeddedSystemAdministrator embeddedSystemAdministrator;
private static final Map optypeToAction = initOptypeMap();
@Override
public void postConstruct() {
/*
* Indicate whether this is the DAS or an instance in the security
* environment so the provider implementation can see it and use it
* in making authorization decisions.
*/
securityContextService.getEnvironmentAttributes().addAttribute(
AuthorizationAdminConstants.ISDAS_ATTRIBUTE,
Boolean.toString(serverEnv.isDas()), true);
}
/**
* Maps RestEndpoint HTTP methods to the corresponding security action.
* @return
*/
private static EnumMap initOptypeMap() {
final EnumMap result = new EnumMap(RestEndpoint.OpType.class);
result.put(RestEndpoint.OpType.DELETE, "delete");
result.put(RestEndpoint.OpType.GET, "read");
result.put(RestEndpoint.OpType.POST, "update"); // write
result.put(RestEndpoint.OpType.PUT, "create"); // insert
return result;
}
/*
* Group 1 contains the token from $token; group 2 contains the token from ${token}
*/
private final static Pattern TOKEN_PATTERN = Pattern.compile("(?:\\$(\\w+))|(?:\\$\\{(\\w+)\\})");
/**
* Reports whether the Subject is allowed to perform the specified admin command.
* @param subject Subject for the current user to authorize
* @param env environmental settings that might be used in the resource name expression
* @param command the admin command the Subject wants to execute
* @return
*/
public boolean authorize(Subject subject,
final Map env,
final AdminCommand command,
final AdminCommandContext adminCommandContext) throws SecurityException {
if (subject == null) {
ADMSEC_AUTHZ_LOGGER.log(Level.WARNING, command.getClass().getName(),
new IllegalArgumentException("subject"));
subject = new Subject();
}
boolean result;
try {
if (command instanceof AdminCommandSecurity.Preauthorization) {
/*
* Invoke preAuthorization in the context of the Subject.
*/
result = Subject.doAs(subject, new PrivilegedAction () {
@Override
public Boolean run() {
return ((AdminCommandSecurity.Preauthorization) command).preAuthorization(adminCommandContext);
}
});
if ( ! result) {
return false;
}
}
final List accessChecks = assembleAccessCheckWork(command, subject);
result = (embeddedSystemAdministrator.matches(subject)) ||
checkAccessRequired(subject, env, command, accessChecks);
} catch (Exception ex) {
ADMSEC_AUTHZ_LOGGER.log(Level.SEVERE, AdminLoggerInfo.mUnexpectedException, ex);
throw new RuntimeException(ex);
}
/*
* Check the result and throw the SecurityException outside the previous
* try block. Otherwise the earlier catch will dump the stack which we
* do not need for simple authorization errors.
*/
if ( ! result) {
// final List failedAccessChecks = new ArrayList();
// for (AccessCheckWork acWork : accessChecks) {
// if ( ! acWork.accessCheck.isSuccessful()) {
// failedAccessChecks.add(acWork.accessCheck);
// }
// }
throw new SecurityException();
}
return result;
}
private List assembleAccessCheckWork(
final AdminCommand command,
final Subject subject) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
final boolean isTaggable = ADMSEC_AUTHZ_LOGGER.isLoggable(PROGRESS_LEVEL);
final List accessChecks = new ArrayList();
/*
* The CRUD classes such as GenericCreateCommand implement AccessRequired.AccessCheckProvider
* and so provide their own AccessCheck objects. So the addChecksFromAccessCheckProvider
* method will cover the CRUD commands.
*/
final boolean isCommandAccessCheckProvider = addChecksFromAccessCheckProvider(command, accessChecks, isTaggable, subject);
addChecksFromExplicitAccessRequiredAnnos(command, accessChecks, isTaggable);
addChecksFromReSTEndpoints(command, accessChecks, isTaggable);
/*
* If this command has no access requirements specified and does not
* implement AccessCheckProvider, use
* one from the "unguarded" part of the resource tree.
*/
if (accessChecks.isEmpty() && ! isCommandAccessCheckProvider) {
accessChecks.add(new UnguardedCommandAccessCheckWork(command));
}
return accessChecks;
}
private boolean checkAccessRequired(Subject subject,
final Map env,
final AdminCommand command,
final List accessChecks)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, URISyntaxException, UnsupportedEncodingException {
final boolean isTaggable = ADMSEC_AUTHZ_LOGGER.isLoggable(PROGRESS_LEVEL);
boolean result = true;
final StringBuilder sb = (isTaggable ? (new StringBuilder(LINE_SEP)).append("AccessCheck processing on ").append(command.getClass().getName()).append(LINE_SEP) : null);
for (final AccessCheckWork a : accessChecks) {
final URI resourceURI = resourceURIFromAccessCheck(a.accessCheck);
final AzSubject azSubject = authService.makeAzSubject(subject);
final AzResource azResource = authService.makeAzResource(resourceURI);
final AzAction azAction = authService.makeAzAction(a.accessCheck.action());
final Map subjectAttrs = new HashMap();
final Map resourceAttrs = new HashMap();
final Map actionAttrs = new HashMap();
for (AuthorizationPreprocessor ap : authPreprocessors) {
ap.describeAuthorization(
subject,
a.accessCheck.resourceName(),
a.accessCheck.action(),
command,
env,
subjectAttrs,
resourceAttrs,
actionAttrs);
}
mapToAzAttrs(subjectAttrs, azSubject);
mapToAzAttrs(resourceAttrs, azResource);
mapToAzAttrs(actionAttrs, azAction);
final AzResult azResult = authService.getAuthorizationDecision(azSubject, azResource, azAction);
a.accessCheck.setSuccessful(azResult.getDecision() == AzResult.Decision.PERMIT);
if (isTaggable) {
sb.append(a.tag).append(LINE_SEP).
append(" ").append(formattedAccessCheck(resourceURI, a.accessCheck)).append(LINE_SEP);
}
result &= ( (! a.accessCheck.isFailureFinal()) || a.accessCheck.isSuccessful());
}
if (isTaggable) {
sb.append(LINE_SEP).append("...final result: ").append(result).append(LINE_SEP);
ADMSEC_AUTHZ_LOGGER.log(PROGRESS_LEVEL, sb.toString());
}
return result;
}
private String formattedAccessCheck(final URI resourceURI, final AccessCheck a) {
return (new StringBuilder("AccessCheck ")).
append(resourceURI.toASCIIString()).
append("=").
append(a.action()).
append(", isSuccessful=").
append(a.isSuccessful()).
append(", isFailureFatal=").
append(a.isFailureFinal()).
append("//").
append(a.note()).
toString();
}
private void mapToAzAttrs(final Map info, final Attributes attrs) {
for (Map.Entry i : info.entrySet()) {
attrs.addAttribute(i.getKey(), i.getValue(), false /* replace */);
}
}
/**
* Returns all AccessCheck objects which apply to the specified command.
* @param command the AdminCommand for which the AccessChecks are needed
* @param subject the Subject resulting from successful authentication
* @return the AccessChecks resulting from analyzing the command
* @throws NoSuchFieldException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public Collection extends AccessCheck> getAccessChecks(final AdminCommand command,
final Subject subject) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
final List work = assembleAccessCheckWork(command, subject);
final Collection accessChecks = new ArrayList();
for (AccessCheckWork w : work) {
accessChecks.add(w.accessCheck());
}
return accessChecks;
}
/**
* Adds access checks from the command's explicit getAccessChecks method.
* @param subject
* @param command
* @param accessChecks
* @param isTaggable
* @return
*/
private boolean addChecksFromAccessCheckProvider(
final AdminCommand command, final List accessChecks,
final boolean isTaggable,
final Subject subject) {
if (command instanceof AdminCommandSecurity.AccessCheckProvider) {
/*
* Invoke getAccessChecks in the context of the Subject.
*/
final Collection extends AccessCheck> checks =
Subject.doAs(subject, new PrivilegedAction>() {
@Override
public Collection extends AccessCheck> run() {
return ((AdminCommandSecurity.AccessCheckProvider) command).getAccessChecks();
}
});
for (AccessCheck ac : checks) {
accessChecks.add(new AccessCheckWork(ac,
isTaggable ? " Class's getAccessChecks()" : null));
}
return true;
}
return false;
}
private URI resourceURIFromAccessCheck(final AccessCheck c) throws URISyntaxException, UnsupportedEncodingException {
return new URI(ADMIN_RESOURCE_SCHEME,
resourceNameFromAccessCheck(c) /* ssp */,
null /* fragment */);
}
private String resourceNameFromAccessCheck(final AccessCheck c) throws UnsupportedEncodingException {
String resourceName = c.resourceName();
if (resourceName == null) {
resourceName = AccessRequired.Util.resourceNameFromConfigBeanType(c.parent(), null, c.childType());
}
if ( ! resourceName.startsWith("/")) {
resourceName = '/' + resourceName;
}
return resourceName;
}
private boolean addChecksFromExplicitAccessRequiredAnnos(final AdminCommand command,
final List accessChecks,
final boolean isTaggable)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
boolean isAnnotated = false;
for (ClassLineageIterator cIt = new ClassLineageIterator(command.getClass()); cIt.hasNext();) {
final Class> c = cIt.next();
final AccessRequired ar = c.getAnnotation(AccessRequired.class);
if (ar != null) {
isAnnotated = true;
addAccessChecksFromAnno(ar, command, accessChecks, c, isTaggable);
}
final AccessRequired.List arList = c.getAnnotation(AccessRequired.List.class);
if (arList != null) {
isAnnotated = true;
for (final AccessRequired repeatedAR : arList.value()) {
addAccessChecksFromAnno(repeatedAR, command, accessChecks, c, isTaggable);
}
}
isAnnotated |= addAccessChecksFromFields(c, command, accessChecks, isTaggable);
}
return isAnnotated;
}
private boolean addAccessChecksFromFields(
final Class> c,
final AdminCommand command,
final List accessChecks,
final boolean isTaggable) throws IllegalArgumentException, IllegalAccessException {
boolean isAnnotatedOnFields = false;
for (Field f : c.getDeclaredFields()) {
isAnnotatedOnFields |= addAccessChecksFromAnno(f, command, accessChecks, isTaggable);
}
return isAnnotatedOnFields;
}
private void addAccessChecksFromAnno(final AccessRequired ar, final AdminCommand command,
final List accessChecks, final Class> currentClass, final boolean isTaggable)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
for (final String resource : ar.resource()) {
final String translatedResource = processTokens(resource, command);
for (final String action : ar.action()) {
final AccessCheck a = new AccessCheck(translatedResource, action);
String tag = null;
if (isTaggable) {
tag = " @AccessRequired on " + currentClass.getName() + LINE_SEP;
}
accessChecks.add(new AccessCheckWork(a, tag));
}
}
}
private boolean addAccessChecksFromAnno(final Field f, final AdminCommand command,
final List accessChecks, final boolean isTaggable) throws IllegalArgumentException, IllegalAccessException {
boolean isAnnotated = false;
f.setAccessible(true);
final AccessRequired.To arTo = f.getAnnotation(AccessRequired.To.class);
if (arTo != null) {
isAnnotated = true;
final String resourceNameForField = resourceNameFromField(f, command);
for (final String access : arTo.value()) {
final AccessCheck a = new AccessCheck(resourceNameForField, access);
String tag = null;
if (isTaggable) {
tag = " @AccessRequired.To on field " + f.getDeclaringClass().getName() + "#" + f.getName();
}
accessChecks.add(new AccessCheckWork(a, tag));
}
}
final AccessRequired.NewChild arNC = f.getAnnotation(AccessRequired.NewChild.class);
if (arNC != null) {
isAnnotated = true;
String resourceNameForField = resourceNameFromNewChildAnno(arNC, f, command);
/*
* We have the resource name for the parent. Compute the rest of
* the resource name using the explicit collection name in the
* anno or the inferred name from the child type.
*/
for (final String action : arNC.action()) {
final AccessCheck a = new AccessCheck(resourceNameForField, action);
String tag = null;
if (isTaggable) {
tag = " @AccessRequired.NewChild on field " + f.getDeclaringClass().getName() + "#" + f.getName();
}
accessChecks.add(new AccessCheckWork(a, tag));
}
}
return isAnnotated;
}
private String resourceNameFromNewChildAnno(final AccessRequired.NewChild arNC, final Field f, final AdminCommand command) throws IllegalArgumentException, IllegalAccessException {
/*
* The config beans convention - although not a requirement - is that
* an owner bean which has a collection of sub-beans has a single
* child named for the plural of the subtype, and then that single
* child actually holds the collection. For example, the domain
* can contain multiple resources, each of which can be of a different
* subtype of resource. This is modeled with the Domain having
*
* Resources getResources();
*
* (this returns a single Resources object) and
* the Resources object has
*
* List<*> getResources(); // plural
*
* Note that the name for the method on the single container object is not
* consistent. For example, Domain also has
*
* Servers getServers();
*
* and then the Servers bean has
*
* List<*> getServer(); // singular
*
* But in either case, the config path to an actual child is
*
* parent/collectionName/childType/childID
*
* or in these examples,
*
* domain/resources/javamail-resource/MyIMAPMail
* domain/servers/server/MyInstance
*
* The AccessRequired.NewChild annotation requires the developer to provide
* the type of the child to be created and the name of the collection
* in which it is to be stored. (Maybe in the future we can be
* smarter about inferring one from the other.)
*/
final StringBuilder sb = new StringBuilder();
final Object parent = f.get(command);
final Class> childType = arNC.type();
if ( ! ConfigBeanProxy.class.isAssignableFrom(childType)) {
throw new SecurityException(Strings.get("secure.admin.childNotConfigBeanProxy", childType.getName()));
}
if (ConfigBeanProxy.class.isAssignableFrom(parent.getClass())) {
sb.append(AccessRequired.Util.resourceNameFromConfigBeanType(
(ConfigBeanProxy) parent,
arNC.collection(),
(Class extends ConfigBeanProxy>) childType));
} else if (ConfigBean.class.isAssignableFrom(parent.getClass())) {
sb.append(AccessRequired.Util.resourceNameFromConfigBeanType(
(ConfigBean) parent,
arNC.collection(),
(Class extends ConfigBeanProxy>) childType));
}
return sb.toString();
}
private String processTokens(final String expr, final AdminCommand command) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
final Matcher m = TOKEN_PATTERN.matcher(expr);
final StringBuffer translated = new StringBuffer();
while (m.find()) {
final String token = (m.group(1) != null ? m.group(1) : m.group(2));
String replacementValue = token; // in case we can't find or process the field
final Field f = findField(command, token);
if (f != null) {
f.setAccessible(true);
replacementValue = resourceNameFromField(f, command);
}
m.appendReplacement(translated, replacementValue);
}
m.appendTail(translated);
return translated.toString();
}
private Field findField(final AdminCommand command, final String fieldName) throws NoSuchFieldException {
Field result = null;
for (Class c = command.getClass(); c != null; c = c.getSuperclass()) {
try {
result = c.getDeclaredField(fieldName);
return result;
} catch (NoSuchFieldException ex) {
continue;
}
}
return result;
}
private String resourceNameFromField(final Field f, final AdminCommand command)
throws IllegalArgumentException, IllegalAccessException {
f.setAccessible(true);
if (ConfigBeanProxy.class.isAssignableFrom(f.getType())) {
return AccessRequired.Util.resourceNameFromConfigBeanProxy((ConfigBeanProxy) f.get(command));
} else if (ConfigBean.class.isAssignableFrom(f.getType())) {
return AccessRequired.Util.resourceNameFromDom((ConfigBean) f.get(command));
} else {
final String savedResourceName = namedResourceMgr.find(f.get(command));
if (savedResourceName != null) {
return savedResourceName;
}
}
final Object fieldValue = f.get(command);
if (fieldValue == null) {
throw new IllegalArgumentException(command.getClass().getName() + "." + f.getName() + "== null");
}
return fieldValue.toString();
}
private void addChecksFromReSTEndpoints(final AdminCommand command,
final List accessChecks,
final boolean isTaggable) {
for (ClassLineageIterator cIt = new ClassLineageIterator(command.getClass()); cIt.hasNext();) {
final Class> c = cIt.next();
final RestEndpoint restEndpoint;
if ((restEndpoint = c.getAnnotation(RestEndpoint.class)) != null) {
addAccessChecksFromReSTEndpoint(restEndpoint, accessChecks, isTaggable);
}
final RestEndpoints restEndpoints = c.getAnnotation(RestEndpoints.class);
if (restEndpoints != null) {
for (RestEndpoint re : restEndpoints.value()) {
addAccessChecksFromReSTEndpoint(re, accessChecks, isTaggable);
}
}
}
}
private void addAccessChecksFromReSTEndpoint(
final RestEndpoint restEndpoint, final List accessChecks,
final boolean isTaggable) {
if ( ! restEndpoint.useForAuthorization()) {
return;
}
final String action = optypeToAction.get(restEndpoint.opType());
/*
* For the moment, if there is no RestParam then the config bean class given
* in the anno is an unnamed singleton target of the action. If there is a RestParam
* then the target is (probably) the unnamed singleton parent of the config bean class given
* and the new child's type is the config bean class.
*/
String resource;
// if (restEndpoint.params().length == 0) {
resource = resourceNameFromRestEndpoint(restEndpoint.configBean(),
restEndpoint.path(),
locator);
// } else {
// // TODO need to do something with the endpoint params
// resource = resourceNameFromRestEndpoint(restEndpoint.configBean(),
// restEndpoint.path(),
// locator);
// }
final AccessCheck a = new AccessCheck(resource, action);
String tag = null;
if (isTaggable) {
tag = " @RestEndpoint " + restEndpoint.configBean().getName() + ", op=" + restEndpoint.opType();
}
accessChecks.add(new AccessCheckWork(a, tag));
}
private static String resourceNameFromRestEndpoint(Class extends ConfigBeanProxy> c,
final String path,
final ServiceLocator locator) {
ConfigBeanProxy b = locator.getService(c);
String name = (b != null ? AccessRequired.Util.resourceNameFromConfigBeanProxy(b) : "?");
if (path != null) {
name += '/' + path;
}
return name;
}
// private String resourceKeyFromReSTEndpoint(final RestEndpoint restEndpoint) {
// for (RestParam p : restEndpoint.params()) {
//
// }
// return "";
// }
private static class AccessCheckWork {
private final AccessCheck accessCheck;
private final String tag;
private AccessCheckWork(final AccessCheck accessCheck, final String tag) {
this.accessCheck = accessCheck;
this.tag = tag;
}
private AccessCheck accessCheck() {
return accessCheck;
}
}
private static class UnguardedCommandAccessCheckWork extends AccessCheckWork {
private UnguardedCommandAccessCheckWork(final AdminCommand c) {
/*
* Get the name of the command from the @Service annotation.
*/
super(new AccessCheck("unguarded/" + getCommandName(c), "execute")," Unguarded access control on " + c.getClass().getName());
}
}
private static String getCommandName(final AdminCommand c) {
final Service serviceAnno = c.getClass().getAnnotation(Service.class);
if (serviceAnno == null) {
return "no-name";
}
return serviceAnno.name();
}
}