Chapter 4

Examples

This section contains some examples of different use cases, and their config files.

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Subsections of Examples

01. Single datasource

Context

In this example, we have a unique Datasource (an Oracle database) that we’ll use to convert typical users, password, groups and group membership data to fill an LDAP server.

Oracle schema

classDiagram
    direction BT
    ORA_USERPASSWORDS <-- ORA_USERS
    ORA_GROUPSMEMBERS <-- ORA_USERS
    ORA_GROUPSMEMBERS <-- ORA_GROUPS
    class ORA_USERS{
      USER_ID - NUMBER, NOT NULL
      LOGIN - VARCHAR2
      FIRSTNAME - VARCHAR2
      LASTNAME - VARCHAR2
      EMAIL - VARCHAR2
    }
    class ORA_USERPASSWORDS{
      USER_ID - NUMBER, NOT NULL
      PASSWORD_ENCRYPTED - RAW
      LDAP_HASHES - VARCHAR2
    }
    class ORA_GROUPS{
      GROUP_ID - NUMBER, NOT NULL
      GROUP_NAME - VARCHAR2
      GROUP_DESC - VARCHAR2
    }
    class ORA_GROUPSMEMBERS{
      USER_ID - NUMBER, NOT NULL
      GROUP_ID - NUMBER, NOT NULL
    }

hermes-server-config

hermes:
  cache:
    dirpath: /path/to/.hermes/hermes-server/cache
    enable_compression: true
    backup_count: 1
  cli_socket:
    path: /path/to/.hermes/hermes-server.sock # Facultative, required to use cli
    owner: user_login # Facultative
    group: group_name # Facultative
    # Facultative, '0600' will be used by default.
    # The value MUST be prefixed by a 0 to indicate that it's an octal integer
    mode: 0660
  logs:
    logfile: /path/to/.hermes/hermes-server/logs/hermes-server.log
    backup_count: 31 # 1 month
    verbosity: info
  mail:
    server: dummy.example.com
    from: Hermes Server <no-reply@example.com>
    to:
      - user@example.com
  plugins:
    # Attribute transform plugins (jinja filters)
    attributes:
      ldapPasswordHash:
        settings:
          default_hash_types:
            - SMD5
            - SSHA
            - SSHA256
            - SSHA512

      crypto_RSA_OAEP:
        settings:
          keys:
            decrypt_from_datasource:
              hash: SHA256
              # WARNING - THIS KEY IS WEAK AND PUBLIC, NEVER USE IT
              rsa_key: |-
                -----BEGIN RSA PRIVATE KEY-----
                MIGrAgEAAiEAstltWwDzmtSSHi7lfKqtUIO4dI8aX/EAopNdR/cWXH8CAwEAAQIh
                AKfflFjGNOJQwvJX3Io+/juxO+HFd7SRC++zBD9paZqZAhEA5OtjZQUapRrV/aC5
                NXFsswIRAMgBtgpz+t0FxyEXdzlcTwUCEHU6WZ8M2xU7xePpH49Ps2MCEQC+78s+
                /WvfNtXcRI+gJfyVAhAjcIWzHC5q4wzgL7psbPGy
                -----END RSA PRIVATE KEY-----                

    # SERVER ONLY - Sources used to fetch data. At lease one must be defined
    datasources:
      datasource_of_example1: # Source name. Use whatever you want. Will be used in datamodel
        type: oracle # Source type. A datasource plugin with this name must exist
        settings: # Settings of current source
          login: HERMES_DUMMY
          password: "DuMmY_p4s5w0rD"
          port: 1234
          server: dummy.example.com
          sid: DUMMY

    messagebus:
      kafka:
        settings:
          servers:
            - dummy.example.com:9093
          ssl:
            certfile: /path/to/.hermes/dummy.crt
            keyfile: /path/to/.hermes/dummy.pem
            cafile: /path/to/.hermes/INTERNAL-CA-chain.crt
          topic: hermes

