Skip to content

Commit cb20016

Browse files
committed
chore(user): check user and app status
1 parent bd6daa0 commit cb20016

22 files changed

Lines changed: 227 additions & 552 deletions

rootfs/api/authentication.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from rest_framework import authentication
66
from rest_framework.authentication import get_authorization_header
77
from rest_framework import exceptions
8+
from api.clients import ManagerAPI
89

910
logger = logging.getLogger(__name__)
1011

@@ -21,7 +22,7 @@ def authenticate(self, request):
2122
class DryccAuthentication(authentication.BaseAuthentication):
2223

2324
keywords = ('token', 'bearer')
24-
ignore_authentication_failed = False
25+
manager_api = ManagerAPI()
2526

2627
def parse_header(self, request):
2728
try:
@@ -42,22 +43,21 @@ def parse_header(self, request):
4243
raise exceptions.AuthenticationFailed(msg)
4344

4445
def authenticate(self, request):
45-
token_type, token = self.parse_header(request)
46+
user, (token_type, token) = None, self.parse_header(request)
4647
if token_type is None or token is None:
4748
return None
48-
try:
49-
if token_type == 'bearer': # drycc oauth access token
50-
from api.apps_extra.social_core.backends import OauthCacheManager
51-
return OauthCacheManager().get_user(token), token
52-
# drycc token
49+
if token_type == 'bearer': # drycc oauth access token
50+
from api.apps_extra.social_core.backends import OauthCacheManager
51+
user = OauthCacheManager().get_user(token)
52+
elif token_type == 'token': # drycc token
5353
user = cache.get(token, None)
5454
if not user:
55-
return self.authenticate_credentials(token)
56-
return user, token
57-
except exceptions.AuthenticationFailed as e:
58-
if not self.ignore_authentication_failed:
59-
raise e
60-
return None
55+
user, token = self.authenticate_credentials(token)
56+
if user:
57+
is_active, message = self.manager_api.get_user_status(user.id)
58+
if not is_active:
59+
raise exceptions.AuthenticationFailed(message)
60+
return user, token if user else None
6161

6262
def authenticate_credentials(self, key):
6363
from api.models.base import Token

rootfs/api/clients.py

Lines changed: 53 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import base64
21
import logging
32
import requests
43
import urllib.parse
@@ -11,98 +10,6 @@
1110
logger = logging.getLogger(__name__)
1211

1312

14-
class ManagerAPI(object):
15-
16-
def __init__(self, timeout=3):
17-
self.timeout = timeout
18-
token = base64.b85encode(b"%s:%s" % (
19-
settings.SOCIAL_AUTH_DRYCC_KEY.encode("utf8"),
20-
settings.SOCIAL_AUTH_DRYCC_SECRET.encode("utf8"),
21-
)).decode("utf8")
22-
self.headers = {
23-
'Content-Type': 'application/json',
24-
'Authorization': 'token %s' % token,
25-
'User-Agent': user_agent('Drycc Controller ', drycc_version)
26-
}
27-
28-
def request(self, method, url, **kwargs):
29-
headers = kwargs.get("headers", {})
30-
headers.update(self.headers)
31-
kwargs["headers"] = headers
32-
kwargs["timeout"] = self.timeout
33-
return requests.request(method, url, **kwargs)
34-
35-
def get(self, url, params=None, **kwargs):
36-
return self.request('get', url, params=params, **kwargs)
37-
38-
def options(self, url, **kwargs):
39-
return self.request('options', url, **kwargs)
40-
41-
def head(self, url, **kwargs):
42-
kwargs.setdefault('allow_redirects', False)
43-
return self.request('head', url, **kwargs)
44-
45-
def post(self, url, data=None, json=None, **kwargs):
46-
return self.request('post', url, data=data, json=json, **kwargs)
47-
48-
def put(self, url, data=None, **kwargs):
49-
return self.request('put', url, data=data, **kwargs)
50-
51-
def patch(self, url, data=None, **kwargs):
52-
return self.request('patch', url, data=data, **kwargs)
53-
54-
def delete(self, url, **kwargs):
55-
return self.request('delete', url, **kwargs)
56-
57-
58-
class WorkspaceAPI(ManagerAPI):
59-
60-
def get_status(self, workspace_id):
61-
"""
62-
{
63-
"is_active": False,
64-
"message": "The user is in arrears"
65-
}
66-
"""
67-
key = f"workspace:status:{workspace_id}"
68-
status = cache.get(key)
69-
if not status:
70-
url = f"{settings.WORKFLOW_MANAGER_URL}/workspaces/{workspace_id}/status/"
71-
try:
72-
status = self.get(url=url, timeout=self.timeout).json()
73-
except requests.exceptions.Timeout as ex:
74-
msg = f"request workspace {workspace_id} timeout, skipping verification."
75-
status = {"is_active": True, "message": msg}
76-
logger.error(msg)
77-
logger.exception(ex)
78-
cache.set(key, status, timeout=settings.DRYCC_CACHE_USER_TIME)
79-
return status
80-
81-
82-
class UserAPI(WorkspaceAPI):
83-
"""Backward-compatible alias for legacy call sites."""
84-
85-
86-
class UsageAPI(ManagerAPI):
87-
88-
def post(self, usage: List[Dict[str, str]]):
89-
"""
90-
[
91-
{
92-
"app_id": "test",
93-
"workspace": "test",
94-
"name": "web",
95-
"type": "limits",
96-
"unit": "std1.large.c1m1",
97-
"usage": "2",
98-
"timestamp": "1609231998.9103732"
99-
}
100-
]
101-
"""
102-
url = "%s/usage/" % settings.WORKFLOW_MANAGER_URL
103-
return super().post(url=url, json=usage)
104-
105-
10613
class PassportAPI(object):
10714
"""Service-to-service client for Drycc Passport using OAuth2 client_credentials."""
10815

