Skip to content

Commit cfeaa47

Browse files
authored
Merge pull request #67 from anomaly/alpha-7
Alpha 7
2 parents c6f3c80 + 3579e58 commit cfeaa47

File tree

15 files changed

+1051
-812
lines changed

15 files changed

+1051
-812
lines changed

.github/workflows/run-tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ jobs:
2626
poetry install
2727
- name: Run tests
2828
run: |
29-
export GACC_API_KEY=${{ secrets.GACC_API_KEY }}
29+
export GACC_API_KEY="${{ secrets.GACC_API_KEY }}"
30+
export CERTIFICATE_ANOMALY="${{ secrets.CERTIFICATE_ANOMALY }}"
31+
export PRIVATE_KEY_ANOMALY="${{ secrets.PRIVATE_KEY_ANOMALY }}"
3032
task test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.envrc
66

77
# Byte-compiled / optimized / DLL files
8+
.certs
89
.secret
910
.DS_Store
1011
__pycache__/

README.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,9 @@ import os
4040
import asyncio
4141

4242
# Import the client and models
43-
from gallagher import (
44-
cc,
45-
)
46-
from gallagher.dto.summary import (
47-
CardholderSummary,
48-
)
49-
from gallagher.cc.cardholders import (
50-
Cardholder,
51-
)
43+
from gallagher import cc
44+
from gallagher.dto.summary import CardholderSummary
45+
from gallagher.cc.cardholders import Cardholder
5246

5347
# Set the API key from the environment
5448
api_key = os.environ.get("GACC_API_KEY")
@@ -287,8 +281,6 @@ class VisitorTypeDetail(
287281
AppBaseModel,
288282
IdentityMixin
289283
):
290-
"""
291-
"""
292284
access_group : AccessGroupRef
293285
host_access_groups: list[AccessGroupSummary]
294286
visitor_access_groups: list[AccessGroupSummary]
@@ -317,8 +309,6 @@ class VisitorTypeDetail(
317309
AppBaseModel,
318310
IdentityMixin
319311
):
320-
"""
321-
"""
322312
access_group : AccessGroupRef
323313
host_access_groups: list[AccessGroupSummary]
324314
visitor_access_groups: list[AccessGroupSummary]

Taskfile.yml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
version: '3'
1+
version: "3"
22

3-
dotenv: ['.env']
3+
dotenv: [".env"]
44

55
tasks:
66
publish:tag:
77
vars:
8-
PROJ_VERSION:
8+
PROJ_VERSION:
99
sh: poetry version | awk '{print $2}'
1010
prompt: "Before we build, is the version {{.PROJ_VERSION}} number up to date?"
1111
desc: tags the current commit
@@ -19,24 +19,21 @@ tasks:
1919
- poetry build
2020
test:
2121
desc: runs tests inside the virtualenv
22-
summary:
23-
runs all the tests inside the virtualenv, optionally
22+
summary: runs all the tests inside the virtualenv, optionally
2423
provide the name of the test as an argument to run a single test
2524

2625
this does not run coverage or provide tap output
2726
cmds:
2827
- poetry run coverage run -m pytest -s tests/{{.CLI_ARGS}}
2928
test:tap:
3029
desc: runs tests with tap output
31-
summary:
32-
runs all the tests inside the virtualenv, optionally
30+
summary: runs all the tests inside the virtualenv, optionally
3331
provide the name of the test as an argument to run a single test
3432
cmds:
3533
- poetry run coverage run -m pytest -s --tap tests/{{.CLI_ARGS}}
3634
test:list:
3735
desc: lists the available tests
38-
summary:
39-
runs collect only on pytest to list the tests available
36+
summary: runs collect only on pytest to list the tests available
4037
cmds:
4138
- poetry run pytest --co
4239
test:coverreport:
@@ -54,19 +51,19 @@ tasks:
5451
dev:textual:
5552
desc: runs the textual cli
5653
cmds:
57-
- poetry run textual -- {{.CLI_ARGS}}
54+
- poetry run textual -- {{.CLI_ARGS}}
5855
dev:tui:
5956
desc: runs text gallagher console in dev mode
6057
cmds:
6158
- poetry run textual run --dev gallagher.tui
6259
dev:py:
6360
desc: runs python in the poetry shell
64-
cmds:
61+
cmds:
6562
- poetry run python -- {{.CLI_ARGS}}
6663
dev:docs:
6764
desc: run the mkdocs server with appropriate flags
68-
cmds:
69-
- cd docs && mkdocs serve --open -a localhost:8001
65+
cmds:
66+
- cd docs && poetry run mkdocs serve --open -a localhost:8001
7067
debug:get:
7168
desc: use httpie to get payload from CC
7269
summary: |
@@ -99,4 +96,3 @@ tasks:
9996
http delete \
10097
https://commandcentre-api-au.security.gallagher.cloud/api/{{.CLI_ARGS}} \
10198
"Authorization: GGL-API-KEY $GACC_API_KEY"
102-