hermes-server:
  updateInterval: 60 # Interval between two data update, in seconds

  # The declaration order of data types is important:
  # - add/modify events will be processed in the declaration order
  # - remove events will be processed in the reversed declaration order
  datamodel:
    SRVGroups: # Settings for SRVGroups data type
      primarykeyattr: srv_group_id # Attribute name that will be used as primary key
      # Facultative template of object string representation that will be used in logs
      toString: "<SRVGroups[{{ srv_group_id }}, {{ srv_group_name | default('#UNDEF#') }}]>"
      sources: # datasource(s) to use to fetch data. Usually one, but several could be used
        datasource_of_example1: # The source name set in hermes.plugins.datasources
          # The query to fetch data.
          # 'type' is mandatory and indicate to plugin which flavor of query to proceed
          #   Possible 'type' values are 'add', 'delete', 'fetch' and 'modify'
          # 'query' is the query to send
          # 'vars' is a dict with vars to use (and sanitize !) in query
          #
          # According to source type, 'query' and 'vars' may be facultative.
          # A Jinja template can be inserted in 'query' and 'vars' values to avoid wildcards
          # and manually typing the attribute list, or to filter the query using a cached value.
          #
          # Jinja vars available are [REMOTE_ATTRIBUTES, CACHED_VALUES].
          # See documentation for details:
          # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.sources.datasource-name.fetch
          fetch:
            type: fetch
            query: >-
              SELECT {{ REMOTE_ATTRIBUTES | join(', ') }}
              FROM ORA_GROUPS              
          attrsmapping:
            srv_group_id: GROUP_ID
            srv_group_name: GROUP_NAME
            srv_group_desc: GROUP_DESC

    SRVUsers: # Settings for SRVUsers data type
      primarykeyattr: srv_user_id # Attribute name that will be used as primary key
      # Facultative template of object string representation that will be used in logs
      toString: "<SRVUsers[{{ srv_user_id }}, {{ srv_login | default('#UNDEF#') }}]>"
      sources: # datasource(s) to use to fetch data. Usually one, but several could be used
        datasource_of_example1: # The source name set in hermes.plugins.datasources
          # The query to fetch data.
          # 'type' is mandatory and indicate to plugin which flavor of query to proceed
          #   Possible 'type' values are 'add', 'delete', 'fetch' and 'modify'
          # 'query' is the query to send
          # 'vars' is a dict with vars to use (and sanitize !) in query
          #
          # According to source type, 'query' and 'vars' may be facultative.
          # A Jinja template can be inserted in 'query' and 'vars' values to avoid wildcards
          # and manually typing the attribute list, or to filter the query using a cached value.
          #
          # Jinja vars available are [REMOTE_ATTRIBUTES, CACHED_VALUES].
          # See documentation for details:
          # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.sources.datasource-name.fetch
          fetch:
            type: fetch
            query: >-
              SELECT {{ REMOTE_ATTRIBUTES | join(', ') }}
              FROM ORA_USERS              

          attrsmapping:
            srv_user_id: USER_ID
            srv_login: LOGIN
            # Ensure first letter of each names is uppercase, and other are lowercase
            srv_firstname: "{{ FIRSTNAME | title}}"
            srv_lastname: "{{ LASTNAME | title}}"
            srv_mail: MAIL

    SRVUserPasswords: # Settings for SRVUserPasswords data type
      primarykeyattr: srv_user_id # Attribute name that will be used as primary key

      # Integrity constraints between datamodel type, in Jinja.
      # WARNING: could be very slow, keep it as simple as possible, and focused upon
      # primary keys
      # Jinja vars available are '_SELF': the current object, and every types declared
      # For each "typename" declared, two vars are available:
      # - typename_pkeys: a set with every primary keys
      # - typename: a list of dict containing each entries
      # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.integrity_constraints
      integrity_constraints:
        - "{{ _SELF.srv_user_id in SRVUsers_pkeys }}"
      
      sources: # datasource(s) to use to fetch data. Usually one, but several could be used
        datasource_of_example1: # The source name set in hermes.plugins.datasources
          # The query to fetch data.
          # 'type' is mandatory and indicate to plugin which flavor of query to proceed
          #   Possible 'type' values are 'add', 'delete', 'fetch' and 'modify'
          # 'query' is the query to send
          # 'vars' is a dict with vars to use (and sanitize !) in query
          #
          # According to source type, 'query' and 'vars' may be facultative.
          # A Jinja template can be inserted in 'query' and 'vars' values to avoid wildcards
          # and manually typing the attribute list, or to filter the query using a cached value.
          #
          # Jinja vars available are [REMOTE_ATTRIBUTES, CACHED_VALUES].
          # See documentation for details:
          # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.sources.datasource-name.fetch
          fetch:
            type: fetch
            query: >-
              SELECT p.{{ REMOTE_ATTRIBUTES | join(', p.') }}
              FROM ORA_USERPASSWORDS p              

          # For each entry successfully processed, we'll remove PASSWORD_ENCRYPTED
          # and store the freshly computed LDAP_HASHES.
          #
          # Facultative. The query to run each time an item of current data have been processed
          # without errors.
          # 'type' is mandatory and indicate to plugin which flavor of query to proceed
          #   Possible 'type' values are 'add', 'delete', 'fetch' and 'modify'
          # 'query' is the query to send
          # 'vars' is a dict with vars to use (and sanitize !) in query
          #
          # According to source type, 'query' and 'vars' may be facultative.
          # A Jinja template can be inserted in 'query' and 'vars' values to avoid wildcards
          # and manually typing the attribute list, or to filter the query using a cached value.
          #
          # Jinja vars available are [REMOTE_ATTRIBUTES, ITEM_CACHED_VALUES, ITEM_FETCHED_VALUES].
          # See documentation for details:
          # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.sources.datasource-name.commit_one
          commit_one:
            type: modify
            query: >-
              UPDATE ORA_USERPASSWORDS
              SET
                PASSWORD_ENCRYPTED = NULL,
                LDAP_HASHES = :ldap_hashes
              WHERE USER_ID = :user_id              

            vars:
              user_id: "{{ ITEM_FETCHED_VALUES.srv_user_id }}"
              ldap_hashes: "{{ ';'.join(ITEM_FETCHED_VALUES.srv_password_ldap) }}"

          attrsmapping:
            srv_user_id: USER_ID
            # Decipher PASSWORD_ENCRYPTED value to generate the LDAP hashes.
            srv_password_ldap: >-
              {{
                (
                  PASSWORD_ENCRYPTED
                  | crypto_RSA_OAEP('decrypt_from_datasource')
                  | ldapPasswordHash
                )
                | default(None if LDAP_HASHES is None else LDAP_HASHES.split(';'))
              }}              

    SRVGroupsMembers:
      # Attribute names that will be used as primary key: here is is a tuple
      primarykeyattr: [srv_group_id, srv_user_id]
      # Foreign keys declaration between data types
      # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.foreignkeys
      foreignkeys:
        srv_group_id:
          from_objtype: SRVGroups
          from_attr: srv_group_id
        srv_user_id:
          from_objtype: SRVUsers
          from_attr: srv_user_id
      # Integrity constraints between datamodel type, in Jinja.
      # WARNING: could be very slow, keep it as simple as possible, and focused upon
      # primary keys
      # Jinja vars available are '_SELF': the current object, and every types declared
      # For each "typename" declared, two vars are available:
      # - typename_pkeys: a set with every primary keys
      # - typename: a list of dict containing each entries
      # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.integrity_constraints
      integrity_constraints:
        - "{{ _SELF.srv_user_id in SRVUsers_pkeys and _SELF.srv_group_id in SRVGroups_pkeys }}"
      sources: # datasource(s) to use to fetch data. Usually one, but several could be used
        datasource_of_example1: # The source name set in hermes.plugins.datasources
          # The query to fetch data.
          # 'type' is mandatory and indicate to plugin which flavor of query to proceed
          #   Possible 'type' values are 'add', 'delete', 'fetch' and 'modify'
          # 'query' is the query to send
          # 'vars' is a dict with vars to use (and sanitize !) in query
          #
          # According to source type, 'query' and 'vars' may be facultative.
          # A Jinja template can be inserted in 'query' and 'vars' values to avoid wildcards
          # and manually typing the attribute list, or to filter the query using a cached value.
          #
          # Jinja vars available are [REMOTE_ATTRIBUTES, CACHED_VALUES].
          # See documentation for details:
          # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.sources.datasource-name.fetch
          fetch:
            type: fetch
            query: >-
              SELECT {{ REMOTE_ATTRIBUTES | join(', ') }}
              FROM ORA_GROUPSMEMBERS              
          attrsmapping:
            srv_user_id: USER_ID
            srv_group_id: GROUP_ID
