Commits

David Carr committed 39ce48e

shiro-integration: add an integration with shiro

Comments (0)

Files changed (9)

.idea/libraries/commons_beanutils_1_8_3.xml

+<component name="libraryTable">
+  <library name="commons-beanutils-1.8.3">
+    <CLASSES>
+      <root url="jar://$USER_HOME$/.gradle/caches/artifacts-15/filestore/commons-beanutils/commons-beanutils/1.8.3/jar/686ef3410bcf4ab8ce7fd0b899e832aaba5facf7/commons-beanutils-1.8.3.jar!/" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES>
+      <root url="jar://$USER_HOME$/.gradle/caches/artifacts-15/filestore/commons-beanutils/commons-beanutils/1.8.3/source/170d03db4cb48be2f8f35ce3243d62c4b2204e2e/commons-beanutils-1.8.3-sources.jar!/" />
+    </SOURCES>
+  </library>
+</component>

.idea/libraries/shiro_core_1_2_1.xml

+<component name="libraryTable">
+  <library name="shiro-core-1.2.1">
+    <CLASSES>
+      <root url="jar://$USER_HOME$/.gradle/caches/artifacts-15/filestore/org.apache.shiro/shiro-core/1.2.1/bundle/a0eea086515a48eedff94e209578139921d33471/shiro-core-1.2.1.jar!/" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES>
+      <root url="jar://$USER_HOME$/.gradle/caches/artifacts-15/filestore/org.apache.shiro/shiro-core/1.2.1/source/f1a80658ef7f0e6d578ce0170836f9a911280ef5/shiro-core-1.2.1-sources.jar!/" />
+    </SOURCES>
+  </library>
+</component>

.idea/modules.xml

       <module fileurl="file://$PROJECT_DIR$/authc4j.iml" filepath="$PROJECT_DIR$/authc4j.iml" />
       <module fileurl="file://$PROJECT_DIR$/mac-bindings.iml" filepath="$PROJECT_DIR$/mac-bindings.iml" />
       <module fileurl="file://$PROJECT_DIR$/mac-provider.iml" filepath="$PROJECT_DIR$/mac-provider.iml" />
+      <module fileurl="file://$PROJECT_DIR$/shiro-integration/shiro-integration.iml" filepath="$PROJECT_DIR$/shiro-integration/shiro-integration.iml" />
       <module fileurl="file://$PROJECT_DIR$/windows-provider.iml" filepath="$PROJECT_DIR$/windows-provider.iml" />
     </modules>
   </component>
-include "api", "mac-provider", "windows-provider", "mac-bindings"
+include "api", "mac-provider", "windows-provider", "mac-bindings", "shiro-integration"

shiro-integration/build.gradle

+dependencies {
+    compile project(":api")
+    compile "org.apache.shiro:shiro-core:1.2.1"
+    testCompile "org.slf4j:slf4j-simple:1.7.2"
+}

shiro-integration/src/main/java/us/carrclan/david/authc4j/shiro/AbstractAuthC4JRealm.java

