Skip to content

Commit 4d8a20d

Browse files
committed
feat(controller): add check app owner status
1 parent 222f7b5 commit 4d8a20d

7 files changed

Lines changed: 123 additions & 65 deletions

File tree

rootfs/api/authentication.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from rest_framework.authentication import TokenAuthentication, \
88
get_authorization_header
99
from rest_framework import exceptions
10-
11-
from api import manager
1210
from api.oauth import OAuthManager
1311

1412
logger = logging.getLogger(__name__)
@@ -66,10 +64,6 @@ def _get_user(key):
6664
user_info = OAuthManager().get_user_by_token(key)
6765
if not user_info.get('email'):
6866
user_info['email'] = OAuthManager().get_email_by_token(key)
69-
if settings.WORKFLOW_MANAGER_URL and settings.WORKFLOW_MANAGER_TOKEN:
70-
status = manager.User().get_status(user_info.username)
71-
if not status["is_active"]:
72-
raise exceptions.AuthenticationFailed(_(status["message"]))
7367
except Exception as e:
7468
logger.info(e)
7569
raise exceptions.AuthenticationFailed(_('Verify token fail.'))

rootfs/api/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ class Meta:
140140

141141
from .app import App, validate_app_id, validate_reserved_names, validate_app_structure # noqa
142142
from .appsettings import AppSettings # noqa
143+
from .blocklist import Blocklist # noqa
143144
from .build import Build # noqa
144145
from .certificate import Certificate, validate_certificate # noqa
145146
from .config import Config # noqa

rootfs/api/permissions.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1+
import base64
12
from rest_framework import permissions
23
from django.conf import settings
34
from django.contrib.auth.models import AnonymousUser
5+
from api.models import Blocklist, App
46

5-
from api import models
67

7-
8-
def is_app_user(request, obj):
9-
if request.user.is_superuser or \
10-
isinstance(obj, models.App) and obj.owner == request.user or \
11-
hasattr(obj, 'app') and obj.app.owner == request.user:
12-
return True
13-
elif request.user.has_perm('use_app', obj) or \
14-
hasattr(obj, 'app') and request.user.has_perm('use_app', obj.app):
15-
return request.method != 'DELETE'
16-
else:
17-
return False
8+
def has_app_permission(request, obj):
9+
if isinstance(obj, App) or hasattr(obj, 'app'):
10+
app = obj if isinstance(obj, App) else obj.app
11+
blocklist = Blocklist.get_blocklist(app)
12+
if blocklist:
13+
return False, blocklist.remark
14+
if request.user.is_superuser:
15+
return True, None
16+
elif app.owner == request.user:
17+
return True, None
18+
elif request.user.is_staff or request.user.has_perm('use_app', app):
19+
if request.method != 'DELETE':
20+
return True, None
21+
else:
22+
return False, "User does not have permission to delete"
23+
return False, "App object does not exist or does not have permission."
1824

1925

