├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .gitpod.yml ├── .mvn ├── extensions.xml ├── jvm.config └── maven.config ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── RELEASE.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ ├── GitHubOAuthScope.java │ │ ├── GitHubRepositoryName.java │ │ ├── GithubAccessTokenProperty.java │ │ ├── GithubAuthenticationToken.java │ │ ├── GithubAuthorizationStrategy.java │ │ ├── GithubLogoutAction.java │ │ ├── GithubOAuthGroupDetails.java │ │ ├── GithubOAuthUserDetails.java │ │ ├── GithubRequireOrganizationMembershipACL.java │ │ ├── GithubSecretStorage.java │ │ ├── GithubSecurityRealm.java │ │ └── JenkinsProxyAuthenticator.java ├── resources │ ├── index.jelly │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ ├── GithubAuthorizationStrategy │ │ └── config.jelly │ │ ├── GithubLogoutAction │ │ └── index.jelly │ │ └── GithubSecurityRealm │ │ └── config.jelly └── webapp │ ├── github.png │ └── help │ ├── auth │ ├── admin-user-names-help.html │ ├── agent-user-name-help.html │ ├── grant-create-job-to-authenticated-help.html │ ├── grant-read-to-anonymous-help.html │ ├── grant-read-to-authenticated-help.html │ ├── grant-read-to-cctray-help.html │ ├── grant-read-to-github-webhook-help.html │ ├── grant-viewstatus-to-anonymous-help.html │ ├── organization-names-help.html │ └── use-repository-permissions-help.html │ ├── help-authorization-strategy.html │ ├── help-security-realm.html │ └── realm │ ├── client-id-help.html │ ├── client-secret-help.html │ ├── github-api-uri-help.html │ ├── github-web-uri-help.html │ └── oauth-scopes-help.html └── test └── java └── org └── jenkinsci └── plugins ├── GithubAccessTokenPropertySEC797Test.java ├── GithubAccessTokenPropertyTest.java ├── GithubAuthenticationTokenTest.java ├── GithubAuthorizationStrategyTest.java ├── GithubLogoutActionTest.java ├── GithubRequireOrganizationMembershipACLTest.java ├── GithubSecretStorageTest.java ├── GithubSecurityRealmTest.java ├── JenkinsProxyAuthenticatorTest.java └── api └── GithubAPITest.java /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/github-oauth-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: maven 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /.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@v1 17 | secrets: 18 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 19 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | # Jenkins Security Scan 2 | # For more information, see: https://www.jenkins.io/doc/developer/security/scan/ 3 | 4 | name: Jenkins Security Scan 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | types: [opened, synchronize, reopened] 12 | workflow_dispatch: 13 | 14 | permissions: 15 | security-events: write 16 | contents: read 17 | actions: read 18 | 19 | jobs: 20 | security-scan: 21 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 22 | with: 23 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 24 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | .classpath 4 | .project 5 | .settings 6 | *.iml 7 | .idea 8 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: mvn clean verify 3 | 4 | vscode: 5 | extensions: 6 | - bierner.markdown-preview-github-styles 7 | - vscjava.vscode-java-pack 8 | - redhat.java 9 | - vscjava.vscode-java-debug 10 | - vscjava.vscode-java-dependency 11 | - vscjava.vscode-java-test 12 | - vscjava.vscode-maven 13 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | -Xmx256m -Djava.awt.headless=true 2 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 0.33 (Released Aug 5, 2019) 2 | 3 | - Bugfix configureSecurity HTTP 403 errors and CSRF crumb form validation issues 4 | tracked by [JENKINS-57154][JENKINS-57154]. (pull request [#115][#115]) 5 | - Bugfix Reference GitHub teams by slug tracked by 6 | [JENKINS-34835][JENKINS-34835]. (pull request [#116][#116]) 7 | 8 | [#115]: https://github.com/jenkinsci/github-oauth-plugin/pull/115 9 | [#116]: https://github.com/jenkinsci/github-oauth-plugin/pull/116 10 | [JENKINS-34835]: https://issues.jenkins-ci.org/browse/JENKINS-34835 11 | [JENKINS-57154]: https://issues.jenkins-ci.org/browse/JENKINS-57154 12 | 13 | # Version 0.32 (Released Apr 12, 2019) 14 | 15 | - Refactored to make fewer GitHub v3 API calls. (pull request [#106][#106]) 16 | - CSRF protection bugfix by using state parameter (pull request [#107][#107]) 17 | - Use the correct access token when impersonating a user (pull request 18 | [#109][#109]) 19 | 20 | [#106]: https://github.com/jenkinsci/github-oauth-plugin/pull/106 21 | [#107]: https://github.com/jenkinsci/github-oauth-plugin/pull/107 22 | [#108]: https://github.com/jenkinsci/github-oauth-plugin/pull/108 23 | [#109]: https://github.com/jenkinsci/github-oauth-plugin/pull/109 24 | 25 | # Version 0.31 (Released Dec 6, 2018) 26 | 27 | - Bugfix GitHub Committer Authorization Strategy bug introduced by Jenkins 2.146 28 | security release tracked by [JENKINS-54031][JENKINS-54031]. (pull request 29 | [#103][#103]) 30 | - Enabled Cache for User Teams. (pull request [#100][#100]) 31 | - Authenticated team members have read/build permissions when using GitHub 32 | Committer Authorization Strategy tracked by [JENKINS-42509][JENKINS-42509]. 33 | (pull request [#91][#91]) 34 | 35 | [#100]: https://github.com/jenkinsci/github-oauth-plugin/pull/100 36 | [#103]: https://github.com/jenkinsci/github-oauth-plugin/pull/103 37 | [#91]: https://github.com/jenkinsci/github-oauth-plugin/pull/91 38 | [JENKINS-42509]: https://issues.jenkins-ci.org/browse/JENKINS-42509 39 | [JENKINS-54031]: https://issues.jenkins-ci.org/browse/JENKINS-42509 40 | 41 | # Version 0.30 42 | 43 | - [SECURITY-602] Mask client secret in UI - the round-trip is now done in 44 | encrypted format 45 | - [SECURITY-797] Prevent session fixation - by the invalidation of the session 46 | after a successful login 47 | - [SECURITY-798] Prevent open redirect. Use the "from" in priority as it is 48 | managed directly inside the main layout. Otherwise, fallback to the referer 49 | header value. In all cases, check the URL is either relative or inside 50 | Jenkins. 51 | 52 | # Version 0.29 (Released Jan 22, 2018) 53 | 54 | - New feature: When users authorize OAuth apps from GitHub the token is now 55 | stored in a user property. This will allow Jenkins admins to provide tigher 56 | integration with GitHub on the user's behalf. Use case: Job DSL scripts which 57 | configures webhooks for user projects. This is tracked by 58 | [JENKINS-47113][JENKINS-47113]. (pull request [#87][#87]) 59 | - Significant performance improvement when visiting user pages when rendering 60 | GitHub organizations and teams. It now uses the built-in cache. (pull 61 | request [#92][#92]) 62 | - Bugfix rendering GitHub teams on user pages tracked by 63 | [JENKINS-42421][JENKINS-42421]. (pull request [#92][#92]) 64 | - Grammar and typo fixes. (pull request [#89][#89]) 65 | 66 | [#87]: https://github.com/jenkinsci/github-oauth-plugin/pull/87 67 | [#89]: https://github.com/jenkinsci/github-oauth-plugin/pull/89 68 | [#92]: https://github.com/jenkinsci/github-oauth-plugin/pull/92 69 | [JENKINS-42421]: https://issues.jenkins-ci.org/browse/JENKINS-42421 70 | [JENKINS-47113]: https://issues.jenkins-ci.org/browse/JENKINS-47113 71 | 72 | # Version 0.28.1 (Released Nov 2, 2017) 73 | 74 | - Fix a botched release. 0.28 was not released to Artifactory so this is 75 | another attempt. 76 | 77 | # Version 0.28 (Released Oct 1, 2017) 78 | 79 | - Corrected a connectivity error on auth with proxy tracked by 80 | [JENKINS-45726][JENKINS-45726]. (pull request [#85][#85]) 81 | 82 | [#85]: https://github.com/jenkinsci/github-oauth-plugin/pull/85 83 | [JENKINS-45726]: https://issues.jenkins-ci.org/browse/JENKINS-45726 84 | 85 | # Version 0.27 (Released May 1, 2017) 86 | 87 | - Allow collaborators to cancel/abort a build tracked by 88 | [JENKINS-40566][JENKINS-40566]. (pull request [#81][#81]) 89 | - Bugfix breaking SSH key authentication and transport authentication in Jenkins 90 | CLI tracked by [JENKINS-43822][JENKINS-43822]. (pull request [#83][#83]) 91 | 92 | [#81]: https://github.com/jenkinsci/github-oauth-plugin/pull/81 93 | [#83]: https://github.com/jenkinsci/github-oauth-plugin/pull/83 94 | [JENKINS-40566]: https://issues.jenkins-ci.org/browse/JENKINS-40566 95 | [JENKINS-43822]: https://issues.jenkins-ci.org/browse/JENKINS-43822 96 | 97 | # Version 0.26 (Released Apr 21, 2017) 98 | 99 | - Bugfix Fix for NPE in `GithubOAuthUserDetails.getAuthorities()`. (pull request 100 | [#76][#76]) 101 | - Bugfix [JENKINS-27045][JENKINS-27045] Jenkins CLI --username/--password 102 | options. (pull request [#77][#77]) 103 | - Bugfix [JENKINS-38096][JENKINS-38096] add in authorization checks for 104 | multibranch workflow jobs. (pull request [#78][#78]) 105 | 106 | [#76]: https://github.com/jenkinsci/github-oauth-plugin/pull/76 107 | [#77]: https://github.com/jenkinsci/github-oauth-plugin/pull/77 108 | [#78]: https://github.com/jenkinsci/github-oauth-plugin/pull/78 109 | [JENKINS-27045]: https://issues.jenkins-ci.org/browse/JENKINS-27045 110 | [JENKINS-38096]: https://issues.jenkins-ci.org/browse/JENKINS-38096 111 | 112 | # Version 0.25 (Released Dec 3, 2016) 113 | 114 | - Security improvement: Added support for SSL server name indication. (pull 115 | request [#59][#59]) 116 | - Security improvement: release over HTTPS. (pull request [#67][#67]) 117 | - Performance enhancement: Fixes github client rate limitor waits and eats web 118 | threads causing Jenkins to be unresponsive tracked by 119 | [JENKINS-39200][JENKINS-39200]. (pull request [#63][#63]) 120 | - Performance enhancement: cache user lookups from GitHub. (pull requests 121 | [#64][#64], [#65][#65], [#71][#71], [#72][#72], [#73][#73]) 122 | - Bugfix skip searching users when searching for teams tracked by 123 | [JENKINS-34896][JENKINS-34896] (pull request [#68][#68]) 124 | - Bugfix logout/login process tracked by [JENKINS-16350][JENKINS-16350]. (pull 125 | request [#58][#58]) 126 | - Bugfix building plugin with JDK7 and JDK8. (pull request [#73][#73]) 127 | - General bug fixes and code cleanup. (pull requests [#61][#61], [#62][#62], 128 | [#66][#66], [#69][#69], [#70][#70]) 129 | 130 | [#58]: https://github.com/jenkinsci/github-oauth-plugin/pull/58 131 | [#59]: https://github.com/jenkinsci/github-oauth-plugin/pull/59 132 | [#61]: https://github.com/jenkinsci/github-oauth-plugin/pull/61 133 | [#62]: https://github.com/jenkinsci/github-oauth-plugin/pull/62 134 | [#63]: https://github.com/jenkinsci/github-oauth-plugin/pull/63 135 | [#64]: https://github.com/jenkinsci/github-oauth-plugin/pull/64 136 | [#65]: https://github.com/jenkinsci/github-oauth-plugin/pull/65 137 | [#66]: https://github.com/jenkinsci/github-oauth-plugin/pull/66 138 | [#67]: https://github.com/jenkinsci/github-oauth-plugin/pull/67 139 | [#68]: https://github.com/jenkinsci/github-oauth-plugin/pull/68 140 | [#69]: https://github.com/jenkinsci/github-oauth-plugin/pull/69 141 | [#70]: https://github.com/jenkinsci/github-oauth-plugin/pull/70 142 | [#71]: https://github.com/jenkinsci/github-oauth-plugin/pull/71 143 | [#72]: https://github.com/jenkinsci/github-oauth-plugin/pull/72 144 | [#73]: https://github.com/jenkinsci/github-oauth-plugin/pull/73 145 | [JENKINS-16350]: https://issues.jenkins-ci.org/browse/JENKINS-16350 146 | [JENKINS-34896]: https://issues.jenkins-ci.org/browse/JENKINS-34896 147 | [JENKINS-39200]: https://issues.jenkins-ci.org/browse/JENKINS-39200 148 | 149 | # Version 0.24 (Released May 26, 2016) 150 | 151 | - Bugfix [JENKINS-34775][JENKINS-34775] Don't cast inconvertible un/pw token. 152 | ([pull request #56][#56]) 153 | - Bugfix [JENKINS-33883][JENKINS-33883] by allowing `.*/cc.xml` instead of only 154 | root one. ([pull request #51][#52]) 155 | - Bugfix loading orgs as groups when orgs contain no teams. ([pull request 156 | #54][#54]) 157 | - Correct spelling of GitHub and committer. (pull requests [#53][#53] and 158 | [#55][#55]) 159 | 160 | [#52]: https://github.com/jenkinsci/github-oauth-plugin/pull/52 161 | [#53]: https://github.com/jenkinsci/github-oauth-plugin/pull/53 162 | [#54]: https://github.com/jenkinsci/github-oauth-plugin/pull/54 163 | [#55]: https://github.com/jenkinsci/github-oauth-plugin/pull/55 164 | [#56]: https://github.com/jenkinsci/github-oauth-plugin/pull/56 165 | [JENKINS-33883]: https://issues.jenkins-ci.org/browse/JENKINS-33883 166 | [JENKINS-34775]: https://issues.jenkins-ci.org/browse/JENKINS-34775 167 | 168 | # Version 0.23 (Released May 1, 2016) 169 | 170 | - Encrypt client secret in stored settings ([pull request #51][#51]) 171 | 172 | [#51]: https://github.com/jenkinsci/github-oauth-plugin/pull/51 173 | 174 | # Version 0.22.2 (Released July 25, 2015) 175 | 176 | - The wiki page was having issues rendering plugin information. Unless I 177 | renamed it back (tracked by [JENKINS-29636][JENKINS-29636]). I renamed the 178 | wiki page back to "Github OAuth Plugin" so plugin info would be rendered. I 179 | released 0.22.2 to revert release 0.22.1. 180 | 181 | 182 | # Version 0.22.1 (Released July 25, 2015) 183 | 184 | - I renamed the wiki page to "Github Authentication Plugin" which caused the 185 | plugin to disappear from the update center (tracked by 186 | [JENKINS-29636][JENKINS-29636]). I released the plugin with the new wiki link. 187 | 188 | [JENKINS-29636]: https://issues.jenkins-ci.org/browse/JENKINS-29636 189 | 190 | # Version 0.22 (Released July 24, 2015) 191 | 192 | - Bugfix Java 7 compatibility. The plugin now compiles and tests with Java 7 193 | ([pull request #42][#42]) 194 | - Scripting feature: equals() method available for idempotent groovy 195 | configuration ([pull request #43][#43]) 196 | - Allow limited oauth scopes ([pull request #45][#45]) 197 | - Allow Jenkins email to be set using GitHub private email ([pull request 198 | - #47][#47]) 199 | - Private GitHub organization memberships can be used for authorization ([pull 200 | request #48][#48]) 201 | 202 | [#42]: https://github.com/jenkinsci/github-oauth-plugin/pull/42 203 | [#43]: https://github.com/jenkinsci/github-oauth-plugin/pull/43 204 | [#45]: https://github.com/jenkinsci/github-oauth-plugin/pull/45 205 | [#47]: https://github.com/jenkinsci/github-oauth-plugin/pull/47 206 | [#48]: https://github.com/jenkinsci/github-oauth-plugin/pull/48 207 | 208 | # Version 0.21.2 (Released July 20, 2015) 209 | 210 | - Bugfix migrating settings from plugin 0.20 to 0.21+ ([pull request #46][#46]) 211 | - Improved README ([pull request #44][#44]) 212 | - Improved code style by fixing white space ([pull request #40][#40]) 213 | 214 | [#40]: https://github.com/jenkinsci/github-oauth-plugin/pull/40 215 | [#44]: https://github.com/jenkinsci/github-oauth-plugin/pull/44 216 | [#46]: https://github.com/jenkinsci/github-oauth-plugin/pull/46 217 | 218 | # Version 0.21.1 (Released July 12, 2015) 219 | 220 | - Add support for allowing anonymous ViewStatus permission ([pull request 221 | #29][#29]) 222 | 223 | [#29]: https://github.com/jenkinsci/github-oauth-plugin/pull/29 224 | 225 | # Version 0.21 (Released July 11, 2015) 226 | 227 | - Fewer github api calls for performance ([pull request #27][#27]) 228 | - Fix for when user enters a badly formed github url for repo ([pull request 229 | #32][#32]) 230 | - Make Github OAuth scopes configurable in Security Realm of Global Security 231 | configuration ([pull request #35][#35]) 232 | - Default GitHub OAuth scope is now read:org ([pull request #39][#39]) 233 | - Include GitHub teams as groups when doing matrix based authorization 234 | strategies ([pull request #41][#41]) 235 | - Allow username and GitHub Personal Access Token to be used to access Jenkins 236 | API instead of requiring a Jenkins token to be generated ([pull request 237 | #37][#37]) 238 | 239 | [#27]: https://github.com/jenkinsci/github-oauth-plugin/pull/27 240 | [#32]: https://github.com/jenkinsci/github-oauth-plugin/pull/32 241 | [#35]: https://github.com/jenkinsci/github-oauth-plugin/pull/35 242 | [#37]: https://github.com/jenkinsci/github-oauth-plugin/pull/37 243 | [#39]: https://github.com/jenkinsci/github-oauth-plugin/pull/39 244 | [#41]: https://github.com/jenkinsci/github-oauth-plugin/pull/41 245 | 246 | # Version 0.20 (Released Sept 30, 2014) 247 | 248 | - Minor code comments and updated GitHub API dependency. 249 | 250 | # Version 0.19 (Released July 2, 2014) 251 | 252 | - Honor proxy configuration ([pull request #15][#15]) 253 | - Flag to allow authenticated users to create new jobs ([pull request #21][#21]) 254 | - `SecurityListener` callback 255 | 256 | [#15]: https://github.com/jenkinsci/github-oauth-plugin/pull/15 257 | [#21]: https://github.com/jenkinsci/github-oauth-plugin/pull/21 258 | 259 | # Version 0.15 (Released March 21, 2014) 260 | 261 | - Don't attempt to set email address property for a user upon login ([pull 262 | request #14][#14]) 263 | - Use hasExplicitlyConfiguredAddress instead of getAddress(which scans all 264 | projects and builds to find users's email address) ([committed 265 | directly][bc21838]). 266 | - Fix API token usage on Jenkins core 1.551 ([pull request #18][#18]) 267 | 268 | [#14]: https://github.com/jenkinsci/github-oauth-plugin/pull/14 269 | [#18]: https://github.com/jenkinsci/github-oauth-plugin/pull/18 270 | [bc21838]: https://github.com/jenkinsci/github-oauth-plugin/commit/bc21838bb0e28a8219086d0a28170305c38b6516 271 | 272 | # Version 0.14 (Released July 11, 2013) 273 | 274 | - don't overwrite the e-mail address from GitHub if one is already set ([pull 275 | request #4][#4]) 276 | - fixed an NPE ([pull request #10][#10]) 277 | - Caching of the org/user mapping ([pull request #3][#3]) 278 | 279 | [#3]: https://github.com/jenkinsci/github-oauth-plugin/pull/3 280 | [#4]: https://github.com/jenkinsci/github-oauth-plugin/pull/4 281 | [#10]: https://github.com/jenkinsci/github-oauth-plugin/pull/10 282 | 283 | # Version 0.12 (Released June 13, 2012) 284 | 285 | - Removed the GitHub V2 API dependency. 286 | 287 | # Version 0.10 (Released March 4, 2012) 288 | 289 | - Thanks to virtix for reporting a bug with the plugin not working with github 290 | enterprise. 291 | - Note that you also have to upgrade the github-api plugin to version 1.17 292 | 293 | # Version 0.9 (Released January 8, 2012) 294 | 295 | - Thanks to Kohsuke Kawaguchi for several commits that allow github 296 | organizations to be specified using the matrix-based security. 297 | 298 | # Version 0.8.1 (Released November 1, 2011) 299 | 300 | - Fix the custom XStream Converter to allow the configurations to be saved 301 | correctly. 302 | 303 | # Version 0.8 (Released November 1, 2011) 304 | 305 | - Use custom XStream Converter to let < 0.7 configurations to still work. 306 | 307 | # Version 0.7 (Released October 29, 2011) 308 | 309 | - Adds support for Github Enterprise/Firewall installs. 310 | 311 | # Version 0.6 (Released September 17, 2011) 312 | 313 | - Adds checkbox to the AuthorizationStrategy configuration page to enable the 314 | anonymous read permission. (default is false: no anonymous reads). 315 | 316 | # Version 0.5 (Released September 10, 2011) 317 | 318 | - Fixes a problem where all users of the plugin would see a stack trace instead 319 | of Jenkins. The regex for detecting the github-webhook url was reworked to 320 | support that text appearing anywhere in the request URI. 321 | 322 | # Version 0.4 (Released September 9, 2011) 323 | 324 | - Thanks to vkravets for testing and contributing a patch to fix the regex so 325 | that it actually works for the github-wehook. 326 | 327 | # Version 0.3 (Released September 8, 2011) 328 | 329 | - Adds support for github-plugin's /github-webhook which can be enabled to allow 330 | anonymous READ access to this url. This permits a post commit hook in Github 331 | to notify Jenkins to build the related projects. 332 | 333 | # Version 0.2 (Released July 25, 2011) 334 | 335 | - Fixes serialization issue that prevented plugin from working after Jenkins was 336 | restarted. 337 | 338 | # Version 0.1 (Released July 16, 2011) 339 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | All contributions to the GitHub OAuth Plugin are welcome. This project runs 4 | completely off of pull requests and code review. There are a couple ways of 5 | contributing. This document serves as helpful guidelines to contributing and 6 | isn't necessarily the full scope of available ways to contribute. 7 | 8 | ## Contribute code 9 | 10 | 1. Fork the project. 11 | 2. Create a feature or bugfix branch. 12 | 3. Commit your fix or feature. Be sure to reference any related Jenkins issues 13 | in the commit message surrounded by square brackets. e.g. `[JENKINS-12345]` 14 | 4. Create a pull request. 15 | 16 | Please keep in mind that pull requests tend to stay open for a week or two. 17 | This allows sufficient time for interested individuals to code review open pull 18 | requests. 19 | 20 | What sort of code could use contributing? 21 | 22 | * Working on [open issues][issue-open]. 23 | * Unit tests - there simply aren't enough so writing unit tests alone is a plus. 24 | * Adding code coverage metrics such as cobertura. 25 | * Javadoc - it would be nice if Javadoc was complete. 26 | 27 | ### Building the plugin from master 28 | 29 | #### Prerequisites 30 | 31 | This plugin was last compiled with the following versions. 32 | 33 | * Ubuntu 16.04.1 LTS 34 | * Apache Maven 3.3.9 35 | * Java version: `1.8.0_131`, vendor: Oracle Corporation 36 | 37 | Newer/older versions may work. 38 | 39 | #### Packaging HPI for Jenkins 40 | 41 | To create `github-oauth.hpi` which is the plugin that would be loaded in Jenkins 42 | execute the following command. 43 | 44 | mvn clean package 45 | 46 | The command assumes both Maven and Java are in your `$PATH` and that you have 47 | `$JAVA_HOME` set up. 48 | 49 | ## Contribute code reviews 50 | 51 | Review [open pull requests][pr-open]. When reviewing, a simple `:+1:` comment 52 | is good enough. Make a best effort at catching bugs. For extra credit, build 53 | the plugin and manually test it yourself. Things to look out for: 54 | 55 | * Potential bugs with the way methods are called. 56 | * Missing unit tests and perhaps suggestion on improving unit tests. 57 | * Code style. 58 | * Not mixing tabs with spaces. All indentation should be spaces only. Typical 59 | indentation is 4 spaces. 60 | 61 | The current maintainers make a best effort to build and test the plugin manually 62 | before merging a pull request. 63 | 64 | ## File issues or comment 65 | 66 | Filing new issues and commenting on existing issues is a great help for 67 | validating or debunking potential bug reports. 68 | 69 | * [All issues][issue-all] 70 | * [Open issues][issue-open] 71 | 72 | [issue-all]: https://issues.jenkins-ci.org/browse/JENKINS-29373?jql=project%20%3D%20JENKINS%20AND%20component%20%3D%20github-oauth-plugin 73 | [issue-open]: https://issues.jenkins-ci.org/browse/JENKINS-29373?jql=project%20%3D%20JENKINS%20AND%20status%20in%20%28Open%2C%20%22In%20Progress%22%2C%20Reopened%29%20AND%20component%20%3D%20github-oauth-plugin 74 | [pr-open]: https://github.com/jenkinsci/github-oauth-plugin/pulls 75 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | forkCount: '1C', // Run parallel tests on ci.jenkins.io for lower costs, faster feedback 7 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests 8 | configurations: [ 9 | [platform: 'linux', jdk: 21], 10 | [platform: 'windows', jdk: 17], 11 | ]) 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Michael O'Cleirigh 4 | Copyright (c) 2013-2014 Sam Kottler 5 | Copyright (c) 2015-2017 Sam Gleske 6 | Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors - https://github.com/jenkinsci/jenkins 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins GitHub OAuth Plugin 2 | 3 | * License: [MIT Licensed](LICENSE.txt) 4 | * Read more: [GitHub OAuth Plugin wiki page][wiki] 5 | * Latest build: [![Build Status][build-image]][build-link] 6 | * [Contributions are welcome](CONTRIBUTING.md). 7 | 8 | # Overview 9 | 10 | The GitHub Authentication plugin provides a means of securing a Jenkins instance by 11 | offloading authentication and authorization to GitHub. The plugin authenticates 12 | by using a [GitHub OAuth Application][github-wiki-oauth]. It can use multiple 13 | authorization strategies for authorizing users. GitHub users are surfaced as 14 | Jenkins users for authorization. GitHub organizations and teams are surfaced as 15 | Jenkins groups for authorization. This plugin supports GitHub Enterprise. 16 | 17 | ## Setup 18 | 19 | Before configuring the plugin you must create a GitHub application 20 | registration. 21 | 22 | 1. Visit to create a 23 | GitHub application registration. 24 | 2. The values for application name, homepage URL, or application 25 | description don't matter. They can be customized however desired. 26 | 3. However, the authorization callback URL takes a specific value. It 27 | must be `https://jenkins.example.com/securityRealm/finishLogin` 28 | where jenkins.example.com is the location of the Jenkins server. 29 | 30 | The important part of the callback URL is 31 | `/securityRealm/finishLogin` 32 | 33 | 4. Finish by clicking *Register application*. 34 | 35 | The *Client ID* and the *Client Secret* will be used to configure the 36 | Jenkins Security Realm. Keep the page open to the application 37 | registration so this information can be copied to your Jenkins 38 | configuration. 39 | 40 | #### Security Realm in Global Security 41 | 42 | The security realm in Jenkins controls authentication (i.e. you are who 43 | you say you are). The GitHub Authentication Plugin provides a security 44 | realm to authenticate Jenkins users via GitHub OAuth. 45 | 46 | 1. In the Global Security configuration choose the Security Realm to be 47 | **GitHub Authentication Plugin**. 48 | 2. The settings to configure are: GitHub Web URI, GitHub API URI, 49 | Client ID, Client Secret, and OAuth Scope(s). 50 | 3. If you're using GitHub Enterprise then the API URI is 51 | . 52 | 53 | The GitHub Enterprise API URI ends with `/api/v3`. 54 | 55 | 4. The recommended minimum [GitHub OAuth 56 | scopes](https://developer.github.com/v3/oauth/#scopes) are 57 | `read:org,user:email`. 58 | 59 | The recommended scopes are designed for using both authentication 60 | and authorization functions in the plugin. If only authentication is 61 | being used then the scope can be further limited to `(no scope)` or 62 | `user:email`. 63 | 64 | In the plugin configuration pages each field has a little 65 | (❓) next to it. Click on it for help about the setting. 66 | 67 | #### Authorization in Global Security. 68 | 69 | The authorization configuration in Jenkins controls what your users can 70 | do (i.e. read jobs, execute builds, administer permissions, etc.). The 71 | GitHub OAuth Plugin supports multiple ways of configuring authorization. 72 | 73 | It is highly recommended that you configure the security realm and log 74 | in via GitHub OAuth before configuring authorization. This way Jenkins 75 | can look up and verify users and groups if configuring matrix-based 76 | authorization. 77 | 78 | ##### Github Committer Authorization Strategy 79 | 80 | Control user authorization using the **Github Committer Authorization 81 | Strategy**. This is the simplest authorization strategy to get up and 82 | running. It handles authorization based on the git URL of a job and the 83 | type of access a user has to that project (i.e. Admin, Read/Write, 84 | Read-Only). 85 | 86 | There is a way to authorize the use of the `/github-webhook` callback 87 | url to receive post commit hooks from GitHub. This authorization 88 | strategy has a checkbox that can allow GitHub POST data to be received. 89 | You will still need to run the [GitHub 90 | Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitHub+Plugin) to 91 | have the message trigger the build. 92 | 93 | ##### Logged-in users can do anything 94 | 95 | There are a few ways to configure the plugin so that everyone on your 96 | team has `Overall/Administer` access. 97 | 98 | 1. Choose **Logged-in users can do anything** authorization strategy. 99 | 2. Choose one of the matrix-based authorization strategies. Set 100 | `authenticated` users to `Overall/Administer` permissions. Set 101 | `anonymous` users to have `Overall/Read` permissions and perhaps the 102 | `ViewStatus` permission. 103 | 104 | ##### Matrix-based Authorization strategy 105 | 106 | Control user authorization using **Matrix-based security** or 107 | **Project-based Matrix Authorization Strategy**. Project-based Matrix 108 | Authorization Strategy allows one to configure authorization globally 109 | per project and, when using Project-based Matrix Authorization Strategy 110 | with the CloudBees folder plugin, per folder. 111 | 112 | There are a few built-in authorizations to consider. 113 | 114 | - `anonymous` - is anyone who has not logged in. Recommended 115 | permissions are just `Job/Discover` and `Job/ViewStatus`. 116 | - `authenticated` - is anyone who has logged in. You can configure 117 | permissions for anybody who has logged into Jenkins. Recommended 118 | permissions are `Overall/Read` and `View/Read`. 119 | 120 | `anonymous` and `authenticated` usernames are case sensitive and 121 | must be lower case. This is a consideration when configuring 122 | authorizations via Groovy. Keep in mind that `anonymous` shows up as 123 | *Anonymous* in the Jenkins UI. 124 | 125 | You can configure authorization based on GitHub users, organizations, or 126 | teams. 127 | 128 | - `username` - give permissions to a specific GitHub username. 129 | - `organization` - give permissions to every user that belongs to a 130 | specific GitHub organization. 131 | - `organization*team` - give permissions to a specific GitHub team of 132 | a GitHub organization. Notice that organization and team are 133 | separated by an asterisk (`*`). 134 | 135 | ## Other usage 136 | 137 | #### Calling Jenkins API using GitHub Personal Access Tokens 138 | 139 | You can make Jenkins API calls by using a GitHub personal access token. 140 | One can still call the Jenkins API by using Jenkins tokens or use the 141 | Jenkins CLI with an SSH key for authentication. However, the GitHub 142 | OAuth plugin provides another way to call the Jenkins API by allowing 143 | the use of a GitHub Personal Access Token. 144 | 145 | 1. Generate a [GitHub *Personal Access 146 | Token*](https://github.com/settings/tokens) and give it only 147 | `read:org` scope. 148 | 2. Use a username and GitHub personal access token to authenticate with 149 | the Jenkins API. 150 | 151 | Here's an example using curl to start a build using parameters (username 152 | `samrocketman` and password using the personal access token). 153 | 154 | ``` syntaxhighlighter-pre 155 | curl -X POST https://jenkins.example.com/job/_jervis_generator/build --user "samrocketman:myGitHubPersonalAccessToken" --data-urlencode json='{"parameter": [{"name":"project", "value":"samrocketman/jervis"}]}' 156 | ``` 157 | 158 | #### Automatically configure security realm via script console 159 | 160 | Configuration management could be used to configure the security realm 161 | via the [Jenkins Script 162 | Console](https://wiki.jenkins.io/display/JENKINS/Jenkins+Script+Console). 163 | Here's a sample configuring plugin version 0.22. 164 | 165 | ``` syntaxhighlighter-pre 166 | import hudson.security.SecurityRealm 167 | import org.jenkinsci.plugins.GithubSecurityRealm 168 | String githubWebUri = 'https://github.com' 169 | String githubApiUri = 'https://api.github.com' 170 | String clientID = 'someid' 171 | String clientSecret = 'somesecret' 172 | String oauthScopes = 'read:org' 173 | SecurityRealm github_realm = new GithubSecurityRealm(githubWebUri, githubApiUri, clientID, clientSecret, oauthScopes) 174 | //check for equality, no need to modify the runtime if no settings changed 175 | if(!github_realm.equals(Jenkins.instance.getSecurityRealm())) { 176 | Jenkins.instance.setSecurityRealm(github_realm) 177 | Jenkins.instance.save() 178 | } 179 | ``` 180 | 181 | #### Automatically configure authorization strategy via script console 182 | 183 | Configuration management could be used to configure the authorization 184 | strategy via the [Jenkins Script 185 | Console](https://wiki.jenkins.io/display/JENKINS/Jenkins+Script+Console). 186 | Here's a sample configuring plugin version 0.22. 187 | 188 | ``` syntaxhighlighter-pre 189 | import org.jenkinsci.plugins.GithubAuthorizationStrategy 190 | import hudson.security.AuthorizationStrategy 191 | 192 | //permissions are ordered similar to web UI 193 | //Admin User Names 194 | String adminUserNames = 'samrocketman' 195 | //Participant in Organization 196 | String organizationNames = '' 197 | //Use Github repository permissions 198 | boolean useRepositoryPermissions = true 199 | //Grant READ permissions to all Authenticated Users 200 | boolean authenticatedUserReadPermission = false 201 | //Grant CREATE Job permissions to all Authenticated Users 202 | boolean authenticatedUserCreateJobPermission = false 203 | //Grant READ permissions for /github-webhook 204 | boolean allowGithubWebHookPermission = false 205 | //Grant READ permissions for /cc.xml 206 | boolean allowCcTrayPermission = false 207 | //Grant READ permissions for Anonymous Users 208 | boolean allowAnonymousReadPermission = false 209 | //Grant ViewStatus permissions for Anonymous Users 210 | boolean allowAnonymousJobStatusPermission = false 211 | 212 | AuthorizationStrategy github_authorization = new GithubAuthorizationStrategy(adminUserNames, 213 | authenticatedUserReadPermission, 214 | useRepositoryPermissions, 215 | authenticatedUserCreateJobPermission, 216 | organizationNames, 217 | allowGithubWebHookPermission, 218 | allowCcTrayPermission, 219 | allowAnonymousReadPermission, 220 | allowAnonymousJobStatusPermission) 221 | 222 | //check for equality, no need to modify the runtime if no settings changed 223 | if(!github_authorization.equals(Jenkins.instance.getAuthorizationStrategy())) { 224 | Jenkins.instance.setAuthorizationStrategy(github_authorization) 225 | Jenkins.instance.save() 226 | } 227 | ``` 228 | 229 | ## Troubleshooting Installation 230 | 231 | After installing, the `` class should have 232 | been updated in your `/var/lib/jenkins/config.xml` file. The value of 233 | `` should agree with what you pasted into the admin UI. If it doesn't 234 | or you still can't log in, reset to `` and restart Jenkins from 236 | the command-line. 237 | 238 | 239 | [build-image]: https://ci.jenkins.io/buildStatus/icon?job=Plugins/github-oauth-plugin/master 240 | [build-link]: https://ci.jenkins.io/job/Plugins/job/github-oauth-plugin/job/master/ 241 | [github-wiki-oauth]: https://developer.github.com/v3/oauth/ 242 | [wiki]: https://wiki.jenkins-ci.org/display/JENKINS/Github+OAuth+Plugin 243 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Steps to release 2 | 3 | This outlines the maintainers steps to release the Jenkins GitHub Authentication 4 | plugin. Follow the Jenkins documentation for [making a new 5 | release][plugin-release]. 6 | 7 | - [ ] Configure your credentials in `~/.m2/settings.xml`. (outlined in [making a 8 | new release][plugin-release] doc) 9 | - [ ] Create a new issue to track the release and give it the label `maintainer 10 | communication`. 11 | - [ ] Create a release branch. `git checkout origin/master -b prepare_release` 12 | - [ ] Update the release notes in `CHANGELOG.md`. 13 | - [ ] Open a pull request from `prepare_release` branch to `master` branch. 14 | Merge it. 15 | - [ ] Fetch the latest `master`. 16 | - [ ] Clean the workspace `git clean -xfd`. 17 | - [ ] Execute the release plugin. 18 | 19 | ``` 20 | mvn org.apache.maven.plugins:maven-release-plugin:2.5:prepare org.apache.maven.plugins:maven-release-plugin:2.5:perform 21 | ``` 22 | 23 | - [ ] Wait for the plugin to be released into the Jenkins Update Center. 24 | - [ ] Successfully perform an upgrade from the last stable plugin release to the 25 | current release. 26 | 27 | I pin which version of the release plugin to use because of the working around 28 | common issues section of the [release document][plugin-release]. 29 | 30 | 31 | [plugin-release]: https://wiki.jenkins-ci.org/display/JENKINS/Hosting+Plugins 32 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 5.17 8 | 9 | 10 | 11 | 12 | 999999-SNAPSHOT 13 | 14 | 2.479 15 | ${jenkins.baseline}.3 16 | jenkinsci/${project.artifactId}-plugin 17 | 18 | 19 | org.jenkins-ci.plugins 20 | github-oauth 21 | ${changelist} 22 | GitHub Authentication plugin 23 | A Jenkins authentication plugin that delegates to GitHub. We also implement an Authorization Strategy that uses the acquired OAuth token to interact with the GitHub API to determine a user's level of access to Jenkins. 24 | hpi 25 | 26 | https://github.com/jenkinsci/${project.artifactId}-plugin 27 | 28 | 29 | 30 | MIT License 31 | https://opensource.org/licenses/MIT 32 | All source code is under the MIT license. 33 | 34 | 35 | 36 | 37 | 38 | sag47 39 | Sam Gleske 40 | sam.mxracer@gmail.com 41 | https://github.com/samrocketman 42 | 43 | maintainer 44 | 45 | America/Los_Angeles 46 | 47 | 48 | 49 | 50 | scm:git:https://github.com/${gitHubRepo}.git 51 | scm:git:git@github.com:${gitHubRepo}.git 52 | https://github.com/${gitHubRepo} 53 | ${scmTag} 54 | 55 | 56 | 57 | 58 | repo.jenkins-ci.org 59 | https://repo.jenkins-ci.org/public/ 60 | 61 | 62 | 63 | 64 | repo.jenkins-ci.org 65 | https://repo.jenkins-ci.org/public/ 66 | 67 | 68 | 69 | 70 | 71 | 72 | io.jenkins.tools.bom 73 | bom-${jenkins.baseline}.x 74 | 4862.vc32a_71c3e731 75 | import 76 | pom 77 | 78 | 79 | 80 | 81 | 82 | 83 | io.jenkins.plugins 84 | caffeine-api 85 | 86 | 87 | 88 | org.jenkins-ci.plugins 89 | mailer 90 | 91 | 92 | 93 | org.jenkins-ci.plugins 94 | github-api 95 | 96 | 97 | 98 | org.jenkins-ci.plugins 99 | jackson2-api 100 | 101 | 102 | 103 | org.jenkins-ci.plugins 104 | matrix-project 105 | 106 | 107 | 108 | org.jenkins-ci.plugins.workflow 109 | workflow-multibranch 110 | 111 | 112 | 113 | org.jenkins-ci.plugins 114 | github-branch-source 115 | 116 | 117 | 118 | org.jenkins-ci.plugins 119 | git 120 | 121 | 122 | 123 | 124 | org.mockito 125 | mockito-core 126 | test 127 | 128 | 129 | org.mockito 130 | mockito-junit-jupiter 131 | test 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.jenkins-ci.tools 139 | maven-hpi-plugin 140 | true 141 | 142 | 0.3 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GitHubOAuthScope.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins; 2 | 3 | import hudson.ExtensionPoint; 4 | import java.util.Collection; 5 | 6 | /** 7 | * Extension point to be implemented by plugins to request additional scopes. 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public abstract class GitHubOAuthScope implements ExtensionPoint { 11 | /** 12 | * Returns a collection of scopes to request. 13 | * See http://developer.github.com/v3/oauth/ for valid scopes 14 | */ 15 | public abstract Collection getScopesToRequest(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GitHubRepositoryName.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | 23 | 24 | */ 25 | package org.jenkinsci.plugins; 26 | 27 | import java.util.logging.Level; 28 | import java.util.logging.Logger; 29 | import java.util.regex.Matcher; 30 | import java.util.regex.Pattern; 31 | 32 | /** 33 | * Uniquely identifies a repository on GitHub. 34 | * 35 | * This is a simplified version of the file 36 | * https://github.com/jenkinsci/github-plugin/blob/master/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java 37 | * 38 | * It has been duplicated to avoid introducing a dependency on "github-plugin" 39 | * 40 | * @author Kohsuke Kawaguchi 41 | */ 42 | public class GitHubRepositoryName { 43 | 44 | private static final Pattern[] URL_PATTERNS = { 45 | /** 46 | * The first set of patterns extract the host, owner and repository names 47 | * from URLs that include a '.git' suffix, removing the suffix from the 48 | * repository name. 49 | */ 50 | Pattern.compile("git@(.+):([^/]+)/([^/]+)\\.git"), 51 | Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)\\.git"), 52 | Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)\\.git"), 53 | Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)\\.git"), 54 | Pattern.compile("ssh://git@([^/]+)/([^/]+)/([^/]+)\\.git"), 55 | /** 56 | * The second set of patterns extract the host, owner and repository names 57 | * from all other URLs. Note that these patterns must be processed *after* 58 | * the first set, to avoid any '.git' suffix that may be present being included 59 | * in the repository name. 60 | */ 61 | Pattern.compile("git@(.+):([^/]+)/([^/]+)/?"), 62 | Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"), 63 | Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"), 64 | Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"), 65 | Pattern.compile("ssh://git@([^/]+)/([^/]+)/([^/]+)/?") 66 | }; 67 | 68 | /** 69 | * Create {@link GitHubRepositoryName} from URL 70 | * 71 | * @param url 72 | * must be non-null 73 | * @return parsed {@link GitHubRepositoryName} or null if it cannot be 74 | * parsed from the specified URL 75 | */ 76 | public static GitHubRepositoryName create(final String url) { 77 | LOGGER.log(Level.FINE, "Constructing from URL {0}", url); 78 | for (Pattern p : URL_PATTERNS) { 79 | Matcher m = p.matcher(url.trim()); 80 | if (m.matches()) { 81 | LOGGER.log(Level.FINE, "URL matches {0}", m); 82 | GitHubRepositoryName ret = new GitHubRepositoryName(m.group(1), m.group(2), 83 | m.group(3)); 84 | LOGGER.log(Level.FINE, "Object is {0}", ret); 85 | return ret; 86 | } 87 | } 88 | LOGGER.log(Level.WARNING, "Could not match URL {0}", url); 89 | return null; 90 | } 91 | 92 | public final String host, userName, repositoryName; 93 | 94 | public GitHubRepositoryName(String host, String userName, String repositoryName) { 95 | this.host = host; 96 | this.userName = userName; 97 | this.repositoryName = repositoryName; 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return "GitHubRepository[host="+host+",username="+userName+",repository="+repositoryName+"]"; 103 | } 104 | 105 | private static final Logger LOGGER = Logger.getLogger(GitHubRepositoryName.class.getName()); 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubAccessTokenProperty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017, 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 org.jenkinsci.plugins; 25 | 26 | import edu.umd.cs.findbugs.annotations.NonNull; 27 | import hudson.Extension; 28 | import hudson.model.User; 29 | import hudson.model.UserProperty; 30 | import hudson.model.UserPropertyDescriptor; 31 | import hudson.util.Secret; 32 | import org.jenkinsci.Symbol; 33 | 34 | /** 35 | * Remembers the access token used to connect to the Github server 36 | * 37 | * @since TODO 38 | */ 39 | public class GithubAccessTokenProperty extends UserProperty { 40 | private final Secret accessToken; 41 | 42 | public GithubAccessTokenProperty(String accessToken) { 43 | this.accessToken = Secret.fromString(accessToken); 44 | } 45 | 46 | public @NonNull Secret getAccessToken() { 47 | return accessToken; 48 | } 49 | 50 | @Extension 51 | @Symbol("githubAccessToken") 52 | public static final class DescriptorImpl extends UserPropertyDescriptor { 53 | @Override 54 | public boolean isEnabled() { 55 | // does not show elements in //configure/ 56 | return false; 57 | } 58 | 59 | @Override 60 | public UserProperty newInstance(User user) { 61 | // no default property 62 | return null; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2011 Michael O'Cleirigh 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 | 26 | */ 27 | package org.jenkinsci.plugins; 28 | 29 | import com.github.benmanes.caffeine.cache.Cache; 30 | import com.github.benmanes.caffeine.cache.Caffeine; 31 | import edu.umd.cs.findbugs.annotations.NonNull; 32 | import edu.umd.cs.findbugs.annotations.Nullable; 33 | import hudson.model.Item; 34 | import hudson.security.Permission; 35 | import hudson.security.SecurityRealm; 36 | import java.io.IOException; 37 | import java.io.UncheckedIOException; 38 | import java.net.MalformedURLException; 39 | import java.net.Proxy; 40 | import java.net.URL; 41 | import java.util.ArrayList; 42 | import java.util.Collection; 43 | import java.util.Collections; 44 | import java.util.List; 45 | import java.util.Map; 46 | import java.util.Set; 47 | import java.util.concurrent.TimeUnit; 48 | import java.util.logging.Level; 49 | import java.util.logging.Logger; 50 | import jenkins.model.Jenkins; 51 | import okhttp3.OkHttpClient; 52 | import org.kohsuke.github.GHMyself; 53 | import org.kohsuke.github.GHOrganization; 54 | import org.kohsuke.github.GHRepository; 55 | import org.kohsuke.github.GHTeam; 56 | import org.kohsuke.github.GHUser; 57 | import org.kohsuke.github.GitHub; 58 | import org.kohsuke.github.GitHubBuilder; 59 | import org.kohsuke.github.RateLimitHandler; 60 | import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector; 61 | import org.springframework.security.authentication.AbstractAuthenticationToken; 62 | import org.springframework.security.core.GrantedAuthority; 63 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 64 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 65 | 66 | /** 67 | * @author mocleiri 68 | * 69 | * to hold the authentication token from the github oauth process. 70 | * 71 | */ 72 | public class GithubAuthenticationToken extends AbstractAuthenticationToken { 73 | 74 | private static final long serialVersionUID = 2L; 75 | 76 | private final String accessToken; 77 | private final String githubServer; 78 | private final String userName; 79 | 80 | private transient GitHub gh; 81 | private transient GHMyself me; 82 | private transient GithubSecurityRealm myRealm = null; 83 | 84 | public static final TimeUnit CACHE_EXPIRY = TimeUnit.HOURS; 85 | /** 86 | * Cache for faster organization based security 87 | */ 88 | private static final Cache> userOrganizationCache = 89 | Caffeine.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); 90 | 91 | /** 92 | * This is a double-layered cached. The first mapping is from github username 93 | * to a secondary cache of repositories. This is so we can mass populate 94 | * the initial set of repos a user is a collaborator on at once. 95 | * 96 | * The secondary layer is from repository names (full names) to rights the 97 | * user has for that repo. Here we may add single entries occasionally, and this 98 | * is primarily about adding entries for public repos that they're not explicitly 99 | * a collaborator on (or updating a given repo's entry) 100 | * 101 | * We could make this a single layer since this token object should be per-user, 102 | * but I'm unsure of how long it actually lives in memory. 103 | */ 104 | private static final Cache> repositoriesByUserCache = 105 | Caffeine.newBuilder().expireAfterWrite(24, CACHE_EXPIRY).build(); 106 | 107 | /** 108 | * Here we keep a global cache of whether repos are public or private, since that 109 | * can be shared across users (and public repos are global read/pull, so we 110 | * can avoid asking for user repos if the repo is known to be public and they want read rights) 111 | */ 112 | private static final Cache repositoriesPublicStatusCache = 113 | Caffeine.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); 114 | 115 | private static final Cache usersByIdCache = 116 | Caffeine.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); 117 | 118 | private static final Cache usersByTokenCache = 119 | Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(); 120 | 121 | private static final Cache>> userTeamsCache = 122 | Caffeine.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); 123 | 124 | private final List authorities = new ArrayList<>(); 125 | 126 | private static final GithubUser UNKNOWN_USER = new GithubUser(null); 127 | private static final GithubMyself UNKNOWN_TOKEN = new GithubMyself(null); 128 | 129 | /** Wrappers for cache **/ 130 | static class GithubUser { 131 | public final GHUser user; 132 | 133 | public GithubUser(GHUser user) { 134 | this.user = user; 135 | } 136 | } 137 | 138 | static class GithubMyself { 139 | public final GHMyself me; 140 | 141 | public GithubMyself(GHMyself me) { 142 | this.me = me; 143 | } 144 | } 145 | 146 | static class RepoRights { 147 | public final boolean hasAdminAccess; 148 | public final boolean hasPullAccess; 149 | public final boolean hasPushAccess; 150 | public final boolean isPrivate; 151 | 152 | public RepoRights(@Nullable GHRepository repo) { 153 | if (repo != null) { 154 | this.hasAdminAccess = repo.hasAdminAccess(); 155 | this.hasPullAccess = repo.hasPullAccess(); 156 | this.hasPushAccess = repo.hasPushAccess(); 157 | this.isPrivate = repo.isPrivate(); 158 | } else { 159 | // assume null repo means we had no rights to view it 160 | // so must be private 161 | this.hasAdminAccess = false; 162 | this.hasPullAccess = false; 163 | this.hasPushAccess = false; 164 | this.isPrivate = true; 165 | } 166 | } 167 | 168 | public boolean hasAdminAccess() { 169 | return this.hasAdminAccess; 170 | } 171 | 172 | public boolean hasPullAccess() { 173 | return this.hasPullAccess; 174 | } 175 | 176 | public boolean hasPushAccess() { 177 | return this.hasPushAccess; 178 | } 179 | 180 | public boolean isPrivate() { 181 | return this.isPrivate; 182 | } 183 | } 184 | 185 | public GithubAuthenticationToken(final String accessToken, final String githubServer) throws IOException { 186 | this(accessToken, githubServer, false); 187 | } 188 | 189 | public GithubAuthenticationToken(final String accessToken, final String githubServer, final boolean clearUserCache) throws IOException { 190 | super(List.of()); 191 | 192 | this.accessToken = accessToken; 193 | this.githubServer = githubServer; 194 | 195 | this.me = loadMyself(accessToken); 196 | 197 | if(this.me == null) { 198 | throw new UsernameNotFoundException("Token not valid"); 199 | } 200 | 201 | setAuthenticated(true); 202 | 203 | this.userName = this.me.getLogin(); 204 | if (clearUserCache) { 205 | // Clear the cache when requested. In particular, for new logins as groups and teams 206 | // may have changed due to SSO [JENKINS-60200]. 207 | clearCacheForUser(this.userName); 208 | } 209 | authorities.add(SecurityRealm.AUTHENTICATED_AUTHORITY2); 210 | 211 | // This stuff only really seems useful if *not* using GithubAuthorizationStrategy 212 | // but instead using matrix so org/team can be granted rights 213 | Jenkins jenkins = Jenkins.get(); 214 | if (jenkins.getSecurityRealm() instanceof GithubSecurityRealm) { 215 | if (myRealm == null) { 216 | myRealm = (GithubSecurityRealm) jenkins.getSecurityRealm(); 217 | } 218 | // Search for scopes that allow fetching team membership. This is documented online. 219 | // https://developer.github.com/v3/orgs/#list-your-organizations 220 | // https://developer.github.com/v3/orgs/teams/#list-user-teams 221 | if (myRealm.hasScope("read:org") || myRealm.hasScope("admin:org") || myRealm.hasScope("user") || myRealm.hasScope("repo")) { 222 | Set myOrgs = getUserOrgs(); 223 | 224 | Map> myTeams = userTeamsCache.get(this.userName, unused -> { 225 | try { 226 | return getGitHub().getMyTeams(); 227 | } catch (IOException e) { 228 | throw new UncheckedIOException("authorization failed for user = " + this.userName, e); 229 | } 230 | }); 231 | 232 | // fetch organization-only memberships (i.e.: groups without teams) 233 | for (String orgLogin : myOrgs) { 234 | if (!myTeams.containsKey(orgLogin)) { 235 | myTeams.put(orgLogin, Collections.emptySet()); 236 | } 237 | } 238 | 239 | for (Map.Entry> teamEntry : myTeams.entrySet()) { 240 | String orgLogin = teamEntry.getKey(); 241 | LOGGER.log(Level.FINE, "Fetch teams for user " + userName + " in organization " + orgLogin); 242 | authorities.add(new SimpleGrantedAuthority(orgLogin)); 243 | for (GHTeam team : teamEntry.getValue()) { 244 | String teamIdentifier = team.getSlug() == null ? team.getName() : team.getSlug(); 245 | 246 | authorities.add(new SimpleGrantedAuthority(orgLogin + GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR 247 | + teamIdentifier)); 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * Necessary for testing 256 | */ 257 | public static void clearCaches() { 258 | userOrganizationCache.invalidateAll(); 259 | repositoriesByUserCache.invalidateAll(); 260 | repositoriesPublicStatusCache.invalidateAll(); 261 | usersByIdCache.invalidateAll(); 262 | usersByTokenCache.invalidateAll(); 263 | userTeamsCache.invalidateAll(); 264 | } 265 | 266 | /** 267 | * Clear caches by username for use in new logins 268 | */ 269 | public static void clearCacheForUser(String userName) { 270 | userOrganizationCache.invalidate(userName); 271 | repositoriesByUserCache.invalidate(userName); 272 | usersByIdCache.invalidate(userName); 273 | userTeamsCache.invalidate(userName); 274 | } 275 | 276 | 277 | /** 278 | * Gets the OAuth access token, so that it can be persisted and used elsewhere. 279 | * @return accessToken 280 | */ 281 | String getAccessToken() { 282 | return accessToken; 283 | } 284 | 285 | /** 286 | * Gets the Github server used for this token 287 | * @return githubServer 288 | */ 289 | String getGithubServer() { 290 | return githubServer; 291 | } 292 | 293 | GitHub getGitHub() throws IOException { 294 | if (this.gh == null) { 295 | 296 | String host; 297 | try { 298 | host = new URL(this.githubServer).getHost(); 299 | } catch (MalformedURLException e) { 300 | throw new IOException("Invalid GitHub API URL: " + this.githubServer, e); 301 | } 302 | 303 | OkHttpClient client = 304 | new OkHttpClient.Builder() 305 | .proxy(getProxy(host)) 306 | .proxyAuthenticator( 307 | new JenkinsProxyAuthenticator(Jenkins.get().getProxy())) 308 | .build(); 309 | 310 | this.gh = 311 | GitHubBuilder.fromEnvironment() 312 | .withEndpoint(this.githubServer) 313 | .withOAuthToken(this.accessToken) 314 | .withRateLimitHandler(RateLimitHandler.FAIL) 315 | .withConnector(new OkHttpGitHubConnector(client)) 316 | .build(); 317 | } 318 | return gh; 319 | } 320 | 321 | /** 322 | * Uses proxy if configured on pluginManager/advanced page 323 | * 324 | * @param host GitHub's hostname to build proxy to 325 | * 326 | * @return proxy to use it in connector. Should not be null as it can lead to unexpected behaviour 327 | */ 328 | @NonNull 329 | private static Proxy getProxy(@NonNull String host) { 330 | Jenkins jenkins = Jenkins.get(); 331 | 332 | if (jenkins.proxy == null) { 333 | return Proxy.NO_PROXY; 334 | } else { 335 | return jenkins.proxy.createProxy(host); 336 | } 337 | } 338 | 339 | @Override 340 | public Collection getAuthorities() { 341 | return authorities; 342 | } 343 | 344 | @Override 345 | public Object getCredentials() { 346 | return ""; // do not expose the credential 347 | } 348 | 349 | /** 350 | * Returns the login name in GitHub. 351 | * @return principal 352 | */ 353 | @Override 354 | public String getPrincipal() { 355 | return this.userName; 356 | } 357 | 358 | /** 359 | * Returns the GHMyself object from this instance. 360 | * @return myself 361 | */ 362 | public GHMyself getMyself() throws IOException { 363 | if (me == null) { 364 | me = getGitHub().getMyself(); 365 | } 366 | return me; 367 | } 368 | 369 | /** 370 | * Wraps grabbing a user's github orgs with our caching 371 | * @return the Set of org names current user is a member of 372 | */ 373 | @NonNull 374 | private Set getUserOrgs() { 375 | return userOrganizationCache.get(this.userName, unused -> { 376 | try { 377 | return getGitHub().getMyOrganizations().keySet(); 378 | } catch (IOException e) { 379 | throw new UncheckedIOException("authorization failed for user = " + this.userName, e); 380 | } 381 | }); 382 | } 383 | 384 | @NonNull 385 | boolean isMemberOfAnyOrganizationInList(@NonNull Collection organizations) { 386 | Set userOrgs = getUserOrgs(); 387 | for (String orgName : organizations) { 388 | if (userOrgs.contains(orgName)) { 389 | return true; 390 | } 391 | } 392 | return false; 393 | } 394 | 395 | @NonNull 396 | boolean hasRepositoryPermission(@NonNull String repositoryName, @NonNull Permission permission) { 397 | LOGGER.log(Level.FINEST, "Checking for permission: " + permission + " on repo: " + repositoryName + " for user: " + this.userName); 398 | boolean isReadPermission = isReadRelatedPermission(permission); 399 | if (isReadPermission) { 400 | // here we do a 2-pass system since public repos are global read, so if *any* user has retrieved tha info 401 | // for the repo, we can use it here to possibly skip loading the full repo details for the user. 402 | Boolean isPublic = repositoriesPublicStatusCache.getIfPresent(repositoryName); 403 | if (isPublic != null && isPublic) { 404 | return true; 405 | } 406 | } 407 | // repo is not public (or we don't yet know) so load it up... 408 | RepoRights repository = loadRepository(repositoryName); 409 | // let admins do anything 410 | if (repository.hasAdminAccess()) { 411 | return true; 412 | } 413 | // WRITE or READ (or public repo) can Read/Build/View Workspace 414 | if (isReadPermission) { 415 | return !repository.isPrivate() || repository.hasPullAccess() || repository.hasPushAccess(); 416 | } 417 | // WRITE can cancel builds or view config 418 | if (permission.equals(Item.CANCEL) || permission.equals(Item.EXTENDED_READ)) { 419 | return repository.hasPushAccess(); 420 | } 421 | // Need ADMIN rights to do rest: configure, create, delete, wipeout 422 | return false; 423 | } 424 | 425 | @NonNull 426 | private boolean isReadRelatedPermission(@NonNull Permission permission) { 427 | return permission.equals(Item.DISCOVER) || 428 | permission.equals(Item.READ) || 429 | permission.equals(Item.BUILD) || 430 | permission.equals(Item.WORKSPACE); 431 | } 432 | 433 | /** 434 | * Returns a mapping from repo names to repo rights for the current user 435 | * @return [description] 436 | */ 437 | @NonNull 438 | private Cache myRepositories() { 439 | return repositoriesByUserCache.get(this.userName, unused -> { 440 | // listRepositories returns all repos owned by user, where they are a collaborator, 441 | // and any user has access through org membership 442 | List userRepositoryList; 443 | try { 444 | userRepositoryList = getMyself().listRepositories(100).asList(); // use max page size of 100 to limit API calls 445 | } catch (IOException e) { 446 | throw new UncheckedIOException("authorization failed for user = " + this.userName, e); 447 | } 448 | // Now we want to cache each repo's rights too 449 | Cache repoNameToRightsCache = 450 | Caffeine.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); 451 | for (GHRepository repo : userRepositoryList) { 452 | RepoRights rights = new RepoRights(repo); 453 | String repositoryName = repo.getFullName(); 454 | // store in user's repo cache 455 | repoNameToRightsCache.put(repositoryName, rights); 456 | // store public/private flag in our global cache 457 | repositoriesPublicStatusCache.put(repositoryName, !rights.isPrivate()); 458 | } 459 | return repoNameToRightsCache; 460 | } 461 | ); 462 | } 463 | 464 | private static final Logger LOGGER = Logger 465 | .getLogger(GithubAuthenticationToken.class.getName()); 466 | 467 | @Nullable 468 | GHUser loadUser(@NonNull String username) throws IOException { 469 | GithubUser user; 470 | try { 471 | user = usersByIdCache.getIfPresent(username); 472 | if (gh != null && user == null && isAuthenticated()) { 473 | GHUser ghUser = getGitHub().getUser(username); 474 | user = new GithubUser(ghUser); 475 | usersByIdCache.put(username, user); 476 | } 477 | } catch (IOException e) { 478 | LOGGER.log(Level.FINEST, e.getMessage(), e); 479 | user = UNKNOWN_USER; 480 | usersByIdCache.put(username, UNKNOWN_USER); 481 | } 482 | return user != null ? user.user : null; 483 | } 484 | 485 | private GHMyself loadMyself(@NonNull String token) throws IOException { 486 | GithubMyself me; 487 | try { 488 | me = usersByTokenCache.getIfPresent(token); 489 | if (me == null) { 490 | GHMyself ghMyself = getGitHub().getMyself(); 491 | me = new GithubMyself(ghMyself); 492 | usersByTokenCache.put(token, me); 493 | // Also stick into usersByIdCache (to have latest copy) 494 | String username = ghMyself.getLogin(); 495 | usersByIdCache.put(username, new GithubUser(ghMyself)); 496 | } 497 | } catch (IOException e) { 498 | LOGGER.log(Level.INFO, e.getMessage(), e); 499 | me = UNKNOWN_TOKEN; 500 | usersByTokenCache.put(token, UNKNOWN_TOKEN); 501 | } 502 | return me.me; 503 | } 504 | 505 | @Nullable 506 | GHOrganization loadOrganization(@NonNull String organization) { 507 | try { 508 | if (gh != null && isAuthenticated()) 509 | return getGitHub().getOrganization(organization); 510 | } catch (IOException | RuntimeException e) { 511 | LOGGER.log(Level.FINEST, e.getMessage(), e); 512 | } 513 | return null; 514 | } 515 | 516 | @NonNull 517 | private RepoRights loadRepository(@NonNull final String repositoryName) { 518 | try { 519 | if (gh != null && isAuthenticated() && (myRealm.hasScope("repo") || myRealm.hasScope("public_repo"))) { 520 | Cache repoNameToRightsCache = myRepositories(); 521 | return repoNameToRightsCache.get(repositoryName, unused -> { 522 | GHRepository repo; 523 | try { 524 | repo = getGitHub().getRepository(repositoryName); 525 | } catch (IOException e) { 526 | throw new UncheckedIOException(e); 527 | } 528 | RepoRights rights = new RepoRights(repo); 529 | // store public/private flag in our cache 530 | repositoriesPublicStatusCache.put(repositoryName, !rights.isPrivate()); 531 | return rights; 532 | } 533 | ); 534 | } 535 | } catch (Exception e) { 536 | LOGGER.log(Level.SEVERE, "an exception was thrown", e); 537 | LOGGER.log(Level.WARNING, 538 | "Looks like a bad GitHub URL OR the Jenkins user {0} does not have access to the repository {1}. May need to add 'repo' or 'public_repo' to the list of oauth scopes requested.", 539 | new Object[] { this.userName, repositoryName }); 540 | } 541 | return new RepoRights(null); // treat as a private repo 542 | } 543 | 544 | @Nullable 545 | GHTeam loadTeam(@NonNull String organization, @NonNull String team) { 546 | try { 547 | GHOrganization org = loadOrganization(organization); 548 | if (org != null) { 549 | 550 | // JENKINS-34835 favor getting by slug but fall back to getting 551 | // by name for compatibility since most Jenkins setups older 552 | // than github-oauth 0.33 will be using team name 553 | if(org.getTeamBySlug(team) != null) { 554 | return org.getTeamBySlug(team); 555 | } else { 556 | return org.getTeamByName(team); 557 | } 558 | } 559 | } catch (IOException e) { 560 | LOGGER.log(Level.FINEST, e.getMessage(), e); 561 | } 562 | return null; 563 | } 564 | 565 | @Nullable 566 | GithubOAuthUserDetails getUserDetails(@NonNull String username) throws IOException { 567 | GHUser user = loadUser(username); 568 | if (user != null) { 569 | return new GithubOAuthUserDetails(user.getLogin(), getAuthorities()); 570 | } 571 | return null; 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubAuthorizationStrategy.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2011 Michael O'Cleirigh 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 | 26 | */ 27 | package org.jenkinsci.plugins; 28 | 29 | import edu.umd.cs.findbugs.annotations.NonNull; 30 | import hudson.Extension; 31 | import hudson.model.AbstractItem; 32 | import hudson.model.AbstractProject; 33 | import hudson.model.Descriptor; 34 | import hudson.model.Job; 35 | import hudson.security.ACL; 36 | import hudson.security.AuthorizationStrategy; 37 | import java.util.Collection; 38 | import java.util.Collections; 39 | import jenkins.branch.MultiBranchProject; 40 | import org.apache.commons.lang.StringUtils; 41 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 42 | import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; 43 | import org.kohsuke.stapler.DataBoundConstructor; 44 | import org.kohsuke.stapler.DataBoundSetter; 45 | 46 | /** 47 | * @author mocleiri 48 | * 49 | * 50 | * 51 | */ 52 | public class GithubAuthorizationStrategy extends AuthorizationStrategy { 53 | 54 | @DataBoundConstructor 55 | public GithubAuthorizationStrategy(String adminUserNames, 56 | boolean authenticatedUserReadPermission, 57 | boolean useRepositoryPermissions, 58 | boolean authenticatedUserCreateJobPermission, 59 | String organizationNames, 60 | boolean allowGithubWebHookPermission, 61 | boolean allowCcTrayPermission, 62 | boolean allowAnonymousReadPermission, 63 | boolean allowAnonymousJobStatusPermission) { 64 | super(); 65 | 66 | rootACL = new GithubRequireOrganizationMembershipACL(adminUserNames, 67 | organizationNames, 68 | authenticatedUserReadPermission, 69 | useRepositoryPermissions, 70 | authenticatedUserCreateJobPermission, 71 | allowGithubWebHookPermission, 72 | allowCcTrayPermission, 73 | allowAnonymousReadPermission, 74 | allowAnonymousJobStatusPermission); 75 | } 76 | 77 | private final GithubRequireOrganizationMembershipACL rootACL; 78 | 79 | /* 80 | * (non-Javadoc) 81 | * @return rootAcl 82 | * @see hudson.security.AuthorizationStrategy#getRootACL() 83 | */ 84 | @NonNull 85 | @Override 86 | public ACL getRootACL() { 87 | return rootACL; 88 | } 89 | 90 | @NonNull 91 | public ACL getACL(@NonNull AbstractItem item) { 92 | if(item instanceof MultiBranchProject) { 93 | GithubRequireOrganizationMembershipACL githubACL = (GithubRequireOrganizationMembershipACL) getRootACL(); 94 | return githubACL.cloneForProject(item); 95 | } else { 96 | return getRootACL(); 97 | } 98 | } 99 | 100 | @NonNull 101 | public ACL getACL(@NonNull Job job) { 102 | if(job instanceof WorkflowJob && job.getProperty(BranchJobProperty.class) != null || job instanceof AbstractProject) { 103 | GithubRequireOrganizationMembershipACL githubACL = (GithubRequireOrganizationMembershipACL) getRootACL(); 104 | return githubACL.cloneForProject(job); 105 | } else { 106 | return getRootACL(); 107 | } 108 | } 109 | 110 | /** 111 | * (non-Javadoc) 112 | * @return groups 113 | * @see hudson.security.AuthorizationStrategy#getGroups() 114 | */ 115 | @NonNull 116 | @Override 117 | public Collection getGroups() { 118 | return Collections.emptyList(); 119 | } 120 | 121 | private Object readResolve() { 122 | return this; 123 | } 124 | 125 | /** 126 | * @return organizationNames 127 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#getOrganizationNameList() 128 | */ 129 | public String getOrganizationNames() { 130 | return StringUtils.join(rootACL.getOrganizationNameList().iterator(), ", "); 131 | } 132 | 133 | /** 134 | * @return adminUserNames 135 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#getAdminUserNameList() 136 | */ 137 | public String getAdminUserNames() { 138 | return StringUtils.join(rootACL.getAdminUserNameList().iterator(), ", "); 139 | } 140 | 141 | /** Set the agent username. We use a setter instead of a constructor to make this an optional field 142 | * to avoid a breaking change. 143 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#setAgentUserName(String) 144 | */ 145 | @DataBoundSetter 146 | public void setAgentUserName(String agentUserName) { 147 | rootACL.setAgentUserName(agentUserName); 148 | } 149 | 150 | /** 151 | * @return agentUserName 152 | * @see GithubRequireOrganizationMembershipACL#getAgentUserName() 153 | */ 154 | public String getAgentUserName() { 155 | return rootACL.getAgentUserName(); 156 | } 157 | 158 | /** 159 | * @return isUseRepositoryPermissions 160 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isUseRepositoryPermissions() 161 | */ 162 | public boolean isUseRepositoryPermissions() { 163 | return rootACL.isUseRepositoryPermissions(); 164 | } 165 | 166 | /** 167 | * @return isAuthenticatedUserCreateJobPermission 168 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isAuthenticatedUserCreateJobPermission() 169 | */ 170 | public boolean isAuthenticatedUserCreateJobPermission() { 171 | return rootACL.isAuthenticatedUserCreateJobPermission(); 172 | } 173 | 174 | /** 175 | * @return isAuthenticatedUserReadPermission 176 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isAuthenticatedUserReadPermission() 177 | */ 178 | public boolean isAuthenticatedUserReadPermission() { 179 | return rootACL.isAuthenticatedUserReadPermission(); 180 | } 181 | 182 | /** 183 | * @return isAllowGithubWebHookPermission 184 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isAllowGithubWebHookPermission() 185 | */ 186 | public boolean isAllowGithubWebHookPermission() { 187 | return rootACL.isAllowGithubWebHookPermission(); 188 | } 189 | 190 | /** 191 | * @return isAllowCcTrayPermission 192 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isAllowCcTrayPermission() 193 | */ 194 | public boolean isAllowCcTrayPermission() { 195 | return rootACL.isAllowCcTrayPermission(); 196 | } 197 | 198 | 199 | /** 200 | * @return isAllowAnonymousReadPermission 201 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isAllowAnonymousReadPermission() 202 | */ 203 | public boolean isAllowAnonymousReadPermission() { 204 | return rootACL.isAllowAnonymousReadPermission(); 205 | } 206 | 207 | /** 208 | * @return isAllowAnonymousJobStatusPermission 209 | * @see org.jenkinsci.plugins.GithubRequireOrganizationMembershipACL#isAllowAnonymousJobStatusPermission() 210 | */ 211 | public boolean isAllowAnonymousJobStatusPermission() { 212 | return rootACL.isAllowAnonymousJobStatusPermission(); 213 | } 214 | 215 | /** 216 | * Compare an object against this instance for equivalence. 217 | * @param object An object to campare this instance to. 218 | * @return true if the objects are the same instance and configuration. 219 | */ 220 | @Override 221 | public boolean equals(Object object){ 222 | if(object instanceof GithubAuthorizationStrategy) { 223 | GithubAuthorizationStrategy obj = (GithubAuthorizationStrategy) object; 224 | return this.getOrganizationNames().equals(obj.getOrganizationNames()) && 225 | this.getAdminUserNames().equals(obj.getAdminUserNames()) && 226 | this.getAgentUserName().equals(obj.getAgentUserName()) && 227 | this.isUseRepositoryPermissions() == obj.isUseRepositoryPermissions() && 228 | this.isAuthenticatedUserCreateJobPermission() == obj.isAuthenticatedUserCreateJobPermission() && 229 | this.isAuthenticatedUserReadPermission() == obj.isAuthenticatedUserReadPermission() && 230 | this.isAllowGithubWebHookPermission() == obj.isAllowGithubWebHookPermission() && 231 | this.isAllowCcTrayPermission() == obj.isAllowCcTrayPermission() && 232 | this.isAllowAnonymousReadPermission() == obj.isAllowAnonymousReadPermission() && 233 | this.isAllowAnonymousJobStatusPermission() == obj.isAllowAnonymousJobStatusPermission(); 234 | } else { 235 | return false; 236 | } 237 | } 238 | 239 | @Override 240 | public int hashCode() { 241 | return rootACL != null ? rootACL.hashCode() : 0; 242 | } 243 | 244 | @Extension 245 | public static final class DescriptorImpl extends 246 | Descriptor { 247 | 248 | public String getDisplayName() { 249 | return "GitHub Committer Authorization Strategy"; 250 | } 251 | 252 | public String getHelpFile() { 253 | return "/plugin/github-oauth/help/help-authorization-strategy.html"; 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubLogoutAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 CloudBees, Inc., James Nord, Sam Gleske 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 org.jenkinsci.plugins; 25 | 26 | import hudson.Extension; 27 | import hudson.model.UnprotectedRootAction; 28 | import hudson.security.SecurityRealm; 29 | import jenkins.model.Jenkins; 30 | import org.kohsuke.accmod.Restricted; 31 | import org.kohsuke.accmod.restrictions.NoExternalUse; 32 | 33 | /** 34 | * A page that shows a simple message when the user logs out. 35 | * This prevents a logout -> login loop when using this security realm and Anonymous does not have {@code Overall.READ} permission. 36 | */ 37 | @Extension 38 | public class GithubLogoutAction implements UnprotectedRootAction { 39 | 40 | /** The URL of the action. */ 41 | static final String POST_LOGOUT_URL = "githubLogout"; 42 | 43 | @Override 44 | public String getDisplayName() { 45 | return "Github Logout"; 46 | } 47 | 48 | @Override 49 | public String getIconFileName() { 50 | // hide it 51 | return null; 52 | } 53 | 54 | @Override 55 | public String getUrlName() { 56 | return POST_LOGOUT_URL; 57 | } 58 | 59 | @Restricted(NoExternalUse.class) // jelly only 60 | public String getGitHubURL() { 61 | Jenkins j = Jenkins.get(); 62 | SecurityRealm r = j.getSecurityRealm(); 63 | if (r instanceof GithubSecurityRealm) { 64 | GithubSecurityRealm ghsr = (GithubSecurityRealm) r; 65 | return ghsr.getGithubWebUri(); 66 | } 67 | // only called from the Jelly if the GithubSecurityRealm is set... 68 | return ""; 69 | } 70 | 71 | @Restricted(NoExternalUse.class) // jelly only 72 | public String getGitHubText() { 73 | Jenkins j = Jenkins.get(); 74 | SecurityRealm r = j.getSecurityRealm(); 75 | if (r instanceof GithubSecurityRealm) { 76 | GithubSecurityRealm ghsr = (GithubSecurityRealm) r; 77 | return (ghsr.getDescriptor().getDefaultGithubWebUri().equals(ghsr.getGithubWebUri()))? "GitHub" : "GitHub Enterprise"; 78 | } 79 | // only called from the Jelly if the GithubSecurityRealm is set... 80 | return ""; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubOAuthGroupDetails.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package org.jenkinsci.plugins; 5 | 6 | import hudson.security.GroupDetails; 7 | import java.io.IOException; 8 | import java.io.UncheckedIOException; 9 | import org.kohsuke.github.GHOrganization; 10 | import org.kohsuke.github.GHTeam; 11 | 12 | /** 13 | * @author Mike 14 | * 15 | */ 16 | public class GithubOAuthGroupDetails extends GroupDetails { 17 | 18 | private final GHOrganization org; 19 | private final GHTeam team; 20 | static final String ORG_TEAM_SEPARATOR = "*"; 21 | 22 | /** 23 | * Group based on organization name 24 | * @param org the github organization 25 | */ 26 | public GithubOAuthGroupDetails(GHOrganization org) { 27 | super(); 28 | this.org = org; 29 | this.team = null; 30 | } 31 | 32 | /** 33 | * Group based on team name 34 | * @param team the github team 35 | */ 36 | public GithubOAuthGroupDetails(GHTeam team) { 37 | super(); 38 | try { 39 | this.org = team.getOrganization(); 40 | } catch (IOException e) { 41 | throw new UncheckedIOException(e); 42 | } 43 | this.team = team; 44 | } 45 | 46 | /* (non-Javadoc) 47 | * @see hudson.security.GroupDetails#getName() 48 | */ 49 | @Override 50 | public String getName() { 51 | if (team != null) 52 | return org.getLogin() + ORG_TEAM_SEPARATOR + team.getName(); 53 | if (org != null) 54 | return org.getLogin(); 55 | return null; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubOAuthUserDetails.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package org.jenkinsci.plugins; 5 | 6 | import edu.umd.cs.findbugs.annotations.NonNull; 7 | import java.util.Collection; 8 | import org.springframework.security.core.GrantedAuthority; 9 | import org.springframework.security.core.userdetails.User; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | 12 | /** 13 | * @author Mike 14 | * 15 | */ 16 | public class GithubOAuthUserDetails extends User implements UserDetails { 17 | 18 | private static final long serialVersionUID = 1L; 19 | 20 | public GithubOAuthUserDetails(@NonNull String login, @NonNull Collection authorities) { 21 | super(login, "", true, true, true, true, authorities); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACL.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2011 Michael O'Cleirigh 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 | 26 | */ 27 | package org.jenkinsci.plugins; 28 | 29 | import edu.umd.cs.findbugs.annotations.NonNull; 30 | import edu.umd.cs.findbugs.annotations.Nullable; 31 | import hudson.model.AbstractItem; 32 | import hudson.model.AbstractProject; 33 | import hudson.model.Computer; 34 | import hudson.model.Describable; 35 | import hudson.model.Hudson; 36 | import hudson.model.Item; 37 | import hudson.plugins.git.GitSCM; 38 | import hudson.plugins.git.UserRemoteConfig; 39 | import hudson.security.ACL; 40 | import hudson.security.Permission; 41 | import java.net.URI; 42 | import java.util.LinkedList; 43 | import java.util.List; 44 | import java.util.logging.Logger; 45 | import jenkins.branch.MultiBranchProject; 46 | import jenkins.model.Jenkins; 47 | import jenkins.scm.api.SCMSource; 48 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 49 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 50 | import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; 51 | import org.kohsuke.stapler.Stapler; 52 | import org.kohsuke.stapler.StaplerRequest2; 53 | import org.springframework.security.core.Authentication; 54 | 55 | /** 56 | * @author Mike 57 | * 58 | */ 59 | public class GithubRequireOrganizationMembershipACL extends ACL { 60 | 61 | private static final Logger log = Logger 62 | .getLogger(GithubRequireOrganizationMembershipACL.class.getName()); 63 | 64 | private final List organizationNameList; 65 | private final List adminUserNameList; 66 | private String agentUserName; 67 | private final boolean authenticatedUserReadPermission; 68 | private final boolean useRepositoryPermissions; 69 | private final boolean authenticatedUserCreateJobPermission; 70 | private final boolean allowGithubWebHookPermission; 71 | private final boolean allowCcTrayPermission; 72 | private final boolean allowAnonymousReadPermission; 73 | private final boolean allowAnonymousJobStatusPermission; 74 | private final AbstractItem item; 75 | 76 | /* 77 | * (non-Javadoc) 78 | * 79 | * @see hudson.security.ACL#hasPermission(org.springframework.security.core.Authentication, 80 | * hudson.security.Permission) 81 | */ 82 | @Override 83 | public boolean hasPermission2(@NonNull Authentication a, @NonNull Permission permission) { 84 | if (a instanceof GithubAuthenticationToken) { 85 | if (!a.isAuthenticated()) 86 | return false; 87 | 88 | GithubAuthenticationToken authenticationToken = (GithubAuthenticationToken) a; 89 | 90 | String candidateName = a.getName(); 91 | 92 | if (adminUserNameList.contains(candidateName)) { 93 | // if they are an admin then they have permission 94 | log.finest("Granting Admin rights to user " + candidateName); 95 | return true; 96 | } 97 | 98 | // Streamline checks! 99 | 100 | // Are they trying to create something and we have that setting enabled? Return quickly! 101 | if (authenticatedUserCreateJobPermission && permission.equals(Item.CREATE)) { 102 | return true; 103 | } 104 | 105 | // Grant agent permissions to agent user 106 | if (candidateName.equalsIgnoreCase(agentUserName) && checkAgentUserPermission(permission)) { 107 | log.finest("Granting Agent Connect rights to user " + candidateName); 108 | return true; 109 | } 110 | 111 | // Are they trying to read? 112 | if (checkReadPermission(permission)) { 113 | // if we support authenticated read return early 114 | if (authenticatedUserReadPermission) { 115 | log.finest("Granting Authenticated User read permission to user " 116 | + candidateName); 117 | return true; 118 | } 119 | 120 | // allow them to read if in whitelisted orgs 121 | if (isInWhitelistedOrgs(authenticationToken)) { // 1 API call per-user, per-hour 122 | log.finest("Granting READ rights to user " 123 | + candidateName + " as a member of whitelisted organization"); 124 | return true; 125 | } 126 | // falls through to try to use repo permissions... 127 | } 128 | // allow them to BUILD if in whitelisted orgs 129 | else if (testBuildPermission(permission) && isInWhitelistedOrgs(authenticationToken)) { // 1 API call per-user, per-hour 130 | log.finest("Granting BUILD rights to user " 131 | + candidateName + " as a member of whitelisted organization"); 132 | return true; 133 | } 134 | 135 | // regardless of what permissions they're seeking, use the repo permissions to determine if possible 136 | if (useRepositoryPermissions && this.item != null) { 137 | String repositoryName = getRepositoryName(); 138 | 139 | if (repositoryName == null) { 140 | return false; 141 | } 142 | 143 | // best case 0 API calls (repo is public and that flag is cached, or user's repo listing is already cached with repo in it) 144 | // worst case, 2+ API calls to gather user repos (1 call per 100 for batch load, 1 add'l call if public repo not in list) 145 | return authenticationToken.hasRepositoryPermission(repositoryName, permission); 146 | } 147 | 148 | // no match. 149 | return false; 150 | } else { 151 | String authenticatedUserName = a.getName(); 152 | if (authenticatedUserName == null) { 153 | throw new IllegalArgumentException("Authentication must have a valid name"); 154 | } 155 | 156 | if (authenticatedUserName.equals(SYSTEM2.getPrincipal())) { 157 | // give system user full access 158 | log.finest("Granting Full rights to SYSTEM user."); 159 | return true; 160 | } 161 | 162 | // Grant agent permissions to agent user 163 | if (authenticatedUserName.equalsIgnoreCase(agentUserName) && checkAgentUserPermission(permission)) { 164 | log.finest("Granting Agent Connect rights to user " + authenticatedUserName); 165 | return true; 166 | } 167 | 168 | if (authenticatedUserName.equals("anonymous")) { 169 | if (checkJobStatusPermission(permission) && allowAnonymousJobStatusPermission) { 170 | return true; 171 | } 172 | 173 | if (checkReadPermission(permission)) { 174 | if (allowAnonymousReadPermission) { 175 | return true; 176 | } 177 | if (allowGithubWebHookPermission && 178 | (currentUriPathEquals("github-webhook") || 179 | currentUriPathEquals("github-webhook/"))) { 180 | log.finest("Granting READ access for github-webhook url: " + requestURI()); 181 | return true; 182 | } 183 | if (allowCcTrayPermission && currentUriPathEndsWithSegment("cc.xml")) { 184 | log.finest("Granting READ access for cctray url: " + requestURI()); 185 | return true; 186 | } 187 | log.finer("Denying anonymous READ permission to url: " + requestURI()); 188 | } 189 | return false; 190 | } 191 | 192 | if (adminUserNameList.contains(authenticatedUserName)) { 193 | // if they are an admin then they have all permissions 194 | log.finest("Granting Admin rights to user " + a.getName()); 195 | return true; 196 | } 197 | 198 | // else: 199 | // deny request 200 | // 201 | return false; 202 | } 203 | } 204 | 205 | @NonNull 206 | private boolean isInWhitelistedOrgs(@NonNull GithubAuthenticationToken authenticationToken) { 207 | return authenticationToken.isMemberOfAnyOrganizationInList(this.organizationNameList); 208 | } 209 | 210 | private boolean currentUriPathEquals( String specificPath ) { 211 | Jenkins jenkins = Jenkins.get(); 212 | String rootUrl = jenkins.getRootUrl(); 213 | if (rootUrl == null) { 214 | throw new IllegalStateException("Could not determine Jenkins URL"); 215 | } 216 | String requestUri = requestURI(); 217 | if (requestUri != null) { 218 | String basePath = URI.create(rootUrl).getPath(); 219 | return URI.create(requestUri).getPath().equals(basePath + specificPath); 220 | } else { 221 | return false; 222 | } 223 | } 224 | 225 | private boolean currentUriPathEndsWithSegment( String segment ) { 226 | String requestUri = requestURI(); 227 | if (requestUri != null) { 228 | return requestUri.substring(requestUri.lastIndexOf('/') + 1).equals(segment); 229 | } else { 230 | return false; 231 | } 232 | } 233 | 234 | @Nullable 235 | private String requestURI() { 236 | StaplerRequest2 currentRequest = Stapler.getCurrentRequest2(); 237 | return (currentRequest == null) ? null : currentRequest.getOriginalRequestURI(); 238 | } 239 | 240 | private boolean testBuildPermission(@NonNull Permission permission) { 241 | String id = permission.getId(); 242 | return id.equals("hudson.model.Hudson.Build") 243 | || id.equals("hudson.model.Item.Build"); 244 | } 245 | 246 | private boolean checkReadPermission(@NonNull Permission permission) { 247 | String id = permission.getId(); 248 | return (id.equals("hudson.model.Hudson.Read") 249 | || id.equals("hudson.model.Item.Workspace") 250 | || id.equals("hudson.model.Item.Discover") 251 | || id.equals("hudson.model.Item.Read")); 252 | } 253 | 254 | private boolean checkAgentUserPermission(@NonNull Permission permission) { 255 | return permission.equals(Hudson.READ) 256 | || permission.equals(Computer.CREATE) 257 | || permission.equals(Computer.CONNECT) 258 | || permission.equals(Computer.CONFIGURE); 259 | } 260 | 261 | private boolean checkJobStatusPermission(@NonNull Permission permission) { 262 | return permission.getId().equals("hudson.model.Item.ViewStatus"); 263 | } 264 | 265 | @Nullable 266 | private String getRepositoryName() { 267 | String repositoryName = null; 268 | String repoUrl = null; 269 | Describable scm = null; 270 | if (this.item instanceof WorkflowJob) { 271 | WorkflowJob project = (WorkflowJob) item; 272 | scm = project.getProperty(BranchJobProperty.class).getBranch().getScm(); 273 | } else if (this.item instanceof MultiBranchProject) { 274 | MultiBranchProject project = (MultiBranchProject) item; 275 | scm = (SCMSource) project.getSCMSources().get(0); 276 | } else if (this.item instanceof AbstractProject) { 277 | AbstractProject project = (AbstractProject) item; 278 | scm = project.getScm(); 279 | } 280 | if (scm instanceof GitHubSCMSource) { 281 | GitHubSCMSource git = (GitHubSCMSource) scm; 282 | repoUrl = git.getRemote(); 283 | } else if (scm instanceof GitSCM) { 284 | GitSCM git = (GitSCM) scm; 285 | List userRemoteConfigs = git.getUserRemoteConfigs(); 286 | if (!userRemoteConfigs.isEmpty()) { 287 | repoUrl = userRemoteConfigs.get(0).getUrl(); 288 | } 289 | } 290 | if (repoUrl != null) { 291 | GitHubRepositoryName githubRepositoryName = 292 | GitHubRepositoryName.create(repoUrl); 293 | if (githubRepositoryName != null) { 294 | repositoryName = githubRepositoryName.userName + "/" 295 | + githubRepositoryName.repositoryName; 296 | } 297 | } 298 | return repositoryName; 299 | } 300 | 301 | public GithubRequireOrganizationMembershipACL(String adminUserNames, 302 | String organizationNames, 303 | boolean authenticatedUserReadPermission, 304 | boolean useRepositoryPermissions, 305 | boolean authenticatedUserCreateJobPermission, 306 | boolean allowGithubWebHookPermission, 307 | boolean allowCcTrayPermission, 308 | boolean allowAnonymousReadPermission, 309 | boolean allowAnonymousJobStatusPermission) { 310 | super(); 311 | 312 | this.authenticatedUserReadPermission = authenticatedUserReadPermission; 313 | this.useRepositoryPermissions = useRepositoryPermissions; 314 | this.authenticatedUserCreateJobPermission = authenticatedUserCreateJobPermission; 315 | this.allowGithubWebHookPermission = allowGithubWebHookPermission; 316 | this.allowCcTrayPermission = allowCcTrayPermission; 317 | this.allowAnonymousReadPermission = allowAnonymousReadPermission; 318 | this.allowAnonymousJobStatusPermission = allowAnonymousJobStatusPermission; 319 | this.adminUserNameList = new LinkedList<>(); 320 | 321 | String[] parts = adminUserNames.split(","); 322 | 323 | for (String part : parts) { 324 | adminUserNameList.add(part.trim()); 325 | } 326 | 327 | this.organizationNameList = new LinkedList<>(); 328 | 329 | parts = organizationNames.split(","); 330 | 331 | for (String part : parts) { 332 | organizationNameList.add(part.trim()); 333 | } 334 | 335 | this.item = null; 336 | this.agentUserName = ""; // Initially blank - populated by a setter since this field is optional 337 | } 338 | 339 | public GithubRequireOrganizationMembershipACL cloneForProject(AbstractItem item) { 340 | GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL( 341 | this.adminUserNameList, 342 | this.organizationNameList, 343 | this.authenticatedUserReadPermission, 344 | this.useRepositoryPermissions, 345 | this.authenticatedUserCreateJobPermission, 346 | this.allowGithubWebHookPermission, 347 | this.allowCcTrayPermission, 348 | this.allowAnonymousReadPermission, 349 | this.allowAnonymousJobStatusPermission, 350 | item); 351 | acl.setAgentUserName(agentUserName); 352 | return acl; 353 | } 354 | 355 | public GithubRequireOrganizationMembershipACL(List adminUserNameList, 356 | List organizationNameList, 357 | boolean authenticatedUserReadPermission, 358 | boolean useRepositoryPermissions, 359 | boolean authenticatedUserCreateJobPermission, 360 | boolean allowGithubWebHookPermission, 361 | boolean allowCcTrayPermission, 362 | boolean allowAnonymousReadPermission, 363 | boolean allowAnonymousJobStatusPermission, 364 | AbstractItem item) { 365 | super(); 366 | 367 | this.adminUserNameList = adminUserNameList; 368 | this.organizationNameList = organizationNameList; 369 | this.authenticatedUserReadPermission = authenticatedUserReadPermission; 370 | this.useRepositoryPermissions = useRepositoryPermissions; 371 | this.authenticatedUserCreateJobPermission = authenticatedUserCreateJobPermission; 372 | this.allowGithubWebHookPermission = allowGithubWebHookPermission; 373 | this.allowCcTrayPermission = allowCcTrayPermission; 374 | this.allowAnonymousReadPermission = allowAnonymousReadPermission; 375 | this.allowAnonymousJobStatusPermission = allowAnonymousJobStatusPermission; 376 | this.item = item; 377 | } 378 | 379 | public List getOrganizationNameList() { 380 | return organizationNameList; 381 | } 382 | 383 | public List getAdminUserNameList() { 384 | return adminUserNameList; 385 | } 386 | 387 | public void setAgentUserName(String agentUserName) { 388 | this.agentUserName = agentUserName; 389 | } 390 | public String getAgentUserName() { return agentUserName; } 391 | 392 | public boolean isUseRepositoryPermissions() { 393 | return useRepositoryPermissions; 394 | } 395 | 396 | public boolean isAuthenticatedUserCreateJobPermission() { 397 | return authenticatedUserCreateJobPermission; 398 | } 399 | 400 | public boolean isAuthenticatedUserReadPermission() { 401 | return authenticatedUserReadPermission; 402 | } 403 | 404 | public boolean isAllowGithubWebHookPermission() { 405 | return allowGithubWebHookPermission; 406 | } 407 | 408 | public boolean isAllowCcTrayPermission() { 409 | return allowCcTrayPermission; 410 | } 411 | 412 | /** 413 | * @return the allowAnonymousReadPermission 414 | */ 415 | public boolean isAllowAnonymousReadPermission() { 416 | return allowAnonymousReadPermission; 417 | } 418 | 419 | public boolean isAllowAnonymousJobStatusPermission() { 420 | return allowAnonymousJobStatusPermission; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubSecretStorage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017, 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 org.jenkinsci.plugins; 25 | 26 | import edu.umd.cs.findbugs.annotations.CheckForNull; 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.model.User; 29 | import java.io.IOException; 30 | import java.util.logging.Level; 31 | import java.util.logging.Logger; 32 | 33 | public class GithubSecretStorage { 34 | 35 | private GithubSecretStorage(){ 36 | // no accessible constructor 37 | } 38 | 39 | public static boolean contains(@NonNull User user) { 40 | return user.getProperty(GithubAccessTokenProperty.class) != null; 41 | } 42 | 43 | public static @CheckForNull String retrieve(@NonNull User user) { 44 | GithubAccessTokenProperty property = user.getProperty(GithubAccessTokenProperty.class); 45 | if (property == null) { 46 | LOGGER.log(Level.FINE, "Cache miss for username: " + user.getId()); 47 | return null; 48 | } else { 49 | LOGGER.log(Level.FINE, "Token retrieved using cache for username: " + user.getId()); 50 | return property.getAccessToken().getPlainText(); 51 | } 52 | } 53 | 54 | public static void put(@NonNull User user, @NonNull String accessToken) { 55 | LOGGER.log(Level.FINE, "Populating the cache for username: " + user.getId()); 56 | try { 57 | user.addProperty(new GithubAccessTokenProperty(accessToken)); 58 | } catch (IOException e) { 59 | LOGGER.log(Level.WARNING, "Received an exception when trying to add the GitHub access token to the user: " + user.getId(), e); 60 | } 61 | } 62 | 63 | /** 64 | * Logger for debugging purposes. 65 | */ 66 | private static final Logger LOGGER = Logger.getLogger(GithubSecretStorage.class.getName()); 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2011-2016 Michael O'Cleirigh, James Nord, 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 | 26 | */ 27 | package org.jenkinsci.plugins; 28 | 29 | import com.thoughtworks.xstream.converters.ConversionException; 30 | import com.thoughtworks.xstream.converters.Converter; 31 | import com.thoughtworks.xstream.converters.MarshallingContext; 32 | import com.thoughtworks.xstream.converters.UnmarshallingContext; 33 | import com.thoughtworks.xstream.io.HierarchicalStreamReader; 34 | import com.thoughtworks.xstream.io.HierarchicalStreamWriter; 35 | import edu.umd.cs.findbugs.annotations.NonNull; 36 | import edu.umd.cs.findbugs.annotations.Nullable; 37 | import hudson.Extension; 38 | import hudson.ProxyConfiguration; 39 | import hudson.Util; 40 | import hudson.model.Descriptor; 41 | import hudson.model.User; 42 | import hudson.security.AbstractPasswordBasedSecurityRealm; 43 | import hudson.security.GroupDetails; 44 | import hudson.security.SecurityRealm; 45 | import hudson.security.UserMayOrMayNotExistException2; 46 | import hudson.tasks.Mailer; 47 | import hudson.util.Secret; 48 | import jakarta.servlet.http.HttpSession; 49 | import java.io.IOException; 50 | import java.io.UncheckedIOException; 51 | import java.net.InetSocketAddress; 52 | import java.net.Proxy; 53 | import java.security.SecureRandom; 54 | import java.util.Arrays; 55 | import java.util.Collection; 56 | import java.util.HashSet; 57 | import java.util.List; 58 | import java.util.Set; 59 | import java.util.logging.Level; 60 | import java.util.logging.Logger; 61 | import jenkins.model.Jenkins; 62 | import jenkins.security.SecurityListener; 63 | import org.apache.commons.lang.builder.HashCodeBuilder; 64 | import org.apache.http.HttpEntity; 65 | import org.apache.http.HttpHost; 66 | import org.apache.http.auth.AuthScope; 67 | import org.apache.http.auth.UsernamePasswordCredentials; 68 | import org.apache.http.client.CredentialsProvider; 69 | import org.apache.http.client.config.RequestConfig; 70 | import org.apache.http.client.methods.HttpPost; 71 | import org.apache.http.impl.client.BasicCredentialsProvider; 72 | import org.apache.http.impl.client.CloseableHttpClient; 73 | import org.apache.http.impl.client.HttpClientBuilder; 74 | import org.apache.http.impl.client.HttpClients; 75 | import org.apache.http.util.EntityUtils; 76 | import org.kohsuke.github.GHEmail; 77 | import org.kohsuke.github.GHMyself; 78 | import org.kohsuke.github.GHOrganization; 79 | import org.kohsuke.github.GHTeam; 80 | import org.kohsuke.github.GHUser; 81 | import org.kohsuke.stapler.DataBoundConstructor; 82 | import org.kohsuke.stapler.Header; 83 | import org.kohsuke.stapler.HttpRedirect; 84 | import org.kohsuke.stapler.HttpResponse; 85 | import org.kohsuke.stapler.HttpResponses; 86 | import org.kohsuke.stapler.QueryParameter; 87 | import org.kohsuke.stapler.StaplerRequest2; 88 | import org.springframework.security.authentication.AuthenticationManager; 89 | import org.springframework.security.authentication.AuthenticationServiceException; 90 | import org.springframework.security.authentication.BadCredentialsException; 91 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 92 | import org.springframework.security.core.Authentication; 93 | import org.springframework.security.core.AuthenticationException; 94 | import org.springframework.security.core.GrantedAuthority; 95 | import org.springframework.security.core.context.SecurityContextHolder; 96 | import org.springframework.security.core.userdetails.UserDetails; 97 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 98 | 99 | /** 100 | * 101 | * Implementation of the AbstractPasswordBasedSecurityRealm that uses github 102 | * oauth to verify the user can login. 103 | * 104 | * This is based on the MySQLSecurityRealm from the mysql-auth-plugin written by 105 | * Alex Ackerman. 106 | */ 107 | public class GithubSecurityRealm extends AbstractPasswordBasedSecurityRealm { 108 | private static final String DEFAULT_WEB_URI = "https://github.com"; 109 | private static final String DEFAULT_API_URI = "https://api.github.com"; 110 | private static final String DEFAULT_ENTERPRISE_API_SUFFIX = "/api/v3"; 111 | private static final String DEFAULT_OAUTH_SCOPES = "read:org,user:email,repo"; 112 | 113 | private String githubWebUri; 114 | private String githubApiUri; 115 | private String clientID; 116 | private Secret clientSecret; 117 | private String oauthScopes; 118 | private String[] myScopes; 119 | 120 | /** 121 | * @param githubWebUri The URI to the root of the web UI for GitHub or GitHub Enterprise, 122 | * including the protocol (e.g. https). 123 | * @param githubApiUri The URI to the root of the API for GitHub or GitHub Enterprise, 124 | * including the protocol (e.g. https). 125 | * @param clientID The client ID for the created OAuth Application. 126 | * @param clientSecret The client secret for the created GitHub OAuth Application. 127 | * @param oauthScopes A comma separated list of OAuth Scopes to request access to. 128 | */ 129 | @DataBoundConstructor 130 | public GithubSecurityRealm(String githubWebUri, 131 | String githubApiUri, 132 | String clientID, 133 | String clientSecret, 134 | String oauthScopes) { 135 | super(); 136 | 137 | this.githubWebUri = Util.fixEmptyAndTrim(githubWebUri); 138 | this.githubApiUri = Util.fixEmptyAndTrim(githubApiUri); 139 | this.clientID = Util.fixEmptyAndTrim(clientID); 140 | setClientSecret(Util.fixEmptyAndTrim(clientSecret)); 141 | this.oauthScopes = Util.fixEmptyAndTrim(oauthScopes); 142 | } 143 | 144 | private GithubSecurityRealm() { } 145 | 146 | /** 147 | * Tries to automatically determine the GitHub API URI based on 148 | * a GitHub Web URI. 149 | * 150 | * @param githubWebUri The URI to the root of the Web UI for GitHub or GitHub Enterprise. 151 | * @return The expected API URI for the given Web UI 152 | */ 153 | private String determineApiUri(String githubWebUri) { 154 | if(githubWebUri.equals(DEFAULT_WEB_URI)) { 155 | return DEFAULT_API_URI; 156 | } else { 157 | return githubWebUri + DEFAULT_ENTERPRISE_API_SUFFIX; 158 | } 159 | } 160 | 161 | /** 162 | * @param githubWebUri 163 | * the string representation of the URI to the root of the Web UI for 164 | * GitHub or GitHub Enterprise. 165 | */ 166 | private void setGithubWebUri(String githubWebUri) { 167 | this.githubWebUri = githubWebUri; 168 | } 169 | 170 | /** 171 | * @param clientID the clientID to set 172 | */ 173 | private void setClientID(String clientID) { 174 | this.clientID = clientID; 175 | } 176 | 177 | /** 178 | * @param clientSecret the clientSecret to set 179 | */ 180 | private void setClientSecret(String clientSecret) { 181 | this.clientSecret = Secret.fromString(clientSecret); 182 | } 183 | 184 | /** 185 | * @param oauthScopes the oauthScopes to set 186 | */ 187 | private void setOauthScopes(String oauthScopes) { 188 | this.oauthScopes = oauthScopes; 189 | } 190 | 191 | /** 192 | * Checks the security realm for a GitHub OAuth scope. 193 | * @param scope A scope to check for in the security realm. 194 | * @return true if security realm has the scope or false if it does not. 195 | */ 196 | public boolean hasScope(String scope) { 197 | if(this.myScopes == null) { 198 | this.myScopes = this.oauthScopes.split(","); 199 | Arrays.sort(this.myScopes); 200 | } 201 | return Arrays.binarySearch(this.myScopes, scope) >= 0; 202 | } 203 | 204 | /** 205 | * 206 | * @return the URI to the API root of GitHub or GitHub Enterprise. 207 | */ 208 | public String getGithubApiUri() { 209 | return githubApiUri; 210 | } 211 | 212 | /** 213 | * @param githubApiUri the URI to the API root of GitHub or GitHub Enterprise. 214 | */ 215 | private void setGithubApiUri(String githubApiUri) { 216 | this.githubApiUri = githubApiUri; 217 | } 218 | 219 | public static final class ConverterImpl implements Converter { 220 | 221 | public boolean canConvert(Class type) { 222 | return type == GithubSecurityRealm.class; 223 | } 224 | 225 | public void marshal(Object source, HierarchicalStreamWriter writer, 226 | MarshallingContext context) { 227 | GithubSecurityRealm realm = (GithubSecurityRealm) source; 228 | 229 | writer.startNode("githubWebUri"); 230 | writer.setValue(realm.getGithubWebUri()); 231 | writer.endNode(); 232 | 233 | writer.startNode("githubApiUri"); 234 | writer.setValue(realm.getGithubApiUri()); 235 | writer.endNode(); 236 | 237 | writer.startNode("clientID"); 238 | writer.setValue(realm.getClientID()); 239 | writer.endNode(); 240 | 241 | writer.startNode("clientSecret"); 242 | writer.setValue(realm.getClientSecret().getEncryptedValue()); 243 | writer.endNode(); 244 | 245 | writer.startNode("oauthScopes"); 246 | writer.setValue(realm.getOauthScopes()); 247 | writer.endNode(); 248 | 249 | } 250 | 251 | public Object unmarshal(HierarchicalStreamReader reader, 252 | UnmarshallingContext context) { 253 | 254 | GithubSecurityRealm realm = new GithubSecurityRealm(); 255 | 256 | String node; 257 | String value; 258 | 259 | while (reader.hasMoreChildren()) { 260 | reader.moveDown(); 261 | node = reader.getNodeName(); 262 | value = reader.getValue(); 263 | setValue(realm, node, value); 264 | reader.moveUp(); 265 | } 266 | 267 | if (realm.getGithubWebUri() == null) { 268 | realm.setGithubWebUri(DEFAULT_WEB_URI); 269 | } 270 | 271 | if (realm.getGithubApiUri() == null) { 272 | realm.setGithubApiUri(DEFAULT_API_URI); 273 | } 274 | 275 | return realm; 276 | } 277 | 278 | private void setValue(GithubSecurityRealm realm, String node, 279 | String value) { 280 | if (node.equalsIgnoreCase("clientid")) { 281 | realm.setClientID(value); 282 | } else if (node.equalsIgnoreCase("clientsecret")) { 283 | realm.setClientSecret(value); 284 | } else if (node.equalsIgnoreCase("githubweburi")) { 285 | realm.setGithubWebUri(value); 286 | } else if (node.equalsIgnoreCase("githuburi")) { // backwards compatibility for old field 287 | realm.setGithubWebUri(value); 288 | String apiUrl = realm.determineApiUri(value); 289 | realm.setGithubApiUri(apiUrl); 290 | } else if (node.equalsIgnoreCase("githubapiuri")) { 291 | realm.setGithubApiUri(value); 292 | } else if (node.equalsIgnoreCase("oauthscopes")) { 293 | realm.setOauthScopes(value); 294 | } else { 295 | throw new ConversionException("Invalid node value = " + node); 296 | } 297 | } 298 | 299 | } 300 | 301 | /** 302 | * @return the uri to the web root of Github (varies for Github Enterprise Edition) 303 | */ 304 | public String getGithubWebUri() { 305 | return githubWebUri; 306 | } 307 | 308 | /** 309 | * @deprecated use {@link org.jenkinsci.plugins.GithubSecurityRealm#getGithubWebUri()} instead. 310 | * @return the uri to the web root of Github (varies for Github Enterprise Edition) 311 | */ 312 | @Deprecated 313 | public String getGithubUri() { 314 | return getGithubWebUri(); 315 | } 316 | 317 | /** 318 | * @return the clientID 319 | */ 320 | public String getClientID() { 321 | return clientID; 322 | } 323 | 324 | /** 325 | * @return the clientSecret 326 | */ 327 | public Secret getClientSecret() { 328 | return clientSecret; 329 | } 330 | 331 | /** 332 | * @return the oauthScopes 333 | */ 334 | public String getOauthScopes() { 335 | return oauthScopes; 336 | } 337 | 338 | public HttpResponse doCommenceLogin(StaplerRequest2 request, @QueryParameter String from, @Header("Referer") final String referer) 339 | throws IOException { 340 | // https://tools.ietf.org/html/rfc6749#section-10.10 dictates that probability that an attacker guesses the string 341 | // SHOULD be less than or equal to 2^(-160) and our Strings consist of 65 chars. (65^27 ~= 2^160) 342 | final String state = getSecureRandomString(27); 343 | String redirectOnFinish; 344 | if (from != null && Util.isSafeToRedirectTo(from)) { 345 | redirectOnFinish = from; 346 | } else if (referer != null && (referer.startsWith(Jenkins.get().getRootUrl()) || Util.isSafeToRedirectTo(referer))) { 347 | redirectOnFinish = referer; 348 | } else { 349 | redirectOnFinish = Jenkins.get().getRootUrl(); 350 | } 351 | 352 | request.getSession().setAttribute(REFERER_ATTRIBUTE, redirectOnFinish); 353 | request.getSession().setAttribute(STATE_ATTRIBUTE, state); 354 | 355 | Set scopes = new HashSet<>(); 356 | for (GitHubOAuthScope s : Jenkins.get().getExtensionList(GitHubOAuthScope.class)) { 357 | scopes.addAll(s.getScopesToRequest()); 358 | } 359 | String suffix=""; 360 | if (!scopes.isEmpty()) { 361 | suffix = "&scope=" + String.join(",", scopes) + "&state=" + state; 362 | } else { 363 | // We need repo scope in order to access private repos 364 | // See https://developer.github.com/v3/oauth/#scopes 365 | suffix = "&scope=" + oauthScopes +"&state="+state; 366 | } 367 | 368 | return new HttpRedirect(githubWebUri + "/login/oauth/authorize?client_id=" 369 | + clientID + suffix); 370 | } 371 | 372 | /** 373 | * This is where the user comes back to at the end of the OAuth redirect 374 | * ping-pong. 375 | */ 376 | public HttpResponse doFinishLogin(StaplerRequest2 request) 377 | throws IOException { 378 | String code = request.getParameter("code"); 379 | String state = request.getParameter(STATE_ATTRIBUTE); 380 | String referer = (String)request.getSession().getAttribute(REFERER_ATTRIBUTE); 381 | String expectedState = (String)request.getSession().getAttribute(STATE_ATTRIBUTE); 382 | 383 | if (code == null || code.trim().length() == 0) { 384 | LOGGER.info("doFinishLogin: missing code."); 385 | return HttpResponses.redirectToContextRoot(); 386 | } 387 | 388 | if (state == null){ 389 | LOGGER.info("doFinishLogin: missing state parameter from Github response."); 390 | return HttpResponses.redirectToContextRoot(); 391 | } else if (expectedState == null){ 392 | LOGGER.info("doFinishLogin: missing state parameter from user's session."); 393 | return HttpResponses.redirectToContextRoot(); 394 | } else if (!state.equals(expectedState)){ 395 | LOGGER.info("state parameter value ["+state+"] does not match the expected one ["+expectedState+"]"); 396 | return HttpResponses.redirectToContextRoot(); 397 | } 398 | 399 | 400 | String accessToken = getAccessToken(code); 401 | 402 | if (accessToken != null && accessToken.trim().length() > 0) { 403 | // only set the access token if it exists. 404 | GithubAuthenticationToken auth = new GithubAuthenticationToken(accessToken, getGithubApiUri(), true); 405 | 406 | HttpSession session = request.getSession(false); 407 | if(session != null){ 408 | // avoid session fixation 409 | session.invalidate(); 410 | } 411 | request.getSession(true); 412 | 413 | SecurityContextHolder.getContext().setAuthentication(auth); 414 | 415 | GHMyself self = auth.getMyself(); 416 | User u = User.current(); 417 | if (u == null) { 418 | throw new IllegalStateException("Can't find user"); 419 | } 420 | 421 | GithubSecretStorage.put(u, accessToken); 422 | 423 | u.setFullName(self.getName()); 424 | // Set email from github only if empty 425 | if (!u.getProperty(Mailer.UserProperty.class).hasExplicitlyConfiguredAddress()) { 426 | if(hasScope("user") || hasScope("user:email")) { 427 | String primary_email = null; 428 | for(GHEmail e : self.getEmails2()) { 429 | if(e.isPrimary()) { 430 | primary_email = e.getEmail(); 431 | } 432 | } 433 | if(primary_email != null) { 434 | u.addProperty(new Mailer.UserProperty(primary_email)); 435 | } 436 | } else { 437 | u.addProperty(new Mailer.UserProperty(auth.getGitHub().getMyself().getEmail())); 438 | } 439 | } 440 | 441 | SecurityListener.fireAuthenticated2(new GithubOAuthUserDetails(self.getLogin(), auth.getAuthorities())); 442 | 443 | // While LastGrantedAuthorities are triggered by that event, we cannot trigger it there 444 | // or modifications in organizations will be not reflected when using API Token, due to that caching 445 | // SecurityListener.fireLoggedIn(self.getLogin()); 446 | } else { 447 | LOGGER.info("Github did not return an access token."); 448 | } 449 | 450 | if (referer!=null) return HttpResponses.redirectTo(referer); 451 | return HttpResponses.redirectToContextRoot(); // referer should be always there, but be defensive 452 | } 453 | 454 | @Nullable 455 | private String getAccessToken(@NonNull String code) throws IOException { 456 | String content; 457 | HttpPost httpost = new HttpPost(githubWebUri 458 | + "/login/oauth/access_token?" + "client_id=" + clientID + "&" 459 | + "client_secret=" + clientSecret.getPlainText() + "&" + "code=" + code); 460 | 461 | try (CloseableHttpClient httpClient = configureClientWithProxy(httpost)) { 462 | org.apache.http.HttpResponse response = httpClient.execute(httpost); 463 | HttpEntity entity = response.getEntity(); 464 | content = EntityUtils.toString(entity); 465 | 466 | } 467 | String[] parts = content.split("&"); 468 | for (String part : parts) { 469 | if (part.startsWith("access_token=")) { 470 | String[] tokenParts = part.split("="); 471 | return tokenParts[1]; 472 | } 473 | } 474 | return null; 475 | } 476 | 477 | private CloseableHttpClient configureClientWithProxy(HttpPost postLocation) { 478 | ProxyConfiguration proxyConfiguration = Jenkins.get().proxy; 479 | 480 | if (proxyConfiguration == null) return HttpClients.createDefault(); 481 | 482 | HttpHost proxyHost = getProxy(proxyConfiguration, postLocation.getURI().getHost()); 483 | 484 | HttpClientBuilder httpClientBuilder = HttpClients.custom(); 485 | 486 | if (proxyHost != null) { 487 | RequestConfig requestConfig = RequestConfig.custom() 488 | .setProxy(proxyHost) 489 | .build(); 490 | 491 | postLocation.setConfig(requestConfig); 492 | 493 | if(proxyConfiguration.getUserName() != null && proxyConfiguration.getSecretPassword() != null ) { 494 | CredentialsProvider credsProvider = new BasicCredentialsProvider(); 495 | credsProvider.setCredentials( 496 | new AuthScope(proxyHost.getHostName(), proxyHost.getPort()), 497 | new UsernamePasswordCredentials(proxyConfiguration.getUserName(), proxyConfiguration.getSecretPassword().getPlainText())); 498 | httpClientBuilder.setDefaultCredentialsProvider(credsProvider); 499 | } 500 | } 501 | 502 | return httpClientBuilder.build(); 503 | } 504 | 505 | 506 | /** 507 | * Generates a random URL Safe String of n characters 508 | */ 509 | private String getSecureRandomString(int n) { 510 | if (n < 0){ 511 | throw new IllegalArgumentException("Length must be a positive integer"); 512 | } 513 | // See RFC3986 514 | final String urlSafeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_"; 515 | final StringBuilder sb = new StringBuilder(); 516 | for (int i = 0; i < n ; i++){ 517 | sb.append(urlSafeChars.charAt(SECURE_RANDOM.nextInt(urlSafeChars.length()))); 518 | } 519 | return sb.toString(); 520 | } 521 | /** 522 | * Returns the proxy to be used when connecting to the given URI. 523 | */ 524 | private HttpHost getProxy(ProxyConfiguration proxy, String host) { 525 | Proxy p = proxy.createProxy(host); 526 | switch (p.type()) { 527 | case DIRECT: 528 | return null; // no proxy 529 | case HTTP: 530 | InetSocketAddress sa = (InetSocketAddress) p.address(); 531 | return new HttpHost(sa.getHostName(),sa.getPort()); 532 | case SOCKS: 533 | default: 534 | return null; // not supported yet 535 | } 536 | } 537 | 538 | /* 539 | * (non-Javadoc) 540 | * 541 | * @see hudson.security.SecurityRealm#allowsSignup() 542 | */ 543 | @Override 544 | public boolean allowsSignup() { 545 | return false; 546 | } 547 | 548 | @Override 549 | public SecurityComponents createSecurityComponents() { 550 | return new SecurityComponents(new AuthenticationManager() { 551 | 552 | public Authentication authenticate(Authentication authentication) 553 | throws AuthenticationException { 554 | if (authentication instanceof GithubAuthenticationToken) 555 | return authentication; 556 | if (authentication instanceof UsernamePasswordAuthenticationToken) 557 | try { 558 | UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; 559 | GithubAuthenticationToken github = new GithubAuthenticationToken(token.getCredentials().toString(), getGithubApiUri()); 560 | SecurityContextHolder.getContext().setAuthentication(github); 561 | 562 | User user = User.getById(token.getName(), false); 563 | if(user != null){ 564 | GithubSecretStorage.put(user, token.getCredentials().toString()); 565 | } 566 | 567 | SecurityListener.fireAuthenticated2(new GithubOAuthUserDetails(token.getName(), github.getAuthorities())); 568 | 569 | return github; 570 | } catch (IOException e) { 571 | throw new RuntimeException(e); 572 | } 573 | throw new BadCredentialsException( 574 | "Unexpected authentication type: " + authentication); 575 | } 576 | }, GithubSecurityRealm.this::loadUserByUsername2); 577 | } 578 | 579 | @Override 580 | protected GithubOAuthUserDetails authenticate2(String username, String password) throws AuthenticationException { 581 | try { 582 | GithubAuthenticationToken github = new GithubAuthenticationToken(password, getGithubApiUri()); 583 | if(username.equals(github.getPrincipal())) { 584 | SecurityContextHolder.getContext().setAuthentication(github); 585 | return github.getUserDetails(username); 586 | } 587 | } catch (IOException e) { 588 | throw new RuntimeException(e); 589 | } 590 | throw new BadCredentialsException("Invalid GitHub username or personal access token: " + username); 591 | } 592 | 593 | @Override 594 | public String getLoginUrl() { 595 | return "securityRealm/commenceLogin"; 596 | } 597 | 598 | @Override 599 | protected String getPostLogOutUrl2(StaplerRequest2 req, Authentication auth) { 600 | // if we just redirect to the root and anonymous does not have Overall read then we will start a login all over again. 601 | // we are actually anonymous here as the security context has been cleared 602 | Jenkins j = Jenkins.get(); 603 | if (j.hasPermission(Jenkins.READ)) { 604 | return super.getPostLogOutUrl2(req, auth); 605 | } 606 | return req.getContextPath()+ "/" + GithubLogoutAction.POST_LOGOUT_URL; 607 | } 608 | 609 | @Extension 610 | public static final class DescriptorImpl extends Descriptor { 611 | 612 | @Override 613 | public String getHelpFile() { 614 | return "/plugin/github-oauth/help/help-security-realm.html"; 615 | } 616 | 617 | @Override 618 | public String getDisplayName() { 619 | return "Github Authentication Plugin"; 620 | } 621 | 622 | public String getDefaultGithubWebUri() { 623 | return DEFAULT_WEB_URI; 624 | } 625 | 626 | public String getDefaultGithubApiUri() { 627 | return DEFAULT_API_URI; 628 | } 629 | 630 | public String getDefaultOauthScopes() { 631 | return DEFAULT_OAUTH_SCOPES; 632 | } 633 | 634 | public DescriptorImpl() { 635 | super(); 636 | // TODO Auto-generated constructor stub 637 | } 638 | 639 | public DescriptorImpl(Class clazz) { 640 | super(clazz); 641 | // TODO Auto-generated constructor stub 642 | } 643 | 644 | } 645 | 646 | // Overridden for better type safety. 647 | // If your plugin doesn't really define any property on Descriptor, 648 | // you don't have to do this. 649 | @Override 650 | public DescriptorImpl getDescriptor() { 651 | return (DescriptorImpl) super.getDescriptor(); 652 | } 653 | 654 | /** 655 | * 656 | * @param username username to lookup 657 | * @return userDetails 658 | */ 659 | @Override 660 | public UserDetails loadUserByUsername2(String username) 661 | throws UsernameNotFoundException { 662 | //username is in org*team format 663 | if(username.contains(GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR)) { 664 | throw new UsernameNotFoundException("Using org*team format instead of username: " + username); 665 | } 666 | 667 | User localUser = User.getById(username, false); 668 | 669 | Authentication token = SecurityContextHolder.getContext().getAuthentication(); 670 | 671 | try { 672 | if(localUser != null && GithubSecretStorage.contains(localUser)) { 673 | String accessToken = GithubSecretStorage.retrieve(localUser); 674 | token = new GithubAuthenticationToken(accessToken, getGithubApiUri()); 675 | } 676 | } catch(IOException | UsernameNotFoundException e) { 677 | if(e instanceof IOException) { 678 | throw new UserMayOrMayNotExistException2("Could not connect to GitHub API server, target URL = " + getGithubApiUri(), e); 679 | } else { 680 | // user not found so continuing normally re-using the current context holder 681 | LOGGER.log(Level.FINE, "Attempted to impersonate " + username + " but token in user property was invalid."); 682 | } 683 | } 684 | 685 | GithubAuthenticationToken authToken; 686 | 687 | if (token instanceof GithubAuthenticationToken) { 688 | authToken = (GithubAuthenticationToken) token; 689 | } else { 690 | throw new UserMayOrMayNotExistException2("Unexpected authentication type: " + token); 691 | } 692 | 693 | /** 694 | * Always lookup the local user first. If we can't resolve it then we can burn an API request to Github for this user 695 | * Taken from hudson.security.HudsonPrivateSecurityRealm#loadUserByUsername(java.lang.String) 696 | */ 697 | if (localUser != null) { 698 | GHUser user; 699 | try { 700 | user = authToken.loadUser(username); 701 | } catch (IOException e) { 702 | throw new UncheckedIOException(e); 703 | } 704 | Collection authorities; 705 | if (user != null) { 706 | authorities = authToken.getAuthorities(); 707 | } else { 708 | authorities = List.of(); 709 | } 710 | return new GithubOAuthUserDetails(username, authorities); 711 | } 712 | 713 | try { 714 | GithubOAuthUserDetails userDetails = authToken.getUserDetails(username); 715 | if (userDetails == null) { 716 | throw new UsernameNotFoundException("Unknown user: " + username); 717 | } 718 | 719 | // Check the username is not an homonym of an organization 720 | GHOrganization ghOrg = authToken.loadOrganization(username); 721 | if (ghOrg != null) { 722 | throw new UsernameNotFoundException("user(" + username + ") is also an organization"); 723 | } 724 | 725 | return userDetails; 726 | } catch (IOException | Error e) { 727 | throw new AuthenticationServiceException("loadUserByUsername (username=" + username +")", e); 728 | } 729 | } 730 | 731 | /** 732 | * Compare an object against this instance for equivalence. 733 | * @param object An object to campare this instance to. 734 | * @return true if the objects are the same instance and configuration. 735 | */ 736 | @Override 737 | public boolean equals(Object object){ 738 | if(object instanceof GithubSecurityRealm) { 739 | GithubSecurityRealm obj = (GithubSecurityRealm) object; 740 | return this.getGithubWebUri().equals(obj.getGithubWebUri()) && 741 | this.getGithubApiUri().equals(obj.getGithubApiUri()) && 742 | this.getClientID().equals(obj.getClientID()) && 743 | this.getClientSecret().equals(obj.getClientSecret()) && 744 | this.getOauthScopes().equals(obj.getOauthScopes()); 745 | } else { 746 | return false; 747 | } 748 | } 749 | 750 | @Override 751 | public int hashCode() { 752 | return new HashCodeBuilder() 753 | .append(this.getGithubWebUri()) 754 | .append(this.getGithubApiUri()) 755 | .append(this.getClientID()) 756 | .append(this.getClientSecret()) 757 | .append(this.getOauthScopes()) 758 | .toHashCode(); 759 | } 760 | 761 | /** 762 | * 763 | * @param groupName groupName to look up 764 | * @return groupDetails 765 | */ 766 | @Override 767 | public GroupDetails loadGroupByGroupname2(String groupName, boolean fetchMembers) 768 | throws UsernameNotFoundException { 769 | GithubAuthenticationToken authToken = (GithubAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); 770 | 771 | if(authToken == null) 772 | throw new UsernameNotFoundException("No known group: " + groupName); 773 | 774 | try { 775 | int idx = groupName.indexOf(GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR); 776 | if (idx > -1 && groupName.length() > idx + 1) { // groupName = "GHOrganization*GHTeam" 777 | String orgName = groupName.substring(0, idx); 778 | String teamName = groupName.substring(idx + 1); 779 | LOGGER.config(String.format("Lookup for team %s in organization %s", teamName, orgName)); 780 | GHTeam ghTeam = authToken.loadTeam(orgName, teamName); 781 | if (ghTeam == null) { 782 | throw new UsernameNotFoundException("Unknown GitHub team: " + teamName + " in organization " 783 | + orgName); 784 | } 785 | return new GithubOAuthGroupDetails(ghTeam); 786 | } else { // groupName = "GHOrganization" 787 | GHOrganization ghOrg = authToken.loadOrganization(groupName); 788 | if (ghOrg == null) { 789 | throw new UsernameNotFoundException("Unknown GitHub organization: " + groupName); 790 | } 791 | return new GithubOAuthGroupDetails(ghOrg); 792 | } 793 | } catch (Error e) { 794 | throw new AuthenticationServiceException("loadGroupByGroupname (groupname=" + groupName + ")", e); 795 | } 796 | } 797 | 798 | /** 799 | * Logger for debugging purposes. 800 | */ 801 | private static final Logger LOGGER = Logger.getLogger(GithubSecurityRealm.class.getName()); 802 | 803 | private static final String REFERER_ATTRIBUTE = GithubSecurityRealm.class.getName()+".referer"; 804 | private static final String STATE_ATTRIBUTE = "state"; 805 | 806 | private static final SecureRandom SECURE_RANDOM = new SecureRandom(); 807 | 808 | } 809 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/JenkinsProxyAuthenticator.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins; 2 | 3 | import edu.umd.cs.findbugs.annotations.CheckForNull; 4 | import edu.umd.cs.findbugs.annotations.NonNull; 5 | import hudson.ProxyConfiguration; 6 | import hudson.util.Secret; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | import okhttp3.Authenticator; 10 | import okhttp3.Challenge; 11 | import okhttp3.Credentials; 12 | import okhttp3.Request; 13 | import okhttp3.Response; 14 | import okhttp3.Route; 15 | 16 | public class JenkinsProxyAuthenticator implements Authenticator { 17 | 18 | 19 | private static final Logger LOGGER = 20 | Logger.getLogger(JenkinsProxyAuthenticator.class.getName()); 21 | 22 | private final ProxyConfiguration proxy; 23 | 24 | public JenkinsProxyAuthenticator(ProxyConfiguration proxy) { 25 | this.proxy = proxy; 26 | } 27 | 28 | @CheckForNull 29 | @Override 30 | public Request authenticate(@CheckForNull Route route, @NonNull Response response) { 31 | 32 | if (response.request().header("Proxy-Authorization") != null) { 33 | return null; // Give up since we already tried to authenticate 34 | } 35 | 36 | if (response.challenges().isEmpty()) { 37 | // Proxy does not require authentication 38 | return null; 39 | } 40 | 41 | // Refuse pre-emptive challenge 42 | if (response.challenges().size() == 1) { 43 | Challenge challenge = response.challenges().get(0); 44 | if (challenge.scheme().equalsIgnoreCase("OkHttp-Preemptive")) { 45 | return null; 46 | } 47 | } 48 | 49 | for (Challenge challenge : response.challenges()) { 50 | if (challenge.scheme().equalsIgnoreCase("Basic")) { 51 | String username = proxy.getUserName(); 52 | Secret password = proxy.getSecretPassword(); 53 | if (username != null && password != null) { 54 | String credentials = Credentials.basic(username, password.getPlainText()); 55 | return response.request() 56 | .newBuilder() 57 | .header("Proxy-Authorization", credentials) 58 | .build(); 59 | } else { 60 | LOGGER.log( 61 | Level.WARNING, 62 | "Proxy requires Basic authentication but no username and password have been configured for the proxy"); 63 | } 64 | break; 65 | } 66 | } 67 | 68 | LOGGER.log( 69 | Level.WARNING, 70 | "Proxy requires authentication, but does not support Basic authentication"); 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Authentication plugin using GitHub OAuth to provide authentication and authorization capabilities for GitHub and GitHub Enterprise. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/GithubAuthorizationStrategy/config.jelly: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/GithubLogoutAction/index.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

