Skip to content

Commit

Permalink
Merge branch '2.0-alpha1' into 2.0-alpha1-update
Browse files Browse the repository at this point in the history
  • Loading branch information
tdviet authored Nov 1, 2023
2 parents e6c9ec2 + d8d5955 commit 33a6c28
Show file tree
Hide file tree
Showing 16 changed files with 1,109 additions and 259 deletions.
156 changes: 156 additions & 0 deletions fedcloudclient/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""
Class for managing tokens
"""

import jwt
import liboidcagent as agent
import requests

from fedcloudclient.conf import CONF as CONF
from fedcloudclient.exception import TokenError
from fedcloudclient.logger import log_and_raise


class Token:
"""
Abstract object for managing tokens
"""

def get_token(self):
...

def get_token_type(self):
...


class OIDCToken(Token):
"""
OIDC tokens. Managing access tokens, oidc-agent account and mytoken
"""

def __init__(self, access_token=None):
super().__init__()
self.access_token = access_token
self.payload = None
self.oidc_agent_account = None
self.mytoken = None
self.user_id = None

def get_token(self):
"""
Return access token or raise error
:return:
"""
if self.access_token:
return self.access_token
else:
error_msg = "Token is not initialized"
log_and_raise(error_msg, TokenError)

def decode_token(self) -> dict:
"""
Decoding access token to payload
:return:
"""
if not self.payload:
try:
self.payload = jwt.decode(self.access_token, options={"verify_signature": False})
self.user_id = self.payload["sub"]
except jwt.exceptions.InvalidTokenError:
error_msg = "Invalid access token"
log_and_raise(error_msg, TokenError)

return self.payload

def get_user_id(self) -> str:
"""
Return use ID
:return:
"""
if not self.payload:
self.decode_token()
return self.user_id

def get_token_from_oidc_agent(self, oidc_agent_account: str) -> str:
"""
Get access token from oidc-agent
:param oidc_agent_account: account name in oidc-agent
:return: access token, and set internal token, raise TokenError on None
"""

if oidc_agent_account:
try:
access_token = agent.get_access_token(
oidc_agent_account,
min_valid_period=CONF.get("min_access_token_time"),
application_hint="fedcloudclient",
)
self.access_token = access_token
self.oidc_agent_account = oidc_agent_account
return access_token
except agent.OidcAgentError as exception:
error_msg = f"Error getting access token from oidc-agent: {exception}"
log_and_raise(error_msg, TokenError)

else:
error_msg = f"Error getting access token from oidc-agent: account name is {oidc_agent_account}"
log_and_raise(error_msg, TokenError)

def get_token_from_mytoken(self, mytoken: str, mytoken_server: str = None) -> str:
"""
Get access token from mytoken server
:param mytoken:
:param mytoken_server:
:return: access token, or None on error
"""
if not mytoken_server:
mytoken_server = CONF.get("mytoken_server")

if mytoken:
try:
data = {
"grant_type": "mytoken",
"mytoken": mytoken,
}
req = requests.post(
mytoken_server + "/api/v0/token/access",
json=data,
)
req.raise_for_status()
access_token = req.json().get("access_token")
self.access_token = access_token
self.mytoken = mytoken
return access_token

except requests.exceptions.HTTPError as exception:
error_msg = f"Error getting access token from mytoken server: {exception}"
log_and_raise(error_msg, TokenError)

else:
error_msg = f"Error getting access token from mytoken server: mytoken is {mytoken}"
log_and_raise(error_msg, TokenError)

def multiple_token(self, access_token: str, oidc_agent_account: str, mytoken: str):
"""
Select valid token from multiple options
:param access_token:
:param oidc_agent_account:
:param mytoken:
:return:
"""
if mytoken:
try:
self.get_token_from_mytoken(mytoken)
return
except TokenError:
pass
if oidc_agent_account:
try:
self.get_token_from_oidc_agent(oidc_agent_account)
return
except TokenError:
pass
if access_token:
self.access_token = access_token
return
log_and_raise("Cannot get access token", TokenError)
24 changes: 24 additions & 0 deletions fedcloudclient/auth_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Testing unit for auth.py
"""
import os

import fedcloudclient.auth as auth


def get_token_from_mytoken_decode_verify(mytoken: str, user_id: str):
"""
Get access token from mytoken server, decode, get user ID and verify
:return:
"""

token = auth.OIDCToken()
token.get_token_from_mytoken(mytoken)
token_id = token.get_user_id()
assert token_id == user_id


if __name__ == "__main__":
mytoken = os.environ["FEDCLOUD_MYTOKEN"]
user_id = os.environ["FEDCLOUD_ID"]
get_token_from_mytoken_decode_verify(mytoken, user_id)
3 changes: 2 additions & 1 deletion fedcloudclient/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import click

