diff --git a/README.md b/README.md index c59ae50..2a7f9c1 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ # setup-cf -This action provides the following functionality for GitHub Actions users: +## Overview -- Installing a version of [Cloud Foundry CLI](https://github.com/cloudfoundry/cli) and adding it to the PATH -- Authenticating to the Cloud Foundry API using different grant types: +The `setup-cf` GitHub Action enables seamless integration with Cloud Foundry in your CI/CD pipelines. It simplifies the process of installing the Cloud Foundry CLI (cf cli), authenticating with Cloud Foundry services, and targeting specific organizations and spaces. + +This action is particularly useful for teams who deploy applications to Cloud Foundry platforms and want to automate their deployment workflows. + +## Features + +- **Installation**: Automatically installs a specified version of [Cloud Foundry CLI](https://github.com/cloudfoundry/cli) and adds it to the PATH +- **Authentication**: Supports multiple authentication grant types: - Password - Client Credentials - Client Credentials with JWT - JWT Bearer Token Grant -- Target Org and Space +- **Targeting**: Automatically targets specified organization and space +- **GitHub OIDC Integration**: Works with GitHub's OpenID Connect (OIDC) for secure authentication -## Basic usage +## Basic Usage -See [action.yml](action.yml) +See [action.yml](action.yml) for complete action definition. ```yaml steps: @@ -28,113 +35,69 @@ steps: run: cf apps ``` -## Parameter -* `api` - * Url of the cloud controller api - * required -* `audience` - * audience for requesting the GitHub `id_token` -* `client_id` - * client id for `client_credentals` or `jwt-bearer` -* `client_secret` - * client secret for `client_credentals` or `jwt-bearer` -* `grant_type` - * grant type for access - * required - * default: `password` - * valid values: - * `password` - * `client_credentals` - * `jwt-bearer` -* `jwt` - * jwt for usage with `client_credentals` or `jwt-bearer`. If omitted, a GitHub `id_token` will be requested -* `username` - * username for `password` grant -* `password` - * password for `password` grant -* `org` - * Cloud Foundry organization name -* `skip_ssl_validation` - * Skip verification of the API endpoint - * default: `false` -* `space` - * Cloud Foundry space name -* `version` - * cf cli version - * required - * default: `8.12.0` - -## Advanced - -Requires at least UAA `77.20.4`. - -### GitHub id_token - -https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect - -To allow a workflow to request an `id_token`, the workflow needs to have the correct permissions: +## Parameters -``` -permissions: - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout -``` - -> The `sub` may not be used for the `user_name` attribute mapping, as it can include unsupported characters like `/` and `:`. +| Parameter | Description | Required | Default | +|-----------|-------------|:--------:|:-------:| +| `api` | URL of the Cloud Foundry API endpoint | Yes | - | +| `audience` | Audience for requesting the GitHub `id_token` | No | - | +| `client_id` | Client ID for `client_credentials` or `jwt-bearer` grant types | No | - | +| `client_secret` | Client secret for `client_credentials` or `jwt-bearer` grant types | No | - | +| `grant_type` | Authentication grant type (`password`, `client_credentials`, or `jwt-bearer`) | Yes | `password` | +| `jwt` | JWT token for use with `client_credentials` or `jwt-bearer`. If omitted with these grant types, a GitHub `id_token` will be requested automatically | No | - | +| `org` | Cloud Foundry organization name to target | No | - | +| `origin` | Identity provider origin to use for authentication with `jwt-bearer` or `password` | No | - | +| `username` | Username for `password` grant type | No | - | +| `password` | Password for `password` grant type | No | - | +| `skip_ssl_validation` | Skip verification of the API endpoint (not recommended for production) | No | `false` | +| `space` | Cloud Foundry space name to target | No | - | +| `version` | Cloud Foundry CLI version to install | Yes | `8.12.0` | -The sub can be customized https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-subject-claims-for-an-organization-or-repository +## Authentication Methods -### setup UAA for JWT Bearer Token Grant with GitHub +### Password Authentication -Add the GitHub OIDC provider and use e.g. the `repository_owner` claim as the `user_name`: +The simplest authentication method using username and password: -``` -uaac curl /identity-providers -X POST -H "Content-Type: application/json" -d '{"type": "oidc1.0", "name": "GitHub", "originKey": "github", "config": {"discoveryUrl": "https://token.actions.githubusercontent.com/.well-known/openid-configuration", "scopes": ["read:user", "user:email"], "linkText": "Login with GitHub", "showLinkText": false, "addShadowUserOnLogin": true, "clientAuthInBody": true, "relyingPartyId": "uaa", "addShadowUserOnLogin": true, "attributeMappings" : {"given_name": "repository_owner", "family_name": "repository_owner_id", "user_name": "repository_owner"}}}' +```yaml +- uses: vchrisb/setup-cf@v2 + with: + api: ${{ secrets.CF_API }} + grant_type: password + username: ${{ secrets.CF_USERNAME }} + password: ${{ secrets.CF_PASSWORD }} + org: myorg + space: myspace ``` -The UAA client used does need to include `urn:ietf:params:oauth:grant-type:jwt-bearer` in the `authorized_grant_types`. -This can be the default `cf` client, but also a dedicated one: +### JWT Bearer Token Grant with GitHub OIDC -``` -uaac curl /oauth/clients -X POST -H "Content-Type: application/json" -d '{"client_id" : "jwt-bearer-client", "access_token_validity": 1800, "authorities" : [ "uaa.resource" ], "authorized_grant_types" : [ "urn:ietf:params:oauth:grant-type:jwt-bearer" ], "scope": ["openid", "cloud_controller.read"], "allowedproviders" : [ "github" ], "name" : "JWT Bearer Client"}' -``` +This method leverages GitHub's OIDC provider for secure, token-based authentication: ```yaml -name: Jwt Bearer Flow using GitHub id_token +name: JWT Bearer Flow using GitHub id_token on: [push] permissions: - id-token: write - contents: read + id-token: write # Required for requesting the JWT + contents: read # Required for actions/checkout jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: vchrisb/setup-cf@v2 - with: + with: api: ${{ secrets.CF_API }} grant_type: jwt-bearer org: test space: dev - name: run cf command - run: cf apps + run: cf apps ``` -The cf cli will be authenticated as an user, which username is defined by the `attributeMappings`. +### Client Credentials with JWT -### setup UAA for JWT client credentials - -The UAA client used does need to include `client_credentials` in the `authorized_grant_types`. - -``` -uaac client add setup-cf --scope uaa.none --authorities cloud_controller.read --authorized_grant_type "client_credentials" -``` - -Add the jwt configuration to the client. -The following example is for GitHub. You can also pass a different token using `jwt` parameter, but will need to adapt the configuration to your idp. -``` -uaac client jwt add setup-cf --issuer https://token.actions.githubusercontent.com --subject repo:vchrisb/setup-cf:environment:Production --aud https://github.com/vchrisb -``` +This method uses client credentials with JWT verification: ```yaml name: Client Credentials using GitHub id_token @@ -148,24 +111,144 @@ jobs: steps: - uses: actions/checkout@v4 - uses: vchrisb/setup-cf@v2 - with: + with: api: ${{ secrets.CF_API }} client_id: setup-cf grant_type: client_credentials org: test space: dev - name: run cf command - run: cf apps + run: cf apps +``` + +## Advanced Configuration + +### Setting up UAA for GitHub Authentication + +#### Prerequisites +- UAA version 77.20.4 or higher +- Administrative access to UAA + +#### Configuring UAA for JWT Bearer Token Grant with GitHub + +1. Add the GitHub OIDC provider to UAA: + +``` +uaac curl /identity-providers -X POST -H "Content-Type: application/json" -d '{ + "type": "oidc1.0", + "name": "GitHub", + "originKey": "github", + "config": { + "discoveryUrl": "https://token.actions.githubusercontent.com/.well-known/openid-configuration", + "scopes": ["read:user", "user:email"], + "linkText": "Login with GitHub", + "showLinkText": false, + "addShadowUserOnLogin": false, + "clientAuthInBody": true, + "relyingPartyId": "uaa", + "addShadowUserOnLogin": true, + "attributeMappings": { + "given_name": "repository_owner", + "family_name": "repository_owner_id", + "user_name": "repository_owner" + } + } +}' +``` + +2. Ensure your UAA client includes the JWT bearer grant type: + +Either create a dedicated client to be used for JWT bearer grant: + +``` +uaac curl /oauth/clients -X POST -H "Content-Type: application/json" -d '{ + "client_id": "jwt-bearer-client", + "access_token_validity": 1800, + "authorities": ["uaa.resource"], + "authorized_grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "scope": ["openid", "cloud_controller.read"], + "allowedproviders": ["github"], + "name": "JWT Bearer Client" +}' +``` + +Or add the grant type to the default `cf` client: + +``` +uaac client update cf \ + --authorized_grant_types refresh_token,password,urn:ietf:params:oauth:grant-type:jwt-bearer +``` + +#### Configuring UAA for JWT Client Credentials + +1. Create a client with client credentials grant type: + +``` +uaac client add setup-cf \ + --scope uaa.none \ + --authorities cloud_controller.read,cloud_controller.write,clients.read \ + --authorized_grant_type "client_credentials" ``` -The cf cli will be authenticated as the client `setup-cf`. +2. Add JWT configuration to the client: -## Developmet +``` +uaac client jwt add setup-cf \ + --issuer https://token.actions.githubusercontent.com \ + --subject repo:vchrisb/setup-cf:environment:Production \ + --aud https://github.com/vchrisb +``` -### update action +Subject and Audience need to adapted to your repo and workflow. + +### GitHub OIDC Configuration + +To use GitHub's OIDC provider, your workflow must have the appropriate permissions: + +```yaml +permissions: + id-token: write # Required for requesting the JWT + contents: read # Required for actions/checkout +``` + +Note: The `sub` claim from GitHub may contain characters like `/` and `:` which are not supported for the `user_name` attribute. Consider using alternative claims or customizing the subject as described in [GitHub's documentation](https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-subject-claims-for-an-organization-or-repository). + +## Troubleshooting + +### Common Issues + +1. **Authentication Failures** + - Verify your credentials are correct + - Check that your client has the necessary authorities and grant types + - Ensure the UAA version is 77.20.4 or higher for JWT-based auth + +2. **Permission Issues** + - For GitHub OIDC, make sure the workflow has `id-token: write` permission + - Verify the client or user has appropriate Cloud Foundry permissions + +3. **Targeting Issues** + - Confirm the organization and space exist + - Check that the authenticated user/client has access to the specified org/space + +### Debugging + +Add the following to your workflow to see more detailed output: + +```yaml +env: + CF_LOG_LEVEL: DEBUG +``` + +## Development + +To update the action: ``` npm i -g @vercel/ncc npm run format npm run build ``` + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/action.yaml b/action.yaml index 1dff1f8..300feb8 100644 --- a/action.yaml +++ b/action.yaml @@ -24,15 +24,18 @@ inputs: jwt: description: "jwt for usage with `client_credentials` or `jwt-bearer`." required: false + org: + description: "org" + required: false + origin: + description: "origin" + required: false username: description: "username" required: false password: description: "password" required: false - org: - description: "org" - required: false skip_ssl_validation: description: "skip_ssl_validation" required: false diff --git a/dist/index.js b/dist/index.js index cc29f33..bad090e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -34416,7 +34416,7 @@ function setup_cf(api, skip_ssl_validation) { } }); } -function handleJwtBearer(audience, client_id, client_secret, jwt) { +function handleJwtBearer(audience, client_id, client_secret, jwt, origin) { return __awaiter(this, void 0, void 0, function* () { if (!jwt) { core.info(">>> Requesting GitHub ID token"); @@ -34438,6 +34438,10 @@ function handleJwtBearer(audience, client_id, client_secret, jwt) { } // Add assertion parameter and JWT token args.push("--assertion", jwt); + // Add origin only if passed + if (origin) { + args.push("--origin", origin); + } // Execute command with constructed arguments yield exec.exec("cf", args, { silent: false }); core.info(">>> Successfully authenticated using JWT Bearer Token Grant"); @@ -34462,14 +34466,19 @@ function handleClientCredentials(client_id, client_secret) { core.info(">>> Successfully authenticated using client credentials"); }); } -function handlePassword(username, password) { +function handlePassword(username, password, origin) { return __awaiter(this, void 0, void 0, function* () { if (!username || !password) { throw new Error("Password authentication requires username and password"); } - yield exec.exec("cf", ["auth", username, password], { - silent: false, - }); + // Build command arguments array + const args = ["auth", username, password]; + // Add origin only if passed + if (origin) { + args.push("--origin", origin); + } + // Execute command with constructed arguments + yield exec.exec("cf", args, { silent: false }); core.info(">>> Successfully authenticated using password"); }); } @@ -34499,6 +34508,7 @@ function run() { grant_type: core.getInput("grant_type", { required: true }), jwt: core.getInput("jwt"), org: core.getInput("org"), + origin: core.getInput("origin"), password: core.getInput("password"), skip_ssl_validation: core.getInput("skip_ssl_validation").toLowerCase() === "true", space: core.getInput("space"), @@ -34519,7 +34529,7 @@ function run() { // Handle authentication based on grant type switch (inputs.grant_type) { case GRANT_TYPES.JWT_BEARER: - yield handleJwtBearer(inputs.audience, inputs.client_id, inputs.client_secret, inputs.jwt); + yield handleJwtBearer(inputs.audience, inputs.client_id, inputs.client_secret, inputs.jwt, inputs.origin); break; case GRANT_TYPES.CLIENT_CREDENTIALS: if (inputs.client_id && inputs.client_secret) { @@ -34533,7 +34543,7 @@ function run() { } break; case GRANT_TYPES.PASSWORD: - yield handlePassword(inputs.username, inputs.password); + yield handlePassword(inputs.username, inputs.password, inputs.origin); break; default: throw new Error(`Unsupported grant type: ${inputs.grant_type}`); diff --git a/package-lock.json b/package-lock.json index f630a43..1b232ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "setup-cf", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "setup-cf", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { "@actions/core": "^1.11.1", @@ -15,10 +15,10 @@ "jsonwebtoken": "^9.0.2" }, "devDependencies": { - "@types/node": "^22.13.17", + "@types/node": "^22.14.0", "@vercel/ncc": "^0.38.3", "prettier": "^3.5.3", - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "engines": { "node": ">=20.0.0 <21.0.0" @@ -82,13 +82,13 @@ } }, "node_modules/@types/node": { - "version": "22.13.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.17.tgz", - "integrity": "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@vercel/ncc": { @@ -274,9 +274,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -300,9 +300,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index 531567b..1ba7405 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "setup-cf", - "version": "2.0.0", + "version": "2.0.1", "description": "Github action to setup Cloud Foundry cli", "main": "src/index.ts", "scripts": { @@ -19,10 +19,10 @@ "jsonwebtoken": "^9.0.2" }, "devDependencies": { - "@types/node": "^22.13.17", + "@types/node": "^22.14.0", "@vercel/ncc": "^0.38.3", "prettier": "^3.5.3", - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "engines": { "node": ">=20.0.0 <21.0.0" diff --git a/src/index.ts b/src/index.ts index 6be91d8..3e96665 100755 --- a/src/index.ts +++ b/src/index.ts @@ -46,7 +46,13 @@ async function setup_cf(api, skip_ssl_validation) { } } -async function handleJwtBearer(audience, client_id, client_secret, jwt) { +async function handleJwtBearer( + audience, + client_id, + client_secret, + jwt, + origin, +) { if (!jwt) { core.info(">>> Requesting GitHub ID token"); if (!audience) { @@ -72,6 +78,11 @@ async function handleJwtBearer(audience, client_id, client_secret, jwt) { // Add assertion parameter and JWT token args.push("--assertion", jwt); + // Add origin only if passed + if (origin) { + args.push("--origin", origin); + } + // Execute command with constructed arguments await exec.exec("cf", args, { silent: false }); @@ -106,13 +117,21 @@ async function handleClientCredentials(client_id, client_secret) { core.info(">>> Successfully authenticated using client credentials"); } -async function handlePassword(username, password) { +async function handlePassword(username, password, origin) { if (!username || !password) { throw new Error("Password authentication requires username and password"); } - await exec.exec("cf", ["auth", username, password], { - silent: false, - }); + + // Build command arguments array + const args = ["auth", username, password]; + + // Add origin only if passed + if (origin) { + args.push("--origin", origin); + } + + // Execute command with constructed arguments + await exec.exec("cf", args, { silent: false }); core.info(">>> Successfully authenticated using password"); } @@ -139,6 +158,7 @@ async function run() { grant_type: core.getInput("grant_type", { required: true }), jwt: core.getInput("jwt"), org: core.getInput("org"), + origin: core.getInput("origin"), password: core.getInput("password"), skip_ssl_validation: core.getInput("skip_ssl_validation").toLowerCase() === "true", @@ -169,6 +189,7 @@ async function run() { inputs.client_id, inputs.client_secret, inputs.jwt, + inputs.origin, ); break; @@ -187,7 +208,7 @@ async function run() { break; case GRANT_TYPES.PASSWORD: - await handlePassword(inputs.username, inputs.password); + await handlePassword(inputs.username, inputs.password, inputs.origin); break; default: