From b4c6dd1eff6062eaa435be2dfb8da7bf8c3be731 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 13 Feb 2015 16:44:51 +0100 Subject: [PATCH] moved to the use of permissions instead of roles for the AdminConsole; however, this will currently disable the effects of role ADMIN --- .../META-INF/MANIFEST.MF | 3 +- .../adminconsole/AdminConsoleFeatures.java | 10 +- .../gwt/adminconsole/AdminConsolePanel.java | 14 +- .../security/shared/WildcardPermission.java | 252 ++++++++++++++++++ .../ui/client/component/UserDetailsView.java | 11 +- .../component/UserListDataProvider.java | 1 - .../client/component/UserManagementPanel.java | 6 +- .../ui/server/UserManagementServiceImpl.java | 6 +- .../sap/sse/security/ui/shared/UserDTO.java | 55 ++-- 9 files changed, 302 insertions(+), 56 deletions(-) create mode 100755 java/com.sap.sse.security.common/src/com/sap/sse/security/shared/WildcardPermission.java diff --git a/java/com.sap.sse.gwt.adminconsole/META-INF/MANIFEST.MF b/java/com.sap.sse.gwt.adminconsole/META-INF/MANIFEST.MF index 7bdbb366278..0fa8c2cf366 100644 --- a/java/com.sap.sse.gwt.adminconsole/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.gwt.adminconsole/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Require-Bundle: com.google.gwt.osgi;bundle-version="2.7.0", com.sap.sse.gwt, com.sap.sse.security.ui, com.sap.sse.security, - com.sap.sse.common + com.sap.sse.common, + com.sap.sse.security.common Export-Package: com.sap.sse.gwt.adminconsole Bundle-ActivationPolicy: lazy diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsoleFeatures.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsoleFeatures.java index 9b57feda0e6..777a13c7c2e 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsoleFeatures.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsoleFeatures.java @@ -11,17 +11,17 @@ import com.sap.sse.common.impl.NamedImpl; */ public class AdminConsoleFeatures extends NamedImpl { private static final long serialVersionUID = 7441608351911944793L; - private String[] enabledRoles; + private String requiredPermission; - public AdminConsoleFeatures(String name, String... enabledRoles) { + public AdminConsoleFeatures(String name, String requiredPermission) { super(name); - this.enabledRoles = enabledRoles; + this.requiredPermission = requiredPermission; } /** * If the user has at least one of these roles, he/she is permitted to use this feature. */ - public String[] getEnabledRoles() { - return enabledRoles; + public String getRequiredPermission() { + return requiredPermission; } } diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java index 5a069641414..b4a6c44845e 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java @@ -273,23 +273,11 @@ public class AdminConsolePanel extends DockLayoutPanel { for (Map.Entry, AdminConsoleFeatures> e : roleSpecificTabs .entrySet()) { final Widget panelToAdd = e.getKey().getB(); - if (user != null && isUserInRole(e.getValue().getEnabledRoles())) { + if (user != null && user.hasPermission(e.getValue().getRequiredPermission())) { e.getKey().getA().add(panelToAdd, e.getKey().getC(), /* asHtml */false); } else { e.getKey().getA().remove(panelToAdd); } } } - - private boolean isUserInRole(String... roles) { - boolean result = false; - UserDTO user = getUserService().getCurrentUser(); - for (String enabledRole : roles) { - if (user.hasRole(enabledRole)) { - result = true; - break; - } - } - return result; - } } diff --git a/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/WildcardPermission.java b/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/WildcardPermission.java new file mode 100755 index 00000000000..ee88454a0e9 --- /dev/null +++ b/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/WildcardPermission.java @@ -0,0 +1,252 @@ +/* + * 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 com.sap.sse.security.shared; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * A WildcardPermission is a very flexible permission construct supporting multiple levels of + * permission matching. However, most people will probably follow some standard conventions as explained below. + *

+ *

Simple Usage

+ *

+ * In the simplest form, WildcardPermission can be used as a simple permission string. You could grant a + * user an "editNewsletter" permission and then check to see if the user has the editNewsletter + * permission by calling + *

+ * subject.isPermitted("editNewsletter") + *

+ * This is (mostly) equivalent to + *

+ * subject.isPermitted( new WildcardPermission("editNewsletter") ) + *

+ * but more on that later. + *

+ * The simple permission string may work for simple applications, but it requires you to have permissions like + * "viewNewsletter", "deleteNewsletter", + * "createNewsletter", etc. You can also grant a user "*" permissions + * using the wildcard character (giving this class its name), which means they have all permissions. But + * using this approach there's no way to just say a user has "all newsletter permissions". + *

+ * For this reason, WildcardPermission supports multiple levels of permissioning. + *

+ *

Multiple Levels

+ *

+ * WildcardPermission also supports the concept of multiple levels. For example, you could + * restructure the previous simple example by granting a user the permission "newsletter:edit". + * The colon in this example is a special character used by the WildcardPermission that delimits the + * next token in the permission. + *

+ * In this example, the first token is the domain that is being operated on + * and the second token is the action being performed. Each level can contain multiple values. So you + * could simply grant a user the permission "newsletter:view,edit,create" which gives them + * access to perform view, edit, and create actions in the newsletter + * domain. Then you could check to see if the user has the "newsletter:create" + * permission by calling + *

+ * subject.isPermitted("newsletter:create") + *

+ * (which would return true). + *

+ * In addition to granting multiple permissions via a single string, you can grant all permission for a particular + * level. So if you wanted to grant a user all actions in the newsletter domain, you could simply give + * them "newsletter:*". Now, any permission check for "newsletter:XXX" + * will return true. It is also possible to use the wildcard token at the domain level (or both): so you + * could grant a user the "view" action across all domains "*:view". + *

+ *

Instance-level Access Control

+ *

+ * Another common usage of the WildcardPermission is to model instance-level Access Control Lists. + * In this scenario you use three tokens - the first is the domain, the second is the action, and + * the third is the instance you are acting on. + *

+ * So for example you could grant a user "newsletter:edit:12,13,18". In this example, assume + * that the third token is the system's ID of the newsletter. That would allow the user to edit newsletters + * 12, 13, and 18. This is an extremely powerful way to express permissions, + * since you can now say things like "newsletter:*:13" (grant a user all actions for newsletter + * 13), "newsletter:view,create,edit:*" (allow the user to + * view, create, or edit any newsletter), or + * "newsletter:*:* (allow the user to perform any action on any newsletter). + *

+ * To perform checks against these instance-level permissions, the application should include the instance ID in the + * permission check like so: + *

+ * subject.isPermitted( "newsletter:edit:13" ) + *

+ * There is no limit to the number of tokens that can be used, so it is up to your imagination in terms of ways that + * this could be used in your application. However, the Shiro team likes to standardize some common usages shown + * above to help people get started and provide consistency in the Shiro community.

+ * + * This class has been slightly adjusted from the original Shiro version such that it no longer depends on any Shiro + * classes but still offers the same services that the Shiro WildcardPermission does. This way, the + * permission can also be used by a GWT client and an Android implementation. + */ +public class WildcardPermission implements Serializable { + + //TODO - JavaDoc methods + + /** + * + */ + private static final long serialVersionUID = -7136806951296823464L; + /*-------------------------------------------- + | C O N S T A N T S | + ============================================*/ + protected static final String WILDCARD_TOKEN = "*"; + protected static final String PART_DIVIDER_TOKEN = ":"; + protected static final String SUBPART_DIVIDER_TOKEN = ","; + protected static final boolean DEFAULT_CASE_SENSITIVE = false; + + /*-------------------------------------------- + | I N S T A N C E V A R I A B L E S | + ============================================*/ + private List> parts; + + /*-------------------------------------------- + | C O N S T R U C T O R S | + ============================================*/ + /** + * Default no-arg constructor for subclasses only - end-user developers instantiating Permission instances must + * provide a wildcard string at a minimum, since Permission instances are immutable once instantiated. + *

+ * Note that the WildcardPermission class is very robust and typically subclasses are not necessary unless you + * wish to create type-safe Permission objects that would be used in your application, such as perhaps a + * {@code UserPermission}, {@code SystemPermission}, {@code PrinterPermission}, etc. If you want such type-safe + * permission usage, consider subclassing the {@link DomainPermission DomainPermission} class for your needs. + */ + protected WildcardPermission() { + } + + public WildcardPermission(String wildcardString) { + this(wildcardString, DEFAULT_CASE_SENSITIVE); + } + + public WildcardPermission(String wildcardString, boolean caseSensitive) { + setParts(wildcardString, caseSensitive); + } + + protected void setParts(String wildcardString) { + setParts(wildcardString, DEFAULT_CASE_SENSITIVE); + } + + protected void setParts(String wildcardString, boolean caseSensitive) { + if (wildcardString == null || wildcardString.trim().length() == 0) { + throw new IllegalArgumentException("Wildcard string cannot be null or empty. Make sure permission strings are properly formatted."); + } + + wildcardString = wildcardString.trim(); + + List parts = Arrays.asList(wildcardString.split(PART_DIVIDER_TOKEN)); + + this.parts = new ArrayList>(); + for (String part : parts) { + Set subparts = new HashSet<>(Arrays.asList(part.split(SUBPART_DIVIDER_TOKEN))); + if (!caseSensitive) { + subparts = lowercase(subparts); + } + if (subparts.isEmpty()) { + throw new IllegalArgumentException("Wildcard string cannot contain parts with only dividers. Make sure permission strings are properly formatted."); + } + this.parts.add(subparts); + } + + if (this.parts.isEmpty()) { + throw new IllegalArgumentException("Wildcard string cannot contain only dividers. Make sure permission strings are properly formatted."); + } + } + + private Set lowercase(Set subparts) { + Set lowerCasedSubparts = new LinkedHashSet(subparts.size()); + for (String subpart : subparts) { + lowerCasedSubparts.add(subpart.toLowerCase()); + } + return lowerCasedSubparts; + } + + /*-------------------------------------------- + | A C C E S S O R S / M O D I F I E R S | + ============================================*/ + protected List> getParts() { + return this.parts; + } + + /*-------------------------------------------- + | M E T H O D S | + ============================================*/ + + public boolean implies(WildcardPermission wp) { + // By default only supports comparisons with other WildcardPermissions + List> otherParts = wp.getParts(); + + int i = 0; + for (Set otherPart : otherParts) { + // If this permission has less parts than the other permission, everything after the number of parts contained + // in this permission is automatically implied, so return true + if (getParts().size() - 1 < i) { + return true; + } else { + Set part = getParts().get(i); + if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) { + return false; + } + i++; + } + } + + // If this permission has more parts than the other parts, only imply it if all of the other parts are wildcards + for (; i < getParts().size(); i++) { + Set part = getParts().get(i); + if (!part.contains(WILDCARD_TOKEN)) { + return false; + } + } + + return true; + } + + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (Set part : parts) { + if (buffer.length() > 0) { + buffer.append(":"); + } + buffer.append(part); + } + return buffer.toString(); + } + + public boolean equals(Object o) { + if (o instanceof WildcardPermission) { + WildcardPermission wp = (WildcardPermission) o; + return parts.equals(wp.parts); + } + return false; + } + + public int hashCode() { + return parts.hashCode(); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserDetailsView.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserDetailsView.java index 6ee94c0fe1e..45331f2ca45 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserDetailsView.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserDetailsView.java @@ -53,7 +53,7 @@ public class UserDetailsView extends FlowPanel { private final VerticalPanel accountPanels; private UserDTO user; - public UserDetailsView(final UserService userService, UserDTO user, final StringMessages stringMessages) { + public UserDetailsView(final UserService userService, UserDTO user, final StringMessages stringMessages, final UserListDataProvider userListDataProvider) { final UserManagementServiceAsync userManagementService = userService.getUserManagementService(); this.stringMessages = stringMessages; this.user = user; @@ -85,7 +85,7 @@ public class UserDetailsView extends FlowPanel { if (!result.isSuccessful()) { Window.alert(stringMessages.errorUpdatingRoles(selectedUser.getName(), result.getMessage())); } else { - selectedUser.setRoles(newRoleList); + userListDataProvider.updateDisplays(); if (userService.getCurrentUser().getName().equals(selectedUser.getName())) { // if the current user's roles changed, update the user object in the user service and notify others userService.updateUser(/* notify other instances */ true); @@ -95,7 +95,8 @@ public class UserDetailsView extends FlowPanel { })); } }); - permissionsEditor = new StringListEditorComposite(user==null?Collections.emptySet():user.getPermissions(), stringMessages, com.sap.sse.gwt.client.IconResources.INSTANCE.removeIcon(), defaultPermissionNames, + permissionsEditor = new StringListEditorComposite(user==null?Collections.emptySet():user.getStringPermissions(), stringMessages, + com.sap.sse.gwt.client.IconResources.INSTANCE.removeIcon(), defaultPermissionNames, stringMessages.enterPermissionName()); permissionsEditor.addValueChangeHandler(new ValueChangeHandler>() { @Override @@ -114,7 +115,7 @@ public class UserDetailsView extends FlowPanel { if (!result.isSuccessful()) { Window.alert(stringMessages.errorUpdatingPermissions(selectedUser.getName(), result.getMessage())); } else { - selectedUser.setPermissions(newPermissionList); + userListDataProvider.updateDisplays(); if (userService.getCurrentUser().getName().equals(selectedUser.getName())) { // if the current user's permissions changed, update the user object in the user service and notify others userService.updateUser(/* notify other instances */ true); @@ -240,7 +241,7 @@ public class UserDetailsView extends FlowPanel { accountPanels.add(accountPanelDecorator); } rolesEditor.setValue(user.getRoles(), /* fireEvents */ false); - permissionsEditor.setValue(user.getPermissions(), /* fireEvents */ false); + permissionsEditor.setValue(user.getStringPermissions(), /* fireEvents */ false); } } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserListDataProvider.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserListDataProvider.java index e9d7b37c1ab..1b610f25013 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserListDataProvider.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserListDataProvider.java @@ -76,6 +76,5 @@ public class UserListDataProvider extends AbstractDataProvider { for (HasData hd : getDataDisplays()) { onRangeChanged(hd); } -// //TODO [D056866] SelectionChangeEvent.fire(singleSelectionModel); } } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java index ec1f706e1bc..ae04b102da6 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java @@ -80,7 +80,9 @@ public class UserManagementPanel extends DockPanel { deleteButton.setEnabled(singleSelectionModel.getSelectedObject() != null); final UserList userList = new UserList(); userList.setSelectionModel(singleSelectionModel); - final UserDetailsView userDetailsView = new UserDetailsView(userService, singleSelectionModel.getSelectedObject(), stringMessages); + TextBox filterBox = new TextBox(); + userListDataProvider = new UserListDataProvider(userManagementService, filterBox); + final UserDetailsView userDetailsView = new UserDetailsView(userService, singleSelectionModel.getSelectedObject(), stringMessages, userListDataProvider); add(userDetailsView, DockPanel.CENTER); userDetailsView.addUserChangeEventHandler(new UserChangeEventHandler() { @Override @@ -96,8 +98,6 @@ public class UserManagementPanel extends DockPanel { } }); userList.setPageSize(20); - TextBox filterBox = new TextBox(); - userListDataProvider = new UserListDataProvider(userManagementService, filterBox); userListDataProvider.addDataDisplay(userList); SimplePager pager = new SimplePager(TextLocation.CENTER, false, /* fast forward step size */ 50, true); pager.setDisplay(userList); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java index a04ae8a236d..a82e319f250 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java @@ -272,7 +272,7 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U } } - private UserDTO createUserDTOFromUser(User user){ + private UserDTO createUserDTOFromUser(User user) { UserDTO userDTO; Map accounts = user.getAllAccounts(); List accountDTOs = new ArrayList<>(); @@ -288,9 +288,7 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U break; } } - userDTO = new UserDTO(user.getName(), user.getEmail(), user.isEmailValidated(), accountDTOs); - userDTO.addRoles(user.getRoles()); - userDTO.addPermissions(user.getPermissions()); + userDTO = new UserDTO(user.getName(), user.getEmail(), user.isEmailValidated(), accountDTOs, user.getRoles(), user.getPermissions()); return userDTO; } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java index de53f0b7ffd..07ed428082f 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java @@ -1,27 +1,35 @@ package com.sap.sse.security.ui.shared; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import com.google.gwt.user.client.rpc.IsSerializable; import com.sap.sse.common.Util; +import com.sap.sse.security.shared.WildcardPermission; public class UserDTO implements IsSerializable { private String name; private String email; private List accounts; - private Set roles = new HashSet<>(); - private Set permissions = new HashSet<>(); + private Set roles; + private Set permissions; private boolean emailValidated; UserDTO() {} // for serialization only - public UserDTO(String name, String email, boolean emailValidated, List accounts) { + public UserDTO(String name, String email, boolean emailValidated, List accounts, Iterable roles, Iterable stringPermissions) { this.name = name; this.email = email; this.emailValidated = emailValidated; this.accounts = accounts; + this.roles = new HashSet<>(); + Util.addAll(roles, this.roles); + this.permissions = new HashSet<>(); + for (String stringPermission : stringPermissions) { + this.permissions.add(new WildcardPermission(stringPermission)); + } } public String getName() { @@ -36,32 +44,31 @@ public class UserDTO implements IsSerializable { return roles.contains(role); } - public void addRoles(Iterable roles) { - Util.addAll(roles, this.roles); - } - - public void setRoles(Iterable newRoleList) { - this.roles.clear(); - Util.addAll(newRoleList, this.roles); - } - - public Iterable getPermissions() { + public Iterable getPermissions() { return permissions; } - public boolean hasPermission(String permission) { - return permissions.contains(permission); - } - - public void addPermissions(Iterable permissions) { - Util.addAll(permissions, this.permissions); - } - - public void setPermissions(Iterable newPermissionList) { - this.permissions.clear(); - Util.addAll(newPermissionList, this.permissions); + public Iterable getStringPermissions() { + List result = new ArrayList<>(); + for (WildcardPermission wp : getPermissions()) { + result.add(wp.toString()); + } + return result; } + public boolean hasPermission(String permission) { + return hasPermission(new WildcardPermission(permission)); + } + + public boolean hasPermission(WildcardPermission permission) { + for (WildcardPermission p : permissions) { + if (p.implies(permission)) { + return true; + } + } + return false; + } + public List getAccounts() { return accounts; }