Skip to main content

GitHub

Proxy for the GitHub REST API. Each managed resource exposes seven MCP tools mapped onto specific api.github.com routes, with per-route OAuth scopes (gh:repo:read, gh:repo:write, gh:issues:read, gh:issues:write, gh:workflows:write).

Overview​

Pick an auth mode at install time:

  • idp_passthrough (recommended) — each caller's own GitHub OAuth token is forwarded upstream. The agent acts as the user; their GitHub permissions apply on top of PBAC policy. Requires a GitHub IdP registered on the AS (one OAuth App per instance).
  • static — a single Personal Access Token (PAT) is forwarded for every caller. Use for unattended automations where the operator owns the GitHub identity and there is no human in the loop. The PAT is stored as a secret reference; it is not typed into a form field directly.

A managed resource is bound to one auth mode. To serve both shapes of caller, create two managed resources (e.g. github-user and github-bot). Policy can key off input.context.rs_context.resource_name to apply different rules.

Setting up GitHub as an IdP​

The admin portal's "Connect a resource" wizard inlines this step the first time you install GitHub with idp_passthrough. The flow is:

  1. Open GitHub Developer Settings → OAuth Apps → New OAuth App.
  2. Set Homepage URL to your AS issuer (e.g. http://localhost:8080).
  3. Set Authorization callback URL to <issuer>/oauth2/callback — the wizard shows the exact value with a copy button.
  4. Generate a client secret. Copy both the Client ID and Client secret into the wizard.

GitHub OAuth Apps don't have you pre-pick scopes at registration — scopes are requested at authorize time and approved by the user. The connector requests read:user, repo, and read:org so callers can read user metadata, work with both public and private repos they own, and resolve org memberships for ABAC decisions. Editing the scopes stored on the managed resource does not change this set, because the AS reads it from the connector's manifest, so you cannot swap repo for the narrower public_repo. You can withhold repo. Open the resource in the Admin Portal, enter repo under Upstream scope control in Excluded (space-separated), and choose Save excluded scopes. The AS then removes repo from the GitHub IdP's scopes unless another enabled resource on that IdP needs it, and stops requesting it at sign-in. In our run repo dropped out of the scope the AS sent to the IdP. The portal notes that tools that need an excluded scope fail.

Policy patterns​

Unlike Gmail, the GitHub connector does not ship its own Rego policy, and no bundled policy carries the rules below. They are examples to add as policy rules yourself. The managed-gateway demo's seed script writes only the subject attributes they read (github_subject_attributes), not the rules.

The three rules below are one Rego file in package pbac.connectors.identos.github, which the policy aggregator walks. The file passes opa check on OPA 1.10.1. They read only fields the gateway sends: input.context.rs_context.resource_type, input.context.rs_context.tool_arguments and input.subject.sub.

package pbac.connectors.identos.github

import rego.v1

_is_github if input.context.rs_context.resource_type == "urn:connector:identos:github"

_args := object.get(input.context.rs_context, "tool_arguments", {})

# A subject with no entry, or a token with no subject, gets no orgs.
default _allowed_orgs := []

_allowed_orgs := orgs if {
orgs := data.github_subject_attributes[input.subject.sub].allowed_orgs
}

# 1. Repository owner allowlist
deny contains msg if {
_is_github
owner := _args.owner
not owner in _allowed_orgs
msg := sprintf("github: %v is not in your allowed orgs", [owner])
}

# 2. Sensitive file path block
deny contains msg if {
_is_github
path := _args.path
_github_sensitive_path(path)
msg := sprintf("github: path %v is blocked", [path])
}

_github_sensitive_path(p) if contains(p, ".env")

_github_sensitive_path(p) if endswith(p, ".key")

_github_sensitive_path(p) if endswith(p, ".pem")

_github_sensitive_path(p) if contains(p, "secrets/")

# 3. Inactive-user kill switch
deny contains msg if {
_is_github
data.github_subject_attributes[input.subject.sub].employment_status != "active"
msg := "github: inactive employee"
}

1. Repository owner allowlist. Every connector route includes an {owner} path variable, which the gateway passes as tool_arguments.owner over REST and MCP. Subject attributes seed each developer with the orgs (or personal namespaces) they are allowed to touch. A subject with no entry, and a client_credentials token, which has no subject, are denied every call that names an owner.

2. Sensitive file path block. Fires for get_repo_file and upsert_repo_file. No subject attributes needed. Customize the predicates to match your secret-scanning conventions.

3. Inactive-user kill switch. Flipping employment_status for a user blocks every GitHub tool call within the OPA bundle poll interval (~2 s). No token revocation is needed, because the deny fires at introspect time. A subject with no entry is not blocked by this rule.

On a local instance with allowed_orgs set to ["acme"] for the user, GET /gateway/<name>/repos/acme/demo/contents/README.md returned 200 for the user, and 403 for owner evil, for path .env, and for a client_credentials token.

Subject-attribute schema​

The reference rules read from data.github_subject_attributes[<sub>]. The shape used by the demo:

{
"allowed_orgs": ["acme", "acme-research"],
"teams": ["eng", "platform"],
"employment_status": "active",
"clearance": "high"
}

The sub claim on a token is a numeric GitHub user ID (not the login). Find it via https://api.github.com/users/<login> and store the id field as the key.

Scope model​

ScopeUsed for
PBAC scopes (internal)gh:repo:read, gh:repo:write, gh:issues:read, gh:issues:write, gh:workflows:writePer-route OAuth gating on the managed gateway. A token without gh:repo:write cannot reach upsert_repo_file regardless of ABAC outcome.
Upstream OAuth scopesread:user, repo, read:orgThe grants the user is asked to approve when they sign in to GitHub via the AS.

Splitting reads from writes at the OAuth scope layer keeps the ABAC policy small — most operator concerns (who can write where, what paths are sensitive) compose with the scope gate rather than reimplementing it.

Static (PAT) mode​

When the connector is installed with upstream_auth.type = "static", the wizard asks for a secret reference (pat_env) instead of running the IdP setup. Create the secret first via the Secrets page or inline via the form's Add secret button. The PAT must carry whichever GitHub OAuth scopes the routes will exercise — typically repo and workflow.

PAT mode forwards the same token for every caller. Subject attributes still apply (the PBAC sub and scopes come from the token PBAC issued, not the upstream PAT) but you lose the per-user GitHub permission layer. Use this mode for automations where the operator owns the identity and the permission model is enforced entirely in PBAC.

Verifying the install​

A direct introspection shows what policy decides for one call. Authenticate with a confidential client entitled to introspection or uma_protection on urn:as:introspect, and pass a token issued for the GitHub resource:

curl -s -X POST "$PBAC_AS_URL/introspect" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode "token=$TOKEN" \
--data-urlencode 'context={
"resource_type": "urn:connector:identos:github",
"resource_name": "github",
"scopes": ["gh:repo:read"],
"tool_arguments": {"owner": "acme", "repo": "demo", "path": ".env"}
}' | jq '{active, inactive_reason}'

