├── .github ├── CODEOWNERS ├── renovate.json └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .mvn ├── maven.config └── extensions.xml ├── .vscode └── settings.json ├── docs └── images │ ├── Capture_d’écran_2012-08-21_à_15.03.47.png │ └── Capture_d’écran_2012-08-21_à_15.08.01.png ├── Jenkinsfile ├── LICENSE ├── src ├── test │ └── java │ │ └── com │ │ └── cloudbees │ │ └── jenkins │ │ └── plugins │ │ ├── AdditionalIdentityTest.java │ │ ├── AdditionalIdentitiesTest.java │ │ ├── AdditionalIdentitiesIntegrationTest.java │ │ └── AdditionalIdentityResolverTest.java └── main │ ├── resources │ ├── com │ │ └── cloudbees │ │ │ └── jenkins │ │ │ └── plugins │ │ │ └── AdditionalIdentities │ │ │ ├── help-id.html │ │ │ ├── help-realm.html │ │ │ └── config.jelly │ └── index.jelly │ └── java │ └── com │ └── cloudbees │ └── jenkins │ └── plugins │ ├── AdditionalIdentity.java │ ├── AdditionalIdentities.java │ └── AdditionalIdentityResolver.java ├── README.md ├── .gitignore └── pom.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/additional-identities-plugin-developers 2 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic", 3 | "java.compile.nullAnalysis.mode": "automatic" 4 | } 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>visualon/renovate-config//jenkins.json"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/images/Capture_d’écran_2012-08-21_à_15.03.47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/additional-identities-plugin/main/docs/images/Capture_d’écran_2012-08-21_à_15.03.47.png -------------------------------------------------------------------------------- /docs/images/Capture_d’écran_2012-08-21_à_15.08.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/additional-identities-plugin/main/docs/images/Capture_d’écran_2012-08-21_à_15.08.01.png -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | useContainerAgent: true, 7 | configurations: [ 8 | [platform: 'linux', jdk: 25], 9 | [platform: 'windows', jdk: 21], 10 | ]) 11 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.13 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | permissions: 11 | checks: read 12 | contents: write 13 | 14 | jobs: 15 | maven-cd: 16 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@820822d414ad889e8d46a311a6706758d570d4de # v1.8.0 17 | secrets: 18 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 19 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@7ced5457323763df99467bb8546e7a30ce4ae5a5 # v2.2.1 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012, CloudBees, Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/java/com/cloudbees/jenkins/plugins/AdditionalIdentityTest.java: -------------------------------------------------------------------------------- 1 | package com.cloudbees.jenkins.plugins; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class AdditionalIdentityTest { 10 | 11 | @Test 12 | @DisplayName("Should create an AdditionalIdentity with the correct values") 13 | void testConstructor() { 14 | // Given 15 | var id = "user123"; 16 | var realm = "github"; 17 | 18 | // When 19 | var identity = new AdditionalIdentity(id, realm); 20 | 21 | // Then 22 | assertNotNull(identity); 23 | assertEquals(id, identity.getId()); 24 | assertEquals(realm, identity.getRealm()); 25 | } 26 | 27 | @Test 28 | @DisplayName("Descriptor should have correct display name") 29 | void testDescriptor() { 30 | // Given 31 | var descriptor = new AdditionalIdentity.DescriptorImpl(); 32 | 33 | // When 34 | var displayName = descriptor.getDisplayName(); 35 | 36 | // Then 37 | assertEquals("Additional identity", displayName); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/com/cloudbees/jenkins/plugins/AdditionalIdentities/help-id.html: -------------------------------------------------------------------------------- 1 | 24 |

25 | User identify as defined by external service, typically SCM ID. 26 |

27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/com/cloudbees/jenkins/plugins/AdditionalIdentities/help-realm.html: -------------------------------------------------------------------------------- 1 | 24 |

25 | Optional realm this identity applies to. Keep empty for global identity. For SCM identity, 26 | the repository domain name is used, assuming the SCM plugin handles realm. 27 |

