Commit e03de59f authored by Marcus's avatar Marcus
Browse files

initial template

parent 83114780
*swp
pyve
*.conf
delme
Changelog
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
doc/_build/
doc/build/
# PyBuilder
target/
# DotEnv configuration
.env
# Database
*.db
*.rdb
# Pycharm
.idea
# VS Code
.vscode/
# Spyder
.spyproject/
# Jupyter NB Checkpoints
.ipynb_checkpoints/
# exclude data from source control by default
#data/
# exclude rclone related stuff
.rclone/
rclone/
.rclone
rclone.conf
# Mac OS-specific storage files
.DS_Store
AUTHORS
ChangeLog
cache.sqlite
source.tgz
Dockerfile
AUTHORS
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
builder: html
configuration: doc/source/conf.py
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
formats: [] # no PDF / ePub
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: doc/source/requirements.txt # e.g. sphinx
- requirements: requirements.txt # project dependencies
- method: setuptools
path: .
MIT License
Copyright (c) 2019 Karlsruhe Institute of Technology - Steinbuch Centre for Computing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''Check entitlements according to the AARC G002 recommendation
https://aarc-project.eu/guidelines/aarc-g002'''
# This code is distributed under the MIT License
# pylint
# vim: tw=100 foldmethod=indent
# pylint: disable=bad-continuation, invalid-name, superfluous-parens
# pylint: disable=bad-whitespace
import logging
import regex
# python2 / python3 compatible way to get access to urlencode and decode
try:
from urllib.parse import unquote, quote_plus
except ImportError:
from urllib import unquote, quote_plus
logger = logging.getLogger(__name__)
# These regexes are not compatible with stdlib 're', we need 'regex'!
# (because of repeated captures, see https://bugs.python.org/issue7132)
ENTITLEMENT_REGEX = {
'strict': regex.compile(
r'urn:' +
r'(?P<nid>[^:]+):(?P<delegated_namespace>[^:]+)' + # Namespace-ID and delegated URN namespace
r'(:(?P<subnamespace>[^:]+))*?' + # Sub-namespaces
r':group:' +
r'(?P<group>[^:]+)' + # Root group
r'(:(?P<subgroup>[^:]+))*?' + # Sub-groups
r'(:role=(?P<role>.+))?' + # Role of the user in the deepest group
r'#(?P<group_authority>.+)' # Authoritative soruce of the entitlement (URN)
),
'lax': regex.compile(
r'urn:' +
r'(?P<nid>[^:]+):(?P<delegated_namespace>[^:]+)' + # Namespace-ID and delegated URN namespace
r'(:(?P<subnamespace>[^:]+))*?' + # Sub-namespaces
r':group:' +
r'(?P<group>[^:#]+)' + # Root group
r'(:(?P<subgroup>[^:#]+))*?' + # Sub-groups
r'(:role=(?P<role>[^#]+))?' + # Role of the user in the deepest group
r'(#(?P<group_authority>.+))?' # Authoritative source of the entitlement (URN)
)
}
class Aarc_g002_entitlement:
"""Entitlement allows EduPerson Entitlement parsing and comparision,
as specified in https://aarc-project.eu/guidelines/aarc-g002.
Class instances can be tested for equality and less-than-or-equality.
The py:meth:is_contained_in can be used to checks if a user with an entitlement `U` is permitted to use a resource which requires a certain entitlement `R`, like so:
`R`.is_contained_in(`U`)
:param str raw: The entitlement to parse. If the entitlement is '%xx' encoded it is decoded before parsing.
:param strict: `False` to ignore a missing group_authority and `True` otherwise, defaults to `True`.
:type strict: bool, optional
:param force: `False` to allow entitlements which don't follow the AARC-G002 recommendation and `True` otherwise, defaults to `True`.
:type force: bool, optional
:raises ValueError:
If raw does not contain a group_authority and strict is `True`,
or if the raw entitlement is not following the AARC-G002 recommendation at all and force is `True`.
:raises Exception: If the attributes extracted from the entitlement could not be assigned to this instance.
Available attributes for AARC-G002 entitlements are listed here.
For entitlements not following the recommendation, these are set to their default values.
"""
#:
namespace_id = ''
#:
delegated_namespace = ''
#: List of subnamespaces. May be empty.
subnamespaces = []
#:
group = ''
#: List of subgroups. May be empty
subgroups = []
#: None if the entitlement has no role.
role = None
#: None if the entitlement has no group_authority.
group_authority = None
def __init__(self, raw, strict=True, force=True):
"""Parse a raw EduPerson entitlement string in the AARC-G002 format."""
self._raw = unquote(raw)
match = ENTITLEMENT_REGEX['strict' if strict else 'lax'].fullmatch(self._raw)
if match is None:
logger.info('Input did not match (strict=%s): %s', strict, self._raw)
msg = 'Input does not seem to be an AARC-G002 Entitlement'
if force:
if strict:
raise ValueError(msg)
raise ValueError(msg + ' (Omitting the group authority was permitted)')
# no attributes captured for non-g002
return
capturesdict = match.capturesdict()
logger.debug("Extracting entitlement attributes: %s", capturesdict)
try:
[self.namespace_id] = capturesdict.get('nid')
[self.delegated_namespace] = capturesdict.get('delegated_namespace')
self.subnamespaces = capturesdict.get('subnamespace')
[self.group] = capturesdict.get('group')
self.subgroups = capturesdict.get('subgroup')
[self.role] = capturesdict.get('role') or [None]
[self.group_authority] = capturesdict.get('group_authority') or [None]
except ValueError as e:
logger.error('On assigning the captured attributes: %s', e)
raise Exception('Error extracting captured attributes')
def __repr__(self):
"""Serialize the entitlement to the AARC-G002 format.
This is the inverse to `__init__` and thus `ent_str == repr(Aarc_g002_entitlement(ent_str))`
holds for any valid entitlement.
"""
# handle non-g002
if not self.is_aarc_g002:
return self._raw
return ((
'urn:{namespace_id}:{delegated_namespace}{subnamespaces}' +
':group:{group}{subgroups}{role}' +
'#{group_authority}'
).format(**{
'namespace_id': self.namespace_id,
'delegated_namespace': self.delegated_namespace,
'group': self.group,
'group_authority': self.group_authority,
'subnamespaces': ''.join([':{}'.format(ns) for ns in self.subnamespaces]),
'subgroups': ''.join([':{}'.format(grp) for grp in self.subgroups]),
'role': ':role={}'.format(self.role) if self.role else ''
}))
def __str__(self):
"""Return the entitlement in human-readable string form."""
# handle non-g002
if not self.is_aarc_g002:
return self._raw
return ((
'<Aarc_g002_entitlement' +
' namespace={namespace_id}:{delegated_namespace}{subnamespaces}' +
' group={group}{subgroups}' +
'{role}' +
' auth={group_authority}>'
).format(**{
'namespace_id': self.namespace_id,
'delegated_namespace': self.delegated_namespace,
'group': self.group,
'group_authority': self.group_authority,
'subnamespaces': ''.join([',{}'.format(ns) for ns in self.subnamespaces]),
'subgroups': ''.join([',{}'.format(grp) for grp in self.subgroups]),
'role': ' role={}'.format(self.role) if self.role else ''
}))
def __mstr__(self):
# handle non-g002
if not self.is_aarc_g002:
return self._raw
return ((
'namespace_id: {namespace_id}' +
'\ndelegated_namespace: {delegated_namespace}' +
'\nsubnamespaces: {subnamespaces}' +
'\ngroup: {group}' +
'\nsubgroups: {subgroups}' +
'\nrole_in_subgroup {role}' +
'\ngroup_authority: {group_authority}'
).format(**{
'namespace_id': self.namespace_id,
'delegated_namespace': self.delegated_namespace,
'group': self.group,
'group_authority': self.group_authority,
'subnamespaces': ','.join(['{}'.format(ns) for ns in self.subnamespaces]),
'subgroups': ','.join(['{}'.format(grp) for grp in self.subgroups]),
'role':'{}'.format(self.role) if self.role else 'n/a'
}))
def __eq__(self, other):
""" Check if other object is equal """
# handle non-g002
if not self.is_aarc_g002:
if not other.is_aarc_g002:
return self._raw == other.raw
return False
if self.namespace_id != other.namespace_id:
return False
if self.delegated_namespace != other.delegated_namespace:
return False
for subnamespace in self.subnamespaces:
if subnamespace not in other.subnamespaces:
return False
if self.group != other.group:
return False
if self.subgroups != other.subgroups:
return False
if self.role != other.role:
return False
return True
def __le__(self, other):
""" Check if self is contained in other.
Please use "is_contained_in", see below"""
# handle non-g002
if not self.is_aarc_g002:
if not other.is_aarc_g002:
return self._raw == other._raw
return False
if self.namespace_id != other.namespace_id:
return False
if self.delegated_namespace != other.delegated_namespace:
return False
for subnamespace in self.subnamespaces:
if subnamespace not in other.subnamespaces:
return False
if self.group != other.group:
return False
for subgroup in self.subgroups:
if subgroup not in other.subgroups:
return False
if self.role is not None:
if self.role != other.role:
return False
try:
myown_subgroup_for_role = self.subgroups[-1]
except IndexError:
myown_subgroup_for_role = None
try:
other_subgroup_for_role = other.subgroups[-1]
except IndexError:
other_subgroup_for_role = None
if myown_subgroup_for_role != other_subgroup_for_role:
return False
return True
def is_contained_in(self, other):
""" Check if self is contained in other """
return (self <= other)
@property
def is_aarc_g002(self):
""" Check if this entitlements follows the AARC-G002 recommendation
:return: True if the recommendation is followed
:rtype: bool
"""
return self.group != ''
# TODO: Add more Weird combinations of these with roles
# pylint: disable=bad-whitespace, invalid-name, missing-docstring
import pytest
from aarc_g002_entitlement import Aarc_g002_entitlement
class TestAarc_g002_entitlement:
def test_equality(self):
required_group = "urn:geant:h-df.de:group:aai-admin:role = member#unity.helmholtz-data-federation.de"
actual_group = "urn:geant:h-df.de:group:aai-admin:role = member#unity.helmholtz-data-federation.de"
req_entitlement = Aarc_g002_entitlement(required_group)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert act_entitlement == req_entitlement
assert req_entitlement.is_contained_in(act_entitlement)
def test_simple(self):
required_group = "urn:geant:h-df.de:group:aai-admin:role=member#unity.helmholtz-data-federation.de"
actual_group = "urn:geant:h-df.de:group:aai-admin:role=member#backupserver.used.for.developmt.de"
req_entitlement = Aarc_g002_entitlement(required_group)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert req_entitlement.is_contained_in(act_entitlement)
def test_role_not_required(self):
required_group = "urn:geant:h-df.de:group:aai-admin#unity.helmholtz-data-federation.de"
actual_group = "urn:geant:h-df.de:group:aai-admin:role=member#backupserver.used.for.developmt.de"
req_entitlement = Aarc_g002_entitlement(required_group)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert req_entitlement.is_contained_in(act_entitlement)
def test_role_required(self):
required_group = "urn:geant:h-df.de:group:aai-admin:role=member#unity.helmholtz-data-federation.de"
actual_group = "urn:geant:h-df.de:group:aai-admin#backupserver.used.for.developmt.de"
req_entitlement = Aarc_g002_entitlement(required_group)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert not req_entitlement.is_contained_in(act_entitlement)
def test_subgroup_required(self):
required_group = "urn:geant:h-df.de:group:aai-admin:special-admins#unity.helmholtz-data-federation.de"
actual_group = (
"urn:geant:h-df.de:group:aai-admin#backupserver.used.for.developmt.de"
)
req_entitlement = Aarc_g002_entitlement(required_group)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert not req_entitlement.is_contained_in(act_entitlement)
def test_user_in_subgroup(self):
required_group = "urn:geant:h-df.de:group:aai-admin"
actual_group = "urn:geant:h-df.de:group:aai-admin:special-admins#backupserver.used.for.developmt.de"
req_entitlement = Aarc_g002_entitlement(required_group, strict=False)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert req_entitlement.is_contained_in(act_entitlement)
def test_role_required_for_supergroup(self):
required_group = "urn:geant:h-df.de:group:aai-admin:role=admin#unity.helmholtz-data-federation.de"
actual_group = "urn:geant:h-df.de:group:aai-admin:special-admins:role=admin#backupserver.used.for.developmt.de"
req_entitlement = Aarc_g002_entitlement(required_group)
act_entitlement = Aarc_g002_entitlement(actual_group)
assert not req_entitlement.is_contained_in(act_entitlement)
@pytest.mark.parametrize(
'required_group,actual_group',
[
("urn:geant:h-df.de:group:aai-admin", "urn:geant:kit.edu:group:bwUniCluster"),
("urn:geant:h-df.de:group:myExampleColab#unity.helmholtz-data-federation.de", "urn:geant:kit.edu:group:bwUniCluster"),
("urn:geant:h-df.de:group:aai-admin", "urn:geant:kit.edu:group:aai-admin"),
]
)
def test_foreign_entitlement(self, required_group, actual_group):
actual_group = "urn:geant:kit.edu:group:bwUniCluster"
req_entitlement = Aarc_g002_entitlement(required_group, strict=False)
act_entitlement = Aarc_g002_entitlement(actual_group, strict=False)
assert not req_entitlement.is_contained_in(act_entitlement)
# #
@pytest.mark.parametrize(
'actual_group',
[
"urn:mace:dir:entitlement:common-lib-terms",
"urn:geant:kit.edu:group:DFN-SLCS",
]
)
def test_non_aarc_entitlement(self, actual_group):
required_group = "urn:geant:h-df.de:group:aai-admin"
req_entitlement = Aarc_g002_entitlement(required_group, strict=False)
act_entitlement = Aarc_g002_entitlement(actual_group, strict=False, force=False)
assert not req_entitlement.is_contained_in(act_entitlement)
@pytest.mark.parametrize(
'required_group',
[
"urn:geant:h-df.de:group:aai-admin:role=admin",
"urn:geant:h-df.de:group:aai-admin",
]
)
def test_failure_incomplete_but_valid_entitlement(self, required_group):
Aarc_g002_entitlement(required_group, strict=False)
def test_failure_incomplete_invalid_entitlement(self):
required_group = "urn:geant:h-df.de"
with pytest.raises(ValueError):
Aarc_g002_entitlement(required_group)
<hr/>
<p style="font-size: 10px;">
This software is part of the <a href="https://aarc-project.eu">AARC</a> project, that has received funding from the European Union’s Horizon 2020 research and innovation programme under grant agreement No 653965 and 730941
</p>
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# nova documentation build configuration file
#
# Refer to the Sphinx documentation for advice on configuring this file:
#
# http://www.sphinx-doc.org/en/stable/config.html
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('../../'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
todo_include_todos = True
source_parsers = {
# '.md': 'recommonmark.parser.CommonMarkParser'
}
# The suffix of source filenames.
source_suffix = ['.rst', '.md']
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = "aarc_g002_entitlement"
copyright = "2017-present, Marcus Hardt, Lukas Burgey"
author = "Marcus Hardt, Lukas Burgey"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
# release = version.release_string
# The short X.Y version.
# version = version.version_string
# A list of glob-style patterns that should be excluded when looking for
# source files. They are matched against the source file names relative to the
# source directory, using slashes as directory separators on all platforms.
exclude_patterns = []
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['aarc_g002_entitlement.']