Skip to content
Snippets Groups Projects
Commit e223f161 authored by Julian Rother's avatar Julian Rother
Browse files

Imported code from uffd branch "ldap-orm"

parents
No related branches found
No related tags found
No related merge requests found
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# 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/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
# Auto-generated development key/certificate
devcert.crt
devcert.key
import ldap3
from .core import LDAPCommitError
from . import model, attribute, relationship
__all__ = ['LDAPMapper', 'LDAPCommitError']
class LDAPMapper:
def __init__(self, server=None, bind_dn=None, bind_password=None):
class Model(model.Model):
ldap_mapper = self
self.Model = Model # pylint: disable=invalid-name
self.Session = model.Session # pylint: disable=invalid-name
self.Attribute = attribute.Attribute # pylint: disable=invalid-name
self.Relationship = relationship.Relationship # pylint: disable=invalid-name
self.Backreference = relationship.Backreference # pylint: disable=invalid-name
if not hasattr(type(self), 'server'):
self.server = server
if not hasattr(type(self), 'bind_dn'):
self.bind_dn = bind_dn
if not hasattr(type(self), 'bind_password'):
self.bind_password = bind_password
if not hasattr(type(self), 'session'):
self.session = self.Session(self.get_connection)
def get_connection(self):
return ldap3.Connection(self.server, self.bind_dn, self.bind_password, auto_bind=True)
from collections.abc import MutableSequence
class AttributeList(MutableSequence):
def __init__(self, ldap_object, name, aliases):
self.__ldap_object = ldap_object
self.__name = name
self.__aliases = [name] + aliases
def __get(self):
return list(self.__ldap_object.getattr(self.__name))
def __set(self, values):
for name in self.__aliases:
self.__ldap_object.setattr(name, values)
def __repr__(self):
return repr(self.__get())
def __setitem__(self, key, value):
tmp = self.__get()
tmp[key] = value
self.__set(tmp)
def __delitem__(self, key):
tmp = self.__get()
del tmp[key]
self.__set(tmp)
def __len__(self):
return len(self.__get())
def __getitem__(self, key):
return self.__get()[key]
def insert(self, index, value):
tmp = self.__get()
tmp.insert(index, value)
self.__set(tmp)
class Attribute:
def __init__(self, name, aliases=None, multi=False, default=None):
self.name = name
self.aliases = aliases or []
self.multi = multi
self.default = default
def add_hook(self, obj):
if obj.ldap_object.getattr(self.name) == []:
self.__set__(obj, self.default() if callable(self.default) else self.default)
def __set_name__(self, cls, name):
if self.default is not None:
cls.ldap_add_hooks = cls.ldap_add_hooks + (self.add_hook,)
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.multi:
return AttributeList(obj.ldap_object, self.name, self.aliases)
return (obj.ldap_object.getattr(self.name) or [None])[0]
def __set__(self, obj, values):
if not self.multi:
values = [values]
for name in [self.name] + self.aliases:
obj.ldap_object.setattr(name, values)
from ldap3 import MODIFY_REPLACE, MODIFY_DELETE, MODIFY_ADD, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
from ldap3.utils.conv import escape_filter_chars
def encode_filter(filter_params):
return '(&%s)'%(''.join(['(%s=%s)'%(attr, escape_filter_chars(value)) for attr, value in filter_params]))
def match_dn(dn, base):
return dn.endswith(base) # Probably good enougth for all valid dns
def make_cache_key(search_base, filter_params):
res = (search_base,)
for attr, value in sorted(filter_params):
res += ((attr, value),)
return res
class LDAPCommitError(Exception):
pass
class SessionState:
def __init__(self, objects=None, deleted_objects=None, references=None):
self.objects = objects or {}
self.deleted_objects = deleted_objects or {}
self.references = references or {} # {(attr_name, value): {srcobj, ...}, ...}
def copy(self):
objects = self.objects.copy()
deleted_objects = self.deleted_objects.copy()
references = {key: objs.copy() for key, objs in self.references.items()}
return SessionState(objects, deleted_objects, references)
def ref(self, obj, attr, values):
for value in values:
key = (attr, value)
if key not in self.references:
self.references[key] = {obj}
else:
self.references[key].add(obj)
def unref(self, obj, attr, values):
for value in values:
self.references.get((attr, value), set()).discard(obj)
class ObjectState:
def __init__(self, session=None, attributes=None, dn=None):
self.session = session
self.attributes = attributes or {}
self.dn = dn
def copy(self):
attributes = {name: values.copy() for name, values in self.attributes.items()}
return ObjectState(attributes=attributes, dn=self.dn, session=self.session)
class AddOperation:
def __init__(self, obj, dn, object_classes):
self.obj = obj
self.dn = dn
self.object_classes = object_classes
self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()}
def apply_object(self, obj_state):
obj_state.dn = self.dn
obj_state.attributes = {name: values.copy() for name, values in self.attributes.items()}
obj_state.attributes['objectClass'] = obj_state.attributes.get('objectClass', []) + list(self.object_classes)
def apply_session(self, session_state):
assert self.dn not in session_state.objects
session_state.objects[self.dn] = self.obj
for name, values in self.attributes.items():
session_state.ref(self.obj, name, values)
session_state.ref(self.obj, 'objectClass', self.object_classes)
def apply_ldap(self, conn):
success = conn.add(self.dn, self.object_classes, self.attributes)
if not success:
raise LDAPCommitError()
class DeleteOperation:
def __init__(self, obj):
self.dn = obj.state.dn
self.obj = obj
self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()}
def apply_object(self, obj_state):
obj_state.dn = None
def apply_session(self, session_state):
assert self.dn in session_state.objects
del session_state.objects[self.dn]
session_state.deleted_objects[self.dn] = self.obj
for name, values in self.attributes.items():
session_state.unref(self.obj, name, values)
def apply_ldap(self, conn):
success = conn.delete(self.dn)
if not success:
raise LDAPCommitError()
class ModifyOperation:
def __init__(self, obj, changes):
self.obj = obj
self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()}
self.changes = changes
def apply_object(self, obj_state):
for attr, changes in self.changes.items():
for action, values in changes:
if action == MODIFY_REPLACE:
obj_state.attributes[attr] = values
elif action == MODIFY_ADD:
obj_state.attributes[attr] += values
elif action == MODIFY_DELETE:
for value in values:
if value in obj_state.attributes[attr]:
obj_state.attributes[attr].remove(value)
def apply_session(self, session_state):
for attr, changes in self.changes.items():
for action, values in changes:
if action == MODIFY_REPLACE:
session_state.unref(self.obj, attr, self.attributes.get(attr, []))
session_state.ref(self.obj, attr, values)
elif action == MODIFY_ADD:
session_state.ref(self.obj, attr, values)
elif action == MODIFY_DELETE:
session_state.unref(self.obj, attr, values)
def apply_ldap(self, conn):
success = conn.modify(self.obj.state.dn, self.changes)
if not success:
raise LDAPCommitError()
class Session:
def __init__(self, get_connection):
self.get_connection = get_connection
self.committed_state = SessionState()
self.state = SessionState()
self.changes = []
self.cached_searches = set()
def add(self, obj, dn, object_classes):
if self.state.objects.get(dn) == obj:
return
assert obj.state.session is None
oper = AddOperation(obj, dn, object_classes)
oper.apply_object(obj.state)
obj.state.session = self
oper.apply_session(self.state)
self.changes.append(oper)
def delete(self, obj):
if obj.state.dn not in self.state.objects:
return
assert obj.state.session == self
oper = DeleteOperation(obj)
oper.apply_object(obj.state)
obj.state.session = None
oper.apply_session(self.state)
self.changes.append(oper)
def record(self, oper):
assert oper.obj.state.session == self
self.changes.append(oper)
def commit(self):
conn = self.get_connection()
while self.changes:
oper = self.changes.pop(0)
try:
oper.apply_ldap(conn)
except Exception as err:
self.changes.insert(0, oper)
raise err
oper.apply_object(oper.obj.committed_state)
oper.apply_session(self.committed_state)
self.committed_state = self.state.copy()
def rollback(self):
for obj in self.state.objects.values():
obj.state = obj.committed_state.copy()
for obj in self.state.deleted_objects.values():
obj.state = obj.committed_state.copy()
self.state = self.committed_state.copy()
self.changes.clear()
def get(self, dn, filter_params):
if dn in self.state.objects:
obj = self.state.objects[dn]
return obj if obj.match(filter_params) else None
if dn in self.state.deleted_objects:
return None
conn = self.get_connection()
conn.search(dn, encode_filter(filter_params), attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])
if not conn.response:
return None
assert len(conn.response) == 1
assert conn.response[0]['dn'] == dn
obj = Object(self, conn.response[0])
self.state.objects[dn] = obj
self.committed_state.objects[dn] = obj
for attr, values in obj.state.attributes.items():
self.state.ref(obj, attr, values)
return obj
def filter(self, search_base, filter_params):
if not filter_params:
matches = self.state.objects.values()
else:
submatches = [self.state.references.get((attr, value), set()) for attr, value in filter_params]
matches = submatches.pop(0)
while submatches:
matches = matches.intersection(submatches.pop(0))
res = [obj for obj in matches if match_dn(obj.state.dn, search_base)]
cache_key = make_cache_key(search_base, filter_params)
if cache_key in self.cached_searches:
return res
conn = self.get_connection()
conn.search(search_base, encode_filter(filter_params), attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])
for response in conn.response:
dn = response['dn']
if dn in self.state.objects or dn in self.state.deleted_objects:
continue
obj = Object(self, response)
self.state.objects[dn] = obj
self.committed_state.objects[dn] = obj
for attr, values in obj.state.attributes.items():
self.state.ref(obj, attr, values)
res.append(obj)
self.cached_searches.add(cache_key)
return res
class Object:
def __init__(self, session=None, response=None):
if response is None:
self.committed_state = ObjectState()
else:
assert session is not None
attrs = {attr: value if isinstance(value, list) else [value] for attr, value in response['attributes'].items()}
self.committed_state = ObjectState(session, attrs, response['dn'])
self.state = self.committed_state.copy()
@property
def dn(self):
return self.state.dn
@property
def session(self):
return self.state.session
def getattr(self, name):
return self.state.attributes.get(name, [])
def setattr(self, name, values):
oper = ModifyOperation(self, {name: [(MODIFY_REPLACE, values)]})
oper.apply_object(self.state)
if self.state.session:
oper.apply_session(self.state.session.state)
self.state.session.changes.append(oper)
def attr_append(self, name, value):
oper = ModifyOperation(self, {name: [(MODIFY_ADD, [value])]})
oper.apply_object(self.state)
if self.state.session:
oper.apply_session(self.state.session.state)
self.state.session.changes.append(oper)
def attr_remove(self, name, value):
oper = ModifyOperation(self, {name: [(MODIFY_DELETE, [value])]})
oper.apply_object(self.state)
if self.state.session:
oper.apply_session(self.state.session.state)
self.state.session.changes.append(oper)
def match(self, filter_params):
for attr, value in filter_params:
if value not in self.getattr(attr):
return False
return True
from collections.abc import MutableSet
from .model import add_to_session
class DBRelationshipSet(MutableSet):
def __init__(self, dbobj, relattr, ldapcls, mapcls):
self.__dbobj = dbobj
self.__relattr = relattr
self.__ldapcls = ldapcls
self.__mapcls = mapcls
def __get_dns(self):
return [mapobj.dn for mapobj in getattr(self.__dbobj, self.__relattr)]
def __repr__(self):
return repr(set(self))
def __contains__(self, value):
if value is None or not isinstance(value, self.__ldapcls):
return False
return value.ldap_object.dn in self.__get_dns()
def __iter__(self):
return iter(filter(lambda obj: obj is not None, [self.__ldapcls.query.get(dn) for dn in self.__get_dns()]))
def __len__(self):
return len(set(self))
def add(self, value):
if not isinstance(value, self.__ldapcls):
raise TypeError()
if value.ldap_object.session is None:
add_to_session(value, self.__ldapcls.ldap_mapper.session.ldap_session)
if value.ldap_object.dn not in self.__get_dns():
getattr(self.__dbobj, self.__relattr).append(self.__mapcls(dn=value.ldap_object.dn))
def discard(self, value):
if not isinstance(value, self.__ldapcls):
raise TypeError()
rel = getattr(self.__dbobj, self.__relattr)
for mapobj in list(rel):
if mapobj.dn == value.ldap_object.dn:
rel.remove(mapobj)
class DBRelationship:
def __init__(self, relattr, ldapcls, mapcls, backref=None, backattr=None):
self.relattr = relattr
self.ldapcls = ldapcls
self.mapcls = mapcls
self.backref = backref
self.backattr = backattr
def __set_name__(self, cls, name):
if self.backref:
assert self.backattr
setattr(self.ldapcls, self.backref, DBBackreference(cls, self.relattr, self.mapcls, self.backattr))
def __get__(self, obj, objtype=None):
if obj is None:
return self
return DBRelationshipSet(obj, self.relattr, self.ldapcls, self.mapcls)
def __set__(self, obj, values):
tmp = self.__get__(obj)
tmp.clear()
for value in values:
tmp.add(value)
class DBBackreferenceSet(MutableSet):
def __init__(self, ldapobj, dbcls, relattr, mapcls, backattr):
self.__ldapobj = ldapobj
self.__dbcls = dbcls
self.__relattr = relattr
self.__mapcls = mapcls
self.__backattr = backattr
@property
def __dn(self):
return self.__ldapobj.ldap_object.dn
def __get(self):
return {getattr(mapobj, self.__backattr) for mapobj in self.__mapcls.query.filter_by(dn=self.__dn)}
def __repr__(self):
return repr(self.__get())
def __contains__(self, value):
return value in self.__get()
def __iter__(self):
return iter(self.__get())
def __len__(self):
return len(self.__get())
def add(self, value):
# TODO: add value to db session if necessary
assert self.__ldapobj.ldap_object.session is not None
if not isinstance(value, self.__dbcls):
raise TypeError()
rel = getattr(value, self.__relattr)
if self.__dn not in {mapobj.dn for mapobj in rel}:
rel.append(self.__mapcls(dn=self.__dn))
def discard(self, value):
if not isinstance(value, self.__dbcls):
raise TypeError()
rel = getattr(value, self.__relattr)
for mapobj in list(rel):
if mapobj.dn == self.__dn:
rel.remove(mapobj)
class DBBackreference:
def __init__(self, dbcls, relattr, mapcls, backattr):
self.dbcls = dbcls
self.relattr = relattr
self.mapcls = mapcls
self.backattr = backattr
def __get__(self, obj, objtype=None):
if obj is None:
return self
return DBBackreferenceSet(obj, self.dbcls, self.relattr, self.mapcls, self.backattr)
def __set__(self, obj, values):
tmp = self.__get__(obj)
tmp.clear()
for value in values:
tmp.add(value)
try:
# Added in v2.5
from ldap3.utils.dn import escape_rdn
except ImportError:
# From ldap3 source code, Copyright Giovanni Cannata, LGPL v3 license
def escape_rdn(rdn):
# '/' must be handled first or the escape slashes will be escaped!
for char in ['\\', ',', '+', '"', '<', '>', ';', '=', '\x00']:
rdn = rdn.replace(char, '\\' + char)
if rdn[0] == '#' or rdn[0] == ' ':
rdn = ''.join(('\\', rdn))
if rdn[-1] == ' ':
rdn = ''.join((rdn[:-1], '\\ '))
return rdn
from . import core
def add_to_session(obj, session):
for func in obj.ldap_add_hooks:
func(obj)
session.add(obj.ldap_object, obj.dn, obj.ldap_object_classes)
class Session:
def __init__(self, get_connection):
self.ldap_session = core.Session(get_connection)
def add(self, obj):
add_to_session(obj, self.ldap_session)
def delete(self, obj):
self.ldap_session.delete(obj.ldap_object)
def commit(self):
self.ldap_session.commit()
def rollback(self):
self.ldap_session.rollback()
def make_modelobj(obj, model):
if obj is None:
return None
if not hasattr(obj, 'model'):
obj.model = model()
obj.model.ldap_object = obj
if not isinstance(obj.model, model):
return None
return obj.model
def make_modelobjs(objs, model):
modelobjs = []
for obj in objs:
modelobj = make_modelobj(obj, model)
if modelobj is not None:
modelobjs.append(modelobj)
return modelobjs
class ModelQuery:
def __init__(self, model):
self.model = model
def get(self, dn):
session = self.model.ldap_mapper.session.ldap_session
return make_modelobj(session.get(dn, self.model.ldap_filter_params), self.model)
def all(self):
session = self.model.ldap_mapper.session.ldap_session
objs = session.filter(self.model.ldap_search_base, self.model.ldap_filter_params)
return make_modelobjs(objs, self.model)
def filter_by(self, **kwargs):
filter_params = self.model.ldap_filter_params
filter_params += tuple((getattr(self.model, attr).name, value) for attr, value in kwargs.items())
session = self.model.ldap_mapper.session.ldap_session
objs = session.filter(self.model.ldap_search_base, filter_params)
return make_modelobjs(objs, self.model)
class ModelQueryWrapper:
def __get__(self, obj, objtype=None):
return ModelQuery(objtype)
class Model:
# Overwritten by mapper
ldap_mapper = None
query = ModelQueryWrapper()
ldap_add_hooks = ()
# Overwritten by models
ldap_search_base = None
ldap_filter_params = ()
ldap_object_classes = ()
ldap_dn_base = None
ldap_dn_attribute = None
def __init__(self, **kwargs):
self.ldap_object = core.Object()
for key, value, in kwargs.items():
if not hasattr(self, key):
raise Exception()
setattr(self, key, value)
@property
def dn(self):
if self.ldap_object.dn is not None:
return self.ldap_object.dn
if self.ldap_dn_base is None or self.ldap_dn_attribute is None:
return None
values = self.ldap_object.getattr(self.ldap_dn_attribute)
if not values:
return None
# escape_rdn can't handle empty strings
rdn = escape_rdn(values[0]) if values[0] else ''
return '%s=%s,%s'%(self.ldap_dn_attribute, rdn, self.ldap_dn_base)
def __repr__(self):
cls_name = '%s.%s'%(type(self).__module__, type(self).__name__)
if self.dn is not None:
return '<%s %s>'%(cls_name, self.dn)
return '<%s>'%cls_name
from collections.abc import MutableSet
from .model import make_modelobj, make_modelobjs, add_to_session
class UnboundObjectError(Exception):
pass
class RelationshipSet(MutableSet):
def __init__(self, ldap_object, name, model, destmodel):
self.__ldap_object = ldap_object
self.__name = name
self.__model = model
self.__destmodel = destmodel
def __modify_check(self, value):
if self.__ldap_object.session is None:
raise UnboundObjectError()
if not isinstance(value, self.__destmodel):
raise TypeError()
def __repr__(self):
return repr(set(self))
def __contains__(self, value):
if value is None or not isinstance(value, self.__destmodel):
return False
return value.ldap_object.dn in self.__ldap_object.getattr(self.__name)
def __iter__(self):
def get(dn):
return make_modelobj(self.__ldap_object.session.get(dn, self.__destmodel.ldap_filter_params), self.__destmodel)
dns = set(self.__ldap_object.getattr(self.__name))
return iter(filter(lambda obj: obj is not None, map(get, dns)))
def __len__(self):
return len(set(self))
def add(self, value):
self.__modify_check(value)
if value.ldap_object.session is None:
add_to_session(value, self.__ldap_object.session)
assert value.ldap_object.session == self.__ldap_object.session
self.__ldap_object.attr_append(self.__name, value.dn)
def discard(self, value):
self.__modify_check(value)
self.__ldap_object.attr_remove(self.__name, value.dn)
def update(self, values):
for value in values:
self.add(value)
class Relationship:
def __init__(self, name, destmodel, backref=None):
self.name = name
self.destmodel = destmodel
self.backref = backref
def __set_name__(self, cls, name):
if self.backref is not None:
setattr(self.destmodel, self.backref, Backreference(self.name, cls))
def __get__(self, obj, objtype=None):
if obj is None:
return self
return RelationshipSet(obj.ldap_object, self.name, type(obj), self.destmodel)
def __set__(self, obj, values):
tmp = self.__get__(obj)
tmp.clear()
for value in values:
tmp.add(value)
class BackreferenceSet(MutableSet):
def __init__(self, ldap_object, name, model, srcmodel):
self.__ldap_object = ldap_object
self.__name = name
self.__model = model
self.__srcmodel = srcmodel
def __modify_check(self, value):
if self.__ldap_object.session is None:
raise UnboundObjectError()
if not isinstance(value, self.__srcmodel):
raise TypeError()
def __get(self):
if self.__ldap_object.session is None:
return set()
filter_params = self.__srcmodel.ldap_filter_params + ((self.__name, self.__ldap_object.dn),)
objs = self.__ldap_object.session.filter(self.__srcmodel.ldap_search_base, filter_params)
return set(make_modelobjs(objs, self.__srcmodel))
def __repr__(self):
return repr(self.__get())
def __contains__(self, value):
return value in self.__get()
def __iter__(self):
return iter(self.__get())
def __len__(self):
return len(self.__get())
def add(self, value):
self.__modify_check(value)
if value.ldap_object.session is None:
add_to_session(value, self.__ldap_object.session)
assert value.ldap_object.session == self.__ldap_object.session
if self.__ldap_object.dn not in value.ldap_object.getattr(self.__name):
value.ldap_object.attr_append(self.__name, self.__ldap_object.dn)
def discard(self, value):
self.__modify_check(value)
value.ldap_object.attr_remove(self.__name, self.__ldap_object.dn)
def update(self, values):
for value in values:
self.add(value)
class Backreference:
def __init__(self, name, srcmodel):
self.name = name
self.srcmodel = srcmodel
def __get__(self, obj, objtype=None):
if obj is None:
return self
return BackreferenceSet(obj.ldap_object, self.name, type(obj), self.srcmodel)
def __set__(self, obj, values):
tmp = self.__get__(obj)
tmp.clear()
for value in values:
tmp.add(value)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment