Swagger + Spring security - Hide methods based on roles

6.3k views Asked by At

I have an API that has different consumers. I'd like them to get relevant documentation based on their roles in Spring Security.

E.g

API operation A is constricted to Role A and Role B

API operation B is constricted to Role B

API operation C is open for all

I'm using SpringFox, Spring 4, Spring Rest, Security

I know there is an annotation called @ApiIgnore, which could perhaps be utilized.

Is this at all possible?

5

There are 5 answers

2
The Student Soul On

You may have already seen this, but SpringFox itself provides mechanism for configuring security. See this section in the official SpringFox documentation, and this section for an example (note points #14 and #15).

If you are open to allowing different consumers viewing the APIs, but still not being able to execute the APIs, you can consider adding @Secured annotation on the APIs with the appropriate roles.

For example:

@Secured ({"ROLE_A", "ROLE_B")
@RequestMapping ("/open/to/both")
public String operationA() {
    // do something
}

@Secured ("ROLE_B")
@RequestMapping ("/open/to/b/only")
public String operationB() {
    // do something
}

// No @Secured annotation here
@RequestMapping ("/open/to/all")
public String operationC() {
    // do something
}

Make sure that you have added @EnableGlobalMethodSecurity (securedEnabled = true) in your SecurityConfig class (or whatever the one that you have) for @Secured to work.

0
Shivakoti On

Blockquote You can use below code snippet in your security config file and you need extends GlobalMethodSecurityConfiguration.

@Autowired Auth2ServerConfiguration auth2ServerConfiguration;

 @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }

in API's use below code as follows

@PreAuthorize("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")
@Transactional(readOnly = true)
 public @ResponseBody ModelAndView abc()  {
    //do something
  }
0
Sergey Karpushin On

I've posted similar question and found solution in a bit. Since I've found 3 similar questions on stackoverflow, I don't know whether I should just copy-paste answer in all of them, or provide a link to my answer.

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

Details can be found here: https://stackoverflow.com/a/61860729/285060

1
Yuriy Chih On

Sharing solution from my project. Idea - to filter part of swagger config - URL map that are returned to swaggerUI based on user role.

swagger config to filter based on user role

spring boot version 2.6.14 used and springdoc-openapi-ui:1.7.0, springdoc:springdoc-openapi-security:1.7.0

 @Bean
public OpenAPI openAPI() {
    return new OpenAPI().info(defaultInfo());
}

private Info defaultInfo() {
    return new Info()
            .title("brandName")
            .version("version");
}

@Bean
public GroupedOpenApi userApi() {
    return GroupedOpenApi.builder()
            .group(USER_GROUP)
            .pathsToMatch("/api/user/**")
            .build();
}

@Bean
public GroupedOpenApi payrollApi() {
    return GroupedOpenApi.builder()
            .group(PAYROLL_GROUP)
            .pathsToMatch("/api/payroll/**")
            .addOpenApiCustomiser(oauth2OpenAPI())
            .build();
}

@Bean
public GroupedOpenApi utilApi() {
    return GroupedOpenApi.builder()
            .group(UTIL_GROUP)
            .pathsToMatch("/actuator/**")
            .build();
}

@Bean
public OpenApiCustomiser oauth2OpenAPI() {
    return openApi -> {
        var securitySchemeName = "OAuth2 flow";
        openApi
                .info(defaultInfo())
                .addSecurityItem(new SecurityRequirement().addList(securitySchemeName));
        openApi.getComponents()
                .addSecuritySchemes(securitySchemeName,
                        new SecurityScheme()
                                .name(securitySchemeName)
                                .type(SecurityScheme.Type.OAUTH2)
                                .in(SecurityScheme.In.HEADER)
                                .flows(new OAuthFlows()
                                        .clientCredentials(new OAuthFlow()
                                                .tokenUrl("payrollTokenUrl"))));
    };
}

/**
 * Bean that shows or hides Swagger groups based on user rules
 */
@Bean
@Primary
public SwaggerUiConfigParameters customUIConfiguration(SwaggerUiConfigProperties swaggerUiConfig, SecurityService securityService) {
    return new CustomSwaggerUiConfigParameters(swaggerUiConfig, securityService);
}

public static class CustomSwaggerUiConfigParameters extends SwaggerUiConfigParameters {

    private static final Map<String, List<String>> ROLE_GROUP_ACCESS_RULES = Map.of(
            "admin", List.of(ADMIN_GROUP, USER_GROUP, PAYROLL_GROUP, PARTNER_GROUP, UTIL_GROUP),
            "user", List.of(PARTNER_GROUP)
            );

    public final SecurityService securityService;

    public CustomSwaggerUiConfigParameters(SwaggerUiConfigProperties swaggerUiConfig, SecurityService securityService) {
        super(swaggerUiConfig);
        this.securityService = securityService;
    }

    /**
     * Filter accessible resources based on user role
     * @return filtered swagger config parameters
     */
    @Override
    public Map<String, Object> getConfigParameters() {
        var userAuthorities = securityService.getUserAuthorities();
        var allowedUrls = ROLE_GROUP_ACCESS_RULES.entrySet().stream()
                .filter(entry -> userAuthorities.contains(entry.getKey()))
                .flatMap(entry -> entry.getValue().stream())
                .collect(Collectors.toSet());
        var configParameters = super.getConfigParameters();
        Collection<SwaggerUrl> allUrls = (Collection<SwaggerUrl>) configParameters.get("urls");
        List<SwaggerUrl> filtered = new ArrayList<>();
        for (var url : allUrls) {
            if (allowedUrls.stream().anyMatch(a -> StringUtils.equalsIgnoreCase(a,  url.getName()))){
                filtered.add(url);
            }
        }
        configParameters.put("urls", filtered);
        return configParameters;
    }
}
1
Mohsen On

After a bit of searching I found there are no ways offer for this problem in web. So I solved it with my own solution.

I wrote a filter that modify the response and remove the apis which the user has no access to them.

The filter is something like this:

 @Override
 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    String url = httpServletRequest.getRequestURI();
        if (url.contains("v2/api-docs")) {
            CharResponseWrapper wrapper = new CharResponseWrapper((HttpServletResponse) response);
            chain.doFilter(httpServletRequest, wrapper);
            refineApiBaseOnACL(wrapper);
            return;
        }
    chain.doFilter(httpServletRequest, response);
}

