diff --git a/pom.xml b/pom.xml index cea843eb..49074724 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 3.1.11 -SNAPSHOT jenkinsci/${project.artifactId}-plugin - 3.0 + 3.2 2.387.3 false diff --git a/src/main/java/com/cloudbees/hudson/plugins/folder/properties/AuthorizationMatrixProperty.java b/src/main/java/com/cloudbees/hudson/plugins/folder/properties/AuthorizationMatrixProperty.java index f3734e2a..075b8111 100644 --- a/src/main/java/com/cloudbees/hudson/plugins/folder/properties/AuthorizationMatrixProperty.java +++ b/src/main/java/com/cloudbees/hudson/plugins/folder/properties/AuthorizationMatrixProperty.java @@ -109,12 +109,13 @@ public AuthorizationMatrixProperty(Map> grante } } - @DataBoundConstructor // JENKINS-49199: Used for job-dsl - @Restricted(NoExternalUse.class) - public AuthorizationMatrixProperty(List permissions) { - for (String permission : permissions) { - add(permission); - } + /** + * Exists for reflective Job DSL / Pipeline use only. + */ + @DataBoundConstructor + @Restricted(DoNotUse.class) + public AuthorizationMatrixProperty(List entries) { + setEntries(entries); } @Override @@ -143,6 +144,13 @@ protected void setOwner(@NonNull AbstractFolder owner) { FolderContributor.record(owner); } + @Override + @Restricted(DoNotUse.class) + public List getEntries() { + // ReflectionUtils#getPublicProperty / PropertyDescriptor#getPropertyDescriptor can't find default methods + return AuthorizationProperty.super.getEntries(); + } + @Extension(optional = true) @Symbol("authorizationMatrix") public static class DescriptorImpl extends AbstractFolderPropertyDescriptor diff --git a/src/main/java/hudson/security/AuthorizationMatrixProperty.java b/src/main/java/hudson/security/AuthorizationMatrixProperty.java index a4637cf4..0770d416 100644 --- a/src/main/java/hudson/security/AuthorizationMatrixProperty.java +++ b/src/main/java/hudson/security/AuthorizationMatrixProperty.java @@ -134,13 +134,13 @@ public AuthorizationMatrixProperty(Map> grantedPermissio } } + /** + * Exists for reflective Job DSL / Pipeline use only. + */ + @Restricted(DoNotUse.class) @DataBoundConstructor - public AuthorizationMatrixProperty(List permissions) { - for (String str : permissions) { - if (str != null) { - this.add(str); - } - } + public AuthorizationMatrixProperty(List entries) { + setEntries(entries); } /** @@ -182,6 +182,13 @@ public Permission getEditingPermission() { return Item.CONFIGURE; } + @Override + @Restricted(DoNotUse.class) + public List getEntries() { + // ReflectionUtils#getPublicProperty / PropertyDescriptor#getPropertyDescriptor can't find default methods + return AuthorizationProperty.super.getEntries(); + } + @Extension @Symbol("authorizationMatrix") public static class DescriptorImpl extends JobPropertyDescriptor diff --git a/src/main/java/org/jenkinsci/plugins/matrixauth/AuthorizationProperty.java b/src/main/java/org/jenkinsci/plugins/matrixauth/AuthorizationProperty.java index f53a975a..2ad66bd9 100644 --- a/src/main/java/org/jenkinsci/plugins/matrixauth/AuthorizationProperty.java +++ b/src/main/java/org/jenkinsci/plugins/matrixauth/AuthorizationProperty.java @@ -23,11 +23,29 @@ */ package org.jenkinsci.plugins.matrixauth; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.security.Permission; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; import org.jenkinsci.plugins.matrixauth.inheritance.InheritGlobalStrategy; import org.jenkinsci.plugins.matrixauth.inheritance.InheritanceStrategy; import org.jenkinsci.plugins.matrixauth.inheritance.NonInheritingStrategy; +import org.jenkinsci.plugins.matrixauth.integrations.PermissionFinder; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; @Restricted(NoExternalUse.class) public interface AuthorizationProperty extends AuthorizationContainer { @@ -72,4 +90,181 @@ default void setBlocksInheritance(boolean blocksInheritance) { default boolean isBlocksInheritance() { return getInheritanceStrategy() instanceof NonInheritingStrategy; } + + /** + * Set entries from DSL in Job DSL or Pipeline plugins. + * + * @param entries list of entries to use for permission assignment + */ + @Restricted(DoNotUse.class) + default void setEntries(List entries) { + for (DslEntry entry : entries) { + entry.addPermission(this); + } + } + + /** + * Getter supporting nicer DSL syntax for Job DSL and Pipeline job property definitions. + * @return a list of {@link DslEntry} + */ + default List getEntries() { + final Map> mapping = new HashMap<>(); + getGrantedPermissionEntries() + .forEach((permission, value) -> + value.forEach(sid -> mapping.computeIfAbsent(sid, unused -> new TreeSet<>()) + .add(permission.group.getId() + "/" + permission.name))); + return mapping.entrySet().stream() + .map(e -> { + final PermissionEntry key = e.getKey(); + if (key.getType() == AuthorizationType.USER) { + return new DslUser(key.getSid(), new ArrayList<>(e.getValue())); + } + if (key.getType() == AuthorizationType.GROUP) { + return new DslGroup(key.getSid(), new ArrayList<>(e.getValue())); + } + if (key.getType() == AuthorizationType.EITHER) { + return new DslUserOrGroup(key.getSid(), new ArrayList<>(e.getValue())); + } + throw new IllegalStateException("Got unexpected key type in: " + key); + }) + .distinct() + .sorted() + .collect(Collectors.toList()); + } + + /** + * Common superclass for {@link DslUser}, {@link DslGroup}, and {@link DslUserOrGroup}, supporting nicer DSLs + * for Job DSL and Pipeline Job definitions/reconfigurations. + * Job DSL and Pipeline use this for {@link hudson.security.AuthorizationMatrixProperty}. + * Job DSL additionally uses this for {@link com.cloudbees.hudson.plugins.folder.properties.AuthorizationMatrixProperty}. + */ + @Restricted(NoExternalUse.class) + abstract class DslEntry implements Describable, Comparable { + private final String name; + private final List permissions; + + /** + * + * @param name the sid of the DSL entity + * @param permissions the list of string-typed permissions + */ + public DslEntry(String name, List permissions) { + this.name = name; + this.permissions = permissions; + } + + public String getName() { + return name; + } + + public List getPermissions() { + return permissions; + } + + @Override + public Descriptor getDescriptor() { + return Jenkins.get().getDescriptorOrDie(getClass()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DslEntry that = (DslEntry) o; + return Objects.equals(name, that.name) && Objects.equals(permissions, that.permissions); + } + + @Override + public int hashCode() { + return Objects.hash(name, permissions, getClass()); + } + + @Override + public int compareTo(@NonNull DslEntry that) { + if (this.getClass() != that.getClass()) { + return this.getClass().getName().compareTo(that.getClass().getName()); + } + if (!this.name.equals(that.name)) { + return this.name.compareTo(that.name); + } + return this.permissions.size() - that.permissions.size(); + } + + public abstract void addPermission(AuthorizationProperty authorizationProperty); + + protected static Permission findPermission(String value) { + final Permission permission = Permission.fromId(value); + if (permission != null) { + return permission; + } + return PermissionFinder.findPermission(value); + } + } + + /** + * Represents a user being assigned permissions. + */ + @Restricted(NoExternalUse.class) + class DslUser extends DslEntry { + @DataBoundConstructor + public DslUser(String name, List permissions) { + super(name, permissions); + } + + @Override + public void addPermission(AuthorizationProperty authorizationProperty) { + getPermissions() + .forEach(permission -> + authorizationProperty.add(findPermission(permission), PermissionEntry.user(getName()))); + } + + @Extension + @Symbol("user") + public static class DescriptorImpl extends Descriptor {} + } + + /** + * Represents a group being assigned permissions. + */ + @Restricted(NoExternalUse.class) + class DslGroup extends DslEntry { + @DataBoundConstructor + public DslGroup(String name, List permissions) { + super(name, permissions); + } + + @Override + public void addPermission(AuthorizationProperty authorizationProperty) { + getPermissions() + .forEach(permission -> + authorizationProperty.add(findPermission(permission), PermissionEntry.group(getName()))); + } + + @Extension + @Symbol("group") + public static class DescriptorImpl extends Descriptor {} + } + + /** + * Represents a user or group being assigned permissions. + * Generally discouraged "ambiguous" permission assignments. + */ + @Restricted(NoExternalUse.class) + class DslUserOrGroup extends DslEntry { + @DataBoundConstructor + public DslUserOrGroup(String name, List permissions) { + super(name, permissions); + } + + @Override + public void addPermission(AuthorizationProperty authorizationProperty) { + getPermissions() + .forEach(permission -> authorizationProperty.add( + findPermission(permission), new PermissionEntry(AuthorizationType.EITHER, getName()))); + } + + @Extension + @Symbol("userOrGroup") + public static class DescriptorImpl extends Descriptor {} + } } diff --git a/src/test/java/hudson/security/AuthorizationMatrixPropertyTest.java b/src/test/java/hudson/security/AuthorizationMatrixPropertyTest.java index b7a4955a..9b4df5d4 100644 --- a/src/test/java/hudson/security/AuthorizationMatrixPropertyTest.java +++ b/src/test/java/hudson/security/AuthorizationMatrixPropertyTest.java @@ -40,8 +40,7 @@ public void testSnippetizer() throws Exception { SnippetizerTester tester = new SnippetizerTester(j); tester.assertRoundTrip( new JobPropertyStep(Collections.singletonList(property)), - "properties([authorizationMatrix(inheritanceStrategy: nonInheriting(), " - + "permissions: ['hudson.model.Item.Configure:alice', 'hudson.model.Item.Read:alice', 'hudson.model.Item.Read:bob', 'hudson.scm.SCM.Tag:bob'])])"); + "properties([authorizationMatrix(entries: [userOrGroup(name: 'alice', permissions: ['Job/Configure', 'Job/Read']), userOrGroup(name: 'bob', permissions: ['Job/Read', 'SCM/Tag'])], inheritanceStrategy: nonInheriting())])"); } @Test @@ -56,8 +55,7 @@ public void testSnippetizer2() throws Exception { SnippetizerTester tester = new SnippetizerTester(j); tester.assertRoundTrip( new JobPropertyStep(Collections.singletonList(property)), - "properties([authorizationMatrix(inheritanceStrategy: nonInheriting(), " - + "permissions: ['USER:hudson.model.Item.Configure:alice', 'USER:hudson.model.Item.Read:alice', 'USER:hudson.model.Item.Read:bob', 'USER:hudson.scm.SCM.Tag:bob'])])"); + "properties([authorizationMatrix(entries: [user(name: 'alice', permissions: ['Job/Configure', 'Job/Read']), user(name: 'bob', permissions: ['Job/Read', 'SCM/Tag'])], inheritanceStrategy: nonInheriting())])"); } @Test @@ -79,8 +77,7 @@ public void testSnippetizerInapplicablePermission() throws Exception { SnippetizerTester tester = new SnippetizerTester(j); tester.assertRoundTrip( new JobPropertyStep(Collections.singletonList(property)), - "properties([authorizationMatrix(inheritanceStrategy: nonInheriting(), " - + "permissions: ['hudson.model.Item.Configure:alice', 'hudson.model.Item.Read:alice', 'hudson.model.Item.Read:bob', 'hudson.scm.SCM.Tag:bob'])])"); + "properties([authorizationMatrix(entries: [userOrGroup(name: 'alice', permissions: ['Job/Configure', 'Job/Read']), userOrGroup(name: 'bob', permissions: ['Job/Read', 'SCM/Tag'])], inheritanceStrategy: nonInheriting())])"); Assert.assertTrue(l.getMessages().stream() .anyMatch(m -> m.contains("Tried to add inapplicable permission") @@ -126,16 +123,16 @@ public void testPipelineReconfiguration() throws Exception { project.setDefinition(new CpsFlowDefinition( "properties([authorizationMatrix(inheritanceStrategy: nonInheriting(), " - + "permissions: ['hudson.model.Item.Read:bob', 'hudson.model.Item.Configure:bob', 'hudson.scm.SCM.Tag:bob'])])", + + "entries: [user(name: 'bob', permissions: ['Job/Read', 'Job/Configure', 'hudson.scm.SCM.Tag'])])])", true)); j.buildAndAssertSuccess(project); // let's look ast the property AuthorizationMatrixProperty property = project.getProperty(AuthorizationMatrixProperty.class); Assert.assertTrue(property.getInheritanceStrategy() instanceof NonInheritingStrategy); - Assert.assertEquals(3, property.getGrantedPermissions().size()); + Assert.assertEquals(0, property.getGrantedPermissions().size()); // Unambiguous permissions are 0 Assert.assertEquals(3, property.getGrantedPermissionEntries().size()); - Assert.assertEquals("bob", property.getGroups().toArray()[0]); + Assert.assertEquals(0, property.getGroups().size()); // now bob has access, including configure j.createWebClient().login("bob").goTo(project.getUrl()); @@ -179,7 +176,7 @@ public void testPipelineReconfiguration2() throws Exception { project.setDefinition(new CpsFlowDefinition( "properties([authorizationMatrix(inheritanceStrategy: nonInheriting(), " - + "permissions: ['USER:hudson.model.Item.Read:bob', 'USER:hudson.model.Item.Configure:bob', 'USER:hudson.scm.SCM.Tag:bob'])])", + + "entries: [user(name: 'bob', permissions: ['Job/Read', 'Job/Configure', 'hudson.scm.SCM.Tag'])])])", true)); j.buildAndAssertSuccess(project); diff --git a/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2-ambiguous.yml b/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2-ambiguous.yml index 4b9a2d0f..42011782 100644 --- a/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2-ambiguous.yml +++ b/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2-ambiguous.yml @@ -62,12 +62,12 @@ jobs: inheritanceStrategy { nonInheriting() } - permissions([ - 'Job/Build:authenticated', - 'Job/Configure:authenticated', - 'Job/Delete:authenticated', - 'Job/Read:authenticated', - ]) + entries { + group { + name('authenticated') + permissions([ 'Job/Build', 'Job/Configure', 'Job/Delete', 'Job/Read' ]) + } + } } } } \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2.yml b/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2.yml index 2633cb80..24f5910a 100644 --- a/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2.yml +++ b/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v2.yml @@ -62,12 +62,12 @@ jobs: inheritanceStrategy { nonInheriting() } - permissions([ - 'GROUP:Job/Build:authenticated', - 'GROUP:Job/Configure:authenticated', - 'GROUP:Job/Delete:authenticated', - 'GROUP:Job/Read:authenticated', - ]) + entries { + group { + name('authenticated') + permissions([ 'Job/Build', 'Job/Configure', 'Job/Delete', 'Job/Read' ]) + } + } } } } \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v3.yml b/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v3.yml index 2420505f..db8a9a75 100644 --- a/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v3.yml +++ b/src/test/resources/org/jenkinsci/plugins/matrixauth/integrations/casc/configuration-as-code-v3.yml @@ -82,13 +82,12 @@ jobs: inheritanceStrategy { nonInheriting() } - /* TODO Adapt to https://github.com/jenkinsci/matrix-auth-plugin/pull/144 once that's merged */ - permissions([ - 'GROUP:Job/Build:authenticated', - 'GROUP:Job/Configure:authenticated', - 'GROUP:Job/Delete:authenticated', - 'GROUP:Job/Read:authenticated', - ]) + entries { + group { + name('authenticated') + permissions([ 'Job/Build', 'Job/Configure', 'Job/Delete', 'Job/Read' ]) + } + } } } } \ No newline at end of file