Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/+app_auth.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added native GitHub App authentication via the {ghconf}`app_id`, {ghconf}`private_key`/{ghconf}`private_key_path` and optional {ghconf}`installation_id` and {ghconf}`permissions` profile parameters. Installation access tokens are minted and refreshed automatically (optionally restricted to a subset of the app's granted permissions), the app's installation on {ghconf}`owner` is discovered when not pinned, and the connection identity derives from the app's identity instead of its short-lived tokens, keeping the persistent {ghconf}`cache` effective across token rotation (e.g. in CI)
1 change: 1 addition & 0 deletions changelog/+revoke_token.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `github.revoke_app_token` for immediately revoking a GitHub App profile's current installation access token (otherwise valid for one hour), e.g. as the last step of an ephemeral CI run
113 changes: 110 additions & 3 deletions docs/topics/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ All modules provided by this extension read their configuration from
profile
: A profile is a mapping under a top-level key in the minion
configuration, minion pillar or master configuration
(grains work also, but should not contain secrets such as the required {ghconf}`token`).
(grains work also, but should not contain secrets such as
{ghconf}`token` or {ghconf}`private_key`).
Functions and states use the profile named `github` by default and accept a
`profile` argument to select a different one.

Expand All @@ -21,7 +22,9 @@ The following keys can be specified in the profile:
:::{ghconf} token
:::
`token`
: **Required**. Access token to authenticate with.
: Access token to authenticate with. Either this or GitHub App
credentials ({ghconf}`app_id` + a private key) are required,
mutually exclusively.

:::{ghconf} owner
:::
Expand All @@ -33,6 +36,7 @@ The following keys can be specified in the profile:

Needs to be set explicitly when the token does not have an associated
resolvable user account such as GitHub App installation tokens.
Required for GitHub App ({ghconf}`app_id`) profiles.

:::{versionadded} 1.1.0
Renamed from `org_name`. The new {py:mod}`github_repo <saltext.github.modules.github_repo>`
Expand All @@ -52,8 +56,12 @@ The following keys can be specified in the profile:
`cache`
: Keep a persistent cross-process cache for specific API data,
stored in the minion's {conf_minion}`cachedir`.
All entries are namespaced by a token/API URL-derived identity,
All entries are namespaced by a credential/API URL-derived identity,
hence rotated credentials never serve stale data.
For GitHub App profiles, the identity derives from the app's identity
({ghconf}`app_id` + {ghconf}`owner` + {ghconf}`permissions`) rather
than its short-lived installation tokens, so the cache survives token
rotation.
The data that is cached is either static or refreshed as soon
as it changes. Defaults to true.

Expand All @@ -65,6 +73,7 @@ The following keys can be specified in the profile:

* {ghconf}`token`-associated user handle (never changes)
* account name => account type lookups (should never change)
* discovered GitHub App installation IDs (see {ghconf}`app_id`)

When the {py:mod}`github resource type <saltext.github.resources.github>` is configured
with {ghconf}`grains_files <resource:grains_files>`, file contents
Expand Down Expand Up @@ -110,6 +119,7 @@ my_user:
salt-call github_repo.list profile=my_user
```

(secret-management)=
## Secret management

Profile values containing [SDB URIs](https://docs.saltproject.io/en/latest/topics/sdb/index.html)
Expand All @@ -129,6 +139,103 @@ github:
owner: my_organization
```

## GitHub App authentication

:::{versionadded} 1.1.0
:::

Instead of a static {ghconf}`token`, a profile can authenticate as a
[GitHub App installation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation).

:::{hint}
Two short-lived credentials are involved: an app JWT (signed with the
private key, freshly minted per app-scoped request, only used to
discover installations and mint tokens) and the installation access
tokens performing the actual API calls, which are valid for one hour
(fixed by GitHub) and never persisted by this extension. For explicit
cleanup – e.g. as the last step of an ephemeral CI run –
{py:func}`github.revoke_app_token <saltext.github.modules.github.revoke_app_token>`
revokes the current token immediately.
:::

:::{ghconf} app_id
:::
`app_id`
: App ID (or client ID) of a GitHub App to authenticate as, together
with exactly one of {ghconf}`private_key` or {ghconf}`private_key_path`
and a required {ghconf}`owner`. Installation access tokens are minted
(and refreshed before expiry) automatically, removing the need to
pass short-lived tokens into the profile (e.g. via
`actions/create-github-app-token` in CI).

The app's installation on {ghconf}`owner` is discovered via the API
(an organization, falling back to user accounts) unless pinned with
{ghconf}`installation_id`. The discovered ID is kept in the
persistent {ghconf}`cache`, when enabled.

Minted tokens carry all of the app's granted permissions on all
repositories the installation covers, unless restricted via
{ghconf}`permissions` (per token) or by installing the app on
selected repositories only (per installation).

:::{versionadded} 1.1.0
:::

:::{ghconf} private_key
:::
`private_key`
: Private key of the GitHub App ({ghconf}`app_id`) in PEM format.
Like all profile values, it can be an [SDB URI](#secret-management),
avoiding plain text keys in the configuration.

:::{versionadded} 1.1.0
:::

:::{ghconf} private_key_path
:::
`private_key_path`
: Path to a local file containing the private key of the GitHub App
({ghconf}`app_id`) in PEM format, as an alternative to
{ghconf}`private_key`.

:::{versionadded} 1.1.0
:::

:::{ghconf} installation_id
:::
`installation_id`
: ID of the GitHub App's ({ghconf}`app_id`) installation on
{ghconf}`owner`. Optional – when unset, it is discovered via the API.

:::{versionadded} 1.1.0
:::

:::{ghconf} permissions
:::
`permissions`
: Restrict the installation access tokens minted for this GitHub App
({ghconf}`app_id`) profile to a subset of the app's granted
permissions – a mapping of permission name to access level, e.g.
`contents: read`. Defaults to all granted permissions.

Allows profiles with different privileges to share a single app,
e.g. a read-only profile for CI validation runs (`test=True`)
next to an unrestricted one for applying:

```yaml
github_readonly:
app_id: 123456
private_key: sdb://osenv/GITHUB_APP_PRIVATE_KEY
owner: my_organization
permissions:
metadata: read
contents: read
administration: read
```

:::{versionadded} 1.1.0
:::

(github-resource-configuration)=
## Repository resource declaration

Expand Down
37 changes: 37 additions & 0 deletions src/saltext/github/modules/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,43 @@ def rate_limit(profile="github"):
return gh_client.rate_limit(_get_conn(profile))


def revoke_app_token(profile="github"):
"""
Revoke the GitHub App installation access token the profile
currently authenticates with and drop the associated cached
connections, ensuring a subsequent call mints a fresh token.
Only valid for GitHub App (:ghconf:`app_id`) profiles.

Minted tokens are valid for one hour (fixed by GitHub) and are
never persisted by this extension, so revocation is optional
hygiene – useful e.g. as the last step of an ephemeral CI run.

.. versionadded:: 1.1.0

CLI Example:

.. code-block:: bash

salt-call github.revoke_app_token

profile
Profile configuration name to use. Defaults to ``github``.
"""
conn = _get_conn(profile)
gh_client.revoke_installation_token(conn)
# The revoked token stays attached to the connection's auth
# (PyGithub only re-mints on expiry, not on auth failures) -
# evict all context entries sharing it so later invocations
# build a fresh connection.
for key in list(__context__):
if key == f"github.conn.{conn.ident}" or (
key.startswith("github.profile.")
and getattr(__context__[key], "ident", None) == conn.ident
):
__context__.pop(key)
return True


def account_info(name=None, profile="github"):
"""
Return the public profile of any user or organization account
Expand Down
Loading
Loading