diff --git a/.github/workflows/oidc-test.yml b/.github/workflows/oidc-test.yml index fee073c6e..ab0033685 100644 --- a/.github/workflows/oidc-test.yml +++ b/.github/workflows/oidc-test.yml @@ -44,6 +44,15 @@ jobs: "provider_type": "GitHub", "description": "This is a test configuration created for OIDC-Access integration test" }' + - name: Set subject + shell: bash + run: | + if [[ $GITHUB_EVENT_NAME == 'pull_request_target' ]]; then + echo "SUB=repo:${{ github.repository_owner }}/setup-jfrog-cli:pull_request" >> "$GITHUB_ENV" + else + echo "SUB=repo:${{ github.repository_owner }}/setup-jfrog-cli:ref:${{ github.ref }}" >> "$GITHUB_ENV" + fi + - name: Create OIDC integration Identity Mapping shell: bash run: | @@ -54,7 +63,7 @@ jobs: "name": "oidc-test-identity-mapping", "priority": "1", "claims": { - "sub": "repo:${{ github.repository_owner }}/setup-jfrog-cli:ref:${{ github.ref }}", + "sub": "${{ env.SUB }}", "iss": "https://token.actions.githubusercontent.com" }, "token_spec": { diff --git a/.github/workflows/remove-label.yml b/.github/workflows/remove-label.yml new file mode 100644 index 000000000..67be7e8d3 --- /dev/null +++ b/.github/workflows/remove-label.yml @@ -0,0 +1,18 @@ +name: Remove Label +on: + pull_request_target: + types: [labeled] +# Ensures that only the latest commit is running for each PR at a time. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.ref }} + cancel-in-progress: true +jobs: + Remove-Label: + if: contains(github.event.pull_request.labels.*.name, 'safe to test') + name: Remove label + runs-on: ubuntu-latest + steps: + - name: Remove 'safe to test' + uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: "safe to test" diff --git a/.vscode/settings.json b/.vscode/settings.json index 59cd97ec6..7e91dc5cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,6 @@ "typescript.tsc.autoDetect": "off", "workbench.editor.enablePreview": false, "workbench.editor.enablePreviewFromQuickOpen": false, - "workbench.editor.showTabs": true, + "workbench.editor.showTabs": "multiple", "files.autoSave": "afterDelay" } \ No newline at end of file diff --git a/README.md b/README.md index fe8628bdf..54800d38f 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,9 @@ To utilize the OIDC protocol, follow these steps:
2. **[Configure an identity mapping](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-identity-mappings)**: This phase generates a reference token for authenticating against the JFrog platform. It involves defining the necessary details to enable server authentication of the action issuer and granting the issuer an appropriate access token. - You have the flexibility to define any valid list of claims required for request authentication. You can check a list of the possible claims [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token). - Example Claims JSON: - ```yml +You have the flexibility to define any valid list of claims required for request authentication. You can check a list of the possible claims [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token). +Example Claims JSON: + ```json { "sub": "repo:my-user-name/project1:ref:refs/heads/main", "aud": "https://github.com/my-user-name", @@ -131,22 +131,23 @@ To utilize the OIDC protocol, follow these steps: 1. **Set required permissions**: In the course of the protocol's execution, it's imperative to acquire a JSON Web Token (JWT) from GitHub's OIDC provider. To request this token, it's essential to configure the specified permission in the workflow file: ```yml permissions: - id-token: write + id-token: write ```
-2. **Pass the 'oidc-provider-name' input to the Action (Required)**: The 'oidc-provider-name' parameter designates the OIDC configuration whose one of its identity mapping should align with the generated JWT claims. This input needs to align with the 'Provider Name' value established within the OIDC configuration. -3. **Pass the 'oidc-audience' input to the Action (Optional)**: The 'oidc-audience' input defines the intended recipients of an ID token (JWT), ensuring access is restricted to authorized recipients for the cloud (Artifactory). By default, it contains the URL of the repository owner. - This value, if transmitted, will be used as an argument in core.getIDToken(), which generates the JWT. It enforces a condition, allowing only workflows within the designated repository/organization to access the cloud role. Read more about it [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-audience-value). - ```yml - - name: Install JFrog CLI - uses: jfrog/setup-jfrog-cli@v3 - env: - JF_URL: ${{ secrets.JF_URL }} - with: - oidc-provider-name: - oidc-audience: - ``` +2. **Pass the 'oidc-provider-name' input to the Action (Required)**: The 'oidc-provider-name' parameter designates the OIDC configuration whose one of its identity mapping should align with the generated JWT claims. This input needs to align with the 'Provider Name' value established within the OIDC configuration in the JFrog Platform. +3. **Pass the 'oidc-audience' input to the Action (Optional)**: The 'oidc-audience' input defines the intended recipients of an ID token (JWT), ensuring access is restricted to authorized recipients for the cloud (Artifactory). By default, it contains the URL of the GitHub repository owner. +This value, if transmitted, will be used as an argument in core.getIDToken(), which generates the JWT. It enforces a condition, allowing only workflows within the designated repository/organization to access the cloud role. Read more about it [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-audience-value). + + ```yml + - name: Install JFrog CLI + uses: jfrog/setup-jfrog-cli@v3 + env: + JF_URL: ${{ secrets.JF_URL }} + with: + oidc-provider-name: + oidc-audience: + ``` ## Setting the build name and build number when publishing build-info to Artifactory The Action automatically sets the following environment variables: diff --git a/action.yml b/action.yml index 44b03a1af..631eedfe1 100644 --- a/action.yml +++ b/action.yml @@ -9,11 +9,11 @@ inputs: download-repository: description: "Remote repository in Artifactory pointing to 'https://releases.jfrog.io/artifactory/jfrog-cli'. Use this parameter in case you don't have an Internet access." required: false - oidc-audience: - description: "Recipient for which the JWT is intended. By default it contains the URL to the repository owner." - required: false oidc-provider-name: - description: "Provider Name's value that was set in OpenId Connect integration." + description: "Provider Name's value that was set in OpenId Connect integration in the JFrog platform." + required: false + oidc-audience: + description: "By default, this is the URL of the GitHub repository owner, such as the organization that owns the repository." required: false runs: diff --git a/lib/utils.js b/lib/utils.js index 73cc6b396..d0543502f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -45,17 +45,21 @@ class Utils { /** * Retrieves server credentials for accessing JFrog's server * searching for existing environment variables such as JF_ACCESS_TOKEN or the combination of JF_USER and JF_PASSWORD. - * If neither is found, and if the request and requester are authorized, it generates an access token for the specified JFrog's server using the OpenID Connect mechanism. + * If the 'oidc-provider-name' argument was provided, it generates an access token for the specified JFrog's server using the OpenID Connect mechanism. * @returns JfrogCredentials struct filled with collected credentials */ static getJfrogCredentials() { return __awaiter(this, void 0, void 0, function* () { let jfrogCredentials = this.collectJfrogCredentialsFromEnvVars(); - if (!this.shouldUseOpenIDConnect(jfrogCredentials)) { + const oidcProviderName = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME); + if (!oidcProviderName) { // Use JF_ENV or the credentials found in the environment variables return jfrogCredentials; } - core.info('The JFrog platform credentials were not configured. Obtaining an access token through OpenID Connect.'); + if (!jfrogCredentials.jfrogUrl) { + throw new Error(`JF_URL must be provided when oidc-provider-name is specified`); + } + core.info('Obtaining an access token through OpenID Connect...'); const audience = core.getInput(Utils.OIDC_AUDIENCE_ARG); let jsonWebToken; try { @@ -66,34 +70,13 @@ class Utils { throw new Error(`Getting openID Connect JSON web token failed: ${error.message}`); } try { - return yield this.getAccessTokenFromJWT(jfrogCredentials, jsonWebToken); + return yield this.getJfrogAccessTokenThroughOidcProtocol(jfrogCredentials, jsonWebToken, oidcProviderName); } catch (error) { throw new Error(`Exchanging JSON web token with an access token failed: ${error.message}`); } }); } - /** - * Returns true if OpenID Connect authentication should be used. - * @param jfrogCredentials - Credentials retrieved from the environment variables - * @returns true if OpenID Connect authentication should be used - */ - static shouldUseOpenIDConnect(jfrogCredentials) { - if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) { - // To enable OpenIDConnect authentication, users must configure the 'id-token: write' permission, which sets the ACTIONS_ID_TOKEN_REQUEST_URL environment variable. - // If this variable is empty, it indicates that OIDC should not be utilized. - return false; - } - if (!jfrogCredentials.jfrogUrl) { - // If no JFrog URL is specified, we can't use OpenID Connect - return false; - } - if (jfrogCredentials.password || jfrogCredentials.accessToken) { - // If credentials are specified - use them instead - return false; - } - return true; - } /** * Gathers JFrog's credentials from environment variables and delivers them in a JfrogCredentials structure * @returns JfrogCredentials struct with all credentials found in environment variables @@ -115,23 +98,23 @@ class Utils { return jfrogCredentials; } /** - * Exchanges JWT with a valid access token + * Exchanges GitHub JWT with a valid JFrog access token * @param jfrogCredentials existing JFrog credentials - url, access token, username + password * @param jsonWebToken JWT achieved from GitHub JWT provider + * @param oidcProviderName OIDC provider name * @returns an access token for the requested Artifactory server */ - static getAccessTokenFromJWT(jfrogCredentials, jsonWebToken) { + static getJfrogAccessTokenThroughOidcProtocol(jfrogCredentials, jsonWebToken, oidcProviderName) { return __awaiter(this, void 0, void 0, function* () { // If we've reached this stage, the jfrogCredentials.jfrogUrl field should hold a non-empty value obtained from process.env.JF_URL const exchangeUrl = jfrogCredentials.jfrogUrl.replace(/\/$/, '') + '/access/api/v1/oidc/token'; - core.debug('Exchanging JSON web token with an access token'); - const providerName = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME, { required: true }); + core.debug('Exchanging GitHub JSON web token with a JFrog access token...'); const httpClient = new http_client_1.HttpClient(); const data = `{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", "subject_token": "${jsonWebToken}", - "provider_name": "${providerName}" + "provider_name": "${oidcProviderName}" }`; const additionalHeaders = { 'Content-Type': 'application/json', @@ -143,6 +126,9 @@ class Utils { if (jfrogCredentials.accessToken) { core.setSecret(jfrogCredentials.accessToken); } + if (responseJson.errors) { + throw new Error(`${JSON.stringify(responseJson.errors)}`); + } return jfrogCredentials; }); } diff --git a/src/utils.ts b/src/utils.ts index ce031da52..0100772a3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,17 +45,21 @@ export class Utils { /** * Retrieves server credentials for accessing JFrog's server * searching for existing environment variables such as JF_ACCESS_TOKEN or the combination of JF_USER and JF_PASSWORD. - * If neither is found, and if the request and requester are authorized, it generates an access token for the specified JFrog's server using the OpenID Connect mechanism. + * If the 'oidc-provider-name' argument was provided, it generates an access token for the specified JFrog's server using the OpenID Connect mechanism. * @returns JfrogCredentials struct filled with collected credentials */ public static async getJfrogCredentials(): Promise { let jfrogCredentials: JfrogCredentials = this.collectJfrogCredentialsFromEnvVars(); - if (!this.shouldUseOpenIDConnect(jfrogCredentials)) { + const oidcProviderName: string = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME); + if (!oidcProviderName) { // Use JF_ENV or the credentials found in the environment variables return jfrogCredentials; } - core.info('The JFrog platform credentials were not configured. Obtaining an access token through OpenID Connect.'); + if (!jfrogCredentials.jfrogUrl) { + throw new Error(`JF_URL must be provided when oidc-provider-name is specified`); + } + core.info('Obtaining an access token through OpenID Connect...'); const audience: string = core.getInput(Utils.OIDC_AUDIENCE_ARG); let jsonWebToken: string | undefined; try { @@ -66,34 +70,12 @@ export class Utils { } try { - return await this.getAccessTokenFromJWT(jfrogCredentials, jsonWebToken); + return await this.getJfrogAccessTokenThroughOidcProtocol(jfrogCredentials, jsonWebToken, oidcProviderName); } catch (error: any) { throw new Error(`Exchanging JSON web token with an access token failed: ${error.message}`); } } - /** - * Returns true if OpenID Connect authentication should be used. - * @param jfrogCredentials - Credentials retrieved from the environment variables - * @returns true if OpenID Connect authentication should be used - */ - private static shouldUseOpenIDConnect(jfrogCredentials: JfrogCredentials): boolean { - if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) { - // To enable OpenIDConnect authentication, users must configure the 'id-token: write' permission, which sets the ACTIONS_ID_TOKEN_REQUEST_URL environment variable. - // If this variable is empty, it indicates that OIDC should not be utilized. - return false; - } - if (!jfrogCredentials.jfrogUrl) { - // If no JFrog URL is specified, we can't use OpenID Connect - return false; - } - if (jfrogCredentials.password || jfrogCredentials.accessToken) { - // If credentials are specified - use them instead - return false; - } - return true; - } - /** * Gathers JFrog's credentials from environment variables and delivers them in a JfrogCredentials structure * @returns JfrogCredentials struct with all credentials found in environment variables @@ -117,23 +99,27 @@ export class Utils { } /** - * Exchanges JWT with a valid access token + * Exchanges GitHub JWT with a valid JFrog access token * @param jfrogCredentials existing JFrog credentials - url, access token, username + password * @param jsonWebToken JWT achieved from GitHub JWT provider + * @param oidcProviderName OIDC provider name * @returns an access token for the requested Artifactory server */ - private static async getAccessTokenFromJWT(jfrogCredentials: JfrogCredentials, jsonWebToken: string): Promise { + private static async getJfrogAccessTokenThroughOidcProtocol( + jfrogCredentials: JfrogCredentials, + jsonWebToken: string, + oidcProviderName: string, + ): Promise { // If we've reached this stage, the jfrogCredentials.jfrogUrl field should hold a non-empty value obtained from process.env.JF_URL const exchangeUrl: string = jfrogCredentials.jfrogUrl!.replace(/\/$/, '') + '/access/api/v1/oidc/token'; - core.debug('Exchanging JSON web token with an access token'); + core.debug('Exchanging GitHub JSON web token with a JFrog access token...'); - const providerName: string = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME, { required: true }); const httpClient: HttpClient = new HttpClient(); const data: string = `{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", "subject_token": "${jsonWebToken}", - "provider_name": "${providerName}" + "provider_name": "${oidcProviderName}" }`; const additionalHeaders: OutgoingHttpHeaders = { @@ -147,6 +133,9 @@ export class Utils { if (jfrogCredentials.accessToken) { core.setSecret(jfrogCredentials.accessToken); } + if (responseJson.errors) { + throw new Error(`${JSON.stringify(responseJson.errors)}`); + } return jfrogCredentials; } @@ -464,4 +453,5 @@ export interface JfrogCredentials { export interface TokenExchangeResponseData { access_token: string; + errors: string; }