Skip to content

Commit 9ce6d9f

Browse files
committed
chore(channels): add cache user permission
1 parent 3de1269 commit 9ce6d9f

13 files changed

Lines changed: 96 additions & 96 deletions

File tree

rootfs/api/authentication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def authenticate_credentials(self, key):
6565
from api.backend import OauthCacheManager
6666
token.refresh_token()
6767
user = OauthCacheManager().get_user(token.oauth['access_token'])
68-
cache.set(token, user, timeout=token.oauth['expires_in'])
68+
cache.set(key, user, timeout=token.oauth['expires_in'])
6969
return user, token.key
7070
return (token.owner, token.key)
7171

rootfs/api/backend.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,27 @@ class DryccOIDC(OpenIdConnectAuth):
3232
('scope', 'scope'),
3333
]
3434

35+
def __init__(self, *args, **kwargs):
36+
self.timeout = 3 # request timeout
37+
super().__init__(*args, **kwargs)
38+
3539
@social_cache(ttl=86400)
3640
def oidc_config(self):
37-
return self.get_json(self.OIDC_ENDPOINT +
38-
'/.well-known/openid-configuration/')
41+
return self.get_json(
42+
self.OIDC_ENDPOINT + '/.well-known/openid-configuration/',
43+
timeout=self.timeout
44+
)
3945

4046
def get_user_data(self, access_token):
4147
"""Loads user data from service"""
4248
url = settings.SOCIAL_AUTH_DRYCC_USERINFO_URL
43-
response = self.get_json(url, headers={
44-
'authorization': 'Bearer ' + access_token})
49+
response = self.get_json(
50+
url,
51+
headers={
52+
'authorization': 'Bearer ' + access_token
53+
},
54+
timeout=self.timeout,
55+
)
4556
return {
4657
'id': response.get('id'),
4758
'username': response.get('username'),
@@ -62,13 +73,13 @@ def refresh_token(self, refresh_token):
6273
'client_id': settings.SOCIAL_AUTH_DRYCC_KEY,
6374
'refresh_token': refresh_token,
6475
},
76+
timeout=self.timeout,
6577
)
6678

6779

6880
class OauthCacheManager(object):
6981

70-
def __init__(self, timeout=60 * 10):
71-
self.timeout = timeout
82+
def __init__(self):
7283
self.drycc_oauth = DryccOIDC()
7384

7485
def get_user(self, access_token):
@@ -82,13 +93,13 @@ def _get_user(access_token):
8293
logger.info(e)
8394
raise exceptions.AuthenticationFailed(gettext_lazy('Verify token fail.'))
8495
return cache.get_or_set(
85-
access_token, lambda: _get_user(access_token), settings.OAUTH_CACHE_USER_TIME)
96+
access_token, lambda: _get_user(access_token), settings.DRYCC_CACHE_USER_TIME)
8697

8798
def set_state(self, key, state):
88-
cache.set("oidc_key_" + key, state, self.timeout)
99+
cache.set("oidc_key_" + key, state, settings.DRYCC_CACHE_USER_TIME)
89100

90101
def set_token(self, state, data):
91-
cache.set("oidc_state_" + state, data, self.timeout)
102+
cache.set("oidc_state_" + state, data, settings.DRYCC_CACHE_USER_TIME)
92103

93104
def get_token(self, key):
94105
state = cache.get("oidc_key_" + key, "")

rootfs/api/consumers.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import ssl
55
import aiohttp
66
import asyncio
7-
import collections
87
from django.conf import settings
8+
from django.core.cache import cache
99

1010
from asgiref.sync import sync_to_async, async_to_sync
1111

@@ -22,21 +22,24 @@
2222
from .permissions import has_app_permission
2323

2424

25-
Request = collections.namedtuple("Request", ["user", "method"])
26-
27-
2825
class BaseAppConsumer(AsyncWebsocketConsumer):
26+
timeout = 60 * 60
2927

3028
@database_sync_to_async
3129
def has_perm(self):
3230
if self.scope["user"] is None:
3331
return False, "user not login"
34-
request = Request(self.scope["user"], "POST")
35-
try:
36-
app = App.objects.get(id=self.id)
37-
return has_app_permission(request, app)
38-
except App.DoesNotExist:
39-
return False, "user not exists"
32+
key = f"permission:user:{self.scope["user"].id}:app:{self.id}"
33+
permission = cache.get(key)
34+
if permission is None:
35+
try:
36+
app = App.objects.get(id=self.id)
37+
permission = has_app_permission(self.scope["user"], app, "GET")
38+
if permission[0]:
39+
cache.set(key, permission, timeout=self.timeout)
40+
except App.DoesNotExist:
41+
permission = (False, "user not exists")
42+
return permission
4043

4144
async def connect(self):
4245
self.id = self.scope["url_route"]["kwargs"]["id"]

rootfs/api/manager.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import base64
2+
import logging
23
import requests
34
from typing import List, Dict
5+
from django.core.cache import cache
46
from requests_toolbelt import user_agent
57
from django.conf import settings
68
from api import __version__ as drycc_version
79

