Exemple #1
0
        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());
        }
Exemple #2
0
        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);
        }