ResourceSetRegistrationEndpoint.java

/*******************************************************************************
 * Copyright 2017 The MIT Internet Trust Consortium
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package org.mitre.uma.web;


import static org.mitre.oauth2.web.AuthenticationUtilities.ensureOAuthScope;
import static org.mitre.util.JsonUtils.getAsLong;
import static org.mitre.util.JsonUtils.getAsString;
import static org.mitre.util.JsonUtils.getAsStringSet;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.mitre.oauth2.model.SystemScope;
import org.mitre.oauth2.service.SystemScopeService;
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
import org.mitre.openid.connect.view.HttpCodeView;
import org.mitre.openid.connect.view.JsonEntityView;
import org.mitre.openid.connect.view.JsonErrorView;
import org.mitre.uma.model.ResourceSet;
import org.mitre.uma.service.ResourceSetService;
import org.mitre.uma.view.ResourceSetEntityAbbreviatedView;
import org.mitre.uma.view.ResourceSetEntityView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.google.common.base.Strings;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;

@Controller
@RequestMapping("/" + ResourceSetRegistrationEndpoint.URL)
@PreAuthorize("hasRole('ROLE_USER')")
public class ResourceSetRegistrationEndpoint {

	private static final Logger logger = LoggerFactory.getLogger(ResourceSetRegistrationEndpoint.class);

	public static final String DISCOVERY_URL = "resource_set";
	public static final String URL = DISCOVERY_URL + "/resource_set";

	@Autowired
	private ResourceSetService resourceSetService;

	@Autowired
	private ConfigurationPropertiesBean config;

	@Autowired
	private SystemScopeService scopeService;

	private JsonParser parser = new JsonParser();

	@RequestMapping(method = RequestMethod.POST, produces = MimeTypeUtils.APPLICATION_JSON_VALUE, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE)
	public String createResourceSet(@RequestBody String jsonString, Model m, Authentication auth) {
		ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE);

		ResourceSet rs = parseResourceSet(jsonString);

		if (rs == null) { // there was no resource set in the body
			logger.warn("Resource set registration missing body.");

			m.addAttribute("code", HttpStatus.BAD_REQUEST);
			m.addAttribute("error_description", "Resource request was missing body.");
			return JsonErrorView.VIEWNAME;
		}

		if (auth instanceof OAuth2Authentication) {
			// if it's an OAuth mediated call, it's on behalf of a client, so store that
			OAuth2Authentication o2a = (OAuth2Authentication) auth;
			rs.setClientId(o2a.getOAuth2Request().getClientId());
			rs.setOwner(auth.getName()); // the username is going to be in the auth object
		} else {
			// this one shouldn't be called if it's not OAuth
			m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
			m.addAttribute(JsonErrorView.ERROR_MESSAGE, "This call must be made with an OAuth token");
			return JsonErrorView.VIEWNAME;
		}

		rs = validateScopes(rs);

		if (Strings.isNullOrEmpty(rs.getName()) // there was no name (required)
				|| rs.getScopes() == null // there were no scopes (required)
				) {

			logger.warn("Resource set registration missing one or more required fields.");

			m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
			m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Resource request was missing one or more required fields.");
			return JsonErrorView.VIEWNAME;
		}

		ResourceSet saved = resourceSetService.saveNew(rs);

		m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED);
		m.addAttribute(JsonEntityView.ENTITY, saved);
		m.addAttribute(ResourceSetEntityAbbreviatedView.LOCATION, config.getIssuer() + URL + "/" + saved.getId());

		return ResourceSetEntityAbbreviatedView.VIEWNAME;

	}

	@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
	public String readResourceSet(@PathVariable ("id") Long id, Model m, Authentication auth) {
		ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE);

		ResourceSet rs = resourceSetService.getById(id);

		if (rs == null) {
			m.addAttribute("code", HttpStatus.NOT_FOUND);
			m.addAttribute("error", "not_found");
			return JsonErrorView.VIEWNAME;
		} else {

			rs = validateScopes(rs);

			if (!auth.getName().equals(rs.getOwner())) {

				logger.warn("Unauthorized resource set request from wrong user; expected " + rs.getOwner() + " got " + auth.getName());

				// it wasn't issued to this user
				m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN);
				return JsonErrorView.VIEWNAME;
			} else {
				m.addAttribute(JsonEntityView.ENTITY, rs);
				return ResourceSetEntityView.VIEWNAME;
			}

		}

	}

	@RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
	public String updateResourceSet(@PathVariable ("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) {
		ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE);

		ResourceSet newRs = parseResourceSet(jsonString);

		if (newRs == null // there was no resource set in the body
				|| Strings.isNullOrEmpty(newRs.getName()) // there was no name (required)
				|| newRs.getScopes() == null // there were no scopes (required)
				|| newRs.getId() == null || !newRs.getId().equals(id) // the IDs didn't match
				) {

			logger.warn("Resource set registration missing one or more required fields.");

			m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
			m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Resource request was missing one or more required fields.");
			return JsonErrorView.VIEWNAME;
		}

		ResourceSet rs = resourceSetService.getById(id);

		if (rs == null) {
			m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
			m.addAttribute(JsonErrorView.ERROR, "not_found");
			return JsonErrorView.VIEWNAME;
		} else {
			if (!auth.getName().equals(rs.getOwner())) {

				logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName());

				// it wasn't issued to this user
				m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN);
				return JsonErrorView.VIEWNAME;
			} else {

				ResourceSet saved = resourceSetService.update(rs, newRs);

				m.addAttribute(JsonEntityView.ENTITY, saved);
				m.addAttribute(ResourceSetEntityAbbreviatedView.LOCATION, config.getIssuer() + URL + "/" + rs.getId());
				return ResourceSetEntityAbbreviatedView.VIEWNAME;
			}

		}
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
	public String deleteResourceSet(@PathVariable ("id") Long id, Model m, Authentication auth) {
		ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE);

		ResourceSet rs = resourceSetService.getById(id);

		if (rs == null) {
			m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
			m.addAttribute(JsonErrorView.ERROR, "not_found");
			return JsonErrorView.VIEWNAME;
		} else {
			if (!auth.getName().equals(rs.getOwner())) {

				logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName());

				// it wasn't issued to this user
				m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN);
				return JsonErrorView.VIEWNAME;
			} else if (auth instanceof OAuth2Authentication &&
					!((OAuth2Authentication)auth).getOAuth2Request().getClientId().equals(rs.getClientId())){

				logger.warn("Unauthorized resource set request from bad client; expected " + rs.getClientId() + " got " + ((OAuth2Authentication)auth).getOAuth2Request().getClientId());

				// it wasn't issued to this client
				m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN);
				return JsonErrorView.VIEWNAME;
			} else {

				// user and client matched
				resourceSetService.remove(rs);

				m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT);
				return HttpCodeView.VIEWNAME;
			}

		}
	}

	@RequestMapping(method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
	public String listResourceSets(Model m, Authentication auth) {
		ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE);

		String owner = auth.getName();

		Collection<ResourceSet> resourceSets = Collections.emptySet();
		if (auth instanceof OAuth2Authentication) {
			// if it's an OAuth mediated call, it's on behalf of a client, so look that up too
			OAuth2Authentication o2a = (OAuth2Authentication) auth;
			resourceSets = resourceSetService.getAllForOwnerAndClient(owner, o2a.getOAuth2Request().getClientId());
		} else {
			// otherwise get everything for the current user
			resourceSets = resourceSetService.getAllForOwner(owner);
		}

		// build the entity here and send to the display

		Set<String> ids = new HashSet<>();
		for (ResourceSet resourceSet : resourceSets) {
			ids.add(resourceSet.getId().toString()); // add them all as strings so that gson renders them properly
		}

		m.addAttribute(JsonEntityView.ENTITY, ids);
		return JsonEntityView.VIEWNAME;
	}

	private ResourceSet parseResourceSet(String jsonString) {

		try {
			JsonElement el = parser.parse(jsonString);

			if (el.isJsonObject()) {
				JsonObject o = el.getAsJsonObject();

				ResourceSet rs = new ResourceSet();
				rs.setId(getAsLong(o, "_id"));
				rs.setName(getAsString(o, "name"));
				rs.setIconUri(getAsString(o, "icon_uri"));
				rs.setType(getAsString(o, "type"));
				rs.setScopes(getAsStringSet(o, "scopes"));
				rs.setUri(getAsString(o, "uri"));

				return rs;

			}

			return null;

		} catch (JsonParseException e) {
			return null;
		}

	}


	/**
	 *
	 * Make sure the resource set doesn't have any restricted or reserved scopes.
	 *
	 * @param rs
	 */
	private ResourceSet validateScopes(ResourceSet rs) {
		// scopes that the client is asking for
		Set<SystemScope> requestedScopes = scopeService.fromStrings(rs.getScopes());

		// the scopes that the resource set can have must be a subset of the dynamically allowed scopes
		Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes);

		rs.setScopes(scopeService.toStrings(allowedScopes));

		return rs;
	}

}