How to trim Swagger docs based on current User Role in Java Spring?

烂漫一生 提交于 2020-06-17 09:37:10

问题


I'm developing application using Spring Boot, and I'm using Swagger to auto-generate API docs and also I use swagger-ui.html to interact with those APIs.

I have Spring Security enabled too, and I have Users with different roles. Different REST APIs are available to different roles.

Question: how do I configure Swagger to respect Spring's @Secured annotation and trim operations displayed by swagger-ui.html so that only operations available to current user are available?

I.e. imagine following controller

@RestController
@Secured(ROLE_USER)
public void SomeRestController {
  @GetMapping
  @Secured(ROLE_USER_TOP_MANAGER)
  public String getInfoForTopManager() { /*...*/ }

  @GetMapping
  @Secured(ROLE_USER_MIDDLE_MANAGER)
  public String getInfoForMiddleManager() { /*...*/ }

  @GetMapping
  public String getInfoForAnyUser() { /*...*/ }
}

Swagger will show both operations getInfoForTopManager and getInfoForMiddleManager regardless of current user role. In case currently authenticated user role is ROLE_USER_MIDDLE_MANAGER, I want only getInfoForMiddleManager and getInfoForAnyUser operations to be available in the Swagger.


回答1:


Ok, I think found good solution to that question. Solution consists of 2 parts:

  1. Extend controllers scanning logic through OperationBuilderPlugin to retain roles in the Swagger's vendor extensions
  2. Override ServiceModelToSwagger2MapperImpl bean to filter out actions based on current security context

In your project this might look a bit different (i.e. most likely you don't have thing like securityContextResolver), but I believe you'll get the gist of this solution from following code:

Part 1: Extend controllers scanning logic to retain roles in the Swagger's vendor extensions

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1000)
public class OperationBuilderPluginSecuredAware implements OperationBuilderPlugin {
    @Override
    public void apply(OperationContext context) {
        Set<String> roles = new HashSet<>();
        Secured controllerAnnotation = context.findControllerAnnotation(Secured.class).orNull();
        if (controllerAnnotation != null) {
            roles.addAll(List.of(controllerAnnotation.value()));
        }

        Secured methodAnnotation = context.findAnnotation(Secured.class).orNull();
        if (methodAnnotation != null) {
            roles.addAll(List.of(methodAnnotation.value()));
        }

        if (!roles.isEmpty()) {
            context.operationBuilder().extensions(List.of(new TrimToRoles(roles.toArray(new String[0]))));
        }
    }

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

Part 2: Filter out actions based on current security context

@Primary
@Component
public class ServiceModelToSwagger2MapperImplEx extends ServiceModelToSwagger2MapperImpl {
    @Autowired
    private SecurityContextResolver<User> securityContextResolver;

    @Override
    protected io.swagger.models.Operation mapOperation(Operation from) {
        if (from == null) {
            return null;
        }
        if (!isPermittedForCurrentUser(findTrimToRolesExtension(from.getVendorExtensions()))) {
            return null;
        }
        return super.mapOperation(from);
    }

    private boolean isPermittedForCurrentUser(TrimToRoles trimToRoles) {
        if (trimToRoles == null) {
            return true;
        }
        if (securityContextResolver.hasAnyRole(trimToRoles.getValue())) {
            return true;
        }
        return false;
    }

    private TrimToRoles findTrimToRolesExtension(@SuppressWarnings("rawtypes") List<VendorExtension> list) {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        return list.stream().filter(x -> x instanceof TrimToRoles).map(TrimToRoles.class::cast).findFirst()
                .orElse(null);
    }

    @Override
    protected Map<String, Path> mapApiListings(Multimap<String, ApiListing> apiListings) {
        Map<String, Path> paths = super.mapApiListings(apiListings);
        return paths.entrySet().stream().filter(x -> !x.getValue().isEmpty())
                .collect(Collectors.toMap(x -> x.getKey(), v -> v.getValue()));
    }

    @Override
    public Swagger mapDocumentation(Documentation from) {
        Swagger ret = super.mapDocumentation(from);
        Predicate<? super Tag> hasAtLeastOneOperation = tag -> ret.getPaths().values().stream()
                .anyMatch(x -> x.getOperations().stream().anyMatch(y -> y.getTags().contains(tag.getName())));
        ret.setTags(ret.getTags().stream().filter(hasAtLeastOneOperation).collect(Collectors.toList()));
        return ret;
    }
}

p.s. these impls are not efficient, but given their usage scenarios I preferred simple impl



来源:https://stackoverflow.com/questions/61859801/how-to-trim-swagger-docs-based-on-current-user-role-in-java-spring

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