10+
logger = logging.getLogger(__name__)
11+
812

913
class ManagerAPI(object):
1014

11-
def __init__(self):
15+
def __init__(self, timeout=3):
16+
self.timeout = timeout
1217
token = base64.b85encode(b"%s:%s" % (
1318
settings.WORKFLOW_MANAGER_ACCESS_KEY.encode("utf8"),
1419
settings.WORKFLOW_MANAGER_SECRET_KEY.encode("utf8"),
@@ -23,6 +28,7 @@ def request(self, method, url, **kwargs):
2328
headers = kwargs.get("headers", {})
2429
headers.update(self.headers)
2530
kwargs["headers"] = headers
31+
kwargs["timeout"] = self.timeout
2632
return requests.request(method, url, **kwargs)
2733

2834
def get(self, url, params=None, **kwargs):
@@ -57,8 +63,19 @@ def get_status(self, id):
5763
"message": "The user is in arrears"
5864
}
5965
"""
60-
url = f"{settings.WORKFLOW_MANAGER_URL}/users/{id}/status/"
61-
return self.get(url=url).json()
66+
key = f"user:status:{id}"
67+
status = cache.get(key)
68+
if not status:
69+
url = f"{settings.WORKFLOW_MANAGER_URL}/users/{id}/status/"
70+
try:
71+
status = self.get(url=url, timeout=self.timeout).json()
72+
except requests.exceptions.Timeout as ex:
73+
msg = f"request user {id} timeout, skipping verification."
74+
status = {"is_active": True, "message": msg}
75+
logger.error(msg)
76+
logger.exception(ex)
77+
cache.set(key, status, timeout=settings.DRYCC_CACHE_USER_TIME)
78+
return status
6279

6380

6481
class Measurement(ManagerAPI):

rootfs/api/middleware.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
44
See https://docs.djangoproject.com/en/1.11/topics/http/middleware/
55
"""
6-
import collections
76
from api import __version__
7+
from django.http.request import HttpRequest
88

99
from channels.middleware import BaseMiddleware
1010
from channels.db import database_sync_to_async
@@ -36,18 +36,20 @@ class ChannelOAuthMiddleware(BaseMiddleware):
3636
"""
3737
Middleware which populates scope["user"] from a auth2 token.
3838
"""
39-
Request = collections.namedtuple('Request', ["META"])
39+
authentication = DryccAuthentication()
4040

41-
async def get_user(self, scope):
41+
@database_sync_to_async
42+
def get_user(self, scope):
4243
headers = {}
4344
for header in scope["headers"]:
44-
if header[0] in (b"user-agent", b"authorization"):
45+
if header[0] in (b"authorization", ):
4546
key = "HTTP_%s" % header[0].decode().replace("-", "_").upper()
4647
headers[key] = header[1].decode()
47-
if len(headers) != 2:
48+
if len(headers) < 1:
4849
return None
49-
request = self.Request(headers)
50-
user, _ = await database_sync_to_async(DryccAuthentication().authenticate)(request)
50+
request = HttpRequest()
51+
request.META = headers
52+
user, _ = self.authentication.authenticate(request)
5153
return user
5254

5355
async def __call__(self, scope, receive, send):

rootfs/api/models/app.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import backoff
21
import base64
32
import functools
43
import json
@@ -341,33 +340,6 @@ def cleanup_old(self, procfile_types=None):
341340
self.scheduler().deployments.delete(self.id, name, True)
342341
self.log(f"cleanup old kubernetes deployments for {self.id}")
343342

344-
@backoff.on_exception(backoff.expo, ServiceUnavailable, max_tries=3)
345-
def logs(self, log_lines=str(settings.LOG_LINES)):
346-
"""Return aggregated log data for this application."""
347-
url = "http://{}:{}/logs/{}?log_lines={}".format(
348-
settings.LOGGER_HOST, settings.LOGGER_PORT, self.id, log_lines)
349-
try:
350-
r = requests.get(url)
351-
# Handle HTTP request errors
352-
except requests.exceptions.RequestException as e:
353-
msg = "Error accessing drycc-logger using url '{}': {}".format(url, e)
354-
logger.error(msg)
355-
raise ServiceUnavailable(msg) from e
356-
357-
# Handle logs empty or not found
358-
if r.status_code == 204 or r.status_code == 404:
359-
logger.info("GET {} returned a {} status code".format(url, r.status_code))
360-
raise NotFound('Could not locate logs')
361-
362-
# Handle unanticipated status codes
363-
if r.status_code != 200:
364-
logger.error("Error accessing drycc-logger: GET {} returned a {} status code"
365-
.format(url, r.status_code))
366-
raise ServiceUnavailable('Error accessing drycc-logger')
367-
368-
# cast content to string since it comes as bytes via the requests object
369-
return str(r.content.decode('utf-8'))
370-
371343
def run(self, user, image=None, command=None, args=None, volumes=None,
372344
timeout=3600, expires=3600, **kwargs):
373345
def pod_name(size=5, chars=string.ascii_lowercase + string.digits):

rootfs/api/models/certificate.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ def domains(self):
130130

131131
return domains
132132

133+
@property
134+
def certname(self):
135+
return '%s-certificate' % self.name
136+
133137
def __str__(self):
134138
return self.name
135139

@@ -197,24 +201,23 @@ def attach_in_kubernetes(self, domain):
197201
"""Creates the certificate as a kubernetes secret"""
198202
# only create if it exists - We raise an exception when a secret doesn't exist
199203
try:
200-
name = '%s-certificate' % self.name
201204
namespace = domain.app.id
202205
data = {
203206
'tls.crt': self.certificate,
204207
'tls.key': self.key
205208
}
206209

207-
secret = self.scheduler().secret.get(namespace, name).json()['data']
210+
secret = self.scheduler().secret.get(namespace, self.certname).json()['data']
208211
except KubeException:
209-
self.scheduler().secret.create(namespace, name, data)
212+
self.scheduler().secret.create(namespace, self.certname, data)
210213
else:
211214
# update cert secret to the TLS Ingress format if required
212215
if secret != data:
213216
try:
214-
self.scheduler().secret.update(namespace, name, data)
217+
self.scheduler().secret.update(namespace, self.certname, data)
215218
except KubeException as e:
216219
msg = 'There was a problem updating the certificate secret ' \
217-
'{} for {}'.format(name, namespace)
220+
'{} for {}'.format(self.certname, namespace)
218221
raise ServiceUnavailable(msg) from e
219222

220223
def detach(self, *args, **kwargs):
@@ -223,14 +226,15 @@ def detach(self, *args, **kwargs):
223226
domain.certificate = None
224227
domain.save()
225228

226-
name = '%s-certificate' % self.name
227229
namespace = domain.app.id
228230

229231
# only delete if it exists and if no other domains depend on secret
230232
if len(self.domains) == 0:
231233
try:
232234
# We raise an exception when a secret doesn't exist
233-
self.scheduler().secret.get(namespace, name)
234-
self.scheduler().secret.delete(namespace, name)
235+
self.scheduler().secret.get(namespace, self.certname)
236+
self.scheduler().secret.delete(namespace, self.certname)
235237
except KubeException as e:
236-
raise ServiceUnavailable("Could not delete certificate secret {} for application {}".format(name, namespace)) from e # noqa
238+
raise ServiceUnavailable(
239+
"Could not delete certificate secret {} for application {}".format(
240+
self.certname, namespace)) from e

rootfs/api/models/gateway.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def listeners(self):
6868
"protocol": protocol,
6969
}
7070
secret_name = f"{self.app.id}-auto-tls" if auto_tls else (
71-
domain.certificate.name if domain.certificate else None)
71+
domain.certificate.certname if domain.certificate else None)
7272
if secret_name and protocol in TLS_PROTOCOLS:
7373
listener["tls"] = {
7474
"certificateRefs": [{"kind": "Secret", "name": secret_name}]}

