Commit a73fc6cb authored by Ivan Tyagov's avatar Ivan Tyagov

Merge branch 'security_groups' into ivan

parents efaa1597 e21b9e1e
...@@ -31,8 +31,8 @@ ...@@ -31,8 +31,8 @@
DELETE FROM roles_and_users\n DELETE FROM roles_and_users\n
<dtml-var sql_delimiter>\n <dtml-var sql_delimiter>\n
INSERT INTO roles_and_users (uid, allowedRolesAndUsers) VALUES\n INSERT INTO roles_and_users (uid, allowedRolesAndUsers) VALUES\n
<dtml-in expr="getPortalObject().portal_catalog.getSQLCatalog().getRoleAndSecurityUidList()">\n <dtml-in expr="role" expr="getPortalObject().portal_catalog.getSQLCatalog().getRoleAndSecurityUidList()">\n
(<dtml-sqlvar sequence-item type="int">,<dtml-sqlvar sequence-key type="string">)<dtml-if sequence-end><dtml-else>,</dtml-if>\n (<dtml-sqlvar expr="role_item[0]" type="int">, <dtml-sqlvar expr="role_item[1]" type="string">, <dtml-sqlvar expr="role_item[2]" type="string">)<dtml-if sequence-end><dtml-else>,</dtml-if>\n
\n \n
</dtml-in> </dtml-in>
......
...@@ -54,6 +54,7 @@ ...@@ -54,6 +54,7 @@
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string>CREATE TABLE roles_and_users (\n <value> <string>CREATE TABLE roles_and_users (\n
uid INT UNSIGNED NOT NULL,\n uid INT UNSIGNED NOT NULL,\n
local_roles_group_id VARCHAR(255),\n
allowedRolesAndUsers VARCHAR(255) NOT NULL,\n allowedRolesAndUsers VARCHAR(255) NOT NULL,\n
KEY `uid` (`uid`),\n KEY `uid` (`uid`),\n
KEY `allowedRolesAndUsers` (`allowedRolesAndUsers`)\n KEY `allowedRolesAndUsers` (`allowedRolesAndUsers`)\n
......
...@@ -545,7 +545,7 @@ class BaseTemplateItem(Implicit, Persistent): ...@@ -545,7 +545,7 @@ class BaseTemplateItem(Implicit, Persistent):
classname = klass.__name__ classname = klass.__name__
attr_set = set(('_dav_writelocks', '_filepath', '_owner', 'last_id', 'uid', attr_set = set(('_dav_writelocks', '_filepath', '_owner', 'last_id', 'uid',
'__ac_local_roles__')) '__ac_local_roles__', '__ac_local_roles_group_id_dict__'))
if export: if export:
if not keep_workflow_history: if not keep_workflow_history:
attr_set.add('workflow_history') attr_set.add('workflow_history')
...@@ -3051,7 +3051,8 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem): ...@@ -3051,7 +3051,8 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
k = {'id': 'object_id', # for stable sort k = {'id': 'object_id', # for stable sort
'role_base_category': 'base_category', 'role_base_category': 'base_category',
'role_base_category_script_id': 'base_category_script', 'role_base_category_script_id': 'base_category_script',
'role_category': 'category'}.get(k) 'role_category': 'category',
'local_roles_group_id': 'local_roles_group_id'}.get(k)
if not k: if not k:
continue continue
type_role_dict[k] = v type_role_dict[k] = v
...@@ -3066,7 +3067,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem): ...@@ -3066,7 +3067,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
xml_data += "\n <role id='%s'>" % role['id'] xml_data += "\n <role id='%s'>" % role['id']
# uniq # uniq
for property in ('title', 'description', 'condition', for property in ('title', 'description', 'condition',
'base_category_script'): 'base_category_script', 'local_roles_group_id'):
prop_value = role.get(property) prop_value = role.get(property)
if prop_value: if prop_value:
if isinstance(prop_value, str): if isinstance(prop_value, str):
...@@ -3161,7 +3162,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem): ...@@ -3161,7 +3162,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
path = 'portal_types/%s' % roles_path.split('/', 1)[1] path = 'portal_types/%s' % roles_path.split('/', 1)[1]
try: try:
obj = p.unrestrictedTraverse(path) obj = p.unrestrictedTraverse(path)
setattr(obj, '_roles', []) obj.manage_delObjects([x.id for x in obj.getRoleInformationList()])
except (NotFound, KeyError): except (NotFound, KeyError):
pass pass
...@@ -4360,6 +4361,12 @@ class CatalogLocalRoleKeyTemplateItem(CatalogSearchKeyTemplateItem): ...@@ -4360,6 +4361,12 @@ class CatalogLocalRoleKeyTemplateItem(CatalogSearchKeyTemplateItem):
key_list_title = 'local_role_key_list' key_list_title = 'local_role_key_list'
key_title = 'LocalRole key' key_title = 'LocalRole key'
class CatalogSecurityUidColumnTemplateItem(CatalogSearchKeyTemplateItem):
key_list_attr = 'sql_catalog_security_uid_columns'
key_list_title = 'security_uid_column_list'
key_title = 'Security Uid Columns'
class MessageTranslationTemplateItem(BaseTemplateItem): class MessageTranslationTemplateItem(BaseTemplateItem):
def build(self, context, **kw): def build(self, context, **kw):
...@@ -4532,13 +4539,27 @@ class LocalRolesTemplateItem(BaseTemplateItem): ...@@ -4532,13 +4539,27 @@ class LocalRolesTemplateItem(BaseTemplateItem):
obj = p.unrestrictedTraverse(path.split('/', 1)[1]) obj = p.unrestrictedTraverse(path.split('/', 1)[1])
local_roles_dict = getattr(obj, '__ac_local_roles__', local_roles_dict = getattr(obj, '__ac_local_roles__',
{}) or {} {}) or {}
self._objects[path] = (local_roles_dict, ) local_roles_group_id_dict = getattr(
obj, '__ac_local_roles_group_id_dict__', {}) or {}
self._objects[path] = (local_roles_dict, local_roles_group_id_dict)
# Function to generate XML Code Manually # Function to generate XML Code Manually
def generateXml(self, path=None): def generateXml(self, path=None):
local_roles_dict = self._objects[path][0] # With local roles groups id, self._object contains for each path a tuple
# local roles # containing the dict of local roles and the dict of local roles group ids.
# Before it was only containing the dict of local roles. This method is
# also used on installed business templates to show a diff during
# installation, so it might be called on old format objects.
if len(self._objects[path]) == 2:
# new format
local_roles_dict, local_roles_group_id_dict = self._objects[path]
else:
# old format, before local roles group id
local_roles_group_id_dict = dict()
local_roles_dict, = self._objects[path]
xml_data = '<local_roles_item>' xml_data = '<local_roles_item>'
# local roles
xml_data += '\n <local_roles>' xml_data += '\n <local_roles>'
for key in sorted(local_roles_dict): for key in sorted(local_roles_dict):
xml_data += "\n <role id='%s'>" %(key,) xml_data += "\n <role id='%s'>" %(key,)
...@@ -4547,6 +4568,19 @@ class LocalRolesTemplateItem(BaseTemplateItem): ...@@ -4547,6 +4568,19 @@ class LocalRolesTemplateItem(BaseTemplateItem):
xml_data += "\n <item>%s</item>" %(item,) xml_data += "\n <item>%s</item>" %(item,)
xml_data += '\n </role>' xml_data += '\n </role>'
xml_data += '\n </local_roles>' xml_data += '\n </local_roles>'
if local_roles_group_id_dict:
# local roles group id dict (not included by default to be stable with
# old bts)
xml_data += '\n <local_roles_group_id>'
for principal, local_roles_group_id_list in sorted(local_roles_group_id_dict.items()):
xml_data += "\n <principal id='%s'>" % escape(principal)
for local_roles_group_id in local_roles_group_id_list:
xml_data += "\n <local_roles_group_id>%s</local_roles_group_id>" % \
escape(local_roles_group_id)
xml_data += "\n </principal>"
xml_data += '\n </local_roles_group_id>'
xml_data += '\n</local_roles_item>' xml_data += '\n</local_roles_item>'
if isinstance(xml_data, unicode): if isinstance(xml_data, unicode):
xml_data = xml_data.encode('utf8') xml_data = xml_data.encode('utf8')
...@@ -4571,7 +4605,16 @@ class LocalRolesTemplateItem(BaseTemplateItem): ...@@ -4571,7 +4605,16 @@ class LocalRolesTemplateItem(BaseTemplateItem):
id = role.get('id') id = role.get('id')
item_type_list = [item.text for item in role] item_type_list = [item.text for item in role]
local_roles_dict[id] = item_type_list local_roles_dict[id] = item_type_list
self._objects['local_roles/%s' % (file_name[:-4],)] = (local_roles_dict, )
# local roles group id
local_roles_group_id_dict = {}
for principal in xml.findall('//principal'):
local_roles_group_id_dict[principal.get('id')] = tuple(
[group_id.text for group_id in
principal.findall('./local_roles_group_id')])
self._objects['local_roles/%s' % (file_name[:-4],)] = (
local_roles_dict, local_roles_group_id_dict)
def install(self, context, trashbin, **kw): def install(self, context, trashbin, **kw):
update_dict = kw.get('object_to_update') update_dict = kw.get('object_to_update')
...@@ -4585,8 +4628,22 @@ class LocalRolesTemplateItem(BaseTemplateItem): ...@@ -4585,8 +4628,22 @@ class LocalRolesTemplateItem(BaseTemplateItem):
continue continue
path = roles_path.split('/')[1:] path = roles_path.split('/')[1:]
obj = p.unrestrictedTraverse(path) obj = p.unrestrictedTraverse(path)
local_roles_dict = self._objects[roles_path][0] # again we might be installing an business template in format before
# existance of local roles group id.
if len(self._objects[roles_path]) == 2:
local_roles_dict, local_roles_group_id_dict = self._objects[roles_path]
else:
local_roles_group_id_dict = dict()
local_roles_dict, = self._objects[roles_path]
setattr(obj, '__ac_local_roles__', local_roles_dict) setattr(obj, '__ac_local_roles__', local_roles_dict)
if local_roles_group_id_dict:
setattr(obj, '__ac_local_roles_group_id_dict__',
local_roles_group_id_dict)
# we try to have __ac_local_roles_group_id_dict__ set only if
# it is actually defining something else than default
elif getattr(aq_base(obj), '__ac_local_roles_group_id_dict__',
None) is not None:
delattr(obj, '__ac_local_roles_group_id_dict__')
obj.reindexObject() obj.reindexObject()
def uninstall(self, context, object_path=None, **kw): def uninstall(self, context, object_path=None, **kw):
...@@ -4748,6 +4805,7 @@ Business Template is a set of definitions, such as skins, portal types and categ ...@@ -4748,6 +4805,7 @@ Business Template is a set of definitions, such as skins, portal types and categ
'_catalog_scriptable_key_item', '_catalog_scriptable_key_item',
'_catalog_role_key_item', '_catalog_role_key_item',
'_catalog_local_role_key_item', '_catalog_local_role_key_item',
'_catalog_security_uid_column_item',
] ]
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
...@@ -4910,6 +4968,9 @@ Business Template is a set of definitions, such as skins, portal types and categ ...@@ -4910,6 +4968,9 @@ Business Template is a set of definitions, such as skins, portal types and categ
self._catalog_local_role_key_item = \ self._catalog_local_role_key_item = \
CatalogLocalRoleKeyTemplateItem( CatalogLocalRoleKeyTemplateItem(
self.getTemplateCatalogLocalRoleKeyList()) self.getTemplateCatalogLocalRoleKeyList())
self._catalog_security_uid_column_item = \
CatalogSecurityUidColumnTemplateItem(
self.getTemplateCatalogSecurityUidColumnList())
security.declareProtected(Permissions.ManagePortal, 'build') security.declareProtected(Permissions.ManagePortal, 'build')
def build(self, no_action=0): def build(self, no_action=0):
...@@ -5631,6 +5692,7 @@ Business Template is a set of definitions, such as skins, portal types and categ ...@@ -5631,6 +5692,7 @@ Business Template is a set of definitions, such as skins, portal types and categ
'CatalogScriptableKey' : '_catalog_scriptable_key_item', 'CatalogScriptableKey' : '_catalog_scriptable_key_item',
'CatalogRoleKey' : '_catalog_role_key_item', 'CatalogRoleKey' : '_catalog_role_key_item',
'CatalogLocalRoleKey' : '_catalog_local_role_key_item', 'CatalogLocalRoleKey' : '_catalog_local_role_key_item',
'CatalogSecurityUidColumn' : '_catalog_security_uid_column_item',
} }
object_id = REQUEST.object_id object_id = REQUEST.object_id
...@@ -5693,6 +5755,7 @@ Business Template is a set of definitions, such as skins, portal types and categ ...@@ -5693,6 +5755,7 @@ Business Template is a set of definitions, such as skins, portal types and categ
'_catalog_scriptable_key_item', '_catalog_scriptable_key_item',
'_catalog_role_key_item', '_catalog_role_key_item',
'_catalog_local_role_key_item', '_catalog_local_role_key_item',
'_catalog_security_uid_column_item',
'_portal_type_allowed_content_type_item', '_portal_type_allowed_content_type_item',
'_portal_type_hidden_content_type_item', '_portal_type_hidden_content_type_item',
'_portal_type_property_sheet_item', '_portal_type_property_sheet_item',
......
...@@ -91,6 +91,7 @@ ...@@ -91,6 +91,7 @@
<string>my_template_catalog_topic_key_list</string> <string>my_template_catalog_topic_key_list</string>
<string>my_template_catalog_scriptable_key_list</string> <string>my_template_catalog_scriptable_key_list</string>
<string>my_template_catalog_local_role_key_list</string> <string>my_template_catalog_local_role_key_list</string>
<string>my_template_catalog_security_uid_column_list</string>
</list> </list>
</value> </value>
</item> </item>
......
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
<string>my_title</string> <string>my_title</string>
<string>my_role_name_list</string> <string>my_role_name_list</string>
<string>my_condition</string> <string>my_condition</string>
<string>my_local_roles_group_id</string>
</list> </list>
</value> </value>
</item> </item>
......
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
</item> </item>
<item> <item>
<key> <string>arguments_src</string> </key> <key> <string>arguments_src</string> </key>
<value> <string>security_uid\r\n <value> <string>optimised_roles_and_users</string> </value>
optimised_roles_and_users</string> </value>
</item> </item>
<item> <item>
<key> <string>cache_time_</string> </key> <key> <string>cache_time_</string> </key>
...@@ -56,19 +55,17 @@ optimised_roles_and_users</string> </value> ...@@ -56,19 +55,17 @@ optimised_roles_and_users</string> </value>
<value> <string encoding="cdata"><![CDATA[ <value> <string encoding="cdata"><![CDATA[
<dtml-let row_list="[]">\n <dtml-let row_list="[]">\n
<dtml-in prefix="loop" expr="_.range(_.len(security_uid))">\n <dtml-in prefix="loop" expr="_.range(_.len(optimised_roles_and_users))">\n
<dtml-if expr="optimised_roles_and_users[loop_item]">\n
<dtml-in prefix="role" expr="optimised_roles_and_users[loop_item]">\n <dtml-in prefix="role" expr="optimised_roles_and_users[loop_item]">\n
<dtml-call expr="row_list.append([security_uid[loop_item], role_item])">\n <dtml-call expr="row_list.append([role_item[0], role_item[1], role_item[2]])">\n
</dtml-in>\n </dtml-in>\n
</dtml-if>\n
</dtml-in>\n </dtml-in>\n
<dtml-if expr="row_list">\n <dtml-if expr="row_list">\n
INSERT INTO\n INSERT INTO\n
roles_and_users\n roles_and_users\n
VALUES\n VALUES\n
<dtml-in prefix="row" expr="row_list">\n <dtml-in prefix="row" expr="row_list">\n
(<dtml-sqlvar expr="row_item[0]" type="string">, <dtml-sqlvar expr="row_item[1]" type="string">)\n (<dtml-sqlvar expr="row_item[0]" type="string">, <dtml-sqlvar expr="row_item[1]" type="string">, <dtml-sqlvar expr="row_item[2]" type="string">)\n
<dtml-if sequence-end><dtml-else>,</dtml-if>\n <dtml-if sequence-end><dtml-else>,</dtml-if>\n
</dtml-in>\n </dtml-in>\n
</dtml-if>\n </dtml-if>\n
......
...@@ -54,6 +54,7 @@ ...@@ -54,6 +54,7 @@
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string>CREATE TABLE roles_and_users (\n <value> <string>CREATE TABLE roles_and_users (\n
uid INT UNSIGNED,\n uid INT UNSIGNED,\n
local_roles_group_id VARCHAR(255),\n
allowedRolesAndUsers VARCHAR(255),\n allowedRolesAndUsers VARCHAR(255),\n
KEY `uid` (`uid`),\n KEY `uid` (`uid`),\n
KEY `allowedRolesAndUsers` (`allowedRolesAndUsers`)\n KEY `allowedRolesAndUsers` (`allowedRolesAndUsers`)\n
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<value> <string encoding="cdata"><![CDATA[ <value> <string encoding="cdata"><![CDATA[
SELECT \n SELECT \n
DISTINCT uid \n DISTINCT uid, local_roles_group_id\n
FROM \n FROM \n
roles_and_users \n roles_and_users \n
WHERE \n WHERE \n
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_local_properties</string> </key>
<value>
<tuple>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>mode</string> </value>
</item>
<item>
<key> <string>type</string> </key>
<value> <string>string</string> </value>
</item>
</dictionary>
</tuple>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/lines</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>template_catalog_security_uid_column_property</string> </value>
</item>
<item>
<key> <string>mode</string> </key>
<value> <string>w</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
<item>
<key> <string>property_default</string> </key>
<value> <string>python: ()</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_local_properties</string> </key>
<value>
<tuple>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>mode</string> </value>
</item>
<item>
<key> <string>type</string> </key>
<value> <string>string</string> </value>
</item>
</dictionary>
</tuple>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/string</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>local_roles_group_id_property</string> </value>
</item>
<item>
<key> <string>mode</string> </key>
<value> <string>w</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
<item>
<key> <string>property_default</string> </key>
<value> <string>python: \'\'</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -6524,6 +6524,84 @@ class TestBusinessTemplate(BusinessTemplateMixin): ...@@ -6524,6 +6524,84 @@ class TestBusinessTemplate(BusinessTemplateMixin):
new_bt5_obj.build() new_bt5_obj.build()
template_tool.export(new_bt5_obj) template_tool.export(new_bt5_obj)
def test_local_roles_group_id(self):
"""Tests that roles definition defining local roles group ids are properly
exported and installed.
"""
# change security uid columns
sql_catalog = self.portal.portal_catalog.getSQLCatalog()
saved_sql_catalog_security_uid_columns = \
sql_catalog.sql_catalog_security_uid_columns
sql_catalog.sql_catalog_security_uid_columns = (
' | security_uid',
'Alternate | alternate_security_uid',
)
types_tool = self.portal.portal_types
object_type = types_tool.newContent('Geek Object', 'Base Type',
type_class='Person')
types_tool.newContent('Geek Module', 'Base Type',
type_class='Folder',
type_filter_content_type=1,
type_allowed_content_type_list=('Geek Object',), )
self.portal.newContent(portal_type='Geek Module', id='geek_module')
new_object = self.portal.geek_module.newContent(
portal_type='Geek Object', id='1')
# simulate role assignment
new_object.__ac_local_roles__ = dict(group=['Assignee'])
new_object.__ac_local_roles_group_id_dict__ = dict(group=('Alternate',))
self.tic()
object_type.newContent(portal_type='Role Information',
local_roles_group_id='Alternate',
role_name_list=('Assignee', ))
bt = self.portal.portal_templates.newContent(
portal_type='Business Template',
title=self.id(),
template_local_roles_list=('geek_module/1',),
template_path_list=('geek_module/1',),
template_portal_type_role_list=('Geek Object',),)
self.tic()
bt.build()
self.tic()
export_dir = tempfile.mkdtemp()
try:
bt.export(path=export_dir, local=True)
self.tic()
new_bt = self.portal.portal_templates.download(
url='file://%s' % export_dir)
finally:
shutil.rmtree(export_dir)
# uninstall role information and paths
object_type.manage_delObjects([x.id for x in object_type.getRoleInformationList()])
self.portal.geek_module.manage_delObjects(['1'])
self.tic()
new_bt.install()
try:
role, = object_type.getRoleInformationList()
self.assertEquals('Alternate', role.getLocalRolesGroupId())
path = self.portal.geek_module['1']
self.assertEquals([('group', ['Assignee'],)], [item for item in
path.__ac_local_roles__.items() if item[1] != ['Owner']])
self.assertEquals(dict(group=('Alternate',)),
path.__ac_local_roles_group_id_dict__)
finally:
# restore state
sql_catalog.sql_catalog_security_uid_columns = \
saved_sql_catalog_security_uid_columns
types_tool.manage_delObjects(['Geek Object', 'Geek Module'])
self.portal.manage_delObjects(['geek_module'])
self.tic()
def test_BusinessTemplateWithTest(self): def test_BusinessTemplateWithTest(self):
sequence_list = SequenceList() sequence_list = SequenceList()
sequence_string = '\ sequence_string = '\
......
This diff is collapsed.
...@@ -4426,6 +4426,133 @@ VALUES ...@@ -4426,6 +4426,133 @@ VALUES
self.assertEqual([x.getObject() for x in catalog.searchResults(**query_lj)], self.assertEqual([x.getObject() for x in catalog.searchResults(**query_lj)],
[org_a.default_address]) [org_a.default_address])
def test_local_roles_group_id_on_role_information(self):
"""Test usage of local_roles_group_id when searching catalog.
"""
sql_connection = self.getSQLConnection()
sql_catalog = self.portal.portal_catalog.getSQLCatalog()
# Add a catalog table (uid, alternate_security_uid)
sql_connection.manage_test(
"""DROP TABLE IF EXISTS alternate_roles_and_users""")
sql_connection.manage_test("""
CREATE TABLE alternate_roles_and_users (
`uid` BIGINT UNSIGNED NOT NULL,
`alternate_security_uid` INT UNSIGNED) """)
# make it a search table
current_sql_search_tables = sql_catalog.sql_search_tables
sql_catalog.sql_search_tables = sql_catalog.sql_search_tables + [
'alternate_roles_and_users']
# Configure sql method to insert this table
sql_catalog.manage_addProduct['ZSQLMethods'].manage_addZSQLMethod(
id='z_catalog_alternate_roles_and_users_list',
title='',
connection_id='erp5_sql_connection',
arguments="\n".join(['uid', 'alternate_security_uid']),
template="""REPLACE INTO alternate_roles_and_users VALUES
<dtml-in prefix="loop" expr="_.range(_.len(uid))">
( <dtml-sqlvar expr="uid[loop_item]" type="int">,
<dtml-sqlvar expr="alternate_security_uid[loop_item]" type="int" optional>
)<dtml-unless sequence-end>,</dtml-unless>
</dtml-in>""")
current_sql_catalog_object_list = sql_catalog.sql_catalog_object_list
sql_catalog.sql_catalog_object_list = \
current_sql_catalog_object_list + \
('z_catalog_alternate_roles_and_users_list',)
# configure Alternate local roles group id to go in alternate_security_uid
current_sql_catalog_security_uid_columns =\
sql_catalog.sql_catalog_security_uid_columns
sql_catalog.sql_catalog_security_uid_columns = (
' | security_uid',
'Alternate | alternate_security_uid', )
# configure security on person, each user will be able to see his own
# person thanks to an Auditor role on "Alternate" local roles group id.
self.portal.portal_types.Person.newContent(
portal_type='Role Information',
role_name='Auditor',
role_base_category_script_id='ERP5Type_getSecurityCategoryFromSelf',
role_base_category='agent',
local_roles_group_id='Alternate')
self.portal.portal_caches.clearAllCache()
self.tic()
try:
# create two persons and users
user1 = self.portal.person_module.newContent(portal_type='Person',
reference='user1')
user1.newContent(portal_type='Assignment').open()
user1.updateLocalRolesOnSecurityGroups()
self.assertEquals(user1.__ac_local_roles__.get('user1'), ['Auditor'])
user2 = self.portal.person_module.newContent(portal_type='Person',
reference='user2')
user2.newContent(portal_type='Assignment').open()
user2.updateLocalRolesOnSecurityGroups()
self.assertEquals(user2.__ac_local_roles__.get('user2'), ['Auditor'])
self.tic()
# security_uid_dict in catalog contains entries for user1 and user2:
user1_alternate_security_uid = sql_catalog.security_uid_dict[
('Alternate', ('user:user1', 'user:user1:Auditor'))]
bob_alternate_security_uid = sql_catalog.security_uid_dict[
('Alternate', ('user:user2', 'user:user2:Auditor'))]
# those entries are in alternate security table
alternate_roles_and_users = sql_connection.manage_test(
"SELECT * from alternate_roles_and_users").dictionaries()
self.assertTrue(dict(uid=user1.getUid(),
alternate_security_uid=user1_alternate_security_uid) in
alternate_roles_and_users)
self.assertTrue(dict(uid=user2.getUid(),
alternate_security_uid=bob_alternate_security_uid) in
alternate_roles_and_users)
# low level check of the security query of a logged in user
self.login('user1')
security_query = self.portal.portal_catalog.getSecurityQuery()
# This query is a complex query wrapping another complex query with a
# criterion on altenate_security_uid. This check is quite low level and
# is subject to change.
security_uid_query = security_query.query_list[0]
alternate_security_query, = [q for q in
security_query.query_list[0].query_list if
q.kw.get('alternate_security_uid')]
self.assertEquals([user1_alternate_security_uid],
alternate_security_query.kw['alternate_security_uid'])
# high level check that that logged in user can see document
self.assertEquals([user1],
[o.getObject() for o in self.portal.portal_catalog(portal_type='Person')])
# also with local_roles= argument which is used in worklists
self.assertEquals([user1],
[o.getObject() for o in self.portal.portal_catalog(portal_type='Person',
local_roles='Auditor')])
# searches still work for other users
self.login('ERP5TypeTestCase')
self.assertSameSet([user1, user2],
[o.getObject() for o in
self.portal.portal_catalog(portal_type='Person')])
finally:
# restore catalog configuration
sql_catalog.sql_search_tables = current_sql_search_tables
sql_catalog.sql_catalog_object_list = current_sql_catalog_object_list
sql_catalog.sql_catalog_security_uid_columns =\
current_sql_catalog_security_uid_columns
self.portal.portal_types.Person.manage_delObjects(
[role.getId() for role in
self.portal.portal_types.Person.contentValues(
portal_type='Role Information')])
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestERP5Catalog)) suite.addTest(unittest.makeSuite(TestERP5Catalog))
......
...@@ -571,6 +571,25 @@ class TestLocalRoleManagement(ERP5TypeTestCase): ...@@ -571,6 +571,25 @@ class TestLocalRoleManagement(ERP5TypeTestCase):
self.assertFalse('Assignee' in user.getRolesInContext(obj)) self.assertFalse('Assignee' in user.getRolesInContext(obj))
self.abort() self.abort()
def testLocalRolesGroupId(self):
"""Assigning a role with local roles group id.
"""
self._getTypeInfo().newContent(portal_type='Role Information',
role_name='Assignor',
local_roles_group_id='Alternate',
role_category=self.defined_category)
self.loginAsUser(self.username)
user = getSecurityManager().getUser()
obj = self._makeOne()
self.assertEqual(['Assignor'], obj.__ac_local_roles__.get('F1_G1_S1'))
self.assertTrue('Assignor' in user.getRolesInContext(obj))
self.assertEqual(('Alternate',),
obj.__ac_local_roles_group_id_dict__.get('F1_G1_S1'))
self.abort()
def testDynamicLocalRole(self): def testDynamicLocalRole(self):
"""Test simple case of setting a dynamic role. """Test simple case of setting a dynamic role.
The site category is not defined explictly the role, and will have the The site category is not defined explictly the role, and will have the
......
...@@ -183,5 +183,4 @@ class RoleInformation(XMLObject): ...@@ -183,5 +183,4 @@ class RoleInformation(XMLObject):
return group_id_role_dict return group_id_role_dict
InitializeClass(RoleInformation) InitializeClass(RoleInformation)
...@@ -89,7 +89,22 @@ class LocalRoleAssignorMixIn(object): ...@@ -89,7 +89,22 @@ class LocalRoleAssignorMixIn(object):
else: else:
user_name = getSecurityManager().getUser().getId() user_name = getSecurityManager().getUser().getId()
group_id_role_dict = self.getLocalRolesFor(ob, user_name) group_id_role_dict = {}
local_roles_group_id_group_id = {}
# Merge results from applicable roles
for role_generator in self.getFilteredRoleListFor(ob):
local_roles_group_id = role_generator.getProperty('local_roles_group_id', '')
for group_id, role_list \
in role_generator.getLocalRolesFor(ob, user_name).iteritems():
group_id_role_dict.setdefault(group_id, set()).update(role_list)
# don't keep track of default group not to increase db size
if local_roles_group_id:
if local_roles_group_id not in \
local_roles_group_id_group_id.get(group_id, ()):
local_roles_group_id_group_id[group_id] = \
local_roles_group_id_group_id.get(group_id, ()) +\
(local_roles_group_id,)
## Update role assignments to groups ## Update role assignments to groups
# Save the owner # Save the owner
...@@ -101,24 +116,17 @@ class LocalRoleAssignorMixIn(object): ...@@ -101,24 +116,17 @@ class LocalRoleAssignorMixIn(object):
for group, role_list in group_id_role_dict.iteritems(): for group, role_list in group_id_role_dict.iteritems():
if role_list: if role_list:
ac_local_roles[group] = list(role_list) ac_local_roles[group] = list(role_list)
if local_roles_group_id_group_id:
ob.__ac_local_roles_group_id_dict__ = local_roles_group_id_group_id
elif getattr(aq_base(ob),
'__ac_local_roles_group_id_dict__', None) is not None:
delattr(ob, '__ac_local_roles_group_id_dict__')
## Make sure that the object is reindexed ## Make sure that the object is reindexed
if reindex: if reindex:
ob.reindexObjectSecurity() ob.reindexObjectSecurity()
security.declarePrivate("getLocalRolesFor")
def getLocalRolesFor(self, ob, user_name=None):
"""Compute the security that should be applied on an object
Returned value is a dict: {groud_id: role_name_set, ...}
"""
group_id_role_dict = {}
# Merge results from applicable roles
for role in self.getFilteredRoleListFor(ob):
for group_id, role_list \
in role.getLocalRolesFor(ob, user_name).iteritems():
group_id_role_dict.setdefault(group_id, set()).update(role_list)
return group_id_role_dict
security.declarePrivate('getFilteredRoleListFor') security.declarePrivate('getFilteredRoleListFor')
def getFilteredRoleListFor(self, ob=None): def getFilteredRoleListFor(self, ob=None):
"""Return all role generators applicable to the object.""" """Return all role generators applicable to the object."""
......
...@@ -39,7 +39,7 @@ class ILocalRoleGenerator(Interface): ...@@ -39,7 +39,7 @@ class ILocalRoleGenerator(Interface):
Returned value is a dict: {groud_id: role_name_set, ...} Returned value is a dict: {groud_id: role_name_set, ...}
""" """
class ILocalRoleAssignor(ILocalRoleGenerator): class ILocalRoleAssignor(Interface):
""" """
""" """
def updateLocalRolesOnDocument(ob, user_name=None, reindex=True): def updateLocalRolesOnDocument(ob, user_name=None, reindex=True):
......
...@@ -89,7 +89,6 @@ DCWorkflowDefinition.notifySuccess = DCWorkflowDefinition_notifySuccess ...@@ -89,7 +89,6 @@ DCWorkflowDefinition.notifySuccess = DCWorkflowDefinition_notifySuccess
WORKLIST_METADATA_KEY = 'metadata' WORKLIST_METADATA_KEY = 'metadata'
SECURITY_PARAMETER_ID = 'local_roles' SECURITY_PARAMETER_ID = 'local_roles'
SECURITY_COLUMN_ID = 'security_uid'
COUNT_COLUMN_TITLE = 'count' COUNT_COLUMN_TITLE = 'count'
class ExclusionList(list): class ExclusionList(list):
...@@ -145,7 +144,7 @@ def updateWorklistSetDict(worklist_set_dict, workflow_worklist_key, valid_criter ...@@ -145,7 +144,7 @@ def updateWorklistSetDict(worklist_set_dict, workflow_worklist_key, valid_criter
[workflow_worklist_key] = valid_criterion_dict [workflow_worklist_key] = valid_criterion_dict
def groupWorklistListByCondition(worklist_dict, sql_catalog, def groupWorklistListByCondition(worklist_dict, sql_catalog,
getSecurityUidListAndRoleColumnDict=None): getSecurityUidDictAndRoleColumnDict=None):
""" """
Get a list of dict of WorklistVariableMatchDict grouped by compatible Get a list of dict of WorklistVariableMatchDict grouped by compatible
conditions. conditions.
...@@ -185,7 +184,7 @@ def groupWorklistListByCondition(worklist_dict, sql_catalog, ...@@ -185,7 +184,7 @@ def groupWorklistListByCondition(worklist_dict, sql_catalog,
for workflow_id, worklist in worklist_dict.iteritems(): for workflow_id, worklist in worklist_dict.iteritems():
for worklist_id, worklist_match_dict in worklist.iteritems(): for worklist_id, worklist_match_dict in worklist.iteritems():
workflow_worklist_key = '/'.join((workflow_id, worklist_id)) workflow_worklist_key = '/'.join((workflow_id, worklist_id))
if getSecurityUidListAndRoleColumnDict is None: if getSecurityUidDictAndRoleColumnDict is None:
valid_criterion_dict, metadata = getValidCriterionDict( valid_criterion_dict, metadata = getValidCriterionDict(
worklist_match_dict=worklist_match_dict, worklist_match_dict=worklist_match_dict,
sql_catalog=sql_catalog, sql_catalog=sql_catalog,
...@@ -201,15 +200,18 @@ def groupWorklistListByCondition(worklist_dict, sql_catalog, ...@@ -201,15 +200,18 @@ def groupWorklistListByCondition(worklist_dict, sql_catalog,
security_kw = {} security_kw = {}
if len(security_parameter): if len(security_parameter):
security_kw[SECURITY_PARAMETER_ID] = security_parameter security_kw[SECURITY_PARAMETER_ID] = security_parameter
uid_list, role_column_dict, local_role_column_dict = \ uid_dict, role_column_dict, local_role_column_dict = \
getSecurityUidListAndRoleColumnDict(**security_kw) getSecurityUidDictAndRoleColumnDict(**security_kw)
for key, value in local_role_column_dict.items(): for key, value in local_role_column_dict.items():
worklist_match_dict[key] = [value] worklist_match_dict[key] = [value]
if len(uid_list): catalog_security_uid_groups_columns_dict = \
uid_list.sort() sql_catalog.getSQLCatalogSecurityUidGroupsColumnsDict()
role_column_dict[SECURITY_COLUMN_ID] = uid_list for local_roles_group_id, uid_list in uid_dict.iteritems():
role_column_dict[
catalog_security_uid_groups_columns_dict[local_roles_group_id]] = uid_list
# Make sure every item is a list - or a tuple # Make sure every item is a list - or a tuple
for security_column_id in role_column_dict.iterkeys(): for security_column_id in role_column_dict.iterkeys():
value = role_column_dict[security_column_id] value = role_column_dict[security_column_id]
...@@ -271,6 +273,8 @@ def generateNestedQuery(priority_list, criterion_dict, ...@@ -271,6 +273,8 @@ def generateNestedQuery(priority_list, criterion_dict,
**{my_criterion_id: criterion_value}) **{my_criterion_id: criterion_value})
if isinstance(criterion_value, ExclusionTuple): if isinstance(criterion_value, ExclusionTuple):
query = NegatedQuery(query) query = NegatedQuery(query)
query = ComplexQuery(operator='OR',
*(query, Query(**{my_criterion_id: None})))
append(ComplexQuery(query, subcriterion_query, operator='AND')) append(ComplexQuery(query, subcriterion_query, operator='AND'))
else: else:
possible_value_list = tuple() possible_value_list = tuple()
...@@ -296,6 +300,8 @@ def generateNestedQuery(priority_list, criterion_dict, ...@@ -296,6 +300,8 @@ def generateNestedQuery(priority_list, criterion_dict,
if len(impossible_value_list): if len(impossible_value_list):
query = Query(operator='IN', **{my_criterion_id: impossible_value_list}) query = Query(operator='IN', **{my_criterion_id: impossible_value_list})
query = NegatedQuery(query) query = NegatedQuery(query)
query = ComplexQuery(operator='OR',
*(query, Query(**{my_criterion_id: None})))
value_query_list.append(query) value_query_list.append(query)
append(ComplexQuery(operator='AND', *value_query_list)) append(ComplexQuery(operator='AND', *value_query_list))
if len(query_list): if len(query_list):
...@@ -462,8 +468,8 @@ def WorkflowTool_listActions(self, info=None, object=None, src__=False): ...@@ -462,8 +468,8 @@ def WorkflowTool_listActions(self, info=None, object=None, src__=False):
else: else:
search_result = portal_catalog.unrestrictedSearchResults search_result = portal_catalog.unrestrictedSearchResults
select_expression_prefix = 'count(*) as %s' % (COUNT_COLUMN_TITLE, ) select_expression_prefix = 'count(*) as %s' % (COUNT_COLUMN_TITLE, )
getSecurityUidListAndRoleColumnDict = \ getSecurityUidDictAndRoleColumnDict = \
portal_catalog.getSecurityUidListAndRoleColumnDict portal_catalog.getSecurityUidDictAndRoleColumnDict
security_query_cache_dict = {} security_query_cache_dict = {}
def _getWorklistActionList(): def _getWorklistActionList():
worklist_result_dict = {} worklist_result_dict = {}
...@@ -474,8 +480,8 @@ def WorkflowTool_listActions(self, info=None, object=None, src__=False): ...@@ -474,8 +480,8 @@ def WorkflowTool_listActions(self, info=None, object=None, src__=False):
groupWorklistListByCondition( groupWorklistListByCondition(
worklist_dict=worklist_dict, worklist_dict=worklist_dict,
sql_catalog=sql_catalog, sql_catalog=sql_catalog,
getSecurityUidListAndRoleColumnDict=\ getSecurityUidDictAndRoleColumnDict=\
getSecurityUidListAndRoleColumnDict) getSecurityUidDictAndRoleColumnDict)
if src__: if src__:
action_list = [] action_list = []
for grouped_worklist_dict in worklist_list_grouped_by_condition: for grouped_worklist_dict in worklist_list_grouped_by_condition:
...@@ -576,7 +582,8 @@ def WorkflowTool_refreshWorklistCache(self): ...@@ -576,7 +582,8 @@ def WorkflowTool_refreshWorklistCache(self):
sql_catalog = portal_catalog.getSQLCatalog() sql_catalog = portal_catalog.getSQLCatalog()
table_column_id_set = ImmutableSet( table_column_id_set = ImmutableSet(
[COUNT_COLUMN_TITLE] + self.Base_getWorklistTableColumnIDList()) [COUNT_COLUMN_TITLE] + self.Base_getWorklistTableColumnIDList())
security_column_id_list = ['security_uid'] + \ security_column_id_list = list(
sql_catalog.getSQLCatalogSecurityUidGroupsColumnsDict().values()) + \
[x[1] for x in sql_catalog.getSQLCatalogRoleKeysList()] + \ [x[1] for x in sql_catalog.getSQLCatalogRoleKeysList()] + \
[x[1] for x in sql_catalog.getSQLCatalogLocalRoleKeysList()] [x[1] for x in sql_catalog.getSQLCatalogLocalRoleKeysList()]
(worklist_list_grouped_by_condition, worklist_metadata) = \ (worklist_list_grouped_by_condition, worklist_metadata) = \
......
...@@ -524,6 +524,14 @@ class Catalog(Folder, ...@@ -524,6 +524,14 @@ class Catalog(Folder,
'a monovalued local role', 'a monovalued local role',
'type': 'lines', 'type': 'lines',
'mode': 'w' }, 'mode': 'w' },
{ 'id': 'sql_catalog_security_uid_columns',
'title': 'Security Uid Columns',
'description': 'A list of mappings "local_roles_group_id | security_uid_column"'
' local_roles_group_id will be the name of a local roles'
' group, and security_uid_column is the corresponding'
' column in catalog table',
'type': 'lines',
'mode': 'w' },
{ 'id': 'sql_catalog_table_vote_scripts', { 'id': 'sql_catalog_table_vote_scripts',
'title': 'Table vote scripts', 'title': 'Table vote scripts',
'description': 'Scripts helping column mapping resolution', 'description': 'Scripts helping column mapping resolution',
...@@ -575,6 +583,7 @@ class Catalog(Folder, ...@@ -575,6 +583,7 @@ class Catalog(Folder,
sql_catalog_scriptable_keys = () sql_catalog_scriptable_keys = ()
sql_catalog_role_keys = () sql_catalog_role_keys = ()
sql_catalog_local_role_keys = () sql_catalog_local_role_keys = ()
sql_catalog_security_uid_columns = (' | security_uid',)
sql_catalog_table_vote_scripts = () sql_catalog_table_vote_scripts = ()
sql_catalog_raise_error_on_uid_check = True sql_catalog_raise_error_on_uid_check = True
...@@ -626,6 +635,18 @@ class Catalog(Folder, ...@@ -626,6 +635,18 @@ class Catalog(Folder,
role_key_dict[role.strip()] = column.strip() role_key_dict[role.strip()] = column.strip()
return role_key_dict.items() return role_key_dict.items()
def getSQLCatalogSecurityUidGroupsColumnsDict(self):
"""
Return a mapping of local_roles_group_id name to the name of the column
storing corresponding security_uid.
The default mappiny is {'': 'security_uid'}
"""
local_roles_group_id_dict = {}
for local_roles_group_id_key in self.sql_catalog_security_uid_columns:
local_roles_group_id, column = local_roles_group_id_key.split('|')
local_roles_group_id_dict[local_roles_group_id.strip()] = column.strip()
return local_roles_group_id_dict
def getSQLCatalogLocalRoleKeysList(self): def getSQLCatalogLocalRoleKeysList(self):
""" """
Return the list of local role keys. Return the list of local role keys.
...@@ -765,38 +786,41 @@ class Catalog(Folder, ...@@ -765,38 +786,41 @@ class Catalog(Folder,
self.subject_set_uid_dict = OIBTree() self.subject_set_uid_dict = OIBTree()
self.subject_set_uid_index = None self.subject_set_uid_index = None
security.declarePrivate('getSecurityUid') security.declarePrivate('getSecurityUidDict')
def getSecurityUid(self, wrapped_object): def getSecurityUidDict(self, wrapped_object):
""" """
Cache a uid for each security permission returns a tuple with a dict of security uid by local group id, and a tuple
Return a tuple with a security uid (string) and a new tuple content the containing optimised_roles_and_users that might have been created.
roles and users if not exist already.
With the roles of object, search the security_uid associate in the
catalog_innodb:
- if the security not exist a security uid is generated with id_tool
or security_uid_index property and
return the new security_uid and the tuple contains the new roles
to add the roles in roles_and_user table of the database.
- if the security exist the security uid is returned and the second
element is None for not recreate the security in roles_and_user
table of the database.
We try to create a unique security (to reduce number of lines)
and to assign security only to root document
""" """
# Get security information
allowed_roles_and_users = tuple(wrapped_object.allowedRolesAndUsers())
# Make sure no duplicates
if getattr(aq_base(self), 'security_uid_dict', None) is None: if getattr(aq_base(self), 'security_uid_dict', None) is None:
self._clearSecurityCache() self._clearSecurityCache()
elif self.security_uid_dict.has_key(allowed_roles_and_users):
return (self.security_uid_dict[allowed_roles_and_users], None) id_tool = getattr(self.getPortalObject(), 'portal_ids', None)
optimised_roles_and_users = []
local_roles_group_id_to_security_uid_mapping= dict()
# Get security information
for local_roles_group_id, allowed_roles_and_users in\
wrapped_object.getLocalRolesGroupIdDict().iteritems():
allowed_roles_and_users = tuple(sorted(allowed_roles_and_users))
key = (local_roles_group_id, allowed_roles_and_users)
if self.security_uid_dict.has_key(key):
local_roles_group_id_to_security_uid_mapping[local_roles_group_id] \
= self.security_uid_dict[key]
elif self.security_uid_dict.has_key(allowed_roles_and_users)\
and not local_roles_group_id:
# This key is present in security_uid_dict without
# local_roles_group_id, it has been inserted before
# local_roles_group_id were introduced.
local_roles_group_id_to_security_uid_mapping[local_roles_group_id] = \
self.security_uid_dict[allowed_roles_and_users]
else:
# If the id_tool is there, it is better to use it, it allows # If the id_tool is there, it is better to use it, it allows
# to create many new security uids by the same time # to create many new security uids by the same time
# because with this tool we are sure that we will have 2 different # because with this tool we are sure that we will have 2 different
# uids if two instances are doing this code in the same time # uids if two instances are doing this code in the same time
id_tool = getattr(self.getPortalObject(), 'portal_ids', None)
if id_tool is not None: if id_tool is not None:
default = 1 default = 1
# We must keep compatibility with existing sites # We must keep compatibility with existing sites
...@@ -818,8 +842,17 @@ class Catalog(Folder, ...@@ -818,8 +842,17 @@ class Catalog(Folder,
previous_security_uid = previous_security_uid() previous_security_uid = previous_security_uid()
security_uid = previous_security_uid + 1 security_uid = previous_security_uid + 1
self.security_uid_index = security_uid self.security_uid_index = security_uid
self.security_uid_dict[allowed_roles_and_users] = security_uid
return (security_uid, allowed_roles_and_users) self.security_uid_dict[key] = security_uid
local_roles_group_id_to_security_uid_mapping[local_roles_group_id]\
= security_uid
# If some optimised_roles_and_users are returned by this method it
# means that new entries will have to be added to roles_and_users table.
for user in allowed_roles_and_users:
optimised_roles_and_users.append((security_uid, local_roles_group_id, user))
return (local_roles_group_id_to_security_uid_mapping, optimised_roles_and_users)
def getRoleAndSecurityUidList(self): def getRoleAndSecurityUidList(self):
""" """
...@@ -828,7 +861,8 @@ class Catalog(Folder, ...@@ -828,7 +861,8 @@ class Catalog(Folder,
""" """
result = [] result = []
extend = result.extend extend = result.extend
for role_list, security_uid in getattr(aq_base(self), 'security_uid_dict', {}).iteritems(): for role_list, security_uid in getattr(
aq_base(self), 'security_uid_dict', {}).iteritems():
extend([(role, security_uid) for role in role_list]) extend([(role, security_uid) for role in role_list])
return result return result
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment