From 687d6f19b9594db44beae8e99b8b36d07bf2b4e8 Mon Sep 17 00:00:00 2001 From: David Bosschaert Date: Mon, 12 Aug 2013 15:29:43 +0100 Subject: [PATCH] Add support for groups in security. A user can be a member of a group or have roles assigned directly. Groups typically have one or more roles assigned. Users that are part of that group will get these roles associated too. Users have the union of the roles associated with their groups together with their own roles. When logging in the user will get RolePrincipal objects for all of these. The user will also get a GroupPrincipal object associated for each group (s)he is in. Groups are currently implemented for the PropertiesLoginModule and PropertiesBackingEngine. They can be gradually added to other login modules too. This commit also contains shell commands to manage groups as well as a bunch of new tests. For example the following can be used to set up some users and groups: jaas:realm-manage --realm karaf jaas:group-add managergroup jaas:group-add --help jaas:user-add joe joe jaas:group-add joe managergroup jaas:group-role-add managergroup manager jaas:group-role-add managergroup viewer jaas:update jaas:realm-manage --realm karaf jaas:user-list This prints out (note that the admin group is configured by default): User Name | Group | Role ---------------------------------- karaf | admingroup | admin karaf | admingroup | manager karaf | admingroup | viewer joe | managergroup | manager joe | managergroup | viewer --- .../main/resources/resources/etc/users.properties | 4 +- .../karaf/jaas/boot/principal/GroupPrincipal.java | 42 ++-- .../karaf/jaas/boot/principal/RolePolicy.java | 19 +- jaas/command/pom.xml | 2 +- .../apache/karaf/jaas/command/GroupAddCommand.java | 57 ++++++ .../karaf/jaas/command/GroupDeleteCommand.java | 56 +++++ .../karaf/jaas/command/GroupRoleAddCommand.java | 56 +++++ .../karaf/jaas/command/GroupRoleDeleteCommand.java | 56 +++++ .../karaf/jaas/command/ListUsersCommand.java | 43 ++-- .../resources/OSGI-INF/blueprint/jaas-command.xml | 12 ++ .../apache/karaf/jaas/modules/BackingEngine.java | 41 +++- .../karaf/jaas/modules/jdbc/JDBCBackingEngine.java | 51 ++++- .../properties/PropertiesBackingEngine.java | 156 +++++++++----- .../modules/properties/PropertiesLoginModule.java | 29 ++- .../properties/PropertiesBackingEngineTest.java | 203 +++++++++++++++++++ .../properties/PropertiesLoginModuleTest.java | 213 ++++++++++++++++++++ 16 files changed, 926 insertions(+), 114 deletions(-) create mode 100644 jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupAddCommand.java create mode 100644 jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupDeleteCommand.java create mode 100644 jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleAddCommand.java create mode 100644 jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleDeleteCommand.java create mode 100644 jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngineTest.java diff --git a/assemblies/features/framework/src/main/resources/resources/etc/users.properties b/assemblies/features/framework/src/main/resources/resources/etc/users.properties index 10e7d17..f93f1fc 100644 --- a/assemblies/features/framework/src/main/resources/resources/etc/users.properties +++ b/assemblies/features/framework/src/main/resources/resources/etc/users.properties @@ -27,4 +27,6 @@ # and modifiable via the JAAS command group. These users reside in a JAAS domain # with the name "karaf".. # -karaf=karaf,admin +karaf = karaf,_g_:admingroup +_g_\:admingroup = group,admin,manager,viewer + diff --git a/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/GroupPrincipal.java b/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/GroupPrincipal.java index caa1faf..4ffb672 100644 --- a/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/GroupPrincipal.java +++ b/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/GroupPrincipal.java @@ -2,9 +2,9 @@ * 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. @@ -16,39 +16,39 @@ package org.apache.karaf.jaas.boot.principal; import java.security.Principal; -import java.security.acl.Group; -import java.util.Enumeration; -import java.util.Hashtable; -public class GroupPrincipal implements Group { +public class GroupPrincipal implements Principal { private String name; - private Hashtable members = new Hashtable(); public GroupPrincipal(String name) { + assert name != null; this.name = name; } - - public boolean addMember(Principal user) { - members.put(user.getName(), user); - return true; - } - public boolean removeMember(Principal user) { - members.remove(user.getName()); - return true; + public String getName() { + return name; } - public boolean isMember(Principal member) { - return members.contains(member.getName()); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GroupPrincipal)) return false; + + GroupPrincipal that = (GroupPrincipal) o; + + if (name != null ? !name.equals(that.name) : that.name != null) return false; + return true; } - public Enumeration members() { - return members.elements(); + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; } - public String getName() { - return name; + @Override + public String toString() { + return "GroupPrincipal[" + name + "]"; } } diff --git a/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/RolePolicy.java b/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/RolePolicy.java index 06d48cf..38dedad 100644 --- a/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/RolePolicy.java +++ b/jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/RolePolicy.java @@ -2,9 +2,9 @@ * 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. @@ -16,11 +16,11 @@ package org.apache.karaf.jaas.boot.principal; import java.security.Principal; -import java.security.acl.Group; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; + import javax.security.auth.Subject; public enum RolePolicy { @@ -36,19 +36,6 @@ public enum RolePolicy { } } } - }, - GROUP_ROLES("group") { - public void handleRoles(Subject subject,Set principals,String discriminator) { - Group group = new GroupPrincipal(discriminator); - for(Principal p:principals) { - if(p instanceof RolePrincipal) { - group.addMember(p); - } else { - subject.getPrincipals().add(p); - } - } - subject.getPrincipals().add(group); - } }; private String value; diff --git a/jaas/command/pom.xml b/jaas/command/pom.xml index ed43531..2aaac18 100644 --- a/jaas/command/pom.xml +++ b/jaas/command/pom.xml @@ -30,7 +30,7 @@ org.apache.karaf.jaas.command bundle - Apache Karaf :: Jaas :: Command + Apache Karaf :: JAAS :: Command This bundle provides Karaf shell commands to manipulate JAAS security framework. diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupAddCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupAddCommand.java new file mode 100644 index 0000000..37bb3a0 --- /dev/null +++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupAddCommand.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.karaf.jaas.command; + +import org.apache.karaf.jaas.modules.BackingEngine; +import org.apache.karaf.shell.commands.Argument; +import org.apache.karaf.shell.commands.Command; + +@Command(scope = "jaas", name = "group-add", description = "Make a user part of a group") +public class GroupAddCommand extends JaasCommandSupport { + @Argument(index = 0, name = "username", description = "User Name", required = true, multiValued = false) + private String username; + + @Argument(index = 1, name = "group", description = "Group", required = true, multiValued = false) + private String group; + + @Override + protected Object doExecute(BackingEngine engine) throws Exception { + engine.addGroup(username, group); + return null; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + @Override + public String toString() { + return "GroupAddCommand {username='" + username + "', group='" + group + "'}"; + } + +} diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupDeleteCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupDeleteCommand.java new file mode 100644 index 0000000..2ef007d --- /dev/null +++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupDeleteCommand.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.karaf.jaas.command; + +import org.apache.karaf.jaas.modules.BackingEngine; +import org.apache.karaf.shell.commands.Argument; +import org.apache.karaf.shell.commands.Command; + +@Command(scope = "jaas", name = "group-del", description = "Remove a user from a group") +public class GroupDeleteCommand extends JaasCommandSupport { + @Argument(index = 0, name = "username", description = "User Name", required = true, multiValued = false) + private String username; + + @Argument(index = 1, name = "group", description = "Group", required = true, multiValued = false) + private String group; + + @Override + protected Object doExecute(BackingEngine engine) throws Exception { + engine.deleteGroup(username, group); + return null; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + @Override + public String toString() { + return "GroupDeleteCommand {username='" + username + "', group='" + group + "'}"; + } +} diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleAddCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleAddCommand.java new file mode 100644 index 0000000..55e123c --- /dev/null +++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleAddCommand.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.karaf.jaas.command; + +import org.apache.karaf.jaas.modules.BackingEngine; +import org.apache.karaf.shell.commands.Argument; +import org.apache.karaf.shell.commands.Command; + +@Command(scope = "jaas", name = "group-role-add", description = "Add a role to a group") +public class GroupRoleAddCommand extends JaasCommandSupport { + @Argument(index = 0, name = "groupname", description = "Group Name", required = true, multiValued = false) + private String groupname; + + @Argument(index = 1, name = "role", description = "Role", required = true, multiValued = false) + private String role; + + @Override + protected Object doExecute(BackingEngine engine) throws Exception { + engine.addGroupRole(groupname, role); + return null; + } + + public String getGroupname() { + return groupname; + } + + public void setGroupname(String groupname) { + this.groupname = groupname; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + @Override + public String toString() { + return "GroupRoleAddCommand {groupname='" + groupname + "', role='" + role + "'}"; + } +} diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleDeleteCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleDeleteCommand.java new file mode 100644 index 0000000..c510068 --- /dev/null +++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/GroupRoleDeleteCommand.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.karaf.jaas.command; + +import org.apache.karaf.jaas.modules.BackingEngine; +import org.apache.karaf.shell.commands.Argument; +import org.apache.karaf.shell.commands.Command; + +@Command(scope = "jaas", name = "group-role-del", description = "Remove a role from a group") +public class GroupRoleDeleteCommand extends JaasCommandSupport { + @Argument(index = 0, name = "groupname", description = "Group Name", required = true, multiValued = false) + private String groupname; + + @Argument(index = 1, name = "role", description = "Role", required = true, multiValued = false) + private String role; + + @Override + protected Object doExecute(BackingEngine engine) throws Exception { + engine.deleteGroupRole(groupname, role); + return null; + } + + public String getGroupname() { + return groupname; + } + + public void setGroupname(String groupname) { + this.groupname = groupname; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + @Override + public String toString() { + return "GroupRoleDeleteCommand {groupname='" + groupname + "', role='" + role + "'}"; + } +} diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/ListUsersCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/ListUsersCommand.java index 7524303..00f5942 100644 --- a/jaas/command/src/main/java/org/apache/karaf/jaas/command/ListUsersCommand.java +++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/ListUsersCommand.java @@ -15,16 +15,20 @@ */ package org.apache.karaf.jaas.command; -import org.apache.karaf.shell.commands.Command; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import javax.security.auth.login.AppConfigurationEntry; + +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; import org.apache.karaf.jaas.boot.principal.RolePrincipal; import org.apache.karaf.jaas.boot.principal.UserPrincipal; import org.apache.karaf.jaas.config.JaasRealm; import org.apache.karaf.jaas.modules.BackingEngine; +import org.apache.karaf.shell.commands.Command; import org.apache.karaf.shell.table.ShellTable; -import javax.security.auth.login.AppConfigurationEntry; -import java.util.List; - @Command(scope = "jaas", name = "user-list", description = "List the users of the selected JAAS realm/login module") public class ListUsersCommand extends JaasCommandSupport { @@ -54,21 +58,21 @@ public class ListUsersCommand extends JaasCommandSupport { ShellTable table = new ShellTable(); table.column("User Name"); + table.column("Group"); table.column("Role"); for (UserPrincipal user : users) { + List reportedRoles = new ArrayList(); String userName = user.getName(); - List roles = engine.listRoles(user); - - if (roles != null && roles.size() >= 1) { - for (RolePrincipal role : roles) { - String roleName = role.getName(); - table.addRow().addContent(userName, roleName); - } - } else { - table.addRow().addContent(userName, ""); + + for (GroupPrincipal group : engine.listGroups(user)) { + String groupName = group.getName(); + reportedRoles.addAll(displayRole(engine, userName, groupName, group, table)); } + if (reportedRoles.size() == 0) { + table.addRow().addContent(userName, "", ""); + } } table.print(System.out); @@ -76,4 +80,17 @@ public class ListUsersCommand extends JaasCommandSupport { return null; } + private List displayRole(BackingEngine engine, String userName, String groupName, Principal principal, ShellTable table) { + List names = new ArrayList(); + List roles = engine.listRoles(principal); + + if (roles != null && roles.size() >= 1) { + for (RolePrincipal role : roles) { + String roleName = role.getName(); + names.add(roleName); + table.addRow().addContent(userName, groupName, roleName); + } + } + return names; + } } diff --git a/jaas/command/src/main/resources/OSGI-INF/blueprint/jaas-command.xml b/jaas/command/src/main/resources/OSGI-INF/blueprint/jaas-command.xml index e8ed566..9767676 100644 --- a/jaas/command/src/main/resources/OSGI-INF/blueprint/jaas-command.xml +++ b/jaas/command/src/main/resources/OSGI-INF/blueprint/jaas-command.xml @@ -63,6 +63,18 @@ + + + + + + + + + + + + diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java index 9c7be4d..1cbd3cc 100644 --- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java +++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java @@ -14,8 +14,10 @@ */ package org.apache.karaf.jaas.modules; +import java.security.Principal; import java.util.List; +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; import org.apache.karaf.jaas.boot.principal.RolePrincipal; import org.apache.karaf.jaas.boot.principal.UserPrincipal; @@ -43,12 +45,34 @@ public interface BackingEngine { List listUsers(); /** - * List Roles for {@param user}. + * List groups that a user is in + * @param user + * @return the groups that the user is in + */ + List listGroups(UserPrincipal user); + + /** + * Add a user to a group + * @param username + * @param group + */ + void addGroup(String username, String group); + + /** + * Remote a user from a group + * @param username + * @param group + */ + void deleteGroup(String username, String group); + + /** + * List Roles for {@param principal}. This could either be a + * {@link UserPrincipal} or a {@link GroupPrincipal}. * * @param user * @return */ - List listRoles(UserPrincipal user); + List listRoles(Principal principal); /** * Add a role to the user @@ -66,4 +90,17 @@ public interface BackingEngine { */ void deleteRole(String username, String role); + /** + * Add a role to a group + * @param groupname + * @param role + */ + void addGroupRole(String groupname, String role); + + /** + * Remote a role from a group + * @param groupname + * @param role + */ + void deleteGroupRole(String groupname, String role); } diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/jdbc/JDBCBackingEngine.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/jdbc/JDBCBackingEngine.java index 8dbda28..f0784ff 100644 --- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/jdbc/JDBCBackingEngine.java +++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/jdbc/JDBCBackingEngine.java @@ -16,21 +16,25 @@ package org.apache.karaf.jaas.modules.jdbc; -import org.apache.karaf.jaas.boot.principal.RolePrincipal; -import org.apache.karaf.jaas.boot.principal.UserPrincipal; -import org.apache.karaf.jaas.modules.BackingEngine; -import org.apache.karaf.jaas.modules.encryption.EncryptionSupport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; +import java.security.Principal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import javax.sql.DataSource; + +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; +import org.apache.karaf.jaas.boot.principal.RolePrincipal; +import org.apache.karaf.jaas.boot.principal.UserPrincipal; +import org.apache.karaf.jaas.modules.BackingEngine; +import org.apache.karaf.jaas.modules.encryption.EncryptionSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class JDBCBackingEngine implements BackingEngine { private final Logger logger = LoggerFactory.getLogger(JDBCBackingEngine.class); @@ -221,7 +225,7 @@ public class JDBCBackingEngine implements BackingEngine { * @param user * @return */ - public List listRoles(UserPrincipal user) { + public List listRoles(Principal user) { List roles = new ArrayList(); Connection connection = null; @@ -406,4 +410,33 @@ public class JDBCBackingEngine implements BackingEngine { this.selectRolesQuery = selectRolesQuery; } + @Override + public List listGroups(UserPrincipal user) { + // Support for groups will need to be added. + return Collections.emptyList(); + } + + @Override + public void addGroup(String userName, String group) { + // Support for groups will need to be added. + throw new UnsupportedOperationException(); + } + + @Override + public void deleteGroup(String userName, String group) { + // Support for groups will need to be added. + throw new UnsupportedOperationException(); + } + + @Override + public void addGroupRole(String groupname, String role) { + // Support for groups will need to be added. + throw new UnsupportedOperationException(); + } + + @Override + public void deleteGroupRole(String groupname, String role) { + // Support for groups will need to be added. + throw new UnsupportedOperationException(); + } } diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngine.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngine.java index 0370e4b..4e9f496 100644 --- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngine.java +++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngine.java @@ -16,7 +16,12 @@ package org.apache.karaf.jaas.modules.properties; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + import org.apache.felix.utils.properties.Properties; +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; import org.apache.karaf.jaas.boot.principal.RolePrincipal; import org.apache.karaf.jaas.boot.principal.UserPrincipal; import org.apache.karaf.jaas.modules.BackingEngine; @@ -24,12 +29,9 @@ import org.apache.karaf.jaas.modules.encryption.EncryptionSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - public class PropertiesBackingEngine implements BackingEngine { + static final String GROUP_PREFIX = "_g_:"; private static final transient Logger LOGGER = LoggerFactory.getLogger(PropertiesBackingEngine.class); private Properties users; @@ -49,13 +51,15 @@ public class PropertiesBackingEngine implements BackingEngine { this.encryptionSupport = encryptionSupport; } - /** - * Add a user. - * - * @param username - * @param password - */ + @Override public void addUser(String username, String password) { + if (username.startsWith(GROUP_PREFIX)) + throw new IllegalArgumentException("Prefix not permitted: " + GROUP_PREFIX); + + addUserInternal(username, password); + } + + private void addUserInternal(String username, String password) { String[] infos = null; StringBuffer userInfoBuffer = new StringBuffer(); @@ -72,7 +76,7 @@ public class PropertiesBackingEngine implements BackingEngine { } } - String userInfos = (String) users.get(username); + String userInfos = users.get(username); //If user already exists, update password if (userInfos != null && userInfos.length() > 0) { @@ -96,12 +100,13 @@ public class PropertiesBackingEngine implements BackingEngine { } } - /** - * Delete a User. - * - * @param username - */ + @Override public void deleteUser(String username) { + // Delete all its groups first, for garbage collection of the groups + for (GroupPrincipal gp : listGroups(username)) { + deleteGroup(username, gp.getName()); + } + users.remove(username); try { @@ -111,45 +116,54 @@ public class PropertiesBackingEngine implements BackingEngine { } } - /** - * List Users - * - * @return - */ + @Override public List listUsers() { List result = new ArrayList(); - for (String userNames : (Set) users.keySet()) { - UserPrincipal userPrincipal = new UserPrincipal(userNames); + for (String userName : users.keySet()) { + if (userName.startsWith(GROUP_PREFIX)) + continue; + + UserPrincipal userPrincipal = new UserPrincipal(userName); result.add(userPrincipal); } return result; } - /** - * List the Roles of the {@param user} - * - * @param user - * @return - */ - public List listRoles(UserPrincipal user) { + @Override + public List listRoles(Principal principal) { + String userName = principal.getName(); + if (principal instanceof GroupPrincipal) { + userName = GROUP_PREFIX + userName; + } + return listRoles(userName); + } + + private List listRoles(String name) { List result = new ArrayList(); - String userInfo = (String) users.get(user.getName()); + String userInfo = users.get(name); String[] infos = userInfo.split(","); for (int i = 1; i < infos.length; i++) { - result.add(new RolePrincipal(infos[i])); + String roleName = infos[i]; + if (roleName.startsWith(GROUP_PREFIX)) { + for (RolePrincipal rp : listRoles(roleName)) { + if (!result.contains(rp)) { + result.add(rp); + } + } + } else { + RolePrincipal rp = new RolePrincipal(roleName); + if (!result.contains(rp)) { + result.add(rp); + } + } } return result; } - /** - * Add a role to a User. - * - * @param username - * @param role - */ + @Override public void addRole(String username, String role) { - String userInfos = (String) users.get(username); + String userInfos = users.get(username); if (userInfos != null) { String newUserInfos = userInfos + "," + role; users.put(username, newUserInfos); @@ -161,17 +175,12 @@ public class PropertiesBackingEngine implements BackingEngine { } } - /** - * Delete a Role form User. - * - * @param username - * @param role - */ + @Override public void deleteRole(String username, String role) { String[] infos = null; StringBuffer userInfoBuffer = new StringBuffer(); - String userInfos = (String) users.get(username); + String userInfos = users.get(username); //If user already exists, remove the role if (userInfos != null && userInfos.length() > 0) { @@ -196,4 +205,59 @@ public class PropertiesBackingEngine implements BackingEngine { } } + @Override + public List listGroups(UserPrincipal user) { + String userName = user.getName(); + return listGroups(userName); + } + + private List listGroups(String userName) { + List result = new ArrayList(); + String userInfo = users.get(userName); + String[] infos = userInfo.split(","); + for (int i = 1; i < infos.length; i++) { + String name = infos[i]; + if (name.startsWith(GROUP_PREFIX)) { + result.add(new GroupPrincipal(name.substring(GROUP_PREFIX.length()))); + } + } + return result; + } + + @Override + public void addGroup(String username, String group) { + String groupName = GROUP_PREFIX + group; + if (users.get(groupName) == null) { + addUserInternal(groupName, "group"); + } + addRole(username, groupName); + } + + @Override + public void deleteGroup(String username, String group) { + deleteRole(username, GROUP_PREFIX + group); + + // Garbage collection, clean up the groups if needed + for (UserPrincipal user : listUsers()) { + for (GroupPrincipal g : listGroups(user)) { + if (group.equals(g.getName())) { + // There is another user of this group, nothing to clean up + return; + } + } + } + + // Nobody is using this group any more, remove it... + deleteUser(GROUP_PREFIX + group); + } + + @Override + public void addGroupRole(String groupname, String role) { + addRole(GROUP_PREFIX + groupname, role); + } + + @Override + public void deleteGroupRole(String groupname, String role) { + deleteRole(GROUP_PREFIX + groupname, role); + } } diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModule.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModule.java index 681a24c..865abc3 100644 --- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModule.java +++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModule.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.security.Principal; import java.util.HashSet; import java.util.Map; + import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -31,6 +32,7 @@ import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import org.apache.felix.utils.properties.Properties; +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; import org.apache.karaf.jaas.boot.principal.RolePrincipal; import org.apache.karaf.jaas.boot.principal.UserPrincipal; import org.apache.karaf.jaas.modules.AbstractKarafLoginModule; @@ -44,7 +46,7 @@ public class PropertiesLoginModule extends AbstractKarafLoginModule { private static final transient Logger LOGGER = LoggerFactory.getLogger(PropertiesLoginModule.class); - private static final String USER_FILE = "users"; + static final String USER_FILE = "users"; private String usersFile; @@ -84,6 +86,11 @@ public class PropertiesLoginModule extends AbstractKarafLoginModule { } // user callback get value user = ((NameCallback) callbacks[0]).getName(); + if (user.startsWith(PropertiesBackingEngine.GROUP_PREFIX)) { + // You cannot log in under a group name + throw new FailedLoginException("login failed"); + } + // password callback get value String password = new String(((PasswordCallback) callbacks[1]).getPassword()); @@ -91,7 +98,7 @@ public class PropertiesLoginModule extends AbstractKarafLoginModule { String userInfos = null; try { - userInfos = (String) users.get(user); + userInfos = users.get(user); } catch (NullPointerException e) { //error handled in the next statement } @@ -102,11 +109,11 @@ public class PropertiesLoginModule extends AbstractKarafLoginModule { throw new FailedLoginException("User " + user + " does not exist"); } } - + // the password is in the first position String[] infos = userInfos.split(","); String storedPassword = infos[0]; - + // check if the stored password is flagged as encrypted String encryptedPassword = getEncryptedPassword(storedPassword); if (!storedPassword.equals(encryptedPassword)) { @@ -155,7 +162,19 @@ public class PropertiesLoginModule extends AbstractKarafLoginModule { principals = new HashSet(); principals.add(new UserPrincipal(user)); for (int i = 1; i < infos.length; i++) { - principals.add(new RolePrincipal(infos[i])); + if (infos[i].startsWith(PropertiesBackingEngine.GROUP_PREFIX)) { + // It's a group reference + principals.add(new GroupPrincipal(infos[i].substring(PropertiesBackingEngine.GROUP_PREFIX.length()))); + String groupInfo = users.get(infos[i]); + if (groupInfo != null) { + String[] roles = groupInfo.split(","); + for (int j = 1; j < roles.length; j++) { + principals.add(new RolePrincipal(roles[j])); + } + } + } else { + principals.add(new RolePrincipal(infos[i])); + } } users.clear(); diff --git a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngineTest.java b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngineTest.java new file mode 100644 index 0000000..ddf0097 --- /dev/null +++ b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesBackingEngineTest.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.karaf.jaas.modules.properties; + + +import java.io.File; +import java.io.IOException; + +import junit.framework.TestCase; + +import org.apache.felix.utils.properties.Properties; +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; +import org.apache.karaf.jaas.boot.principal.RolePrincipal; +import org.apache.karaf.jaas.boot.principal.UserPrincipal; + +public class PropertiesBackingEngineTest extends TestCase { + + public void testUserRoles() throws IOException { + File f = File.createTempFile(getClass().getName(), ".tmp"); + try { + Properties p = new Properties(f); + + PropertiesBackingEngine engine = new PropertiesBackingEngine(p); + engine.addUser("a", "aa"); + assertEquals(1, engine.listUsers().size()); + UserPrincipal upa = engine.listUsers().iterator().next(); + assertEquals("a", upa.getName()); + engine.addUser("b", "bb"); + + engine.addRole("a", "role1"); + engine.addRole("a", "role2"); + assertEquals(2, engine.listRoles(upa).size()); + + boolean foundR1 = false; + boolean foundR2 = false; + for (RolePrincipal rp : engine.listRoles(upa)) { + if ("role1".equals(rp.getName())) { + foundR1 = true; + } else if ("role2".equals(rp.getName())) { + foundR2 = true; + } + } + assertTrue(foundR1); + assertTrue(foundR2); + + engine.addGroup("a", "g"); + engine.addGroupRole("g", "role2"); + engine.addGroupRole("g", "role3"); + engine.addGroup("b", "g"); + engine.addGroup("b", "g2"); + engine.addGroupRole("g2", "role4"); + + assertEquals(2, engine.listUsers().size()); + UserPrincipal upa_1 = null; + UserPrincipal upb_1 = null; + for (UserPrincipal u : engine.listUsers()) { + if ("a".equals(u.getName())) { + upa_1 = u; + } else if ("b".equals(u.getName())) { + upb_1 = u; + } + } + assertNotNull(upa_1); + assertNotNull(upb_1); + + assertEquals(3, engine.listRoles(upa).size()); + boolean foundR1_2 = false; + boolean foundR2_2 = false; + boolean foundR3_2 = false; + for (RolePrincipal rp : engine.listRoles(upa)) { + if ("role1".equals(rp.getName())) { + foundR1_2 = true; + } else if ("role2".equals(rp.getName())) { + foundR2_2 = true; + } else if ("role3".equals(rp.getName())) { + foundR3_2 = true; + } + } + assertTrue(foundR1_2); + assertTrue(foundR2_2); + assertTrue(foundR3_2); + + // Check that the loading works + PropertiesBackingEngine engine2 = new PropertiesBackingEngine(new Properties(f)); + assertEquals(2, engine2.listUsers().size()); + UserPrincipal upa_2 = null; + UserPrincipal upb_2 = null; + for (UserPrincipal u : engine2.listUsers()) { + if ("a".equals(u.getName())) { + upa_2 = u; + } else if ("b".equals(u.getName())) { + upb_2 = u; + } + } + assertNotNull(upa_2); + assertNotNull(upb_2); + assertEquals(3, engine2.listRoles(upa_2).size()); + boolean foundR1_3 = false; + boolean foundR2_3 = false; + boolean foundR3_3 = false; + for (RolePrincipal rp : engine2.listRoles(upa_2)) { + if ("role1".equals(rp.getName())) { + foundR1_3 = true; + } else if ("role2".equals(rp.getName())) { + foundR2_3 = true; + } else if ("role3".equals(rp.getName())) { + foundR3_3 = true; + } + } + assertTrue(foundR1_3); + assertTrue(foundR2_3); + assertTrue(foundR3_3); + assertEquals(3, engine2.listRoles(upb_2).size()); + boolean foundR2_4 = false; + boolean foundR3_4 = false; + boolean foundR4_4 = false; + for (RolePrincipal rp : engine2.listRoles(upb_2)) { + if ("role2".equals(rp.getName())) { + foundR2_4 = true; + } else if ("role3".equals(rp.getName())) { + foundR3_4 = true; + } else if ("role4".equals(rp.getName())) { + foundR4_4 = true; + } + } + assertTrue(foundR2_4); + assertTrue(foundR3_4); + assertTrue(foundR4_4); + + // Let's start removing some things... + UserPrincipal upb = null; + for (UserPrincipal up : engine.listUsers()) { + if ("b".equals(up.getName())) { + upb = up; + } + } + assertEquals(1, engine.listGroups(upa).size()); + assertEquals(2, engine.listGroups(upb).size()); + + GroupPrincipal gp = engine.listGroups(upa).iterator().next(); + engine.deleteGroupRole("g", "role2"); + assertEquals(1, engine.listRoles(gp).size()); + assertEquals("role3", engine.listRoles(gp).iterator().next().getName()); + + // Check that the user roles are reported correctly + assertEquals("role2 should still be there as it was added to the user directly too", + 3, engine.listRoles(upa).size()); + boolean foundR1_5 = false; + boolean foundR2_5 = false; + boolean foundR3_5 = false; + for (RolePrincipal rp : engine.listRoles(upa)) { + if ("role1".equals(rp.getName())) { + foundR1_5 = true; + } else if ("role2".equals(rp.getName())) { + foundR2_5 = true; + } else if ("role3".equals(rp.getName())) { + foundR3_5 = true; + } + } + assertTrue(foundR1_5); + assertTrue(foundR2_5); + assertTrue(foundR3_5); + assertEquals(2, engine.listRoles(upb).size()); + boolean foundR3_6 = false; + boolean foundR4_6 = false; + for (RolePrincipal rp : engine.listRoles(upb)) { + if ("role3".equals(rp.getName())) { + foundR3_6 = true; + } else if ("role4".equals(rp.getName())) { + foundR4_6 = true; + } + } + assertTrue(foundR3_6); + assertTrue(foundR4_6); + + engine.deleteGroup("b", "g"); + engine.deleteGroup("b", "g2"); + assertEquals(0, engine.listRoles(upb).size()); + engine.deleteUser("b"); + + engine.deleteUser("a"); + assertEquals("Properties should be empty now", 0, p.size()); + } finally { + if (!f.delete()) { + fail("Could not delete temporary file: " + f); + } + } + } +} diff --git a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModuleTest.java b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModuleTest.java index af465dc..d271669 100644 --- a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModuleTest.java +++ b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/properties/PropertiesLoginModuleTest.java @@ -1,20 +1,233 @@ package org.apache.karaf.jaas.modules.properties; +import java.io.File; import java.io.IOException; +import java.security.Principal; import java.util.HashMap; import java.util.Map; import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import junit.framework.Assert; +import org.apache.felix.utils.properties.Properties; +import org.apache.karaf.jaas.boot.principal.GroupPrincipal; +import org.apache.karaf.jaas.boot.principal.RolePrincipal; +import org.apache.karaf.jaas.boot.principal.UserPrincipal; import org.junit.Test; public class PropertiesLoginModuleTest { @Test + public void testBasicLogin() throws Exception { + File f = File.createTempFile(getClass().getName(), ".tmp"); + try { + Properties p = new Properties(f); + PropertiesBackingEngine pbe = new PropertiesBackingEngine(p); + pbe.addUser("abc", "xyz"); + pbe.addRole("abc", "myrole"); + pbe.addUser("pqr", "abc"); + + PropertiesLoginModule module = new PropertiesLoginModule(); + Map options = new HashMap(); + options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath()); + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback cb : callbacks) { + if (cb instanceof NameCallback) { + ((NameCallback) cb).setName("abc"); + } else if (cb instanceof PasswordCallback) { + ((PasswordCallback) cb).setPassword("xyz".toCharArray()); + } + } + } + }; + Subject subject = new Subject(); + module.initialize(subject, cb, null, options); + + Assert.assertEquals("Precondition", 0, subject.getPrincipals().size()); + Assert.assertTrue(module.login()); + Assert.assertTrue(module.commit()); + + Assert.assertEquals(2, subject.getPrincipals().size()); + boolean foundUser = false; + boolean foundRole = false; + for (Principal pr : subject.getPrincipals()) { + if (pr instanceof UserPrincipal) { + Assert.assertEquals("abc", pr.getName()); + foundUser = true; + } else if (pr instanceof RolePrincipal) { + Assert.assertEquals("myrole", pr.getName()); + foundRole = true; + } + } + Assert.assertTrue(foundUser); + Assert.assertTrue(foundRole); + + Assert.assertTrue(module.logout()); + Assert.assertEquals("Principals should be gone as the user has logged out", 0, subject.getPrincipals().size()); + } finally { + if (!f.delete()) { + Assert.fail("Could not delete temporary file: " + f); + } + } + } + + @Test + public void testLoginIncorrectPassword() throws Exception { + File f = File.createTempFile(getClass().getName(), ".tmp"); + try { + Properties p = new Properties(f); + PropertiesBackingEngine pbe = new PropertiesBackingEngine(p); + pbe.addUser("abc", "xyz"); + pbe.addUser("pqr", "abc"); + + PropertiesLoginModule module = new PropertiesLoginModule(); + Map options = new HashMap(); + options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath()); + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback cb : callbacks) { + if (cb instanceof NameCallback) { + ((NameCallback) cb).setName("abc"); + } else if (cb instanceof PasswordCallback) { + ((PasswordCallback) cb).setPassword("abc".toCharArray()); + } + } + } + }; + module.initialize(new Subject(), cb, null, options); + try { + module.login(); + Assert.fail("The login should have failed as the passwords didn't match"); + } catch (FailedLoginException fle) { + // good + } + } finally { + if (!f.delete()) { + Assert.fail("Could not delete temporary file: " + f); + } + } + } + + @Test + public void testLoginWithGroups() throws Exception { + File f = File.createTempFile(getClass().getName(), ".tmp"); + try { + Properties p = new Properties(f); + PropertiesBackingEngine pbe = new PropertiesBackingEngine(p); + pbe.addUser("abc", "xyz"); + pbe.addRole("abc", "myrole"); + pbe.addUser("pqr", "abc"); + pbe.addGroup("pqr", "group1"); + pbe.addGroupRole("group1", "r1"); + + PropertiesLoginModule module = new PropertiesLoginModule(); + Map options = new HashMap(); + options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath()); + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback cb : callbacks) { + if (cb instanceof NameCallback) { + ((NameCallback) cb).setName("pqr"); + } else if (cb instanceof PasswordCallback) { + ((PasswordCallback) cb).setPassword("abc".toCharArray()); + } + } + } + }; + Subject subject = new Subject(); + module.initialize(subject, cb, null, options); + + Assert.assertEquals("Precondition", 0, subject.getPrincipals().size()); + Assert.assertTrue(module.login()); + Assert.assertTrue(module.commit()); + + Assert.assertEquals(3, subject.getPrincipals().size()); + boolean foundUser = false; + boolean foundRole = false; + boolean foundGroup = false; + for (Principal pr : subject.getPrincipals()) { + if (pr instanceof UserPrincipal) { + Assert.assertEquals("pqr", pr.getName()); + foundUser = true; + } else if (pr instanceof GroupPrincipal) { + Assert.assertEquals("group1", pr.getName()); + foundGroup = true; + } else if (pr instanceof RolePrincipal) { + Assert.assertEquals("r1", pr.getName()); + foundRole = true; + } + } + Assert.assertTrue(foundUser); + Assert.assertTrue(foundGroup); + Assert.assertTrue(foundRole); + } finally { + if (!f.delete()) { + Assert.fail("Could not delete temporary file: " + f); + } + } + } + + // This is a fairly important test that ensures that you cannot log in under the name of a + // group directly. + @Test + public void testCannotLoginAsGroupDirectly() throws Exception { + testCannotLoginAsGroupDirectly("group1"); + testCannotLoginAsGroupDirectly("_g_:group1"); + testCannotLoginAsGroupDirectly(PropertiesBackingEngine.GROUP_PREFIX + "group1"); + } + + private void testCannotLoginAsGroupDirectly(final String name) throws IOException, LoginException { + File f = File.createTempFile(getClass().getName(), ".tmp"); + try { + Properties p = new Properties(f); + PropertiesBackingEngine pbe = new PropertiesBackingEngine(p); + pbe.addUser("abc", "xyz"); + pbe.addRole("abc", "myrole"); + pbe.addUser("pqr", "abc"); + pbe.addGroup("pqr", "group1"); + pbe.addGroupRole("group1", "r1"); + + PropertiesLoginModule module = new PropertiesLoginModule(); + Map options = new HashMap(); + options.put(PropertiesLoginModule.USER_FILE, f.getAbsolutePath()); + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback cb : callbacks) { + if (cb instanceof NameCallback) { + ((NameCallback) cb).setName(name); + } else if (cb instanceof PasswordCallback) { + ((PasswordCallback) cb).setPassword("group".toCharArray()); + } + } + } + }; + module.initialize(new Subject(), cb, null, options); + try { + module.login(); + Assert.fail("The login should have failed as you cannot log in under a group name directly"); + } catch (FailedLoginException fle) { + // good + } + } finally { + if (!f.delete()) { + Assert.fail("Could not delete temporary file: " + f); + } + } + } + + @Test public void testNullUsersFile() { try { testWithUsersFile(null); -- 1.7.10.2 (Apple Git-33)