From 30481c18e11346ead2bce845f5d67c3a97a28b61 Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 26 2016 22:47:10 +0000 Subject: Rewrite ipsilon.util.policy.Policy class documentation Rewrite docstring documentation of the Policy class to be easier to understand, more accurate, with examples. Also fleshes out inline comments to make the code a bit easier to follow. Fixes ticket #178 Signed-off-by: Howard Johnson --- diff --git a/ipsilon/util/policy.py b/ipsilon/util/policy.py index f1915cc..1d8adad 100644 --- a/ipsilon/util/policy.py +++ b/ipsilon/util/policy.py @@ -6,32 +6,130 @@ import sys class Policy(Log): + """ + A policy engine to filter and map (rename) attributes. + """ def __init__(self, mappings=None, allowed=None): - """ A Policy engine to filter attributes. - Mappings is a list of lists where the first value ia a list itself - and the second value is an attribute name or a list if the values - should go in a sub dictionary. - Note that mappings is a list and not a dictionary as this allows - to map the same original attribute to different resulting attributes - if wanted, by simply repeating the 'key list' with different values - or 'value lists'. + """ + Create a new policy engine instance with the specified mapping + and filter configuration. + + mappings is a list of mapping rules. + + Each mapping rule is a two-element list. The first element defines the + name of an attribute to map. The second element is the name to map the + attribute to in the output. + + For example, given this mapping, + + [ + ['name', 'username'] + ] + + and this input, + + { + 'name': 'bob', + 'groups': ['bob', 'allbobs', 'people'] + } + + the output will be + + { + 'username': 'bob' + } + + Either the input or output name may itself be a two-element list + instead of a string. If the input name is a list, the first element is + the name of a dict or list in the input, and the second element is the + name of a parameter in that iterable. If the output name is a list, + then the first element is the name of a dict or list to create in the + output, and the second element is the name of the value to add to it. + + Given this mapping, + + [ + [['gecos', 'roomno'], 'roomno'], + [['groups', 'people'], ['groups', 'peoplegroup']] + ] + + and this input, + + { + 'gecos': { + 'roomno': '12B' + }, + 'groups': ['bob', 'allbobs', 'people'] + } + + the output will be + + { + 'roomno': '12B', + 'groups': ['peoplegroup'] + } + + The value '*' can be used as a wildcard. If the input name is '*', + this causes all input attributes to be copied to the output. If the + output name is also '*', the attribute names are unchanged. '*' can be + combined with the two-element for to copy dicts and lists in the input. + + For example: + + [['gecos', '*'], ['gecos', '*']] which is functionally identical to + ['gecos', 'gecos'] + + ['*', ['allparams', '*']] which copies all input attributes into an + output parameter called 'allparams'. + + The default mapping is [['*', '*']] - i.e. all input attributes are + copied as-is to the output. - Example: [[['extras', 'shoes'], 'shoeNumber']] + As the mapping configuration is specified as a list of mappings, this + allows input attributes to be mapping to the output in multiple ways at + the same time. - A '*' can be used to allow any attribute. - The default mapping is [[['*'], '*']] - This copies all attributes without transformation. + allowed is a list of filter rules. - Allowed is a list of allowed attributes. - Normally mapping should be called before filtering, this means - allowed attributes should name the mapped attributes. - Allowed attributes can be multi-element lists + Each filter rule is either a simple string specifying an attribute + name, or a two-element list specifying a dict or list attribute name, + and a value in that iterable. - Example: ['fullname', ['groups', 'domain users']] + The filter list can be used as either a whitelist (filtering attributes + into the output) or as a blacklist (filtering attributes out of + output). - Allowed is '*' by default. + Filter rules can also use the '*' wildcard. A filter rule of ['*'] + matches all attributes. A filter rule of ['gecos', '*'] matches all + elements of a dict or list called 'gecos'. + + The default mapping is ['*'] - i.e. all attributes are matched. + + An example: + + Combining this filter config, + + [ + 'username', + ['groups', 'allbobs'], + ['groups', 'people'] + ] + + with this input, + + { + 'username': 'bob', + 'groups': ['bob', 'allbobs', 'people'] + } + + the output will be + + { + 'username': 'bob', + 'groups': ['allbobs', 'people'] + } """ self.mappings = None @@ -66,6 +164,19 @@ class Policy(Log): self.allowed = allowed def map_attributes(self, attributes, ignore_case=False): + """ + Map the specified dictionary of attributes using the mapping + configuration specified at policy engine creation time. + + Returns a tuple of two dicts, containing the resulting mapped and + unmapped attributes respectively. + + If ignore_case is true, then the mapping list is compared to attribute + names in a case-insensitive fashion. If the attribute list has one or + more attributes that differ only in case, then only one of the + attributes will be mapped (at random, due to Python's default dict + ordering). + """ if not isinstance(attributes, dict): raise ValueError("Attributes must be dictionary, not %s" % @@ -189,6 +300,20 @@ class Policy(Log): return mapped, not_mapped def filter_attributes(self, attributes, whitelist=True): + """ + Filter the specified dictionary of attributes using the filter + configuration specified at policy engine creation time. + + Returns a dict containing the resulting filter attributes. + + If whitelist is true, the filter specifies which attributes are allowed + in the output. If whitelist is false, the filter specifies which + attributes should be removed from the output. + """ + + if not isinstance(attributes, dict): + raise ValueError("Attributes must be dictionary, not %s" % + type(attributes)) filtered = dict() @@ -221,7 +346,8 @@ class Policy(Log): if whitelist: allowed = filtered else: - # filtered contains the blacklisted + # the filtered dict contains the attributes that are blacklisted, + # so take a copy of the original attributes, and remove them allowed = copy.deepcopy(attributes) for lvl1 in filtered: attr = filtered[lvl1]