summaryrefslogtreecommitdiffstats
path: root/ipalib/plugins/stageuser.py
blob: 2a9a7f41323837a78ff5133cd3c2ffe0c1a3a8a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# Authors:
#   Thierry Bordaz <tbordaz@redhat.com>
#
# Copyright (C) 2014  Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from time import gmtime, strftime
import string
import posixpath
import os

from ipalib import api, errors
from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
from ipalib.plugable import Registry
from ipalib.plugins.baseldap import LDAPCreate, DN, entry_to_dict
from ipalib.plugins import baseldap
from ipalib.plugins.baseuser import baseuser, baseuser_add, baseuser_mod, baseuser_find, \
    NO_UPG_MAGIC, radius_dn2pk, \
    baseuser_pwdchars, fix_addressbook_permission_bindrule, normalize_principal, validate_principal, \
    baseuser_output_params, status_baseuser_output_params

from ipalib.request import context
from ipalib import _, ngettext
from ipalib import output
from ipaplatform.paths import paths
from ipapython.ipautil import ipa_generate_password
from ipapython.ipavalidate import Email
from ipalib.capabilities import client_has_capability
from ipalib.util import (normalize_sshpubkey, validate_sshpubkey,
    convert_sshpubkey_post)
if api.env.in_server and api.env.context in ['lite', 'server']:
    from ipaserver.plugins.ldap2 import ldap2

__doc__ = _("""
Stageusers

Manage stage user entries.

Stage user entries are directly under the container: "cn=stage users,
cn=accounts, cn=provisioning, SUFFIX".
User can not authenticate with those entries (even if the entries
contain credentials) and are candidate to become Active entries.

Active user entries are Posix users directly under the container: "cn=accounts, SUFFIX".
User can authenticate with Active entries, at the condition they have
credentials

Delete user enties are Posix users directly under the container: "cn=deleted users,
cn=accounts, cn=provisioning, SUFFIX".
User can not authenticate with those entries (even if the entries contain credentials)

The stage user container contains entries
    - created by 'stageuser-add' commands that are Posix users
    - created by external provisioning system

A valid stage user entry MUST:
    - entry RDN is 'uid'
    - ipaUniqueID is 'autogenerate'

IPA supports a wide range of username formats, but you need to be aware of any
restrictions that may apply to your particular environment. For example,
usernames that start with a digit or usernames that exceed a certain length
may cause problems for some UNIX systems.
Use 'ipa config-mod' to change the username format allowed by IPA tools.


EXAMPLES:

 Add a new stageuser:
   ipa stageuser-add --first=Tim --last=User --password tuser1

 Add a stageuser from the Delete container
   ipa stageuser-add  --first=Tim --last=User --from-delete tuser1

""")

register = Registry()


stageuser_output_params = baseuser_output_params

status_output_params = status_baseuser_output_params

@register()
class stageuser(baseuser):
    """
    Stage User object
    A Stage user is not an Active user and can not be used to bind with.
    Stage container is: cn=staged users,cn=accounts,cn=provisioning,SUFFIX
    Stage entry conforms the schema
    Stage entry RDN attribute is 'uid'
    Stage entry are disabled (nsAccountLock: True) through cos
    """

    container_dn              = baseuser.stage_container_dn
    label                     = _('Stage Users')
    label_singular            = _('Stage User')
    object_name               = _('stage user')
    object_name_plural        = _('stage users')
    managed_permissions       = {}

