Skip to content

Commit

Permalink
Autocreate the auth store when missing
Browse files Browse the repository at this point in the history
In order to ease managing of auth entries and give some examples to end
users, instead of asking them to create the auth store themselves, we
better create one automatically.
  • Loading branch information
ikalnytskyi committed Jun 19, 2024
1 parent 330ee49 commit f888a84
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 21 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ configuration directory. On macOS and Linux, it tries the following locations:

> [!NOTE]
>
> The authentication store is not created automatically; it is the user's
> responsibility to create one.
> The authentication store can be automatically created with few examples
> inside on first plugin activation, e.g. `http -A store https://pie.dev`.
The authentication store is a JSON file that contains two sections: `bindings`
and `secrets`:
Expand Down
6 changes: 3 additions & 3 deletions src/httpie_auth_store/_auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os
import pathlib
import typing as t

import httpie.cli.argtypes
Expand All @@ -23,8 +23,8 @@ def __init__(self, binding_id: t.Optional[str] = None) -> None:
self._binding_id = binding_id

def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
auth_store_dir = httpie.config.DEFAULT_CONFIG_DIR
auth_store = AuthStore.from_filename(os.path.join(auth_store_dir, self.AUTH_STORE_FILENAME))
auth_store_dir = pathlib.Path(httpie.config.DEFAULT_CONFIG_DIR)
auth_store = AuthStore.from_filename(auth_store_dir / self.AUTH_STORE_FILENAME)

# The credentials store plugin provides extended authentication
# capabilities, and therefore requires registering extra HTTPie
Expand Down
34 changes: 26 additions & 8 deletions src/httpie_auth_store/_store.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import collections.abc
import dataclasses
import json
import os
import pathlib
import stat
import string
Expand Down Expand Up @@ -101,24 +100,44 @@ def __contains__(self, key: object) -> bool:
class AuthStore:
"""Authentication store."""

DEFAULT_AUTH_STORE: t.Mapping[str, t.Any] = {
"bindings": [
{
"auth_type": "basic",
"auth": "$PIE_USERNAME:$PIE_PASSWORD",
"resources": ["https://pie.dev/basic-auth/batman/I@mTheN1ght"],
},
{
"auth_type": "bearer",
"auth": "$PIE_TOKEN",
"resources": ["https://pie.dev/bearer"],
},
],
"secrets": {
"PIE_USERNAME": "batman",
"PIE_PASSWORD": "I@mTheN1ght",
"PIE_TOKEN": "000000000000000000000000deadc0de",
},
}

def __init__(self, bindings: t.List[Binding], secrets: Secrets):
self._bindings = bindings
self._secrets = secrets

@classmethod
def from_filename(cls, filename: t.Union[str, pathlib.Path]) -> "AuthStore":
def from_filename(cls, filename: pathlib.Path) -> "AuthStore":
"""Construct an instance from given JSON file."""

if not os.path.exists(filename):
error_message = f"Authentication store is not found: '{filename}'."
raise FileNotFoundError(error_message)
if not filename.exists():
filename.write_text(json.dumps(cls.DEFAULT_AUTH_STORE, indent=2))
filename.chmod(0o600)

# Since an authentication store may contain unencrypted secrets, I
# decided to follow the same practice SSH does and do not work if the
# file can be read by anyone but current user. Windows is ignored
# because I haven't figured out yet how to deal with permissions there.
if sys.platform != "win32":
mode = stat.S_IMODE(os.stat(filename).st_mode)
mode = stat.S_IMODE(filename.stat().st_mode)

if mode & 0o077 > 0o000:
error_message = (
Expand All @@ -134,8 +153,7 @@ def from_filename(cls, filename: t.Union[str, pathlib.Path]) -> "AuthStore":
)
raise PermissionError(error_message)

with open(filename, encoding="UTF-8") as f:
return cls.from_mapping(json.load(f))
return cls.from_mapping(json.loads(filename.read_text(encoding="UTF-8")))

@classmethod
def from_mapping(cls, mapping: t.Mapping[str, t.Any]) -> "AuthStore":
Expand Down
23 changes: 15 additions & 8 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import responses

from httpie_auth_store._auth import StoreAuth
from httpie_auth_store._store import AuthStore


_is_windows = sys.platform == "win32"
Expand Down Expand Up @@ -1217,16 +1218,22 @@ def test_store_permissions_not_enough(


@responses.activate
def test_store_auth_no_database(
def test_store_autocreation_when_missing(
httpie_run: HttpieRunT,
auth_store_path: pathlib.Path,
httpie_stderr: io.StringIO,
) -> None:
"""The plugin raises error if auth store does not exist."""
"""The auth store is created when missing with some examples."""

httpie_run(["-A", "store", "https://yoda.ua"])
httpie_run(["-A", "store", "https://pie.dev/basic-auth/batman/I@mTheN1ght"])
httpie_run(["-A", "store", "https://pie.dev/bearer"])

assert len(responses.calls) == 0
assert httpie_stderr.getvalue().strip() == (
f"http: error: FileNotFoundError: Authentication store is not found: '{auth_store_path}'."
)
assert auth_store_path.exists()
assert json.loads(auth_store_path.read_text()) == AuthStore.DEFAULT_AUTH_STORE

request = responses.calls[0].request
assert request.url == "https://pie.dev/basic-auth/batman/I@mTheN1ght"
assert request.headers["Authorization"] == b"Basic YmF0bWFuOklAbVRoZU4xZ2h0"

request = responses.calls[1].request
assert request.url == "https://pie.dev/bearer"
assert request.headers["Authorization"] == "Bearer 000000000000000000000000deadc0de"

0 comments on commit f888a84

Please sign in to comment.