=====
Users
=====

The REST API can be used to add and remove users, add and remove user
addresses, and change their preferred address, password, or name.  The API can
also be used to verify a user's password.

Users are different than members; the latter represents an email address
subscribed to a specific mailing list.  Users are just people that Mailman
knows about.

There are no users yet.

    >>> from mailman.testing.documentation import dump_json
    >>> dump_json('http://localhost:9001/3.0/users')
    http_etag: "..."
    start: 0
    total_size: 0

Anne is added, with an email address.  Her user record gets a `user_id`.

    >>> from zope.component import getUtility
    >>> from mailman.interfaces.usermanager import IUserManager
    >>> user_manager = getUtility(IUserManager)
    >>> anne = user_manager.create_user('anne@example.com', 'Anne Person')
    >>> from mailman.config import config
    >>> transaction = config.db    
    >>> transaction.commit()
    >>> int(anne.user_id.int)
    1

Anne's user record is returned as an entry into the collection of all users.

    >>> dump_json('http://localhost:9001/3.0/users')
    entry 0:
        created_on: 2005-08-01T07:49:23
        display_name: Anne Person
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/1
        user_id: 1
    http_etag: "..."
    start: 0
    total_size: 1

A user might not have a display name, in which case, the attribute will not be
returned in the REST API.

    >>> bart = user_manager.create_user('bart@example.com')
    >>> transaction.commit()
    >>> dump_json('http://localhost:9001/3.0/users')
    entry 0:
        created_on: 2005-08-01T07:49:23
        display_name: Anne Person
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/1
        user_id: 1
    entry 1:
        created_on: 2005-08-01T07:49:23
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/2
        user_id: 2
    http_etag: "..."
    start: 0
    total_size: 2


Paginating over user records
----------------------------

Instead of returning all the user records at once, it's possible to return
them in pages by adding the GET parameters ``count`` and ``page`` to the
request URI.  Page 1 is the first page and ``count`` defines the size of the
page.
::

    >>> dump_json('http://localhost:9001/3.0/users?count=1&page=1')
    entry 0:
        created_on: 2005-08-01T07:49:23
        display_name: Anne Person
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/1
        user_id: 1
    http_etag: "..."
    start: 0
    total_size: 2

    >>> dump_json('http://localhost:9001/3.0/users?count=1&page=2')
    entry 0:
        created_on: 2005-08-01T07:49:23
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/2
        user_id: 2
    http_etag: "..."
    start: 1
    total_size: 2


Searching for users
-------------------

Instead of returning all the users, it's possible to search for user using
partial match for their display_name or email addresses.
::

    >>> dump_json('http://localhost:9001/3.0/users/find?q=anne')
    entry 0:
        created_on: 2005-08-01T07:49:23
        display_name: Anne Person
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/1
        user_id: 1
    http_etag: "..."
    start: 0
    total_size: 1

Search also matches on user's email addresses::

    >>> dump_json('http://localhost:9001/3.0/users/find?q=bart@example')
    entry 0:
        created_on: 2005-08-01T07:49:23
        http_etag: "..."
        is_server_owner: False
        self_link: http://localhost:9001/3.0/users/2
        user_id: 2
    http_etag: "..."
    start: 0
    total_size: 1


Creating users
==============

New users can be created by POSTing to the users collection.  At a minimum,
the user's email address must be provided.

    >>> dump_json('http://localhost:9001/3.0/users', {
    ...           'email': 'cris@example.com',
    ...           })
    content-length: 0
    content-type: application/json
    date: ...
    location: http://localhost:9001/3.0/users/3
    server: ...
    status: 201

Cris is now a user known to the system, but he has no display name.

    >>> user_manager.get_user('cris@example.com')
    <User "" (3) at ...>

Cris's user record can also be accessed via the REST API, using her user id.
Note that because no password was given when the record was created, a random
one was assigned to her.

    >>> dump_json('http://localhost:9001/3.0/users/3')
    created_on: 2005-08-01T07:49:23
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}...
    self_link: http://localhost:9001/3.0/users/3
    user_id: 3

Because email addresses just have an ``@`` sign in then, there's no confusing
them with user ids.  Thus, Cris's record can be retrieved via her email
address.

    >>> dump_json('http://localhost:9001/3.0/users/cris@example.com')
    created_on: 2005-08-01T07:49:23
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}...
    self_link: http://localhost:9001/3.0/users/3
    user_id: 3


Providing a display name
------------------------

When a user is added, a display name can be provided.

    >>> transaction.abort()
    >>> dump_json('http://localhost:9001/3.0/users', {
    ...           'email': 'dave@example.com',
    ...           'display_name': 'Dave Person',
    ...           })
    content-length: 0
    content-type: application/json
    date: ...
    location: http://localhost:9001/3.0/users/4
    server: ...
    status: 201

Dave's user record includes his display name.

    >>> dump_json('http://localhost:9001/3.0/users/4')
    created_on: 2005-08-01T07:49:23
    display_name: Dave Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}...
    self_link: http://localhost:9001/3.0/users/4
    user_id: 4


