ASP.NET Boilerplate/Zero – hide endpoints from Swagger page

by admin
Swagger

Often you don’t want to show every appservice method on your Swagger page, especially if you use the premium version of ASP.NET Boilerplate (ASP.NET Zero). This articles described how to filter the endpoints shown on your Swagger page, based on a custrom atribute and the user permissions.

This article describes how to implement the endpoint and model filter. This project is running .NET Core version 2.x.x. but it also works in newer versions.

Add helper class to your MVC project

We need several helper classes, add the SwaggerAbpAuthorizeAttributeAuthorizationFilter class in your .Mvc project.

 public class SwaggerAbpAuthorizeAttributeAuthorizationFilter : IDocumentFilter
    {
        private readonly IPermissionChecker _permissionChecker;
        private readonly IModelMetadataProvider _metadataProvider;


        public SwaggerAbpAuthorizeAttributeAuthorizationFilter(IPermissionChecker permissionChecker,
            IModelMetadataProvider metadataProvider)
        {
            _permissionChecker = permissionChecker;
            _metadataProvider = metadataProvider;
        }

        public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
        {
            var descriptions = context.ApiDescriptions;

            List<string> modelsToShow = new List<string>();

            foreach (var description in descriptions)
            {
                var route = "/" + description.RelativePath.TrimEnd('/');
                var path = swaggerDoc.Paths[route];

                var showSwaggerList = description.ActionAttributes()
                .OfType<ShowInSwaggerAttribute>().ToList();

                var isShowSwagger = showSwaggerList.Any();

                if (isShowSwagger == false)
                {
                    swaggerDoc.Paths.Remove(route);

                    if (description.ParameterDescriptions.Count > 0)
                    {
                        var modelName = description.ParameterDescriptions[0].ModelMetadata.ModelType.Name;
                        swaggerDoc.Definitions.Remove(modelName);
                    }
                }
                else
                {
                    //Get allowedmodels
                    foreach (var responseType in description.SupportedResponseTypes)
                    {
                        modelsToShow.AddRange(GetModelMetaData(responseType.ModelMetadata));
                    }

                    //Get post models
                    foreach (var requestType in description.ParameterDescriptions)
                    {
                        modelsToShow.AddRange(GetModelMetaData(requestType.ModelMetadata));
                    }
                }

                var controllerAbpAuthorizeAttributes = description.ControllerAttributes()
                    .OfType<AbpAuthorizeAttribute>().ToList();

                var actionAbpAuthorizeAttributes = description.ActionAttributes()
                    .OfType<AbpAuthorizeAttribute>().ToList();

                var authAttributes = new List<AbpAuthorizeAttribute>();

                if (actionAbpAuthorizeAttributes.Count > 0)
                    authAttributes = actionAbpAuthorizeAttributes;
                else if (controllerAbpAuthorizeAttributes.Count > 0)
                    authAttributes = controllerAbpAuthorizeAttributes;

                // check if this action should be visible
                var forbiddenDuePermissions = IsForbiddenDuePermissions(authAttributes);

                if (!forbiddenDuePermissions)
                {
                    continue; // user passed all permissions checks
                }

                // remove method or entire path (if there are no more methods in this path)
                switch (description.HttpMethod)
                {
                    case "DELETE":
                        path.Delete = null;
                        break;
                    case "GET":
                        path.Get = null;
                        break;
                    case "HEAD":
                        path.Head = null;
                        break;
                    case "OPTIONS":
                        path.Options = null;
                        break;
                    case "PATCH":
                        path.Patch = null;
                        break;
                    case "POST":
                        path.Post = null;
                        break;
                    case "PUT":
                        path.Put = null;
                        break;
                    default: throw new ArgumentOutOfRangeException("Method name not mapped to operation");
                }

                if (path.Delete == null && path.Get == null &&
                    path.Head == null && path.Options == null &&
                    path.Patch == null && path.Post == null && path.Put == null)
                    swaggerDoc.Paths.Remove(route);
            }

            ////Filter model list
            var nonMatchingKeys = swaggerDoc.Definitions.Where(t => !modelsToShow.Contains(t.Key)).ToList();

            foreach (var item in nonMatchingKeys)
            {
                swaggerDoc.Definitions.Remove(item.Key);
            }
        }

        private List<string> GetModelMetaData(ModelMetadata currentModelMetadata)
        {
            List<string> models = new List<string>();

            if (currentModelMetadata.ModelType.IsClass)
            {
                models.Add(currentModelMetadata.ModelType.Name);
            }

            if (currentModelMetadata.ModelType.IsGenericType)
            {
                foreach (var modelType in currentModelMetadata.ModelType.GenericTypeArguments)
                {
                    var modelMetadata = _metadataProvider.GetMetadataForType(modelType);
                    models.Add(modelMetadata?.ElementType?.Name);
                }
            }
            if (currentModelMetadata.IsCollectionType)
            {
                var modelType = currentModelMetadata.ModelType.GetElementType();
                var modelMetadata = _metadataProvider.GetMetadataForType(currentModelMetadata?.ModelType);

                models.Add(modelMetadata?.ElementType?.Name);
            }

            return models;
        }

        private bool IsForbiddenDuePermissions(IEnumerable<AbpAuthorizeAttribute> attributes)
        {
            var authorizeAttributes = attributes
                .Where(p => p.Permissions != null).ToList();

            var permissions = new List<string>();
            if (authorizeAttributes.Count != 0)
                foreach (var authorizeAttribute in authorizeAttributes)
                    permissions.AddRange(authorizeAttribute.Permissions.ToList());
            else
                return false;

            foreach (var permission in permissions)
            {
                var allow = _permissionChecker.IsGranted(permission);

                if (allow == true)
                    return false;
            }

            return true;
        }
    }

This class does two things. Firstly, it makes sure that only the AppService methods are shown which are decorated with the [ShowInSwagger] attribute. Also, the method removes all models which are not needed for the available endpoints.

Add a helper class to your *.Application or *.Application.Shared project

Add the ShowInSwaggerAttribute file to your *.Application (or shared) project.

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public class ShowInSwaggerAttribute : Attribute
    { 
    }

Configure the SwaggerAbpAuthorizeAttributeAuthorizationFilter as a Swagger filter

The last step is to activate our filter when the Swagger page is rendered. To do so, Edit your startup.cs in your *.Mvc project.

Old:

            services.AddSwaggerGen(options =>
            {
                options.DocumentFilter<SwaggerAbpAuthorizeAttributeAuthorizationFilter>();
                options.DocInclusionPredicate((docName, description) => true);
            });

New:

            services.AddSwaggerGen(options =>
            {
                options.DocumentFilter<SwaggerAbpAuthorizeAttributeAuthorizationFilter>();
                options.SwaggerDoc("v1", new Info { Title = "WebApp API", Version = "v1" });
                options.DocInclusionPredicate((docName, description) => true);
            });

Decorate your AppService methods with the new attributes

Add the [ShowInSwagger] to every method you want to display on the Swagger page.

Thats all, your Swagger page is now nice and clean! Without unnecessary endpoints which are never going to be used by your customers!

Related Posts

Leave a Comment