Skip to content

Commit

Permalink
vdk-control-cli: autodetect if authentication is needed (#305)
Browse files Browse the repository at this point in the history
It will prompt for login only if control service require authentication.
This happens if control service returns 401. This makes it easier to use
since this removes the necessity of explicit flag to enable/disable
authentication. And make integration with other library easier.
For example same authentication is used by properties api .

So how it works

1. If users uses vdk login we'd cache their api token (and api token
authorization url which may be provided by plugin)
2. if users users vdk login with interactive flow - we cache the
necessary data as well
3. If users has not used vdk login but has set VDK_API_TOKEN and
VDK_API_TOKEN_AUTHORIZATION_URL we'd use it in api-token folow to login
(similar to step 1)

With 1 and 2 having higher priority than environment variables (3)

Error is not thrown until we try to connect to Control Service and it
returns 401:

If Control Service require authentication then error would be thrown
like this
```
vdkcli list --all
Usage: vdkcli list [OPTIONS]

Error: ¯\_(ツ)_/¯

what: Control Service Error
why: The request has not been applied because it lacks valid
authentication credentials.
consequences: Operation cannot complete.
countermeasures: Try to login again using VDK CLI login command. Make
sure you have permission to execute the given operation.
```

Otherwise request would succeed

Testing Done: unit tests.
Test using vdkcli list --all -u rest-api-with-auth and saw above error
produced and then vdkcli list --all -u rest-api-without-auth and saw it
succeeds.

Signed-off-by: Antoni Ivanov <[email protected]>
  • Loading branch information
antoniivanov authored Sep 30, 2021
1 parent c90e335 commit ecf0aaf
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 39 deletions.
2 changes: 2 additions & 0 deletions projects/vdk-control-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ VDK Control CLI 1.1
Introduced vdk create --local to create job only locally from sample
vdk create without arguments would create job locally and try to detect if it can created in cloud
vdk create --cloud - always created in cloud only and fail if it cannot
* Auto-detect if authentication is necessary
This remove the need of explicit variable to set/unset authentication. Now vdkcli would detect automatically.

* **Bug Fixes**

Expand Down
17 changes: 11 additions & 6 deletions projects/vdk-control-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,20 @@ use `vdkcli` command instead of `vdk`.

### Environment variables:

* VDK_AUTHENTICATION_DISABLE - disables security (vdk login will not be required). See Security section.
* VDK_BASE_CONFIG_FOLDER - Override local base configuration folder (by default $HOME folder) . Use in case multiple users need to login (e.g in case of automation) on same machine.
* VDK_BASE_CONFIG_FOLDER - Override local base configuration folder (by default in $HOME folder). Inside it will create folder .vdk.internal.
CLI state may be kept there (login info). Use in case multiple users need to login (e.g in case of automation) on same machine.

* VDK_CONTROL_SERVICE_REST_API_URL - Default Control Service URL to use if not specified as command line argument
* VDK_API_TOKEN - Default API Token to use if another authentication has not been used with vdk login
* VDK_API_TOKEN_AUTHORIZATION_URL - Default API token URL to use if another authentication has not been used with vdk login.

### Security
By default, all operation require authentication: vdk login must have finished successfully.
You can disable it with environment variable `VDK_AUTHENTICATION_DISABLE=true`
This would only work if Control Service which VDK CLI uses also has security disabled.
If Control Service configured require authentication: vdk login must have finished successfully.
Or alternatively correct VDK_API_TOKEN_AUTHORIZATION_URL and VDK_API_TOKEN must be set correctly and will behave same as `vdk login -t api-token`.
If vdk login is used - it take priority over environment variables set VDK_API_TOKEN_AUTHORIZATION_URL and VDK_API_TOKEN
To clear previous login info (aka logout) use `vdk logout`.

In case of credentials type login flow we start a process on port `31113` to receive the credentials.
In case of credentials type vdk login flow we start a process on port `31113` to receive the credentials.
If you already have process running on `31113` you can override the value.
To override the port set environmental variable `OAUTH_PORT` with free port which the client can use.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2021 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
from typing import Optional

from vdk.internal.control.auth.auth import Authentication
from vdk.internal.control.auth.login_types import LoginTypes
from vdk.internal.control.configuration.vdk_config import VDKConfig


class ApiKeyAuthentication:
"""
Class that execute authentication process using API token.
It will use the API token to get temporary access token using api token authorization URL.
See Authentication class as well.
"""

def __init__(
self,
api_token_authorization_url: Optional[str] = None,
api_token: Optional[str] = None,
):
"""
:param api_token_authorization_url: Authorization URL - Same as login --api-token-authorization-server-url.
:param api_token: API Token - Same as login --api-token.
"""
self.__api_token = api_token
self.__api_token_authorization_url = api_token_authorization_url
self.__auth = Authentication()

def authentication_process(self) -> None:
"""
Executes the authentication process and caches the generated access token so it can be used during REST calls.
"""
self.__auth.update_api_token_authorization_url(
self.__api_token_authorization_url
)
self.__auth.update_api_token(self.__api_token)
self.__auth.update_auth_type(LoginTypes.API_TOKEN.value)
self.__auth.acquire_and_cache_access_token()
26 changes: 18 additions & 8 deletions projects/vdk-control-cli/src/vdk/internal/control/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import json
import logging
import time
from typing import Optional