You are now logged out of Jenkins. However, you have not been logged out of ${it.gitHubText}.

32 |

Have a nice day!

33 |
34 | 35 |

Whoa there...

36 |

You are not logged out - don't run away!

37 |

Whilst you should have been logged out, you are not actually logged out. Press the logout button to try logging out again.

38 |
39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/GithubSecurityRealm/config.jelly: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/webapp/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-oauth-plugin/f5ddfba7349dac0eec130482ca98b6ab236f0c49/src/main/webapp/github.png -------------------------------------------------------------------------------- /src/main/webapp/help/auth/admin-user-names-help.html: -------------------------------------------------------------------------------- 1 |
2 | Comma Separated list of user names that when authenticated should be given administrator rights. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help/auth/agent-user-name-help.html: -------------------------------------------------------------------------------- 1 |
2 | If you are using inbound Jenkins agents, this is the user that is used for authenticating agents. This user will receive rights to create, connect and configure agents. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help/auth/grant-create-job-to-authenticated-help.html: -------------------------------------------------------------------------------- 1 |
2 | If checked will grant the create job permission to all authenticated github users even those that aren't members of the declared organizations. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help/auth/grant-read-to-anonymous-help.html: -------------------------------------------------------------------------------- 1 |
2 | If checked will grant the READ permission to everyone that connects to the Jenkins instance. Anyone will be able to see all the jobs and related state but only authenticated users will be able to BUILD the jobs. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help/auth/grant-read-to-authenticated-help.html: -------------------------------------------------------------------------------- 1 |
2 | If checked will grant the read permission to all authenticated github users even those that aren't members of the declared organizations. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help/auth/grant-read-to-cctray-help.html: -------------------------------------------------------------------------------- 1 |
2 | Open a hole in security to allow unauthenticated access to URLs ending with /cc.xml. 3 | This URI provides monitoring capability 4 | to a range of desktop clients. 5 | 6 | Enabling this option reveals limited information about your build to the whole world. Use with care. 7 |
-------------------------------------------------------------------------------- /src/main/webapp/help/auth/grant-read-to-github-webhook-help.html: -------------------------------------------------------------------------------- 1 |
2 | The github-plugin has a web hook trigger that can be used to notify Jenkins when a push has occurred and that a build should also happen.
By enabling this permission you grant anonymous external READ access to the /github-webhook URL so that the request can be received. 3 |

