ldap_sync

This package provides a standalone LDAP syncer. For more information on how to execute it, run python -m ldap_sync --help.

The process is separated into the following steps:

  1. Fetch the users/groups/properties we want to sync from the database (ldap_sync.sources.db)

  2. Fetch the current users/groups/properties from the ldap (ldap_sync.sources.ldap)

  3. Create a diff (ldap_sync.diff_records)

  4. Execute the actions (ldap_sync.execution)

ldap_sync.__main__

sync_production() None[source]
sync_fake() None[source]
fetch_and_sync(db_session: Session, connection: Connection, base_dn: DN, required_property: str | None = None) None[source]
try_setup_sentry() None[source]
trigger_sentry() NoReturn[source]
add_stdout_logging(logger: Logger, level: int = 20) None[source]
main() int[source]

ldap_sync.concepts.action

Actions (Add/Delete/Modify/Nothing)

class Action(record_dn: ~ldap_sync.concepts.types.DN, *, logger: ~logging.Logger = <factory>)[source]

Base class for the different actions the exporter can execute on an individual entity.

An action in the sense of the LDAP export is something which

  • refers to a record (i.e. something with a DN)

  • can be executed (provided an LDAP connection).

record_dn: DN
logger: Logger
class AddAction(record: Record)[source]

Add an LDAP record

nonempty_attrs: dict[str, Collection[str] | Collection[bytes] | Collection[int]]
class ModifyAction(record_dn: ~ldap_sync.concepts.types.DN, modifications: dict[str, ~typing.Collection[str] | ~typing.Collection[bytes] | ~typing.Collection[int]], *, logger: ~logging.Logger = <factory>)[source]

Modify an LDAP record by changing its attributes.

modifications: dict[str, Collection[str] | Collection[bytes] | Collection[int]]

a dict with entries of the form 'attribute_name': new_value, where the value is a list if the corresponding attribute is not single-valued.

class DeleteAction(record_dn: ~ldap_sync.concepts.types.DN, *, logger: ~logging.Logger = <factory>)[source]

Delete an LDAP record.

class IdleAction(record_dn: ~ldap_sync.concepts.types.DN, *, logger: ~logging.Logger = <factory>)[source]

Do nothing.

ldap_sync.concepts.record

escape_and_normalize_attrs(attrs: dict[str, str | bytes | int | Collection[str] | Collection[bytes] | Collection[int] | None]) dict[str, Collection[str] | Collection[bytes] | Collection[int]][source]
class Record(dn: DN, attrs: dict[str, str | bytes | int | Collection[str] | Collection[bytes] | Collection[int] | None])[source]

Create a new record with a dn and certain attributes.

A record represents an entry which is to be synced to the LDAP, and consists of a dn and relevant attributes. Constructors are provided for SQLAlchemy ORM objects as well as entries of an ldap search response.

Parameters
  • dn – The DN of the record

  • attrs – The attributes of the record. Every value will be canonicalized to a list to allow for a senseful comparison between two records, as well as escaped according to RFC 04515. Additionally, the keys are fixed to what’s given by get_synced_attributes().

dn: DN
attrs: dict[str, Collection[str] | Collection[bytes] | Collection[int]]
SYNCED_ATTRIBUTES: ClassVar[AbstractSet[str]]
class UserRecord(dn: DN, attrs: dict[str, str | bytes | int | Collection[str] | Collection[bytes] | Collection[int] | None])[source]

Create a new user record with a dn and certain attributes.

