Skip to content

CFTL-589 GitHub OAuth as a third git authentication method - #32

Open
Oscar-XXII wants to merge 8 commits into
mainfrom
feature/CFTL-589
Open

CFTL-589 GitHub OAuth as a third git authentication method#32
Oscar-XXII wants to merge 8 commits into
mainfrom
feature/CFTL-589

Conversation

@Oscar-XXII

Copy link
Copy Markdown

Why

Cloning a private repository has so far required the customer to create and rotate a credential by hand, and a classic PAT grants read and write across every private repository the user can reach. Enterprise security reviews keep flagging it.

This adds a third option: authorize a Keboola-owned GitHub App through the OAuth broker and receive a user access token limited to Contents: Read-only on the repositories chosen at installation time. SSH and PAT are untouched.

What changed

  • New oauth value on git.auth. Appended to the existing enum, so the radio order for existing configurations does not move.
  • Token handling. The access token arrives outside parameters, in the authorization section. It is handed to git through a GIT_ASKPASS helper, so it never appears in the command line, in the cloned repository's .git/config, or in the environment of the executed user script.
  • authorization is stripped from the config.json written for the user script. Besides the user's own token it carries #appSecret — the shared application secret of the Keboola GitHub App — which no user code may be able to read. This is the most important change in the PR and it is covered by a test.
  • Repository picker. New listRepositories sync action backed by /user/installations + /user/installations/{id}/repositories, both paginated and aggregated across installations. A user access token carries no installation id of its own, hence the two calls. git.url stays free text for the other auth methods; OAuth configurations select into git.repository instead, because a schema field cannot be a dropdown and a text input at once.
  • Private git dependencies. uv sync shells out to git, which had no credentials — this is what broke the first real test run. SubprocessRunner.run gained an optional env and the dependency install now receives the git credentials explicitly. The executed script keeps inheriting the untouched process environment.
  • Error messages. Missing authorization, a non-GitHub URL, a revoked token, and a repository the app cannot see each produce an actionable UserException instead of a raw git or HTTP error.
  • Docs: setup flow, the app's client ID, and why the installation must use Only select repositories.

37 tests, flake8 clean.

Verified on the platform

Job 53340777 in project 4214 (us-east4.gcp), image CFTL-589-143:

Git OAuth authentication set up for GitHub URL.
Cloning git repository: https://github.com/keboola/component-custom-python-example-3.git
Successfully cloned repository

That run confirms the broker token, the askpass helper and the clone all work end to end against real GitHub. It then failed in uv sync on a private dependency — which is the bug fixed in aba710a. A re-run on a build containing that fix is still pending.

Not verified

  • listRepositories has only been exercised against mocks. Against the keboola organisation it returns a truncated response; whether that is a response size cap or the pod being killed mid-write is not yet established (running it twice and comparing the cut point settles it).
  • The dependency fix has not yet run on the platform.

Reviewer notes

  • The _merge_user_parameters change is the security-critical one.
  • source_git.py looks larger than it is: self.git_cfg.url was replaced by a single resolved self.repo_url, and self.env became a git_env overlay resolved at call time — the previous snapshot predated the virtual environment selection and would have pointed uv sync at the wrong environment.

🤖 Generated with Claude Code

Oscar-XXII and others added 8 commits August 21, 2026 11:38
Cloning a private repository so far required the customer to create and rotate a
credential by hand, and a classic PAT grants read and write across every private
repository the user can reach. A GitHub App user access token is limited to
Contents: Read-only on the repositories selected when the app is installed, so
nothing has to be created or rotated manually.

The token arrives outside "parameters", in the authorization section of the
configuration, and is handed to git through a GIT_ASKPASS helper. That keeps it
out of the command line, out of the cloned repository's .git/config and out of
the environment of the executed user script.

The authorization section is also stripped from the config.json written for that
script. Besides the user's own token it carries the shared application secret of
the Keboola GitHub App, which no user code may be able to read.

The existing none/pat/ssh branches are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Repository selection happens when the GitHub App is installed, not when the
component is authorized, and the two are independent flows on GitHub's side.
Authorizing without installing first yields a valid token that can see no
repositories, and the job then fails with a bare "repository not found".
Spelling the order out makes that failure avoidable rather than merely
explainable after the fact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typing the repository URL by hand works, but it defers every mistake to the
first job run: a repository the app cannot see fails as "not found" only once
the clone is attempted. Listing what the installation actually exposes moves
that feedback into the configuration form, and an empty list is precisely the
signal that the app was authorized but never installed.

The list comes from /user/installations followed by
/user/installations/{id}/repositories, because a user access token carries no
installation id of its own. Both endpoints are paginated and one user may see
several installations, so the results are aggregated.

The picker writes a clone URL into git.repository. The free-text git.url stays
in place for the other authentication methods, because a schema field cannot be
a dropdown and a text input at the same time; GitConfiguration.repository_url
resolves which of the two applies.

Calls go through urllib to avoid adding an HTTP dependency for two endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nnot fill in

The free-text url field is hidden for auth: oauth, where the repository is
picked from a dropdown into git.repository instead, but it stayed listed in
git.required. That leaves an OAuth configuration demanding a field the form
gives no way to fill in.

Only auth stays required. An empty repository is still caught at runtime, now
with wording that fits a dropdown rather than asking for a URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion

The installation dialog defaults to a choice, not to a safe default, and the
onboarding text so far only said that repositories are picked during
installation. Picking "All repositories" grants Contents: Read-only across the
whole account or organisation with a token that does not expire — the same
over-scoped long-lived credential that motivated moving away from personal
access tokens. Nothing on the Keboola side can narrow it afterwards, so the
guidance has to arrive before the user clicks.

Also records the app's client ID and the page where a user can review or revoke
the access they granted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app slug is keboola-custom-python-read; it is derived from the app name and
is not the client ID, so it could not be inferred from what the OAuth
registration already recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Job 53340777 cloned its repository over OAuth and then failed in `uv sync` with
"could not read Username for https://github.com": uv shells out to git to fetch
a private dependency, and that git had no credentials at all. The PAT path is
unaffected because it leaves a ~/.netrc behind, which uv picks up on its own —
but a file in the home directory is also readable by the executed user script,
which is why the OAuth path does not write one.

The credentials now travel to the dependency installation explicitly, through a
new optional env argument on SubprocessRunner.run. The executed script keeps
inheriting the untouched process environment, so the token still does not reach
it.

GitHandler now holds only the git-specific overrides and resolves the full
environment when a subprocess is started, rather than snapshotting os.environ in
the constructor. The snapshot predates the virtual environment selection, so
handing it to `uv sync` would have pointed the install at the wrong environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app is now "Keboola Custom Python", which moves its slug from
keboola-custom-python-read to keboola-custom-python. The old slug returns 404 —
GitHub does not redirect a renamed app — so the previous link would simply have
been dead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 21, 2026

Copy link
Copy Markdown

CFTL-589

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant