Log in to Globus services

    Arguments:
        credentials (str or dict): A string filename, string JSON, or dictionary
                with credential and config information.
                By default, looks in ``~/mdf/credentials/globus_login.json``.
                Contains ``app_name``, ``services``, and ``client_id`` as described below.
        app_name (str): Name of script/client. This will form the name of the token cache file.
                **Default**: ``'UNKNOWN'``.
        services (list of str): Services to authenticate with.
                **Default**: ``[]``.
        client_id (str): The ID of the client, given when registered with Globus.
                **Default**: The MDF Native Clients ID.
        make_clients (bool): If ``True``, will make and return appropriate clients with
                generated tokens. If ``False``, will only return authorizers.
                **Default**: ``True``.
        clear_old_tokens (bool): If ``True``, delete old token file if it exists,
                forcing user to re-login. If ``False``, use existing token file if there is one.
                **Default**: ``False``.
        token_dir (str): The path to the directory to save tokens in and look for
                credentials by default. **Default**: ``DEFAULT_CRED_PATH``.

    Returns:
        dict: The clients and authorizers requested, indexed by service name.
                For example, if ``login()`` is told to auth with ``'search'``
                then the search client will be in the ``'search'`` field.

        Note:
            Previously requested tokens (which are cached) will be returned alongside
            explicitly requested ones.

def login(credentials=None, app_name=None, services=None, client_id=None, make_clients=True,
          clear_old_tokens=False, token_dir=DEFAULT_CRED_PATH, **kwargs):
    """Log in to Globus services

    Arguments:
        credentials (str or dict): A string filename, string JSON, or dictionary
                with credential and config information.
                By default, looks in ``~/mdf/credentials/globus_login.json``.
                Contains ``app_name``, ``services``, and ``client_id`` as described below.
        app_name (str): Name of script/client. This will form the name of the token cache file.
                **Default**: ``'UNKNOWN'``.
        services (list of str): Services to authenticate with.
                **Default**: ``[]``.
        client_id (str): The ID of the client, given when registered with Globus.
                **Default**: The MDF Native Clients ID.
        make_clients (bool): If ``True``, will make and return appropriate clients with
                generated tokens. If ``False``, will only return authorizers.
                **Default**: ``True``.
        clear_old_tokens (bool): If ``True``, delete old token file if it exists,
                forcing user to re-login. If ``False``, use existing token file if there is one.
                **Default**: ``False``.
        token_dir (str): The path to the directory to save tokens in and look for
                credentials by default. **Default**: ``DEFAULT_CRED_PATH``.

    Returns:
        dict: The clients and authorizers requested, indexed by service name.
                For example, if ``login()`` is told to auth with ``'search'``
                then the search client will be in the ``'search'`` field.

        Note:
            Previously requested tokens (which are cached) will be returned alongside
            explicitly requested ones.
    """
    NATIVE_CLIENT_ID = "98bfc684-977f-4670-8669-71f8337688e4"
    DEFAULT_CRED_FILENAME = "globus_login.json"

    def _get_tokens(client, scopes, app_name, force_refresh=False):
        token_path = os.path.join(token_dir, app_name + "_tokens.json")
        if force_refresh:
            if os.path.exists(token_path):
                os.remove(token_path)
        if os.path.exists(token_path):
            with open(token_path, "r") as tf:
                try:
                    tokens = json.load(tf)
                    # Check that requested scopes are present
                    # :all scopes should override any scopes with lesser permissions
                    # Some scopes are returned in multiples and should be separated
                    existing_scopes = []
                    for sc in [val["scope"] for val in tokens.values()]:
                        if " " in sc:
                            existing_scopes += sc.split(" ")
                        else:
                            existing_scopes.append(sc)
                    permissive_scopes = [scope.replace(":all", "")
                                         for scope in existing_scopes
                                         if scope.endswith(":all")]
                    missing_scopes = [scope for scope in scopes.split(" ")
                                      if scope not in existing_scopes
                                      and not any([scope.startswith(per_sc)
                                                   for per_sc in permissive_scopes])
                                      and not scope.strip() == ""]
                    # If some scopes are missing, regenerate tokens
                    # Get tokens for existing scopes and new scopes
                    if len(missing_scopes) > 0:
                        scopes = " ".join(existing_scopes + missing_scopes)
                        os.remove(token_path)
                except ValueError:
                    # Tokens corrupted
                    os.remove(token_path)
        if not os.path.exists(token_path):
            try:
                os.makedirs(token_dir)
            except (IOError, OSError):
                pass
            client.oauth2_start_flow(requested_scopes=scopes, refresh_tokens=True)
            authorize_url = client.oauth2_get_authorize_url()

            print("It looks like this is the first time you're accessing this service.",
                  "\nPlease log in to Globus at this link:\n", authorize_url)
            auth_code = input("Copy and paste the authorization code here: ").strip()

            # Handle 401s
            try:
                token_response = client.oauth2_exchange_code_for_tokens(auth_code)
            except globus_sdk.GlobusAPIError as e:
                if e.http_status == 401:
                    raise ValueError("\nSorry, that code isn't valid."
                                     " You can try again, or contact support.")
                else:
                    raise
            tokens = token_response.by_resource_server

            os.umask(0o077)
            with open(token_path, "w") as tf:
                json.dump(tokens, tf)
            print("Thanks! You're now logged in.")

        return tokens

    # If creds supplied in 'credentials', process
    if credentials:
        if type(credentials) is str:
            try:
                with open(credentials) as cred_file:
                    creds = json.load(cred_file)
            except IOError:
                try:
                    creds = json.loads(credentials)
                except ValueError:
                    raise ValueError("Credential string unreadable")
        elif type(credentials) is dict:
            creds = credentials
        else:
            try:
                with open(os.path.join(os.getcwd(), DEFAULT_CRED_FILENAME)) as cred_file:
                    creds = json.load(cred_file)
            except IOError:
                try:
                    with open(os.path.join(token_dir, DEFAULT_CRED_FILENAME)) as cred_file:
                        creds = json.load(cred_file)
                except IOError:
                    raise ValueError("Credentials/configuration must be passed as a "
                                     + "filename string, JSON string, or dictionary, "
                                     + "or provided in '"
                                     + DEFAULT_CRED_FILENAME
                                     + "' or '"
                                     + token_dir
                                     + "'.")
        app_name = creds.get("app_name")
        services = creds.get("services", services)
        client_id = creds.get("client_id")
    if not app_name:
        app_name = "UNKNOWN"
    if not services:
        services = []
    elif isinstance(services, str):
        services = [services]
    if not client_id:
        client_id = NATIVE_CLIENT_ID

    native_client = globus_sdk.NativeAppAuthClient(client_id, app_name=app_name)

    servs = []
    for serv in services:
        serv = serv.lower().strip()
        if type(serv) is str:
            servs += serv.split(" ")
        else:
            servs += list(serv)
    # Translate services into scopes as possible
    scopes = " ".join([KNOWN_SCOPES.get(sc, sc) for sc in servs])

    all_tokens = _get_tokens(native_client, scopes, app_name, force_refresh=clear_old_tokens)

    # Make authorizers with every returned token
    all_authorizers = {}
    for key, tokens in all_tokens.items():
        # TODO: Allow non-Refresh authorizers
        try:
            all_authorizers[key] = globus_sdk.RefreshTokenAuthorizer(tokens["refresh_token"],
                                                                     native_client)
        except KeyError:
            print("Error: Unable to retrieve tokens for '{}'.\n"
                  "You may need to delete your old tokens and retry.".format(key))
    returnables = {}
    # Populate clients and named services
    # Only translate back services - if user provides scope directly, don't translate back
    # ex. transfer => urn:transfer.globus.org:all => transfer,
    #     but urn:transfer.globus.org:all !=> transfer
    for service in servs:
        token_key = KNOWN_TOKEN_KEYS.get(service)
        # If the .by_resource_server key (token key) for the service was returned
        if token_key in all_authorizers.keys():
            # If there is an applicable client (all clients have known token key)
            # Pop from all_authorizers to remove from final return value
            if make_clients and KNOWN_CLIENTS.get(service):
                try:
                    returnables[service] = KNOWN_CLIENTS[service](
                                                authorizer=all_authorizers.pop(token_key),
                                                http_timeout=STD_TIMEOUT)
                except globus_sdk.GlobusAPIError as e:
                    print("Error: Unable to create {} client: {}".format(service, e.message))
            # If no applicable client, just translate the key
            else:
                returnables[service] = all_authorizers.pop(token_key)
    # Add authorizers not associated with service to returnables
    returnables.update(all_authorizers)

    return returnables