Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aliases #25

Merged
merged 13 commits into from
Dec 14, 2023
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ jobs:
run: poetry install
- name: Lint
run: |
poetry run ruff keycmd
poetry run black --check keycmd
EXIT_STATUS=0
poetry run ruff check keycmd || EXIT_STATUS=$?
poetry run ruff format --check keycmd || EXIT_STATUS=$?
exit $EXIT_STATUS

test:
name: Test
Expand Down
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ optional arguments:

There are two main ways to use the CLI:

* `keycmd ...`
* `keycmd 'your command'`
* `keycmd --shell`

The first is the most preferred method, since your secrets will only be exposed as environment variables during a one-off command. The latter is less preferable, but can be convenient if you are debugging some process that depends on the credentials you are exposing.
Expand All @@ -171,21 +171,58 @@ Configuration files are loaded and merged in the listed order.

### Options

The options are a nested dictionary, defined as follows:
The options schema is defined as follows:

* `keys`: dict
* `{key_name}`: dict
* `credential`: str
* `username`: str
* `b64`: bool, optional
* `format`: str, optional
* `aliases`: dict
* `{alias_name}`: dict
* `key`: str
* `b64`: bool, optional
* `format`: str, optional

You can define as many keys as you like. For each key, you are required to define:

* the `key_name`, which is the name of the environment variable under which the credential will be exposed
* the `credential`, which is the name of the credential in your OS keyring
* the `username`, which is the name of the user owning the credential in the OS keyring

Optionally, you can also set `b64` to `true` to apply base64 encoding to the credential.
Optional fields:

* `b64`, set this to `true` to apply base64 encoding to the password
* `format`, apply a format string (before optionally applying base64 encoding)
* you have access to `credential`, `username` and `password`
* so for example you can put together a basic auth header like this: `"{username}:{password}"`

The aliases make it possible to expose the same credentials in another way, the fields work the same.

### Example

This configuration exposes a specific credential both plainly under the environment variable `MY_TOKEN`, and again with base64 encoding applied as `MY_TOKEN_B64`:

```toml
[keys]
MY_TOKEN = { credential = "MY_TOKEN", username = "azure" }

[aliases]
MY_TOKEN_B64 = { key = "MY_TOKEN", b64 = true }
```

### pyproject.toml example

You can also store your configuration in `pyproject.toml`, by prefixing the keys with `tool.keycmd`. So if we were to convert the previous example it would look like this:

```toml
[tool.keycmd.keys]
MY_TOKEN = { credential = "MY_TOKEN", username = "azure" }

[tool.keycmd.aliases]
MY_TOKEN_B64 = { key = "MY_TOKEN", b64 = true }
```

## OpenAI example

Expand Down Expand Up @@ -286,7 +323,7 @@ always-auth=true
//pkgs.dev.azure.com/my_organization/_packaging/main/npm/:email=email
```

Now, I can set up my `node_modules` just by calling `keycmd npm install`! 🚀
Now, I can set up my `node_modules` just by calling `keycmd 'npm install'`! 🚀

> **Note**
> npm will complain if you make any calls such as `npm run [...]` without the environment variable set. 🙄 You can set them to the empty string to make npm shut up. I use `export PAT_B64=` (or `setx PAT_B64=` on Windows).
Expand All @@ -301,13 +338,15 @@ secrets:
environment: PAT_B64
```

When I call `keycmd docker compose build` these two variables are exposed by keycmd and subsequently they are available as [docker compose build secrets](https://docs.docker.com/compose/use-secrets/). 👌
When I call `keycmd 'docker compose build'` these two variables are exposed by keycmd and subsequently they are available as [docker compose build secrets](https://docs.docker.com/compose/use-secrets/). 👌

## Debugging configuration

If you're not getting the results you expected, use the `-v` flag
to debug your configuration. Keycmd will verbosely tell you about all the steps it's taking.

Here's an example using cmd.exe, otherwise, the command would be `poetry run keycmd -v 'echo $ARTIFACTS_TOKEN_B64'`:

```
❯ poetry run keycmd -v echo %ARTIFACTS_TOKEN_B64%
keycmd: loading config file C:\Users\kvang\.keycmd
Expand Down
2 changes: 1 addition & 1 deletion keycmd/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.6.0"
__version__ = "0.7.0"
1 change: 0 additions & 1 deletion keycmd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from .logs import error, log, set_verbose
from .shell import run_cmd, run_shell


cli = argparse.ArgumentParser(
prog="keycmd",
)
Expand Down
1 change: 0 additions & 1 deletion keycmd/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from .logs import vlog


# exposed for testing
USERPROFILE = "~"

Expand Down
45 changes: 41 additions & 4 deletions keycmd/creds.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,23 @@ def b64(value):
return base64.b64encode(value.encode("utf-8")).decode("utf-8")


def expose(env, key, credential, username, password, apply_b64, format_string):
if format_string:
password = format_string.format(
credential=credential,
username=username,
password=password,
)
if apply_b64:
password = b64(password)
env[key] = password


def get_env(conf):
"""Load credentials from the OS keyring according to user configuration"""
env = environ.copy()

key_data = {}
for key, src in conf["keys"].items():
password = keyring.get_password(src["credential"], src["username"])
if password is None:
Expand All @@ -22,12 +36,35 @@ def get_env(conf):
f" with user {src['username']}"
f" as it does not exist"
)
if src.get("b64"):
password = b64(password)
env[key] = password
apply_b64 = src.get("b64", False)
format_string = src.get("format")
key_data[key] = (
src["credential"],
src["username"],
password,
apply_b64,
format_string,
)
expose(env, key, *key_data[key])
vlog(
f"exposing credential {src['credential']}"
f" with user {src['username']}"
f" as environment variable {key} (b64: {src.get('b64', False)})"
f" as environment variable {key}"
f" (b64: {apply_b64}, format: {format_string})"
)

for alias, src in conf.get("aliases", {}).items():
key = key_data.get(src["key"])
if key is None:
error(f"MISSING alias key {src['key']}")
# re-use base data but replace apply_b64 and format_string
credential, username, password, _, _ = key
apply_b64 = src.get("b64", False)
Korijn marked this conversation as resolved.
Show resolved Hide resolved
format_string = src.get("format")
expose(env, alias, credential, username, password, apply_b64, format_string)
vlog(
f"aliasing {src['key']}"
f" as environment variable {alias}"
f" (b64: {apply_b64}, format: {format_string})"
)
return env
1 change: 0 additions & 1 deletion keycmd/logs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import sys


_verbose = False


Expand Down
7 changes: 4 additions & 3 deletions keycmd/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
from subprocess import run
from sys import exit

from shellingham import detect_shell, ShellDetectionFailure
from shellingham import ShellDetectionFailure, detect_shell

from .logs import vlog, vwarn


USE_SUBPROCESS = False # exposed for testing
IS_WINDOWS = os.name == "nt"
IS_POSIX = os.name == "posix"
Expand Down Expand Up @@ -55,9 +54,11 @@ def run_shell(env=None):
def run_cmd(cmd, env=None):
"""Run a one-off command in a shell."""
shell_name, shell_path = get_shell()
opt = "-c"
if shell_name == "cmd":
opt = "/C"
else:
opt = "-c"
cmd = [" ".join(cmd)]
full_command = [shell_path, opt, *cmd]
vlog(f"running command: {pformat(full_command)}")
exec(full_command, env)
Loading
Loading