docs/docs/design.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ Our current aliases are:
132132

133133
`type` is another `key` that that constant appears in the `JSON` payloads, while this is a reserved function name in Python, it does not conflict with the compiler when used as a variable name. For now we've chosen not to wrap this in an alias.
134134

135+
## Custom Headers
136+
137+
The `httpx` wrappers sets the following headers for all requests sent to the command centre:
138+
139+
- `Content-Type` set to `application/json` to let the command centre know that we are sending JSON payloads
140+
- `User-Agent` set of `GallagherPyToolkit/1.0` where `1.0` is the version number discovered from `gallagher/__init__.py`
141+
- `Authorization` set to `GGL-API-KEY 9939-00-` as prescribed by the official documentation
142+
135143
## API Client Core
136144

137145
The `core` package in `cc` provides two important classes:

docs/docs/installation.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,77 @@ following this you can call any of the SDK methods and the client will performan
4141
- `NoAPIKeyProvidedError` - If the API key is not set.
4242
- `ValueError` - If the API key does not conform to the expected format (which looks like eight tokens separated by `-`).
4343

44+
#### Using TLS Certificates
45+
46+
Command Centre optionally allows you to use self signed client side TLS certificates for authentication. You can use this along side your API key as an additional layer of security.
47+
48+
You can use `openssl` to generate yourself a client side certificate and key.
49+
50+
```bash
51+
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout client.key -out client.pem
52+
```
53+
54+
Fill in the required details for the certificate and then generate a `sha1` hash of the certificate.
55+
56+
```bash
57+
openssl x509 -in client.pem -noout -fingerprint -sha1
58+
```
59+
60+
> Note that the Command Centre does not use the `colon` separated format, see their documentation for more information.
61+
62+
Once you have completed these steps all you have to do is provide the path to the certificate and key files to the client.
63+
64+
```python
65+
from gallagher import cc
66+
api_key = os.environ.get("GACC_API_KEY")
67+
cc.api_key = api_key
68+
69+
cc.file_tls_certificate = '/path/to/client.pem'
70+
cc.file_private_key = '/path/to/client.key'
71+
```
72+
73+
The rest of the requests and operations remain the same, the library will use an `SSL Context` to do the needful.
74+
75+
> Our testsuites are configured to run with and without TLS certificates to ensure that we support both modes of operation.
76+
77+
In instances (such as Github actions, where we store the certificate and key in the Github secrets manager) where you can't store the certficiate and key in the filesystem, you can use Python's `tempfile` module to create temporary files and clean up once you are done using them.
78+
79+
```python
80+
import tempfile
81+
82+
# Read these from the environment variables, if they exists
83+
# they will be written to temporary files
84+
certificate_anomaly = os.environ.get("CERTIFICATE_ANOMALY")
85+
private_key_anomaly = os.environ.get("PRIVATE_KEY_ANOMALY")
86+
87+
# Create temporary files to store the certificate and private key
88+
temp_file_certificate = tempfile.NamedTemporaryFile(
89+
suffix=".crt",
90+
delete=False
91+
)
92+
temp_file_private_key = tempfile.NamedTemporaryFile(
93+
suffix=".key",
94+
delete=False
95+
)
96+
97+
# Write the certificate and private key to temporary files
98+
if certificate_anomaly and temp_file_certificate:
99+
temp_file_certificate.write(certificate_anomaly.encode('utf-8'))
100+
101+
if private_key_anomaly and temp_file_private_key:
102+
temp_file_private_key.write(private_key_anomaly.encode('utf-8'))
103+
```
104+
105+
You can assign these temporary files to the client as shown above.
106+
107+
```python
108+
from gallagher import cc
109+
110+
cc.api_key = api_key
111+
cc.file_tls_certificate = temp_file_certificate.name
112+
cc.file_private_key = temp_file_private_key.name
113+
```
114+
44115
### Command Line Interface
45116

