Skip to content

CLI Authentication #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 31, 2023
45 changes: 45 additions & 0 deletions src/codeflare_sdk/cli/cli_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import ast
import click
from kubernetes import client, config
import pickle

from codeflare_sdk.cluster.auth import _create_api_client_config
from codeflare_sdk.utils.kube_api_helpers import _kube_api_error_handling
import codeflare_sdk.cluster.auth as sdk_auth


class PythonLiteralOption(click.Option):
Expand All @@ -10,3 +16,42 @@ def type_cast_value(self, ctx, value):
return ast.literal_eval(value)
except:
raise click.BadParameter(value)


class AuthenticationConfig:
"""
Authentication configuration that will be stored in a file once
the user logs in using `codeflare login`
"""

def __init__(
self,
token: str,
server: str,
skip_tls: bool,
ca_cert_path: str,
):
self.api_client_config = _create_api_client_config(
token, server, skip_tls, ca_cert_path
)
self.server = server
self.token = token

def create_client(self):
return client.ApiClient(self.api_client_config)


def load_auth():
"""
Loads AuthenticationConfiguration and stores it in global variables
which can be used by the SDK for authentication
"""
try:
with open("auth", "rb") as file:
auth = pickle.load(file)
sdk_auth.api_client = auth.create_client()
return auth
except (IOError, EOFError):
click.echo("No authentication found, trying default kubeconfig")
except client.ApiException:
click.echo("Invalid authentication, trying default kubeconfig")
43 changes: 43 additions & 0 deletions src/codeflare_sdk/cli/commands/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import click
import pickle
from kubernetes import client

from codeflare_sdk.cluster.auth import TokenAuthentication
from codeflare_sdk.cli.cli_utils import AuthenticationConfig
import codeflare_sdk.cluster.auth as sdk_auth


@click.command()
@click.option("--server", "-s", type=str, required=True, help="Cluster API address")
@click.option("--token", "-t", type=str, required=True, help="Authentication token")
@click.option(
"--insecure-skip-tls-verify",
type=bool,
help="If true, server's certificate won't be checked for validity",
default=False,
)
@click.option(
"--certificate-authority",
type=str,
help="Path to cert file for certificate authority",
)
def cli(server, token, insecure_skip_tls_verify, certificate_authority):
"""
Login to your Kubernetes cluster and save login for subsequent use
"""
auth = TokenAuthentication(
token, server, insecure_skip_tls_verify, certificate_authority
)
auth.login()
if not sdk_auth.api_client: # TokenAuthentication failed
return

authConfig = AuthenticationConfig(
token,
server,
insecure_skip_tls_verify,
certificate_authority,
)
with open("auth", "wb") as file:
pickle.dump(authConfig, file)
click.echo(f"Logged into '{server}'")
17 changes: 17 additions & 0 deletions src/codeflare_sdk/cli/commands/logout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import click
import os
import pickle


@click.command()
def cli():
"""
Log out of current Kubernetes cluster
"""
try:
with open("auth", "rb") as file:
auth = pickle.load(file)
os.remove("auth")
click.echo(f"Successfully logged out of '{auth.server}'")
except:
click.echo("Not logged in")
35 changes: 24 additions & 11 deletions src/codeflare_sdk/cluster/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,11 @@ def login(self) -> str:
global config_path
global api_client
try:
configuration = client.Configuration()
configuration.api_key_prefix["authorization"] = "Bearer"
configuration.host = self.server
configuration.api_key["authorization"] = self.token
if self.skip_tls == False and self.ca_cert_path == None:
configuration.verify_ssl = True
elif self.skip_tls == False:
configuration.ssl_ca_cert = self.ca_cert_path
else:
configuration.verify_ssl = False
api_client = client.ApiClient(configuration)
api_client = client.ApiClient(
_create_api_client_config(
self.token, self.server, self.skip_tls, self.ca_cert_path
)
)
client.AuthenticationApi(api_client).get_api_group()
config_path = None
return "Logged into %s" % self.server
Expand Down Expand Up @@ -154,6 +148,25 @@ def load_kube_config(self):
return response


def _create_api_client_config(
token: str, server: str, skip_tls: bool = False, ca_cert_path: str = None
):
"""
Creates Kubernetes client configuration given necessary parameters
"""
configuration = client.Configuration()
configuration.api_key_prefix["authorization"] = "Bearer"
configuration.host = server
configuration.api_key["authorization"] = token
if skip_tls == False and ca_cert_path == None:
configuration.verify_ssl = True
elif skip_tls == False:
configuration.ssl_ca_cert = ca_cert_path
else:
configuration.verify_ssl = False
return configuration


def config_check() -> str:
"""
Function for loading the config file at the default config location ~/.kube/config if the user has not
Expand Down
45 changes: 45 additions & 0 deletions tests/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
export_env,
)
from codeflare_sdk.cli.codeflare_cli import cli
from codeflare_sdk.cli.cli_utils import load_auth
import codeflare_sdk.cluster.auth as sdk_auth

import openshift
from openshift.selector import Selector
Expand Down Expand Up @@ -108,6 +110,48 @@ def test_cluster_definition_cli():
)


def test_login_logout_cli(mocker):
runner = CliRunner()
mocker.patch.object(client, "ApiClient")
k8s_login_command = """
login
--server=testserver:6443
--token=testtoken
"""
login_result = runner.invoke(cli, k8s_login_command)
k8s_logout_command = "logout"
logout_result = runner.invoke(cli, k8s_logout_command)
assert login_result.output == "Logged into 'testserver:6443'\n"
assert logout_result.output == "Successfully logged out of 'testserver:6443'\n"


def test_login_tls_cli(mocker):
runner = CliRunner()
mocker.patch.object(client, "ApiClient")
k8s_tls_login_command = """
login
--server=testserver:6443
--token=testtoken
--insecure-skip-tls-verify=False
"""
k8s_skip_tls_login_command = """
login
--server=testserver:6443
--token=testtoken
--insecure-skip-tls-verify=True
"""
tls_result = runner.invoke(cli, k8s_tls_login_command)
skip_tls_result = runner.invoke(cli, k8s_skip_tls_login_command)
assert (
tls_result.output == skip_tls_result.output == "Logged into 'testserver:6443'\n"
)


def test_load_auth():
load_auth()
assert sdk_auth.api_client is not None


# For mocking openshift client results
fake_res = openshift.Result("fake")

Expand Down Expand Up @@ -2255,3 +2299,4 @@ def test_cleanup():
os.remove("test.yaml")
os.remove("raytest2.yaml")
os.remove("cli-test-cluster.yaml")
os.remove("auth")