hermes:
  cache:
    dirpath: /path/to/.hermes/hermes-server/cache
  cli_socket:
    path: /path/to/.hermes/hermes-server.sock
  logs:
    logfile: /path/to/.hermes/hermes-server/logs/hermes-server.log
    verbosity: info
  mail:
    server: dummy.example.com
    from: Hermes Server <no-reply@example.com>
    to:
      - user@example.com
  plugins:
    attributes:
      ldapPasswordHash:
        settings:
          default_hash_types:
            - SMD5
            - SSHA
            - SSHA256
            - SSHA512

      crypto_RSA_OAEP:
        settings:
          keys:
            decrypt_from_datasource:
              hash: SHA256
              # WARNING - THIS KEY IS WEAK AND PUBLIC, NEVER USE IT
              rsa_key: |-
                -----BEGIN RSA PRIVATE KEY-----
                MIGrAgEAAiEAstltWwDzmtSSHi7lfKqtUIO4dI8aX/EAopNdR/cWXH8CAwEAAQIh
                AKfflFjGNOJQwvJX3Io+/juxO+HFd7SRC++zBD9paZqZAhEA5OtjZQUapRrV/aC5
                NXFsswIRAMgBtgpz+t0FxyEXdzlcTwUCEHU6WZ8M2xU7xePpH49Ps2MCEQC+78s+
                /WvfNtXcRI+gJfyVAhAjcIWzHC5q4wzgL7psbPGy
                -----END RSA PRIVATE KEY-----                

    datasources:
      datasource_of_example1:
        type: oracle
        settings:
          login: HERMES_DUMMY
          password: "DuMmY_p4s5w0rD"
          port: 1234
          server: dummy.example.com
          sid: DUMMY

    messagebus:
      kafka:
        settings:
          servers:
            - dummy.example.com:9093
          ssl:
            certfile: /path/to/.hermes/dummy.crt
            keyfile: /path/to/.hermes/dummy.pem
            cafile: /path/to/.hermes/INTERNAL-CA-chain.crt
          topic: hermes

