Ok, I think found good solution to that question. Solution consists of 2 parts:
- Extend controllers scanning logic through
OperationBuilderPlugin
to retain roles in the Swagger's vendor extensions
- 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