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'