From b57b851088626a695a0ee6c0ab83d87879f46e1c Mon Sep 17 00:00:00 2001 From: Andrey Markelov Date: Wed, 15 Jul 2026 15:04:40 -0700 Subject: [PATCH] Authenticate CI via GitHub OIDC and AWS Secrets Manager Migrate CI credential handling to GitHub OIDC + AWS Secrets Manager, mirroring dropbox-sdk-dotnet: credential-consuming jobs assume an AWS IAM role and fetch the api-sdk-integration-test-creds and codecov-token secrets, replacing long-lived GitHub secrets. Credential-gated jobs are skipped on fork PRs, which cannot assume the role. Also stop injecting the unused legacy user token and authenticate the team integration fixture with a refresh token instead of a static token. --- .github/workflows/ci.yml | 39 ++++++++++------ .github/workflows/coverage.yml | 79 ++++++++++++++++++++++++++------ test/integration/test_dropbox.py | 12 +++-- 3 files changed, 99 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c47fa8a..d1f97f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,8 +93,13 @@ jobs: sphinx-build -b html docs build/html Integration: - continue-on-error: true + # Skip on fork PRs: they cannot assume the OIDC role. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: false runs-on: ${{ matrix.os }} + permissions: # required for OIDC + id-token: write + contents: read strategy: fail-fast: false max-parallel: 1 @@ -126,19 +131,27 @@ jobs: run: | python -m pip install "$(python -c "import glob,sys; w=glob.glob('dist/*.whl'); assert w, f'No wheel built. dist contains: {glob.glob(\"dist/*\")}'; print(w[0])")" + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::082972943155:role/oidc-github-dropbox-dropbox-sdk-python-repo + aws-region: us-west-2 + + - name: Get integration credentials from AWS Secrets Manager + uses: aws-actions/aws-secretsmanager-get-secrets@v3 + with: + secret-ids: | + CREDS,api-sdk-integration-test-creds + parse-json-secrets: true + - name: Run integration tests env: - LEGACY_USER_DROPBOX_TOKEN: ${{ secrets.LEGACY_USER_DROPBOX_TOKEN }} - LEGACY_USER_CLIENT_ID: ${{ secrets.LEGACY_USER_CLIENT_ID }} - LEGACY_USER_CLIENT_SECRET: ${{ secrets.LEGACY_USER_CLIENT_SECRET }} - LEGACY_USER_REFRESH_TOKEN: ${{ secrets.LEGACY_USER_REFRESH_TOKEN }} - SCOPED_USER_CLIENT_ID: ${{ secrets.SCOPED_USER_CLIENT_ID }} - SCOPED_USER_CLIENT_SECRET: ${{ secrets.SCOPED_USER_CLIENT_SECRET }} - SCOPED_USER_REFRESH_TOKEN: ${{ secrets.SCOPED_USER_REFRESH_TOKEN }} - SCOPED_TEAM_DROPBOX_TOKEN: ${{ secrets.SCOPED_TEAM_DROPBOX_TOKEN }} - SCOPED_TEAM_CLIENT_ID: ${{ secrets.SCOPED_TEAM_CLIENT_ID }} - SCOPED_TEAM_CLIENT_SECRET: ${{ secrets.SCOPED_TEAM_CLIENT_SECRET }} - SCOPED_TEAM_REFRESH_TOKEN: ${{ secrets.SCOPED_TEAM_REFRESH_TOKEN }} - DROPBOX_SHARED_LINK: ${{ secrets.DROPBOX_SHARED_LINK }} + SCOPED_USER_CLIENT_ID: ${{ env.CREDS_SCOPED_USER_CLIENT_ID }} + SCOPED_USER_CLIENT_SECRET: ${{ env.CREDS_SCOPED_USER_CLIENT_SECRET }} + SCOPED_USER_REFRESH_TOKEN: ${{ env.CREDS_SCOPED_USER_REFRESH_TOKEN }} + SCOPED_TEAM_CLIENT_ID: ${{ env.CREDS_SCOPED_TEAM_CLIENT_ID }} + SCOPED_TEAM_CLIENT_SECRET: ${{ env.CREDS_SCOPED_TEAM_CLIENT_SECRET }} + SCOPED_TEAM_REFRESH_TOKEN: ${{ env.CREDS_SCOPED_TEAM_REFRESH_TOKEN }} + DROPBOX_SHARED_LINK: ${{ env.CREDS_DROPBOX_SHARED_LINK }} run: | pytest -v test/integration/test_dropbox.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 338de00..c73a9fe 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,14 +32,51 @@ jobs: run: | coverage run --rcfile=.coveragerc -m pytest test/unit/ coverage xml + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: unit-coverage + path: coverage.xml + # Separate job so the whole thing can be gated: fork PRs cannot assume the + # OIDC role, so they run the unit tests above but skip the upload here. + CoverageUpload: + needs: Coverage + # Skip on fork PRs: they cannot assume the OIDC role. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: # required for OIDC + id-token: write + contents: read + steps: + - uses: actions/checkout@v7 + - name: Download coverage artifact + uses: actions/download-artifact@v4 + with: + name: unit-coverage + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::082972943155:role/oidc-github-dropbox-dropbox-sdk-python-repo + aws-region: us-west-2 + - name: Get Codecov token from AWS Secrets Manager + uses: aws-actions/aws-secretsmanager-get-secrets@v3 + with: + secret-ids: | + CODECOV_TOKEN,codecov-token-dropbox-sdk-python + parse-json-secrets: false - name: Publish Coverage uses: codecov/codecov-action@v7 with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ env.CODECOV_TOKEN }} flags: unit fail_ci_if_error: true IntegrationCoverage: + # Skip on fork PRs: they cannot assume the OIDC role. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + permissions: # required for OIDC + id-token: write + contents: read steps: - uses: actions/checkout@v7 - name: Setup Python environment @@ -58,26 +95,38 @@ jobs: pip install -r requirements.txt pip install -r test/requirements.txt pip install . - - name: Generate Unit Test Coverage + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::082972943155:role/oidc-github-dropbox-dropbox-sdk-python-repo + aws-region: us-west-2 + - name: Get integration credentials from AWS Secrets Manager + uses: aws-actions/aws-secretsmanager-get-secrets@v3 + with: + secret-ids: | + CREDS,api-sdk-integration-test-creds + parse-json-secrets: true + - name: Get Codecov token from AWS Secrets Manager + uses: aws-actions/aws-secretsmanager-get-secrets@v3 + with: + secret-ids: | + CODECOV_TOKEN,codecov-token-dropbox-sdk-python + parse-json-secrets: false + - name: Generate Coverage env: - LEGACY_USER_DROPBOX_TOKEN: ${{ secrets.LEGACY_USER_DROPBOX_TOKEN }} - LEGACY_USER_CLIENT_ID: ${{ secrets.LEGACY_USER_CLIENT_ID }} - LEGACY_USER_CLIENT_SECRET: ${{ secrets.LEGACY_USER_CLIENT_SECRET }} - LEGACY_USER_REFRESH_TOKEN: ${{ secrets.LEGACY_USER_REFRESH_TOKEN }} - SCOPED_USER_CLIENT_ID: ${{ secrets.SCOPED_USER_CLIENT_ID }} - SCOPED_USER_CLIENT_SECRET: ${{ secrets.SCOPED_USER_CLIENT_SECRET }} - SCOPED_USER_REFRESH_TOKEN: ${{ secrets.SCOPED_USER_REFRESH_TOKEN }} - SCOPED_TEAM_DROPBOX_TOKEN: ${{ secrets.SCOPED_TEAM_DROPBOX_TOKEN }} - SCOPED_TEAM_CLIENT_ID: ${{ secrets.SCOPED_TEAM_CLIENT_ID }} - SCOPED_TEAM_CLIENT_SECRET: ${{ secrets.SCOPED_TEAM_CLIENT_SECRET }} - SCOPED_TEAM_REFRESH_TOKEN: ${{ secrets.SCOPED_TEAM_REFRESH_TOKEN }} - DROPBOX_SHARED_LINK: ${{ secrets.DROPBOX_SHARED_LINK }} + SCOPED_USER_CLIENT_ID: ${{ env.CREDS_SCOPED_USER_CLIENT_ID }} + SCOPED_USER_CLIENT_SECRET: ${{ env.CREDS_SCOPED_USER_CLIENT_SECRET }} + SCOPED_USER_REFRESH_TOKEN: ${{ env.CREDS_SCOPED_USER_REFRESH_TOKEN }} + SCOPED_TEAM_CLIENT_ID: ${{ env.CREDS_SCOPED_TEAM_CLIENT_ID }} + SCOPED_TEAM_CLIENT_SECRET: ${{ env.CREDS_SCOPED_TEAM_CLIENT_SECRET }} + SCOPED_TEAM_REFRESH_TOKEN: ${{ env.CREDS_SCOPED_TEAM_REFRESH_TOKEN }} + DROPBOX_SHARED_LINK: ${{ env.CREDS_DROPBOX_SHARED_LINK }} run: | coverage run --rcfile=.coveragerc -m pytest test/integration/test_dropbox.py coverage xml - name: Publish Coverage uses: codecov/codecov-action@v7 with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ env.CODECOV_TOKEN }} flags: integration fail_ci_if_error: true diff --git a/test/integration/test_dropbox.py b/test/integration/test_dropbox.py index 5715eeb..5afa372 100644 --- a/test/integration/test_dropbox.py +++ b/test/integration/test_dropbox.py @@ -114,9 +114,15 @@ def bad_secret_dbx_from_env(dbx_session): @pytest.fixture() def dbx_team_from_env(dbx_session): - team_oauth2_token = _value_from_env_or_die( - format_env_name(SCOPED_KEY, TEAM_KEY, ACCESS_TOKEN_KEY)) - return DropboxTeam(team_oauth2_token, session=dbx_session) + refresh_token = _value_from_env_or_die( + format_env_name(SCOPED_KEY, TEAM_KEY, REFRESH_TOKEN_KEY)) + app_key = _value_from_env_or_die( + format_env_name(SCOPED_KEY, TEAM_KEY, CLIENT_ID_KEY)) + app_secret = _value_from_env_or_die( + format_env_name(SCOPED_KEY, TEAM_KEY, CLIENT_SECRET_KEY)) + return DropboxTeam(oauth2_refresh_token=refresh_token, + app_key=app_key, app_secret=app_secret, + session=dbx_session) @pytest.fixture()