4 | The github-plugin checks that a change has actually occurred so this should be safe to enable. 5 | 6 |

-------------------------------------------------------------------------------- /src/main/webapp/help/auth/grant-viewstatus-to-anonymous-help.html: -------------------------------------------------------------------------------- 1 |
2 | If checked will grant the ViewStatus permission to everyone that connects to the Jenkins instance. Anyone will be able to see the status of a job, but nothing else. 3 | This is useful especially for plugins like Embeddable Build Status Plugin. 4 |
-------------------------------------------------------------------------------- /src/main/webapp/help/auth/organization-names-help.html: -------------------------------------------------------------------------------- 1 |
2 | Comma Separated list of organization names that if a user is registered with will have job view and build rights. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help/auth/use-repository-permissions-help.html: -------------------------------------------------------------------------------- 1 |
2 | If checked will use github repository permissions to determine jenkins permissions for each project. 3 |
    4 |
  • Public projects - all authenticated users can READ. Only collaborators can BUILD, EDIT, CONFIGURE, CANCEL or DELETE. 5 |
  • Private projects, only collaborators can READ, BUILD, EDIT, CONFIGURE, CANCEL or DELETE 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-authorization-strategy.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Requires the Github Authentication Plugin to be used as the authentication source. 4 |

5 | We use the OAuth token for each authenticated github user to interact with the Github API to determine the level of access each user should have. 6 | 7 |

