# Authentication using zCompute APIs

Symp CLI commands can be run by authenticating, without requiring a token.

However, it is advisable to authenticate and then generate a token for
use in any automated Symp CLI or API process. This will prevent possible
`connection reset by peer` errors when rate limits are exceeded.

## API Endpoints

The list of a cluster’s API endpoints is available in the UI.

To view the cluster’s API endpoints:

1. In the UI, in the upper right corner near the username, click
   **Help** (the **?** icon).

   The **Help** dropdown menu opens.
2. In the dropdown menu, select **API Endpoints**.

   The **API Endpoints** dialog opens, listing the cluster’s API endpoints.

   ![help-about-dropdown](media/help-about-dropdown.png)  ![api-endpoints-list](media/api-endpoints-list.png)

<a id="api-explorer"></a>

## API Explorer

zCompute provides an interactive Swagger-based API Explorer.

API operations require authorization with a token.

<a id="obtaining-a-token-in-the-ui"></a>

### Obtaining a token in the UI

To obtain a token in the UI:

1. In the UI, in the upper right corner near the username, click
   **Help** (the **?** icon), and in the dropdown menu, select
   **API Explorer**.

   The **Open API Explorer** dialog opens.

   ![api-explorer-open](media/api-explorer-open.png)
2. Click the **Open API Explorer** button.
   * A token is generated and automatically copied to the clipboard.
   * The **zCompute Symp API Explorer** opens, listing the zCompute APIs.

     ![api-explorer](media/api-explorer.png)

### Running API operations in the API Explorer

