How to display required user roles (access control information) in Swagger UI for Spring's endpoints?

匆匆过客 提交于 2021-02-07 06:26:05

问题


I have a rest api made in Spring and am using Swagger for documentation. Recently a token based authentication was implemented. In the token, there are (internal) user's roles (authorities). Each controller is annotated with a couple of Swagger annotations and a @PreAuthorize(some roles..) like so:

@ApiOperation("Delete user")
@ApiResponses(value = {
        @ApiResponse(code = 404, message = "User not found", response = ErrorResponse.class)
})
@PreAuthorize("hasAuthority('ADMIN')")
@DeleteMapping(value = "/{id}")
public ResponseEntity<?> deleteUser(@PathVariable UUID id) {
    userService.delete(id);
    return ResponseEntity.ok().build();
}

Now, I have no idea how I can display in my swagger-ui those roles, so each endpoint has information, what user role is required to access it. I have browsed the internet and found only some really vague information, most of it not concerning Spring at all.

Note: I tried using notes: @ApiOperation(value = "Delete user", notes = "Required roles: ADMIN, USER") to display custom text, but this doesn't seem like a right way to go.


回答1:


Inspired by solution in the artical blog.codecentric.de/2018/11/springfoxswaggerextension
In our case, we have controller decorated by @Secured annotation @Secured("ROLE_Admin")

We add the OperationNotesResourcesReader component to add evaluation for @ApiRoleAccessNotes.
Here the complete solution.

The Controller

  @ApiRoleAccessNotes
  @Secured("ROLE_Admin")
  @PostMapping
  public ResponseEntity<ResponseRestDTO> createVersion(@RequestBody DraftVersionCreateDTO dto) {
    return ResponseEntity.ok()
        .body(ResponseRestDTO.builder().data(versionService.createVersion(dto))
            .message(messageService.get("version.create.ok")).build());
  }

The new annotation

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRoleAccessNotes {
}

Then the OperationNotesResourcesReader

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Component;
import com.google.common.base.Optional;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spring.web.DescriptionResolver;
import springfox.documentation.swagger.common.SwaggerPluginSupport;

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
public class OperationNotesResourcesReader
    implements springfox.documentation.spi.service.OperationBuilderPlugin {
  private final DescriptionResolver descriptions;

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

  @Autowired
  public OperationNotesResourcesReader(DescriptionResolver descriptions) {
    this.descriptions = descriptions;
  }

  @Override
  public void apply(OperationContext context) {
    try {
      Optional<ApiRoleAccessNotes> methodAnnotation =
          context.findAnnotation(ApiRoleAccessNotes.class);
      if (!methodAnnotation.isPresent()) {
        // the REST Resource does not have the @ApiRoleAccessNotes annotation -> ignore
        return;
      }

      String apiRoleAccessNoteText;
      // get @Secured annotation
      Optional<Secured> securedAnnotation = context.findAnnotation(Secured.class);
      if (!securedAnnotation.isPresent()) {
        apiRoleAccessNoteText = "Accessible by all user roles";
      } else {
        apiRoleAccessNoteText = "Accessible by users having one of the following roles: ";
        Secured secure = securedAnnotation.get();
        for (String role : secure.value()) {
          // add the roles to the notes. Use Markdown notation to create a list
          apiRoleAccessNoteText = apiRoleAccessNoteText + "\n * " + String.join("\n * ", role);
        }
      }
      // add the note text to the Swagger UI
      context.operationBuilder().notes(descriptions.resolve(apiRoleAccessNoteText));
    } catch (Exception e) {
      logger.error("Error when creating swagger documentation for security roles: " + e);
    }
  }

  @Override
  public boolean supports(DocumentationType delimiter) {
    return SwaggerPluginSupport.pluginDoesApply(delimiter);
  }
}



回答2:


If you need to display roles for all the endpoints, then no need to create above Custome Annotations and all.

Just create "OperationNotesResourcesReader" class and thats all.

Sample Controller class :

@PostMapping("/api/user")
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize(value = "hasAuthority('" + ROOT_ORG_ADMIN + "')")
@ApiOperation("Create new Organisation")
@Authorization(value = ROOT_ORG_ADMIN)
public ResponseEntity<APIResponse<Data>> create(@Valid @RequestBody OrganisationCreateRequest createRequest, BindingResult bindingResult, HttpServletRequest request) {

    RequestContext requestContext = wiseconnectSecurityContextProvider.getRequestContext();
    LOGGER.debug("Create Organisation request received");
    if (bindingResult.hasErrors()) {
        LOGGER.error(INPUT_VALIDATION_ERROR);
        throw new ControllerException(bindingResult, INPUT_VALIDATION_ERROR);
    }

    OrganisationView organisationView = organisationService.createOrganisation(createRequest, requestContext);

    LOGGER.debug("Created Organisation {} successfully", organisationView.getId());
    APIResponse<Data> apiResponse = new APIResponse<>(CREATE_SUCCESS, "Organisation Created", new Data(organisationView.getId()));
    return new ResponseEntity<>(apiResponse, HttpStatus.CREATED);
}

