http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java ---------------------------------------------------------------------- diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java index 0000000,89f253a..22a4502 mode 000000,100644..100644 --- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java +++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java @@@ -1,0 -1,472 +1,480 @@@ + /* + * 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.rest.resources; + + import static com.google.common.base.Preconditions.checkNotNull; + import static javax.ws.rs.core.Response.created; + import static javax.ws.rs.core.Response.status; + import static javax.ws.rs.core.Response.Status.ACCEPTED; + + import java.net.URI; + import java.net.URISyntaxException; + import java.util.Collection; + import java.util.Collections; + import java.util.Iterator; + import java.util.List; + import java.util.Map; + + import javax.ws.rs.core.Context; + import javax.ws.rs.core.Response; + import javax.ws.rs.core.Response.ResponseBuilder; + import javax.ws.rs.core.UriInfo; + + import org.apache.brooklyn.api.entity.Application; + import org.apache.brooklyn.api.entity.Entity; + import org.apache.brooklyn.api.entity.EntitySpec; + import org.apache.brooklyn.api.entity.Group; + import org.apache.brooklyn.api.location.Location; + import org.apache.brooklyn.api.mgmt.Task; + import org.apache.brooklyn.api.objs.BrooklynObject; + import org.apache.brooklyn.api.sensor.AttributeSensor; + import org.apache.brooklyn.api.sensor.Sensor; ++import org.apache.brooklyn.api.typereg.RegisteredType; + import org.apache.brooklyn.core.config.ConstraintViolationException; + import org.apache.brooklyn.core.entity.Attributes; + import org.apache.brooklyn.core.entity.EntityPredicates; + import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; + import org.apache.brooklyn.core.entity.trait.Startable; + import org.apache.brooklyn.core.mgmt.EntityManagementUtils; + import org.apache.brooklyn.core.mgmt.EntityManagementUtils.CreationResult; + import org.apache.brooklyn.core.mgmt.entitlement.EntitlementPredicates; + import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; + import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.EntityAndItem; + import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument; + import org.apache.brooklyn.core.sensor.Sensors; + import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; + import org.apache.brooklyn.core.typereg.RegisteredTypes; + import org.apache.brooklyn.entity.group.AbstractGroup; + import org.apache.brooklyn.rest.api.ApplicationApi; + import org.apache.brooklyn.rest.domain.ApplicationSpec; + import org.apache.brooklyn.rest.domain.ApplicationSummary; + import org.apache.brooklyn.rest.domain.EntitySummary; + import org.apache.brooklyn.rest.domain.TaskSummary; + import org.apache.brooklyn.rest.filter.HaHotStateRequired; + import org.apache.brooklyn.rest.transform.ApplicationTransformer; + import org.apache.brooklyn.rest.transform.EntityTransformer; + import org.apache.brooklyn.rest.transform.TaskTransformer; + import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; + import org.apache.brooklyn.rest.util.WebResourceUtils; + import org.apache.brooklyn.util.collections.MutableMap; + import org.apache.brooklyn.util.core.ResourceUtils; + import org.apache.brooklyn.util.exceptions.Exceptions; + import org.apache.brooklyn.util.exceptions.UserFacingException; ++import org.apache.brooklyn.util.guava.Maybe; + import org.apache.brooklyn.util.javalang.JavaClassNames; + import org.apache.brooklyn.util.text.Strings; + import org.codehaus.jackson.JsonNode; + import org.codehaus.jackson.node.ArrayNode; + import org.codehaus.jackson.node.ObjectNode; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + import com.google.common.base.Throwables; + import com.google.common.collect.FluentIterable; + import com.google.common.collect.Iterables; + + @HaHotStateRequired + public class ApplicationResource extends AbstractBrooklynRestResource implements ApplicationApi { + + private static final Logger log = LoggerFactory.getLogger(ApplicationResource.class); + + @Context + private UriInfo uriInfo; + + /** @deprecated since 0.6.0 use {@link #fetch(String)} (with slightly different, but better semantics) */ + @Deprecated + @Override + public JsonNode applicationTree() { + ArrayNode apps = mapper().createArrayNode(); + for (Application application : mgmt().getApplications()) + apps.add(recursiveTreeFromEntity(application)); + return apps; + } + + private ObjectNode entityBase(Entity entity) { + ObjectNode aRoot = mapper().createObjectNode(); + aRoot.put("name", entity.getDisplayName()); + aRoot.put("id", entity.getId()); + aRoot.put("type", entity.getEntityType().getName()); + + Boolean serviceUp = entity.getAttribute(Attributes.SERVICE_UP); + if (serviceUp!=null) aRoot.put("serviceUp", serviceUp); + + Lifecycle serviceState = entity.getAttribute(Attributes.SERVICE_STATE_ACTUAL); + if (serviceState!=null) aRoot.put("serviceState", serviceState.toString()); + + String iconUrl = entity.getIconUrl(); + if (iconUrl!=null) { + if (brooklyn().isUrlServerSideAndSafe(iconUrl)) + // route to server if it is a server-side url + iconUrl = EntityTransformer.entityUri(entity)+"/icon"; + aRoot.put("iconUrl", iconUrl); + } + + return aRoot; + } + + private JsonNode recursiveTreeFromEntity(Entity entity) { + ObjectNode aRoot = entityBase(entity); + + if (!entity.getChildren().isEmpty()) + aRoot.put("children", childEntitiesRecursiveAsArray(entity)); + + return aRoot; + } + + // TODO when applicationTree can be removed, replace this with an extension to EntitySummary (without links) + private JsonNode fromEntity(Entity entity) { + ObjectNode aRoot = entityBase(entity); + + aRoot.put("applicationId", entity.getApplicationId()); + + if (entity.getParent()!=null) { + aRoot.put("parentId", entity.getParent().getId()); + } + + if (!entity.groups().isEmpty()) + aRoot.put("groupIds", entitiesIdAsArray(entity.groups())); + + if (!entity.getChildren().isEmpty()) + aRoot.put("children", entitiesIdAndNameAsArray(entity.getChildren())); + + if (entity instanceof Group) { + // use attribute instead of method in case it is read-only + Collection<Entity> members = entity.getAttribute(AbstractGroup.GROUP_MEMBERS); + if (members!=null && !members.isEmpty()) + aRoot.put("members", entitiesIdAndNameAsArray(members)); + } + + return aRoot; + } + + private ArrayNode childEntitiesRecursiveAsArray(Entity entity) { + ArrayNode node = mapper().createArrayNode(); + for (Entity e : entity.getChildren()) { + if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { + node.add(recursiveTreeFromEntity(e)); + } + } + return node; + } + + private ArrayNode entitiesIdAndNameAsArray(Collection<? extends Entity> entities) { + ArrayNode node = mapper().createArrayNode(); + for (Entity entity : entities) { + if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { + ObjectNode holder = mapper().createObjectNode(); + holder.put("id", entity.getId()); + holder.put("name", entity.getDisplayName()); + node.add(holder); + } + } + return node; + } + + private ArrayNode entitiesIdAsArray(Iterable<? extends Entity> entities) { + ArrayNode node = mapper().createArrayNode(); + for (Entity entity : entities) { + if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { + node.add(entity.getId()); + } + } + return node; + } + + @Override + public JsonNode fetch(String entityIds) { + Map<String, JsonNode> jsonEntitiesById = MutableMap.of(); + for (Application application : mgmt().getApplications()) + jsonEntitiesById.put(application.getId(), fromEntity(application)); + if (entityIds != null) { + for (String entityId: entityIds.split(",")) { + Entity entity = mgmt().getEntityManager().getEntity(entityId.trim()); + while (entity != null && entity.getParent() != null) { + if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { + jsonEntitiesById.put(entity.getId(), fromEntity(entity)); + } + entity = entity.getParent(); + } + } + } + + ArrayNode result = mapper().createArrayNode(); + for (JsonNode n: jsonEntitiesById.values()) result.add(n); + return result; + } + + @Override + public List<ApplicationSummary> list(String typeRegex) { + if (Strings.isBlank(typeRegex)) { + typeRegex = ".*"; + } + return FluentIterable + .from(mgmt().getApplications()) + .filter(EntitlementPredicates.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY)) + .filter(EntityPredicates.hasInterfaceMatching(typeRegex)) + .transform(ApplicationTransformer.FROM_APPLICATION) + .toList(); + } + + @Override + public ApplicationSummary get(String application) { + return ApplicationTransformer.summaryFromApplication(brooklyn().getApplication(application)); + } + + public Response create(ApplicationSpec applicationSpec) { + return createFromAppSpec(applicationSpec); + } + + /** @deprecated since 0.7.0 see #create */ @Deprecated + protected Response createFromAppSpec(ApplicationSpec applicationSpec) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, applicationSpec)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to start application %s", + Entitlements.getEntitlementContext().user(), applicationSpec); + } + + checkApplicationTypesAreValid(applicationSpec); + checkLocationsAreValid(applicationSpec); + // TODO duplicate prevention + List<Location> locations = brooklyn().getLocations(applicationSpec); + Application app = brooklyn().create(applicationSpec); + Task<?> t = brooklyn().start(app, locations); + TaskSummary ts = TaskTransformer.FROM_TASK.apply(t); + URI ref = uriInfo.getBaseUriBuilder() + .path(ApplicationApi.class) + .path(ApplicationApi.class, "get") + .build(app.getApplicationId()); + return created(ref).entity(ts).build(); + } + + @Override + public Response createFromYaml(String yaml) { + // First of all, see if it's a URL + URI uri; + try { + uri = new URI(yaml); + } catch (URISyntaxException e) { + // It's not a URI then... + uri = null; + } + if (uri != null) { + log.debug("Create app called with URI; retrieving contents: {}", uri); + yaml = ResourceUtils.create(mgmt()).getResourceAsString(uri.toString()); + } + + log.debug("Creating app from yaml:\n{}", yaml); + EntitySpec<? extends Application> spec = createEntitySpecForApplication(yaml); + + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to start application %s", + Entitlements.getEntitlementContext().user(), yaml); + } + + return launch(yaml, spec); + } + + private Response launch(String yaml, EntitySpec<? extends Application> spec) { + try { + Application app = EntityManagementUtils.createUnstarted(mgmt(), spec); + CreationResult<Application,Void> result = EntityManagementUtils.start(app); + + boolean isEntitled = Entitlements.isEntitled( + mgmt().getEntitlementManager(), + Entitlements.INVOKE_EFFECTOR, + EntityAndItem.of(app, StringAndArgument.of(Startable.START.getName(), null))); + + if (!isEntitled) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to start application %s", + Entitlements.getEntitlementContext().user(), spec.getType()); + } + + log.info("Launched from YAML: " + yaml + " -> " + app + " (" + result.task() + ")"); + + URI ref = URI.create(app.getApplicationId()); + ResponseBuilder response = created(ref); + if (result.task() != null) + response.entity(TaskTransformer.FROM_TASK.apply(result.task())); + return response.build(); + } catch (ConstraintViolationException e) { + throw new UserFacingException(e); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + + @Override + public Response createPoly(byte[] inputToAutodetectType) { + log.debug("Creating app from autodetecting input"); + + boolean looksLikeLegacy = false; + Exception legacyFormatException = null; + // attempt legacy format + try { + ApplicationSpec appSpec = mapper().readValue(inputToAutodetectType, ApplicationSpec.class); + if (appSpec.getType() != null || appSpec.getEntities() != null) { + looksLikeLegacy = true; + } + return createFromAppSpec(appSpec); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + legacyFormatException = e; + log.debug("Input is not legacy ApplicationSpec JSON (will try others): "+e, e); + } + + //TODO infer encoding from request + String potentialYaml = new String(inputToAutodetectType); + EntitySpec<? extends Application> spec = createEntitySpecForApplication(potentialYaml); + + // TODO not json - try ZIP, etc + + if (spec != null) { + return launch(potentialYaml, spec); + } else if (looksLikeLegacy) { + throw Throwables.propagate(legacyFormatException); + } else { + return Response.serverError().entity("Unsupported format; not able to autodetect.").build(); + } + } + + @Override + public Response createFromForm(String contents) { + log.debug("Creating app from form"); + return createPoly(contents.getBytes()); + } + + @Override + public Response delete(String application) { + Application app = brooklyn().getApplication(application); + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(app, + StringAndArgument.of(Entitlements.LifecycleEffectors.DELETE, null)))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to delete application %s", + Entitlements.getEntitlementContext().user(), app); + } + Task<?> t = brooklyn().destroy(app); + TaskSummary ts = TaskTransformer.FROM_TASK.apply(t); + return status(ACCEPTED).entity(ts).build(); + } + + private EntitySpec<? extends Application> createEntitySpecForApplication(String potentialYaml) { + try { + return EntityManagementUtils.createEntitySpecForApplication(mgmt(), potentialYaml); + } catch (Exception e) { + // An IllegalArgumentException for creating the entity spec gets wrapped in a ISE, and possibly a Compound. + // But we want to return a 400 rather than 500, so ensure we throw IAE. + IllegalArgumentException iae = (IllegalArgumentException) Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class); + if (iae != null) { + throw new IllegalArgumentException("Cannot create spec for app: "+iae.getMessage(), e); + } else { + throw Exceptions.propagate(e); + } + } + } + + private void checkApplicationTypesAreValid(ApplicationSpec applicationSpec) { + String appType = applicationSpec.getType(); + if (appType != null) { + checkEntityTypeIsValid(appType); + + if (applicationSpec.getEntities() != null) { + throw WebResourceUtils.preconditionFailed("Application given explicit type '%s' must not define entities", appType); + } + return; + } + + for (org.apache.brooklyn.rest.domain.EntitySpec entitySpec : applicationSpec.getEntities()) { + String entityType = entitySpec.getType(); + checkEntityTypeIsValid(checkNotNull(entityType, "entityType")); + } + } + + private void checkSpecTypeIsValid(String type, Class<? extends BrooklynObject> subType) { - if (RegisteredTypes.validate(mgmt().getTypeRegistry().get(type), RegisteredTypeLoadingContexts.spec(subType)) == null) { - try { - brooklyn().getCatalogClassLoader().loadClass(type); - } catch (ClassNotFoundException e) { - log.debug("Class not found for type '" + type + "'; reporting 404", e); - throw WebResourceUtils.notFound("Undefined type '%s'", type); - } - log.info(JavaClassNames.simpleClassName(subType)+" type '{}' not defined in catalog but is on classpath; continuing", type); ++ Maybe<RegisteredType> typeV = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(type), RegisteredTypeLoadingContexts.spec(subType)); ++ if (!typeV.isNull()) { ++ // found, throw if any problem ++ typeV.get(); ++ return; ++ } ++ ++ // not found, try classloading ++ try { ++ brooklyn().getCatalogClassLoader().loadClass(type); ++ } catch (ClassNotFoundException e) { ++ log.debug("Class not found for type '" + type + "'; reporting 404", e); ++ throw WebResourceUtils.notFound("Undefined type '%s'", type); + } ++ log.info(JavaClassNames.simpleClassName(subType)+" type '{}' not defined in catalog but is on classpath; continuing", type); + } + + private void checkEntityTypeIsValid(String type) { + checkSpecTypeIsValid(type, Entity.class); + } + + @SuppressWarnings("deprecation") + private void checkLocationsAreValid(ApplicationSpec applicationSpec) { + for (String locationId : applicationSpec.getLocations()) { + locationId = BrooklynRestResourceUtils.fixLocation(locationId); + if (!brooklyn().getLocationRegistry().canMaybeResolve(locationId) && brooklyn().getLocationRegistry().getDefinedLocationById(locationId)==null) { + throw WebResourceUtils.notFound("Undefined location '%s'", locationId); + } + } + } + + @Override + public List<EntitySummary> getDescendants(String application, String typeRegex) { + return EntityTransformer.entitySummaries(brooklyn().descendantsOfType(application, application, typeRegex)); + } + + @Override + public Map<String, Object> getDescendantsSensor(String application, String sensor, String typeRegex) { + Iterable<Entity> descs = brooklyn().descendantsOfType(application, application, typeRegex); + return getSensorMap(sensor, descs); + } + + public static Map<String, Object> getSensorMap(String sensor, Iterable<Entity> descs) { + if (Iterables.isEmpty(descs)) + return Collections.emptyMap(); + Map<String, Object> result = MutableMap.of(); + Iterator<Entity> di = descs.iterator(); + Sensor<?> s = null; + while (di.hasNext()) { + Entity potentialSource = di.next(); + s = potentialSource.getEntityType().getSensor(sensor); + if (s!=null) break; + } + if (s==null) + s = Sensors.newSensor(Object.class, sensor); + if (!(s instanceof AttributeSensor<?>)) { + log.warn("Cannot retrieve non-attribute sensor "+s+" for entities; returning empty map"); + return result; + } + for (Entity e: descs) { + Object v = null; + try { + v = e.getAttribute((AttributeSensor<?>)s); + } catch (Exception exc) { + Exceptions.propagateIfFatal(exc); + log.warn("Error retrieving sensor "+s+" for "+e+" (ignoring): "+exc); + } + if (v!=null) + result.put(e.getId(), v); + } + return result; + } + + }
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java ---------------------------------------------------------------------- diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java index 0000000,ecbeffb..01bf992 mode 000000,100644..100644 --- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java +++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java @@@ -1,0 -1,508 +1,516 @@@ + /* + * 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.rest.resources; + + import java.io.InputStream; + import java.net.URI; + import java.util.ArrayList; + import java.util.Iterator; + import java.util.List; + import java.util.Map; + import java.util.NoSuchElementException; + import java.util.Set; + + import javax.annotation.Nullable; + import javax.ws.rs.Consumes; + import javax.ws.rs.core.MediaType; + import javax.ws.rs.core.Response; + import javax.ws.rs.core.Response.Status; + + import org.apache.brooklyn.api.catalog.CatalogItem; + import org.apache.brooklyn.api.entity.Application; + import org.apache.brooklyn.api.entity.Entity; + import org.apache.brooklyn.api.entity.EntitySpec; + import org.apache.brooklyn.api.location.Location; + import org.apache.brooklyn.api.location.LocationSpec; + import org.apache.brooklyn.api.policy.Policy; + import org.apache.brooklyn.api.policy.PolicySpec; + import org.apache.brooklyn.api.typereg.RegisteredType; + import org.apache.brooklyn.core.catalog.CatalogPredicates; + import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; + import org.apache.brooklyn.core.catalog.internal.CatalogDto; + import org.apache.brooklyn.core.catalog.internal.CatalogItemComparator; + import org.apache.brooklyn.core.catalog.internal.CatalogUtils; + import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; + import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument; + import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; + import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; + import org.apache.brooklyn.core.typereg.RegisteredTypes; + import org.apache.brooklyn.rest.api.CatalogApi; + import org.apache.brooklyn.rest.domain.ApiError; + import org.apache.brooklyn.rest.domain.CatalogEntitySummary; + import org.apache.brooklyn.rest.domain.CatalogItemSummary; + import org.apache.brooklyn.rest.domain.CatalogLocationSummary; + import org.apache.brooklyn.rest.domain.CatalogPolicySummary; + import org.apache.brooklyn.rest.filter.HaHotStateRequired; + import org.apache.brooklyn.rest.transform.CatalogTransformer; + import org.apache.brooklyn.rest.util.WebResourceUtils; + import org.apache.brooklyn.util.collections.MutableMap; + import org.apache.brooklyn.util.collections.MutableSet; + import org.apache.brooklyn.util.core.ResourceUtils; + import org.apache.brooklyn.util.exceptions.Exceptions; ++import org.apache.brooklyn.util.guava.Maybe; + import org.apache.brooklyn.util.stream.Streams; + import org.apache.brooklyn.util.text.StringPredicates; + import org.apache.brooklyn.util.text.Strings; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + import com.google.common.base.Function; + import com.google.common.base.Predicate; + import com.google.common.base.Predicates; + import com.google.common.collect.FluentIterable; + import com.google.common.collect.ImmutableList; + import com.google.common.collect.Lists; + import com.google.common.io.Files; + import com.sun.jersey.core.header.FormDataContentDisposition; + + @HaHotStateRequired + public class CatalogResource extends AbstractBrooklynRestResource implements CatalogApi { + + private static final Logger log = LoggerFactory.getLogger(CatalogResource.class); + + @SuppressWarnings("rawtypes") + private final Function<CatalogItem, CatalogItemSummary> TO_CATALOG_ITEM_SUMMARY = new Function<CatalogItem, CatalogItemSummary>() { + @Override + public CatalogItemSummary apply(@Nullable CatalogItem input) { + return CatalogTransformer.catalogItemSummary(brooklyn(), input); + } + }; + + @Override + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response createFromMultipart(InputStream uploadedInputStream, FormDataContentDisposition fileDetail) { + return create(Streams.readFullyString(uploadedInputStream)); + } + + static Set<String> missingIcons = MutableSet.of(); + + @Override + public Response create(String yaml) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, yaml)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to add catalog item", + Entitlements.getEntitlementContext().user()); + } + + Iterable<? extends CatalogItem<?, ?>> items; + try { + items = brooklyn().getCatalog().addItems(yaml); + } catch (IllegalArgumentException e) { + return Response.status(Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of(e)) + .build(); + } + + log.info("REST created catalog items: "+items); + + Map<String,Object> result = MutableMap.of(); + + for (CatalogItem<?,?> item: items) { + result.put(item.getId(), CatalogTransformer.catalogItemSummary(brooklyn(), item)); + } + return Response.status(Status.CREATED).entity(result).build(); + } + + @SuppressWarnings("deprecation") + @Override + public Response resetXml(String xml, boolean ignoreErrors) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, null) || + !Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, null)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + + ((BasicBrooklynCatalog)mgmt().getCatalog()).reset(CatalogDto.newDtoFromXmlContents(xml, "REST reset"), !ignoreErrors); + return Response.ok().build(); + } + + @Override + @Deprecated + public void deleteEntity(String entityId) throws Exception { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(entityId, "delete"))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + try { - RegisteredType item = RegisteredTypes.validate(mgmt().getTypeRegistry().get(entityId), RegisteredTypeLoadingContexts.spec(Entity.class)); - if (item==null) { ++ Maybe<RegisteredType> item = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(entityId), RegisteredTypeLoadingContexts.spec(Entity.class)); ++ if (item.isNull()) { + throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId); + } - brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); ++ if (item.isAbsent()) { ++ throw WebResourceUtils.notFound("Item with id '%s' is not an entity", entityId); ++ } ++ ++ brooklyn().getCatalog().deleteCatalogItem(item.get().getSymbolicName(), item.get().getVersion()); ++ + } catch (NoSuchElementException e) { - throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId); ++ // shouldn't come here ++ throw WebResourceUtils.notFound("Entity with id '%s' could not be deleted", entityId); ++ + } + } + + @Override + public void deleteApplication(String symbolicName, String version) throws Exception { + deleteEntity(symbolicName, version); + } + + @Override + public void deleteEntity(String symbolicName, String version) throws Exception { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(symbolicName+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + + RegisteredType item = mgmt().getTypeRegistry().get(symbolicName, version); + if (item == null) { + throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version); + } else if (!RegisteredTypePredicates.IS_ENTITY.apply(item) && !RegisteredTypePredicates.IS_APPLICATION.apply(item)) { + throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not an entity", symbolicName, version); + } else { + brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); + } + } + + @Override + public void deletePolicy(String policyId, String version) throws Exception { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(policyId+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + + RegisteredType item = mgmt().getTypeRegistry().get(policyId, version); + if (item == null) { + throw WebResourceUtils.notFound("Policy with id '%s:%s' not found", policyId, version); + } else if (!RegisteredTypePredicates.IS_POLICY.apply(item)) { + throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not a policy", policyId, version); + } else { + brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); + } + } + + @Override + public void deleteLocation(String locationId, String version) throws Exception { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(locationId+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + + RegisteredType item = mgmt().getTypeRegistry().get(locationId, version); + if (item == null) { + throw WebResourceUtils.notFound("Location with id '%s:%s' not found", locationId, version); + } else if (!RegisteredTypePredicates.IS_LOCATION.apply(item)) { + throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not a location", locationId, version); + } else { + brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); + } + } + + @Override + public List<CatalogEntitySummary> listEntities(String regex, String fragment, boolean allVersions) { + Predicate<CatalogItem<Entity, EntitySpec<?>>> filter = + Predicates.and( + CatalogPredicates.IS_ENTITY, + CatalogPredicates.<Entity, EntitySpec<?>>disabled(false)); + List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); + return castList(result, CatalogEntitySummary.class); + } + + @Override + public List<CatalogItemSummary> listApplications(String regex, String fragment, boolean allVersions) { + @SuppressWarnings("unchecked") + Predicate<CatalogItem<Application, EntitySpec<? extends Application>>> filter = + Predicates.and( + CatalogPredicates.IS_TEMPLATE, + CatalogPredicates.<Application,EntitySpec<? extends Application>>deprecated(false), + CatalogPredicates.<Application,EntitySpec<? extends Application>>disabled(false)); + return getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); + } + + @Override + @Deprecated + public CatalogEntitySummary getEntity(String entityId) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, entityId)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + CatalogItem<Entity,EntitySpec<?>> result = + CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Entity.class, entityId); + + if (result==null) { + throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId); + } + + return CatalogTransformer.catalogEntitySummary(brooklyn(), result); + } + + @Override + public CatalogEntitySummary getEntity(String symbolicName, String version) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, symbolicName+(Strings.isBlank(version)?"":":"+version))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + //TODO These casts are not pretty, we could just provide separate get methods for the different types? + //Or we could provide asEntity/asPolicy cast methods on the CataloItem doing a safety check internally + @SuppressWarnings("unchecked") + CatalogItem<Entity, EntitySpec<?>> result = + (CatalogItem<Entity, EntitySpec<?>>) brooklyn().getCatalog().getCatalogItem(symbolicName, version); + + if (result==null) { + throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version); + } + + return CatalogTransformer.catalogEntitySummary(brooklyn(), result); + } + + @Override + @Deprecated + public CatalogEntitySummary getApplication(String applicationId) throws Exception { + return getEntity(applicationId); + } + + @Override + public CatalogEntitySummary getApplication(String symbolicName, String version) { + return getEntity(symbolicName, version); + } + + @Override + public List<CatalogPolicySummary> listPolicies(String regex, String fragment, boolean allVersions) { + Predicate<CatalogItem<Policy, PolicySpec<?>>> filter = + Predicates.and( + CatalogPredicates.IS_POLICY, + CatalogPredicates.<Policy, PolicySpec<?>>disabled(false)); + List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); + return castList(result, CatalogPolicySummary.class); + } + + @Override + @Deprecated + public CatalogPolicySummary getPolicy(String policyId) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + CatalogItem<? extends Policy, PolicySpec<?>> result = + CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Policy.class, policyId); + + if (result==null) { + throw WebResourceUtils.notFound("Policy with id '%s' not found", policyId); + } + + return CatalogTransformer.catalogPolicySummary(brooklyn(), result); + } + + @Override + public CatalogPolicySummary getPolicy(String policyId, String version) throws Exception { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId+(Strings.isBlank(version)?"":":"+version))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + @SuppressWarnings("unchecked") + CatalogItem<? extends Policy, PolicySpec<?>> result = + (CatalogItem<? extends Policy, PolicySpec<?>>)brooklyn().getCatalog().getCatalogItem(policyId, version); + + if (result==null) { + throw WebResourceUtils.notFound("Policy with id '%s:%s' not found", policyId, version); + } + + return CatalogTransformer.catalogPolicySummary(brooklyn(), result); + } + + @Override + public List<CatalogLocationSummary> listLocations(String regex, String fragment, boolean allVersions) { + Predicate<CatalogItem<Location, LocationSpec<?>>> filter = + Predicates.and( + CatalogPredicates.IS_LOCATION, + CatalogPredicates.<Location, LocationSpec<?>>disabled(false)); + List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); + return castList(result, CatalogLocationSummary.class); + } + + @Override + @Deprecated + public CatalogLocationSummary getLocation(String locationId) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + CatalogItem<? extends Location, LocationSpec<?>> result = + CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Location.class, locationId); + + if (result==null) { + throw WebResourceUtils.notFound("Location with id '%s' not found", locationId); + } + + return CatalogTransformer.catalogLocationSummary(brooklyn(), result); + } + + @Override + public CatalogLocationSummary getLocation(String locationId, String version) throws Exception { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId+(Strings.isBlank(version)?"":":"+version))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + @SuppressWarnings("unchecked") + CatalogItem<? extends Location, LocationSpec<?>> result = + (CatalogItem<? extends Location, LocationSpec<?>>)brooklyn().getCatalog().getCatalogItem(locationId, version); + + if (result==null) { + throw WebResourceUtils.notFound("Location with id '%s:%s' not found", locationId, version); + } + + return CatalogTransformer.catalogLocationSummary(brooklyn(), result); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private <T,SpecT> List<CatalogItemSummary> getCatalogItemSummariesMatchingRegexFragment(Predicate<CatalogItem<T,SpecT>> type, String regex, String fragment, boolean allVersions) { + List filters = new ArrayList(); + filters.add(type); + if (Strings.isNonEmpty(regex)) + filters.add(CatalogPredicates.xml(StringPredicates.containsRegex(regex))); + if (Strings.isNonEmpty(fragment)) + filters.add(CatalogPredicates.xml(StringPredicates.containsLiteralIgnoreCase(fragment))); + if (!allVersions) + filters.add(CatalogPredicates.isBestVersion(mgmt())); + + filters.add(CatalogPredicates.entitledToSee(mgmt())); + + ImmutableList<CatalogItem<Object, Object>> sortedItems = + FluentIterable.from(brooklyn().getCatalog().getCatalogItems()) + .filter(Predicates.and(filters)) + .toSortedList(CatalogItemComparator.getInstance()); + return Lists.transform(sortedItems, TO_CATALOG_ITEM_SUMMARY); + } + + @Override + @Deprecated + public Response getIcon(String itemId) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId)) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + return getCatalogItemIcon( mgmt().getTypeRegistry().get(itemId) ); + } + + @Override + public Response getIcon(String itemId, String version) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId+(Strings.isBlank(version)?"":":"+version))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", + Entitlements.getEntitlementContext().user()); + } + + return getCatalogItemIcon(mgmt().getTypeRegistry().get(itemId, version)); + } + + @Override + public void setDeprecatedLegacy(String itemId, boolean deprecated) { + log.warn("Use of deprecated \"/v1/catalog/entities/{itemId}/deprecated/{deprecated}\" for "+itemId + +"; use \"/v1/catalog/entities/{itemId}/deprecated\" with request body"); + setDeprecated(itemId, deprecated); + } + + @SuppressWarnings("deprecation") + @Override + public void setDeprecated(String itemId, boolean deprecated) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(itemId, "deprecated"))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + CatalogUtils.setDeprecated(mgmt(), itemId, deprecated); + } + + @SuppressWarnings("deprecation") + @Override + public void setDisabled(String itemId, boolean disabled) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(itemId, "disabled"))) { + throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", + Entitlements.getEntitlementContext().user()); + } + CatalogUtils.setDisabled(mgmt(), itemId, disabled); + } + + private Response getCatalogItemIcon(RegisteredType result) { + String url = result.getIconUrl(); + if (url==null) { + log.debug("No icon available for "+result+"; returning "+Status.NO_CONTENT); + return Response.status(Status.NO_CONTENT).build(); + } + + if (brooklyn().isUrlServerSideAndSafe(url)) { + // classpath URL's we will serve IF they end with a recognised image format; + // paths (ie non-protocol) and + // NB, for security, file URL's are NOT served + log.debug("Loading and returning "+url+" as icon for "+result); + + MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(Files.getFileExtension(url)); + try { + Object content = ResourceUtils.create(CatalogUtils.newClassLoadingContext(mgmt(), result)).getResourceFromUrl(url); + return Response.ok(content, mime).build(); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + synchronized (missingIcons) { + if (missingIcons.add(url)) { + // note: this can be quite common when running from an IDE, as resources may not be copied; + // a mvn build should sort it out (the IDE will then find the resources, until you clean or maybe refresh...) + log.warn("Missing icon data for "+result.getId()+", expected at: "+url+" (subsequent messages will log debug only)"); + log.debug("Trace for missing icon data at "+url+": "+e, e); + } else { + log.debug("Missing icon data for "+result.getId()+", expected at: "+url+" (already logged WARN and error details)"); + } + } + throw WebResourceUtils.notFound("Icon unavailable for %s", result.getId()); + } + } + + log.debug("Returning redirect to "+url+" as icon for "+result); + + // for anything else we do a redirect (e.g. http / https; perhaps ftp) + return Response.temporaryRedirect(URI.create(url)).build(); + } + + // TODO Move to an appropriate utility class? + @SuppressWarnings("unchecked") + private static <T> List<T> castList(List<? super T> list, Class<T> elementType) { + List<T> result = Lists.newArrayList(); + Iterator<? super T> li = list.iterator(); + while (li.hasNext()) { + try { + result.add((T) li.next()); + } catch (Throwable throwable) { + if (throwable instanceof NoClassDefFoundError) { + // happens if class cannot be loaded for any reason during transformation - don't treat as fatal + } else { + Exceptions.propagateIfFatal(throwable); + } + + // item cannot be transformed; we will have logged a warning earlier + log.debug("Ignoring invalid catalog item: "+throwable); + } + } + return result; + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java ---------------------------------------------------------------------- diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java index 0000000,8b2b9da..11884c3 mode 000000,100644..100644 --- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java +++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java @@@ -1,0 -1,165 +1,166 @@@ + /* + * 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.rest.security.provider; + + import java.lang.reflect.Constructor; + import java.util.concurrent.atomic.AtomicLong; + + import javax.servlet.http.HttpSession; + + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.apache.brooklyn.api.mgmt.ManagementContext; + import org.apache.brooklyn.config.StringConfigMap; + import org.apache.brooklyn.core.internal.BrooklynProperties; ++import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; + import org.apache.brooklyn.rest.BrooklynWebConfig; + import org.apache.brooklyn.util.text.Strings; + + public class DelegatingSecurityProvider implements SecurityProvider { + + private static final Logger log = LoggerFactory.getLogger(DelegatingSecurityProvider.class); + protected final ManagementContext mgmt; + + public DelegatingSecurityProvider(ManagementContext mgmt) { + this.mgmt = mgmt; + mgmt.addPropertiesReloadListener(new PropertiesListener()); + } + + private SecurityProvider delegate; + private final AtomicLong modCount = new AtomicLong(); + + private class PropertiesListener implements ManagementContext.PropertiesReloadListener { + private static final long serialVersionUID = 8148722609022378917L; + + @Override + public void reloaded() { + log.debug("{} reloading security provider", DelegatingSecurityProvider.this); + synchronized (DelegatingSecurityProvider.this) { + loadDelegate(); + invalidateExistingSessions(); + } + } + } + + public synchronized SecurityProvider getDelegate() { + if (delegate == null) { + delegate = loadDelegate(); + } + return delegate; + } + + @SuppressWarnings("unchecked") + private synchronized SecurityProvider loadDelegate() { + StringConfigMap brooklynProperties = mgmt.getConfig(); + + SecurityProvider presetDelegate = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE); + if (presetDelegate!=null) { + log.info("REST using pre-set security provider " + presetDelegate); + return presetDelegate; + } + + String className = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME); + + if (delegate != null && BrooklynWebConfig.hasNoSecurityOptions(mgmt.getConfig())) { + log.debug("{} refusing to change from {}: No security provider set in reloaded properties.", + this, delegate); + return delegate; + } + log.info("REST using security provider " + className); + + try { + Class<? extends SecurityProvider> clazz; + try { + clazz = (Class<? extends SecurityProvider>) Class.forName(className); + } catch (Exception e) { + String oldPackage = "brooklyn.web.console.security."; + if (className.startsWith(oldPackage)) { + className = Strings.removeFromStart(className, oldPackage); + className = DelegatingSecurityProvider.class.getPackage().getName() + "." + className; + clazz = (Class<? extends SecurityProvider>) Class.forName(className); + log.warn("Deprecated package " + oldPackage + " detected; please update security provider to point to " + className); + } else throw e; + } + + Constructor<? extends SecurityProvider> constructor; + try { + constructor = clazz.getConstructor(ManagementContext.class); + delegate = constructor.newInstance(mgmt); + } catch (Exception e) { + constructor = clazz.getConstructor(); + Object delegateO = constructor.newInstance(); + if (!(delegateO instanceof SecurityProvider)) { + // if classloaders get mangled it will be a different CL's SecurityProvider + throw new ClassCastException("Delegate is either not a security provider or has an incompatible classloader: "+delegateO); + } + delegate = (SecurityProvider) delegateO; + } + } catch (Exception e) { + log.warn("REST unable to instantiate security provider " + className + "; all logins are being disallowed", e); + delegate = new BlackholeSecurityProvider(); + } + - ((BrooklynProperties)mgmt.getConfig()).put(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE, delegate); ++ ((ManagementContextInternal)mgmt).getBrooklynProperties().put(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE, delegate); + + return delegate; + } + + /** + * Causes all existing sessions to be invalidated. + */ + protected void invalidateExistingSessions() { + modCount.incrementAndGet(); + } + + @Override + public boolean isAuthenticated(HttpSession session) { + if (session == null) return false; + Object modCountWhenFirstAuthenticated = session.getAttribute(getModificationCountKey()); + boolean authenticated = getDelegate().isAuthenticated(session) && + Long.valueOf(modCount.get()).equals(modCountWhenFirstAuthenticated); + return authenticated; + } + + @Override + public boolean authenticate(HttpSession session, String user, String password) { + boolean authenticated = getDelegate().authenticate(session, user, password); + if (authenticated) { + session.setAttribute(getModificationCountKey(), modCount.get()); + } + if (log.isTraceEnabled() && authenticated) { + log.trace("User {} authenticated with provider {}", user, getDelegate()); + } else if (!authenticated && log.isDebugEnabled()) { + log.debug("Failed authentication for user {} with provider {}", user, getDelegate()); + } + return authenticated; + } + + @Override + public boolean logout(HttpSession session) { + boolean logout = getDelegate().logout(session); + if (logout) { + session.removeAttribute(getModificationCountKey()); + } + return logout; + } + + private String getModificationCountKey() { + return getClass().getName() + ".ModCount"; + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java ---------------------------------------------------------------------- diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java index 0000000,a0795cb..562c85b mode 000000,100644..100644 --- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java +++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java @@@ -1,0 -1,117 +1,118 @@@ + /* + * 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.rest.security.provider; + + import java.util.LinkedHashSet; + import java.util.Set; + import java.util.StringTokenizer; + + import javax.servlet.http.HttpSession; + + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.apache.brooklyn.api.mgmt.ManagementContext; + import org.apache.brooklyn.config.StringConfigMap; + import org.apache.brooklyn.core.internal.BrooklynProperties; ++import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; + import org.apache.brooklyn.rest.BrooklynWebConfig; + import org.apache.brooklyn.rest.security.PasswordHasher; + + /** + * Security provider which validates users against passwords according to property keys, + * as set in {@link BrooklynWebConfig#USERS} and {@link BrooklynWebConfig#PASSWORD_FOR_USER(String)} + */ + public class ExplicitUsersSecurityProvider extends AbstractSecurityProvider implements SecurityProvider { + + public static final Logger LOG = LoggerFactory.getLogger(ExplicitUsersSecurityProvider.class); + + protected final ManagementContext mgmt; + private boolean allowAnyUserWithValidPass; + private Set<String> allowedUsers = null; + + public ExplicitUsersSecurityProvider(ManagementContext mgmt) { + this.mgmt = mgmt; + initialize(); + } + + private synchronized void initialize() { + if (allowedUsers != null) return; + + StringConfigMap properties = mgmt.getConfig(); + + allowedUsers = new LinkedHashSet<String>(); + String users = properties.getConfig(BrooklynWebConfig.USERS); + if (users == null) { + LOG.warn("REST has no users configured; no one will be able to log in!"); + } else if ("*".equals(users)) { + LOG.info("REST allowing any user (so long as valid password is set)"); + allowAnyUserWithValidPass = true; + } else { + StringTokenizer t = new StringTokenizer(users, ","); + while (t.hasMoreElements()) { + allowedUsers.add(("" + t.nextElement()).trim()); + } + LOG.info("REST allowing users: " + allowedUsers); + } + } + + @Override + public boolean authenticate(HttpSession session, String user, String password) { + if (session==null || user==null) return false; + + if (!allowAnyUserWithValidPass) { + if (!allowedUsers.contains(user)) { + LOG.debug("REST rejecting unknown user "+user); + return false; + } + } + + if (checkExplicitUserPassword(mgmt, user, password)) { + return allow(session, user); + } + return false; + } + + /** checks the supplied candidate user and password against the + * expect password (or SHA-256 + SALT thereof) defined as brooklyn properties. + */ + public static boolean checkExplicitUserPassword(ManagementContext mgmt, String user, String password) { - BrooklynProperties properties = (BrooklynProperties) mgmt.getConfig(); ++ BrooklynProperties properties = ((ManagementContextInternal)mgmt).getBrooklynProperties(); + String expectedPassword = properties.getConfig(BrooklynWebConfig.PASSWORD_FOR_USER(user)); + String salt = properties.getConfig(BrooklynWebConfig.SALT_FOR_USER(user)); + String expectedSha256 = properties.getConfig(BrooklynWebConfig.SHA256_FOR_USER(user)); + + return checkPassword(password, expectedPassword, expectedSha256, salt); + } + /** + * checks a candidate password against the expected credential defined for a given user. + * the expected credentials can be supplied as an expectedPassword OR as + * a combination of the SHA-256 hash of the expected password plus a defined salt. + * the combination of the SHA+SALT allows credentials to be supplied in a non-plaintext manner. + */ + public static boolean checkPassword(String candidatePassword, String expectedPassword, String expectedPasswordSha256, String salt) { + if (expectedPassword != null) { + return expectedPassword.equals(candidatePassword); + } else if (expectedPasswordSha256 != null) { + String hashedCandidatePassword = PasswordHasher.sha256(salt, candidatePassword); + return expectedPasswordSha256.equals(hashedCandidatePassword); + } + + return false; + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java ---------------------------------------------------------------------- diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java index 0000000,0f710bc..74625fd mode 000000,100644..100644 --- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java +++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java @@@ -1,0 -1,184 +1,186 @@@ + /* + * 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.rest.transform; + + import java.net.URI; + import java.util.Map; + import java.util.Set; ++import java.util.concurrent.atomic.AtomicInteger; + + import org.apache.brooklyn.api.catalog.CatalogItem; + import org.apache.brooklyn.api.catalog.CatalogItem.CatalogItemType; + import org.apache.brooklyn.api.effector.Effector; + import org.apache.brooklyn.api.entity.Entity; + import org.apache.brooklyn.api.entity.EntitySpec; + import org.apache.brooklyn.api.entity.EntityType; + import org.apache.brooklyn.api.location.Location; + import org.apache.brooklyn.api.location.LocationSpec; + import org.apache.brooklyn.api.objs.SpecParameter; + import org.apache.brooklyn.api.policy.Policy; + import org.apache.brooklyn.api.policy.PolicySpec; + import org.apache.brooklyn.api.sensor.Sensor; + import org.apache.brooklyn.core.entity.EntityDynamicType; + import org.apache.brooklyn.core.mgmt.BrooklynTags; + import org.apache.brooklyn.core.objs.BrooklynTypes; + import org.apache.brooklyn.rest.domain.CatalogEntitySummary; + import org.apache.brooklyn.rest.domain.CatalogItemSummary; + import org.apache.brooklyn.rest.domain.CatalogLocationSummary; + import org.apache.brooklyn.rest.domain.CatalogPolicySummary; + import org.apache.brooklyn.rest.domain.EffectorSummary; + import org.apache.brooklyn.rest.domain.EntityConfigSummary; + import org.apache.brooklyn.rest.domain.LocationConfigSummary; + import org.apache.brooklyn.rest.domain.PolicyConfigSummary; + import org.apache.brooklyn.rest.domain.SensorSummary; + import org.apache.brooklyn.rest.domain.SummaryComparators; + import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; + import org.apache.brooklyn.util.collections.MutableMap; + import org.apache.brooklyn.util.collections.MutableSet; + import org.apache.brooklyn.util.exceptions.Exceptions; + import org.apache.brooklyn.util.javalang.Reflections; + import org.slf4j.LoggerFactory; + + import com.google.common.collect.ImmutableSet; + import com.google.common.collect.Sets; + + public class CatalogTransformer { + + private static final org.slf4j.Logger log = LoggerFactory.getLogger(CatalogTransformer.class); + + public static <T extends Entity> CatalogEntitySummary catalogEntitySummary(BrooklynRestResourceUtils b, CatalogItem<T,EntitySpec<? extends T>> item) { + Set<EntityConfigSummary> config = Sets.newLinkedHashSet(); + Set<SensorSummary> sensors = Sets.newTreeSet(SummaryComparators.nameComparator()); + Set<EffectorSummary> effectors = Sets.newTreeSet(SummaryComparators.nameComparator()); + + EntitySpec<?> spec = null; + + try { + spec = (EntitySpec<?>) b.getCatalog().createSpec((CatalogItem) item); + EntityDynamicType typeMap = BrooklynTypes.getDefinedEntityType(spec.getType()); + EntityType type = typeMap.getSnapshot(); + ++ AtomicInteger paramPriorityCnt = new AtomicInteger(); + for (SpecParameter<?> input: spec.getParameters()) - config.add(EntityTransformer.entityConfigSummary(input)); ++ config.add(EntityTransformer.entityConfigSummary(input, paramPriorityCnt)); + for (Sensor<?> x: type.getSensors()) + sensors.add(SensorTransformer.sensorSummaryForCatalog(x)); + for (Effector<?> x: type.getEffectors()) + effectors.add(EffectorTransformer.effectorSummaryForCatalog(x)); + + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + + // templates with multiple entities can't have spec created in the manner above; just ignore + if (item.getCatalogItemType()==CatalogItemType.ENTITY) { + log.warn("Unable to create spec for "+item+": "+e, e); + } + if (log.isTraceEnabled()) { + log.trace("Unable to create spec for "+item+": "+e, e); + } + } + + return new CatalogEntitySummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(), + item.getJavaType(), item.getPlanYaml(), + item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), + makeTags(spec, item), config, sensors, effectors, + item.isDeprecated(), makeLinks(item)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static CatalogItemSummary catalogItemSummary(BrooklynRestResourceUtils b, CatalogItem item) { + try { + switch (item.getCatalogItemType()) { + case TEMPLATE: + case ENTITY: + return catalogEntitySummary(b, item); + case POLICY: + return catalogPolicySummary(b, item); + case LOCATION: + return catalogLocationSummary(b, item); + default: + log.warn("Unexpected catalog item type when getting self link (supplying generic item): "+item.getCatalogItemType()+" "+item); + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.warn("Invalid item in catalog when converting REST summaries (supplying generic item), at "+item+": "+e, e); + } + return new CatalogItemSummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(), + item.getJavaType(), item.getPlanYaml(), + item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), item.tags().getTags(), item.isDeprecated(), makeLinks(item)); + } + + public static CatalogPolicySummary catalogPolicySummary(BrooklynRestResourceUtils b, CatalogItem<? extends Policy,PolicySpec<?>> item) { + Set<PolicyConfigSummary> config = ImmutableSet.of(); + return new CatalogPolicySummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(), + item.getJavaType(), item.getPlanYaml(), + item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), config, + item.tags().getTags(), item.isDeprecated(), makeLinks(item)); + } + + public static CatalogLocationSummary catalogLocationSummary(BrooklynRestResourceUtils b, CatalogItem<? extends Location,LocationSpec<?>> item) { + Set<LocationConfigSummary> config = ImmutableSet.of(); + return new CatalogLocationSummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(), + item.getJavaType(), item.getPlanYaml(), + item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), config, + item.tags().getTags(), item.isDeprecated(), makeLinks(item)); + } + + protected static Map<String, URI> makeLinks(CatalogItem<?,?> item) { + return MutableMap.<String, URI>of().addIfNotNull("self", URI.create(getSelfLink(item))); + } + + protected static String getSelfLink(CatalogItem<?,?> item) { + String itemId = item.getId(); + switch (item.getCatalogItemType()) { + case TEMPLATE: + return "/v1/applications/" + itemId + "/" + item.getVersion(); + case ENTITY: + return "/v1/entities/" + itemId + "/" + item.getVersion(); + case POLICY: + return "/v1/policies/" + itemId + "/" + item.getVersion(); + case LOCATION: + return "/v1/locations/" + itemId + "/" + item.getVersion(); + default: + log.warn("Unexpected catalog item type when getting self link (not supplying self link): "+item.getCatalogItemType()+" "+item); + return null; + } + } + private static String tidyIconLink(BrooklynRestResourceUtils b, CatalogItem<?,?> item, String iconUrl) { + if (b.isUrlServerSideAndSafe(iconUrl)) + return "/v1/catalog/icon/"+item.getSymbolicName() + "/" + item.getVersion(); + return iconUrl; + } + + private static Set<Object> makeTags(EntitySpec<?> spec, CatalogItem<?, ?> item) { + // Combine tags on item with an InterfacesTag. + Set<Object> tags = MutableSet.copyOf(item.tags().getTags()); + if (spec != null) { + Class<?> type; + if (spec.getImplementation() != null) { + type = spec.getImplementation(); + } else { + type = spec.getType(); + } + if (type != null) { + tags.add(new BrooklynTags.TraitsTag(Reflections.getAllInterfaces(type))); + } + } + return tags; + } + + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java ---------------------------------------------------------------------- diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java index 0000000,803acd9..2d9f8a0 mode 000000,100644..100644 --- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java +++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java @@@ -1,0 -1,161 +1,165 @@@ + /* + * 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.rest.transform; + + import static com.google.common.collect.Iterables.transform; + + import java.lang.reflect.Field; + import java.net.URI; + import java.util.List; + import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; + + import org.apache.brooklyn.api.catalog.CatalogConfig; + import org.apache.brooklyn.api.entity.Application; + import org.apache.brooklyn.api.entity.Entity; + import org.apache.brooklyn.api.objs.SpecParameter; + import org.apache.brooklyn.config.ConfigKey; + import org.apache.brooklyn.core.config.render.RendererHints; + import org.apache.brooklyn.rest.domain.EntityConfigSummary; + import org.apache.brooklyn.rest.domain.EntitySummary; + import org.apache.brooklyn.rest.util.WebResourceUtils; + import org.apache.brooklyn.util.collections.MutableMap; + import org.apache.brooklyn.util.net.URLParamEncoder; + + import com.google.common.base.Function; + import com.google.common.collect.ImmutableMap; + import com.google.common.collect.Iterables; + import com.google.common.collect.Lists; + + /** + * @author Adam Lowe + */ + public class EntityTransformer { + + public static final Function<? super Entity, EntitySummary> FROM_ENTITY = new Function<Entity, EntitySummary>() { + @Override + public EntitySummary apply(Entity entity) { + return EntityTransformer.entitySummary(entity); + } + }; + + public static EntitySummary entitySummary(Entity entity) { + String applicationUri = "/v1/applications/" + entity.getApplicationId(); + String entityUri = applicationUri + "/entities/" + entity.getId(); + ImmutableMap.Builder<String, URI> lb = ImmutableMap.<String, URI>builder() + .put("self", URI.create(entityUri)); + if (entity.getParent()!=null) + lb.put("parent", URI.create(applicationUri+"/entities/"+entity.getParent().getId())); + String type = entity.getEntityType().getName(); + lb.put("application", URI.create(applicationUri)) + .put("children", URI.create(entityUri + "/children")) + .put("config", URI.create(entityUri + "/config")) + .put("sensors", URI.create(entityUri + "/sensors")) + .put("effectors", URI.create(entityUri + "/effectors")) + .put("policies", URI.create(entityUri + "/policies")) + .put("activities", URI.create(entityUri + "/activities")) + .put("locations", URI.create(entityUri + "/locations")) + .put("tags", URI.create(entityUri + "/tags")) + .put("expunge", URI.create(entityUri + "/expunge")) + .put("rename", URI.create(entityUri + "/name")) + .put("spec", URI.create(entityUri + "/spec")) + ; + + if (entity.getCatalogItemId() != null) { + lb.put("catalog", URI.create("/v1/catalog/entities/" + WebResourceUtils.getPathFromVersionedId(entity.getCatalogItemId()))); + } + + if (entity.getIconUrl()!=null) + lb.put("iconUrl", URI.create(entityUri + "/icon")); + + return new EntitySummary(entity.getId(), entity.getDisplayName(), type, entity.getCatalogItemId(), lb.build()); + } + + public static List<EntitySummary> entitySummaries(Iterable<? extends Entity> entities) { + return Lists.newArrayList(transform( + entities, + new Function<Entity, EntitySummary>() { + @Override + public EntitySummary apply(Entity entity) { + return EntityTransformer.entitySummary(entity); + } + })); + } + + protected static EntityConfigSummary entityConfigSummary(ConfigKey<?> config, String label, Double priority, Map<String, URI> links) { + Map<String, URI> mapOfLinks = links==null ? null : ImmutableMap.copyOf(links); + return new EntityConfigSummary(config, label, priority, mapOfLinks); + } + /** generates a representation for a given config key, + * with label inferred from annoation in the entity class, + * and links pointing to the entity and the applicaiton */ + public static EntityConfigSummary entityConfigSummary(Entity entity, ConfigKey<?> config) { + /* + * following code nearly there to get the @CatalogConfig annotation + * in the class and use that to populate a label + */ + + // EntityDynamicType typeMap = + // ((AbstractEntity)entity).getMutableEntityType(); + // // above line works if we can cast; line below won't work, but there should some way + // // to get back the handle to the spec from an entity local, which then *would* work + // EntityTypes.getDefinedEntityType(entity.getClass()); + + // String label = typeMap.getConfigKeyField(config.getName()); + String label = null; + Double priority = null; + + String applicationUri = "/v1/applications/" + entity.getApplicationId(); + String entityUri = applicationUri + "/entities/" + entity.getId(); + String selfUri = entityUri + "/config/" + URLParamEncoder.encode(config.getName()); + + MutableMap.Builder<String, URI> lb = MutableMap.<String, URI>builder() + .put("self", URI.create(selfUri)) + .put("application", URI.create(applicationUri)) + .put("entity", URI.create(entityUri)) + .put("action:json", URI.create(selfUri)); + + Iterable<RendererHints.NamedAction> hints = Iterables.filter(RendererHints.getHintsFor(config), RendererHints.NamedAction.class); + for (RendererHints.NamedAction na : hints) { + SensorTransformer.addNamedAction(lb, na, entity.getConfig(config), config, entity); + } + + return entityConfigSummary(config, label, priority, lb.build()); + } + + public static String applicationUri(Application entity) { + return "/v1/applications/" + entity.getApplicationId(); + } + + public static String entityUri(Entity entity) { + return applicationUri(entity.getApplication()) + "/entities/" + entity.getId(); + } + + public static EntityConfigSummary entityConfigSummary(ConfigKey<?> config, Field configKeyField) { + CatalogConfig catalogConfig = configKeyField!=null ? configKeyField.getAnnotation(CatalogConfig.class) : null; + String label = catalogConfig==null ? null : catalogConfig.label(); + Double priority = catalogConfig==null ? null : catalogConfig.priority(); + return entityConfigSummary(config, label, priority, null); + } + - public static EntityConfigSummary entityConfigSummary(SpecParameter<?> input) { - Double priority = input.isPinned() ? Double.valueOf(1d) : null; ++ public static EntityConfigSummary entityConfigSummary(SpecParameter<?> input, AtomicInteger paramPriorityCnt) { ++ // Increment the priority because the config container is a set. Server-side we are using an ordered set ++ // which results in correctly ordered items on the wire (as a list). Clients which use the java bindings ++ // though will push the items in an unordered set - so give them means to recover the correct order. ++ Double priority = input.isPinned() ? Double.valueOf(paramPriorityCnt.incrementAndGet()) : null; + return entityConfigSummary(input.getType(), input.getLabel(), priority, null); + } + + }
