319 lines
12 KiB
Python
319 lines
12 KiB
Python
|
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.credentials import validate_credentials
|
||
|
from auth.decorators import process_auth
|
||
|
from auth.permissions import CreateRepositoryPermission, ModifyRepositoryPermission
|
||
|
from data.logs_model import logs_model
|
||
|
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_cnr import model
|
||
|
from endpoints.decorators import anon_allowed, anon_protect, check_region_blacklisted
|
||
|
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')
|
||
|
|
||
|
result, _ = validate_credentials(username, password)
|
||
|
if not result.auth_valid:
|
||
|
raise UnauthorizedAccess(result.error_message)
|
||
|
|
||
|
return jsonify({'token': "basic " + b64encode("%s:%s" % (username, password))})
|
||
|
|
||
|
|
||
|
# @TODO: Redirect to S3 url
|
||
|
@appr_bp.route(
|
||
|
"/api/v1/packages/<string:namespace>/<string:package_name>/blobs/sha256/<string:digest>",
|
||
|
methods=['GET'],
|
||
|
strict_slashes=False,)
|
||
|
@process_auth
|
||
|
@require_app_repo_read
|
||
|
@check_region_blacklisted(namespace_name_kwarg='namespace')
|
||
|
@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/<string:namespace>/<string:package_name>/<string:release>/<string:media_type>",
|
||
|
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)
|
||
|
logs_model.log_action('delete_tag', namespace, repository_name=package_name,
|
||
|
metadata={'release': release, 'mediatype': media_type})
|
||
|
return jsonify(result)
|
||
|
|
||
|
|
||
|
@appr_bp.route(
|
||
|
"/api/v1/packages/<string:namespace>/<string:package_name>/<string:release>/<string:media_type>",
|
||
|
methods=['GET'], strict_slashes=False)
|
||
|
@process_auth
|
||
|
@require_app_repo_read
|
||
|
@check_region_blacklisted(namespace_name_kwarg='namespace')
|
||
|
@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/<string:namespace>/<string:package_name>", 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/<string:namespace>/<string:package_name>/<string:release>",
|
||
|
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/<string:namespace>/<string:package_name>/<string:release>/<string:media_type>/pull",
|
||
|
methods=['GET'],
|
||
|
strict_slashes=False,)
|
||
|
@process_auth
|
||
|
@require_app_repo_read
|
||
|
@check_region_blacklisted(namespace_name_kwarg='namespace')
|
||
|
@anon_protect
|
||
|
def pull(namespace, package_name, release, media_type):
|
||
|
logger.debug('Pull of release %s of app repository %s/%s', release, namespace, package_name)
|
||
|
reponame = repo_name(namespace, package_name)
|
||
|
data = cnr_registry.pull(reponame, release, media_type, Package, blob_class=Blob)
|
||
|
logs_model.log_action('pull_repo', namespace, repository_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/<string:namespace>/<string:package_name>", 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)
|
||
|
logs_model.log_action('create_repo', namespace, repository_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')
|
||
|
|
||
|
if not 'media_type' in values:
|
||
|
raise InvalidUsage('Missing media_type')
|
||
|
|
||
|
if not 'blob' in values:
|
||
|
raise InvalidUsage('Missing blob')
|
||
|
|
||
|
release_version = str(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)
|
||
|
logs_model.log_action('push_repo', namespace, repository_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/<string:namespace>/<string:package_name>/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/<string:namespace>/<string:package_name>/channels/<string:channel_name>",
|
||
|
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/<string:namespace>/<string:package_name>/channels/<string:channel_name>/<string:release>",
|
||
|
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)
|
||
|
logs_model.log_action('create_tag', namespace, repository_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/<string:namespace>/<string:package_name>/channels/<string:channel_name>/<string:release>",
|
||
|
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)
|
||
|
logs_model.log_action('delete_tag', namespace, repository_name=package_name,
|
||
|
metadata={'channel': channel_name, 'release': release})
|
||
|
return jsonify(result)
|
||
|
|
||
|
|
||
|
@appr_bp.route(
|
||
|
"/api/v1/packages/<string:namespace>/<string:package_name>/channels/<string:channel_name>",
|
||
|
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)
|
||
|
logs_model.log_action('delete_tag', namespace, repository_name=package_name,
|
||
|
metadata={'channel': channel_name})
|
||
|
return jsonify(result)
|