class TestProtectedRepoUtils(unittest.TestCase): def setUp(self): if os.path.exists(TEST_FILE): os.remove(TEST_FILE) self.utils = ProtectedRepoUtils(CONFIG) def tearDown(self): if os.path.exists(TEST_FILE): os.remove(TEST_FILE) global_cert_location = CONFIG.get('repos', 'global_cert_location') if os.path.exists(global_cert_location): shutil.rmtree(global_cert_location) def test_add_protected_repo(self): """ Tests adding a protected repo. """ # Setup repo_id = 'prot-repo-1' relative_path = 'path-1' # Test self.utils.add_protected_repo(relative_path, repo_id) # Verify listings = self.utils.read_protected_repo_listings() self.assertEqual(1, len(listings)) self.assertTrue(relative_path in listings) self.assertEqual(listings[relative_path], repo_id) def test_delete_protected_repo(self): """ Tests deleting an existing protected repo. """ # Setup repo_id = 'prot-repo-1' relative_path = 'path-1' self.utils.add_protected_repo(relative_path, repo_id) # Test self.utils.delete_protected_repo(relative_path) # Verify listings = self.utils.read_protected_repo_listings() self.assertEqual(0, len(listings))
def __init__(self, config): self.config = config self.repo_cert_utils = RepoCertUtils(config) self.protected_repo_utils = ProtectedRepoUtils(config)
class OidValidator: def __init__(self, config): self.config = config self.repo_cert_utils = RepoCertUtils(config) self.protected_repo_utils = ProtectedRepoUtils(config) def is_valid(self, dest, cert_pem, log_func): ''' Returns if the specified certificate should be able to access a certain URL. @param dest: destination URL trying to be accessed @type dest: string @param cert_pem: PEM encoded client certificate sent with the request @type cert_pem: string ''' # Load the repo credentials if they exist passes_individual_ca = False repo_bundle = self._matching_repo_bundle(dest) if repo_bundle is not None: # If there is an individual bundle but no client certificate has been specified, # they are invalid if cert_pem == '': return False # Make sure the client cert is signed by the correct CA is_valid = self.repo_cert_utils.validate_certificate_pem(cert_pem, repo_bundle['ca'], log_func=log_func) if not is_valid: log_func('Client certificate did not match the repo consumer CA certificate') return False else: # Indicate it passed individual check so we don't run the global too passes_individual_ca = True # Load the global repo auth cert bundle and check it's CA against the client cert # if it didn't already pass the individual auth check global_bundle = self.repo_cert_utils.read_global_cert_bundle(['ca']) if not passes_individual_ca and global_bundle is not None: # If there is a global repo bundle but no client certificate has been specified, # they are invalid if cert_pem == '': return False # Make sure the client cert is signed by the correct CA is_valid = self.repo_cert_utils.validate_certificate_pem(cert_pem, global_bundle['ca'], log_func=log_func) if not is_valid: log_func('Client certificate did not match the global repo auth CA certificate') return False # If there were neither global nor repo auth credentials, auth passes. if global_bundle is None and repo_bundle is None: return True # If the credentials were specified for either case, apply the OID checks. is_valid = self._check_extensions(cert_pem, dest, log_func) if not is_valid: log_func("Client certificate failed extension check for destination: %s" % (dest)) return is_valid def _matching_repo_bundle(self, dest): # Load the path -> repo ID mappings prot_repos = self.protected_repo_utils.read_protected_repo_listings() # Extract the repo portion of the URL # Example URL: https://guardian/pulp/repos/my-repo/pulp/fedora-13/i386/repodata/repomd.xml # Repo Portion: /my-repo/pulp/fedora-13/i386/repodata/repomd.xml repo_url = dest[dest.find(RELATIVE_URL) + len(RELATIVE_URL):] # If the repo portion of the URL starts with any of the protected relative URLs, # it is considered to be a request against that protected repo repo_id = None for relative_url in prot_repos.keys(): # Relative URL is inconsistent in Pulp, so a simple "startswith" tends to # break. Changing this to a find helps remove issues where the leading / # is missing, present, or duplicated. if repo_url.find(relative_url) != -1: repo_id = prot_repos[relative_url] break if not repo_id: return None bundle = self.repo_cert_utils.read_consumer_cert_bundle(repo_id, ['ca']) return bundle def _check_extensions(self, cert_pem, dest, log_func): """ Checks the requested destination path against the entitlement cert. :param cert_pem: certificate as PEM :type cert_pem: str :param dest: path of desired destination :type dest: str :param log_func: function used for logging :type log_func: callable taking 1 argument of type basestring :return: True iff request is authorized, else False :rtype: bool """ cert = certificate.create_from_pem(cert_pem) # Extract the repo portion of the URL repo_dest = dest[dest.find(RELATIVE_URL) + len(RELATIVE_URL):] try: valid = cert.check_path(repo_dest) except AttributeError: # not an entitlement certificate, so no entitlements valid = False if not valid: log_func('Request denied to destination [%s]' % dest) return valid
def setUp(self): if os.path.exists(TEST_FILE): os.remove(TEST_FILE) self.utils = ProtectedRepoUtils(CONFIG)
class OidValidator: def __init__(self, config): self.config = config self.repo_cert_utils = RepoCertUtils(config) self.protected_repo_utils = ProtectedRepoUtils(config) def is_valid(self, dest, cert_pem, log_func): ''' Returns if the specified certificate should be able to access a certain URL. @param dest: destination URL trying to be accessed @type dest: string @param cert_pem: PEM encoded client certificate sent with the request @type cert_pem: string ''' # Load the repo credentials if they exist passes_individual_ca = False repo_bundle = self._matching_repo_bundle(dest) if repo_bundle is not None: # If there is an individual bundle but no client certificate has been specified, # they are invalid if cert_pem == '': return False # Make sure the client cert is signed by the correct CA is_valid = self.repo_cert_utils.validate_certificate_pem(cert_pem, repo_bundle['ca'], log_func=log_func) if not is_valid: log_func('Client certificate did not match the repo consumer CA certificate') return False else: # Indicate it passed individual check so we don't run the global too passes_individual_ca = True # Load the global repo auth cert bundle and check it's CA against the client cert # if it didn't already pass the individual auth check global_bundle = self.repo_cert_utils.read_global_cert_bundle(['ca']) if not passes_individual_ca and global_bundle is not None: # If there is a global repo bundle but no client certificate has been specified, # they are invalid if cert_pem == '': return False # Make sure the client cert is signed by the correct CA is_valid = self.repo_cert_utils.validate_certificate_pem(cert_pem, global_bundle['ca'], log_func=log_func) if not is_valid: log_func('Client certificate did not match the global repo auth CA certificate') return False # If there were neither global nor repo auth credentials, auth passes. if global_bundle is None and repo_bundle is None: return True # If the credentials were specified for either case, apply the OID checks. is_valid = self._check_extensions(cert_pem, dest, log_func) if not is_valid: log_func("Client certificate failed extension check for destination: %s" % (dest)) return is_valid def _matching_repo_bundle(self, dest): # Load the path -> repo ID mappings prot_repos = self.protected_repo_utils.read_protected_repo_listings() # Extract the repo portion of the URL # Example URL: https://guardian/pulp/repos/my-repo/pulp/fedora-13/i386/repodata/repomd.xml # Repo Portion: /my-repo/pulp/fedora-13/i386/repodata/repomd.xml repo_url = dest[dest.find(RELATIVE_URL) + len(RELATIVE_URL):] # If the repo portion of the URL starts with any of the protected relative URLs, # it is considered to be a request against that protected repo repo_id = None for relative_url in prot_repos.keys(): # Relative URL is inconsistent in Pulp, so a simple "startswith" tends to # break. Changing this to a find helps remove issues where the leading / # is missing, present, or duplicated. if repo_url.find(relative_url) != -1: repo_id = prot_repos[relative_url] break if not repo_id: return None bundle = self.repo_cert_utils.read_consumer_cert_bundle(repo_id, ['ca']) return bundle def _check_extensions(self, cert_pem, dest, log_func): cert = certificate.Certificate(content=cert_pem) extensions = cert.extensions() # Extract the repo portion of the URL repo_dest = dest[dest.find(RELATIVE_URL) + len(RELATIVE_URL) + 1:] # Remove any initial or trailing slashes repo_dest = repo_dest.strip('/') valid = False for e in extensions: if self._is_download_url_ext(e): oid_url = extensions[e] if self._validate_url(oid_url, repo_dest): valid = True break if not valid: log_func('Request denied to destination [%s]' % dest) return valid def _is_download_url_ext(self, ext_oid): ''' Tests to see if the given OID corresponds to a download URL value. @param ext_oid: OID being tested; cannot be None @type ext_oid: a certificiate.OID object @return: True if the OID contains download URL information; False otherwise @rtype: boolean ''' result = ext_oid.match('1.3.6.1.4.1.2312.9.2.') and ext_oid.match('.1.6') return result def _validate_url(self, oid_url, dest): ''' Returns whether or not the destination matches the OID download URL. @return: True if the OID permits the destination; False otherwise @rtype: bool ''' # Swap out all $ variables (e.g. $basearch, $version) for a reg ex wildcard in that location # # For example, the following entitlement: # content/dist/rhel/server/$version/$basearch/os # # Should allow any value for the variables: # content/dist/rhel/server/.+?/.+?/os # Remove initial and trailing '/', and substitute the $variables for # equivalent regular expressions in oid_url. oid_re = re.sub(r'\$[^/]+(/|$)', '[^/]+/', oid_url.strip('/')) return re.match(oid_re, dest) is not None