Resource Users
==============

The ``Users`` resource is represented in the LDAP tree as user objects.

To list those LDAP objects run in  a terminal:

.. code-block:: console

    FILTER='(|(objectClass=ucsschoolStaff)(objectClass=ucsschoolStudent)(objectClass=ucsschoolTeacher))'
    univention-ldapsearch -LLL "$FILTER"

UCS\@school uses the UDM to access the LDAP directory.
UDM properties have different names than their associated LDAP attributes.
Their values may also differ.
To list the same UDM objects as above, run:

.. code-block:: console

    $ FILTER='(|(objectClass=ucsschoolStaff)(objectClass=ucsschoolStudent)(objectClass=ucsschoolTeacher))'
    $ udm users/user list --filter "$FILTER"

Kelvin API documentation
------------------------

Please see the `Kelvin API documentation section Resource Users`_ about allowed values for the attributes.

User class
----------

The :py:class:`ucsschool.kelvin.client.User` class has the following public attributes and methods:

.. code-block:: python

    class User(KelvinObject):
        def __init__(
            self,
            name: str = None,
            school: str = None,
            *,
            firstname: str = None,
            lastname: str = None,
            birthday: datetime.date = None,
            disabled: bool = False,
            email: str = None,
            expiration_date: datetime.date = None,
            kelvin_password_hashes: PasswordsHashes = None,
            password: str = None,
            record_uid: str = None,
            roles: List[str],
            schools: List[str],
            school_classes: Dict[str, List[str]] = None,
            workgroups: Dict[str, List[str]] = None,
            source_uid: str = None,
            udm_properties: Dict[str, Any] = None,
            ucsschool_roles: List[str] = None,
            dn: str = None,
            url: str = None,
            legal_guardians: List[str] = None,
            legal_wards: List[str] = None,
            session: Session = None,
            language: str = None,
            **kwargs,
        ):
        self.name = name
        self.school = school
        self.firstname = firstname
        self.lastname = lastname
        self.birthday = birthday
        self.disabled = disabled
        self.email = email
        self.expiration_date = expiration_date
        self.kelvin_password_hashes = kelvin_password_hashes
        self.password = password
        self.record_uid = record_uid
        self.roles = roles
        self.schools = schools
        self.school_classes = school_classes or {}
        self.workgroups = workgroups or {}
        self.source_uid = source_uid
        self.udm_properties = udm_properties or {}
        self.ucsschool_roles = ucsschool_roles
        self.dn = dn
        self.url = url
        self.session = session
        self.legal_guardians = legal_guardians or []
        self.legal_wards = legal_wards or []
        if language:
            self.session.language = language

        async def reload(self) -> User:
            ...
        async def save(self) -> User:
            ...
        async def delete(self) -> None:
            ...
        def as_dict(self) -> Dict[str, Any]:
            ...

.. note::
    The field ``expiration_date`` was added to the Kelvin REST API in version ``1.5.1``. The client works with prior server versions, but the attribute will not be read or set.
.. note::
    Since the Kelvin REST API client version ``2.0.0``, the required argument `school` has the default argument `None`. The argument `name` is not required anymore.

UserResource class
------------------

:py:class:`ucsschool.kelvin.client.UserResource` class has the following public attributes and methods:

.. code-block:: python

    class UserResource(KelvinResource):
        def __init__(self, session: Session, language: str = None):
            ...
        async def get(self, **kwargs) -> User:
            ...
        async def get_from_url(self, url: str) -> User:
            ...
        async def search(self, **kwargs) -> AsyncIterator[User]:
            ...


Create user
-----------

.. code-block:: python

    from ucsschool.kelvin.client import Session, User

    async with Session(**credentials) as session:
        user = User(
            school="DEMOSCHOOL",
            schools=["DEMOSCHOOL"],
            roles=["student"],
            name="test1",
            firstname="test",
            lastname="one",
            record_uid="test1",
            source_uid="TESTID",
            session=session
        )
        await user.save()

    user.dn
    'uid=test1,cn=schueler,cn=users,ou=DEMOSCHOOL,dc=example,dc=com'