@@ -113,7 +20,8 @@ def __init__(self, timeout=10):
11320
self.timeout = timeout
11421
self.base_url = settings.DRYCC_PASSPORT_URL.rstrip("/")
11522

116-
def _get_token(self) -> str:
23+
@property
24+
def headers(self) -> str:
11725
token = cache.get(self.TOKEN_CACHE_KEY)
11826
if token and self.get_scopes(token) == set(settings.DRYCC_PASSPORT_SCOPES.split()):
11927
return token
@@ -132,9 +40,14 @@ def _get_token(self) -> str:
13240
token = body["access_token"]
13341
ttl = max(int(body.get("expires_in", 3600)) - self.TOKEN_REFRESH_LEEWAY, 60)
13442
cache.set(self.TOKEN_CACHE_KEY, token, timeout=ttl)
135-
return token
43+
return {
44+
"Authorization": f"Bearer {token}",
45+
"Content-Type": "application/json",
46+
"User-Agent": user_agent("Drycc Controller", drycc_version),
47+
}
13648

137-
def get_scopes(self, token):
49+
@staticmethod
50+
def get_scopes(token):
13851
def _get_scopes():
13952
endpoint = getattr(settings, 'SOCIAL_AUTH_DRYCC_OIDC_ENDPOINT', None)
14053
if not endpoint:
@@ -156,17 +69,54 @@ def _get_scopes():
15669
f"drycc_oauth_scopes_v2_{token}", _get_scopes, settings.DRYCC_CACHE_USER_TIME)
15770

15871
def send_message(self, username: str, message: Dict) -> None:
159-
token = self._get_token()
160-
headers = {
161-
"Authorization": f"Bearer {token}",
162-
"Content-Type": "application/json",
163-
"User-Agent": user_agent("Drycc Controller", drycc_version),
164-
}
16572
body = {**message, "username": username}
16673
resp = requests.post(
16774
f"{self.base_url}/messages/",
16875
json=body,
169-
headers=headers,
76+
headers=self.headers,
17077
timeout=self.timeout,
17178
)
17279
resp.raise_for_status()
80+
81+
82+
class ManagerAPI(object):
83+
84+
def __init__(self, timeout=10):
85+
self.timeout = timeout
86+
self.headers = PassportAPI(timeout=timeout).headers if self.enabled else None
87+
self.base_url = settings.WORKFLOW_MANAGER_URL.rstrip("/") if self.enabled else None
88+
89+
@property
90+
def enabled(self):
91+
return settings.WORKFLOW_MANAGER_URL is not None
92+
93+
def send_usage(self, usage: List[Dict[str, str]]):
94+
if not self.enabled:
95+
logger.info("WORKFLOW_MANAGER_URL is not set, skipping send_usage")
96+
return
97+
url = f"{self.base_url}/usage/"
98+
return requests.post(url=url, json=usage, headers=self.headers, timeout=self.timeout)
99+
100+
def get_status(self, resource_type: str, resource_id: str):
101+
if not self.enabled:
102+
logger.info("WORKFLOW_MANAGER_URL is not set, skipping get_status")
103+
return True, None
104+
key = f"{resource_type}:status:{resource_id}"
105+
status = cache.get(key)
106+
if not status:
107+
url = f"{self.base_url}/{resource_type}s/{resource_id}/status/"
108+
try:
109+
status = requests.get(url=url, headers=self.headers, timeout=self.timeout).json()
110+
except requests.exceptions.Timeout as ex:
111+
msg = f"request {resource_type} {resource_id} timeout, skipping verification."
112+
status = {"is_active": True, "message": msg}
113+
logger.error(msg)
114+
logger.exception(ex)
115+
cache.set(key, status, timeout=settings.DRYCC_CACHE_USER_TIME)
116+
return status.get("is_active", True), status.get("message", None)
117+
118+
def get_app_status(self, app_id):
119+
return self.get_status("app", app_id)
120+
121+
def get_user_status(self, user_id):
122+
return self.get_status("user", user_id)

rootfs/api/consumers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
from channels.generic.http import AsyncHttpConsumer
2222
from channels.generic.websocket import AsyncWebsocketConsumer
2323

24+
from .clients import ManagerAPI
2425
from .models.app import App
2526
from .models.volume import Volume
26-
from .permissions import IsAppUser, get_app_status
27+
from .permissions import IsAppUser
2728

2829