Providing passwords
-------------------

To avoid getting assigned a random, and irretrievable password (but one which
can be reset), you can provide a password when the user is created.  By
default, the password is provided in plain text, and it is hashed by Mailman
before being stored.

    >>> transaction.abort()
    >>> dump_json('http://localhost:9001/3.0/users', {
    ...           'email': 'elly@example.com',
    ...           'display_name': 'Elly Person',
    ...           'password': 'supersekrit',
    ...           })
    content-length: 0
    content-type: application/json
    date: ...
    location: http://localhost:9001/3.0/users/5
    server: ...
    status: 201

When we view Elly's user record, we can tell that her password has been hashed
because it has the hash algorithm prefix (i.e. the *{plaintext}* marker).

    >>> dump_json('http://localhost:9001/3.0/users/5')
    created_on: 2005-08-01T07:49:23
    display_name: Elly Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}supersekrit
    self_link: http://localhost:9001/3.0/users/5
    user_id: 5


Updating users
==============

Dave's display name can be changed through the REST API.

    >>> dump_json('http://localhost:9001/3.0/users/4', {
    ...           'display_name': 'David Person',
    ...           }, method='PATCH')
    date: ...
    server: ...
    status: 204

Dave's display name has been updated.

    >>> dump_json('http://localhost:9001/3.0/users/dave@example.com')
    created_on: 2005-08-01T07:49:23
    display_name: David Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}...
    self_link: http://localhost:9001/3.0/users/4
    user_id: 4

Dave can also be assigned a new password by providing in the new cleartext
password.  Mailman will hash this before it is stored internally.

    >>> dump_json('http://localhost:9001/3.0/users/4', {
    ...           'cleartext_password': 'clockwork angels',
    ...           }, method='PATCH')
    date: ...
    server: ...
    status: 204

As described above, even though you see *{plaintext}clockwork angels* below,
it has still been hashed before storage.  The default hashing algorithm for
the test suite is a plain text hash, but you can see that it works by the
addition of the algorithm prefix.

    >>> dump_json('http://localhost:9001/3.0/users/4')
    created_on: 2005-08-01T07:49:23
    display_name: David Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}clockwork angels
    self_link: http://localhost:9001/3.0/users/4
    user_id: 4

You can change both the display name and the password by PUTing the full
resource.

    >>> dump_json('http://localhost:9001/3.0/users/4', {
    ...           'cleartext_password': 'the garden',
    ...           'display_name': 'David Personhood',
    ...           'is_server_owner': False,
    ...           }, method='PUT')
    date: ...
    server: ...
    status: 204

Dave's user record has been updated.

    >>> dump_json('http://localhost:9001/3.0/users/dave@example.com')
    created_on: 2005-08-01T07:49:23
    display_name: David Personhood
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}the garden
    self_link: http://localhost:9001/3.0/users/4
    user_id: 4


Deleting users via the API
==========================

Users can also be deleted via the API.

    >>> dump_json('http://localhost:9001/3.0/users/cris@example.com',
    ...           method='DELETE')
    date: ...
    server: ...
    status: 204


User addresses
==============

Fred may have any number of email addresses associated with his user account,
and we can find them all through the API.

Through some other means, Fred registers a bunch of email addresses and
associates them with his user account.

    >>> fred = user_manager.create_user('fred@example.com', 'Fred Person')
    >>> fred.register('fperson@example.com')
    <Address: fperson@example.com [not verified] at ...>
    >>> fred.register('fred.person@example.com')
    <Address: fred.person@example.com [not verified] at ...>
    >>> fred.register('Fred.Q.Person@example.com')
    <Address: Fred.Q.Person@example.com [not verified]
              key: fred.q.person@example.com at ...>
    >>> transaction.commit()

When we access Fred's addresses via the REST API, they are sorted in lexical
order by original (i.e. case-preserved) email address.

    >>> dump_json('http://localhost:9001/3.0/users/fred@example.com/addresses')
    entry 0:
        email: fred.q.person@example.com
        http_etag: "..."
        original_email: Fred.Q.Person@example.com
        registered_on: 2005-08-01T07:49:23
        self_link:
            http://localhost:9001/3.0/addresses/fred.q.person@example.com
        user: http://localhost:9001/3.0/users/6
    entry 1:
        email: fperson@example.com
        http_etag: "..."
        original_email: fperson@example.com
        registered_on: 2005-08-01T07:49:23
        self_link: http://localhost:9001/3.0/addresses/fperson@example.com
        user: http://localhost:9001/3.0/users/6
    entry 2:
        email: fred.person@example.com
        http_etag: "..."
        original_email: fred.person@example.com
        registered_on: 2005-08-01T07:49:23
        self_link: http://localhost:9001/3.0/addresses/fred.person@example.com
        user: http://localhost:9001/3.0/users/6
    entry 3:
        display_name: Fred Person
        email: fred@example.com
        http_etag: "..."
        original_email: fred@example.com
        registered_on: 2005-08-01T07:49:23
        self_link: http://localhost:9001/3.0/addresses/fred@example.com
        user: http://localhost:9001/3.0/users/6
    http_etag: "..."
    start: 0
    total_size: 4