8 | 9 | We grant READ and BUILD job permissions to an authenticated user if they are a member in at least one named organization.

10 | 11 | We also support defining a set of Jenkins Admin users and whether or not any authenticated user can have READ access to the jobs. 12 | 13 |

14 | -------------------------------------------------------------------------------- /src/main/webapp/help/help-security-realm.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Authentication requires a valid OAuth application to be registered with github.com. 4 |
5 | Using your github account you can create a new application registration using this link: 6 | https://github.com/settings/applications/new 7 |
8 | 9 | You can use any name you want but bear in mind this is what github will show to the users when they authenticate and authorize this plugin to act on their behalf. 10 |
11 | 12 | The entry should look like this: 13 | 14 | 15 | 16 | 17 |
Main URLhttp://127.0.0.1:8080
Callback URLhttp://127.0.0.1:8080/securityRealm/finishLogin
18 | 19 | With 127.0.0.1:8080 replaced with the public hostname and port of your jenkins instance. If the server is private it should be a url that the browser will know how to get to or the IP that the Jenkins instance is being accessed through. 20 | 21 |

22 | The /securityRealm/finishLogin is required as this is where github will redirect the user for the second part of the autorization process. 23 | 24 |

25 | -------------------------------------------------------------------------------- /src/main/webapp/help/realm/client-id-help.html: -------------------------------------------------------------------------------- 1 |
2 | The Client ID hash from the github application entry being configured. See https://github.com/settings/applications. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help/realm/client-secret-help.html: -------------------------------------------------------------------------------- 1 |
2 | The Client Secret hash from the github application entry being configured. See https://github.com/settings/applications. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help/realm/github-api-uri-help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Normally this value stays as-is. If you are using GitHub Enterprise 4 | then you should enter the URI to the API root of your GitHub installation 5 | (e.g. https://github.company.com/api/v3). 6 |