from fedcloudclient.checkin import token
from fedcloudclient.conf import config
from fedcloudclient.ec3 import ec3
from fedcloudclient.endpoint import endpoint
from fedcloudclient.openstack import openstack, openstack_int
Expand All @@ -29,7 +30,7 @@ def cli():
cli.add_command(select)
cli.add_command(openstack)
cli.add_command(openstack_int)

cli.add_command(config)

if __name__ == "__main__":
cli()
160 changes: 160 additions & 0 deletions fedcloudclient/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""
Read/write configuration files
"""
import json
import os
import sys
from pathlib import Path

import click
import yaml
from tabulate import tabulate

from fedcloudclient.exception import ConfigError

DEFAULT_CONFIG_LOCATION = Path.home() / ".config/fedcloud/config.yaml"
DEFAULT_SETTINGS = {
"site": "IISAS-FedCloud",
"vo": "vo.access.egi.eu",
"site_list_url": "https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml",
"site_dir": str(Path.home() / ".config/fedcloud/site-config/"),
"oidc_url": "https://aai.egi.eu/auth/realms/egi",
"openstack_auth_protocol": "openid",
"openstack_auth_provider": "egi.eu",
"openstack_auth_type": "v3oidcaccesstoken",
"gocdb_public_url": "https://goc.egi.eu/gocdbpi/public/",
"gocdb_service_group": "org.openstack.nova",
"vault_endpoint": "https://vault.services.fedcloud.eu:8200",
"vault_role": "",
"vault_mount_point": "/secrets/",
"vault_locker_mount_point": "/v1/cubbyhole/",
"vault_salt": "fedcloud_salt",
"log_file": str(Path.home() / ".config/fedcloud/logs/fedcloud.log"),
"log_level": "DEBUG",
"log_config_file": str(Path.home() / ".config/fedcloud/logging.conf"),
"requests_cert_file": str(Path.home() / ".config/fedcloud/cert/certs.pem"),
"oidc_agent_account": "egi",
"min_access_token_time": 30,
"mytoken_server": "https://mytoken.data.kit.edu",
}


def save_config(filename: str, config_data: dict):
"""
Save configuration to file
:param filename: name of config file
:param config_data: dict containing configuration
:return: None
"""
config_file = Path(filename).resolve()
try:
with config_file.open(mode="w+", encoding="utf-8") as file:
yaml.dump(config_data, file)
except yaml.YAMLError as exception:
error_msg = f"Error during saving configuration to {filename}: {exception}"
raise ConfigError(error_msg)


def load_config(filename: str) -> dict:
"""
Load configuration file
:param filename:
:return: configuration data
"""

config_file = Path(filename).resolve()

if config_file.is_file():
try:
with config_file.open(mode="r", encoding="utf-8") as file:
return yaml.safe_load(file)
except yaml.YAMLError as exception:
error_msg = f"Error during loading configuration from {filename}: {exception}"
raise ConfigError(error_msg)
else:
return {}


def load_env() -> dict:
"""
Load configs from environment variables
:return: config
"""
env_config = dict()
for env in os.environ:
if env.startswith("FEDCLOUD_"):
config_key = env[9:].lower()
env_config[config_key] = os.environ[env]
return env_config


def init_config() -> dict:
"""
Init config moduls
:return: actual config
"""
env_config = load_env()
config_file = env_config.get("config_file", DEFAULT_CONFIG_LOCATION)

try:
saved_config = load_config(config_file)
except ConfigError as exception:
print(f"Error while loading config: {exception}")
saved_config = {}

act_config = {**DEFAULT_SETTINGS, **saved_config, **env_config}
return act_config


@click.group()
def config():
"""
Managing fedcloud configurations
"""


@config.command()
@click.option(
"--config-file",
type=click.Path(dir_okay=False),
default=DEFAULT_CONFIG_LOCATION,
help="configuration file",
envvar="FEDCLOUD_CONFIG_FILE",
show_default=True,
)
def create(config_file: str):
"""Create default configuration file"""
save_config(config_file, DEFAULT_SETTINGS)
print(f"Default configuration is saved in {config_file}")


@config.command()
@click.option(
"--config-file",
type=click.Path(dir_okay=False),
default=DEFAULT_CONFIG_LOCATION,
help="configuration file",
envvar="FEDCLOUD_CONFIG_FILE",
show_default=True,
)
@click.option(
"--output-format",
"-f",
required=False,
help="Output format",
type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False),
)
def show(config_file: str, output_format: str):
"""Show actual client configuration """
saved_config = load_config(config_file)
env_config = load_env()
act_config = {**DEFAULT_SETTINGS, **saved_config, **env_config}
if output_format == "YAML":
yaml.dump(act_config, sys.stdout, sort_keys=False)
elif output_format == "JSON":
json.dump(act_config, sys.stdout, indent=4)
else:
print(tabulate(act_config.items(), headers=["parameter", "value"]))


CONF = init_config()
Loading

0 comments on commit 33a6c28

Please sign in to comment.