Metadata-Version: 2.3
Name: brolly
Version: 0.3.1
Summary: A pure-Python CLI for AWS IAM Identity Center (SSO)
Keywords: aws,sso,iam-identity-center,cli,boto3,aws-profile
Author: Full Duplex Media
Author-email: Full Duplex Media <contact@fullduplex.media>
License: Apache-2.0
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Utilities
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
Requires-Dist: boto3
Requires-Dist: keyring>=25
Requires-Dist: keyring-pass>=0.9
Requires-Python: >=3.14
Project-URL: Homepage, https://github.com/fduplex/brolly
Project-URL: Repository, https://github.com/fduplex/brolly
Project-URL: Issues, https://github.com/fduplex/brolly/issues
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/fduplex/brolly/main/assets/brolly.svg" width="84" alt="">
</p>

<h1 align="center">brolly</h1>

<p align="center"><em>British informal for umbrella — one login covers every profile under an AWS SSO session.</em></p>

<p align="center">
  <a href="https://pypi.org/project/brolly/"><img src="https://img.shields.io/pypi/v/brolly" alt="PyPI version"></a>
  <a href="https://pypi.org/project/brolly/"><img src="https://img.shields.io/pypi/pyversions/brolly" alt="Python versions"></a>
  <a href="https://github.com/fduplex/brolly/actions/workflows/ci.yml"><img src="https://github.com/fduplex/brolly/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://github.com/fduplex/brolly/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue" alt="License"></a>
</p>

A small, pure-Python CLI for AWS IAM Identity Center (SSO). Authenticate once against an `[sso-session]` and every
profile under it is usable — brolly verifies and refreshes credentials cheaply, repoints a profile's account/role
in place, adds new profiles, and ships a freshness-aware prompt pill. It never touches `$AWS_PROFILE`.

```
brolly                                  verify/refresh the current profile (same as `brolly refresh`)
brolly login [-s <session>]             force a fresh device-code login for a session
brolly switch                           repoint the current profile's account/role
brolly refresh [<profile>] [-s <session>]
brolly add <profile> [-s <session>]
brolly ls [--no-check]                  list every sso-session and its profiles, with token status
brolly secure enable|disable [-s <session>]   opt-in: keep a session's token in your OS keychain
```

## Install

```console
$ uv tool install brolly      # recommended
$ pipx install brolly
$ pip install brolly
```

