Skip to content

Commit 9e88781

Browse files
committed
service auth
1 parent 02ee32f commit 9e88781

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

app/dl_control_api/dl_control_api/app_factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def _setup_auth_middleware_native(
175175
settings=dl_auth_native.MiddlewareSettings(
176176
decoder_key=settings.JWT_KEY,
177177
decoder_algorithms=[settings.JWT_ALGORITHM],
178+
master_token=self._settings.US_MASTER_TOKEN,
178179
)
179180
).set_up(app=app)
180181
LOGGER.info("Native auth setup complete")

lib/dl_auth_native/dl_auth_native/middlewares/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@ class AuthResult:
3939
class MiddlewareSettings:
4040
decoder_key: str = attr.ib()
4141
decoder_algorithms: list[str] = attr.ib()
42+
master_token: str | None = attr.ib(default=None)
4243

4344

4445
@attr.s()
4546
class BaseMiddleware:
4647
_token_decoder: token.DecoderProtocol = attr.ib()
4748
_user_access_header_key: str = attr.ib(default=dl_constants.DLHeadersCommon.AUTHORIZATION_TOKEN)
4849
_token_type: str = attr.ib(default="Bearer")
50+
_master_token: str | None = attr.ib(default=None)
51+
_service_auth_header_key: str = attr.ib(default=dl_constants.DLHeadersCommon.US_MASTER_TOKEN)
4952

5053
@classmethod
5154
def from_settings(cls, settings: MiddlewareSettings) -> Self:
@@ -56,12 +59,17 @@ def from_settings(cls, settings: MiddlewareSettings) -> Self:
5659

5760
return cls(
5861
token_decoder=token_decoder,
62+
master_token=settings.master_token,
5963
)
6064

6165
@attr.s(frozen=True)
6266
class Unauthorized(Exception):
6367
message: str = attr.ib()
6468

69+
@attr.s(frozen=True)
70+
class Forbidden(Exception):
71+
message: str = attr.ib()
72+
6573
def _auth(self, user_access_header: str | None) -> AuthResult:
6674
if user_access_header is None:
6775
raise self.Unauthorized("User access token header is missing")
@@ -86,6 +94,16 @@ def _auth(self, user_access_header: str | None) -> AuthResult:
8694
),
8795
)
8896

97+
def _service_auth(self, service_token_header: str | None) -> None:
98+
if self._master_token is None:
99+
raise self.Unauthorized("Service auth is not configured")
100+
101+
if service_token_header is None:
102+
raise self.Unauthorized("Service token header is missing")
103+
104+
if service_token_header != self._master_token:
105+
raise self.Forbidden("Invalid service token")
106+
89107