7 | 8 | Notes: 9 | 10 |

    11 |
  1. The https:// / http:// part needs to be specified.
  2. 12 |
  3. There should not be any trailing slash (/)
  4. 13 |
  5. For GitHub Enterprise, the API URI is usually the same as the Web URI, with /api/v3 appended.
  6. 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/main/webapp/help/realm/github-web-uri-help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Normally this value stays as-is. If you are using GitHub Enterprise 4 | then you should enter the URL to the web UI root of your GitHub installation 5 | (e.g. https://github.company.com). 6 |

7 | 8 | Notes: 9 | 10 |

    11 |
  1. The https:// / http:// part needs to be specified.
  2. 12 |
  3. There should not be any trailing slash (/).
  4. 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/main/webapp/help/realm/oauth-scopes-help.html: -------------------------------------------------------------------------------- 1 |
2 | A comma separated list of OAuth Scopes to request access to. See https://developer.github.com/v3/oauth/#scopes. 3 |
4 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubAccessTokenPropertySEC797Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017, 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 org.jenkinsci.plugins; 25 | 26 | import com.sun.net.httpserver.HttpExchange; 27 | import com.sun.net.httpserver.HttpHandler; 28 | import com.sun.net.httpserver.HttpServer; 29 | import edu.umd.cs.findbugs.annotations.CheckForNull; 30 | import hudson.model.UnprotectedRootAction; 31 | import hudson.util.HttpResponses; 32 | import jakarta.servlet.http.HttpSession; 33 | import net.sf.json.JSONArray; 34 | import net.sf.json.JSONObject; 35 | import org.apache.commons.lang.StringUtils; 36 | import org.eclipse.jetty.util.Fields; 37 | import org.eclipse.jetty.util.UrlEncoded; 38 | import org.htmlunit.Page; 39 | import org.htmlunit.WebRequest; 40 | import org.junit.jupiter.api.AfterEach; 41 | import org.junit.jupiter.api.BeforeEach; 42 | import org.junit.jupiter.api.Test; 43 | import org.jvnet.hudson.test.Issue; 44 | import org.jvnet.hudson.test.JenkinsRule; 45 | import org.jvnet.hudson.test.TestExtension; 46 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 47 | import org.kohsuke.stapler.HttpResponse; 48 | import org.kohsuke.stapler.StaplerRequest2; 49 | 50 | import java.io.IOException; 51 | import java.io.OutputStream; 52 | import java.net.HttpURLConnection; 53 | import java.net.InetAddress; 54 | import java.net.InetSocketAddress; 55 | import java.net.URI; 56 | import java.net.URL; 57 | import java.nio.charset.StandardCharsets; 58 | import java.util.ArrayList; 59 | import java.util.Collections; 60 | import java.util.HashMap; 61 | import java.util.List; 62 | import java.util.Map; 63 | 64 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 65 | import static org.junit.jupiter.api.Assertions.assertNotNull; 66 | 67 | //TODO merge with GithubAccessTokenPropertyTest after security release, just meant to ease the security merge 68 | // or with GithubSecurityRealmTest, but will require more refactor to move out the mock server 69 | @WithJenkins 70 | class GithubAccessTokenPropertySEC797Test { 71 | 72 | private JenkinsRule j; 73 | private JenkinsRule.WebClient wc; 74 | 75 | private HttpServer server; 76 | private URI serverUri; 77 | private MockGithubServlet servlet; 78 | 79 | private void setupMockGithubServer() throws Exception { 80 | server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); 81 | servlet = new MockGithubServlet(j); 82 | server.createContext("/", servlet); 83 | server.start(); 84 | 85 | InetSocketAddress address = server.getAddress(); 86 | serverUri = new URI(String.format("http://%s:%d/", address.getHostString(), address.getPort())); 87 | servlet.setServerUrl(serverUri); 88 | } 89 | 90 | /** 91 | * Based on documentation found at 92 | * https://developer.github.com/v3/users/ 93 | * https://developer.github.com/v3/orgs/ 94 | * https://developer.github.com/v3/orgs/teams/ 95 | */ 96 | private static class MockGithubServlet implements HttpHandler { 97 | private String currentLogin; 98 | private List organizations; 99 | private List teams; 100 | 101 | private final JenkinsRule jenkinsRule; 102 | private URI serverUri; 103 | 104 | public MockGithubServlet(JenkinsRule jenkinsRule) { 105 | this.jenkinsRule = jenkinsRule; 106 | } 107 | 108 | public void setServerUrl(URI serverUri) { 109 | this.serverUri = serverUri; 110 | } 111 | 112 | @Override 113 | public void handle(HttpExchange he) throws IOException { 114 | switch (he.getRequestURI().getPath()) { 115 | case "/user": 116 | this.onUser(he); 117 | break; 118 | case "/users/_specific_login_": 119 | this.onUser(he); 120 | break; 121 | case "/user/orgs": 122 | this.onUserOrgs(he); 123 | break; 124 | case "/user/teams": 125 | this.onUserTeams(he); 126 | break; 127 | case "/orgs/org-a": 128 | this.onOrgs(he, "org-a"); 129 | break; 130 | case "/orgs/org-a/teams": 131 | this.onOrgsTeam(he, "org-a"); 132 | break; 133 | case "/orgs/org-a/members/alice": 134 | this.onOrgsMember(he, "org-a", "alice"); 135 | break; 136 | case "/teams/7/members/alice": 137 | this.onTeamMember(he, "team-b", "alice"); 138 | break; 139 | case "/orgs/org-c": 140 | this.onOrgs(he, "org-c"); 141 | break; 142 | case "/orgs/org-c/teams": 143 | this.onOrgsTeam(he, "org-c"); 144 | break; 145 | case "/orgs/org-c/members/bob": 146 | this.onOrgsMember(he, "org-c", "bob"); 147 | break; 148 | case "/teams/7/members/bob": 149 | this.onTeamMember(he, "team-d", "bob"); 150 | break; 151 | case "/login/oauth/authorize": 152 | this.onLoginOAuthAuthorize(he); 153 | break; 154 | case "/login/oauth/access_token": 155 | this.onLoginOAuthAccessToken(he); 156 | break; 157 | default: 158 | throw new RuntimeException("Url not mapped yet: " + he.getRequestURI().getPath()); 159 | } 160 | he.close(); 161 | } 162 | 163 | private void onUser(HttpExchange he) throws IOException { 164 | sendResponse(he, JSONObject.fromObject( 165 | new HashMap() {{ 166 | put("login", currentLogin); 167 | put("name", currentLogin + "_name"); 168 | // to avoid triggering a second call, due to GithubSecurityRealm:382 169 | put("created_at", "2008-01-14T04:33:35Z"); 170 | put("url", serverUri + "/users/_specific_login_"); 171 | }} 172 | ).toString()); 173 | } 174 | 175 | private void onUserOrgs(HttpExchange he) throws IOException { 176 | List> responseBody = new ArrayList<>(); 177 | for (String orgName : organizations) { 178 | final String orgName_ = orgName; 179 | responseBody.add(new HashMap<>() {{ 180 | put("login", orgName_); 181 | }}); 182 | } 183 | 184 | sendResponse(he, JSONArray.fromObject(responseBody).toString()); 185 | } 186 | 187 | private void onOrgs(HttpExchange he, final String orgName) throws IOException { 188 | Map responseBody = new HashMap() {{ 189 | put("login", orgName); 190 | }}; 191 | 192 | sendResponse(he, JSONObject.fromObject(responseBody).toString()); 193 | } 194 | 195 | private void onOrgsMember(HttpExchange he, String orgName, String userName) throws IOException { 196 | he.sendResponseHeaders(HttpURLConnection.HTTP_NO_CONTENT, -1); 197 | // 302 / 404 responses not implemented 198 | } 199 | 200 | private void onTeamMember(HttpExchange he, String orgName, String userName) throws IOException { 201 | he.sendResponseHeaders(HttpURLConnection.HTTP_NO_CONTENT, -1); 202 | // 302 / 404 responses not implemented 203 | } 204 | 205 | private void onOrgsTeam(HttpExchange he, final String orgName) throws IOException { 206 | List> responseBody = new ArrayList<>(); 207 | for (String teamName : teams) { 208 | final String teamName_ = teamName; 209 | responseBody.add(new HashMap<>() {{ 210 | put("id", 7); 211 | put("login", teamName_ + "_login"); 212 | put("name", teamName_); 213 | put("organization", new HashMap() {{ 214 | put("login", orgName); 215 | }}); 216 | }}); 217 | } 218 | 219 | sendResponse(he, JSONArray.fromObject(responseBody).toString()); 220 | } 221 | 222 | private void onUserTeams(HttpExchange he) throws IOException { 223 | List> responseBody = new ArrayList<>(); 224 | for (String teamName : teams) { 225 | final String teamName_ = teamName; 226 | responseBody.add(new HashMap<>() {{ 227 | put("login", teamName_ + "_login"); 228 | put("name", teamName_); 229 | put("organization", new HashMap() {{ 230 | put("login", organizations.get(0)); 231 | }}); 232 | }}); 233 | } 234 | 235 | sendResponse(he, JSONArray.fromObject(responseBody).toString()); 236 | } 237 | 238 | private void onLoginOAuthAuthorize(HttpExchange he) throws IOException { 239 | String code = "test"; 240 | Fields fields = new Fields(); 241 | UrlEncoded.decodeUtf8To(he.getRequestURI().getQuery(), fields); 242 | String state = fields.getValue("state"); 243 | he.getResponseHeaders().set("Location", jenkinsRule.getURL() + "securityRealm/finishLogin?code=" + code + "&state=" + state); 244 | he.sendResponseHeaders(302, -1); 245 | } 246 | 247 | private void onLoginOAuthAccessToken(HttpExchange he) throws IOException { 248 | sendResponse(he, "access_token=RANDOM_ACCESS_TOKEN"); 249 | } 250 | 251 | private void sendResponse(HttpExchange he, String response) throws IOException { 252 | byte[] body = response.getBytes(StandardCharsets.UTF_8); 253 | he.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); 254 | try (OutputStream os = he.getResponseBody()) { 255 | os.write(body); 256 | } 257 | } 258 | } 259 | 260 | @BeforeEach 261 | void prepareRealmAndWebClient(JenkinsRule j) throws Exception { 262 | this.j = j; 263 | this.setupMockGithubServer(); 264 | this.setupRealm(); 265 | wc = j.createWebClient(); 266 | } 267 | 268 | private void setupRealm() { 269 | String githubWebUri = serverUri.toString(); 270 | String githubApiUri = serverUri.toString(); 271 | String clientID = "xxx"; 272 | String clientSecret = "yyy"; 273 | String oauthScopes = "read:org"; 274 | 275 | GithubSecurityRealm githubSecurityRealm = new GithubSecurityRealm( 276 | githubWebUri, 277 | githubApiUri, 278 | clientID, 279 | clientSecret, 280 | oauthScopes 281 | ); 282 | 283 | j.jenkins.setSecurityRealm(githubSecurityRealm); 284 | } 285 | 286 | @AfterEach 287 | void stopEmbeddedJettyServer() { 288 | server.stop(1); 289 | } 290 | 291 | // all the code above is reused from GithubAccessTokenPropertyTest 292 | 293 | @Issue("SECURITY-797") 294 | @Test 295 | void preventSessionFixation() throws Exception { 296 | TestRootAction rootAction = j.jenkins.getExtensionList(UnprotectedRootAction.class).get(TestRootAction.class); 297 | assertNotNull(rootAction); 298 | 299 | wc = j.createWebClient(); 300 | 301 | servlet.currentLogin = "alice"; 302 | servlet.organizations = Collections.singletonList("org-a"); 303 | servlet.teams = Collections.singletonList("team-b"); 304 | 305 | String sessionIdBefore = checkSessionFixationWithOAuth(); 306 | String sessionIdAfter = rootAction.sessionId; 307 | assertNotNull(sessionIdAfter); 308 | assertNotEquals(sessionIdBefore, sessionIdAfter, "Session must be invalidated after login"); 309 | } 310 | 311 | @TestExtension("preventSessionFixation") 312 | public static final class TestRootAction implements UnprotectedRootAction { 313 | public String sessionId; 314 | 315 | @Override 316 | public @CheckForNull String getIconFileName() { 317 | return null; 318 | } 319 | 320 | @Override 321 | public @CheckForNull String getDisplayName() { 322 | return null; 323 | } 324 | 325 | @Override 326 | public String getUrlName() { 327 | return "test"; 328 | } 329 | 330 | public HttpResponse doIndex(StaplerRequest2 request) { 331 | HttpSession session = request.getSession(false); 332 | if (session == null) { 333 | sessionId = null; 334 | } else { 335 | sessionId = session.getId(); 336 | } 337 | return HttpResponses.text("ok"); 338 | } 339 | } 340 | 341 | private String checkSessionFixationWithOAuth() throws IOException { 342 | WebRequest req = new WebRequest(new URL(j.getURL(), "securityRealm/commenceLogin")); 343 | req.setEncodingType(null); 344 | 345 | String referer = j.getURL() + "test"; 346 | req.setAdditionalHeader("Referer", referer); 347 | wc.getOptions().setRedirectEnabled(false); 348 | wc.getOptions().setThrowExceptionOnFailingStatusCode(false); 349 | Page p = wc.getPage(req); 350 | 351 | String cookie = p.getWebResponse().getResponseHeaderValue("Set-Cookie"); 352 | String sessionId = StringUtils.substringBetween(cookie, "JSESSIONID=", ";"); 353 | 354 | wc.getOptions().setRedirectEnabled(true); 355 | // continue the process of authentication 356 | wc.getPage(new URL(p.getWebResponse().getResponseHeaderValue("Location"))); 357 | return sessionId; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubAccessTokenPropertyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017, 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 org.jenkinsci.plugins; 25 | 26 | import com.sun.net.httpserver.HttpExchange; 27 | import com.sun.net.httpserver.HttpHandler; 28 | import com.sun.net.httpserver.HttpServer; 29 | import hudson.model.User; 30 | import hudson.util.Scrambler; 31 | import jenkins.security.ApiTokenProperty; 32 | import net.sf.json.JSONArray; 33 | import net.sf.json.JSONObject; 34 | import org.eclipse.jetty.util.Fields; 35 | import org.eclipse.jetty.util.UrlEncoded; 36 | import org.htmlunit.Page; 37 | import org.htmlunit.WebRequest; 38 | import org.junit.jupiter.api.AfterEach; 39 | import org.junit.jupiter.api.BeforeEach; 40 | import org.junit.jupiter.api.Test; 41 | import org.jvnet.hudson.test.Issue; 42 | import org.jvnet.hudson.test.JenkinsRule; 43 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 44 | 45 | import java.io.IOException; 46 | import java.io.OutputStream; 47 | import java.net.HttpURLConnection; 48 | import java.net.InetAddress; 49 | import java.net.InetSocketAddress; 50 | import java.net.URI; 51 | import java.net.URL; 52 | import java.nio.charset.StandardCharsets; 53 | import java.util.ArrayList; 54 | import java.util.Arrays; 55 | import java.util.Collections; 56 | import java.util.HashMap; 57 | import java.util.HashSet; 58 | import java.util.List; 59 | import java.util.Map; 60 | import java.util.Set; 61 | 62 | import static org.junit.jupiter.api.Assertions.assertEquals; 63 | 64 | @WithJenkins 65 | class GithubAccessTokenPropertyTest { 66 | 67 | private JenkinsRule j; 68 | 69 | private JenkinsRule.WebClient wc; 70 | 71 | private HttpServer server; 72 | private URI serverUri; 73 | private MockGithubServlet servlet; 74 | 75 | private void setupMockGithubServer() throws Exception { 76 | server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); 77 | servlet = new MockGithubServlet(j); 78 | server.createContext("/", servlet); 79 | server.start(); 80 | 81 | InetSocketAddress address = server.getAddress(); 82 | serverUri = new URI(String.format("http://%s:%d/", address.getHostString(), address.getPort())); 83 | servlet.setServerUrl(serverUri); 84 | } 85 | 86 | /** 87 | * Based on documentation found at 88 | * https://developer.github.com/v3/users/ 89 | * https://developer.github.com/v3/orgs/ 90 | * https://developer.github.com/v3/orgs/teams/ 91 | */ 92 | private static class MockGithubServlet implements HttpHandler { 93 | private String currentLogin; 94 | private List organizations; 95 | private List> teams; 96 | 97 | private final JenkinsRule jenkinsRule; 98 | private URI serverUri; 99 | 100 | public MockGithubServlet(JenkinsRule jenkinsRule) { 101 | this.jenkinsRule = jenkinsRule; 102 | } 103 | 104 | public void setServerUrl(URI serverUri) { 105 | this.serverUri = serverUri; 106 | } 107 | 108 | @Override 109 | public void handle(HttpExchange he) throws IOException { 110 | switch (he.getRequestURI().getPath()) { 111 | case "/user": 112 | this.onUser(he); 113 | break; 114 | case "/users/_specific_login_": 115 | this.onUser(he); 116 | break; 117 | case "/user/orgs": 118 | this.onUserOrgs(he); 119 | break; 120 | case "/user/teams": 121 | this.onUserTeams(he); 122 | break; 123 | case "/orgs/org-a": 124 | this.onOrgs(he, "org-a"); 125 | break; 126 | case "/orgs/org-a/teams": 127 | this.onOrgsTeam(he, "org-a"); 128 | break; 129 | case "/orgs/org-a/members/alice": 130 | this.onOrgsMember(he, "org-a", "alice"); 131 | break; 132 | case "/teams/7/members/alice": 133 | this.onTeamMember(he, "team-b", "alice"); 134 | break; 135 | case "/orgs/org-c": 136 | this.onOrgs(he, "org-c"); 137 | break; 138 | case "/orgs/org-c/teams": 139 | this.onOrgsTeam(he, "org-c"); 140 | break; 141 | case "/orgs/org-c/members/bob": 142 | this.onOrgsMember(he, "org-c", "bob"); 143 | break; 144 | case "/teams/7/members/bob": 145 | this.onTeamMember(he, "team-d", "bob"); 146 | break; 147 | case "/login/oauth/authorize": 148 | this.onLoginOAuthAuthorize(he); 149 | break; 150 | case "/login/oauth/access_token": 151 | this.onLoginOAuthAccessToken(he); 152 | break; 153 | default: 154 | throw new RuntimeException("Url not mapped yet: " + he.getRequestURI().getPath()); 155 | } 156 | he.close(); 157 | } 158 | 159 | private void onUser(HttpExchange he) throws IOException { 160 | sendResponse(he, JSONObject.fromObject( 161 | new HashMap() {{ 162 | put("login", currentLogin); 163 | put("name", currentLogin + "_name"); 164 | // to avoid triggering a second call, due to GithubSecurityRealm:382 165 | put("created_at", "2008-01-14T04:33:35Z"); 166 | put("url", serverUri + "/users/_specific_login_"); 167 | }} 168 | ).toString()); 169 | } 170 | 171 | private void onUserOrgs(HttpExchange he) throws IOException { 172 | List> responseBody = new ArrayList<>(); 173 | for (String orgName : organizations) { 174 | final String orgName_ = orgName; 175 | responseBody.add(new HashMap<>() {{ 176 | put("login", orgName_); 177 | }}); 178 | } 179 | sendResponse(he, JSONArray.fromObject(responseBody).toString()); 180 | } 181 | 182 | private void onOrgs(HttpExchange he, final String orgName) throws IOException { 183 | Map responseBody = new HashMap() {{ 184 | put("login", orgName); 185 | }}; 186 | sendResponse(he, JSONObject.fromObject(responseBody).toString()); 187 | } 188 | 189 | private void onOrgsMember(HttpExchange he, String orgName, String userName) throws IOException { 190 | he.sendResponseHeaders(HttpURLConnection.HTTP_NO_CONTENT, -1); 191 | // 302 / 404 responses not implemented 192 | } 193 | 194 | private void onTeamMember(HttpExchange he, String orgName, String userName) throws IOException { 195 | he.sendResponseHeaders(HttpURLConnection.HTTP_NO_CONTENT, -1); 196 | // 302 / 404 responses not implemented 197 | } 198 | 199 | private void onOrgsTeam(HttpExchange he, final String orgName) throws IOException { 200 | List> responseBody = new ArrayList<>(); 201 | for (Map team : teams) { 202 | final String teamName_ = team.get("name"); 203 | final String slug = team.get("slug"); 204 | responseBody.add(new HashMap<>() {{ 205 | put("id", 7); 206 | put("login", teamName_ + "_login"); 207 | put("name", teamName_); 208 | put("slug", slug); 209 | put("organization", new HashMap() {{ 210 | put("login", orgName); 211 | }}); 212 | }}); 213 | } 214 | sendResponse(he, JSONArray.fromObject(responseBody).toString()); 215 | } 216 | 217 | private void onUserTeams(HttpExchange he) throws IOException { 218 | List> responseBody = new ArrayList<>(); 219 | for (Map team : teams) { 220 | final String teamName_ = team.get("name"); 221 | final String slug = team.get("slug"); 222 | responseBody.add(new HashMap<>() {{ 223 | put("login", teamName_ + "_login"); 224 | put("name", teamName_); 225 | put("slug", slug); 226 | put("organization", new HashMap() {{ 227 | put("login", organizations.get(0)); 228 | }}); 229 | }}); 230 | } 231 | 232 | sendResponse(he, JSONArray.fromObject(responseBody).toString()); 233 | } 234 | 235 | private void onLoginOAuthAuthorize(HttpExchange he) throws IOException { 236 | String code = "test"; 237 | Fields fields = new Fields(); 238 | UrlEncoded.decodeUtf8To(he.getRequestURI().getQuery(), fields); 239 | String state = fields.getValue("state"); 240 | he.getResponseHeaders().set("Location", jenkinsRule.getURL() + "securityRealm/finishLogin?code=" + code + "&state=" + state); 241 | he.sendResponseHeaders(302, -1); 242 | } 243 | 244 | private void onLoginOAuthAccessToken(HttpExchange he) throws IOException { 245 | sendResponse(he, "access_token=RANDOM_ACCESS_TOKEN"); 246 | } 247 | 248 | private void sendResponse(HttpExchange he, String response) throws IOException { 249 | byte[] body = response.getBytes(StandardCharsets.UTF_8); 250 | he.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); 251 | try (OutputStream os = he.getResponseBody()) { 252 | os.write(body); 253 | } 254 | } 255 | } 256 | 257 | @BeforeEach 258 | void prepareRealmAndWebClient(JenkinsRule j) throws Exception { 259 | this.j = j; 260 | this.setupMockGithubServer(); 261 | this.setupRealm(); 262 | wc = j.createWebClient(); 263 | GithubAuthenticationToken.clearCaches(); 264 | } 265 | 266 | private void setupRealm() { 267 | String githubWebUri = serverUri.toString(); 268 | String githubApiUri = serverUri.toString(); 269 | String clientID = "xxx"; 270 | String clientSecret = "yyy"; 271 | String oauthScopes = "read:org"; 272 | 273 | GithubSecurityRealm githubSecurityRealm = new GithubSecurityRealm( 274 | githubWebUri, 275 | githubApiUri, 276 | clientID, 277 | clientSecret, 278 | oauthScopes 279 | ); 280 | 281 | j.jenkins.setSecurityRealm(githubSecurityRealm); 282 | } 283 | 284 | @AfterEach 285 | void stopEmbeddedJettyServer() { 286 | server.stop(1); 287 | } 288 | 289 | @Issue("JENKINS-47113") 290 | @Test 291 | void testUsingGithubToken() throws IOException { 292 | String aliceLogin = "alice"; 293 | servlet.currentLogin = aliceLogin; 294 | servlet.organizations = Collections.singletonList("org-a"); 295 | Map team = new HashMap<>(); 296 | team.put("slug", "team-b"); 297 | team.put("name", "Team D"); 298 | servlet.teams = Collections.singletonList(team); 299 | 300 | User aliceUser = User.getById(aliceLogin, true); 301 | String aliceApiRestToken = aliceUser.getProperty(ApiTokenProperty.class).getApiToken(); 302 | String aliceGitHubToken = "SPECIFIC_TOKEN"; 303 | 304 | // request whoAmI with ApiRestToken => group populated 305 | makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceApiRestToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); 306 | 307 | // request whoAmI with GitHubToken => group populated 308 | makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceGitHubToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); 309 | 310 | GithubAuthenticationToken.clearCaches(); 311 | 312 | // no authentication in session but use the cache 313 | makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceApiRestToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); 314 | 315 | wc = j.createWebClient(); 316 | // no session at all, use the cache also 317 | makeRequestWithAuthCodeAndVerify(encodeBasic(aliceLogin, aliceApiRestToken), "alice", Arrays.asList("authenticated", "org-a", "org-a*team-b")); 318 | } 319 | 320 | @Issue("JENKINS-47113") 321 | @Test 322 | void testUsingGithubLogin() throws IOException { 323 | String bobLogin = "bob"; 324 | servlet.currentLogin = bobLogin; 325 | servlet.organizations = Collections.singletonList("org-c"); 326 | Map team = new HashMap<>(); 327 | team.put("slug", "team-d"); 328 | team.put("name", "Team D"); 329 | servlet.teams = Collections.singletonList(team); 330 | 331 | User bobUser = User.getById(bobLogin, true); 332 | String bobApiRestToken = bobUser.getProperty(ApiTokenProperty.class).getApiToken(); 333 | 334 | // request whoAmI with ApiRestToken => group populated 335 | makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 336 | // request whoAmI with GitHub OAuth => group populated 337 | makeRequestUsingOAuth("bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 338 | 339 | // use only the session 340 | // request whoAmI with ApiRestToken => group populated (due to login event) 341 | makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 342 | 343 | GithubAuthenticationToken.clearCaches(); 344 | 345 | // retrieve the security group even without the cookie (using LastGrantedAuthorities this time) 346 | makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 347 | } 348 | 349 | @Issue("JENKINS-60200") 350 | @Test 351 | void testInvalidateAuthorizationCacheOnFreshLogin() throws IOException { 352 | String bobLogin = "bob"; 353 | servlet.currentLogin = bobLogin; 354 | servlet.organizations = Collections.singletonList("org-c"); 355 | Map team = new HashMap<>(); 356 | team.put("slug", "team-d"); 357 | team.put("name", "Team D"); 358 | servlet.teams = Collections.singletonList(team); 359 | 360 | User bobUser = User.getById(bobLogin, true); 361 | String bobApiRestToken = bobUser.getProperty(ApiTokenProperty.class).getApiToken(); 362 | 363 | // request whoAmI with ApiRestToken => group populated 364 | makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 365 | // request whoAmI with GitHub OAuth => group populated 366 | makeRequestUsingOAuth("bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 367 | 368 | // Switch the teams 369 | team.put("slug", "team-e"); 370 | team.put("name", "Team E"); 371 | servlet.teams = Collections.singletonList(team); 372 | 373 | // With just AuthCode, the cache is not invalidated 374 | makeRequestWithAuthCodeAndVerify(encodeBasic(bobLogin, bobApiRestToken), "bob", Arrays.asList("authenticated", "org-c", "org-c*team-d")); 375 | 376 | // With OAuth the cache is invalidated 377 | makeRequestUsingOAuth("bob", Arrays.asList("authenticated", "org-c", "org-c*team-e")); 378 | } 379 | 380 | private void makeRequestWithAuthCodeAndVerify(String authCode, String expectedLogin, List expectedAuthorities) throws IOException { 381 | WebRequest req = new WebRequest(new URL(j.getURL(), "whoAmI/api/json")); 382 | req.setEncodingType(null); 383 | if (authCode != null) 384 | req.setAdditionalHeader("Authorization", authCode); 385 | Page p = wc.getPage(req); 386 | 387 | assertResponse(p, expectedLogin, expectedAuthorities); 388 | } 389 | 390 | private void makeRequestUsingOAuth(String expectedLogin, List expectedAuthorities) throws IOException { 391 | WebRequest req = new WebRequest(new URL(j.getURL(), "securityRealm/commenceLogin")); 392 | req.setEncodingType(null); 393 | 394 | String referer = j.getURL() + "whoAmI/api/json"; 395 | req.setAdditionalHeader("Referer", referer); 396 | Page p = wc.getPage(req); 397 | 398 | assertResponse(p, expectedLogin, expectedAuthorities); 399 | } 400 | 401 | private static void assertResponse(Page p, String expectedLogin, List expectedAuthorities) { 402 | String response = p.getWebResponse().getContentAsString().trim(); 403 | JSONObject respObject = JSONObject.fromObject(response); 404 | if (expectedLogin != null) { 405 | assertEquals(expectedLogin, respObject.getString("name")); 406 | } 407 | if (expectedAuthorities != null) { 408 | // we use set to avoid having duplicated "authenticated" 409 | // as that will be corrected in https://github.com/jenkinsci/jenkins/pull/3123 410 | Set actualAuthorities = new HashSet<>( 411 | JSONArray.toCollection( 412 | respObject.getJSONArray("authorities"), 413 | String.class 414 | ) 415 | ); 416 | 417 | Set expectedAuthoritiesSet = new HashSet<>(expectedAuthorities); 418 | 419 | assertEquals(expectedAuthoritiesSet, actualAuthorities, String.format("They do not have the same content, expected=%s, actual=%s", expectedAuthorities, actualAuthorities)); 420 | } 421 | } 422 | 423 | private static String encodeBasic(String login, String credentials) { 424 | return "Basic " + Scrambler.scramble(login + ":" + credentials); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubAuthenticationTokenTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins; 2 | 3 | import jenkins.model.Jenkins; 4 | import org.apache.commons.lang.SerializationUtils; 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.kohsuke.github.GHMyself; 9 | import org.kohsuke.github.GitHub; 10 | import org.kohsuke.github.GitHubBuilder; 11 | import org.kohsuke.github.RateLimitHandler; 12 | import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector; 13 | import org.mockito.Mock; 14 | import org.mockito.MockedStatic; 15 | import org.mockito.Mockito; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | import java.io.IOException; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class GithubAuthenticationTokenTest { 24 | 25 | @Mock(strictness = Mock.Strictness.LENIENT) 26 | private GithubSecurityRealm securityRealm; 27 | 28 | @AfterEach 29 | void tearDown() { 30 | GithubAuthenticationToken.clearCaches(); 31 | } 32 | 33 | private void mockJenkins(MockedStatic mockedJenkins) { 34 | Jenkins jenkins = Mockito.mock(Jenkins.class); 35 | mockedJenkins.when(Jenkins::get).thenReturn(jenkins); 36 | Mockito.when(jenkins.getSecurityRealm()).thenReturn(securityRealm); 37 | Mockito.when(securityRealm.getOauthScopes()).thenReturn("read:org"); 38 | } 39 | 40 | @Test 41 | void testTokenSerialization() throws IOException { 42 | try (MockedStatic mockedJenkins = Mockito.mockStatic(Jenkins.class); 43 | MockedStatic mockedGitHubBuilder = Mockito.mockStatic(GitHubBuilder.class)) { 44 | mockJenkins(mockedJenkins); 45 | mockGHMyselfAs(mockedGitHubBuilder, "bob"); 46 | GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); 47 | byte[] serializedToken = SerializationUtils.serialize(authenticationToken); 48 | GithubAuthenticationToken deserializedToken = (GithubAuthenticationToken) SerializationUtils.deserialize(serializedToken); 49 | assertEquals(deserializedToken.getAccessToken(), authenticationToken.getAccessToken()); 50 | assertEquals(deserializedToken.getPrincipal(), authenticationToken.getPrincipal()); 51 | assertEquals(deserializedToken.getGithubServer(), authenticationToken.getGithubServer()); 52 | assertEquals(deserializedToken.getMyself().getLogin(), deserializedToken.getMyself().getLogin()); 53 | } 54 | } 55 | 56 | private static GHMyself mockGHMyselfAs(MockedStatic mockedGitHubBuilder, String username) throws IOException { 57 | GitHub gh = Mockito.mock(GitHub.class); 58 | GitHubBuilder builder = Mockito.mock(GitHubBuilder.class); 59 | mockedGitHubBuilder.when(GitHubBuilder::fromEnvironment).thenReturn(builder); 60 | Mockito.when(builder.withEndpoint("https://api.github.com")).thenReturn(builder); 61 | Mockito.when(builder.withOAuthToken("accessToken")).thenReturn(builder); 62 | Mockito.when(builder.withRateLimitHandler(RateLimitHandler.FAIL)).thenReturn(builder); 63 | Mockito.when(builder.withConnector(Mockito.any(OkHttpGitHubConnector.class))).thenReturn(builder); 64 | Mockito.when(builder.build()).thenReturn(gh); 65 | GHMyself me = Mockito.mock(GHMyself.class); 66 | Mockito.when(gh.getMyself()).thenReturn(me); 67 | Mockito.when(me.getLogin()).thenReturn(username); 68 | return me; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubAuthorizationStrategyTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2015 Sam Gleske 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 org.jenkinsci.plugins; 26 | 27 | import org.junit.jupiter.api.Test; 28 | 29 | import static org.junit.jupiter.api.Assertions.assertEquals; 30 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 31 | 32 | class GithubAuthorizationStrategyTest { 33 | 34 | @Test 35 | void testEquals_true() { 36 | GithubAuthorizationStrategy a = new GithubAuthorizationStrategy("", false, true, false, "", false, false, false, false); 37 | GithubAuthorizationStrategy b = new GithubAuthorizationStrategy("", false, true, false, "", false, false, false, false); 38 | assertEquals(a, b); 39 | } 40 | 41 | @Test 42 | void testEquals_false() { 43 | GithubAuthorizationStrategy a = new GithubAuthorizationStrategy("", false, true, false, "", false, false, false, false); 44 | GithubAuthorizationStrategy b = new GithubAuthorizationStrategy("", false, false, false, "", false, false, false, false); 45 | assertNotEquals(a, b); 46 | assertNotEquals("", a); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubLogoutActionTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2016 Sam Gleske 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 org.jenkinsci.plugins; 26 | 27 | import jenkins.model.Jenkins; 28 | import org.junit.jupiter.api.Test; 29 | import org.junit.jupiter.api.extension.ExtendWith; 30 | import org.mockito.Mock; 31 | import org.mockito.MockedStatic; 32 | import org.mockito.Mockito; 33 | import org.mockito.junit.jupiter.MockitoExtension; 34 | 35 | import static org.junit.jupiter.api.Assertions.assertEquals; 36 | 37 | @ExtendWith(MockitoExtension.class) 38 | class GithubLogoutActionTest { 39 | 40 | @Mock 41 | private GithubSecurityRealm securityRealm; 42 | 43 | @Mock 44 | private GithubSecurityRealm.DescriptorImpl descriptor; 45 | 46 | private void mockJenkins(MockedStatic mockedJenkins) { 47 | Jenkins jenkins = Mockito.mock(Jenkins.class); 48 | mockedJenkins.when(Jenkins::get).thenReturn(jenkins); 49 | Mockito.when(jenkins.getSecurityRealm()).thenReturn(securityRealm); 50 | Mockito.when(securityRealm.getDescriptor()).thenReturn(descriptor); 51 | Mockito.when(descriptor.getDefaultGithubWebUri()).thenReturn("https://github.com"); 52 | } 53 | 54 | private void mockGithubSecurityRealmWebUriFor(String host) { 55 | Mockito.when(securityRealm.getGithubWebUri()).thenReturn(host); 56 | } 57 | 58 | @Test 59 | void testGetGitHubText_gh() { 60 | try (MockedStatic mockedJenkins = Mockito.mockStatic(Jenkins.class)) { 61 | mockJenkins(mockedJenkins); 62 | mockGithubSecurityRealmWebUriFor("https://github.com"); 63 | GithubLogoutAction ghlogout = new GithubLogoutAction(); 64 | assertEquals("GitHub", ghlogout.getGitHubText()); 65 | } 66 | } 67 | 68 | @Test 69 | void testGetGitHubText_ghe() { 70 | try (MockedStatic mockedJenkins = Mockito.mockStatic(Jenkins.class)) { 71 | mockJenkins(mockedJenkins); 72 | mockGithubSecurityRealmWebUriFor("https://ghe.example.com"); 73 | GithubLogoutAction ghlogout = new GithubLogoutAction(); 74 | assertEquals("GitHub Enterprise", ghlogout.getGitHubText()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubSecretStorageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017, 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 org.jenkinsci.plugins; 25 | 26 | import hudson.model.User; 27 | import org.junit.jupiter.api.Test; 28 | import org.jvnet.hudson.test.JenkinsRule; 29 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertEquals; 32 | import static org.junit.jupiter.api.Assertions.assertFalse; 33 | import static org.junit.jupiter.api.Assertions.assertNull; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | 36 | @WithJenkins 37 | class GithubSecretStorageTest { 38 | 39 | @Test 40 | void correctBehavior(JenkinsRule j) { 41 | User.getById("alice", true); 42 | User.getById("bob", true); 43 | 44 | String secret = "$3cR3t"; 45 | 46 | assertFalse(GithubSecretStorage.contains(retrieveUser())); 47 | assertNull(GithubSecretStorage.retrieve(retrieveUser())); 48 | 49 | assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); 50 | 51 | GithubSecretStorage.put(retrieveUser(), secret); 52 | 53 | assertTrue(GithubSecretStorage.contains(retrieveUser())); 54 | assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); 55 | 56 | assertEquals(secret, GithubSecretStorage.retrieve(retrieveUser())); 57 | } 58 | 59 | private static User retrieveUser() { 60 | return User.getById("alice", false); 61 | } 62 | 63 | private static User retrieveOtherUser() { 64 | return User.getById("bob", false); 65 | } 66 | 67 | @Test 68 | void correctBehaviorEvenAfterRestart(JenkinsRule j) throws Throwable { 69 | final String secret = "$3cR3t"; 70 | 71 | User.getById("alice", true).save(); 72 | User.getById("bob", true).save(); 73 | 74 | assertFalse(GithubSecretStorage.contains(retrieveUser())); 75 | assertNull(GithubSecretStorage.retrieve(retrieveUser())); 76 | 77 | assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); 78 | 79 | GithubSecretStorage.put(retrieveUser(), secret); 80 | 81 | assertTrue(GithubSecretStorage.contains(retrieveUser())); 82 | assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); 83 | 84 | assertEquals(secret, GithubSecretStorage.retrieve(retrieveUser())); 85 | 86 | j.restart(); 87 | 88 | assertTrue(GithubSecretStorage.contains(retrieveUser())); 89 | assertFalse(GithubSecretStorage.contains(retrieveOtherUser())); 90 | 91 | assertEquals(secret, GithubSecretStorage.retrieve(retrieveUser())); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/GithubSecurityRealmTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2015 Sam Gleske 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 org.jenkinsci.plugins; 26 | 27 | import org.junit.jupiter.api.Test; 28 | import org.jvnet.hudson.test.JenkinsRule; 29 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertEquals; 32 | import static org.junit.jupiter.api.Assertions.assertFalse; 33 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | 36 | @WithJenkins 37 | class GithubSecurityRealmTest { 38 | 39 | @Test 40 | void testEquals_true(JenkinsRule rule) { 41 | GithubSecurityRealm a = new GithubSecurityRealm("http://jenkins.acme.com", "http://jenkins.acme.com/api/v3", "someid", "somesecret", "read:org"); 42 | GithubSecurityRealm b = new GithubSecurityRealm("http://jenkins.acme.com", "http://jenkins.acme.com/api/v3", "someid", "somesecret", "read:org"); 43 | assertEquals(a, b); 44 | } 45 | 46 | @Test 47 | void testEquals_false(JenkinsRule rule) { 48 | GithubSecurityRealm a = new GithubSecurityRealm("http://jenkins.acme.com", "http://jenkins.acme.com/api/v3", "someid", "somesecret", "read:org"); 49 | GithubSecurityRealm b = new GithubSecurityRealm("http://jenkins.acme.com", "http://jenkins.acme.com/api/v3", "someid", "somesecret", "read:org,repo"); 50 | assertNotEquals(a, b); 51 | assertNotEquals("", a); 52 | } 53 | 54 | @Test 55 | void testHasScope_true(JenkinsRule rule) { 56 | GithubSecurityRealm a = new GithubSecurityRealm("http://jenkins.acme.com", "http://jenkins.acme.com/api/v3", "someid", "somesecret", "read:org,user,user:email"); 57 | assertTrue(a.hasScope("user")); 58 | assertTrue(a.hasScope("read:org")); 59 | assertTrue(a.hasScope("user:email")); 60 | } 61 | 62 | @Test 63 | void testHasScope_false(JenkinsRule rule) { 64 | GithubSecurityRealm a = new GithubSecurityRealm("http://jenkins.acme.com", "http://jenkins.acme.com/api/v3", "someid", "somesecret", "read:org,user,user:email"); 65 | assertFalse(a.hasScope("somescope")); 66 | } 67 | 68 | @Test 69 | void testDescriptorImplGetDefaultGithubWebUri(JenkinsRule rule) { 70 | GithubSecurityRealm.DescriptorImpl descriptor = new GithubSecurityRealm.DescriptorImpl(); 71 | assertEquals("https://github.com", descriptor.getDefaultGithubWebUri()); 72 | } 73 | 74 | @Test 75 | void testDescriptorImplGetDefaultGithubApiUri(JenkinsRule rule) { 76 | GithubSecurityRealm.DescriptorImpl descriptor = new GithubSecurityRealm.DescriptorImpl(); 77 | assertEquals("https://api.github.com", descriptor.getDefaultGithubApiUri()); 78 | } 79 | 80 | @Test 81 | void testDescriptorImplGetDefaultOauthScopes(JenkinsRule rule) { 82 | GithubSecurityRealm.DescriptorImpl descriptor = new GithubSecurityRealm.DescriptorImpl(); 83 | assertEquals("read:org,user:email,repo", descriptor.getDefaultOauthScopes()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/JenkinsProxyAuthenticatorTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins; 2 | 3 | import hudson.ProxyConfiguration; 4 | import okhttp3.Credentials; 5 | import okhttp3.Protocol; 6 | import okhttp3.Request; 7 | import okhttp3.Response; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNull; 12 | 13 | class JenkinsProxyAuthenticatorTest { 14 | 15 | @Test 16 | void refusesChallengeIfAuthenticationAlreadyFailed() { 17 | Request previousRequest = 18 | new Request.Builder() 19 | .url("https://example.com") 20 | .header("Proxy-Authorization", "notNull") 21 | .build(); 22 | 23 | Response response = 24 | new Response.Builder() 25 | .code(407) 26 | .request(previousRequest) 27 | .protocol(Protocol.HTTP_1_0) 28 | .message("Unauthorized") 29 | .build(); 30 | 31 | assertNull(new JenkinsProxyAuthenticator(null).authenticate(null, response)); 32 | } 33 | 34 | @Test 35 | void refusesPreemptiveOkHttpChallenge() { 36 | Request previousRequest = new Request.Builder().url("https://example.com").build(); 37 | 38 | Response response = 39 | new Response.Builder() 40 | .request(previousRequest) 41 | .header("Proxy-Authenticate", "OkHttp-Preemptive") 42 | .code(407) 43 | .protocol(Protocol.HTTP_1_0) 44 | .message("Unauthorized") 45 | .build(); 46 | 47 | assertNull(new JenkinsProxyAuthenticator(null).authenticate(null, response)); 48 | } 49 | 50 | @Test 51 | void acceptsBasicChallenge() { 52 | Request previousRequest = new Request.Builder().url("https://example.com").build(); 53 | 54 | Response response = 55 | new Response.Builder() 56 | .request(previousRequest) 57 | .header("Proxy-Authenticate", "Basic") 58 | .code(407) 59 | .protocol(Protocol.HTTP_1_0) 60 | .message("Unauthorized") 61 | .build(); 62 | 63 | ProxyConfiguration proxyConfiguration = 64 | new ProxyConfiguration("proxy", 80, "user", "password"); 65 | String credentials = Credentials.basic("user", "password"); 66 | Request requestWithBasicAuth = 67 | new JenkinsProxyAuthenticator(proxyConfiguration).authenticate(null, response); 68 | 69 | assertEquals(requestWithBasicAuth.header("Proxy-Authorization"), credentials); 70 | } 71 | 72 | @Test 73 | void refusesAnyChallengeWhichIsNotBasicAuthentication() { 74 | Request previousRequest = new Request.Builder().url("https://example.com").build(); 75 | 76 | Response response = 77 | new Response.Builder() 78 | .request(previousRequest) 79 | .code(407) 80 | .protocol(Protocol.HTTP_1_0) 81 | .header("Proxy-Authenticate", "Digest") 82 | .message("Unauthorized") 83 | .build(); 84 | 85 | assertNull(new JenkinsProxyAuthenticator(null).authenticate(null, response)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/api/GithubAPITest.java: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License 3 | 4 | Copyright (c) 2011 Michael O'Cleirigh 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 | 26 | */ 27 | package org.jenkinsci.plugins.api; 28 | 29 | import org.junit.jupiter.api.Disabled; 30 | import org.junit.jupiter.api.Test; 31 | import org.kohsuke.github.GHOrganization; 32 | import org.kohsuke.github.GHTeam; 33 | import org.kohsuke.github.GHUser; 34 | import org.kohsuke.github.GitHub; 35 | 36 | import java.io.IOException; 37 | import java.util.Map; 38 | import java.util.Set; 39 | 40 | import static org.junit.jupiter.api.Assertions.assertTrue; 41 | 42 | /** 43 | * @author mocleiri 44 | * 45 | * we ignore this test when running the automated tests. 46 | */ 47 | @Disabled("we ignore this test when running the automated tests") 48 | class GithubAPITest { 49 | 50 | private static final String LOGIN = System.getProperty("github.login"); 51 | private static final String API_TOKEN = System.getProperty("github.api"); 52 | 53 | // I would suggest with the repo level of permission. 54 | private static final String OAUTH_TOKEN = System.getProperty("github.oauth"); 55 | 56 | // the name of the organization to which the login is a participant. 57 | private static final String PARTICIPATING_ORG = System.getProperty("github.org"); 58 | 59 | @Test 60 | void testWithUserAPIToken() throws IOException { 61 | GitHub gh = GitHub.connect(LOGIN, API_TOKEN); 62 | 63 | GHOrganization org = gh.getOrganization(PARTICIPATING_ORG); 64 | 65 | Map teams = org.getTeams(); 66 | 67 | boolean found = false; 68 | 69 | for (GHTeam team : teams.values()) { 70 | System.out.println("team = " + team.getName() + ", permission = " 71 | + team.getPermission()); 72 | 73 | // check for membership 74 | for (GHUser member : team.getMembers()) { 75 | System.out.println("member = " + member.getLogin()); 76 | 77 | if (member.getLogin().equals(LOGIN)) { 78 | found = true; 79 | } 80 | } 81 | } 82 | 83 | assertTrue(found); 84 | } 85 | 86 | @Test 87 | void testOrganizationMembership() throws IOException { 88 | GitHub gh = GitHub.connectUsingOAuth(OAUTH_TOKEN); 89 | 90 | Map orgs = gh.getMyOrganizations(); 91 | 92 | for (String orgName : orgs.keySet()) { 93 | GHOrganization org = orgs.get(orgName); 94 | 95 | Map teams = org.getTeams(); 96 | 97 | System.out.println("org = " + orgName); 98 | 99 | for (String name : teams.keySet()) { 100 | GHTeam team = teams.get(name); 101 | 102 | Set members = team.getMembers(); 103 | 104 | System.out.println("team = " + team.getName()); 105 | 106 | for (GHUser ghUser : members) { 107 | System.out.println("member = " + ghUser.getLogin()); 108 | } 109 | } 110 | } 111 | 112 | assertTrue(true); 113 | } 114 | 115 | @Test 116 | void testOrganizationMembershipAPI() throws IOException { 117 | GitHub gh = GitHub.connect(LOGIN, API_TOKEN); 118 | 119 | Map orgs = gh.getMyOrganizations(); 120 | 121 | for (String orgName : orgs.keySet()) { 122 | GHOrganization org = orgs.get(orgName); 123 | 124 | System.out.println("org = " + orgName); 125 | } 126 | 127 | assertTrue(true); 128 | } 129 | 130 | // /organizations 131 | @Test 132 | void testWithOAuthToken() throws IOException { 133 | GitHub gh = GitHub.connectUsingOAuth(OAUTH_TOKEN); 134 | 135 | GHUser me = gh.getMyself(); 136 | 137 | GHOrganization org = gh.getOrganization(PARTICIPATING_ORG); 138 | 139 | Map teams = org.getTeams(); 140 | 141 | boolean found = false; 142 | 143 | for (GHTeam team : teams.values()) { 144 | System.out.println("team = " + team.getName() + ", permission = " 145 | + team.getPermission()); 146 | 147 | // check for membership 148 | for (GHUser member : team.getMembers()) { 149 | System.out.println("member = " + member.getLogin()); 150 | 151 | if (member.getLogin().equals(LOGIN)) { 152 | found = true; 153 | } 154 | } 155 | } 156 | 157 | assertTrue(found); 158 | } 159 | } 160 | --------------------------------------------------------------------------------