def locked_out() -> None: """ Validate if this user is locked out from accessing the API :return: """ if tacacs_auth_lockout(username=request.authorization.username): current_app.logger.error( f"{request.authorization.username} is currently locked out.") raise LockedOut
def test_tacacs_auth_lockout(self): """ Tests for the tacacs_auth_lockout function :return: """ # Test if no existing failures in cache, and we're not reporting one with self.subTest( msg="Checking failures, none yet reported for user."): self.assertEqual(tacacs_auth_lockout(username=self.username), False) # Test if no existing failures in cache, and we _ARE_ reporting one with self.subTest(msg="Reporting first failure for user."): self.assertEqual( tacacs_auth_lockout(username=self.username, report_failure=True), False) # Iterate up to 9 failures: for _ in range(8): tacacs_auth_lockout(username=self.username, report_failure=True) # Test if 9 existing failures and checking (But not adding new failure) with self.subTest( msg="Checking failures, 9 reported so far for user."): self.assertEqual(tacacs_auth_lockout(username=self.username), False) # Test if 9 existing failures and we report the tenth with self.subTest( msg="Checking failures, 9 reported, reporting 1 more."): self.assertEqual( tacacs_auth_lockout(username=self.username, report_failure=True), True) # Test if 10 failures and we are simply checking with self.subTest( msg="Checking failures, 10 reported so far for user."): self.assertEqual(tacacs_auth_lockout(username=self.username), True) # Test if 10 failures and we try to report another with self.subTest( msg= "Checking failures, 10 reported so far for user, trying to report another failure." ): self.assertEqual( tacacs_auth_lockout(username=self.username, report_failure=True), True) # Test "old" failures by stashing a 9 failures from _before_ ten minutes ago. self.stash_failures(failure_count=9, old=True) # Test if 9 existing failures from greater than 10 minutes ago: with self.subTest( msg="Checking failures, 9 reported > 10 minutes ago."): self.assertEqual(tacacs_auth_lockout(username=self.username), False) # Test if 9 existing failures from greater than 10 minutes ago, and we're reporting a new failure: with self.subTest( msg= "Checking failures, 9 reported > 10 minutes ago, reporting 1 new failure." ): self.assertEqual( tacacs_auth_lockout(username=self.username, report_failure=True), False) # Now add 9 new failures for _ in range(9): tacacs_auth_lockout(username=self.username, report_failure=True) # Finally test that these failures "count" and we're locked out: with self.subTest( msg= "Testing lockout after removing old faiulres, but new came in." ): self.assertEqual( tacacs_auth_lockout(username=self.username, report_failure=True), True)
def post(self): """ Will enqueue an attempt to use netmiko's send_config_set() method to run commands/put configuration on a device. Requires you submit the following in the payload: ip: str commands: Sequence[str] Optional: port: int - Default 22 device_type: str - Default cisco_ios enable: Optional[str] - Default the password provided for basic auth save_config: bool commit: bool Secured by Basic Auth, which is then passed to the network device. :return: A dict of the job ID, a 202 response code, and the job_id as the X-Request-ID header """ # Grab creds off the basic_auth header auth = request.authorization if auth.username is None: raise Unauthorized # Check if this user is locked out or not if tacacs_auth_lockout(username=auth.username): raise Forbidden # Create a credentials object creds = Credentials(username=auth.username, password=auth.password, enable=request.json.get("enable", None)) # Grab x-request-id request_id = g.request_id # Validate there isn't already a job by this ID q = current_app.config["q"] if q.fetch_job(request_id) is not None: raise DuplicateRequestID # Enqueue your job, and return the job ID current_app.logger.debug("%s: Enqueueing job for %s@%s:%s", request_id, creds.username, request.json["ip"], request.json["port"]) job = q.enqueue( netmiko_send_config, ip=request.json["ip"], port=request.json["port"], device_type=request.json["device_type"], credentials=creds, commands=request.json["commands"], save_config=request.json["save_config"], commit=request.json["commit"], job_id=request_id, result_ttl=86460, failure_ttl=86460, ) job_id = job.get_id() current_app.logger.info("%s: Enqueued job for %s@%s:%s", job_id, creds.username, request.json["ip"], request.json["port"]) # Generate the un/pw hash: user_hash = creds.salted_hash() # Stash the job_id in redis, with the user/pass hash so that only that user can retrieve results job_locker(salted_creds=user_hash, job_id=job_id) # Return our payload containing job_id, a 202 Accepted, and the X-Request-ID header return { "job_id": job_id, "app": "naas", "version": __version__ }, 202, { "X-Request-ID": job_id }
def netmiko_send_config( ip: str, credentials: "Credentials", device_type: str, commands: "Sequence[str]", port: int = 22, save_config: bool = False, commit: bool = False, delay_factor: int = 2, verbose: bool = False, ) -> "Tuple[Optional[dict], Optional[str]]": """ Instantiate a netmiko wrapper instance, feed me an IP, Platform Type, Username, Password, any commands to run. :param ip: What IP are we connecting to? :param credentials: A naas.library.auth.Credentials object with the username/password/enable in it :param commands: List of the commands to issue to the device :param device_type: What Netmiko device type are we connecting to? :param port: What TCP Port are we connecting to? :param save_config: Do you want to save this configuration upon insertion? Default: False, don't save the config :param commit: Do you want to commit this candidate configuration to the running config? Default: False :param delay_factor: Netmiko delay factor, default of 2, higher is slower but more reliable on laggy links :param verbose: Turn on Netmiko verbose logging :return: A Tuple of a dict of the results (if any) and a string describing the error (if any) """ # Create device dict to pass netmiko netmiko_device = { "device_type": device_type, "ip": ip, "username": credentials.username, "password": credentials.password, "secret": credentials.enable, "port": port, "ssh_config_file": "/app/naas/ssh_config", "allow_agent": False, "use_keys": False, "verbose": verbose, } try: logger.debug("%s:Establishing connection...", ip) net_connect = netmiko.ConnectHandler(**netmiko_device) net_output = {} logger.debug("%s:Sending config_set: %s", ip, commands) net_output["config_set_output"] = net_connect.send_config_set( commands, delay_factor=delay_factor) if save_config: try: logger.debug("%s: Saving configuration", ip) net_connect.save_config() except NotImplementedError: logger.debug( "%s: This device_type (%s) does not support the save_config operation.", ip, device_type) if commit: try: logger.debug("%s: Committing configuration", ip) net_connect.commit() except AttributeError: logger.debug( "%s: This device_type (%s) does not support the commit operation", ip, device_type) # Perform graceful disconnection of this SSH session net_connect.disconnect() except (netmiko.NetMikoTimeoutException, timeout) as e: logger.debug("%s:Netmiko timed out connecting to device: %s", ip, e) return None, str(e) except netmiko.NetMikoAuthenticationException as e: logger.debug( "%s:Netmiko authentication failure connecting to device: %s", ip, e) tacacs_auth_lockout(username=credentials.username, report_failure=True) return None, str(e) except (ssh_exception.SSHException, ValueError) as e: logger.debug("%s:Netmiko cannot connect to device: %s", ip, e) return None, ("Unknown SSH error connecting to device {0}: {1}".format( ip, str(e))) logger.debug("%s:Netmiko executed successfully.", ip) return net_output, None