Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new azure active directory related modules #179

Merged
merged 30 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions plugins/module_utils/azure_rm_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ class AzureRMModuleBase(object):
def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
check_invalid_arguments=None, mutually_exclusive=None, required_together=None,
required_one_of=None, add_file_common_args=False, supports_check_mode=False,
required_if=None, supports_tags=True, facts_module=False, skip_exec=False):
required_if=None, supports_tags=True, facts_module=False, skip_exec=False, is_ad_resource=False):

merged_arg_spec = dict()
merged_arg_spec.update(AZURE_COMMON_ARGS)
Expand Down Expand Up @@ -418,7 +418,7 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
# self.debug = self.module.params.get('debug')

# delegate auth to AzureRMAuth class (shared with all plugin types)
self.azure_auth = AzureRMAuth(fail_impl=self.fail, **self.module.params)
self.azure_auth = AzureRMAuth(fail_impl=self.fail, is_ad_resource=is_ad_resource, **self.module.params)

# common parameter validation
if self.module.params.get('tags'):
Expand Down Expand Up @@ -827,6 +827,14 @@ def get_api_profile(self, client_type_name, api_profile_name):
# wrap basic strings in a dict that just defines the default
return dict(default_api_version=profile_raw)

def get_graphrbac_client(self, tenant_id):
from azure.graphrbac import GraphRbacManagementClient
cred = self.azure_auth.azure_credentials
base_url = self.azure_auth._cloud_environment.endpoints.active_directory_graph_resource_id
client = GraphRbacManagementClient(cred, tenant_id, base_url)

return client

def get_mgmt_svc_client(self, client_type, base_url=None, api_version=None):
self.log('Getting management service client {0}'.format(client_type.__name__))
self.check_client_version(client_type)
Expand Down Expand Up @@ -1225,7 +1233,7 @@ class AzureRMAuthException(Exception):
class AzureRMAuth(object):
def __init__(self, auth_source='auto', profile=None, subscription_id=None, client_id=None, secret=None,
tenant=None, ad_user=None, password=None, cloud_environment='AzureCloud', cert_validation_mode='validate',
api_profile='latest', adfs_authority_url=None, fail_impl=None, **kwargs):
api_profile='latest', adfs_authority_url=None, fail_impl=None, is_ad_resource=False, **kwargs):