1. After [Obtaining a token in the UI](#obtaining-a-token-in-the-ui), click an API in the
   **zCompute Symp API Explorer** list.

   A Swagger UI opens, displaying the selected API’s REST methods.
2. Scroll down the list to locate the desired operation to run.
3. Click the selected REST method or its description, to expand the view
   to display its parameters.

   ![api-explorer-operation-run](media/api-explorer-operation-run.png)
4. Click the **Lock** icon on the right, to submit the authorization token.

   The **Available Authorizations** dialog opens.

   ![api-explorer-token](media/api-explorer-token.png)
   1. In the **token (apiKey) > Value** field, paste the token from your
      clipboard.
   2. Click **Authorize**.
   3. Click **Close** to close the **Available Authorizations** dialog
      and return to the selected API REST method screen.
5. Click **Try it out**.
6. Enter parameters for the REST method, and click the **Execute** bar.

   The **Responses** pane expands, displaying:
   * The **Curl** command that was executed.
   * The **Request URL**.
   * The **Server response**, comprising the return code, response body
     and response headers.

   ![api-explorer-operation-response](media/api-explorer-operation-response.png)

## Generating a token at a bash prompt

Since some commands are limited to the scope of a project, while others
have the scope of an entire account (domain), it’s necessary to generate
a token for the account (domain) scope, and a separate token for the
project scope.

Using `curl` on the `/api/v2/identity/auth` endpoint,
to generate authentication tokens involves two steps:

First, generating an account (domain) token,
and then using the domain token to generate a project token.

1. Generating a domain token:

   The `/api/v2/identity/auth` endpoint’s authentication token request
   data structure that defines the **scope** of an entire **account**
   (domain):
   ```console
   {
     "auth": {
       "scope": {
         "domain": {
           "name": "<ACCOUNT_NAME>"
         }
       },
       "identity": {
         "password": {
           "user": {
             "domain": {
               "name": "<ACCOUNT_NAME>"
             },
             "password": "<PASSWORD>",
             "name": "<USER_NAME>"
           }
         },
         "methods": [
           "password"
         ]
       }
     }
   }
   ```

   1. The `curl` command at the bash prompt for generating an account (domain)
      authentication token:
      ```console
      curl -D - -H "Content-Type: application/json" -X POST https://<cluster FQDN>/api/v2/identity/auth -d \
      '{"auth": {"scope": {"domain": {"name": "<account>"}}, "identity": {"password": {"user": {"domain": {"name": "<account>"}, "password": "<password>", "name": "<user>"}}, "methods": ["password"]}}}'
      ```

      For example:
      ```console
      curl -D - -H "Content-Type: application/json" -X POST https://compute.example.com/api/v2/identity/auth -d \
      '{"auth": {"scope": {"domain": {"name": "acc1"}}, "identity": {"password": {"user": {"domain": {"name": "acc1"}, "password": "Mypass1!", "name": "user1"}}, "methods": ["password"]}}}'
      ```
   2. The domain token is returned in the `x-subject-token` header, and can be
      copy/pasted to assign it to an environment variable for later use.

      For example:
      ```console
      export ZC_DOMAIN_TOKEN=MIISUgYJKoZIhvcNAQcCoIISQzCC...<2K+ character string>...ngpXjEqQtxlRGuCEIvo46sGMfedc=
      ```
2. Using the previously generated domain token to generate a project token:

   The `/api/v2/identity/auth` endpoint’s authentication token request
   data structure that defines the **scope** as limited to a **project**,
   and the identity method as a **token**:
   ```console
   {
     "auth": {
       "scope": {
         "project": {
           "domain": {
             "name": "<ACCOUNT_NAME>"
           },
           "name": "<PROJECT_NAME>"
         }
       },
       "identity": {
         "token": {
           "id": "'<DOMAIN_TOKEN>'"
         },
         "methods": [
           "token"
         ]
       }
     }
   }
   ```

   1. The `curl` command at the bash prompt for generating a **project**
      authentication token:
      ```console
      curl -D - -H "Content-Type: application/json" -X POST https:<cluster FQDN>/api/v2/identity/auth -d \
      '{"auth": {"scope": {"project": {"domain": {"name": "<account>"}, "name": "<project>"}}, "identity": {"token": {"id": "'<domain token string>'"}, "methods": ["token"]}}}'
      ```

      For example:
      ```console
      curl -D - -H "Content-Type: application/json" -X POST https://compute.example.com/api/v2/identity/auth -d \
      '{"auth": {"scope": {"project": {"domain": {"name": "acc1"}, "name": "vpcproj1"}}, "identity": {"token": {"id": "'$ZC_DOMAIN_TOKEN'"}, "methods": ["token"]}}}'
      ```
   2. The project token is returned in the `x-subject-token` header, and can be
      copy/pasted to assign it to an environment variable for later use.

      For example:
      ```console
      export ZC_PROJECT_TOKEN=MIIScwYJKoZIhvcNAQcCoIISZDCC...<2K+ character string>...VpADxR3RDrScUbgSwMAaZ8zCSrow=
      ```
   3. To use `curl` to call a REST method on an endpoint, provide the token
      string in the `X-Auth-Token` header:
      ```console
      curl -L -X <REST method> "https://<cluster FQDN>/api-explorer/api/v2/<endpoint>/" \
      -H "accept: application/json" \
      -H "X-Auth-Token: <token string>"
      ```

      For example, to retrieve the project’s tags, use the `GET` method on
      the  `/api-explorer/api/v2/tags/` endpoint, and the project
      authentication token (assigned in the previous step to the environment
      variable `$ZC_PROJECT_TOKEN`). This example uses `sed` and `awk` to
      format the output:
      ```console
      curl -L -X GET "https://compute.example.com/api-explorer/api/v2/tags/" \
      -H "accept: application/json" \
      -H "X-Auth-Token: $ZC_PROJECT_TOKEN" | \
      awk 'BEGIN{FS=",";OFS="\n"} FNR==1{$1=$1;print;exit}' | \
      sed '/{/{x;p;x;}'
      ```

      The response:
      ```console
        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                       Dload  Upload   Total   Spent    Left  Speed
      100   116  100   116    0     0  11756      0 --:--:-- --:--:-- --:--:-- 12888
      100  2302  100  2302    0     0  59137      0 --:--:-- --:--:-- --:--:--     0

      [{"description": "Tag2"
       "updated-at": "2023-10-31T08:44:01Z"
        "created-at": "2023-10-31T08:44:01Z"
        "value": ""
        "scope": "public"
        "project-id": "7ff34832b6754f7d91a918302af3e77f"
        "name": "vpctag2"}

        {"description": "Tag 1"
        "updated-at": "2023-10-31T08:10:49Z"
        "created-at": "2023-10-31T08:10:49Z"
        "value": ""
        "scope": "public"
        "project-id": "7ff34832b6754f7d91a918302af3e77f"
        "name": "vpctag1"}]
      ```

### Time-based One-Time Passcode (TOTP) Multi-Factor Authentication (MFA)

If a user has Time-based One-Time Passcode (TOTP) Multi-Factor
Authentication (MFA) enabled, you must also include both of the
following:

* A `totp` entry in the `methods` array.
* A `totp` object block with details.

For example:

```console
{
"auth": {
    "identity": {
    "methods": [
        "password",
        "totp"
    ],
    "password": {
        "user": {
        "domain": {
            "name": "<ACCOUNT_NAME>"
        },
        "name": "<USERNAME>",
        "password": "<PASSWORD>"
        }
    },
    "totp": {
        "user": {
        "domain": {
            "name": "<ACCOUNT_NAME>"
        },
        "name": "<USERNAME>",
        "passcode": "<TOTP_CODE>"
        }
    }
    },
    "scope": {
    "domain": {
        "name": "<ACCOUNT_NAME>"
    }
    }
}
}
```

## Generating a token in Symp CLI

1. To generate an authentication token via the Symp CLI shell, use the Symp
   `login` command:
   ```console
   login <user> <password> <domain> <project> -f value
   ```

   For example:
   ```console
   login user1 Mypass1! acc1 vpcproj1 -f value

   MIISUgYJKoZIhvcNAQcCoIISQzCC...<2K+ character string>...ngpXjEqQtxlRGuCEIvo46sGMfedc=
   ```

   Alternatively, an example of a single command at the bash prompt to generate
   an authentication token and assign it to an environment variable:
   ```console
   export ZC_PROJECT_TOKEN=`symp -k --url https://10.11.12.13 -d acc1 -u user1 --project vpcproj1 -p Mypass1! login user1 Mypass1! acc1 vpcproj1 -f value`

   echo $ZC_PROJECT_TOKEN

   MIISUgYJKoZIhvcNAQcCoIISQzCC...<2K+ character string>...ngpXjEqQtxlRGuCEIvo46sGMfedc=
   ```
2. Using the token to sign on to Symp from the bash prompt:
   ```console
   symp -k --url https://<cluster IP address> --project-token <token string>
   ```

   For example, generating the token (as in the previous step), and using it
   to sign on to Symp:
   ```console
   export ZC_PROJECT_TOKEN=`symp -k --url https://10.11.12.13 -d acc1 -u user1 --project vpcproj1 -p Mypass1! login user1 Mypass1! acc1 vpcproj1 -f value`

   symp -k --url https://10.11.12.13 --project-token $ZC_PROJECT_TOKEN
   ```

   ```console
   Starting new HTTPS connection (1): 10.16.145.114
   Connecting in insecure mode!
   Starting new HTTPS connection (1): 10.16.145.114
   Starting new HTTPS connection (1): 10.16.145.114

    d88888b  dP    dP 8888ba.88ba   888888ba  dP     dP   .88888.  888888ba  dP    dP
   88.     "' Y8.  .8P 88  `8b  `8b  88    `8b 88     88  d8'   `8b 88    `8b Y8.  .8P
   `Y88888b.  Y8aa8P  88   88   88 a88aaaa8P' 88aaaaa88a 88     88 88     88  Y8aa8P
         `8b    88    88   88   88  88        88     88  88     88 88     88    88
   d8'   .8P    88    88   88   88  88        88     88  Y8.   .8P 88     88    88
    Y88888P     dP    dP   dP   dP  dP        dP     dP   `8888P'  dP     dP    dP

   Tap <TAB> twice to get list of available commands.
   Type --help to get help with any command
   Symphony >
   ```

## Python authentication example

When using a zCompute API, the user is required to pass a valid token in an
`X-Auth-Token` HTTP header.

Users can generate a valid token by logging into the system using the
`identity/auth` API.

The following Python script is an example of this process:

```console
#!/usr/bin/python
from __future__ import print_function
import copy
import getpass
import os
import sys
import argparse
import requests
try:
    input = raw_input
except NameError:
    pass

IDENTITY_API_SUFFIX = "identity"
VPC_API_SUFFIX = "vpc"
DEFAULT_HEADERS = {
    "Accept": "application/json",
    "Content-Type": "application/json"
}
_debug = False


def _debug_msg(msg, *args, **kwargs):
    global _debug
    if _debug:
        print(msg.format(*args, **kwargs), file=sys.stderr)


def generate_totp_passcode(secret):
    """Generate TOTP passcode.
    :param bytes secret: A base32 encoded secret for the TOTP authentication
    :returns: totp passcode as bytes
    """
    try:
        import mintotp
    except:
        sys.exit("TOTP code generation library is not present. Please install mintotp using 'pip install mintotp'")
    return mintotp.totp(secret)


class ZComputeApi(object):

    def __init__(self, api_base_url, account_name, project_name, user, password,
                 insecure=False, totp_code=None, mfa_secret=None, debug=False):
        self._api_base_url = '{}/api/v2'.format(api_base_url)
        self._account_name = account_name
        self._project_name = project_name
        self._user = user
        self._password = password
        self._insecure = insecure
        self._token = None
        self._totp_code = totp_code
        self._mfa_secret = mfa_secret
        self._debug = debug

    def reset_api_params(self, token=None, project_name=None):
        if token:
            self._token = token
        if project_name:
            self._project_name = project_name

    def send_request(self, method='GET', api_path='', api_body=None, headers=None, raise_on_status=True):
        api_url = '{}/{}'.format(self._api_base_url, api_path)
        if headers:
            headers = copy.deepcopy(headers)
            headers.update(DEFAULT_HEADERS)
        else:
            headers = copy.deepcopy(DEFAULT_HEADERS)
        if self._token:
            headers['X-Auth-Token'] = self._token
        try:
            response = requests.request(
                method=method,
                url=api_url,
                headers=headers,
                json=api_body,
                verify=not self._insecure
            )
            if raise_on_status:
                response.raise_for_status()
        except Exception:
            _debug_msg("Failed sending {} {} reuquest".format(method, api_url))
            raise
        return response

    def send_api_request(self, method='GET', api_path='', api_body=None):
        return self.send_request(method=method, api_path=api_path, api_body=api_body).json()

    def _get_project_scope(self):
        return {"project": {"name": self._project_name, "domain": {"name": self._account_name}}}

    def _get_domain_scope(self):
        return {"domain": {"name": self._account_name}}

    def _get_password_auth(self):
        return {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "name": self._user,
                    "password": self._password,
                    "domain": {
                        "name": self._account_name
                    }
                }
            }
        }

    def _add_totp_code(self, identity_auth, totp_code):
        if identity_auth.get('methods'):
            identity_auth['methods'].append('totp')
        else:
            identity_auth['methods'] = ['totp']
        identity_auth['totp'] = {
            "user": {
                "name": self._user,
                "passcode": totp_code,
                "domain": {
                    "name": self._account_name
                }
            }
        }
        return identity_auth

    def get_project_token(self):
        identity_auth = self._get_password_auth()
        if self._mfa_secret:
            self._totp_code = generate_totp_passcode(self._mfa_secret)
        if self._totp_code:
            self._add_totp_code(identity_auth, self._totp_code)
        auth_json = {
            "auth": {
                "identity": identity_auth,
                "scope": self._get_project_scope()
            }
        }
        response = self.send_request('POST', '{}/auth'.format(IDENTITY_API_SUFFIX), auth_json, raise_on_status=False)
        if response.status_code == requests.codes.UNAUTHORIZED and response.json().get('receipt'):
            os_receipt = response.headers.get('openstack-auth-receipt')
            if self._mfa_secret:
                _debug_msg("Generating MFA secret")
                totp_code = generate_totp_passcode(self._mfa_secret)
            else:
                totp_code = str(input('MFA Code: ')).lower().strip()
            identity_auth = self._add_totp_code({}, totp_code)
            auth_json = {
                "auth": {
                    "identity": identity_auth,
                    "scope": self._get_project_scope()
                }
            }
            response = self.send_request('POST', '{}/auth'.format(IDENTITY_API_SUFFIX),
                                         auth_json,
                                         headers={"openstack-auth-receipt": os_receipt})
        else:
            response.raise_for_status()

        return response.headers['x-subject-token']

    def get_account_token(self):
        auth_json = {
            "auth": {
                "identity": self._get_password_auth(),
                "scope": self._get_domain_scope()
            }
        }
        auth_response = self.send_request('POST', '{}/auth'.format(IDENTITY_API_SUFFIX), auth_json)
        return auth_response.headers['x-subject-token']

    def get_user_default_project(self):
        details_api_uri = '{}/users/myself/projects'.format(IDENTITY_API_SUFFIX)
        projects = self.send_api_request('GET', details_api_uri)
        if len(projects) > 1:
            sys.exit("There are {} projects: {}\n"
                     "please select one using --project flag".format(
                          len(projects), ', '.join([p['name'] for p in projects])))
        return projects[0]['name']


def ask_for_value(parameter, current, hide=False):
    if hide:
        value = getpass.getpass("{} []: ".format(parameter))
    else:
        value = input("{} [{}]: ".format(parameter, current or '')).strip()
    if value:
        return value
    return current


def main():
    global _debug
    parser = argparse.ArgumentParser(description="Obtain zCompute API Token using user credentials")
    parser.add_argument("cluster", help="Cluster API endpoint IP or host name")
    parser.add_argument("--interactive",
                        help="Will get login credentials interactively",
                        action='store_true',
                        default=False)
    parser.add_argument("--account",
                        help="Account name (will use environment variable ZCOMPUTE_ACCOUNT if not provided)",
                        default=os.environ.get('ZCOMPUTE_ACCOUNT', None),
                        required=False)
    parser.add_argument("--username",
                        help="User name (will use environment variable ZCOMPUTE_USER if not provided)",
                        default=os.environ.get('ZCOMPUTE_USER', None),
                        required=False)
    parser.add_argument("--mfa-secret",
                        help="MFA secret to generate totp code MFA enabled login "
                             "(will use environment variable ZCOMPUTE_MFASECRET if not provided)",
                        default=os.environ.get('ZCOMPUTE_MFASECRET', None),
                        required=False)
    parser.add_argument("--totp-code",
                        help="TOTP code to use for MFA enabled login "
                             "(will use environment variable ZCOMPUTE_TOTP if not provided)",
                        default=os.environ.get('ZCOMPUTE_TOTP', None),
                        required=False)
    parser.add_argument("--project",
                        dest='project_name',
                        help="Project name (will use environment variable ZCOMPUTE_PROJECT if not provided)"
                             " if not provided and only one exists in the account will it",
                        default=os.environ.get('ZCOMPUTE_PROJECT', None),
                        required=False)
    parser.add_argument("--password",
                        help="Password - will use environment variable ZCOMPUTE_PASSWORD if not provided",
                        default=os.environ.get('ZCOMPUTE_PASSWORD', None))
    parser.add_argument("--debug", help="Print debug messages", default=False, action='store_true')
    parser.add_argument("-k", "--insecure", action='store_true', help="Do not verify cluster certificate", default=False)
    args = parser.parse_args()
    if args.debug:
        _debug = True
    if args.interactive:
        args.account = ask_for_value('account', args.account)
        args.project_name = ask_for_value('project name', args.project_name)
        args.username = ask_for_value('User name', args.username)
        args.mfa_secret = ask_for_value('MFA secret', args.mfa_secret, hide=True)
        args.totp_code = ask_for_value('TOTP code', args.totp_code)
        args.password = ask_for_value('Password', args.password, hide=True)

    url = 'https://{}'.format(args.cluster)
    if args.cluster.startswith('https://'):
        url = args.cluster
    api = ZComputeApi(url, args.account, args.project_name, args.username, args.password,
                      insecure=args.insecure, totp_code=args.totp_code, mfa_secret=args.mfa_secret, debug=args.debug)
    if not args.password:
        sys.exit("Please provide a password")
    if not args.project_name:
        if args.account is None:
            sys.exit("Please provide the account name to login into")
        if args.username is None:
            sys.exit("Please provide the user name to login with")
        api.reset_api_params(token=api.get_account_token())
        project_name = api.get_user_default_project()
    else:
        if args.project_name is None:
            sys.exit("Please provide the project name to login into")
        project_name = args.project_name
    api.reset_api_params(project_name=project_name)
    print(api.get_project_token())


if __name__ == '__main__':
    main()

|api-endpoints-list|
```