SYNCED_ATTRIBUTES: ClassVar[AbstractSet[str]] = frozenset({'cn', 'gecos', 'gidNumber', 'homeDirectory', 'loginShell', 'mail', 'objectClass', 'pwdAccountLockedTime', 'shadowExpire', 'sn', 'uid', 'uidNumber', 'userPassword'})
LDAP_OBJECTCLASSES = ['top', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
LDAP_LOGIN_ENABLED_PROPERTY = 'ldap_login_enabled'
PWD_POLICY_BLOCKED = 'login_disabled'
classmethod get_synced_attributes() AbstractSet[str][source]
class GroupRecord(dn: DN, attrs: dict[str, str | bytes | int | Collection[str] | Collection[bytes] | Collection[int] | None])[source]

Create a new groupOfMembers record with a dn and certain attributes. Used to represent groups and properties.

SYNCED_ATTRIBUTES: ClassVar[AbstractSet[str]] = frozenset({'cn', 'member', 'objectClass'})
LDAP_OBJECTCLASSES = ['groupOfMembers']
class RecordState(current: Record | None = None, desired: Record | None = None)[source]

A Class representing the state (current, desired) of a record.

This class is essentially a duple consisting of a current and desired record to represent the difference.

current: Record | None = None
desired: Record | None = None

ldap_sync.concepts.types

Type aliases, NewTypes, etc.

class DN

An LDAP Distinguished Name

alias of str

class LdapRecord[source]
dn: DN
attributes: dict[str, str | bytes | int | Collection[str] | Collection[bytes] | Collection[int] | None]
raw_attributes: dict[str, list[bytes]]

ldap_sync.sources.db

This module is responsible for fetching the list of desired records from the DB. Most prominently:

_fetch_db_groups(session: Session) list[_GroupProxyType][source]

Fetch all groups together with all members

Parameters

session – The SQLAlchemy session to use

Returns

An iterable of (Group, members) ResultProxies.

_fetch_db_properties(session: Session) list[_PropertyProxyType][source]

Fetch the groups who should be synced.

Explicitly, this returns everything in EXPORTED_PROPERTIES together with the current users having the respective property as members.

Parameters

session – The SQLAlchemy session to use

Returns

An iterable of (property_name, members) ResultProxies.

_fetch_db_users(session: Session, required_property: str | None = None) list[_UserProxyType][source]

Fetch users to be synced, plus whether ldap_login_enabled is set.

If the ldap_login_enabled flag is not present, we interpret this as should_be_blocked.

Parameters
  • session – The SQLAlchemy session to use

  • required_property (str) – the property required to export users

Returns

An iterable of (User, should_be_blocked) ResultProxies having the property required_property and a unix_account.

establish_and_return_session(connection_string: str) Session[source]
fetch_db_groups(session: Session, base_dn: DN, user_base_dn: DN) Iterator[GroupRecord][source]

Fetch the groups to be synced (in the form of GroupRecords).

Parameters
  • session – the SQLAlchemy database session

  • base_dn – the group base dn

  • user_base_dn – the base dn of users. Used to infer DNs of the group’s members.

fetch_db_properties(session: Session, base_dn: DN, user_base_dn: DN) Iterator[GroupRecord][source]

Fetch the properties to be synced (in the form of GroupRecords).

Parameters
  • session – the SQLAlchemy database session

  • base_dn – the property base dn

  • user_base_dn – the base dn of users. Used to infer DNs of the users who are currently carrying this property.

fetch_db_users(session: Session, base_dn: DN, required_property: str | None = None) Iterator[UserRecord][source]

Fetch the users to be synced (in the form of UserRecords).

Parameters
  • session – the SQLAlchemy database session

  • base_dn – the user base dn

  • required_property – which property the users need to currently have in order to be synced

EXPORTED_PROPERTIES = frozenset({'ldap_login_enabled', 'mail', 'member', 'membership_fee', 'network_access', 'payment_in_default', 'traffic_limit_exceeded', 'userdb', 'violation'})

The properties of a user we export to LDAP.

ldap_sync.sources.ldap

_fetch_ldap_entries(connection: Connection, base_dn: str, search_filter: str | None = None, attributes: str | Collection[str] = '*') list[LdapRecord][source]
_fetch_ldap_groups(connection: Connection, base_dn: str) list[LdapRecord][source]
_fetch_ldap_properties(connection: Connection, base_dn: str) list[LdapRecord][source]
_fetch_ldap_users(connection: Connection, base_dn: str) list[LdapRecord][source]
establish_and_return_ldap_connection(config: SyncConfig) Connection[source]
fake_connection() Connection[source]
fetch_ldap_groups(connection: Connection, base_dn: str) Iterator[GroupRecord][source]
fetch_ldap_properties(connection: Connection, base_dn: str) Iterator[GroupRecord][source]
fetch_ldap_users(connection: Connection, base_dn: str) Iterator[UserRecord][source]

ldap_sync.config

namedtuple SyncConfig(db_uri, host, port, use_ssl, ca_certs_file, ca_certs_data, bind_dn, bind_pw, base_dn, required_property)[source]

SyncConfig(db_uri, host, port, use_ssl, ca_certs_file, ca_certs_data, bind_dn, bind_pw, base_dn, required_property)

Fields
  1.  db_uri (str) – Alias for field number 0

  2.  host (str) – Alias for field number 1

  3.  port (int) – Alias for field number 2

  4.  use_ssl (bool) – Alias for field number 3

  5.  ca_certs_file (Optional[str]) – Alias for field number 4

  6.  ca_certs_data (Optional[str]) – Alias for field number 5

  7.  bind_dn (NewType(DN, str)) – Alias for field number 6

  8.  bind_pw (str) – Alias for field number 7

  9.  base_dn (NewType(DN, str)) – Alias for field number 8

  10.  required_property (str) – Alias for field number 9

get_config(**defaults: str | None) SyncConfig[source]

Fetch the config from the environments, filling in defaults as specified.

Values are converted in accordance to the types hints of SyncConfig.

The environment variables need to be of the format is PYCROFT_LDAP_$VAR, e.g. PYCROFT_LDAP_PORT.

get_config_or_exit(**defaults: str | None) SyncConfig[source]

See get_config()

ldap_sync.conversion

Converts DB information to concepts.record.Record instances.

db_user_to_record(user: User, base_dn: DN, should_be_blocked: bool = False) UserRecord[source]
db_group_to_record(name: str, members: Iterable[str], base_dn: DN, user_base_dn: DN) GroupRecord[source]
dn_from_username(username: str, base: DN) DN[source]
dn_from_cn(name: str, base: DN) DN[source]
ldap_user_to_record(record: LdapRecord) UserRecord[source]
ldap_group_to_record(record: LdapRecord) GroupRecord[source]

ldap_sync.execution

Execution strategies for an Action. Concretely, the real one and the dry-run.

execute_real(action: Action, connection: Connection) None[source]
execute_real(action: AddAction, connection: Connection) None
execute_real(action: ModifyAction, connection: Connection) None
execute_real(action: DeleteAction, connection: Connection) None
execute_real(action: IdleAction, connection: Connection) None
debug_whether_success(logger: Logger, connection: Connection) None[source]

Communicate whether the last operation on connection has been successful.

ldap_sync.record_diff

diff_attributes(current_attrs: dict[str, Collection[str] | Collection[bytes] | Collection[int]], desired_attrs: dict[str, Collection[str] | Collection[bytes] | Collection[int]]) dict[str, Collection[str] | Collection[bytes] | Collection[int]][source]

Determine which attributes need to be updated.

This function doesn’t check whether both dicts have equal keys, meaning keys not given in desired_attrs won’t end up in the modification dict. Removing attributes has to be done by explicitly setting them to an empty string.

Parameters
  • current_attrs – the attributes of the entity currently in the system.

  • desired_attrs – the attributes we want the entity to have.

diff_user_attributes(current_attrs: dict[str, Collection[str] | Collection[bytes] | Collection[int]], desired_attrs: dict[str, Collection[str] | Collection[bytes] | Collection[int]]) dict[str, Collection[str] | Collection[bytes] | Collection[int]][source]

Like diff_attributes(), but aware of the ppolicy overlay.

diff_records(current: T | None, desired: T | None) Action[source]

Determines an action to take, given a desired and a current record.

Parameters
  • current – the present state

  • desired – the future state

iter_zip_dicts(d1: dict[TKey, TVal1], d2: dict[TKey, TVal2]) Iterator[tuple[TKey, tuple[TVal1 | None, TVal2 | None]]][source]
bulk_diff_records(current: Iterable[Record], desired: Iterable[Record]) dict[DN, Action][source]