diff --git a/action.yml b/action.yml index 1d2909e..9fab349 100644 --- a/action.yml +++ b/action.yml @@ -37,6 +37,26 @@ inputs: github-api-url: description: The URL of the GitHub REST API. default: ${{ github.api_url }} + permissions: + description: | + This option lets you define the specific permissions granted to the access token. Leave this field empty to grant the access token full access to all available permissions on the app. + + For enhanced security, it's recommended to specify only the necessary permissions. This reduces the potential impact if the token is compromised. + + Permissions are listed in a multiline format: :. + + * : Identifies the specific functionality (e.g., pull_requests, administration). + * : Defines the level of access (e.g., read, write). + + For a complete list of available permissions, refer to the permissions body parameter documentation: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app. + + Example: + + permissions: | + pull_requests:write + administration:read + + This example grants write access to pull requests and read access to administrative data. outputs: token: description: "GitHub installation access token" diff --git a/dist/main.cjs b/dist/main.cjs index a8f5b87..7b22aff 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -11863,19 +11863,19 @@ var require_dist_node20 = __commonJS({ permissionsString, singleFileName ] = result.split("|"); - const permissions = options.permissions || permissionsString.split(/,/).reduce((permissions2, string) => { + const permissions2 = options.permissions || permissionsString.split(/,/).reduce((permissions22, string) => { if (/!$/.test(string)) { - permissions2[string.slice(0, -1)] = "write"; + permissions22[string.slice(0, -1)] = "write"; } else { - permissions2[string] = "read"; + permissions22[string] = "read"; } - return permissions2; + return permissions22; }, {}); return { token, createdAt, expiresAt, - permissions, + permissions: permissions2, repositoryIds: options.repositoryIds, repositoryNames: options.repositoryNames, singleFileName, @@ -11899,11 +11899,11 @@ var require_dist_node20 = __commonJS({ } function optionsToCacheKey({ installationId, - permissions = {}, + permissions: permissions2 = {}, repositoryIds = [], repositoryNames = [] }) { - const permissionsString = Object.keys(permissions).sort().map((name) => permissions[name] === "read" ? name : `${name}!`).join(","); + const permissionsString = Object.keys(permissions2).sort().map((name) => permissions2[name] === "read" ? name : `${name}!`).join(","); const repositoryIdsString = repositoryIds.sort().join(","); const repositoryNamesString = repositoryNames.join(","); return [ @@ -11919,7 +11919,7 @@ var require_dist_node20 = __commonJS({ createdAt, expiresAt, repositorySelection, - permissions, + permissions: permissions2, repositoryIds, repositoryNames, singleFileName @@ -11930,7 +11930,7 @@ var require_dist_node20 = __commonJS({ tokenType: "installation", token, installationId, - permissions, + permissions: permissions2, createdAt, expiresAt, repositorySelection @@ -11968,7 +11968,7 @@ var require_dist_node20 = __commonJS({ token: token2, createdAt: createdAt2, expiresAt: expiresAt2, - permissions: permissions2, + permissions: permissions22, repositoryIds: repositoryIds2, repositoryNames: repositoryNames2, singleFileName: singleFileName2, @@ -11979,7 +11979,7 @@ var require_dist_node20 = __commonJS({ token: token2, createdAt: createdAt2, expiresAt: expiresAt2, - permissions: permissions2, + permissions: permissions22, repositorySelection: repositorySelection2, repositoryIds: repositoryIds2, repositoryNames: repositoryNames2, @@ -12010,7 +12010,7 @@ var require_dist_node20 = __commonJS({ authorization: `bearer ${appAuthentication.token}` } }); - const permissions = permissionsOptional || {}; + const permissions2 = permissionsOptional || {}; const repositorySelection = repositorySelectionOptional || "all"; const repositoryIds = repositories2 ? repositories2.map((r) => r.id) : void 0; const repositoryNames = repositories2 ? repositories2.map((repo) => repo.name) : void 0; @@ -12020,7 +12020,7 @@ var require_dist_node20 = __commonJS({ createdAt, expiresAt, repositorySelection, - permissions, + permissions: permissions2, repositoryIds, repositoryNames, singleFileName @@ -12031,7 +12031,7 @@ var require_dist_node20 = __commonJS({ createdAt, expiresAt, repositorySelection, - permissions, + permissions: permissions2, repositoryIds, repositoryNames, singleFileName @@ -29887,7 +29887,7 @@ async function pRetry(input, options) { } // lib/main.js -async function main(appId2, privateKey2, owner2, repositories2, core3, createAppAuth2, request2, skipTokenRevoke2) { +async function main(appId2, privateKey2, owner2, repositories2, core3, createAppAuth2, request2, skipTokenRevoke2, permissions2) { let parsedOwner = ""; let parsedRepositoryNames = ""; if (!owner2 && !repositories2) { @@ -29925,23 +29925,35 @@ async function main(appId2, privateKey2, owner2, repositories2, core3, createApp }); let authentication, installationId, appSlug; if (parsedRepositoryNames) { - ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames), { - onFailedAttempt: (error) => { - core3.info( - `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` - ); - }, - retries: 3 - })); + ({ authentication, installationId, appSlug } = await pRetry( + () => getTokenFromRepository( + request2, + auth, + parsedOwner, + parsedRepositoryNames, + permissions2 + ), + { + onFailedAttempt: (error) => { + core3.info( + `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3 + } + )); } else { - ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromOwner(request2, auth, parsedOwner), { - onFailedAttempt: (error) => { - core3.info( - `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` - ); - }, - retries: 3 - })); + ({ authentication, installationId, appSlug } = await pRetry( + () => getTokenFromOwner(request2, auth, parsedOwner, permissions2), + { + onFailedAttempt: (error) => { + core3.info( + `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3 + } + )); } core3.setSecret(authentication.token); core3.setOutput("token", authentication.token); @@ -29952,7 +29964,7 @@ async function main(appId2, privateKey2, owner2, repositories2, core3, createApp core3.setOutput("expiresAt", authentication.expiresAt); } } -async function getTokenFromOwner(request2, auth, parsedOwner) { +async function getTokenFromOwner(request2, auth, parsedOwner, permissions2) { const response = await request2("GET /orgs/{org}/installation", { org: parsedOwner, request: { @@ -29970,13 +29982,14 @@ async function getTokenFromOwner(request2, auth, parsedOwner) { }); const authentication = await auth({ type: "installation", - installationId: response.data.id + installationId: response.data.id, + permissions: permissions2 }); const installationId = response.data.id; const appSlug = response.data["app_slug"]; return { authentication, installationId, appSlug }; } -async function getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames) { +async function getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames, permissions2) { const response = await request2("GET /repos/{owner}/{repo}/installation", { owner: parsedOwner, repo: parsedRepositoryNames.split(",")[0], @@ -29987,7 +30000,8 @@ async function getTokenFromRepository(request2, auth, parsedOwner, parsedReposit const authentication = await auth({ type: "installation", installationId: response.data.id, - repositoryNames: parsedRepositoryNames.split(",") + repositoryNames: parsedRepositoryNames.split(","), + permissions: permissions2 }); const installationId = response.data.id; const appSlug = response.data["app_slug"]; @@ -30606,6 +30620,9 @@ var repositories = import_core2.default.getInput("repositories"); var skipTokenRevoke = Boolean( import_core2.default.getInput("skip-token-revoke") || import_core2.default.getInput("skip_token_revoke") ); +var permissions = Object.fromEntries( + import_core2.default.getMultilineInput("permissions").map((l) => l.split(":")) +); main( appId, privateKey, @@ -30614,7 +30631,8 @@ main( import_core2.default, import_auth_app.createAppAuth, request_default, - skipTokenRevoke + skipTokenRevoke, + Object.keys(permissions).length ? permissions : void 0 ).catch((error) => { console.error(error); import_core2.default.setFailed(error.message); diff --git a/lib/main.js b/lib/main.js index d685277..5e4c1ed 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,6 +1,10 @@ import pRetry from "p-retry"; // @ts-check +/** + * @typedef {{[k: string]: string}} Permissions + */ + /** * @param {string} appId * @param {string} privateKey @@ -10,6 +14,7 @@ import pRetry from "p-retry"; * @param {import("@octokit/auth-app").createAppAuth} createAppAuth * @param {import("@octokit/request").request} request * @param {boolean} skipTokenRevoke + * @param {Permissions} [permissions] */ export async function main( appId, @@ -19,7 +24,8 @@ export async function main( core, createAppAuth, request, - skipTokenRevoke + skipTokenRevoke, + permissions ) { let parsedOwner = ""; let parsedRepositoryNames = ""; @@ -74,24 +80,37 @@ export async function main( // If at least one repository is set, get installation ID from that repository if (parsedRepositoryNames) { - ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), { - onFailedAttempt: (error) => { - core.info( - `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` - ); - }, - retries: 3, - })); + ({ authentication, installationId, appSlug } = await pRetry( + () => + getTokenFromRepository( + request, + auth, + parsedOwner, + parsedRepositoryNames, + permissions + ), + { + onFailedAttempt: (error) => { + core.info( + `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3, + } + )); } else { // Otherwise get the installation for the owner, which can either be an organization or a user account - ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), { - onFailedAttempt: (error) => { - core.info( - `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` - ); - }, - retries: 3, - })); + ({ authentication, installationId, appSlug } = await pRetry( + () => getTokenFromOwner(request, auth, parsedOwner, permissions), + { + onFailedAttempt: (error) => { + core.info( + `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3, + } + )); } // Register the token with the runner as a secret to ensure it is masked in logs @@ -108,7 +127,7 @@ export async function main( } } -async function getTokenFromOwner(request, auth, parsedOwner) { +async function getTokenFromOwner(request, auth, parsedOwner, permissions) { // https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app const response = await request("GET /orgs/{org}/installation", { org: parsedOwner, @@ -132,15 +151,22 @@ async function getTokenFromOwner(request, auth, parsedOwner) { const authentication = await auth({ type: "installation", installationId: response.data.id, + permissions, }); const installationId = response.data.id; - const appSlug = response.data['app_slug']; + const appSlug = response.data["app_slug"]; return { authentication, installationId, appSlug }; } -async function getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames) { +async function getTokenFromRepository( + request, + auth, + parsedOwner, + parsedRepositoryNames, + permissions +) { // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app const response = await request("GET /repos/{owner}/{repo}/installation", { owner: parsedOwner, @@ -155,10 +181,11 @@ async function getTokenFromRepository(request, auth, parsedOwner, parsedReposito type: "installation", installationId: response.data.id, repositoryNames: parsedRepositoryNames.split(","), + permissions, }); const installationId = response.data.id; - const appSlug = response.data['app_slug']; + const appSlug = response.data["app_slug"]; return { authentication, installationId, appSlug }; - } \ No newline at end of file +} diff --git a/main.js b/main.js index b4d919d..d654bb9 100644 --- a/main.js +++ b/main.js @@ -31,6 +31,10 @@ const skipTokenRevoke = Boolean( core.getInput("skip-token-revoke") || core.getInput("skip_token_revoke") ); +const permissions = Object.fromEntries( + core.getMultilineInput("permissions").map((l) => l.split(":")) +); + main( appId, privateKey, @@ -39,7 +43,8 @@ main( core, createAppAuth, request, - skipTokenRevoke + skipTokenRevoke, + Object.keys(permissions).length ? permissions : undefined ).catch((error) => { /* c8 ignore next 3 */ console.error(error); diff --git a/package-lock.json b/package-lock.json index ebfcae9..6fe9730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-github-app-token", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-github-app-token", - "version": "1.8.1", + "version": "1.9.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1",