+package us.carrclan.david.authc4j.shiro;
+
+import org.apache.shiro.authc.*;
+import org.apache.shiro.authc.credential.CredentialsMatcher;
+import org.apache.shiro.authc.credential.HashingPasswordService;
+import org.apache.shiro.authc.credential.PasswordMatcher;
+import org.apache.shiro.authc.credential.PasswordService;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.cache.CacheManager;
+import org.apache.shiro.crypto.hash.Hash;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.util.ByteSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import us.carrclan.david.authc4j.api.Password;
+import us.carrclan.david.authc4j.api.UserInformation;
+import us.carrclan.david.authc4j.api.UserInformationSource;
+
+/**
+ * A {@link org.apache.shiro.realm.Realm} that authenticates with an AuthC4J {@link UserInformationSource}.
+ * Authorization is left for subclasses to define by implementing the {@link #buildAuthorizationInfo} method.
+ */
+public abstract class AbstractAuthC4JRealm extends AuthorizingRealm {
+    private static final Logger log = LoggerFactory.getLogger(AbstractAuthC4JRealm.class);
+
+    private UserInformationSource userInformationSource;
+
+    public AbstractAuthC4JRealm() {
+        this(null, null, null);
+    }
+
+    public AbstractAuthC4JRealm(UserInformationSource userInformationSource) {
+        this(userInformationSource, null, null);
+    }
+
+    public AbstractAuthC4JRealm(UserInformationSource userInformationSource, CacheManager cacheManager) {
+        this(userInformationSource, cacheManager, null);
+    }
+
+    public AbstractAuthC4JRealm(UserInformationSource userInformationSource, CredentialsMatcher matcher) {
+        this(userInformationSource, null, matcher);
+    }
+
+    public AbstractAuthC4JRealm(UserInformationSource userInformationSource, CacheManager cacheManager, CredentialsMatcher matcher) {
+        super(cacheManager, matcher);
+        this.userInformationSource = userInformationSource;
+    }
+
+    public UserInformationSource getUserInformationSource() {
+        return userInformationSource;
+    }
+
+    public void setUserInformationSource(UserInformationSource userInformationSource) {
+        this.userInformationSource = userInformationSource;
+    }
+
+    @Override
+    protected final AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken)
+            throws AuthenticationException {
+        AuthenticationInfo authenticationInfo = null;
+        if (!(authToken instanceof UsernamePasswordToken)) {
+            return null;
+        }
+        UsernamePasswordToken token = (UsernamePasswordToken) authToken;
+        String username = token.getUsername();
+        Password password = new Password(token.getPassword());
+        log.debug("Attempting login for user {}", username);
+        UserInformation userInformation = userInformationSource.authenticate(username, password);
+        if (userInformation == null) {
+            log.debug("Failed login for user {}", username);
+        } else {
+            Object principal = new AuthC4JPrincipal(userInformation);
+            authenticationInfo = buildAuthenticationInfo(token, principal);
+            log.debug("Successful login for user {}", username);
+        }
+        return authenticationInfo;
+    }
+
+    private AuthenticationInfo buildAuthenticationInfo(UsernamePasswordToken token, Object principal) {
+        AuthenticationInfo authenticationInfo;
+        HashingPasswordService hashService = getHashService();
+        if (hashService != null) {
+            Hash hash = hashService.hashPassword(token.getPassword());
+            ByteSource salt = hash.getSalt();
+            authenticationInfo = new SimpleAuthenticationInfo(principal, hash, salt, getName());
+        } else {
+            Object credentials = token.getCredentials();
+            authenticationInfo = new SimpleAuthenticationInfo(principal, credentials, getName());
+        }
+        return authenticationInfo;
+    }
+
+    @Override
+    protected final AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
+        AuthC4JPrincipal principal = principals.oneByType(AuthC4JPrincipal.class);
+        if (principal != null) {
+            return buildAuthorizationInfo(principal);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Assembles the appropriate authorization information for the specified principal.
+     *
+     * @param principal the principal for which to assemble authorization information
+     * @return the authorization information for the specified principal
+     */
+    protected abstract AuthorizationInfo buildAuthorizationInfo(AuthC4JPrincipal principal);
+
+    private HashingPasswordService getHashService() {
+        CredentialsMatcher matcher = getCredentialsMatcher();
+        if (matcher instanceof PasswordMatcher) {
+            PasswordMatcher passwordMatcher = (PasswordMatcher) matcher;
+            PasswordService passwordService = passwordMatcher.getPasswordService();
+            if (passwordService instanceof HashingPasswordService) {
+                return (HashingPasswordService) passwordService;
+            }
+        }
+        return null;
+    }
+}

shiro-integration/src/main/java/us/carrclan/david/authc4j/shiro/AuthC4JPrincipal.java

+package us.carrclan.david.authc4j.shiro;
+
+import us.carrclan.david.authc4j.api.GroupInformation;
+import us.carrclan.david.authc4j.api.UserInformation;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class AuthC4JPrincipal implements Serializable {
+    private static final long serialVersionUID = 1;
+    private final String shortName;
+    private final String longName;
+    private final Set<String> groupShortNames = new HashSet<String>();
+
+    AuthC4JPrincipal(UserInformation userInformation) {
+        shortName = userInformation.getShortName();
+        longName = userInformation.getLongName();
+        for (GroupInformation group : userInformation.getMemberGroupInformation()) {
+            groupShortNames.add(group.getShortName());
+        }
+    }
+
+    /**
+     * Returns the short name of the user.
+     */
+    public String getShortName() {
+        return shortName;
+    }
+
+    /**
+     * Returns the long name of the user.
+     */
+    public String getLongName() {
+        return longName;
+    }
+
+    /**
+     * Returns the short names of all groups that the user belongs to
+     */
+    public Set<String> getGroupShortNames() {
+        return Collections.unmodifiableSet(groupShortNames);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof AuthC4JPrincipal) {
+            return shortName.equals(((AuthC4JPrincipal) obj).shortName);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return shortName.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "{" + getClass().getSimpleName() + ":" + shortName + "}";
+    }
+}

