Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/python/kfp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from . import components
from . import containers
from . import dsl
from . import auth
from ._client import Client
from ._config import *
from ._local_client import LocalClient
from ._runners import *
from ._credentials import *
32 changes: 31 additions & 1 deletion sdk/python/kfp/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import yaml
import zipfile
import datetime
import copy
from typing import Mapping, Callable, Optional

import kfp_server_api
Expand Down Expand Up @@ -219,7 +220,8 @@ def _load_config(self, host, client_id, namespace, other_client_id, other_client
token = get_gcp_access_token()
self._is_refresh_token = False
elif credentials:
token = credentials.get_token()
config.api_key['authorization'] = 'placeholder'
config.api_key_prefix['authorization'] = 'Bearer'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker for this PR, from #5945 context, we should not add bearer authorization either.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I think we should write a base class for bearer authorization.
Then e.g. others can write a base class for basic authentication

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bobgy unfortunately I don't think we can do this.

If you check the links I've included in #5138 (comment) explaining the Configuration's codebase, refresh_api_key_hook is only triggered when authorization is in api_key (source).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great catch! I didn't know the limitation. Let's leave that use case in the backlog and get this PR merged first.

config.refresh_api_key_hook = credentials.refresh_api_key_hook

if token:
Expand All @@ -241,6 +243,7 @@ def _load_config(self, host, client_id, namespace, other_client_id, other_client

if in_cluster:
config.host = Client.IN_CLUSTER_DNS_NAME.format(namespace)
config = self._get_config_with_default_credentials(config)
return config

try:
Expand Down Expand Up @@ -302,6 +305,33 @@ def _refresh_api_client_token(self):
new_token = get_gcp_access_token()
self._existing_config.api_key['authorization'] = new_token

def _get_config_with_default_credentials(self, config):
"""Apply default credentials to the configuration object.

This method accepts a Configuration object and extends it with some default
credentials interface.
"""
# XXX: The default credentials are audience-based service account tokens
# projected by the kubelet (ServiceAccountTokenVolumeCredentials). As we
# implement more and more credentials, we can have some heuristic and
# choose from a number of options.
# See https://github.com/kubeflow/pipelines/pull/5287#issuecomment-805654121
from kfp import auth
credentials = auth.ServiceAccountTokenVolumeCredentials()
config_copy = copy.deepcopy(config)

try:
credentials.refresh_api_key_hook(config_copy)
except Exception:
logging.warning("Failed to set up default credentials. Proceeding"
" without credentials...")
return config

config.refresh_api_key_hook = credentials.refresh_api_key_hook
config.api_key_prefix['authorization'] = 'Bearer'
config.refresh_api_key_hook(config)
return config

def set_user_namespace(self, namespace):
"""Set user namespace into local context setting file.

Expand Down
19 changes: 19 additions & 0 deletions sdk/python/kfp/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2021 Arrikto Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from ._tokencredentialsbase import TokenCredentialsBase, read_token_from_file
from ._satvolumecredentials import ServiceAccountTokenVolumeCredentials

KF_PIPELINES_SA_TOKEN_ENV = "KF_PIPELINES_SA_TOKEN_PATH"
KF_PIPELINES_SA_TOKEN_PATH = "/var/run/secrets/kubeflow/pipelines/token"
69 changes: 69 additions & 0 deletions sdk/python/kfp/auth/_satvolumecredentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2021 Arrikto Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import logging

from kubernetes.client import configuration

from kfp import auth


class ServiceAccountTokenVolumeCredentials(auth.TokenCredentialsBase):
"""Audience-bound ServiceAccountToken in the local filesystem.

This is a credentials interface for audience-bound ServiceAccountTokens
found in the local filesystem, that get refreshed by the kubelet.

The constructor of the class expects a filesystem path.
If not provided, it uses the path stored in the environment variable
defined in ``auth.KF_PIPELINES_SA_TOKEN_ENV``.
If the environment variable is also empty, it falls back to the path
specified in ``auth.KF_PIPELINES_SA_TOKEN_PATH``.

This method of authentication is meant for use inside a Kubernetes cluster.

Relevant documentation:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection
"""

def __init__(self, path=None):
self._token_path = (path
or os.getenv(auth.KF_PIPELINES_SA_TOKEN_ENV)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @elikatsis. Shouldn't this be os.getenv(KF_PIPELINES_SA_TOKEN_ENV) ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be like this based on the code you wrote above, but that's not the case in this branch. At least not yet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Actually, I misunderstood KF_PIPELINES_SA_TOKEN_ENV to be pod environment variable but instead it is refering to the Path. I get that now. Thanks @elikatsis 🚀

or auth.KF_PIPELINES_SA_TOKEN_PATH)

def _get_token(self):
token = None
try:
token = auth.read_token_from_file(self._token_path)
except OSError as e:
logging.error("Failed to read a token from file '%s' (%s).",
self._token_path, str(e))
raise
return token

def refresh_api_key_hook(self, config: configuration.Configuration):
"""Refresh the api key.

This is a helper function for registering token refresh with swagger
generated clients.

Args:
config (kubernetes.client.configuration.Configuration):
The configuration object that the client uses.

The Configuration object of the kubernetes client's is the same
with kfp_server_api.configuration.Configuration.
"""
config.api_key["authorization"] = self._get_token()
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,34 @@

import abc

from kubernetes.client.configuration import Configuration


__all__ = [
"TokenCredentialsBase",
]
from kubernetes.client import configuration


class TokenCredentialsBase(abc.ABC):

def refresh_api_key_hook(self, config: Configuration):
@abc.abstractmethod
def refresh_api_key_hook(self, config: configuration.Configuration):
"""Refresh the api key.

This is a helper function for registering token refresh with swagger
generated clients.

All classes that inherit from TokenCredentialsBase must implement this
method to refresh the credentials.

Args:
config (kubernetes.client.configuration.Configuration):
The configuration object that the client uses.

The Configuration object of the kubernetes client's is the same
with kfp_server_api.configuration.Configuration.
"""
config.api_key["authorization"] = self.get_token()

@abc.abstractmethod
def get_token(self):
raise NotImplementedError()


def read_token_from_file(path=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this better be placed in _satvolumecredentials.py, since this is where it is used? This can then also be removed from the import in __init__.py.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan is to use this function in multiple places and implementations of TokenCredentials. For instance, we do use it internally for another implementation of credentials we've got

"""Read a token found in some file."""
token = None
with open(path, "r") as f:
token = f.read().strip()
return token
1 change: 1 addition & 0 deletions sdk/python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def find_version(*file_path_parts):
tests_require=TESTS_REQUIRE,
packages=[
'kfp',
'kfp.auth',
'kfp.cli',
'kfp.cli.diagnose_me',
'kfp.compiler',
Expand Down