28 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 | Additional user property for user identity on other systems. Jenkins then won't create duplicated users when such 27 | an identity is extracted during SCM changelog parsing, or other user-related events. 28 |
29 | -------------------------------------------------------------------------------- /src/main/resources/com/cloudbees/jenkins/plugins/AdditionalIdentities/config.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 |
-------------------------------------------------------------------------------- /src/main/java/com/cloudbees/jenkins/plugins/AdditionalIdentity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012, CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package com.cloudbees.jenkins.plugins; 26 | 27 | import hudson.Extension; 28 | import hudson.model.AbstractDescribableImpl; 29 | import hudson.model.Descriptor; 30 | import org.kohsuke.stapler.DataBoundConstructor; 31 | 32 | /** 33 | * @author Nicolas De Loof 34 | */ 35 | public class AdditionalIdentity extends AbstractDescribableImpl { 36 | 37 | final String id; 38 | 39 | final String realm; 40 | 41 | @DataBoundConstructor 42 | public AdditionalIdentity(String id, String realm) { 43 | this.id = id; 44 | this.realm = realm; 45 | } 46 | 47 | public String getId() { 48 | return id; 49 | } 50 | 51 | public String getRealm() { 52 | return realm; 53 | } 54 | 55 | @Extension 56 | public static class DescriptorImpl extends Descriptor { 57 | 58 | @Override 59 | public String getDisplayName() { 60 | return "Additional identity"; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/cloudbees/jenkins/plugins/AdditionalIdentities.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012, CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.cloudbees.jenkins.plugins; 25 | 26 | import hudson.Extension; 27 | import hudson.model.User; 28 | import hudson.model.UserProperty; 29 | import hudson.model.UserPropertyDescriptor; 30 | import java.util.List; 31 | import org.kohsuke.stapler.DataBoundConstructor; 32 | 33 | /** 34 | * @author Nicolas De Loof 35 | */ 36 | public class AdditionalIdentities extends UserProperty { 37 | 38 | private final List identities; 39 | 40 | @DataBoundConstructor 41 | public AdditionalIdentities(List identities) { 42 | this.identities = identities; 43 | } 44 | 45 | public List getIdentities() { 46 | return identities; 47 | } 48 | 49 | @Extension 50 | public static class DescriptorImpl extends UserPropertyDescriptor { 51 | 52 | @Override 53 | public UserProperty newInstance(User user) { 54 | return null; 55 | } 56 | 57 | @Override 58 | public String getDisplayName() { 59 | return "Additional user identities"; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/cloudbees/jenkins/plugins/AdditionalIdentitiesTest.java: -------------------------------------------------------------------------------- 1 | package com.cloudbees.jenkins.plugins; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | import static org.mockito.Mockito.mock; 7 | 8 | import hudson.model.User; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import org.junit.jupiter.api.DisplayName; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class AdditionalIdentitiesTest { 16 | 17 | @Test 18 | @DisplayName("Should create AdditionalIdentities with the correct list of identities") 19 | void testConstructor() { 20 | // Given 21 | var identity1 = new AdditionalIdentity("user123", "github"); 22 | var identity2 = new AdditionalIdentity("user456", "gitlab"); 23 | var identities = Arrays.asList(identity1, identity2); 24 | 25 | // When 26 | var additionalIdentities = new AdditionalIdentities(identities); 27 | 28 | // Then 29 | assertNotNull(additionalIdentities); 30 | assertNotNull(additionalIdentities.getIdentities()); 31 | assertEquals(2, additionalIdentities.getIdentities().size()); 32 | assertEquals(identity1, additionalIdentities.getIdentities().get(0)); 33 | assertEquals(identity2, additionalIdentities.getIdentities().get(1)); 34 | } 35 | 36 | @Test 37 | @DisplayName("Should handle empty list of identities") 38 | void testConstructorWithEmptyList() { 39 | // Given 40 | List identities = new ArrayList<>(); 41 | 42 | // When 43 | var additionalIdentities = new AdditionalIdentities(identities); 44 | 45 | // Then 46 | assertNotNull(additionalIdentities); 47 | assertNotNull(additionalIdentities.getIdentities()); 48 | assertEquals(0, additionalIdentities.getIdentities().size()); 49 | } 50 | 51 | @Test 52 | @DisplayName("Descriptor should have correct display name and return null for new instance") 53 | void testDescriptor() { 54 | // Given 55 | var descriptor = new AdditionalIdentities.DescriptorImpl(); 56 | var user = mock(User.class); 57 | 58 | // When 59 | var displayName = descriptor.getDisplayName(); 60 | var newInstance = descriptor.newInstance(user); 61 | 62 | // Then 63 | assertEquals("Additional user identities", displayName); 64 | assertNull(newInstance); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/cloudbees/jenkins/plugins/AdditionalIdentityResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012, CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package com.cloudbees.jenkins.plugins; 26 | 27 | import static hudson.init.InitMilestone.PLUGINS_STARTED; 28 | 29 | import hudson.Extension; 30 | import hudson.init.Initializer; 31 | import hudson.model.User; 32 | import java.util.Map; 33 | 34 | /** 35 | * @author Nicolas De Loof 36 | */ 37 | @Extension 38 | public class AdditionalIdentityResolver extends User.CanonicalIdResolver { 39 | 40 | @Override 41 | public String resolveCanonicalId(String id, Map context) { 42 | 43 | String realm = null; 44 | 45 | if (context != null) { 46 | realm = (String) context.get(User.CanonicalIdResolver.REALM); 47 | } 48 | 49 | for (User user : User.getAll()) { 50 | var identities = user.getProperty(AdditionalIdentities.class); 51 | if (identities == null) continue; 52 | for (AdditionalIdentity identity : identities.getIdentities()) { 53 | if (identity.id.equals(id)) { 54 | if (realm != null && identity.realm != null && !realm.contains(identity.realm)) { 55 | // realm don't match 56 | continue; 57 | } 58 | return user.getId(); 59 | } 60 | } 61 | } 62 | return null; 63 | } 64 | 65 | @Initializer(before = PLUGINS_STARTED) 66 | public static void addAliases() { 67 | User.XSTREAM.addCompatibilityAlias( 68 | "com.cloudbees.jenkins.plugins.AdditionalItentity", AdditionalIdentity.class); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Additional Identities Plugin for Jenkins 2 | 3 | [![Build Status](https://ci.jenkins.io/job/Plugins/job/additional-identities-plugin/job/main/badge/icon)](https://ci.jenkins.io/job/Plugins/job/additional-identities-plugin/job/main) 4 | [![Coverage](https://ci.jenkins.io/job/Plugins/job/additional-identities-plugin/job/main/badge/icon?status=${instructionCoverage}&subject=coverage&color=${colorInstructionCoverage})](https://ci.jenkins.io/job/Plugins/job/additional-identities-plugin/job/main/coverage) 5 | [![Version](https://img.shields.io/jenkins/plugin/v/additional-identities-plugin.svg)](https://plugins.jenkins.io/additional-identities-plugin) 6 | [![Changelog](https://img.shields.io/github/release/jenkinsci/additional-identities-plugin.svg?label=changelog)](https://github.com/jenkinsci/additional-identities-plugin/releases/latest) 7 | [![Installs](https://img.shields.io/jenkins/plugin/i/additional-identities-plugin.svg?color=blue)](https://plugins.jenkins.io/additional-identities-plugin) 8 | 9 | This plugin allows user to configure additional identities in jenkins 10 | user database for external services, typically SCM repositories. 11 | 12 | ## Introduction 13 | 14 | As SCM plugin parse changelog, they have to map SCM committers IDs with 15 | jenkins user database. This results in many cases in user duplication, 16 | until you exactly have the same ID in jenkins and SCM. 17 | 18 | Jenkins 1.480 introduces an extension point to resolve jenkins user 19 | "canonical" ID when searching for user in Database by id or full name. 20 | This plugin uses this extension point to let user configure external 21 | identities as user properties. 22 | 23 | ## Usage 24 | 25 | On my Jenkins instance, I'm authenticated as "nicolas". As I want to use 26 | the same identity for commits on repositories, I can setup an additional 27 | identity for my account on googlecode : 28 | ![Configuration page](docs/images/Capture_d’écran_2012-08-21_à_15.03.47.png) 29 | 30 | With this additional identity set, Jenkins will be able to match the 31 | committer id in svn "" with the jenkins user 32 | "nicolas", and link the builds I contributed to in my user view : 33 | 34 | ![Matched user](docs/images/Capture_d’écran_2012-08-21_à_15.08.01.png) 35 | 36 | ## Realms 37 | 38 | As for HTTP authentication, a realm can be set to restrict an identity 39 | to a set of network resources (i.e. domain names in most cases). The 40 | *realm* attribute can be used to restrict the sources user ID matching 41 | will apply. In most cases, this is a substring of the SCM repository 42 | URL. If not set, additional identity applies to all user lookups, 43 | whatever the id source is. 44 | 45 | This feature requires SCM plugins to be updated so that they compute 46 | host information form changelog and pass it to extension as 47 | [REALM](http://javadoc.jenkins-ci.org/hudson/model/User.CanonicalIdResolver.html#REALM) context 48 | attribute. Those plugins have been updated to support this advanced 49 | feature : 50 | 51 | - TO BE COMPLETED 52 | 53 | ## Tips 54 | 55 | git plugin uses user name, as set in git commit, as committer ID, so you 56 | don't need an additional identity, just ensure your git client is 57 | configured with correct user name set : 58 | 59 | ``` syntaxhighlighter-pre 60 | git config --global user.name "Your Full Name Comes Here" 61 | ``` 62 | 63 | ## Changelog 64 | 65 | See GitHub [Releases](https://github.com/jenkinsci/additional-identities-plugin/releases) for newer changes. 66 | 67 | ### 1.1 (Oct 20 2015) 68 | 69 | - [JENKINS-28181](https://issues.jenkins-ci.org/browse/JENKINS-28181) 70 | NPE thrown in certain cases. 71 | - Internal class rename. 72 | - Missing descriptor error. 73 | 74 | ### 1.0 75 | 76 | - initial release 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | 217 | #InteliJIdea 218 | target/ 219 | .idea/ 220 | *.iml -------------------------------------------------------------------------------- /src/test/java/com/cloudbees/jenkins/plugins/AdditionalIdentitiesIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.cloudbees.jenkins.plugins; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | 6 | import hudson.model.User; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.DisplayName; 12 | import org.junit.jupiter.api.Test; 13 | import org.jvnet.hudson.test.JenkinsRule; 14 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 15 | 16 | @WithJenkins 17 | class AdditionalIdentitiesIntegrationTest { 18 | 19 | private JenkinsRule jenkins; 20 | 21 | @BeforeEach 22 | void setUp(JenkinsRule jenkins) { 23 | this.jenkins = jenkins; 24 | } 25 | 26 | @Test 27 | @DisplayName("Integration test for adding and resolving additional identities") 28 | void testAddingAndResolvingIdentities() throws Exception { 29 | // Given 30 | var userId = "test-user"; 31 | var identityId = "github-user"; 32 | var realm = "github"; 33 | 34 | // Create user 35 | var user = User.getById(userId, true); 36 | assertNotNull(user); 37 | 38 | // Add additional identity to the user 39 | var identity = new AdditionalIdentity(identityId, realm); 40 | var additionalIdentities = new AdditionalIdentities(Collections.singletonList(identity)); 41 | user.addProperty(additionalIdentities); 42 | user.save(); 43 | 44 | // When we try to resolve the identity 45 | var resolver = new AdditionalIdentityResolver(); 46 | var resolvedId = resolver.resolveCanonicalId(identityId, null); 47 | 48 | // Then the resolved id should match the user id 49 | assertEquals(userId, resolvedId); 50 | } 51 | 52 | @Test 53 | @DisplayName("Integration test for resolving identity with realm check") 54 | void testResolvingIdentityWithRealm() throws Exception { 55 | // Given 56 | var userId = "test-user-realm"; 57 | var identityId = "github-user-realm"; 58 | var realm = "github"; 59 | 60 | // Create user 61 | var user = User.getById(userId, true); 62 | assertNotNull(user); 63 | 64 | // Add additional identity to the user 65 | var identity = new AdditionalIdentity(identityId, realm); 66 | var additionalIdentities = new AdditionalIdentities(Collections.singletonList(identity)); 67 | user.addProperty(additionalIdentities); 68 | user.save(); 69 | 70 | // When we try to resolve the identity with matching realm 71 | var resolver = new AdditionalIdentityResolver(); 72 | Map context = new HashMap<>(); 73 | context.put(User.CanonicalIdResolver.REALM, "jenkins-" + realm); 74 | var resolvedId = resolver.resolveCanonicalId(identityId, context); 75 | 76 | // Then the resolved id should match the user id 77 | assertEquals(userId, resolvedId); 78 | } 79 | 80 | @Test 81 | @DisplayName("Integration test for configuration via JCasC") 82 | void testConfigurationAsCode() throws Exception { 83 | // Given a user with additional identities 84 | var userId = "jcasc-user"; 85 | var identityId = "jcasc-github-user"; 86 | var realm = "github"; 87 | 88 | // Create user 89 | var user = User.getById(userId, true); 90 | assertNotNull(user); 91 | 92 | // Add additional identity to the user 93 | var identity = new AdditionalIdentity(identityId, realm); 94 | var additionalIdentities = new AdditionalIdentities(Collections.singletonList(identity)); 95 | user.addProperty(additionalIdentities); 96 | user.save(); 97 | 98 | // When and Then 99 | var userFromJenkins = User.getById(userId, false); 100 | assertNotNull(userFromJenkins); 101 | 102 | var storedIdentities = userFromJenkins.getProperty(AdditionalIdentities.class); 103 | assertNotNull(storedIdentities); 104 | assertEquals(1, storedIdentities.getIdentities().size()); 105 | assertEquals(identityId, storedIdentities.getIdentities().get(0).getId()); 106 | assertEquals(realm, storedIdentities.getIdentities().get(0).getRealm()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 4.0.0 27 | 28 | org.jenkins-ci.plugins 29 | plugin 30 | 5.28 31 | 32 | 33 | 34 | com.cloudbees.jenkins.plugins 35 | additional-identities-plugin 36 | ${revision}.${changelist} 37 | hpi 38 | 39 | Additional Identities Plugin 40 | https://github.com/jenkinsci/${project.artifactId} 41 | 42 | 43 | 44 | MIT 45 | https://opensource.org/license/mit 46 | repo 47 | 48 | 49 | 50 | 51 | scm:git:https://github.com/jenkinsci/${project.artifactId}.git 52 | scm:git:git@github.com:jenkinsci/${project.artifactId}.git 53 | ${scmTag} 54 | https://github.com/jenkinsci/${project.artifactId} 55 | 56 | 57 | 58 | 59 | 182 60 | 999999-SNAPSHOT 61 | 2.528 62 | ${jenkins.baseline}.3 63 | false 64 | false 65 | true 66 | 67 | 68 | 69 | 70 | 71 | 72 | io.jenkins.tools.bom 73 | bom-${jenkins.baseline}.x 74 | 5804.v80587a_38d937 75 | pom 76 | import 77 | 78 | 79 | 80 | 81 | 82 | 83 | io.jenkins 84 | configuration-as-code 85 | test 86 | 87 | 88 | io.jenkins.configuration-as-code 89 | test-harness 90 | test 91 | 92 | 93 | org.mockito 94 | mockito-core 95 | test 96 | 97 | 98 | org.mockito 99 | mockito-junit-jupiter 100 | test 101 | 102 | 103 | 104 | 105 | 106 | repo.jenkins-ci.org 107 | https://repo.jenkins-ci.org/public/ 108 | 109 | 110 | 111 | 112 | 113 | repo.jenkins-ci.org 114 | https://repo.jenkins-ci.org/public/ 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/test/java/com/cloudbees/jenkins/plugins/AdditionalIdentityResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.cloudbees.jenkins.plugins; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | import static org.mockito.Mockito.lenient; 6 | import static org.mockito.Mockito.mock; 7 | import static org.mockito.Mockito.when; 8 | 9 | import hudson.model.User; 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.DisplayName; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.mockito.MockedStatic; 18 | import org.mockito.Mockito; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | class AdditionalIdentityResolverTest { 23 | 24 | private AdditionalIdentityResolver resolver; 25 | 26 | @BeforeEach 27 | void setUp() { 28 | resolver = new AdditionalIdentityResolver(); 29 | } 30 | 31 | @Test 32 | @DisplayName("Should return null when no users are found") 33 | void testNoUsersFound() { 34 | try (MockedStatic userMock = Mockito.mockStatic(User.class)) { 35 | // Given 36 | userMock.when(User::getAll).thenReturn(Collections.emptyList()); 37 | 38 | // When 39 | var result = resolver.resolveCanonicalId("test-id", null); 40 | 41 | // Then 42 | assertNull(result); 43 | } 44 | } 45 | 46 | @Test 47 | @DisplayName("Should return null when user has no additional identities") 48 | void testUserWithNoAdditionalIdentities() { 49 | try (MockedStatic userMock = Mockito.mockStatic(User.class)) { 50 | // Given 51 | var user = mock(User.class); 52 | when(user.getProperty(AdditionalIdentities.class)).thenReturn(null); 53 | userMock.when(User::getAll).thenReturn(Collections.singletonList(user)); 54 | 55 | // When 56 | var result = resolver.resolveCanonicalId("test-id", null); 57 | 58 | // Then 59 | assertNull(result); 60 | } 61 | } 62 | 63 | @Test 64 | @DisplayName("Should return user ID when identity matches without realm check") 65 | void testMatchingIdentity() { 66 | try (MockedStatic userMock = Mockito.mockStatic(User.class)) { 67 | // Given 68 | var userId = "user123"; 69 | var identityId = "github-user"; 70 | 71 | var user = mock(User.class); 72 | var identity = new AdditionalIdentity(identityId, null); 73 | var identities = new AdditionalIdentities(Collections.singletonList(identity)); 74 | 75 | when(user.getProperty(AdditionalIdentities.class)).thenReturn(identities); 76 | when(user.getId()).thenReturn(userId); 77 | userMock.when(User::getAll).thenReturn(Collections.singletonList(user)); 78 | 79 | // When 80 | var result = resolver.resolveCanonicalId(identityId, null); 81 | 82 | // Then 83 | assertEquals(userId, result); 84 | } 85 | } 86 | 87 | @Test 88 | @DisplayName("Should not match when realms don't match") 89 | void testNonMatchingRealm() { 90 | try (MockedStatic userMock = Mockito.mockStatic(User.class)) { 91 | // Given 92 | var userId = "user123"; 93 | var identityId = "github-user"; 94 | var identityRealm = "github"; 95 | var contextRealm = "gitlab"; 96 | 97 | var user = mock(User.class); 98 | var identity = new AdditionalIdentity(identityId, identityRealm); 99 | var identities = new AdditionalIdentities(Collections.singletonList(identity)); 100 | 101 | // Use lenient() to avoid unnecessary stubbing exception 102 | lenient().when(user.getProperty(AdditionalIdentities.class)).thenReturn(identities); 103 | lenient().when(user.getId()).thenReturn(userId); 104 | userMock.when(User::getAll).thenReturn(Collections.singletonList(user)); 105 | 106 | Map context = new HashMap<>(); 107 | context.put(User.CanonicalIdResolver.REALM, contextRealm); 108 | 109 | // When 110 | var result = resolver.resolveCanonicalId(identityId, context); 111 | 112 | // Then 113 | assertNull(result); 114 | } 115 | } 116 | 117 | @Test 118 | @DisplayName("Should match when realms match") 119 | void testMatchingRealm() { 120 | try (MockedStatic userMock = Mockito.mockStatic(User.class)) { 121 | // Given 122 | var userId = "user123"; 123 | var identityId = "github-user"; 124 | var realm = "github"; 125 | 126 | var user = mock(User.class); 127 | var identity = new AdditionalIdentity(identityId, realm); 128 | var identities = new AdditionalIdentities(Collections.singletonList(identity)); 129 | 130 | when(user.getProperty(AdditionalIdentities.class)).thenReturn(identities); 131 | when(user.getId()).thenReturn(userId); 132 | userMock.when(User::getAll).thenReturn(Collections.singletonList(user)); 133 | 134 | Map context = new HashMap<>(); 135 | context.put(User.CanonicalIdResolver.REALM, "contains-" + realm + "-realm"); 136 | 137 | // When 138 | var result = resolver.resolveCanonicalId(identityId, context); 139 | 140 | // Then 141 | assertEquals(userId, result); 142 | } 143 | } 144 | } 145 | --------------------------------------------------------------------------------