001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 *******************************************************************************/ 016package org.mitre.uma.web; 017 018 019import static org.mitre.oauth2.web.AuthenticationUtilities.ensureOAuthScope; 020import static org.mitre.util.JsonUtils.getAsLong; 021import static org.mitre.util.JsonUtils.getAsString; 022import static org.mitre.util.JsonUtils.getAsStringSet; 023 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.Set; 028 029import org.mitre.oauth2.model.SystemScope; 030import org.mitre.oauth2.service.SystemScopeService; 031import org.mitre.openid.connect.config.ConfigurationPropertiesBean; 032import org.mitre.openid.connect.view.HttpCodeView; 033import org.mitre.openid.connect.view.JsonEntityView; 034import org.mitre.openid.connect.view.JsonErrorView; 035import org.mitre.uma.model.ResourceSet; 036import org.mitre.uma.service.ResourceSetService; 037import org.mitre.uma.view.ResourceSetEntityAbbreviatedView; 038import org.mitre.uma.view.ResourceSetEntityView; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041import org.springframework.beans.factory.annotation.Autowired; 042import org.springframework.http.HttpStatus; 043import org.springframework.security.access.prepost.PreAuthorize; 044import org.springframework.security.core.Authentication; 045import org.springframework.security.oauth2.provider.OAuth2Authentication; 046import org.springframework.stereotype.Controller; 047import org.springframework.ui.Model; 048import org.springframework.util.MimeTypeUtils; 049import org.springframework.web.bind.annotation.PathVariable; 050import org.springframework.web.bind.annotation.RequestBody; 051import org.springframework.web.bind.annotation.RequestMapping; 052import org.springframework.web.bind.annotation.RequestMethod; 053 054import com.google.common.base.Strings; 055import com.google.gson.JsonElement; 056import com.google.gson.JsonObject; 057import com.google.gson.JsonParseException; 058import com.google.gson.JsonParser; 059 060@Controller 061@RequestMapping("/" + ResourceSetRegistrationEndpoint.URL) 062@PreAuthorize("hasRole('ROLE_USER')") 063public class ResourceSetRegistrationEndpoint { 064 065 private static final Logger logger = LoggerFactory.getLogger(ResourceSetRegistrationEndpoint.class); 066 067 public static final String DISCOVERY_URL = "resource_set"; 068 public static final String URL = DISCOVERY_URL + "/resource_set"; 069 070 @Autowired 071 private ResourceSetService resourceSetService; 072 073 @Autowired 074 private ConfigurationPropertiesBean config; 075 076 @Autowired 077 private SystemScopeService scopeService; 078 079 private JsonParser parser = new JsonParser(); 080 081 @RequestMapping(method = RequestMethod.POST, produces = MimeTypeUtils.APPLICATION_JSON_VALUE, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE) 082 public String createResourceSet(@RequestBody String jsonString, Model m, Authentication auth) { 083 ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); 084 085 ResourceSet rs = parseResourceSet(jsonString); 086 087 if (rs == null) { // there was no resource set in the body 088 logger.warn("Resource set registration missing body."); 089 090 m.addAttribute("code", HttpStatus.BAD_REQUEST); 091 m.addAttribute("error_description", "Resource request was missing body."); 092 return JsonErrorView.VIEWNAME; 093 } 094 095 if (auth instanceof OAuth2Authentication) { 096 // if it's an OAuth mediated call, it's on behalf of a client, so store that 097 OAuth2Authentication o2a = (OAuth2Authentication) auth; 098 rs.setClientId(o2a.getOAuth2Request().getClientId()); 099 rs.setOwner(auth.getName()); // the username is going to be in the auth object 100 } else { 101 // this one shouldn't be called if it's not OAuth 102 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 103 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "This call must be made with an OAuth token"); 104 return JsonErrorView.VIEWNAME; 105 } 106 107 rs = validateScopes(rs); 108 109 if (Strings.isNullOrEmpty(rs.getName()) // there was no name (required) 110 || rs.getScopes() == null // there were no scopes (required) 111 ) { 112 113 logger.warn("Resource set registration missing one or more required fields."); 114 115 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 116 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Resource request was missing one or more required fields."); 117 return JsonErrorView.VIEWNAME; 118 } 119 120 ResourceSet saved = resourceSetService.saveNew(rs); 121 122 m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); 123 m.addAttribute(JsonEntityView.ENTITY, saved); 124 m.addAttribute(ResourceSetEntityAbbreviatedView.LOCATION, config.getIssuer() + URL + "/" + saved.getId()); 125 126 return ResourceSetEntityAbbreviatedView.VIEWNAME; 127 128 } 129 130 @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) 131 public String readResourceSet(@PathVariable ("id") Long id, Model m, Authentication auth) { 132 ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); 133 134 ResourceSet rs = resourceSetService.getById(id); 135 136 if (rs == null) { 137 m.addAttribute("code", HttpStatus.NOT_FOUND); 138 m.addAttribute("error", "not_found"); 139 return JsonErrorView.VIEWNAME; 140 } else { 141 142 rs = validateScopes(rs); 143 144 if (!auth.getName().equals(rs.getOwner())) { 145 146 logger.warn("Unauthorized resource set request from wrong user; expected " + rs.getOwner() + " got " + auth.getName()); 147 148 // it wasn't issued to this user 149 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); 150 return JsonErrorView.VIEWNAME; 151 } else { 152 m.addAttribute(JsonEntityView.ENTITY, rs); 153 return ResourceSetEntityView.VIEWNAME; 154 } 155 156 } 157 158 } 159 160 @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) 161 public String updateResourceSet(@PathVariable ("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) { 162 ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); 163 164 ResourceSet newRs = parseResourceSet(jsonString); 165 166 if (newRs == null // there was no resource set in the body 167 || Strings.isNullOrEmpty(newRs.getName()) // there was no name (required) 168 || newRs.getScopes() == null // there were no scopes (required) 169 || newRs.getId() == null || !newRs.getId().equals(id) // the IDs didn't match 170 ) { 171 172 logger.warn("Resource set registration missing one or more required fields."); 173 174 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 175 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Resource request was missing one or more required fields."); 176 return JsonErrorView.VIEWNAME; 177 } 178 179 ResourceSet rs = resourceSetService.getById(id); 180 181 if (rs == null) { 182 m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); 183 m.addAttribute(JsonErrorView.ERROR, "not_found"); 184 return JsonErrorView.VIEWNAME; 185 } else { 186 if (!auth.getName().equals(rs.getOwner())) { 187 188 logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); 189 190 // it wasn't issued to this user 191 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); 192 return JsonErrorView.VIEWNAME; 193 } else { 194 195 ResourceSet saved = resourceSetService.update(rs, newRs); 196 197 m.addAttribute(JsonEntityView.ENTITY, saved); 198 m.addAttribute(ResourceSetEntityAbbreviatedView.LOCATION, config.getIssuer() + URL + "/" + rs.getId()); 199 return ResourceSetEntityAbbreviatedView.VIEWNAME; 200 } 201 202 } 203 } 204 205 @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) 206 public String deleteResourceSet(@PathVariable ("id") Long id, Model m, Authentication auth) { 207 ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); 208 209 ResourceSet rs = resourceSetService.getById(id); 210 211 if (rs == null) { 212 m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); 213 m.addAttribute(JsonErrorView.ERROR, "not_found"); 214 return JsonErrorView.VIEWNAME; 215 } else { 216 if (!auth.getName().equals(rs.getOwner())) { 217 218 logger.warn("Unauthorized resource set request from bad user; expected " + rs.getOwner() + " got " + auth.getName()); 219 220 // it wasn't issued to this user 221 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); 222 return JsonErrorView.VIEWNAME; 223 } else if (auth instanceof OAuth2Authentication && 224 !((OAuth2Authentication)auth).getOAuth2Request().getClientId().equals(rs.getClientId())){ 225 226 logger.warn("Unauthorized resource set request from bad client; expected " + rs.getClientId() + " got " + ((OAuth2Authentication)auth).getOAuth2Request().getClientId()); 227 228 // it wasn't issued to this client 229 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); 230 return JsonErrorView.VIEWNAME; 231 } else { 232 233 // user and client matched 234 resourceSetService.remove(rs); 235 236 m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); 237 return HttpCodeView.VIEWNAME; 238 } 239 240 } 241 } 242 243 @RequestMapping(method = RequestMethod.GET, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) 244 public String listResourceSets(Model m, Authentication auth) { 245 ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); 246 247 String owner = auth.getName(); 248 249 Collection<ResourceSet> resourceSets = Collections.emptySet(); 250 if (auth instanceof OAuth2Authentication) { 251 // if it's an OAuth mediated call, it's on behalf of a client, so look that up too 252 OAuth2Authentication o2a = (OAuth2Authentication) auth; 253 resourceSets = resourceSetService.getAllForOwnerAndClient(owner, o2a.getOAuth2Request().getClientId()); 254 } else { 255 // otherwise get everything for the current user 256 resourceSets = resourceSetService.getAllForOwner(owner); 257 } 258 259 // build the entity here and send to the display 260 261 Set<String> ids = new HashSet<>(); 262 for (ResourceSet resourceSet : resourceSets) { 263 ids.add(resourceSet.getId().toString()); // add them all as strings so that gson renders them properly 264 } 265 266 m.addAttribute(JsonEntityView.ENTITY, ids); 267 return JsonEntityView.VIEWNAME; 268 } 269 270 private ResourceSet parseResourceSet(String jsonString) { 271 272 try { 273 JsonElement el = parser.parse(jsonString); 274 275 if (el.isJsonObject()) { 276 JsonObject o = el.getAsJsonObject(); 277 278 ResourceSet rs = new ResourceSet(); 279 rs.setId(getAsLong(o, "_id")); 280 rs.setName(getAsString(o, "name")); 281 rs.setIconUri(getAsString(o, "icon_uri")); 282 rs.setType(getAsString(o, "type")); 283 rs.setScopes(getAsStringSet(o, "scopes")); 284 rs.setUri(getAsString(o, "uri")); 285 286 return rs; 287 288 } 289 290 return null; 291 292 } catch (JsonParseException e) { 293 return null; 294 } 295 296 } 297 298 299 /** 300 * 301 * Make sure the resource set doesn't have any restricted or reserved scopes. 302 * 303 * @param rs 304 */ 305 private ResourceSet validateScopes(ResourceSet rs) { 306 // scopes that the client is asking for 307 Set<SystemScope> requestedScopes = scopeService.fromStrings(rs.getScopes()); 308 309 // the scopes that the resource set can have must be a subset of the dynamically allowed scopes 310 Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); 311 312 rs.setScopes(scopeService.toStrings(allowedScopes)); 313 314 return rs; 315 } 316 317}