2026
class IsAnonymous(permissions.BasePermission):
@@ -62,7 +68,7 @@ class IsAppUser(permissions.BasePermission):
6268
an app-related model.
6369
"""
6470
def has_object_permission(self, request, view, obj):
65-
return is_app_user(request, obj)
71+
return has_app_permission(request, obj)[0]
6672

6773

6874
class IsAdmin(permissions.BasePermission):
@@ -106,3 +112,20 @@ def has_permission(self, request, view):
106112
if not auth_header:
107113
return False
108114
return auth_header == settings.BUILDER_KEY
115+
116+
117+
class IsWorkflowManager(permissions.BasePermission):
118+
"""
119+
View permission to allow workflow manager to perform actions
120+
with a special HTTP header
121+
"""
122+
123+
def has_permission(self, request, view):
124+
if request.META.get("HTTP_AUTHORIZATION"):
125+
token = request.META.get(
126+
"HTTP_AUTHORIZATION").split(" ")[1].encode("utf8")
127+
access_key, secret_key = base64.b85decode(token).decode("utf8").split(":")
128+
if settings.WORKFLOW_MANAGER_ACCESS_KEY == access_key:
129+
if settings.WORKFLOW_MANAGER_SECRET_KEY == secret_key:
130+
return True
131+
return False

rootfs/api/settings/testing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
app.conf.update(task_always_eager=True)
1111
signals.request_started.send = lambda sender, **named: []
1212
signals.request_finished.send = lambda sender, **named: []
13+
signals.got_request_exception.send = lambda sender, **named: []
1314

1415
# A boolean that turns on/off debug mode.
1516
# https://docs.djangoproject.com/en/1.11/ref/settings/#debug

rootfs/api/tasks.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ def retrieve_resource(self, resource):
2525
raise self.retry(exc=None, countdown=1800)
2626
else:
2727
resource.detach_resource()
28-
except Resource.DoesNotExist:
29-
logger.exception(
30-
"retrieve task not found resource: {}".format(resource.id))
31-
except Exception as e:
28+
except (Exception, Resource.DoesNotExist) as e:
3229
signals.got_request_exception.send(sender=task_id)
33-
raise e
34-
finally:
30+
if isinstance(e, Resource.DoesNotExist):
31+
logger.exception("retrieve task not found resource: {}".format(resource.id))
32+
else:
33+
raise e
34+
else:
3535
signals.request_finished.send(sender=task_id)
3636

3737

@@ -51,7 +51,7 @@ def send_measurements(measurements: List[Dict[str, str]]):
5151
except Exception as e:
5252
signals.got_request_exception.send(sender=task_id)
5353
raise e
54-
finally:
54+
else:
5555
signals.request_finished.send(sender=task_id)
5656

5757

@@ -68,7 +68,7 @@ def scale_app(app, user, structure):
6868
except Exception as e:
6969
signals.got_request_exception.send(sender=task_id)
7070
raise e
71-
finally:
71+
else:
7272
signals.request_finished.send(sender=task_id)
7373

7474

@@ -85,5 +85,5 @@ def restart_app(app, **kwargs):
8585
except Exception as e:
8686
signals.got_request_exception.send(sender=task_id)
8787
raise e
88-
finally:
88+
else:
8989
signals.request_finished.send(sender=task_id)

rootfs/api/urls.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,20 @@
144144
views.UserView.as_view({'patch': 'enable'})),
145145
url(r'^users/(?P<username>[\w.@+-]+)/disable/?$',
146146
views.UserView.as_view({'patch': 'disable'})),
147-
url(r'^apps/(?P<id>{})/metrics/(?P<container_type>[a-z0-9]+(\-[a-z0-9]+)*)/?$'.format(settings.APP_URL_REGEX), # noqa
147+
url(r'^apps/(?P<id>{})/metrics/(?P<container_type>[a-z0-9]+(\-[a-z0-9]+)*)/?$'.format(
148+
settings.APP_URL_REGEX),
148149
views.MetricView.as_view({'get': 'status'})),
150+
url(r'^manager/(?P<type>[\w.@+-]+)s/(?P<id>{})/block/?$'.format(settings.APP_URL_REGEX),
151+
views.WorkflowManagerViewset.as_view({'post': 'block'})),
152+
url(r'^manager/(?P<type>[\w.@+-]+)s/(?P<id>{})/unblock/?$'.format(settings.APP_URL_REGEX),
153+
views.WorkflowManagerViewset.as_view({'delete': 'unblock'})),
149154
]
150155

151156
webhook_urlpatterns = [
152-
url(r'^webhooks/scale/(?P<token>.+)/?$', views.AdmissionWebhook.as_view({'post': 'scale'})),
157+
url(
158+
r'^webhooks/scale/(?P<token>.+)/?$',
159+
views.AdmissionWebhookViewSet.as_view({'post': 'scale'})
160+
),
153161
]
154162

155163
# If there is a mutating admission webhook configuration, use webhook url

rootfs/api/views.py

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,63 @@ def list(self, request, **kwargs):
119119
return Response(serializer.data)
120120

121121

122+
class WorkflowManagerViewset(GenericViewSet):
123+
124+
permission_classes = (permissions.IsWorkflowManager, )
125+
126+
def block(self, request, **kwargs):
127+
try:
128+
blocklist = models.Blocklist(
129+
id=kwargs['id'],
130+
type=models.Blocklist.get_type(kwargs["type"]),
131+
remark=request.data.get("remark")
132+
)
133+
apps = blocklist.related_apps
134+
[scale_app(app, app.owner, {key: 0 for key in app.structure.keys()}) for app in apps]
135+
blocklist.save()
136+
except ValueError as e:
137+
logger.info(e)
138+
raise DryccException("Unsupported block type: %s" % kwargs["type"])
139+
140+
def unblock(self, request, **kwargs):
141+
try:
142+
models.Blocklist.objects.filter(
143+
id=kwargs['id'],
144+
type=models.Blocklist.get_type(kwargs["type"])
145+
).delete()
146+
except ValueError as e:
147+
logger.info(e)
148+
raise DryccException("Unsupported block type: %s" % kwargs["type"])
149+
150+
151+
class AdmissionWebhookViewSet(GenericViewSet):
152+
153+
permission_classes = (AllowAny, )
154+
155+
def scale(self, request, **kwargs):
156+
token = kwargs['token']
157+
data = json.loads(request.body.decode("utf8"))["request"]
158+
if settings.DRYCC_ADMISSION_WEBHOOK_TOKEN == token:
159+
allowed = True
160+
app_id = data["object"]["metadata"]["namespace"]
161+
app = models.App.objects.filter(id=app_id).first()
162+
replicas = data["object"]["spec"].get("replicas", 0)
163+
container_type = data["object"]["metadata"]["name"].replace(f"{app_id}-", "", 1)
164+
if app and app.structure.get(container_type) != replicas: # sync replicas
165+
app.structure[container_type] = replicas
166+
super(models.App, app).save(update_fields=["structure", ])
167+
else:
168+
allowed = False
169+
return Response({
170+
"apiVersion": "admission.k8s.io/v1",
171+
"kind": "AdmissionReview",
172+
"response": {
173+
"uid": data["uid"],
174+
"allowed": allowed,
175+
}
176+
})
177+
178+
122179
class BaseDryccViewSet(viewsets.OwnerViewSet):
123180
"""
124181
A generic ViewSet for objects related to Drycc.
@@ -508,8 +565,9 @@ def users(self, request, *args, **kwargs):
508565
app = get_object_or_404(models.App, id=kwargs['id'])
509566
request.user = get_object_or_404(User, username=kwargs['username'])
510567
# check the user is authorized for this app
511-
if not permissions.is_app_user(request, app):
512-
raise PermissionDenied()
568+
has_permission, message = permissions.has_app_permission(request, app)
569+
if not has_permission:
570+
raise PermissionDenied(message)
513571