Needs **Python 3.14+** and the **AWS CLI v2** on your `$PATH` — brolly shells out to `aws sso login` and
`aws configure set`; everything else goes through boto3. The AWS CLI is *not* a pip dependency
([install it separately](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). The
arrow-key picker needs a POSIX TTY (Linux, macOS, WSL) and falls back to numeric selection without one.
[Secure mode](#secure-mode-os-keychain) is built in — nothing extra to install.

## Mental model

`~/.aws/config` has two layers:

- **`[sso-session <name>]`** — holds `sso_start_url` / `sso_region`. Logging in caches **one** token per session,
  keyed by SHA1 of the session name (`~/.aws/sso/cache/<sha1>.json`).
- **`[profile <name>]`** — references a session via `sso_session` and adds its own `sso_account_id` /
  `sso_role_name`. Any number of profiles can point at the same session.

Because the token is cached per-session, **every profile under one session shares a single login** — which is
where the name comes from.

`$AWS_PROFILE` is a fixed handle you set yourself. **brolly never touches it**: `switch` changes what a profile
resolves to, `add` creates new profiles, and `refresh` targets the `aws` CLI with `--profile` rather than mutating
your environment. No shell wrapper, nothing rewriting your env behind your back.

## Commands

### `brolly`

Shorthand for `brolly refresh` on the current `$AWS_PROFILE` — cheap, no browser unless the session is dead.

```console
$ brolly
✔  corp-dev live → arn:aws:sts::111111111111:assumed-role/AdministratorAccess/alex
```

### `brolly login [-s <session>]`

Forces a fresh device-code login for a session, unconditionally — session-scoped, not profile-scoped. Rarely
needed: `refresh` already logs in whenever a session is actually dead.

### `brolly switch`

Interactively repoints the current `$AWS_PROFILE` to a different account/role under its own session. Arrow-key
picker (`↑`/`↓` or `j`/`k`, `enter` to select, `q` to quit); accounts first, then roles (skipped when there's only
one). Rewrites `sso_account_id`, `sso_role_name`, and `sso_account_name` in place — recording the account name is
what lets the prompt show something friendlier than a raw ID.

<p align="center">
  <img src="https://raw.githubusercontent.com/fduplex/brolly/main/assets/switch.svg" width="720" alt="brolly switch: an arrow-key picker choosing an account, then a role, then the confirmation line">
</p>

The circle marks where the profile points now, the highlighted row is the cursor. `$AWS_PROFILE` is untouched —
only what it resolves to changed, which the next prompt shows.

### `brolly refresh [<profile>] [-s <session>]`

The cheap, no-browser daily driver. `-s` asserts which session you're operating in; the target profile must
actually belong to it, so crossing sessions is always deliberate:

```console
$ AWS_PROFILE=corp-dev brolly refresh corp-prod
✔  corp-prod live → arn:aws:sts::222222222222:assumed-role/AdministratorAccess/alex

$ AWS_PROFILE=customer-admin brolly refresh corp-prod
profile 'corp-prod' is under session 'corp', not 'customer' — use -s corp to target it
```

Under the hood it runs `aws sts get-caller-identity --profile <target>`, which forces credential resolution and
lets botocore refresh the hourly token — but only when that token is lapsed or near expiry, so a healthy one
isn't reset. If the 7-day session is dead it falls through to a device-code login and retries. It also backfills
a missing `sso_account_name` (one `list_accounts` call, made only when absent).

### `brolly add <profile> [-s <session>]`

Creates a new profile under an existing session, walks the same picker, and leaves it authenticated. Refuses if
the profile already exists (use `switch`) or the session is unknown, and copies `region`/`output` from a sibling
profile when there is one.

```console
$ brolly add corp-qa                     # session inferred from $AWS_PROFILE
$ brolly add customer-admin -s customer  # explicit session
```

It does **not** change `$AWS_PROFILE`. If you Ctrl-C out of the picker the profile skeleton is already written, so
finish it with `export AWS_PROFILE=<name>` then `brolly switch` rather than re-running `add`.

### `brolly ls [--no-check]`

Lists every `sso-session` and its profiles as one aligned table, with live/idle/gone token status, expiry, and a
footer naming what `$AWS_PROFILE` currently resolves to — `ls -l` for brolly, where `ps1` is the glance. By default
it silently probes each session over the network (an SSO refresh-token grant, never an interactive login) to tell a
truly-dead session apart from a merely-lapsed token; `--no-check` skips that and reads local expiry files only.

<p align="center">
  <img src="https://raw.githubusercontent.com/fduplex/brolly/main/assets/ls-table.svg" width="900" alt="brolly ls output: two sso-sessions with their profiles, token status, accounts, roles and regions">
</p>

The current profile is the orange one. `secure` marks which profiles keep their token in the OS keychain, and the
whole table needs a **[Nerd Font](https://www.nerdfonts.com/)** for its glyphs, same as the prompt pill.

### Common tasks

| Situation | Command |
|---|---|
| Verify/refresh current creds | `brolly` |
| Force a fresh login | `brolly login` |
| Wrong account or role for the current profile | `brolly switch` |
| New profile under the current session | `brolly add <name>` then `export AWS_PROFILE=<name>` |
| New profile under a different session | `brolly add <name> -s customer` |
| Refresh a profile in another session | `brolly refresh <profile> -s <session>` |
| Interrupted a `brolly add` mid-picker | `export AWS_PROFILE=<name>` then `brolly switch` |
| See every session/profile & token status | `brolly ls` (add `--no-check` to skip the network probe) |
| Keep a session's token out of plaintext | `brolly secure enable -s <session>` |

## Shell prompt integration

`brolly ps1` renders a colored `session/profile · account` pill reflecting the **local, filesystem-only** state of
the session token — no network call, no keychain access, no boto3 import:

- **live** (amber) — token still valid.
- **idle** (grey, clock glyph) — cached but lapsed; refreshes automatically on next use.
- **gone** (red, cross glyph) — no cached token; run `brolly`.
- **plain** (neutral grey) — not an SSO profile.

<p align="center">
  <img src="https://raw.githubusercontent.com/fduplex/brolly/main/assets/prompt-states.svg" width="800" alt="brolly ps1 prompt pill shown in its live, idle, gone, and plain states">
</p>

Add it to your `PS1`. It needs a **[Nerd Font](https://www.nerdfonts.com/)** for the separators and glyphs:

```bash
export PS1='$(brolly ps1)\u@\h:\w\$ '
```

It reads whichever store the profile actually uses — the expiry sidecar for secure-mode profiles, the stock cache
otherwise — so it stays accurate with no configuration. A dead 7-day session can't be detected locally, so it
reads as `idle` rather than `gone`. Cost is ~10ms per prompt.

## Secure mode (OS keychain)

By default brolly is a thin layer over the stock plaintext `~/.aws/sso/cache` — the same cache the `aws` CLI uses.
**Secure mode** is an opt-in that moves the SSO token into your OS keychain and registers brolly as each profile's
`credential_process`, so every SDK and the `aws` CLI keep working with nothing but `$AWS_PROFILE` — no shell
wrapper, no plaintext token. It's built in; [`keyring`](https://github.com/jaraco/keyring) ships as a dependency.

### `brolly secure enable [-s <session>]`

Logs the session in (a device-code login brolly runs itself), rewrites every profile under it to use
`credential_process`, and deletes the now-redundant plaintext token. Idempotent — re-run it after `brolly add` to
pull new profiles into secure mode.

```console
$ brolly secure enable -s corp
→ keychain backend: pass (gpg-agent)
✓ authorized — SSO token stored in your OS keychain
✓ removed plaintext token cache for session 'corp'
✓ secure mode on for session 'corp' — 3 profile(s) now use the OS keychain
```

Nothing else about your workflow changes: `export AWS_PROFILE=corp-prod` and every SDK resolves credentials
through brolly. `refresh` and `switch` keep working and stay in secure mode.

### `brolly secure login` / `brolly secure disable`

`login` re-authorizes a session whose 7-day window has fully lapsed — rarely needed, since refresh is silent.
`disable` reverts every secured profile to a stock plaintext-cache profile and deletes the keychain token: a
clean, complete undo.

### How it works

- **The token** (with its refresh token) lives in the keychain under service `brolly-sso`, keyed the way botocore
  keys its own cache. brolly plugs a keychain-backed cache into botocore's token provider, so **silent hourly
  refresh still happens** — no reimplementation, just a different vault.
- **A secured profile** keeps `sso_session` but moves `sso_account_id` / `sso_role_name` under `brolly_sso_*` and
  adds `credential_process`. That combination deactivates botocore's built-in SSO credential provider so
  resolution flows through brolly — otherwise botocore would find the now-absent plaintext token and fail.
- **The prompt pill** reads a small non-secret expiry sidecar (`~/.aws/brolly/<sha1>.json`) rather than the
  keychain, so it stays a cheap filesystem check.
- **No environment variable to keep exported.** The chosen backend is saved to `~/.aws/brolly/config.json` and
  re-selected on every call, so resolution works from any venv, cron job, or IDE. (It does run the `brolly`
  command, so keep brolly on your `PATH`.)

### Choosing a backend

`credential-process` is spawned fresh and non-interactively on every cold credential resolution, so the backend
has to be one that stays **unlocked for your session** — macOS Keychain and gnome-keyring / KWallet already work
that way. If `keyring` can't find a usable backend, brolly says so and stops rather than failing obscurely.

`secure enable` **auto-detects**: a live OS keychain if there is one, otherwise `pass` when its store is
initialized. Whatever it picks is saved and reused, so you normally never name a backend; `--backend
<dotted.path>` overrides. Backends must live in **brolly's own environment**, since `credential_process` runs the
`brolly` executable — `keyring_pass` is bundled, and others install alongside brolly with
`uv tool install brolly --with <pkg>` (or `pipx inject brolly <pkg>`).

**Linux without a desktop: use `pass` + gpg-agent.** `pass` stores each secret gpg-encrypted and gpg-agent is the
session daemon that keeps your key unlocked, so reads are silent once it's warm — and encrypting a *write* needs
no passphrase, so token refreshes never prompt.

```console
$ sudo apt install pass            # the pass CLI itself
$ pass init <your-gpg-key-id>      # initialize the store
$ brolly secure enable -s corp     # auto-detects pass
```

gpg-agent must already be warm when `credential-process` runs, since it has no TTY to prompt on. Either raise
`max-cache-ttl` in `~/.gnupg/gpg-agent.conf` and unlock once per login, or preset the passphrase with
`gpg-preset-passphrase` for zero-touch warming — the pattern used by
[borg-backup](https://github.com/thevinchi/borg-backup).

**Other vaults.** Anything with a `keyring` backend works via `--backend` — for example `onepassword-keyring` with
a 1Password service-account token. `keyrings.alt`'s `EncryptedKeyring` is a last resort: with no daemon to keep it
unlocked it prompts in *every* new process, meaning a prompt on roughly every `aws` call and a hard failure
anywhere non-interactive.

## How it compares

No single existing tool combines what brolly does. The niche is the *combination*: native to the `sso-session`
block, in-place `switch`/`add`, never mutating `$AWS_PROFILE`, a shipped prompt pill, and pure-Python/pip.

- **[aws-sso-util](https://github.com/benkehoe/aws-sso-util)** is the closest sibling — pure-Python,
  config-native, non-invasive — but has no prompt integration, no in-place single-profile repoint, and predates
  the `sso-session` block.
- **[granted](https://granted.dev/)** and **[aws-sso-cli](https://github.com/synfinatic/aws-sso-cli)** are
  excellent, but they're Go binaries that install a shell wrapper and generate bulk static profiles.
- **[awsume](https://awsu.me/)** targets classic IAM role assumption, not Identity Center.

Positioning, not disparagement: brolly's plaintext default is deliberately thin, and keychain storage is an
opt-in rather than the always-on model of aws-vault, granted, or aws-sso-cli.

## Design notes & caveats

- **`switch` rewrites `~/.aws/config` globally.** A profile lives in one shared file, so repointing a profile
  name silently retargets *any other shell* pinned to that **same name** on its next command. Safe use = one
  distinct profile name per concurrent context. This is the one real footgun.
- **Tokens sit in the stock plaintext cache by default** — thin by design. When you want them off disk, opt into
  [secure mode](#secure-mode-os-keychain).

## Roadmap

- **Windows support.** The picker uses `termios`/`tty` (POSIX only); an `msvcrt`-based key reader would let the
  menus run natively on Windows.
- **Passphrase-backend ergonomics** — smoother first-run setup for the encrypted-file keyring backend.

## License

[Apache-2.0](LICENSE) © 2026 Full Duplex Media
