├── .github
├── dependabot.yml
└── workflows
│ ├── cd.yaml
│ └── jenkins-security-scan.yml
├── .gitignore
├── .mvn
├── extensions.xml
└── maven.config
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Jenkinsfile
├── Jenkinsfile.google
├── LICENSE
├── README.md
├── checkstyle.xml
├── checkstyleJavaHeader
├── docs
├── home.md
├── images
│ ├── dropdown.jpg
│ ├── metadata.jpg
│ └── private-key.jpg
└── source_build_installation.md
├── jenkins
├── maven-pod.yaml
└── saveAndCompress.sh
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── google
│ │ └── jenkins
│ │ └── plugins
│ │ ├── credentials
│ │ └── oauth
│ │ │ ├── GoogleOAuth2Credentials.java
│ │ │ ├── GoogleOAuth2ScopeRequirement.java
│ │ │ ├── GoogleOAuth2ScopeSpecification.java
│ │ │ ├── GoogleRobotCredentials.java
│ │ │ ├── GoogleRobotCredentialsModule.java
│ │ │ ├── GoogleRobotMetadataCredentials.java
│ │ │ ├── GoogleRobotMetadataCredentialsModule.java
│ │ │ ├── GoogleRobotNameProvider.java
│ │ │ ├── GoogleRobotPrivateKeyCredentials.java
│ │ │ ├── JsonKey.java
│ │ │ ├── JsonServiceAccountConfig.java
│ │ │ ├── KeyUtils.java
│ │ │ ├── LegacyJsonKey.java
│ │ │ ├── P12ServiceAccountConfig.java
│ │ │ ├── RemotableGoogleCredentials.java
│ │ │ ├── ServiceAccountConfig.java
│ │ │ └── package-info.java
│ │ └── util
│ │ ├── ConflictException.java
│ │ ├── Executor.java
│ │ ├── ExecutorException.java
│ │ ├── ForbiddenException.java
│ │ ├── MaxRetryExceededException.java
│ │ ├── MetadataReader.java
│ │ ├── MockExecutor.java
│ │ ├── NameValuePair.java
│ │ ├── NotFoundException.java
│ │ ├── RequestCallable.java
│ │ └── Resolve.java
└── resources
│ ├── com
│ └── google
│ │ └── jenkins
│ │ └── plugins
│ │ ├── credentials
│ │ └── oauth
│ │ │ ├── GoogleRobotMetadataCredentials
│ │ │ ├── config.jelly
│ │ │ └── help-projectId.html
│ │ │ ├── GoogleRobotPrivateKeyCredentials
│ │ │ ├── config.jelly
│ │ │ ├── help-credentials.html
│ │ │ └── help-projectId.html
│ │ │ ├── JsonServiceAccountConfig
│ │ │ ├── config.jelly
│ │ │ └── help-jsonKeyFileUpload.html
│ │ │ ├── Messages.properties
│ │ │ └── P12ServiceAccountConfig
│ │ │ ├── config.jelly
│ │ │ ├── help-emailAddress.html
│ │ │ └── help-p12KeyFileUpload.html
│ │ └── util
│ │ └── Messages.properties
│ ├── index.jelly
│ └── lib
│ └── auth
│ ├── blockWrapper.jelly
│ ├── credentials.jelly
│ └── taglib
└── test
├── java
└── com
│ └── google
│ └── jenkins
│ ├── GoogleOAuthPluginTestSuite.java
│ └── plugins
│ ├── CredentialsOAuthTestSuite.java
│ ├── UtilTestSuite.java
│ ├── credentials
│ └── oauth
│ │ ├── ConfigurationAsCodeTest.java
│ │ ├── GoogleOAuth2ScopeSpecificationTest.java
│ │ ├── GoogleRobotCredentialsTest.java
│ │ ├── GoogleRobotMetadataCredentialsTest.java
│ │ ├── GoogleRobotPrivateKeyCredentialsTest.java
│ │ ├── JsonServiceAccountConfigTest.java
│ │ ├── JsonServiceAccountConfigTestUtil.java
│ │ ├── LegacyJsonServiceAccountConfigUtil.java
│ │ ├── P12ServiceAccountConfigTest.java
│ │ ├── P12ServiceAccountConfigTestUtil.java
│ │ ├── RemotableGoogleCredentialsTest.java
│ │ ├── SerializationUtil.java
│ │ ├── TestGoogleOAuth2DomainRequirement.java
│ │ └── TestRobotBuilder.java
│ └── util
│ ├── ExecutorTest.java
│ ├── MetadataReaderTest.java
│ ├── MockExecutorTest.java
│ ├── NameValuePairTest.java
│ └── ResolveTest.java
└── resources
└── com
└── google
└── jenkins
└── plugins
└── credentials
└── oauth
├── GoogleRobotCredentialsTest
└── testMigration
│ └── credentials.xml
├── json-service-account-config.yml
├── p12-service-account-config.yml
├── sample-privatekey.p12
└── test-key.json
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: maven
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 |
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.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 }}
--------------------------------------------------------------------------------
/.github/workflows/jenkins-security-scan.yml:
--------------------------------------------------------------------------------
1 | name: Jenkins Security Scan
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 | pull_request:
8 | types: [ opened, synchronize, reopened ]
9 | workflow_dispatch:
10 |
11 | permissions:
12 | security-events: write
13 | contents: read
14 | actions: read
15 |
16 | jobs:
17 | security-scan:
18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2
19 | with:
20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate.
21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Maven
2 | target
3 | work
4 |
5 | # IntelliJ IDEA
6 | .idea/
7 | *.iml
8 |
9 | # Jenkins
10 | work/
11 |
12 | # Emacs
13 | *~
14 |
15 | # eclipse
16 | .classpath
17 | .project
18 | .settings/
--------------------------------------------------------------------------------
/.mvn/extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | io.jenkins.tools.incrementals
4 | git-changelist-maven-extension
5 | 1.7
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.mvn/maven.config:
--------------------------------------------------------------------------------
1 | -Pconsume-incrementals
2 | -Pmight-produce-incrementals
3 | -Dchangelist.format=%d.v%s
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | sudo: false
3 | script: mvn clean test
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
16 | # Changelog
17 | All notable changes to this project will be documented in this file.
18 |
19 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
20 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
21 |
22 | ## [Unresolved]
23 |
24 | ### Security
25 |
26 | ### Added
27 |
28 | ### Changed
29 |
30 | ### Removed
31 |
32 | ### Fixed
33 |
34 | ## [1.0.0] 2019-10-29
35 |
36 | ### Changed
37 | - com.google.guava:guava version changed: 14.0.1 to 20.0
38 | - com.google.http-client:google-http-client version changed: 1.24.1 to 1.21.0
39 | - com.google.http-client:google-http-client-jackson2 version changed: 1.24.1 to 1.25.0
40 | - com.google.api-client:google-api-client version changed: 1.24.1 to 1.25.0
41 | - com.google.apis:google-api-services-oauth2 version changed: v2-rev140-1.24.1 to v2-rev151-1.25.0
42 |
43 | ## [0.10] 2019-10-09
44 |
45 | ### Security
46 | - SECURITY-1583: Added permissions checking for loading legacy key files when upgrading versions.
47 |
48 | ### Added
49 | - Onboarding project into team CI server.
50 |
51 | ## [0.9] 2019-09-03
52 |
53 | ### Added
54 | - Test suites for different kinds of tests
55 | - Release completion goal to format and tidy pom file after release and before
56 | creating the final git commit.
57 |
58 | ### Changed
59 | - Changed to use fixed link for GCP Slack invite.
60 | - Plugin parent org.jenkinsci.plugins:plugin version changed: 3.36 to 3.43
61 | - Jenkins core requirement changed: 2.60.3 to 2.138.4
62 | - io.jenkins:configuration-as-code version changed: 1.9 to 1.19
63 | - org.jenkins-ci.plugins:credentials version changed: 2.1.16 to 2.2.0
64 | - com.google.http-client:google-http-client-jackson replaced with
65 | com.google.http-client:google-http-client-jackson2
66 |
67 | ### Removed
68 | - Test dependencies previously required for testing configuration as code
69 | compatibility:
70 | - io.jenkins.configuration-as-code:configuration-as-code-support
71 | - org.jenkins-ci.plugins:plain-credentials
72 | - org.jenkins-ci.plugins:ssh-credentials
73 |
74 | ### Fixed
75 | - Javadoc links to classes.
76 |
77 | ## [0.8] 2019-04-24
78 |
79 | ### Security
80 |
81 | ### Added
82 | - License headers that were missing.
83 | - xml-format-maven plugin for formatting pom.xml.
84 | - tidy-maven plugin for maintaining a consistent structure in pom.xml.
85 | - Support for Jenkins Configuration as Code plugin. Use the base64 encoded
86 | secret directly in the configuration. `filename` is optional but recommended
87 | if you plan to also use the Jenkins UI.
88 | - Test-only dependencies on the configuration-as-code, plain credentials
89 | and ssh-credentials plugins.
90 | - fmt-maven-plugin for automatically formatting java files when built.
91 |
92 | ### Changed
93 | - Use SecretBytes to store credentials, rather than files.
94 | - When uploaded through the UI, the file name serves as an identifier for the
95 | key when choosing whether to upload a new key. The file name will be as
96 | uploaded rather than a new randomly generated file name
97 | - Updated base plugin version to 3.36
98 | - Updated Java version to 8
99 | - Updated minimum Jenkins version to 2.60.3
100 | - Updated credentials plugin to 2.1.16
101 | - Updated maven-javadoc-plugin to 2.10.4
102 |
103 | ### Removed
104 | - Issue #49: find-bugs plugin (spot-bugs now inherited from parent plugin pom)
105 |
106 | ### Fixed
107 | - Various issues related to the use of manually generated files
108 | - Not able to load files after system restart, leading to NPEs in some
109 | - Not able to update permissions on created files, so fail to have a usable key.
110 |
111 | ## [0.7] - 2019-02-06
112 |
113 | ### Security
114 |
115 | ### Added
116 |
117 | ### Changed
118 | - Upgraded google.api.version to 1.24.1
119 | - Updated joda-time to 2.9.5
120 | - Added git ignore for intellij and emacs
121 | - Added travis file
122 |
123 | ### Removed
124 |
125 | ### Fixed
126 |
127 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
16 | # How to contribute
17 |
18 | We'd love to accept your patches and contributions to this project. There are
19 | just a few small guidelines you need to follow.
20 |
21 | ## Branching Model
22 |
23 | This repository utilizes the [Git Flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) branch model. All pull requests should be made against the develop branch.
24 |
25 | ## Code reviews
26 |
27 | All submissions, including submissions by project members, require review. We
28 | use GitHub pull requests for this purpose. Consult
29 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
30 | information on using pull requests.
31 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | buildPlugin(useContainerAgent: true, configurations: [
2 | [platform: 'linux', jdk: 21],
3 | [platform: 'windows', jdk: 17],
4 | ])
5 |
--------------------------------------------------------------------------------
/Jenkinsfile.google:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | pipeline {
17 | agent {
18 | kubernetes {
19 | cloud 'kubernetes'
20 | label 'maven-pod'
21 | yamlFile 'jenkins/maven-pod.yaml'
22 | }
23 | }
24 |
25 | environment {
26 | GOOGLE_PROJECT_ID = "${OAUTH_IT_PROJECT_ID}"
27 | BUILD_ARTIFACTS_BUCKET = "${OAUTH_IT_BUCKET}"
28 | CLEAN_BRANCH_NAME = "${BRANCH_NAME}".replaceAll("[/&;<>|\\]]", "_")
29 | BUILD_ARTIFACTS = "build-${CLEAN_BRANCH_NAME}-${BUILD_ID}.tar.gz"
30 | }
31 |
32 | stages {
33 | stage("Build and test") {
34 | steps {
35 | container('maven') {
36 | withCredentials([[$class: 'StringBinding', credentialsId: env.OAUTH_IT_CRED_ID, variable: 'GOOGLE_CREDENTIALS']]) {
37 | catchError {
38 | // build
39 | sh "mvn clean package -ntp"
40 |
41 | // run tests
42 | sh "mvn verify -ntp"
43 | }
44 |
45 | sh "jenkins/saveAndCompress.sh"
46 | step([$class: 'ClassicUploadStep', credentialsId: env.OAUTH_BUCKET_CRED_ID, bucket: "gs://${BUILD_ARTIFACTS_BUCKET}", pattern: env.BUILD_ARTIFACTS])
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
16 | Jenkins Google OAuth Credentials Plugin
17 | =====================
18 | The Google OAuth plugin provides a Google-specific implementation of the OAuth Credentials interfaces.
19 |
20 | ## Documentation
21 | Please see [Google OAuth Plugin](docs/home.md) for complete documentation.
22 |
23 | ## Installation
24 | 1. Go to **Manage Jenkins** then **Manage Plugins**.
25 | 1. (Optional) Make sure the plugin manager has updated data by clicking the **Check now** button.
26 | 1. In the Plugin Manager, click the **Available** tab and look for the "Google OAuth Credentials".
27 | 1. Check the box under the **Install** column and click the **Install without restart** button.
28 | 1. If the plugin does not appear under **Available**, make sure it appears under **Installed** and is enabled.
29 |
30 | ## Plugin Source Build Installation
31 | See [Plugin Source Build Installation](docs/source_build_installation.md) to build and install from source.
32 |
33 | ## Feature requests and bug reports
34 | Please file feature requests and bug reports as [GitHub Issues](https://github.com/jenkinsci/google-oauth-plugin/issues).
35 |
36 | ## Community
37 |
38 | The GCP Jenkins community uses the **#gcp-jenkins** slack channel on
39 | [https://googlecloud-community.slack.com](https://googlecloud-community.slack.com)
40 | to ask questions and share feedback. Invitation link available
41 | here: [gcp-slack](https://cloud.google.com/community#home-support).
42 |
43 | ## License
44 | See [LICENSE](LICENSE)
45 |
46 | ## Contributing
47 | See [CONTRIBUTING.md](CONTRIBUTING.md)
48 |
--------------------------------------------------------------------------------
/checkstyleJavaHeader:
--------------------------------------------------------------------------------
1 | ^/\*$
2 | ^ \* Copyright 20\d\d Google LLC*$
3 | ^ \*$
4 | ^ \* Licensed under the Apache License, Version 2\.0 \(the \"License\"\);$
5 | ^ \* you may not use this file except in compliance with the License\.$
6 | ^ \* You may obtain a copy of the License at$
7 | ^ \*$
8 | ^ \* https://www\.apache\.org/licenses/LICENSE-2\.0$
9 | ^ \*$
10 | ^ \* Unless required by applicable law or agreed to in writing, software$
11 | ^ \* distributed under the License is distributed on an \"AS IS\" BASIS,$
12 | ^ \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.$
13 | ^ \* See the License for the specific language governing permissions and$
14 | ^ \* limitations under the License\.$
15 | ^ \*/
16 |
--------------------------------------------------------------------------------
/docs/home.md:
--------------------------------------------------------------------------------
1 |
14 | # Google OAuth Plugin Documentation
15 |
16 | This plugin implements the OAuth Credentials interfaces for surfacing [Google Service Accounts](https://cloud.google.com/iam/docs/understanding-service-accounts) to Jenkins. This plugin allows for the registration of Google Service Account Credentials with the Jenkins controller, which can be used to interact with Google APIs.
17 |
18 | ## Google OAuth Credentials
19 |
20 | This plugin surfaces two Credential types for generating OAuth credentials for a Google service account:
21 |
22 | 1. [Google Service Account from private key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) use this option with the private key for your Google service account. See our embedded help section for details on how/where this is obtained, or here.
23 | * You can create service accounts via the Google Developers Console, under APIs & auth → Credentials → Create new Client ID → Service account
24 | * The downloaded P12 key and the service account email address can then be used when adding the credential to Jenkins; or if you created a new JSON key, you can use that instead
25 |
26 | 1. [Google Service Account from metadata](https://cloud.google.com/compute/docs/storing-retrieving-metadata) use this option for obtaining the service account information from Google Compute Engine metadata. This option is only available when the Jenkins controller is run on a Google Compute Engine VM.
27 |
28 | 
29 |
30 | ## Private Key Credentials
31 |
32 | Credentials generated from a private key provide uninhibited access to your project as the robot, so we also surface the necessary Domain specification functionality to restrict the scopes for which such a credential may be used by plugins.
33 |
34 | For example, if you only want this service account to be used for “Google Drive”, but there are plugins that might use “Google Calendar”, we want this credential to show up as an option for the former, but not the latter.
35 |
36 | An example of a private key restricted for use with [Google Cloud Storage](https://github.com/jenkinsci/google-storage-plugin).
37 |
38 | 
39 |
40 | **NOTE**: As new plugins are installed, which bring in new OAuth scopes, this domain restriction allows those new scopes to be opt-in vs. opt-out.
41 |
42 | ## Metadata Credentials
43 |
44 | Credentials obtained via [Google Compute Engine metadata](https://cloud.google.com/compute/docs/) are inherently limited to the scopes to which you bound the VM when it was provisioned. You will see this set of scopes listed alongside the credential.
45 |
46 | 
47 |
48 | For an example of how to consume these credentials, please see our [Google Cloud Storage Plugin](https://github.com/jenkinsci/google-storage-plugin).
49 |
50 |
--------------------------------------------------------------------------------
/docs/images/dropdown.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/google-oauth-plugin/31633a5e08313b7871be6e4622b5632699628e98/docs/images/dropdown.jpg
--------------------------------------------------------------------------------
/docs/images/metadata.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/google-oauth-plugin/31633a5e08313b7871be6e4622b5632699628e98/docs/images/metadata.jpg
--------------------------------------------------------------------------------
/docs/images/private-key.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/google-oauth-plugin/31633a5e08313b7871be6e4622b5632699628e98/docs/images/private-key.jpg
--------------------------------------------------------------------------------
/docs/source_build_installation.md:
--------------------------------------------------------------------------------
1 |
14 | # Plugin Source Build Installation
15 |
16 | 1. Clone the plugin and enter the directory:
17 | ```bash
18 | git clone git@github.com:jenkinsci/google-oauth-plugin.git
19 | cd google-oauth-plugin
20 | ```
21 | 1. Checkout the branch that you would like to build from:
22 | ```bash
23 | git checkout
24 | ```
25 | 1. Build the plugin into a .hpi plugin file:
26 | ```bash
27 | mvn hpi:hpi
28 | ```
29 | 1. Go to **Manage Jenkins** then **Manage Plugins**.
30 | 1. In the Plugin Manager, click the **Advanced** tab and then **Choose File** under the **Upload Plugin** section.
31 | 1. Choose the Jenkins plugin file built in Step 3.
32 | 1. Click the **Upload** button.
33 |
--------------------------------------------------------------------------------
/jenkins/maven-pod.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | apiVersion: v1
16 | kind: Pod
17 | spec:
18 | containers:
19 | - name: maven
20 | image: maven:3.6.2-jdk-8
21 | command: ['cat']
22 | tty: true
23 |
--------------------------------------------------------------------------------
/jenkins/saveAndCompress.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Copyright 2019 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | pwd
16 | mkdir result
17 |
18 | # The files will only exist if maven reaches the corresponding phase of the build
19 | function cpe() {
20 | if [[ -e target/$1 ]]; then
21 | cp -rv target/$1 result
22 | else
23 | echo "target/$1 not copied, check the completed build phases."
24 | fi
25 | }
26 |
27 | # Copy over the important artifacts
28 | cpe google-oauth-plugin.hpi
29 | cpe failsafe-reports
30 | cpe surefire-reports
31 |
32 | # Compress the artifacts for upload
33 | tar -zcvf ${BUILD_ARTIFACTS} result
34 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleOAuth2Credentials.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.google.api.client.auth.oauth2.Credential;
19 | import java.security.GeneralSecurityException;
20 |
21 | /**
22 | * Google-specific username / access token combination.
23 | *
24 | *
Implementations surface an API for obtaining the Google-standard {@link Credential} object for
25 | * interacting with OAuth2 APIs.
26 | */
27 | public interface GoogleOAuth2Credentials extends StandardUsernameOAuth2Credentials {
28 | /**
29 | * Fetches a Credential for the set of OAuth 2.0 scopes required.
30 | *
31 | * @param requirement The set of required OAuth 2.0 scopes
32 | * @return The Credential authorizing usage of the API scopes
33 | * @throws GeneralSecurityException when the authentication fails
34 | */
35 | Credential getGoogleCredential(GoogleOAuth2ScopeRequirement requirement) throws GeneralSecurityException;
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleOAuth2ScopeRequirement.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | /** Used to type filter Google-specific {@link OAuth2ScopeRequirement}s. */
19 | public abstract class GoogleOAuth2ScopeRequirement extends OAuth2ScopeRequirement {}
20 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleOAuth2ScopeSpecification.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import hudson.Extension;
19 | import java.util.Collection;
20 | import org.kohsuke.stapler.DataBoundConstructor;
21 |
22 | /**
23 | * A Google-specific implementation of the {@link OAuth2ScopeSpecification} that limits its
24 | * application to Google-specific {@link OAuth2ScopeRequirement}
25 | */
26 | public class GoogleOAuth2ScopeSpecification extends OAuth2ScopeSpecification {
27 | @DataBoundConstructor
28 | public GoogleOAuth2ScopeSpecification(Collection specifiedScopes) {
29 | super(specifiedScopes);
30 | }
31 |
32 | /**
33 | * Denoted this class is a {@code DomainSpecification} plugin, in particular for {@link
34 | * OAuth2ScopeSpecification}
35 | */
36 | @Extension
37 | public static class DescriptorImpl extends OAuth2ScopeSpecification.Descriptor {
38 | public DescriptorImpl() {
39 | super(GoogleOAuth2ScopeRequirement.class);
40 | }
41 |
42 | /** {@inheritDoc} */
43 | @Override
44 | public String getDisplayName() {
45 | return Messages.GoogleOAuth2ScopeSpecification_DisplayName();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleRobotCredentialsModule.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.google.api.client.http.HttpTransport;
19 | import com.google.api.client.http.javanet.NetHttpTransport;
20 | import com.google.api.client.json.JsonFactory;
21 | import com.google.api.client.json.jackson2.JacksonFactory;
22 | import java.io.Serializable;
23 |
24 | /**
25 | * An abstraction interface for instantiating the dependencies of the {@link
26 | * GoogleRobotCredentials}.
27 | */
28 | public class GoogleRobotCredentialsModule implements Serializable {
29 | /** The HttpTransport to use for credential related requests. */
30 | public HttpTransport getHttpTransport() {
31 | return new NetHttpTransport();
32 | }
33 |
34 | /** The HttpTransport to use for credential related requests. */
35 | public JsonFactory getJsonFactory() {
36 | return new JacksonFactory();
37 | }
38 |
39 | /** For {@link Serializable} */
40 | private static final long serialVersionUID = 1L;
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleRobotMetadataCredentialsModule.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.google.jenkins.plugins.util.MetadataReader;
19 |
20 | /**
21 | * An abstraction interface for instantiating the dependencies of the {@link
22 | * GoogleRobotMetadataCredentials}.
23 | */
24 | public class GoogleRobotMetadataCredentialsModule extends GoogleRobotCredentialsModule {
25 | /** Retrieve a MetadataReader for accessing stuff encoded in the instance metadata. */
26 | public MetadataReader getMetadataReader() {
27 | return new MetadataReader.Default();
28 | }
29 |
30 | /** For {@link java.io.Serializable} */
31 | private static final long serialVersionUID = 1L;
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleRobotNameProvider.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.cloudbees.plugins.credentials.CredentialsNameProvider;
19 |
20 | /** Retrieve a user-friendly name to be used when listing the credential for use by plugins. */
21 | public class GoogleRobotNameProvider extends CredentialsNameProvider {
22 | /** {@inheritDoc} */
23 | @Override
24 | public String getName(GoogleRobotCredentials credentials) {
25 | return credentials.getProjectId();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/JsonKey.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.google.api.client.json.GenericJson;
19 | import com.google.api.client.json.JsonFactory;
20 | import com.google.api.client.util.Key;
21 | import com.google.common.base.Charsets;
22 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
23 | import hudson.util.Secret;
24 | import java.io.IOException;
25 | import java.io.InputStream;
26 | import java.io.InputStreamReader;
27 | import org.apache.commons.io.IOUtils;
28 |
29 | /**
30 | * The Google Developer Console provides private
31 | * keys for service accounts in two different ways. one of them is a .json file that can be
32 | * downloaded from the Google Developer Console.
33 | *
34 | *
The structure of this json file is:
35 | * {
36 | * "private_key":"-----BEGIN PRIVATE KEY-----\n
37 | * ...
38 | * \n-----END PRIVATE KEY-----\n",
39 | * "client_email":"...@developer.gserviceaccount.com",
40 | * ...
41 | * }
42 | *
43 | */
44 | @SuppressFBWarnings("EQ_DOESNT_OVERRIDE_EQUALS")
45 | public final class JsonKey extends GenericJson {
46 | @Key("client_email")
47 | private String clientEmail;
48 |
49 | @Key("private_key")
50 | private String privateKey;
51 |
52 | public static JsonKey load(JsonFactory jsonFactory, InputStream inputStream) throws IOException {
53 | InputStreamReader reader = new InputStreamReader(inputStream, Charsets.UTF_8);
54 | try {
55 | Secret decoded = Secret.fromString(IOUtils.toString(reader));
56 | return jsonFactory.fromString(decoded.getPlainText(), JsonKey.class);
57 | } finally {
58 | inputStream.close();
59 | }
60 | }
61 |
62 | public String getClientEmail() {
63 | return clientEmail;
64 | }
65 |
66 | public void setClientEmail(String clientEmail) {
67 | this.clientEmail = clientEmail;
68 | }
69 |
70 | public String getPrivateKey() {
71 | return privateKey;
72 | }
73 |
74 | public void setPrivateKey(String privateKey) {
75 | this.privateKey = privateKey;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/JsonServiceAccountConfig.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.cloudbees.plugins.credentials.SecretBytes;
19 | import com.google.api.client.json.jackson2.JacksonFactory;
20 | import com.google.api.client.util.PemReader;
21 | import com.google.api.client.util.Strings;
22 | import edu.umd.cs.findbugs.annotations.CheckForNull;
23 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
24 | import hudson.Extension;
25 | import java.io.ByteArrayInputStream;
26 | import java.io.IOException;
27 | import java.io.StringReader;
28 | import java.security.KeyFactory;
29 | import java.security.NoSuchAlgorithmException;
30 | import java.security.PrivateKey;
31 | import java.security.spec.InvalidKeySpecException;
32 | import java.security.spec.PKCS8EncodedKeySpec;
33 | import java.util.logging.Level;
34 | import java.util.logging.Logger;
35 | import jenkins.model.Jenkins;
36 | import org.apache.commons.fileupload.FileItem;
37 | import org.kohsuke.accmod.Restricted;
38 | import org.kohsuke.accmod.restrictions.DoNotUse;
39 | import org.kohsuke.stapler.DataBoundConstructor;
40 | import org.kohsuke.stapler.DataBoundSetter;
41 |
42 | /**
43 | * Provides authentication mechanism for a service account by setting a JSON private key file. The
44 | * JSON file structure needs to be:
45 | *
46 | *
47 | * {
48 | * "private_key":"-----BEGIN PRIVATE KEY-----\n
49 | * ...
50 | * \n-----END PRIVATE KEY-----\n",
51 | * "client_email":"...@developer.gserviceaccount.com",
52 | * ...
53 | * }
54 | *
55 | */
56 | public class JsonServiceAccountConfig extends ServiceAccountConfig {
57 | /*
58 | * TODO(jenkinsci/google-oauth-plugin#50): Dedupe shared functionality in
59 | * google-auth-library.
60 | */
61 |
62 | private static final long serialVersionUID = 6818111194672325387L;
63 | private static final Logger LOGGER = Logger.getLogger(JsonServiceAccountConfig.class.getSimpleName());
64 |
65 | @CheckForNull
66 | private String filename;
67 |
68 | @CheckForNull
69 | private SecretBytes secretJsonKey;
70 |
71 | @Deprecated // for migration purpose
72 | @CheckForNull
73 | private transient String jsonKeyFile;
74 |
75 | private transient JsonKey jsonKey;
76 |
77 | /** @since 0.8 */
78 | @DataBoundConstructor
79 | public JsonServiceAccountConfig() {}
80 |
81 | /**
82 | * For being able to load credentials created with versions < 0.8 and backwards compatibility
83 | * with external callers.
84 | *
85 | * @param jsonKeyFile The uploaded JSON key file.
86 | * @param prevJsonKeyFile The path of the previous JSON key file.
87 | * @since 0.3
88 | */
89 | @Deprecated
90 | public JsonServiceAccountConfig(FileItem jsonKeyFile, String prevJsonKeyFile) {
91 | this.setJsonKeyFileUpload(jsonKeyFile);
92 | if (filename == null && prevJsonKeyFile != null) {
93 | this.filename = extractFilename(prevJsonKeyFile);
94 | this.secretJsonKey = getSecretBytesFromFile(prevJsonKeyFile);
95 | }
96 | }
97 |
98 | /** @param jsonKeyFileUpload The uploaded JSON key file. */
99 | @DataBoundSetter // Called on form submit, only used when key file is uploaded
100 | public void setJsonKeyFileUpload(FileItem jsonKeyFileUpload) {
101 | if (jsonKeyFileUpload != null && jsonKeyFileUpload.getSize() > 0) {
102 | try {
103 | JsonKey jsonKey = JsonKey.load(new JacksonFactory(), jsonKeyFileUpload.getInputStream());
104 | if (jsonKey.getClientEmail() != null && jsonKey.getPrivateKey() != null) {
105 | this.filename = extractFilename(jsonKeyFileUpload.getName());
106 | this.secretJsonKey = SecretBytes.fromBytes(jsonKeyFileUpload.get());
107 | }
108 | } catch (IOException e) {
109 | LOGGER.log(Level.SEVERE, "Failed to read JSON key from file", e);
110 | }
111 | }
112 | }
113 |
114 | /** @param filename The JSON key file name. */
115 | @DataBoundSetter
116 | public void setFilename(String filename) {
117 | String newFilename = extractFilename(filename);
118 | if (!Strings.isNullOrEmpty(newFilename)) {
119 | this.filename = newFilename;
120 | }
121 | }
122 |
123 | /** @param secretJsonKey The JSON key file content. */
124 | @DataBoundSetter
125 | public void setSecretJsonKey(SecretBytes secretJsonKey) {
126 | if (secretJsonKey != null && secretJsonKey.getPlainData().length > 0) {
127 | this.secretJsonKey = secretJsonKey;
128 | }
129 | }
130 |
131 | @CheckForNull
132 | private static String extractFilename(@CheckForNull String path) {
133 | if (path == null) {
134 | return null;
135 | }
136 | return path.replaceFirst("^.+[/\\\\]", "");
137 | }
138 |
139 | @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
140 | private Object readResolve() {
141 | if (secretJsonKey == null) {
142 | // google-oauth-plugin < 0.7
143 | return new JsonServiceAccountConfig(null, getJsonKeyFile());
144 | }
145 | return this;
146 | }
147 |
148 | @Override
149 | public DescriptorImpl getDescriptor() {
150 | return (DescriptorImpl) Jenkins.get().getDescriptorOrDie(JsonServiceAccountConfig.class);
151 | }
152 |
153 | /**
154 | * @return Original uploaded file name.
155 | * @since 0.7
156 | */
157 | @CheckForNull
158 | public String getFilename() {
159 | return filename;
160 | }
161 |
162 | @Restricted(DoNotUse.class) // UI: Required for stapler call of setter.
163 | @CheckForNull
164 | public SecretBytes getSecretJsonKey() {
165 | return secretJsonKey;
166 | }
167 |
168 | @Deprecated
169 | public String getJsonKeyFile() {
170 | return jsonKeyFile;
171 | }
172 |
173 | /**
174 | * For use in UI, do not use.
175 | *
176 | * @return The uploaded JSON key file.
177 | */
178 | @Deprecated
179 | @Restricted(DoNotUse.class) // UI: Required for stapler call of setter.
180 | public FileItem getJsonKeyFileUpload() {
181 | return null;
182 | }
183 |
184 | /**
185 | * In this context the service account id is represented by the email address for that service
186 | * account, which should be contained in the JSON key.
187 | *
188 | * @return The service account identifier. Null if no JSON key has been provided.
189 | */
190 | @Override
191 | public String getAccountId() {
192 | JsonKey jsonKey = getJsonKey();
193 | if (jsonKey != null) {
194 | return jsonKey.getClientEmail();
195 | }
196 | return null;
197 | }
198 |
199 | /**
200 | * @return The {@link PrivateKey} that comes from the secret JSON key. Null if this service
201 | * account config contains no key or if the key is malformed.
202 | */
203 | @Override
204 | public PrivateKey getPrivateKey() {
205 | JsonKey jsonKey = getJsonKey();
206 | if (jsonKey != null) {
207 | String privateKey = jsonKey.getPrivateKey();
208 | if (privateKey != null && !privateKey.isEmpty()) {
209 | PemReader pemReader = new PemReader(new StringReader(privateKey));
210 | try {
211 | PemReader.Section section = pemReader.readNextSection();
212 | if (section != null) {
213 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(section.getBase64DecodedBytes());
214 | return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
215 | } else {
216 | LOGGER.severe("The provided private key is malformed.");
217 | }
218 | } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
219 | LOGGER.log(Level.SEVERE, "Failed to read private key", e);
220 | }
221 | }
222 | }
223 | return null;
224 | }
225 |
226 | private JsonKey getJsonKey() {
227 | if (jsonKey == null && secretJsonKey != null && secretJsonKey.getPlainData().length > 0) {
228 | try {
229 | jsonKey = JsonKey.load(new JacksonFactory(), new ByteArrayInputStream(secretJsonKey.getPlainData()));
230 | } catch (IOException ignored) {
231 | }
232 | }
233 | return jsonKey;
234 | }
235 |
236 | /** Descriptor for JSON service account authentication. */
237 | @Extension
238 | public static final class DescriptorImpl extends Descriptor {
239 | @Override
240 | public String getDisplayName() {
241 | return Messages.JsonServiceAccountConfig_DisplayName();
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/KeyUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import hudson.util.Secret;
19 | import java.io.File;
20 | import java.io.FileOutputStream;
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.nio.charset.StandardCharsets;
24 | import jenkins.model.Jenkins;
25 | import org.apache.commons.io.IOUtils;
26 |
27 | /**
28 | * Utility methods for handling key files.
29 | *
30 | * @deprecated Consider to use {@link com.cloudbees.plugins.credentials.SecretBytes} instead.
31 | */
32 | @Deprecated
33 | public class KeyUtils {
34 | /** Utility class should never be instantiated. */
35 | private KeyUtils() {}
36 |
37 | /**
38 | * Creates a file with the given prefix/suffix in a standard Google auth directory, and sets the
39 | * permissions of the file to owner-only read/write. Note: this doesn't work on Windows.
40 | *
41 | * @throws IOException if filesystem interaction fails.
42 | */
43 | public static File createKeyFile(String prefix, String suffix) throws IOException {
44 | File keyFolder = new File(Jenkins.getInstance().getRootDir(), "gauth");
45 | if (keyFolder.exists() || keyFolder.mkdirs()) {
46 | File result = File.createTempFile(prefix, suffix, keyFolder);
47 | if (result == null) {
48 | throw new IOException("Failed to create key file");
49 | }
50 | updatePermissions(result);
51 | return result;
52 | } else {
53 | throw new IOException("Failed to create key folder");
54 | }
55 | }
56 |
57 | /**
58 | * Sets the permissions of the file to owner-only read/write. Note: this doesn't work on Windows.
59 | *
60 | * @throws IOException if filesystem interaction fails.
61 | */
62 | public static void updatePermissions(File file) throws IOException {
63 | if (file == null || !file.exists()) {
64 | return;
65 | }
66 | // Set world read/write permissions to false.
67 | // Set owner read/write permissions to true.
68 | if (!file.setReadable(false, false)
69 | || !file.setWritable(false, false)
70 | || !file.setReadable(true, true)
71 | || !file.setWritable(true, true)) {
72 | throw new IOException("Failed to update key file permissions");
73 | }
74 | }
75 |
76 | /**
77 | * Writes the given key to the given keyfile, passing it through {@link Secret} to encode the
78 | * string. Note that, per the documentation of {@link Secret}, this does not protect against an
79 | * attacker who has full access to the local file system, but reduces the chance of accidental
80 | * exposure.
81 | */
82 | public static void writeKeyToFileEncoded(String key, File file) throws IOException {
83 | if (key == null || file == null) {
84 | return;
85 | }
86 | Secret encoded = Secret.fromString(key);
87 | writeKeyToFile(IOUtils.toInputStream(encoded.getEncryptedValue(), StandardCharsets.UTF_8), file);
88 | }
89 |
90 | /**
91 | * Writes the key contained in the given {@link InputStream} to the given keyfile. Does not close
92 | * the input stream.
93 | */
94 | public static void writeKeyToFile(InputStream keyStream, File file) throws IOException {
95 | if (keyStream == null || file == null) {
96 | return;
97 | }
98 | FileOutputStream out = null;
99 | try {
100 | out = new FileOutputStream(file);
101 | IOUtils.copy(keyStream, out);
102 | } finally {
103 | IOUtils.closeQuietly(out);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/LegacyJsonKey.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
19 | import com.google.api.client.json.GenericJson;
20 | import com.google.api.client.json.JsonFactory;
21 | import com.google.api.client.util.Key;
22 | import com.google.common.base.Charsets;
23 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
24 | import java.io.IOException;
25 | import java.io.InputStream;
26 |
27 | /**
28 | * For "Robot" service account client secrets a key piece of information is the email address
29 | * contained within "client_secrets.json", which the existing {@link GoogleClientSecrets} class does
30 | * not parse. This makeshift partial copy of {@link GoogleClientSecrets} implements *just* the
31 | * "client_email" parsing.
32 | *
33 | * @author Matt Moore
34 | */
35 | @Deprecated
36 | @SuppressWarnings("deprecation")
37 | @SuppressFBWarnings("EQ_DOESNT_OVERRIDE_EQUALS")
38 | public final class LegacyJsonKey extends GenericJson {
39 |
40 | /** Details for web applications. */
41 | @Key
42 | private Details web;
43 |
44 | /** Returns the details for web applications. */
45 | public Details getWeb() {
46 | return web;
47 | }
48 |
49 | public void setWeb(Details web) {
50 | this.web = web;
51 | }
52 |
53 | /** Container for our new field, modeled after: {@link GoogleClientSecrets.Details} */
54 | public static final class Details extends GenericJson {
55 | /** Client email. */
56 | @Key("client_email")
57 | private String clientEmail;
58 |
59 | public void setClientEmail(String clientEmail) {
60 | this.clientEmail = clientEmail;
61 | }
62 |
63 | /** Returns the client email. */
64 | public String getClientEmail() {
65 | return clientEmail;
66 | }
67 | }
68 |
69 | /** Loads the {@code client_secrets.json} file from the given input stream. */
70 | public static LegacyJsonKey load(JsonFactory jsonFactory, InputStream inputStream) throws IOException {
71 | return jsonFactory.fromInputStream(inputStream, Charsets.UTF_8, LegacyJsonKey.class);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/P12ServiceAccountConfig.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.cloudbees.plugins.credentials.SecretBytes;
19 | import com.google.api.client.util.Strings;
20 | import edu.umd.cs.findbugs.annotations.CheckForNull;
21 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22 | import hudson.Extension;
23 | import hudson.util.FormValidation;
24 | import java.io.ByteArrayInputStream;
25 | import java.io.IOException;
26 | import java.io.InputStream;
27 | import java.security.GeneralSecurityException;
28 | import java.security.KeyStore;
29 | import java.security.KeyStoreException;
30 | import java.security.NoSuchAlgorithmException;
31 | import java.security.PrivateKey;
32 | import java.security.cert.CertificateException;
33 | import java.util.logging.Level;
34 | import java.util.logging.Logger;
35 | import jenkins.model.Jenkins;
36 | import org.apache.commons.fileupload.FileItem;
37 | import org.apache.commons.io.IOUtils;
38 | import org.kohsuke.accmod.Restricted;
39 | import org.kohsuke.accmod.restrictions.DoNotUse;
40 | import org.kohsuke.stapler.DataBoundConstructor;
41 | import org.kohsuke.stapler.DataBoundSetter;
42 | import org.kohsuke.stapler.QueryParameter;
43 | import org.kohsuke.stapler.verb.POST;
44 |
45 | /**
46 | * Provides authentication mechanism for a service account by setting a service account email
47 | * address and P12 private key file.
48 | */
49 | public class P12ServiceAccountConfig extends ServiceAccountConfig {
50 | /*
51 | * TODO(jenkinsci/google-oauth-plugin#50): Dedupe shared functionality in
52 | * google-auth-library.
53 | */
54 |
55 | private static final long serialVersionUID = 8706353638974721795L;
56 | private static final Logger LOGGER = Logger.getLogger(P12ServiceAccountConfig.class.getSimpleName());
57 | private static final String DEFAULT_P12_SECRET = "notasecret";
58 | private static final String DEFAULT_P12_ALIAS = "privatekey";
59 | private final String emailAddress;
60 |
61 | @CheckForNull
62 | private String filename;
63 |
64 | @CheckForNull
65 | private SecretBytes secretP12Key;
66 |
67 | @Deprecated // for migration purpose
68 | @CheckForNull
69 | private transient String p12KeyFile;
70 |
71 | /**
72 | * @param emailAddress The service account email address.
73 | * @since 0.8
74 | */
75 | @DataBoundConstructor
76 | public P12ServiceAccountConfig(String emailAddress) {
77 | this.emailAddress = emailAddress;
78 | }
79 |
80 | /**
81 | * For being able to load credentials created with versions < 0.8 and backwards compatibility
82 | * with external callers.
83 | *
84 | * @param emailAddress The service account email address.
85 | * @param p12KeyFileUpload The uploaded p12 key file.
86 | * @param prevP12KeyFile The path of the previous p12 key file.
87 | * @since 0.3
88 | */
89 | @Deprecated
90 | public P12ServiceAccountConfig(String emailAddress, FileItem p12KeyFileUpload, String prevP12KeyFile) {
91 | this(emailAddress);
92 | this.setP12KeyFileUpload(p12KeyFileUpload);
93 | if (filename == null && prevP12KeyFile != null) {
94 | this.setFilename(prevP12KeyFile);
95 | this.setSecretP12Key(getSecretBytesFromFile(prevP12KeyFile));
96 | }
97 | }
98 |
99 | /** @param p12KeyFile The uploaded p12 key file. */
100 | @Deprecated
101 | @DataBoundSetter // Called on form submit, only used when key file is uploaded
102 | public void setP12KeyFileUpload(FileItem p12KeyFile) {
103 | if (p12KeyFile != null && p12KeyFile.getSize() > 0) {
104 | this.filename = extractFilename(p12KeyFile.getName());
105 | this.secretP12Key = SecretBytes.fromBytes(p12KeyFile.get());
106 | }
107 | }
108 |
109 | /** @param filename The previous p12 key file name. */
110 | @DataBoundSetter
111 | public void setFilename(String filename) {
112 | if (!Strings.isNullOrEmpty(filename)) {
113 | this.filename = extractFilename(filename);
114 | }
115 | }
116 |
117 | /** @param secretP12Key The previous p12 key file content. */
118 | @DataBoundSetter
119 | public void setSecretP12Key(SecretBytes secretP12Key) {
120 | if (secretP12Key != null && secretP12Key.getPlainData().length > 0) {
121 | this.secretP12Key = secretP12Key;
122 | }
123 | }
124 |
125 | @CheckForNull
126 | private static String extractFilename(@CheckForNull String path) {
127 | if (path == null) {
128 | return null;
129 | }
130 | return path.replaceFirst("^.+[/\\\\]", "");
131 | }
132 |
133 | @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
134 | private Object readResolve() {
135 | if (secretP12Key == null) {
136 | // google-oauth-plugin < 0.7
137 | return new P12ServiceAccountConfig(
138 | getEmailAddress(),
139 | null, // p12KeyFileUpload
140 | getP12KeyFile());
141 | }
142 | return this;
143 | }
144 |
145 | @Override
146 | public DescriptorImpl getDescriptor() {
147 | return (DescriptorImpl) Jenkins.get().getDescriptorOrDie(P12ServiceAccountConfig.class);
148 | }
149 |
150 | public String getEmailAddress() {
151 | return emailAddress;
152 | }
153 |
154 | /**
155 | * @return Original uploaded file name.
156 | * @since 0.7
157 | */
158 | @CheckForNull
159 | public String getFilename() {
160 | return filename;
161 | }
162 |
163 | /**
164 | * Do not use, required for UI.
165 | *
166 | * @return The secret p12 key.
167 | */
168 | @Restricted(DoNotUse.class) // UI: Required for stapler call of setter.
169 | @CheckForNull
170 | public SecretBytes getSecretP12Key() {
171 | return secretP12Key;
172 | }
173 |
174 | /** @return The path of the previous p12 key file. */
175 | @Deprecated
176 | public String getP12KeyFile() {
177 | return p12KeyFile;
178 | }
179 |
180 | /**
181 | * Do not use, required for UI.
182 | *
183 | * @return The uploaded p12 key file.
184 | */
185 | @Deprecated
186 | @Restricted(DoNotUse.class) // UI: Required for stapler call of setter.
187 | public FileItem getP12KeyFileUpload() {
188 | return null;
189 | }
190 |
191 | @Override
192 | public String getAccountId() {
193 | return getEmailAddress();
194 | }
195 |
196 | @Override
197 | public PrivateKey getPrivateKey() {
198 | try {
199 | KeyStore p12KeyStore = getP12KeyStore();
200 | if (p12KeyStore == null) {
201 | return null;
202 | }
203 | return (PrivateKey) p12KeyStore.getKey(DEFAULT_P12_ALIAS, DEFAULT_P12_SECRET.toCharArray());
204 | } catch (IOException | GeneralSecurityException e) {
205 | LOGGER.log(Level.SEVERE, "Failed to read private key", e);
206 | }
207 | return null;
208 | }
209 |
210 | @CheckForNull
211 | private KeyStore getP12KeyStore()
212 | throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
213 | InputStream in = null;
214 | if (secretP12Key == null) {
215 | return null;
216 | }
217 | try {
218 | KeyStore keyStore = KeyStore.getInstance("PKCS12");
219 | in = new ByteArrayInputStream(secretP12Key.getPlainData());
220 | keyStore.load(in, DEFAULT_P12_SECRET.toCharArray());
221 | return keyStore;
222 | } finally {
223 | IOUtils.closeQuietly(in);
224 | }
225 | }
226 |
227 | /** Descriptor for P12 service account authentication. */
228 | @Extension
229 | public static final class DescriptorImpl extends Descriptor {
230 |
231 | @POST
232 | public FormValidation doCheckEmailAddress(@QueryParameter("emailAddress") final String emailAddress) {
233 | if (Strings.isNullOrEmpty(emailAddress)) {
234 | return FormValidation.error(Messages.P12ServiceAccountConfig_ErrorEmailRequired());
235 | }
236 | return FormValidation.ok();
237 | }
238 |
239 | @Override
240 | public String getDisplayName() {
241 | return Messages.P12ServiceAccountConfig_DisplayName();
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/RemotableGoogleCredentials.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import static com.google.common.base.Preconditions.checkNotNull;
19 |
20 | import com.cloudbees.plugins.credentials.CredentialsScope;
21 | import com.google.api.client.auth.oauth2.Credential;
22 | import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
23 | import com.google.common.collect.Ordering;
24 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
25 | import java.io.IOException;
26 | import java.security.GeneralSecurityException;
27 | import org.joda.time.DateTime;
28 |
29 | /**
30 | * As some implementations of {@link GoogleRobotCredentials} are bound to the controller, this
31 | * ephemeral credential is remoted in place of those. The use case is basically that when a plugin
32 | * needs to remote credential C, with some requirement R, it would instead remote {@code
33 | * C.forRemote(R)} to instantiate one of these.
34 | *
35 | *
TODO(mattmoor): Consider ways to use channels to remove the time limitation that this has
36 | * (access token expires).
37 | *
38 | * @author Matt Moore
39 | */
40 | final class RemotableGoogleCredentials extends GoogleRobotCredentials {
41 | /**
42 | * Construct a remotable credential. This should never be used directly, which is why this class
43 | * is {@code package-private}. This should only be called from {@link
44 | * GoogleRobotCredentials#forRemote}.
45 | */
46 | @SuppressFBWarnings(
47 | value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
48 | justification = "False positive from what I can see in Ordering.natural().nullsFirst()")
49 | public RemotableGoogleCredentials(
50 | GoogleRobotCredentials credentials,
51 | GoogleOAuth2ScopeRequirement requirement,
52 | GoogleRobotCredentialsModule module)
53 | throws GeneralSecurityException {
54 | super(
55 | credentials.getScope() == null ? CredentialsScope.GLOBAL : credentials.getScope(),
56 | "",
57 | checkNotNull(credentials).getProjectId(),
58 | checkNotNull(module));
59 |
60 | this.username = credentials.getUsername();
61 |
62 | // Eagerly create the access token we will use on the remote machine.
63 | Credential credential = credentials.getGoogleCredential(checkNotNull(requirement));
64 | try {
65 | Long rawExpiration = credential.getExpiresInSeconds();
66 |
67 | if (Ordering.natural().nullsFirst().compare(rawExpiration, MINIMUM_DURATION_SECONDS) < 0) {
68 | if (!credential.refreshToken()) {
69 | throw new GeneralSecurityException(Messages.RemotableGoogleCredentials_NoAccessToken());
70 | }
71 | }
72 | } catch (IOException e) {
73 | throw new GeneralSecurityException(Messages.RemotableGoogleCredentials_NoAccessToken(), e);
74 | }
75 | this.accessToken = checkNotNull(credential.getAccessToken());
76 | this.expiration = new DateTime()
77 | .plusSeconds(checkNotNull(credential.getExpiresInSeconds()).intValue())
78 | .getMillis();
79 | }
80 | /**
81 | * Construct a remotable credential. This should never be used directly - this constructor is only
82 | * for migrating old credentials that had no id and relied on the projectId during readResolve().
83 | */
84 | private RemotableGoogleCredentials(
85 | CredentialsScope scope,
86 | String id,
87 | String projectId,
88 | String description,
89 | GoogleRobotCredentialsModule module,
90 | String username,
91 | String accessToken,
92 | long expiration) {
93 | super(scope, id, projectId, description, module);
94 | this.username = username;
95 | this.accessToken = accessToken;
96 | this.expiration = expiration;
97 | }
98 |
99 | @SuppressFBWarnings(
100 | value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE",
101 | justification = "for migrating older credentials that did not have a separate id field, and would really "
102 | + "have a null id when attempted to deserialize. readResolve overwrites these nulls")
103 | private Object readResolve() throws Exception {
104 | return new RemotableGoogleCredentials(
105 | getScope() == null ? CredentialsScope.GLOBAL : getScope(),
106 | getId() == null ? getProjectId() : getId(),
107 | getProjectId(),
108 | getDescription(),
109 | getModule(),
110 | username,
111 | accessToken,
112 | expiration);
113 | }
114 |
115 | /** {@inheritDoc} */
116 | @Override
117 | public AbstractGoogleRobotCredentialsDescriptor getDescriptor() {
118 | throw new UnsupportedOperationException(Messages.RemotableGoogleCredentials_BadGetDescriptor());
119 | }
120 |
121 | /** {@inheritDoc} */
122 | @Override
123 | public String getUsername() {
124 | return username;
125 | }
126 |
127 | /** {@inheritDoc} */
128 | @Override
129 | public Credential getGoogleCredential(GoogleOAuth2ScopeRequirement requirement) throws GeneralSecurityException {
130 | // Return a credential synthesized from our stored access token
131 | // and expiration.
132 | //
133 | // TODO(mattmoor): Consider throwing an exception if the access token
134 | // has expired.
135 | long lifetimeSeconds = (expiration - new DateTime().getMillis()) / 1000;
136 |
137 | return new GoogleCredential.Builder()
138 | .setTransport(getModule().getHttpTransport())
139 | .setJsonFactory(getModule().getJsonFactory())
140 | .build()
141 | .setAccessToken(accessToken)
142 | .setExpiresInSeconds(lifetimeSeconds);
143 | }
144 |
145 | /** The identity of the credential. */
146 | private final String username;
147 |
148 | /** The access token eagerly retrieved from the original credential. */
149 | private final String accessToken;
150 |
151 | /** The time at which the accessToken will expire. */
152 | private final long expiration;
153 |
154 | /**
155 | * The minimum duration {@code 5 minutes} to allow for an access token before attempting to
156 | * refresh it.
157 | */
158 | private static final Long MINIMUM_DURATION_SECONDS = 300L;
159 | }
160 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/ServiceAccountConfig.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.credentials.oauth;
17 |
18 | import com.cloudbees.plugins.credentials.SecretBytes;
19 | import com.google.common.base.Strings;
20 | import edu.umd.cs.findbugs.annotations.CheckForNull;
21 | import hudson.model.Describable;
22 | import java.io.File;
23 | import java.io.IOException;
24 | import java.io.Serializable;
25 | import java.security.PrivateKey;
26 | import java.util.logging.Level;
27 | import java.util.logging.Logger;
28 | import jenkins.model.Jenkins;
29 | import org.apache.commons.io.FileUtils;
30 |
31 | /**
32 | * general abstraction for providing google service account authentication mechanism. subclasses
33 | * need to provide an accountId and a private key to use for authenticating a service account
34 | */
35 | public abstract class ServiceAccountConfig implements Describable, Serializable {
36 | private static final Logger LOGGER = Logger.getLogger(ServiceAccountConfig.class.getName());
37 | private static final long serialVersionUID = 6355493019938144806L;
38 |
39 | public abstract String getAccountId();
40 |
41 | public abstract PrivateKey getPrivateKey();
42 |
43 | @Deprecated // Used only for compatibility purposes.
44 | @CheckForNull
45 | protected SecretBytes getSecretBytesFromFile(@CheckForNull String filePath) {
46 | Jenkins.get().checkPermission(Jenkins.RUN_SCRIPTS);
47 |
48 | if (Strings.isNullOrEmpty(filePath)) {
49 | LOGGER.log(Level.SEVERE, "Provided file path is null or empty.");
50 | return null;
51 | }
52 |
53 | try {
54 | return SecretBytes.fromBytes(FileUtils.readFileToByteArray(new File(filePath)));
55 | } catch (IOException e) {
56 | LOGGER.log(Level.SEVERE, String.format("Failed to read previous key from %s", filePath), e);
57 | return null;
58 | }
59 | }
60 |
61 | /** abstract descriptor for service account authentication */
62 | public abstract static class Descriptor extends hudson.model.Descriptor {}
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/credentials/oauth/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /**
18 | * This package implements Jenkins plugins providing Google-specific OAuth2 Credentials, Domain
19 | * Requirements and Specifications.
20 | *
21 | *
For OAuth2, these are inherently a provider-specific triple because each provider (e.g.
22 | * Google, Facebook, GitHub) may only provide tokens for their own credentials and scopes. In a
23 | * nutshell, an OAuth2 access token is like "limited power of attorney". You are giving the bearer
24 | * of that token permission to interact with the set of limited scopes as the user who provided it.
25 | *
26 | *
This package provides the following Google-specific triple:
27 | *
28 | *
As the set of scopes determine what you may do with a credential, each plugin asks for an
44 | * access token by providing a provider-specific {@code OAuth2ScopeRequirement} to {@code
45 | * OAuth2Credentials.getAccessToken(OAuth2ScopeRequirement)}.
46 | *
47 | *
When enumerating credentials suitable for use with a given plugin, we only want to show those
48 | * that allow a suitable set of scopes. This is where {@code OAuth2ScopeRequirement} pairs with
49 | * {@code OAuth2ScopeSpecification}. An {@code OAuth2ScopeSpecification} is attached to a {@code
50 | * Domain} and is the superset of scopes to which the contained {@code Credentials} may be applied.
51 | *
52 | *
However, since entering OAuth2 scopes is unwieldy, we provide the necessary concepts to make
53 | * it multiple choice. Enter {@code DomainRequirementProvider}, a new {@code ExtensionPoint} that
54 | * allows {@code OAuth2ScopeSpecification} to automatically discover the set of OAuth2 scopes
55 | * required by installed plugins.
56 | *
57 | *
For Example:
58 | *
59 | *
60 | * {@literal @}RequiredDomain(value = MyGoogleOAuth2Requirement.class)
61 | * public class Foo extends SomeDescribable
62 | *
63 | *
64 | * In this example, the {@code DescribableDomainRequirementProvider} would discover that {@code Foo}
65 | * required the set of scopes specified by {@code MyGoogleOAuth2Requirement}. These would be
66 | * aggregated with any other required scopes and presented in the UI for any {@code
67 | * OAuth2ScopeSpecification} whose type parameter is a super-type of {@code
68 | * MyGoogleOAuth2Requirement}.
69 | *
70 | *
So for instance if {@code MyGoogleOAuth2Requirement extends} {@code
71 | * GoogleOAuth2ScopeRequirement} then {@code GoogleOAuth2ScopeSpecification}, which {@code extends}
72 | * {@code OAuth2ScopeSpecification}, would have {@code
73 | * MyGoogleOAuth2Requirement}'s scopes appear in its UI.
74 | *
75 | *
This package provides two types of {@code GoogleOAuth2Credentials}:
76 | *
77 | *
78 | *
{@code GoogleRobotMetadataCredentials}: a robot credential that utilizes the Google Compute
79 | * Engine "metadata" service attached to a virtual machine for providing access tokens.
80 | *
{@code GoogleRobotPrivateKeyCredentials}: a robot credential that retrieves access tokens
81 | * for a robot account using its {@code client_secrets.json} and private key file.
82 | *
83 | */
84 | package com.google.jenkins.plugins.credentials.oauth;
85 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/ConflictException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | /** This exception is used to wrap and propagate 409 (Conflict) HTTP exceptions up the stack. */
19 | public class ConflictException extends ExecutorException {
20 | public ConflictException(Throwable throwable) {
21 | super(throwable);
22 | }
23 |
24 | public ConflictException() {}
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/Executor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_FORBIDDEN;
19 | import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_NOT_FOUND;
20 | import static com.google.common.base.Preconditions.checkNotNull;
21 | import static java.util.logging.Level.SEVERE;
22 |
23 | import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest;
24 | import com.google.api.client.http.HttpResponseException;
25 | import com.google.common.util.concurrent.Uninterruptibles;
26 | import java.io.IOException;
27 | import java.net.SocketTimeoutException;
28 | import java.util.concurrent.TimeUnit;
29 | import java.util.logging.Logger;
30 |
31 | /**
32 | * Interface for a class that executes requests on behalf of a Json API client.
33 | *
34 | *
NOTE: This can be used to intercept or mock all "execute" requests.
35 | */
36 | public abstract class Executor {
37 | private static final Logger logger = Logger.getLogger(Executor.class.getName());
38 |
39 | /**
40 | * Executes the request, returning a response of the appropriate type.
41 | *
42 | * @param The type of the expected response
43 | * @param request The request we are issuing
44 | * @return a Json object of the given type
45 | * @throws IOException if anything goes wrong
46 | */
47 | public T execute(final AbstractGoogleJsonClientRequest request) throws IOException, ExecutorException {
48 | return execute(RequestCallable.from(request));
49 | }
50 |
51 | /**
52 | * Executes the request, returning a response of the appropriate type.
53 | *
54 | * @param The type of the expected response
55 | * @param request The request we are issuing
56 | * @return a Json object of the given type
57 | * @throws IOException if anything goes wrong
58 | */
59 | public abstract T execute(RequestCallable request) throws IOException, ExecutorException;
60 |
61 | /**
62 | * Surface this as a canonical means by which to sleep, so that clients can layer their own retry
63 | * logic on top of the executor using the same sleep facility;
64 | */
65 | public void sleep() {
66 | Uninterruptibles.sleepUninterruptibly(SLEEP_DURATION_SECONDS, TimeUnit.SECONDS);
67 | }
68 |
69 | /**
70 | * Surface this as a canonical means by which to sleep, so that clients can layer their own retry
71 | * logic on top of the executor using the same sleep facility;
72 | *
73 | * @param retryAttempt indicates how many times we had retried, to allow for increasing back-off
74 | * time.
75 | */
76 | public void sleep(int retryAttempt) {
77 | sleep();
78 | }
79 |
80 | /** Seconds to sleep between API request retry attempts */
81 | private static final long SLEEP_DURATION_SECONDS = 15;
82 |
83 | /** A default, failure-tolerant implementation of the {@link Executor} class. */
84 | public static class Default extends Executor {
85 | public Default() {
86 | this(RETRY_COUNT, true /* compose retry */);
87 | }
88 |
89 | /**
90 | * @param maxRetry the maximum number of retries to attempt in {@link
91 | * #execute(RequestCallable)}.
92 | * @param composeRetry whether nested retries block cause retries to compose or not. If set to
93 | * false, we will wrap the exception of the last retry step in an instance of {@link
94 | * MaxRetryExceededException}, which prevents any further retries.
95 | */
96 | public Default(int maxRetry, boolean composeRetry) {
97 | this.maxRetry = maxRetry;
98 | this.composeRetry = composeRetry;
99 | }
100 |
101 | private boolean composeRetry;
102 |
103 | private int getMaxRetry() {
104 | return maxRetry;
105 | }
106 |
107 | private final int maxRetry;
108 |
109 | private IOException propagateRetry(IOException lastException) throws IOException, ExecutorException {
110 | if (composeRetry) {
111 | throw lastException;
112 | } else {
113 | throw new MaxRetryExceededException(lastException);
114 | }
115 | }
116 |
117 | /** {@inheritDoc} */
118 | @Override
119 | public T execute(RequestCallable block) throws IOException, ExecutorException {
120 | IOException lastException = null;
121 | for (int i = 0; i < getMaxRetry(); ++i) {
122 | try {
123 | return checkNotNull(block).call();
124 | } catch (HttpResponseException e) {
125 | lastException = e;
126 | // Wrap a set of exception conditions, which when returned from
127 | // Google APIs are indicative of a state that is unlikely to
128 | // change.
129 | if (e.getStatusCode() == STATUS_CODE_NOT_FOUND) {
130 | throw new NotFoundException(e);
131 | }
132 | if (e.getStatusCode() == STATUS_CODE_FORBIDDEN) {
133 | throw new ForbiddenException(e);
134 | }
135 | if (e.getStatusCode() == 409 /* STATUS_CODE_CONFLICT */) {
136 | throw new ConflictException(e);
137 | }
138 | // Many other status codes may simply relate to ephemeral
139 | // service availability hiccups, that could simply go away
140 | // on retry.
141 | logger.log(SEVERE, Messages.Executor_HttpError(), e);
142 | } catch (SocketTimeoutException e) {
143 | logger.log(SEVERE, Messages.Executor_TimeoutError(), e);
144 | lastException = e;
145 | }
146 |
147 | if (!block.canRetry()) {
148 | // If this request contained a media upload, then it cannot simply
149 | // be retried.
150 | break;
151 | }
152 | // Pause before we retry
153 | sleep(i);
154 | }
155 | throw propagateRetry(checkNotNull(lastException));
156 | }
157 |
158 | private static final int RETRY_COUNT = 5;
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/ExecutorException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | /**
19 | * This base exception class is used to wrap and propagate HTTP exceptions for specific status codes
20 | * up the stack. We distinguish this kind from {@link java.io.IOException} so that all other kinds
21 | * of exceptions may be safely handled while letting these propagate.
22 | */
23 | public abstract class ExecutorException extends Exception {
24 | public ExecutorException(Throwable throwable) {
25 | super(throwable);
26 | }
27 |
28 | public ExecutorException() {}
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/ForbiddenException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | /** This exception is used to wrap and propagate 403 (Forbidden) HTTP exceptions up the stack. */
19 | public class ForbiddenException extends ExecutorException {
20 | public ForbiddenException(Throwable throwable) {
21 | super(throwable);
22 | }
23 |
24 | public ForbiddenException() {}
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/MaxRetryExceededException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | /**
19 | * This exception is used to signify that the maximum number of retries has been exceeded, that we
20 | * shouldn't make further attempt.
21 | */
22 | public class MaxRetryExceededException extends ExecutorException {
23 | public MaxRetryExceededException(Throwable throwable) {
24 | super(throwable);
25 | }
26 |
27 | public MaxRetryExceededException() {}
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/MetadataReader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_FORBIDDEN;
19 | import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_NOT_FOUND;
20 | import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_UNAUTHORIZED;
21 | import static com.google.common.base.Preconditions.checkNotNull;
22 |
23 | import com.google.api.client.http.GenericUrl;
24 | import com.google.api.client.http.HttpRequest;
25 | import com.google.api.client.http.HttpRequestFactory;
26 | import com.google.api.client.http.HttpResponse;
27 | import com.google.api.client.http.HttpResponseException;
28 | import com.google.api.client.http.javanet.NetHttpTransport;
29 | import com.google.common.base.Charsets;
30 | import java.io.IOException;
31 | import java.io.InputStreamReader;
32 | import java.io.StringWriter;
33 | import org.apache.commons.io.IOUtils;
34 |
35 | /**
36 | * This helper utility is used for reading values out of a Google Compute Engine instance's attached
37 | * metadata service.
38 | *
39 | * @author Matt Moore
40 | */
41 | public interface MetadataReader {
42 | /** Are we on a Google Compute Engine instance? */
43 | boolean hasMetadata() throws IOException;
44 |
45 | /**
46 | * Reads the specified sub-element out of the Google Compute Engine instance's metadata. These
47 | * relative paths are expected to start with:
48 | *
49 | *
50 | *
/instance/...
51 | *
/project/...
52 | *
53 | */
54 | String readMetadata(String metadataPath) throws IOException, ExecutorException;
55 |
56 | /** A simple default implementation that reads metadata via http requests. */
57 | public static class Default implements MetadataReader {
58 | public Default() {
59 | this(new NetHttpTransport().createRequestFactory());
60 | }
61 |
62 | public Default(HttpRequestFactory requestFactory) {
63 | this.requestFactory = checkNotNull(requestFactory);
64 | }
65 |
66 | private final HttpRequestFactory requestFactory;
67 |
68 | /** {@inheritDoc} */
69 | @Override
70 | public String readMetadata(String metadataPath) throws IOException, ExecutorException {
71 | HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(METADATA_SERVER + metadataPath));
72 |
73 | // GCE v1 requires requests to the metadata service to specify
74 | // this header in order to get anything back.
75 | request.getHeaders().set("Metadata-Flavor", "Google");
76 |
77 | HttpResponse response;
78 | try {
79 | response = request.execute();
80 | } catch (HttpResponseException e) {
81 | switch (e.getStatusCode()) {
82 | case STATUS_CODE_UNAUTHORIZED:
83 | case STATUS_CODE_FORBIDDEN:
84 | throw new ForbiddenException(e);
85 | case STATUS_CODE_NOT_FOUND:
86 | throw new NotFoundException(e);
87 | default:
88 | throw e;
89 | }
90 | }
91 |
92 | try (InputStreamReader inChars =
93 | new InputStreamReader(checkNotNull(response.getContent()), Charsets.UTF_8)) {
94 | StringWriter output = new StringWriter();
95 | IOUtils.copy(inChars, output);
96 | return output.toString();
97 | }
98 | }
99 |
100 | /** {@inheritDoc} */
101 | @Override
102 | public boolean hasMetadata() {
103 | try {
104 | readMetadata("");
105 | return true;
106 | } catch (IOException | ExecutorException e) {
107 | return false;
108 | }
109 | }
110 |
111 | /**
112 | * The address of the GCE Metadata service that provides GCE instances with information about
113 | * the default service account.
114 | */
115 | public static final String METADATA_SERVER = "http://metadata/computeMetadata/v1";
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/com/google/jenkins/plugins/util/MockExecutor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.jenkins.plugins.util;
17 |
18 | import static com.google.common.base.Preconditions.checkNotNull;
19 |
20 | import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest;
21 | import com.google.common.base.Predicate;
22 | import com.google.common.base.Predicates;
23 | import java.io.IOException;
24 | import java.util.LinkedList;
25 |
26 | /**
27 | * This is an implementation of {@link Executor} that can be injected to inject a set of canned
28 | * responses to requests including:
29 | *
30 | *
31 | *
A pre-determined object
32 | *
Throwing an {@link IOException} or {@link ExecutorException}
33 | *
Passing through a part of the request as the response
34 | *
35 | */
36 | public class MockExecutor extends Executor {
37 | public MockExecutor() {
38 | requestTypes = new LinkedList<>();
39 | responses = new LinkedList<>();
40 | exceptions = new LinkedList<>();
41 | predicates = new LinkedList<>();
42 | sawUnexpected = false;
43 | }
44 |
45 | /** {@inheritDoc} */
46 | @Override
47 | public void sleep() {
48 | // Never sleep, this is a test library, we want fast tests.
49 | }
50 |
51 | /** {@inheritDoc} */
52 | @Override
53 | public T execute(RequestCallable request) throws IOException, ExecutorException {
54 | // TODO(nghia): Think about implementing this.
55 | throw new UnsupportedOperationException();
56 | }
57 |
58 | /** {@inheritDoc} */
59 | @Override
60 | public T execute(AbstractGoogleJsonClientRequest request) throws IOException, ExecutorException {
61 | Class> requestClass = request.getClass();
62 | if (requestTypes.isEmpty()) {
63 | sawUnexpected = true;
64 | throw new IllegalStateException("Unexpected request: " + requestClass);
65 | }
66 |
67 | // Remove all three states to keep the lists in sync
68 | Class> clazz = requestTypes.removeFirst();
69 | Object response = responses.removeFirst();
70 | Exception exception = exceptions.removeFirst();
71 | Predicate> predicate =
72 | (Predicate>) predicates.removeFirst();
73 |
74 | if (requestClass != clazz) {
75 | sawUnexpected = true;
76 | throw new IllegalStateException(
77 | "Unexpected (or out of order) request: " + requestClass + " expected: " + clazz);
78 | }
79 |
80 | if (!predicate.apply(request)) {
81 | sawUnexpected = true;
82 | throw new IllegalStateException("User predicate: " + predicate + " failed for request: " + requestClass);
83 | }
84 |
85 | if (response == null) {
86 | if (exception != null) {
87 | if (exception instanceof IOException) {
88 | throw (IOException) exception; // throwWhen(IOException)
89 | } else {
90 | throw (ExecutorException) exception; // throwWhen(ExecutorException)
91 | }
92 | }
93 | return (T) request.getJsonContent(); // passThruWhen
94 | }
95 | return (T) response; // when
96 | }
97 |
98 | /**
99 | * When the next request matches the given {@code requestType} and the provided user {@link
100 | * Predicate} return {@code response} as the response.
101 | */
102 | public , C extends S> void when(
103 | Class requestType, T response, Predicate predicate) {
104 | requestTypes.add(checkNotNull(requestType));
105 | responses.add(response); // must allow null for delete's Void return type
106 | exceptions.add(null);
107 | predicates.add(checkNotNull(predicate));
108 | }
109 |
110 | /**
111 | * When the next request matches the given {@code requestType} return {@code response} as the
112 | * response.
113 | */
114 | public > void when(Class requestType, T response) {
115 | when(requestType, response, Predicates.alwaysTrue());
116 | }
117 |
118 | /**
119 | * When the next request matches the given {@code requestType} and the provided user {@link
120 | * Predicate} throw {@code exception} instead of responding.
121 | */
122 | public , C extends S> void throwWhen(
123 | Class requestType, IOException exception, Predicate predicate) {
124 | throwWhenInternal(requestType, exception, predicate);
125 | }
126 |
127 | /**
128 | * When the next request matches the given {@code requestType} throw {@code exception} instead of
129 | * responding.
130 | */
131 | public > void throwWhen(
132 | Class requestType, IOException exception) {
133 | throwWhen(requestType, exception, Predicates.alwaysTrue());
134 | }
135 |
136 | /**
137 | * When the next request matches the given {@code requestType} and the provided user {@link
138 | * Predicate} throw {@code exception} instead of responding.
139 | */
140 | public , C extends S> void throwWhen(
141 | Class requestType, ExecutorException exception, Predicate predicate) {
142 | throwWhenInternal(requestType, exception, predicate);
143 | }
144 |
145 | /**
146 | * When the next request matches the given {@code requestType} throw {@code exception} instead of
147 | * responding.
148 | */
149 | public > void throwWhen(
150 | Class requestType, ExecutorException exception) {
151 | throwWhen(requestType, exception, Predicates.alwaysTrue());
152 | }
153 |
154 | /**
155 | * When the next request matches the given {@code requestType} and the provided user {@link
156 | * Predicate} throw {@code exception} instead of responding.
157 | */
158 | private , C extends S> void throwWhenInternal(
159 | Class requestType, Exception exception, Predicate predicate) {
160 | requestTypes.add(checkNotNull(requestType));
161 | responses.add(null);
162 | exceptions.add(exception);
163 | predicates.add(checkNotNull(predicate));
164 | }
165 |
166 | /**
167 | * When the next request matches the given {@code requestType} and the provided user {@link
168 | * Predicate} pass through the request's {@code getJsonContent()} cast to the expected response
169 | * type.
170 | */
171 | public , C extends S> void passThruWhen(
172 | Class requestType, Predicate predicate) {
173 | requestTypes.add(checkNotNull(requestType));
174 | responses.add(null);
175 | exceptions.add(null);
176 | predicates.add(checkNotNull(predicate));
177 | }
178 |
179 | /**
180 | * When the next request matches the given {@code requestType} pass through the request's {@code
181 | * getJsonContent()} cast to the expected response type.
182 | */
183 | public > void passThruWhen(Class requestType) {
184 | passThruWhen(requestType, Predicates.alwaysTrue());
185 | }
186 |
187 | /** Did we see all of the expected requests? */
188 | public boolean sawAll() {
189 | return requestTypes.isEmpty();
190 | }
191 |
192 | /** Did we see any unexpected requests? */
193 | public boolean sawUnexpected() {
194 | return sawUnexpected;
195 | }
196 |
197 | private final LinkedList> requestTypes;
198 | private final LinkedList