argus_api.helpers.authentication — Argus-Toolbelt 1.0 documentation

# Source code for argus_api.helpers.authentication

    import getpass
    import hmac
    from time import time
    from hashlib import sha256
    from base64 import urlsafe_b64encode, b64decode
    from urllib.parse import urlparse
    from requests import post
    from argus_api import __version__
    from argus_cli.settings import settings

    def csrf_token_for(url: str, key: str) -> str:
        """Generates a CSRF token for an API URL

        :param url: URL endpoint
        :param key: User's session key
        :returns: CSRF token
        """
        # Parse the URL into its components
        u = urlparse(url)

        # Create the original path, i.e https://domain.tld/path
        path = "%s://%s/%s" % (u.scheme, u.netloc, u.path)

        # Decode the session key
        session_key = b64decode(key)

        canonical_string = """{"alg": "HS256", "typ": "JWT"}.{"url": "%s", "timestamp": "%s"}""" % (path, int(round(time()*1000)))

        # Sign the URL, timestamp,
        signature = hmac.new(session_key, msg=canonical_string.encode("utf-8"), digestmod=sha256).digest()
        token = "%s.%s" % (canonical_string, urlsafe_b64encode(signature))

        return token

    def with_api_key(api_key: str) -> dict:
        """Authenticates the user towards the API with an API key

        :param api_key: User's API key
        :return: Authentication headers
        """
        # TODO: Check if the API Key is valid for the user
        return {
            "Argus-API-Key": api_key
        }

    def with_credentials(username: str = "", password: str = "", method: str = "ldap", base_url: str = None) -> callable:
        """Authenticates the user with the API via TOTP, Radius or LDAP, and returns a function
        that creates the correct headers with CSRF token for a given URL, using the user's
        sessionKey and cookie.

        :param base_url: API URL
        :param method: Authentication method
        :param username: The user's username
        :param password: The user's password
        :return: Authentication data
        """
        base_url = base_url or settings["api"]["api_url"]
        auth_url = "%s/authentication/v1/%s/authenticate" % (base_url, method)

        parameters = {
            "userName": username,
            "password": password
        }
        if not username:
            parameters["userName"] = input("Username")
        if not password:
            parameters["password"] = getpass.getpass("Enter %spassword:" % ("AD3 " if method == "ldap" else ""))
        if method == "totp":
            parameters["tokenCode"] = input("Enter TOTP token code:")
        elif method == "radius":
            parameters["tokenCode"] = input("Enter Radius token code:")
            parameters["mode"] = "AUTHENTICATION"
        elif method not in ("ldap", "password"):
            raise ValueError("Invalid authentication method: %s" % method)

        response = post(auth_url, json=parameters)

        # Accept both 200 and 201 response
        if response.status_code in (200, 201):
            authentication_data = response.json()["data"]

            def authenticate(url: str) -> dict:
                """Authenticator function, generates a CSRF token for the given URL

                :param str url: URL to create CSRF token for
                :returns: Headers to pass to a request
                """
                return {
                    "Cookie": "argus_username=%s; argus_cookie=%s;" % (username, authentication_data["cookie"]),
                    "Argus-CSRF-Token": csrf_token_for(url, authentication_data["sessionKey"])
                }
            return authenticate
        else:
            print("Authentication failed %d" % response.status_code)

            # Return empty headers — failed authentication does not need to raise
            # any further errors.
            return lambda x: {}

    def with_authentication(mode: str = "ldap", base_url: str = None, username: str = "", api_key: str = "") -> callable:
        """Creates a decorator that can be used by plugins to have authentication handled for them,
        by calling with_credentials or with_api_key and then passing the authentication headers
        to the decorated function as `authentication`.

        Generated API methods accept `authentication` parameter, so after decorating a function
        with this, the authentication argument can be passed directly to any API method.

        :param str mode: LDAP, Radius, Password or API key
        :returns: decorator function
        """
        base_url = base_url or settings["api"]["api_url"]
        def decorator(function):
            """Authenticate with {mode}""".format(mode=mode)
            def authenticate(*args, **kwargs):
                """Returns a callable that can be used to create authentication headers for a URL,
                for LDAP, TOTP, Password or API key authentication. This will be passed in to the function
                as authentication=function, and can be passed to any API call
                """
                if mode.lower() not in ('apikey', 'api_key'):
                    kwargs.update({ "authentication": with_credentials(method=mode, username=username, base_url=base_url) })
                else:
                    kwargs.update({
                        "authentication": lambda url: with_api_key(api_key) if api_key else input("Enter Argus API key")
                    })
                return function(*args, **kwargs)
            return authenticate
        return decorator