No GitHub rule ships, so on a fresh install both a .env path and a README.md path return "active": true. Once you add a deny rule to package pbac.connectors.identos.github that matches only the .env path, such as rule 2 above on its own, that request returns {"active": false, "inactive_reason": "insufficient_scope"} and the README.md request still returns "active": true. The response has no allow field.

Troubleshooting​

SymptomLikely cause
Wizard shows "Step 1 · Set up GitHub sign-in" but you want PAT modePick static from the Authentication dropdown in Step 2; the wizard now treats Step 1 as advisory and lets you submit.
First-time PKCE login fails with a JSON parse errorOlder AS versions sent no Accept header to GitHub's token endpoint. Upgrade — the fix is in IdpFederationService.
idp_passthrough returns 401 from api.github.com after PBAC allowsThe user's GitHub token does not carry the requested scope (e.g. repo for a private repo). Have them re-authorize the OAuth App and accept the missing scope.
All tools return 403 even for an authenticated userIf you use rule 1, confirm the subject attributes are keyed by the user's numeric GitHub ID. An entry keyed by login matches no sub, so rule 1 finds no orgs and denies every call. Rule 3 ignores a subject with no entry, so it does not block a user keyed by login.

Manifest reference​

  • ID: identos.github
  • Version: 1.0.0
  • Resource type: urn:connector:identos:github

Supported auth modes​

TypeDetails
idp_passthroughrequires IdP github
staticscheme bearer; setup fields: pat_env

Setup fields​

IDLabelDefaultSecret?Notes
upstream_auth.typeAuthenticationidp_passthroughnoidp_passthrough forwards each user's own GitHub OAuth token (recommended). static uses a single shared Personal Access Token (machine identity).
pat_envPersonal Access Token—yesPick a secret containing the GitHub PAT. Required only for the static auth mode. / shown when upstream_auth.type == 'static'

Scopes​

Scope
gh:repo:read
gh:repo:write
gh:issues:read
gh:issues:write
gh:workflows:write

Routes​

MethodPatternScopeResource template
GET/repos/{owner}/{repo}gh:repo:readgithub://{{owner}}/{{repo}}
GET/repos/{owner}/{repo}/contents/{path:.*}gh:repo:readgithub://{{owner}}/{{repo}}
PUT/repos/{owner}/{repo}/contents/{path:.*}gh:repo:writegithub://{{owner}}/{{repo}}
GET/repos/{owner}/{repo}/issuesgh:issues:readgithub://{{owner}}/{{repo}}
POST/repos/{owner}/{repo}/issuesgh:issues:writegithub://{{owner}}/{{repo}}
PATCH/repos/{owner}/{repo}/issues/{issue_number}gh:issues:writegithub://{{owner}}/{{repo}}/issues/{{issue_number}}
POST/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatchesgh:workflows:writegithub://{{owner}}/{{repo}}/workflows/{{workflow_id}}

MCP tools​

NameScopeDescription
get_repogh:repo:readGet metadata for a GitHub repository.
get_repo_filegh:repo:readRead a file or directory listing from a GitHub repository at a given path.
upsert_repo_filegh:repo:writeCreate or update a file in a GitHub repository.
list_issuesgh:issues:readList issues in a GitHub repository.
create_issuegh:issues:writeOpen a new issue in a GitHub repository.
update_issuegh:issues:writeUpdate fields on an existing GitHub issue.
dispatch_workflowgh:workflows:writeTrigger a manual run of a GitHub Actions workflow on a given ref, with optional inputs.