Commit 95d81690 authored by Lukas Burgey's avatar Lukas Burgey

Switch from using groups to VOs (groups + entitlements)

parent 84a5d56f
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
from . import models
from .models import brokers as broker_models
from .auth.v1.models import OIDCConfig
from .auth.v1.models.vo import VO, Group, Entitlement, EntitlementNameSpace
class TypeFilter(admin.SimpleListFilter):
......@@ -23,13 +25,32 @@ class TypeFilter(admin.SimpleListFilter):
class ClientAdmin(UserAdmin):
list_filter = (TypeFilter,)
@admin.register(Group)
class GroupAdmin(PolymorphicChildModelAdmin):
base_model = Group # Explicitly set here!
# define custom features here
@admin.register(Entitlement)
class EntitlementAdmin(PolymorphicChildModelAdmin):
base_model = Entitlement # Explicitly set here!
show_in_index = True # makes child model admin visible in main admin site
# define custom features here
@admin.register(VO)
class VOParentAdmin(PolymorphicParentModelAdmin):
""" The parent model admin """
base_model = VO # Optional, explicitly set here.
child_models = (Group, Entitlement)
admin.site.register(EntitlementNameSpace)
admin.site.register(OIDCConfig)
admin.site.register(broker_models.RabbitMQInstance)
admin.site.register(models.RabbitMQInstance)
admin.site.register(models.User, ClientAdmin)
admin.site.unregister(Group)
admin.site.register(Group)
admin.site.register(models.GroupDescription)
admin.site.register(models.Site)
admin.site.register(models.Service)
......
......@@ -39,6 +39,43 @@ class OIDCConfig(db_models.Model):
scopes = JSONField(
default=scopes_default,
editable=True,
help_text='The scopes we request when requesting user infos',
)
# ENTITLEMENT CHANGES
# path in the group tree to the VO Groups
# can be empty if we use the root
vo_subtree_path = db_models.CharField(
max_length=200,
blank=True,
null=True,
help_text='If not emtpy: Operate with groups of the described subtree of group (or entitlements). For example: Let\'s say the groups [/,/foo,/bar] exist and you set vo_subtree_path to "/". In that case the VO-Groups would be /foo and /bar',
)
# If True we shall ignore subgroups of the VO-Groups
# (VO-Group are the group on the path described by subtree_path)
ignore_subgroups = db_models.BooleanField(
default=False,
help_text='Ignore subgroups of VO describing groups. E.g. ignores the group :foo:bar if :foo exists.',
)
# The field in the userinfo (served by this IdP) that describes groups of the user
userinfo_field_groups = db_models.CharField(
max_length=200,
help_text="The field in the userinfo (served by this IdP) that contains groups of the user. Leave blank if you don't want to use groups of this IdP",
default=None,
blank=True,
null=True,
)
# The field in the userinfo (served by this IdP) that describes entitlements of the user
userinfo_field_entitlements = db_models.CharField(
max_length=200,
help_text="The field in the userinfo (served by this IdP) that contains entitlements of the user. Leave blank if you don't want to use entitlements of this IdP",
default=None,
blank=True,
null=True,
)
@property
......
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from ..vo import VO, Group, Entitlement, EntitlementNameSpace
class EntitlementNameSpaceSerializer(serializers.ModelSerializer):
class Meta:
model = EntitlementNameSpace
fields = ('name', )
# polymorphic serializer
VO_FIELDS = (
'id',
'name',
'pretty_name',
'description',
)
class AbstractVOSerializer(serializers.ModelSerializer):
class Meta:
model = VO
fields = VO_FIELDS
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = VO_FIELDS
class EntitlementSerializer(serializers.ModelSerializer):
name_space = EntitlementNameSpaceSerializer()
class Meta:
model = Entitlement
fields = VO_FIELDS + (
'name_space',
'group_authority',
'role',
)
class VOSerializer(PolymorphicSerializer):
model_serializer_mapping = {
VO: AbstractVOSerializer,
Group: GroupSerializer,
Entitlement: EntitlementSerializer,
}
import logging
import re
from polymorphic.models import PolymorphicModel
from django.db import models
from . import OIDCConfig
LOGGER = logging.getLogger(__name__)
class EntitlementNameSpace(models.Model):
name = models.CharField(
max_length=200,
unique=True,
)
@classmethod
def get_name_space(cls, name):
try:
return cls.objects.get(
name=name,
)
except cls.DoesNotExist:
name_space = cls(
name=name,
)
LOGGER.info('New EntitlementNameSpace: %s', name)
name_space.save()
return name_space
def __str__(self):
return self.name
class VO(PolymorphicModel):
name = models.CharField(
max_length=200,
unique=True,
)
idp = models.ForeignKey(
OIDCConfig,
related_name='vos',
on_delete=models.CASCADE,
blank=True,
null=True,
)
description = models.TextField(
max_length=1000,
blank=True,
null=True,
)
@property
def pretty_name(self):
return self.name
def __str__(self):
return self.name
class Group(VO):
@classmethod
def get_group(cls, name, idp=None):
try:
# searching only by name!
group = cls.objects.get(
name=name,
)
# use case: a client caused the group to be created
# we apply the idp here
if group.idp is None and idp is not None:
group.idp = idp
group.save()
return group
except cls.DoesNotExist:
group = cls(
name=name,
idp=idp,
)
LOGGER.info('New Group: %s', name)
group.save()
return group
@property
def pretty_name(self):
return self.name
@property
def broker_exchange(self):
return 'groups'
def __str__(self):
return 'VO-GROUP-'+self.name
class Entitlement(VO):
# Entitlement.name does not include the group-authority!
# for this we have Entitlement.full_name
full_name = models.CharField(
max_length=200,
)
name_space = models.ForeignKey(
EntitlementNameSpace,
related_name='entitlements',
on_delete=models.SET_NULL,
blank=True,
null=True,
)
group_authority = models.CharField(
max_length=200,
blank=True,
null=True,
)
role = models.CharField(
max_length=200,
blank=True,
null=True,
)
@property
def pretty_name(self):
return self.name
@property
def broker_exchange(self):
return 'entitlements'
@staticmethod
def extract_name(raw_name):
name_search = re.search('^(.*)#', raw_name)
if name_search:
return name_search.group(1)
return raw_name
@staticmethod
def extract_group_authority(raw_name):
group_authority_search = re.search('#(.*)$', raw_name)
if group_authority_search:
return group_authority_search.group(1)
return ''
@staticmethod
def extract_role(raw_name):
role_search = re.search(':role=(.*)#', raw_name)
if role_search:
return role_search.group(1)
return ''
@classmethod
def get_entitlement(cls, name, idp=None):
try:
# searching only by name! not idp
entitlement = cls.objects.get(
name=cls.extract_name(name),
)
# use case: a client caused the group to be created
# we apply the idp here
if entitlement.idp is None and idp is not None:
entitlement.idp = idp
entitlement.save()
return entitlement
except cls.DoesNotExist:
entitlement = cls(
name=cls.extract_name(name),
full_name=name,
group_authority=cls.extract_group_authority(name),
role=cls.extract_role(name),
idp=idp,
)
LOGGER.info('New Entitlement: %s', entitlement.name)
name_space_search = re.search('^(.*):group', name)
if name_space_search:
entitlement.name_space = EntitlementNameSpace.get_name_space(name_space_search.group(1))
entitlement.save()
return entitlement
@classmethod
def exists(cls, raw_name):
try:
cls.objects.get(
name=cls.extract_name(raw_name),
)
return True
except cls.DoesNotExist:
return False
def __str__(self):
return 'VO-ENTITLEMENT-'+self.name
......@@ -2,13 +2,13 @@
import logging
from django.contrib.auth.models import Group
from django.http import HttpResponse
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.sessions.models import Session
from .... import models
from feudal.backend.auth.v1.models.vo import Group, Entitlement
from feudal.backend import models
LOGGER = logging.getLogger(__name__)
......@@ -76,7 +76,7 @@ def _webpage_client_valid(request):
return session.get_decoded().get('_auth_user_id') == userid
except Session.DoesNotExist:
LOGGER.info("User %s has no session", userid)
# LOGGER.info("User %s has no session", userid)
return False
# VIEWS: authentication and authorization for
......@@ -229,22 +229,13 @@ def topic_endpoint_apiclient(request, apiclient):
name = request.POST.get('name', '')
routing_key = request.POST.get('routing_key', '')
if name == 'services':
# TODO is this check sufficient?
if apiclient.site.services.filter(name=routing_key).exists():
return ALLOW
elif name == 'sites':
if routing_key == apiclient.site.name:
return topic_auth_decision(request, ALLOW)
return topic_auth_decision(request, DENY)
elif name == 'groups':
if name == 'groups':
try:
group = Group.objects.get(name=routing_key)
try:
models.Site.objects.get(
services__groups=group,
services__vos=group,
client=apiclient,
)
return topic_auth_decision(request, ALLOW)
......@@ -258,6 +249,30 @@ def topic_endpoint_apiclient(request, apiclient):
except Group.DoesNotExist:
return topic_auth_decision(request, DENY)
if name == 'entitlements':
try:
entitlement = Entitlement.objects.get(
# we strip the group authority from the routing key if it was included
name=Entitlement.extract_name(routing_key),
)
try:
models.Site.objects.get(
services__vos=entitlement,
client=apiclient,
)
return topic_auth_decision(request, ALLOW)
except models.Site.MultipleObjectsReturned:
return topic_auth_decision(request, ALLOW)
except models.Site.DoesNotExist:
return topic_auth_decision(request, DENY)
except Entitlement.DoesNotExist:
LOGGER.error('Entitlement does not exist: %s', routing_key)
return topic_auth_decision(request, DENY)
return topic_auth_decision(request, DENY)
......
......@@ -13,6 +13,8 @@ from oic.oauth2.exception import HttpError
from rest_framework import generics, views
from rest_framework.permissions import AllowAny
from feudal.backend.views.webpage import state_view_data
from .. import utils
from ..models import OIDCConfig, default_idp
from ..models.serializers import AuthInfoSerializer
......
# Generated by Django 2.1.3 on 2018-11-14 17:39
from django.db import migrations, models
import django.db.models.deletion
import django_mysql.models
import feudal.backend.auth.v1.models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('backend', '0019_remove_newdeployment_service'),
]
operations = [
migrations.CreateModel(
name='EntitlementNameSpace',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, unique=True)),
],
),
migrations.CreateModel(
name='VO',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, unique=True)),
('description', models.TextField(blank=True, max_length=1000, null=True)),
],
options={
'base_manager_name': 'objects',
'abstract': False,
},
),
migrations.RemoveField(
model_name='groupdescription',
name='group',
),
migrations.RemoveField(
model_name='newdeployment',
name='group',
),
migrations.RemoveField(
model_name='service',
name='groups',
),
migrations.AddField(
model_name='oidcconfig',
name='ignore_subgroups',
field=models.BooleanField(default=False, help_text='Ignore subgroups of VO describing groups. E.g. ignores the group :foo:bar if :foo exists.'),
),
migrations.AddField(
model_name='oidcconfig',
name='userinfo_field_entitlements',
field=models.CharField(blank=True, default=None, help_text="The field in the userinfo (served by this IdP) that contains entitlements of the user. Leave blank if you don't want to use entitlements of this IdP", max_length=200, null=True),
),
migrations.AddField(
model_name='oidcconfig',
name='userinfo_field_groups',
field=models.CharField(blank=True, default=None, help_text="The field in the userinfo (served by this IdP) that contains groups of the user. Leave blank if you don't want to use groups of this IdP", max_length=200, null=True),
),
migrations.AddField(
model_name='oidcconfig',
name='vo_subtree_path',
field=models.CharField(blank=True, help_text='If not emtpy: Operate with groups of the described subtree of group (or entitlements). For example: Let\'s say the groups [/,/foo,/bar] exist and you set vo_subtree_path to "/". In that case the VO-Groups would be /foo and /bar', max_length=200, null=True),
),
migrations.AlterField(
model_name='oidcconfig',
name='scopes',
field=django_mysql.models.JSONField(default=feudal.backend.auth.v1.models.scopes_default, help_text='The scopes we request when requesting user infos'),
),
migrations.CreateModel(
name='Entitlement',
fields=[
('vo_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='backend.VO')),
('group_authority', models.CharField(max_length=200)),
('role', models.CharField(max_length=200)),
('name_space', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='entitlements', to='backend.EntitlementNameSpace')),
],
options={
'base_manager_name': 'objects',
'abstract': False,
},
bases=('backend.vo',),
),
migrations.CreateModel(
name='Group',
fields=[
('vo_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='backend.VO')),
],
options={
'base_manager_name': 'objects',
'abstract': False,
},
bases=('backend.vo',),
),
migrations.DeleteModel(
name='GroupDescription',
),
migrations.AddField(
model_name='vo',
name='idp',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vos', to='backend.OIDCConfig'),
),
migrations.AddField(
model_name='vo',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_backend.vo_set+', to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='newdeployment',
name='vo',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='deployments', to='backend.VO'),
),
migrations.AddField(
model_name='service',
name='vos',
field=models.ManyToManyField(blank=True, related_name='services', to='backend.VO'),
),
migrations.AddField(
model_name='user',
name='vos',
field=models.ManyToManyField(blank=True, to='backend.VO'),
),
migrations.AddField(
model_name='group',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_childs', to='backend.VO'),
),
migrations.AddField(
model_name='entitlement',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='entitlement_childs', to='backend.VO'),
),
]
# Generated by Django 2.1.3 on 2018-11-15 10:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('backend', '0020_auto_20181114_1839'),
]
operations = [
migrations.RemoveField(
model_name='entitlement',
name='parent',
),
migrations.RemoveField(
model_name='group',
name='parent',
),
migrations.AddField(
model_name='entitlement',
name='full_name',
field=models.CharField(default='', max_length=200),
preserve_default=False,
),
migrations.AlterField(
model_name='entitlement',
name='group_authority',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterField(
model_name='entitlement',
name='role',
field=models.CharField(blank=True, max_length=200, null=True),
),
]
......@@ -2,15 +2,14 @@
from json import dumps
from logging import getLogger
from django.contrib.auth.models import Group
from django.db import models
from django_mysql.models import JSONField
from django.conf import settings
# these imports are exports!
from ..auth.v1.models.vo import VO
from .brokers import RabbitMQInstance
from .users import User, SSHPublicKey
from .groups import GroupDescription
LOGGER = getLogger(__name__)
......@@ -83,26 +82,30 @@ class Site(models.Model):
class Service(models.Model):
name = models.CharField(
max_length=150,
unique=True,
)
description = models.TextField(
max_length=300,
blank=True,
)
site = models.ManyToManyField(
Site,
related_name='services',
)
groups = models.ManyToManyField(
Group,
vos = models.ManyToManyField(
VO,
related_name='services',