From 398b8a279c4989a347a4d6b2b99f428e07864c94 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 6 Jul 2023 21:57:03 +0200 Subject: [PATCH] [JENKINS-68755] Add support for explicit user and group assignment (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding support to explicitly assign a role to a user or a group. This avoids confusion when a name matches both. A warning will be shown on the role assignment page for ambiguous entries with the possibility to make this a user or a group assignment. ❗ This is an incompatible change and after migration, going back to the previous version will require to undo the config changes. While the changes to the config.xml can be read by older versions, you can run into problems with duplicate entries. Changes done on Configuration As Code yaml files need to be reverted to the old format before downgrading. The new version can read existing configurations files (both from the config.xml and Casc). After startup of Jenkins with the new plugin version, the config.xml will be automatically saved with the new format. The plugin APIs have significantly changed. Plugins that depend on role-strategy will probably need to be adjusted as well or might not work as expected (ownership-plugin, dynamic_extended_choice_parameter). --- pom.xml | 299 ++++----- .../rolestrategy/AuthorizationType.java | 61 ++ .../plugins/rolestrategy/PermissionEntry.java | 136 +++++ .../RoleBasedAuthorizationStrategy.java | 578 +++++++++++++----- .../hudson/plugins/rolestrategy/RoleMap.java | 248 ++++++-- .../plugins/rolestrategy/ValidationUtil.java | 159 ++++- .../plugins/rolestrategy/IMacroExtension.java | 8 +- .../macros/BuildableJobMacro.java | 4 +- .../macros/ContainedInViewMacro.java | 3 +- .../rolestrategy/macros/FolderMacro.java | 3 +- .../rolestrategy/macros/StubMacro.java | 3 +- .../AmbiguousSidsAdminMonitor.java | 65 ++ .../RoleBasedProjectNamingStrategy.java | 39 +- .../plugins/rolestrategy/Settings.java | 2 +- .../rolestrategy/casc/GrantedRoles.java | 7 +- ...asedAuthorizationStrategyConfigurator.java | 44 +- .../rolestrategy/casc/RoleDefinition.java | 155 ++++- .../plugins/rolestrategy/Messages.properties | 1 + .../assign-agent-roles.jelly | 43 +- .../assign-global-roles.jelly | 47 +- .../assign-project-roles.jelly | 47 +- .../RoleStrategyConfig/assign-roles.jelly | 103 +++- .../assign-roles.properties | 37 ++ .../RoleStrategyConfig/index.jelly | 16 +- ...oles_fr.properties => index_fr.properties} | 0 .../description.jelly | 4 + .../description.properties | 1 + .../AmbiguousSidsAdminMonitor/message.jelly | 25 + src/main/webapp/css/role-strategy.css | 51 +- src/main/webapp/js/tableAssign.js | 114 +++- src/main/webapp/js/tableManage.js | 6 +- .../hudson/plugins/rolestrategy/ApiTest.java | 161 ++++- .../java/jmh/benchmarks/CascBenchmark.java | 3 +- .../jmh/benchmarks/FolderAccessBenchmark.java | 16 +- .../jmh/benchmarks/PermissionBenchmark.java | 4 +- .../java/jmh/benchmarks/RoleMapBenchmark.java | 13 +- .../rolestrategy/ConfigurationAsCodeTest.java | 5 +- .../RoleBasedProjectNamingStrategyTest.java | 49 +- .../rolestrategy/Security2374Test.java | 160 +++++ .../rolestrategy/UserGroupSeparationTest.java | 108 ++++ .../AuthorizeProjectTest/config.xml | 6 +- .../Configuration-as-Code-Export.yml | 32 +- .../Configuration-as-Code-Macro.yml | 38 +- .../Configuration-as-Code-Naming.yml | 36 +- ...guration-as-Code-no-permissions-export.yml | 38 +- .../Configuration-as-Code-no-permissions.yml | 38 +- .../rolestrategy/Configuration-as-Code.yml | 22 +- .../rolestrategy/Configuration-as-Code2.yml | 8 +- .../rolestrategy/Configuration-as-Code3.yml | 8 +- .../plugins/rolestrategy/OwnershipTest.yml | 24 +- .../dangerousPermissionsAreIgnored/config.xml | 6 +- .../testRoleAssignment/config.xml | 4 +- .../rolestrategy/Security2182Test/config.xml | 8 +- .../rolestrategy/Security2374Test/casc.yaml | 22 + .../test3xDataMigration/config.xml | 83 +++ .../admin_1229880828125156033/config.xml | 43 ++ .../markus_7995840758412173137/config.xml | 37 ++ .../test3xDataMigration/users/users.xml | 14 + .../testRoleAuthority/config.xml | 2 +- .../UserGroupSeparationTest/config.xml | 50 ++ 60 files changed, 2669 insertions(+), 678 deletions(-) create mode 100644 src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/AuthorizationType.java create mode 100644 src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/PermissionEntry.java create mode 100644 src/main/java/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor.java create mode 100644 src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.properties rename src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/{manage-roles_fr.properties => index_fr.properties} (100%) create mode 100644 src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/message.jelly create mode 100644 src/test/java/org/jenkinsci/plugins/rolestrategy/Security2374Test.java create mode 100644 src/test/java/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest.java create mode 100644 src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/casc.yaml create mode 100644 src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/config.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/admin_1229880828125156033/config.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/markus_7995840758412173137/config.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/users.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest/config.xml diff --git a/pom.xml b/pom.xml index 7338f3b0..d15535c1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,128 +1,129 @@ - 4.0.0 + 4.0.0 - - org.jenkins-ci.plugins - plugin - 4.69 - - + + org.jenkins-ci.plugins + plugin + 4.69 + + - role-strategy - ${changelist} - hpi + role-strategy + ${changelist} + hpi - Role-based Authorization Strategy - https://github.com/jenkinsci/role-strategy-plugin - - - MIT License - https://opensource.org/licenses/MIT - repo - - + Role-based Authorization Strategy + https://github.com/jenkinsci/role-strategy-plugin + + + MIT License + https://opensource.org/licenses/MIT + repo + + - - - tmaurel - Thomas Maurel - +1 - - - rseguy - Romain Seguy - +1 - - - Oleg Nenashev - oleg_nenashev - o.v.nenashev@gmail.com - - + + + tmaurel + Thomas Maurel + +1 + + + rseguy + Romain Seguy + +1 + + + Oleg Nenashev + oleg_nenashev + o.v.nenashev@gmail.com + + - - scm:git:https://github.com/jenkinsci/${project.artifactId}-plugin.git - scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git - https://github.com/jenkinsci/${project.artifactId}-plugin - ${scmTag} - + + scm:git:https://github.com/jenkinsci/${project.artifactId}-plugin.git + scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git + https://github.com/jenkinsci/${project.artifactId}-plugin + ${scmTag} + - - 999999-SNAPSHOT - 2.387.3 - 10.12.1 - + + 999999-SNAPSHOT + 2.387.3 + 10.12.1 + 640 + - - - - io.jenkins.tools.bom - bom-2.387.x - 2198.v39c76fc308ca - pom - import - - - - - - io.jenkins.plugins - ionicons-api - - - io.jenkins.plugins - caffeine-api - - - io.jenkins - configuration-as-code - true - - - org.jenkins-ci.plugins - cloudbees-folder - true - - - io.jenkins.configuration-as-code - test-harness - test - - - com.synopsys.jenkinsci - ownership - 0.13.0 - test - - - org.apache.commons - commons-lang3 - 3.12.0 - test - - - org.jenkins-ci.plugins - authorize-project - 1.7.0 - test - + + + + io.jenkins.tools.bom + bom-2.387.x + 2198.v39c76fc308ca + pom + import + + + + + io.jenkins.plugins + ionicons-api + + + io.jenkins.plugins + caffeine-api + + + io.jenkins + configuration-as-code + true + + + org.jenkins-ci.plugins + cloudbees-folder + true + + + io.jenkins.configuration-as-code + test-harness + test + + + com.synopsys.jenkinsci + ownership + 0.13.0 + test + + + org.apache.commons + commons-lang3 + 3.12.0 + test + + + org.jenkins-ci.plugins + authorize-project + 1.7.0 + test + + - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - + org.apache.maven.plugins @@ -146,42 +147,42 @@ - org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - - - ${project.basedir}/.build-config/checkstyle.xml - ${project.basedir}/.build-config/checkstyle-suppressions.xml - checkstyle.suppressions.file - true - true - - - - compile-checkstyle - - checkstyle - - compile - - - test-check - - check - - test - - warning - - - + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + ${project.basedir}/.build-config/checkstyle.xml + ${project.basedir}/.build-config/checkstyle-suppressions.xml + checkstyle.suppressions.file + true + true + + + + compile-checkstyle + + checkstyle + + compile + + + test-check + + check + + test + + warning + + + diff --git a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/AuthorizationType.java b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/AuthorizationType.java new file mode 100644 index 00000000..0f10f4f3 --- /dev/null +++ b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/AuthorizationType.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * + * Copyright (c) 2021 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.michelin.cio.hudson.plugins.rolestrategy; + +/** + * The type of object being granted authorization. + */ +public enum AuthorizationType { + + USER("User"), + GROUP("Group"), + /** + * Either type is being granted permissions. + * This is the legacy default. + */ + EITHER("User/Group"); + + private final String description; + + private AuthorizationType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + /** + * The prefix used in the persistence of an permission entry. + * + * @return prefix + */ + public String toPrefix() { + if (this == AuthorizationType.EITHER) { + return ""; // Same as legacy format + } + return this + ":"; + } +} diff --git a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/PermissionEntry.java b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/PermissionEntry.java new file mode 100644 index 00000000..3d33932f --- /dev/null +++ b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/PermissionEntry.java @@ -0,0 +1,136 @@ +/* + * The MIT License + * + * Copyright (c) 2021 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.michelin.cio.hudson.plugins.rolestrategy; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Combines sid with {@link AuthorizationType type}. + */ +public class PermissionEntry implements Comparable { + private final AuthorizationType type; + private final String sid; + + @DataBoundConstructor + public PermissionEntry(@NonNull AuthorizationType type, @NonNull String sid) { + this.type = type; + this.sid = sid; + } + + public AuthorizationType getType() { + return type; + } + + public String getSid() { + return sid; + } + + /** + * Utility method checking whether this entry applies based on whether we're looking for a principal. + */ + protected boolean isApplicable(boolean principal) { + if (getType() == AuthorizationType.EITHER) { + return true; + } + return getType() == (principal ? AuthorizationType.USER : AuthorizationType.GROUP); + } + + /** + * Creates a {@code PermissionEntry} from a string. + * + * @param permissionEntryString String from which to create the entry + * @return the PermissinoEntry + */ + @Restricted(NoExternalUse.class) + @CheckForNull + public static PermissionEntry fromString(@NonNull String permissionEntryString) { + Objects.requireNonNull(permissionEntryString); + int idx = permissionEntryString.indexOf(':'); + if (idx < 0) { + return null; + } + String typeString = permissionEntryString.substring(0, idx); + AuthorizationType type; + try { + type = AuthorizationType.valueOf(typeString); + } catch (RuntimeException ex) { + return null; + } + String sid = permissionEntryString.substring(idx + 1); + + if (sid.isEmpty()) { + return null; + } + + return new PermissionEntry(type, sid); + } + + public static PermissionEntry user(String sid) { + return new PermissionEntry(AuthorizationType.USER, sid); + } + + public static PermissionEntry group(String sid) { + return new PermissionEntry(AuthorizationType.GROUP, sid); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PermissionEntry that = (PermissionEntry) o; + return type == that.type && sid.equals(that.sid); + } + + @Override + public int hashCode() { + return Objects.hash(type, sid); + } + + @Override + public String toString() { + return "PermissionEntry{" + + "type=" + type + + ", sid='" + sid + "'" + + '}'; + } + + @Override + public int compareTo(PermissionEntry o) { + int typeCompare = this.type.compareTo(o.type); + if (typeCompare == 0) { + return this.sid.compareTo(o.sid); + } + return typeCompare; + } +} diff --git a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy.java b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy.java index 3e7a9e44..9e1f0160 100644 --- a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy.java +++ b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy.java @@ -25,6 +25,9 @@ package com.michelin.cio.hudson.plugins.rolestrategy; +import static com.michelin.cio.hudson.plugins.rolestrategy.ValidationUtil.formatNonExistentUserGroupValidationResponse; +import static com.michelin.cio.hudson.plugins.rolestrategy.ValidationUtil.formatUserGroupValidationResponse; + import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; @@ -37,7 +40,8 @@ import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.Functions; -import hudson.Util; +import hudson.init.InitMilestone; +import hudson.init.Initializer; import hudson.model.AbstractItem; import hudson.model.Computer; import hudson.model.Descriptor; @@ -46,7 +50,6 @@ import hudson.model.Job; import hudson.model.Node; import hudson.model.Run; -import hudson.model.User; import hudson.model.View; import hudson.scm.SCM; import hudson.security.ACL; @@ -55,7 +58,6 @@ import hudson.security.PermissionGroup; import hudson.security.SecurityRealm; import hudson.security.SidACL; -import hudson.security.UserMayOrMayNotExistException2; import hudson.util.FormValidation; import java.io.IOException; import java.io.Writer; @@ -69,6 +71,8 @@ import java.util.Map; import java.util.Set; import java.util.SortedMap; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; @@ -77,6 +81,7 @@ import net.sf.json.JSONObject; import org.acegisecurity.acls.sid.PrincipalSid; import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.rolestrategy.AmbiguousSidsAdminMonitor; import org.jenkinsci.plugins.rolestrategy.permissions.PermissionHelper; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; @@ -87,8 +92,6 @@ import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.verb.GET; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; /** * Role-based authorization strategy. @@ -97,6 +100,8 @@ */ public class RoleBasedAuthorizationStrategy extends AuthorizationStrategy { + private static Logger LOGGER = Logger.getLogger(RoleBasedAuthorizationStrategy.class.getName()); + public static final String GLOBAL = "globalRoles"; public static final String PROJECT = "projectRoles"; public static final String SLAVE = "slaveRoles"; @@ -204,12 +209,18 @@ public ACL getACL(@NonNull Node node) { @NonNull public Collection getGroups() { Set sids = new HashSet<>(); - sids.addAll(globalRoles.getSids(true)); - sids.addAll(itemRoles.getSids(true)); - sids.addAll(agentRoles.getSids(true)); + + sids.addAll(filterRoleSids(globalRoles)); + sids.addAll(filterRoleSids(itemRoles)); + sids.addAll(filterRoleSids(agentRoles)); return sids; } + private Set filterRoleSids(RoleMap roleMap) { + return roleMap.getSidEntries(false).stream().filter(entry -> entry.getType() != AuthorizationType.USER) + .map(PermissionEntry::getSid).collect(Collectors.toSet()); + } + /** * Get the roles from the global {@link RoleMap}. *

