Skip to content

Commit 23fbc60

Browse files
committed
Implement async TokenAuth for ContentApi
SDESK-7484
1 parent cde4cfa commit 23fbc60

File tree

7 files changed

+78
-89
lines changed

7 files changed

+78
-89
lines changed

content_api/app/__init__.py

-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from eve.io.mongo.mongo import MongoJSONEncoder
2525

2626
from superdesk.flask import Config
27-
from content_api.tokens import SubscriberTokenAuth
2827
from superdesk.datalayer import SuperdeskDataLayer
2928
from superdesk.factory.elastic_apm import setup_apm
3029
from superdesk.validator import SuperdeskValidator
@@ -63,7 +62,6 @@ def get_app(config=None):
6362
media_storage = get_media_storage_class(app_config)
6463

6564
app = SuperdeskEve(
66-
auth=SubscriberTokenAuth,
6765
settings=app_config,
6866
data=SuperdeskDataLayer,
6967
media=media_storage,

content_api/app/settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@
5555

5656
MODULES = [
5757
"content_api.items.module",
58+
"superdesk.publish.subscriber_token",
5859
]
5960

61+
ASYNC_AUTH_CLASS = "content_api.tokens.auth:SubscriberTokenAuth"
62+
6063
CONTENTAPI_DOMAIN = {}
6164

6265
# NOTE: no trailing slash for the CONTENTAPI_URL setting!

content_api/tokens/__init__.py

+3-48
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,4 @@
1-
# -*- coding: utf-8; -*-
2-
#
3-
# This file is part of Superdesk.
4-
#
5-
# Copyright 2013, 2014, 2015 Sourcefabric z.u. and contributors.
6-
#
7-
# For the full copyright and license information, please see the
8-
# AUTHORS and LICENSE files distributed with this source code, or
9-
# at https://www.sourcefabric.org/superdesk/license
1+
from .resource import CompanyTokenResource
2+
from .service import CompanyTokenService
103

11-
import superdesk
12-
13-
from eve.auth import TokenAuth
14-
15-
from superdesk.core import get_current_app
16-
from superdesk.flask import g
17-
from superdesk.utc import utcnow
18-
from superdesk.publish.subscriber_token import SubscriberTokenResource, SubscriberTokenService
19-
20-
from content_api.tokens.resource import CompanyTokenResource # noqa
21-
from content_api.tokens.service import CompanyTokenService # noqa
22-
23-
24-
TOKEN_RESOURCE = "subscriber_token"
25-
26-
27-
class AuthSubscriberTokenResource(SubscriberTokenResource):
28-
item_methods = []
29-
resource_methods = []
30-
31-
32-
class SubscriberTokenAuth(TokenAuth):
33-
def check_auth(self, token, allowed_roles, resource, method):
34-
"""Try to find auth token and if valid put subscriber id into ``g.user``."""
35-
app = get_current_app()
36-
data = app.data.mongo.find_one(TOKEN_RESOURCE, req=None, _id=token)
37-
if not data:
38-
return False
39-
now = utcnow()
40-
if data.get("expiry") and data.get("expiry") < now:
41-
app.data.mongo.remove(TOKEN_RESOURCE, {"_id": token})
42-
return False
43-
g.user = str(data.get("subscriber"))
44-
return g.user
45-
46-
47-
def init_app(app) -> None:
48-
# superdesk.register_resource(TOKEN_RESOURCE, AuthSubscriberTokenResource, SubscriberTokenService, _app=app)
49-
pass
4+
__all__ = ["CompanyTokenResource", "CompanyTokenService"]

content_api/tokens/auth.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Any, cast
2+
from quart_babel import _
3+
4+
from superdesk.utc import utcnow
5+
from superdesk.core.types.web import Request
6+
from superdesk.errors import SuperdeskApiError
7+
from superdesk.core.auth.user_auth import UserAuthProtocol
8+
from superdesk.publish.subscriber_token import SubscriberTokenService, SubscriberToken
9+
10+
11+
class SubscriberTokenAuth(UserAuthProtocol):
12+
def get_token_from_request(self, request: Request) -> str | None:
13+
"""
14+
Extracts the token from `Authorization` header. Code taken partly
15+
from eve.Auth module
16+
"""
17+
18+
auth = (request.get_header("Authorization") or "").strip()
19+
if len(auth):
20+
if auth.lower().startswith(("token", "bearer", "basic")):
21+
return auth.split(" ")[1] if " " in auth else None
22+
return auth
23+
24+
return None
25+
26+
async def authenticate(self, request: Request) -> None:
27+
"""
28+
Tries to find the auth token in the request and if valid put subscriber id into ``g.user``.
29+
"""
30+
token_service = SubscriberTokenService()
31+
token_missing_exception = SuperdeskApiError.forbiddenError(message=_("Authorization token missing."))
32+
token_id = self.get_token_from_request(request)
33+
34+
if token_id is None:
35+
raise token_missing_exception
36+
37+
token = await token_service.find_by_id(token_id)
38+
if token is None:
39+
raise token_missing_exception
40+
41+
await self.check_token_validity(token)
42+
await self.start_session(request, None, token=token)
43+
44+
async def check_token_validity(self, token: SubscriberToken) -> None:
45+
"""
46+
Checks if the token is valid and if it has expired.
47+
"""
48+
49+
if token.expiry and token.expiry < utcnow():
50+
await SubscriberTokenService().delete(token)
51+
raise SuperdeskApiError.forbiddenError(message=_("Authorization token expired."))
52+
53+
async def start_session(self, request: Request, user: Any, **kwargs) -> None:
54+
"""
55+
Puts the subscriber id into ``g.user``.
56+
"""
57+
token = cast(SubscriberToken, kwargs.get("token"))
58+
request.storage.request.set("user", str(token.subscriber))
59+
60+
async def stop_session(self, request: Request) -> None:
61+
"""
62+
Removes the subscriber id from ``g.user``.
63+
"""
64+
request.storage.request.set("user", None)
65+
66+
def get_current_user(self, request: Request) -> str | None:
67+
"""Overrides as it is needed."""
68+
return None

superdesk/core/resources/resource_rest_endpoints.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import math
1212
from inspect import get_annotations
13-
from typing import Annotated, List, Optional, cast, Dict, Any, Type, get_args, get_origin, get_type_hints
13+
from typing import Annotated, List, Optional, cast, Dict, Any, Type, get_args, get_origin
1414

1515
from dataclasses import dataclass
1616
from pydantic import ValidationError, BaseModel, NonNegativeInt

superdesk/publish/__init__.py

-4
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ def transmit():
8585
from superdesk.publish.subscribers import SubscribersResource, SubscribersService # NOQA
8686
from superdesk.publish.publish_queue import PublishQueueResource, PublishQueueService # NOQA
8787

88-
from superdesk.publish.subscriber_token import SubscriberTokenResource, SubscriberTokenService # NOQA
89-
9088

9189
def init_app(app) -> None:
9290
# XXX: we need to do imports for transmitters and formatters here
@@ -104,8 +102,6 @@ def init_app(app) -> None:
104102
service = PublishQueueService(endpoint_name, backend=get_backend())
105103
PublishQueueResource(endpoint_name, app=app, service=service)
106104

107-
# superdesk.register_resource("subscriber_token", SubscriberTokenResource, SubscriberTokenService)
108-
109105
app.client_config.update(
110106
{
111107
"transmitter_types": registered_transmitters_list,

superdesk/publish/subscriber_token.py

+3-34
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import superdesk
21
from pydantic import Field
32
from typing import Annotated
43
from datetime import timedelta, datetime
54

6-
from content_api import MONGO_PREFIX
7-
from superdesk.core.module import Module
85
from superdesk.utc import utcnow
6+
from superdesk.core.module import Module
97
from superdesk.utils import get_random_token
108
from superdesk.core.auth.privilege_rules import http_method_privilege_based_rules
119
from superdesk.core.resources import (
@@ -18,43 +16,14 @@
1816
from superdesk.core.resources.validators import validate_data_relation_async
1917

2018

21-
class SubscriberTokenResource(superdesk.Resource):
22-
schema = {
23-
"_id": {"type": "string", "unique": True},
24-
"expiry": {"readonly": True},
25-
"expiry_days": {"type": "integer"},
26-
"subscriber": superdesk.Resource.rel("subscribers", required=True),
27-
}
28-
29-
item_url = 'regex(".+")'
30-
resource_methods = ["GET", "POST"]
31-
item_methods = ["GET", "DELETE"]
32-
privileges = {"POST": "subscribers", "DELETE": "subscribers"}
33-
34-
datasource = {
35-
"default_sort": [("_created", 1)],
36-
}
37-
38-
mongo_prefix = MONGO_PREFIX
39-
40-
41-
class SubscriberTokenService(superdesk.Service):
42-
def create(self, docs, **kwargs):
43-
for doc in docs:
44-
doc["_id"] = get_random_token()
45-
if doc.get("expiry_days"):
46-
doc.setdefault("expiry", utcnow() + timedelta(days=doc["expiry_days"]))
47-
return super().create(docs, **kwargs)
48-
49-
5019
class SubscriberToken(ResourceModel):
5120
id: Annotated[str, Field(alias="_id", default_factory=get_random_token)]
5221
expiry: datetime | None = None
5322
expiry_days: int | None = None
5423
subscriber: Annotated[fields.ObjectId, validate_data_relation_async("subscribers")]
5524

5625

57-
class AsyncSubscriberTokenService(AsyncResourceService[SubscriberToken]):
26+
class SubscriberTokenService(AsyncResourceService[SubscriberToken]):
5827
async def on_create(self, docs: list[SubscriberToken]) -> None:
5928
"""
6029
Calculates and sets expiry dates based on expiry_days param.
@@ -73,7 +42,7 @@ async def on_create(self, docs: list[SubscriberToken]) -> None:
7342
resource_config = ResourceConfig(
7443
name="subscriber_token",
7544
data_class=SubscriberToken,
76-
service=AsyncSubscriberTokenService,
45+
service=SubscriberTokenService,
7746
default_sort=[("_created", 1)],
7847
rest_endpoints=RestEndpointConfig(
7948
resource_methods=["GET", "POST"],

0 commit comments

Comments
 (0)