1#!/usr/local/bin/python3.8
2
3# (c) 2018-2019, NetApp, Inc
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6'''
7na_ontap_user
8'''
9
10from __future__ import absolute_import, division, print_function
11__metaclass__ = type
12
13
14DOCUMENTATION = '''
15
16module: na_ontap_user
17
18short_description: NetApp ONTAP user configuration and management
19extends_documentation_fragment:
20    - netapp.ontap.netapp.na_ontap
21version_added: 2.6.0
22author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
23
24description:
25- Create or destroy users.
26
27options:
28  state:
29    description:
30    - Whether the specified user should exist or not.
31    choices: ['present', 'absent']
32    type: str
33    default: 'present'
34  name:
35    description:
36    - The name of the user to manage.
37    required: true
38    type: str
39  application_strs:
40    version_added: 21.6.0
41    description:
42    - List of applications to grant access to.
43    - This option maintains backward compatibility with the existing C(applications) option, but is limited.
44    - It is recommended to use the new C(application_dicts) option which provides more flexibility.
45    - Creating a login with application console, telnet, rsh, and service-processor for a data vserver is not supported.
46    - Module supports both service-processor and service_processor choices.
47    - ZAPI requires service-processor, while REST requires service_processor, except for an issue with ONTAP 9.6 and 9.7.
48    - snmp is not supported in REST.
49    - Either C(application_dicts) or C(application_strs) is required.
50    type: list
51    elements: str
52    choices: ['console', 'http','ontapi','rsh','snmp','service_processor','service-processor','sp','ssh','telnet']
53    aliases:
54      - application
55      - applications
56  application_dicts:
57    version_added: 21.6.0
58    description:
59    - List of applications to grant access to.  Provides better control on applications and authentication methods.
60    - Creating a login with application console, telnet, rsh, and service-processor for a data vserver is not supported.
61    - Module supports both service-processor and service_processor choices.
62    - ZAPI requires service-processor, while REST requires service_processor, except for an issue with ONTAP 9.6 and 9.7.
63    - snmp is not supported in REST.
64    - Either C(application_dicts) or C(application_strs) is required.
65    type: list
66    elements: dict
67    suboptions:
68      application:
69        description: name of the application.
70        type: str
71        choices: ['console', 'http','ontapi','rsh','snmp','service_processor','service-processor','sp','ssh','telnet']
72        required: true
73      authentication_methods:
74        description: list of authentication methods for the application.
75        type: list
76        elements: str
77        choices: ['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert']
78        required: true
79      second_authentication_method:
80        description: when using ssh, optional additional authentication method for MFA.
81        type: str
82        choices: ['none', 'password', 'publickey', 'nsswitch']
83  authentication_method:
84    description:
85    - Authentication method for the application.  If you need more than one method, use C(application_dicts).
86    - Not all authentication methods are valid for an application.
87    - Valid authentication methods for each application are as denoted in I(authentication_choices_description).
88    - Password for console application
89    - Password, domain, nsswitch, cert for http application.
90    - Password, domain, nsswitch, cert for ontapi application.
91    - Community for snmp application (when creating SNMPv1 and SNMPv2 users).
92    - The usm and community for snmp application (when creating SNMPv3 users).
93    - Password for sp application.
94    - Password for rsh application.
95    - Password for telnet application.
96    - Password, publickey, domain, nsswitch for ssh application.
97    - Required when C(application_strs) is present.
98    type: str
99    choices: ['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert']
100  set_password:
101    description:
102    - Password for the user account.
103    - It is ignored for creating snmp users, but is required for creating non-snmp users.
104    - For an existing user, this value will be used as the new password.
105    type: str
106  role_name:
107    description:
108    - The name of the role. Required when C(state=present)
109    type: str
110  lock_user:
111    description:
112    - Whether the specified user account is locked.
113    type: bool
114  vserver:
115    description:
116    - The name of the vserver to use.
117    - With REST, for cluster scope, use a vserver entry with no value.
118    aliases:
119      - svm
120    required: true
121    type: str
122  authentication_protocol:
123    description:
124    - Authentication protocol for the snmp user.
125    - When cluster FIPS mode is on, 'sha' and 'sha2-256' are the only possible and valid values.
126    - When cluster FIPS mode is off, the default value is 'none'.
127    - When cluster FIPS mode is on, the default value is 'sha'.
128    - Only available for 'usm' authentication method and non modifiable.
129    choices: ['none', 'md5', 'sha', 'sha2-256']
130    type: str
131    version_added: '20.6.0'
132  authentication_password:
133    description:
134    - Password for the authentication protocol. This should be minimum 8 characters long.
135    - This is required for 'md5', 'sha' and 'sha2-256' authentication protocols and not required for 'none'.
136    - Only available for 'usm' authentication method and non modifiable.
137    type: str
138    version_added: '20.6.0'
139  engine_id:
140    description:
141    - Authoritative entity's EngineID for the SNMPv3 user.
142    - This should be specified as a hexadecimal string.
143    - Engine ID with first bit set to 1 in first octet should have a minimum of 5 or maximum of 32 octets.
144    - Engine Id with first bit set to 0 in the first octet should be 12 octets in length.
145    - Engine Id cannot have all zeros in its address.
146    - Only available for 'usm' authentication method and non modifiable.
147    type: str
148    version_added: '20.6.0'
149  privacy_protocol:
150    description:
151    - Privacy protocol for the snmp user.
152    - When cluster FIPS mode is on, 'aes128' is the only possible and valid value.
153    - When cluster FIPS mode is off, the default value is 'none'. When cluster FIPS mode is on, the default value is 'aes128'.
154    - Only available for 'usm' authentication method and non modifiable.
155    choices: ['none', 'des', 'aes128']
156    type: str
157    version_added: '20.6.0'
158  privacy_password:
159    description:
160    - Password for the privacy protocol. This should be minimum 8 characters long.
161    - This is required for 'des' and 'aes128' privacy protocols and not required for 'none'.
162    - Only available for 'usm' authentication method and non modifiable.
163    type: str
164    version_added: '20.6.0'
165  remote_switch_ipaddress:
166    description:
167    - This optionally specifies the IP Address of the remote switch.
168    - The remote switch could be a cluster switch monitored by Cluster Switch Health Monitor (CSHM)
169      or a Fiber Channel (FC) switch monitored by Metro Cluster Health Monitor (MCC-HM).
170    - This is applicable only for a remote SNMPv3 user i.e. only if user is a remote (non-local) user,
171      application is snmp and authentication method is usm.
172    type: str
173    version_added: '20.6.0'
174  replace_existing_apps_and_methods:
175    description:
176    - If the user already exists, the current applications and authentications methods are replaced when state=present.
177    - If the user already exists, the current applications and authentications methods are removed when state=absent.
178    - When using application_dicts or REST, this the only supported behavior.
179    - When using application_strs and ZAPI, this is the behavior when this option is set to always.
180    - When using application_strs and ZAPI, if the option is set to auto, applications that are not listed are not removed.
181    - When using application_strs and ZAPI, if the option is set to auto, authentication mehods that are not listed are not removed.
182    - C(auto) preserve the existing behavior for backward compatibility, but note that REST and ZAPI have inconsistent behavior.
183    - This is another reason to recommend to use C(application_dicts).
184    type: str
185    choices: ['always', 'auto']
186    default: 'auto'
187    version_added: '20.6.0'
188'''
189
190EXAMPLES = """
191
192    - name: Create User
193      netapp.ontap.na_ontap_user:
194        state: present
195        name: SampleUser
196        applications: ssh,console
197        authentication_method: password
198        set_password: apn1242183u1298u41
199        lock_user: True
200        role_name: vsadmin
201        vserver: ansibleVServer
202        hostname: "{{ netapp_hostname }}"
203        username: "{{ netapp_username }}"
204        password: "{{ netapp_password }}"
205
206    - name: Delete User
207      netapp.ontap.na_ontap_user:
208        state: absent
209        name: SampleUser
210        applications: ssh
211        authentication_method: password
212        vserver: ansibleVServer
213        hostname: "{{ netapp_hostname }}"
214        username: "{{ netapp_username }}"
215        password: "{{ netapp_password }}"
216
217    - name: Create user with snmp application (ZAPI)
218      netapp.ontap.na_ontap_user:
219        state: present
220        name: test_cert_snmp
221        applications: snmp
222        authentication_method: usm
223        role_name: admin
224        authentication_protocol: md5
225        authentication_password: '12345678'
226        privacy_protocol: 'aes128'
227        privacy_password: '12345678'
228        engine_id: '7063514941000000000000'
229        remote_switch_ipaddress: 10.0.0.0
230        vserver: "{{ vserver }}"
231        hostname: "{{ hostname }}"
232        username: "{{ username }}"
233        password: "{{ password }}"
234
235    - name: Create user
236      netapp.ontap.na_ontap_user:
237        state: present
238        name: test123
239        application_dicts:
240          - application: http
241            authentication_methods: password
242          - application: ssh
243            authentication_methods: password,publickey
244        role_name: vsadmin
245        set_password: bobdole1234566
246        vserver: "{{ vserver }}"
247        hostname: "{{ hostname }}"
248        username: "{{ username }}"
249        password: "{{ password }}"
250"""
251
252RETURN = """
253
254"""
255import traceback
256
257from ansible.module_utils.basic import AnsibleModule
258from ansible.module_utils._text import to_native
259import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
260from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
261from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI
262
263HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
264
265
266class NetAppOntapUser():
267    """
268    Common operations to manage users and roles.
269    """
270
271    def __init__(self):
272        self.use_rest = False
273        self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
274        self.argument_spec.update(dict(
275            state=dict(type='str', choices=['present', 'absent'], default='present'),
276            name=dict(required=True, type='str'),
277
278            application_strs=dict(type='list', elements='str', aliases=['application', 'applications'],
279                                  choices=['console', 'http', 'ontapi', 'rsh', 'snmp',
280                                           'sp', 'service-processor', 'service_processor', 'ssh', 'telnet'],),
281            application_dicts=dict(type='list', elements='dict',
282                                   options=dict(
283                                       application=dict(required=True, type='str',
284                                                        choices=['console', 'http', 'ontapi', 'rsh', 'snmp',
285                                                                 'sp', 'service-processor', 'service_processor', 'ssh', 'telnet'],),
286                                       authentication_methods=dict(required=True, type='list', elements='str',
287                                                                   choices=['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert']),
288                                       second_authentication_method=dict(type='str', choices=['none', 'password', 'publickey', 'nsswitch']))),
289            authentication_method=dict(type='str',
290                                       choices=['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert']),
291            set_password=dict(type='str', no_log=True),
292            role_name=dict(type='str'),
293            lock_user=dict(type='bool'),
294            vserver=dict(required=True, type='str', aliases=['svm']),
295            authentication_protocol=dict(type='str', choices=['none', 'md5', 'sha', 'sha2-256']),
296            authentication_password=dict(type='str', no_log=True),
297            engine_id=dict(type='str'),
298            privacy_protocol=dict(type='str', choices=['none', 'des', 'aes128']),
299            privacy_password=dict(type='str', no_log=True),
300            remote_switch_ipaddress=dict(type='str'),
301            replace_existing_apps_and_methods=dict(type='str', choices=['always', 'auto'], default='auto')
302        ))
303
304        self.module = AnsibleModule(
305            argument_spec=self.argument_spec,
306            required_if=[
307                ('state', 'present', ['role_name']),
308                ('state', 'present', ['application_strs', 'application_dicts'], True)
309            ],
310            mutually_exclusive=[
311                ('application_strs', 'application_dicts')
312            ],
313            required_together=[
314                ('application_strs', 'authentication_method')
315            ],
316            supports_check_mode=True
317        )
318
319        self.na_helper = NetAppModule()
320        self.parameters = self.na_helper.set_parameters(self.module.params)
321        self.strs_to_dicts()
322
323        # REST API should be used for ONTAP 9.6 or higher
324        self.rest_api = OntapRestAPI(self.module)
325        # some attributes are not supported in earlier REST implementation
326        unsupported_rest_properties = ['authentication_password', 'authentication_protocol', 'engine_id',
327                                       'privacy_password', 'privacy_protocol']
328        used_unsupported_rest_properties = [x for x in unsupported_rest_properties if x in self.parameters]
329        self.use_rest, error = self.rest_api.is_rest(used_unsupported_rest_properties)
330        if error is not None:
331            self.module.fail_json(msg=error)
332        if not self.use_rest:
333            if not HAS_NETAPP_LIB:
334                self.module.fail_json(msg="the python NetApp-Lib module is required")
335            else:
336                if self.parameters['applications'] is None:
337                    self.module.fail_json(msg="application_dicts or application_strs is a required parameter with ZAPI")
338                self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver'])
339        elif self.parameters['applications']:
340            if any(application['application'] == 'snmp' for application in self.parameters['applications']):
341                self.module.fail_json(msg="snmp as application is not supported in REST.")
342            for application in self.parameters['applications']:
343                # REST prefers certificate to cert
344                application['authentication_methods'] = ['certificate' if x == 'cert' else x for x in application['authentication_methods']]
345                # REST get always returns 'second_authentication_method'
346                if 'second_authentication_method' not in application:
347                    application['second_authentication_method'] = None
348
349    def strs_to_dicts(self):
350        """transform applications list of strs to a list of dicts if application_strs in use"""
351        if 'application_dicts' in self.parameters:
352            self.parameters['applications'] = self.parameters['application_dicts']
353            self.parameters['replace_existing_apps_and_methods'] = 'always'
354        elif 'application_strs' in self.parameters:
355            # actual conversion
356            self.parameters['applications'] = [
357                dict(application=application,
358                     authentication_methods=[self.parameters['authentication_method']],
359                     second_authentication_method=None
360                     ) for application in self.parameters['application_strs']]
361        else:
362            self.parameters['applications'] = None
363
364    def get_user_rest(self):
365        api = 'security/accounts'
366        params = {
367            'name': self.parameters['name']
368        }
369        if self.parameters.get('vserver') is None:
370            # vserser is empty for cluster
371            params['scope'] = 'cluster'
372        else:
373            params['owner.name'] = self.parameters['vserver']
374
375        message, error = self.rest_api.get(api, params)
376        if error:
377            self.module.fail_json(msg='Error while fetching user info: %s' % error)
378        if message['num_records'] == 1:
379            return message['records'][0]['owner']['uuid'], message['records'][0]['name']
380        if message['num_records'] > 1:
381            self.module.fail_json(msg='Error while fetching user info, found multiple entries: %s' % repr(message))
382
383        return None
384
385    def get_user_details_rest(self, name, uuid):
386        params = {
387            'fields': 'role,applications,locked'
388        }
389        api = "security/accounts/%s/%s" % (uuid, name)
390        message, error = self.rest_api.get(api, params)
391        if error:
392            self.module.fail_json(msg='Error while fetching user details: %s' % error)
393        if message:
394            # replace "none" values with None for comparison
395            for application in message['applications']:
396                if application.get('second_authentication_method') == 'none':
397                    application['second_authentication_method'] = None
398            return_value = {
399                'role_name': message['role']['name'],
400                'applications': message['applications']
401            }
402            if "locked" in message:
403                return_value['lock_user'] = message['locked']
404        return return_value
405
406    def get_user(self):
407        """
408        Checks if the user exists.
409        :param: application: application to grant access to, a dict
410        :return:
411            Dictionary if user found
412            None if user is not found
413        """
414        desired_applications = [application['application'] for application in self.parameters['applications']]
415        desired_method = self.parameters.get('authentication_method')
416        security_login_get_iter = netapp_utils.zapi.NaElement('security-login-get-iter')
417        query_details = netapp_utils.zapi.NaElement.create_node_with_children(
418            'security-login-account-info', **{'vserver': self.parameters['vserver'],
419                                              'user-name': self.parameters['name']})
420
421        query = netapp_utils.zapi.NaElement('query')
422        query.add_child_elem(query_details)
423        security_login_get_iter.add_child_elem(query)
424        try:
425            result = self.server.invoke_successfully(security_login_get_iter,
426                                                     enable_tunneling=False)
427            if result.get_child_by_name('num-records') and \
428                    int(result.get_child_content('num-records')) >= 1:
429                applications = dict()
430                attr = result.get_child_by_name('attributes-list')
431                for info in attr.get_children():
432                    lock_user = self.na_helper.get_value_for_bool(True, info.get_child_content('is-locked'))
433                    role_name = info.get_child_content('role-name')
434                    application = info.get_child_content('application')
435                    auth_method = info.get_child_content('authentication-method')
436                    sec_method = info.get_child_content('second-authentication-method')
437                    if self.parameters['replace_existing_apps_and_methods'] == 'always' and application in applications:
438                        # with auto, only a single method is supported
439                        applications[application][0].append(auth_method)
440                        if sec_method != 'none':
441                            applications[application][1] = sec_method
442                    else:
443                        if self.parameters['replace_existing_apps_and_methods'] == 'always' or\
444                           (application in desired_applications and auth_method == desired_method):
445                            # with 'auto' we ignore existing apps that were not asked for
446                            applications[application] = ([auth_method], None)
447
448                apps = [dict(application=application, authentication_methods=methods, second_authentication_method=sec_method)
449                        for application, (methods, sec_method) in applications.items()]
450                return dict(
451                    lock_user=lock_user,
452                    role_name=role_name,
453                    applications=apps
454                )
455            return None
456        except netapp_utils.zapi.NaApiError as error:
457            # Error 16034 denotes a user not being found.
458            if to_native(error.code) == "16034":
459                return None
460            # Error 16043 denotes the user existing, but the application missing
461            elif to_native(error.code) == "16043":
462                return None
463            else:
464                self.module.fail_json(msg='Error getting user %s: %s' % (self.parameters['name'], to_native(error)),
465                                      exception=traceback.format_exc())
466
467    def create_user_rest(self, apps=None):
468        if apps is not None:
469            api = 'security/accounts'
470            params = {
471                'name': self.parameters['name'],
472                'role.name': self.parameters['role_name'],
473                'applications': self.na_helper.filter_out_none_entries(apps)
474            }
475            if self.parameters.get('vserver') is not None:
476                # vserser is empty for cluster
477                params['owner.name'] = self.parameters['vserver']
478            if 'set_password' in self.parameters:
479                params['password'] = self.parameters['set_password']
480            if 'lock_user' in self.parameters:
481                params['locked'] = self.parameters['lock_user']
482            dummy, error = self.rest_api.post(api, params)
483            error_sp = None
484            if error:
485                if 'invalid value' in error['message']:
486                    if 'service-processor' in error['message'] or 'service_processor' in error['message']:
487                        # find if there is error for service processor application value
488                        # update value as per ONTAP version support
489                        app_list_sp = params['applications']
490                        for app_item in app_list_sp:
491                            if app_item['application'] == 'service-processor':
492                                app_item['application'] = 'service_processor'
493                            elif app_item['application'] == 'service_processor':
494                                app_item['application'] = 'service-processor'
495                        params['applications'] = app_list_sp
496                        # post again and throw first error in case of an error
497                        dummy, error_sp = self.rest_api.post(api, params)
498                        if error_sp:
499                            self.module.fail_json(msg='Error while creating user: %s' % error)
500                        return True
501
502            # non-sp errors thrown
503            if error:
504                self.module.fail_json(msg='Error while creating user: %s' % error)
505
506    def create_user(self, application):
507        for index in range(len(application['authentication_methods'])):
508            self.create_user_with_auth(application, index)
509
510    def create_user_with_auth(self, application, index):
511        """
512        creates the user for the given application and authentication_method
513        application is now a directory
514        :param: application: application to grant access to
515        """
516        user_create = netapp_utils.zapi.NaElement.create_node_with_children(
517            'security-login-create', **{'vserver': self.parameters['vserver'],
518                                        'user-name': self.parameters['name'],
519                                        'application': application['application'],
520                                        'authentication-method': application['authentication_methods'][index],
521                                        'role-name': self.parameters.get('role_name')})
522        if application.get('second_authentication_method') is not None:
523            user_create.add_new_child('second-authentication-method', application['second_authentication_method'])
524        if self.parameters.get('set_password') is not None:
525            user_create.add_new_child('password', self.parameters.get('set_password'))
526        if application['authentication_methods'][0] == 'usm':
527            if self.parameters.get('remote_switch_ipaddress') is not None:
528                user_create.add_new_child('remote-switch-ipaddress', self.parameters.get('remote_switch_ipaddress'))
529            snmpv3_login_info = netapp_utils.zapi.NaElement('snmpv3-login-info')
530            if self.parameters.get('authentication_password') is not None:
531                snmpv3_login_info.add_new_child('authentication-password', self.parameters['authentication_password'])
532            if self.parameters.get('authentication_protocol') is not None:
533                snmpv3_login_info.add_new_child('authentication-protocol', self.parameters['authentication_protocol'])
534            if self.parameters.get('engine_id') is not None:
535                snmpv3_login_info.add_new_child('engine-id', self.parameters['engine_id'])
536            if self.parameters.get('privacy_password') is not None:
537                snmpv3_login_info.add_new_child('privacy-password', self.parameters['privacy_password'])
538            if self.parameters.get('privacy_protocol') is not None:
539                snmpv3_login_info.add_new_child('privacy-protocol', self.parameters['privacy_protocol'])
540            user_create.add_child_elem(snmpv3_login_info)
541
542        try:
543            self.server.invoke_successfully(user_create,
544                                            enable_tunneling=False)
545        except netapp_utils.zapi.NaApiError as error:
546            self.module.fail_json(msg='Error creating user %s: %s' % (self.parameters['name'], to_native(error)),
547                                  exception=traceback.format_exc())
548
549    def lock_unlock_user_rest(self, useruuid, username, value=None):
550        data = {
551            'locked': value
552        }
553        params = {
554            'name': self.parameters['name'],
555            'owner.uuid': useruuid,
556        }
557        api = "security/accounts/%s/%s" % (useruuid, username)
558        dummy, error = self.rest_api.patch(api, data, params)
559        if error:
560            self.module.fail_json(msg='Error while locking/unlocking user: %s' % error)
561
562    def lock_given_user(self):
563        """
564        locks the user
565
566        :return:
567            True if user locked
568            False if lock user is not performed
569        :rtype: bool
570        """
571        user_lock = netapp_utils.zapi.NaElement.create_node_with_children(
572            'security-login-lock', **{'vserver': self.parameters['vserver'],
573                                      'user-name': self.parameters['name']})
574
575        try:
576            self.server.invoke_successfully(user_lock,
577                                            enable_tunneling=False)
578        except netapp_utils.zapi.NaApiError as error:
579            self.module.fail_json(msg='Error locking user %s: %s' % (self.parameters['name'], to_native(error)),
580                                  exception=traceback.format_exc())
581
582    def unlock_given_user(self):
583        """
584        unlocks the user
585
586        :return:
587            True if user unlocked
588            False if unlock user is not performed
589        :rtype: bool
590        """
591        user_unlock = netapp_utils.zapi.NaElement.create_node_with_children(
592            'security-login-unlock', **{'vserver': self.parameters['vserver'],
593                                        'user-name': self.parameters['name']})
594
595        try:
596            self.server.invoke_successfully(user_unlock,
597                                            enable_tunneling=False)
598        except netapp_utils.zapi.NaApiError as error:
599            if to_native(error.code) == '13114':
600                return False
601            else:
602                self.module.fail_json(msg='Error unlocking user %s: %s' % (self.parameters['name'], to_native(error)),
603                                      exception=traceback.format_exc())
604        return True
605
606    def delete_user_rest(self):
607        uuid, username = self.get_user_rest()
608        api = "security/accounts/%s/%s" % (uuid, username)
609        dummy, error = self.rest_api.delete(api)
610        if error:
611            self.module.fail_json(msg='Error while deleting user : %s' % error)
612
613    def delete_user(self, application, methods_to_keep=None):
614        for index, method in enumerate(application['authentication_methods']):
615            if methods_to_keep is None or method not in methods_to_keep:
616                self.delete_user_with_auth(application, index)
617
618    def delete_user_with_auth(self, application, index):
619        """
620        deletes the user for the given application and authentication_method
621        application is now a dict
622        :param: application: application to grant access to
623        """
624        user_delete = netapp_utils.zapi.NaElement.create_node_with_children(
625            'security-login-delete', **{'vserver': self.parameters['vserver'],
626                                        'user-name': self.parameters['name'],
627                                        'application': application['application'],
628                                        'authentication-method': application['authentication_methods'][index]})
629
630        try:
631            self.server.invoke_successfully(user_delete,
632                                            enable_tunneling=False)
633        except netapp_utils.zapi.NaApiError as error:
634            self.module.fail_json(msg='Error removing user %s: %s - application: %s'
635                                  % (self.parameters['name'], to_native(error), application),
636                                  exception=traceback.format_exc())
637
638    @staticmethod
639    def is_repeated_password(message):
640        return message.startswith('New password must be different than last 6 passwords.') \
641            or message.startswith('New password must be different from last 6 passwords.') \
642            or message.startswith('New password must be different than the old password.') \
643            or message.startswith('New password must be different from the old password.')
644
645    def change_password_rest(self, useruuid, username):
646        data = {
647            'password': self.parameters['set_password'],
648        }
649        params = {
650            'name': self.parameters['name'],
651            'owner.uuid': useruuid,
652        }
653        api = "security/accounts/%s/%s" % (useruuid, username)
654        dummy, error = self.rest_api.patch(api, data, params)
655        if error:
656            if 'message' in error and self.is_repeated_password(error['message']):
657                # if the password is reused, assume idempotency
658                return False
659            else:
660                self.module.fail_json(msg='Error while updating user password: %s' % error)
661        return True
662
663    def change_password(self):
664        """
665        Changes the password
666
667        :return:
668            True if password updated
669            False if password is not updated
670        :rtype: bool
671        """
672        # self.server.set_vserver(self.parameters['vserver'])
673        modify_password = netapp_utils.zapi.NaElement.create_node_with_children(
674            'security-login-modify-password', **{
675                'new-password': str(self.parameters.get('set_password')),
676                'user-name': self.parameters['name']})
677        try:
678            self.server.invoke_successfully(modify_password,
679                                            enable_tunneling=True)
680        except netapp_utils.zapi.NaApiError as error:
681            if to_native(error.code) == '13114':
682                return False
683            # if the user give the same password, instead of returning an error, return ok
684            if to_native(error.code) == '13214' and self.is_repeated_password(error.message):
685                return False
686            self.module.fail_json(msg='Error setting password for user %s: %s' % (self.parameters['name'], to_native(error)),
687                                  exception=traceback.format_exc())
688
689        self.server.set_vserver(None)
690        return True
691
692    def modify_apps_rest(self, useruuid, username, apps=None):
693        data = {
694            'role.name': self.parameters['role_name'],
695            'applications': self.na_helper.filter_out_none_entries(apps)
696        }
697        params = {
698            'name': self.parameters['name'],
699            'owner.uuid': useruuid,
700        }
701        api = "security/accounts/%s/%s" % (useruuid, username)
702        dummy, error = self.rest_api.patch(api, data, params)
703        if error:
704            self.module.fail_json(msg='Error while modifying user details: %s' % error)
705
706    def modify_user(self, application, current_methods):
707        for index, method in enumerate(application['authentication_methods']):
708            if method in current_methods:
709                self.modify_user_with_auth(application, index)
710            else:
711                self.create_user_with_auth(application, index)
712
713    def modify_user_with_auth(self, application, index):
714        """
715        Modify user
716        application is now a dict
717        """
718        user_modify = netapp_utils.zapi.NaElement.create_node_with_children(
719            'security-login-modify', **{'vserver': self.parameters['vserver'],
720                                        'user-name': self.parameters['name'],
721                                        'application': application['application'],
722                                        'authentication-method': application['authentication_methods'][index],
723                                        'role-name': self.parameters.get('role_name')})
724
725        try:
726            self.server.invoke_successfully(user_modify,
727                                            enable_tunneling=False)
728        except netapp_utils.zapi.NaApiError as error:
729            self.module.fail_json(msg='Error modifying user %s: %s' % (self.parameters['name'], to_native(error)),
730                                  exception=traceback.format_exc())
731
732    def change_sp_application(self, current_apps):
733        """Adjust requested app name to match ONTAP convention"""
734        if not self.parameters['applications']:
735            return
736        app_list = [app['application'] for app in current_apps]
737        for application in self.parameters['applications']:
738            if application['application'] == 'service_processor' and 'service-processor' in app_list:
739                application['application'] = 'service-processor'
740            elif application['application'] == 'service-processor' and 'service_processor' in app_list:
741                application['application'] = 'service_processor'
742
743    def apply(self):
744        if self.use_rest:
745            current = self.get_user_rest()
746            if current is not None:
747                uuid, name = current
748                current = self.get_user_details_rest(name, uuid)
749                self.change_sp_application(current['applications'])
750        else:
751            netapp_utils.ems_log_event("na_ontap_user", self.server)
752            current = self.get_user()
753
754        cd_action = self.na_helper.get_cd_action(current, self.parameters)
755        modify_decision = self.na_helper.get_modified_attributes(current, self.parameters)
756
757        if self.use_rest and current and 'lock_user' not in current:
758            # REST does not return locked if password is not set
759            if cd_action is None and self.parameters.get('lock_user') is not None:
760                if self.parameters.get('set_password') is None:
761                    self.module.fail_json(msg='Error: cannot modify lock state if password is not set.')
762                modify_decision['lock_user'] = self.parameters['lock_user']
763                self.na_helper.changed = True
764
765        if self.na_helper.changed and not self.module.check_mode:
766            if cd_action == 'create':
767                if self.use_rest:
768                    self.create_user_rest(self.parameters['applications'])
769                else:
770                    for application in self.parameters['applications']:
771                        self.create_user(application)
772            elif cd_action == 'delete':
773                if self.use_rest:
774                    self.delete_user_rest()
775                else:
776                    for application in current['applications']:
777                        self.delete_user(application)
778            elif modify_decision:
779                if self.use_rest:
780                    if 'role_name' in modify_decision or 'applications' in modify_decision:
781                        self.modify_apps_rest(uuid, name, self.parameters['applications'])
782                else:
783                    if 'role_name' in modify_decision or 'applications' in modify_decision:
784                        if 'applications' not in modify_decision:
785                            # to change roles, we need at least one app
786                            modify_decision['applications'] = self.parameters['applications']
787                        current_apps = dict((application['application'], application['authentication_methods']) for application in current['applications'])
788                        for application in modify_decision['applications']:
789                            if application['application'] in current_apps:
790                                self.modify_user(application, current_apps[application['application']])
791                            else:
792                                self.create_user(application)
793                        desired_apps = dict((application['application'], application['authentication_methods'])
794                                            for application in self.parameters['applications'])
795                        for application in current['applications']:
796                            if application['application'] not in desired_apps:
797                                self.delete_user(application)
798                            else:
799                                self.delete_user(application, desired_apps[application['application']])
800
801        if cd_action is None and self.parameters.get('set_password') is not None:
802            # if check_mode, don't attempt to change the password, but assume it would be changed
803            if self.use_rest:
804                self.na_helper.changed = self.module.check_mode or self.change_password_rest(uuid, name)
805            else:
806                self.na_helper.changed = self.module.check_mode or self.change_password()
807
808        if cd_action is None and self.na_helper.changed and not self.module.check_mode:
809            # lock/unlock actions require password to be set
810            if modify_decision and 'lock_user' in modify_decision:
811                if self.use_rest:
812                    self.lock_unlock_user_rest(uuid, name, self.parameters['lock_user'])
813                else:
814                    if self.parameters.get('lock_user'):
815                        self.lock_given_user()
816                    else:
817                        self.unlock_given_user()
818
819        self.module.exit_json(changed=self.na_helper.changed, current=current, modify=modify_decision)
820
821
822def main():
823    obj = NetAppOntapUser()
824    obj.apply()
825
826
827if __name__ == '__main__':
828    main()
829