shiro-integration/src/main/java/us/carrclan/david/authc4j/shiro/GroupMappingAuthC4JRealm.java

+package us.carrclan.david.authc4j.shiro;
+
+import org.apache.shiro.authc.credential.CredentialsMatcher;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.cache.CacheManager;
+import us.carrclan.david.authc4j.api.UserInformationSource;
+
+import java.util.*;
+
+/**
+ * A {@link org.apache.shiro.realm.Realm} that authenticates with AuthC4J and assigns roles to users based on a mapping
+ * from their groups.  To define permissions based on these roles, set a
+ * {@link org.apache.shiro.authz.permission.RolePermissionResolver}.
+ */
+public class GroupMappingAuthC4JRealm extends AbstractAuthC4JRealm {
+    private final Map<String, String> groupRolesMap = new HashMap<String, String>();
+
+    public GroupMappingAuthC4JRealm() {
+    }
+
+    public GroupMappingAuthC4JRealm(UserInformationSource userInformationSource) {
+        super(userInformationSource);
+    }
+
+    public GroupMappingAuthC4JRealm(UserInformationSource userInformationSource, CacheManager cacheManager) {
+        super(userInformationSource, cacheManager);
+    }
+
+    public GroupMappingAuthC4JRealm(UserInformationSource userInformationSource, CredentialsMatcher matcher) {
+        super(userInformationSource, matcher);
+    }
+
+    public GroupMappingAuthC4JRealm(UserInformationSource userInformationSource, CacheManager cacheManager, CredentialsMatcher matcher) {
+        super(userInformationSource, cacheManager, matcher);
+    }
+
+    /**
+     * Sets the translation from group names to role names. If not set, the map is empty, resulting in no users getting
+     * roles.
+     */
+    public void setGroupRolesMap(Map<String, String> groupRolesMap) {
+        this.groupRolesMap.clear();
+        if (groupRolesMap != null) {
+            this.groupRolesMap.putAll(groupRolesMap);
+        }
+    }
+
+    /**
+     * This method is called by to translate group names to role names. This implementation uses the groupRolesMap to
+     * map group names to role names.
+     *
+     * @param groupNames the group names that apply to the current user
+     * @return a collection of roles that are implied by the given role names
+     * @see {@link #setGroupRolesMap}
+     */
+    protected Collection<String> getRoleNamesForGroups(Collection<String> groupNames) {
+        Set<String> roleNames = new HashSet<String>();
+        for (String groupName : groupNames) {
+            String roleName = groupRolesMap.get(groupName);
+            if (roleName != null) {
+                roleNames.add(roleName);
+            }
+        }
+        return roleNames;
+    }
+
+    /**
+     * Builds an {@link AuthorizationInfo} object based on the user's groups. The groups are translated to roles names
+     * by using the configured groupRolesMap.
+     *
+     * @param principal the principal of Subject that is being authorized
+     * @return the AuthorizationInfo for the given Subject principal
+     * @see {@link #setGroupRolesMap}
+     * @see {@link #getRoleNamesForGroups}
+     */
+    protected AuthorizationInfo buildAuthorizationInfo(AuthC4JPrincipal principal) {
+        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
+        authorizationInfo.addRoles(getRoleNamesForGroups(principal.getGroupShortNames()));
+        return authorizationInfo;
+    }
+}

shiro-integration/src/test/groovy/us/carrclan/david/authc4j/shiro/GroupMappingAuthC4JRealmSpec.groovy

