Skip to content

Commit

Permalink
Merge 5ac153f into bca751e
Browse files Browse the repository at this point in the history
  • Loading branch information
nuwang committed Nov 3, 2019
2 parents bca751e + 5ac153f commit 28c61e6
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 33 deletions.
9 changes: 8 additions & 1 deletion cloudman/cloudman/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
'djangooidc',
'clusterman',
'helmsman',
'projman'
'projman',
# Discover and apply permission rules in each project
'rules.apps.AutodiscoverRulesConfig'
]

AUTHENTICATION_BACKENDS = [
'rules.permissions.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend',
'bossoidc.backend.OpenIdConnectBackend'
]
Expand Down Expand Up @@ -60,6 +63,10 @@
FORCE_SCRIPT_NAME = CLOUDLAUNCH_PATH_PREFIX
REST_SCHEMA_BASE_URL = CLOUDLAUNCH_PATH_PREFIX + "/cloudman/cloudlaunch/"

REST_AUTH_SERIALIZERS = {
'USER_DETAILS_SERIALIZER': 'projman.serializers.UserSerializer'
}

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.' + os.environ.get('CLOUDMAN_DB_ENGINE', 'sqlite3'),
Expand Down
1 change: 1 addition & 0 deletions cloudman/cloudman/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
urlconf='cloudman.urls')

urlpatterns = [
url(r'^cloudman/cloudlaunch/cloudlaunch/api/v1/auth/user/', include('cloudlaunchserver.urls')),
url(r'^cloudman/', include('cloudlaunchserver.urls')),
url(r'^cloudman/api/v1/', include('clusterman.urls')),
url(r'^cloudman/api/v1/', include('helmsman.urls')),
Expand Down
66 changes: 56 additions & 10 deletions cloudman/projman/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ def context(self):
"""
return self._context

def has_permissions(self, scopes, obj=None):
if not isinstance(scopes, list):
scope = [scopes]
return self.context.user.has_perms(scope, obj)

def check_permissions(self, scopes, obj=None):
if not self.has_permissions(scopes, obj):
self.raise_no_permissions(scopes)

def raise_no_permissions(self, scopes):
raise PermissionError(
"Object does not exist or you do not have permissions to "
"perform '%s'" % (scopes,))


class ProjManAPI(PMService):

Expand All @@ -58,34 +72,51 @@ class PMProjectService(PMService):
def __init__(self, context):
super(PMProjectService, self).__init__(context)

def to_api_object(self, project):
# Remap the returned django model's delete method to the API method
# This is just a lazy alternative to writing an actual wrapper around
# the django object.
project.delete = lambda: self.delete(project.id)
return self.add_child_services(project)

def add_child_services(self, project):
project.service = self
project.charts = PMProjectChartService(self.context, project)
return project

def list(self):
return list(map(self.add_child_services,
models.CMProject.objects.all()))
return list(map(
self.to_api_object,
(proj for proj in models.CMProject.objects.all()
if self.has_permissions('projects.view_project', proj))))

def get(self, project_id):
return self.add_child_services(
models.CMProject.objects.get(id=project_id))
obj = models.CMProject.objects.get(id=project_id)
self.check_permissions('projects.view_project', obj)
return self.to_api_object(obj)

def create(self, name):
self.check_permissions('projects.add_project')
obj = models.CMProject.objects.create(
name=name)
project = self.add_child_services(obj)
name=name, owner=self.context.user)
project = self.to_api_object(obj)
return project

def delete(self, project_id):
obj = models.CMProject.objects.get(id=project_id)
if obj:
self.check_permissions('projects.delete_project', obj)
obj.delete()
else:
self.raise_no_permissions('projects.delete_project')

def find(self, name):
try:
return self.add_child_services(
models.CMProject.objects.get(name=name))
obj = models.CMProject.objects.get(name=name)
if self.has_permissions('projects.view_project', obj):
return self.to_api_object(obj)
else:
return None
except models.CMProject.DoesNotExist:
return None

Expand All @@ -101,25 +132,40 @@ def _get_helmsman_api(self):

def _to_proj_chart(self, chart):
chart.project = self.project
# Remap the helm API's delete method to the project chart API method
# This is just a lazy alternative to writing an actual wrapper around
# the HelmChart object.
chart.delete = lambda: self.delete(chart.id)
return chart

def list(self):
return [self._to_proj_chart(chart) for chart
in self._get_helmsman_api().charts.list()
if chart.namespace == self.project.name]
if chart.namespace == self.project.name and
self.has_permissions('charts.view_chart', chart)]

def get(self, chart_id):
chart = self._get_helmsman_api().charts.get(chart_id)
self.check_permissions('charts.view_chart', chart)
return (self._to_proj_chart(chart)
if chart and chart.namespace == self.project.name else None)

def create(self, repo_name, chart_name, release_name=None, version=None,
values=None):
self.check_permissions('charts.add_chart')
return self._to_proj_chart(self._get_helmsman_api().charts.create(
repo_name, chart_name, self.project.name, release_name, version,
values))

def update(self, chart, values):
self.check_permissions('charts.change_chart', chart)
updated_chart = self._get_helmsman_api().charts.update(chart, values)
return self._to_proj_chart(updated_chart)

def delete(self, chart_id):
obj = self.get(chart_id)
if obj:
obj.delete()
self.check_permissions('charts.delete_chart', obj)
self._get_helmsman_api().charts.delete(obj)
else:
self.raise_no_permissions('charts.delete_chart')
30 changes: 30 additions & 0 deletions cloudman/projman/migrations/0002_cmproject_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.2.6 on 2019-11-01 13:38

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import migrations, models
import django.db.models.deletion


User = get_user_model()
try:
default_owner = User.objects.filter(is_superuser=True).first().id
except:
default_owner = None


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('projman', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='cmproject',
name='owner',
field=models.ForeignKey(default=default_owner, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]
3 changes: 3 additions & 0 deletions cloudman/projman/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.conf import settings
from django.db import models


Expand All @@ -9,6 +10,8 @@ class CMProject(models.Model):
updated = models.DateTimeField(auto_now=True)
# Each project corresponds to a k8s namespace and therefore, must be unique
name = models.CharField(max_length=60, unique=True)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
null=False)

class Meta:
verbose_name = "Project"
Expand Down
30 changes: 30 additions & 0 deletions cloudman/projman/rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import rules

# Delegate to keycloak in future iteration

# Predicates
@rules.predicate
def is_project_owner(user, project):
if not project:
return False
return project.owner == user


@rules.predicate
def is_chart_owner(user, proj_chart):
# Should have update rights on the parent project
if not proj_chart:
return False
return user.has_perm('projects.change_project', proj_chart.project)


# Permissions
rules.add_perm('projects.view_project', rules.is_authenticated)
rules.add_perm('projects.add_project', is_project_owner | rules.is_staff)
rules.add_perm('projects.change_project', is_project_owner | rules.is_staff)
rules.add_perm('projects.delete_project', is_project_owner | rules.is_staff)

rules.add_perm('charts.view_chart', rules.is_authenticated)
rules.add_perm('charts.add_chart', is_chart_owner | rules.is_staff)
rules.add_perm('charts.change_chart', is_chart_owner | rules.is_staff)
rules.add_perm('charts.delete_chart', is_chart_owner | rules.is_staff)
44 changes: 43 additions & 1 deletion cloudman/projman/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""DRF serializers for the CloudMan Create API endpoints."""

from rest_framework import serializers
from cloudlaunch import serializers as cl_serializers
from djcloudbridge import serializers as dj_serializers
from helmsman import serializers as helmsman_serializers
from .api import ProjManAPI
from rest_framework.exceptions import ValidationError
Expand All @@ -10,6 +10,17 @@
class PMProjectSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True)
name = serializers.CharField()
permissions = serializers.SerializerMethodField()