90108
__all__ = [
91109
"AuthData",

lib/dl_auth_native/dl_auth_native/middlewares/flask.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ def process(self) -> None:
3636
LOGGER.info("Auth was skipped due to SKIP_AUTH flag in target view")
3737
return None
3838

39+
if RequiredResourceCommon.ONLY_SERVICES_ALLOWED in required_resources:
40+
LOGGER.info("Using service auth flow due to ONLY_SERVICES_ALLOWED flag in target view")
41+
service_token_header = flask.request.headers.get(self._service_auth_header_key)
42+
43+
try:
44+
self._service_auth(service_token_header)
45+
except self.Unauthorized as exc:
46+
LOGGER.info(f"Unauthorized: {exc.message}")
47+
raise werkzeug_exceptions.Unauthorized(description=exc.message) from exc
48+
except self.Forbidden as exc:
49+
LOGGER.info(f"Forbidden: {exc.message}")
50+
raise werkzeug_exceptions.Forbidden(description=exc.message) from exc
51+
52+
return None
53+
3954
user_access_token_header = flask.request.headers.get(self._user_access_header_key)
4055

4156
try:

lib/dl_auth_native/dl_auth_native_tests/unit/middleware/test_flask.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
import unittest.mock as mock
33

44
import flask
5+
from flask.views import MethodView
56
import pytest
67

78
import dl_api_commons.flask.middlewares as dl_api_commons_flask_middlewares
9+
from dl_api_commons.flask.middlewares.commit_rci_middleware import ReqCtxInfoMiddleware
10+
from dl_api_commons.flask.required_resources import RequiredResourceCommon
811
import dl_auth_native
12+
from dl_constants.api_constants import DLHeadersCommon
913

1014

1115
@pytest.fixture(name="flask_app")
@@ -81,3 +85,119 @@ def test_invalid_token(
8185

8286
assert response.status_code == 401
8387
assert "Invalid user access token: invalid token" in response.text
88+
89+
90+
MASTER_TOKEN = "test-master-token-secret"
91+
92+
93+
@pytest.fixture(name="flask_app_with_service_auth")
94+
def fixture_flask_app_with_service_auth(
95+
token_decoder: dl_auth_native.DecoderProtocol,
96+
) -> flask.Flask:
97+
app = flask.Flask(__name__)
98+
99+
dl_api_commons_flask_middlewares.ContextVarMiddleware().wrap_flask_app(app)
100+
dl_api_commons_flask_middlewares.RequestLoggingContextControllerMiddleWare().set_up(app)
101+
dl_api_commons_flask_middlewares.RequestIDService(
102+
append_local_req_id=False,
103+
request_id_app_prefix=None,
104+
).set_up(app)
105+
dl_auth_native.FlaskMiddleware(
106+
token_decoder=token_decoder,
107+
master_token=MASTER_TOKEN,
108+
).set_up(app)
109+
ReqCtxInfoMiddleware().set_up(app)
110+
111+
class ServiceView(MethodView):
112+
REQUIRED_RESOURCES = frozenset({RequiredResourceCommon.ONLY_SERVICES_ALLOWED})
113+
114+
def get(self) -> flask.Response:
115+
return flask.jsonify({"ok": True})
116+
117+
app.add_url_rule("/service", view_func=ServiceView.as_view("service"))
118+
119+
@app.route("/user")
120+
def user_handler() -> flask.Response:
121+
return flask.jsonify({"ok": True})
122+
123+
return app
124+
125+
126+
def test_service_auth_correct_token(
127+
flask_app_with_service_auth: flask.Flask,
128+
) -> None:
129+
with flask_app_with_service_auth.test_client() as client:
130+
response = client.get(
131+
"/service",
132+
headers={DLHeadersCommon.US_MASTER_TOKEN.value: MASTER_TOKEN},
133+
)
134+
assert response.status_code == 200
135+
136+
137+
def test_service_auth_wrong_token(
138+
flask_app_with_service_auth: flask.Flask,
139+
) -> None:
140+
with flask_app_with_service_auth.test_client() as client:
141+
response = client.get(
142+
"/service",
143+
headers={DLHeadersCommon.US_MASTER_TOKEN.value: "wrong-token"},
144+
)
145+
assert response.status_code == 403
146+
assert "Invalid service token" in response.text
147+
148+
149+
def test_service_auth_missing_token(
150+
flask_app_with_service_auth: flask.Flask,
151+
) -> None:
152+
with flask_app_with_service_auth.test_client() as client:
153+
response = client.get("/service")
154+
assert response.status_code == 401
155+
assert "Service token header is missing" in response.text
156+
157+
158+
def test_service_auth_not_configured(
159+
token_decoder: dl_auth_native.DecoderProtocol,
160+
) -> None:
161+
"""When master_token is None, service endpoints should return 401."""
162+
app = flask.Flask(__name__)
163+
164+
dl_api_commons_flask_middlewares.ContextVarMiddleware().wrap_flask_app(app)
165+
dl_api_commons_flask_middlewares.RequestLoggingContextControllerMiddleWare().set_up(app)
166+
dl_api_commons_flask_middlewares.RequestIDService(
167+
append_local_req_id=False,
168+
request_id_app_prefix=None,
169+
).set_up(app)
170+
dl_auth_native.FlaskMiddleware(
171+
token_decoder=token_decoder,
172+
).set_up(app)
173+
ReqCtxInfoMiddleware().set_up(app)
174+
175+
class ServiceView(MethodView):
176+
REQUIRED_RESOURCES = frozenset({RequiredResourceCommon.ONLY_SERVICES_ALLOWED})
177+
178+
def get(self) -> flask.Response:
179+
return flask.jsonify({"ok": True})
180+
181+
app.add_url_rule("/service", view_func=ServiceView.as_view("service"))
182+
183+
with app.test_client() as client:
184+
response = client.get(
185+
"/service",
186+
headers={DLHeadersCommon.US_MASTER_TOKEN.value: "some-token"},
187+
)
188+
assert response.status_code == 401
189+
assert "Service auth is not configured" in response.text
190+
191+
192+
def test_user_endpoint_still_uses_jwt(
193+
flask_app_with_service_auth: flask.Flask,
194+
token_decoder: mock.Mock,
195+
) -> None:
196+
"""Regular endpoints should still require JWT auth, not accept master token."""
197+
with flask_app_with_service_auth.test_client() as client:
198+
response = client.get(
199+
"/user",
200+
headers={DLHeadersCommon.US_MASTER_TOKEN.value: MASTER_TOKEN},
201+
)
202+
assert response.status_code == 401
203+
assert "User access token header is missing" in response.text

0 commit comments

Comments
 (0)