hermes-server:
  # The declaration order of data types is important:
  # - add/modify events will be processed in the declaration order
  # - remove events will be processed in the reversed declaration order
  datamodel:
    SRVGroups:
      primarykeyattr: srv_group_id
      toString: "<SRVGroups[{{ srv_group_id }}, {{ srv_group_name | default('#UNDEF#') }}]>"
      sources:
        datasource_of_example1:
          fetch:
            type: fetch
            query: >-
              SELECT {{ REMOTE_ATTRIBUTES | join(', ') }}
              FROM ORA_GROUPS              
          attrsmapping:
            srv_group_id: GROUP_ID
            srv_group_name: GROUP_NAME
            srv_group_desc: GROUP_DESC

    SRVUsers:
      primarykeyattr: srv_user_id
      toString: "<SRVUsers[{{ srv_user_id }}, {{ srv_login | default('#UNDEF#') }}]>"
      sources:
        datasource_of_example1:
          fetch:
            type: fetch
            query: >-
              SELECT {{ REMOTE_ATTRIBUTES | join(', ') }}
              FROM ORA_USERS              

          attrsmapping:
            srv_user_id: USER_ID
            srv_login: LOGIN
            # Ensure first letter of each names is uppercase, and other are lowercase
            srv_firstname: "{{ FIRSTNAME | title}}"
            srv_lastname: "{{ LASTNAME | title}}"
            srv_mail: MAIL

    SRVUserPasswords:
      primarykeyattr: srv_user_id

      # Integrity constraints between datamodel type, in Jinja.
      # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.integrity_constraints
      integrity_constraints:
        - "{{ _SELF.srv_user_id in SRVUsers_pkeys }}"
      
      sources:
        datasource_of_example1:
          fetch:
            type: fetch
            query: >-
              SELECT p.{{ REMOTE_ATTRIBUTES | join(', p.') }}
              FROM ORA_USERPASSWORDS p              

          # For each entry successfully processed, we'll remove PASSWORD_ENCRYPTED
          # and store the freshly computed LDAP_HASHES.
          # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.sources.datasource-name.commit_one
          commit_one:
            type: modify
            query: >-
              UPDATE ORA_USERPASSWORDS
              SET
                PASSWORD_ENCRYPTED = NULL,
                LDAP_HASHES = :ldap_hashes
              WHERE USER_ID = :user_id              

            vars:
              user_id: "{{ ITEM_FETCHED_VALUES.srv_user_id }}"
              ldap_hashes: "{{ ';'.join(ITEM_FETCHED_VALUES.srv_password_ldap) }}"

          attrsmapping:
            srv_user_id: USER_ID
            # Decipher PASSWORD_ENCRYPTED value to generate the LDAP hashes.
            srv_password_ldap: >-
              {{
                (
                  PASSWORD_ENCRYPTED
                  | crypto_RSA_OAEP('decrypt_from_datasource')
                  | ldapPasswordHash
                )
                | default(None if LDAP_HASHES is None else LDAP_HASHES.split(';'))
              }}              

    SRVGroupsMembers:
      # The primary key is a tuple
      primarykeyattr: [srv_group_id, srv_user_id]
      foreignkeys:
        srv_group_id:
          from_objtype: SRVGroups
          from_attr: srv_group_id
        srv_user_id:
          from_objtype: SRVUsers
          from_attr: srv_user_id
      # Integrity constraints between datamodel type, in Jinja.
      # https://hermes.insa-strasbourg.fr/en/setup/configuration/hermes-server/#hermes-server.datamodel.data-type-name.integrity_constraints
      integrity_constraints:
        - "{{ _SELF.srv_user_id in SRVUsers_pkeys and _SELF.srv_group_id in SRVGroups_pkeys }}"
      sources:
        datasource_of_example1:
          fetch:
            type: fetch
            query: >-
              SELECT {{ REMOTE_ATTRIBUTES | join(', ') }}
              FROM ORA_GROUPSMEMBERS              
          attrsmapping:
            srv_user_id: USER_ID
            srv_group_id: GROUP_ID