rootfs/api/permissions.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ def get_app_status(app):
1717
return True, None
1818

1919

20-
def has_app_permission(request, obj):
20+
def has_app_permission(user, obj, method):
2121
if isinstance(obj, App) or hasattr(obj, 'app'):
2222
app = obj if isinstance(obj, App) else obj.app
2323
is_ok, message = get_app_status(app)
2424
if is_ok:
25-
if request.user.is_superuser:
25+
if user.is_superuser:
2626
return True, None
27-
elif app.owner == request.user:
27+
elif app.owner == user:
2828
return True, None
29-
elif request.user.is_staff or request.user.has_perm('use_app', app):
30-
if request.method != 'DELETE':
29+
elif user.is_staff or user.has_perm('use_app', app):
30+
if method != 'DELETE':
3131
return True, None
3232
else:
3333
return False, "User does not have permission to delete"
@@ -81,7 +81,7 @@ class IsAppUser(permissions.BasePermission):
8181
an app-related model.
8282
"""
8383
def has_object_permission(self, request, view, obj):
84-
return has_app_permission(request, obj)[0]
84+
return has_app_permission(request.user, obj, request.method)[0]
8585

8686

8787
class IsAdmin(permissions.BasePermission):

rootfs/api/settings/production.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@
457457
'social_core.pipeline.user.user_details',
458458
)
459459
AUTHENTICATION_BACKENDS = ("api.backend.DryccOIDC", ) + AUTHENTICATION_BACKENDS
460-
OAUTH_CACHE_USER_TIME = int(os.environ.get('OAUTH_CACHE_USER_TIME', 30 * 60))
460+
DRYCC_CACHE_USER_TIME = int(os.environ.get('DRYCC_CACHE_USER_TIME', 30 * 60))
461461

462462
# Redis Configuration
463463
DRYCC_REDIS_ADDRS = os.environ.get('DRYCC_REDIS_ADDRS', '127.0.0.1:6379').split(",")

0 commit comments

Comments
 (0)