This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
veridic/acs/abac/core.py

670 lines
26 KiB
Python

'''
VERIDIC - Towards a centralized access control system
Copyright (C) 2011 Mikael Ates
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import re
import logging
import datetime
import time
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.utils.translation import ugettext as _
from acs.models import Namespace, UserAlias
from acs.abac.models import *
from attribute_aggregator.xacml_constants import *
from attribute_aggregator.core import get_attribute_type_of_definition
from acs.core import get_alias_in_policy_from_namespace, \
stack_of_roles_from_user
logger = logging.getLogger('abac')
def extract_predicate_ids(expression):
if not expression:
return []
ids = []
res = re.search(r'\d+', expression)
while res and res.group(0):
ids.append(res.group(0))
p = r'(%s)' % res.group(0)
expression = re.sub(p, '', expression)
res = re.search(r'\d+', expression)
return ids
def check_predicate(predicate, profile):
logger.debug("check_predicate: predicate id %s" % predicate.id)
if isinstance(predicate, PredicateRequired):
logger.debug("check_predicate: PredicateRequired %s" % predicate)
return check_predicate_required(predicate, profile)
if isinstance(predicate, PredicateComparison):
logger.debug("check_predicate: PredicateComparison %s" % predicate)
return check_predicate_comparison(predicate, profile)
if isinstance(predicate, PredicateRole):
logger.debug("check_predicate: PredicateRole %s" % predicate)
return check_predicate_role(predicate, profile)
return False
'''
Every assertionData in the profile with a definition and a list of
sources are candidates for redicate checking.
This match assertions provided by multiple source or even multiple
from the same source.
!!! 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()
if isinstance(data1, AssertionDefinition):
'''
find in profile the data corresponding to this definition
'''
for source in data1.sources.all():
l_data1 += profile.get_data_of_definition_and_source(\
data1.get_definition(), source)
else:
l_data1 = [data1.get_attribute_data()]
#operand2
l_data2 = []
data2 = predicate.operand2.get_assertion_instance()
if isinstance(data2, AssertionDefinition):
'''
find in profile the data corresponding to this definition
'''
for source in data2.sources.all():
l_data2 += profile.get_data_of_definition_and_source(\
data2.get_definition(), source)
else:
l_data2 = [data2.get_attribute_data()]
for d1 in l_data1:
for d2 in l_data2:
if compare_two_data(predicate, d1, d2):
return True
return False
'''
A predicate contains an option indicating if an attribute must be single valued.
Since considered has an enforcement, a check includes the test that attribute has a single value if required,
else that a multivalue option not NO_MULTIVALUES must be set.
All:
NO_MULTIVALUES
Both operand are single valued attributes
Equality:
EQUAL_ONE_VALUE
At least one value of the values of OP1 is equal to one value of the values of OP2
EQUAL_OP1_SUBSET_OP2
The values of OP1 is is a subset of the values of OP2
EQUAL_EXACT_MATCH
Equal set of values
Diff strict:
DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT
All values of OP1 must be less than the highest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT
All values of OP1 must be greater than the highest value of OP2
DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT
All values of OP1 must be less than the smallest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT
All values of OP1 must be greater than the smallest value of OP2
DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT
At least one value of OP1 must be less than the highest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT
At least one value of OP1 must be greater than the highest value of OP2
DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT
At least one value of OP1 must be less than the smallest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT
At least one value of OP1 must be greater than the smallest value of OP2
Diff or equal:
Same as for strict and equality is treated as follows:
DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT_OE
All values of OP1 must be less than or equal to the highest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT_OE
All values of OP1 must be greater than or equal to the highest value of OP2
DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT_OE
All values of OP1 must be less than or equal to the smallest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT_OE
All values of OP1 must be greater than or equal to the smallest value of OP2
DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT_OE
At least one value of OP1 must be less than or equal to the highest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT_OE
At least one value of OP1 must be greater than or equal to the highest value of OP2
DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2
ACS_XACML_COMPARISON_INTEGER_LT_OE
At least one value of OP1 must be less than or equal to the smallest value of OP2
ACS_XACML_COMPARISON_INTEGER_GRT_OE
At least one value of OP1 must be greater than or equal to the smallest value of OP2
To deal with richer comparison and equality of multivalued attributes, a 'or' statement should be used
'''
def compare_two_data(predicate, data1, data2):
logger.debug("compare_two_data: check if definitions match")
if data1.get_definition() != data2.get_definition():
logger.debug("compare_two_data: \
Definition %s does not match with %s" \
% (data1.get_definition(), data2.get_definition()))
return False
logger.debug("compare_two_data: convert values...")
data1_values = data1.get_converted_values()
data2_values = data2.get_converted_values()
if not data1_values or not data2_values:
logger.debug("compare_two_data: \
Return False because no values was found for predicate %s" \
% predicate)
return False
if predicate.operand1_single_value and len(data1_values) > 1:
logger.debug("compare_two_data: \
Return False because a single value is required for operand one \
and multiple were found %s for predicate %s" \
% ([x for x in data1_values], predicate))
return False
if predicate.operand2_single_value and len(data2_values) > 1:
logger.debug("compare_two_data: \
Return False because a single value is required for operand two \
and multiple were found %s for predicate %s" \
% ([x for x in data2_values], predicate))
return False
if not(predicate.operand1_single_value \
and predicate.operand2_single_value) \
and ((predicate.comparison_type in XACML_COMPARISON_EQUALITY \
and not predicate.multivalues in \
('EQUAL_ONE_VALUE',
'EQUAL_OP1_SUBSET_OP2',
'EQUAL_EXACT_MATCH')) \
or (predicate.comparison_type in ACS_XACML_COMPARISON \
and not predicate.multivalues in \
('DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2',
'DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2',
'DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2',
'DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2'))):
logger.debug("compare_two_data: \
Return False because multivalued attributes are accepted and no \
suitable management option has been selected for predicate %s" \
% str(predicate))
return False
logger.debug("compare_two_data: \
Evaluation of predicate %s" % str(predicate))
if predicate.comparison_type in XACML_COMPARISON_EQUALITY:
return test_equality_of_values(data1_values, data2_values,
get_attribute_type_of_definition(data1.get_definition()),
predicate.comparison_type,
predicate.multivalues)
elif predicate.comparison_type in ACS_XACML_COMPARISON:
return test_diff_of_multivalues(data1_values, data2_values,
get_attribute_type_of_definition(data1.get_definition()),
predicate.comparison_type,
predicate.multivalues)
logger.debug("compare_two_data: \
Return False because of unknown comparison type \
for predicate %s" \
% predicate)
return False
def test_diff_of_multivalues(data1_values, data2_values, data_type,
comparison_type, test_type='NO_MULTIVALUES'):
if test_type == 'NO_MULTIVALUES':
logger.debug("test_diff_of_multivalues: \
No multivalues, test %s and %s" \
% (data1_values[0], data2_values[0]))
if test_diff_two_values(data1_values[0], data2_values[0],
comparison_type):
logger.debug("test_diff_of_multivalues: True")
return True
else:
logger.debug("test_diff_of_multivalues: False")
return False
# BOTTOM LIMIT with LT and UPPER LIMIT with GTR
# OP1a <= OP1b <= OP2a <= OP2b
# OP1b >= OP1a >= OP2b >= OP2a
# With ALL and LT, no value of OP1 must be superior to the bottom limit of OP2
# With ALL and GRT, no value of OP1 must be less than the upper limit of OP2
elif (test_type == 'DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_LT + \
ACS_XACML_COMPARISON_LT_OE) \
or (test_type == 'DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_GRT + \
ACS_XACML_COMPARISON_GRT_OE):
for v1 in data1_values:
for v2 in data2_values:
if not test_diff_two_values(v1, v2, comparison_type):
logger.debug("test_diff_of_multivalues: \
DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2 (LT) or \
DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2 (GRT) - \
predicate not satisfied")
return False
return True
# UPPER LIMIT with LT and BOTTOM LIMIT with GTR
# OP2a <= OP1a <= OP1b <= OP2b
# OP2b >= OP1b >= OP2a >= OP2a
# With ALL and LT, no value of OP1 must be superior to the upper limit of OP2
# With ALL and GRT, no value of OP1 must be less than the bottom limit of OP2
elif (test_type == 'DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_GRT + \
ACS_XACML_COMPARISON_GRT_OE) \
or (test_type == 'DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_LT + \
ACS_XACML_COMPARISON_LT_OE):
for v2 in data2_values:
found = True
for v1 in data1_values:
if not test_diff_two_values(v1, v2, comparison_type):
found = False
if found:
return True
logger.debug("test_diff_of_multivalues: \
DIFF_ALL_OP1_WITH_BOTTOM_LIMIT_OP2 (GRT) or \
DIFF_ALL_OP1_WITH_UPPER_LIMIT_OP2 (LT) - \
predicate not satisfied")
return False
# BOTTOM LIMIT with LT and UPPER LIMIT with GTR
# OP1a <= OP2a <= OP1b <= OP2b
# OP1b >= OP2b >= OP2a >= OP2a
# With ONE and LT, some values of OP1 may be superior to the bottom limit of OP2
# With ONE and GRT, some values of OP1 must be less than the upper limit of OP2
elif (test_type == 'DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_LT + \
ACS_XACML_COMPARISON_LT_OE) \
or (test_type == 'DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_GRT + \
ACS_XACML_COMPARISON_GRT_OE):
for v1 in data1_values:
found = True
for v2 in data2_values:
if not test_diff_two_values(v1, v2, comparison_type):
found = False
if found:
return True
logger.debug("test_diff_of_multivalues: \
DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2 (LT) or \
DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2 (GRT) - \
predicate not satisfied")
return False
# UPPER LIMIT with LT and BOTTOM LIMIT with GTR
# OP1a <= OP2a <= OP1b <= OP2b
# OP1b >= OP2b >= OP2a >= OP2a
# With ONE and LT, some values of OP1 may be superior to the upper limit of OP2
# With ONE and GRT, some values of OP1 must be less than the bottom limit of OP2
elif (test_type == 'DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_LT + \
ACS_XACML_COMPARISON_LT_OE) \
or (test_type == 'DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2' \
and comparison_type in ACS_XACML_COMPARISON_GRT + \
ACS_XACML_COMPARISON_GRT_OE):
for v1 in data1_values:
for v2 in data2_values:
if test_diff_two_values(v1, v2, comparison_type):
return True
logger.debug("test_diff_of_multivalues: \
DIFF_ONE_OP1_WITH_BOTTOM_LIMIT_OP2 (GRT) or \
DIFF_ONE_OP1_WITH_UPPER_LIMIT_OP2 (LT) - \
predicate not satisfied")
return False
logger.debug("test_diff_of_multivalues: \
Unknown multivalue option, \
predicate not satisfied")
return False
def test_diff_two_values(value1, value2, comparison_type):
if comparison_type in ACS_XACML_COMPARISON_GRT:
if value1 > value2:
return True
elif comparison_type in ACS_XACML_COMPARISON_GRT_OE:
if value1 >= value2:
return True
elif comparison_type in ACS_XACML_COMPARISON_LT:
if value1 < value2:
return True
elif comparison_type in ACS_XACML_COMPARISON_LT_OE:
if value1 <= value2:
return True
return False
def test_equality_of_values(data1_values, data2_values, data_type,
comparison_type, test_type='NO_MULTIVALUES'):
if test_type == 'NO_MULTIVALUES':
if data_type == ACS_XACML_DATATYPE_STRING \
and comparison_type == \
ACS_XACML_COMPARISON_EQUALITY_STRING_IGN_CASE:
if data1_values[0].lower() == data2_values[0].lower():
return True
elif data1_values[0] == data2_values[0]:
return True
elif test_type == 'EQUAL_ONE_VALUE':
for v1 in data1_values:
for v2 in data2_values:
if data_type == ACS_XACML_DATATYPE_STRING \
and comparison_type == \
ACS_XACML_COMPARISON_EQUALITY_STRING_IGN_CASE:
if v1.lower() == v2.lower():
return True
elif v1 == v2:
return True
elif test_type == 'EQUAL_OP1_SUBSET_OP2':
for v1 in data1_values:
found = False
for v2 in data2_values:
if data_type == ACS_XACML_DATATYPE_STRING \
and comparison_type == \
ACS_XACML_COMPARISON_EQUALITY_STRING_IGN_CASE:
if v1.lower() == v2.lower():
found = True
elif v1 == v2:
found = True
if not found:
return False
return True
elif test_type == 'EQUAL_EXACT_MATCH':
if len(data1_values) != len(data2_values):
return False
for v1 in data1_values:
found = False
for v2 in data2_values:
if data_type == ACS_XACML_DATATYPE_STRING \
and comparison_type == \
ACS_XACML_COMPARISON_EQUALITY_STRING_IGN_CASE:
if v1.lower() == v2.lower():
found = True
elif v1 == v2:
found = True
if not found:
return False
return True
return False
def check_predicate_required(predicate, profile):
'''
Look in profile all attributedata with that attribute type
Then check the source and return True on the first acceptable source
A single value attribute is checked per sources
Declaring multiple source is a OR between sources.
So the single-valued is checked per source.
If it is expected to check a unique value with multiple sources
it is required to use multiple required predicates and NOT operators
'''
logger.debug("check_predicate_required: check %s" % predicate)
if not predicate.single_value:
logger.debug("check_predicate_required: single value not required")
for source in predicate.get_sources():
d = profile.get_data_of_definition_and_source(\
predicate.get_definition(), source)
if d:
logger.debug("check_predicate_required: satisfied by %s" \
% str([a.__unicode__() for a in d]))
return True
else:
logger.debug("check_predicate_required: single value required")
found = False
match = [profile.get_data_of_definition_and_source(\
predicate.get_definition(), source) \
for source in predicate.get_sources()]
if len(match) == 1:
logger.debug("check_predicate_required: OK only one attribute \
with a single value in profile")
return True
else:
logger.debug("check_predicate_required: Failed - no value found \
or multiple")
return False
def check_predicate_role(predicate, profile):
'''
Check that the user has the role or a senior role.
The user must be in the profile.
'''
if not predicate or not profile \
or not isinstance(predicate, PredicateRole) \
or not predicate.role \
or not profile.user:
return False
alias = get_alias_in_policy_from_namespace(profile.user,
predicate.role.namespace)
if not alias:
logger.debug("check_predicate_role: no alias found for user: %s \
in namespace %s" % (profile.user, predicate.role.namespace))
return False
logger.debug("check_predicate_role: check if user %s has role %s" \
% (alias, predicate.role))
stack = stack_of_roles_from_user(alias)
logger.debug("check_predicate_role: roles of the user: %s" \
% stack)
if predicate.role in stack:
logger.debug("check_predicate_role: success")
return True
logger.debug("check_predicate_role: failure")
return False
def check_predicates(rule, profile):
'''
Parse rule and list predicates
Check presence, source, values...
'''
if not rule or not profile or not rule.expression:
return {}
predicates = []
dic = {}
for p in Predicate.objects.filter(rule=rule):
p_t = p.get_predicate_instance()
predicates.append(p_t)
dic[str(p.id)] = check_predicate(p_t, profile)
logger.debug("check_predicates: found %s" \
% [str(x) for x in predicates])
logger.debug("check_predicates: computed %s" % dic)
return dic
@transaction.commit_manually
def remove_predicate(predicate):
try:
if not predicate:
raise Exception(_('No predicate provided'))
else:
logger.debug(\
'remove_predicate: Begin deletion of predicate %s with id %s' \
% (predicate, predicate.id))
'''
Objects to delete for predicate required:
- AssertionDefinition
Objects to delete for predicate role:
- None
Objects to delete for predicate comparisons:
- AssertionDefinition
- AssertionData
'''
instance = predicate.get_predicate_instance()
if isinstance(instance, PredicateRole):
pass
elif isinstance(instance, PredicateRequired):
logger.debug('remove_predicate: predicate required found')
instance.assertion_definition.delete()
elif isinstance(instance, PredicateComparison):
logger.debug('remove_predicate: predicate comparison found')
assertion = instance.operand1.get_assertion_instance()
if isinstance(assertion, AssertionDefinition):
logger.debug('remove_predicate: \
operand one is an assertion definition')
logger.debug('remove_predicate: \
remove assertion definition with id %s' %assertion.id)
assertion.delete()
elif isinstance(assertion, AssertionData):
logger.debug('remove_predicate: \
operand one is an assertion data')
logger.debug('remove_predicate: \
remove assertion data with id %s' % assertion.id)
assertion.delete()
else:
raise Exception(_('Unknown operand one'))
assertion = instance.operand2.get_assertion_instance()
if isinstance(assertion, AssertionDefinition):
logger.debug('remove_predicate: \
operand two is an assertion definition')
logger.debug('remove_predicate: \
remove assertion definition with id %s' %assertion.id)
assertion.delete()
elif isinstance(assertion, AssertionData):
logger.debug('remove_predicate: \
operand two is an assertion data')
logger.debug('remove_predicate: \
remove assertion data with id %s' % assertion.id)
assertion.delete()
else:
raise Exception(_('Unknown operand two'))
else:
raise Exception(_('Unknown predicate type'))
logger.debug('remove_predicate: deletion of the predicate')
predicate.delete()
except Exception, err:
transaction.rollback()
logger.critical('remove_predicate: error deleting predicate due to %s'
% err)
raise err
else:
transaction.commit()
logger.debug('remove_predicate: predicate deleted')
@transaction.commit_manually
def remove_rule(rule):
try:
if not rule:
raise Exception(_('No rule provided'))
else:
logger.debug(\
'remove_rule: Begin deletion of rule %s with id %s' \
% (rule, rule.id))
for p in Predicate.objects.filter(rule=rule):
logger.debug('remove_rule: found predicate %s' % p)
remove_predicate(p)
logger.debug('remove_rule: deletion of the rule')
rule.delete()
except Exception, err:
transaction.rollback()
logger.critical('remove_rule: error deleting rule due to %s'
% err)
raise err
else:
transaction.commit()
logger.debug('remove_rule: rule deleted')
def make_new_rule_from_missing_predicates(missing_predicates):
if not missing_predicates:
return None
s = ''
for it in missing_predicates:
for r in missing_predicates[it]:
'Or'
s += '('
s_tmp = ''
for p_id, now, exp in r:
'And'
'If True to False put negative sign -'
if now:
s_tmp += '(-'
s_tmp += p_id
if now:
s_tmp += ')'
s_tmp += '&'
s += s_tmp[:-1]
s += ')'
if r:
s += '|'
if s:
return s[:-1]
else:
return None
def arrange_missing_predicates(missing_predicates, profile):
'''
Here we have to retreat the priority of possibilities to satisfy
the rule.
missing_predicates present mutliples combinaisons possible to satisfy
the rule.
Output: we want to produce a rule with all the possibilities sorted by
the easiest to the hardest: rule = easiest_rule | ... | hardest_rule
This sort is based on the number of predicate not statisfied but also
on the following rules:
- First predicates required
- Comparison if predicate is False because an operande is missing
- Comparisons with only def
- comparison with data not satisfied
In practice, we prefer to ask the user to present its surname and its
firstname (2 predicate False up to now) rather to ask to present its
age if it has already been presented but did not satisfied a
comparison (only one predicate False but harder to satisfy)
TODO
'''
pass