2930
class AppPermChecker(object):
3031
timeout = 60 * 60
32+
manager_api = ManagerAPI()
3133
check_permission = IsAppUser().has_object_permission
3234

3335
def __init__(self, scope):
@@ -44,7 +46,7 @@ async def has_perm(self):
4446
app = await App.objects.aget(id=app_id)
4547
request = namedtuple("Request", ["user", "method"])(self.scope["user"], "GET")
4648
if await sync_to_async(self.check_permission)(request, None, app):
47-
permission = await sync_to_async(get_app_status)(app)
49+
permission = await sync_to_async(self.manager_api.get_app_status)(app_id)
4850
else:
4951
permission = (False, "permission denied")
5052
if permission[0]:

rootfs/api/management/commands/upload_app_usage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from datetime import timedelta
55
from django.utils import timezone
66
from django.core.management.base import BaseCommand
7-
from django.conf import settings
87
from api.models.app import App
98
from api.tasks import send_usage
9+
from api.clients import ManagerAPI
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -15,7 +15,7 @@ class Command(BaseCommand):
1515
"""Management command for push data to manager"""
1616

1717
def handle(self, *args, **options):
18-
if settings.WORKFLOW_MANAGER_URL:
18+
if ManagerAPI().enabled:
1919
now = timezone.now()
2020
start_time, timestamp, task_id = now, int(now.timestamp()), uuid.uuid4().hex
2121
logger.info(f"pushing {task_id} resources to workflow_manager when {now}")

rootfs/api/management/commands/upload_gateway_usage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from datetime import timedelta
55
from django.utils import timezone
66
from django.core.management.base import BaseCommand
7-
from django.conf import settings
87
from api.models.gateway import Gateway
98
from api.tasks import send_usage
9+
from api.clients import ManagerAPI
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -15,7 +15,7 @@ class Command(BaseCommand):
1515
"""Management command for push data to manager"""
1616

1717
def handle(self, *args, **options):
18-
if settings.WORKFLOW_MANAGER_URL:
18+
if ManagerAPI().enabled:
1919
now = timezone.now()
2020
start_time, timestamp, task_id = now, int(now.timestamp()), uuid.uuid4().hex
2121
logger.info(f"pushing {task_id} gateways to workflow_manager when {now}")

rootfs/api/management/commands/upload_network_usage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from asgiref.sync import async_to_sync
77
from django.utils import timezone
88
from django.core.management.base import BaseCommand
9-
from django.conf import settings
109
from api import monitor
1110
from api.models.app import App
1211
from api.tasks import send_usage
12+
from api.clients import ManagerAPI
1313

1414
logger = logging.getLogger(__name__)
1515

@@ -44,7 +44,7 @@ def _upload_network_usage(self, start_time, app_map, timestamp):
4444
)
4545

4646
def handle(self, *args, **options):
47-
if settings.WORKFLOW_MANAGER_URL:
47+
if ManagerAPI().enabled:
4848
now = timezone.now()
4949
start_time, timestamp, task_id = now, int(now.timestamp()), uuid.uuid4().hex
5050
logger.info(f"pushing {task_id} networks to workflow_manager when {now}")

rootfs/api/management/commands/upload_resource_usage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from datetime import timedelta
55
from django.utils import timezone
66
from django.core.management.base import BaseCommand
7-
from django.conf import settings
87
from api.models.resource import Resource
98
from api.tasks import send_usage
9+
from api.clients import ManagerAPI
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -15,7 +15,7 @@ class Command(BaseCommand):
1515
"""Management command for push data to manager"""
1616

1717
def handle(self, *args, **options):
18-
if settings.WORKFLOW_MANAGER_URL:
18+
if ManagerAPI().enabled:
1919
now = timezone.now()
2020
start_time, timestamp, task_id = now, int(now.timestamp()), uuid.uuid4().hex
2121
logger.info(f"pushing {task_id} resources to workflow_manager when {now}")

rootfs/api/management/commands/upload_volume_usage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from asgiref.sync import async_to_sync
77
from django.utils import timezone
88
from django.core.management.base import BaseCommand
9-
from django.conf import settings
109
from api import monitor
1110
from api.models.app import App
1211
from api.tasks import send_usage
12+
from api.clients import ManagerAPI
1313

1414
logger = logging.getLogger(__name__)
1515

@@ -44,7 +44,7 @@ def _upload_volume_usage(self, start_time, app_map, timestamp):
4444
)
4545

4646
def handle(self, *args, **options):
47-
if settings.WORKFLOW_MANAGER_URL:
47+
if ManagerAPI().enabled:
4848
now = timezone.now()
4949
start_time, timestamp, task_id = now, int(now.timestamp()), uuid.uuid4().hex
5050
logger.info(f"pushing {task_id} volumes to workflow_manager when {now}")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.2.9 on 2026-05-20 08:51
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0029_key_api_key_fingerp_4e77c5_idx_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.DeleteModel(
14+
name='Blocklist',
15+
),
16+
migrations.RemoveField(
17+
model_name='app',
18+
name='suspended_state',
19+
),
20+
]

0 commit comments

Comments
 (0)