To modify the response you should follow this link .

Then we need to refine the generated api:

private List<String> httpCommands = List.of("get", "head", "post", "put", "delete", "options", "patch");

public void refineApiBaseOnACL(CharResponseWrapper wrapper) {
    try {
        byte[] bytes = wrapper.getByteArray();

        if (wrapper.getContentType().contains("application/json")) {
            String out = refineContentBaseOnACL(new String(bytes));
            wrapper.getResponse().getOutputStream().write(out.getBytes());
        } else {
            wrapper.getResponse().getOutputStream().write(bytes);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private String refineContentBaseOnACL(String originalContent) {
    JSONObject object = new JSONObject(originalContent);
    JSONObject paths = object.getJSONObject("paths");
    JSONArray tags = object.getJSONArray("tags");

    Iterator keys = paths.keys();
    Set<String> toRemovePath = new HashSet<>();
    Set<Integer> toRemoveTags = new HashSet<>();
    Set<String> tagSet = new HashSet<>();
    while (keys.hasNext()) {
        String key = (String) keys.next();
        String[] split = key.split("/");
        if (!getAccessHandler().checkAccessRest(split[1], split[2]))
            toRemovePath.add(key);
        else {
            for (String httpCommand : httpCommands)
                if (paths.getJSONObject(key).has(httpCommand)) {
                    JSONObject command = paths.getJSONObject(key).getJSONObject(httpCommand);
                    JSONArray tagsArray = command.getJSONArray("tags");
                    for (int i = 0; i < tagsArray.length(); i++)
                        tagSet.add(tagsArray.getString(i));
                }
        }
    }

    for (String key : toRemovePath)
        paths.remove(key);

    for (int i = 0; i < tags.length(); i++)
        if (!tagSet.contains(tags.getJSONObject(i).getString("name")))
            toRemoveTags.add(i);

    List<Integer> sortedTags = new ArrayList<>(toRemoveTags);
    sortedTags.sort(Collections.reverseOrder());
    for (Integer key : sortedTags)
        tags.remove(key);


    Pattern modelPattern = Pattern.compile("\"#/definitions/(.*?)\"");
    Set<String> modelSet = new HashSet<>();
    Matcher matcher = modelPattern.matcher(object.toString());
    while (matcher.find())
        modelSet.add(matcher.group(1));

    JSONObject definitions = object.getJSONObject("definitions");
    Set<String> toRemoveModel = new HashSet<>();
    Iterator definitionModel = definitions.keys();
    while (definitionModel.hasNext()) {
        String definition = (String) definitionModel.next();
        boolean found = false;
        for (String model : modelSet)
            if (definition.equals(model)) {
                found = true;
                break;
            }
        if (!found)
            toRemoveModel.add(definition);
    }

    for (String model : toRemoveModel) {
        definitions.remove(model);
    }

    return object.toString();
}

In my case I have a AccessHandler which handles the access control with the url. You should write this section on your logic. For the spring security roles you can use something like this:

request.isUserInRole("Role_A");