Skip to content

Commit

Permalink
feat: Add instance support to tacl
Browse files Browse the repository at this point in the history
This commit add an instance support for TACL. You can user instances
with api_key for a client that is allowed instances authentication

example:

tacl p11 --api-key <api_key> --link-id <> --upload myfile
  • Loading branch information
kouylekov-usit committed Oct 24, 2024
1 parent 0314312 commit 0731844
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 7 deletions.
32 changes: 31 additions & 1 deletion tsdapiclient/authapi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

"""Module for the TSD Auth API."""

import json
from uuid import UUID
import requests
import time

Expand Down Expand Up @@ -45,6 +45,36 @@ def get_jwt_basic_auth(
msg = resp.text
raise AuthnError(msg)


def get_jwt_instance_auth(
env: str,
pnum: str,
api_key: str,
link_id: UUID,
secret_challenge: str|None = None,
token_type: str = "import",
) -> tuple:
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
url = f'{auth_api_url(env, pnum, "instances")}?type={token_type}'
try:
debug_step(f"POST {url}")
request_body = {"id": str(link_id)}
if secret_challenge:
request_body["secret_challenge"] = secret_challenge
resp = requests.post(url, headers=headers, data=json.dumps(request_body))
except Exception as e:
raise AuthnError from e
if resp.status_code in [200, 201]:
data = json.loads(resp.text)
return data.get("token"), data.get("refresh_token")
else:
if resp.status_code == 403:
msg = f"Instance auth not authorized from current IP address, contact USIT at {HELP_URL}"
else:
msg = resp.text
raise AuthnError(msg)


@handle_request_errors
def get_jwt_two_factor_auth(
env: str,
Expand Down
30 changes: 29 additions & 1 deletion tsdapiclient/guide.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

from tsdapiclient.tools import HELP_URL

topics = """
config
uploads
downloads
automation
instances
debugging
sync
encryption
Expand Down Expand Up @@ -137,6 +137,34 @@
Invoking tacl like this will over-ride any other local config.
"""

instances = f"""
In order to use the instances, you need to have a link ID and an api
key. To request an api key, you need to contact USIT at {HELP_URL}.
The link will be provided to you by the project owner.
The instances are currently only used for uploading data. To upload
data to with an instance, you can use the following command:
tacl p11 --api-key <api_key> --link-id <link_id> (--secret-challenge <secret_challenge>)? --upload myfile
where <link_id> is the link id provided to you by the project owner,
and <secret_challenge> is the secret challenge provided to you by the
project owner. The secret challenge is used to verify that the instance
is the correct one. The link id is used to identify the instance it can
be provided as UUID or a https link. Example of an https link is:
tacl p11 --api-key <api_key> --link-id https://data.tsd.usit.no/i/d3bd40e1-0a15-4575-9745-830ec52a4b3f --upload myfile
tacl p11 --api-key <api_key> --link-id https://data.tsd.usit.no/c/1154a666-4ae3-49e5-b1dd-cf1ea2cc86f9 --secret-challenge --upload myfile
where the 'c' and 'i' are the type of the link. The 'c' for instance
that requires a secret challenge and the 'i' for instance that does not
require a secret challenge. The UUID variant is the same as the https link
but without the https and the domain.
tacl p11 --api-key <api_key>--link-id d3bd40e1-0a15-4575-9745-830ec52a4b3f --upload myfile
tacl p11 --api-key <api_key> --link-id 1154a666-4ae3-49e5-b1dd-cf1ea2cc86f9 --secret-challenge secret --upload myfile
"""
debugging = f"""
If you are having trouble while running a command, check the version,
and run the problematic command in verbose mode:
Expand Down
39 changes: 35 additions & 4 deletions tsdapiclient/tacl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
import getpass
import os
import platform
import re
import sys

from dataclasses import dataclass
from textwrap import dedent
from typing import Optional
import uuid

import click
import requests

from tsdapiclient import __version__
from tsdapiclient.administrator import get_tsd_api_key
from tsdapiclient.authapi import get_jwt_two_factor_auth, get_jwt_basic_auth
from tsdapiclient.authapi import get_jwt_two_factor_auth, get_jwt_basic_auth, get_jwt_instance_auth
from tsdapiclient.client_config import ENV, CHUNK_THRESHOLD, CHUNK_SIZE
from tsdapiclient.configurer import (
read_config, update_config, print_config, delete_config,
Expand All @@ -37,7 +39,7 @@
export_delete,
)
from tsdapiclient.guide import (
topics, config, uploads, downloads, debugging, automation, sync, encryption,
topics, config, uploads, downloads, debugging, automation, sync, encryption, instances
)
from tsdapiclient.session import (
session_is_expired,
Expand Down Expand Up @@ -116,6 +118,7 @@
'downloads': downloads,
'debugging': debugging,
'automation': automation,
'instances': instances,
'sync': sync,
'encryption': encryption,
}
Expand Down Expand Up @@ -467,6 +470,18 @@ def construct_correct_upload_path(path: str) -> str:
default=None,
help='Pass an explicit API key, pasting the key or as a path to a file: --api-key @path-to-file'
)
@click.option(
'--link-id',
required=False,
default=None,
help='Pass a download link obtained from the TSD API. This must be used with --api-key as well as it requires a specific client'
)
@click.option(
'--secret-challenge',
required=False,
default=None,
help='Pass a secret challenge for instance authentication'
)
@click.option(
'--encrypt',
is_flag=True,
Expand Down Expand Up @@ -522,6 +537,8 @@ def cli(
keep_updated: bool,
download_delete: str,
api_key: str,
link_id: str,
secret_challenge: str,
encrypt: bool,
chunk_size: int,
resumable_threshold: int,
Expand All @@ -545,7 +562,7 @@ def cli(
if basic or api_key:
requires_user_credentials, token_type = False, TOKENS[env]['upload']
else:
requires_user_credentials, token_type = True, TOKENS[env]['upload']
requires_user_credentials, token_type = False if link_id else True, TOKENS[env]['upload']
elif download or download_list or download_sync or download_delete:
if env == 'alt' and basic:
requires_user_credentials, token_type = False, TOKENS[env]['download']
Expand Down Expand Up @@ -619,7 +636,21 @@ def cli(
debug_step("API key has expired")
api_key = renew_api_key(env, pnum, api_key, key_file)
debug_step('using basic authentication')
token, refresh_token = get_jwt_basic_auth(env, pnum, api_key, token_type)
if link_id:

if link_id.startswith("https://"):
click.echo("extracting link_id from URL")
patten = r"https://(?P<HOST>.+)/(?P<instance_type>c|i)/(?P<link_id>[a-f\d0-9-]{36})"
if match:=re.compile(patten).match(link_id):
link_id = uuid.UUID(match.group("link_id"))
if match.group("instance_type") == "c" and not secret_challenge:
click.echo("instance authentication requires a secret challenge")
sys.exit(1)
else:
link_id = uuid.UUID(link_id)
token, refresh_token = get_jwt_instance_auth(env, pnum, api_key, link_id, secret_challenge, token_type)
else:
token, refresh_token = get_jwt_basic_auth(env, pnum, api_key, token_type)
if (requires_user_credentials or basic) and not token:
click.echo('authentication failed')
sys.exit(1)
Expand Down
4 changes: 3 additions & 1 deletion tsdapiclient/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ def auth_api_url(env: str, pnum: str, auth_method: str) -> str:
endpoints = {
'default': {
'basic': f'{pnum}/auth/basic/token',
'instances': '/all/auth/instances/token',
'tsd': f'{pnum}/auth/tsd/token',
'iam': f'{pnum}/auth/iam/token',
'refresh': f'{pnum}/auth/refresh/token',
'renew': f'{pnum}/auth/clients/secret',
},
'int': {
'basic': f'{pnum}/internal/basic/token',
'instances': '/all/internal/auth/instances/token',
'tsd': f'{pnum}/internal/tsd/token',
'refresh': f'{pnum}/auth/refresh/token',
'renew': f'{pnum}/auth/client/secret',
Expand All @@ -64,7 +66,7 @@ def auth_api_url(env: str, pnum: str, auth_method: str) -> str:
}
try:
if auth_method not in [
'basic', 'tsd', 'iam', 'refresh', 'renew',
'basic', 'tsd', 'iam', 'refresh', 'renew', 'instances'
]:
raise Exception(f'Unrecognised auth_method: {auth_method}')
host = HOSTS.get(env)
Expand Down

0 comments on commit 0731844

Please sign in to comment.