diff --git a/ibmsecurity/isam/aac/api_protection/clients.py b/ibmsecurity/isam/aac/api_protection/clients.py index 087e411d..c1df817e 100644 --- a/ibmsecurity/isam/aac/api_protection/clients.py +++ b/ibmsecurity/isam/aac/api_protection/clients.py @@ -349,7 +349,6 @@ def set(isamAppliance, name, definitionName, companyName, redirectUri=None, comp requirePkce=requirePkce, encryptionDb=encryptionDb, encryptionCert=encryptionCert, jwksUri=jwksUri, extProperties=extProperties, check_mode=check_mode, force=force) - def compare(isamAppliance1, isamAppliance2): """ Compare API Protection Definitions between two appliances diff --git a/ibmsecurity/isam/aac/authentication/mechanisms.py b/ibmsecurity/isam/aac/authentication/mechanisms.py index eaa1b74a..d5961055 100644 --- a/ibmsecurity/isam/aac/authentication/mechanisms.py +++ b/ibmsecurity/isam/aac/authentication/mechanisms.py @@ -1,6 +1,9 @@ import logging from ibmsecurity.utilities import tools from ibmsecurity.isam.aac.authentication import mechanism_types +from ibmsecurity.isam.aac.server_connections import smtp +from ibmsecurity.isam.aac.server_connections import ci +from ibmsecurity.isam.aac.server_connections import ws logger = logging.getLogger(__name__) @@ -99,6 +102,20 @@ def add(isamAppliance, name, uri, description="", attributes=None, properties=No if attributes is not None: json_data['attributes'] = attributes if properties is not None: + logger.info("Searching for keys to substitute value with uuids") + id = {} + for property in properties: + if property['key'] == "EmailMessage.serverConnection": + id = smtp._get_id(isamAppliance, property['value'])['data'] + logger.info("Found EmailMessage.serverConnection by name[{}] with uuid[{}]".format(property['value'], id)) + elif property['key'] == "ScimConfig.serverConnection": + id = ws.search(isamAppliance, property['value'])['data'] + logger.info("Found ScimConfig.serverConnection by name[{}] with uuid[{}]".format(property['value'], id)) + elif property['key'] == "CI.serverConnection": + id = ci._get_id(isamAppliance, property['value'])['data'] + logger.info("Found CI.serverConnection by name[{}] with uuid[{}]".format(property['value'], id)) + if id != {}: + property['value'] = id json_data['properties'] = properties return isamAppliance.invoke_post( "Create a new federation", module_uri, json_data, @@ -197,6 +214,20 @@ def _check(isamAppliance, name, description, attributes, properties, predefined, except: pass if properties is not None: + logger.info("Searching for keys to substitute value with uuids") + id = {} + for property in properties: + if property['key'] == "EmailMessage.serverConnection": + id = smtp._get_id(isamAppliance, property['value'])['data'] + logger.info("Found EmailMessage.serverConnection by name[{}] with uuid[{}]".format(property['value'], id)) + elif property['key'] == "ScimConfig.serverConnection": + id = ws.search(isamAppliance, property['value'])['data'] + logger.info("Found ScimConfig.serverConnection by name[{}] with uuid[{}]".format(property['value'], id)) + elif property['key'] == "CI.serverConnection": + id = ci._get_id(isamAppliance, property['value'])['data'] + logger.info("Found CI.serverConnection by name[{}] with uuid[{}]".format(property['value'], id)) + if id != {}: + property['value'] = id json_data['properties'] = properties else: # May not exist so skip any exceptions when deleting diff --git a/ibmsecurity/isam/aac/mapping_rules.py b/ibmsecurity/isam/aac/mapping_rules.py index 09280578..89eea7f8 100644 --- a/ibmsecurity/isam/aac/mapping_rules.py +++ b/ibmsecurity/isam/aac/mapping_rules.py @@ -78,9 +78,9 @@ def set(isamAppliance, name, category, filename=None, content=None, upload_filen else: filename = _extract_filename(upload_filename) if _check(isamAppliance, name=name) is False: - # Force the add - we already know connection does not exist + # need to check for duplicate filename constraint violations on add return add(isamAppliance, name=name, filename=filename, content=content, category=category, - check_mode=check_mode, force=True) + check_mode=check_mode, force=force) else: # Update request return update(isamAppliance, name=name, content=content, check_mode=check_mode, force=force) @@ -90,7 +90,7 @@ def add(isamAppliance, name, filename=None, content=None, category="OAUTH", chec """ Add a mapping rule """ - if force is True or _check(isamAppliance, name) is False: + if force is True or _check(isamAppliance, name=name, filename=filename) is False: if check_mode is True: return isamAppliance.create_return_object(changed=True) else: @@ -106,7 +106,7 @@ def add(isamAppliance, name, filename=None, content=None, category="OAUTH", chec "category": category }) - return isamAppliance.create_return_object() + return isamAppliance.create_return_object(rc=1, warnings=["mapping rule {} could not be added.".format(name)]) def delete(isamAppliance, name, check_mode=False, force=False): @@ -245,15 +245,18 @@ def import_file(isamAppliance, name, filename, check_mode=False, force=False): return isamAppliance.create_return_object() -def _check(isamAppliance, name): +def _check(isamAppliance, name, filename=None): """ - Check if Mapping Rules already exists + Check if Mapping Rules already exists based on the name and filename """ ret_obj = get_all(isamAppliance) for obj in ret_obj['data']: if obj['name'] == name: return True + if filename is not None and obj['fileName'] == filename: + logger.warning("Found mapping rule [{}] with same filename[{}]. This filename violates duplicate key value unique constraint. Choose another filename for [{}] or delete corresponding mapping rule [{}] to solve this".format(obj['name'],filename,filename,obj['name'])) + return True return False diff --git a/ibmsecurity/isam/aac/runtime_template/directory.py b/ibmsecurity/isam/aac/runtime_template/directory.py index 333d79c7..2a051ce1 100755 --- a/ibmsecurity/isam/aac/runtime_template/directory.py +++ b/ibmsecurity/isam/aac/runtime_template/directory.py @@ -97,6 +97,40 @@ def create(isamAppliance, path, name, check_mode=False, force=False): return isamAppliance.create_return_object(warnings=warnings) +def create(isamAppliance, id, check_mode=False, force=False): + """ + Creating a directory in the runtime template files directory + + :param isamAppliance: + :param id: + :param name: + :param check_mode: + :param force: + :return: + """ + warnings = [] + + path = os.path.dirname(id) + name = os.path.basename(id) + + check_dir = _check(isamAppliance, id) + if check_dir != None: + warnings.append("Directory {0} exists. Ignoring create.".format(id)) + + if force is True or check_dir == None: + if check_mode is True: + return isamAppliance.create_return_object(changed=True, warnings=warnings) + else: + return isamAppliance.invoke_post( + "Creating a directory in the runtime template files directory", + "/mga/template_files/{0}".format(path), + { + 'dir_name': name, + 'type': 'dir' + }) + + return isamAppliance.create_return_object(warnings=warnings) + def delete(isamAppliance, id, check_mode=False, force=False): """ diff --git a/ibmsecurity/isam/aac/runtime_template/root.py b/ibmsecurity/isam/aac/runtime_template/root.py index c4d57fd7..70e8c7fa 100644 --- a/ibmsecurity/isam/aac/runtime_template/root.py +++ b/ibmsecurity/isam/aac/runtime_template/root.py @@ -1,8 +1,13 @@ import logging import os.path +import ibmsecurity.utilities.tools +import zipfile +import difflib +import hashlib +import shutil from ibmsecurity.isam.aac.runtime_template import directory from ibmsecurity.isam.aac.runtime_template import file - +from ibmsecurity.utilities.tools import get_random_temp_dir, files_same_zip_content logger = logging.getLogger(__name__) uri = "/mga/template_files" @@ -28,42 +33,108 @@ def export_file(isamAppliance, filename, check_mode=False, force=False): return isamAppliance.create_return_object() -def import_file(isamAppliance, filename, check_mode=False, force=False): +def import_file(isamAppliance, filename, delete_missing=False, check_mode=False, force=False): """ - Replace all Runtime Template Files + Import all Runtime Template Files """ - if check_mode is True: - return isamAppliance.create_return_object(changed=True) - else: - return isamAppliance.invoke_post_files( - "Replace all Runtime Template Files", - uri, - [ + warnings = [] + + if force is True or _check_import(isamAppliance, filename): + if delete_missing is True: + tempdir = get_random_temp_dir() + tempfilename = "template_files.zip" + tempfile = os.path.join(tempdir, tempfilename) + export_file(isamAppliance, tempfile) + + zServerFile = zipfile.ZipFile(tempfile) + zClientFile = zipfile.ZipFile(filename) + + files_on_server = []; + for info in zServerFile.infolist(): + files_on_server.append(info.filename) + files_on_client = []; + for info in zClientFile.infolist(): + files_on_client.append(info.filename) + missing_client_files = [x for x in files_on_server if x not in files_on_client] + + if missing_client_files != []: + logger.info("list all missing files in {}, which will be deleted on the server: {}.".format(filename, missing_client_files)) + + for x in missing_client_files: + if x.endswith('/'): + search_dir= os.path.dirname(x[:-1]) + '/' + if search_dir not in missing_client_files: + logger.debug("delete directory on the server: {0}.".format(x)) + delete(isamAppliance, x, "directory", check_mode=check_mode) + else: + search_dir= os.path.dirname(x) + '/' + if search_dir not in missing_client_files: + logger.debug("delete file on the server: {0}.".format(x)) + delete(isamAppliance, x, "file", check_mode=check_mode) + shutil.rmtree(tempdir) + + if check_mode is True: + return isamAppliance.create_return_object(changed=True) + else: + return isamAppliance.invoke_post_files( + "Replace all Runtime Template Files", + uri, + [ + { + 'file_formfield': 'file', + 'filename': filename, + 'mimetype': 'application/octet-stream' + } + ], { - 'file_formfield': 'file', - 'filename': filename, - 'mimetype': 'application/octet-stream' - } - ], - { - "force": force - }, json_response=False, requires_modules=requires_modules, - requires_version=requires_version) + "force": force + }, json_response=False) + + return isamAppliance.create_return_object(warnings=warnings) + + +def _check_import(isamAppliance, filename): + """ + Checks if runtime template zip from server and client differ + :param isamAppliance: + :param filename: + :return: + """ + + tempdir = get_random_temp_dir() + tempfilename = "template_files.zip" + tempfile = os.path.join(tempdir, tempfilename) + export_file(isamAppliance, tempfile) + + if os.path.exists(tempfile): + identical = files_same_zip_content(filename,tempfile) + + shutil.rmtree(tempdir) + if identical: + logger.info("runtime template files {} are identical with the server content. No update necessary.".format(filename)) + return False + else: + logger.info("runtime template files {} differ from the server content. Updating runtime template files necessary.".format(filename)) + return True + else: + logger.info("missing zip file from server. Comparison skipped.") + return False + def check(isamAppliance, id, type, check_mode=False, force=False): ret_obj = None + name = os.path.basename(id) + path = os.path.dirname(id) + if (type.lower() == 'directory'): ret_obj = directory._check(isamAppliance, id) elif (type.lower() == 'file'): - ret_obj = file._check(isamAppliance, id) + ret_obj = file._check(isamAppliance, path, name) else: type = 'unknown' - name = os.path.basename(id) - path = os.path.dirname(id) - data = { 'id': ret_obj, 'path': path, @@ -85,7 +156,11 @@ def delete(isamAppliance, id, type, check_mode=False, force=False): :param force: :return: """ - if (type.lower() == 'directory'): - return directory.delete(isamAppliance, id) - elif (type.lower() == 'file'): - return file.delete(isamAppliance, id) + + name = os.path.basename(id) + path = os.path.dirname(id) + + if(type.lower() == 'directory'): + return directory.delete(isamAppliance, id, check_mode, force) + elif(type.lower() == 'file'): + return file.delete(isamAppliance, path, name, check_mode, force) \ No newline at end of file diff --git a/ibmsecurity/isam/aac/scim.py b/ibmsecurity/isam/aac/scim.py index ed3b510e..be1b68ea 100755 --- a/ibmsecurity/isam/aac/scim.py +++ b/ibmsecurity/isam/aac/scim.py @@ -1,4 +1,5 @@ import logging +from ibmsecurity.utilities import tools logger = logging.getLogger(__name__) @@ -63,3 +64,36 @@ def update_isam_user(isamAppliance, isam_domain, update_native_users, ldap_conne "Update SCIM ISAM user settings", "/mga/scim/configuration/urn:ietf:params:scim:schemas:extension:isam:1.0:User", ret_obj) + +def set_all(isamAppliance, scim_configuration, check_mode=False, force=False): + """ + Update entire SCIM settings + """ + if scim_configuration is None or scim_configuration == '': + return isamAppliance.create_return_object( + warnings="Need to pass content for scim configuration") + else: + if force is True or _check(isamAppliance, scim_configuration) is False : + if check_mode is True: + return isamAppliance.create_return_object(changed=True) + else: + return isamAppliance.invoke_put( + "Update SCIM settings", + "/mga/scim/configuration", + scim_configuration ) + + return isamAppliance.create_return_object() + +def _check(isamAppliance, scim_configuration): + """ + Check if scim configuration is identical with server + """ + ret_obj = get_all(isamAppliance) + logger.debug("Comparing server scim configuration with desired configuration.") + logger.debug("Server JSON: {0}".format(tools.json_sort(ret_obj['data']))) + logger.debug("Desired JSON: {0}".format(tools.json_sort(scim_configuration))) + if tools.json_sort(scim_configuration) != tools.json_sort(ret_obj['data']): + return False + + logger.debug("Server configuration is identical with desired configuration. No change necessary.") + return True \ No newline at end of file diff --git a/ibmsecurity/isam/aac/server_connections/ci.py b/ibmsecurity/isam/aac/server_connections/ci.py index 14f36efa..5a09061a 100644 --- a/ibmsecurity/isam/aac/server_connections/ci.py +++ b/ibmsecurity/isam/aac/server_connections/ci.py @@ -1,9 +1,9 @@ import logging -import ibmsecurity.utilities.tools +from ibmsecurity.utilities import tools logger = logging.getLogger(__name__) -requires_modules = ["mga"] +requires_modules = ["mga", "federation"] requires_version = "9.0.5.0" @@ -37,12 +37,10 @@ def set(isamAppliance, name, connection, description='', locked=False, new_name= """ if _check_exists(isamAppliance, name=name) is False: # Force the add - we already know connection does not exist - return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, check_mode=check_mode, force=True) + return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, check_mode=check_mode, force=True) else: # Update request - return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, new_name=new_name, check_mode=check_mode, force=force) + return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, new_name=new_name, check_mode=check_mode, force=force) def add(isamAppliance, name, connection, description='', locked=False, check_mode=False, force=False): @@ -62,17 +60,18 @@ def add(isamAppliance, name, connection, description='', locked=False, check_mod return isamAppliance.create_return_object() - -def delete(isamAppliance, name, check_mode=False, force=False): +def delete(isamAppliance, name=None, id=None, check_mode=False, force=False): """ Deleting a CI server connection """ - if force is True or _check_exists(isamAppliance, name=name) is True: + if force is True or _check_exists(isamAppliance, name=name, id=id) is True: if check_mode is True: return isamAppliance.create_return_object(changed=True) else: - ret_obj = search(isamAppliance, name=name) - id = ret_obj['data'] + if id is None: + ret_obj = search(isamAppliance, name=name) + id = ret_obj['data'] + return isamAppliance.invoke_delete( "Deleting a CI server connection", "/mga/server_connections/ci/{0}/v1".format(id), requires_modules=requires_modules, @@ -84,25 +83,48 @@ def delete(isamAppliance, name, check_mode=False, force=False): def update(isamAppliance, name, connection, description='', locked=False, new_name=None, check_mode=False, force=False): """ Modifying a CI server connection - Use new_name to rename the connection, cannot compare password so update will take place everytime + Use new_name to rename the connection. """ + ret_obj = get(isamAppliance, name) + warnings = ret_obj["warnings"] + + if ret_obj["data"] == {}: + warnings.append("CI Service connection {0} not found, skipping update.".format(name)) + return isamAppliance.create_return_object(warnings=warnings) + else: + id = ret_obj["data"]["uuid"] + + needs_update = False + + json_data = _create_json(name=name, description=description, locked=locked, connection=connection) + if new_name is not None: # Rename condition + json_data['name'] = new_name - if force is True or _check_exists(isamAppliance, name): + if force is not True: + if 'uuid' in ret_obj['data']: + del ret_obj['data']['uuid'] + if 'clientSecret' in connection: + warnings.append("Since existing clientSecret cannot be read for ci connections - this parameter will be ignored for idempotency. Add 'force' parameter to update the connection with a new clientSecret.") + connection.pop('clientSecret', None) + + sorted_ret_obj = tools.json_sort(ret_obj['data']) + sorted_json_data = tools.json_sort(json_data) + logger.debug("Sorted Existing Data:{0}".format(sorted_ret_obj)) + logger.debug("Sorted Desired Data:{0}".format(sorted_json_data)) + + if sorted_ret_obj != sorted_json_data: + needs_update = True + + if force is True or needs_update is True: if check_mode is True: - return isamAppliance.create_return_object(changed=True) + return isamAppliance.create_return_object(changed=True, warnings=warnings) else: - json_data = _create_json(name=name, description=description, locked=locked, connection=connection) - if new_name is not None: # Rename condition - json_data['name'] = new_name - ret_obj = search(isamAppliance, name=name) - id = ret_obj['data'] return isamAppliance.invoke_put( "Modifying a CI server connection", "/mga/server_connections/ci/{0}/v1".format(id), json_data, requires_modules=requires_modules, requires_version=requires_version) - return isamAppliance.create_return_object() - + return isamAppliance.create_return_object(warnings=warnings) def _create_json(name, description, locked, connection): """ diff --git a/ibmsecurity/isam/aac/server_connections/jdbc.py b/ibmsecurity/isam/aac/server_connections/jdbc.py index 372aeebe..6c45d460 100644 --- a/ibmsecurity/isam/aac/server_connections/jdbc.py +++ b/ibmsecurity/isam/aac/server_connections/jdbc.py @@ -1,7 +1,9 @@ import logging -import ibmsecurity.utilities.tools +from ibmsecurity.utilities import tools logger = logging.getLogger(__name__) +requires_modules = ["mga", "federation"] +requires_version = "9.0.2.1" # Will change if introduced in an earlier version. def get_all(isamAppliance, check_mode=False, force=False): @@ -26,24 +28,20 @@ def get(isamAppliance, name, check_mode=False, force=False): "/mga/server_connections/jdbc/{0}/v1".format(id)) -def set(isamAppliance, name, connection, type, jndiId, description='', locked=False, connectionManager=None, +def set(isamAppliance, name, connection, jndiId, description='', locked=False, connectionManager=None, new_name=None, check_mode=False, force=False): """ Creating or Modifying an JDBC server connection """ if _check_exists(isamAppliance, name=name) is False: # Force the add - we already know connection does not exist - return add(isamAppliance, name=name, connection=connection, type=type, jndiId=jndiId, description=description, - locked=locked, connectionManager=connectionManager, check_mode=check_mode, - force=True) + return add(isamAppliance=isamAppliance, name=name, connection=connection, jndiId=jndiId, description=description, locked=locked, connectionManager=connectionManager, check_mode=check_mode, force=True) else: # Update request - return update(isamAppliance=isamAppliance, name=name, connection=connection, type=type, jndiId=jndiId, - description=description, locked=locked, connectionManager=connectionManager, new_name=new_name, - check_mode=check_mode, force=force) + return update(isamAppliance=isamAppliance, name=name, connection=connection, jndiId=jndiId, description=description, locked=locked, connectionManager=connectionManager, new_name=new_name, check_mode=check_mode, force=force) -def add(isamAppliance, name, connection, type, jndiId, description='', locked=False, connectionManager=None, +def add(isamAppliance, name, connection, jndiId, description='', locked=False, connectionManager=None, check_mode=False, force=False): """ Creating a JDBC server connection @@ -55,7 +53,7 @@ def add(isamAppliance, name, connection, type, jndiId, description='', locked=Fa return isamAppliance.invoke_post( "Creating a JDBC server connection", "/mga/server_connections/jdbc/v1", - _create_json(name=name, description=description, locked=locked, type=type, connection=connection, + _create_json(name=name, description=description, locked=locked, connection=connection, connectionManager=connectionManager, jndiId=jndiId)) return isamAppliance.create_return_object() @@ -78,39 +76,62 @@ def delete(isamAppliance, name, check_mode=False, force=False): return isamAppliance.create_return_object() -def update(isamAppliance, name, connection, type, jndiId, description='', locked=False, connectionManager=None, +def update(isamAppliance, name, connection, jndiId, description='', locked=False, connectionManager=None, new_name=None, check_mode=False, force=False): """ Modifying a JDBC server connection - Use new_name to rename the connection, cannot compare password so update will take place everytime + Use new_name to rename the connection. """ + ret_obj = get(isamAppliance, name) + warnings = ret_obj["warnings"] - if force is True or _check_exists(isamAppliance, name): - if check_mode is True: - return isamAppliance.create_return_object(changed=True) - else: - json_data = _create_json(name=name, description=description, locked=locked, type=type, - connection=connection, connectionManager=connectionManager, jndiId=jndiId) - ret_obj = search(isamAppliance, name) - id = ret_obj['data'] + if ret_obj["data"] == {}: + warnings.append("JDBC Service connection {0} not found, skipping update.".format(name)) + return isamAppliance.create_return_object(warnings=warnings) + else: + id = ret_obj["data"]["uuid"] - if new_name is not None: # Rename condition - json_data['name'] = new_name + needs_update = False - return isamAppliance.invoke_put("Modifying a JDBC server connection", - "/mga/server_connections/jdbc/{0}/v1".format(id), json_data) + json_data = _create_json(name=name, description=description, locked=locked, connection=connection, connectionManager=connectionManager, jndiId=jndiId) + if new_name is not None: # Rename condition + json_data['name'] = new_name - return isamAppliance.create_return_object() + if force is not True: + if 'uuid' in ret_obj['data']: + del ret_obj['data']['uuid'] + if 'password' in connection: + warnings.append("Since existing password cannot be read for jdbc connections - this parameter will be ignored for idempotency. Add 'force' parameter to update the connection with a new password.") + connection.pop('password', None) + + sorted_ret_obj = tools.json_sort(ret_obj['data']) + sorted_json_data = tools.json_sort(json_data) + logger.debug("Sorted Existing Data:{0}".format(sorted_ret_obj)) + logger.debug("Sorted Desired Data:{0}".format(sorted_json_data)) + + if sorted_ret_obj != sorted_json_data: + needs_update = True + + if force is True or needs_update is True: + if check_mode is True: + return isamAppliance.create_return_object(changed=True, warnings=warnings) + else: + return isamAppliance.invoke_put( + "Modifying a JDBC server connection", + "/mga/server_connections/jdbc/{0}/v1".format(id), json_data, requires_modules=requires_modules, + requires_version=requires_version, warnings=warnings) + + return isamAppliance.create_return_object(warnings=warnings) -def _create_json(name, description, locked, type, jndiId, connection, connectionManager): +def _create_json(name, description, locked, jndiId, connection, connectionManager): """ Create a JSON to be used for the REST API call """ json = { "connection": connection, - "type": type, + "type": "db2", "jndiId": jndiId, "name": name, "description": description, diff --git a/ibmsecurity/isam/aac/server_connections/ldap.py b/ibmsecurity/isam/aac/server_connections/ldap.py index 2c1575a3..c1b8b41c 100644 --- a/ibmsecurity/isam/aac/server_connections/ldap.py +++ b/ibmsecurity/isam/aac/server_connections/ldap.py @@ -1,7 +1,9 @@ import logging -import ibmsecurity.utilities.tools +from ibmsecurity.utilities import tools logger = logging.getLogger(__name__) +requires_modules = ["mga", "federation"] +requires_version = "9.0.2.1" # Will change if introduced in an earlier version. def get_all(isamAppliance, check_mode=False, force=False): @@ -33,14 +35,10 @@ def set(isamAppliance, name, connection, description='', locked=False, connectio """ if _check_exists(isamAppliance, name=name) is False: # Force the add - we already know connection does not exist - return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, connectionManager=connectionManager, servers=servers, check_mode=check_mode, - force=True) + return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, connectionManager=connectionManager, servers=servers, check_mode=check_mode, force=True) else: # Update request - return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, connectionManager=connectionManager, servers=servers, new_name=new_name, - check_mode=check_mode, force=force) + return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, connectionManager=connectionManager, servers=servers, new_name=new_name, check_mode=check_mode, force=force) def add(isamAppliance, name, connection, description='', locked=False, connectionManager=None, servers=None, @@ -83,26 +81,49 @@ def update(isamAppliance, name, connection, description='', locked=False, connec """ Modifying an LDAP server connection - Use new_name to rename the connection, cannot compare password so update will take place everytime + Use new_name to rename the connection. """ - if force is True or _check_exists(isamAppliance, name=name) is True: - if check_mode is True: - return isamAppliance.create_return_object(changed=True) - else: - json_data = _create_json(name=name, description=description, locked=locked, servers=servers, - connection=connection, connectionManager=connectionManager) - if new_name is not None: # Rename condition - json_data['name'] = new_name - ret_obj = search(isamAppliance, name=name) - id = ret_obj['data'] + ret_obj = get(isamAppliance, name) + warnings = ret_obj["warnings"] + + if ret_obj["data"] == {}: + warnings.append("LDAP Service connection {0} not found, skipping update.".format(name)) + return isamAppliance.create_return_object(warnings=warnings) + else: + id = ret_obj["data"]["uuid"] + + needs_update = False + json_data = _create_json(name=name, description=description, locked=locked, servers=servers, connection=connection, connectionManager=connectionManager) + if new_name is not None: # Rename condition + json_data['name'] = new_name + + if force is not True: + if 'uuid' in ret_obj['data']: + del ret_obj['data']['uuid'] + if 'bindPwd' in connection: + warnings.append("Since existing bindPwd cannot be read for ldap connections - this parameter will be ignored for idempotency. Add 'force' parameter to update the connection with a new bindPwd.") + connection.pop('bindPwd', None) + + sorted_ret_obj = tools.json_sort(ret_obj['data']) + sorted_json_data = tools.json_sort(json_data) + logger.debug("Sorted Existing Data:{0}".format(sorted_ret_obj)) + logger.debug("Sorted Desired Data:{0}".format(sorted_json_data)) + + if sorted_ret_obj != sorted_json_data: + needs_update = True + + if force is True or needs_update is True: + if check_mode is True: + return isamAppliance.create_return_object(changed=True, warnings=warnings) + else: return isamAppliance.invoke_put( "Modifying an LDAP server connection", - "/mga/server_connections/ldap/{0}/v1".format(id), json_data) - - return isamAppliance.create_return_object() + "/mga/server_connections/ldap/{0}/v1".format(id), json_data, requires_modules=requires_modules, + requires_version=requires_version, warnings=warnings) + return isamAppliance.create_return_object(warnings=warnings) def _create_json(name, description, locked, servers, connection, connectionManager): """ diff --git a/ibmsecurity/isam/aac/server_connections/smtp.py b/ibmsecurity/isam/aac/server_connections/smtp.py index dfdb3713..1195d14c 100644 --- a/ibmsecurity/isam/aac/server_connections/smtp.py +++ b/ibmsecurity/isam/aac/server_connections/smtp.py @@ -1,7 +1,9 @@ import logging -import ibmsecurity.utilities.tools +from ibmsecurity.utilities import tools logger = logging.getLogger(__name__) +requires_modules = ["mga", "federation"] +requires_version = "9.0.2.1" # Will change if introduced in an earlier version. def get_all(isamAppliance, check_mode=False, force=False): @@ -26,20 +28,16 @@ def get(isamAppliance, name, check_mode=False, force=False): "/mga/server_connections/smtp/{0}/v1".format(id)) -def set(isamAppliance, name, connection, description='', locked=False, connectionManager=None, new_name=None, - check_mode=False, force=False): +def set(isamAppliance, name, connection, description='', locked=False, connectionManager=None, new_name=None, check_mode=False, force=False): """ Creating or Modifying an SMTP server connection """ if _check_exists(isamAppliance, name=name) is False: # Force the add - we already know connection does not exist - return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, connectionManager=connectionManager, check_mode=check_mode, force=True) + return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, connectionManager=connectionManager, check_mode=check_mode, force=True) else: # Update request - return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, connectionManager=connectionManager, new_name=new_name, - check_mode=check_mode, force=force) + return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, connectionManager=connectionManager, new_name=new_name, check_mode=check_mode, force=force) def add(isamAppliance, name, connection, description='', locked=False, connectionManager=None, check_mode=False, @@ -82,26 +80,48 @@ def update(isamAppliance, name, connection, description='', locked=False, connec """ Modifying a SMTP server connection - Use new_name to rename the connection, cannot compare password so update will take place everytime + Use new_name to rename the connection. """ + ret_obj = get(isamAppliance, name) + warnings = ret_obj["warnings"] - if force is True or _check_exists(isamAppliance, name): - if check_mode is True: - return isamAppliance.create_return_object(changed=True) - else: - json_data = _create_json(name=name, description=description, locked=locked, connection=connection, - connectionManager=connectionManager) - if new_name is not None: # Rename condition - json_data['name'] = new_name + if ret_obj["data"] == {}: + warnings.append("SMTP Service connection {0} not found, skipping update.".format(name)) + return isamAppliance.create_return_object(warnings=warnings) + else: + id = ret_obj["data"]["uuid"] - ret_obj = search(isamAppliance, name=name) - id = ret_obj['data'] + needs_update = False + + json_data = _create_json(name=name, description=description, locked=locked, connection=connection, connectionManager=connectionManager) + if new_name is not None: # Rename condition + json_data['name'] = new_name + if force is not True: + if 'uuid' in ret_obj['data']: + del ret_obj['data']['uuid'] + if 'password' in connection: + warnings.append("Since existing password cannot be read for smtp connections - this parameter will be ignored for idempotency. Add 'force' parameter to update the connection with a new password.") + connection.pop('password', None) + + sorted_ret_obj = tools.json_sort(ret_obj['data']) + sorted_json_data = tools.json_sort(json_data) + logger.debug("Sorted Existing Data:{0}".format(sorted_ret_obj)) + logger.debug("Sorted Desired Data:{0}".format(sorted_json_data)) + + if sorted_ret_obj != sorted_json_data: + needs_update = True + + if force is True or needs_update is True: + if check_mode is True: + return isamAppliance.create_return_object(changed=True, warnings=warnings) + else: return isamAppliance.invoke_put( "Modifying a SMTP server connection", - "/mga/server_connections/smtp/{0}/v1".format(id), json_data) + "/mga/server_connections/smtp/{0}/v1".format(id), json_data, requires_modules=requires_modules, + requires_version=requires_version, warnings=warnings) - return isamAppliance.create_return_object() + return isamAppliance.create_return_object(warnings=warnings) def _create_json(name, description, locked, connection, connectionManager): diff --git a/ibmsecurity/isam/aac/server_connections/ws.py b/ibmsecurity/isam/aac/server_connections/ws.py index a07bf062..754ace22 100644 --- a/ibmsecurity/isam/aac/server_connections/ws.py +++ b/ibmsecurity/isam/aac/server_connections/ws.py @@ -95,6 +95,36 @@ def update(isamAppliance, name, connection, description='', locked=False, new_na Use new_name to rename the connection, cannot compare password so update will take place everytime """ + ret_obj = get(isamAppliance, name) + warnings = ret_obj["warnings"] + + if ret_obj["data"] == {}: + warnings.append("Web Service connection {0} not found, skipping update.".format(name)) + return isamAppliance.create_return_object(warnings=warnings) + else: + id = ret_obj["data"]["uuid"] + + needs_update = False + + json_data = _create_json(name=name, description=description, locked=locked, connection=connection) + if new_name is not None: # Rename condition + json_data['name'] = new_name + + if force is not True: + if 'uuid' in ret_obj['data']: + del ret_obj['data']['uuid'] + + if 'password' in connection: + warnings.append("Since existing password cannot be read for ws connections - this parameter will be ignored for idempotency. Add 'force' parameter to update the connection with a new password.") + connection.pop('password', None) + + sorted_ret_obj = tools.json_sort(ret_obj['data']) + sorted_json_data = tools.json_sort(json_data) + logger.debug("Sorted Existing Data:{0}".format(sorted_ret_obj)) + logger.debug("Sorted Desired Data:{0}".format(sorted_json_data)) + + if sorted_ret_obj != sorted_json_data: + needs_update = True if force is True or _check_exists(isamAppliance, name): if check_mode is True: @@ -120,12 +150,10 @@ def set(isamAppliance, name, connection, description='', locked=False, new_name= """ if (search(isamAppliance, name=name))['data'] == {}: # Force the add - we already know connection does not exist - return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, check_mode=check_mode, force=True) + return add(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, check_mode=check_mode, force=True) else: # Update request - return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, - locked=locked, new_name=new_name, check_mode=check_mode, force=force) + return update(isamAppliance=isamAppliance, name=name, connection=connection, description=description, locked=locked, new_name=new_name, check_mode=check_mode, force=force) def _create_json(name, description, locked, connection): diff --git a/ibmsecurity/isam/base/ssl_certificates/signer_certificate.py b/ibmsecurity/isam/base/ssl_certificates/signer_certificate.py index be4bd466..254de82d 100644 --- a/ibmsecurity/isam/base/ssl_certificates/signer_certificate.py +++ b/ibmsecurity/isam/base/ssl_certificates/signer_certificate.py @@ -22,11 +22,19 @@ def get(isamAppliance, kdb_id, cert_id, check_mode=False, force=False): "/isam/ssl_certificates/{0}/signer_cert/{1}".format(kdb_id, cert_id)) -def load(isamAppliance, kdb_id, label, server, port, check_mode=False, force=False): +def load(isamAppliance, kdb_id, label, server, port, check_remote=False, check_mode=False, force=False): """ Load a certificate from a server + + check_remote controls if ansible should check remote certificate by retrieving it or simply by + checking for existence of the label in the kdb """ - if force is True or _check_load(isamAppliance, kdb_id, label, server, port) is False: + if check_remote: + tmp_check = _check_load(isamAppliance, kdb_id, label, server, port) + else: + tmp_check = _check(isamAppliance, kdb_id, label) + + if force is True or tmp_check is False: if check_mode is True: return isamAppliance.create_return_object(changed=True) else: @@ -105,18 +113,9 @@ def delete(isamAppliance, kdb_id, cert_id, check_mode=False, force=False): if check_mode is True: return isamAppliance.create_return_object(changed=True) else: - try: - # Assume Python3 and import package - from urllib.parse import quote - except ImportError: - # Now try to import Python2 package - from urllib import quote - - # URL being encoded primarily to handle spaces and other special characers in them - f_uri = "/isam/ssl_certificates/{0}/signer_cert/{1}".format(kdb_id, cert_id) - full_uri = quote(f_uri) return isamAppliance.invoke_delete( - "Deleting a signer certificate from a certificate database", full_uri) + "Deleting a signer certificate from a certificate database", + "/isam/ssl_certificates/{0}/signer_cert/{1}".format(kdb_id, cert_id)) return isamAppliance.create_return_object() diff --git a/ibmsecurity/isam/web/reverse_proxy/configuration/entry.py b/ibmsecurity/isam/web/reverse_proxy/configuration/entry.py index 31009b06..1886d115 100644 --- a/ibmsecurity/isam/web/reverse_proxy/configuration/entry.py +++ b/ibmsecurity/isam/web/reverse_proxy/configuration/entry.py @@ -82,7 +82,7 @@ def set(isamAppliance, reverseproxy_id, stanza_id, entries, check_mode=False, fo """ Set a configuration entry or entries by stanza - Reverse Proxy - Note: entries has to be [['key', 'value1'], ['key', 'value2]], cannot provide [['key', ['value1', 'value2']]] + Note: entries has to be [['key', 'value1'], ['key', 'value2']], cannot provide [['key', ['value1', 'value2']]] get() returns the second format - thus lots of logic to handle this discrepancy. Smart enough to update only that which is needed. diff --git a/ibmsecurity/isam/web/reverse_proxy/junctions.py b/ibmsecurity/isam/web/reverse_proxy/junctions.py index 8fec8d67..ccf2e329 100644 --- a/ibmsecurity/isam/web/reverse_proxy/junctions.py +++ b/ibmsecurity/isam/web/reverse_proxy/junctions.py @@ -309,7 +309,7 @@ def set(isamAppliance, reverseproxy_id, junction_point, server_hostname, server_ http2_junction=None, http2_proxy=None, sni_name=None): """ Setting a standard or virtual junction - compares with existing junction and replaces if changes are detected - TODO: Compare all the parameters in the function - LTPA, BA are some that are not being compared + TODO: Compare all the parameters in the function - BA are some that are not being compared """ warnings = [] add_required = False @@ -500,6 +500,24 @@ def set(isamAppliance, reverseproxy_id, junction_point, server_hostname, server_ sni_name = None else: jct_json['sni_name'] = sni_name + if insert_ltpa_cookies is not None: + if insert_ltpa_cookies != 'no': + jct_json['insert_ltpa_cookies'] = insert_ltpa_cookies + + if ltpa_keyfile is not None: + jct_json['ltpa_keyfile'] = ltpa_keyfile + + if version_two_cookies is not None: + jct_json['version_two_cookies'] = version_two_cookies + + if ltpa_keyfile_password is not None: + if not force: + logger.debug("Skipping ltpa_keyfile_password for idempotency.") + warnings.append("Module can not compare ltpa_keyfile_password with server. Skipping parameter for idempotency. Force update of ltpa_keyfile_password by setting force=true.") + if 'ltpa_keyfile_password' in exist_jct: + del exist_jct['ltpa_keyfile_password'] + else: + jct_json['ltpa_keyfile_password'] = ltpa_keyfile_password # TODO: Not sure of how to match following attributes! Need to revisit. # TODO: Not all function parameters are being checked - need to add! diff --git a/ibmsecurity/isam/web/reverse_proxy/junctions_server.py b/ibmsecurity/isam/web/reverse_proxy/junctions_server.py index 86631515..f61d8876 100644 --- a/ibmsecurity/isam/web/reverse_proxy/junctions_server.py +++ b/ibmsecurity/isam/web/reverse_proxy/junctions_server.py @@ -22,7 +22,7 @@ def search(isamAppliance, reverseproxy_id, junction_point, server_hostname, serv def add(isamAppliance, reverseproxy_id, junction_point, server_hostname, junction_type, server_port, server_dn=None, stateful_junction='no', case_sensitive_url='no', windows_style_url='no', virtual_hostname=None, virtual_https_hostname=None, query_contents=None, https_port=None, http_port=None, proxy_hostname=None, - proxy_port=None, sms_environment=None, vhost_label=None, server_uuid=None, check_mode=False, force=False): + proxy_port=None, sms_environment=None, vhost_label=None, server_uuid=None, local_ip=None, check_mode=False, force=False): """ Adding a back-end server to an existing standard or virtual junctions @@ -46,6 +46,7 @@ def add(isamAppliance, reverseproxy_id, junction_point, server_hostname, junctio :param sms_environment: :param vhost_label: :param server_uuid: + :param local_ip: :param check_mode: :param force: :return: @@ -89,6 +90,8 @@ def add(isamAppliance, reverseproxy_id, junction_point, server_hostname, junctio jct_srv_json["query_contents"] = query_contents if server_uuid is not None and server_uuid != '': jct_srv_json["server_uuid"] = server_uuid + if local_ip is not None and local_ip != '': + jct_srv_json['local_ip'] = local_ip return isamAppliance.invoke_put( "Adding a back-end server to an existing standard or virtual junctions", diff --git a/ibmsecurity/isam/web/reverse_proxy/management_root/all.py b/ibmsecurity/isam/web/reverse_proxy/management_root/all.py index 43a5f8fb..3dac33af 100644 --- a/ibmsecurity/isam/web/reverse_proxy/management_root/all.py +++ b/ibmsecurity/isam/web/reverse_proxy/management_root/all.py @@ -1,8 +1,12 @@ import logging import ibmsecurity.utilities.tools import os.path +import shutil +import zipfile from ibmsecurity.isam.web.reverse_proxy.management_root import directory from ibmsecurity.isam.web.reverse_proxy.management_root import file +from ibmsecurity.isam.web.reverse_proxy import instance +from ibmsecurity.utilities.tools import get_random_temp_dir, files_same_zip_content logger = logging.getLogger(__name__) @@ -22,50 +26,113 @@ def export_zip(isamAppliance, instance_id, filename, check_mode=False, force=Fal if check_mode is False: return isamAppliance.invoke_get_file( "Exporting the contents of the administration pages root as a .zip file", - "/wga/reverseproxy/{0}/management_root?index=&name=&enc_name=&type=&browser=".format(instance_id), - filename) + "/wga/reverseproxy/{0}/management_root/?export=true".format(instance_id), + filename, no_headers=True) return isamAppliance.create_return_object() -def import_zip(isamAppliance, instance_id, filename, check_mode=False, force=False): +def import_zip(isamAppliance, instance_id, filename, delete_missing=False, check_mode=False, force=False): """ Importing the contents of a .zip file to the administration pages root """ - if check_mode is True: - return isamAppliance.create_return_object(changed=True) - else: - return isamAppliance.invoke_post_files( - "Importing the contents of a .zip file to the administration pages root", - "/wga/reverseproxy/{0}/management_root".format(instance_id), - [ + warnings = [] + + if force is True or _check_import(isamAppliance, instance_id, filename): + if delete_missing is True: + tempdir = get_random_temp_dir() + tempfilename = "management_root.zip" + tempfile = os.path.join(tempdir, tempfilename) + export_zip(isamAppliance, instance_id, tempfile) + + zServerFile = zipfile.ZipFile(tempfile) + zClientFile = zipfile.ZipFile(filename) + + files_on_server = []; + for info in zServerFile.infolist(): + files_on_server.append(info.filename) + files_on_client = []; + for info in zClientFile.infolist(): + files_on_client.append(info.filename) + missing_client_files = [x for x in files_on_server if x not in files_on_client] + + if missing_client_files != []: + logger.info("list all missing files in {}, which will be deleted on the server: {}.".format(filename, missing_client_files)) + + for x in missing_client_files: + if x.endswith('/'): + search_dir= os.path.dirname(x[:-1]) + '/' + if search_dir not in missing_client_files: + logger.debug("delete directory on the server: {0}.".format(x)) + directory.delete(isamAppliance, instance_id, x, check_mode=check_mode) + else: + search_dir= os.path.dirname(x) + '/' + if search_dir not in missing_client_files: + logger.debug("delete file on the server: {0}.".format(x)) + file.delete(isamAppliance, instance_id, x, check_mode=check_mode) + shutil.rmtree(tempdir) + + if check_mode is True: + return isamAppliance.create_return_object(changed=True) + else: + return isamAppliance.invoke_post_files( + "Importing the contents of a .zip file to the administration pages root", + "/wga/reverseproxy/{0}/management_root".format(instance_id), + [ + { + 'file_formfield': 'file', + 'filename': filename, + 'mimetype': 'application/octet-stream' + } + ], { - 'file_formfield': 'file', - 'filename': filename, - 'mimetype': 'application/octet-stream' - } - ], - { - 'type': 'file', - 'force': force - }) + "force": force + }, json_response=False) + return isamAppliance.create_return_object(warnings=warnings) -def check(isamAppliance, instance_id, id, name, type, check_mode=False, force=False): - ret_obj = None +def _check_import(isamAppliance, instance_id, filename): + """ + Checks if runtime template zip from server and client differ + :param isamAppliance: + :param filename: + :return: + """ + + if not instance._check(isamAppliance, instance_id): + logger.info("instance {} does not exist on this server. Skip import".format(instance_id)) + return False + + tempdir = get_random_temp_dir() + tempfilename = "management_root.zip" + tempfile = os.path.join(tempdir, tempfilename) + export_zip(isamAppliance, instance_id, tempfile) - if (type.lower() == 'directory'): - ret_obj = directory._check(isamAppliance, instance_id, id, name) - elif (type.lower() == 'file'): - ret_obj = file._check(isamAppliance, instance_id, id, name) + identical = files_same_zip_content(filename,tempfile) + + shutil.rmtree(tempdir) + if identical: + logger.info("management_root files {} are identical with the server content. No update necessary.".format(filename)) + return False else: - type = 'unknown' + logger.info("management_root files {} differ from the server content. Updating management_root files necessary.".format(filename)) + return True + +def check(isamAppliance, instance_id, id, name, type, check_mode=False, force=False): + ret_obj = None + + if(type.lower() == 'directory'): + ret_obj = directory._check(isamAppliance, instance_id, id, name) + elif(type.lower() == 'file'): + ret_obj = file._check(isamAppliance, instance_id, id, name) + else: + type = 'unknown' - data = { - 'id': ret_obj, - 'top': id, - 'name': name, - 'type': type - } + data = { + 'id': ret_obj, + 'top': id, + 'name': name, + 'type': type + } - return isamAppliance.create_return_object(data=data) + return isamAppliance.create_return_object(data=data) diff --git a/ibmsecurity/utilities/tools.py b/ibmsecurity/utilities/tools.py index da0c09cf..24860f40 100644 --- a/ibmsecurity/utilities/tools.py +++ b/ibmsecurity/utilities/tools.py @@ -6,6 +6,7 @@ import hashlib import ntpath import re +import zipfile logger = logging.getLogger(__name__) @@ -176,7 +177,39 @@ def files_same(original_file, new_file): return True else: return False + +def files_same_zip_content(original_file, new_file): + identical = True + + z1 = zipfile.ZipFile(open(original_file)) + z2 = zipfile.ZipFile(open(new_file)) + + if len(z1.infolist()) != len(z2.infolist()): + logger.debug("number of archive elements differ: {} in {} vs {} from server".format(len(z1.infolist()), z1.filename, len(z2.infolist()))) + identical = False + # Can stop comparison of zip files for perfomance + return identical + for zipentry in z1.infolist(): + if zipentry.filename not in z2.namelist(): + logger.debug("no file named {} found in {}".format(zipentry.filename, z2.filename)) + identical = False + else: + with z1.open(zipentry.filename) as f: + original_file_contents = f.read() + with z2.open(zipentry.filename) as f: + new_file_contents = f.read() + hash_original_file = hashlib.sha224(original_file_contents).hexdigest() + hash_new_file = hashlib.sha224(new_file_contents).hexdigest() + if hash_original_file != hash_new_file: + identical = False + logger.debug("content for zip file {} differs.".format(zipentry.filename)) + + if identical: + logger.info("content for zip files {} and {} are the same.".format(original_file,new_file)) + else: + logger.info("content for zip files {} and {} are different.".format(original_file,new_file)) + return identical def get_random_temp_dir(): """