hermes-client-usersgroups_ldap-config

hermes:
  cache:
    dirpath: /path/to/.hermes/hermes-client-usersgroups_ldap/cache
  cli_socket:
    path: /path/to/.hermes/hermes-client-usersgroups_ldap.sock
  logs:
    logfile: /path/to/.hermes/hermes-client-usersgroups_ldap/logs/hermes-client-usersgroups_ldap.log
    verbosity: info
  mail:
    server: dummy.example.com
    from: hermes-client-usersgroups_ldap <no-reply@example.com>
    to:
      - user@example.com
  plugins:
    messagebus:
      kafka:
        settings:
          servers:
            - dummy.example.com:9093
          ssl:
            certfile: /path/to/.hermes/dummy.crt
            keyfile: /path/to/.hermes/dummy.pem
            cafile: /path/to/.hermes/INTERNAL-CA-chain.crt
          topic: hermes
          group_id: hermes-grp

hermes-client-usersgroups_ldap:
    uri: ldaps://ldap.example.com:636
    binddn: cn=account,dc=example,dc=com
    bindpassword: s3cReT_p4s5w0rD
    basedn: dc=example,dc=com
    users_ou: ou=users,dc=example,dc=com
    groups_ou: ou=groups,dc=example,dc=com
    
    # MANDATORY: Name of DN attribute for Users, UserPasswords and Groups
    # You have to set up values for the three, even if you don't use some of the types
    dnAttributes:
      Users: uid
      UserPasswords: uid
      Groups: cn
    
    propagateUserDNChangeOnGroupMember: true
    groupsObjectclass: groupOfNames

    # It is possible to set a default value for some attributes for Users,
    # UserPasswords and Groups. The default value will be set on added and modified
    # events if the local attribute has no value
    defaultValues:
      # Hack to allow creation of an empty group, because of the "MUST member" in schema
      Groups:
        member: ""

    # The local attributes listed here won't be stored in LDAP for Users,
    # UserPasswords and Groups
    attributesToIgnore:
      Users:
        - user_pkey
      UserPasswords:
        - user_pkey
      Groups:
        - group_pkey

