Create external group names claim in token builder directly

This commit is contained in:
René Pfeuffer
2019-02-15 10:23:46 +01:00
parent 83076dba46
commit 5d601293bf
6 changed files with 155 additions and 84 deletions

View File

@@ -176,7 +176,7 @@ public final class GroupNames implements Serializable, Iterable<String>
@Override @Override
public String toString() public String toString()
{ {
return Joiner.on(", ").join(collection); return Joiner.on(", ").join(collection) + "(" + (external? "external": "internal") + ")";
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -45,7 +45,12 @@ import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.AdministrationContext;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
/** /**
* Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated
@@ -80,6 +85,9 @@ public final class SyncingRealmHelper {
this.groupManager = groupManager; this.groupManager = groupManager;
} }
/**
* Create {@link AuthenticationInfo} from user and groups.
*/
public AuthenticationInfoBuilder.ForRealm authenticationInfo() { public AuthenticationInfoBuilder.ForRealm authenticationInfo() {
return new AuthenticationInfoBuilder().new ForRealm(); return new AuthenticationInfoBuilder().new ForRealm();
} }
@@ -95,6 +103,13 @@ public final class SyncingRealmHelper {
} }
public class ForRealm { public class ForRealm {
private ForRealm() {
}
/**
* Sets the realm.
* @param realm name of the realm
*/
public ForUser forRealm(String realm) { public ForUser forRealm(String realm) {
AuthenticationInfoBuilder.this.realm = realm; AuthenticationInfoBuilder.this.realm = realm;
return AuthenticationInfoBuilder.this.new ForUser(); return AuthenticationInfoBuilder.this.new ForUser();
@@ -102,6 +117,13 @@ public final class SyncingRealmHelper {
} }
public class ForUser { public class ForUser {
private ForUser() {
}
/**
* Sets the user.
* @param user authenticated user
*/
public AuthenticationInfoBuilder.WithGroups andUser(User user) { public AuthenticationInfoBuilder.WithGroups andUser(User user) {
AuthenticationInfoBuilder.this.user = user; AuthenticationInfoBuilder.this.user = user;
return AuthenticationInfoBuilder.this.new WithGroups(); return AuthenticationInfoBuilder.this.new WithGroups();
@@ -109,35 +131,60 @@ public final class SyncingRealmHelper {
} }
public class WithGroups { public class WithGroups {
private WithGroups() {
}
/**
* Build the authentication info without groups.
* @return The complete {@link AuthenticationInfo}
*/
public AuthenticationInfo withoutGroups() {
return withGroups(emptyList());
}
/**
* Set the internal groups for the user.
* @param groups groups of the authenticated user
* @return The complete {@link AuthenticationInfo}
*/
public AuthenticationInfo withGroups(String... groups) {
return withGroups(asList(groups));
}
/**
* Set the internal groups for the user.
* @param groups groups of the authenticated user
* @return The complete {@link AuthenticationInfo}
*/
public AuthenticationInfo withGroups(Collection<String> groups) { public AuthenticationInfo withGroups(Collection<String> groups) {
AuthenticationInfoBuilder.this.groups = groups; AuthenticationInfoBuilder.this.groups = groups;
AuthenticationInfoBuilder.this.external = false; AuthenticationInfoBuilder.this.external = false;
return build(); return build();
} }
/**
* Set the external groups for the user.
* @param groups external groups of the authenticated user
* @return The complete {@link AuthenticationInfo}
*/
public AuthenticationInfo withExternalGroups(String... groups) {
return withExternalGroups(asList(groups));
}
/**
* Set the external groups for the user.
* @param groups external groups of the authenticated user
* @return The complete {@link AuthenticationInfo}
*/
public AuthenticationInfo withExternalGroups(Collection<String> groups) { public AuthenticationInfo withExternalGroups(Collection<String> groups) {
AuthenticationInfoBuilder.this.groups = groups; AuthenticationInfoBuilder.this.groups = groups;
AuthenticationInfoBuilder.this.external = false; AuthenticationInfoBuilder.this.external = true;
return build(); return build();
} }
} }
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
/**
* Create {@link AuthenticationInfo} from user and groups.
*
*
* @param realm name of the realm
* @param user authenticated user
* @param groups groups of the authenticated user
*
* @return authentication info
*/
public AuthenticationInfo createAuthenticationInfo(String realm, User user,
String... groups) {
return createAuthenticationInfo(realm, user, ImmutableList.copyOf(groups));
}
/** /**
* Create {@link AuthenticationInfo} from user and groups. * Create {@link AuthenticationInfo} from user and groups.
@@ -149,22 +196,7 @@ public final class SyncingRealmHelper {
* *
* @return authentication info * @return authentication info
*/ */
public AuthenticationInfo createAuthenticationInfo(String realm, User user, private AuthenticationInfo createAuthenticationInfo(String realm, User user,
Collection<String> groups) {
return this.createAuthenticationInfo(realm, user, groups, false);
}
/**
* Create {@link AuthenticationInfo} from user and groups.
*
*
* @param realm name of the realm
* @param user authenticated user
* @param groups groups of the authenticated user
*
* @return authentication info
*/
public AuthenticationInfo createAuthenticationInfo(String realm, User user,
Collection<String> groups, boolean externalGroups) { Collection<String> groups, boolean externalGroups) {
SimplePrincipalCollection collection = new SimplePrincipalCollection(); SimplePrincipalCollection collection = new SimplePrincipalCollection();

View File

@@ -37,6 +37,7 @@ package sonia.scm.security;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
import org.assertj.core.api.Assertions;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -54,6 +55,7 @@ import sonia.scm.web.security.PrivilegedAction;
import java.io.IOException; import java.io.IOException;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.util.Arrays.asList;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -109,46 +111,6 @@ public class SyncingRealmHelperTest {
helper = new SyncingRealmHelper(ctx, userManager, groupManager); helper = new SyncingRealmHelper(ctx, userManager, groupManager);
} }
/**
* Tests {@link SyncingRealmHelper#createAuthenticationInfo(String, User, String...)}.
*/
@Test
public void testCreateAuthenticationInfo() {
User user = new User("tricia");
AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test",
user, singletonList("heartOfGold"));
assertNotNull(authInfo);
assertEquals("tricia", authInfo.getPrincipals().getPrimaryPrincipal());
assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
GroupNames groups = authInfo.getPrincipals().oneByType(GroupNames.class);
assertThat(groups, hasItem("heartOfGold"));
assertFalse(groups.isExternal());
}
/**
* Tests {@link SyncingRealmHelper#createAuthenticationInfo(String, User, String...)}.
*/
@Test
public void testCreateAuthenticationInfoWithExternalGroups() {
User user = new User("tricia");
AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test",
user, singletonList("heartOfGold"), true);
assertNotNull(authInfo);
assertEquals("tricia", authInfo.getPrincipals().getPrimaryPrincipal());
assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
GroupNames groups = authInfo.getPrincipals().oneByType(GroupNames.class);
assertThat(groups, hasItem("heartOfGold"));
assertTrue(groups.isExternal());
}
/** /**
* Tests {@link SyncingRealmHelper#store(Group)}. * Tests {@link SyncingRealmHelper#store(Group)}.
* *
@@ -222,4 +184,45 @@ public class SyncingRealmHelperTest {
helper.store(user); helper.store(user);
verify(userManager, times(1)).modify(user); verify(userManager, times(1)).modify(user);
} }
@Test
public void builderShouldSetInternalGroups() {
AuthenticationInfo authenticationInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(new User("ziltoid"))
.withGroups("internal");
GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
Assertions.assertThat(groupNames.getCollection()).containsOnly("internal");
Assertions.assertThat(groupNames.isExternal()).isFalse();
}
@Test
public void builderShouldSetExternalGroups() {
AuthenticationInfo authenticationInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(new User("ziltoid"))
.withExternalGroups("external");
GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
Assertions.assertThat(groupNames.getCollection()).containsOnly("external");
Assertions.assertThat(groupNames.isExternal()).isTrue();
}
@Test
public void builderShouldSetValues() {
User user = new User("ziltoid");
AuthenticationInfo authInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(user)
.withoutGroups();
assertNotNull(authInfo);
assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal());
assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
}
} }

View File

@@ -8,7 +8,6 @@ import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.group.GroupNames;
import sonia.scm.security.*; import sonia.scm.security.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -92,11 +91,6 @@ public class AuthenticationResource {
tokenBuilder.scope(Scope.valueOf(authentication.getScope())); tokenBuilder.scope(Scope.valueOf(authentication.getScope()));
} }
GroupNames groupNames = subject.getPrincipals().oneByType(GroupNames.class);
if (groupNames != null && groupNames.isExternal()) {
tokenBuilder.groups(groupNames.getCollection().toArray(new String[]{}));
}
AccessToken token = tokenBuilder.build(); AccessToken token = tokenBuilder.build();
if (authentication.isCookie()) { if (authentication.isCookie()) {

View File

@@ -50,6 +50,7 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.group.GroupNames;
/** /**
* Jwt implementation of {@link AccessTokenBuilder}. * Jwt implementation of {@link AccessTokenBuilder}.
@@ -207,6 +208,12 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
if (!groups.isEmpty()) { if (!groups.isEmpty()) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups); claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups);
} else {
Subject currentSubject = SecurityUtils.getSubject();
GroupNames groupNames = currentSubject.getPrincipals().oneByType(GroupNames.class);
if (groupNames != null && groupNames.isExternal()) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groupNames.getCollection().toArray(new String[]{}));
}
} }
// sign token and create compact version // sign token and create compact version

View File

@@ -36,23 +36,32 @@ import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadContext;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.group.GroupNames;
import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
@@ -62,6 +71,11 @@ import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini",
username = "trillian",
password = "secret"
)
public class JwtAccessTokenBuilderTest { public class JwtAccessTokenBuilderTest {
{ {
@@ -96,11 +110,6 @@ public class JwtAccessTokenBuilderTest {
* Tests {@link JwtAccessTokenBuilder#build()} with subject from shiro context. * Tests {@link JwtAccessTokenBuilder#build()} with subject from shiro context.
*/ */
@Test @Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini",
username = "trillian",
password = "secret"
)
public void testBuildWithoutSubject() { public void testBuildWithoutSubject() {
JwtAccessToken token = factory.create().build(); JwtAccessToken token = factory.create().build();
assertEquals("trillian", token.getSubject()); assertEquals("trillian", token.getSubject());
@@ -151,6 +160,32 @@ public class JwtAccessTokenBuilderTest {
assertClaims(new JwtAccessToken(claims, compact)); assertClaims(new JwtAccessToken(claims, compact));
} }
@Test
public void testWithExternalGroups() {
applyExternalGroupsToSubject(true, "external");
JwtAccessToken token = factory.create().subject("dent").build();
assertArrayEquals(new String[]{"external"}, token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).map(x -> (String[]) x).get());
}
@Test
public void testWithInternalGroups() {
applyExternalGroupsToSubject(false, "external");
JwtAccessToken token = factory.create().subject("dent").build();
assertFalse(token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).isPresent());
}
private void applyExternalGroupsToSubject(boolean external, String... groups) {
Subject subject = spy(SecurityUtils.getSubject());
when(subject.getPrincipals()).thenAnswer(invocation -> enrichWithGroups(invocation, groups, external));
shiro.setSubject(subject);
}
private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable {
PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod());
when(principals.oneByType(GroupNames.class)).thenReturn(new GroupNames(Arrays.asList(groups), external));
return principals;
}
private void assertClaims(JwtAccessToken token){ private void assertClaims(JwtAccessToken token){
assertThat(token.getId(), not(isEmptyOrNullString())); assertThat(token.getId(), not(isEmptyOrNullString()));
assertNotNull( token.getIssuedAt() ); assertNotNull( token.getIssuedAt() );