+package us.carrclan.david.authc4j.shiro
+
+import org.apache.shiro.authc.AuthenticationInfo
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.authc.credential.DefaultPasswordService
+import org.apache.shiro.authc.credential.PasswordMatcher
+import org.apache.shiro.authc.credential.PasswordService
+import org.apache.shiro.authz.AuthorizationInfo
+import org.apache.shiro.authz.permission.AllPermission
+import org.apache.shiro.authz.permission.RolePermissionResolver
+import org.apache.shiro.authz.permission.WildcardPermission
+import org.apache.shiro.crypto.hash.Hash
+import org.apache.shiro.subject.PrincipalCollection
+import org.apache.shiro.subject.SimplePrincipalCollection
+import spock.lang.Specification
+import us.carrclan.david.authc4j.api.GroupInformation
+import us.carrclan.david.authc4j.api.ImmutableGroupInformation
+import us.carrclan.david.authc4j.api.ImmutableUserInformation
+import us.carrclan.david.authc4j.api.Password
+import us.carrclan.david.authc4j.api.UserInformation
+import us.carrclan.david.authc4j.api.UserInformationSource
+
+class GroupMappingAuthC4JRealmSpec extends Specification {
+    UserInformationSource userInfoSource
+    GroupMappingAuthC4JRealm realm
+
+    def setup() {
+        userInfoSource = Mock()
+        realm = new GroupMappingAuthC4JRealm(userInfoSource)
+    }
+
+    def "username password tokens are supported"() {
+        expect:
+        realm.authenticationTokenClass == UsernamePasswordToken
+        realm.supports(new UsernamePasswordToken())
+    }
+
+    def "realm has a default name"() {
+        expect:
+        realm.name != null
+        realm.name == "${realm.class.name}_1"
+    }
+
+    def "realm is nameable"() {
+        given:
+        String newRealmName = "myRealm"
+        UsernamePasswordToken token = new UsernamePasswordToken("someUser", "someGoodPass")
+        UserInformation userInfo = new ImmutableUserInformation(userInfoSource, token.username)
+        userInfoSource.authenticate(token.username, _ as Password) >> userInfo
+        when:
+        realm.name = newRealmName
+        then:
+        realm.name == "myRealm"
+        realm.getAuthenticationInfo(token).principals.realmNames == [newRealmName] as Set
+    }
+
+    def "failed authentication results returns null authentication info"() {
+        given:
+        UsernamePasswordToken token = new UsernamePasswordToken("someUser", "someBadPass")
+        userInfoSource.authenticate(token.username, _ as Password) >> null
+        when:
+        AuthenticationInfo authcInfo = realm.getAuthenticationInfo(token)
+        then:
+        authcInfo == null
+    }
+
+    def "successful authentication returns authentication based on user information"() {
+        given:
+        UsernamePasswordToken token = new UsernamePasswordToken("someUser", "someGoodPass")
+        Set<GroupInformation> groupInfo = [new ImmutableGroupInformation(userInfoSource, "SomeGroup")]
+        UserInformation userInfo = new ImmutableUserInformation(userInfoSource, token.username, "Some Cool User", groupInfo)
+        userInfoSource.authenticate(token.username, _ as Password) >> userInfo
+        when:
+        AuthenticationInfo authcInfo = realm.getAuthenticationInfo(token)
+        then:
+        authcInfo != null
+        authcInfo.principals.primaryPrincipal instanceof AuthC4JPrincipal
+        AuthC4JPrincipal principal = authcInfo.principals.primaryPrincipal
+        principal.shortName == userInfo.shortName
+        principal.longName == userInfo.longName
+        principal.groupShortNames == groupInfo.collect {it.shortName} as Set
+        authcInfo.credentials != null
+    }
+
+    def "hashed credentials are used when a hashing service is available"() {
+        given:
+        UsernamePasswordToken token = new UsernamePasswordToken("someUser", "someGoodPass")
+        UserInformation userInfo = new ImmutableUserInformation(userInfoSource, token.username)
+        userInfoSource.authenticate(token.username, _ as Password) >> userInfo
+        DefaultPasswordService defaultPasswordService = new DefaultPasswordService()
+        realm.credentialsMatcher = new PasswordMatcher(passwordService: defaultPasswordService)
+        when:
+        AuthenticationInfo authcInfo = realm.getAuthenticationInfo(token)
+        then:
+        authcInfo.credentials instanceof Hash
+        defaultPasswordService.passwordsMatch(token.credentials, authcInfo.credentials)
+    }
+
+    def "raw credentials are used when no hashing service is available"() {
+        given:
+        UsernamePasswordToken token = new UsernamePasswordToken("someUser", "someGoodPass")
+        UserInformation userInfo = new ImmutableUserInformation(userInfoSource, token.username)
+        userInfoSource.authenticate(token.username, _ as Password) >> userInfo
+        PasswordService mockPasswordService = [
+                encryptPassword: {it.toString()},
+                passwordsMatch: {plaintext, encrypted -> plaintext.toString() == encrypted}
+        ] as PasswordService
+        realm.credentialsMatcher = new PasswordMatcher(passwordService: mockPasswordService)
+        when:
+        AuthenticationInfo authcInfo = realm.getAuthenticationInfo(token)
+        then:
+        authcInfo.credentials instanceof char[]
+        authcInfo.credentials == token.credentials
+    }
+
+    def "permissions are applied based on mapped groups and role permission resolver"() {
+        given:
+        String adminRole = "admin"
+        String userRole = "user"
+        String billingRole = "billing"
+        GroupInformation adminGroup = new ImmutableGroupInformation(userInfoSource, "Administrators")
+        GroupInformation userGroup = new ImmutableGroupInformation(userInfoSource, "Users")
+        GroupInformation billingGroup = new ImmutableGroupInformation(userInfoSource, "Accountants")
+        realm.groupRolesMap = [
+                (adminGroup.shortName):adminRole, (userGroup.shortName):userRole, (billingGroup.shortName):billingRole
+        ]
+        realm.rolePermissionResolver = { String roleString ->
+            switch(roleString) {
+                case adminRole:
+                    return [new AllPermission()]
+                case userRole:
+                    return [new WildcardPermission("auth:login"), new WildcardPermission("auth:logout"),
+                            new WildcardPermission("home")]
+                case billingRole:
+                    return [new WildcardPermission("billing:*")]
+                default:
+                    return []
+            }
+        } as RolePermissionResolver
+        PrincipalCollection principals
+        when:
+        principals = newPrincipalCollection([])
+        then:
+        !realm.hasRole(principals, adminRole)
+        !realm.hasRole(principals, userRole)
+        !realm.hasRole(principals, billingRole)
+        !realm.isPermitted(principals, "admin:config")
+        !realm.isPermitted(principals, "auth:login")
+        !realm.isPermitted(principals, "billing:view")
+        when:
+        principals = newPrincipalCollection([adminGroup])
+        then:
+        realm.hasRole(principals, adminRole)
+        !realm.hasRole(principals, userRole)
+        !realm.hasRole(principals, billingRole)
+        realm.isPermitted(principals, "admin:config")
+        realm.isPermitted(principals, "auth:login")
+        realm.isPermitted(principals, "billing:view")
+        when:
+        principals = newPrincipalCollection([userGroup])
+        then:
+        !realm.hasRole(principals, adminRole)
+        realm.hasRole(principals, userRole)
+        !realm.hasRole(principals, billingRole)
+        !realm.isPermitted(principals, "admin:config")
+        realm.isPermitted(principals, "home")
+        realm.isPermitted(principals, "auth:login")
+        realm.isPermitted(principals, "auth:logout")
+        !realm.isPermitted(principals, "auth:changePassword")
+        !realm.isPermitted(principals, "billing:view")
+        when:
+        principals = newPrincipalCollection([billingGroup])
+        then:
+        !realm.hasRole(principals, adminRole)
+        !realm.hasRole(principals, userRole)
+        realm.hasRole(principals, billingRole)
+        !realm.isPermitted(principals, "admin:config")
+        !realm.isPermitted(principals, "auth:login")
+        realm.isPermitted(principals, "billing:view")
+        when:
+        principals = newPrincipalCollection([adminGroup, userGroup])
+        then:
+        realm.hasRole(principals, adminRole)
+        realm.hasRole(principals, userRole)
+        !realm.hasRole(principals, billingRole)
+        realm.isPermitted(principals, "admin:config")
+        realm.isPermitted(principals, "auth:login")
+        realm.isPermitted(principals, "billing:view")
+        when:
+        principals = newPrincipalCollection([userGroup, billingGroup])
+        then:
+        !realm.hasRole(principals, adminRole)
+        realm.hasRole(principals, userRole)
+        realm.hasRole(principals, billingRole)
+        !realm.isPermitted(principals, "admin:config")
+        realm.isPermitted(principals, "home")
+        realm.isPermitted(principals, "auth:login")
+        realm.isPermitted(principals, "auth:logout")
+        !realm.isPermitted(principals, "auth:changePassword")
+        realm.isPermitted(principals, "billing:view")
+    }
+
+    private def newPrincipalCollection(List<? extends GroupInformation> groups) {
+        def userInfo = new ImmutableUserInformation(userInfoSource, "someuser", null, groups as Set)
+        def principal = new AuthC4JPrincipal(userInfo)
+        return new SimplePrincipalCollection(principal, realm.name)
+    }
+}