/*
* 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.brooklyn.core.mgmt.entitlement;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementClass;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementContext;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementManager;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.config.Sanitizer;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.mgmt.persist.DeserializingClassRenamesProvider;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.TypeToken;
/** @since 0.7.0 */
@Beta
public class Entitlements {
private static final Logger log = LoggerFactory.getLogger(Entitlements.class);
// ------------------- individual permissions
public static EntitlementClass<String> SEE_CATALOG_ITEM = new BasicEntitlementClassDefinition<String>("catalog.see", String.class);
public static EntitlementClass<Object> ADD_CATALOG_ITEM = new BasicEntitlementClassDefinition<Object>("catalog.add", Object.class);
public static EntitlementClass<StringAndArgument> MODIFY_CATALOG_ITEM = new BasicEntitlementClassDefinition<StringAndArgument>("catalog.modify", StringAndArgument.class);
public static EntitlementClass<Entity> SEE_ENTITY = new BasicEntitlementClassDefinition<Entity>("entity.see", Entity.class);
public static EntitlementClass<EntityAndItem<String>> SEE_SENSOR = new BasicEntitlementClassDefinition<EntityAndItem<String>>("sensor.see", EntityAndItem. typeToken(String.class));
// string is effector name; argument may be a map or a list, depending how the args were supplied
public static EntitlementClass<EntityAndItem<StringAndArgument>> INVOKE_EFFECTOR = new BasicEntitlementClassDefinition<EntityAndItem<StringAndArgument>>("effector.invoke", EntityAndItem.typeToken(StringAndArgument.class));
public static EntitlementClass<Entity> MODIFY_ENTITY = new BasicEntitlementClassDefinition<Entity>("entity.modify", Entity.class);
/** the permission to deploy an application, where parameter is some representation of the app to be deployed (spec instance or yaml plan) */
public static EntitlementClass<Object> DEPLOY_APPLICATION = new BasicEntitlementClassDefinition<Object>("app.deploy", Object.class);
/** catch-all for catalog, locations, scripting, usage, etc - exporting persistence, shutting down, etc;
* this is significantly more powerful than {@link #SERVER_STATUS}.
* NB: this may be refactored and deprecated in future */
public static EntitlementClass<Void> SEE_ALL_SERVER_INFO = new BasicEntitlementClassDefinition<Void>("server.info.all.see", Void.class);
/** permission to see general server status info: basically HA status; not nearly as much as {@link #SEE_ALL_SERVER_INFO} */
public static EntitlementClass<Void> SERVER_STATUS = new BasicEntitlementClassDefinition<Void>("server.status", Void.class);
/** permission to run untrusted code or embedded scripts at the server;
* secondary check required for any operation which could potentially grant root-level access */
public static EntitlementClass<Void> ROOT = new BasicEntitlementClassDefinition<Void>("root", Void.class);
@SuppressWarnings("unchecked")
public static enum EntitlementClassesEnum {
ENTITLEMENT_SEE_CATALOG_ITEM(SEE_CATALOG_ITEM) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeCatalogItem((String)argument); } },
ENTITLEMENT_ADD_CATALOG_ITEM(ADD_CATALOG_ITEM) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleAddCatalogItem(argument); } },
ENTITLEMENT_MODIFY_CATALOG_ITEM(MODIFY_CATALOG_ITEM) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleModifyCatalogItem((StringAndArgument)argument); } },
ENTITLEMENT_SEE_ENTITY(SEE_ENTITY) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeEntity((Entity)argument); } },
ENTITLEMENT_SEE_SENSOR(SEE_SENSOR) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeSensor((EntityAndItem<String>)argument); } },
ENTITLEMENT_INVOKE_EFFECTOR(INVOKE_EFFECTOR) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleInvokeEffector((EntityAndItem<StringAndArgument>)argument); } },
ENTITLEMENT_MODIFY_ENTITY(MODIFY_ENTITY) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleModifyEntity((Entity)argument); } },
ENTITLEMENT_DEPLOY_APPLICATION(DEPLOY_APPLICATION) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleDeployApplication(argument); } },
ENTITLEMENT_SEE_ALL_SERVER_INFO(SEE_ALL_SERVER_INFO) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeAllServerInfo(); } },
ENTITLEMENT_SERVER_STATUS(SERVER_STATUS) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeServerStatus(); } },
ENTITLEMENT_ROOT(ROOT) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleRoot(); } },
;
private EntitlementClass<?> entitlementClass;
private EntitlementClassesEnum(EntitlementClass<?> specificClass) {
this.entitlementClass = specificClass;
}
public EntitlementClass<?> getEntitlementClass() {
return entitlementClass;
}
public abstract <T> T handle(EntitlementClassesHandler<T> handler, Object argument);
public static EntitlementClassesEnum of(EntitlementClass<?> entitlementClass) {
for (EntitlementClassesEnum x: values()) {
if (entitlementClass.equals(x.getEntitlementClass())) return x;
}
return null;
}
}
public interface EntitlementClassesHandler<T> {
public T handleSeeCatalogItem(String catalogItemId);
public T handleSeeServerStatus();
public T handleAddCatalogItem(Object catalogItemBeingAdded);
public T handleModifyCatalogItem(StringAndArgument catalogItemIdAndModification);
public T handleSeeEntity(Entity entity);
public T handleSeeSensor(EntityAndItem<String> sensorInfo);
public T handleInvokeEffector(EntityAndItem<StringAndArgument> effectorInfo);
public T handleModifyEntity(Entity entity);
public T handleDeployApplication(Object app);
public T handleSeeAllServerInfo();
public T handleRoot();
}
protected static class Pair<T1,T2> {
protected final T1 p1;
protected final T2 p2;
protected Pair(T1 p1, T2 p2) { this.p1 = p1; this.p2 = p2; }
}
public static class EntityAndItem<T> extends Pair<Entity,T> {
public static <TT> TypeToken<EntityAndItem<TT>> typeToken(Class<TT> type) {
return new TypeToken<Entitlements.EntityAndItem<TT>>() {
private static final long serialVersionUID = -738154831809025407L;
};
}
public EntityAndItem(Entity entity, T item) { super (entity, item); }
public Entity getEntity() { return p1; }
public T getItem() { return p2; }
public static <T> EntityAndItem<T> of(Entity entity, T item) {
return new EntityAndItem<T>(entity, item);
}
}
public static class StringAndArgument extends Pair<String,Object> {
public StringAndArgument(String string, Object argument) { super(string, argument); }
public String getString() { return p1; }
public Object getArgument() { return p2; }
public static StringAndArgument of(String string, Object argument) {
return new StringAndArgument(string, argument);
}
}
/**
* These lifecycle operations are currently treated as effectors. This may change in the future.
* @since 0.7.0 */
@Beta
public static class LifecycleEffectors {
public static final String DELETE = "delete";
}
// ------------- permission sets -------------
/** always ALLOW access to everything */
public static EntitlementManager root() {
return new EntitlementManager() {
@Override
public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
return true;
}
@Override
public String toString() {
return "Entitlements.root";
}
};
}
/** always DENY access to anything which requires entitlements */
public static EntitlementManager minimal() {
return new EntitlementManager() {
@Override
public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
return false;
}
@Override
public String toString() {
return "Entitlements.minimal";
}
};
}
public static class FineGrainedEntitlements {
private static final Joiner COMMA_JOINER = Joiner.on(',');
public static EntitlementManager anyOf(final EntitlementManager... checkers) {
return anyOf(Arrays.asList(checkers));
}
public static EntitlementManager anyOf(final Iterable<? extends EntitlementManager> checkers) {
return new EntitlementManager() {
@Override
public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
for (EntitlementManager checker: checkers)
if (checker.isEntitled(context, permission, typeArgument))
return true;
return false;
}
@Override
public String toString() {
return "Entitlements.anyOf(" + COMMA_JOINER.join(checkers) + ")";
}
};
}
public static EntitlementManager allOf(final EntitlementManager... checkers) {
return allOf(Arrays.asList(checkers));
}
public static EntitlementManager allOf(final Iterable<? extends EntitlementManager> checkers) {
return new EntitlementManager() {
@Override
public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
for (EntitlementManager checker: checkers)
if (checker.isEntitled(context, permission, typeArgument))
return true;
return false;
}
@Override
public String toString() {
return "Entitlements.allOf(" + COMMA_JOINER.join(checkers) + ")";
}
};
}
public static <U> EntitlementManager allowing(EntitlementClass<U> permission, Predicate<U> test) {
return new SinglePermissionEntitlementChecker<U>(permission, test);
}
public static <U> EntitlementManager allowing(EntitlementClass<U> permission) {
return new SinglePermissionEntitlementChecker<U>(permission, Predicates.<U>alwaysTrue());
}
public static class SinglePermissionEntitlementChecker<U> implements EntitlementManager {
final EntitlementClass<U> permission;
final Predicate<U> test;
protected SinglePermissionEntitlementChecker(EntitlementClass<U> permission, Predicate<U> test) {
this.permission = permission;
this.test = test;
}
@SuppressWarnings("unchecked")
@Override
public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
if (!Objects.equal(this.permission, permission)) return false;
return test.apply((U)typeArgument);
}
@Override
public String toString() {
return "Entitlements.allowing(" + permission + " -> " + test + ")";
}
}
public static EntitlementManager seeNonSecretSensors() {
return allowing(SEE_SENSOR, new Predicate<EntityAndItem<String>>() {
@Override
public boolean apply(EntityAndItem<String> input) {
if (input == null) return false;
return !Sanitizer.IS_SECRET_PREDICATE.apply(input.getItem());
}
@Override
public String toString() {
return "Predicates.nonSecret";
}
});
}
}
/** allow read-only */
public static EntitlementManager readOnly() {
return FineGrainedEntitlements.anyOf(
FineGrainedEntitlements.allowing(SEE_ENTITY),
FineGrainedEntitlements.seeNonSecretSensors()
);
}
/** allow healthcheck */
public static EntitlementManager serverStatusOnly() {
return FineGrainedEntitlements.allowing(SERVER_STATUS);
}
// ------------- lookup conveniences -------------
private static class PerThreadEntitlementContextHolder {
public static final ThreadLocal<EntitlementContext> perThreadEntitlementsContextHolder = new ThreadLocal<EntitlementContext>();
}
/**
* Finds the currently applicable {@link EntitlementContext} by examining the current thread
* then by investigating the current task, its submitter, etc. */
// NOTE: entitlements are propagated to tasks whenever they are created, as tags
// (see BrooklynTaskTags.tagForEntitlement and BasicExecutionContext.submitInternal).
// It might be cheaper to only do this lookup, not to propagate as tags, and to ensure
// all entitlement operations are wrapped in a task at source; but currently we do not
// do that so we need at least to set entitlement on the outermost task.
// Setting it on tasks submitted by a task is not strictly necessary (i.e. in BasicExecutionContext)
// but seems cheap enough, and means checking entitlements is fast, if we choose to do that more often.
public static EntitlementContext getEntitlementContext() {
EntitlementContext context;
context = PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.get();
if (context!=null) return context;
Task<?> task = Tasks.current();
while (task!=null) {
context = BrooklynTaskTags.getEntitlement(task);
if (context!=null) return context;
task = task.getSubmittedByTask();
}
// no entitlements set -- assume entitlements not used, or system internal
return null;
}
public static void setEntitlementContext(EntitlementContext context) {
EntitlementContext oldContext = PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.get();
if (oldContext!=null && context!=null) {
log.warn("Changing entitlement context from "+oldContext+" to "+context+"; context should have been reset or extended, not replaced");
log.debug("Trace for entitlement context duplicate overwrite", new Throwable("Trace for entitlement context overwrite"));
}
PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.set(context);
}
public static void clearEntitlementContext() {
PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.set(null);
}
public static <T> boolean isEntitled(EntitlementManager checker, EntitlementClass<T> permission, T typeArgument) {
return checker.isEntitled(getEntitlementContext(), permission, typeArgument);
}
/** throws {@link NotEntitledException} if entitlement not available for current {@link #getEntitlementContext()} */
public static <T> void checkEntitled(EntitlementManager checker, EntitlementClass<T> permission, T typeArgument) {
if (!isEntitled(checker, permission, typeArgument)) {
throw new NotEntitledException(getEntitlementContext(), permission, typeArgument);
}
}
/** throws {@link NotEntitledException} if entitlement not available for current {@link #getEntitlementContext()}
* @since 0.7.0
* @deprecated since 0.7.0, use {@link #checkEntitled(EntitlementManager, EntitlementClass, Object)};
* kept briefly because there is some downstream usage*/
public static <T> void requireEntitled(EntitlementManager checker, EntitlementClass<T> permission, T typeArgument) {
checkEntitled(checker, permission, typeArgument);
}
// ----------------- initialization ----------------
public final static String ENTITLEMENTS_CONFIG_PREFIX = "brooklyn.entitlements";
public static ConfigKey<String> GLOBAL_ENTITLEMENT_MANAGER = ConfigKeys.newStringConfigKey(ENTITLEMENTS_CONFIG_PREFIX+".global",
"Class for entitlements in effect globally; "
+ "short names 'minimal', 'readonly', or 'root' are permitted here, with the default 'root' giving full access to all declared users; "
+ "or supply the name of an "+EntitlementManager.class+" class to instantiate, taking a 1-arg BrooklynProperties constructor or a 0-arg constructor",
"root");
public static EntitlementManager newManager(ManagementContext mgmt, BrooklynProperties brooklynProperties) {
return newGlobalManager(mgmt, brooklynProperties);
}
private static EntitlementManager newGlobalManager(ManagementContext mgmt, BrooklynProperties brooklynProperties) {
return load(mgmt, brooklynProperties, brooklynProperties.getConfig(GLOBAL_ENTITLEMENT_MANAGER));
}
public static EntitlementManager load(@Nullable ManagementContext mgmt, BrooklynProperties brooklynProperties, String type) {
if ("root".equalsIgnoreCase(type)) return root();
if ("readonly".equalsIgnoreCase(type) || "read_only".equalsIgnoreCase(type)) return readOnly();
if ("minimal".equalsIgnoreCase(type)) return minimal();
if (Strings.isNonBlank(type)) {
try {
ClassLoader cl = mgmt==null ? null : ((ManagementContextInternal)mgmt).getCatalogClassLoader();
if (cl==null) cl = Entitlements.class.getClassLoader();
Class<?> clazz = cl.loadClass(DeserializingClassRenamesProvider.findMappedName(type));
return (EntitlementManager) instantiate(clazz, ImmutableList.of(
new Object[] {mgmt, brooklynProperties},
new Object[] {mgmt},
new Object[] {brooklynProperties},
new Object[0]));
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
throw new IllegalStateException("Invalid entitlement manager specified: '"+type+"'");
}
private static Object instantiate(Class<?> clazz, List<Object[]> constructorArgOptions) {
try {
for (Object[] constructorArgOption : constructorArgOptions) {
Optional<?> result = Reflections.invokeConstructorWithArgs(clazz, constructorArgOption);
if (result.isPresent()) return result.get();
}
} catch (Exception e) {
throw Exceptions.propagate(e);
}
throw new IllegalStateException("No matching constructor to instantiate "+clazz);
}
}