if fail_impl:
self._fail_impl = fail_impl
Expand All @@ -1234,6 +1242,7 @@ def __init__(self, auth_source='auto', profile=None, subscription_id=None, clien

self._cloud_environment = None
self._adfs_authority_url = None
self.is_ad_resource = is_ad_resource

# authenticate
self.credentials = self._get_credentials(
Expand Down Expand Up @@ -1379,8 +1388,10 @@ def _get_msi_credentials(self, subscription_id_param=None, **kwargs):
'subscription_id': subscription_id
}

def _get_azure_cli_credentials(self):
credentials, subscription_id = get_azure_cli_credentials()
def _get_azure_cli_credentials(self, resource=None):
if self.is_ad_resource:
resource = 'https://graph.windows.net/'
credentials, subscription_id = get_azure_cli_credentials(resource)
cloud_environment = get_cli_active_cloud()

cli_credentials = {
Expand Down
289 changes: 289 additions & 0 deletions plugins/modules/azure_ad_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
#!/usr/bin/python
#
# Copyright (c) 2020 Haiyuan Zhang, <haiyzhan@microsoft.com>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
import datetime
try:
from dateutil.relativedelta import relativedelta
except ImportError:
pass

__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}

DOCUMENTATION = '''
---
module: azure_ad_password

version_added: "2.10"

short_description: Manage application password

description:
- Manage application password.

options:
app_id:
description:
- The application ID.
type: str
service_principal_id:
description:
- The service principal ID.
type: str
key_id:
description:
- Password key ID.
type: str
tenant:
description:
- The tenant ID.
type: str
required: True
end_date:
description:
- Date or datemtime after which credentials expire.
- Default value is one year after current time.
type: str
value:
description:
- Application password value.
- Length greater than 18 characters.
type: str
state:
description:
- Assert the state of Active Dirctory Password.
- Use C(present) to create or update a Password and use C(absent) to delete.
- Update is not supported, if I(state=absent) and I(key_id=None), then all passwords of the application will be deleted.
default: present
choices:
- absent
- present
type: str

extends_documentation_fragment:
- azure.azcollection.azure
- azure.azcollection.azure_tags

author:
haiyuan_zhang (@haiyuazhang)
Fred-sun (@Fred-sun)

'''

EXAMPLES = '''
- name: create ad password
azure_ad_password:
app_id: "{{ app_id }}"
state: present
value: "$abc12345678"
tenant: "{{ tenant_id }}"
'''

RETURN = '''
end_date:
description:
- Date or datemtime after which credentials expire.
- Default value is one year after current time.
type: str
returned: always
sample: 2021-06-28T06:00:32.637070+00:00
key_id:
description:
- Password key ID
type: str
returned: always
sample: 512f259c-c397-4ec6-8598-4f940d411970
start_date:
description:
- Date or datetime at which credentials become valid.
- Default value is current time.
type: str
returned: always
sample: 2020-06-28T06:00:32.637070+00:00

'''

from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase

try:
from msrestazure.azure_exceptions import CloudError
from azure.graphrbac.models import GraphErrorException
from azure.graphrbac.models import PasswordCredential
from azure.graphrbac.models import ApplicationUpdateParameters
except ImportError:
# This is handled in azure_rm_common
pass


class AzureADPassword(AzureRMModuleBase):
def __init__(self):

self.module_arg_spec = dict(
app_id=dict(type='str'),
service_principal_id=dict(type='str'),
key_id=dict(type='str'),
tenant=dict(type='str', required=True),
value=dict(type='str'),
end_date=dict(type='str'),
state=dict(type='str', default='present', choices=['present', 'absent']),
)

self.state = None
self.tenant = None
self.app_id = None
self.service_principal_id = None
self.app_object_id = None
self.key_id = None
self.value = None
self.end_date = None
self.results = dict(changed=False)

self.client = None

super(AzureADPassword, self).__init__(derived_arg_spec=self.module_arg_spec,
supports_check_mode=False,
supports_tags=False,
is_ad_resource=True)

def exec_module(self, **kwargs):
for key in list(self.module_arg_spec.keys()):
setattr(self, key, kwargs[key])

self.client = self.get_graphrbac_client(self.tenant)
self.resolve_app_obj_id()
passwords = self.get_all_passwords()

if self.state == 'present':
if self.key_id and self.key_exists(passwords):
self.fail("It can't update existing password")
else:
self.create_password(passwords)
else:
if self.key_id is None:
self.delete_all_passwords(passwords)
else:
self.delete_password(passwords)

return self.results

def key_exists(self, old_passwords):
for pd in old_passwords:
if pd.key_id == self.key_id:
return True
return False

def resolve_app_obj_id(self):
try:
if self.app_object_id is not None:
return
elif self.app_id or self.service_principal_object_id:
if not self.app_id:
sp = self.client.service_principals.get(self.service_principal_id)
self.app_id = sp.app_id
if not self.app_id:
self.fail("can't resolve app via service principal object id {0}".format(self.service_principal_object_id))

result = list(self.client.applications.list(filter="appId eq {0}".format(self.app_id)))
if result:
self.app_object_id = result[0].object_id
else:
self.fail("can't resolve app via app id {0}".format(self.app_id))
else:
self.fail("one of the [app_id, app_object_id, service_principal_id] must be set")

except GraphErrorException as ge:
self.fail("error in resolve app_object_id {0}".format(str(ge)))

def get_all_passwords(self):

try:
return list(self.client.applications.list_password_credentials(self.app_object_id))
except GraphErrorException as ge:
self.fail("failed to fetch passwords for app {0}: {1}".format(self.app_object_id, str(ge)))

def delete_all_passwords(self, old_passwords):

if len(old_passwords) == 0:
self.results['changed'] = False
return
try:
self.client.applications.patch(self.app_object_id, ApplicationUpdateParameters(password_credentials=[]))
self.results['changed'] = True
except GraphErrorException as ge:
self.fail("fail to purge all passwords for app: {0} - {1}".format(self.app_object_id, str(ge)))

def delete_password(self, old_passwords):
if not self.key_exists(old_passwords):
self.results['changed'] = False
return

num_of_passwords_before_delete = len(old_passwords)

for pd in old_passwords:
if pd.key_id == self.key_id:
old_passwords.remove(pd)
break
try:
self.client.applications.patch(self.app_object_id, ApplicationUpdateParameters(password_credentials=old_passwords))
num_of_passwords_after_delete = len(self.get_all_passwords())
if num_of_passwords_after_delete != num_of_passwords_before_delete:
self.results['changed'] = True
self.results['num_of_passwords_before_delete'] = num_of_passwords_before_delete
self.results['num_of_passwords_after_delete'] = num_of_passwords_after_delete

except GraphErrorException as ge:
self.fail("failed to delete password with key id {0} - {1}".format(self.app_id, str(ge)))

def create_password(self, old_passwords):
def gen_guid():
import uuid
return uuid.uuid4()

if self.value is None:
self.fail("when creating a new password, module parameter value can't be None")

start_date = datetime.datetime.now(datetime.timezone.utc)
end_date = self.end_date or start_date + relativedelta(years=1)
value = self.value
key_id = self.key_id or str(gen_guid())

new_password = PasswordCredential(start_date=start_date, end_date=end_date, key_id=key_id,
value=value, custom_key_identifier=None)
old_passwords.append(new_password)

try:
client = self.get_graphrbac_client(self.tenant)
app_patch_parameters = ApplicationUpdateParameters(password_credentials=old_passwords)
client.applications.patch(self.app_object_id, app_patch_parameters)

new_passwords = self.get_all_passwords()
for pd in new_passwords:
if pd.key_id == key_id:
self.results['changed'] = True
self.results.update(self.to_dict(pd))
except GraphErrorException as ge:
self.fail("failed to create new password: {0}".format(str(ge)))

@staticmethod
def to_dict(pd):
return dict(
end_date=pd.end_date,
start_date=pd.start_date,
key_id=pd.key_id
)


def main():
AzureADPassword()


if __name__ == '__main__':
main()
Loading