46117
### Terminal User Interface

gallagher/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
Distributed under the terms of the MIT License.
88
"""
99

10-
__version__ = "0.1.0-alpha.6"
10+
__version__ = "0.1.0-alpha.7"

gallagher/cc/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
# to obtain an API key
3030
api_key: str = None
3131

32+
# Certificate file to be used for authentication
33+
file_tls_certificate: str = None
34+
35+
# Private key file to be used for authentication
36+
file_private_key: str = None
37+
3238
# By default the base API is set to the Australian Gateway
3339
# Override this with the US gateway or a local DNS/IP address
3440
api_base: str = URL.CLOUD_GATEWAY_AU

gallagher/cc/core.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from http import HTTPStatus # Provides constants for HTTP status codes
2828

29+
import ssl
2930
import httpx
3031

3132
from . import proxy as proxy_address
@@ -232,6 +233,23 @@ async def get_config(cls) -> EndpointConfig:
232233
provide additional configuration options.
233234
"""
234235
raise NotImplementedError("get_config method not implemented")
236+
237+
@classmethod
238+
def _ssl_context(cls):
239+
"""Returns the SSL context for the endpoint
240+
241+
This method can be overridden by the child class to
242+
provide additional SSL context options.
243+
"""
244+
from . import file_tls_certificate, file_private_key
245+
246+
if not file_tls_certificate:
247+
"""TLS certificate is required for SSL context"""
248+
return None
249+
250+
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
251+
context.load_cert_chain(file_tls_certificate, file_private_key)
252+
return context
235253

236254
@classmethod
237255
async def _discover(cls):
@@ -269,7 +287,10 @@ async def _discover(cls):
269287
# be called as part of the bootstrapping process
270288
from . import api_base
271289

272-
async with httpx.AsyncClient(proxy=proxy_address) as _httpx_async:
290+
async with httpx.AsyncClient(
291+
proxy=proxy_address,
292+
verify=cls._ssl_context(),
293+
) as _httpx_async:
273294
# Don't use the _get wrapper here, we need to get the raw response
274295
response = await _httpx_async.get(
275296
api_base,
@@ -490,7 +511,11 @@ async def follow(
490511
# Initial url is set to endpoint_follow
491512
url = f"{cls.__config__.endpoint_follow.href}"
492513

493-
async with httpx.AsyncClient(proxy=proxy_address) as _httpx_async:
514+
async with httpx.AsyncClient(
515+
proxy=proxy_address,
516+
verify=cls._ssl_context(),
517+
) as _httpx_async:
518+
494519
while event.is_set():
495520
try:
496521
response = await _httpx_async.get(
@@ -546,7 +571,10 @@ async def _get(
546571
:param str url: URL to fetch the data from
547572
:param AppBaseModel response_class: DTO to be used for list requests
548573
"""
549-
async with httpx.AsyncClient(proxy=proxy_address) as _httpx_async:
574+
async with httpx.AsyncClient(
575+
proxy=proxy_address,
576+
verify=cls._ssl_context(),
577+
) as _httpx_async:
550578

551579
try:
552580

@@ -594,7 +622,10 @@ async def _post(
594622
The behaviour is very similar to the _get method, except
595623
parsing and sending out a body as part of the request.
596624
"""
597-
async with httpx.AsyncClient(proxy=proxy_address) as _httpx_async:
625+
async with httpx.AsyncClient(
626+
proxy=proxy_address,
627+
verify=cls._ssl_context(),
628+
) as _httpx_async:
598629

599630
try:
600631

0 commit comments

Comments
 (0)