├── .mvn ├── maven.config └── extensions.xml ├── docs ├── images │ ├── assign-role.png │ ├── screenshot.png │ └── add-folder-role.png ├── usage.md └── rest-api.adoc ├── .github └── release-drafter.yml ├── Jenkinsfile ├── src ├── main │ ├── resources │ │ ├── io │ │ │ └── jenkins │ │ │ │ └── plugins │ │ │ │ └── folderauth │ │ │ │ ├── FolderBasedAuthorizationStrategy │ │ │ │ ├── config.properties │ │ │ │ ├── config.jelly │ │ │ │ └── help.html │ │ │ │ ├── Messages.properties │ │ │ │ └── FolderAuthorizationStrategyManagementLink │ │ │ │ ├── _api.jelly │ │ │ │ ├── index.properties │ │ │ │ └── index.jelly │ │ └── index.jelly │ ├── java │ │ └── io │ │ │ └── jenkins │ │ │ └── plugins │ │ │ └── folderauth │ │ │ ├── roles │ │ │ ├── package-info.java │ │ │ ├── GlobalRole.java │ │ │ ├── AgentRole.java │ │ │ ├── FolderRole.java │ │ │ └── AbstractRole.java │ │ │ ├── misc │ │ │ ├── GlobalRoleCreationRequest.java │ │ │ ├── AgentRoleCreationRequest.java │ │ │ ├── FolderRoleCreationRequest.java │ │ │ ├── PermissionFinder.java │ │ │ └── PermissionWrapper.java │ │ │ ├── acls │ │ │ ├── GenericAclImpl.java │ │ │ ├── GlobalAclImpl.java │ │ │ └── AbstractAcl.java │ │ │ ├── FolderBasedAuthorizationStrategy.java │ │ │ ├── FolderAuthorizationStrategyAPI.java │ │ │ └── FolderAuthorizationStrategyManagementLink.java │ └── webapp │ │ ├── js │ │ ├── collapsible.js │ │ ├── folders.js │ │ ├── filter.js │ │ ├── managesids.js │ │ └── addrole.js │ │ └── css │ │ └── folder-strategy.css └── test │ ├── java │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── folderauth │ │ ├── misc │ │ └── PermissionWrapperTest.java │ │ ├── casc │ │ ├── ConfigurationWithEmptyFolderRolesTest.java │ │ └── ConfigurationAsCodeTest.java │ │ ├── jmh │ │ ├── BenchmarkRunner.java │ │ └── benchmarks │ │ │ ├── GlobalRoleBenchmark.java │ │ │ └── FolderRoleBenchmark.java │ │ ├── GlobalAclImplTest.java │ │ ├── RestartSurvivabilityTest.java │ │ ├── FolderBasedAuthorizationStrategyTest.java │ │ └── FolderAuthorizationStrategyAPITest.java │ └── resources │ └── io │ └── jenkins │ └── plugins │ └── folderauth │ └── casc │ ├── expected.yml │ ├── expected3.yml │ ├── config2.yml │ ├── config3.yml │ └── config.yml ├── .editorconfig ├── .dependabot └── config.yml ├── .gitignore ├── LICENSE ├── README.md └── pom.xml /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -------------------------------------------------------------------------------- /docs/images/assign-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhyudayaSharma/folder-auth-plugin/master/docs/images/assign-role.png -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhyudayaSharma/folder-auth-plugin/master/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/images/add-folder-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhyudayaSharma/folder-auth-plugin/master/docs/images/add-folder-role.png -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | version-template: $MAJOR.$MINOR-$PATCH 3 | tag-template: folder-auth-$NEXT_MINOR_VERSION 4 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | // Builds a module using https://github.com/jenkins-infra/pipeline-library 2 | buildPlugin(configurations: buildPlugin.recommendedConfigurations()) 3 | runBenchmarks('jmh-report.json') 4 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/FolderBasedAuthorizationStrategy/config.properties: -------------------------------------------------------------------------------- 1 | blurb=Roles can be configured on the 'Folder Authorization Strategy' page available from \ 2 | Manage Jenkins. 3 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Allows configuring users' permissions to projects organized in 5 | folders from the Cloudbees Folder plugin. 6 |