In fact, since these are all associated with Fred's user account, any of the
addresses can be used to look up Fred's user record.
::

    >>> dump_json('http://localhost:9001/3.0/users/fred@example.com')
    created_on: 2005-08-01T07:49:23
    display_name: Fred Person
    http_etag: "..."
    is_server_owner: False
    self_link: http://localhost:9001/3.0/users/6
    user_id: 6

    >>> dump_json('http://localhost:9001/3.0/users/fred.person@example.com')
    created_on: 2005-08-01T07:49:23
    display_name: Fred Person
    http_etag: "..."
    is_server_owner: False
    self_link: http://localhost:9001/3.0/users/6
    user_id: 6

    >>> dump_json('http://localhost:9001/3.0/users/fperson@example.com')
    created_on: 2005-08-01T07:49:23
    display_name: Fred Person
    http_etag: "..."
    is_server_owner: False
    self_link: http://localhost:9001/3.0/users/6
    user_id: 6

    >>> dump_json('http://localhost:9001/3.0/users/Fred.Q.Person@example.com')
    created_on: 2005-08-01T07:49:23
    display_name: Fred Person
    http_etag: "..."
    is_server_owner: False
    self_link: http://localhost:9001/3.0/users/6
    user_id: 6


Verifying passwords
===================

A user's password is stored internally in hashed form.  Logging in a user is
the process of verifying a provided clear text password against the hashed
internal password.

When Elly was added as a user, she provided a password in the clear.  Now the
password is hashed and getting her user record returns the hashed password.

    >>> dump_json('http://localhost:9001/3.0/users/5')
    created_on: 2005-08-01T07:49:23
    display_name: Elly Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}supersekrit
    self_link: http://localhost:9001/3.0/users/5
    user_id: 5

Unless the client can run the hashing algorithm on the login text that Elly
provided, and do its own comparison, the client should let the REST API handle
password verification.

This time, Elly successfully logs into Mailman.

    >>> dump_json('http://localhost:9001/3.0/users/5/login', {
    ...           'cleartext_password': 'supersekrit',
    ...           }, method='POST')
    date: ...
    server: ...
    status: 204


Server owners
=============

Users can be designated as server owners.  Elly is not currently a server
owner.

    >>> dump_json('http://localhost:9001/3.0/users/5')
    created_on: 2005-08-01T07:49:23
    display_name: Elly Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}supersekrit
    self_link: http://localhost:9001/3.0/users/5
    user_id: 5

Let's make her a server owner.
::

    >>> dump_json('http://localhost:9001/3.0/users/5', {
    ...           'is_server_owner': True,
    ...           }, method='PATCH')
    date: ...
    server: ...
    status: 204

    >>> dump_json('http://localhost:9001/3.0/users/5')
    created_on: 2005-08-01T07:49:23
    display_name: Elly Person
    http_etag: "..."
    is_server_owner: True
    password: {plaintext}supersekrit
    self_link: http://localhost:9001/3.0/users/5
    user_id: 5

Elly later retires as server owner.
::

    >>> dump_json('http://localhost:9001/3.0/users/5', {
    ...           'is_server_owner': False,
    ...           }, method='PATCH')
    date: ...
    server: ...
    status: 204

    >>> dump_json('http://localhost:9001/3.0/users/5')
    created_on: 2005-08-01T07:49:23
    display_name: Elly Person
    http_etag: "..."
    is_server_owner: False
    password: {plaintext}...
    self_link: http://localhost:9001/3.0/users/5
    user_id: 5

Gwen, a new users, takes over as a server owner.
::

    >>> dump_json('http://localhost:9001/3.0/users', {
    ...           'display_name': 'Gwen Person',
    ...           'email': 'gwen@example.com',
    ...           'is_server_owner': True,
    ...           })
    content-length: 0
    content-type: application/json
    date: ...
    location: http://localhost:9001/3.0/users/7
    server: ...
    status: 201

    >>> dump_json('http://localhost:9001/3.0/users/7')
    created_on: 2005-08-01T07:49:23
    display_name: Gwen Person
    http_etag: "..."
    is_server_owner: True
    password: {plaintext}...
    self_link: http://localhost:9001/3.0/users/7
    user_id: 7


Linking users
=============

If an address already exists, but is not yet linked to a user, and a new user
is requested for that address, the user will be linked to the existing
address.

Herb's address already exists, but no user is linked to it.

    >>> herb = user_manager.create_address('herb@example.com')
    >>> print(herb.user)
    None
    >>> transaction.commit()

Now, a user creation request is received, using Herb's email address.

    >>> dump_json('http://localhost:9001/3.0/users', {
    ...           'email': 'herb@example.com',
    ...           'display_name': 'Herb Person',
    ...           })
    content-length: 0
    content-type: application/json
    date: ...
    location: http://localhost:9001/3.0/users/8
    server: ...
    status: 201

Herb's email address is now linked to the new user.

    >>> herb.user
    <User "Herb Person" (8) at ...
