def test_add_ca_to_dir(self, temp_mock, os_mock, open_mock): """ Test the add_ca_to_dir functions. """ os_mock.path.join.side_effect = os.path.join os_mock.path.exists.return_value = False cert_pem = TESTCERT_AND_KEY[0] cert_hash = TESTCERT_HASH # Try just writing a single CA and check it gets written to the # correct name. res = X509Utils.add_ca_to_dir([cert_pem], "/mydir") self.assertEqual(res, "/mydir") open_mock.assert_has_calls([ mock.call("/mydir/%s.0" % cert_hash, "w"), mock.call("/mydir/%s.signing_policy" % cert_hash, "w") ], any_order=True) # Test directory creation temp_mock.mkdtemp.return_value = "/tmpca.test" res = X509Utils.add_ca_to_dir([cert_pem]) self.assertEqual(res, "/tmpca.test") # Check that duplicate CA causes an exception os_mock.path.exists.return_value = True self.assertRaises(Exception, X509Utils.add_ca_to_dir, [cert_pem], "/mydir") def pol_exists(path): return path.endswith('.signing_policy') os_mock.path.exists.side_effect = pol_exists self.assertRaises(Exception, X509Utils.add_ca_to_dir, [cert_pem], "/mydir")
def __check_entry(self, entry, allow_group): """ Checks an entry is in a valid format and expands groups. If groups are allowed, then inputting a group entry will result in the expanded group entries on the output. entry - The entry string to check. allow_group - Boolean, on whether to allow group names. Raises a ValueError if it isn't valid. Returns a list of entries. Each returned entry is a tuple of (auth_mode, auth_data). """ if entry == "TOKEN": return [(ACLManager.AUTH_MODE_TOKEN, None)] elif entry == "CERT": return [(ACLManager.AUTH_MODE_X509, None)] elif entry == "SESSION": return [(ACLManager.AUTH_MODE_SESSION, None)] elif entry == "ALL": return [(ACLManager.AUTH_MODE_ALLOW_ALL, None)] elif entry.startswith("CERT:"): raw_dn = entry.split(':', 1)[1] if not "=" in raw_dn: raise ValueError("Bad CERT DN in ACL rule: '%s'" % entry) return [(ACLManager.AUTH_MODE_X509, X509Utils.normalise_dn(raw_dn)) ] elif allow_group and entry.startswith("@"): group_name = entry[1:] if not group_name in self.__groups: raise ValueError("Unrecognised group used in ACL rule: %s" % \ group_name) return deepcopy(self.__groups[group_name]) raise ValueError("Invalid auth entry '%s'." % entry)
def __gen_req(self, path, method="GET", auth_mode=ACLManager.AUTH_MODE_NONE, auth_data=None, cert_ok=True, token_ok=True): """ Call self.__inst.check_request while generating a fake request with the given parameters (without using test_mode on the ACLManager). Returns True if the request was successful (i.e. access would have been allowed). """ app = Flask("ACLManagertest") app.secret_key = "TestKey" # Required for session support token_svc = FakeTokenSVC(token_ok) try: headers = {} enable_session = False if auth_mode == ACLManager.AUTH_MODE_X509: if cert_ok: headers['Ssl-Client-Verify'] = 'SUCCESS' else: headers['Ssl-Client-Verify'] = 'FAILED' headers['Ssl-Client-S-Dn'] = auth_data elif auth_mode == ACLManager.AUTH_MODE_TOKEN: headers['X-Token'] = json.dumps(auth_data) elif auth_mode == ACLManager.AUTH_MODE_SESSION: enable_session = True with app.test_request_context(path=path, method=method, headers=headers): if enable_session: set_session_state(True) # Prepare a standard looking request current_app.log = self.__log current_app.token_svc = token_svc request.uuid = "Test-Test-Test" # Call the check function self.__inst.check_request() # Check that request info was correctly propagated if auth_mode == ACLManager.AUTH_MODE_X509: norm_dn = X509Utils.normalise_dn(auth_data) self.assertEqual(request.dn, norm_dn) elif auth_mode == ACLManager.AUTH_MODE_TOKEN: if token_ok: self.assertEqual(request.token, auth_data) self.assertEqual(request.raw_token, json.dumps(auth_data)) self.assertTrue(request.token_ok) else: self.assertFalse(request.token_ok) elif auth_mode == ACLManager.AUTH_MODE_SESSION: self.assertTrue(request.session_ok) # Access was allowed (no exception raised) return True except Forbidden: # Access was denied (Forbidden exception thrown) return False
def test_add_ca_to_dir_template(self, dir_util_mock, temp_mock): """ Test that the add_ca_to_dir template function works as expected. """ temp_mock.mkdtemp.return_value = "/new/dir" res = X509Utils.add_ca_to_dir([], template_dir="/my/template") self.assertEqual(res, "/new/dir") dir_util_mock.copy_tree.assert_called_with("/my/template", "/new/dir", preserve_symlinks=True)
def test_rfc_to_openssl(self): """ Test X509Utils.rfc_to_openssl. """ TEST_PAIRS = [ # Simple Conversion ('C = XX, L = YY, OU = ZZ', '/C=XX/L=YY/OU=ZZ'), # Input already in correct format ('/C=XX/L=YY/OU=ZZ', '/C=XX/L=YY/OU=ZZ'), ] for test_in, test_out in TEST_PAIRS: self.assertEqual(X509Utils.rfc_to_openssl(test_in), test_out) self.assertRaises(ValueError, X509Utils.rfc_to_openssl, "")
def __get_fake_request_auth(self): """ Fills the request object with te test (fake) authentication details. """ if self.__test_mode == ACLManager.AUTH_MODE_X509: request.dn = X509Utils.normalise_dn(self.__test_data) elif self.__test_mode == ACLManager.AUTH_MODE_TOKEN: request.token = self.__test_data request.raw_token = self.__test_data request.token_ok = True elif self.__test_mode == ACLManager.AUTH_MODE_SESSION: request.session_ok = True
def test_x509name(self): """ Test the X509_Name import/export functions. """ from M2Crypto import X509 # Convert a DN into an object and back TEST_DN = 'C=UK, L=London, O=Test Corp., OU=Security, CN=DN Tester' x509_obj = X509Utils.str_to_x509name(TEST_DN) self.assertIsInstance(x509_obj, X509.X509_Name) self.assertEqual(x509_obj.C, 'UK') self.assertEqual(x509_obj.L, 'London') self.assertEqual(x509_obj.O, 'Test Corp.') self.assertEqual(x509_obj.OU, 'Security') self.assertEqual(x509_obj.CN, 'DN Tester') self.assertEqual(X509Utils.x509name_to_str(x509_obj), TEST_DN) # Bonus: Convert a DN with two similar segments TEST_DN = 'C=UK, CN=Test User, CN=Proxy' x509_obj = X509Utils.str_to_x509name(TEST_DN) self.assertIsInstance(x509_obj, X509.X509_Name) # Interface currently prevents access to multiple fields # i.e. x509_obj.CN will only return "Test User". # Just check that all fields appear if object is converted back: self.assertEqual(X509Utils.x509name_to_str(x509_obj), TEST_DN)
def test_openssl_to_rfc(self): """ Test X509Utils.openssl_to_rfc. """ # Test some DNs in various formats TEST_PAIRS = [ # Simple conversion ('/C=XX/L=YY/OU=ZZ', 'C = XX, L = YY, OU = ZZ'), # Input already in correct format ('C = XX, L = YY, OU = ZZ', 'C = XX, L = YY, OU = ZZ'), ] for test_in, test_out in TEST_PAIRS: self.assertEqual(X509Utils.openssl_to_rfc(test_in), test_out) # Test error cases self.assertRaises(ValueError, X509Utils.openssl_to_rfc, "")
def __check_test_mode(self, auth_mode, auth_data): """ Helper function for testing test_mode. """ self.__inst.test_mode(auth_mode, auth_data) app = Flask("ACLManagertest") with app.test_request_context(path="/test", method="GET"): request.uuid = "Test-Test-Test" # Call the check function self.__inst.check_request() # Check that request info was correctly propagated if auth_mode == ACLManager.AUTH_MODE_X509: norm_dn = X509Utils.normalise_dn(auth_data) self.assertEqual(request.dn, norm_dn) elif auth_mode == ACLManager.AUTH_MODE_TOKEN: self.assertEqual(request.token, auth_data) self.assertEqual(request.raw_token, auth_data) self.assertTrue(request.token_ok) elif auth_mode == ACLManager.AUTH_MODE_SESSION: self.assertTrue(request.session_ok)
def __get_real_request_auth(): """ Fills the details of the presented credentials into the request object. """ # Cert auth if 'Ssl-Client-Verify' in request.headers \ and 'Ssl-Client-S-Dn' in request.headers: # Request has client cert if request.headers['Ssl-Client-Verify'] == 'SUCCESS': raw_dn = request.headers['Ssl-Client-S-Dn'] request.dn = X509Utils.normalise_dn(raw_dn) # Token Auth if 'X-Token' in request.headers: raw_token = request.headers['X-Token'] try: token_value = current_app.token_svc.check(raw_token) # Check if this looks like a standard token with an expiry value if isinstance(token_value, dict): if 'expiry' in token_value: exp_str = token_value['expiry'] exp_value = datetime.strptime(exp_str, '%Y-%m-%dT%H:%M:%S.%f') if exp_value < datetime.utcnow(): # Token has already expired current_app.log.info( "Request %s token has expired (at %s)", request.uuid, exp_str) return "403 Expired Token", 403 request.token = token_value request.raw_token = raw_token request.token_ok = True except ValueError: # Token decoding failed, it is probably corrupt or has been # tampered with. current_app.log.info("Request %s token validation failed.", request.uuid) return "403 Invalid Token", 403 if 'logged_in' in session: if session['logged_in']: request.session_ok = True
def temporary_ca_dir(cas, dir_path=None, template_dir=None): """ Context for creating a temporary CA directory. Temporary directory is automatically removed when exiting context. Args: cas (list): List of CA certs in string form. dir_path (str): Path to use for temporary ca directory. If None (default) then a random dir_path is created. template_dir (str): Path to a directory to use as a template for the temporary ca dir. All certs in this directory are duplicated in the new one. If None (default) then don't use a template directory. Returns: str: The temporary ca dir. """ ca_dir = X509Utils.add_ca_to_dir(cas, dir_path=dir_path, template_dir=template_dir) yield ca_dir shutil.rmtree(ca_dir, ignore_errors=True)
def test_dn_normalisation(self): """ Test the X509 DN normalisation function. """ # Test DN in expected output format TEST_DN = "C=XX, L=YY, CN=Test CA" # Test both RFC and OpenSSL style DNs with increasing amounts of space # All should match the TEST_DN after normalisation. self.assertEqual(X509Utils.normalise_dn(TEST_DN), TEST_DN) self.assertEqual(X509Utils.normalise_dn("/C=XX/L=YY/CN=Test CA"), TEST_DN) self.assertEqual( X509Utils.normalise_dn("/C=XX / L = YY / CN = Test CA "), TEST_DN) # Check that leading space doesn't upset the algorithm self.assertEqual( X509Utils.normalise_dn(" / C =XX / L = YY / CN = Test CA "), TEST_DN) self.assertEqual( X509Utils.normalise_dn("C = XX, L = YY, CN = Test CA"), TEST_DN) self.assertEqual( X509Utils.normalise_dn("C = XX, L = YY, CN = Test CA "), TEST_DN) self.assertEqual( X509Utils.normalise_dn( " C = XX, L = YY, CN = Test CA"), TEST_DN)
def logon(myproxy_server, username, password, ca_certs=None, voms=None, hours=12, myproxy_bin=None, vomses=None, log=None): """ Runs the myproxy-logon command with the various parameters. myproxy_server - Server to contact in hostname:port format. username - Username to use a remote site. password - Password to use at remote site. ca_certs - Either None to use the system CA, A string to use as the path to a CA dir, Or a list of strings containing individual PEM files to use as the CA(s). voms - An optional VO name to request a VOMS extension for. hours - Number of hours to request as the lifetime of the new credential. myproxy_bin - Location of the myproxy-logon executable to use, if unset, $PATH will be searched instead. vomses - Location of the vomses directory to use if issuing a VOMS proxy. Inherited from parent process otherwise. log - Optional logger object to write debug information to. Returns a string with the new credential PEM. Raises a RuntimeError exception if anything goes wrong. """ with tempfile.NamedTemporaryFile() as proxy: hostname, port = myproxy_server.split(':', 1) myproxy_opts = [ 'myproxy-logon', # Exectuable name '-s', hostname, # MyProxy server name '-p', '%s' % port, # MyProxy port number '-l', username, # Username at remote site '-t', '%u' % hours, # Lifetime in hours '-o', proxy.name, # Proxy on stdout '-q', # Quiet (output only on error) '-S', # Password on stdin ] if myproxy_bin: myproxy_opts[0] = myproxy_bin if voms: myproxy_opts.extend(['-m', voms]) env = copy.deepcopy(os.environ) ca_dir = None if ca_certs: if isinstance(ca_certs, str): # CA certs is a path to a cert dir env["X509_CERT_DIR"] = ca_certs else: # ca_certs is a list of PEM strings ca_dir = X509Utils.add_ca_to_dir(ca_certs, None) env["X509_CERT_DIR"] = ca_dir if vomses: env["VOMS_USERCONF"] = vomses # Actually run the command if log: log.debug("Running myproxy-logon with: %s", " ".join(myproxy_opts)) log.debug(" myproxy-logon env: %s", str(env)) proc = Popen(myproxy_opts, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) try: stdout, stderr = proc.communicate('%s\n' % password) except Exception as err: if log: log.warn("myproxy-logon command failed: %s", str(err)) raise RuntimeError("Logon error: Failed to run myproxy-logon") finally: # Make sure we tidy up the CA dir if we created one if ca_dir: shutil.rmtree(ca_dir, ignore_errors=True) # Check the return code if proc.returncode != 0: # Command failed, attempt to infer the reason error_str = "Unknown myproxy failure" if "invalid password" in stderr: error_str = "Incorrect password" elif "Unable to connect to" in stderr: error_str = "Connection error" elif "No credentials exist for username" in stderr: error_str = "Unrecognised user" elif "Error in service module" in stderr: error_str = "Unrecognised user/config error" if log: log.warn("myproxy-logon command failed with code %u (%s)", proc.returncode, error_str) log.debug("myproxy-logon stderr: %s", stderr) raise RuntimeError("Logon error: %s" % error_str) # Re-open the file to avoid any buffering with open(proxy.name, "r") as proxy_fd: proxy_str = proxy_fd.read().strip() return proxy_str # Proxy is just a string on stdout
def logon_session(site_id): """ Create a session for the current user at a given site. """ log = current_app.log db = request.db Site = db.tables.Site Cred = db.tables.Cred user_id = SiteService.get_current_uid() # Decode POST data if not request.data: log.warn("Missing post data for logon.") return "Missing POST data", 400 cred_data = json.loads(request.data) username = cred_data.get("username", None) password = cred_data.get("password", None) lifetime = cred_data.get("lifetime", None) vo_name = cred_data.get("vo", None) if not username or not password or not lifetime: log.warn("Missing post field in logon.") return "Required field missing", 400 # Check user can see the site site = Site.query.filter_by(site_id=site_id).first_or_404() is_owner = (site.site_owner == user_id) if not (is_owner or site.public): log.warn("User %u tried to login to site %u (access denied).", user_id, site_id) abort(404) # This user can't see the requested site # Check the site auth configuration if site.auth_type == 1: # VOMS login if not vo_name: log.warn( "User %u did not specify required VO name for site %u", user_id, site_id) return "VO required", 400 if not vo_name in current_app.vo_list: log.warn( "User %u requested unknown VO '%s' for login to site %u.", user_id, vo_name, site_id) return "Unknown VO name", 400 # Process the different possible CA info combinations ca_info = None if site.user_ca_cert or site.service_ca_cert: ca_info = [] if site.user_ca_cert: ca_info.append(site.user_ca_cert) if site.service_ca_cert: ca_info.append(site.service_ca_cert) elif current_app.cadir: ca_info = current_app.cadir # Actually run the myproxy command try: proxy = MyProxyUtils.logon(site.auth_uri, username, password, ca_info, vo_name, lifetime, myproxy_bin=current_app.myproxy_bin, vomses=current_app.vomses, log=log) except Exception as err: log.error("Failed to login user: %s", str(err)) return "Login failed: %s" % str(err), 400 # Clear the TZInfo as it should be UTC anyway and the database # uses naive date-time formats. cred_expiry = X509Utils.get_cert_expiry(proxy).replace(tzinfo=None) new_cred = Cred(cred_owner=user_id, site_id=site_id, cred_username=username, cred_expiry=cred_expiry, cred_value=proxy) with managed_session(request, message="Database error while storing proxy", http_error_code=500) as session: session.merge(new_cred) return ""