7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/FolderBasedAuthorizationStrategy/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${%blurb(rootURL)} 4 | 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.properties] 12 | charset = latin1 13 | 14 | [*.{yaml, yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.0-beta-7 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/FolderBasedAuthorizationStrategy/help.html: -------------------------------------------------------------------------------- 1 |

2 | Allows configuring users' permissions for jobs organized in folders and agents connected to Jenkins. 3 | Also provides support for permissions that are applicable everywhere inside Jenkins. Please visit the 4 | 5 | GitHub page 6 | 7 | for learning how to configure and use this plugin. 8 |

9 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: "java:maven" 4 | directory: "/" 5 | update_schedule: "weekly" 6 | default_reviewers: 7 | - "oleg-nenashev" 8 | - "AbhyudayaSharma" 9 | ignored_updates: 10 | - match: 11 | dependency_name: "org.jenkins-ci.main:jenkins-core" 12 | - match: 13 | dependency_name: "org.jenkins-ci.plugins*" 14 | - match: 15 | dependency_name: "io.jenkins.plugins*" 16 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/roles/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains roles used by {@link io.jenkins.plugins.folderauth.FolderBasedAuthorizationStrategy} 3 | * to allow or deny access to users identified by their {@link org.acegisecurity.acls.sid.Sid}s as 4 | * {@link java.lang.String}s. 5 | * 6 | * @see hudson.security.SidACL 7 | */ 8 | @ParametersAreNonnullByDefault 9 | package io.jenkins.plugins.folderauth.roles; 10 | 11 | import javax.annotation.ParametersAreNonnullByDefault; 12 | -------------------------------------------------------------------------------- /src/main/webapp/js/collapsible.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cols = document.getElementsByClassName("collapsible"); 4 | for (let i = 0; i < cols.length; i++) { 5 | cols[i].addEventListener("click", function () { 6 | this.classList.toggle("active"); 7 | const content = this.nextElementSibling; 8 | if (content.style.display === "block") { 9 | content.style.display = "none"; 10 | } else { 11 | content.style.display = "block"; 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/Messages.properties: -------------------------------------------------------------------------------- 1 | FolderBasedAuthorizationStrategy.DisplayName=Folder Authorization Strategy 2 | FolderBasedAuthorizationStrategy.NotCurrentStrategy=FolderBasedAuthorizationStrategy is not the AuthorizationStrategy 3 | FolderBasedAuthorizationStrategy.ManageGlobalRoles=Manage Global Roles 4 | FolderBasedAuthorizationStrategy.Description=Manage roles and the users assigned to them 5 | PermissionWrapper.NoDangerousPermissions=Dangerous permissions are not supported. 6 | PermissionWrapper.UnknownPermission=Unable to infer permission from the given Id: {0} 7 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/FolderAuthorizationStrategyManagementLink/_api.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | REST APIs for the Folder Authorization Plugin 5 |

6 |

7 | The documentation for the REST API methods for the Folder Auth plugin can be found in the 8 | 9 | GitHub repository 10 | . Swagger API for plugin is available at 11 | 12 | SwaggerHub 13 | . 14 |

15 |
16 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/misc/PermissionWrapperTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.misc; 2 | 3 | import jenkins.model.Jenkins; 4 | import org.junit.ClassRule; 5 | import org.junit.Test; 6 | import org.jvnet.hudson.test.JenkinsRule; 7 | 8 | public class PermissionWrapperTest { 9 | @ClassRule 10 | public static JenkinsRule j = new JenkinsRule(); 11 | 12 | @Test(expected = IllegalArgumentException.class) 13 | public void shouldNotAllowDangerousPermissions() { 14 | new PermissionWrapper(Jenkins.RUN_SCRIPTS.getId()); 15 | } 16 | 17 | @Test(expected = IllegalArgumentException.class) 18 | public void shouldNotAllowNullPermissions() { 19 | new PermissionWrapper("this is not a permission id"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/misc/GlobalRoleCreationRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.misc; 2 | 3 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 4 | import org.kohsuke.accmod.Restricted; 5 | import org.kohsuke.accmod.restrictions.NoExternalUse; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | @Restricted(NoExternalUse.class) 13 | public class GlobalRoleCreationRequest { 14 | public String name = ""; 15 | public List permissions = Collections.emptyList(); 16 | 17 | @Nonnull 18 | public GlobalRole getGlobalRole() { 19 | return new GlobalRole(name, permissions.stream().map(PermissionWrapper::new).collect(Collectors.toSet())); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/roles/GlobalRole.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.roles; 2 | 3 | import io.jenkins.plugins.folderauth.misc.PermissionWrapper; 4 | import org.kohsuke.stapler.DataBoundConstructor; 5 | 6 | import java.util.Collections; 7 | import java.util.Set; 8 | 9 | /** 10 | * An {@link AbstractRole} that's applicable everywhere inside Jenkins. 11 | */ 12 | public class GlobalRole extends AbstractRole { 13 | @DataBoundConstructor 14 | public GlobalRole(String name, Set permissions, Set sids) { 15 | super(name, permissions, sids); 16 | this.sids.addAll(sids); 17 | } 18 | 19 | public GlobalRole(String name, Set permissions) { 20 | this(name, permissions, Collections.emptySet()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/misc/AgentRoleCreationRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.misc; 2 | 3 | import io.jenkins.plugins.folderauth.roles.AgentRole; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Collections; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | @SuppressWarnings("WeakerAccess") 11 | public class AgentRoleCreationRequest { 12 | public String name = ""; 13 | public Set agentNames = Collections.emptySet(); 14 | public Set permissions = Collections.emptySet(); 15 | 16 | @Nonnull 17 | public AgentRole getAgentRole() { 18 | Set perms = permissions.stream().map(PermissionWrapper::new).collect(Collectors.toSet()); 19 | return new AgentRole(name, perms, agentNames); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/misc/FolderRoleCreationRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.misc; 2 | 3 | import io.jenkins.plugins.folderauth.roles.FolderRole; 4 | import org.kohsuke.accmod.Restricted; 5 | import org.kohsuke.accmod.restrictions.NoExternalUse; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.util.Collections; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | 12 | @SuppressWarnings("WeakerAccess") 13 | @Restricted(NoExternalUse.class) 14 | public class FolderRoleCreationRequest { 15 | public String name = ""; 16 | public Set folderNames = Collections.emptySet(); 17 | public Set permissions = Collections.emptySet(); 18 | 19 | @Nonnull 20 | public FolderRole getFolderRole() { 21 | Set perms = permissions.stream().map(PermissionWrapper::new).collect(Collectors.toSet()); 22 | return new FolderRole(name, perms, folderNames); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | 4 | # Eclipse project files 5 | .settings 6 | .classpath 7 | .factorypath 8 | .project 9 | /nb-configuration.xml 10 | 11 | # From https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 12 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 13 | 14 | ## Directory-based project format 15 | .idea/ 16 | # if you remove the above rule, at least ignore user-specific stuff: 17 | # .idea/workspace.xml 18 | # .idea/tasks.xml 19 | # and these sensitive or high-churn files: 20 | # .idea/dataSources.ids 21 | # .idea/dataSources.xml 22 | # .idea/sqlDataSources.xml 23 | # .idea/dynamic.xml 24 | 25 | ## File-based project format 26 | *.ipr 27 | *.iws 28 | *.iml 29 | 30 | ## Additional for IntelliJ 31 | out/ 32 | 33 | # generated by mpeltonen/sbt-idea plugin 34 | .idea_modules/ 35 | 36 | # generated by JIRA plugin 37 | atlassian-ide-plugin.xml 38 | 39 | # generated by Crashlytics plugin (for Android Studio and Intellij) 40 | com_crashlytics_export_strings.xml 41 | 42 | # JMH benchmark reports 43 | jmh-report.json 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Abhyudaya Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/acls/GenericAclImpl.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.acls; 2 | 3 | import hudson.security.Permission; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | /** 9 | * An {@link hudson.security.ACL} for one {@link hudson.model.Job} or one {@link hudson.model.AbstractProject} or 10 | * one {@link hudson.model.Computer}. 11 | */ 12 | public class GenericAclImpl extends AbstractAcl { 13 | 14 | /** 15 | * Assigns {@code permissions} to each sid in {@code sid}. 16 | * 17 | * @param sids the sids to be assigned {@code permissions} 18 | * @param permissions the {@link Permission}s to be assigned 19 | */ 20 | public void assignPermissions(Set sids, Set permissions) { 21 | sids.parallelStream().forEach(sid -> { 22 | Set assignedPermissions = permissionList.get(sid); 23 | if (assignedPermissions == null) { 24 | assignedPermissions = new HashSet<>(); 25 | } 26 | assignedPermissions.addAll(permissions); 27 | permissionList.put(sid, assignedPermissions); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/FolderAuthorizationStrategyManagementLink/index.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | title=Folder Authorization Strategy 3 | manageGlobalRoles=Manage Global Roles 4 | currentGlobalRoles=Current Global Roles 5 | name=Name 6 | sid=SID 7 | sids=SIDs 8 | addGlobalRole=Add a Global Role 9 | roleName=Name of the Role 10 | permissions=Permissions 11 | viewPermissions=View Permissions 12 | addRole=Add Role 13 | confirmDelete=Do you really want to delete this role? 14 | manageFolderRoles=Manage Folder Roles 15 | currentFolderRoles=Current Folder Roles 16 | folders=Folders 17 | addFolderRole=Add a Folder Role 18 | applyOnFolders=Apply on 19 | loadingFolders=Loading Folders... 20 | manageAgentRoles=Manage Agent Roles 21 | currentAgentRoles=Current Agent Roles 22 | addAgentRole=Add an Agent Role 23 | applyOn=Apply on 24 | agents=Agents 25 | emptyFolderRoles=No Folder Roles present currently. 26 | emptyAgentRoles=No Agent Roles present currently. 27 | filterPlaceholder=Filter roles by name 28 | submit=Submit 29 | helpNeeded=Need some help configuring the plugin? Checkout our 30 | docs=documentation 31 | assign=Assign 32 | remove=Remove 33 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/folderauth/casc/expected.yml: -------------------------------------------------------------------------------- 1 | agentRoles: 2 | - agents: 3 | - "agent1" 4 | name: "agentRole1" 5 | permissions: 6 | - id: "Agent/Configure" 7 | - id: "Agent/Disconnect" 8 | sids: 9 | - "user1" 10 | folderRoles: 11 | - folders: 12 | - "root" 13 | name: "viewRoot" 14 | permissions: 15 | - id: "Job/Read" 16 | sids: 17 | - "user1" 18 | globalRoles: 19 | - name: "admin" 20 | permissions: 21 | - id: "Agent/Build" 22 | - id: "Agent/Configure" 23 | - id: "Agent/Connect" 24 | - id: "Agent/Create" 25 | - id: "Agent/Delete" 26 | - id: "Agent/Disconnect" 27 | - id: "Agent/ExtendedRead" 28 | - id: "Agent/Provision" 29 | - id: "Overall/Administer" 30 | - id: "Overall/Read" 31 | - id: "Job/Build" 32 | - id: "Job/Cancel" 33 | - id: "Job/Configure" 34 | - id: "Job/Create" 35 | - id: "Job/Delete" 36 | - id: "Job/Discover" 37 | - id: "Job/ExtendedRead" 38 | - id: "Job/Move" 39 | - id: "Job/Read" 40 | - id: "Job/WipeOut" 41 | - id: "Job/Workspace" 42 | - id: "View/Configure" 43 | - id: "View/Create" 44 | - id: "View/Delete" 45 | - id: "View/Read" 46 | - id: "SCM/Tag" 47 | sids: 48 | - "admin" 49 | - name: "read" 50 | permissions: 51 | - id: "Overall/Read" 52 | sids: 53 | - "user1" 54 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/folderauth/casc/expected3.yml: -------------------------------------------------------------------------------- 1 | agentRoles: 2 | - agents: 3 | - "agent1" 4 | name: "agentRole1" 5 | permissions: 6 | - id: "Agent/Configure" 7 | - id: "Agent/Disconnect" 8 | sids: 9 | - "user1" 10 | folderRoles: 11 | - folders: 12 | - "root" 13 | name: "viewRoot" 14 | permissions: 15 | - id: "Job/Read" 16 | sids: 17 | - "user1" 18 | globalRoles: 19 | - name: "admin" 20 | permissions: 21 | - id: "Agent/Build" 22 | - id: "Agent/Configure" 23 | - id: "Agent/Connect" 24 | - id: "Agent/Create" 25 | - id: "Agent/Delete" 26 | - id: "Agent/Disconnect" 27 | - id: "Agent/ExtendedRead" 28 | - id: "Agent/Provision" 29 | - id: "Overall/Administer" 30 | - id: "Overall/Read" 31 | - id: "Job/Build" 32 | - id: "Job/Cancel" 33 | - id: "Job/Configure" 34 | - id: "Job/Create" 35 | - id: "Job/Delete" 36 | - id: "Job/Discover" 37 | - id: "Job/ExtendedRead" 38 | - id: "Job/Move" 39 | - id: "Job/Read" 40 | - id: "Job/WipeOut" 41 | - id: "Job/Workspace" 42 | - id: "View/Configure" 43 | - id: "View/Create" 44 | - id: "View/Delete" 45 | - id: "View/Read" 46 | - id: "SCM/Tag" 47 | sids: 48 | - "admin" 49 | - name: "read" 50 | permissions: 51 | - id: "Overall/Read" 52 | sids: 53 | - "user1" 54 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/casc/ConfigurationWithEmptyFolderRolesTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.casc; 2 | 3 | import hudson.security.AuthorizationStrategy; 4 | import io.jenkins.plugins.casc.misc.ConfiguredWithCode; 5 | import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; 6 | import io.jenkins.plugins.folderauth.FolderBasedAuthorizationStrategy; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | public class ConfigurationWithEmptyFolderRolesTest { 14 | @Rule 15 | public JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); 16 | 17 | @Test 18 | @ConfiguredWithCode("config2.yml") 19 | public void shouldNotThrowErrorWithEmptyFolderRoles() { 20 | AuthorizationStrategy authorizationStrategy = j.jenkins.getAuthorizationStrategy(); 21 | assertTrue(authorizationStrategy instanceof FolderBasedAuthorizationStrategy); 22 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) authorizationStrategy; 23 | assertEquals(0, strategy.getFolderRoles().size()); 24 | assertEquals(0, strategy.getAgentRoles().size()); 25 | assertEquals(2, strategy.getGlobalRoles().size()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/jmh/BenchmarkRunner.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.jmh; 2 | 3 | import jenkins.benchmark.jmh.BenchmarkFinder; 4 | import org.junit.Test; 5 | import org.openjdk.jmh.annotations.Mode; 6 | import org.openjdk.jmh.results.format.ResultFormatType; 7 | import org.openjdk.jmh.runner.Runner; 8 | import org.openjdk.jmh.runner.RunnerException; 9 | import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; 10 | import org.openjdk.jmh.runner.options.OptionsBuilder; 11 | 12 | import java.io.IOException; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public class BenchmarkRunner { 16 | @Test 17 | public void runBenchmarks() throws IOException, RunnerException { 18 | ChainedOptionsBuilder options = new OptionsBuilder() 19 | .forks(2) 20 | .mode(Mode.AverageTime) 21 | .shouldDoGC(true) 22 | .shouldFailOnError(true) 23 | .result("jmh-report.json") 24 | .resultFormat(ResultFormatType.JSON) 25 | .timeUnit(TimeUnit.MICROSECONDS) 26 | .threads(2); 27 | 28 | new BenchmarkFinder(BenchmarkRunner.class).findBenchmarks(options); 29 | new Runner(options.build()).run(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/roles/AgentRole.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.roles; 2 | 3 | import io.jenkins.plugins.folderauth.misc.PermissionWrapper; 4 | import org.kohsuke.stapler.DataBoundConstructor; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.ParametersAreNonnullByDefault; 8 | import java.util.Collections; 9 | import java.util.HashSet; 10 | import java.util.Set; 11 | import java.util.TreeSet; 12 | 13 | @ParametersAreNonnullByDefault 14 | public class AgentRole extends AbstractRole { 15 | private final Set agents; 16 | 17 | @DataBoundConstructor 18 | public AgentRole(String name, Set permissions, Set agents, Set sids) { 19 | super(name, permissions, sids); 20 | this.agents = new HashSet<>(agents); 21 | } 22 | 23 | public AgentRole(String name, Set permissions, Set agents) { 24 | this(name, permissions, agents, Collections.emptySet()); 25 | } 26 | 27 | @Nonnull 28 | public Set getAgents() { 29 | return Collections.unmodifiableSet(agents); 30 | } 31 | 32 | /** 33 | * Returns sorted agent names as a comma separated string list 34 | * 35 | * @return sorted agent names as a comma separated string list 36 | */ 37 | @Nonnull 38 | @SuppressWarnings("unused") // used in index.jelly 39 | public String getAgentNamesCommaSeparated() { 40 | String csv = new TreeSet<>(agents).toString(); 41 | return csv.substring(1, csv.length() - 1); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/webapp/js/folders.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Sends a GET request and renders the result on the web page. 5 | */ 6 | const getFolders = () => { 7 | const xhr = new XMLHttpRequest(); 8 | xhr.open('GET', `${rootURL}/folder-auth/getAllFolders`, true); 9 | xhr.send(null); 10 | 11 | xhr.onload = () => { 12 | renderFoldersAsOptions(JSON.parse(xhr.responseText)); 13 | }; 14 | 15 | xhr.onerror = () => { 16 | alert(`Unable to get the list of folders: ${xhr.responseText}`); 17 | }; 18 | }; 19 | 20 | /** 21 | * Renders all folders on the web UI. 22 | * @param folders list of folder names 23 | */ 24 | const renderFoldersAsOptions = (folders) => { 25 | const select = document.getElementById('folder-select'); 26 | const loadingLabel = document.getElementById('loading-folders'); 27 | 28 | if (!(Array.isArray(folders) && folders.length)) { 29 | loadingLabel.innerText = 'Please create a folder before adding a folder role.'; 30 | return; 31 | } 32 | 33 | folders.forEach(folder => { 34 | const option = document.createElement('option'); 35 | option.value = folder; 36 | option.innerHTML = folder; 37 | select.appendChild(option); 38 | }); 39 | 40 | // make the that contains the sid 9 | */ 10 | function assignSid(roleType, roleName, sidInputBoxId) { 11 | const formData = new FormData(); 12 | formData.append('sid', document.getElementById(sidInputBoxId).value); 13 | formData.append('roleName', roleName); 14 | 15 | if (!(roleType === 'agent' || roleType === 'global' || roleType === 'folder')) { 16 | throw new Error('Unknown Role Type'); 17 | } 18 | 19 | const url = `${rootURL}/folder-auth/assignSidTo${roleType[0].toUpperCase()}${roleType.substring(1)}Role`; 20 | const request = new XMLHttpRequest(); 21 | request.open('POST', url); 22 | request.onload = () => { 23 | if (request.status === 200) { 24 | alert('Sid added successfully.'); 25 | location.reload(); 26 | } else { 27 | alert('Unable to remove the sid.' + request.responseText); 28 | } 29 | 30 | }; 31 | 32 | request.onerror = () => { 33 | alert('Unable to add the sid to the role: ' + request.responseText); 34 | }; 35 | 36 | // see addRole.js 37 | request.setRequestHeader('Jenkins-Crumb', crumb.value); 38 | request.send(formData); 39 | } 40 | 41 | /** 42 | * Removes a sid from a role. 43 | * 44 | * @param roleType {('agent' | 'global' | 'folder')} the type of the role 45 | * @param roleName the name of the role 46 | * @param sidInputBoxId id of the that contains the sid 47 | */ 48 | function removeSid(roleType, roleName, sidInputBoxId) { 49 | const formData = new FormData(); 50 | formData.append('sid', document.getElementById(sidInputBoxId).value); 51 | formData.append('roleName', roleName); 52 | 53 | if (!(roleType === 'agent' || roleType === 'global' || roleType === 'folder')) { 54 | throw new Error('Unknown Role Type'); 55 | } 56 | 57 | const url = `${rootURL}/folder-auth/removeSidFrom${roleType[0].toUpperCase()}${roleType.substring(1)}Role`; 58 | const request = new XMLHttpRequest(); 59 | request.open('POST', url); 60 | request.onload = () => { 61 | if (request.status === 200) { 62 | alert('Sid removed successfully.'); 63 | location.reload(); 64 | } else { 65 | alert('Unable to remove the sid.' + request.responseText); 66 | } 67 | }; 68 | 69 | request.onerror = () => { 70 | alert('Unable to remove the sid from the role: ' + request.responseText); 71 | }; 72 | 73 | // see addRole.js 74 | request.setRequestHeader('Jenkins-Crumb', crumb.value); 75 | request.send(formData); 76 | } 77 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/folderauth/casc/config2.yml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | agentProtocols: 3 | - "CLI2-connect" 4 | - "JNLP2-connect" 5 | - "JNLP3-connect" 6 | - "JNLP4-connect" 7 | - "Ping" 8 | authorizationStrategy: 9 | folderBased: 10 | globalRoles: 11 | - name: "admin" 12 | permissions: 13 | - id: "hudson.model.View.Delete" 14 | - id: "hudson.model.Computer.Connect" 15 | - id: "hudson.model.Computer.Create" 16 | - id: "hudson.model.View.Configure" 17 | - id: "hudson.model.Item.Configure" 18 | - id: "hudson.model.Computer.Build" 19 | - id: "hudson.model.Hudson.Administer" 20 | - id: "hudson.model.Item.Cancel" 21 | - id: "hudson.model.Item.Read" 22 | - id: "hudson.model.Computer.Delete" 23 | - id: "hudson.model.Item.Build" 24 | - id: "hudson.model.Item.ExtendedRead" 25 | - id: "hudson.scm.SCM.Tag" 26 | - id: "hudson.model.Item.Move" 27 | - id: "hudson.model.Item.Discover" 28 | - id: "hudson.model.Hudson.Read" 29 | - id: "hudson.model.Item.Create" 30 | - id: "hudson.model.Item.Workspace" 31 | - id: "hudson.model.Computer.Provision" 32 | - id: "hudson.model.Item.WipeOut" 33 | - id: "hudson.model.View.Read" 34 | - id: "hudson.model.View.Create" 35 | - id: "hudson.model.Item.Delete" 36 | - id: "hudson.model.Computer.ExtendedRead" 37 | - id: "hudson.model.Computer.Configure" 38 | - id: "hudson.model.Computer.Disconnect" 39 | sids: 40 | - "admin" 41 | - name: "read" 42 | permissions: 43 | - id: "hudson.model.Hudson.Read" 44 | sids: 45 | - "user1" 46 | 47 | disableRememberMe: false 48 | markupFormatter: "plainText" 49 | mode: NORMAL 50 | myViewsTabBar: "standard" 51 | numExecutors: 2 52 | primaryView: 53 | all: 54 | name: "all" 55 | projectNamingStrategy: "standard" 56 | quietPeriod: 5 57 | remotingSecurity: 58 | enabled: false 59 | scmCheckoutRetryCount: 0 60 | slaveAgentPort: 0 61 | updateCenter: 62 | sites: 63 | - id: "default" 64 | url: "http://updates.jenkins-ci.org/update-center.json" 65 | 66 | # System for test 67 | securityRealm: 68 | local: 69 | allowsSignup: false 70 | users: 71 | - id: "admin" 72 | password: "1234" 73 | - id: "user1" 74 | password: "" 75 | 76 | nodes: 77 | - dumb: 78 | mode: NORMAL 79 | name: "agent1" 80 | remoteFS: "/home/user1" 81 | launcher: jnlp 82 | - dumb: 83 | mode: NORMAL 84 | name: "agent2" 85 | remoteFS: "/home/user1" 86 | launcher: jnlp 87 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/acls/AbstractAcl.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.acls; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.security.Permission; 5 | import hudson.security.SidACL; 6 | import io.jenkins.plugins.folderauth.misc.PermissionWrapper; 7 | import jenkins.model.Jenkins; 8 | import org.acegisecurity.acls.sid.Sid; 9 | import org.apache.commons.collections.CollectionUtils; 10 | 11 | import javax.annotation.Nullable; 12 | import java.util.HashSet; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | abstract class AbstractAcl extends SidACL { 18 | 19 | private static ConcurrentHashMap> implyingPermissionsCache = new ConcurrentHashMap<>(); 20 | 21 | static { 22 | Permission.getAll().forEach(AbstractAcl::cacheImplyingPermissions); 23 | } 24 | 25 | private static Set cacheImplyingPermissions(Permission permission) { 26 | Set implyingPermissions; 27 | 28 | if (PermissionWrapper.DANGEROUS_PERMISSIONS.contains(permission)) { 29 | // dangerous permissions should be deferred to Jenkins.ADMINISTER 30 | implyingPermissions = getImplyingPermissions(Jenkins.ADMINISTER); 31 | } else { 32 | implyingPermissions = new HashSet<>(); 33 | 34 | for (Permission p = permission; p != null; p = p.impliedBy) { 35 | implyingPermissions.add(p); 36 | } 37 | } 38 | 39 | implyingPermissionsCache.put(permission, implyingPermissions); 40 | return implyingPermissions; 41 | } 42 | 43 | private static Set getImplyingPermissions(Permission p) { 44 | Set permissions = implyingPermissionsCache.get(p); 45 | if (permissions != null) { 46 | return permissions; 47 | } else { 48 | return cacheImplyingPermissions(p); 49 | } 50 | } 51 | 52 | /** 53 | * Maps each sid to the set of permissions assigned to it. 54 | *

55 | * The implementation should ensure that this list contains accurate permissions for each sid. 56 | */ 57 | protected Map> permissionList = new ConcurrentHashMap<>(); 58 | 59 | @Override 60 | @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", 61 | justification = "hudson.security.SidACL requires null when unknown") 62 | @Nullable 63 | protected Boolean hasPermission(Sid sid, Permission permission) { 64 | if (PermissionWrapper.DANGEROUS_PERMISSIONS.contains(permission)) { 65 | permission = Jenkins.ADMINISTER; 66 | } 67 | 68 | Set permissions = permissionList.get(toString(sid)); 69 | if (permissions != null && CollectionUtils.containsAny(permissions, getImplyingPermissions(permission))) { 70 | return true; 71 | } 72 | 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/folderauth/casc/config3.yml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | agentProtocols: 3 | - "CLI2-connect" 4 | - "JNLP2-connect" 5 | - "JNLP3-connect" 6 | - "JNLP4-connect" 7 | - "Ping" 8 | authorizationStrategy: 9 | folderBased: 10 | agentRoles: 11 | - agents: 12 | - "agent1" 13 | name: "agentRole1" 14 | permissions: 15 | - id: "hudson.model.Computer.Configure" 16 | - id: "hudson.model.Computer.Disconnect" 17 | sids: 18 | - "user1" 19 | folderRoles: 20 | - folders: 21 | - "root" 22 | name: "viewRoot" 23 | permissions: 24 | - id: "hudson.model.Item.Read" 25 | sids: 26 | - "user1" 27 | globalRoles: 28 | - name: "admin" 29 | permissions: 30 | - id: "View/Delete" 31 | - id: "Agent/Connect" 32 | - id: "Agent/Create" 33 | - id: "View/Configure" 34 | - id: "Job/Configure" 35 | - id: "Agent/Build" 36 | - id: "Overall/Administer" 37 | - id: "Job/Cancel" 38 | - id: "Job/Read" 39 | - id: "Agent/Delete" 40 | - id: "Job/Build" 41 | - id: "Job/ExtendedRead" 42 | - id: "hudson.scm.SCM.Tag" 43 | - id: "Job/Move" 44 | - id: "Job/Discover" 45 | - id: "Overall/Read" 46 | - id: "Job/Create" 47 | - id: "Job/Workspace" 48 | - id: "Agent/Provision" 49 | - id: "Job/WipeOut" 50 | - id: "View/Read" 51 | - id: "View/Create" 52 | - id: "Job/Delete" 53 | - id: "Agent/ExtendedRead" 54 | - id: "Agent/Configure" 55 | - id: "Agent/Disconnect" 56 | sids: 57 | - "admin" 58 | - name: "read" 59 | permissions: 60 | - id: "hudson.model.Hudson.Read" 61 | sids: 62 | - "user1" 63 | 64 | disableRememberMe: false 65 | markupFormatter: "plainText" 66 | mode: NORMAL 67 | myViewsTabBar: "standard" 68 | numExecutors: 2 69 | primaryView: 70 | all: 71 | name: "all" 72 | projectNamingStrategy: "standard" 73 | quietPeriod: 5 74 | remotingSecurity: 75 | enabled: false 76 | scmCheckoutRetryCount: 0 77 | slaveAgentPort: 0 78 | updateCenter: 79 | sites: 80 | - id: "default" 81 | url: "http://updates.jenkins-ci.org/update-center.json" 82 | 83 | # System for test 84 | securityRealm: 85 | local: 86 | allowsSignup: false 87 | users: 88 | - id: "admin" 89 | password: "1234" 90 | - id: "user1" 91 | password: "" 92 | 93 | nodes: 94 | - dumb: 95 | mode: NORMAL 96 | name: "agent1" 97 | remoteFS: "/home/user1" 98 | launcher: jnlp 99 | - dumb: 100 | mode: NORMAL 101 | name: "agent2" 102 | remoteFS: "/home/user1" 103 | launcher: jnlp 104 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 3.55 8 | 9 | 10 | 11 | io.jenkins.plugins 12 | folder-auth 13 | hpi 14 | ${revision}${changelist} 15 | Folder-based Authorization Strategy 16 | Manage access to Folders 17 | https://github.com/jenkinsci/folder-auth-plugin 18 | 19 | 20 | 21 | AbhyudayaSharma 22 | Abhyudaya Sharma 23 | sharmaabhyudaya@gmail.com 24 | 25 | 26 | 27 | 28 | 1.4 29 | -SNAPSHOT 30 | 2.164.1 31 | 8 32 | 1.35 33 | 34 | 35 | 36 | 37 | repo.jenkins-ci.org 38 | https://repo.jenkins-ci.org/public/ 39 | 40 | 41 | 42 | 43 | 44 | repo.jenkins-ci.org 45 | https://repo.jenkins-ci.org/public/ 46 | 47 | 48 | 49 | 50 | 51 | MIT License 52 | https://www.opensource.org/licenses/mit-license.php 53 | repo 54 | 55 | 56 | 57 | 58 | scm:git:ssh://github.com/jenkinsci/${project.artifactId}-plugin.git 59 | 60 | scm:git:ssh://git@github.com/jenkinsci/${project.artifactId}-plugin.git 61 | 62 | https://github.com/jenkinsci/folder-auth-plugin 63 | ${scmTag} 64 | 65 | 66 | 67 | 68 | org.jenkins-ci.plugins 69 | cloudbees-folder 70 | 6.4 71 | 72 | 73 | io.jenkins 74 | configuration-as-code 75 | ${configuration-as-code.version} 76 | true 77 | 78 | 79 | io.jenkins.configuration-as-code 80 | test-harness 81 | ${configuration-as-code.version} 82 | test 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/folderauth/casc/config.yml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | agentProtocols: 3 | - "CLI2-connect" 4 | - "JNLP2-connect" 5 | - "JNLP3-connect" 6 | - "JNLP4-connect" 7 | - "Ping" 8 | authorizationStrategy: 9 | folderBased: 10 | agentRoles: 11 | - agents: 12 | - "agent1" 13 | name: "agentRole1" 14 | permissions: 15 | - id: "hudson.model.Computer.Configure" 16 | - id: "hudson.model.Computer.Disconnect" 17 | sids: 18 | - "user1" 19 | folderRoles: 20 | - folders: 21 | - "root" 22 | name: "viewRoot" 23 | permissions: 24 | - id: "hudson.model.Item.Read" 25 | sids: 26 | - "user1" 27 | globalRoles: 28 | - name: "admin" 29 | permissions: 30 | - id: "hudson.model.View.Delete" 31 | - id: "hudson.model.Computer.Connect" 32 | - id: "hudson.model.Computer.Create" 33 | - id: "hudson.model.View.Configure" 34 | - id: "hudson.model.Item.Configure" 35 | - id: "hudson.model.Computer.Build" 36 | - id: "hudson.model.Hudson.Administer" 37 | - id: "hudson.model.Item.Cancel" 38 | - id: "hudson.model.Item.Read" 39 | - id: "hudson.model.Computer.Delete" 40 | - id: "hudson.model.Item.Build" 41 | - id: "hudson.model.Item.ExtendedRead" 42 | - id: "hudson.scm.SCM.Tag" 43 | - id: "hudson.model.Item.Move" 44 | - id: "hudson.model.Item.Discover" 45 | - id: "hudson.model.Hudson.Read" 46 | - id: "hudson.model.Item.Create" 47 | - id: "hudson.model.Item.Workspace" 48 | - id: "hudson.model.Computer.Provision" 49 | - id: "hudson.model.Item.WipeOut" 50 | - id: "hudson.model.View.Read" 51 | - id: "hudson.model.View.Create" 52 | - id: "hudson.model.Item.Delete" 53 | - id: "hudson.model.Computer.ExtendedRead" 54 | - id: "hudson.model.Computer.Configure" 55 | - id: "hudson.model.Computer.Disconnect" 56 | sids: 57 | - "admin" 58 | - name: "read" 59 | permissions: 60 | - id: "hudson.model.Hudson.Read" 61 | sids: 62 | - "user1" 63 | 64 | disableRememberMe: false 65 | markupFormatter: "plainText" 66 | mode: NORMAL 67 | myViewsTabBar: "standard" 68 | numExecutors: 2 69 | primaryView: 70 | all: 71 | name: "all" 72 | projectNamingStrategy: "standard" 73 | quietPeriod: 5 74 | remotingSecurity: 75 | enabled: false 76 | scmCheckoutRetryCount: 0 77 | slaveAgentPort: 0 78 | updateCenter: 79 | sites: 80 | - id: "default" 81 | url: "http://updates.jenkins-ci.org/update-center.json" 82 | 83 | # System for test 84 | securityRealm: 85 | local: 86 | allowsSignup: false 87 | users: 88 | - id: "admin" 89 | password: "1234" 90 | - id: "user1" 91 | password: "" 92 | 93 | nodes: 94 | - dumb: 95 | mode: NORMAL 96 | name: "agent1" 97 | remoteFS: "/home/user1" 98 | launcher: jnlp 99 | - dumb: 100 | mode: NORMAL 101 | name: "agent2" 102 | remoteFS: "/home/user1" 103 | launcher: jnlp 104 | -------------------------------------------------------------------------------- /src/main/webapp/css/folder-strategy.css: -------------------------------------------------------------------------------- 1 | div.form-row { 2 | padding-top: 5px; 3 | padding-bottom: 10px; 4 | padding-right: 10px; 5 | } 6 | 7 | .form-label { 8 | padding-right: 20px; 9 | padding-bottom: 10px; 10 | font-size: larger; 11 | font-weight: bold; 12 | } 13 | 14 | div.permission-row { 15 | padding: 5px 0; 16 | } 17 | 18 | div.role-container { 19 | display: flex; 20 | flex-direction: row; 21 | flex-basis: auto; 22 | flex-wrap: wrap; 23 | justify-content: flex-start; 24 | max-height: 525px; 25 | overflow-y: scroll; 26 | } 27 | 28 | .loading { 29 | padding-top: 7px; 30 | font-size: 12px; 31 | font-weight: lighter; 32 | } 33 | 34 | div.role { 35 | display: flex; 36 | flex-direction: column; 37 | padding: 10px; 38 | border-width: thin; 39 | border-radius: 5px; 40 | border-style: solid; 41 | border-color: black; 42 | background-color: #fdfdfd; 43 | margin: 3px 3px; 44 | position: relative; 45 | } 46 | 47 | .delete-role { 48 | position: absolute; 49 | top: 5px; 50 | right: 5px; 51 | border-style: solid; 52 | -webkit-border-radius: 5px; 53 | -moz-border-radius: 5px; 54 | border-radius: 5px; 55 | border-color: #fff; 56 | background-color: red; 57 | color: white; 58 | padding: 2px 5px; 59 | } 60 | 61 | .delete-role:hover { 62 | background-color: firebrick; 63 | } 64 | 65 | input[type="text"] { 66 | margin-left: 3px; 67 | border-radius: 5px; 68 | border-style: solid; 69 | border-width: thin; 70 | padding: 3px 3px; 71 | } 72 | 73 | button.submit-button, input.submit { 74 | -webkit-border-radius: 5px; 75 | -moz-border-radius: 5px; 76 | border-radius: 5px; 77 | padding: 5px 5px; 78 | margin-top: 5px; 79 | margin-left: 10px; 80 | line-height: 1.25; 81 | border-style: solid; 82 | border-color: #007bff; 83 | background-color: #007bff; 84 | color: #ffffff; 85 | display: inline-block; 86 | } 87 | 88 | button.submit-button:hover, input.submit:hover { 89 | background-color: #0069d9; 90 | } 91 | 92 | button.submit-button:active, input.submit:hover { 93 | background-color: #0062cc; 94 | } 95 | 96 | input.filter { 97 | margin-bottom: 10px; 98 | padding: 5px 5px; 99 | -webkit-border-radius: 5px; 100 | -moz-border-radius: 5px; 101 | border-radius: 5px; 102 | border-style: solid; 103 | border-width: thin; 104 | width: 100%; 105 | max-width: 250px; 106 | } 107 | 108 | select[multiple] { 109 | border-color: #000; 110 | -webkit-border-radius: 10px; 111 | -moz-border-radius: 10px; 112 | border-radius: 10px; 113 | border-style: solid; 114 | border-width: thin; 115 | padding: 10px; 116 | scroll-padding: 10px; 117 | display: block; 118 | margin: 10px; 119 | width: content-box; 120 | } 121 | 122 | /* Taken from https://www.w3schools.com/howto/howto_js_collapsible.asp */ 123 | 124 | .collapsible { 125 | background-color: #eee; 126 | color: #444; 127 | cursor: pointer; 128 | padding: 5px; 129 | width: 100%; 130 | border: none; 131 | text-align: left; 132 | outline: none; 133 | font-size: inherit; 134 | } 135 | 136 | .active, .collapsible:hover { 137 | background-color: #ccc; 138 | } 139 | 140 | .collapsible-content { 141 | padding: 0 18px; 142 | display: none; 143 | overflow-x: hidden; 144 | overflow-y: scroll; 145 | background-color: #f1f1f1; 146 | transition: max-height 0.2s ease-out; 147 | min-height: 125px; 148 | max-height: 125px; 149 | } 150 | 151 | .center { 152 | justify-content: center; 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/roles/AbstractRole.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.roles; 2 | 3 | import io.jenkins.plugins.folderauth.misc.PermissionWrapper; 4 | import org.kohsuke.accmod.Restricted; 5 | import org.kohsuke.accmod.restrictions.NoExternalUse; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.util.Collections; 9 | import java.util.HashSet; 10 | import java.util.Objects; 11 | import java.util.Set; 12 | import java.util.SortedSet; 13 | import java.util.TreeSet; 14 | 15 | /** 16 | * A role as an immutable object 17 | */ 18 | @Restricted(NoExternalUse.class) 19 | public abstract class AbstractRole implements Comparable { 20 | @Override 21 | public int compareTo(@Nonnull AbstractRole other) { 22 | return this.name.compareTo(other.name); 23 | } 24 | 25 | /** 26 | * The unique name of the role. 27 | */ 28 | @Nonnull 29 | protected final String name; 30 | 31 | /** 32 | * Wrappers of permissions that are assigned to this role. Should not be modified. 33 | */ 34 | @Nonnull 35 | private final Set permissionWrappers; 36 | 37 | /** 38 | * The sids on which this role is applicable. 39 | */ 40 | @Nonnull 41 | protected final Set sids; 42 | 43 | AbstractRole(String name, Set permissions, Set sids) { 44 | this.name = name; 45 | this.permissionWrappers = new HashSet<>(permissions); 46 | this.sids = new HashSet<>(sids); 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | AbstractRole role = (AbstractRole) o; 54 | return name.equals(role.name) && 55 | permissionWrappers.equals(role.permissionWrappers) && 56 | sids.equals(role.sids); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return Objects.hash(name, permissionWrappers, sids); 62 | } 63 | 64 | /** 65 | * The name of the Role 66 | * 67 | * @return the name of the role 68 | */ 69 | @Nonnull 70 | public String getName() { 71 | return name; 72 | } 73 | 74 | /** 75 | * The permissions assigned to the role. 76 | *

77 | * This method, however, does not return all permissions implied by this {@link AbstractRole} 78 | * 79 | * @return the permissions assigned to the role. 80 | * @see AbstractRole#getPermissionsUnsorted() when the permissions are not needed in a sorted order. 81 | */ 82 | @Nonnull 83 | public SortedSet getPermissions() { 84 | return Collections.unmodifiableSortedSet(new TreeSet<>(permissionWrappers)); 85 | } 86 | 87 | /** 88 | * The permissions assigned to the role in an unsorted order. 89 | * 90 | * @return permissions in an unsorted order. 91 | * @see AbstractRole#getPermissions() when permissions are needed in a sorted order. 92 | */ 93 | @Nonnull 94 | public Set getPermissionsUnsorted() { 95 | return Collections.unmodifiableSet(permissionWrappers); 96 | } 97 | 98 | /** 99 | * List of sids on which the role is applicable. 100 | * 101 | * @return list of sids on which this role is applicable. 102 | */ 103 | @Nonnull 104 | public Set getSids() { 105 | return Collections.unmodifiableSet(sids); 106 | } 107 | 108 | /** 109 | * Return a sorted comma separated list of sids assigned to this role 110 | * 111 | * @return a sorted comma separated list of sids assigned to this role 112 | */ 113 | @Nonnull 114 | @SuppressWarnings("unused") // used by index.jelly 115 | public String getSidsCommaSeparated() { 116 | String string = new TreeSet<>(sids).toString(); 117 | return string.substring(1, string.length() - 1); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/webapp/js/addrole.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // noinspection JSUnusedGlobalSymbols 4 | /** 5 | * Adds a global role 6 | */ 7 | const addGlobalRole = () => { 8 | const roleName = document.getElementById('globalRoleName').value; 9 | if (!roleName || roleName.length < 3) { 10 | alert('Please enter a valid name for the role to be added.'); 11 | return; 12 | } 13 | 14 | const response = { 15 | name: roleName, 16 | permissions: document.getElementById('global-permission-select').getValue(), 17 | }; 18 | 19 | if (response.permissions.length <= 0) { 20 | alert('Please select at least one permission'); 21 | return; 22 | } 23 | 24 | sendPostRequest(`${rootURL}/folder-auth/addGlobalRole`, response); 25 | }; 26 | 27 | // noinspection JSUnusedGlobalSymbols 28 | /** 29 | * Adds a Folder Role 30 | */ 31 | const addFolderRole = () => { 32 | const roleName = document.getElementById('folderRoleName').value; 33 | if (!roleName || roleName.length < 3) { 34 | alert('Please enter a valid name for the role to be added'); 35 | return; 36 | } 37 | 38 | const response = { 39 | name: roleName, 40 | permissions: document.getElementById('folder-permission-select').getValue(), 41 | folderNames: document.getElementById('folder-select').getValue(), 42 | }; 43 | 44 | if (!response.permissions || response.permissions.length <= 0) { 45 | alert('Please select at least one permission'); 46 | return; 47 | } 48 | 49 | if (!response.folderNames || response.folderNames.length <= 0) { 50 | alert('Please select at least one folder on which this role will be applicable'); 51 | return; 52 | } 53 | 54 | sendPostRequest(`${rootURL}/folder-auth/addFolderRole`, response); 55 | }; 56 | 57 | // noinspection JSUnusedGlobalSymbols 58 | /** 59 | * Adds an agent Role 60 | */ 61 | const addAgentRole = () => { 62 | const roleName = document.getElementById('agentRoleName').value; 63 | if (!roleName || roleName.length < 3) { 64 | alert('Please enter a valid name for the role to be added'); 65 | return; 66 | } 67 | 68 | const response = { 69 | name: roleName, 70 | agentNames: document.getElementById('agent-select').getValue(), 71 | permissions: document.getElementById('agent-permission-select').getValue(), 72 | }; 73 | 74 | if (!response.permissions || response.permissions.length <= 0) { 75 | alert('Please select at least one permission'); 76 | return; 77 | } 78 | 79 | if (!response.agentNames || response.agentNames.length <= 0) { 80 | alert('Please select at least one agent on which this role will be applicable'); 81 | return; 82 | } 83 | 84 | sendPostRequest(`${rootURL}/folder-auth/addAgentRole`, response); 85 | }; 86 | 87 | /** 88 | * Sends a POST request to {@code postUrl} 89 | * @param postUrl the URL 90 | * @param json JSON data to be sent 91 | */ 92 | const sendPostRequest = (postUrl, json) => { 93 | const xhr = new XMLHttpRequest(); 94 | xhr.open('POST', postUrl, true); 95 | xhr.setRequestHeader('Content-Type', 'application/json'); 96 | // Jelly file sets up the crumb value for CSRF protection 97 | if (crumb.value) { 98 | xhr.setRequestHeader('Jenkins-Crumb', crumb.value); 99 | } 100 | 101 | xhr.onload = () => { 102 | if (xhr.status === 200) { 103 | alert('The role was added successfully'); 104 | location.reload(); // refresh the page 105 | } else { 106 | alert('Unable to add the role\n' + xhr.responseText); 107 | } 108 | }; 109 | 110 | // this is really bad. 111 | // See https://github.com/jenkinsci/jenkins/blob/75468da366c1d257a51655dcbe952d55b8aeeb9c/war/src/main/js/util/jenkins.js#L22 112 | const oldPrototype = Array.prototype.toJSON; 113 | delete Array.prototype.toJSON; 114 | 115 | try { 116 | xhr.send(JSON.stringify(json)); 117 | } finally { 118 | Array.prototype.toJSON = oldPrototype; 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/casc/ConfigurationAsCodeTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.casc; 2 | 3 | import com.cloudbees.hudson.plugins.folder.Folder; 4 | import hudson.model.Computer; 5 | import hudson.model.Item; 6 | import hudson.model.User; 7 | import hudson.security.ACL; 8 | import hudson.security.ACLContext; 9 | import hudson.slaves.DumbSlave; 10 | import hudson.slaves.JNLPLauncher; 11 | import io.jenkins.plugins.casc.ConfigurationContext; 12 | import io.jenkins.plugins.casc.ConfiguratorRegistry; 13 | import io.jenkins.plugins.casc.misc.ConfiguredWithCode; 14 | import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; 15 | import io.jenkins.plugins.casc.model.CNode; 16 | import jenkins.model.Jenkins; 17 | import org.junit.Before; 18 | import org.junit.Rule; 19 | import org.junit.Test; 20 | 21 | import java.util.Objects; 22 | 23 | import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; 24 | import static io.jenkins.plugins.casc.misc.Util.toStringFromYamlFile; 25 | import static io.jenkins.plugins.casc.misc.Util.toYamlString; 26 | import static org.hamcrest.MatcherAssert.assertThat; 27 | import static org.hamcrest.core.Is.is; 28 | import static org.junit.Assert.assertFalse; 29 | import static org.junit.Assert.assertTrue; 30 | 31 | public class ConfigurationAsCodeTest { 32 | private Folder folder; 33 | 34 | @Rule 35 | public JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); 36 | 37 | @Before 38 | public void setUp() throws Exception { 39 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); 40 | j.jenkins.addNode(new DumbSlave("agent1", "", new JNLPLauncher(true))); 41 | folder = j.jenkins.createProject(Folder.class, "root"); 42 | } 43 | 44 | @Test 45 | @ConfiguredWithCode("config.yml") 46 | public void configurationImportTest() { 47 | try (ACLContext ignored = ACL.as(User.getOrCreateByIdOrFullName("admin"))) { 48 | assertTrue(j.jenkins.hasPermission(Jenkins.ADMINISTER)); 49 | } 50 | 51 | try (ACLContext ignored = ACL.as(User.getOrCreateByIdOrFullName("user1"))) { 52 | assertTrue(folder.hasPermission(Item.READ)); 53 | assertFalse(j.jenkins.hasPermission(Jenkins.ADMINISTER)); 54 | 55 | assertTrue(Objects.requireNonNull(j.jenkins.getComputer("agent1")).hasPermission(Computer.CONFIGURE)); 56 | assertFalse(Objects.requireNonNull(j.jenkins.getComputer("agent1")).hasPermission(Computer.DELETE)); 57 | } 58 | } 59 | 60 | @Test 61 | @ConfiguredWithCode("config3.yml") 62 | public void configurationImportWithHumanReadableTest() { 63 | try (ACLContext ignored = ACL.as(User.getOrCreateByIdOrFullName("admin"))) { 64 | assertTrue(j.jenkins.hasPermission(Jenkins.ADMINISTER)); 65 | } 66 | 67 | try (ACLContext ignored = ACL.as(User.getOrCreateByIdOrFullName("user1"))) { 68 | assertTrue(folder.hasPermission(Item.READ)); 69 | assertFalse(j.jenkins.hasPermission(Jenkins.ADMINISTER)); 70 | 71 | assertTrue(Objects.requireNonNull(j.jenkins.getComputer("agent1")).hasPermission(Computer.CONFIGURE)); 72 | assertFalse(Objects.requireNonNull(j.jenkins.getComputer("agent1")).hasPermission(Computer.DELETE)); 73 | } 74 | } 75 | 76 | @Test 77 | @ConfiguredWithCode("config.yml") 78 | public void configurationExportTest() throws Exception { 79 | ConfiguratorRegistry registry = ConfiguratorRegistry.get(); 80 | ConfigurationContext context = new ConfigurationContext(registry); 81 | CNode yourAttribute = getJenkinsRoot(context).get("authorizationStrategy").asMapping() 82 | .get("folderBased"); 83 | 84 | String exported = toYamlString(yourAttribute); 85 | String expected = toStringFromYamlFile(this, "expected.yml"); 86 | 87 | assertThat(exported, is(expected)); 88 | } 89 | 90 | @Test 91 | @ConfiguredWithCode("config3.yml") 92 | public void configurationExportWithHumanReadableTest() throws Exception { 93 | ConfiguratorRegistry registry = ConfiguratorRegistry.get(); 94 | ConfigurationContext context = new ConfigurationContext(registry); 95 | CNode yourAttribute = getJenkinsRoot(context).get("authorizationStrategy").asMapping() 96 | .get("folderBased"); 97 | 98 | String exported = toYamlString(yourAttribute); 99 | String expected = toStringFromYamlFile(this, "expected3.yml"); 100 | 101 | assertThat(exported, is(expected)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/jmh/benchmarks/GlobalRoleBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.jmh.benchmarks; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import hudson.model.Item; 5 | import hudson.model.User; 6 | import io.jenkins.plugins.folderauth.FolderBasedAuthorizationStrategy; 7 | import io.jenkins.plugins.folderauth.acls.GlobalAclImpl; 8 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 9 | import jenkins.benchmark.jmh.JmhBenchmark; 10 | import jenkins.benchmark.jmh.JmhBenchmarkState; 11 | import org.acegisecurity.context.SecurityContext; 12 | import org.acegisecurity.context.SecurityContextHolder; 13 | import org.jvnet.hudson.test.JenkinsRule; 14 | import org.openjdk.jmh.annotations.Benchmark; 15 | import org.openjdk.jmh.annotations.Level; 16 | import org.openjdk.jmh.annotations.Scope; 17 | import org.openjdk.jmh.annotations.Setup; 18 | import org.openjdk.jmh.annotations.State; 19 | import org.openjdk.jmh.infra.Blackhole; 20 | 21 | import java.util.Collections; 22 | import java.util.HashSet; 23 | import java.util.Objects; 24 | import java.util.Set; 25 | 26 | import static io.jenkins.plugins.folderauth.misc.PermissionWrapper.wrapPermissions; 27 | import static org.junit.Assert.assertFalse; 28 | 29 | /** 30 | * This benchmark is created to test the performance of GlobalRoles. This test is inspired from 31 | * https://github.com/jenkinsci/role-strategy-plugin/blob/master/src/test/java/jmh/benchmarks/RoleMapBenchmark.java . 32 | *

33 | * This tests the scalability of the performance of {@link GlobalAclImpl} with increased number of roles. 34 | * Do note that "user3" does not have the {@link Item#CREATE} permission. 35 | */ 36 | @JmhBenchmark 37 | @SuppressWarnings("unused") 38 | public class GlobalRoleBenchmark { 39 | public static class GlobalRoles050 extends GlobalRoleBenchmarkState { 40 | @Override 41 | int getRoleCount() { 42 | return 50; 43 | } 44 | } 45 | 46 | public static class GlobalRoles100 extends GlobalRoleBenchmarkState { 47 | @Override 48 | int getRoleCount() { 49 | return 100; 50 | } 51 | } 52 | 53 | public static class GlobalRoles200 extends GlobalRoleBenchmarkState { 54 | @Override 55 | int getRoleCount() { 56 | return 200; 57 | } 58 | } 59 | 60 | public static class GlobalRoles500 extends GlobalRoleBenchmarkState { 61 | @Override 62 | int getRoleCount() { 63 | return 500; 64 | } 65 | } 66 | 67 | @State(Scope.Thread) 68 | public static class ThreadState { 69 | @Setup(Level.Iteration) 70 | public void setup() { 71 | SecurityContext holder = SecurityContextHolder.getContext(); 72 | holder.setAuthentication(Objects.requireNonNull(User.getById("user3", true)).impersonate()); 73 | } 74 | } 75 | 76 | @Benchmark 77 | public void benchmark050(GlobalRoles050 state, ThreadState threadState, Blackhole blackhole) { 78 | assertFalse(state.acl.hasPermission(Item.CREATE)); 79 | } 80 | 81 | @Benchmark 82 | public void benchmark100(GlobalRoles100 state, ThreadState threadState, Blackhole blackhole) { 83 | blackhole.consume(state.acl.hasPermission(Item.CREATE)); 84 | } 85 | 86 | @Benchmark 87 | public void benchmark200(GlobalRoles200 state, ThreadState threadState, Blackhole blackhole) { 88 | blackhole.consume(state.acl.hasPermission(Item.CREATE)); 89 | } 90 | 91 | @Benchmark 92 | public void benchmark500(GlobalRoles500 state, ThreadState threadState, Blackhole blackhole) { 93 | blackhole.consume(state.acl.hasPermission(Item.CREATE)); 94 | } 95 | } 96 | 97 | abstract class GlobalRoleBenchmarkState extends JmhBenchmarkState { 98 | 99 | GlobalAclImpl acl; 100 | 101 | @Override 102 | public void setup() { 103 | getJenkins().setSecurityRealm(new JenkinsRule().createDummySecurityRealm()); 104 | Set globalRoles = new HashSet<>(); 105 | for (int i = 0; i < getRoleCount(); i++) { 106 | globalRoles.add(new GlobalRole("role" + i, wrapPermissions(Item.DISCOVER, Item.CONFIGURE), 107 | ImmutableSet.of("user" + i))); 108 | } 109 | 110 | FolderBasedAuthorizationStrategy strategy = new FolderBasedAuthorizationStrategy( 111 | globalRoles, Collections.emptySet(), Collections.emptySet()); 112 | acl = strategy.getRootACL(); 113 | assertFalse(acl.hasPermission(Objects.requireNonNull(User.getById("user3", true)).impersonate(), 114 | Item.CREATE)); 115 | } 116 | 117 | abstract int getRoleCount(); 118 | } 119 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/RestartSurvivabilityTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth; 2 | 3 | import com.cloudbees.hudson.plugins.folder.Folder; 4 | import com.google.common.collect.ImmutableSet; 5 | import hudson.model.Computer; 6 | import hudson.model.Item; 7 | import hudson.model.User; 8 | import hudson.security.ACL; 9 | import hudson.security.ACLContext; 10 | import hudson.security.AuthorizationStrategy; 11 | import hudson.util.XStream2; 12 | import io.jenkins.plugins.folderauth.roles.AgentRole; 13 | import io.jenkins.plugins.folderauth.roles.FolderRole; 14 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 15 | import jenkins.model.Jenkins; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.junit.runners.model.Statement; 19 | import org.jvnet.hudson.test.Issue; 20 | import org.jvnet.hudson.test.RestartableJenkinsRule; 21 | 22 | import java.util.Collections; 23 | import java.util.HashSet; 24 | import java.util.Set; 25 | 26 | import static io.jenkins.plugins.folderauth.misc.PermissionWrapper.wrapPermissions; 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertFalse; 29 | import static org.junit.Assert.assertNotNull; 30 | import static org.junit.Assert.assertTrue; 31 | 32 | public class RestartSurvivabilityTest { 33 | @Rule 34 | public RestartableJenkinsRule rule = new RestartableJenkinsRule(); 35 | 36 | @Test 37 | @Issue("JENKINS-58485") 38 | public void shouldHaveSameConfigurationAfterRestart() { 39 | rule.addStep(new Statement() { 40 | @Override 41 | public void evaluate() throws Exception { 42 | rule.j.createProject(Folder.class, "folder"); 43 | rule.j.jenkins.setSecurityRealm(rule.j.createDummySecurityRealm()); 44 | rule.j.jenkins.setAuthorizationStrategy(createNewFolderBasedAuthorizationStrategy()); 45 | rule.j.jenkins.addNode(rule.j.createSlave("foo", null, null)); 46 | checkConfiguration(); 47 | } 48 | }); 49 | 50 | rule.addStep(new Statement() { 51 | @Override 52 | public void evaluate() { 53 | rule.j.jenkins.setSecurityRealm(rule.j.createDummySecurityRealm()); 54 | checkConfiguration(); 55 | 56 | // JENKINS-58485 57 | XStream2 xStream = new XStream2(); 58 | String xml = xStream.toXML(rule.j.jenkins.getAuthorizationStrategy()); 59 | assertFalse(xml.contains("ConcurrentHashMap$KeySetView")); 60 | } 61 | }); 62 | } 63 | 64 | private FolderBasedAuthorizationStrategy createNewFolderBasedAuthorizationStrategy() { 65 | Set globalRoles = new HashSet<>(); 66 | globalRoles.add(new GlobalRole("admin", wrapPermissions(Jenkins.ADMINISTER), ImmutableSet.of("admin"))); 67 | globalRoles.add(new GlobalRole("read", wrapPermissions(Jenkins.READ), ImmutableSet.of("authenticated"))); 68 | 69 | Set folderRoles = new HashSet<>(); 70 | folderRoles.add(new FolderRole("read", wrapPermissions(Item.READ), ImmutableSet.of("folder"), 71 | ImmutableSet.of("user1"))); 72 | 73 | Set agentRoles = new HashSet<>(); 74 | agentRoles.add(new AgentRole("configureMaster", wrapPermissions(Computer.CONFIGURE), 75 | Collections.singleton("foo"), Collections.singleton("user1"))); 76 | 77 | return new FolderBasedAuthorizationStrategy(globalRoles, folderRoles, agentRoles); 78 | } 79 | 80 | private void checkConfiguration() { 81 | Jenkins jenkins = Jenkins.get(); 82 | try (ACLContext ignored = ACL.as(User.getById("admin", true))) { 83 | assertTrue(jenkins.hasPermission(Jenkins.ADMINISTER)); 84 | } 85 | 86 | try (ACLContext ignored = ACL.as(User.getById("user1", true))) { 87 | Folder folder = (Folder) jenkins.getItem("folder"); 88 | assertNotNull(folder); 89 | assertTrue(jenkins.hasPermission(Jenkins.READ)); 90 | assertTrue(folder.hasPermission(Item.READ)); 91 | assertFalse(folder.hasPermission(Item.CONFIGURE)); 92 | assertFalse(jenkins.hasPermission(Jenkins.ADMINISTER)); 93 | 94 | Computer computer = jenkins.getComputer("foo"); 95 | assertNotNull(computer); 96 | assertTrue(computer.hasPermission(Computer.CONFIGURE)); 97 | assertFalse(computer.hasPermission(Computer.DELETE)); 98 | } 99 | 100 | AuthorizationStrategy a = Jenkins.get().getAuthorizationStrategy(); 101 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 102 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 103 | assertEquals(strategy.getGlobalRoles().size(), 2); 104 | assertEquals(strategy.getFolderRoles().size(), 1); 105 | assertEquals(strategy.getAgentRoles().size(), 1); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/jmh/benchmarks/FolderRoleBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.jmh.benchmarks; 2 | 3 | import com.cloudbees.hudson.plugins.folder.Folder; 4 | import com.google.common.collect.ImmutableSet; 5 | import hudson.model.FreeStyleProject; 6 | import hudson.model.Item; 7 | import hudson.model.User; 8 | import io.jenkins.plugins.folderauth.FolderBasedAuthorizationStrategy; 9 | import io.jenkins.plugins.folderauth.roles.FolderRole; 10 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 11 | import jenkins.benchmark.jmh.JmhBenchmark; 12 | import jenkins.benchmark.jmh.JmhBenchmarkState; 13 | import jenkins.model.Jenkins; 14 | import org.acegisecurity.context.SecurityContext; 15 | import org.acegisecurity.context.SecurityContextHolder; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | import org.openjdk.jmh.annotations.Benchmark; 18 | import org.openjdk.jmh.annotations.Level; 19 | import org.openjdk.jmh.annotations.Scope; 20 | import org.openjdk.jmh.annotations.Setup; 21 | import org.openjdk.jmh.annotations.State; 22 | import org.openjdk.jmh.infra.Blackhole; 23 | 24 | import java.util.Collections; 25 | import java.util.HashSet; 26 | import java.util.Objects; 27 | import java.util.Random; 28 | import java.util.Set; 29 | 30 | import static io.jenkins.plugins.folderauth.misc.PermissionWrapper.wrapPermissions; 31 | 32 | /** 33 | * Benchmarks for {@link FolderRole}s based on the configuration of 34 | * https://github.com/jenkinsci/role-strategy-plugin/blob/master/src/test/java/jmh/benchmarks/FolderAccessBenchmark.java 35 | */ 36 | @JmhBenchmark 37 | public class FolderRoleBenchmark { 38 | public static class MyState extends JmhBenchmarkState { 39 | 40 | @Override 41 | public void setup() throws Exception { 42 | Jenkins jenkins = getJenkins(); 43 | jenkins.setSecurityRealm(new JenkinsRule().createDummySecurityRealm()); 44 | 45 | Set globalRoles = ImmutableSet.of(( 46 | new GlobalRole("ADMIN", wrapPermissions(Jenkins.ADMINISTER), Collections.singleton("admin"))), 47 | new GlobalRole("read", wrapPermissions(Jenkins.READ), Collections.singleton("authenticated")) 48 | ); 49 | 50 | Set folderRoles = new HashSet<>(); 51 | 52 | Random random = new Random(100L); 53 | 54 | // create the folders 55 | for (int i = 0; i < 10; i++) { 56 | String topFolderName = "TopFolder" + i; 57 | Folder folder = jenkins.createProject(Folder.class, topFolderName); 58 | 59 | Set users = new HashSet<>(); 60 | for (int k = 0; k < random.nextInt(5); k++) { 61 | users.add("user" + random.nextInt(100)); 62 | } 63 | 64 | FolderRole userRole = new FolderRole(topFolderName, wrapPermissions(Item.READ, Item.DISCOVER), 65 | Collections.singleton(topFolderName), users); 66 | 67 | folderRoles.add(userRole); 68 | 69 | for (int j = 0; j < 5; j++) { 70 | Folder bottom = folder.createProject(Folder.class, "BottomFolder" + j); 71 | 72 | Set maintainers = new HashSet<>(2); 73 | maintainers.add("user" + random.nextInt(100)); 74 | maintainers.add("user" + random.nextInt(100)); 75 | 76 | FolderRole maintainerRole = new FolderRole(bottom.getFullName(), 77 | wrapPermissions(Item.READ, Item.DISCOVER, Item.CREATE), 78 | Collections.singleton(topFolderName), maintainers); 79 | 80 | Set admin = Collections.singleton("user" + random.nextInt(100)); 81 | 82 | FolderRole folderAdminRole = new FolderRole(bottom.getFullName(), wrapPermissions(Item.READ, Item.DISCOVER, 83 | Item.CONFIGURE, Item.CREATE), Collections.singleton(topFolderName), admin); 84 | folderRoles.add(maintainerRole); 85 | folderRoles.add(folderAdminRole); 86 | 87 | for (int k = 0; k < 5; k++) { 88 | bottom.createProject(FreeStyleProject.class, "Project" + k); 89 | } 90 | } 91 | } 92 | 93 | jenkins.setAuthorizationStrategy(new FolderBasedAuthorizationStrategy(globalRoles, folderRoles, 94 | Collections.emptySet())); 95 | } 96 | } 97 | 98 | @State(Scope.Thread) 99 | public static class ThreadState { 100 | @Setup(Level.Iteration) 101 | public void setup() { 102 | SecurityContext securityContext = SecurityContextHolder.getContext(); 103 | securityContext.setAuthentication(Objects.requireNonNull(User.getById("user33", true)).impersonate()); 104 | } 105 | } 106 | 107 | @Benchmark 108 | public void renderViewSimulation(MyState state, ThreadState threadState, Blackhole blackhole) { 109 | blackhole.consume(state.getJenkins().getAllItems()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/misc/PermissionWrapper.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth.misc; 2 | 3 | import hudson.PluginManager; 4 | import hudson.security.Permission; 5 | import io.jenkins.plugins.folderauth.Messages; 6 | import io.jenkins.plugins.folderauth.roles.AbstractRole; 7 | import jenkins.model.Jenkins; 8 | import org.kohsuke.accmod.Restricted; 9 | import org.kohsuke.accmod.restrictions.NoExternalUse; 10 | import org.kohsuke.stapler.DataBoundConstructor; 11 | 12 | import javax.annotation.Nonnull; 13 | import javax.annotation.ParametersAreNonnullByDefault; 14 | import java.util.Arrays; 15 | import java.util.Collection; 16 | import java.util.Collections; 17 | import java.util.HashSet; 18 | import java.util.Set; 19 | import java.util.stream.Collectors; 20 | import java.util.stream.Stream; 21 | 22 | /** 23 | * A wrapper for efficient serialization of a {@link Permission} 24 | * when stored as a part of an {@link AbstractRole}. 25 | */ 26 | @ParametersAreNonnullByDefault 27 | public final class PermissionWrapper implements Comparable { 28 | // should've been final but needs to be setup when the 29 | // object is deserialized from the XML config 30 | private transient Permission permission; 31 | private final String id; 32 | 33 | @Restricted(NoExternalUse.class) 34 | public static final Set DANGEROUS_PERMISSIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( 35 | Jenkins.RUN_SCRIPTS, 36 | PluginManager.CONFIGURE_UPDATECENTER, 37 | PluginManager.UPLOAD_PLUGINS 38 | ))); 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param id the id of the permission this {@link PermissionWrapper} contains. 44 | */ 45 | @DataBoundConstructor 46 | public PermissionWrapper(String id) { 47 | this.id = id; 48 | permission = PermissionFinder.findPermission(id); 49 | checkPermission(); 50 | } 51 | 52 | public String getId() { 53 | return String.format("%s/%s", permission.group.getId(), permission.name); 54 | } 55 | 56 | /** 57 | * Used to setup the permission when deserialized 58 | * 59 | * @return the {@link PermissionWrapper} 60 | */ 61 | @Nonnull 62 | @SuppressWarnings("unused") 63 | private Object readResolve() { 64 | permission = PermissionFinder.findPermission(id); 65 | checkPermission(); 66 | return this; 67 | } 68 | 69 | /** 70 | * Get the permission corresponding to this {@link PermissionWrapper} 71 | * 72 | * @return the permission corresponding to this {@link PermissionWrapper} 73 | */ 74 | @Nonnull 75 | public Permission getPermission() { 76 | return permission; 77 | } 78 | 79 | 80 | @Override 81 | public boolean equals(Object o) { 82 | if (this == o) return true; 83 | if (o == null || getClass() != o.getClass()) return false; 84 | PermissionWrapper that = (PermissionWrapper) o; 85 | return id.equals(that.id); 86 | } 87 | 88 | @Override 89 | public int hashCode() { 90 | return id.hashCode(); 91 | } 92 | 93 | /** 94 | * Checks if the permission for this {@link PermissionWrapper} is valid. 95 | * 96 | * @throws IllegalArgumentException when the permission did not exist, was null or was dangerous. 97 | */ 98 | private void checkPermission() { 99 | if (permission == null) { 100 | throw new IllegalArgumentException(Messages.PermissionWrapper_UnknownPermission(id)); 101 | } else if (DANGEROUS_PERMISSIONS.contains(permission)) { 102 | throw new IllegalArgumentException(Messages.PermissionWrapper_NoDangerousPermissions()); 103 | } 104 | } 105 | 106 | /** 107 | * Convenience method to wrap {@link Permission}s into {@link PermissionWrapper}s. 108 | * 109 | * @param permissions permissions to be wrapped up 110 | * @return a set containing a {@link PermissionWrapper} for each permission in {@code permissions} 111 | */ 112 | @Nonnull 113 | public static Set wrapPermissions(Permission... permissions) { 114 | return _wrapPermissions(Arrays.stream(permissions)); 115 | } 116 | 117 | /** 118 | * Convenience method to wrap {@link Permission}s into {@link PermissionWrapper}s. 119 | * 120 | * @param permissions permissions to be wrapped up 121 | * @return a set containing a {@link PermissionWrapper} for each permission in {@code permissions} 122 | */ 123 | @Nonnull 124 | public static Set wrapPermissions(Collection permissions) { 125 | return _wrapPermissions(permissions.stream()); 126 | } 127 | 128 | @Nonnull 129 | private static Set _wrapPermissions(Stream stream) { 130 | return stream 131 | .map(Permission::getId) 132 | .map(PermissionWrapper::new) 133 | .collect(Collectors.toSet()); 134 | } 135 | 136 | @Override 137 | public int compareTo(@Nonnull PermissionWrapper other) { 138 | return Permission.ID_COMPARATOR.compare(this.permission, other.permission); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/FolderBasedAuthorizationStrategyTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth; 2 | 3 | import com.cloudbees.hudson.plugins.folder.Folder; 4 | import com.google.common.collect.ImmutableSet; 5 | import hudson.model.FreeStyleProject; 6 | import hudson.model.Item; 7 | import hudson.model.User; 8 | import hudson.security.ACL; 9 | import hudson.security.ACLContext; 10 | import hudson.security.Permission; 11 | import hudson.security.PermissionGroup; 12 | import io.jenkins.plugins.folderauth.roles.FolderRole; 13 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 14 | import jenkins.model.Jenkins; 15 | import org.junit.Before; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.jvnet.hudson.test.JenkinsRule; 19 | 20 | import java.util.Collections; 21 | import java.util.HashSet; 22 | 23 | import static io.jenkins.plugins.folderauth.misc.PermissionWrapper.wrapPermissions; 24 | import static junit.framework.TestCase.assertFalse; 25 | import static junit.framework.TestCase.assertTrue; 26 | 27 | public class FolderBasedAuthorizationStrategyTest { 28 | @Rule 29 | public JenkinsRule jenkinsRule = new JenkinsRule(); 30 | 31 | private Folder root; 32 | private Folder child1; 33 | private Folder child2; 34 | private Folder child3; 35 | 36 | private FreeStyleProject job1; 37 | private FreeStyleProject job2; 38 | 39 | private User admin; 40 | private User user1; 41 | private User user2; 42 | 43 | @Before 44 | public void setUp() throws Exception { 45 | Jenkins jenkins = jenkinsRule.jenkins; 46 | jenkins.setSecurityRealm(jenkinsRule.createDummySecurityRealm()); 47 | 48 | FolderBasedAuthorizationStrategy strategy = new FolderBasedAuthorizationStrategy(Collections.emptySet(), 49 | Collections.emptySet(), Collections.emptySet()); 50 | jenkins.setAuthorizationStrategy(strategy); 51 | 52 | final String adminRoleName = "adminRole"; 53 | final String overallReadRoleName = "overallRead"; 54 | 55 | FolderAuthorizationStrategyAPI.addGlobalRole(new GlobalRole(adminRoleName, 56 | wrapPermissions(FolderAuthorizationStrategyManagementLink.getSafePermissions( 57 | new HashSet<>(PermissionGroup.getAll()))))); 58 | 59 | FolderAuthorizationStrategyAPI.assignSidToGlobalRole("admin", adminRoleName); 60 | 61 | FolderAuthorizationStrategyAPI.addGlobalRole(new GlobalRole(overallReadRoleName, wrapPermissions(Permission.READ))); 62 | FolderAuthorizationStrategyAPI.assignSidToGlobalRole("authenticated", overallReadRoleName); 63 | 64 | FolderAuthorizationStrategyAPI.addFolderRole(new FolderRole("folderRole1", wrapPermissions(Item.READ), 65 | ImmutableSet.of("root"))); 66 | FolderAuthorizationStrategyAPI.assignSidToFolderRole("user1", "folderRole1"); 67 | FolderAuthorizationStrategyAPI.assignSidToFolderRole("user2", "folderRole1"); 68 | 69 | FolderAuthorizationStrategyAPI.addFolderRole(new FolderRole("folderRole2", wrapPermissions(Item.CONFIGURE, Item.DELETE), 70 | ImmutableSet.of("root/child1"))); 71 | FolderAuthorizationStrategyAPI.assignSidToFolderRole("user2", "folderRole2"); 72 | 73 | /* 74 | * Folder hierarchy for the test 75 | * 76 | * root 77 | * / \ 78 | * child1 child2 79 | * / \ 80 | * child3 job1 81 | * / 82 | * job2 83 | */ 84 | 85 | root = jenkins.createProject(Folder.class, "root"); 86 | child1 = root.createProject(Folder.class, "child1"); 87 | child2 = root.createProject(Folder.class, "child2"); 88 | child3 = child1.createProject(Folder.class, "child3"); 89 | 90 | job1 = child2.createProject(FreeStyleProject.class, "job1"); 91 | job2 = child3.createProject(FreeStyleProject.class, "job2"); 92 | 93 | admin = User.getById("admin", true); 94 | user1 = User.getById("user1", true); 95 | user2 = User.getById("user2", true); 96 | } 97 | 98 | @Test 99 | public void permissionTest() { 100 | Jenkins jenkins = jenkinsRule.jenkins; 101 | 102 | try (ACLContext ignored = ACL.as(admin)) { 103 | assertTrue(jenkins.hasPermission(Jenkins.ADMINISTER)); 104 | assertTrue(child3.hasPermission(Item.CONFIGURE)); 105 | assertTrue(job1.hasPermission(Item.READ)); 106 | assertTrue(job2.hasPermission(Item.CREATE)); 107 | } 108 | 109 | try (ACLContext ignored = ACL.as(user1)) { 110 | assertTrue(jenkins.hasPermission(Permission.READ)); 111 | assertTrue(root.hasPermission(Item.READ)); 112 | assertTrue(job1.hasPermission(Item.READ)); 113 | assertTrue(job2.hasPermission(Item.READ)); 114 | 115 | assertFalse(job1.hasPermission(Item.CREATE)); 116 | assertFalse(job1.hasPermission(Item.DELETE)); 117 | assertFalse(job1.hasPermission(Item.CONFIGURE)); 118 | assertFalse(job2.hasPermission(Item.CREATE)); 119 | assertFalse(job2.hasPermission(Item.CONFIGURE)); 120 | } 121 | 122 | try (ACLContext ignored = ACL.as(user2)) { 123 | assertTrue(jenkins.hasPermission(Permission.READ)); 124 | assertTrue(child2.hasPermission(Item.READ)); 125 | assertTrue(child1.hasPermission(Item.READ)); 126 | assertTrue(job2.hasPermission(Item.CONFIGURE)); 127 | assertFalse(job1.hasPermission(Item.CONFIGURE)); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Using the plugin 2 | 3 | This plugin allows administrator users to allow or restrict users permissions. 4 | 5 | This plugin follows a 'role'-based model in where, depending on the context, 6 | a 'role' is applicable on multiple objects, can be assigned to multiple 7 | users and can grant a number of permissions. All users assigned to the role 8 | get all the permissions granted by that role for every object the role is 9 | valid on. 10 | 11 | This plugin provides multiple types of roles for managing permissions which 12 | can all be used simultaneously to provide a flexible way to manage permissions: 13 | 14 | * **Global Roles**: these roles give users permissions which are applicable 15 | everywhere in Jenkins. 16 | * **Folder Roles**: these roles allow managing permissions for individual 17 | projects organized in 'folders' from Cloudbees' 18 | [Folders plugin](https://plugins.jenkins.io/cloudbees-folder). 19 | * **Agent Roles**: these roles allow managing permissions for agents connected 20 | to Jenkins. 21 | 22 | ## Setting up 23 | 24 | To use this plugin, you need to set it as the authorization strategy from the 25 | 'Configure Global Security' page. To do that: 26 | 27 | 1. Log in as a user with administrator permissions. 28 | 2. Go to the 'Manage Jenkins' page which should visible on the sidebar from 29 | Jenkins' home page. 30 | 3. Go to the 'Configure Global Security' page. 31 | 4. Ensure that security for your Jenkins instance is enabled. 32 | 5. Choose 'Folder Authorization Strategy' as the authorization and save the 33 | configuration. 34 | 35 | Folder based authorization is now active 🎉. Now go back to the 'Manage 36 | Jenkins' page and scroll down. You should see a linkfor the 'Folder 37 | Authorization Strategy'. Click on the link and you can now 38 | start configuring permissions for users. 39 | 40 | ### Adding Roles 41 | 42 | The process to add any type of role is similar. Here we show how to add a 43 | 'Folder Role'. 44 | 45 | ![Adding a folder role](/docs/images/add-folder-role.png) 46 | 47 | 1. Choose a name for the role. The name of the role uniquely identifies it and 48 | is useful for invoking the REST API methods. 49 | 2. Choose the permissions that would be granted through this role. You can 50 | select multiple permissions by holding down the control key on your keyboard. 51 | 3. Choose the folders on which this role will be applicable. 52 | 4. Click the 'Add role' button and you're done! 53 | 54 | ### Assigning Roles to users 55 | 56 | It is really easy to assign roles to users and groups. Just follow the steps 57 | below. 58 | 59 | ![Assigning a role](/docs/images/assign-role.png) 60 | 61 | 1. Find the role, type the user's ID in the text field. 62 | 2. Click the submit button. 63 | 3. The role was assigned to the user! 64 | 65 | **Note**: Apart from the user IDs provided by your security realm, Jenkins 66 | provides two groups, `authenticated` and `anonymous`, which as their names 67 | suggest, apply to authenticated and non-authenticated users, respectively. 68 | Therefore, if you assign a role to the `authenticated` sid, permissions would 69 | be granted to all logged-in users. 70 | 71 | ### Deleting a Role 72 | 73 | Just click the big red ❌ on the top right of the role you want to delete and 74 | you're done. 75 | 76 | ## Inheritance of Folder Roles 77 | 78 | When you add a folder role that is applicable on a *parent* folder, the 79 | permissions granted by that role are applicable to all of its children. 80 | Therefore, all permissions granted to a folder for a user are applicable to 81 | all folders and jobs nested under it. 82 | 83 | ## Using Agent Roles 84 | 85 | Agent roles can be used to configure users' permissions for agents connected 86 | to Jenkins. Multiple agents can be selected when creating an agent role, just 87 | like with Folder roles. All users assigned to this role will get all 88 | permissions granted by an agent role for all agents on which this role is 89 | applicable. 90 | 91 | Unlike Folder Roles, there is no support for inheritance of Agent Roles 92 | because Jenkins does not support nesting of agents. 93 | 94 | ## Jenkins Configuration-as-Code Support 95 | 96 | This plugin supports configuration as code for hands-free setup of your 97 | Jenkins instance. The easiest way to get a configuration for your uses-case is 98 | to configure the plugin through the Web UI and then export the configuration 99 | as YAML and store it for later use. 100 | 101 | You can write the configuration manually too. As an example, a YAML 102 | configuration for this plugin typically looks like this: 103 | 104 | ```yaml 105 | jenkins: 106 | authorizationStrategy: 107 | folderBased: 108 | agentRoles: 109 | - agents: 110 | - "agent1" 111 | name: "agentRole1" 112 | permissions: 113 | - id: "Agent/Configure" 114 | - id: "Agent/Disconnect" 115 | sids: 116 | - "user1" 117 | folderRoles: 118 | - folders: 119 | - "root" 120 | name: "viewRoot" 121 | permissions: 122 | - id: "Job/Read" 123 | sids: 124 | - "user1" 125 | globalRoles: 126 | - name: "admin" 127 | permissions: 128 | - id: "Overall/Administer" 129 | sids: 130 | - "admin" 131 | - name: "read" 132 | permissions: 133 | - id: "Overall/Read" 134 | sids: 135 | - "user1" 136 | ``` 137 | 138 | The configuration YAML also supports permission IDs which are used internally by Jenkins, 139 | for example, `hudson.model.Hudson.Administer` 140 | 141 | **Note**: You need to have [Configuration-as-Code](https://plugins.jenkins.io/configuration-as-code) 142 | plugin ≥ [1.24](https://github.com/jenkinsci/configuration-as-code-plugin/releases/tag/configuration-as-code-1.24) 143 | installed for using Jenkins Configuration-as-Code with this plugin. The CasC 144 | plugin will **not** be installed when you install this plugin. 145 | -------------------------------------------------------------------------------- /docs/rest-api.adoc: -------------------------------------------------------------------------------- 1 | = REST API methods 2 | :toc: 3 | 4 | The plugin provides REST APIs for modifying the roles. All of these methods 5 | require the user invoking them to have the `Jenkins.ADMINISTER` permission. 6 | 7 | == API methods for adding a role 8 | 9 | === `addGlobalRole` 10 | 11 | Adds a global role with no sids assigned to it. Requires POST to `${JENKINS_URL}/folder-auth/addGlobalRole`. 12 | The request body should specify the role to be added as a JSON object. For 13 | example, to add a role with name `foo` and providing the `Item | Delete` and the `Item | Configure` permissions, the request body should look like this: 14 | 15 | [source,json] 16 | ---- 17 | { 18 | "name": "foo", 19 | "permissions": [ 20 | "hudson.model.Item.Delete", 21 | "hudson.model.Item.Configure" 22 | ] 23 | } 24 | ---- 25 | 26 | For example, you can add the role using `curl` like this: 27 | 28 | [source,bash] 29 | ---- 30 | curl -X POST -H "Content-Type: application/json" -d \ 31 | '{ 32 | "name": "foo", 33 | "permissions": [ 34 | "hudson.model.Item.Delete", 35 | "hudson.model.Item.Configure" 36 | ] 37 | }' localhost:8080/folder-auth/addGlobalRole 38 | ---- 39 | 40 | === `addFolderRole` 41 | 42 | Adds a folder role with no sids assigned to it. Requires POST to `${JENKINS_URL}/folder-auth/addFolderRole`. 43 | The request body should specify the role to be added as a JSON object. For 44 | example, to add a role with name `foo` and providing the`Item | Delete` and 45 | the `Item | Configure` permissions on folders `bar`and `foo/baz`, the 46 | request body should look like this: 47 | 48 | [source,json] 49 | ---- 50 | { 51 | "name": "foo", 52 | "permissions": [ 53 | "hudson.model.Item.Delete", 54 | "hudson.model.Item.Configure" 55 | ], 56 | "folderNames": [ 57 | "foo/baz", 58 | "bar" 59 | ] 60 | } 61 | ---- 62 | 63 | === `addAgentRole` 64 | 65 | Adds an agent role with no sids assigned to it. Requires POST to `${JENKINS_URL}/folder-auth/addAgentRole`. 66 | The request body should specify the role to be added as a JSON object. For 67 | example, to add a role with the equal to `foo` and providing the 68 | `Agent | Configure` permissions on agents `bar` and `baz`, the request body 69 | should look like this: 70 | 71 | [source,json] 72 | ---- 73 | { 74 | "name": "foo", 75 | "permissions": [ 76 | "hudson.model.Computer.Configure" 77 | ], 78 | "agentNames": [ 79 | "bar", 80 | "baz" 81 | ] 82 | } 83 | ---- 84 | 85 | == API methods for assigning a sid to a role 86 | 87 | === `assignSidToGlobalRole` 88 | 89 | Assigns a sid to the role identified by its name. Requires POST to 90 | `${JENKINS_URL}/folder-auth/assignSidToGlobalRole`. The following query 91 | parameters are required: 92 | 93 | * `roleName`: The sid will be assigned to the global role with the name equal 94 | to this parameter. 95 | * `sid`: The sid to be assigned to the role with the name equal to the value of 96 | `roleName`. 97 | 98 | Using `curl`, for example, a sid "foo" can be assigned to the role "bar" 99 | 100 | [source,bash] 101 | ---- 102 | curl -X POST -d 'roleName=bar&sid=foo' \ 103 | http://localhost:8080/folder-auth/assignSidToGlobalRole 104 | ---- 105 | 106 | === `assignSidToFolderRole` 107 | 108 | Assigns a sid to the role identified by its name. Requires POST to 109 | `${JENKINS_URL}/folder-auth/assignSidToFolderRole`. The following query 110 | parameters are required: 111 | 112 | * `roleName`: The sid will be assigned to the folder role with the name equal 113 | to this parameter. 114 | * `sid`: The sid to be assigned to the role with the name equal to the value of 115 | `roleName`. 116 | 117 | Using `curl`, for example, a sid "foo" can be assigned to the role "bar" 118 | 119 | [source,bash] 120 | ---- 121 | curl -X POST -d 'roleName=bar&sid=foo' \ 122 | http://localhost:8080/folder-auth/assignSidToFolderRole 123 | ---- 124 | 125 | === `assignSidToAgentRole` 126 | 127 | Assigns a sid to the role identified by its name. Requires POST to 128 | `${JENKINS_URL}/folder-auth/assignSidToAgentRole`. The following query 129 | parameters are required: 130 | 131 | * `roleName`: The sid will be assigned to the agent role with the name equal 132 | to this parameter. 133 | * `sid`: The sid to be assigned to the role with the name equal to the value of 134 | `roleName`. 135 | 136 | Using `curl`, for example, a sid "foo" can be assigned to the role "bar" 137 | 138 | [source,bash] 139 | ---- 140 | curl -X POST -d 'roleName=bar&sid=foo' \ 141 | http://localhost:8080/folder-auth/assignSidToAgentRole 142 | ---- 143 | 144 | == API methods for deleting a role 145 | 146 | === `deleteGlobalRole` 147 | 148 | Deletes a global role identified by its name. Requires POST to 149 | `${JENKINS_URL}/folder-auth/deleteGlobalRole`. The query parameter 150 | `roleName` is required. 151 | 152 | Using `curl`, for example, a role with name "foo" can be deleted 153 | 154 | [source,bash] 155 | ---- 156 | curl -X POST -d 'roleName=foo' http://localhost:8080/folder-auth/deleteGlobalRole 157 | ---- 158 | 159 | === `deleteFolderRole` 160 | 161 | Deletes a folder role identified by its name. Requires POST to 162 | `${JENKINS_URL}/folder-auth/deleteGlobalRole`. The parameter 163 | `roleName` is required. 164 | 165 | [source,bash] 166 | ---- 167 | curl -X POST -d 'roleName=foo' http://localhost:8080/folder-auth/deleteFolderRole 168 | ---- 169 | 170 | === `deleteAgentRole` 171 | 172 | Deletes an agent role identified by its name. Requires POST to 173 | `${JENKINS_URL}/folder-auth/deleteGlobalRole`. The parameter 174 | `roleName` is required. 175 | 176 | [source,bash] 177 | ---- 178 | curl -X POST -d 'roleName=foo' http://localhost:8080/folder-auth/deleteAgentRole 179 | ---- 180 | 181 | == Logging in to Jenkins 182 | 183 | When using cURL to invoke the API, you need to login as a user with the 184 | administrator permissions. See the example below for viewing the home page: 185 | 186 | [source,bash] 187 | ---- 188 | curl -X GET -u $USERNAME:$TOKEN http://localhost:8080/ 189 | ---- 190 | 191 | The API token can be obtained by clicking on your logged in user on the top-right 192 | of your Jenkins Home page and then clicking the 'Configure' button in the side bar. 193 | For more information about authentication, please see https://wiki.jenkins.io/display/JENKINS/Remote+access+API 194 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/folderauth/FolderAuthorizationStrategyAPITest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth; 2 | 3 | import hudson.model.Computer; 4 | import hudson.model.Item; 5 | import hudson.security.AuthorizationStrategy; 6 | import hudson.security.Permission; 7 | import io.jenkins.plugins.folderauth.roles.AgentRole; 8 | import io.jenkins.plugins.folderauth.roles.FolderRole; 9 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 10 | import jenkins.model.Jenkins; 11 | import net.sf.json.JSONObject; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.jvnet.hudson.test.JenkinsRule; 16 | 17 | import static io.jenkins.plugins.folderauth.misc.PermissionWrapper.wrapPermissions; 18 | import static java.util.Collections.emptySet; 19 | import static java.util.Collections.singleton; 20 | import static org.junit.Assert.assertEquals; 21 | import static org.junit.Assert.assertFalse; 22 | import static org.junit.Assert.assertNotSame; 23 | import static org.junit.Assert.assertTrue; 24 | 25 | 26 | public class FolderAuthorizationStrategyAPITest { 27 | 28 | @Rule 29 | public JenkinsRule j = new JenkinsRule(); 30 | 31 | @Before 32 | public void setUp() { 33 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); 34 | FolderBasedAuthorizationStrategy strategy = new FolderBasedAuthorizationStrategy.DescriptorImpl() 35 | .newInstance(null, new JSONObject(true)); 36 | // should only create the admin global role 37 | assertEquals(1, strategy.getGlobalRoles().size()); 38 | assertEquals(0, strategy.getFolderRoles().size()); 39 | assertEquals(0, strategy.getAgentRoles().size()); 40 | 41 | j.jenkins.setAuthorizationStrategy(strategy); 42 | } 43 | 44 | @Test 45 | public void addGlobalRole() { 46 | GlobalRole readRole = new GlobalRole("readEverything", wrapPermissions(Jenkins.READ), singleton("user1")); 47 | FolderAuthorizationStrategyAPI.addGlobalRole(readRole); 48 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 49 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 50 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 51 | assertTrue(strategy.getGlobalRoles().contains(readRole)); 52 | } 53 | 54 | @Test 55 | public void addFolderRole() { 56 | FolderRole role = new FolderRole("readEverything", wrapPermissions(Jenkins.READ), 57 | singleton("folder1"), singleton("user1")); 58 | FolderAuthorizationStrategyAPI.addFolderRole(role); 59 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 60 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 61 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 62 | assertTrue(strategy.getFolderRoles().contains(role)); 63 | } 64 | 65 | @Test 66 | public void addAgentRole() { 67 | AgentRole role = new AgentRole("readEverything", wrapPermissions(Jenkins.READ), 68 | singleton("agent1"), singleton("user1")); 69 | FolderAuthorizationStrategyAPI.addAgentRole(role); 70 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 71 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 72 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 73 | assertTrue(strategy.getAgentRoles().contains(role)); 74 | } 75 | 76 | @Test 77 | public void assignSidToGlobalRole() { 78 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 79 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 80 | FolderBasedAuthorizationStrategy oldStrategy = (FolderBasedAuthorizationStrategy) a; 81 | String adminUserSid = "adminUserSid"; 82 | oldStrategy.getGlobalRoles().forEach(role -> assertFalse(role.getSids().contains(adminUserSid))); 83 | String adminRoleName = "admin"; 84 | FolderAuthorizationStrategyAPI.assignSidToGlobalRole(adminUserSid, adminRoleName); 85 | 86 | // a new authorization strategy should have been set 87 | AuthorizationStrategy b = j.jenkins.getAuthorizationStrategy(); 88 | assertTrue(b instanceof FolderBasedAuthorizationStrategy); 89 | assertNotSame("A new instance of FolderBasedAuthorizationStrategy should have been set.", a, b); 90 | FolderBasedAuthorizationStrategy newStrategy = (FolderBasedAuthorizationStrategy) b; 91 | GlobalRole role = newStrategy.getGlobalRoles().stream().filter(r -> r.getName().equals(adminRoleName)) 92 | .findAny().orElseThrow(() -> new RuntimeException("The admin role should exist")); 93 | assertTrue(role.getSids().contains(adminUserSid)); 94 | } 95 | 96 | @Test 97 | public void assignSidToFolderRole() { 98 | String sid = "user1"; 99 | FolderRole role = new FolderRole("foo", wrapPermissions(Item.READ), singleton("folderFoo")); 100 | assertEquals(0, role.getSids().size()); 101 | FolderAuthorizationStrategyAPI.addFolderRole(role); 102 | FolderAuthorizationStrategyAPI.assignSidToFolderRole(sid, "foo"); 103 | 104 | 105 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 106 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 107 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 108 | FolderRole updatedRole = strategy.getFolderRoles().stream().filter(r -> r.getName().equals("foo")) 109 | .findAny().orElseThrow(() -> new RuntimeException("The created role should exist")); 110 | assertTrue(updatedRole.getSids().contains(sid)); 111 | } 112 | 113 | @Test 114 | public void assignSidToAgentRole() { 115 | String sid = "user1"; 116 | AgentRole role = new AgentRole("bar", wrapPermissions(Item.READ), singleton("agentBar")); 117 | assertEquals(0, role.getSids().size()); 118 | FolderAuthorizationStrategyAPI.addAgentRole(role); 119 | FolderAuthorizationStrategyAPI.assignSidToAgentRole(sid, "bar"); 120 | 121 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 122 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 123 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 124 | AgentRole updatedRole = strategy.getAgentRoles().stream().filter(r -> r.getName().equals("bar")) 125 | .findAny().orElseThrow(() -> new RuntimeException("The created role should exist")); 126 | assertTrue(updatedRole.getSids().contains(sid)); 127 | } 128 | 129 | @Test 130 | public void removeSidFromGlobalRole() { 131 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 132 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 133 | final String adminRoleName = "admin"; 134 | FolderAuthorizationStrategyAPI.assignSidToGlobalRole("user1", adminRoleName); 135 | FolderAuthorizationStrategyAPI.removeSidFromGlobalRole("user1", adminRoleName); 136 | 137 | // a new authorization strategy should have been set 138 | AuthorizationStrategy b = j.jenkins.getAuthorizationStrategy(); 139 | assertTrue(b instanceof FolderBasedAuthorizationStrategy); 140 | assertNotSame("A new instance of FolderBasedAuthorizationStrategy should have been set.", a, b); 141 | FolderBasedAuthorizationStrategy newStrategy = (FolderBasedAuthorizationStrategy) b; 142 | GlobalRole role = newStrategy.getGlobalRoles().stream().filter(r -> r.getName().equals(adminRoleName)) 143 | .findAny().orElseThrow(() -> new RuntimeException("The admin role should exist")); 144 | assertFalse(role.getSids().contains("user1")); 145 | } 146 | 147 | @Test 148 | public void removeSidFromFolderRole() { 149 | String sid = "user1"; 150 | FolderRole role = new FolderRole("foo", wrapPermissions(Item.READ), singleton("folderFoo")); 151 | assertEquals(0, role.getSids().size()); 152 | FolderAuthorizationStrategyAPI.addFolderRole(role); 153 | FolderAuthorizationStrategyAPI.assignSidToFolderRole(sid, "foo"); 154 | FolderAuthorizationStrategyAPI.removeSidFromFolderRole(sid, "foo"); 155 | 156 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 157 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 158 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 159 | FolderRole updatedRole = strategy.getFolderRoles().stream().filter(r -> r.getName().equals("foo")) 160 | .findAny().orElseThrow(() -> new RuntimeException("The created role should exist")); 161 | assertFalse(updatedRole.getSids().contains(sid)); 162 | } 163 | 164 | @Test 165 | public void removeSidFromAgentRole() { 166 | String sid = "user1"; 167 | AgentRole role = new AgentRole("bar", wrapPermissions(Item.READ), singleton("agentBar")); 168 | assertEquals(0, role.getSids().size()); 169 | FolderAuthorizationStrategyAPI.addAgentRole(role); 170 | FolderAuthorizationStrategyAPI.assignSidToAgentRole(sid, "bar"); 171 | FolderAuthorizationStrategyAPI.removeSidFromAgentRole(sid, "bar"); 172 | 173 | AuthorizationStrategy a = j.jenkins.getAuthorizationStrategy(); 174 | assertTrue(a instanceof FolderBasedAuthorizationStrategy); 175 | FolderBasedAuthorizationStrategy strategy = (FolderBasedAuthorizationStrategy) a; 176 | AgentRole updatedRole = strategy.getAgentRoles().stream().filter(r -> r.getName().equals("bar")) 177 | .findAny().orElseThrow(() -> new RuntimeException("The created role should exist")); 178 | assertFalse(updatedRole.getSids().contains(sid)); 179 | } 180 | 181 | @Test(expected = IllegalArgumentException.class) 182 | public void shouldNotAllowDuplicateNamesInGlobalRoles() { 183 | // the "admin" role should already exist 184 | FolderAuthorizationStrategyAPI.addGlobalRole(new GlobalRole("admin", wrapPermissions(Permission.READ), 185 | emptySet())); 186 | } 187 | 188 | @Test(expected = IllegalArgumentException.class) 189 | public void shouldNotAllowDuplicateNamesInFolderRoles() { 190 | FolderAuthorizationStrategyAPI.addFolderRole(new FolderRole("baz", wrapPermissions(Item.READ), 191 | singleton("folder42"))); 192 | // different permissions shouldn't matter 193 | FolderAuthorizationStrategyAPI.addFolderRole(new FolderRole("baz", wrapPermissions(Item.CONFIGURE), 194 | singleton("folder42"))); 195 | } 196 | 197 | @Test(expected = IllegalArgumentException.class) 198 | public void shouldNotAllowDuplicateNamesInAgentRoles() { 199 | FolderAuthorizationStrategyAPI.addAgentRole(new AgentRole("baz", wrapPermissions(Computer.DELETE), 200 | singleton("agent42"))); 201 | // different agent names shouldn't matter 202 | FolderAuthorizationStrategyAPI.addAgentRole(new AgentRole("baz", wrapPermissions(Computer.CONFIGURE), 203 | singleton("agent43"))); 204 | } 205 | 206 | @Test(expected = IllegalArgumentException.class) 207 | public void shouldNotAllowBlankSidInGlobalRoles() { 208 | FolderAuthorizationStrategyAPI.assignSidToGlobalRole("", "admin"); 209 | } 210 | 211 | @Test(expected = IllegalArgumentException.class) 212 | public void shouldNotAllowBlankSidInFolderRoles() { 213 | FolderAuthorizationStrategyAPI.addFolderRole(new FolderRole("qwerty", wrapPermissions(Item.EXTENDED_READ), 214 | singleton("sampleFolder"))); 215 | FolderAuthorizationStrategyAPI.assignSidToFolderRole(" \t", "qwerty"); 216 | } 217 | 218 | @Test(expected = IllegalArgumentException.class) 219 | public void shouldNotAllowBlankSidInAgentRoles() { 220 | FolderAuthorizationStrategyAPI.addFolderRole(new FolderRole("foo", wrapPermissions(Item.EXTENDED_READ), 221 | singleton("sampleAgent"))); 222 | FolderAuthorizationStrategyAPI.assignSidToFolderRole("\t\t \t", "foo"); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/FolderBasedAuthorizationStrategy.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | import hudson.Extension; 6 | import hudson.model.AbstractItem; 7 | import hudson.model.Computer; 8 | import hudson.model.Descriptor; 9 | import hudson.model.Job; 10 | import hudson.security.ACL; 11 | import hudson.security.AuthorizationStrategy; 12 | import hudson.security.Permission; 13 | import hudson.security.PermissionGroup; 14 | import hudson.security.SidACL; 15 | import io.jenkins.plugins.folderauth.acls.GenericAclImpl; 16 | import io.jenkins.plugins.folderauth.acls.GlobalAclImpl; 17 | import io.jenkins.plugins.folderauth.misc.PermissionWrapper; 18 | import io.jenkins.plugins.folderauth.roles.AbstractRole; 19 | import io.jenkins.plugins.folderauth.roles.AgentRole; 20 | import io.jenkins.plugins.folderauth.roles.FolderRole; 21 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 22 | import jenkins.model.Jenkins; 23 | import net.sf.json.JSONObject; 24 | import org.acegisecurity.acls.sid.PrincipalSid; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | import org.kohsuke.stapler.StaplerRequest; 27 | 28 | import javax.annotation.Nonnull; 29 | import javax.annotation.Nullable; 30 | import javax.annotation.ParametersAreNonnullByDefault; 31 | import java.util.Collection; 32 | import java.util.Collections; 33 | import java.util.HashSet; 34 | import java.util.Set; 35 | import java.util.concurrent.ConcurrentHashMap; 36 | import java.util.concurrent.TimeUnit; 37 | import java.util.stream.Collectors; 38 | 39 | /** 40 | * An {@link AuthorizationStrategy} that controls access to {@link com.cloudbees.hudson.plugins.folder.AbstractFolder}s 41 | * through {@link FolderRole}s, to {@link Computer}s through {@link AgentRole}s. Also provides global permissions 42 | * through {@link GlobalRole}s. 43 | *

44 | * All objects of this class are immutable. To modify the data for this strategy, 45 | * please use the {@link FolderAuthorizationStrategyAPI}. 46 | * 47 | * @see FolderAuthorizationStrategyAPI for modifying the roles 48 | * @see FolderAuthorizationStrategyManagementLink for REST API methods 49 | */ 50 | @ParametersAreNonnullByDefault 51 | public class FolderBasedAuthorizationStrategy extends AuthorizationStrategy { 52 | private static final String ADMIN_ROLE_NAME = "admin"; 53 | private static final String FOLDER_SEPARATOR = "/"; 54 | 55 | private final Set agentRoles; 56 | private final Set globalRoles; 57 | private final Set folderRoles; 58 | 59 | /** 60 | * An {@link ACL} that works only on {@link #globalRoles}. 61 | *

62 | * All other {@link ACL} should inherit from this {@link ACL}. 63 | */ 64 | private transient GlobalAclImpl globalAcl; 65 | /** 66 | * Maps full name of jobs to their respective {@link ACL}s. The {@link ACL}s here do not 67 | * get inheritance from their parents. 68 | */ 69 | private transient ConcurrentHashMap jobAcls; 70 | /** 71 | * Maps full name of the Agents to their respective {@link ACL}s. Inheritance is not needed here 72 | * because Agents are not nestable. 73 | */ 74 | private transient ConcurrentHashMap agentAcls; 75 | /** 76 | * Contains the ACLs for projects that do not need any further inheritance. 77 | *

78 | * Invalidate this cache whenever folder roles are updated. 79 | */ 80 | private transient Cache jobAclCache; 81 | 82 | @DataBoundConstructor 83 | public FolderBasedAuthorizationStrategy(Set globalRoles, Set folderRoles, 84 | Set agentRoles) { 85 | this.agentRoles = new HashSet<>(agentRoles); 86 | this.globalRoles = new HashSet<>(globalRoles); 87 | this.folderRoles = new HashSet<>(folderRoles); 88 | 89 | // the sets above should NOT be modified. They are not Collections.unmodifiableSet() 90 | // because that complicates the serialized XML and add unnecessary nesting. 91 | 92 | init(); 93 | } 94 | 95 | /** 96 | * Clears and recalculates {@code jobAcls}. 97 | */ 98 | private void updateJobAcls() { 99 | jobAcls.clear(); 100 | 101 | for (FolderRole role : folderRoles) { 102 | updateAclForFolderRole(role); 103 | } 104 | } 105 | 106 | private synchronized void updateAgentAcls() { 107 | agentAcls.clear(); 108 | 109 | for (AgentRole role : agentRoles) { 110 | updateAclForAgentRole(role); 111 | } 112 | } 113 | 114 | /** 115 | * {@inheritDoc} 116 | * 117 | * @return an {@link ACL} formed using just globalRoles 118 | */ 119 | @Nonnull 120 | @Override 121 | public GlobalAclImpl getRootACL() { 122 | return globalAcl; 123 | } 124 | 125 | /** 126 | * Used to initialize transient fields when loaded from disk 127 | * 128 | * @return {@code this} 129 | */ 130 | @Nonnull 131 | @SuppressWarnings("unused") 132 | private FolderBasedAuthorizationStrategy readResolve() { 133 | init(); 134 | return this; 135 | } 136 | 137 | /** 138 | * Gets the {@link ACL} for a {@link Job} 139 | * 140 | * @return the {@link ACL} for the {@link Job} 141 | */ 142 | @Nonnull 143 | @Override 144 | public SidACL getACL(Job project) { 145 | return getACL((AbstractItem) project); 146 | } 147 | 148 | /** 149 | * {@inheritDoc} 150 | */ 151 | @Nonnull 152 | @Override 153 | public SidACL getACL(AbstractItem item) { 154 | String fullName = item.getFullName(); 155 | SidACL acl = jobAclCache.getIfPresent(fullName); 156 | 157 | if (acl != null) { 158 | return acl; 159 | } 160 | 161 | String[] splits = fullName.split(FOLDER_SEPARATOR); 162 | StringBuilder sb = new StringBuilder(fullName.length()); 163 | acl = globalAcl; 164 | 165 | // Roles on a folder are applicable to all children 166 | for (String str : splits) { 167 | sb.append(str); 168 | SidACL newAcl = jobAcls.get(sb.toString()); 169 | if (newAcl != null) { 170 | acl = acl.newInheritingACL(newAcl); 171 | } 172 | sb.append(FOLDER_SEPARATOR); 173 | } 174 | 175 | jobAclCache.put(fullName, acl); 176 | return acl; 177 | } 178 | 179 | /** 180 | * {@inheritDoc} 181 | */ 182 | @Nonnull 183 | @Override 184 | public SidACL getACL(@Nonnull Computer computer) { 185 | String name = computer.getName(); 186 | SidACL acl = agentAcls.get(name); 187 | if (acl == null) { 188 | return globalAcl; 189 | } else { 190 | // TODO: cache these ACLs 191 | return globalAcl.newInheritingACL(acl); 192 | } 193 | } 194 | 195 | /** 196 | * {@inheritDoc} 197 | */ 198 | @Nonnull 199 | @Override 200 | public Collection getGroups() { 201 | Set groups = ConcurrentHashMap.newKeySet(); 202 | agentRoles.stream().parallel().map(AbstractRole::getSids).forEach(groups::addAll); 203 | globalRoles.stream().parallel().map(AbstractRole::getSids).forEach(groups::addAll); 204 | folderRoles.stream().parallel().map(AbstractRole::getSids).forEach(groups::addAll); 205 | return Collections.unmodifiableCollection(groups); 206 | } 207 | 208 | /** 209 | * Returns the {@link GlobalRole}s on which this {@link AuthorizationStrategy} works. 210 | * 211 | * @return set of {@link GlobalRole}s on which this {@link AuthorizationStrategy} works. 212 | */ 213 | @Nonnull 214 | public Set getGlobalRoles() { 215 | return Collections.unmodifiableSet(globalRoles); 216 | } 217 | 218 | /** 219 | * Returns the {@link AgentRole}s on which this {@link AuthorizationStrategy} works. 220 | * 221 | * @return set of {@link AgentRole}s on which this {@link AuthorizationStrategy} works. 222 | */ 223 | @Nonnull 224 | public Set getAgentRoles() { 225 | return Collections.unmodifiableSet(agentRoles); 226 | } 227 | 228 | /** 229 | * Returns the {@link FolderRole}s on which this {@link AuthorizationStrategy} works. 230 | * 231 | * @return {@link FolderRole}s on which this {@link AuthorizationStrategy} works 232 | */ 233 | @Nonnull 234 | public Set getFolderRoles() { 235 | return Collections.unmodifiableSet(folderRoles); 236 | } 237 | 238 | /** 239 | * Updates the ACL for the folder role 240 | *

241 | * Note: does not invalidate the cache 242 | *

243 | * Should be called when a folderRole has been updated. 244 | * 245 | * @param role the role to be updated 246 | */ 247 | private void updateAclForFolderRole(FolderRole role) { 248 | for (String name : role.getFolderNames()) { 249 | updateGenericAcl(name, jobAcls, role); 250 | } 251 | } 252 | 253 | /** 254 | * Updates the ACL for the agent role 255 | *

256 | * Note: does not invalidate the cache 257 | *

258 | * Should be called when an agentRole has been updated. 259 | * 260 | * @param role the role to be updated 261 | */ 262 | private void updateAclForAgentRole(AgentRole role) { 263 | for (String agent : role.getAgents()) { 264 | updateGenericAcl(agent, agentAcls, role); 265 | } 266 | } 267 | 268 | private void updateGenericAcl(String fullName, ConcurrentHashMap acls, AbstractRole role) { 269 | GenericAclImpl acl = acls.get(fullName); 270 | if (acl == null) { 271 | acl = new GenericAclImpl(); 272 | } 273 | acl.assignPermissions(role.getSids(), 274 | role.getPermissionsUnsorted().stream().map(PermissionWrapper::getPermission).collect(Collectors.toSet())); 275 | acls.put(fullName, acl); 276 | } 277 | 278 | /** 279 | * Initializes the cache, generates ACLs and makes the {@link FolderBasedAuthorizationStrategy} 280 | * ready to work. 281 | */ 282 | private void init() { 283 | jobAcls = new ConcurrentHashMap<>(); 284 | agentAcls = new ConcurrentHashMap<>(); 285 | 286 | jobAclCache = CacheBuilder.newBuilder() 287 | .expireAfterWrite(1, TimeUnit.HOURS) 288 | .maximumSize(2048) 289 | .build(); 290 | 291 | globalAcl = new GlobalAclImpl(globalRoles); 292 | updateJobAcls(); 293 | updateAgentAcls(); 294 | } 295 | 296 | @Extension 297 | public static class DescriptorImpl extends Descriptor { 298 | @Nonnull 299 | @Override 300 | public String getDisplayName() { 301 | return Messages.FolderBasedAuthorizationStrategy_DisplayName(); 302 | } 303 | 304 | @Nonnull 305 | @Override 306 | public FolderBasedAuthorizationStrategy newInstance(@Nullable StaplerRequest req, @Nonnull JSONObject formData) { 307 | AuthorizationStrategy strategy = Jenkins.get().getAuthorizationStrategy(); 308 | if (strategy instanceof FolderBasedAuthorizationStrategy) { 309 | // this action was invoked from the 'Configure Global Security' page when the 310 | // old strategy was FolderBasedAuthorizationStrategy; return it back as formData would be empty 311 | return (FolderBasedAuthorizationStrategy) strategy; 312 | } else { 313 | // when this AuthorizationStrategy is selected for the first time, this makes the current 314 | // user admin (give all permissions) and prevents him/her from getting access denied. 315 | // The same thing happens in Role Strategy plugin. See RoleBasedStrategy.DESCRIPTOR.newInstance() 316 | 317 | HashSet groups = new HashSet<>(PermissionGroup.getAll()); 318 | groups.remove(PermissionGroup.get(Permission.class)); 319 | Set adminPermissions = PermissionWrapper.wrapPermissions( 320 | FolderAuthorizationStrategyManagementLink.getSafePermissions(groups)); 321 | 322 | GlobalRole adminRole = new GlobalRole(ADMIN_ROLE_NAME, adminPermissions, 323 | Collections.singleton(new PrincipalSid(Jenkins.getAuthentication()).getPrincipal())); 324 | 325 | return new FolderBasedAuthorizationStrategy(Collections.singleton(adminRole), Collections.emptySet(), 326 | Collections.emptySet()); 327 | } 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/folderauth/FolderAuthorizationStrategyAPI.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.folderauth; 2 | 3 | import hudson.security.AuthorizationStrategy; 4 | import io.jenkins.plugins.folderauth.roles.AgentRole; 5 | import io.jenkins.plugins.folderauth.roles.FolderRole; 6 | import io.jenkins.plugins.folderauth.roles.GlobalRole; 7 | import jenkins.model.Jenkins; 8 | import org.apache.commons.lang.StringUtils; 9 | 10 | import javax.annotation.ParametersAreNonnullByDefault; 11 | import java.util.HashSet; 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import java.util.function.Consumer; 15 | import java.util.function.Function; 16 | 17 | /** 18 | * Public-facing methods for modifying {@link FolderBasedAuthorizationStrategy}. 19 | *

20 | * These methods should only be called when {@link Jenkins#getAuthorizationStrategy()}} is 21 | * {@link FolderBasedAuthorizationStrategy}. This class does not provide REST API methods. 22 | * 23 | * @see FolderAuthorizationStrategyManagementLink for REST API methods. 24 | */ 25 | @ParametersAreNonnullByDefault 26 | @SuppressWarnings("WeakerAccess") 27 | public class FolderAuthorizationStrategyAPI { 28 | 29 | private FolderAuthorizationStrategyAPI() { 30 | } 31 | 32 | /** 33 | * Checks the {@link AuthorizationStrategy} and runs the {@link Consumer} when it is an instance of 34 | * {@link FolderBasedAuthorizationStrategy}. 35 | *

36 | * All attempts to access the {@link FolderBasedAuthorizationStrategy} must go through this method 37 | * for thread-safety. 38 | * 39 | * @param runner a function that consumes the current {@link FolderBasedAuthorizationStrategy} and returns a non 40 | * null {@link FolderBasedAuthorizationStrategy} object. The object may be the same as the one 41 | * consumed if no modification was needed. 42 | * @throws IllegalStateException when {@link Jenkins#getAuthorizationStrategy()} is not 43 | * {@link FolderBasedAuthorizationStrategy} 44 | */ 45 | private synchronized static void run(Function runner) { 46 | Jenkins jenkins = Jenkins.get(); 47 | AuthorizationStrategy strategy = jenkins.getAuthorizationStrategy(); 48 | if (strategy instanceof FolderBasedAuthorizationStrategy) { 49 | FolderBasedAuthorizationStrategy newStrategy = runner.apply((FolderBasedAuthorizationStrategy) strategy); 50 | jenkins.setAuthorizationStrategy(newStrategy); 51 | } else { 52 | throw new IllegalStateException("FolderBasedAuthorizationStrategy is not the" + " current authorization strategy"); 53 | } 54 | } 55 | 56 | /** 57 | * Adds a {@link GlobalRole} to the {@link FolderBasedAuthorizationStrategy}. 58 | * 59 | * @param role the role to be added. 60 | * @throws IllegalArgumentException when a role with the given name already exists. 61 | */ 62 | public static void addGlobalRole(GlobalRole role) { 63 | run(strategy -> { 64 | Set globalRoles = new HashSet<>(strategy.getGlobalRoles()); 65 | String name = role.getName(); 66 | Optional existing = globalRoles.stream().filter(r -> r.getName().equals(name)).findAny(); 67 | if (existing.isPresent()) { 68 | throw new IllegalArgumentException("A global role with the name \"" + name + "\" already exists."); 69 | } 70 | globalRoles.add(role); 71 | return new FolderBasedAuthorizationStrategy(globalRoles, strategy.getFolderRoles(), strategy.getAgentRoles()); 72 | }); 73 | } 74 | 75 | /** 76 | * Adds a {@link FolderRole} to the {@link FolderBasedAuthorizationStrategy}. 77 | * 78 | * @param role the role to be added. 79 | * @throws IllegalArgumentException when a role with the given name already exists. 80 | */ 81 | public static void addFolderRole(FolderRole role) { 82 | run(strategy -> { 83 | Set folderRoles = new HashSet<>(strategy.getFolderRoles()); 84 | String name = role.getName(); 85 | Optional existing = folderRoles.stream().filter(r -> r.getName().equals(name)).findAny(); 86 | if (existing.isPresent()) { 87 | throw new IllegalArgumentException("A folder role with the name \"" + name + "\" already exists."); 88 | } 89 | folderRoles.add(role); 90 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), folderRoles, strategy.getAgentRoles()); 91 | }); 92 | } 93 | 94 | /** 95 | * Adds an {@link AgentRole} to the {@link FolderBasedAuthorizationStrategy}. 96 | * 97 | * @param role the role to be added. 98 | * @throws IllegalArgumentException when a role with the given name already exists. 99 | */ 100 | public static void addAgentRole(AgentRole role) { 101 | run(strategy -> { 102 | Set agentRoles = new HashSet<>(strategy.getAgentRoles()); 103 | String name = role.getName(); 104 | Optional existing = agentRoles.stream().filter(r -> r.getName().equals(name)).findAny(); 105 | if (existing.isPresent()) { 106 | throw new IllegalArgumentException("An agent role with the name \"" + name + "\" already exists."); 107 | } 108 | agentRoles.add(role); 109 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), strategy.getFolderRoles(), agentRoles); 110 | }); 111 | } 112 | 113 | /** 114 | * Assigns the {@code sid} to the {@link GlobalRole} identified by {@code roleName}. 115 | * 116 | * @param sid this sid will be assigned to the global role with the name equal to {@code roleName}. 117 | * @param roleName the name of the global role 118 | * @throws IllegalArgumentException when no global role with name equal to {@code roleName} exists 119 | * @throws IllegalArgumentException when the {@code sid} is empty 120 | */ 121 | public static void assignSidToGlobalRole(String sid, String roleName) { 122 | if (StringUtils.isBlank(sid)) { 123 | throw new IllegalArgumentException("Sid should not be blank."); 124 | } 125 | 126 | run(strategy -> { 127 | Set globalRoles = new HashSet<>(strategy.getGlobalRoles()); 128 | GlobalRole role = globalRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 129 | () -> new IllegalArgumentException("No global role with name = \"" + roleName + "\" exists")); 130 | HashSet newSids = new HashSet<>(role.getSids()); 131 | newSids.add(sid); 132 | globalRoles.remove(role); 133 | globalRoles.add(new GlobalRole(role.getName(), role.getPermissionsUnsorted(), newSids)); 134 | return new FolderBasedAuthorizationStrategy(globalRoles, strategy.getFolderRoles(), strategy.getAgentRoles()); 135 | }); 136 | } 137 | 138 | /** 139 | * Assigns the {@code sid} to the {@link AgentRole} identified by {@code roleName}. 140 | * 141 | * @param sid this sid will be assigned to the {@link AgentRole} with the name equal to {@code roleName}. 142 | * @param roleName the name of the agent role 143 | * @throws IllegalArgumentException when no agent role with name equal to {@code roleName} exists 144 | * @throws IllegalArgumentException when the {@code sid} is empty 145 | */ 146 | public static void assignSidToAgentRole(String sid, String roleName) { 147 | if (StringUtils.isBlank(sid)) { 148 | throw new IllegalArgumentException("Sid should not be blank."); 149 | } 150 | 151 | run(strategy -> { 152 | Set agentRoles = new HashSet<>(strategy.getAgentRoles()); 153 | AgentRole role = agentRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 154 | () -> new IllegalArgumentException("No agent role with name = \"" + roleName + "\" exists")); 155 | HashSet newSids = new HashSet<>(role.getSids()); 156 | newSids.add(sid); 157 | agentRoles.remove(role); 158 | agentRoles.add(new AgentRole(role.getName(), role.getPermissionsUnsorted(), role.getAgents(), newSids)); 159 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), strategy.getFolderRoles(), agentRoles); 160 | }); 161 | } 162 | 163 | /** 164 | * Assigns the {@code sid} to the {@link FolderRole} identified by {@code roleName}. 165 | * 166 | * @param sid this sid will be assigned to the {@link FolderRole} with the name equal to {@code roleName}. 167 | * @param roleName the name of the folder role 168 | * @throws IllegalArgumentException when no folder role with name equal to {@code roleName} exists 169 | * @throws IllegalArgumentException when the {@code sid} is empty 170 | */ 171 | public static void assignSidToFolderRole(String sid, String roleName) { 172 | if (StringUtils.isBlank(sid)) { 173 | throw new IllegalArgumentException("Sid should not be blank."); 174 | } 175 | 176 | run(strategy -> { 177 | Set folderRoles = new HashSet<>(strategy.getFolderRoles()); 178 | FolderRole role = folderRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 179 | () -> new IllegalArgumentException("No folder role with name = \"" + roleName + "\" exists")); 180 | HashSet newSids = new HashSet<>(role.getSids()); 181 | newSids.add(sid); 182 | folderRoles.remove(role); 183 | folderRoles.add(new FolderRole(role.getName(), role.getPermissionsUnsorted(), role.getFolderNames(), newSids)); 184 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), folderRoles, strategy.getAgentRoles()); 185 | }); 186 | } 187 | 188 | /** 189 | * Deletes the {@link GlobalRole} with name equal to {@code roleName}. 190 | * 191 | * @param roleName the name of the role to be deleted 192 | * @throws IllegalArgumentException when no global role with name equal to {@code roleName} exists 193 | */ 194 | public static void deleteGlobalRole(String roleName) { 195 | if (roleName.equals("admin")) { 196 | throw new IllegalArgumentException("Cannot delete the admin role."); 197 | } 198 | 199 | run(strategy -> { 200 | Set globalRoles = new HashSet<>(strategy.getGlobalRoles()); 201 | GlobalRole role = globalRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 202 | () -> new IllegalArgumentException("No global role with name = \"" + roleName + "\" exists")); 203 | globalRoles.remove(role); 204 | return new FolderBasedAuthorizationStrategy(globalRoles, strategy.getFolderRoles(), strategy.getAgentRoles()); 205 | }); 206 | } 207 | 208 | /** 209 | * Deletes the {@link FolderRole} with name equal to {@code roleName}. 210 | * 211 | * @param roleName the name of the role to be deleted 212 | * @throws IllegalArgumentException when no role with name equal to {@code roleName} exists 213 | */ 214 | public static void deleteFolderRole(String roleName) { 215 | run(strategy -> { 216 | Set folderRoles = new HashSet<>(strategy.getFolderRoles()); 217 | FolderRole role = folderRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 218 | () -> new IllegalArgumentException("No folder role with name = \"" + roleName + "\" exists")); 219 | folderRoles.remove(role); 220 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), folderRoles, strategy.getAgentRoles()); 221 | }); 222 | } 223 | 224 | /** 225 | * Deletes the {@link AgentRole} with name equal to {@code roleName}. 226 | * 227 | * @param roleName the name of the role to be deleted 228 | * @throws IllegalArgumentException when no role with name equal to {@code roleName} exists 229 | */ 230 | public static void deleteAgentRole(String roleName) { 231 | run(strategy -> { 232 | Set agentRoles = new HashSet<>(strategy.getAgentRoles()); 233 | AgentRole role = agentRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 234 | () -> new IllegalArgumentException("No agent role with name = \"" + roleName + "\" exists")); 235 | agentRoles.remove(role); 236 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), strategy.getFolderRoles(), agentRoles); 237 | }); 238 | } 239 | 240 | /** 241 | * Removes the {@code sid} from the {@link GlobalRole} with name equal to @{code roleName}. 242 | * 243 | * @param roleName the name of the role. 244 | * @param sid the sid that will be removed. 245 | * @throws IllegalArgumentException when no {@link GlobalRole} with the given {@code roleName} exists. 246 | * @since TODO 247 | */ 248 | public static void removeSidFromGlobalRole(String sid, String roleName) { 249 | run(strategy -> { 250 | Set globalRoles = new HashSet<>(strategy.getGlobalRoles()); 251 | GlobalRole role = globalRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 252 | () -> new IllegalArgumentException("No global role with name equal to \"" + roleName + "\" exists.") 253 | ); 254 | Set sids = new HashSet<>(role.getSids()); 255 | sids.remove(sid); 256 | globalRoles.remove(role); 257 | globalRoles.add(new GlobalRole(role.getName(), role.getPermissions(), sids)); 258 | return new FolderBasedAuthorizationStrategy(globalRoles, strategy.getFolderRoles(), strategy.getAgentRoles()); 259 | }); 260 | } 261 | 262 | /** 263 | * Removes the {@code sid} from the {@link FolderRole} with name equal to @{code roleName}. 264 | * 265 | * @param roleName the name of the role. 266 | * @param sid the sid that will be removed. 267 | * @throws IllegalArgumentException when no {@link FolderRole} with the given {@code roleName} exists. 268 | * @since TODO 269 | */ 270 | public static void removeSidFromFolderRole(String sid, String roleName) { 271 | run(strategy -> { 272 | Set folderRoles = new HashSet<>(strategy.getFolderRoles()); 273 | FolderRole role = folderRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 274 | () -> new IllegalArgumentException("No folder role with name equal to \"" + roleName + "\" exists.") 275 | ); 276 | Set sids = new HashSet<>(role.getSids()); 277 | sids.remove(sid); 278 | folderRoles.remove(role); 279 | folderRoles.add(new FolderRole(role.getName(), role.getPermissions(), role.getFolderNames(), sids)); 280 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), folderRoles, strategy.getAgentRoles()); 281 | }); 282 | } 283 | 284 | /** 285 | * Removes the {@code sid} from the {@link AgentRole} with name equal to @{code roleName}. 286 | * 287 | * @param roleName the name of the role. 288 | * @param sid the sid that will be removed. 289 | * @throws IllegalArgumentException when no {@link AgentRole} with the given {@code roleName} exists. 290 | * @since TODO 291 | */ 292 | public static void removeSidFromAgentRole(String sid, String roleName) { 293 | run(strategy -> { 294 | Set agentRoles = new HashSet<>(strategy.getAgentRoles()); 295 | AgentRole role = agentRoles.stream().filter(r -> r.getName().equals(roleName)).findAny().orElseThrow( 296 | () -> new IllegalArgumentException("No agent role with name equal to \"" + roleName + "\" exists.") 297 | ); 298 | Set sids = new HashSet<>(role.getSids()); 299 | sids.remove(sid); 300 | agentRoles.remove(role); 301 | agentRoles.add(new AgentRole(role.getName(), role.getPermissions(), role.getAgents(), sids)); 302 | return new FolderBasedAuthorizationStrategy(strategy.getGlobalRoles(), strategy.getFolderRoles(), agentRoles); 303 | }); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/folderauth/FolderAuthorizationStrategyManagementLink/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 174 | 177 |

178 | ${%loadingFolders} 179 |
180 | 183 | 184 | 187 | 194 |
195 | 199 |
200 | 201 | 202 | 203 |
204 | 205 |
206 |

207 | ${%manageAgentRoles} 208 |

209 | 210 | 211 |

212 | ${%emptyAgentRoles} 213 |

214 |
215 | 216 |

217 | ${%currentAgentRoles} 218 |

219 |
220 | 221 |
222 | 223 |
224 | ${%name}: ${agentRole.name} 225 |
226 | ${%sids}: ${agentRole.getSidsCommaSeparated()} 227 |
228 | ${%agents}: ${agentRole.getAgentNamesCommaSeparated()} 229 |
230 |
231 |
232 | 235 | 236 |
237 |
238 | 242 | 246 |
247 |
248 | 249 |
250 |
    251 | 252 |
  • 253 | ${wrapper.permission.group.title}/${wrapper.permission.name} 254 |
  • 255 |
    256 |
257 |
258 |
260 | 261 | 262 |
263 |
264 |
265 |
266 |
267 |
268 | 269 |

270 | ${%addAgentRole} 271 |

272 |
273 |
274 | 278 |
279 |
280 | 283 | 288 |
289 | 292 | 299 |
300 | 301 |
302 |
303 |
304 | 305 |