hermes-client:
  # Autoremediation policy to use in error queue for events concerning a same object
  # - "disabled" : no autoremediation, events are stacked as is (default)
  # - "conservative" :
  #   - merge an added event with a following modified event
  #   - merge two successive modified events
  # - "maximum" :
  #   - merge an added event with a following modified event
  #   - merge two successive modified events
  #   - delete both events when an added event is followed by a removed event
  #   - merge a removed event followed by an added event in a modified event
  #   - delete a modified event when it is followed by a removed event
  autoremediation: conservative

  datamodel:
    Users:
      hermesType: SRVUsers
      # Facultative template of object string representation that will be used in logs
      toString: "<Users[{{ user_pkey }}, {{ uid | default('#UNDEF#') }}]>"
      attrsmapping:
        user_pkey: srv_user_id
        uid: srv_login
        givenname: srv_firstname
        sn: srv_lastname
        mail: srv_mail
        # Compose the displayname with two other attributes
        displayname: "{{ srv_firstname ~ ' ' ~  srv_lastname }}"
        #
        # Static values
        # Defining them here instead of in default values will allow changes
        # propagation on each entry
        #
        objectclass: "{{ ['person', 'inetOrgPerson', 'eduPerson'] }}"

    UserPasswords:
      hermesType: SRVUserPasswords
      attrsmapping:
        user_pkey: srv_user_id
        userPassword: srv_password_ldap

    Groups:
      hermesType: SRVGroups
      toString: "<Groups[{{ group_pkey }}, {{ cn | default('#UNDEF#') }}]>"
      attrsmapping:
        group_pkey: srv_group_id
        cn: srv_group_name
        description: srv_group_desc
        #
        # Static values
        # Defining them here instead of in default values will allow changes
        # propagation on each entry
        #
        objectclass: "{{ ['groupOfNames'] }}"

    GroupsMembers:
      hermesType: SRVGroupsMembers
      attrsmapping:
        # 'user_pkey' and 'group_pkey' keys can't be renamed
        user_pkey: srv_user_id
        group_pkey: srv_group_id

Attributes flow

