import logging from base64 import b64encode import cnr from cnr.api.impl import registry as cnr_registry from cnr.api.registry import _pull, repo_name from cnr.exception import ( ChannelNotFound, CnrException, Forbidden, InvalidParams, InvalidRelease, InvalidUsage, PackageAlreadyExists, PackageNotFound, PackageReleaseNotFound, UnableToLockResource, UnauthorizedAccess, Unsupported) from flask import jsonify, request from auth.auth_context import get_authenticated_user from auth.decorators import process_auth from auth.permissions import CreateRepositoryPermission, ModifyRepositoryPermission from endpoints.appr import appr_bp, require_app_repo_read, require_app_repo_write from endpoints.appr.cnr_backend import Blob, Channel, Package, User from endpoints.appr.decorators import disallow_for_image_repository from endpoints.appr.models_oci import model from endpoints.decorators import anon_allowed, anon_protect from util.names import REPOSITORY_NAME_REGEX, TAG_REGEX logger = logging.getLogger(__name__) @appr_bp.errorhandler(Unsupported) @appr_bp.errorhandler(PackageAlreadyExists) @appr_bp.errorhandler(InvalidRelease) @appr_bp.errorhandler(Forbidden) @appr_bp.errorhandler(UnableToLockResource) @appr_bp.errorhandler(UnauthorizedAccess) @appr_bp.errorhandler(PackageNotFound) @appr_bp.errorhandler(PackageReleaseNotFound) @appr_bp.errorhandler(CnrException) @appr_bp.errorhandler(InvalidUsage) @appr_bp.errorhandler(InvalidParams) @appr_bp.errorhandler(ChannelNotFound) def render_error(error): response = jsonify({"error": error.to_dict()}) response.status_code = error.status_code return response @appr_bp.route("/version") @anon_allowed def version(): return jsonify({"cnr-api": cnr.__version__}) @appr_bp.route("/api/v1/users/login", methods=['POST']) @anon_allowed def login(): values = request.get_json(force=True, silent=True) or {} username = values.get('user', {}).get('username') password = values.get('user', {}).get('password') if not username or not password: raise InvalidUsage('Missing username or password') user, err = User.get_user(username, password) if err is not None: raise UnauthorizedAccess(err) return jsonify({'token': "basic " + b64encode("%s:%s" % (user.username, password))}) # @TODO: Redirect to S3 url @appr_bp.route( "/api/v1/packages///blobs/sha256/", methods=['GET'], strict_slashes=False,) @process_auth @require_app_repo_read @anon_protect def blobs(namespace, package_name, digest): reponame = repo_name(namespace, package_name) data = cnr_registry.pull_blob(reponame, digest, blob_class=Blob) json_format = request.args.get('format', None) == 'json' return _pull(data, json_format=json_format) @appr_bp.route("/api/v1/packages", methods=['GET'], strict_slashes=False) @process_auth @anon_protect def list_packages(): namespace = request.args.get('namespace', None) media_type = request.args.get('media_type', None) query = request.args.get('query', None) user = get_authenticated_user() username = None if user: username = user.username result_data = cnr_registry.list_packages(namespace, package_class=Package, search=query, media_type=media_type, username=username) return jsonify(result_data) @appr_bp.route( "/api/v1/packages////", methods=['DELETE'], strict_slashes=False) @process_auth @require_app_repo_write @anon_protect def delete_package(namespace, package_name, release, media_type): reponame = repo_name(namespace, package_name) result = cnr_registry.delete_package(reponame, release, media_type, package_class=Package) model.log_action('delete_tag', namespace, repo_name=package_name, metadata={'release': release, 'mediatype': media_type}) return jsonify(result) @appr_bp.route( "/api/v1/packages////", methods=['GET'], strict_slashes=False) @process_auth @require_app_repo_read @anon_protect def show_package(namespace, package_name, release, media_type): reponame = repo_name(namespace, package_name) result = cnr_registry.show_package(reponame, release, media_type, channel_class=Channel, package_class=Package) return jsonify(result) @appr_bp.route("/api/v1/packages//", methods=['GET'], strict_slashes=False) @process_auth @require_app_repo_read @anon_protect def show_package_releases(namespace, package_name): reponame = repo_name(namespace, package_name) media_type = request.args.get('media_type', None) result = cnr_registry.show_package_releases(reponame, media_type=media_type, package_class=Package) return jsonify(result) @appr_bp.route("/api/v1/packages///", methods=['GET'], strict_slashes=False) @process_auth @require_app_repo_read @anon_protect def show_package_release_manifests(namespace, package_name, release): reponame = repo_name(namespace, package_name) result = cnr_registry.show_package_manifests(reponame, release, package_class=Package) return jsonify(result) @appr_bp.route( "/api/v1/packages/////pull", methods=['GET'], strict_slashes=False,) @process_auth @require_app_repo_read @anon_protect def pull(namespace, package_name, release, media_type): reponame = repo_name(namespace, package_name) logger.info("pull %s", reponame) data = cnr_registry.pull(reponame, release, media_type, Package, blob_class=Blob) model.log_action('pull_repo', namespace, repo_name=package_name, metadata={'release': release, 'mediatype': media_type}) json_format = request.args.get('format', None) == 'json' return _pull(data, json_format) @appr_bp.route("/api/v1/packages//", methods=['POST'], strict_slashes=False) @disallow_for_image_repository() @process_auth @anon_protect def push(namespace, package_name): reponame = repo_name(namespace, package_name) if not REPOSITORY_NAME_REGEX.match(package_name): logger.debug('Found invalid repository name CNR push: %s', reponame) raise InvalidUsage('invalid repository name: %s' % reponame) values = request.get_json(force=True, silent=True) or {} private = values.get('visibility', 'private') owner = get_authenticated_user() if not Package.exists(reponame): if not CreateRepositoryPermission(namespace).can(): raise Forbidden("Unauthorized access for: %s" % reponame, {"package": reponame, "scopes": ['create']}) Package.create_repository(reponame, private, owner) model.log_action('create_repo', namespace, repo_name=package_name) if not ModifyRepositoryPermission(namespace, package_name).can(): raise Forbidden("Unauthorized access for: %s" % reponame, {"package": reponame, "scopes": ['push']}) if not 'release' in values: raise InvalidUsage('Missing release') release_version = values['release'] media_type = values['media_type'] force = request.args.get('force', 'false') == 'true' blob = Blob(reponame, values['blob']) app_release = cnr_registry.push(reponame, release_version, media_type, blob, force, package_class=Package, user=owner, visibility=private) model.log_action('push_repo', namespace, repo_name=package_name, metadata={'release': release_version}) return jsonify(app_release) @appr_bp.route("/api/v1/packages/search", methods=['GET'], strict_slashes=False) @process_auth @anon_protect def search_packages(): query = request.args.get("q") user = get_authenticated_user() username = None if user: username = user.username search_results = cnr_registry.search(query, Package, username=username) return jsonify(search_results) # CHANNELS @appr_bp.route("/api/v1/packages///channels", methods=['GET'], strict_slashes=False) @process_auth @require_app_repo_read @anon_protect def list_channels(namespace, package_name): reponame = repo_name(namespace, package_name) return jsonify(cnr_registry.list_channels(reponame, channel_class=Channel)) @appr_bp.route( "/api/v1/packages///channels/", methods=['GET'], strict_slashes=False) @process_auth @require_app_repo_read @anon_protect def show_channel(namespace, package_name, channel_name): reponame = repo_name(namespace, package_name) channel = cnr_registry.show_channel(reponame, channel_name, channel_class=Channel) return jsonify(channel) @appr_bp.route( "/api/v1/packages///channels//", methods=['POST'], strict_slashes=False,) @process_auth @require_app_repo_write @anon_protect def add_channel_release(namespace, package_name, channel_name, release): _check_channel_name(channel_name, release) reponame = repo_name(namespace, package_name) result = cnr_registry.add_channel_release(reponame, channel_name, release, channel_class=Channel, package_class=Package) model.log_action('create_tag', namespace, repo_name=package_name, metadata={'channel': channel_name, 'release': release}) return jsonify(result) def _check_channel_name(channel_name, release=None): if not TAG_REGEX.match(channel_name): logger.debug('Found invalid channel name CNR add channel release: %s', channel_name) raise InvalidUsage("Found invalid channelname %s" % release, {'name': channel_name, 'release': release}) if release is not None and not TAG_REGEX.match(release): logger.debug('Found invalid release name CNR add channel release: %s', release) raise InvalidUsage('Found invalid channel release name %s' % release, {'name': channel_name, 'release': release}) @appr_bp.route( "/api/v1/packages///channels//", methods=['DELETE'], strict_slashes=False,) @process_auth @require_app_repo_write @anon_protect def delete_channel_release(namespace, package_name, channel_name, release): _check_channel_name(channel_name, release) reponame = repo_name(namespace, package_name) result = cnr_registry.delete_channel_release(reponame, channel_name, release, channel_class=Channel, package_class=Package) model.log_action('delete_tag', namespace, repo_name=package_name, metadata={'channel': channel_name, 'release': release}) return jsonify(result) @appr_bp.route( "/api/v1/packages///channels/", methods=['DELETE'], strict_slashes=False,) @process_auth @require_app_repo_write @anon_protect def delete_channel(namespace, package_name, channel_name): _check_channel_name(channel_name) reponame = repo_name(namespace, package_name) result = cnr_registry.delete_channel(reponame, channel_name, channel_class=Channel) model.log_action('delete_tag', namespace, repo_name=package_name, metadata={'channel': channel_name}) return jsonify(result)