Skip to content

Commit bdc5542

Browse files
allow for using contents or path for private key (#5)
1 parent ea25b13 commit bdc5542

File tree

3 files changed

+78
-23
lines changed

3 files changed

+78
-23
lines changed

README.md

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ The library is async-only at the moment (following gidgethub), with sync support
9494
> from pathlib import Path
9595
>
9696
> GITHUB_APP = {
97-
> "PRIVATE_KEY": Path(env.path("GITHUB_PRIVATE_KEY_PATH")).read_text(),
97+
> "PRIVATE_KEY": env.path("GITHUB_PRIVATE_KEY_PATH"),
9898
> }
9999
> ```
100100
@@ -312,45 +312,44 @@ The GitHub App's name as registered on GitHub.
312312
313313
### `PRIVATE_KEY`
314314
315-
> ❗ **Required** | `str`
315+
> 🔴 **Required** | `str`
316+
317+
The GitHub App's private key for authentication. Can be provided as either:
316318
317-
The contents of the GitHub App's private key for authentication. Can be provided as:
319+
- Raw key contents (e.g., from environment variable)
320+
- Path to key file (as string or Path object)
318321
319-
You can provide the key contents directly:
322+
The library will automatically detect and read the key file if a path is provided.
320323
321324
```python
325+
from pathlib import Path
322326
from environs import Env
323327
324328
env = Env()
325329
326-
# Direct key contents from environment variable
330+
# Key contents from environment
327331
GITHUB_APP = {
328332
"PRIVATE_KEY": env.str("GITHUB_PRIVATE_KEY"),
329333
}
330-
```
331-
332-
Or read it from a file:
333-
334-
```python
335-
from pathlib import Path
336334
337-
from environs import Env
338-
339-
# Using pathlib.Path and a local path directly
335+
# Path to local key file (as string)
340336
GITHUB_APP = {
341-
"PRIVATE_KEY": Path("path/to/private-key.pem").read_text(),
337+
"PRIVATE_KEY": "/path/to/private-key.pem",
342338
}
343339
340+
# Path to local key file (as Path object)
341+
GITHUB_APP = {
342+
"PRIVATE_KEY": Path("path/to/private-key.pem"),
343+
}
344344
345-
env = Env()
346-
347-
# Or with environs for environment-based path
345+
# Path from environment
348346
GITHUB_APP = {
349-
"PRIVATE_KEY": Path(env.path("GITHUB_PRIVATE_KEY_PATH")).read_text(),
347+
"PRIVATE_KEY": env.path("GITHUB_PRIVATE_KEY_PATH"),
350348
}
351349
```
352350
353-
Note that the private key should be kept secure and never committed to version control. Using environment variables or secure file storage is recommended.
351+
> [!NOTE]
352+
> The private key should be kept secure and never committed to version control. Using environment variables or secure file storage is recommended.
354353
355354
### `WEBHOOK_SECRET`
356355

src/django_github_app/conf.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from pathlib import Path
45
from typing import Any
56

67
from django.conf import settings
@@ -24,7 +25,29 @@ class AppSettings:
2425
@override
2526
def __getattribute__(self, __name: str) -> Any:
2627
user_settings = getattr(settings, GITHUB_APP_SETTINGS_NAME, {})
27-
return user_settings.get(__name, super().__getattribute__(__name))
28+
value = user_settings.get(__name, super().__getattribute__(__name))
29+
30+
match __name:
31+
case "PRIVATE_KEY":
32+
return self._parse_private_key(value)
33+
case _:
34+
return value
35+
36+
def _parse_private_key(self, value: Any) -> str:
37+
if not value:
38+
return ""
39+
40+
if not isinstance(value, (str, Path)):
41+
return str(value)
42+
43+
if isinstance(value, str) and value.startswith("-----BEGIN"):
44+
return value
45+
46+
path = value if isinstance(value, Path) else Path(value)
47+
if path.is_file():
48+
return path.read_text()
49+
50+
return str(value)
2851

2952
@property
3053
def SLUG(self):

tests/test_conf.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
35
import pytest
46
from django.conf import settings
57

@@ -26,6 +28,37 @@ def test_default_settings(setting, default_setting):
2628
assert getattr(app_settings, setting) == default_setting
2729

2830

31+
@pytest.mark.parametrize(
32+
"private_key,expected",
33+
[
34+
(
35+
"-----BEGIN RSA PRIVATE KEY-----\nkey content\n-----END RSA PRIVATE KEY-----",
36+
"-----BEGIN RSA PRIVATE KEY-----\nkey content\n-----END RSA PRIVATE KEY-----",
37+
),
38+
("/path/that/does/not/exist.pem", "/path/that/does/not/exist.pem"),
39+
(Path("/path/that/does/not/exist.pem"), "/path/that/does/not/exist.pem"),
40+
("", ""),
41+
("/path/with/BEGIN/in/it/key.pem", "/path/with/BEGIN/in/it/key.pem"),
42+
("////", "////"),
43+
(123, "123"),
44+
(None, ""),
45+
],
46+
)
47+
def test_private_key_handling(private_key, expected, override_app_settings):
48+
with override_app_settings(PRIVATE_KEY=private_key):
49+
assert app_settings.PRIVATE_KEY == expected
50+
51+
52+
def test_private_key_from_file(tmp_path, override_app_settings):
53+
key_content = "-----BEGIN RSA PRIVATE KEY-----\ntest key content\n-----END RSA PRIVATE KEY-----"
54+
key_file = tmp_path / "test_key.pem"
55+
key_file.write_text(key_content)
56+
57+
for key_path in (str(key_file), key_file):
58+
with override_app_settings(PRIVATE_KEY=key_path):
59+
assert app_settings.PRIVATE_KEY == key_content
60+
61+
2962
@pytest.mark.parametrize(
3063
"name,expected",
3164
[
@@ -40,8 +73,8 @@ def test_default_settings(setting, default_setting):
4073
("special-&*()-chars", "special-chars"),
4174
("emoji🚀app", "emojiapp"),
4275
("@user/multiple/slashes/app", "usermultipleslashesapp"),
43-
("", ""), # Empty string case
44-
(" ", ""), # Whitespace only case
76+
("", ""),
77+
(" ", ""),
4578
("app-name_123", "app-name_123"),
4679
("v1.0.0-beta", "v100-beta"),
4780
],

0 commit comments

Comments
 (0)