Skip to content

ADD PermissionVoter for wildcard permissions gh-4611 #5467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.access.vote;

import java.util.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.AntPathMatcher;

/**
* A {@link GrantedAuthority} which represents a hirachical security permission based on a permission string. A
* permission consists at least of a path, can contain permission tokens and instance object identifiers. The three
* token groups (path, permission object identifier) are seperated with a ":". The levels of the path are seperated with
* a ".". Multiple permission token or object identifiers are seperated with a ",". * Example:
* <ul>
* <li>"test.userManagmenet.users:read,write:userId1,userId2" = Permits to read and write users of the userManagmenet of
* test. </li>
* <li>"**" = Permits to all actions on every resource. </li>
* </ul>
*
* @author Marco Schaub
*/
class Permission implements GrantedAuthority {

public static final String PERMISSION_SEPERATOR = ":";
public static final String PATH_SEPERATOR = ".";
public static final String WILDCARD = "*";
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(".");

private final String permissionString;
private Set<String> instanceObjects = new HashSet(Arrays.asList(WILDCARD));
private Set<String> permissionTokens = new HashSet(Arrays.asList(WILDCARD));
private final String path;

/**
* Creates a {@code Permission} using the given permissionString
*
* @param permissionString
*/
public Permission(String permissionString) {
this.permissionString = permissionString;
String[] permissionParts = permissionString.split(PERMISSION_SEPERATOR);
switch (permissionParts.length) {
case 3:
instanceObjects = new HashSet(Arrays.asList(permissionParts[2].split(",")));
case 2:
permissionTokens = new HashSet(Arrays.asList(permissionParts[1].split(",")));
case 1:
path = permissionParts[0];
break;
default:
throw new IllegalArgumentException();
}
}

@Override
public String getAuthority() {
return permissionString;
}

/**
* Compares the current {@code Permission} to another given {@code Permission}.
*
* @param requiredPermission The other {@code Permission} which should be implied by the current {@code Permission}.
* @return true if the current {@code Permission} implies the given other {@code Permission}
*/
public boolean implies(Permission requiredPermission) {
if (PATH_MATCHER.matchStart(path, requiredPermission.path)) {
if (requiredPermission.permissionTokens.contains(WILDCARD) || permissionTokens.contains(WILDCARD) || permissionTokens.containsAll(requiredPermission.permissionTokens)) {
if (requiredPermission.instanceObjects.contains(WILDCARD) || instanceObjects.contains(WILDCARD) || instanceObjects.containsAll(requiredPermission.instanceObjects)) {
return true;
}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.access.vote;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

/**
* Votes if a given given user permission string implies a defined permission string. This voter can be used instead of
* a {@link org.springframework.security.access.vote.RoleVoter} when working with permissions. The voter turns and keeps
* given permissions strings into {@link Permission} for easier processing.
*
* @author Marco Schaub
*/
public class PermissionVoter implements AccessDecisionVoter<Object> {

private static final HashMap<String, Permission> PERMISSION_MAP = new HashMap();

@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null && attribute instanceof SecurityConfig;
}

@Override
public boolean supports(Class clazz) {
return true;
}

@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
if (authentication != null) {
for (ConfigAttribute requiredPermissionString : attributes) {
Permission requiredPermission = getPermissionForString(requiredPermissionString.getAttribute());
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
Permission grantedPermission = getPermissionForString(grantedAuthority.getAuthority());
if (grantedPermission.implies(requiredPermission)) {
return ACCESS_GRANTED;
}
}
}
}
return ACCESS_DENIED;
}

/**
* A static method to verify a permission based access programmatically. If multiple permissions Strings are
* supplied, an OR evaluation will be made.
*
* @param authentication A {@link Authentication} object.
* @param requiredPermissions One or multiple permissions to check for.
* @return true if the {@code Authentication} contains a {@code GrantedAuthority} which implies one of the given
* permission strings.
*/
public static boolean vote(Authentication authentication, String... requiredPermissions) {
List<ConfigAttribute> attributes = Arrays.stream(requiredPermissions).map(permissionString -> new SecurityConfig(permissionString)).collect(Collectors.toList());
int decision = new PermissionVoter().vote(authentication, null, attributes);
return decision > 0;
}

private static Permission getPermissionForString(String permissionString) {
Permission permission = PERMISSION_MAP.get(permissionString);
if (permission == null) {
permission = new Permission(permissionString);
PERMISSION_MAP.put(permissionString, permission);
}
return permission;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.access.vote;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;

/**
*
* @author Marco Schaub
*/
public class PermissionVoterTests {

@Test
public void nullAuthenticationDenies() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = null;
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}

@Test
public void matchingSinglePathPermission() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void nonMatchingSinglePathPermission() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "A", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}

@Test
public void matchingMultiPathPermission() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test.module1.sub1", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.module1.sub1"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void nonMatchingMultiPathPermission() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test.module1.sub1", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.module1.sub2"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}

@Test
public void matchingPathPermissionSingleLevelWildcard() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test.*.sub1", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.modul1.sub1"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.modul2.sub1"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void nonMatchingPathPermissionSingleLevelWildcard() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test.*.sub1", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.modul1.sub2"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.modul2.sub2"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}

@Test
public void matchingPathPermissionMultiLevelWildcard() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test.**", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.modul1.sub1.config1"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.modul2.sub2.config1"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void matchingPermissionToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test:read,write", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:read"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:write"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void nonMatchingPermissionToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test:read,write", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:delete"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}

@Test
public void matchingImpliedWildcardPermissionToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:read"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:write"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void matchingWildcardPermissionToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test:*", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:read"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:write"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void matchingInstanceObjectToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test:*:500", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:read:500"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:write:500"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void matchingImpliedWildcardInstanceObjectToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test:*", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:read:500"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:write:500"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

@Test
public void nonMatchingInstanceObjectToken() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test:*:500", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test:read:501"))).isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}

@Test
public void matchingCombinations() {
PermissionVoter voter = new PermissionVoter();
Authentication userAB = new TestingAuthenticationToken("user", "pass", "test.*.sub1.**:write,read:500", "B");
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.module1.sub1:read"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
assertThat(voter.vote(userAB, this, SecurityConfig.createList("test.module2.sub1.config1:write:500"))).isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}

}