from requests import post
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
from vdk.internal.control.auth.auth_request_values import AuthRequestValues
from vdk.internal.control.auth.login_types import LoginTypes
from vdk.internal.control.configuration.vdk_config import VDKConfig
from vdk.internal.control.configuration.vdk_config import VDKConfigFolder
from vdk.internal.control.exception.vdk_exception import VDKException

Expand Down Expand Up @@ -82,8 +84,9 @@ def deserialize(content: str):
class Authentication:
REFRESH_TOKEN_GRANT_TYPE = "refresh_token" # nosec

def __init__(self, conf=VDKConfigFolder()):
def __init__(self, conf=VDKConfigFolder(), vdk_config=VDKConfig()):
self._conf = conf
self.__vdk_config = vdk_config
self._cache = self.__load_cache()

def __load_cache(self):
Expand Down Expand Up @@ -129,12 +132,12 @@ def __exchange_api_for_access_token(self):
+ int(token_response.get(AuthRequestValues.EXPIRATION_TIME_KEY.value, "0"))
)

def read_access_token(self):
def read_access_token(self) -> Optional[str]:
"""
Read access token from _cache or fetch it from Authorization server.
If not available in _cache it will get it using provided configuration during VDK CLI login to fetch it.
If it detects that token is about to expire it will try to refresh it.
:return: the access token
:return: the access token or None if it cannot detect any credentials.
"""
if (
not self._cache.access_token
Expand All @@ -159,12 +162,19 @@ def acquire_and_cache_access_token(self):
and self._configured_refresh_token()
):
self.__exchange_refresh_for_access_token()
elif (
self.__vdk_config.api_token
and self.__vdk_config.api_token_authorization_url
):
self.update_api_token(self.__vdk_config.api_token)
self.update_api_token_authorization_url(
self.__vdk_config.api_token_authorization_url
)
self.__exchange_api_for_access_token()
else:
raise VDKException(
what="Not authenticated.",
why="Most likely VDK CLI login need to be completed first.",
consequence="Operation that require authorization will not work.",
countermeasure="Please execute login using VDK CLI login command",
log.debug(
"No authentication mechanism found. Will not cache access token."
"If Control Service authentication is enabled, API calls will fail."
)

def __exchange_refresh_for_access_token(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Copyright 2021 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
import click
from vdk.internal.control.auth.apikey_auth import ApiKeyAuthentication
from vdk.internal.control.auth.auth import Authentication
from vdk.internal.control.auth.login_types import LoginTypes
from vdk.internal.control.auth.redirect_auth import RedirectAuthentication
from vdk.internal.control.configuration.vdk_config import VDKConfig
from vdk.internal.control.exception.vdk_exception import VDKException
from vdk.internal.control.utils import cli_utils
from vdk.internal.control.utils.cli_utils import extended_option
Expand Down Expand Up @@ -131,11 +133,8 @@ def login(
countermeasure="Please login providing correct API Token. ",
)
else:
auth = Authentication()
auth.update_api_token_authorization_url(api_token_authorization_url)
auth.update_api_token(api_token)
auth.update_auth_type(auth_type)
auth.acquire_and_cache_access_token()
apikey_auth = ApiKeyAuthentication(api_token_authorization_url, api_token)
apikey_auth.authentication_process()
click.echo("Login Successful")
else:
click.echo(f"Login type: {auth_type} not supported")
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,17 @@ def local_config_folder(self) -> str:
"""
return os.getenv("VDK_BASE_CONFIG_FOLDER", str(Path.home()))

@property
def authentication_disabled(self) -> bool:
return os.getenv("VDK_AUTHENTICATION_DISABLE", "False").lower() in (
"true",
"1",
"t",
)

@property
def control_service_rest_api_url(self) -> str:
return os.getenv("VDK_CONTROL_SERVICE_REST_API_URL", None)

@property
def api_token_authorization_server_url(self) -> str:
def api_token_authorization_url(self) -> str:
"""
Location of the API Token OAuth2 provider. Same as login --api-token-authorization-server-url
This is used as default.
"""
return os.getenv("VDK_API_TOKEN_AUTHORIZATION_SERVER_URL", None)
return os.getenv("VDK_API_TOKEN_AUTHORIZATION_URL", None)

@property
def api_token(self) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,11 @@ def __init__(self, rest_api_url):
)
self.config.client_side_validation = False
self.config.verify_ssl = self.vdk_config.http_verify_ssl
if (
self.vdk_config.authentication_disabled
or load_default_authentication_disable()
):
log.info("Authentication is disabled.")
else:
auth = Authentication()
# For now there's no need to add auto-update since this is called usually in a shell script
# and each command will have short execution life even when multiple requests to API are made.
self.config.access_token = auth.read_access_token()

auth = Authentication()
# For now there's no need to add auto-update since this is called usually in a shell script
# and each command will have short execution life even when multiple requests to API are made.
self.config.access_token = auth.read_access_token()

def _new_api_client(self):
api_client = ApiClient(self.config)
Expand Down

0 comments on commit ecf0aaf

Please sign in to comment.