The returned sorted map is unmodifiable. @@ -217,12 +228,12 @@ public Collection getGroups() { * * @param type The object type controlled by the {@link RoleMap} * @return All roles from the global {@link RoleMap}. - * @deprecated Use {@link RoleBasedAuthorizationStrategy#getGrantedRoles(RoleType)} + * @deprecated Use {@link RoleBasedAuthorizationStrategy#getGrantedRolesEntries(RoleType)} */ @Nullable @Deprecated public SortedMap> getGrantedRoles(String type) { - return getGrantedRoles(RoleType.fromString(type)); + return getRoleMap(RoleType.fromString(type)).getGrantedRoles(); } /** @@ -231,17 +242,53 @@ public SortedMap> getGrantedRoles(String type) { * @param type the type of the role * @return roles mapped to the set of user sids assigned to that role * @since 2.12 + * @deprecated use {@link #getGrantedRolesEntries(RoleType)} */ + @Deprecated public SortedMap> getGrantedRoles(@NonNull RoleType type) { return getRoleMap(type).getGrantedRoles(); } + /** + * Get the {@link Role}s and the sids assigned to them for the given {@link RoleType}. + * + * @param type the type of the role + * @return roles mapped to the set of user sids assigned to that role + */ + public SortedMap> getGrantedRolesEntries(@NonNull String type) { + return getGrantedRolesEntries(RoleType.fromString(type)); + } + + /** + * Get the {@link Role}s and the sids assigned to them for the given {@link RoleType}. + * + * @param type the type of the role + * @return roles mapped to the set of user sids assigned to that role + */ + public SortedMap> getGrantedRolesEntries(@NonNull RoleType type) { + return getRoleMap(type).getGrantedRolesEntries(); + } + + /** * Get all the SIDs referenced by specified {@link RoleMap} type. * * @param type The object type controlled by the {@link RoleMap} * @return All SIDs from the specified {@link RoleMap}. */ + public Set getSidEntries(String type) { + return getRoleMap(RoleType.fromString(type)).getSidEntries(); + } + + + /** + * Get all the SIDs referenced by specified {@link RoleMap} type. + * + * @param type The object type controlled by the {@link RoleMap} + * @return All SIDs from the specified {@link RoleMap}. + * @deprecated use {@link #getSidEntries(String)} + */ + @Deprecated @CheckForNull @SuppressWarnings("checkstyle:AbbreviationAsWordInName") public Set getSIDs(String type) { @@ -281,13 +328,31 @@ private void addRole(RoleType roleType, Role role) { * @param role The role to assign * @param sid The sid to assign to */ - private void assignRole(RoleType type, Role role, String sid) { + private void assignRole(RoleType type, Role role, PermissionEntry sid) { RoleMap roleMap = getRoleMap(type); if (roleMap.hasRole(role)) { roleMap.assignRole(role, sid); } } + private static void persistChanges() throws IOException { + Jenkins j = instance(); + j.save(); + AuthorizationStrategy as = j.getAuthorizationStrategy(); + if (as instanceof RoleBasedAuthorizationStrategy) { + RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) as; + rbas.validateConfig(); + } + } + + private static Jenkins instance() { + return Jenkins.get(); + } + + private static void checkAdminPerm() { + instance().checkPermission(Jenkins.ADMINISTER); + } + /** * API method to add a role. * @@ -338,63 +403,6 @@ public void doAddRole(@QueryParameter(required = true) String type, persistChanges(); } - /** - * API method to get the granted permissions of a role and the SIDs assigned to it. - * - *

- * Example: {@code curl -XGET 'http://localhost:8080/jenkins/role-strategy/strategy/getRole - * ?type=globalRoles&roleName=admin'} - * - *

- * Returns json with granted permissions and assigned sids.
- * Example: - * - *

{@code
-   *   {
-   *     "permissionIds": {
-   *         "hudson.model.Hudson.Read":true,
-   *         "hudson.model.Item.Read":true,
-   *         "hudson.model.Item.Build":true,
-   *      },
-   *      "sids": ["user1", "group1"]
-   *   }
-   * }
-   * 
- * - * - * @param type (globalRoles, projectRoles, slaveRoles) - * @param roleName name of role (single, no list) - * @throws IOException In case write response failed - * @since 2.8.3 - */ - @GET - @Restricted(NoExternalUse.class) - public void doGetRole(@QueryParameter(required = true) String type, - @QueryParameter(required = true) String roleName) throws IOException { - checkAdminPerm(); - JSONObject responseJson = new JSONObject(); - RoleMap roleMap = getRoleMap(RoleType.fromString(type)); - Role role = roleMap.getRole(roleName); - if (role != null) { - Set permissions = role.getPermissions(); - Map permissionsMap = new HashMap<>(); - for (Permission permission : permissions) { - permissionsMap.put(permission.getId(), permission.getEnabled()); - } - responseJson.put("permissionIds", permissionsMap); - if (!type.equals(RoleBasedAuthorizationStrategy.GLOBAL)) { - responseJson.put("pattern", role.getPattern().pattern()); - } - Map> grantedRoleMap = roleMap.getGrantedRoles(); - responseJson.put("sids", grantedRoleMap.get(role)); - } - - Stapler.getCurrentResponse().setContentType("application/json;charset=UTF-8"); - Writer writer = Stapler.getCurrentResponse().getCompressedWriter(Stapler.getCurrentRequest()); - responseJson.write(writer); - writer.close(); - } - /** * API method to remove roles. * @@ -425,8 +433,9 @@ public void doRemoveRoles(@QueryParameter(required = true) String type, @QueryPa } /** - * API method to assign SID to role. + * API method to assign a SID of type EITHER to role. * + * This method should no longer be used. *

* Example: * {@code curl -X POST localhost:8080/role-strategy/strategy/assignRole --data "type=globalRoles&roleName=ADM @@ -437,7 +446,9 @@ public void doRemoveRoles(@QueryParameter(required = true) String type, @QueryPa * @param sid user ID (single, no list) * @throws IOException in case saving changes fails * @since 2.5.0 + * @deprecated Use {@link #doAssignUserRole} or {@link #doAssignGroupRole} to create unambiguous entries */ + @Deprecated @RequirePOST @Restricted(NoExternalUse.class) public void doAssignRole(@QueryParameter(required = true) String type, @@ -447,32 +458,77 @@ public void doAssignRole(@QueryParameter(required = true) String type, final RoleType roleType = RoleType.fromString(type); Role role = getRoleMap(roleType).getRole(roleName); if (role != null) { - assignRole(roleType, role, sid); + assignRole(roleType, role, new PermissionEntry(AuthorizationType.EITHER, sid)); } persistChanges(); } - private static void persistChanges() throws IOException { - instance().save(); - } - - private static Jenkins instance() { - return Jenkins.get(); + /** + * API method to assign a User to role. + * + *

+ * Example: + * {@code curl -X POST localhost:8080/role-strategy/strategy/assignUserRole --data "type=globalRoles&roleName=ADM + * &user=username"} + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param roleName name of role (single, no list) + * @param user user ID (single, no list) + * @throws IOException in case saving changes fails + * @since TODO + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doAssignUserRole(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String roleName, + @QueryParameter(required = true) String user) throws IOException { + checkAdminPerm(); + final RoleType roleType = RoleType.fromString(type); + Role role = getRoleMap(roleType).getRole(roleName); + if (role != null) { + assignRole(roleType, role, new PermissionEntry(AuthorizationType.USER, user)); + } + persistChanges(); } - private static void checkAdminPerm() { - instance().checkPermission(Jenkins.ADMINISTER); + /** + * API method to assign a Group to role. + * + *

+ * Example: + * {@code curl -X POST localhost:8080/role-strategy/strategy/assignGroupRole --data "type=globalRoles&roleName=ADM + * &group=groupname"} + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param roleName name of role (single, no list) + * @param group group ID (single, no list) + * @throws IOException in case saving changes fails + * @since TODO + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doAssignGroupRole(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String roleName, + @QueryParameter(required = true) String group) throws IOException { + checkAdminPerm(); + final RoleType roleType = RoleType.fromString(type); + Role role = getRoleMap(roleType).getRole(roleName); + if (role != null) { + assignRole(roleType, role, new PermissionEntry(AuthorizationType.GROUP, group)); + } + persistChanges(); } /** * API method to delete a SID from all granted roles. + * Only SIDS of type EITHER with the given name will be deleted. * *

* Example: * {@code curl -X POST localhost:8080/role-strategy/strategy/deleteSid --data "type=globalRoles&sid=username"} * * @param type (globalRoles, projectRoles, slaveRoles) - * @param sid user ID to remove + * @param sid user/group ID to remove * @throws IOException in case saving changes fails * @since 2.4.1 */ @@ -481,12 +537,58 @@ private static void checkAdminPerm() { public void doDeleteSid(@QueryParameter(required = true) String type, @QueryParameter(required = true) String sid) throws IOException { checkAdminPerm(); - getRoleMap(RoleType.fromString(type)).deleteSids(sid); + getRoleMap(RoleType.fromString(type)).deleteSids(new PermissionEntry(AuthorizationType.EITHER, sid)); + persistChanges(); + } + + /** + * API method to delete a user from all granted roles. + * + *

+ * Example: + * {@code curl -X POST localhost:8080/role-strategy/strategy/deleteUser --data "type=globalRoles&user=username"} + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param user user ID to remove + * @throws IOException in case saving changes fails + * @since 2.4.1 + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doDeleteUser(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String user) throws IOException { + checkAdminPerm(); + getRoleMap(RoleType.fromString(type)).deleteSids(new PermissionEntry(AuthorizationType.USER, user)); + persistChanges(); + } + + /** + * API method to delete a group from all granted roles. + * + *

+ * Example: + * {@code curl -X POST localhost:8080/role-strategy/strategy/deleteGroup --data "type=globalRoles&group=groupname"} + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param group group ID to remove + * @throws IOException in case saving changes fails + * @since 2.4.1 + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doDeleteGroup(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String group) throws IOException { + checkAdminPerm(); + getRoleMap(RoleType.fromString(type)).deleteSids(new PermissionEntry(AuthorizationType.GROUP, group)); persistChanges(); } /** * API method to remove a SID from a role. + * Only entries of type EITHER will be removed. + * + * use {@link #doUnassignUserRole(String, String, String)} or {@link #doUnassignGroupRole(String, String, String)} to unassign a + * User or a Group. * *

* Example: @@ -496,6 +598,7 @@ public void doDeleteSid(@QueryParameter(required = true) String type, * @param roleName unassign role with sid * @param sid user ID to remove * @throws IOException in case saving changes fails + * * @since 2.6.0 */ @RequirePOST @@ -507,11 +610,124 @@ public void doUnassignRole(@QueryParameter(required = true) String type, RoleMap roleMap = getRoleMap(RoleType.fromString(type)); Role role = roleMap.getRole(roleName); if (role != null) { - roleMap.deleteRoleSid(sid, role.getName()); + roleMap.deleteRoleSid(new PermissionEntry(AuthorizationType.EITHER, sid), role.getName()); + } + persistChanges(); + } + + /** + * API method to remove a user from a role. + * + *

+ * Example: + * {@code curl -X POST localhost:8080/role-strategy/strategy/unassignUserRole --data + * "type=globalRoles&roleName=AMD&user=username"} + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param roleName unassign role with sid + * @param user user ID to remove + * @throws IOException in case saving changes fails + * @since TODO + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doUnassignUserRole(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String roleName, + @QueryParameter(required = true) String user) throws IOException { + checkAdminPerm(); + RoleMap roleMap = getRoleMap(RoleType.fromString(type)); + Role role = roleMap.getRole(roleName); + if (role != null) { + roleMap.deleteRoleSid(new PermissionEntry(AuthorizationType.USER, user), role.getName()); + } + persistChanges(); + } + + /** + * API method to remove a user from a role. + * + *

+ * Example: + * {@code curl -X POST localhost:8080/role-strategy/strategy/unassignGroupRole --data + * "type=globalRoles&roleName=AMD&user=username"} + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param roleName unassign role with sid + * @param group user ID to remove + * @throws IOException in case saving changes fails + * @since TODO + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doUnassignGroupRole(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String roleName, + @QueryParameter(required = true) String group) throws IOException { + checkAdminPerm(); + RoleMap roleMap = getRoleMap(RoleType.fromString(type)); + Role role = roleMap.getRole(roleName); + if (role != null) { + roleMap.deleteRoleSid(new PermissionEntry(AuthorizationType.GROUP, group), role.getName()); } persistChanges(); } + /** + * API method to get the granted permissions of a role and the SIDs assigned to it. + * + *

+ * Example: {@code curl -XGET 'http://localhost:8080/jenkins/role-strategy/strategy/getRole + * ?type=globalRoles&roleName=admin'} + * + *

+ * Returns json with granted permissions and assigned sids.
+ * Example: + * + *

{@code
+   *   {
+   *     "permissionIds": {
+   *         "hudson.model.Hudson.Read":true,
+   *         "hudson.model.Item.Read":true,
+   *         "hudson.model.Item.Build":true,
+   *      },
+   *      "sids": [{"type":"USER","sid":"user1"}, {"type":"USER","sid":"user2"}]
+   *   }
+   * }
+   * 
+ * + * + * @param type (globalRoles, projectRoles, slaveRoles) + * @param roleName name of role (single, no list) + * @throws IOException In case write response failed + * @since 2.8.3 + */ + @GET + @Restricted(NoExternalUse.class) + public void doGetRole(@QueryParameter(required = true) String type, + @QueryParameter(required = true) String roleName) throws IOException { + checkAdminPerm(); + JSONObject responseJson = new JSONObject(); + RoleMap roleMap = getRoleMap(RoleType.fromString(type)); + Role role = roleMap.getRole(roleName); + if (role != null) { + Set permissions = role.getPermissions(); + Map permissionsMap = new HashMap<>(); + for (Permission permission : permissions) { + permissionsMap.put(permission.getId(), permission.getEnabled()); + } + responseJson.put("permissionIds", permissionsMap); + if (!type.equals(RoleBasedAuthorizationStrategy.GLOBAL)) { + responseJson.put("pattern", role.getPattern().pattern()); + } + Map> grantedRoleMap = roleMap.getGrantedRolesEntries(); + responseJson.put("sids", grantedRoleMap.get(role)); + } + + Stapler.getCurrentResponse().setContentType("application/json;charset=UTF-8"); + Writer writer = Stapler.getCurrentResponse().getCompressedWriter(Stapler.getCurrentRequest()); + responseJson.write(writer); + writer.close(); + } + /** * API method to get all roles and the SIDs assigned to the roles for a roletype. * @@ -524,8 +740,8 @@ public void doUnassignRole(@QueryParameter(required = true) String type, * *
{@code
    *   {
-   *     "role2": ["user1", "user2"],
-   *     "role2": ["group1", "user2"]
+   *     "role2": [{"type":"USER","sid":"user1"}, {"type":"USER","sid":"user2"}],
+   *     "role2": [{"type":"GROUP","sid":"group1"}, {"type":"USER","sid":"user2"}]
    *   }
    * }
* @@ -543,7 +759,7 @@ public void doGetAllRoles(@QueryParameter(fixEmpty = true) String type) throws I roleMap = getRoleMap(RoleType.fromString(type)); } - for (Map.Entry> grantedRole : roleMap.getGrantedRoles().entrySet()) { + for (Map.Entry> grantedRole : roleMap.getGrantedRolesEntries().entrySet()) { responseJson.put(grantedRole.getKey().getName(), grantedRole.getValue()); } @@ -607,6 +823,31 @@ public void doGetMatchingAgents(@QueryParameter(required = true) String pattern, writer.close(); } + /** + * Checks if there are ambiguous entries and adds them to the monitor. + */ + @Restricted(NoExternalUse.class) + public void validateConfig() { + List sids = new ArrayList<>(); + sids.addAll(getSidEntries(RoleBasedAuthorizationStrategy.GLOBAL)); + sids.addAll(getSidEntries(RoleBasedAuthorizationStrategy.SLAVE)); + sids.addAll(getSidEntries(RoleBasedAuthorizationStrategy.PROJECT)); + AmbiguousSidsAdminMonitor.get().updateEntries(sids); + } + + /** + * Validate the config after System config was loaded. + */ + @Initializer(after = InitMilestone.SYSTEM_CONFIG_LOADED) + public static void init() { + Jenkins j = instance(); + AuthorizationStrategy as = j.getAuthorizationStrategy(); + if (as instanceof RoleBasedAuthorizationStrategy) { + RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) as; + rbas.validateConfig(); + } + } + @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @@ -636,7 +877,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC writer.startNode("roleMap"); writer.addAttribute("type", map.getKey().getStringType()); - for (Map.Entry> grantedRole : roleMap.getGrantedRoles().entrySet()) { + for (Map.Entry> grantedRole : roleMap.getGrantedRolesEntries().entrySet()) { Role role = grantedRole.getKey(); if (role != null) { writer.startNode("role"); @@ -652,9 +893,10 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC writer.endNode(); writer.startNode("assignedSIDs"); - for (String sid : grantedRole.getValue()) { + for (PermissionEntry entry : grantedRole.getValue()) { writer.startNode("sid"); - writer.setValue(sid); + writer.addAttribute("type", entry.getType().toString()); + writer.setValue(entry.getSid()); writer.endNode(); } writer.endNode(); @@ -705,7 +947,20 @@ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingCont reader.moveDown(); while (reader.hasMoreChildren()) { reader.moveDown(); - map.assignRole(role, reader.getValue()); + String entryTypeValue = reader.getAttribute("type"); + AuthorizationType authType = AuthorizationType.EITHER; + String sid = reader.getValue(); + if (entryTypeValue != null) { + try { + authType = AuthorizationType.valueOf(entryTypeValue); + } catch (IllegalArgumentException ex) { + LOGGER.log(Level.WARNING, "Unknown AuthorizationType {0} for SID {1} in Role {2}/{3}", + new Object[] { entryTypeValue, sid, type, name }); + throw ex; + } + } + PermissionEntry pe = new PermissionEntry(authType, sid); + map.assignRole(role, pe); reader.moveUp(); } reader.moveUp(); @@ -823,11 +1078,14 @@ public void doAssignSubmit(StaplerRequest req, StaplerResponse rsp) throws Servl for (Map.Entry r : (Set>) roles.getJSONObject("data").entrySet()) { String sid = r.getKey(); - for (Map.Entry e : (Set>) r.getValue().entrySet()) { - if (e.getValue()) { - Role role = roleMap.getRole(e.getKey()); - if (role != null && sid != null && !sid.equals("")) { - roleMap.assignRole(role, sid); + if (sid != null && !sid.equals("")) { + PermissionEntry pe = PermissionEntry.fromString(sid); + for (Map.Entry e : (Set>) r.getValue().entrySet()) { + if (e.getValue()) { + Role role = roleMap.getRole(e.getKey()); + if (role != null) { + roleMap.assignRole(role, pe); + } } } } @@ -867,9 +1125,9 @@ public AuthorizationStrategy newInstance(StaplerRequest req, JSONObject formData strategy.addRole(RoleType.Global, role); RoleMap roleMap = ((RoleBasedAuthorizationStrategy) oldStrategy).getRoleMap(RoleType.Global); if (roleMap != null) { - Set sids = roleMap.getSidsForRole(roleName); + Set sids = roleMap.getSidEntriesForRole(roleName); if (sids != null) { - for (String sid : sids) { + for (PermissionEntry sid : sids) { strategy.assignRole(RoleType.Global, role, sid); } } @@ -890,7 +1148,7 @@ public AuthorizationStrategy newInstance(StaplerRequest req, JSONObject formData strategy = new RoleBasedAuthorizationStrategy(); Role adminRole = createAdminRole(); strategy.addRole(RoleType.Global, adminRole); - strategy.assignRole(RoleType.Global, adminRole, getCurrentUser()); + strategy.assignRole(RoleType.Global, adminRole, new PermissionEntry(AuthorizationType.USER, getCurrentUser())); } return strategy; @@ -930,9 +1188,9 @@ private void readRoles(JSONObject formData, final RoleType roleType, RoleBasedAu RoleMap roleMap = oldStrategy.getRoleMap(roleType); if (roleMap != null) { - Set sids = roleMap.getSidsForRole(roleName); + Set sids = roleMap.getSidEntriesForRole(roleName); if (sids != null) { - for (String sid : sids) { + for (PermissionEntry sid : sids) { targetStrategy.assignRole(roleType, role, sid); } } @@ -1044,6 +1302,21 @@ public String impliedByList(Permission p) { return StringUtils.join(impliedBys.stream().map(Permission::getId).collect(Collectors.toList()), " "); } + /** + * Create PermissionEntry. + * + * @param type AuthorizationType + * @param sid SID + * @return PermissionEntry + */ + @Restricted(DoNotUse.class) // Jelly only + public PermissionEntry entryFor(String type, String sid) { + if (type == null) { + return null; // template row only + } + return new PermissionEntry(AuthorizationType.valueOf(type), sid); + } + /** * Validate the pattern. * @@ -1073,61 +1346,86 @@ public FormValidation doCheckPattern(@QueryParameter String value) { */ @RequirePOST public FormValidation doCheckName(@QueryParameter String value) { - final String v = value.substring(1, value.length() - 1); - String ev = Functions.escape(v); + final String unbracketedValue = value.substring(1, value.length() - 1); + + final int splitIndex = unbracketedValue.indexOf(':'); + if (splitIndex < 0) { + return FormValidation.error("No type prefix: " + unbracketedValue); + } + + final String typeString = unbracketedValue.substring(0, splitIndex); + final AuthorizationType type; + try { + type = AuthorizationType.valueOf(typeString); + } catch (Exception ex) { + return FormValidation.error("Invalid type prefix: " + unbracketedValue); + } + String sid = unbracketedValue.substring(splitIndex + 1); + String escapedSid = Functions.escape(sid); if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - return FormValidation.ok(ev); // can't check + return FormValidation.ok(escapedSid); // can't check } SecurityRealm sr = Jenkins.get().getSecurityRealm(); - if (v.equals("authenticated")) { + if (sid.equals("authenticated") && type == AuthorizationType.EITHER) { // system reserved group - return FormValidation.respond(FormValidation.Kind.OK, ValidationUtil.formatUserGroupValidationResponse("user", ev, "Group")); + return FormValidation.respond(FormValidation.Kind.OK, + ValidationUtil.formatUserGroupValidationResponse(type, escapedSid, + "Internal group found; but permissions would also be granted to a user of this name", true)); } - try { - try { - sr.loadUserByUsername2(v); - User u = User.getById(v, true); - if (v.equals(u.getFullName())) { - return FormValidation.respond(FormValidation.Kind.OK, ValidationUtil.formatUserGroupValidationResponse("person", ev, "User")); - } - return FormValidation.respond(FormValidation.Kind.OK, ValidationUtil.formatUserGroupValidationResponse("person", - Util.escape(StringUtils.abbreviate(u.getFullName(), 50)), "User " + ev)); - } catch (UserMayOrMayNotExistException2 e) { - // undecidable, meaning the user may exist - return FormValidation.respond(FormValidation.Kind.OK, ev); - } catch (UsernameNotFoundException e) { - // fall through next - } catch (AuthenticationException e) { - // other seemingly unexpected error. - return FormValidation.error(e, "Failed to test the validity of the user name " + v); - } + if (sid.equals("anonymous") && type == AuthorizationType.EITHER) { + // system reserved user + return FormValidation.respond(FormValidation.Kind.OK, + formatUserGroupValidationResponse(type, escapedSid, + "Internal user found; but permissions would also be granted to a group of this name", true)); + } - try { - sr.loadGroupByGroupname2(v, false); - return FormValidation.respond(FormValidation.Kind.OK, ValidationUtil.formatUserGroupValidationResponse("user", ev, "Group")); - } catch (UserMayOrMayNotExistException2 e) { - // undecidable, meaning the group may exist - return FormValidation.respond(FormValidation.Kind.WARNING, v); - } catch (UsernameNotFoundException e) { - // fall through next - } catch (AuthenticationException e) { - // other seemingly unexpected error. - return FormValidation.error(e, "Failed to test the validity of the group name " + v); + try { + FormValidation groupValidation; + FormValidation userValidation; + switch (type) { + case GROUP: + groupValidation = ValidationUtil.validateGroup(sid, sr, false); + if (groupValidation != null) { + return groupValidation; + } + return FormValidation.respond(FormValidation.Kind.OK, + formatNonExistentUserGroupValidationResponse(type, escapedSid, "Group not found")); + case USER: + userValidation = ValidationUtil.validateUser(sid, sr, false); + if (userValidation != null) { + return userValidation; + } + return FormValidation.respond(FormValidation.Kind.OK, + formatNonExistentUserGroupValidationResponse(type, escapedSid, "User not found")); + case EITHER: + userValidation = ValidationUtil.validateUser(sid, sr, true); + if (userValidation != null) { + return userValidation; + } + groupValidation = ValidationUtil.validateGroup(sid, sr, true); + if (groupValidation != null) { + return groupValidation; + } + return FormValidation.respond(FormValidation.Kind.OK, + formatNonExistentUserGroupValidationResponse(type, escapedSid, "User or group not found", true)); + default: + return FormValidation.error("Unexpected type: " + type); } - - // couldn't find it. it doesn't exist - return FormValidation.respond(FormValidation.Kind.ERROR, - ValidationUtil.formatNonExistentUserGroupValidationResponse(ev, "User or group not found")); // TODO i18n } catch (Exception e) { - // if the check fails miserably, we still want the user to be able to see the - // name of the user, - // so use 'ev' as the message - return FormValidation.error(e, ev); + // if the check fails miserably, we still want the user to be able to see the name of the user, + // so use 'escapedSid' as the message + return FormValidation.error(e, escapedSid); } } + + @Restricted(DoNotUse.class) + public boolean hasAmbiguousEntries(SortedMap> grantedRoles) { + return grantedRoles.entrySet().stream() + .anyMatch(entry -> entry.getValue().stream().anyMatch(pe -> pe.getType() == AuthorizationType.EITHER)); + } } } diff --git a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleMap.java b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleMap.java index 0bd777ab..888cf7d6 100644 --- a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleMap.java +++ b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/RoleMap.java @@ -26,12 +26,14 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.synopsys.arc.jenkins.plugins.rolestrategy.IMacroExtension; import com.synopsys.arc.jenkins.plugins.rolestrategy.Macro; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleMacroExtension; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Util; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.Node; @@ -59,8 +61,10 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import jenkins.model.Jenkins; import jenkins.model.ProjectNamingStrategy; +import org.acegisecurity.acls.sid.PrincipalSid; import org.acegisecurity.acls.sid.Sid; import org.jenkinsci.plugins.rolestrategy.RoleBasedProjectNamingStrategy; import org.jenkinsci.plugins.rolestrategy.Settings; @@ -82,7 +86,7 @@ public class RoleMap { /** * Map associating each {@link Role} with the concerned {@link User}s/groups. */ - private final SortedMap> grantedRoles; + private final SortedMap> grantedRoles; private static final Logger LOGGER = Logger.getLogger(RoleMap.class.getName()); @@ -113,9 +117,9 @@ public class RoleMap { * @param grantedRoles Roles to be granted. */ @DataBoundConstructor - public RoleMap(@NonNull SortedMap> grantedRoles) { + public RoleMap(@NonNull SortedMap> grantedRoles) { this(); - for (Map.Entry> entry : grantedRoles.entrySet()) { + for (Map.Entry> entry : grantedRoles.entrySet()) { this.grantedRoles.put(entry.getKey(), new HashSet<>(entry.getValue())); } } @@ -126,38 +130,70 @@ public RoleMap(@NonNull SortedMap> grantedRoles) { * @return True if the sid's granted permission */ @Restricted(NoExternalUse.class) - public boolean hasPermission(String sid, Permission permission, RoleType roleType, AccessControlled controlledItem) { + public boolean hasPermission(PermissionEntry sid, Permission permission, RoleType roleType, AccessControlled controlledItem) { final Set permissions = getImplyingPermissions(permission); final boolean[] hasPermission = { false }; // Walk through the roles, and only add the roles having the given permission, // or a permission implying the given permission new RoleWalker() { + + /** + * Checks whether the given sid is granted permission. + * First checks if there is a dedicated match for user/group. + * If not checks if there is an entry for either. + * + * @param current The current role + * @param entry The permission entry to check + * @return The PermissionEntry that matched or null if nothing matched. + */ + @CheckForNull + private PermissionEntry hasPermission(Role current, PermissionEntry entry) { + if (grantedRoles.get(current).contains(entry)) { + return entry; + } + entry = new PermissionEntry(AuthorizationType.EITHER, entry.getSid()); + if (grantedRoles.get(current).contains(entry)) { + return entry; + } + return null; + } + @Override public void perform(Role current) { if (current.hasAnyPermission(permissions)) { - if (grantedRoles.get(current).contains(sid)) { + PermissionEntry entry = hasPermission(current, sid); + if (entry != null) { // Handle roles macro if (Macro.isMacro(current)) { Macro macro = RoleMacroExtension.getMacro(current.getName()); if (controlledItem != null && macro != null) { RoleMacroExtension macroExtension = RoleMacroExtension.getMacroExtension(macro.getName()); - if (macroExtension.IsApplicable(roleType) - && macroExtension.hasPermission(sid, permission, roleType, controlledItem, macro)) { - hasPermission[0] = true; - abort(); + if (macroExtension.IsApplicable(roleType)) { + if (Util.isOverridden(IMacroExtension.class, macroExtension.getClass(), "hasPermission", PermissionEntry.class, + Permission.class, RoleType.class, AccessControlled.class, Macro.class)) { + if (macroExtension.hasPermission(entry, permission, roleType, controlledItem, macro)) { + hasPermission[0] = true; + abort(); + } + } else { + if (macroExtension.hasPermission(entry.getSid(), permission, roleType, controlledItem, macro)) { + hasPermission[0] = true; + abort(); + } + } } } } else { hasPermission[0] = true; abort(); } - } else if (Settings.TREAT_USER_AUTHORITIES_AS_ROLES) { + } else if (Settings.TREAT_USER_AUTHORITIES_AS_ROLES && sid.getType() == AuthorizationType.USER) { try { - UserDetails userDetails = cache.getIfPresent(sid); + UserDetails userDetails = cache.getIfPresent(sid.getSid()); if (userDetails == null) { - userDetails = Jenkins.get().getSecurityRealm().loadUserByUsername2(sid); - cache.put(sid, userDetails); + userDetails = Jenkins.get().getSecurityRealm().loadUserByUsername2(sid.getSid()); + cache.put(sid.getSid(), userDetails); } for (GrantedAuthority grantedAuthority : userDetails.getAuthorities()) { if (grantedAuthority.getAuthority().equals(current.getName())) { @@ -255,7 +291,7 @@ public void addRole(Role role) { * @param role The {@link Role} to assign the sid to * @param sid The sid to assign */ - public void assignRole(Role role, String sid) { + public void assignRole(Role role, PermissionEntry sid) { if (this.hasRole(role)) { this.grantedRoles.get(role).add(sid); matchingRoleMapCache.invalidateAll(); @@ -263,16 +299,50 @@ public void assignRole(Role role, String sid) { } /** - * unAssign the sid to the given {@link Role}. + * Assign the sid to the given {@link Role}. + * Assigns are a {@link AuthorizationType#EITHER} * - * @param role The {@link Role} to unassign the sid to + * @param role The {@link Role} to assign the sid to * @param sid The sid to assign + * + * @deprecated use {@link #assignRole(Role, PermissionEntry)} + */ + @Deprecated + public void assignRole(Role role, String sid) { + if (this.hasRole(role)) { + this.grantedRoles.get(role).add(new PermissionEntry(AuthorizationType.EITHER, sid)); + matchingRoleMapCache.invalidateAll(); + } + } + + /** + * unAssign the sid from the given {@link Role}. + * + * @param role The {@link Role} to unassign the sid to + * @param sid The sid to unassign + */ + public void unAssignRole(Role role, PermissionEntry sid) { + Set sids = grantedRoles.get(role); + if (sids != null) { + sids.remove(sid); + matchingRoleMapCache.invalidateAll(); + } + } + + /** + * unAssign the sid from the given {@link Role}. + * This will only unassign entries of type {@link AuthorizationType#EITHER}. + * + * @param role The {@link Role} to unassign the sid to + * @param sid The sid to unassign * @since 2.6.0 + * @deprecated use {@link #unAssignRole(Role, PermissionEntry)} */ + @Deprecated public void unAssignRole(Role role, String sid) { - Set sids = grantedRoles.get(role); + Set sids = grantedRoles.get(role); if (sids != null) { - sids.remove(sid); + sids.remove(new PermissionEntry(AuthorizationType.EITHER, sid)); matchingRoleMapCache.invalidateAll(); } } @@ -294,23 +364,40 @@ public void clearSidsForRole(Role role) { * * @param sid The sid for which you want to clear the {@link Role}s */ - public void deleteSids(String sid) { - for (Map.Entry> entry : grantedRoles.entrySet()) { - Set sids = entry.getValue(); + public void deleteSids(PermissionEntry sid) { + for (Map.Entry> entry : grantedRoles.entrySet()) { + Set sids = entry.getValue(); sids.remove(sid); } matchingRoleMapCache.invalidateAll(); } + /** + * Clear all the roles associated to the given sid. + * This will only find sids of type {@link AuthorizationType#EITHER} + * + * @param sid The sid for which you want to clear the {@link Role}s + * + * @deprecated use {@link #deleteSids(PermissionEntry)} + */ + @Deprecated + public void deleteSids(String sid) { + for (Map.Entry> entry : grantedRoles.entrySet()) { + Set sids = entry.getValue(); + sids.remove(new PermissionEntry(AuthorizationType.EITHER, sid)); + } + matchingRoleMapCache.invalidateAll(); + } + /** * Clear specific role associated to the given sid. * - * @param sid The sid for thwich you want to clear the {@link Role}s - * @param rolename The role for thwich you want to clear the {@link Role}s + * @param sid The sid for wich you want to clear the {@link Role}s + * @param rolename The role for wich you want to clear the {@link Role}s * @since 2.6.0 */ - public void deleteRoleSid(String sid, String rolename) { - for (Map.Entry> entry : grantedRoles.entrySet()) { + public void deleteRoleSid(PermissionEntry sid, String rolename) { + for (Map.Entry> entry : grantedRoles.entrySet()) { Role role = entry.getKey(); if (role.getName().equals(rolename)) { unAssignRole(role, sid); @@ -319,11 +406,33 @@ public void deleteRoleSid(String sid, String rolename) { } } + + /** + * Clear specific role associated to the given sid. + * This will only find sids of type {@link AuthorizationType#EITHER} + * + * @param sid The sid for wich you want to clear the {@link Role}s + * @param rolename The role for wich you want to clear the {@link Role}s + * @since 2.6.0 + * @deprecated use {@link #deleteRoleSid(PermissionEntry, String)} + */ + @Deprecated + public void deleteRoleSid(String sid, String rolename) { + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.EITHER, sid); + for (Map.Entry> entry : grantedRoles.entrySet()) { + Role role = entry.getKey(); + if (role.getName().equals(rolename)) { + unAssignRole(role, sidEntry); + break; + } + } + } + /** * Clear all the sids for each {@link Role} of the {@link RoleMap}. */ public void clearSids() { - for (Map.Entry> entry : this.grantedRoles.entrySet()) { + for (Map.Entry> entry : this.grantedRoles.entrySet()) { Role role = entry.getKey(); this.clearSidsForRole(role); } @@ -360,10 +469,27 @@ public void removeRole(Role role) { * * @return An unmodifiable sorted map containing the {@link Role}s and their associated sids */ - public SortedMap> getGrantedRoles() { + public SortedMap> getGrantedRolesEntries() { return Collections.unmodifiableSortedMap(this.grantedRoles); } + /** + * Get an unmodifiable sorted map containing {@link Role}s and their assigned sids. + * All types are returned to keep the api as compatible as possible. + * + * @return An unmodifiable sorted map containing the {@link Role}s and their associated sids + * @deprecated use {@link #getGrantedRolesEntries()} + */ + @Deprecated + public SortedMap> getGrantedRoles() { + SortedMap> ret = new TreeMap<>(); + for (Map.Entry> entry : this.grantedRoles.entrySet()) { + Set allGrants = entry.getValue().stream().map(PermissionEntry::getSid).collect(Collectors.toSet()); + ret.put(entry.getKey(), allGrants); + } + return ret; + } + /** * Get an unmodifiable set containing all the {@link Role}s of this {@link RoleMap}. * @@ -375,42 +501,88 @@ public Set getRoles() { /** * Get all the sids referenced in this {@link RoleMap}, minus the {@code Anonymous} sid. + * All types are returned to keep the api as compatible as possible. * * @return A sorted set containing all the sids, minus the {@code Anonymous} sid + * @deprecated use {@link #getSidEntries()} */ + @Deprecated public SortedSet getSids() { - return this.getSids(false); + return getSids(false); } /** * Get all the sids referenced in this {@link RoleMap}. + * All types are returned to keep the api as compatible as possible. * * @param includeAnonymous True if you want the {@code Anonymous} sid to be included in the set * @return A sorted set containing all the sids + * @deprecated use {@link #getSidEntries(Boolean)} */ + @Deprecated public SortedSet getSids(Boolean includeAnonymous) { - TreeSet sids = new TreeSet<>(); - for (Map.Entry> entry : this.grantedRoles.entrySet()) { + SortedSet ret = new TreeSet<>(this.getSidEntries(includeAnonymous) + .stream().map(PermissionEntry::getSid).collect(Collectors.toSet())); + return ret; + } + + /** + * Get all the sids referenced in this {@link RoleMap}, minus the {@code Anonymous} sid. + * + * @return A sorted set containing all the sids, minus the {@code Anonymous} sid + */ + public SortedSet getSidEntries() { + return this.getSidEntries(false); + } + + /** + * Get all the sids referenced in this {@link RoleMap}. + * + * @param includeAnonymous True if you want the {@code Anonymous} sid to be included in the set + * @return A sorted set containing all the sids + */ + public SortedSet getSidEntries(Boolean includeAnonymous) { + TreeSet sids = new TreeSet<>(); + for (Map.Entry> entry : this.grantedRoles.entrySet()) { sids.addAll(entry.getValue()); } // Remove the anonymous sid if asked to if (!includeAnonymous) { - sids.remove("anonymous"); + sids.remove(new PermissionEntry(AuthorizationType.USER, "anonymous")); } return Collections.unmodifiableSortedSet(sids); } + /** + * Get all the permission entries assigned to the {@link Role} named after the {@code roleName} param. + * + * @param roleName The name of the role + * @return A sorted set containing all the sids. {@code null} if the role is missing. + */ + @CheckForNull + public Set getSidEntriesForRole(String roleName) { + Role role = this.getRole(roleName); + if (role != null) { + return Collections.unmodifiableSet(this.grantedRoles.get(role)); + } + return null; + } + /** * Get all the sids assigned to the {@link Role} named after the {@code roleName} param. + * All types are returned to keep the api as compatible as possible. * * @param roleName The name of the role * @return A sorted set containing all the sids. {@code null} if the role is missing. + * @deprecated use {@link #getSidEntriesForRole(String)} */ @CheckForNull + @Deprecated public Set getSidsForRole(String roleName) { Role role = this.getRole(roleName); if (role != null) { - return Collections.unmodifiableSet(this.grantedRoles.get(role)); + Set ret = this.grantedRoles.get(role); + return ret.stream().map(PermissionEntry::getSid).collect(Collectors.toSet()); } return null; } @@ -427,7 +599,7 @@ public RoleMap newMatchingRoleMap(String itemNamePrefix) { } private RoleMap createMatchingRoleMap(String itemNamePrefix) { - SortedMap> roleMap = new TreeMap<>(); + SortedMap> roleMap = new TreeMap<>(); new RoleWalker() { @Override public void perform(Role current) { @@ -464,8 +636,8 @@ public static List getMatchingJobNames(Pattern pattern, int maxJobs) { * Get all job names matching the given pattern, viewable to the requesting user. * * @param matchedItems List that will take the matched item names - * @param pattern Pattern to match against - * @param maxJobs Max matching jobs to look for + * @param pattern Pattern to match against + * @param maxJobs Max matching jobs to look for * @return Number of matched jobs */ @Restricted(NoExternalUse.class) @@ -551,7 +723,9 @@ public AclImpl(RoleType roleType, AccessControlled item) { @Override @CheckForNull protected Boolean hasPermission(Sid sid, Permission permission) { - if (RoleMap.this.hasPermission(toString(sid), permission, roleType, item)) { + boolean principal = sid instanceof PrincipalSid ? true : false; + PermissionEntry entry = new PermissionEntry(principal ? AuthorizationType.USER : AuthorizationType.GROUP, toString(sid)); + if (RoleMap.this.hasPermission(entry, permission, roleType, item)) { if (item instanceof Item) { final ItemGroup parent = ((Item) item).getParent(); if (parent instanceof Item && (Item.DISCOVER.equals(permission) || Item.READ.equals(permission)) @@ -576,7 +750,7 @@ && shouldCheckParentPermissions()) { if (auth instanceof RoleBasedAuthorizationStrategy && pns instanceof RoleBasedProjectNamingStrategy) { RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) auth; RoleMap roleMapProject = rbas.getRoleMap(RoleType.Project); - if (roleMapProject.hasPermission(toString(sid), permission, RoleType.Project, item)) { + if (roleMapProject.hasPermission(entry, permission, RoleType.Project, item)) { return true; } } diff --git a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/ValidationUtil.java b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/ValidationUtil.java index 835ce9f6..992b8ed6 100644 --- a/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/ValidationUtil.java +++ b/src/main/java/com/michelin/cio/hudson/plugins/rolestrategy/ValidationUtil.java @@ -1,34 +1,159 @@ package com.michelin.cio.hudson.plugins.rolestrategy; -import jenkins.model.Jenkins; -import org.apache.commons.jelly.JellyContext; -import org.jenkins.ui.icon.Icon; -import org.jenkins.ui.icon.IconSet; +import hudson.Functions; +import hudson.Util; +import hudson.model.User; +import hudson.security.SecurityRealm; +import hudson.security.UserMayOrMayNotExistException2; +import hudson.util.FormValidation; +import org.apache.commons.lang.StringUtils; +import org.jenkins.ui.symbol.Symbol; +import org.jenkins.ui.symbol.SymbolRequest; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.Stapler; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; @Restricted(NoExternalUse.class) class ValidationUtil { + + private static String userSymbol; + private static String groupSymbol; + + private static String warningSymbol; + private ValidationUtil() { // do not use } - static String formatNonExistentUserGroupValidationResponse(String user, String tooltip) { - return formatUserGroupValidationResponse(null, "" + user + "", - tooltip); + static String formatNonExistentUserGroupValidationResponse(AuthorizationType type, String user, String tooltip) { + return formatNonExistentUserGroupValidationResponse(type, user, tooltip, false); } - static String formatUserGroupValidationResponse(String img, String user, String tooltip) { - if (img == null) { - return String.format("%s", tooltip, user); + static String formatNonExistentUserGroupValidationResponse(AuthorizationType type, String user, String tooltip, boolean alert) { + return formatUserGroupValidationResponse(type, "" + user + "", + tooltip, alert); + } + + private static String getSymbol(String symbol, String clazzes) { + SymbolRequest.Builder builder = new SymbolRequest.Builder(); + + return Symbol.get(builder.withRaw("symbol-" + symbol + "-outline plugin-ionicons-api").withClasses(clazzes).build()); + } + + private static void loadUserSymbol() { + if (userSymbol == null) { + userSymbol = getSymbol("person", "icon-sm"); + } + } + + private static void loadGroupSymbol() { + if (groupSymbol == null) { + groupSymbol = getSymbol("people", "icon-sm"); } + } - String imageFormat = String.format("icon-%s icon-sm", img); - Icon icon = IconSet.icons.getIconByClassSpec(imageFormat); - JellyContext ctx = new JellyContext(); - ctx.setVariable("resURL", Stapler.getCurrentRequest().getContextPath() + Jenkins.RESOURCE_PATH); - String url = icon.getQualifiedUrl(ctx); - return String.format("%s", tooltip, url, icon.getStyle(), user); + private static void loadWarningSymbol() { + if (warningSymbol == null) { + warningSymbol = getSymbol("warning", "icon-md rsp-table__icon-alert"); + } + } + + static String formatUserGroupValidationResponse(AuthorizationType type, String user, String tooltip) { + return formatUserGroupValidationResponse(type, user, tooltip, false); + } + + static String formatUserGroupValidationResponse(AuthorizationType type, String user, String tooltip, boolean alert) { + String symbol; + switch (type) { + case GROUP: + loadGroupSymbol(); + symbol = groupSymbol; + break; + case EITHER: + case USER: + default: + loadUserSymbol(); + symbol = userSymbol; + break; + } + if (alert) { + loadWarningSymbol(); + return String.format("
%s%s%s
", tooltip, warningSymbol, symbol, user); + } + return String.format("
%s%s
", tooltip, symbol, user); + } + + static FormValidation validateGroup(String groupName, SecurityRealm sr, boolean ambiguous) { + String escapedSid = Functions.escape(groupName); + try { + sr.loadGroupByGroupname2(groupName, false); + if (ambiguous) { + return FormValidation.respond(FormValidation.Kind.WARNING, + formatUserGroupValidationResponse(AuthorizationType.GROUP, escapedSid, + "Group found; but permissions would also be granted to a user of this name", true)); + } else { + return FormValidation.respond(FormValidation.Kind.OK, formatUserGroupValidationResponse(AuthorizationType.GROUP, + escapedSid, "Group")); + } + } catch (UserMayOrMayNotExistException2 e) { + // undecidable, meaning the group may exist + if (ambiguous) { + return FormValidation.respond(FormValidation.Kind.WARNING, + formatUserGroupValidationResponse(AuthorizationType.GROUP, escapedSid, + "Permissions would also be granted to a user or group of this name", true)); + } else { + return FormValidation.ok(escapedSid); + } + } catch (UsernameNotFoundException e) { + // fall through next + } catch (AuthenticationException e) { + // other seemingly unexpected error. + return FormValidation.error(e, "Failed to test the validity of the group name " + groupName); + } + return null; + } + + static FormValidation validateUser(String userName, SecurityRealm sr, boolean ambiguous) { + String escapedSid = Functions.escape(userName); + try { + sr.loadUserByUsername2(userName); + User u = User.getById(userName, true); + if (userName.equals(u.getFullName())) { + // Sid and full name are identical, no need for tooltip + if (ambiguous) { + return FormValidation.respond(FormValidation.Kind.WARNING, + formatUserGroupValidationResponse(AuthorizationType.EITHER, escapedSid, + "User found; but permissions would also be granted to a group of this name", true)); + } else { + return FormValidation.respond(FormValidation.Kind.OK, + formatUserGroupValidationResponse(AuthorizationType.USER, escapedSid, "User")); + } + } + if (ambiguous) { + return FormValidation.respond(FormValidation.Kind.WARNING, + formatUserGroupValidationResponse(AuthorizationType.EITHER, Util.escape(StringUtils.abbreviate(u.getFullName(), 50)), + "User " + escapedSid + " found, but permissions would also be granted to a group of this name", true)); + } else { + return FormValidation.respond(FormValidation.Kind.OK, + formatUserGroupValidationResponse(AuthorizationType.USER, Util.escape(StringUtils.abbreviate(u.getFullName(), 50)), + "User " + escapedSid)); + } + } catch (UserMayOrMayNotExistException2 e) { + // undecidable, meaning the user may exist + if (ambiguous) { + return FormValidation.respond(FormValidation.Kind.WARNING, + formatUserGroupValidationResponse(AuthorizationType.EITHER, escapedSid, + "Permissions would also be granted to a user or group of this name", true)); + } else { + return FormValidation.ok(escapedSid); + } + } catch (UsernameNotFoundException e) { + // fall through next + } catch (AuthenticationException e) { + // other seemingly unexpected error. + return FormValidation.error(e, "Failed to test the validity of the user name " + escapedSid); + } + return null; } } diff --git a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/IMacroExtension.java b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/IMacroExtension.java index cf99830e..770bcebc 100644 --- a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/IMacroExtension.java +++ b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/IMacroExtension.java @@ -24,6 +24,8 @@ package com.synopsys.arc.jenkins.plugins.rolestrategy; +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.security.AccessControlled; import hudson.security.Permission; @@ -73,5 +75,9 @@ public interface IMacroExtension { * @param macro Macro with parameters * @return True if user satisfies macro's requirements */ - boolean hasPermission(String sid, Permission p, RoleType type, AccessControlled item, Macro macro); + boolean hasPermission(PermissionEntry sid, Permission p, RoleType type, AccessControlled item, Macro macro); + + default boolean hasPermission(String sid, Permission p, RoleType type, AccessControlled item, Macro macro) { + return hasPermission(new PermissionEntry(AuthorizationType.EITHER, sid), p, type, item, macro); + } } diff --git a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/BuildableJobMacro.java b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/BuildableJobMacro.java index 9e2a31ef..1f09ba5f 100644 --- a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/BuildableJobMacro.java +++ b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/BuildableJobMacro.java @@ -24,6 +24,7 @@ package com.synopsys.arc.jenkins.plugins.rolestrategy.macros; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.synopsys.arc.jenkins.plugins.rolestrategy.Macro; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleMacroExtension; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -55,7 +56,7 @@ public boolean IsApplicable(RoleType roleType) { } @Override - public boolean hasPermission(String sid, Permission p, RoleType type, AccessControlled item, Macro macro) { + public boolean hasPermission(PermissionEntry sid, Permission p, RoleType type, AccessControlled item, Macro macro) { if (Job.class.isAssignableFrom(item.getClass())) { Job job = (Job) item; return job.isBuildable(); @@ -68,4 +69,5 @@ public boolean hasPermission(String sid, Permission p, RoleType type, AccessCont public String getDescription() { return "Filters out unbuildable items, e.g. folders"; } + } diff --git a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/ContainedInViewMacro.java b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/ContainedInViewMacro.java index e670c4d5..971726c1 100644 --- a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/ContainedInViewMacro.java +++ b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/ContainedInViewMacro.java @@ -27,6 +27,7 @@ import com.cloudbees.hudson.plugins.folder.AbstractFolder; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.synopsys.arc.jenkins.plugins.rolestrategy.Macro; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleMacroExtension; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -87,7 +88,7 @@ public boolean IsApplicable(RoleType roleType) { @Override @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "we know that the cache has no null entries") - public boolean hasPermission(String sid, Permission p, RoleType type, AccessControlled accessControlledItem, Macro macro) { + public boolean hasPermission(PermissionEntry sid, Permission p, RoleType type, AccessControlled accessControlledItem, Macro macro) { if (accessControlledItem instanceof Item) { Item item = (Item) accessControlledItem; Map> items = cache.get(macro, this::getItemsForMacro); diff --git a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/FolderMacro.java b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/FolderMacro.java index bd8125c4..5eb404bc 100644 --- a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/FolderMacro.java +++ b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/FolderMacro.java @@ -25,6 +25,7 @@ package com.synopsys.arc.jenkins.plugins.rolestrategy.macros; import com.cloudbees.hudson.plugins.folder.AbstractFolder; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.synopsys.arc.jenkins.plugins.rolestrategy.Macro; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleMacroExtension; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -53,7 +54,7 @@ public boolean IsApplicable(RoleType roleType) { } @Override - public boolean hasPermission(String sid, Permission p, RoleType type, AccessControlled item, Macro macro) { + public boolean hasPermission(PermissionEntry sid, Permission p, RoleType type, AccessControlled item, Macro macro) { return AbstractFolder.class.isAssignableFrom(item.getClass()); } diff --git a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/StubMacro.java b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/StubMacro.java index e79d938c..dc65c5ca 100644 --- a/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/StubMacro.java +++ b/src/main/java/com/synopsys/arc/jenkins/plugins/rolestrategy/macros/StubMacro.java @@ -24,6 +24,7 @@ package com.synopsys.arc.jenkins.plugins.rolestrategy.macros; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.synopsys.arc.jenkins.plugins.rolestrategy.Macro; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleMacroExtension; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -56,7 +57,7 @@ public boolean IsApplicable(RoleType roleType) { } @Override - public boolean hasPermission(String sid, Permission p, RoleType type, AccessControlled item, Macro macro) { + public boolean hasPermission(PermissionEntry sid, Permission p, RoleType type, AccessControlled item, Macro macro) { return false; } diff --git a/src/main/java/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor.java b/src/main/java/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor.java new file mode 100644 index 00000000..2f487458 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor.java @@ -0,0 +1,65 @@ +package org.jenkinsci.plugins.rolestrategy; + +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; +import com.michelin.cio.hudson.plugins.rolestrategy.Messages; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.AdministrativeMonitor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Alert administrators in case ambiguous SID are declared. + * + * @see SECURITY-2374 + */ +@Extension +@Restricted(NoExternalUse.class) +public class AmbiguousSidsAdminMonitor extends AdministrativeMonitor { + + private @Nonnull List ambiguousEntries = Collections.emptyList(); + + public static @Nonnull AmbiguousSidsAdminMonitor get() { + return ExtensionList.lookupSingleton(AmbiguousSidsAdminMonitor.class); + } + + /** + * To be called everytime Permission Entries are updated. + * + * @param entries All entries in the system. + */ + public void updateEntries(@Nonnull Collection entries) { + List ambiguous = new ArrayList<>(); + for (PermissionEntry entry : entries) { + try { + if (entry.getType() == AuthorizationType.EITHER) { + ambiguous.add(entry.getSid()); + } + } catch (IllegalArgumentException ex) { + // Invalid, but not the problem we are looking for + } + } + ambiguousEntries = ambiguous; + } + + public @Nonnull List getAmbiguousEntries() { + return ambiguousEntries; + } + + @Override + public boolean isActivated() { + return !ambiguousEntries.isEmpty(); + } + + @Override + public String getDisplayName() { + return Messages.RoleBasedProjectNamingStrategy_Ambiguous(); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategy.java b/src/main/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategy.java index adb92967..3a7adb6b 100644 --- a/src/main/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategy.java +++ b/src/main/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategy.java @@ -1,6 +1,8 @@ package org.jenkinsci.plugins.rolestrategy; +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; import com.michelin.cio.hudson.plugins.rolestrategy.Messages; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.michelin.cio.hudson.plugins.rolestrategy.RoleMap; @@ -88,7 +90,8 @@ public void checkName(String parentName, String name) throws Failure { if (a == ACL.SYSTEM2) { return; } - String principal = new PrincipalSid(a).getPrincipal(); + PermissionEntry principal = new PermissionEntry(AuthorizationType.USER, new PrincipalSid(a).getPrincipal()); + RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) auth; RoleMap global = rbas.getRoleMap(RoleType.Global); List authorities = a.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); @@ -105,23 +108,17 @@ public void checkName(String parentName, String name) throws Failure { } // check project role with pattern - SortedMap> roles = rbas.getGrantedRoles(RoleType.Project); + SortedMap> roles = rbas.getGrantedRolesEntries(RoleType.Project); ArrayList badList = new ArrayList<>(roles.size()); - for (SortedMap.Entry> entry : roles.entrySet()) { + for (SortedMap.Entry> entry : roles.entrySet()) { Role key = entry.getKey(); if (!Macro.isMacro(key) && key.hasPermission(Item.CREATE)) { - Set sids = entry.getValue(); + Set sids = entry.getValue(); Pattern namePattern = key.getPattern(); if (StringUtils.isNotBlank(namePattern.toString())) { if (namePattern.matcher(fullName).matches()) { - if (sids.contains(principal)) { + if (hasAnyPermission(principal, authorities, sids)) { return; - } else { - for (String authority : authorities) { - if (sids.contains(authority)) { - return; - } - } } } else { badList.add(namePattern.toString()); @@ -139,12 +136,28 @@ public void checkName(String parentName, String name) throws Failure { } } - private boolean hasCreatePermission(RoleMap roleMap, String principal, List authorities, RoleType roleType) { + private boolean hasAnyPermission(PermissionEntry principal, List authorities, Set sids) { + PermissionEntry eitherUser = new PermissionEntry(AuthorizationType.EITHER, principal.getSid()); + if (sids.contains(principal) || sids.contains(eitherUser)) { + return true; + } else { + for (String authority : authorities) { + if (sids.contains(new PermissionEntry(AuthorizationType.GROUP, authority)) + || sids.contains(new PermissionEntry(AuthorizationType.EITHER, authority))) { + return true; + } + } + } + return false; + } + + private boolean hasCreatePermission(RoleMap roleMap, PermissionEntry principal, List authorities, RoleType roleType) { if (roleMap.hasPermission(principal, Item.CREATE, roleType, null)) { return true; } for (String group : authorities) { - if (roleMap.hasPermission(group, Item.CREATE, roleType, null)) { + PermissionEntry groupEntry = new PermissionEntry(AuthorizationType.GROUP, group); + if (roleMap.hasPermission(groupEntry, Item.CREATE, roleType, null)) { return true; } } diff --git a/src/main/java/org/jenkinsci/plugins/rolestrategy/Settings.java b/src/main/java/org/jenkinsci/plugins/rolestrategy/Settings.java index f0c2636f..4ce32750 100644 --- a/src/main/java/org/jenkinsci/plugins/rolestrategy/Settings.java +++ b/src/main/java/org/jenkinsci/plugins/rolestrategy/Settings.java @@ -75,7 +75,7 @@ public class Settings { /** * Enabling processing of User Authorities. Alters the behavior of - * {@link RoleMap#hasPermission(java.lang.String, hudson.security.Permission, + * {@link RoleMap#hasPermission(com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry, hudson.security.Permission, * com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType, hudson.security.AccessControlled)}. * Since 2.3.0 this value was {@code true}, but it has been switched due to the performance reasons. The behavior can be * reverted (even dynamically via System Groovy Script). diff --git a/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/GrantedRoles.java b/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/GrantedRoles.java index 9c806ccc..197694c4 100644 --- a/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/GrantedRoles.java +++ b/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/GrantedRoles.java @@ -1,5 +1,6 @@ package org.jenkinsci.plugins.rolestrategy.casc; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.michelin.cio.hudson.plugins.rolestrategy.RoleMap; @@ -9,6 +10,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.stream.Collectors; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; @@ -58,9 +60,10 @@ protected Map toMap() { @NonNull private RoleMap retrieveRoleMap(List definitions) { - TreeMap> resMap = new TreeMap<>(); + TreeMap> resMap = new TreeMap<>(); for (RoleDefinition definition : definitions) { - resMap.put(definition.getRole(), definition.getAssignments()); + resMap.put(definition.getRole(), + definition.getEntries().stream().map(RoleDefinition.RoleDefinitionEntry::asPermissionEntry).collect(Collectors.toSet())); } return new RoleMap(resMap); } diff --git a/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleBasedAuthorizationStrategyConfigurator.java b/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleBasedAuthorizationStrategyConfigurator.java index 247d9def..41528f45 100644 --- a/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleBasedAuthorizationStrategyConfigurator.java +++ b/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleBasedAuthorizationStrategyConfigurator.java @@ -1,5 +1,6 @@ package org.jenkinsci.plugins.rolestrategy.casc; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -15,7 +16,8 @@ import io.jenkins.plugins.casc.model.Mapping; import java.util.Collections; import java.util.List; -import java.util.Map.Entry; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.function.Function; @@ -30,7 +32,7 @@ * @since 2.11 */ @Extension(optional = true, ordinal = 2) -@Restricted({ NoExternalUse.class }) +@Restricted({NoExternalUse.class}) public class RoleBasedAuthorizationStrategyConfigurator extends BaseConfigurator { @Override @@ -57,15 +59,26 @@ protected RoleBasedAuthorizationStrategy instance(Mapping map, ConfigurationCont return new RoleBasedAuthorizationStrategy(roles.toMap()); } + @Override + protected void configure(Mapping config, RoleBasedAuthorizationStrategy instance, boolean dryrun, + ConfigurationContext context) throws ConfiguratorException { + super.configure(config, instance, dryrun, context); + + if (!dryrun) { + instance.validateConfig(); + } + } + @Override @NonNull public Set> describe() { - return Collections.singleton(new Attribute("roles", GrantedRoles.class).getter(target -> { - List globalRoles = getRoleDefinitions(target.getGrantedRoles(RoleType.Global)); - List agentRoles = getRoleDefinitions(target.getGrantedRoles(RoleType.Slave)); - List projectRoles = getRoleDefinitions(target.getGrantedRoles(RoleType.Project)); - return new GrantedRoles(globalRoles, projectRoles, agentRoles); - })); + return Collections.singleton( + new Attribute("roles", GrantedRoles.class).getter(target -> { + List globalRoles = getRoleDefinitions(target.getGrantedRolesEntries(RoleType.Global)); + List agentRoles = getRoleDefinitions(target.getGrantedRolesEntries(RoleType.Slave)); + List projectRoles = getRoleDefinitions(target.getGrantedRolesEntries(RoleType.Project)); + return new GrantedRoles(globalRoles, projectRoles, agentRoles); + })); } @CheckForNull @@ -74,19 +87,26 @@ public CNode describe(RoleBasedAuthorizationStrategy instance, ConfigurationCont return compare(instance, new RoleBasedAuthorizationStrategy(Collections.emptyMap()), context); } - private List getRoleDefinitions(@CheckForNull SortedMap> roleMap) { + private List getRoleDefinitions(@CheckForNull SortedMap> roleMap) { if (roleMap == null) { return Collections.emptyList(); } return roleMap.entrySet().stream().map(getRoleDefinition()).collect(Collectors.toList()); } - private Function>, RoleDefinition> getRoleDefinition() { + private Function>, RoleDefinition> getRoleDefinition() { return roleSetEntry -> { Role role = roleSetEntry.getKey(); List permissions = role.getPermissions().stream() - .map(permission -> permission.group.getId() + "/" + permission.name).collect(Collectors.toList()); - return new RoleDefinition(role.getName(), role.getDescription(), role.getPattern().pattern(), permissions, roleSetEntry.getValue()); + .map(permission -> permission.group.title.toString( + Locale.US) + "/" + permission.name).collect(Collectors.toList()); + Set roleDefinitionEntries = roleSetEntry.getValue().stream() + .map(RoleDefinition.RoleDefinitionEntry::fromPermissionEntry) + .collect(Collectors.toSet()); + final RoleDefinition roleDefinition = new RoleDefinition(role.getName(), role.getDescription(), + role.getPattern().pattern(), permissions); + roleDefinition.setEntries(roleDefinitionEntries); + return roleDefinition; }; } } diff --git a/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleDefinition.java b/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleDefinition.java index 9c6b0824..7a3ab90c 100644 --- a/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleDefinition.java +++ b/src/main/java/org/jenkinsci/plugins/rolestrategy/casc/RoleDefinition.java @@ -1,5 +1,7 @@ package org.jenkinsci.plugins.rolestrategy.casc; +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -7,12 +9,18 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Pattern; import org.jenkinsci.plugins.rolestrategy.permissions.PermissionHelper; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; /** * Role definition. Used for custom formatting @@ -23,6 +31,7 @@ @Restricted(NoExternalUse.class) public class RoleDefinition { + public static final Logger LOGGER = Logger.getLogger(RoleDefinition.class.getName()); private transient Role role; @NonNull @@ -32,7 +41,8 @@ public class RoleDefinition { @CheckForNull private final String pattern; private final Set permissions; - private final Set assignments; + + private SortedSet entries = Collections.emptySortedSet(); /** * Creates a RoleDefinition. @@ -41,19 +51,47 @@ public class RoleDefinition { * @param description Role description * @param pattern Role pattern * @param permissions Assigned permissions - * @param assignments Assigned SIDs */ @DataBoundConstructor - public RoleDefinition(@NonNull String name, @CheckForNull String description, @CheckForNull String pattern, - Collection permissions, Collection assignments) { + public RoleDefinition(String name, String description, String pattern, Collection permissions) { this.name = name; this.description = description; this.pattern = pattern; this.permissions = permissions != null ? new HashSet<>(permissions) : Collections.emptySet(); - this.assignments = assignments != null ? new HashSet<>(assignments) : Collections.emptySet(); this.role = getRole(); } + /** + * Legacy setter for string based assignments. + * + * @param assignments The assigned sids + * @deprecated Use {@link #setEntries(java.util.Collection)} instead. + */ + @DataBoundSetter + @Deprecated + public void setAssignments(Collection assignments) { + LOGGER.log(Level.WARNING, "Loading ambiguous role assignments via via configuration-as-code support"); + if (assignments != null) { + SortedSet entries = new TreeSet<>(); + for (String assignment : assignments) { + final RoleDefinitionEntry rde = new RoleDefinitionEntry(); + rde.setEither(assignment); + entries.add(rde); + } + this.entries = entries; + } + } + + /** + * Setter for entries. + * + * @param entries The permission entries + */ + @DataBoundSetter + public void setEntries(Collection entries) { + this.entries = entries != null ? new TreeSet<>(entries) : Collections.emptySortedSet(); + } + /** * Returns the corresponding Role object. * @@ -73,12 +111,10 @@ public String getName() { return name; } - @CheckForNull public String getDescription() { return description; } - @CheckForNull public String getPattern() { return pattern; } @@ -87,8 +123,109 @@ public Set getPermissions() { return Collections.unmodifiableSet(permissions); } - public Set getAssignments() { - return Collections.unmodifiableSet(assignments); + /** + * Deprecated, always returns null. + * + * @return null + */ + public Collection getAssignments() { + return null; + } + + public SortedSet getEntries() { + return entries; } + /** + * Maps a permission entry to the casc line. + */ + public static class RoleDefinitionEntry implements Comparable { + private /* quasi-final */ AuthorizationType type; + private /* quasi-final */ String name; + + @DataBoundConstructor + public RoleDefinitionEntry() { + } + + private void setTypeIfUndefined(AuthorizationType type) { + if (this.type == null) { + this.type = type; + } else { + throw new IllegalStateException("Cannot set two different types for '" + name + "'"); // TODO Add test for this + } + } + + @DataBoundSetter + public void setUser(String name) { + this.name = name; + setTypeIfUndefined(AuthorizationType.USER); + } + + @DataBoundSetter + public void setGroup(String name) { + this.name = name; + setTypeIfUndefined(AuthorizationType.GROUP); + } + + @DataBoundSetter + public void setEither(String name) { + this.name = name; + setTypeIfUndefined(AuthorizationType.EITHER); + } + + public String getUser() { + return type == AuthorizationType.USER ? name : null; + } + + public String getGroup() { + return type == AuthorizationType.GROUP ? name : null; + } + + public String getEither() { + return type == AuthorizationType.EITHER ? name : null; + } + + public PermissionEntry asPermissionEntry() { + return new PermissionEntry(type, name); + } + + /** + * Creates a RoleDefinitionEntry from a PermissionNetry. + * + * @param entry {@link PermissionEntry} + * @return RoleDefinitionEntry + */ + public static RoleDefinitionEntry fromPermissionEntry(PermissionEntry entry) { + final RoleDefinitionEntry roleDefinitionEntry = new RoleDefinitionEntry(); + roleDefinitionEntry.type = entry.getType(); + roleDefinitionEntry.name = entry.getSid(); + return roleDefinitionEntry; + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RoleDefinitionEntry that = (RoleDefinitionEntry) o; + return type == that.type && name.equals(that.name); + } + + @Override + public int compareTo(@NonNull RoleDefinitionEntry o) { + int typeCompare = this.type.compareTo(o.type); + if (typeCompare == 0) { + return this.name.compareTo(o.name); + } + return typeCompare; + } + } } diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/Messages.properties b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/Messages.properties index cdc5f577..b652940d 100644 --- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/Messages.properties +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/Messages.properties @@ -32,3 +32,4 @@ RoleBasedProjectNamingStrategy.NoPattern=Not matches to any pattern from role ba RoleBasedProjectNamingStrategy.JobNameConventionNotApplyed=\u2018{0}\u2019 does not match the job name convention pattern {1} RoleBasedProjectNamingStrategy.WhiteSpaceWillBeTrimmed=Leading and trailing whitespace characters will be removed RoleBasedProjectNamingStrategy.NotConfigured=Role-Based Naming Strategy not enabled +RoleBasedProjectNamingStrategy.Ambiguous=Ambiguous Permission Assignments in Role Strategy diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly index 9eea7af4..2e9580d4 100644 --- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly @@ -29,8 +29,8 @@ xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:local="local"> - - + + @@ -39,7 +39,7 @@ @@ -54,16 +54,21 @@ - - + + - - - - + + + + + + + + + - - + + @@ -81,19 +86,13 @@ ${role.key.name} -
- ${%User/group} + ${%User/Group}
+
- -

- - - - - - -
- + +
+ +
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly index 305877d4..da571908 100644 --- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly @@ -27,12 +27,11 @@ xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:local="local"> - - - + +
- +
@@ -48,7 +47,7 @@ - ${%User/group} + ${%User/Group} @@ -63,20 +62,25 @@ - - + + - - - - + + + + + + + + + - - + + - + - + @@ -95,13 +99,8 @@ - -

- - - - - - -
+ +
+ +
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly index 103ec531..e93456b4 100644 --- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly @@ -27,11 +27,10 @@ xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:local="local"> - - - + +
- +
@@ -48,7 +47,7 @@ - ${%User/group} + ${%User/Group} @@ -63,20 +62,25 @@ - - + + - - - - + + + + + + + + + - - + + - + - + @@ -95,13 +99,8 @@ - -

- - - - - - -
+ +
+ +
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.jelly index 40f29548..f6a4c957 100644 --- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.jelly +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.jelly @@ -48,41 +48,90 @@
- - + +
+ + +
+ +
+ ${%ambiguous} +
+
- - - - - - - - ${title} - + + + + +
+ +
+
+
+ + + + +
+ ${%Authenticated Users} +
+ +
+ + +
+ ${%Anonymous} +
+ +
+ + ${title} + +
+ + - - + + - - - - - - - + + + +
+
+ +
+ + +
+ +
+
+ +
+
+
+
+
+
-

${it.assignRolesName} @@ -94,12 +143,14 @@ +
- +
+ @@ -114,4 +165,4 @@ - + \ No newline at end of file diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.properties b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.properties new file mode 100644 index 00000000..b4fd4830 --- /dev/null +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-roles.properties @@ -0,0 +1,37 @@ +# The MIT License +# +# Copyright (c) 2022, Markus Winter +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +migrate_user=Migrate ambiguous permissions assignment to user {0} +migrate_group=Migrate ambiguous permissions assignment to group {0} + +promptUser=User name: +userExists=An entry for this user already exists +emptyUser=Please enter a user name + +promptGroup=Group name: +groupExists=An entry for this group already exists +emptyGroup=Please enter a group name + +ambiguous=This table contains rows with ambiguous entries. This means that they apply to both users and groups of the specified name. \ + If the current security realm does not distinguish between user names and group names unambiguously, and if users can either choose their own \ + user name or create new groups, this configuration may allow them to obtain greater permissions. \ + It is recommended that all ambiguous entries are replaced with ones that are either explicitly a user or a group. diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/index.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/index.jelly index 3a190b6b..e8605c4a 100644 --- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/index.jelly +++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/index.jelly @@ -49,14 +49,14 @@ - + - - - +
+ +
${title} @@ -95,11 +95,9 @@ - - - +
+ +
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-roles_fr.properties b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/index_fr.properties similarity index 100% rename from src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-roles_fr.properties rename to src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/index_fr.properties diff --git a/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.jelly b/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.jelly new file mode 100644 index 00000000..bb405e93 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.jelly @@ -0,0 +1,4 @@ + + + ${%blurb} + diff --git a/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.properties b/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.properties new file mode 100644 index 00000000..e203ca8e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/description.properties @@ -0,0 +1 @@ +blurb=Warns about ambiguous entries in Role Strategy assignments. diff --git a/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/message.jelly b/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/message.jelly new file mode 100644 index 00000000..914e2deb --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/rolestrategy/AmbiguousSidsAdminMonitor/message.jelly @@ -0,0 +1,25 @@ + + +
+
+
+ + +
+
+
+ + +
There are several permissions declared in Role Based Strategy plugin configuration, that are ambiguous. Classify them correctly with 'USER:username' or 'GROUP:groupname'.
+ +
    + +
  • + ${entry} +
  • +
    +
+ +
+ \ No newline at end of file diff --git a/src/main/webapp/css/role-strategy.css b/src/main/webapp/css/role-strategy.css index 2b9e84c6..ca54f8d3 100644 --- a/src/main/webapp/css/role-strategy.css +++ b/src/main/webapp/css/role-strategy.css @@ -26,7 +26,7 @@ .global-matrix-authorization-strategy-table { border-collapse: collapse; border-spacing: 0; - border: 1px solid #D3D7CF; + #border: 1px solid #D3D7CF; } .global-matrix-authorization-strategy-table .caption-row TH span { @@ -49,16 +49,13 @@ } .global-matrix-authorization-strategy-table TD.start { - border-top: 1px solid transparent; - border-left: 1px solid transparent; - border-bottom: 1px solid transparent; + border: 0; } .global-matrix-authorization-strategy-table TD.stop { - border-top: 1px solid transparent; - border-right: 1px solid transparent; - border-bottom: 1px solid transparent; white-space: nowrap; + text-align: left; + border: 0; } label.attach-previous { @@ -109,6 +106,45 @@ label.attach-previous { margin: 3px 0px; } +@keyframes highlightentry { + from { background: #C4C080; } + to { background: transparent; } +} + +.highlight-entry { + -webkit-animation: highlightentry 5s; + -moz-animation: highlightentry 5s; + animation: highlightentry 5s; +} + +.alert { + margin-top: 10px; +} + +.rsp-entry-not-found { + text-decoration: line-through; + color: grey; +} + +.rsp-table__icon { + vertical-align: initial!important; +} + +.rsp-table__icon-alert { + color: orange; +} + +.rsp-table__cell { + display: flex; + align-items: center; + gap: 3px; +} + +.rsp-remove, .migrate { + cursor: pointer; + height: 16px; +} + .patternAnchor { cursor: pointer; color: var(--link-color); @@ -121,6 +157,7 @@ label.attach-previous { .row-filter, .user-filter, .role-filter { display: none; + max-width: 500px; } .modal { diff --git a/src/main/webapp/js/tableAssign.js b/src/main/webapp/js/tableAssign.js index f758b0cb..7f0c3bd0 100644 --- a/src/main/webapp/js/tableAssign.js +++ b/src/main/webapp/js/tableAssign.js @@ -85,7 +85,7 @@ Behaviour.specify(".role-input-filter", "RoleBasedAuthorizationStrategy", 0, fun Behaviour.specify( ".role-strategy-add-button", "RoleBasedAuthorizationStrategy", 0, function(elem) { - makeButton(elem, function(e) { + elem.onclick = function(e) { let tableId = elem.getAttribute("data-table-id"); let table = document.getElementById(tableId); let templateId = elem.getAttribute("data-template-id"); @@ -105,41 +105,47 @@ Behaviour.specify( if (tbody.children.length >= footerLimit) { table.tFoot.style.display = "table-footer-group"; } - }); + } } ); - addButtonAction = function (e, template, table, tableHighlighter, tableId) { + let dataReference = e.target; + let type = dataReference.getAttribute('data-type'); let tbody = table.tBodies[0]; - let userInput = document.getElementById(tableId+'text') - let name = userInput.value; + + let name = prompt(dataReference.getAttribute('data-prompt')).trim(); if (name=="") { - alert("Please enter a user/group name"); + alert(dataReference.getAttribute('data-empty-message')); return; } - if (findElementsBySelector(table,"TR").find(function(n){return n.getAttribute("name")=='['+name+']';})!=null) { - alert("Entry for '"+name+"' already exists"); + if (findElementsBySelector(table,"TR").find(function(n){return n.getAttribute("name")=='['+type+':'+name+']';})!=null) { + alert(dataReference.getAttribute('data-error-message')); return; } - + copy = document.importNode(template,true); copy.removeAttribute("id"); copy.removeAttribute("style"); - + let children = copy.childNodes; + let tooltipDescription = "Group"; + if (type==="USER") { + tooltipDescription = "User"; + } children.forEach(function(item){ - item.outerHTML= item.outerHTML.replace(/{{USER}}/g, doubleEscapeHTML(name)); + item.outerHTML= item.outerHTML.replace(/{{USER}}/g, doubleEscapeHTML(name)).replace(/{{USERGROUP}}/g, tooltipDescription); }); - + copy.childNodes[1].innerHTML = escapeHTML(name); - copy.setAttribute("name",'['+name+']'); + copy.setAttribute("name",'['+type+':'+name+']'); tbody.appendChild(copy); Behaviour.applySubtree(table, true); tableHighlighter.scan(copy); -} + } + -Behaviour.specify(".global-matrix-authorization-strategy-table A.remove", 'RoleBasedAuthorizationStrategy', 0, function(e) { +Behaviour.specify(".global-matrix-authorization-strategy-table .rsp-remove", 'RoleBasedAuthorizationStrategy', 0, function(e) { e.onclick = function() { let table = findAncestor(this,"TABLE"); let tableId = table.getAttribute("id"); @@ -179,6 +185,82 @@ Behaviour.specify(".global-matrix-authorization-strategy-table TR.permission-row }); +/* + * Behavior for 'Migrate to user' element that exists for each ambiguous row + */ +Behaviour.specify(".global-matrix-authorization-strategy-table TD.stop .migrate", 'RoleBasedAuthorizationStrategy', 0, function(e) { + e.onclick = function() { + var tr = findAncestor(this,"TR"); + var name = tr.getAttribute('name'); + + var newName = name.replace('[EITHER:', '[USER:'); // migrate_user behavior + if (this.classList.contains('migrate_group')) { + newName = name.replace('[EITHER:', '[GROUP:'); + } + + var table = findAncestor(this,"TABLE"); + var tableRows = table.getElementsByTagName('tr'); + var newNameElement = null; + for (var i = 0; i < tableRows.length; i++) { + if (tableRows[i].getAttribute('name') === newName) { + newNameElement = tableRows[i]; + break; + } + } + if (newNameElement === tr) { + // uh-oh, we shouldn't be able to find ourselves, so just do nothing + return false; + } + if (newNameElement == null) { + // no row for this name exists yet, so transform the ambiguous row to unambiguous + tr.setAttribute('name', newName); + tr.removeAttribute('data-checked'); + + // remove migration buttons from updated row + var buttonContainer = findAncestor(this, "TD"); + var migrateButtons = buttonContainer.getElementsByClassName('migrate'); + for (var i = migrateButtons.length - 1; i >= 0; i--) { + migrateButtons[i].remove(); + } + } else { + // there's already a row for the migrated name (unusual but OK), so merge them + + // migrate permissions from this row + var ambiguousPermissionInputs = tr.getElementsByTagName("INPUT"); + var unambiguousPermissionInputs = newNameElement.getElementsByTagName("INPUT"); + for (var i = 0; i < ambiguousPermissionInputs.length; i++){ + if (ambiguousPermissionInputs[i].type == "checkbox") { + unambiguousPermissionInputs[i].checked |= ambiguousPermissionInputs[i].checked; + } + newNameElement.classList.add('highlight-entry'); + } + + // remove this row + tr.parentNode.removeChild(tr); + } + Behaviour.applySubtree(table, true); + + var hasAmbiguousRows = false; + + for (var i = 0; i < tableRows.length; i++) { + if (tableRows[i].getAttribute('name') !== null && tableRows[i].getAttribute('name').startsWith('[EITHER')) { + hasAmbiguousRows = true; + } + } + if (!hasAmbiguousRows) { + var alertElements = document.getElementsByClassName("alert"); + for (var i = 0; i < alertElements.length; i++) { + if (alertElements[i].hasAttribute('data-table-id') && alertElements[i].getAttribute('data-table-id') === table.getAttribute('id')) { + alertElements[i].style.display = 'none'; // TODO animate this? + } + } + } + + return false; + }; + e = null; // avoid memory leak +}); + document.addEventListener('DOMContentLoaded', function() { // global roles initialization var globalRoleInputFilter = document.getElementById('globalRoleInputFilter'); @@ -220,4 +302,4 @@ document.addEventListener('DOMContentLoaded', function() { tbody.removeChild(newAgentRowTemplate); agentTableHighlighter = new TableHighlighter('agentRoles', 0); -}); \ No newline at end of file +}); diff --git a/src/main/webapp/js/tableManage.js b/src/main/webapp/js/tableManage.js index 33e7930f..5358d4f1 100644 --- a/src/main/webapp/js/tableManage.js +++ b/src/main/webapp/js/tableManage.js @@ -171,7 +171,7 @@ updateTooltip = function(tr, td, pattern) { Behaviour.specify( ".role-strategy-add-button", "RoleBasedAuthorizationStrategy", 0, function(elem) { - makeButton(elem, function(e) { + elem.onclick = function(e) { let tableId = elem.getAttribute("data-table-id"); let table = document.getElementById(tableId); let templateId = elem.getAttribute("data-template-id"); @@ -190,7 +190,7 @@ Behaviour.specify( if (tbody.children.length >= footerLimit) { table.tFoot.style.display = "table-footer-group"; } - }); + } } ); @@ -253,7 +253,7 @@ addButtonAction = function(e, template, table, tableHighlighter, tableId) { } -Behaviour.specify(".global-matrix-authorization-strategy-table A.remove", 'RoleBasedAuthorizationStrategy', 0, function(e) { +Behaviour.specify(".global-matrix-authorization-strategy-table span.rsp-remove", 'RoleBasedAuthorizationStrategy', 0, function(e) { e.onclick = function() { let table = findAncestor(this, "TABLE"); let tableId = table.getAttribute("id"); diff --git a/src/test/java/com/michelin/cio/hudson/plugins/rolestrategy/ApiTest.java b/src/test/java/com/michelin/cio/hudson/plugins/rolestrategy/ApiTest.java index 073cf41b..229017b8 100644 --- a/src/test/java/com/michelin/cio/hudson/plugins/rolestrategy/ApiTest.java +++ b/src/test/java/com/michelin/cio/hudson/plugins/rolestrategy/ApiTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.DummySecurityRealm; import org.jvnet.hudson.test.MockFolder; import org.springframework.security.core.Authentication; @@ -39,17 +40,19 @@ public class ApiTest { @Rule public final JenkinsRule jenkinsRule = new JenkinsRule(); private JenkinsRule.WebClient webClient; + private DummySecurityRealm securityRealm; @Before public void setUp() throws Exception { // Setting up jenkins configurations - jenkinsRule.jenkins.setSecurityRealm(jenkinsRule.createDummySecurityRealm()); + securityRealm = jenkinsRule.createDummySecurityRealm(); + jenkinsRule.jenkins.setSecurityRealm(securityRealm); jenkinsRule.jenkins.setAuthorizationStrategy(new RoleBasedAuthorizationStrategy()); jenkinsRule.jenkins.setCrumbIssuer(null); // Adding admin role and assigning adminUser RoleBasedAuthorizationStrategy.getInstance().doAddRole("globalRoles", "adminRole", "hudson.model.Hudson.Read,hudson.model.Hudson.Administer,hudson.security.Permission.GenericRead", "false", ""); - RoleBasedAuthorizationStrategy.getInstance().doAssignRole("globalRoles", "adminRole", "adminUser"); + RoleBasedAuthorizationStrategy.getInstance().doAssignUserRole("globalRoles", "adminRole", "adminUser"); webClient = jenkinsRule.createWebClient(); webClient.login("adminUser", "adminUser"); } @@ -72,9 +75,9 @@ public void testAddRole() throws IOException { // Verifying that the role is in RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); - SortedMap> grantedRoles = strategy.getGrantedRoles(RoleType.Project); + SortedMap> grantedRoles = strategy.getGrantedRolesEntries(RoleType.Project); boolean foundRole = false; - for (Map.Entry> entry : grantedRoles.entrySet()) { + for (Map.Entry> entry : grantedRoles.entrySet()) { Role role = entry.getKey(); if (role.getName().equals("new-role") && role.getPattern().pattern().equals(pattern)) { foundRole = true; @@ -105,6 +108,7 @@ public void testGetRole() throws IOException { public void testAssignRole() throws IOException { String roleName = "new-role"; String sid = "alice"; + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.EITHER, sid); Authentication alice = User.getById(sid, true).impersonate2(); // Confirming that alice does not have access before assigning MockFolder folder = jenkinsRule.createFolder("test-folder"); @@ -121,12 +125,12 @@ public void testAssignRole() throws IOException { // Verifying that alice is assigned to the role "new-role" RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); - SortedMap> roles = strategy.getGrantedRoles(RoleType.Project); + SortedMap> roles = strategy.getGrantedRolesEntries(RoleType.Project); boolean found = false; - for (Map.Entry> entry : roles.entrySet()) { + for (Map.Entry> entry : roles.entrySet()) { Role role = entry.getKey(); - Set sids = entry.getValue(); - if (role.getName().equals(roleName) && sids.contains(sid)) { + Set sids = entry.getValue(); + if (role.getName().equals(roleName) && sids.contains(sidEntry)) { found = true; break; } @@ -142,6 +146,7 @@ public void testUnassignRole() throws IOException { String roleName = "new-role"; String sid = "alice"; + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.EITHER, sid); testAssignRole(); // assign alice to a role named "new-role" that has configure access to "test-folder.*" URL apiURL = new URL(jenkinsRule.jenkins.getRootUrl() + "role-strategy/strategy/unassignRole"); WebRequest request = new WebRequest(apiURL, HttpMethod.POST); @@ -152,11 +157,143 @@ public void testUnassignRole() throws IOException { // Verifying that alice no longer has permissions RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); - SortedMap> roles = strategy.getGrantedRoles(RoleType.Project); - for (Map.Entry> entry : roles.entrySet()) { + SortedMap> roles = strategy.getGrantedRolesEntries(RoleType.Project); + for (Map.Entry> entry : roles.entrySet()) { Role role = entry.getKey(); - Set sids = entry.getValue(); - assertFalse("Checking if Alice is still assigned to new-role", role.getName().equals("new-role") && sids.contains("alice")); + Set sids = entry.getValue(); + assertFalse("Checking if Alice is still assigned to new-role", role.getName().equals("new-role") && sids.contains(sidEntry)); + } + // Verifying that ACL is updated + Authentication alice = User.getById("alice", false).impersonate2(); + Item folder = jenkinsRule.jenkins.getItemByFullName("test-folder"); + assertFalse(folder.hasPermission2(alice, Item.CONFIGURE)); + } + + @Test + public void testAssignUserRole() throws IOException { + String roleName = "new-role"; + String sid = "alice"; + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.USER, sid); + Authentication alice = User.getById(sid, true).impersonate2(); + // Confirming that alice does not have access before assigning + MockFolder folder = jenkinsRule.createFolder("test-folder"); + assertFalse(folder.hasPermission2(alice, Item.CONFIGURE)); + + // Assigning role using web request + testAddRole(); // adds a role "new-role" that has configure access on "test-folder.*" + URL apiUrl = new URL(jenkinsRule.jenkins.getRootUrl() + "role-strategy/strategy/assignUserRole"); + WebRequest request = new WebRequest(apiUrl, HttpMethod.POST); + request.setRequestParameters(Arrays.asList(new NameValuePair("type", RoleType.Project.getStringType()), + new NameValuePair("roleName", roleName), new NameValuePair("user", sid))); + Page page = webClient.getPage(request); + assertEquals("Testing if request is successful", HttpURLConnection.HTTP_OK, page.getWebResponse().getStatusCode()); + + // Verifying that alice is assigned to the role "new-role" + RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); + SortedMap> roles = strategy.getGrantedRolesEntries(RoleType.Project); + boolean found = false; + for (Map.Entry> entry : roles.entrySet()) { + Role role = entry.getKey(); + Set sids = entry.getValue(); + if (role.getName().equals(roleName) && sids.contains(sidEntry)) { + found = true; + break; + } + } + assertTrue(found); + // Verifying that ACL is updated + assertTrue(folder.hasPermission2(alice, Item.CONFIGURE)); + } + + @Test + public void testUnassignUserRole() throws IOException { + + String roleName = "new-role"; + String sid = "alice"; + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.USER, sid); + testAssignUserRole(); // assign alice to a role named "new-role" that has configure access to "test-folder.*" + URL apiURL = new URL(jenkinsRule.jenkins.getRootUrl() + "role-strategy/strategy/unassignUserRole"); + WebRequest request = new WebRequest(apiURL, HttpMethod.POST); + request.setRequestParameters(Arrays.asList(new NameValuePair("type", RoleType.Project.getStringType()), + new NameValuePair("roleName", roleName), new NameValuePair("user", sid))); + Page page = webClient.getPage(request); + assertEquals("Testing if request is successful", HttpURLConnection.HTTP_OK, page.getWebResponse().getStatusCode()); + + // Verifying that alice no longer has permissions + RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); + SortedMap> roles = strategy.getGrantedRolesEntries(RoleType.Project); + for (Map.Entry> entry : roles.entrySet()) { + Role role = entry.getKey(); + Set sids = entry.getValue(); + assertFalse("Checking if Alice is still assigned to new-role", role.getName().equals("new-role") && sids.contains(sidEntry)); + } + // Verifying that ACL is updated + Authentication alice = User.getById("alice", false).impersonate2(); + Item folder = jenkinsRule.jenkins.getItemByFullName("test-folder"); + assertFalse(folder.hasPermission2(alice, Item.CONFIGURE)); + } + + @Test + public void testAssignGroupRole() throws IOException { + String roleName = "new-role"; + String sid = "alice"; + String group = "group"; + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.GROUP, group); + User user = User.getById(sid, true); + securityRealm.addGroups(sid, group); + Authentication alice = user.impersonate2(); + // Confirming that alice does not have access before assigning + MockFolder folder = jenkinsRule.createFolder("test-folder"); + assertFalse(folder.hasPermission2(alice, Item.CONFIGURE)); + + // Assigning role using web request + testAddRole(); // adds a role "new-role" that has configure access on "test-folder.*" + URL apiUrl = new URL(jenkinsRule.jenkins.getRootUrl() + "role-strategy/strategy/assignGroupRole"); + WebRequest request = new WebRequest(apiUrl, HttpMethod.POST); + request.setRequestParameters(Arrays.asList(new NameValuePair("type", RoleType.Project.getStringType()), + new NameValuePair("roleName", roleName), new NameValuePair("group", group))); + Page page = webClient.getPage(request); + assertEquals("Testing if request is successful", HttpURLConnection.HTTP_OK, page.getWebResponse().getStatusCode()); + + // Verifying that alice is assigned to the role "new-role" + RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); + SortedMap> roles = strategy.getGrantedRolesEntries(RoleType.Project); + boolean found = false; + for (Map.Entry> entry : roles.entrySet()) { + Role role = entry.getKey(); + Set sids = entry.getValue(); + if (role.getName().equals(roleName) && sids.contains(sidEntry)) { + found = true; + break; + } + } + assertTrue(found); + // Verifying that ACL is updated + assertTrue(folder.hasPermission2(alice, Item.CONFIGURE)); + } + + @Test + public void testUnassignGroupRole() throws IOException { + + String roleName = "new-role"; + String sid = "alice"; + String group = "group"; + PermissionEntry sidEntry = new PermissionEntry(AuthorizationType.USER, sid); + testAssignGroupRole(); // assign alice to a role named "new-role" that has configure access to "test-folder.*" + URL apiURL = new URL(jenkinsRule.jenkins.getRootUrl() + "role-strategy/strategy/unassignGroupRole"); + WebRequest request = new WebRequest(apiURL, HttpMethod.POST); + request.setRequestParameters(Arrays.asList(new NameValuePair("type", RoleType.Project.getStringType()), + new NameValuePair("roleName", roleName), new NameValuePair("group", group))); + Page page = webClient.getPage(request); + assertEquals("Testing if request is successful", HttpURLConnection.HTTP_OK, page.getWebResponse().getStatusCode()); + + // Verifying that alice no longer has permissions + RoleBasedAuthorizationStrategy strategy = RoleBasedAuthorizationStrategy.getInstance(); + SortedMap> roles = strategy.getGrantedRolesEntries(RoleType.Project); + for (Map.Entry> entry : roles.entrySet()) { + Role role = entry.getKey(); + Set sids = entry.getValue(); + assertFalse("Checking if Alice is still assigned to new-role", role.getName().equals("new-role") && sids.contains(sidEntry)); } // Verifying that ACL is updated Authentication alice = User.getById("alice", false).impersonate2(); diff --git a/src/test/java/jmh/benchmarks/CascBenchmark.java b/src/test/java/jmh/benchmarks/CascBenchmark.java index 4e779f73..c2c55c9b 100644 --- a/src/test/java/jmh/benchmarks/CascBenchmark.java +++ b/src/test/java/jmh/benchmarks/CascBenchmark.java @@ -7,6 +7,7 @@ import static org.jenkinsci.plugins.rolestrategy.PermissionAssert.assertHasPermission; import com.cloudbees.hudson.plugins.folder.Folder; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -58,7 +59,7 @@ public void setup() throws Exception { assertThat("Authorization Strategy has been read incorrectly", s, instanceOf(RoleBasedAuthorizationStrategy.class)); rbas = (RoleBasedAuthorizationStrategy) s; - Map> globalRoles = rbas.getGrantedRoles(RoleType.Global); + Map> globalRoles = rbas.getGrantedRolesEntries(RoleType.Global); assertThat(Objects.requireNonNull(globalRoles).size(), equalTo(2)); // Admin has configuration access diff --git a/src/test/java/jmh/benchmarks/FolderAccessBenchmark.java b/src/test/java/jmh/benchmarks/FolderAccessBenchmark.java index c60ff095..0e1d574d 100644 --- a/src/test/java/jmh/benchmarks/FolderAccessBenchmark.java +++ b/src/test/java/jmh/benchmarks/FolderAccessBenchmark.java @@ -1,6 +1,8 @@ package jmh.benchmarks; import com.cloudbees.hudson.plugins.folder.Folder; +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.michelin.cio.hudson.plugins.rolestrategy.RoleMap; @@ -47,7 +49,7 @@ public void setup() throws Exception { Jenkins jenkins = Objects.requireNonNull(Jenkins.getInstanceOrNull()); jenkins.setSecurityRealm(new JenkinsRule().createDummySecurityRealm()); - SortedMap> projectRoles = new TreeMap<>(); + SortedMap> projectRoles = new TreeMap<>(); Set userPermissions = new HashSet<>(); Collections.addAll(userPermissions, "hudson.model.Item.Discover", "hudson.model.Item.Read"); @@ -82,16 +84,16 @@ public void setup() throws Exception { } } - Set users = new HashSet<>(); + Set users = new HashSet<>(); for (int k = 0; k < random.nextInt(5); k++) { - users.add("user" + random.nextInt(100)); + users.add(new PermissionEntry(AuthorizationType.USER, "user" + random.nextInt(100))); } - Set maintainers = new HashSet<>(2); - maintainers.add("user" + random.nextInt(100)); - maintainers.add("user" + random.nextInt(100)); + Set maintainers = new HashSet<>(2); + maintainers.add(new PermissionEntry(AuthorizationType.USER, "user" + random.nextInt(100))); + maintainers.add(new PermissionEntry(AuthorizationType.USER, "user" + random.nextInt(100))); - Set admin = Collections.singleton("user" + random.nextInt(100)); + Set admin = Collections.singleton(new PermissionEntry(AuthorizationType.USER, "user" + random.nextInt(100))); Role userRole = new Role(String.format("user%d-%d", i, j), "TopFolder" + i + "(/BottomFolder" + j + "/.*)?", userPermissions, ""); Role maintainerRole = new Role(String.format("maintainer%d-%d", i, j), "TopFolder" + i + "/BottomFolder" + j + "(/.*)", diff --git a/src/test/java/jmh/benchmarks/PermissionBenchmark.java b/src/test/java/jmh/benchmarks/PermissionBenchmark.java index 184dec0d..10d232a7 100644 --- a/src/test/java/jmh/benchmarks/PermissionBenchmark.java +++ b/src/test/java/jmh/benchmarks/PermissionBenchmark.java @@ -1,5 +1,7 @@ package jmh.benchmarks; +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.michelin.cio.hudson.plugins.rolestrategy.RoleMap; @@ -34,7 +36,7 @@ public void setup() { Set permissionSet = Collections.singleton("hudson.model.Hudson.Administer"); Role role = new Role("USERS", ".*", permissionSet, "description"); RoleMap roleMap = new RoleMap(new TreeMap<>(// expects a sorted map - Collections.singletonMap(role, Collections.singleton("alice")))); + Collections.singletonMap(role, Collections.singleton(new PermissionEntry(AuthorizationType.USER, "alice"))))); jenkins.setAuthorizationStrategy( new RoleBasedAuthorizationStrategy(Collections.singletonMap(RoleBasedAuthorizationStrategy.GLOBAL, roleMap))); diff --git a/src/test/java/jmh/benchmarks/RoleMapBenchmark.java b/src/test/java/jmh/benchmarks/RoleMapBenchmark.java index a259e6d0..ad32c475 100644 --- a/src/test/java/jmh/benchmarks/RoleMapBenchmark.java +++ b/src/test/java/jmh/benchmarks/RoleMapBenchmark.java @@ -1,5 +1,7 @@ package jmh.benchmarks; +import com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleMap; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -79,20 +81,21 @@ abstract class RoleMapBenchmarkState extends JmhBenchmarkState { @Override public void setup() throws Exception { - SortedMap> map = new TreeMap<>(); + SortedMap> map = new TreeMap<>(); final int roleCount = getRoleCount(); for (int i = 0; i < roleCount; i++) { Role role = new Role("role" + i, ".*", new HashSet<>(Arrays.asList("hudson.model.Item.Discover", "hudson.model.Item.Configure")), ""); - map.put(role, Collections.singleton("user" + i)); + map.put(role, Collections.singleton(new PermissionEntry(AuthorizationType.USER, "user" + i))); } roleMap = new RoleMap(map); // RoleMap#hasPermission is private in RoleMap - hasPermission = Class.forName("com.michelin.cio.hudson.plugins.rolestrategy.RoleMap").getDeclaredMethod("hasPermission", String.class, - Permission.class, RoleType.class, AccessControlled.class); + hasPermission = Class.forName("com.michelin.cio.hudson.plugins.rolestrategy.RoleMap").getDeclaredMethod("hasPermission", + PermissionEntry.class, Permission.class, RoleType.class, AccessControlled.class); hasPermission.setAccessible(true); - functionArgs = new Object[] { "user3", Permission.CREATE, null, null }; + + functionArgs = new Object[] { new PermissionEntry(AuthorizationType.USER, "user3"), Permission.CREATE, null, null }; } abstract int getRoleCount(); diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/ConfigurationAsCodeTest.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/ConfigurationAsCodeTest.java index febc59b6..4e6d32b3 100644 --- a/src/test/java/org/jenkinsci/plugins/rolestrategy/ConfigurationAsCodeTest.java +++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/ConfigurationAsCodeTest.java @@ -13,6 +13,7 @@ import static org.junit.Assert.assertNotNull; import com.cloudbees.hudson.plugins.folder.Folder; +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; import com.michelin.cio.hudson.plugins.rolestrategy.Role; import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType; @@ -76,7 +77,7 @@ public void shouldReadRolesCorrectly() throws Exception { assertThat("Authorization Strategy has been read incorrectly", s, instanceOf(RoleBasedAuthorizationStrategy.class)); RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) s; - Map> globalRoles = rbas.getGrantedRoles(RoleType.Global); + Map> globalRoles = rbas.getGrantedRolesEntries(RoleType.Global); assertThat(globalRoles.size(), equalTo(2)); // Admin has configuration access @@ -131,7 +132,7 @@ public void shouldHandleNullItemsAndAgentsCorrectly() { assertThat("Authorization Strategy has been read incorrectly", s, instanceOf(RoleBasedAuthorizationStrategy.class)); RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) s; - Map> globalRoles = rbas.getGrantedRoles(RoleType.Global); + Map> globalRoles = rbas.getGrantedRolesEntries(RoleType.Global); assertThat(globalRoles.size(), equalTo(2)); } diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategyTest.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategyTest.java index ceb14f9b..cae6c3ea 100644 --- a/src/test/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategyTest.java +++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/RoleBasedProjectNamingStrategyTest.java @@ -36,6 +36,12 @@ public class RoleBasedProjectNamingStrategyTest { private User userGlobalGroup; private User userJobCreateGroup; private User userReadGroup; + private User eitherGlobal; + private User eitherJobCreate; + private User eitherRead; + private User userEitherGlobalGroup; + private User userEitherJobCreateGroup; + private User userEitherReadGroup; @Before public void setup() { @@ -47,9 +53,18 @@ public void setup() { userGlobalGroup = User.getById("userGlobalGroup", true); userJobCreateGroup = User.getById("userJobCreateGroup", true); userReadGroup = User.getById("userReadGroup", true); + eitherGlobal = User.getById("eitherGlobal", true); + eitherJobCreate = User.getById("eitherJobCreate", true); + eitherRead = User.getById("eitherRead", true); + userEitherGlobalGroup = User.getById("userEitherGlobalGroup", true); + userEitherReadGroup = User.getById("userEitherReadGroup", true); + userEitherJobCreateGroup = User.getById("userEitherJobCreateGroup", true); securityRealm.addGroups("userGlobalGroup", "groupGlobal"); securityRealm.addGroups("userJobCreateGroup", "groupJobCreate"); securityRealm.addGroups("userReadGroup", "groupRead"); + securityRealm.addGroups("userEitherGlobalGroup", "eitherGlobal"); + securityRealm.addGroups("userEitherJobCreateGroup", "eitherJobCreate"); + securityRealm.addGroups("userEitherReadGroup", "eitherRead"); } @Test @@ -65,6 +80,13 @@ public void createPermission() { assertHasPermission(userGlobalGroup, j.jenkins, Item.CREATE); assertHasPermission(userJobCreateGroup, j.jenkins, Item.CREATE); assertHasNoPermission(userReadGroup, j.jenkins, Item.CREATE); + + assertHasPermission(eitherGlobal, j.jenkins, Item.CREATE); + assertHasPermission(eitherJobCreate, j.jenkins, Item.CREATE); + assertHasPermission(userEitherGlobalGroup, j.jenkins, Item.CREATE); + assertHasPermission(userEitherJobCreateGroup, j.jenkins, Item.CREATE); + assertHasNoPermission(eitherRead, j.jenkins, Item.CREATE); + assertHasNoPermission(userEitherReadGroup, j.jenkins, Item.CREATE); } @Test @@ -76,6 +98,8 @@ public void globalUserCanCreateAnyJob() { checkName(userGlobal, "anyJobName", null); checkName(userGlobalGroup, "anyJobName", null); + checkName(eitherGlobal, "anyJobName", null); + checkName(userEitherGlobalGroup, "anyJobName", null); } @Test @@ -89,6 +113,8 @@ public void itemUserCanCreateOnlyAllowedJobs() { checkName(userJobCreate, "jobAllowed", "folder"); checkName(userJobCreateGroup, "jobAllowed", null); checkName(userJobCreateGroup, "jobAllowed", "folder"); + checkName(eitherJobCreate, "jobAllowed", null); + checkName(userEitherJobCreateGroup, "jobAllowed", "folder"); Failure f = Assert.assertThrows(Failure.class, () -> checkName(userJobCreate, "notAllowed", null)); assertThat(f.getMessage(), containsString("does not match the job name convention")); f = Assert.assertThrows(Failure.class, () -> checkName(userJobCreate, "notAllowed", "folder")); @@ -101,6 +127,19 @@ public void itemUserCanCreateOnlyAllowedJobs() { assertThat(f.getMessage(), containsString("does not match the job name convention")); f = Assert.assertThrows(Failure.class, () -> checkName(userJobCreateGroup, "jobAllowed", "folder2")); assertThat(f.getMessage(), containsString("does not match the job name convention")); + + f = Assert.assertThrows(Failure.class, () -> checkName(eitherJobCreate, "notAllowed", null)); + assertThat(f.getMessage(), containsString("does not match the job name convention")); + f = Assert.assertThrows(Failure.class, () -> checkName(eitherJobCreate, "notAllowed", "folder")); + assertThat(f.getMessage(), containsString("does not match the job name convention")); + f = Assert.assertThrows(Failure.class, () -> checkName(eitherJobCreate, "jobAllowed", "folder2")); + assertThat(f.getMessage(), containsString("does not match the job name convention")); + f = Assert.assertThrows(Failure.class, () -> checkName(userEitherJobCreateGroup, "notAllowed", null)); + assertThat(f.getMessage(), containsString("does not match the job name convention")); + f = Assert.assertThrows(Failure.class, () -> checkName(userEitherJobCreateGroup, "notAllowed", "folder")); + assertThat(f.getMessage(), containsString("does not match the job name convention")); + f = Assert.assertThrows(Failure.class, () -> checkName(userEitherJobCreateGroup, "jobAllowed", "folder2")); + assertThat(f.getMessage(), containsString("does not match the job name convention")); } @Test @@ -143,6 +182,15 @@ public void readUserCantCreateAllowedJobs() { assertThat(f.getMessage(), is("No Create Permissions!")); f = Assert.assertThrows(Failure.class, () -> checkName(userReadGroup, "jobAllowed", "folder")); assertThat(f.getMessage(), is("No Create Permissions!")); + + f = Assert.assertThrows(Failure.class, () -> checkName(eitherRead, "jobAllowed", null)); + assertThat(f.getMessage(), is("No Create Permissions!")); + f = Assert.assertThrows(Failure.class, () -> checkName(eitherRead, "jobAllowed", "folder")); + assertThat(f.getMessage(), is("No Create Permissions!")); + f = Assert.assertThrows(Failure.class, () -> checkName(userEitherReadGroup, "jobAllowed", null)); + assertThat(f.getMessage(), is("No Create Permissions!")); + f = Assert.assertThrows(Failure.class, () -> checkName(userEitherReadGroup, "jobAllowed", "folder")); + assertThat(f.getMessage(), is("No Create Permissions!")); } @Test @@ -168,5 +216,4 @@ private void checkName(final String jobName, final String parentName) { RoleBasedProjectNamingStrategy rbpns = (RoleBasedProjectNamingStrategy) pns; rbpns.checkName(parentName, jobName); } - } diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/Security2374Test.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/Security2374Test.java new file mode 100644 index 00000000..14bd47ab --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/Security2374Test.java @@ -0,0 +1,160 @@ +/* + * The MIT License + * + * Copyright (c) Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.rolestrategy; + +import static com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType.EITHER; +import static com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType.GROUP; +import static com.michelin.cio.hudson.plugins.rolestrategy.AuthorizationType.USER; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.michelin.cio.hudson.plugins.rolestrategy.PermissionEntry; +import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy; +import hudson.model.User; +import hudson.security.ACL; +import hudson.security.SidACL; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import java.io.File; +import java.io.FileReader; +import java.io.Reader; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.apache.commons.io.IOUtils; +import org.hamcrest.Matchers; +import org.htmlunit.html.HtmlPage; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; +import org.jvnet.hudson.test.recipes.LocalData; + + +public class Security2374Test { + + @Rule + public JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); + + @Test + @ConfiguredWithCode("Security2374Test/casc.yaml") + public void readFromCasc() throws Exception { + RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) j.jenkins.getAuthorizationStrategy(); + + // So we can log in + JenkinsRule.DummySecurityRealm dsr = j.createDummySecurityRealm(); + dsr.addGroups("gerry", "groupname"); + + dsr.addGroups("intruderA", "username"); + dsr.addGroups("intruderB", "eitherSID"); + dsr.addGroups("intruderC", "indifferentSID"); + + j.jenkins.setSecurityRealm(dsr); + + ACL acl = j.jenkins.getACL(); + assertTrue(acl.hasPermission2(User.getById("indifferentSID", true).impersonate2(), Jenkins.ADMINISTER)); + assertTrue(acl.hasPermission2(User.getById("username", true).impersonate2(), Jenkins.ADMINISTER)); + assertTrue(acl.hasPermission2(User.getById("gerry", true).impersonate2(), Jenkins.ADMINISTER)); + assertFalse(acl.hasPermission2(User.getById("intruderA", true).impersonate2(), Jenkins.ADMINISTER)); + // Users with group named after one of the EITHER sids (explicit or implicit) are let in + assertTrue(acl.hasPermission2(User.getById("intruderB", true).impersonate2(), Jenkins.ADMINISTER)); + assertTrue(acl.hasPermission2(User.getById("intruderC", true).impersonate2(), Jenkins.ADMINISTER)); + + AmbiguousSidsAdminMonitor am = AmbiguousSidsAdminMonitor.get(); + assertTrue(am.isActivated()); + assertThat(am.getAmbiguousEntries(), Matchers.containsInAnyOrder("eitherSID", "indifferentSID")); + + HtmlPage manage; + try (JenkinsRule.WebClient wc = j.createWebClient()) { + wc.login("gerry", "gerry"); + manage = wc.goTo("manage"); + String source = manage.getWebResponse().getContentAsString(); + assertThat(source, Matchers.containsString("'USER:username' or 'GROUP:groupname'")); + assertThat(source, Matchers.containsString("indifferentSID")); + assertThat(source, Matchers.containsString("eitherSID")); + } + } + + @Test + @WithoutJenkins + public void createPermissionEntry() { + assertThat(PermissionEntry.user("foo"), equalTo(permissionEntry("USER:foo"))); + assertThat(PermissionEntry.group("foo"), equalTo(permissionEntry("GROUP:foo"))); + assertThat(permissionEntry(""), nullValue()); + assertThat(permissionEntry(":-)"), nullValue()); + assertThat(permissionEntry("Re:"), nullValue()); + assertThat(permissionEntry("GROUP:"), nullValue()); + assertThat(permissionEntry("USER:"), nullValue()); + } + + public PermissionEntry permissionEntry(String in) { + return PermissionEntry.fromString(in); + } + + @Test + public void adminMonitor() throws Exception { + AmbiguousSidsAdminMonitor am = AmbiguousSidsAdminMonitor.get(); + assertFalse(am.isActivated()); + assertThat(am.getAmbiguousEntries(), Matchers.emptyIterable()); + + am.updateEntries(Collections.singletonList(new PermissionEntry(EITHER, "foo"))); + assertTrue(am.isActivated()); + assertThat(am.getAmbiguousEntries(), equalTo(Collections.singletonList("foo"))); + + am.updateEntries(Collections.emptyList()); + assertFalse(am.isActivated()); + assertThat(am.getAmbiguousEntries(), Matchers.emptyIterable()); + + am.updateEntries(Arrays.asList(new PermissionEntry(USER, "foo"), new PermissionEntry(GROUP, "bar"))); + assertFalse(am.isActivated()); + assertThat(am.getAmbiguousEntries(), Matchers.emptyIterable()); + + am.updateEntries(Arrays.asList(new PermissionEntry(USER, "foo"), new PermissionEntry(GROUP, "bar"), + new PermissionEntry(EITHER, "baz"))); + assertTrue(am.isActivated()); + assertThat(am.getAmbiguousEntries(), equalTo(Collections.singletonList("baz"))); + } + + @LocalData + @Test + public void test3xDataMigration() throws Exception { + assertTrue(j.jenkins.getAuthorizationStrategy() instanceof RoleBasedAuthorizationStrategy); + final RoleBasedAuthorizationStrategy authorizationStrategy = (RoleBasedAuthorizationStrategy) j.jenkins.getAuthorizationStrategy(); + final SidACL acl = authorizationStrategy.getRootACL(); + final File configXml = new File(j.jenkins.getRootDir(), "config.xml"); + List configLines; + try (Reader reader = new FileReader(configXml)) { + configLines = IOUtils.readLines(reader); + } + assertFalse(acl.hasPermission2(User.getById("markus", true).impersonate2(), Jenkins.ADMINISTER)); + assertTrue(acl.hasPermission2(User.getById("markus", true).impersonate2(), Jenkins.READ)); + assertTrue(acl.hasPermission2(User.getById("admin", true).impersonate2(), Jenkins.ADMINISTER)); + assertTrue(configLines.stream().anyMatch(line -> line.contains("admin"))); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest.java new file mode 100644 index 00000000..ec4cbbd9 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest.java @@ -0,0 +1,108 @@ +package org.jenkinsci.plugins.rolestrategy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import hudson.model.User; +import hudson.security.ACL; +import hudson.security.ACLContext; +import jenkins.model.Jenkins; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.DummySecurityRealm; +import org.jvnet.hudson.test.recipes.LocalData; + +public class UserGroupSeparationTest { + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + private User user; + private User group; + private User either; + private User userWithGroup; + private User groupAsUser; + private User eitherGroup; + + @Before + public void setup() { + DummySecurityRealm securityRealm = jenkinsRule.createDummySecurityRealm(); + jenkinsRule.jenkins.setSecurityRealm(securityRealm); + user = User.getById("user", true); + group = User.getById("group", true); + userWithGroup = User.getById("userWithGroup", true); + groupAsUser = User.getById("groupAsUser", true); + either = User.getById("either", true); + eitherGroup = User.getById("eitherGroup", true); + securityRealm.addGroups("userWithGroup", "group"); + securityRealm.addGroups("groupAsUser", "user"); + securityRealm.addGroups("eitherGroup", "either"); + } + + /** + * A user that matches an entry of type user should be granted access. + */ + @LocalData + @Test + public void user_matches_user_has_access() { + try (ACLContext c = ACL.as(User.getById("user", false))) { + assertTrue(jenkinsRule.jenkins.hasPermission(Jenkins.ADMINISTER)); + } + } + + /** + * A user that is in a group that matches an entry of type group should be granted access. + */ + @LocalData + @Test + public void usergroup_matches_group_has_acess() { + try (ACLContext c = ACL.as(User.getById("userWithGroup", false))) { + assertTrue(jenkinsRule.jenkins.hasPermission(Jenkins.ADMINISTER)); + } + } + + /** + * A user that has a name matching a group should not have access. + */ + @LocalData + @Test + public void user_matches_group_has_no_access() { + try (ACLContext c = ACL.as(User.getById("group", false))) { + assertFalse(jenkinsRule.jenkins.hasPermission(Jenkins.ADMINISTER)); + } + } + + /** + * A user that is in a group that matches an entry of type user should not have access. + */ + @LocalData + @Test + public void group_matches_user_has_no_acess() { + try (ACLContext c = ACL.as(User.getById("groupAsUser", false))) { + assertFalse(jenkinsRule.jenkins.hasPermission(Jenkins.ADMINISTER)); + } + } + + /** + * A user that matches an entry of type either should have access. + */ + @LocalData + @Test + public void user_matches_either_has_access() { + try (ACLContext c = ACL.as(User.getById("either", false))) { + assertTrue(jenkinsRule.jenkins.hasPermission(Jenkins.ADMINISTER)); + } + } + + /** + * A user that is in a group matches an entry of type either should have access. + */ + @LocalData + @Test + public void group_matches_either_has_access() { + try (ACLContext c = ACL.as(User.getById("eitherGroup", false))) { + assertTrue(jenkinsRule.jenkins.hasPermission(Jenkins.ADMINISTER)); + } + } +} diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/AuthorizeProjectTest/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/AuthorizeProjectTest/config.xml index f070fca4..9fa4214e 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/AuthorizeProjectTest/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/AuthorizeProjectTest/config.xml @@ -14,7 +14,7 @@ hudson.model.Hudson.Administer - admin + admin @@ -24,7 +24,7 @@ hudson.model.Item.Build - authenticated + authenticated @@ -34,7 +34,7 @@ hudson.model.Computer.Build - tester + tester diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Export.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Export.yml index 671a6a46..63473d2b 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Export.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Export.yml @@ -1,43 +1,43 @@ roleBased: roles: agents: - - assignments: - - "user1" - description: "Agent 1" + - description: "Agent 1" + entries: + - user: "user1" name: "Agent1" pattern: "agent1" permissions: - "Agent/Build" global: - - assignments: - - "admin" - description: "Jenkins administrators" + - description: "Jenkins administrators" + entries: + - user: "admin" name: "admin" pattern: ".*" permissions: - "Overall/Administer" - - assignments: - - "authenticated" - description: "Read-only users" + - description: "Read-only users" + entries: + - group: "authenticated" name: "readonly" pattern: ".*" permissions: - "Overall/Read" - "Job/Read" items: - - assignments: - - "user1" - - "user2" - description: "Jobs in Folder A, but not the folder itself" + - description: "Jobs in Folder A, but not the folder itself" + entries: + - user: "user1" + - user: "user2" name: "FolderA" pattern: "A/.*" permissions: - "Job/Build" - "Job/Delete" - "Job/Configure" - - assignments: - - "user2" - description: "Jobs in Folder B, but not the folder itself" + - description: "Jobs in Folder B, but not the folder itself" + entries: + - user: "user2" name: "FolderB" pattern: "B.*" permissions: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Macro.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Macro.yml index cfa8561b..e52c3b1d 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Macro.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Macro.yml @@ -7,23 +7,23 @@ jenkins: description: "Jenkins administrators" permissions: - "Overall/Administer" - assignments: - - "admin" + entries: + - user: "admin" - name: "readonly" description: "Read-only users" permissions: - "Overall/Read" - "Job/Read" - assignments: - - "authenticated" + entries: + - group: "authenticated" - name: "jobcreate" description: "Create Job User" permissions: - "Job/Create" - "Job/Configure" - assignments: - - "userGlobal" - - "groupGlobal" + entries: + - user: "userGlobal" + - group: "groupGlobal" items: - name: "jobcreate" description: "Can create jobs" @@ -34,9 +34,9 @@ jenkins: - "Job/Delete" - "Job/Create" - "Job/Read" - assignments: - - "userJobCreate" - - "groupJobCreate" + entries: + - user: "userJobCreate" + - group: "groupJobCreate" - name: "folderjobcreate" description: "Can create jobs in a folder" pattern: "^folder/job.*" @@ -46,18 +46,18 @@ jenkins: - "Job/Delete" - "Job/Create" - "Job/Read" - assignments: - - "userJobCreate" - - "groupJobCreate" + entries: + - user: "userJobCreate" + - group: "groupJobCreate" - name: "jobread" description: "Can only read jobs" pattern: "^job.*" permissions: - "Job/Build" - "Job/Read" - assignments: - - "userRead" - - "groupRead" + entries: + - user: "userRead" + - group: "groupRead" - name: "@Folder" description: "grants access to folders" pattern: ".*" @@ -66,9 +66,9 @@ jenkins: - "Job/Read" - "Job/Configure" - "Job/Create" - assignments: - - "userJobCreate" - - "groupJobCreate" + entries: + - user: "userJobCreate" + - group: "groupJobCreate" # System for test securityRealm: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Naming.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Naming.yml index 5d244593..9051b42e 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Naming.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-Naming.yml @@ -7,23 +7,24 @@ jenkins: description: "Jenkins administrators" permissions: - "Overall/Administer" - assignments: - - "admin" + entries: + - user: "admin" - name: "readonly" description: "Read-only users" permissions: - "Overall/Read" - "Job/Read" - assignments: - - "authenticated" + entries: + - group: "authenticated" - name: "jobcreate" description: "Create Job User" permissions: - "Job/Create" - "Job/Configure" - assignments: - - "userGlobal" - - "groupGlobal" + entries: + - user: "userGlobal" + - group: "groupGlobal" + - either: "eitherGlobal" items: - name: "jobcreate" description: "Can create jobs" @@ -34,9 +35,10 @@ jenkins: - "Job/Delete" - "Job/Create" - "Job/Read" - assignments: - - "userJobCreate" - - "groupJobCreate" + entries: + - user: "userJobCreate" + - group: "groupJobCreate" + - either: "eitherJobCreate" - name: "folderjobcreate" description: "Can create jobs in a folder" pattern: "^folder/job.*" @@ -46,18 +48,20 @@ jenkins: - "Job/Delete" - "Job/Create" - "Job/Read" - assignments: - - "userJobCreate" - - "groupJobCreate" + entries: + - user: "userJobCreate" + - group: "groupJobCreate" + - either: "eitherJobCreate" - name: "jobread" description: "Can only read jobs" pattern: "^job.*" permissions: - "Job/Build" - "Job/Read" - assignments: - - "userRead" - - "groupRead" + entries: + - user: "userRead" + - group: "groupRead" + - either: "eitherRead" # System for test securityRealm: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions-export.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions-export.yml index f95ae42b..e486d147 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions-export.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions-export.yml @@ -6,22 +6,22 @@ roleBased: permissions: - "Agent/Build" global: - - assignments: - - "builders" - - "builder" + - entries: + - user: "builder" + - group: "builders" name: "access" pattern: ".*" permissions: - "Overall/Read" - "Agent/Build" - - assignments: - - "administrators" + - entries: + - group: "administrators" name: "admin" pattern: ".*" permissions: - "Overall/Administer" - - assignments: - - "global-build-user" + - entries: + - user: "global-build-user" name: "builder" pattern: ".*" permissions: @@ -30,8 +30,8 @@ roleBased: - "Job/Build" - "Job/Read" - "Job/Workspace" - - assignments: - - "global-creator-user" + - entries: + - user: "global-creator-user" name: "creator" pattern: ".*" permissions: @@ -40,8 +40,8 @@ roleBased: - "Job/Create" - "Job/Read" items: - - assignments: - - "item-creator-user" + - entries: + - user: "item-creator-user" name: "@CurrentUserIsOwner" pattern: ".*" permissions: @@ -51,16 +51,16 @@ roleBased: - "Job/Read" - "Job/Configure" - "Job/Workspace" - - assignments: - - "item-builder-user" - - "builders" + - entries: + - user: "item-builder-user" + - group: "builders" name: "folder-access" pattern: "(?i)folder" permissions: - "Job/Read" - - assignments: - - "item-builder-user" - - "builders" + - entries: + - user: "item-builder-user" + - group: "builders" name: "folder-builder" pattern: "(?i)folder/.*" permissions: @@ -68,8 +68,8 @@ roleBased: - "Job/Build" - "Job/Read" - "Job/Workspace" - - assignments: - - "item-creator-user" + - entries: + - user: "item-creator-user" name: "folder-creator" pattern: "(?i)folder/.*" permissions: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions.yml index 767aa7e5..6abe3156 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code-no-permissions.yml @@ -8,22 +8,22 @@ jenkins: permissions: - "Agent/Build" global: - - assignments: - - "builders" - - "builder" + - entries: + - user: "builder" + - group: "builders" name: "access" pattern: ".*" permissions: - "Overall/Read" - "Agent/Build" - - assignments: - - "administrators" + - entries: + - group: "administrators" name: "admin" pattern: ".*" permissions: - "Overall/Administer" - - assignments: - - "global-build-user" + - entries: + - user: "global-build-user" name: "builder" pattern: ".*" permissions: @@ -32,8 +32,8 @@ jenkins: - "Job/Build" - "Job/Read" - "Job/Workspace" - - assignments: - - "global-creator-user" + - entries: + - user: "global-creator-user" name: "creator" pattern: ".*" permissions: @@ -42,8 +42,8 @@ jenkins: - "Job/Create" - "Job/Read" items: - - assignments: - - "item-creator-user" + - entries: + - user: "item-creator-user" name: "@CurrentUserIsOwner" pattern: ".*" permissions: @@ -53,16 +53,16 @@ jenkins: - "Job/Read" - "Job/Configure" - "Job/Workspace" - - assignments: - - "item-builder-user" - - "builders" + - entries: + - user: "item-builder-user" + - group: "builders" name: "folder-access" pattern: "(?i)folder" permissions: - "Job/Read" - - assignments: - - "item-builder-user" - - "builders" + - entries: + - user: "item-builder-user" + - group: "builders" name: "folder-builder" pattern: "(?i)folder/.*" permissions: @@ -70,8 +70,8 @@ jenkins: - "Job/Build" - "Job/Read" - "Job/Workspace" - - assignments: - - "item-creator-user" + - entries: + - user: "item-creator-user" name: "folder-creator" pattern: "(?i)folder/.*" permissions: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code.yml index c12b5358..4dc71f48 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code.yml @@ -7,15 +7,15 @@ jenkins: description: "Jenkins administrators" permissions: - "Overall/Administer" - assignments: - - "admin" + entries: + - user: "admin" - name: "readonly" description: "Read-only users" permissions: - "Overall/Read" - "Job/Read" - assignments: - - "authenticated" + entries: + - group: "authenticated" items: - name: "FolderA" description: "Jobs in Folder A, but not the folder itself" @@ -24,25 +24,25 @@ jenkins: - "Job/Configure" - "Job/Build" - "Job/Delete" - assignments: - - "user1" - - "user2" + entries: + - user: "user1" + - user: "user2" - name: "FolderB" description: "Jobs in Folder B, but not the folder itself" pattern: "B.*" permissions: - "Job/Configure" - "Job/Build" - assignments: - - "user2" + entries: + - user: "user2" agents: - name: "Agent1" description: "Agent 1" pattern: "agent1" permissions: - "Agent/Build" - assignments: - - "user1" + entries: + - user: "user1" # System for test securityRealm: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code2.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code2.yml index 34be3737..0c8131e2 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code2.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code2.yml @@ -7,15 +7,15 @@ jenkins: description: "Jenkins administrators" permissions: - "Overall/Administer" - assignments: - - "admin" + entries: + - user: "admin" - name: "readonly" description: "Read-only users" permissions: - "Overall/Read" - "Job/Read" - assignments: - - "authenticated" + entries: + - group: "authenticated" # System for test securityRealm: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code3.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code3.yml index 9e16d30d..07085674 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code3.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Configuration-as-Code3.yml @@ -7,8 +7,8 @@ jenkins: description: "Jenkins administrators" permissions: - "Overall/Administer" - assignments: - - "admin" + entries: + - user: "admin" - name: "dangerous" description: "Dangerous" permissions: @@ -16,8 +16,8 @@ jenkins: - "Overall/ConfigureUpdateCenter" - "Overall/UploadPlugins" - "Job/Read" - assignments: - - "test" + entries: + - user: "test" # System for test securityRealm: diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/OwnershipTest.yml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/OwnershipTest.yml index 4d3e3c49..8ebcc5d7 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/OwnershipTest.yml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/OwnershipTest.yml @@ -3,8 +3,8 @@ jenkins: roleBased: roles: agents: - - assignments: - - "authenticated" + - entries: + - group: "authenticated" name: "@CurrentUserIsPrimaryOwner" pattern: ".*" description: "User is Primary Owner" @@ -17,8 +17,8 @@ jenkins: - "Agent/Connect" - "Agent/Build" - "Agent/Disconnect" - - assignments: - - "authenticated" + - entries: + - group: "authenticated" name: "@CurrentUserIsOwner" pattern: ".*" description: "User is Owner" @@ -32,22 +32,22 @@ jenkins: - "Agent/Build" - "Agent/Disconnect" global: - - assignments: - - "admin" + - entries: + - user: "admin" name: "admin" pattern: ".*" description: "Admin Users" permissions: - "Overall/Administer" - - assignments: - - "authenticated" + - entries: + - group: "authenticated" name: "reader" pattern: ".*" permissions: - "Overall/Read" items: - - assignments: - - "authenticated" + - entries: + - group: "authenticated" name: "@CurrentUserIsOwner" pattern: ".*" description: "User is Owner" @@ -63,8 +63,8 @@ jenkins: - "Job/Configure" - "Job/Workspace" - "Job/ViewStatus" # System for test - - assignments: - - "authenticated" + - entries: + - group: "authenticated" name: "@CurrentUserIsPrimaryOwner" pattern: ".*" description: "User is Primary Owner" diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/dangerousPermissionsAreIgnored/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/dangerousPermissionsAreIgnored/config.xml index 119995a9..8c458761 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/dangerousPermissionsAreIgnored/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/dangerousPermissionsAreIgnored/config.xml @@ -1,7 +1,7 @@ - 1.480 + 2.303.3 2 NORMAL true @@ -12,7 +12,7 @@ hudson.model.Hudson.Administer - alice + alice @@ -22,7 +22,7 @@ hudson.model.Hudson.UploadPlugins - berta + berta diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/testRoleAssignment/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/testRoleAssignment/config.xml index fa1250e4..495fc37c 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/testRoleAssignment/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/RoleStrategyTest/testRoleAssignment/config.xml @@ -1,7 +1,7 @@ - 1.480 + 2.303.3 2 NORMAL true @@ -12,7 +12,7 @@ hudson.model.Hudson.Administer - alice + alice diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2182Test/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2182Test/config.xml index c07bb017..9392e7dc 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2182Test/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2182Test/config.xml @@ -1,7 +1,7 @@ - 2.279 + 2.303.3 RUNNING 2 NORMAL @@ -13,7 +13,7 @@ hudson.model.Hudson.Administer - admin + admin @@ -21,7 +21,7 @@ hudson.model.Hudson.Read - anonymous + anonymous @@ -33,7 +33,7 @@ hudson.model.Item.Configure - anonymous + anonymous diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/casc.yaml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/casc.yaml new file mode 100644 index 00000000..34e81be4 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/casc.yaml @@ -0,0 +1,22 @@ +configuration-as-code: + version: 1 + deprecated: warn + +jenkins: + authorizationStrategy: + roleBased: + roles: + global: + - name: "admin" + entries: + - user: "username" + - group: "groupname" + - either: "eitherSID" + permissions: + - "Overall/Administer" + # legacy format + - name: admin2 + assignments: + - indifferentSID + permissions: + - "Overall/Administer" \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/config.xml new file mode 100644 index 00000000..0924fadd --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/config.xml @@ -0,0 +1,83 @@ + + + + 2.222.4 + DEVELOPMENT + 0 + NORMAL + true + + + + + + + hudson.model.Hudson.Read + hudson.model.View.Delete + hudson.model.Computer.Connect + hudson.model.Item.Create + hudson.model.Item.Workspace + hudson.model.Computer.Create + hudson.model.View.Configure + hudson.model.Computer.Provision + hudson.model.Computer.Build + hudson.model.Item.Configure + hudson.model.View.Read + hudson.model.View.Create + hudson.model.Hudson.Administer + hudson.model.Item.Cancel + hudson.model.Item.Delete + hudson.model.Item.Read + hudson.model.Computer.Configure + hudson.model.Computer.Delete + hudson.model.Item.Build + hudson.model.Computer.Disconnect + hudson.model.Item.Move + hudson.model.Item.Discover + + + admin + + + + + hudson.model.Hudson.Read + + + markus + + + + + + false + false + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULL_NAME} + ${ITEM_ROOTDIR}/builds + + + + + + 0 + + + + all + false + false + + + + all + 0 + + + false + + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/admin_1229880828125156033/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/admin_1229880828125156033/config.xml new file mode 100644 index 00000000..8882e202 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/admin_1229880828125156033/config.xml @@ -0,0 +1,43 @@ + + + 10 + admin + admin + + + + + + + + + + + all + false + false + + + + + + + + + 068b69e9de803af6 + + + true + + + + #jbcrypt:$2a$10$3iHuqz901JbcpxkuAqvot.0lmtoe38urKH3FuT1V8LaFxxzjHPMJa + + + + authenticated + + 1638370806367 + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/markus_7995840758412173137/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/markus_7995840758412173137/config.xml new file mode 100644 index 00000000..01296280 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/markus_7995840758412173137/config.xml @@ -0,0 +1,37 @@ + + + 10 + markus + markus + + + + + + + + + + + all + false + false + + + + + + + + + 845bb1ab103259bf + + + true + + + + #jbcrypt:$2a$10$NyplFLRhWoygsRkVAaaSOORTmNEbv1cve5hFAuy7w4rS8zkZ4pbie + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/users.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/users.xml new file mode 100644 index 00000000..8485bb63 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/Security2374Test/test3xDataMigration/users/users.xml @@ -0,0 +1,14 @@ + + + 1 + + + markus + markus_7995840758412173137 + + + admin + admin_1229880828125156033 + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserAuthoritiesAsRolesTest/testRoleAuthority/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserAuthoritiesAsRolesTest/testRoleAuthority/config.xml index d287a40f..0ac0cf8d 100644 --- a/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserAuthoritiesAsRolesTest/testRoleAuthority/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserAuthoritiesAsRolesTest/testRoleAuthority/config.xml @@ -1,7 +1,7 @@ - 1.480 + 2.303.3 2 NORMAL true diff --git a/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest/config.xml b/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest/config.xml new file mode 100644 index 00000000..469342c1 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/rolestrategy/UserGroupSeparationTest/config.xml @@ -0,0 +1,50 @@ + + + + 2.303.3 + 2 + NORMAL + true + + + + + hudson.model.Hudson.Administer + + + user + group + either + + + + + + + ${ITEM_ROOTDIR}/workspace + ${ITEM_ROOTDIR}/builds + + false + + + + + + + 5 + 0 + + + + All + false + false + + + + All + 0 + + + + \ No newline at end of file