diff --git a/README.md b/README.md index 68b169d..7787730 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It can also be set up to transparently proxy an upstream Galaxy server, storing This project is _heavily_ inspired by [amanda](https://github.com/sivel/amanda/). # Artifactory compatibility -For the time being some features may require an Artifactory Pro license. Work is being done to workaround the (many) limitations that JFrog has placed on API calls. [These limitations have also complicated the ability to run integration tests](https://github.com/briantist/galactory/issues/6). As a result, the test of whether all Pro license dependent API calls have been rooted out, will be whether test coverage exists that can run against Artifactory OSS. +All features of galactory should work with the free-of-cost Artifactory OSS. Please report any usage that appears to require a Pro license. # How to use There isn't any proper documentation yet. The help output is below. @@ -33,9 +33,12 @@ usage: python -m galactory [-h] [-c CONFIG] [--listen-addr LISTEN_ADDR] [--listen-port LISTEN_PORT] [--server-name SERVER_NAME] [--preferred-url-scheme PREFERRED_URL_SCHEME] --artifactory-path ARTIFACTORY_PATH - [--artifactory-api-key ARTIFACTORY_API_KEY] [--use-galaxy-key] - [--prefer-configured-key] [--publish-skip-configured-key] - [--log-file LOG_FILE] + [--artifactory-api-key ARTIFACTORY_API_KEY] + [--artifactory-access-token ARTIFACTORY_ACCESS_TOKEN] + [--use-galaxy-key] [--use-galaxy-auth] + [--galaxy-auth-type {api_key,access_token}] [--prefer-configured-key] + [--prefer-configured-auth] [--publish-skip-configured-key] + [--publish-skip-configured-auth] [--log-file LOG_FILE] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--log-headers] [--log-body] [--proxy-upstream PROXY_UPSTREAM] [-npns NO_PROXY_NAMESPACE] [--cache-minutes CACHE_MINUTES] @@ -65,16 +68,39 @@ optional arguments: The URL of the path in Artifactory where collections are stored. [env var: GALACTORY_ARTIFACTORY_PATH] --artifactory-api-key ARTIFACTORY_API_KEY - If set, is the API key used to access Artifactory. + If set, is the API key used to access Artifactory. If set with artifactory-access-token, this + value will not be used. [env var: GALACTORY_ARTIFACTORY_API_KEY] - --use-galaxy-key If set, uses the Galaxy token as the Artifactory API key. + --artifactory-access-token ARTIFACTORY_ACCESS_TOKEN + If set, is the Access Token used to access Artifactory. If set with artifactory-api-key, this + value will be used and the API key will be ignored. + [env var: GALACTORY_ARTIFACTORY_ACCESS_TOKEN] + --use-galaxy-key If set, uses the Galaxy token sent in the request as the Artifactory auth. DEPRECATED: This + option will be removed in v0.11.0. Please use --use-galaxy-auth going forward. [env var: GALACTORY_USE_GALAXY_KEY] + --use-galaxy-auth If set, uses the Galaxy token sent in the request as the Artifactory auth. + [env var: GALACTORY_USE_GALAXY_AUTH] + --galaxy-auth-type {api_key,access_token} + Auth received via a Galaxy request should be interpreted as this type of auth. + [env var: GALACTORY_GALAXY_AUTH_TYPE] --prefer-configured-key - If set, prefer the confgured Artifactory key over the Galaxy token. + If set, prefer the confgured Artifactory auth over the Galaxy token. + DEPRECATED: This option will be removed in v0.11.0. + Please use --prefer-configured-auth going forward. [env var: GALACTORY_PREFER_CONFIGURED_KEY] - --publish-skip-configured-key - If set, publish endpoint will not use a configured key, only Galaxy token. + --prefer-configured-auth + If set, prefer the confgured Artifactory auth over the Galaxy token. + [env var: GALACTORY_PREFER_CONFIGURED_AUTH] + --publish-skip-configured-key + If set, publish endpoint will not use configured auth, only auth included in a Galaxy + request. + DEPRECATED: This option will be removed in v0.11.0. + Please use --publish-skip-configured-auth going forward. [env var: GALACTORY_PUBLISH_SKIP_CONFIGURED_KEY] + --publish-skip-configured-auth + If set, publish endpoint will not use configured auth, only auth included in a Galaxy + request. + [env var: GALACTORY_PUBLISH_SKIP_CONFIGURED_AUTH] --log-file LOG_FILE If set, logging will go to this file instead of the console. [env var: GALACTORY_LOG_FILE] --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL} @@ -96,8 +122,8 @@ optional arguments: Look for upsteam caches and use their values. [env var: GALACTORY_CACHE_READ] --cache-write CACHE_WRITE - Populate the upstream cache in Artifactory. Should be false when no API key is - provided or the key has no permission to write. + Populate the upstream cache in Artifactory. Should be false when no auth is + provided or the auth has no permission to write. [env var: GALACTORY_CACHE_WRITE] --use-property-fallback Set properties of an uploaded collection in a separate request after publshinng. diff --git a/changelogs/fragments/77-access-token-bearer-auth-support.yml b/changelogs/fragments/77-access-token-bearer-auth-support.yml new file mode 100644 index 0000000..13a64a0 --- /dev/null +++ b/changelogs/fragments/77-access-token-bearer-auth-support.yml @@ -0,0 +1,9 @@ +--- +minor_changes: + - Add support for Artifactory Access Tokens (bearer auth) in both configured auth and galaxy requests, via the new ``ARTIFACTORY_ACCESS_TOKEN`` and ``GALAXY_AUTH_TYPE`` configuration options (https://github.com/briantist/galactory/pull/77). + +deprecated_features: + - The default value of the new ``GALAXY_AUTH_TYPE`` configuration option, added in this release, will change from ``api_key`` to ``access_token`` in ``v0.11.0`` (https://github.com/briantist/galactory/pull/77). + - The ``PREFER_CONFIGURED_KEY`` configuration option has been replaced by ``PREFER_CONFIGURED_AUTH`` and the old name will be removed in ``v0.11.0`` (https://github.com/briantist/galactory/pull/77). + - The ``USE_GALAXY_KEY`` configuration option has been replaced by ``USE_GALAXY_AUTH`` and the old name will be removed in ``v0.11.0`` (https://github.com/briantist/galactory/pull/77). + - The ``PUBLISH_SKIP_CONFIGURED_KEY`` configuration option has been replaced by ``PUBLISH_SKIP_CONFIGURED_AUTH`` and the old name will be removed in ``v0.11.0`` (https://github.com/briantist/galactory/pull/77). diff --git a/galactory/__init__.py b/galactory/__init__.py index 408821b..d32d6fa 100644 --- a/galactory/__init__.py +++ b/galactory/__init__.py @@ -2,6 +2,7 @@ # (c) 2022 Brian Scholer (@briantist) import logging +import warnings from flask import Flask, request from werkzeug.middleware.proxy_fix import ProxyFix @@ -72,10 +73,15 @@ def create_configured_app(run=False, parse_known_only=True, parse_allow_abbrev=F parser.add_argument('--server-name', type=str, env_var='GALACTORY_SERVER_NAME', help='The host name and port of the server, as seen from clients. Used for generating links.') parser.add_argument('--preferred-url-scheme', type=str, env_var='GALACTORY_PREFERRED_URL_SCHEME', help='Sets the preferred scheme to use when constructing URLs. Defaults to the request scheme, but is unaware of reverse proxies.') parser.add_argument('--artifactory-path', type=str, required=True, env_var='GALACTORY_ARTIFACTORY_PATH', help='The URL of the path in Artifactory where collections are stored.') - parser.add_argument('--artifactory-api-key', type=str, env_var='GALACTORY_ARTIFACTORY_API_KEY', help='If set, is the API key used to access Artifactory.') - parser.add_argument('--use-galaxy-key', action='store_true', env_var='GALACTORY_USE_GALAXY_KEY', help='If set, uses the Galaxy token as the Artifactory API key.') - parser.add_argument('--prefer-configured-key', action='store_true', env_var='GALACTORY_PREFER_CONFIGURED_KEY', help='If set, prefer the confgured Artifactory key over the Galaxy token.') - parser.add_argument('--publish-skip-configured-key', action='store_true', env_var='GALACTORY_PUBLISH_SKIP_CONFIGURED_KEY', help='If set, publish endpoint will not use a configured key, only Galaxy token.') + parser.add_argument('--artifactory-api-key', type=str, env_var='GALACTORY_ARTIFACTORY_API_KEY', help='If set, is the API key used to access Artifactory. If set with artifactory-access-token, this value will not be used.') + parser.add_argument('--artifactory-access-token', type=str, env_var='GALACTORY_ARTIFACTORY_ACCESS_TOKEN', help='If set, is the Access Token used to access Artifactory. If set with artifactory-api-key, this value will be used and the API key will be ignored.') + parser.add_argument('--use-galaxy-key', action='store_true', env_var='GALACTORY_USE_GALAXY_KEY', help='If set, uses the Galaxy token sent in the request as the Artifactory auth. DEPRECATED: This option will be removed in v0.11.0. Please use --use-galaxy-auth going forward.') + parser.add_argument('--use-galaxy-auth', action='store_true', env_var='GALACTORY_USE_GALAXY_AUTH', help='If set, uses the Galaxy token sent in the request as the Artifactory auth.') + parser.add_argument('--galaxy-auth-type', type=str, env_var='GALACTORY_GALAXY_AUTH_TYPE', choices=['api_key', 'access_token'], help='Auth received via a Galaxy request should be interpreted as this type of auth.') + parser.add_argument('--prefer-configured-key', action='store_true', env_var='GALACTORY_PREFER_CONFIGURED_KEY', help='If set, prefer the confgured Artifactory auth over the Galaxy token. DEPRECATED: This option will be removed in v0.11.0. Please use --prefer-configured-auth going forward.') + parser.add_argument('--prefer-configured-auth', action='store_true', env_var='GALACTORY_PREFER_CONFIGURED_AUTH', help='If set, prefer the confgured Artifactory auth over the Galaxy token.') + parser.add_argument('--publish-skip-configured-key', action='store_true', env_var='GALACTORY_PUBLISH_SKIP_CONFIGURED_KEY', help='If set, publish endpoint will not use configured auth, only auth included in a Galaxy request. DEPRECATED: This option will be removed in v0.11.0. Please use --publish-skip-configured-auth going forward.') + parser.add_argument('--publish-skip-configured-auth', action='store_true', env_var='GALACTORY_PUBLISH_SKIP_CONFIGURED_AUTH', help='If set, publish endpoint will not use configured auth, only auth included in a Galaxy request.') parser.add_argument('--log-file', type=str, env_var='GALACTORY_LOG_FILE', help='If set, logging will go to this file instead of the console.') parser.add_argument( '--log-level', @@ -90,7 +96,7 @@ def create_configured_app(run=False, parse_known_only=True, parse_allow_abbrev=F parser.add_argument('-npns', '--no-proxy-namespace', action='append', default=[], env_var='GALACTORY_NO_PROXY_NAMESPACE', help='Requests for this namespace should never be proxied. Can be specified multiple times.') parser.add_argument('--cache-minutes', default=60, type=int, env_var='GALACTORY_CACHE_MINUTES', help='The time period that a cache entry should be considered valid.') parser.add_argument('--cache-read', action=_StrBool, default=True, env_var='GALACTORY_CACHE_READ', help='Look for upsteam caches and use their values.') - parser.add_argument('--cache-write', action=_StrBool, default=True, env_var='GALACTORY_CACHE_WRITE', help='Populate the upstream cache in Artifactory. Should be false when no API key is provided or the key has no permission to write.') + parser.add_argument('--cache-write', action=_StrBool, default=True, env_var='GALACTORY_CACHE_WRITE', help='Populate the upstream cache in Artifactory. Should be false when no auth is provided or the auth has no permission to write.') parser.add_argument('--use-property-fallback', action='store_true', env_var='GALACTORY_USE_PROPERTY_FALLBACK', help='Set properties of an uploaded collection in a separate request after publshinng. Requires a Pro license of Artifactory. This feature is a workaround for an Artifactory proxy configuration error and may be removed in a future version.') parser.add_argument('--health-check-custom-text', type=str, default='', env_var='GALACTORY_HEALTH_CHECK_CUSTOM_TEXT', help='Sets custom_text field for health check endpoint responses.') @@ -101,6 +107,55 @@ def create_configured_app(run=False, parse_known_only=True, parse_allow_abbrev=F logging.basicConfig(filename=args.log_file, level=args.log_level) + # TODO: v0.11.0 - remove conditional old name + if args.use_galaxy_key and not args.use_galaxy_auth: + use_galaxy_auth = True + warnings.warn( + message=( + "USE_GALAXY_KEY has been replaced by USE_GALAXY_AUTH and the old name will be removed in v0.11.0." + " To suppress this warning, set USE_GALAXY_AUTH." + ), category=DeprecationWarning, stacklevel=2 + ) + else: + use_galaxy_auth = args.use_galaxy_auth + + # TODO: v0.11.0 - remove conditional & warning, set default on argument + if args.galaxy_auth_type is None and use_galaxy_auth: + galaxy_auth_type = 'api_key' + warnings.warn( + message=( + "USE_GALAXY_AUTH is True but GALAXY_AUTH_TYPE is not set." + " The default value used will be 'api_key' for backward compatibility, but will change to 'access_token' in v0.11.0." + " To suppress this warning, set an explicit value." + ), category=FutureWarning, stacklevel=2 + ) + else: + galaxy_auth_type = args.galaxy_auth_type + + # TODO: v0.11.0 - remove conditional old name + if args.prefer_configured_key and not args.prefer_configured_auth: + prefer_configured_auth = True + warnings.warn( + message=( + "PREFER_CONFIGURED_KEY has been replaced by PREFER_CONFIGURED_AUTH and the old name will be removed in v0.11.0." + " To suppress this warning, set PREFER_CONFIGURED_AUTH." + ), category=DeprecationWarning, stacklevel=2 + ) + else: + prefer_configured_auth = args.prefer_configured_auth + + # TODO: v0.11.0 - remove conditional old name + if args.publish_skip_configured_key and not args.publish_skip_configured_auth: + publish_skip_configured_auth = True + warnings.warn( + message=( + "PUBLISH_SKIP_CONFIGURED_KEY has been replaced by PUBLISH_SKIP_CONFIGURED_AUTH and the old name will be removed in v0.11.0." + " To suppress this warning, set PUBLISH_SKIP_CONFIGURED_AUTH." + ), category=DeprecationWarning, stacklevel=2 + ) + else: + publish_skip_configured_auth = args.publish_skip_configured_auth + app = create_app( ARTIFACTORY_PATH=ArtifactoryPath(args.artifactory_path), LOG_HEADERS=args.log_headers, @@ -108,9 +163,11 @@ def create_configured_app(run=False, parse_known_only=True, parse_allow_abbrev=F PROXY_UPSTREAM=args.proxy_upstream, NO_PROXY_NAMESPACES=args.no_proxy_namespace, ARTIFACTORY_API_KEY=args.artifactory_api_key, - USE_GALAXY_KEY=args.use_galaxy_key, - PREFER_CONFIGURED_KEY=args.prefer_configured_key, - PUBLISH_SKIP_CONFIGURED_KEY=args.publish_skip_configured_key, + ARTIFACTORY_ACCESS_TOKEN=args.artifactory_access_token, + USE_GALAXY_AUTH=use_galaxy_auth, + GALAXY_AUTH_TYPE=galaxy_auth_type, + PREFER_CONFIGURED_AUTH=prefer_configured_auth, + PUBLISH_SKIP_CONFIGURED_AUTH=publish_skip_configured_auth, SERVER_NAME=args.server_name, PREFERRED_URL_SCHEME=args.preferred_url_scheme, CACHE_MINUTES=args.cache_minutes, diff --git a/galactory/api/v2/collections.py b/galactory/api/v2/collections.py index 029a259..d83cd9a 100644 --- a/galactory/api/v2/collections.py +++ b/galactory/api/v2/collections.py @@ -174,11 +174,11 @@ def version(namespace, collection, version): def publish(): sha256 = request.form['sha256'] file = request.files['file'] - skip_configured_key = current_app.config['PUBLISH_SKIP_CONFIGURED_KEY'] + skip_configured_auth = current_app.config['PUBLISH_SKIP_CONFIGURED_AUTH'] property_fallback = current_app.config.get('USE_PROPERTY_FALLBACK', False) _scheme = current_app.config.get('PREFERRED_URL_SCHEME') - target = authorize(request, current_app.config['ARTIFACTORY_PATH'] / file.filename, skip_configured_key=skip_configured_key) + target = authorize(request, current_app.config['ARTIFACTORY_PATH'] / file.filename, skip_configured_auth=skip_configured_auth) with _chunk_to_temp(Base64IO(file)) as tmp: if tmp.sha256 != sha256: diff --git a/galactory/utilities.py b/galactory/utilities.py index 2c08fd3..9d9af38 100644 --- a/galactory/utilities.py +++ b/galactory/utilities.py @@ -19,7 +19,7 @@ from flask import url_for, request, current_app, abort, Response, Request from flask.json.provider import DefaultJSONProvider from artifactory import ArtifactoryPath, ArtifactoryException -from dohq_artifactory.auth import XJFrogArtApiAuth +from dohq_artifactory.auth import XJFrogArtApiAuth, XJFrogArtBearerAuth from . import constants as C from .iter_tar import iter_tar @@ -48,19 +48,27 @@ def _session_with_retries(retry=None, auth=None) -> Session: return session -def authorize(request: Request, artifactory_path: ArtifactoryPath, retry=None, skip_configured_key: bool = False) -> ArtifactoryPath: +def authorize(request: Request, artifactory_path: ArtifactoryPath, retry=None, skip_configured_auth: bool = False) -> ArtifactoryPath: auth = None - apikey = None - if not skip_configured_key: + if not skip_configured_auth: + accesstoken = current_app.config['ARTIFACTORY_ACCESS_TOKEN'] apikey = current_app.config['ARTIFACTORY_API_KEY'] + if accesstoken is not None: + auth = XJFrogArtBearerAuth(accesstoken) + elif apikey is not None: + auth = XJFrogArtApiAuth(apikey) - if current_app.config['USE_GALAXY_KEY'] and (not current_app.config['PREFER_CONFIGURED_KEY'] or not apikey): + if current_app.config['USE_GALAXY_AUTH'] and (not current_app.config['PREFER_CONFIGURED_AUTH'] or auth is None): + galaxy_auth_type = current_app.config['GALAXY_AUTH_TYPE'] authorization = request.headers.get('Authorization') if authorization: - apikey = authorization.split(' ')[1] - - if apikey: - auth = XJFrogArtApiAuth(apikey) + token = authorization.split(' ')[1] + if galaxy_auth_type == 'access_token': + auth = XJFrogArtBearerAuth(token) + elif galaxy_auth_type == 'api_key': + auth = XJFrogArtApiAuth(token) + else: + raise ValueError(f"Unknown galaxy auth type '{galaxy_auth_type}'.") session = _session_with_retries(retry=retry, auth=auth) return ArtifactoryPath(artifactory_path, session=session)