Then create "OperationNotesResourcesReader" class as follows

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
@Slf4j // or LOGGER
public class OperationNotesResourcesReader implements springfox.documentation.spi.service.OperationBuilderPlugin {

private final DescriptionResolver descriptions;

@Autowired
public OperationNotesResourcesReader(DescriptionResolver descriptions) {
    this.descriptions = descriptions;
}

@Override
public void apply(OperationContext context) {
    try {

        String apiRoleAccessNoteText = "Endpoint Access Privilieges & Ruels : Nil";
        Optional<PreAuthorize> preAuthorizeAnnotation = context.findAnnotation(PreAuthorize.class);
        if (preAuthorizeAnnotation.isPresent()) {
            apiRoleAccessNoteText = "Endpoint Access Privilieges & Ruels : " + preAuthorizeAnnotation.get().value();
        }
        // add the note text to the Swagger UI
        context.operationBuilder().notes(descriptions.resolve(apiRoleAccessNoteText));
    } catch (Exception e) {
        LOGGER.error("Error when creating swagger documentation for security roles: " + e);
    }
}

@Override
public boolean supports(DocumentationType delimiter) {
    return SwaggerPluginSupport.pluginDoesApply(delimiter);
}

}




回答3:


If you need to keep the existing notes added through annotation and add @Authorisation value together, then do the following..

@ApiOperation(value = "Create new Organisation", notes = "Notes added through annotation")
@Authorization(value = ROOT_ORG_ADMIN)

create "OperationNotesResourcesReader" class as follows

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1)
@Slf4j
public class OperationNotesResourcesReader implements OperationBuilderPlugin {

private final DescriptionResolver descriptions;

@Autowired
public OperationNotesResourcesReader(DescriptionResolver descriptions) {
    this.descriptions = descriptions;
}

@Override
public void apply(OperationContext context) {
    try {
        Optional<ApiOperation> annotation = context.findAnnotation(ApiOperation.class);
        String notes = (annotation.isPresent() && StringUtils.hasText(annotation.get().notes())) ? (annotation.get().notes()) : "";
        String apiRoleAccessNoteText = "<b>Access Privilieges & Ruels : </b> Nil";
        Optional<PreAuthorize> preAuthorizeAnnotation = context.findAnnotation(PreAuthorize.class);
        if (preAuthorizeAnnotation.isPresent()) {
            apiRoleAccessNoteText = "<b>Access Privilieges & Ruels : </b>" + preAuthorizeAnnotation.get().value();
        }
        notes = apiRoleAccessNoteText + " \n\n " + notes;
        // add the note text to the Swagger UI
        context.operationBuilder().notes(descriptions.resolve(notes));
    } catch (Exception e) {
        LOGGER.error("Error when creating swagger documentation for security roles: " + e);
    }
}

@Override
public boolean supports(DocumentationType delimiter) {
    return SwaggerPluginSupport.pluginDoesApply(delimiter);
}
}

Note the following changes

@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1)

notes = apiRoleAccessNoteText + " \n\n " + notes;
        // add the note text to the Swagger UI
        context.operationBuilder().notes(descriptions.resolve(notes));



回答4:


This is my personal version started from @Unni version, I just played a little bit with html markup.

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1)
public class OperationNotesResourcesReader implements OperationBuilderPlugin {
    private static final Logger LOG = Logger.getLogger(OperationNotesResourcesReader.class.getName());

    private final DescriptionResolver descriptions;

    @Autowired
    public OperationNotesResourcesReader(DescriptionResolver descriptions) {
        this.descriptions = descriptions;
    }

    @Override
    public void apply(OperationContext context) {
        try {
            StringBuilder sb = new StringBuilder();

            // Check authorization
            Optional<PreAuthorize> preAuthorizeAnnotation = context.findAnnotation(PreAuthorize.class);
            sb.append("<b>Access Privileges & Rules</b>: ");
            if (preAuthorizeAnnotation.isPresent()) {
                sb.append("<em>" + preAuthorizeAnnotation.get().value() + "</em>");
            } else {
                sb.append("<em>NOT_FOUND</em>");
            }

            // Check notes
            Optional<ApiOperation> annotation = context.findAnnotation(ApiOperation.class);
            if (annotation.isPresent() && StringUtils.hasText(annotation.get().notes())) {
                sb.append("<br /><br />");
                sb.append(annotation.get().notes());
            }

            // Add the note text to the Swagger UI
            context.operationBuilder().notes(descriptions.resolve(sb.toString()));
        } catch (Exception e) {
            LOG.log(Level.SEVERE, "Error when creating swagger documentation for security roles: ", e);
        }
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return SwaggerPluginSupport.pluginDoesApply(delimiter);
    }
}

This is the final rendering:



来源:https://stackoverflow.com/questions/43524355/how-to-display-required-user-roles-access-control-information-in-swagger-ui-fo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!