.. note::
    Since version ``2.0.0``, all attributes except ``school``, ``schools`` and ``roles`` can be automatically generated on the server by defining a schema. If a schema is defined for an attribute, it can be skipped. The attributes ``name`` and ``record_uid`` have to be passed in either the constructor or a schema must exist. You can find more about schemas in the [UCS@school - Handbuch zur CLI-Import Schnittstelle (german only)](https://docs.software-univention.de/ucsschool-import/5.0/de/configuration/scheme-formatting.html#formatierungsschema).


Retrieve user
-------------

.. code-block:: python

    from ucsschool.kelvin.client import Session, UserResource

    async with Session(**credentials) as session:
        user = await UserResource(session=session).get(name="test1")

    user.as_dict()

    {'name': 'test1',
     'ucsschool_roles': ['student:school:DEMOSCHOOL'],
     'school': 'DEMOSCHOOL',
     'firstname': 'test',
     'lastname': 'one',
     'birthday': None,
     'disabled': False,
     'email': None,
     'expiration_date': None,
     'kelvin_password_hashes': None,
     'password': None,
     'record_uid': 'test1',
     'roles': ['student'],
     'schools': ['DEMOSCHOOL'],
     'school_classes': {},
     'workgroups': {},
     'source_uid': 'TESTID',
     'udm_properties': {},
     'dn': 'uid=test1,cn=schueler,cn=users,ou=DEMOSCHOOL,dc=example,dc=com',
     'url': 'https://master.ucs.local/ucsschool/kelvin/v1/users/test1',
     'legal_guardians': ["https://master.ucs.local/ucsschool/kelvin/v1/users/demo_parent"],
     }


Check if user exists
--------------------

.. code-block:: python

    from ucsschool.kelvin.client import Session, UserResource

    async with Session(**credentials) as session:
        if await UserResource(session=session).exists(name="test1"):
            print("The user exists!")


Search users
------------

The :py:meth:`search()` method allows searching for users, using a number of filters.
Most (but now all) attributes support searching inexact, using an asterisk (``*``) as placeholder.

In the following examples the search is always limited to users of the school ``DEMOSCHOOL``.
In the 1. search *all* users (of the school ``DEMOSCHOOL``) are searched,
2. users with a *username* starting with ``t``,
3. users with a *family name* starting with ``tea`` and
4. users that have the *role* ``teacher``.

.. code-block:: python

    from ucsschool.kelvin.client import Session, UserResource

    async with Session(**credentials) as session:
        async for user in UserResource(session=session).search(school="DEMOSCHOOL"):
            print(user)

    User('name'='demo_admin', dn='uid=demo_admin,cn=lehrer,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')
    User('name'='demo_student', dn='uid=demo_student,cn=schueler,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')
    User('name'='demo_teacher', dn='uid=demo_teacher,cn=lehrer,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')
    User('name'='test1', dn='uid=test1,cn=schueler,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')

        async for user in UserResource(session=session).search(
            name="t*", school="DEMOSCHOOL"
        ):
            print(user)

    User('name'='test1', dn='uid=test1,cn=schueler,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')

        async for user in UserResource(session=session).search(
            lastname="tea*", school="DEMOSCHOOL"
        ):
            print(user)

    User('name'='demo_teacher', dn='uid=demo_teacher,cn=lehrer,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')

        async for user in UserResource(session=session).search(
            roles=["teacher"], school="DEMOSCHOOL"
        ):
            print(user)

    User('name'='demo_admin', dn='uid=demo_admin,cn=lehrer,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')
    User('name'='demo_teacher', dn='uid=demo_teacher,cn=lehrer,cn=users,ou=DEMOSCHOOL,dc=example,dc=com')


Change user properties
----------------------

Get the current user object, change some attributes and save the changes back to LDAP:

.. code-block:: python

    from ucsschool.kelvin.client import Session, User, UserResource

    async def change_properties(username: str, **changes) -> User:
        async with Session(**credentials) as session:
            user = await UserResource(session=session).get(name=username)
            for property, value in changes.items():
                setattr(user, property, value)
            return await user.save()

    user = await change_properties(
        "test1",
        firstname="newfn",
        lastname="newln",
        password="password123",
    )
    assert user.firstname == "newfn"
    assert user.lastname == "newln"


Hint: users cannot be modified, unless their ``record_uid`` and ``source_uid`` attributes are set (as is the case with the ``demo_*`` users).

Move user
---------

User objects support changing both ``school`` and ``name``.

When the ``school`` attribute of a user is changed, the new value *must* be part of the list in the ``schools`` attribute.

In the following example both ``school`` and ``name`` are changed.

.. code-block:: python

    from ucsschool.kelvin.client import Session, User, UserResource

    async with Session(**credentials) as session:
        user = User(
            school="DEMOSCHOOL", schools=["DEMOSCHOOL"],
            roles=["student"], name="test1", firstname="test",
            lastname="one", record_uid="test1",
            source_uid="TESTID", session=session
        )
        await user.save()
        user.dn
        'uid=test1,cn=schueler,cn=users,ou=DEMOSCHOOL,dc=example,dc=com'
        user.name = "test2"
        user.school = "DEMOSCHOOL2"
        user.schools = ["DEMOSCHOOL2"]
        await user.save()
        user.dn
        'uid=test2,cn=schueler,cn=users,ou=DEMOSCHOOL2,dc=example,dc=com'


Delete user
-----------

Get the current user object and delete it:

.. code-block:: python

    from ucsschool.kelvin.client import Session, User, UserResource

    async with Session(**credentials) as session:
        user = await UserResource(session=session).get(name="test1")
        await user.delete()

Trying to retrieve the deleted user will raise a :py:exc:`ucsschool.kelvin.client.NoObject` exception.


.. _`Kelvin API documentation section Resource Users`: https://docs.software-univention.de/ucsschool-kelvin-rest-api/resource-users.html
