public Task <HttpResponseMessage> ExecuteAuthorizationFilterAsync( HttpActionContext actionContext, CancellationToken cancellationToken, Func <Task <HttpResponseMessage> > continuation) { // check if the http method sent needs to be checked by this filter if (!(actionContext.Request.Method == HttpMethod.Put || actionContext.Request.Method == HttpMethod.Post || actionContext.Request.Method == HttpMethod.Get)) { return(continuation()); } // Try to get the current API key context var assignedProfiles = new List <string>(); var apiKeyContext = _apiKeyContextProvider.GetApiKeyContext(); // check that the ApiKeyContext is available so that we can get the assigned profiles if (apiKeyContext != null && apiKeyContext != ApiKeyContext.Empty) { assignedProfiles = _apiKeyContextProvider.GetApiKeyContext() .Profiles.ToList(); } // declare profile content type variable because it can be retrieved from the // accept header or the content-type header string profileContentType = null; ProfileContentTypeDetails profileContentTypeDetails = null; // get singluar spelling of the controller name for comparison to the resource in the profiles string resourceCollectionName = actionContext.ControllerContext.ControllerDescriptor.ControllerName.TrimSuffix("Controller"); string resourceItemName = CompositeTermInflector.MakeSingular(resourceCollectionName); // try to get the Profile Content type and parse if successful if (TryGetProfileContentType(actionContext, out profileContentType)) { profileContentTypeDetails = profileContentType.GetContentTypeDetails(); } // If the caller has not specified a profile specific content type and there are no assigned // profiles then allow the request to be processed (there is nothing left to check) if (string.IsNullOrEmpty(profileContentType) && !assignedProfiles.Any()) { return(continuation()); } // If the caller has not specified a profile specific content type but the targeted // resource is covered by an assigned profile then we must refuse the request if (string.IsNullOrEmpty(profileContentType) && IsResourceCoveredByAssignedProfile(assignedProfiles, resourceItemName)) { if (actionContext.Request.Method == HttpMethod.Get) { return (Task.FromResult( ForbiddenHttpResponseMessage( actionContext, string.Format( "One of the following profile-specific content types is required when requesting this resource: '{0}'.", string.Join( "', '", assignedProfiles.Select( p => ProfilesContentTypeHelper.CreateContentType( resourceCollectionName, p, ContentTypeUsage.Readable))))))); } // PUT or POST return (Task.FromResult( ForbiddenHttpResponseMessage( actionContext, string.Format( "Based on the assigned profiles, one of the following profile-specific content types is required when updating this resource: '{0}'.", string.Join( "', '", assignedProfiles.Select( p => ProfilesContentTypeHelper.CreateContentType(resourceCollectionName, p, ContentTypeUsage.Writable))))))); } // if there is no profile specific content at this point there are no more checks to make so proceed with request processing if (string.IsNullOrEmpty(profileContentType)) { return(continuation()); } // If the caller is "opting in" to a profile, and the targeted resource exists in the specified profile, proceed with request processing if (!assignedProfiles.Any() && IsTheResourceInTheProfile(resourceItemName, profileContentTypeDetails.Profile)) { return(continuation()); } // If the caller is not assigned any profiles that cover the targeted resource, then proceed with request processing. if (!AnyAssignedProfilesCoverTheResource(assignedProfiles, resourceItemName)) { return(continuation()); } // Check if the resource is covered under an assigned profile that is not the profile content type sent if (!IsResourceCoveredByAnotherAssignedProfile(assignedProfiles, resourceItemName, profileContentTypeDetails.Profile)) { // create the response based on the method if (actionContext.Request.Method == HttpMethod.Get) { return(Task.FromResult( ForbiddenHttpResponseMessage( actionContext, string.Format( "One of the following profile-specific content types is required when requesting this resource: '{0}'.", string.Join( "', '", GetApplicableContentTypes( assignedProfiles, resourceCollectionName, resourceItemName, actionContext.Request.Method)))))); } if (actionContext.Request.Method == HttpMethod.Put || actionContext.Request.Method == HttpMethod.Post) { return(Task.FromResult( ForbiddenHttpResponseMessage( actionContext, string.Format( "Based on the assigned profiles, one of the following profile-specific content types is required when updating this resource: '{0}'.", string.Join( "', '", GetApplicableContentTypes( assignedProfiles, resourceCollectionName, resourceItemName, actionContext.Request.Method)))))); } } return(continuation()); }
public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { if (_duplicates.Any()) { throw new HttpResponseException( request.CreateErrorResponse( HttpStatusCode.InternalServerError, $@"Duplicate controllers have been detected. Due to ambiguity, no requests will be served until this is resolved. The following controllers have been detected as duplicates: {string.Join(", ", _duplicates)}")); } IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } string controllerName = GetRouteVariable <string>(routeData, ControllerKey); // schema section of url ie /data/v3/{schema}/{resource} ex: /data/v3/ed-fi/schools string resourceSchema = GetRouteVariable <string>(routeData, ResourceOwnerKey); if (ShouldUseDefaultSelectController(controllerName, resourceSchema)) { // If there's no controller or schema name, defer to the base class for direct route handling // Also if the controller is a composite controller we defer to base class. return(base.SelectController(request)); } string resourceSchemaNamespace = string.Empty; if (controllerName.EqualsIgnoreCase("deletes")) { resourceSchemaNamespace = "EdFi.Ods.ChangeQueries.Controllers"; } else { try { string properCaseName = _schemaNameMapProvider .GetSchemaMapByUriSegment(resourceSchema) .ProperCaseName; // for now the check is including the ignore case due to the way the schema name map provider is implemented // this should be address on the changes in phase 4 where the map provider is using dictionaries. resourceSchemaNamespace = EdFiConventions.BuildNamespace( Namespaces.Api.Controllers, properCaseName, controllerName, !EdFiConventions.ProperCaseName.EqualsIgnoreCase(properCaseName)); } // Quietly ignore any failure to find the schema name, allowing NotFound response logic below to run catch (KeyNotFoundException) {} } // Verify org section matches a known resource owner derived from any assembly implementing ISchemaNameMap if (string.IsNullOrEmpty(resourceSchemaNamespace)) { throw new HttpResponseException( request.CreateErrorResponse( HttpStatusCode.NotFound, "Resource schema value provided in uri does not match any known values.")); } ProfileContentTypeDetails profileContentTypeDetails = null; // http method type determines where the profile specific content type will be sent. if (request.Method == HttpMethod.Get) { profileContentTypeDetails = (from a in request.Headers.Accept where a.MediaType.StartsWith("application/vnd.ed-fi.") let details = a.MediaType.GetContentTypeDetails() select details) .SingleOrDefault(); } else if (request.Method == HttpMethod.Put || request.Method == HttpMethod.Post) { if (request.Content != null) { var contentType = request.Content.Headers.ContentType; // check that there was a content type on the request if (contentType != null) { // check if the content type is a profile specific content type if (contentType.MediaType.StartsWith("application/vnd.ed-fi.")) { // parse the profile specific content type string into the object. profileContentTypeDetails = contentType.MediaType.GetContentTypeDetails(); // Was a profile-specific content type for the controller/resource found and was it parseable? // if it was not parsable but started with "application/vnd.ed-fi." then throw an error if (profileContentTypeDetails == null) { throw new HttpResponseException( BadRequestHttpResponseMessage( request, "Content type not valid on this resource.")); } } } } } HttpControllerDescriptor controllerDescriptor; string key; bool profileControllerNotFound = false; // Was a profile-specific content type for the controller/resource found? if (profileContentTypeDetails != null) { // Ensure that the content type resource matchs requested resource if (!profileContentTypeDetails.Resource .EqualsIgnoreCase(CompositeTermInflector.MakeSingular(controllerName))) { throw new HttpResponseException( BadRequestHttpResponseMessage( request, "The resource is not accessible through the profile specified by the content type.")); } // Ensure that if the method is get that the profile specific content type sent readable if (request.Method == HttpMethod.Get && profileContentTypeDetails.Usage != ContentTypeUsage.Readable) { throw new HttpResponseException( BadRequestHttpResponseMessage( request, "The resource is not accessible through the profile specified by the content type.")); } // Ensure that if the http method is PUT or POST that the profile specific type sent was writable if ((request.Method == HttpMethod.Put || request.Method == HttpMethod.Post) && profileContentTypeDetails.Usage != ContentTypeUsage.Writable) { throw new HttpResponseException( BadRequestHttpResponseMessage( request, "The resource is not accessible through the profile specified by the content type.")); } // Use the profile name as the namespace for controller matching string profileName = profileContentTypeDetails.Profile.Replace('-', '_'); // + "_" + profileContentTypeDetails.Usage; // Find a matching controller. // ex : EdFi.Ods.Api.Services.Controllers.AcademicHonorCategoryTypes.Academic_Week_Readable_Excludes_Optional_References.AcademicHonorCategoryTypes key = string.Format( CultureInfo.InvariantCulture, "{0}.{1}.{2}", resourceSchemaNamespace, profileName, controllerName); if (_controllers.Value.TryGetValue(key, out controllerDescriptor)) { return(controllerDescriptor); } profileControllerNotFound = true; } // Find a matching controller. // ex : EdFi.Ods.Api.Services.Controllers.AcademicHonorCategoryTypes.AcademicHonorCategoryTypes key = string.Format( CultureInfo.InvariantCulture, "{0}.{1}", resourceSchemaNamespace, controllerName); if (_controllers.Value.TryGetValue(key, out controllerDescriptor)) { // If there is a controller, just not with the content type specified, it's a bad request if (profileControllerNotFound) { // If the profile does not exist in this installation of the api throw an error dependent on the http method if (!ProfileExists(profileContentTypeDetails.Profile)) { if (request.Method == HttpMethod.Get) { throw new HttpResponseException(NotAcceptableHttpResponseMessage(request)); } if (request.Method == HttpMethod.Put || request.Method == HttpMethod.Post) { throw new HttpResponseException(UnsupportedMediaTypeHttpResponseMessage(request)); } } // The profile exists but the resource doesnt exist under it throw new HttpResponseException( BadRequestHttpResponseMessage( request, string.Format( "The '{0}' resource is not accessible through the '{1}' profile specified by the content type.", CompositeTermInflector.MakeSingular(controllerName), profileContentTypeDetails.Profile))); } return(controllerDescriptor); } throw new HttpResponseException(HttpStatusCode.NotFound); }