def get_permissions(self, project):
"""
Implementation of permissions field
"""
user = self.context['view'].request.user
return {
'change_project': user.has_perm('projects.change_project', project),
'delete_project': user.has_perm('projects.delete_project', project)
}

def create(self, valid_data):
return ProjManAPI.from_request(self.context['request']).projects.create(
Expand All @@ -20,6 +31,17 @@ class PMProjectChartSerializer(helmsman_serializers.HMChartSerializer):
# remove the inherited field
namespace = None
project = PMProjectSerializer(read_only=True)
permissions = serializers.SerializerMethodField()

def get_permissions(self, chart):
"""
Implementation of permissions field
"""
user = self.context['view'].request.user
return {
'change_chart': user.has_perm('charts.change_chart', chart),
'delete_chart': user.has_perm('charts.delete_chart', chart)
}

def create(self, valid_data):
project_id = self.context['view'].kwargs.get("project_pk")
Expand All @@ -31,3 +53,23 @@ def create(self, valid_data):
valid_data.get('repo_name', 'cloudve'), valid_data.get('name'),
valid_data.get('release_name'), valid_data.get('chart_version'),
valid_data.get('values'))

def update(self, chart, validated_data):
project_id = self.context['view'].kwargs.get("project_pk")
project = ProjManAPI.from_request(self.context['request']).projects.get(project_id)
if not project:
raise ValidationError("Specified project id: %s does not exist"
% project_id)
return project.charts.update(chart, validated_data)


class UserSerializer(dj_serializers.UserDetailsSerializer):
permissions = serializers.SerializerMethodField()

def get_permissions(self, user_obj):
return {
'is_admin': user_obj.is_staff
}

class Meta(dj_serializers.UserDetailsSerializer.Meta):
fields = dj_serializers.UserDetailsSerializer.Meta.fields + ('permissions',)

0 comments on commit 28c61e6

Please sign in to comment.