From e0ddd4baec3a0c4f641dc67605ad2cb830a595dc Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Tue, 12 Mar 2024 18:01:44 -0700 Subject: [PATCH 01/18] update action output to use newer github env method Signed-off-by: Steve Arnold --- token_getter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/token_getter.py b/token_getter.py index 19f759c..e1604a3 100644 --- a/token_getter.py +++ b/token_getter.py @@ -134,7 +134,7 @@ def generate_installation_curl(self, endpoint): print(f'curl -i -H "Authorization: token {iat}" -H "Accept: application/vnd.github.machine-man-preview+json" https://api.github.com{endpoint}') if __name__ == '__main__': - + pem_path = 'pem.txt' app_id = os.getenv('INPUT_APP_ID') nwo = os.getenv('GITHUB_REPOSITORY') @@ -148,5 +148,5 @@ def generate_installation_curl(self, endpoint): token = app.get_installation_access_token(installation_id=id) assert token, 'Token not returned!' - print(f"::add-mask::{token}") - print(f"::set-output name=app_token::{token}") \ No newline at end of file + os.system(f'echo ::add-mask::{token}') + os.system(f'echo "app_token={token}">> $GITHUB_ENV') From 88e3e8ba82d3522378c03e93c18b1f83b1b0c367 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Tue, 12 Mar 2024 18:45:40 -0700 Subject: [PATCH 02/18] chg: update docker file and python deps Signed-off-by: Steve Arnold --- action.yml | 2 +- prebuild.Dockerfile | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/action.yml b/action.yml index 26ea6d3..aad720c 100644 --- a/action.yml +++ b/action.yml @@ -16,4 +16,4 @@ branding: icon: 'unlock' runs: using: 'docker' - image: 'docker://hamelsmu/app-token' + image: 'Dockerfile' diff --git a/prebuild.Dockerfile b/prebuild.Dockerfile index ad8e6da..fcb49fb 100644 --- a/prebuild.Dockerfile +++ b/prebuild.Dockerfile @@ -1,10 +1,10 @@ -FROM python:3.6-slim-stretch +FROM python:3.9-alpine3.16 RUN pip install \ - cryptography==2.6.1 \ - github3.py==1.3.0 \ - jwcrypto==0.6.0 \ - pyjwt==1.7.1 + cryptography==42.0.5 \ + github3.py==4.0.1 \ + jwcrypto==1.5.6 \ + pyjwt==2.8.0 COPY token_getter.py app/ COPY entrypoint.sh app/ From 2a6043f04ef3fbf72399799f7dc709a503d1b4a4 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Tue, 12 Mar 2024 19:22:13 -0700 Subject: [PATCH 03/18] chg: dev: strip test.yml and expect failures in CI Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 78 ++++++++++++++----------------------- 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e4df65f..22e50a0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,55 +1,37 @@ -name: Tests -on: [push] +name: action-tests -jobs: - build-temp-container: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - - name: build-temp-container - run: | - echo ${PASSWORD} | docker login -u $USERNAME --password-stdin - docker build -t hamelsmu/app-token:temp -f prebuild.Dockerfile . - docker push hamelsmu/app-token:temp - env: - USERNAME: ${{ secrets.DOCKER_USERNAME }} - PASSWORD: ${{ secrets.DOCKER_PASSWORD }} +on: + workflow_dispatch: + pull_request: + push: + branches: [ main ] - test-container: - needs: [build-temp-container] + +jobs: + test_defaults: runs-on: ubuntu-latest + name: Test alpine python pkgs steps: - - - uses: actions/checkout@master - - # - name: Setup tmate session - # uses: mxschmitt/action-tmate@v1 - # env: - # INPUT_APP_PEM: ${{ secrets.APP_PEM }} - # INPUT_APP_ID: ${{ secrets.APP_ID }} + # To use this repository's private action, + # you must check out the repository + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 - # tested with https://github.com/apps/fastpages-chatops - - name: test - id: test - uses: docker://hamelsmu/app-token:temp - env: - INPUT_APP_PEM: ${{ secrets.APP_PEM }} - INPUT_APP_ID: ${{ secrets.APP_ID }} + - name: Environment + run: | + bash -c set - - name: pre-build action image - run: | - cd $GITHUB_WORKSPACE - echo ${PASSWORD} | docker login -u $USERNAME --password-stdin - docker build -t hamelsmu/app-token -f prebuild.Dockerfile . - docker push hamelsmu/app-token - env: - USERNAME: ${{ secrets.DOCKER_USERNAME }} - PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - name: Get token + id: get_token + uses: ./ # Uses an action in the root directory + with: + APP_PEM: ${{ secrets.APP_PEM }} + APP_ID: ${{ secrets.APP_ID }} - # tested withhttps://github.com/apps/fastpages-chatops - - name: final-test - uses: machine-learning-apps/actions-app-token@master - with: - APP_PEM: ${{ secrets.APP_PEM }} - APP_ID: ${{ secrets.APP_ID }} \ No newline at end of file + - name: Get App Installation Token + run: | + echo "This token is masked: ${TOKEN}" + env: + TOKEN: ${{ steps.get_token.outputs.app_token }} From 156e5476e8ebb3d6e951c16337ad1b8ca78df15d Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Tue, 12 Mar 2024 19:28:45 -0700 Subject: [PATCH 04/18] chg: rename and simplify docker file Signed-off-by: Steve Arnold --- Dockerfile | 16 ++++++++++++++++ entrypoint.sh | 4 ++-- prebuild.Dockerfile | 14 -------------- 3 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 Dockerfile mode change 100755 => 100644 entrypoint.sh delete mode 100644 prebuild.Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a7c462 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9-alpine3.16 + +RUN apk --no-cache add git bash + +ADD entrypoint.sh / +ADD token_getter.py / + +RUN pip install --no-cache-dir \ + cryptography==42.0.5 \ + github3.py==4.0.1 \ + jwcrypto==1.5.6 \ + pyjwt==2.8.0 + +RUN chmod u+x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh old mode 100755 new mode 100644 index 5be9090..2532a8d --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash echo $INPUT_APP_PEM | base64 -d > pem.txt -python /app/token_getter.py +python token_getter.py diff --git a/prebuild.Dockerfile b/prebuild.Dockerfile deleted file mode 100644 index fcb49fb..0000000 --- a/prebuild.Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM python:3.9-alpine3.16 - -RUN pip install \ - cryptography==42.0.5 \ - github3.py==4.0.1 \ - jwcrypto==1.5.6 \ - pyjwt==2.8.0 - -COPY token_getter.py app/ -COPY entrypoint.sh app/ -RUN chmod u+x app/entrypoint.sh -WORKDIR app/ - -CMD /app/entrypoint.sh From d2fbf368c58d06d4408bc14cbee1ec7a5be510dd Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Tue, 12 Mar 2024 21:11:34 -0700 Subject: [PATCH 05/18] chg: fix grammar in action.yml message Signed-off-by: Steve Arnold --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index aad720c..fb695c2 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,7 @@ inputs: description: a base64 encoded string version of your PEM file used to authenticate as a GitHub App. You can apply this encoding in the terminal `cat key.pem | base64` required: true APP_ID: - description: you GITHUB App ID. + description: your GITHUB App ID. required: true outputs: app_token: From 23a1e899b57acf4fcbe659865242eda8ef4645ef Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 11:28:03 -0700 Subject: [PATCH 06/18] chg: add org-level test bits for this repo and update test.yaml Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 22e50a0..5dd7a2e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,8 +27,8 @@ jobs: id: get_token uses: ./ # Uses an action in the root directory with: - APP_PEM: ${{ secrets.APP_PEM }} - APP_ID: ${{ secrets.APP_ID }} + APP_PEM: ${{ secrets.VCT_GHT_APP_PEM }} + APP_ID: ${{ vars.VCT_GHT_APP_ID }} - name: Get App Installation Token run: | From df5d5c14762ef128bacd0a91e4b0c8793627cd8a Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 12:10:34 -0700 Subject: [PATCH 07/18] fix: replace deprecated backend import with correct module * cryptography stopped requiring the use of backend arguments in 3.1 Signed-off-by: Steve Arnold --- token_getter.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/token_getter.py b/token_getter.py index e1604a3..2989098 100644 --- a/token_getter.py +++ b/token_getter.py @@ -1,7 +1,7 @@ from collections import namedtuple, Counter from github3 import GitHub from pathlib import Path -from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization import time import json import jwt @@ -12,10 +12,10 @@ class GitHubApp(GitHub): """ This is a small wrapper around the github3.py library - + Provides some convenience functions for testing purposes. """ - + def __init__(self, pem_path, app_id, nwo): super().__init__() self.app_id = app_id @@ -23,16 +23,16 @@ def __init__(self, pem_path, app_id, nwo): self.path = Path(pem_path) self.app_id = app_id if not self.path.is_file(): - raise ValueError(f'argument: `pem_path` must be a valid filename. {pem_path} was not found.') + raise ValueError(f'argument: `pem_path` must be a valid filename. {pem_path} was not found.') self.nwo = nwo - + def get_app(self): with open(self.path, 'rb') as key_file: client = GitHub() client.login_as_app(private_key_pem=key_file.read(), app_id=self.app_id) return client - + def get_installation(self, installation_id): "login as app installation without requesting previously gathered data." with open(self.path, 'rb') as key_file: @@ -41,30 +41,30 @@ def get_installation(self, installation_id): app_id=self.app_id, installation_id=installation_id) return client - + def get_test_installation_id(self): "Get a sample test_installation id." client = self.get_app() return next(client.app_installations()).id - + def get_test_installation(self): "login as app installation with the first installation_id retrieved." return self.get_installation(self.get_test_installation_id()) - + def get_test_repo(self): repo = self.get_all_repos(self.get_test_installation_id())[0] appInstallation = self.get_test_installation() owner, name = repo['full_name'].split('/') return appInstallation.repository(owner, name) - + def get_test_issue(self): test_repo = self.get_test_repo() return next(test_repo.issues()) - + def get_jwt(self): """ - This is needed to retrieve the installation access token (for debugging). - + This is needed to retrieve the installation access token (for debugging). + Useful for debugging purposes. Must call .decode() on returned object to get string. """ now = self._now_int() @@ -74,9 +74,9 @@ def get_jwt(self): "iss": self.app_id } with open(self.path, 'rb') as key_file: - private_key = default_backend().load_pem_private_key(key_file.read(), None) + private_key = serialization.load_pem_private_key(key_file.read(), None) return jwt.encode(payload, private_key, algorithm='RS256') - + def get_installation_id(self): "https://developer.github.com/v3/apps/#find-repository-installation" @@ -86,7 +86,7 @@ def get_installation_id(self): headers = {'Authorization': f'Bearer {self.get_jwt().decode()}', 'Accept': 'application/vnd.github.machine-man-preview+json'} - + response = requests.get(url=url, headers=headers) if response.status_code != 200: raise Exception(f'Status code : {response.status_code}, {response.json()}') @@ -94,11 +94,11 @@ def get_installation_id(self): def get_installation_access_token(self, installation_id): "Get the installation access token for debugging." - + url = f'https://api.github.com/app/installations/{installation_id}/access_tokens' headers = {'Authorization': f'Bearer {self.get_jwt().decode()}', 'Accept': 'application/vnd.github.machine-man-preview+json'} - + response = requests.post(url=url, headers=headers) if response.status_code != 201: raise Exception(f'Status code : {response.status_code}, {response.json()}') @@ -107,24 +107,24 @@ def get_installation_access_token(self, installation_id): def _extract(self, d, keys): "extract selected keys from a dict." return dict((k, d[k]) for k in keys if k in d) - + def _now_int(self): return int(time.time()) def get_all_repos(self, installation_id): """Get all repos that this installation has access to. - + Useful for testing and debugging. """ url = 'https://api.github.com/installation/repositories' headers={'Authorization': f'token {self.get_installation_access_token(installation_id)}', 'Accept': 'application/vnd.github.machine-man-preview+json'} - + response = requests.get(url=url, headers=headers) - + if response.status_code >= 400: raise Exception(f'Status code : {response.status_code}, {response.json()}') - + fields = ['name', 'full_name', 'id'] return [self._extract(x, fields) for x in response.json()['repositories']] From ed9807bf37ad4f22055e031f4584a8185edfba4e Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 13:36:47 -0700 Subject: [PATCH 08/18] chg: swap out local action for GH action and test org settings Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5dd7a2e..72d96b6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,7 +15,7 @@ jobs: # To use this repository's private action, # you must check out the repository - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -23,15 +23,29 @@ jobs: run: | bash -c set - - name: Get token - id: get_token - uses: ./ # Uses an action in the root directory + #- name: Get token local action + #id: get_token + #uses: ./ # Uses an action in the root directory + #with: + #APP_PEM: ${{ secrets.VCT_GHT_APP_PEM }} + #APP_ID: ${{ vars.VCT_GHT_APP_ID }} + + #- name: Get App Installation Token + #run: | + #echo "This token is masked: ${TOKEN}" + #env: + #TOKEN: ${{ steps.get_token.outputs.app_token }} + + - name: Generate GH token + id: generate-token + uses: actions/create-github-app-token@v1 with: - APP_PEM: ${{ secrets.VCT_GHT_APP_PEM }} - APP_ID: ${{ vars.VCT_GHT_APP_ID }} + app-id: ${{ vars.VCT_GHT_APP_ID }} + private-key: ${{ secrets.VCT_GHT_APP_PEM }} + owner: ${{ github.repository_owner }} - - name: Get App Installation Token - run: | - echo "This token is masked: ${TOKEN}" + - name: Check the GH token env: - TOKEN: ${{ steps.get_token.outputs.app_token }} + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + run: | + gh api actions-app-token From 80895624e9a2599ebb1e37c8eb247360ea2ce920 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 13:52:59 -0700 Subject: [PATCH 09/18] chg: restrict repo scope and use super-simple check Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 72d96b6..b95af0a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,18 +24,12 @@ jobs: bash -c set #- name: Get token local action - #id: get_token + #id: generate-token #uses: ./ # Uses an action in the root directory #with: #APP_PEM: ${{ secrets.VCT_GHT_APP_PEM }} #APP_ID: ${{ vars.VCT_GHT_APP_ID }} - #- name: Get App Installation Token - #run: | - #echo "This token is masked: ${TOKEN}" - #env: - #TOKEN: ${{ steps.get_token.outputs.app_token }} - - name: Generate GH token id: generate-token uses: actions/create-github-app-token@v1 @@ -43,9 +37,17 @@ jobs: app-id: ${{ vars.VCT_GHT_APP_ID }} private-key: ${{ secrets.VCT_GHT_APP_PEM }} owner: ${{ github.repository_owner }} + repositories: "actions-app-token,redis-ipc" - - name: Check the GH token - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} + - name: Check App Installation Token run: | - gh api actions-app-token + echo "This token is masked: ${TOKEN}" + env: + TOKEN: ${{ steps.generate-token.outputs.app_token }} + + # the example 'gh' command is not there + #- name: Check the GH token + #env: + #GH_TOKEN: ${{ steps.generate-token.outputs.token }} + #run: | + #gh api actions-app-token From 939f4d6d973e40ca0c0eb6f9566d5a8d0a3fe4d0 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 14:00:33 -0700 Subject: [PATCH 10/18] chg: set repositories to a single repo * not found (scope error?) when set to 'repo1,repo2' * same error with single (host) repo * only seems to work with 'owner' set and 'repositories' unset Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b95af0a..89ccc84 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -37,13 +37,13 @@ jobs: app-id: ${{ vars.VCT_GHT_APP_ID }} private-key: ${{ secrets.VCT_GHT_APP_PEM }} owner: ${{ github.repository_owner }} - repositories: "actions-app-token,redis-ipc" + # repositories: "actions-app-token" - name: Check App Installation Token run: | echo "This token is masked: ${TOKEN}" env: - TOKEN: ${{ steps.generate-token.outputs.app_token }} + TOKEN: ${{ steps.generate-token.outputs.token }} # the example 'gh' command is not there #- name: Check the GH token From 97c276dcf78d390347edca0c7e99f3ee3f992ff4 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 18:45:07 -0700 Subject: [PATCH 11/18] chg: switch back to local action test, remove input decode * refactor get_jwt to use text input and encode at load time Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 30 +++++++----------------------- README.md | 10 +++++----- entrypoint.sh | 3 ++- token_getter.py | 10 +++++++--- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 89ccc84..0776954 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,7 +15,7 @@ jobs: # To use this repository's private action, # you must check out the repository - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -23,31 +23,15 @@ jobs: run: | bash -c set - #- name: Get token local action - #id: generate-token - #uses: ./ # Uses an action in the root directory - #with: - #APP_PEM: ${{ secrets.VCT_GHT_APP_PEM }} - #APP_ID: ${{ vars.VCT_GHT_APP_ID }} - - - name: Generate GH token - id: generate-token - uses: actions/create-github-app-token@v1 + - name: Get token local action + id: get_token + uses: ./ # Uses an action in the root directory with: - app-id: ${{ vars.VCT_GHT_APP_ID }} - private-key: ${{ secrets.VCT_GHT_APP_PEM }} - owner: ${{ github.repository_owner }} - # repositories: "actions-app-token" + APP_ID: ${{ vars.VCT_GHT_APP_ID }} + APP_PEM: ${{ secrets.VCT_GHT_APP_PEM }} - name: Check App Installation Token run: | echo "This token is masked: ${TOKEN}" env: - TOKEN: ${{ steps.generate-token.outputs.token }} - - # the example 'gh' command is not there - #- name: Check the GH token - #env: - #GH_TOKEN: ${{ steps.generate-token.outputs.token }} - #run: | - #gh api actions-app-token + TOKEN: ${{ steps.get_token.outputs.app_token }} diff --git a/README.md b/README.md index 256ab03..40ac063 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Actions Status](https://github.com/machine-learning-apps/actions-app-token/workflows/Tests/badge.svg) +![Actions Status](https://github.com/VCTLabs/actions-app-token/workflows/Tests/badge.svg) # Impersonate Your GitHub App In A GitHub Action @@ -9,13 +9,13 @@ This action helps you retrieve an authenticated app token with a GitHub app id a Actions have certain limitations. Many of these limitations are for security and stability reasons, however not all of them are. Some examples where you might want to impersonate a GitHub App temporarily in your workflow: -- You want an [event to trigger a workflow](https://help.github.com/en/articles/events-that-trigger-workflows) on a specific ref or branch in a way that is not natively supported by Actions. For example, a pull request comment fires the [issue_comment event](https://help.github.com/en/articles/events-that-trigger-workflows#issue-comment-event-issue_comment) which is sent to the default branch and not the PR's branch. You can temporarily impersonate a GitHub App to make an event, such as a [label a pull_request](https://help.github.com/en/articles/events-that-trigger-workflows#pull-request-event-pull_request) to trigger a workflow on the right branch. This takes advantage of the fact that Actions cannot create events that trigger workflows, however other Apps can. +- You want an [event to trigger a workflow](https://help.github.com/en/articles/events-that-trigger-workflows) on a specific ref or branch in a way that is not natively supported by Actions. For example, a pull request comment fires the [issue_comment event](https://help.github.com/en/articles/events-that-trigger-workflows#issue-comment-event-issue_comment) which is sent to the default branch and not the PR's branch. You can temporarily impersonate a GitHub App to make an event, such as a [label a pull_request](https://help.github.com/en/articles/events-that-trigger-workflows#pull-request-event-pull_request) to trigger a workflow on the right branch. This takes advantage of the fact that Actions cannot create events that trigger workflows, however other Apps can. # Usage 1. If you do not already own a GitHub App you want to impersonate, [create a new GitHub App](https://developer.github.com/apps/building-github-apps/creating-a-github-app/) with your desired permissions. If only creating a new app for the purposes of impersonation by Actions, you do not need to provide a `Webhook URL or Webhook Secret` -2. Install the App on your repositories. +2. Install the App on your repositories. 3. See [action.yml](action.yml) for the api spec. @@ -33,7 +33,7 @@ steps: - name: Get App Installation Token run: | echo "This token is masked: ${TOKEN}" - env: + env: TOKEN: ${{ steps.get_token.outputs.app_token }} ``` @@ -46,7 +46,7 @@ cat your_app_key.pem | base64 -w 0 && echo ## Mandatory Inputs -- `APP_PEM`: description: string version of your PEM file used to authenticate as a GitHub App. +- `APP_PEM`: description: string version of your PEM file used to authenticate as a GitHub App. - `APP_ID`: your GitHub App ID. diff --git a/entrypoint.sh b/entrypoint.sh index 2532a8d..81f597b 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,5 @@ #! /usr/bin/env bash -echo $INPUT_APP_PEM | base64 -d > pem.txt +#echo $INPUT_APP_PEM | base64 -d > pem.txt +echo $INPUT_APP_PEM > pem.txt python token_getter.py diff --git a/token_getter.py b/token_getter.py index 2989098..d3af09d 100644 --- a/token_getter.py +++ b/token_getter.py @@ -2,6 +2,7 @@ from github3 import GitHub from pathlib import Path from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa import time import json import jwt @@ -73,9 +74,12 @@ def get_jwt(self): "exp": now + (60), "iss": self.app_id } - with open(self.path, 'rb') as key_file: - private_key = serialization.load_pem_private_key(key_file.read(), None) - return jwt.encode(payload, private_key, algorithm='RS256') + private_key = Path(self.path).read_text() + + private_key_loaded = serialization.load_pem_private_key( + data=private_key.encode(), password=None + ) + return jwt.encode(payload=payload, key=private_key_loaded, algorithm="RS256") def get_installation_id(self): "https://developer.github.com/v3/apps/#find-repository-installation" From ef081ada5fbddb082094935ba5f55452baf8cc34 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 19:30:27 -0700 Subject: [PATCH 12/18] chg: revert to loading bytes per upstream docs Signed-off-by: Steve Arnold --- token_getter.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/token_getter.py b/token_getter.py index d3af09d..2c7a354 100644 --- a/token_getter.py +++ b/token_getter.py @@ -74,11 +74,12 @@ def get_jwt(self): "exp": now + (60), "iss": self.app_id } - private_key = Path(self.path).read_text() - private_key_loaded = serialization.load_pem_private_key( - data=private_key.encode(), password=None - ) + with open(self.path, 'rb') as key_file: + private_key_loaded = serialization.load_pem_private_key( + data=key_file.read(), + password=None, + ) return jwt.encode(payload=payload, key=private_key_loaded, algorithm="RS256") def get_installation_id(self): From c7205bf172286dadaa7c9711316b0f4a0322bb0e Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 20:34:49 -0700 Subject: [PATCH 13/18] chg: mid-cleanup commit, next pre-commit Signed-off-by: Steve Arnold --- .gitignore | 130 +++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 7 +-- requirements.txt | 4 ++ token_getter.py | 69 ++++++++++++++----------- tox.ini | 100 ++++++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+), 36 deletions(-) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e17e6b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +.idea diff --git a/Dockerfile b/Dockerfile index 0a7c462..fe979ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,9 @@ RUN apk --no-cache add git bash ADD entrypoint.sh / ADD token_getter.py / +ADD requirements.txt / -RUN pip install --no-cache-dir \ - cryptography==42.0.5 \ - github3.py==4.0.1 \ - jwcrypto==1.5.6 \ - pyjwt==2.8.0 +RUN pip install --no-cache-dir -r /requirements.txt RUN chmod u+x /entrypoint.sh diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5c78e44 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +cryptography==42.0.5 +github3.py==4.0.1 +jwcrypto==1.5.6 +pyjwt==2.8.0 diff --git a/token_getter.py b/token_getter.py index 2c7a354..fa36ee8 100644 --- a/token_getter.py +++ b/token_getter.py @@ -1,14 +1,12 @@ -from collections import namedtuple, Counter -from github3 import GitHub -from pathlib import Path -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa +import os import time -import json +from pathlib import Path + import jwt import requests -from typing import List -import os +from cryptography.hazmat.primitives import serialization +from github3 import GitHub + class GitHubApp(GitHub): """ @@ -24,23 +22,26 @@ def __init__(self, pem_path, app_id, nwo): self.path = Path(pem_path) self.app_id = app_id if not self.path.is_file(): - raise ValueError(f'argument: `pem_path` must be a valid filename. {pem_path} was not found.') + raise ValueError( + f'argument: `pem_path` must be a valid filename. {pem_path} was not found.' + ) self.nwo = nwo def get_app(self): - with open(self.path, 'rb') as key_file: + with open(str(self.path), 'rb') as key_file: client = GitHub() - client.login_as_app(private_key_pem=key_file.read(), - app_id=self.app_id) + client.login_as_app(private_key_pem=key_file.read(), app_id=self.app_id) return client def get_installation(self, installation_id): "login as app installation without requesting previously gathered data." - with open(self.path, 'rb') as key_file: + with open(str(self.path), 'rb') as key_file: client = GitHub() - client.login_as_app_installation(private_key_pem=key_file.read(), - app_id=self.app_id, - installation_id=installation_id) + client.login_as_app_installation( + private_key_pem=key_file.read(), + app_id=self.app_id, + installation_id=installation_id, + ) return client def get_test_installation_id(self): @@ -69,13 +70,9 @@ def get_jwt(self): Useful for debugging purposes. Must call .decode() on returned object to get string. """ now = self._now_int() - payload = { - "iat": now, - "exp": now + (60), - "iss": self.app_id - } + payload = {"iat": now, "exp": now + (60), "iss": self.app_id} - with open(self.path, 'rb') as key_file: + with open(str(self.path)h, 'rb') as key_file: private_key_loaded = serialization.load_pem_private_key( data=key_file.read(), password=None, @@ -89,8 +86,10 @@ def get_installation_id(self): url = f'https://api.github.com/repos/{owner}/{repo}/installation' - headers = {'Authorization': f'Bearer {self.get_jwt().decode()}', - 'Accept': 'application/vnd.github.machine-man-preview+json'} + headers = { + 'Authorization': f'Bearer {self.get_jwt().decode()}', + 'Accept': 'application/vnd.github.machine-man-preview+json', + } response = requests.get(url=url, headers=headers) if response.status_code != 200: @@ -100,9 +99,13 @@ def get_installation_id(self): def get_installation_access_token(self, installation_id): "Get the installation access token for debugging." - url = f'https://api.github.com/app/installations/{installation_id}/access_tokens' - headers = {'Authorization': f'Bearer {self.get_jwt().decode()}', - 'Accept': 'application/vnd.github.machine-man-preview+json'} + url = ( + f'https://api.github.com/app/installations/{installation_id}/access_tokens' + ) + headers = { + 'Authorization': f'Bearer {self.get_jwt().decode()}', + 'Accept': 'application/vnd.github.machine-man-preview+json', + } response = requests.post(url=url, headers=headers) if response.status_code != 201: @@ -122,8 +125,10 @@ def get_all_repos(self, installation_id): Useful for testing and debugging. """ url = 'https://api.github.com/installation/repositories' - headers={'Authorization': f'token {self.get_installation_access_token(installation_id)}', - 'Accept': 'application/vnd.github.machine-man-preview+json'} + headers = { + 'Authorization': f'token {self.get_installation_access_token(installation_id)}', + 'Accept': 'application/vnd.github.machine-man-preview+json', + } response = requests.get(url=url, headers=headers) @@ -133,10 +138,12 @@ def get_all_repos(self, installation_id): fields = ['name', 'full_name', 'id'] return [self._extract(x, fields) for x in response.json()['repositories']] - def generate_installation_curl(self, endpoint): iat = self.get_installation_access_token() - print(f'curl -i -H "Authorization: token {iat}" -H "Accept: application/vnd.github.machine-man-preview+json" https://api.github.com{endpoint}') + print( + f'curl -i -H "Authorization: token {iat}" -H "Accept: application/vnd.github.machine-man-preview+json" https://api.github.com{endpoint}' + ) + if __name__ == '__main__': diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2177c0c --- /dev/null +++ b/tox.ini @@ -0,0 +1,100 @@ +[tox] +envlist = tests +skip_missing_interpreters = true +skipsdist = true + +[testenv] +install_command = pip install {opts} {packages} + +[testenv:tests] +passenv = + CI + GITHUB* + PYTHON + PYTHONIOENCODING + +setenv = + PYTHONPATH = {toxinidir} + +deps = + pip>=22.1 + pytest + pylint + -r requirements.txt + +commands = + pytest -v . [] + +[testenv:dev] +# requires GH environment vars + +passenv = + {[testenv:tests]passenv} + +setenv = + {[testenv:tests]setenv} + +deps = + {[testenv:tests]deps} + +commands = + python --version + +[testenv:lint] +envdir = {toxworkdir}/tests + +passenv = + {[testenv:tests]passenv} + +deps = + {[testenv:tests]deps} + +commands = + pylint --fail-under=5 token_getter.py + +[testenv:black] +skip_install = true + +deps = + {[testenv:tests]deps} + black + +commands = + black -v -S token_getter.py {posargs} + +[testenv:isort] +skip_install = true + +deps = + {[testenv:tests]deps} + isort + -r requirements.txt + +commands = + python -m isort token_getter.py + +[testenv:sec] +envdir = {toxworkdir}/tests + +passenv = + {[testenv:tests]passenv} + +deps = + pip>=22.1 + bandit + +commands = + bandit token_getter.py + +[testenv:style] +envdir = {toxworkdir}/tests + +passenv = + {[testenv:tests]passenv} + +deps = + pip>=22.1 + flake8 + +commands = + flake8 token_getter.py From 91867993a7882ab921cea290b30f643482cbe0b0 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Wed, 13 Mar 2024 23:23:21 -0700 Subject: [PATCH 14/18] chg: add pre-commit config, apply source and general file cleanup * address bandit and flake8 warnings Signed-off-by: Steve Arnold --- .github/workflows/test.yaml | 1 - .pre-commit-config.yaml | 58 ++++++++++++++++++++++++++++++ .whitesource | 2 +- token_getter.py | 72 ++++++++++++++++++------------------- 4 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0776954..0f82eb7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,6 @@ on: push: branches: [ main ] - jobs: test_defaults: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..18241d5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,58 @@ +# To install the git pre-commit hook run: +# pre-commit install +# To update the pre-commit hooks run: +# pre-commit install-hooks +exclude: '^(.tox/|.*\.sh$)' +repos: + - repo: meta + hooks: + - id: check-useless-excludes + - id: check-hooks-apply + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + #- id: check-json + - id: check-yaml + # exclude: '.pep8speaks.yml' + + - repo: https://github.com/ambv/black + rev: 24.2.0 + hooks: + - id: black + name: "Format code" + language_version: python3 + + - repo: https://github.com/myint/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + files: token_getter.py + args: + - --in-place + - --remove-all-unused-imports + - --remove-duplicate-keys + - --remove-unused-variables + + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + args: ["--max-line-length=148"] + additional_dependencies: ["flake8-bugbear"] + files: token_getter.py + + - repo: https://github.com/PyCQA/bandit + rev: 1.7.8 + hooks: + - id: bandit + args: ["-ll"] + files: token_getter.py + + #- repo: https://github.com/lovesegfault/beautysh + #rev: v6.2.1 + #hooks: + #- id: beautysh diff --git a/.whitesource b/.whitesource index e0aaa3e..6b6a735 100644 --- a/.whitesource +++ b/.whitesource @@ -5,4 +5,4 @@ "issueSettings": { "minSeverityLevel": "LOW" } -} \ No newline at end of file +} diff --git a/token_getter.py b/token_getter.py index fa36ee8..35e56a0 100644 --- a/token_getter.py +++ b/token_getter.py @@ -18,24 +18,22 @@ class GitHubApp(GitHub): def __init__(self, pem_path, app_id, nwo): super().__init__() self.app_id = app_id - self.path = Path(pem_path) - self.app_id = app_id if not self.path.is_file(): raise ValueError( - f'argument: `pem_path` must be a valid filename. {pem_path} was not found.' + f"argument: `pem_path` must be a valid filename. {pem_path} was not found." ) self.nwo = nwo def get_app(self): - with open(str(self.path), 'rb') as key_file: + with open(str(self.path), "rb") as key_file: client = GitHub() client.login_as_app(private_key_pem=key_file.read(), app_id=self.app_id) return client def get_installation(self, installation_id): "login as app installation without requesting previously gathered data." - with open(str(self.path), 'rb') as key_file: + with open(str(self.path), "rb") as key_file: client = GitHub() client.login_as_app_installation( private_key_pem=key_file.read(), @@ -56,7 +54,7 @@ def get_test_installation(self): def get_test_repo(self): repo = self.get_all_repos(self.get_test_installation_id())[0] appInstallation = self.get_test_installation() - owner, name = repo['full_name'].split('/') + owner, name = repo["full_name"].split("/") return appInstallation.repository(owner, name) def get_test_issue(self): @@ -72,7 +70,7 @@ def get_jwt(self): now = self._now_int() payload = {"iat": now, "exp": now + (60), "iss": self.app_id} - with open(str(self.path)h, 'rb') as key_file: + with open(str(self.path), "rb") as key_file: private_key_loaded = serialization.load_pem_private_key( data=key_file.read(), password=None, @@ -82,35 +80,35 @@ def get_jwt(self): def get_installation_id(self): "https://developer.github.com/v3/apps/#find-repository-installation" - owner, repo = self.nwo.split('/') + owner, repo = self.nwo.split("/") - url = f'https://api.github.com/repos/{owner}/{repo}/installation' + url = f"https://api.github.com/repos/{owner}/{repo}/installation" headers = { - 'Authorization': f'Bearer {self.get_jwt().decode()}', - 'Accept': 'application/vnd.github.machine-man-preview+json', + "Authorization": f"Bearer {self.get_jwt().decode()}", + "Accept": "application/vnd.github.machine-man-preview+json", } - response = requests.get(url=url, headers=headers) + response = requests.get(url=url, headers=headers, timeout=5) if response.status_code != 200: - raise Exception(f'Status code : {response.status_code}, {response.json()}') - return response.json()['id'] + raise Exception(f"Status code : {response.status_code}, {response.json()}") + return response.json()["id"] def get_installation_access_token(self, installation_id): "Get the installation access token for debugging." url = ( - f'https://api.github.com/app/installations/{installation_id}/access_tokens' + f"https://api.github.com/app/installations/{installation_id}/access_tokens" ) headers = { - 'Authorization': f'Bearer {self.get_jwt().decode()}', - 'Accept': 'application/vnd.github.machine-man-preview+json', + "Authorization": f"Bearer {self.get_jwt().decode()}", + "Accept": "application/vnd.github.machine-man-preview+json", } - response = requests.post(url=url, headers=headers) + response = requests.post(url=url, headers=headers, timeout=10) if response.status_code != 201: - raise Exception(f'Status code : {response.status_code}, {response.json()}') - return response.json()['token'] + raise Exception(f"Status code : {response.status_code}, {response.json()}") + return response.json()["token"] def _extract(self, d, keys): "extract selected keys from a dict." @@ -124,19 +122,19 @@ def get_all_repos(self, installation_id): Useful for testing and debugging. """ - url = 'https://api.github.com/installation/repositories' + url = "https://api.github.com/installation/repositories" headers = { - 'Authorization': f'token {self.get_installation_access_token(installation_id)}', - 'Accept': 'application/vnd.github.machine-man-preview+json', + "Authorization": f"token {self.get_installation_access_token(installation_id)}", + "Accept": "application/vnd.github.machine-man-preview+json", } - response = requests.get(url=url, headers=headers) + response = requests.get(url=url, headers=headers, timeout=5) if response.status_code >= 400: - raise Exception(f'Status code : {response.status_code}, {response.json()}') + raise Exception(f"Status code : {response.status_code}, {response.json()}") - fields = ['name', 'full_name', 'id'] - return [self._extract(x, fields) for x in response.json()['repositories']] + fields = ["name", "full_name", "id"] + return [self._extract(x, fields) for x in response.json()["repositories"]] def generate_installation_curl(self, endpoint): iat = self.get_installation_access_token() @@ -145,20 +143,22 @@ def generate_installation_curl(self, endpoint): ) -if __name__ == '__main__': +if __name__ == "__main__": - pem_path = 'pem.txt' - app_id = os.getenv('INPUT_APP_ID') - nwo = os.getenv('GITHUB_REPOSITORY') + pem_path = "pem.txt" + app_id = os.getenv("INPUT_APP_ID") + nwo = os.getenv("GITHUB_REPOSITORY") + env_file = os.getenv("GITHUB_ENV") - assert pem_path, 'Must supply input APP_PEM' - assert app_id, 'Must supply input APP_ID' + assert pem_path, "Must supply input APP_PEM" + assert app_id, "Must supply input APP_ID" assert nwo, "The environment variable GITHUB_REPOSITORY was not found." app = GitHubApp(pem_path=pem_path, app_id=app_id, nwo=nwo) id = app.get_installation_id() token = app.get_installation_access_token(installation_id=id) - assert token, 'Token not returned!' + assert token, "Token not returned!" - os.system(f'echo ::add-mask::{token}') - os.system(f'echo "app_token={token}">> $GITHUB_ENV') + print(f"::add-mask::{token}") + with open(env_file, "a") as myfile: + myfile.write(f"app_token={token}\n") From 7a00f876ee981db68318e57d643ace3d1f572025 Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Thu, 14 Mar 2024 10:10:59 -0700 Subject: [PATCH 15/18] chg: add more hooks, cleanup problematic code snippets Signed-off-by: Steve Arnold --- .pre-commit-config.yaml | 9 ++++++++- token_getter.py | 10 ++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18241d5..2ee3cbc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,13 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer + - id: check-ast + - id: check-builtin-literals + - id: check-merge-conflict + - id: debug-statements + - id: detect-private-key + - id: fix-encoding-pragma + args: ['--remove'] - id: mixed-line-ending args: [--fix=lf] #- id: check-json @@ -41,7 +48,7 @@ repos: rev: 7.0.0 hooks: - id: flake8 - args: ["--max-line-length=148"] + args: ["--max-line-length=102"] additional_dependencies: ["flake8-bugbear"] files: token_getter.py diff --git a/token_getter.py b/token_getter.py index 35e56a0..63f6024 100644 --- a/token_getter.py +++ b/token_getter.py @@ -136,12 +136,6 @@ def get_all_repos(self, installation_id): fields = ["name", "full_name", "id"] return [self._extract(x, fields) for x in response.json()["repositories"]] - def generate_installation_curl(self, endpoint): - iat = self.get_installation_access_token() - print( - f'curl -i -H "Authorization: token {iat}" -H "Accept: application/vnd.github.machine-man-preview+json" https://api.github.com{endpoint}' - ) - if __name__ == "__main__": @@ -155,8 +149,8 @@ def generate_installation_curl(self, endpoint): assert nwo, "The environment variable GITHUB_REPOSITORY was not found." app = GitHubApp(pem_path=pem_path, app_id=app_id, nwo=nwo) - id = app.get_installation_id() - token = app.get_installation_access_token(installation_id=id) + inst_id = app.get_installation_id() + token = app.get_installation_access_token(installation_id=inst_id) assert token, "Token not returned!" print(f"::add-mask::{token}") From 03b71510aad4031c047d85d5a4b5dca400090f4a Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Thu, 14 Mar 2024 13:34:19 -0700 Subject: [PATCH 16/18] chg: swap out JWT packages and try te one from GH doc examples Signed-off-by: Steve Arnold --- .gitignore | 3 +++ requirements.txt | 5 +++-- tests/test_jwt.py | 32 ++++++++++++++++++++++++++++++++ token_getter.py | 34 +++++++++++++++++++--------------- tox.ini | 5 +++-- 5 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 tests/test_jwt.py diff --git a/.gitignore b/.gitignore index e17e6b7..09a789a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +# user/env/test bits +pem.txt + # C extensions *.so diff --git a/requirements.txt b/requirements.txt index 5c78e44..6e3d89e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ cryptography==42.0.5 github3.py==4.0.1 -jwcrypto==1.5.6 -pyjwt==2.8.0 +#jwcrypto==1.5.6 +#pyjwt==2.8.0 +jwt==1.3.1 diff --git a/tests/test_jwt.py b/tests/test_jwt.py new file mode 100644 index 0000000..d436877 --- /dev/null +++ b/tests/test_jwt.py @@ -0,0 +1,32 @@ +import json +from pathlib import Path + +from token_getter import GitHubApp + +import pytest + + +inputs = { + "pem_path": "pem.txt", + "app_id": "153667", + "nwo": "VCTLabs/actions-app-token", +} + +test_app = GitHubApp(inputs["pem_path"], inputs["app_id"], inputs["nwo"]) + + +def test_inputs(): + assert inputs["pem_path"] + assert inputs["app_id"] + assert inputs["nwo"] + + +def test_get_jwt(): + res = test_app.get_jwt() + print(f"jwt type is: {type(res)}") + print(res) + + +# def test_get_install_id(): +# res = test_app.get_installation_id() +# print(res) diff --git a/token_getter.py b/token_getter.py index 63f6024..9c021af 100644 --- a/token_getter.py +++ b/token_getter.py @@ -2,9 +2,8 @@ import time from pathlib import Path -import jwt import requests -from cryptography.hazmat.primitives import serialization +from jwt import JWT, jwk_from_pem from github3 import GitHub @@ -65,17 +64,17 @@ def get_jwt(self): """ This is needed to retrieve the installation access token (for debugging). - Useful for debugging purposes. Must call .decode() on returned object to get string. + Useful for debugging purposes. """ now = self._now_int() - payload = {"iat": now, "exp": now + (60), "iss": self.app_id} + payload = {"iat": now, "exp": now + (180), "iss": self.app_id} - with open(str(self.path), "rb") as key_file: - private_key_loaded = serialization.load_pem_private_key( - data=key_file.read(), - password=None, - ) - return jwt.encode(payload=payload, key=private_key_loaded, algorithm="RS256") + # Open PEM file + with open(str(self.path), "rb") as pem_file: + signing_key = jwk_from_pem(pem_file.read()) + + jwt_instance = JWT() + return jwt_instance.encode(payload, signing_key, alg="RS256") def get_installation_id(self): "https://developer.github.com/v3/apps/#find-repository-installation" @@ -85,11 +84,13 @@ def get_installation_id(self): url = f"https://api.github.com/repos/{owner}/{repo}/installation" headers = { - "Authorization": f"Bearer {self.get_jwt().decode()}", - "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": f"Bearer {self.get_jwt()}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", } response = requests.get(url=url, headers=headers, timeout=5) + if response.status_code != 200: raise Exception(f"Status code : {response.status_code}, {response.json()}") return response.json()["id"] @@ -101,11 +102,13 @@ def get_installation_access_token(self, installation_id): f"https://api.github.com/app/installations/{installation_id}/access_tokens" ) headers = { - "Authorization": f"Bearer {self.get_jwt().decode()}", - "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": f"Bearer {self.get_jwt()}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", } response = requests.post(url=url, headers=headers, timeout=10) + if response.status_code != 201: raise Exception(f"Status code : {response.status_code}, {response.json()}") return response.json()["token"] @@ -125,7 +128,8 @@ def get_all_repos(self, installation_id): url = "https://api.github.com/installation/repositories" headers = { "Authorization": f"token {self.get_installation_access_token(installation_id)}", - "Accept": "application/vnd.github.machine-man-preview+json", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", } response = requests.get(url=url, headers=headers, timeout=5) diff --git a/tox.ini b/tox.ini index 2177c0c..479a5b6 100644 --- a/tox.ini +++ b/tox.ini @@ -19,11 +19,11 @@ setenv = deps = pip>=22.1 pytest - pylint -r requirements.txt commands = - pytest -v . [] + pytest -v tests/ --capture=no + #python -m pytest {posargs:"-v"} tests/ --capture=fd --cov=token_getter --cov-branch --cov-report term-missing [testenv:dev] # requires GH environment vars @@ -48,6 +48,7 @@ passenv = deps = {[testenv:tests]deps} + pylint commands = pylint --fail-under=5 token_getter.py From 4c858b8a9fd754717d71691c7d209d26377cfc0f Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Thu, 14 Mar 2024 14:16:07 -0700 Subject: [PATCH 17/18] chg: revert entrypoint.sh to original base64 decode Signed-off-by: Steve Arnold --- entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 81f597b..48ee42a 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,5 @@ #! /usr/bin/env bash -#echo $INPUT_APP_PEM | base64 -d > pem.txt -echo $INPUT_APP_PEM > pem.txt +echo $INPUT_APP_PEM | base64 -d > pem.txt +#echo $INPUT_APP_PEM > pem.txt python token_getter.py From 81373a2000564f2f79a86c0deabb7cd293321c5a Mon Sep 17 00:00:00 2001 From: Steve Arnold Date: Mon, 15 Apr 2024 19:53:38 -0700 Subject: [PATCH 18/18] let python-jwt package pull in the deps Signed-off-by: Steve Arnold --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6e3d89e..1ea8328 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==42.0.5 +#cryptography==42.0.5 github3.py==4.0.1 #jwcrypto==1.5.6 #pyjwt==2.8.0