Skip to content

Commit 1b76eec

Browse files
committed
Autocreate the auth store when missing
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.
1 parent 12a5ddc commit 1b76eec

File tree

4 files changed

+46
-21
lines changed

4 files changed

+46
-21
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ configuration directory. On macOS and Linux, it tries the following locations:
5959

6060
> [!NOTE]
6161
>
62-
> The authentication store is not created automatically; it is the user's
63-
> responsibility to create one.
62+
> The authentication store can be automatically created with few examples
63+
> inside on first plugin activation, e.g. `http -A store https://pie.dev`.
6464
6565
The authentication store is a JSON file that contains two sections: `bindings`
6666
and `secrets`:

src/httpie_auth_store/_auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os
1+
import pathlib
22
import typing as t
33

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

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

2929
# The credentials store plugin provides extended authentication
3030
# capabilities, and therefore requires registering extra HTTPie

src/httpie_auth_store/_store.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import collections.abc
22
import dataclasses
33
import json
4-
import os
54
import pathlib
65
import stat
76
import string
@@ -101,24 +100,44 @@ def __contains__(self, key: object) -> bool:
101100
class AuthStore:
102101
"""Authentication store."""
103102

103+
DEFAULT_AUTH_STORE: t.Mapping[str, t.Any] = {
104+
"bindings": [
105+
{
106+
"auth_type": "basic",
107+
"auth": "$PIE_USERNAME:$PIE_PASSWORD",
108+
"resources": ["https://pie.dev/basic-auth/batman/I@mTheN1ght"],
109+
},
110+
{
111+
"auth_type": "bearer",
112+
"auth": "$PIE_TOKEN",
113+
"resources": ["https://pie.dev/bearer"],
114+
},
115+
],
116+
"secrets": {
117+
"PIE_USERNAME": "batman",
118+
"PIE_PASSWORD": "I@mTheN1ght",
119+
"PIE_TOKEN": "000000000000000000000000deadc0de",
120+
},
121+
}
122+
104123
def __init__(self, bindings: t.List[Binding], secrets: Secrets):
105124
self._bindings = bindings
106125
self._secrets = secrets
107126

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

112-
if not os.path.exists(filename):
113-
error_message = f"Authentication store is not found: '{filename}'."
114-
raise FileNotFoundError(error_message)
131+
if not filename.exists():
132+
filename.write_text(json.dumps(cls.DEFAULT_AUTH_STORE, indent=2))
133+
filename.chmod(0o600)
115134

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

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

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

140158
@classmethod
141159
def from_mapping(cls, mapping: t.Mapping[str, t.Any]) -> "AuthStore":

tests/test_plugin.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import responses
1414

1515
from httpie_auth_store._auth import StoreAuth
16+
from httpie_auth_store._store import AuthStore
1617

1718

1819
_is_windows = sys.platform == "win32"
@@ -1217,16 +1218,22 @@ def test_store_permissions_not_enough(
12171218

12181219

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

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

1229-
assert len(responses.calls) == 0
1230-
assert httpie_stderr.getvalue().strip() == (
1231-
f"http: error: FileNotFoundError: Authentication store is not found: '{auth_store_path}'."
1232-
)
1230+
assert auth_store_path.exists()
1231+
assert json.loads(auth_store_path.read_text()) == AuthStore.DEFAULT_AUTH_STORE
1232+
1233+
request = responses.calls[0].request
1234+
assert request.url == "https://pie.dev/basic-auth/batman/I@mTheN1ght"
1235+
assert request.headers["Authorization"] == b"Basic YmF0bWFuOklAbVRoZU4xZ2h0"
1236+
1237+
request = responses.calls[1].request
1238+
assert request.url == "https://pie.dev/bearer"
1239+
assert request.headers["Authorization"] == "Bearer 000000000000000000000000deadc0de"

0 commit comments

Comments
 (0)