@register()
class stageuser_add(baseuser_add):
    __doc__ = _('Add a new stage user.')

    msg_summary = _('Added stage user "%(value)s"')

    has_output_params = baseuser_add.has_output_params + stageuser_output_params

    takes_options = LDAPCreate.takes_options + (
        Flag('from_delete?',
            doc=_('Create Stage user in from a delete user'),
            cli_name='from_delete',
            default=False,
        ),
    )

    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
        assert isinstance(dn, DN)

        if not options.get('from_delete'):
            # then givenname and sn are required attributes
            if 'givenname' not in entry_attrs:
                raise errors.RequirementError(name='givenname', error=_('givenname is required'))

            if 'sn' not in entry_attrs:
                raise errors.RequirementError(name='sn', error=_('sn is required'))

        # we don't want an user private group to be created for this user
        # add NO_UPG_MAGIC description attribute to let the DS plugin know
        entry_attrs.setdefault('description', [])
        entry_attrs['description'].append(NO_UPG_MAGIC)

        # uidNumber/gidNumber
        entry_attrs.setdefault('uidnumber', baseldap.DNA_MAGIC)
        entry_attrs.setdefault('gidnumber', baseldap.DNA_MAGIC)

        if not client_has_capability(
                options['version'], 'optional_uid_params'):
            # https://fedorahosted.org/freeipa/ticket/2886
            # Old clients say 999 (OLD_DNA_MAGIC) when they really mean
            # "assign a value dynamically".
            OLD_DNA_MAGIC = 999
            if entry_attrs.get('uidnumber') == OLD_DNA_MAGIC:
                entry_attrs['uidnumber'] = baseldap.DNA_MAGIC
            if entry_attrs.get('gidnumber') == OLD_DNA_MAGIC:
                entry_attrs['gidnumber'] = baseldap.DNA_MAGIC


        # Check the lenght of the RDN (uid) value
        config = ldap.get_ipa_config()
        if 'ipamaxusernamelength' in config:
            if len(keys[-1]) > int(config.get('ipamaxusernamelength')[0]):
                raise errors.ValidationError(
                    name=self.obj.primary_key.cli_name,
                    error=_('can be at most %(len)d characters') % dict(
                        len = int(config.get('ipamaxusernamelength')[0])
                    )
                )
        default_shell = config.get('ipadefaultloginshell', [paths.SH])[0]
        entry_attrs.setdefault('loginshell', default_shell)
        # hack so we can request separate first and last name in CLI
        full_name = '%s %s' % (entry_attrs['givenname'], entry_attrs['sn'])
        entry_attrs.setdefault('cn', full_name)

        # Homedirectory
        # (order is : option, placeholder (TBD), CLI default value (here in config))
        if 'homedirectory' not in entry_attrs:
            # get home's root directory from config
            homes_root = config.get('ipahomesrootdir', [paths.HOME_DIR])[0]
            # build user's home directory based on his uid
            entry_attrs['homedirectory'] = posixpath.join(homes_root, keys[-1])

        # Kerberos principal
        entry_attrs.setdefault('krbprincipalname', '%s@%s' % (entry_attrs['uid'], api.env.realm))


        # If requested, generate a userpassword
        if 'userpassword' not in entry_attrs and options.get('random'):
            entry_attrs['userpassword'] = ipa_generate_password(baseuser_pwdchars)
            # save the password so it can be displayed in post_callback
            setattr(context, 'randompassword', entry_attrs['userpassword'])

        # Check the email or create it
        if 'mail' in entry_attrs:
            entry_attrs['mail'] = self.obj.normalize_and_validate_email(entry_attrs['mail'], config)
        else:
            # No e-mail passed in. If we have a default e-mail domain set
            # then we'll add it automatically.
            defaultdomain = config.get('ipadefaultemaildomain', [None])[0]
            if defaultdomain:
                entry_attrs['mail'] = self.obj.normalize_and_validate_email(keys[-1], config)

        # If the manager is defined, check it is a ACTIVE user to validate it
        if 'manager' in entry_attrs:
            entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], self.obj.active_container_dn)

        if ('objectclass' in entry_attrs
            and 'userclass' in entry_attrs
            and 'ipauser' not in entry_attrs['objectclass']):
            entry_attrs['objectclass'].append('ipauser')

        if 'ipatokenradiusconfiglink' in entry_attrs:
            cl = entry_attrs['ipatokenradiusconfiglink']
            if cl:
                if 'objectclass' not in entry_attrs:
                    _entry = ldap.get_entry(dn, ['objectclass'])
                    entry_attrs['objectclass'] = _entry['objectclass']

                if 'ipatokenradiusproxyuser' not in entry_attrs['objectclass']:
                    entry_attrs['objectclass'].append('ipatokenradiusproxyuser')

                answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl)
                entry_attrs['ipatokenradiusconfiglink'] = answer

        return dn

    def execute(self, *keys, **options):
        '''
        A stage entry may be taken from the Delete container.
        In that case we rather do 'MODRDN' than 'ADD'.
        '''
        if options.get('from_delete'):
            ldap = self.obj.backend

            staging_dn = self.obj.get_dn(*keys, **options)
            delete_dn = DN(staging_dn[0], self.obj.delete_container_dn, api.env.basedn)

            # Check that this value is a Active user
            try:
                entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(delete_dn, ['dn'])
            except errors.NotFound:
                raise
            self._exc_wrapper(keys, options, ldap.move_entry_newsuperior)(delete_dn, str(DN(self.obj.stage_container_dn, api.env.basedn)))

            entry_attrs = entry_to_dict(entry_attrs, **options)
            entry_attrs['dn'] = delete_dn

            if self.obj.primary_key and keys[-1] is not None:
                return dict(result=entry_attrs, value=keys[-1])
            return dict(result=entry_attrs, value=u'')
        else:
            return super(stageuser_add, self).execute(*keys, **options)

    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
        assert isinstance(dn, DN)
        config = ldap.get_ipa_config()

        # Fetch the entry again to update memberof, mep data, etc updated
        # at the end of the transaction.
        newentry = ldap.get_entry(dn, ['*'])
        entry_attrs.update(newentry)

        if options.get('random', False):
            try:
                entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword'))
            except AttributeError:
                # if both randompassword and userpassword options were used
                pass

        self.obj.get_password_attributes(ldap, dn, entry_attrs)
        convert_sshpubkey_post(ldap, dn, entry_attrs)
        radius_dn2pk(self.api, entry_attrs)
        return dn