Swagger + Spring security - Hide methods based on roles
Asked Answered
I

5

21

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?

Inflated answered 3/1, 2017 at 13:59 Comment(5)
How would @ApiIgnore be useful to your use case? It just prevents some resources from being displayed... If you're using Spring Security model with tables to store the roles, users, permissions, etc. you can create an endpoint to retrieve this data. Create all this as resources as a Swagger mapping wouldn't work bacause you need truly Rest annotations around them.Diacid
Well I meant, perhaps I could write my own annotation that utilized apiignore where the roles weren't present. or something like that.Inflated
Did you find any solution?Grope
We ended up offering different documentation per integrator. Since it's just a matter of generating up the swagger doc, there was some creative scripting done. Horrible solution, but atleast it was solved.Inflated
Does this answer your question? How to trim Swagger docs based on current User Role in Java Spring?Reentry
F
1

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");
Fluorite answered 25/11, 2019 at 12:31 Comment(1)
There is an alternative approach which does not involve modifying json object. Check out this answer: https://mcmap.net/q/661549/-how-to-trim-swagger-docs-based-on-current-user-role-in-java-springFritts
F
1

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://mcmap.net/q/661549/-how-to-trim-swagger-docs-based-on-current-user-role-in-java-spring

Fritts answered 18/5, 2020 at 0:54 Comment(0)
G
0

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;
    }
}
Getz answered 12/5, 2023 at 15:35 Comment(1)
OpenApiCustomiser may be intend of OpenApiCustomizerUnpredictable
C
-2

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
  }
Clearing answered 18/1, 2017 at 13:35 Comment(0)
M
-4

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.

Manage answered 11/1, 2017 at 16:50 Comment(2)
Thank you for your answer, but this does not solve my problem. I already secure my application with roles, I want the documentation to take that into account.Inflated
@EspenSchulstad Did you find a solution for your problem? ThanksTranslucid

© 2022 - 2025 — McMap. All rights reserved.