514572
data = {request.user.username: []}
515573
keys = models.Key.objects \
@@ -537,8 +595,9 @@ def create(self, request, *args, **kwargs):
537595
app = get_object_or_404(models.App, id=request.data['receive_repo'])
538596
self.user = request.user = get_object_or_404(User, username=request.data['receive_user'])
539597
# check the user is authorized for this app
540-
if not permissions.is_app_user(request, app):
541-
raise PermissionDenied()
598+
has_permission, message = permissions.has_app_permission(request, app)
599+
if not has_permission:
600+
raise PermissionDenied(message)
542601
request.data['app'] = app
543602
request.data['owner'] = self.user
544603
super(BuildHookViewSet, self).create(request, *args, **kwargs)
@@ -559,8 +618,9 @@ def create(self, request, *args, **kwargs):
559618
app = get_object_or_404(models.App, id=request.data['receive_repo'])
560619
request.user = get_object_or_404(User, username=request.data['receive_user'])
561620
# check the user is authorized for this app
562-
if not permissions.is_app_user(request, app):
563-
raise PermissionDenied()
621+
has_permission, message = permissions.has_app_permission(request, app)
622+
if not has_permission:
623+
raise PermissionDenied(message)
564624
config = app.release_set.filter(failed=False).latest().config
565625
serializer = self.get_serializer(config)
566626
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -908,32 +968,3 @@ def status(self, request, **kwargs):
908968
"networks": self._get_networks(
909969
app_id, container_type, start, stop, every)
910970
})
911-
912-
913-
class AdmissionWebhook(GenericViewSet):
914-
915-
permission_classes = (AllowAny, )
916-
917-
def scale(self, request, **kwargs):
918-
token = kwargs['token']
919-
print(request.body.decode("utf8"))
920-
data = json.loads(request.body.decode("utf8"))["request"]
921-
if settings.DRYCC_ADMISSION_WEBHOOK_TOKEN == token:
922-
allowed = True
923-
app_id = data["object"]["metadata"]["namespace"]
924-
app = models.App.objects.filter(id=app_id).first()
925-
replicas = data["object"]["spec"].get("replicas", 0)
926-
container_type = data["object"]["metadata"]["name"].replace(f"{app_id}-", "", 1)
927-
if app and app.structure.get(container_type) != replicas: # sync replicas
928-
app.structure[container_type] = replicas
929-
super(models.App, app).save(update_fields=["structure", ])
930-
else:
931-
allowed = False
932-
return Response({
933-
"apiVersion": "admission.k8s.io/v1",
934-
"kind": "AdmissionReview",
935-
"response": {
936-
"uid": data["uid"],
937-
"allowed": allowed,
938-
}
939-
})

0 commit comments

Comments
 (0)