flowchart LR
  subgraph Oracle
    direction LR
    ORA_GROUPS
    ORA_USERS
    ORA_USERPASSWORDS
    ORA_GROUPSMEMBERS
  end

  subgraph ORA_GROUPS
    direction LR
    ORA_GROUPS_GROUP_ID["GROUP_ID"]
    ORA_GROUPS_GROUP_NAME["GROUP_NAME"]
    ORA_GROUPS_GROUP_DESC["GROUP_DESC"]
  end

  subgraph ORA_USERS
    direction LR
    ORA_USERS_USER_ID["USER_ID"]
    ORA_USERS_LOGIN["LOGIN"]
    ORA_USERS_FIRSTNAME["FIRSTNAME"]
    ORA_USERS_LASTNAME["LASTNAME"]
    ORA_USERS_EMAIL["EMAIL"]
  end

  subgraph ORA_USERPASSWORDS
    direction LR
    ORA_USERPASSWORDS_USER_ID["USER_ID"]
    ORA_USERPASSWORDS_PASSWORD_ENCRYPTED["PASSWORD_ENCRYPTED"]
    ORA_USERPASSWORDS_LDAP_HASHES["LDAP_HASHES"]
  end

  subgraph ORA_GROUPSMEMBERS
    direction LR
    ORA_GROUPSMEMBERS_USER_ID["USER_ID"]
    ORA_GROUPSMEMBERS_GROUP_ID["GROUP_ID"]
  end



  subgraph hermes-server
    direction LR
    SRVGroups
    SRVUsers
    SRVUserPasswords
    SRVGroupsMembers
  end

  subgraph SRVGroups
    direction LR
    SRVGroups_srv_group_id["srv_group_id"]
    SRVGroups_srv_group_name["srv_group_name"]
    SRVGroups_srv_group_desc["srv_group_desc"]
  end
  ORA_GROUPS_GROUP_ID --> SRVGroups_srv_group_id
  ORA_GROUPS_GROUP_NAME --> SRVGroups_srv_group_name
  ORA_GROUPS_GROUP_DESC --> SRVGroups_srv_group_desc

  subgraph SRVUsers
    direction LR
    SRVUsers_srv_user_id["srv_user_id"]
    SRVUsers_srv_login["srv_login"]
    SRVUsers_srv_firstname["srv_firstname"]
    SRVUsers_srv_lastname["srv_lastname"]
    SRVUsers_srv_mail["srv_mail"]
  end
  ORA_USERS_USER_ID --> SRVUsers_srv_user_id
  ORA_USERS_LOGIN --> SRVUsers_srv_login
  ORA_USERS_FIRSTNAME -->|'title' Jinja filter| SRVUsers_srv_firstname
  ORA_USERS_LASTNAME -->|'title' Jinja filter| SRVUsers_srv_lastname
  ORA_USERS_EMAIL --> SRVUsers_srv_mail

  subgraph SRVUserPasswords
    direction LR
    SRVUserPasswords_srv_user_id["srv_user_id"]
    SRVUserPasswords_srv_password_ldap["srv_password_ldap"]
  end
  ORA_USERPASSWORDS_USER_ID --> SRVUserPasswords_srv_user_id
  ORA_USERPASSWORDS_PASSWORD_ENCRYPTED -->|"'crypto_RSA_OAEP | ldapPasswordHash' Jinja filter"| SRVUserPasswords_srv_password_ldap
  ORA_USERPASSWORDS_LDAP_HASHES <-->|LDAP_HASHED is filled by, or provide its value| SRVUserPasswords_srv_password_ldap

  subgraph SRVGroupsMembers
    direction LR
    SRVGroupsMembers_srv_user_id["srv_user_id"]
    SRVGroupsMembers_srv_group_id["srv_group_id"]
  end
  ORA_GROUPSMEMBERS_USER_ID --> SRVGroupsMembers_srv_user_id
  ORA_GROUPSMEMBERS_GROUP_ID --> SRVGroupsMembers_srv_group_id



  subgraph hermes-client-usersgroups_ldap
    direction LR
    ClientGroups
    ClientUsers
    ClientUserPasswords
    ClientGroupsMembers
  end

  subgraph ClientGroups
    direction LR
    ClientGroups_group_pkey["group_pkey"]
    ClientGroups_cn["cn"]
    ClientGroups_description["description"]
    ClientGroups_objectclass["objectclass"]
  end
  SRVGroups_srv_group_id --> ClientGroups_group_pkey
  SRVGroups_srv_group_name --> ClientGroups_cn
  SRVGroups_srv_group_desc --> ClientGroups_description
  
  subgraph ClientUsers
    direction LR
    ClientUsers_user_pkey["user_pkey"]
    ClientUsers_uid["uid"]
    ClientUsers_givenname["givenname"]
    ClientUsers_sn["sn"]
    ClientUsers_mail["mail"]
    ClientUsers_displayname["displayname"]
    ClientUsers_objectclass["objectclass"]
  end
  SRVUsers_srv_user_id --> ClientUsers_user_pkey
  SRVUsers_srv_login --> ClientUsers_uid
  SRVUsers_srv_firstname --> ClientUsers_givenname
  SRVUsers_srv_firstname --> ClientUsers_displayname
  SRVUsers_srv_lastname --> ClientUsers_displayname
  SRVUsers_srv_lastname --> ClientUsers_sn
  SRVUsers_srv_mail --> ClientUsers_mail
  
  subgraph ClientUserPasswords
    direction LR
    ClientUserPasswords_user_pkey["user_pkey"]
    ClientUserPasswords_userPassword["userPassword"]
  end
  SRVUserPasswords_srv_user_id --> ClientUserPasswords_user_pkey
  SRVUserPasswords_srv_password_ldap --> ClientUserPasswords_userPassword


  subgraph ClientGroupsMembers
    direction LR
    ClientGroupsMembers_user_pkey["user_pkey"]
    ClientGroupsMembers_group_pkey["group_pkey"]
  end
  SRVGroupsMembers_srv_user_id --> ClientGroupsMembers_user_pkey
  SRVGroupsMembers_srv_group_id --> ClientGroupsMembers_group_pkey




  subgraph LDAP
    direction LR
    LDAPGroups
    LDAPUsers
  end

  subgraph LDAPGroups
    direction LR
    LDAPGroups_cn["cn"]
    LDAPGroups_description["description"]
    LDAPGroups_objectclass["objectclass"]
    LDAPGroups_member["member"]
  end
  ClientGroups_cn --> LDAPGroups_cn
  ClientGroups_description --> LDAPGroups_description
  ClientGroups_objectclass --> LDAPGroups_objectclass
  ClientGroupsMembers_user_pkey -->|converted to user DN| LDAPGroups_member
  ClientGroupsMembers_group_pkey -->|converted to group DN| LDAPGroups_member

  subgraph LDAPUsers
    direction LR
    LDAPUsers_uid["uid"]
    LDAPUsers_givenname["givenname"]
    LDAPUsers_displayname["displayname"]
    LDAPUsers_displayname["displayname"]
    LDAPUsers_sn["sn"]
    LDAPUsers_mail["mail"]
    LDAPUsers_objectclass["objectclass"]
    LDAPUsers_userPassword["userPassword"]
  end
  ClientUsers_uid --> LDAPUsers_uid
  ClientUsers_givenname --> LDAPUsers_givenname
  ClientUsers_displayname --> LDAPUsers_displayname
  ClientUsers_sn --> LDAPUsers_sn
  ClientUsers_mail --> LDAPUsers_mail
  ClientUsers_objectclass --> LDAPUsers_objectclass
  ClientUserPasswords_userPassword --> LDAPUsers_userPassword

  classDef global fill:#fafafa,stroke-dasharray: 5 5
  class Oracle,hermes-server,hermes-client-usersgroups_ldap,LDAP global