def convert_provider_data(provider_data, mapping, key_filter=None): """Converts data coming from the provider to be used by the application The result will have all the keys listed in `keys` with values coming either from `data` (using the key mapping defined in `mapping`) or ``None`` in case the key is not present. If `key_filter` is ``None``, all keys from `provider_data` will be used unless they are mapped to a different key in `mapping`. :param provider_data: dict -- Data coming from the provider. :param mapping: dict -- Mapping between keys used to define the data in the provider and those used by the application. All application keys will be present in the return value, defaulting to ``None``. :param key_filter: list -- Keys to be exclusively considered. If ``None``, all items will be returned. Keys not present in the mapped data, will have a value of ``None``. :return: dict -- containing the values of `app_data` mapped to the keys of the application as defined in the `mapping` and filtered out by `key_filter`. """ provider_keys = set(mapping.values()) result = {key: value for key, value in iteritems(provider_data) if key not in provider_keys} result.update((app_key, provider_data.get(provider_key)) for app_key, provider_key in iteritems(mapping)) if key_filter is not None: key_filter = set(key_filter) result = {key: value for key, value in iteritems(result) if key in key_filter} result.update({key: None for key in key_filter - set(result)}) return result
def search_identities(self, criteria, exact=False): compare = operator.eq if exact else operator.contains for identifier, user in iteritems(self.settings['identities']): for key, values in iteritems(criteria): if not any(compare(user[key], v) for v in values): break else: yield IdentityInfo(self, identifier, **user)
def search_identities(self, criteria, exact=False): for identifier, user in iteritems(self.settings['identities']): for key, values in iteritems(criteria): # same logic as multidict user_value = user.get(key) user_values = set(user_value) if isinstance(user_value, (tuple, list)) else {user_value} if not any(user_values): break elif exact and not user_values & set(values): break elif not exact and not any(sv in uv for sv, uv in itertools.product(values, user_values)): break else: yield IdentityInfo(self, identifier, **user)
def convert_app_data(app_data, mapping, key_filter=None): """Converts data coming from the application to be used by the provider. :param app_data: dict -- Data coming from the application. :param mapping: dict -- Mapping between keys used to define the data in the application and those used by the provider. :param key_filter: list -- Keys to be exclusively considered. If ``None``, all items will be returned. :return: dict -- containing the values of `app_data` mapped to the keys of the provider as defined in the `mapping` and filtered out by `key_filter`. """ if key_filter: key_filter = set(key_filter) app_data = {k: v for k, v in iteritems(app_data) if k in key_filter} return {mapping.get(key, key): value for key, value in iteritems(app_data)}
def search_identities(self, providers=None, exact=False, **criteria): """Searches user identities matching certain criteria :param providers: A list of providers to search in. If not specified, all providers are searched. :param exact: If criteria need to match exactly, i.e. no substring matches are performed. :param criteria: The criteria to search for. A criterion can have a list, tuple or set as value if there are many values for the same criterion. :return: An iterable of matching user identities. """ for k, v in iteritems(criteria): if isinstance(v, multi_value_types): criteria[k] = v = set(v) elif not isinstance(v, set): criteria[k] = v = {v} if any(not x for x in v): raise ValueError('Empty search criterion: ' + k) for provider in itervalues(self.identity_providers): if providers is not None and provider.name not in providers: continue if not provider.supports_search: continue for identity_info in provider.search_identities(provider.map_search_criteria(criteria), exact=exact): yield identity_info
def search_identities(self, providers=None, exact=False, **criteria): """Searches user identities matching certain criteria :param providers: A list of providers to search in. If not specified, all providers are searched. :param exact: If criteria need to match exactly, i.e. no substring matches are performed. :param criteria: The criteria to search for. A criterion can have a list, tuple or set as value if there are many values for the same criterion. :return: An iterable of matching user identities. """ for k, v in iteritems(criteria): if isinstance(v, multi_value_types): criteria[k] = v = set(v) elif not isinstance(v, set): criteria[k] = v = {v} if any(not x for x in v): raise ValueError('Empty search criterion: ' + k) for provider in itervalues(self.identity_providers): if providers is not None and provider.name not in providers: continue if not provider.supports_search: continue for identity_info in provider.search_identities( provider.map_search_criteria(criteria), exact=exact): yield identity_info
def search_identities(self, criteria, exact=False): for identifier, user in iteritems(self.settings['identities']): for key, values in iteritems(criteria): # same logic as multidict user_value = user.get(key) user_values = set(user_value) if isinstance( user_value, (tuple, list)) else {user_value} if not any(user_values): break elif exact and not user_values & set(values): break elif not exact and not any( sv in uv for sv, uv in itertools.product(values, user_values)): break else: yield IdentityInfo(self, identifier, **user)
def ldap_connect(settings, use_cache=True): """Establishes an LDAP connection. Establishes a connection to the LDAP server from the `uri` in the ``settings``. To establish a connection, the settings must be specified: - ``uri``: valid URI which points to a LDAP server, - ``bind_dn``: `dn` used to initially bind every LDAP connection - ``bind_password``" password used for the initial bind - ``tls``: ``True`` if the connection should use TLS encryption - ``starttls``: ``True`` to negotiate TLS with the server `Note`: ``starttls`` is ignored if the URI uses LDAPS and ``tls`` is set to ``True``. This function re-uses an existing LDAP connection if there is one available in the application context, unless caching is disabled. :param settings: dict -- The settings for a LDAP provider. :param use_cache: bool -- If the connection should be cached. :return: The ldap connection. """ if use_cache: cache = _get_ldap_cache() cache_key = frozenset( (k, hash(v)) for k, v in iteritems(settings) if k in conn_keys) conn = cache.get(cache_key) if conn is not None: return conn uri_info = urlparse(settings['uri']) use_ldaps = uri_info.scheme == 'ldaps' credentials = (settings['bind_dn'], settings['bind_password']) ldap_connection = ReconnectLDAPObject(settings['uri']) ldap_connection.protocol_version = ldap.VERSION3 ldap_connection.set_option(ldap.OPT_REFERRALS, 0) ldap_connection.set_option( ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND if use_ldaps else ldap.OPT_X_TLS_NEVER) ldap_connection.set_option( ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND if settings['verify_cert'] else ldap.OPT_X_TLS_ALLOW) # force the creation of a new TLS context. This must be the last TLS option. # see: http://stackoverflow.com/a/27713355/298479 ldap_connection.set_option(ldap.OPT_X_TLS_NEWCTX, 0) if use_ldaps and settings['starttls']: warn( "Unable to start TLS, LDAP connection already secured over SSL (LDAPS)" ) elif settings['starttls']: ldap_connection.start_tls_s() # TODO: allow anonymous bind ldap_connection.simple_bind_s(*credentials) if use_cache: cache[cache_key] = ldap_connection return ldap_connection
def test_validate_provider_map(valid, auth_providers, identity_providers, provider_map): state = _MultipassState(None, None) state.auth_providers = {x: {} for x in auth_providers} state.identity_providers = {x: {} for x in identity_providers} state.provider_map = {a: [{'identity_provider': u}] for a, u in iteritems(provider_map)} if valid: validate_provider_map(state) else: pytest.raises(ValueError, validate_provider_map, state)
def _shibboleth_callback(self): attributes = { k: v for k, v in iteritems(request.environ) if k.startswith(self.settings['attrs_prefix']) } if not attributes: raise AuthenticationFailed("No valid data received") return self.multipass.handle_auth_success(AuthInfo(self, **attributes))
def get_canonical_provider_map(provider_map): """Converts the configured provider map to a canonical form""" canonical = {} for auth_provider_name, identity_providers in iteritems(provider_map): if not isinstance(identity_providers, (list, tuple, set)): identity_providers = [identity_providers] identity_providers = tuple({'identity_provider': p} if isinstance(p, string_types) else p for p in identity_providers) canonical[auth_provider_name] = identity_providers return canonical
def build_search_filter(criteria, type_filter, mapping=None, exact=False): """Builds a valid LDAP search filter for retrieving entries. :param criteria: dict -- Criteria to be ANDed together to build the filter, if a criterion has many values they will be ORed together. :param mapping: dict -- Mapping from criteria to LDAP attributes :param exact: bool -- Match attributes values exactly if ``True``, othewise perform substring matching. :return: str -- Valid LDAP search filter. """ assertions = convert_app_data(criteria, mapping or {}) assert_templates = [_build_assert_template(value, exact) for _, value in iteritems(assertions)] assertions = [(k, v) for k, values in iteritems(assertions) if k and values for v in values] if not assertions: return None filter_template = '(&{}{})'.format("".join(assert_templates), type_filter) return filter_format(filter_template, (item for assertion in assertions for item in assertion))
def ldap_connect(settings, use_cache=True): """Establishes an LDAP connection. Establishes a connection to the LDAP server from the `uri` in the ``settings``. To establish a connection, the settings must be specified: - ``uri``: valid URI which points to a LDAP server, - ``bind_dn``: `dn` used to initially bind every LDAP connection - ``bind_password``" password used for the initial bind - ``tls``: ``True`` if the connection should use TLS encryption - ``starttls``: ``True`` to negotiate TLS with the server `Note`: ``starttls`` is ignored if the URI uses LDAPS and ``tls`` is set to ``True``. This function re-uses an existing LDAP connection if there is one available in the application context, unless caching is disabled. :param settings: dict -- The settings for a LDAP provider. :param use_cache: bool -- If the connection should be cached. :return: The ldap connection. """ if use_cache: cache = _get_ldap_cache() cache_key = frozenset((k, hash(v)) for k, v in iteritems(settings) if k in conn_keys) conn = cache.get(cache_key) if conn is not None: return conn uri_info = urlparse(settings['uri']) use_ldaps = uri_info.scheme == 'ldaps' credentials = (settings['bind_dn'], settings['bind_password']) ldap_connection = ReconnectLDAPObject(settings['uri']) ldap_connection.protocol_version = ldap.VERSION3 ldap_connection.set_option(ldap.OPT_REFERRALS, 0) ldap_connection.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND if use_ldaps else ldap.OPT_X_TLS_NEVER) if settings['cert_file']: ldap_connection.set_option(ldap.OPT_X_TLS_CACERTFILE, settings['cert_file']) ldap_connection.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND if settings['verify_cert'] else ldap.OPT_X_TLS_ALLOW) # force the creation of a new TLS context. This must be the last TLS option. # see: http://stackoverflow.com/a/27713355/298479 ldap_connection.set_option(ldap.OPT_X_TLS_NEWCTX, 0) if use_ldaps and settings['starttls']: warn("Unable to start TLS, LDAP connection already secured over SSL (LDAPS)") elif settings['starttls']: ldap_connection.start_tls_s() # TODO: allow anonymous bind ldap_connection.simple_bind_s(*credentials) if use_cache: cache[cache_key] = ldap_connection return ldap_connection
def get_canonical_provider_map(provider_map): """Converts the configured provider map to a canonical form""" canonical = {} for auth_provider_name, identity_providers in iteritems(provider_map): if not isinstance(identity_providers, (list, tuple, set)): identity_providers = [identity_providers] identity_providers = tuple( {'identity_provider': p} if isinstance(p, string_types) else p for p in identity_providers) canonical[auth_provider_name] = identity_providers return canonical
def to_unicode(data): if isinstance(data, bytes): return data.decode('utf-8', 'replace') elif isinstance(data, dict): return {to_unicode(k): to_unicode(v) for k, v in iteritems(data)} elif isinstance(data, list): return [to_unicode(x) for x in data] elif isinstance(data, set): return {to_unicode(x) for x in data} elif isinstance(data, tuple): return tuple(to_unicode(x) for x in data) else: return data
def to_bytes_recursive(obj): if isinstance(obj, dict): return dict((bytes(k), to_bytes_recursive(v)) for k, v in iteritems(obj)) elif isinstance(obj, list): return map(to_bytes_recursive, obj) elif isinstance(obj, set): return {to_bytes_recursive(x) for x in obj} elif isinstance(obj, tuple): return tuple(to_bytes_recursive(x) for x in obj) elif isinstance(obj, unicode): return bytes(obj) else: return obj
def search_identities_ex(self, providers=None, exact=False, limit=None, criteria=None): """Search user identities matching search criteria. This is very similar to :meth:`search_identities`, but instead of just yielding identities, it allows specifying a limit and only returns up to that number of identities *per provider*. It also returns the total number of found identities so the application can decide to inform the user that their search criteria may be too broad. :return: A tuple containing ``(identities, total_count)``. """ for k, v in iteritems(criteria): if isinstance(v, multi_value_types): criteria[k] = v = set(v) elif not isinstance(v, set): criteria[k] = v = {v} if any(not x for x in v): raise ValueError('Empty search criterion: ' + k) found_identities = [] total = 0 for provider in itervalues(self.identity_providers): if providers is not None and provider.name not in providers: continue if not provider.supports_search: continue if provider.supports_search_ex: result, subtotal = provider.search_identities_ex( provider.map_search_criteria(criteria), exact=exact, limit=limit) found_identities += result total += subtotal else: result_iter = provider.search_identities( provider.map_search_criteria(criteria), exact=exact) if limit is not None: result = list(itertools.islice(result_iter, limit)) found_identities += result total += len(result) + sum(1 for _ in result_iter) else: result = list(result_iter) found_identities += result total += len(result) return found_identities, total
def set_defaults(self): self.ldap_settings.setdefault('timeout', 30) self.ldap_settings.setdefault('verify_cert', True) self.ldap_settings.setdefault('cert_file', certifi.where() if certifi else None) self.ldap_settings.setdefault('starttls', False) self.ldap_settings.setdefault('page_size', 1000) self.ldap_settings.setdefault('uid', 'uid') self.ldap_settings.setdefault('user_filter', '(objectClass=person)') if not self.ldap_settings['cert_file'] and self.ldap_settings['verify_cert']: warn("You should install certifi or provide a certificate file in order to verify the LDAP certificate.") # Convert LDAP settings to bytes since python-ldap chokes on unicode strings for key, value in iteritems(self.ldap_settings): if isinstance(value, unicode): self.ldap_settings[key] = bytes(value)
def to_bytes_recursive(obj): if isinstance(obj, dict): return dict( (bytes(k), to_bytes_recursive(v)) for k, v in iteritems(obj)) elif isinstance(obj, list): return map(to_bytes_recursive, obj) elif isinstance(obj, set): return {to_bytes_recursive(x) for x in obj} elif isinstance(obj, tuple): return tuple(to_bytes_recursive(x) for x in obj) elif isinstance(obj, unicode): return bytes(obj) else: return obj
def convert_provider_data(provider_data, mapping, key_filter=None): """Converts data coming from the provider to be used by the application The result will have all the keys listed in `keys` with values coming either from `data` (using the key mapping defined in `mapping`) or ``None`` in case the key is not present. If `key_filter` is ``None``, all keys from `provider_data` will be used unless they are mapped to a different key in `mapping`. :param provider_data: dict -- Data coming from the provider. :param mapping: dict -- Mapping between keys used to define the data in the provider and those used by the application. All application keys will be present in the return value, defaulting to ``None``. :param key_filter: list -- Keys to be exclusively considered. If ``None``, all items will be returned. Keys not present in the mapped data, will have a value of ``None``. :return: dict -- containing the values of `app_data` mapped to the keys of the application as defined in the `mapping` and filtered out by `key_filter`. """ provider_keys = set(mapping.values()) result = { key: value for key, value in iteritems(provider_data) if key not in provider_keys } result.update((app_key, provider_data.get(provider_key)) for app_key, provider_key in iteritems(mapping)) if key_filter is not None: key_filter = set(key_filter) result = { key: value for key, value in iteritems(result) if key in key_filter } result.update({key: None for key in key_filter - set(result)}) return result
def build_search_filter(criteria, type_filter, mapping=None, exact=False): """Builds a valid LDAP search filter for retrieving entries. :param criteria: dict -- Criteria to be ANDed together to build the filter, if a criterion has many values they will be ORed together. :param mapping: dict -- Mapping from criteria to LDAP attributes :param exact: bool -- Match attributes values exactly if ``True``, othewise perform substring matching. :return: str -- Valid LDAP search filter. """ assertions = convert_app_data(criteria, mapping or {}) assert_templates = [ _build_assert_template(value, exact) for _, value in iteritems(assertions) ] assertions = [(k, v) for k, values in iteritems(assertions) if k and values for v in values] if not assertions: return None filter_template = '(&{}{})'.format("".join(assert_templates), type_filter) return filter_format(filter_template, (item for assertion in assertions for item in assertion))
def test_validate_provider_map(valid, auth_providers, identity_providers, provider_map): state = _MultipassState(None, None) state.auth_providers = {x: {} for x in auth_providers} state.identity_providers = {x: {} for x in identity_providers} state.provider_map = { a: [{ 'identity_provider': u }] for a, u in iteritems(provider_map) } if valid: validate_provider_map(state) else: pytest.raises(ValueError, validate_provider_map, state)
def set_defaults(self): self.ldap_settings.setdefault('timeout', 30) self.ldap_settings.setdefault('verify_cert', True) self.ldap_settings.setdefault('cert_file', certifi.where() if certifi else None) self.ldap_settings.setdefault('starttls', False) self.ldap_settings.setdefault('page_size', 1000) self.ldap_settings.setdefault('uid', 'uid') self.ldap_settings.setdefault('user_filter', '(objectClass=person)') if not self.ldap_settings['cert_file'] and self.ldap_settings[ 'verify_cert']: warn( "You should install certifi or provide a certificate file in order to verify the LDAP certificate." ) # Convert LDAP settings to bytes since python-ldap chokes on unicode strings for key, value in iteritems(self.ldap_settings): if isinstance(value, unicode): self.ldap_settings[key] = bytes(value)
def _create_providers(self, key, base): """Instantiates all providers :param key: The key to insert into the config option name ``MULTIPASS_*_PROVIDERS`` :param base: The base class of the provider type. """ registry = self.provider_registry[AuthProvider if key == 'AUTH' else IdentityProvider] providers = {} provider_classes = set() for name, settings in iteritems(current_app.config['MULTIPASS_{}_PROVIDERS'.format(key)]): settings = settings.copy() cls = resolve_provider_type(base, settings.pop('type'), registry) if not cls.multi_instance and cls in provider_classes: raise RuntimeError('Provider does not support multiple instances: ' + cls.__name__) providers[name] = cls(self, name, settings) provider_classes.add(cls) return providers
def _shibboleth_callback(self): mapping = _lower_keys( iteritems( request.headers if self.from_headers else request.environ)) # get all attrs in the 'attrs' list, if empty use 'attrs_prefix' if self.attrs is None: attributes = { k: _to_unicode(v) for k, v in mapping if k.startswith(self.attrs_prefix) } else: attributes = { k: _to_unicode(v) for k, v in mapping if k in self.attrs } if not attributes: raise AuthenticationFailed("No valid data received", provider=self) return self.multipass.handle_auth_success(AuthInfo(self, **attributes))
def __new__(mcs, name, bases, dct): cls = type.__new__(mcs, name, bases, dct) base = next((x for x in reversed(getmro(cls)) if type(x) is mcs and x is not cls), None) if base is None: return cls for attr, methods in iteritems(base.__support_attrs__): if isinstance(methods, string_types): methods = methods, if isinstance(attr, tuple): supported = attr[0](cls) message = attr[1] else: supported = getattr(cls, attr, getattr(base, attr)) message = '{} is True'.format(attr) for method in methods: is_overridden = (getattr(base, method) != getattr(cls, method)) if not supported and is_overridden: raise TypeError('{} cannot override {} unless {}'.format(name, method, message)) elif supported and not is_overridden: raise TypeError('{} must override {} if {}'.format(name, method, message)) return cls
def __new__(mcs, name, bases, dct): cls = type.__new__(mcs, name, bases, dct) base = next((x for x in reversed(getmro(cls)) if type(x) is mcs and x is not cls), None) if base is None: return cls for attr, methods in iteritems(base.__support_attrs__): if isinstance(methods, string_types): methods = methods, if isinstance(attr, tuple): supported = attr[0](cls) message = attr[1] else: supported = getattr(cls, attr, getattr(base, attr)) message = '{} is True'.format(attr) for method in methods: is_overridden = (getattr(base, method) != getattr(cls, method)) if not supported and is_overridden: raise TypeError('{} cannot override {} unless {}'.format( name, method, message)) elif supported and not is_overridden: raise TypeError('{} must override {} if {}'.format( name, method, message)) return cls
def to_unicode(data): return {text_type(k): [x.decode('utf-8', 'replace') for x in v] for k, v in iteritems(data)}
def to_unicode(data): return { text_type(k): [x.decode('utf-8', 'replace') for x in v] for k, v in iteritems(data) }
def _shibboleth_callback(self): attributes = {k: v for k, v in iteritems(request.environ) if k.startswith(self.settings['attrs_prefix'])} if not attributes: raise AuthenticationFailed("No valid data received") return self.multipass.handle_auth_success(AuthInfo(self, **attributes))