From 8ff4a7d42a7d6f4a03b89ad273c995535524ef41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Ates?= Date: Mon, 5 Sep 2011 12:17:27 +0200 Subject: [PATCH] [abac] Adding of the attribute profile management. All attributes are stored in an object profile used at the treatement of the ABAC permissions. Some of these attributes may be used in more that a single single user session. Then, a profile is recorded for each user. At the beginning of the permission checking, the profile is loaded or created. If it is loaded, all outdated attributes are removed. The function to load dictionnary of attributes handle the expiration date if it is ISO 8601 formatted: attr = {} attr['name'] = 'certificate_type' attr['namespace'] = 'ACS-ABAC' attr['values'] = ('eID',) attr['expiration_date'] = 2011-09-05T11:48:24 --- acs/abac/core.py | 322 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 261 insertions(+), 61 deletions(-) diff --git a/acs/abac/core.py b/acs/abac/core.py index 3e90183..85e30dc 100644 --- a/acs/abac/core.py +++ b/acs/abac/core.py @@ -19,6 +19,8 @@ import re import logging +import datetime +import time from django.db import transaction from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned @@ -59,24 +61,23 @@ def check_predicate(predicate, profile): return False -def check_predicate_comparison(predicate, profile): ''' - Grab definition of the predicate - Look in profile for a corresponding AssertionData - Then compare values + Every assertionData in the profile with a definition and a list of + sources are candidates for redicate checking. - An AssertionDefinition may indicate multiple sources - Then multiple AssertionData may correspond. It means that any of them - may satisfy the predicate + This match assertions provided by multiple source or even multiple + from the same source. - Here, we look for every assertionData in the profile - for a definition and a source - We accept provided by multiple source or even multiple from the same - source - One of them is enough to satisfy the comparison + !!! One of them is enough to satisfy the comparison. !!! + + In other words the multivalue management is handled only for + multivalues of the assertion, not between multiple assertions See function below to see how multi valued attributes are treated ''' + + +def check_predicate_comparison(predicate, profile): #operand1 l_data1 = [] data1 = predicate.operand1.get_assertion_instance() @@ -86,9 +87,9 @@ def check_predicate_comparison(predicate, profile): return False definition = data1.attribute_definition for source in sources: - ads = AssertionData.objects.filter(profile=profile, source=source.source) - for ad in ads: - if ad.attribute_data.definition.id == definition.id: + for ad in profile.assertions.all(): + if ad.source.id == source.source.id \ + and ad.attribute_data.definition.id == definition.id: l_data1.append(ad.attribute_data) if not l_data1: return False @@ -103,9 +104,9 @@ def check_predicate_comparison(predicate, profile): return False definition = data2.attribute_definition for source in sources: - ads = AssertionData.objects.filter(profile=profile, source=source.source) - for ad in ads: - if ad.attribute_data.definition.id == definition.id: + for ad in profile.assertions.all(): + if ad.source.id == source.source.id \ + and ad.attribute_data.definition.id == definition.id: l_data2.append(ad.attribute_data) if not l_data2: return False @@ -470,19 +471,19 @@ def check_predicate_required(predicate, profile): if not predicate.single_value: logger.debug("check_predicate_required: single value not required") for source in sources: - ads = AssertionData.objects.filter(profile=profile, source=source.source) - for ad in ads: - if ad.attribute_data.definition.id == definition.id: + for ad in profile.assertions.all(): + if ad.source.id == source.source.id \ + and ad.attribute_data.definition.id == definition.id: logger.debug("check_predicate_required: satisfied") return True else: logger.debug("check_predicate_required: single value required") found = False for source in sources: - ads = AssertionData.objects.filter(profile=profile, source=source.source) c = 0 - for ad in ads: - if ad.attribute_data.definition.id == definition.id: + for ad in profile.assertions.all(): + if ad.source.id == source.source.id \ + and ad.attribute_data.definition.id == definition.id: c = c + 1 if c > 1: logger.debug("check_predicate_required: multiple values found \ @@ -564,47 +565,178 @@ def get_def_from_name_and_ns(name, namespace): return None -def add_assertion_to_profile(profile, source, definition, values): +#@transaction.commit_manually +def add_assertion_to_profile(profile, source, definition, values, + expiration_date=None): ''' - values is a list of strings - converted to the right data type according to the datatype given by the - definition + Values is a list of strings converted to the data type given by the + definition datatype ''' - data = None - if definition.attribute_type == ACS_XACML_DATATYPE_STRING: - data = AttributeData(definition=definition) - data.save() - for value in values: - StringM(data=data, value=value).save() - elif definition.attribute_type == ACS_XACML_DATATYPE_BOOLEAN: - return None - elif definition.attribute_type == ACS_XACML_DATATYPE_INTEGER: - data = AttributeData(definition=definition) - data.save() - for value in values: - v = int(value) - IntegerM(data=data, value=v).save() - elif definition.attribute_type == ACS_XACML_DATATYPE_DOUBLE: - return None - elif definition.attribute_type == ACS_XACML_DATATYPE_TIME: - return None - elif definition.attribute_type == ACS_XACML_DATATYPE_DATE: - return None - elif definition.attribute_type == ACS_XACML_DATATYPE_DATETIME: - return None - elif definition.attribute_type == ACS_XACML_DATATYPE_RFC822NAME: - return None - elif definition.attribute_type == ACS_XACML_DATATYPE_IPADDRESS: + a = None + logger.debug('add_assertion_to_profile: Add assertion to profile %s \ + - (%s, %s, %s)' % (profile, source, definition, str(values))) + try: + data = None + try: + data = AttributeData(definition=definition) + data.save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to add the assertion \ + data due to %s' % err) + raise err + + if definition.attribute_type == ACS_XACML_DATATYPE_STRING: + for value in values: + try: + StringM(data=data, value=value).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to add value \ + %s due to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_BOOLEAN: + for value in values: + try: + v = False + if value in ('True', 'true', 'Vrai', 'vrai'): + v = True + BooleanM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + boolean or add value %sdue to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_INTEGER: + for value in values: + try: + v = int(value) + IntegerM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + integer or add value %s due to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_DOUBLE: + for value in values: + try: + v = float(value) + IntegerM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + double or add value %sdue to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_TIME: + for value in values: + try: + v = time.strptime(value,"%h:%m:%s") #12:15:00 + TimeM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + time or add value %sdue to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_DATE: + for value in values: + try: + v = time.strptime(value,"%d/%b/%Y") #28/01/1982 + DateM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + date or add value %sdue to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_DATETIME: + for value in values: + try: + v = time.strptime(value,"%d/%b/%Y-%h:%m:%s") #28/01/1982-03:00:00 + DateTimeM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + datetime or add value %sdue to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_RFC822NAME: + for value in values: + try: + v = None + r = re.compile('[a-zA-Z0-9+_\-\.]+@[0-9a-zA-Z][.-0-9a-zA-Z]*.[a-zA-Z]+') # email + if r.search(value): + v = value + else: + logger.error('add_assertion_to_profile: Unable to convert in \ + rfc822name or add value %sdue to %s' % (value, err)) + Rfc822NameM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + rfc822name or add value %sdue to %s' % (value, err)) + elif definition.attribute_type == ACS_XACML_DATATYPE_IPADDRESS: + for value in values: + try: + v = None + r = re.compile('(?:[\d]{1,3})\.(?:[\d]{1,3})\.(?:[\d]{1,3})\.(?:[\d]{1,3})') # x.x.x.x + if r.search(value): + v = value + else: + logger.error('add_assertion_to_profile: Unable to convert in \ + ipaddress or add value %sdue to %s' % (value, err)) + IpAddressM(data=data, value=v).save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to convert in \ + ipaddress or add value %sdue to %s' % (value, err)) + else: + logger.error('add_assertion_to_profile: Unknown definition data type \ + %s' % definition.attribute_type) + raise Exception() + + if expiration_date: + #expiration_date = time.strptime(expiration_date,"%d/%b/%Y-%h:%m:%s") #28/01/1982-03:00:00 + logger.debug('add_assertion_to_profile: expiration date %s found \ + - conversion' % expiration_date) + try: + expiration_date = iso8601_to_datetime(expiration_date) + except: + logger.error('add_assertion_to_profile: Conversion failed - \ + the date is not ISO8601') + expiration_date = None + + + ''' + Possibilities if the profile contains an assertion with the same + definition and the same source: + - append values if expiration_date are the same + - remove and the new assertion + - add a new assertion withtou checking + + The third choices is made. + Then the profile may contain multiple assertion for a same + definition and source. + + !!! It is enough that one of them satisfies the predicate. !!! + cf. functions check_predicate_comparison and + check_predicate_required + + If it is not the behaviour expected, reduce expiration date. + ''' + try: + a = AssertionData(source=source, attribute_data=data, + expiration_date=expiration_date) + a.save() + profile.assertions.add(a) + profile.save() + except Exception, err: + logger.error('add_assertion_to_profile: Unable to add the assertion \ + data due to %s' % err) + raise err + + except Exception, err: +# transaction.rollback() return None else: - return None - - try: - a = AssertionData(profile=profile, source=source, attribute_data=data) - a.save() +# transaction.commit() + logger.debug('add_assertion_to_profile: everything successfully \ + performed- %s added' % a) return a - except: - return None + + +def iso8601_to_datetime(date_string): + ''' + Convert a string formatted as an ISO8601 date into a time_t value. + This function ignores the sub-second resolution. + ''' + m = re.match(r'(\d+-\d+-\d+T\d+:\d+:\d+)(?:\.\d+)?Z?$', date_string) + logger.debug('add_assertion_to_profile: m %s' % str(m)) + if not m: + raise ValueError('Invalid ISO8601 date') + tm = time.strptime(m.group(1)+'Z', "%Y-%m-%dT%H:%M:%SZ") + logger.debug('add_assertion_to_profile: tm %s' % str(tm)) + return datetime.datetime.fromtimestamp(time.mktime(tm)) def arrange_missing_predicates(missing_predicates, profile): @@ -706,6 +838,62 @@ def get_all_sources(): return None +@transaction.commit_manually +def load_or_create_user_profile(user, no_cleanup=False): + ''' + If the user has a profile, remove assertions outdated and return + profile, else create a new one. + + If cleanup: expiration_date < now() remove assertion data from profile + + If no_cleanup: return profile if any without removing outdated + assertions + ''' + profile = None + try: + if user: + try: + logger.info('load_or_create_user_profile: search profile for %s' \ + % user) + profile = UserAttributeProfile.objects.get(user=user) + except ObjectDoesNotExist: + profile = UserAttributeProfile(user=user) + profile.save() + logger.info('load_or_create_user_profile: profile with id %s for \ + user %s created' % (str(profile.id), user)) + else: + if no_cleanup: + logger.info('load_or_create_user_profile: Existing user \ + profile %s for %s - No cleanup' % (profile, user)) + else: + for ad in profile.assertions.all(): + if not ad.expiration_date \ + or ad.expiration_date < datetime.datetime.now(): + logger.debug('load_or_create_user_profile: \ + Remove outdated assertion %s of profile %s' \ + % (ad, profile)) + ad.delete() + else: + logger.debug('load_or_create_user_profile: \ + %s kept in profile' \ + % ad) + elif not user: + profile = UserAttributeProfile() + profile.save() + logger.info('load_or_create_user_profile: profile with id %s for \ + an anonymous user created' % str(profile.id)) + except Exception, err: + transaction.rollback() + logger.error('load_or_create_user_profile: An error occured %s' \ + % str(err)) + return None + else: + transaction.commit() + logger.debug('load_or_create_user_profile: everything successfully \ + performed') + return profile + + def load_profile_by_dic(profile, dic): if not profile or not dic: logger.error('load_profile_by_dic: \ @@ -721,6 +909,11 @@ def load_profile_by_dic(profile, dic): for attr in dic[source]: logger.debug('load_profile_by_dic: attribute %s of %s with values %s' \ % (attr['name'], attr['namespace'], str([x for x in attr['values']]))) + expiration_date = None + if 'expiration_date' in attr: + logger.debug('load_profile_by_dic: expire at %s' \ + % attr['expiration_date']) + expiration_date = attr['expiration_date'] d = get_def_from_name_and_ns(attr['name'], attr['namespace']) if not d: logger.error('load_profile_by_dic: \ @@ -729,7 +922,14 @@ def load_profile_by_dic(profile, dic): else: logger.debug('load_profile_by_dic: \ definition %s found' % d) - add_assertion_to_profile(profile, s, d, attr['values']) + a = add_assertion_to_profile(profile, s, d, attr['values'], + expiration_date=expiration_date) + if not a: + logger.debug('load_profile_by_dic: \ + error adding assertion') + else: + logger.debug('load_profile_by_dic: \ + assertion %s added' % a) else: logger.critical('load_profile_by_dic: \ The source with name %s and attributes %s is unknown of the system'