Skip to content

Commit 0eb4e1d

Browse files
authored
Merge pull request #124 from pvarki/product_as_admin_to_another
Make it possible for product integration APIs to enable admin privileges for themselves to another product
2 parents f864707 + 95b84cb commit 0eb4e1d

File tree

9 files changed

+145
-5
lines changed

9 files changed

+145
-5
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 1.7.0
2+
current_version = 1.8.0
33
commit = False
44
tag = False
55

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "rasenmaeher_api"
3-
version = "1.7.0"
3+
version = "1.8.0"
44
description = "python-rasenmaeher-api"
55
authors = [
66
"Aciid <703382+Aciid@users.noreply.github.com>",

src/rasenmaeher_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""python-rasenmaeher-api"""
22

3-
__version__ = "1.7.0" # NOTE Use `bump2version --config-file patch` to bump versions correctly
3+
__version__ = "1.8.0" # NOTE Use `bump2version --config-file patch` to bump versions correctly

src/rasenmaeher_api/web/api/product/schema.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,24 @@ class Config: # pylint: disable=too-few-public-methods
9595
},
9696
]
9797
}
98+
99+
100+
# FIXME: Move to libpvarki
101+
class ProductAddRequest(BaseModel): # pylint: disable=too-few-public-methods
102+
"""Request to add product interoperability."""
103+
104+
certcn: str = Field(description="CN of the certificate")
105+
x509cert: str = Field(description="Certificate encoded with CFSSL conventions (newlines escaped)")
106+
107+
class Config: # pylint: disable=too-few-public-methods
108+
"""Example values for schema"""
109+
110+
extra = Extra.forbid
111+
schema_extra = {
112+
"examples": [
113+
{
114+
"certcn": "product.deployment.tld",
115+
"x509cert": "-----BEGIN CERTIFICATE-----\\nMIIEwjCC...\\n-----END CERTIFICATE-----\\n",
116+
},
117+
],
118+
}

src/rasenmaeher_api/web/api/product/views.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Product registration API views."""
22

3+
from typing import cast
34
import logging
45

56
from fastapi import APIRouter, Depends, HTTPException, Request
@@ -11,14 +12,15 @@
1112
from OpenSSL.crypto import load_certificate_request, FILETYPE_PEM # FIXME: use cryptography instead of pyOpenSSL
1213

1314

14-
from .schema import CertificatesResponse, CertificatesRequest, RevokeRequest, KCClientToken
15+
from .schema import CertificatesResponse, CertificatesRequest, RevokeRequest, KCClientToken, ProductAddRequest
1516
from ....db.nonces import SeenToken
1617
from ....db.errors import NotFound
1718
from ....cfssl.public import get_ca, get_bundle
1819
from ....cfssl.private import sign_csr, revoke_pem
1920
from ....cfssl.base import CFSSLError
2021
from ....rmsettings import RMSettings
2122
from ....kchelpers import KCClient
23+
from ....productapihelpers import post_to_product
2224

2325

2426
router = APIRouter()
@@ -138,3 +140,29 @@ async def get_kc_token(
138140
raise HTTPException(403, detail="KC is not enabled")
139141
data = await KCClient.singleton().client_access_token()
140142
return KCClientToken.parse_obj(data)
143+
144+
145+
@router.post("/interop/{tgtproduct}", dependencies=[Depends(MTLSHeader(auto_error=True))])
146+
async def add_interop(
147+
srcproduct: ProductAddRequest,
148+
tgtproduct: str,
149+
request: Request,
150+
) -> OperationResultResponse:
151+
"""Product needs interop privileges with another"""
152+
payload = request.state.mtlsdn
153+
if payload.get("CN") not in RMSettings.singleton().valid_product_cns:
154+
raise HTTPException(status_code=403)
155+
156+
# TODO: Verify that srcproduct certcn and actual cert contents match
157+
158+
manifest = RMSettings.singleton().kraftwerk_manifest_dict
159+
if "products" not in manifest:
160+
LOGGER.error("Manifest does not have products key")
161+
raise HTTPException(status_code=500, detail="Manifest does not have products key")
162+
if tgtproduct not in manifest["products"]:
163+
raise HTTPException(status_code=404, detail=f"Unknown product {tgtproduct}")
164+
resp = await post_to_product(tgtproduct, "/api/v1/interop/add", srcproduct.dict(), OperationResultResponse)
165+
if resp is None:
166+
return OperationResultResponse(success=False, error="post_to_product returned None")
167+
resp = cast(OperationResultResponse, resp)
168+
return resp

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ def session_env_config( # pylint: disable=R0915,R0914
110110
"uri": "https://nonexistent.localmaeher.dev.pvarki.fi:844/", # Not actually there
111111
"certcn": "nonexistent.localmaeher.dev.pvarki.fi",
112112
},
113+
"interoptest": {
114+
"api": "https://localhost:4657/",
115+
"uri": "https://interoptest.localmaeher.dev.pvarki.fi:844/", # Not actually there
116+
"certcn": "interoptest.localmaeher.dev.pvarki.fi",
117+
},
113118
},
114119
}
115120
)

tests/ptfpapi/fprun.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@
1818
UserInstructionFragment,
1919
UserCRUDRequest,
2020
)
21+
from pydantic import BaseModel, Field, Extra
22+
23+
24+
# FIXME: Move to libpvarki
25+
class ProductAddRequest(BaseModel): # pylint: disable=too-few-public-methods,R0801
26+
"""Request to add product interoperability."""
27+
28+
certcn: str = Field(description="CN of the certificate")
29+
x509cert: str = Field(description="Certificate encoded with CFSSL conventions (newlines escaped)")
30+
31+
class Config: # pylint: disable=too-few-public-methods
32+
"""Example values for schema"""
33+
34+
extra = Extra.forbid
35+
schema_extra = {
36+
"examples": [
37+
{
38+
"certcn": "product.deployment.tld",
39+
"x509cert": "-----BEGIN CERTIFICATE-----\\nMIIEwjCC...\\n-----END CERTIFICATE-----\\n",
40+
},
41+
],
42+
}
43+
2144

2245
LOGGER = logging.getLogger(__name__)
2346

@@ -128,6 +151,13 @@ async def handle_admin_fragment(request: web.Request) -> web.Response:
128151
return web.json_response(resp.dict())
129152

130153

154+
async def handle_interop_add(request: web.Request) -> web.Response:
155+
"""Respond to additions"""
156+
_req = ProductAddRequest.parse_raw(await request.text())
157+
resp = OperationResultResponse(success=True, extra="Nothing was actually done, this is a fake endpoint for testing")
158+
return web.json_response(resp.dict())
159+
160+
131161
def main() -> int:
132162
"""Main entrypoint, return exit code"""
133163
LOGGER.debug("Called")
@@ -146,6 +176,7 @@ def main() -> int:
146176
[
147177
web.get("/", handle_get_hello),
148178
web.get("/{name}", handle_get_hello),
179+
web.post("/api/v1/interop/add", handle_interop_add),
149180
web.post("/api/v1/users/created", handle_user_crud),
150181
web.post("/api/v1/users/revoked", handle_user_crud),
151182
web.post("/api/v1/users/promoted", handle_user_crud),

tests/test_interop.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Test the interop route"""
2+
3+
import logging
4+
5+
import pytest
6+
from async_asgi_testclient import TestClient
7+
8+
from rasenmaeher_api.web.api.product.schema import ProductAddRequest
9+
10+
LOGGER = logging.getLogger(__name__)
11+
12+
13+
@pytest.mark.asyncio
14+
async def test_valid_products(ginosession: None, unauth_client: TestClient) -> None:
15+
"""Test requesting interop with product 'fake'"""
16+
_ = ginosession
17+
client = unauth_client
18+
client.headers.update({"X-ClientCert-DN": "CN=interoptest.localmaeher.dev.pvarki.fi,O=N/A"})
19+
req = ProductAddRequest(
20+
certcn="interoptest.localmaeher.dev.pvarki.fi",
21+
x509cert="-----BEGIN CERTIFICATE-----\\nMIIEwjCC...\\n-----END CERTIFICATE-----\\n",
22+
)
23+
payload = req.dict()
24+
resp = await client.post("/api/v1/product/interop/fake", json=payload)
25+
assert resp.status_code == 200
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_invalid_requester(ginosession: None, unauth_client: TestClient) -> None:
30+
"""Test requesting interop with product 'fake' with product that is not valid"""
31+
_ = ginosession
32+
client = unauth_client
33+
client.headers.update({"X-ClientCert-DN": "CN=callsigndude,O=N/A"})
34+
req = ProductAddRequest(
35+
certcn="callsigndude",
36+
x509cert="-----BEGIN CERTIFICATE-----\\nMIIEwjCC...\\n-----END CERTIFICATE-----\\n",
37+
)
38+
payload = req.dict()
39+
resp = await client.post("/api/v1/product/interop/fake", json=payload)
40+
assert resp.status_code == 403
41+
42+
43+
@pytest.mark.asyncio
44+
async def test_invalid_tgt(ginosession: None, unauth_client: TestClient) -> None:
45+
"""Test requesting interop with product 'nosuch'"""
46+
_ = ginosession
47+
client = unauth_client
48+
client.headers.update({"X-ClientCert-DN": "CN=interoptest.localmaeher.dev.pvarki.fi,O=N/A"})
49+
req = ProductAddRequest(
50+
certcn="interoptest.localmaeher.dev.pvarki.fi",
51+
x509cert="-----BEGIN CERTIFICATE-----\\nMIIEwjCC...\\n-----END CERTIFICATE-----\\n",
52+
)
53+
payload = req.dict()
54+
resp = await client.post("/api/v1/product/interop/nosuch", json=payload)
55+
assert resp.status_code == 404

tests/test_rasenmaeher_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
def test_version() -> None:
1818
"""Make sure version matches expected"""
19-
assert __version__ == "1.7.0"
19+
assert __version__ == "1.8.0"
2020

2121

2222
@pytest.mark.asyncio(loop_scope="session")

0 commit comments

Comments
 (0)