initial import for Open Source 🎉

This commit is contained in:
Jimmy Zelinskie 2019-11-12 11:09:47 -05:00
parent 1898c361f3
commit 9c0dd3b722
2048 changed files with 218743 additions and 0 deletions

View file

@ -0,0 +1,43 @@
import logging
from functools import wraps
from cnr.exception import Forbidden
from flask import Blueprint
from app import metric_queue
from auth.permissions import (AdministerRepositoryPermission, ReadRepositoryPermission,
ModifyRepositoryPermission)
from endpoints.appr.decorators import require_repo_permission
from util.metrics.metricqueue import time_blueprint
appr_bp = Blueprint('appr', __name__)
time_blueprint(appr_bp, metric_queue)
logger = logging.getLogger(__name__)
def _raise_method(repository, scopes):
raise Forbidden("Unauthorized access for: %s" % repository,
{"package": repository, "scopes": scopes})
def _get_reponame_kwargs(*args, **kwargs):
return [kwargs['namespace'], kwargs['package_name']]
require_app_repo_read = require_repo_permission(ReadRepositoryPermission,
scopes=['pull'],
allow_public=True,
raise_method=_raise_method,
get_reponame_method=_get_reponame_kwargs)
require_app_repo_write = require_repo_permission(ModifyRepositoryPermission,
scopes=['pull', 'push'],
raise_method=_raise_method,
get_reponame_method=_get_reponame_kwargs)
require_app_repo_admin = require_repo_permission(AdministerRepositoryPermission,
scopes=['pull', 'push'],
raise_method=_raise_method,
get_reponame_method=_get_reponame_kwargs)

View file

@ -0,0 +1,177 @@
import base64
from cnr.exception import raise_package_not_found
from cnr.models.blob_base import BlobBase
from cnr.models.channel_base import ChannelBase
from cnr.models.db_base import CnrDB
from cnr.models.package_base import PackageBase, manifest_media_type
from flask import request
from app import storage
from endpoints.appr.models_cnr import model
from util.request import get_request_ip
class Blob(BlobBase):
@classmethod
def upload_url(cls, digest):
return "cnr/blobs/sha256/%s/%s" % (digest[0:2], digest)
def save(self, content_media_type):
model.store_blob(self, content_media_type)
@classmethod
def delete(cls, package_name, digest):
pass
@classmethod
def _fetch_b64blob(cls, package_name, digest):
blobpath = cls.upload_url(digest)
locations = model.get_blob_locations(digest)
if not locations:
raise_package_not_found(package_name, digest)
return base64.b64encode(storage.get_content(locations, blobpath))
@classmethod
def download_url(cls, package_name, digest):
blobpath = cls.upload_url(digest)
locations = model.get_blob_locations(digest)
if not locations:
raise_package_not_found(package_name, digest)
return storage.get_direct_download_url(locations, blobpath, get_request_ip())
class Channel(ChannelBase):
""" CNR Channel model implemented against the Quay data model. """
def __init__(self, name, package, current=None):
super(Channel, self).__init__(name, package, current=current)
self._channel_data = None
def _exists(self):
""" Check if the channel is saved already """
return model.channel_exists(self.package, self.name)
@classmethod
def get(cls, name, package):
chanview = model.fetch_channel(package, name, with_releases=False)
return cls(name, package, chanview.current)
def save(self):
model.update_channel(self.package, self.name, self.current)
def delete(self):
model.delete_channel(self.package, self.name)
@classmethod
def all(cls, package_name):
return [
Channel(c.name, package_name, c.current) for c in model.list_channels(package_name)
]
@property
def _channel(self):
if self._channel_data is None:
self._channel_data = model.fetch_channel(self.package, self.name)
return self._channel_data
def releases(self):
""" Returns the list of versions """
return self._channel.releases
def _add_release(self, release):
return model.update_channel(self.package, self.name, release)._asdict
def _remove_release(self, release):
model.delete_channel(self.package, self.name)
class User(object):
""" User in CNR models """
@classmethod
def get_user(cls, username, password):
""" Returns True if user creds is valid """
return model.get_user(username, password)
class Package(PackageBase):
""" CNR Package model implemented against the Quay data model. """
@classmethod
def _apptuple_to_dict(cls, apptuple):
return {
'release': apptuple.release,
'created_at': apptuple.created_at,
'digest': apptuple.manifest.digest,
'mediaType': apptuple.manifest.mediaType,
'package': apptuple.name,
'content': apptuple.manifest.content._asdict()
}
@classmethod
def create_repository(cls, package_name, visibility, owner):
model.create_application(package_name, visibility, owner)
@classmethod
def exists(cls, package_name):
return model.application_exists(package_name)
@classmethod
def all(cls, organization=None, media_type=None, search=None, username=None, **kwargs):
return [
dict(x._asdict())
for x in model.list_applications(namespace=organization, media_type=media_type,
search=search, username=username)
]
@classmethod
def _fetch(cls, package_name, release, media_type):
data = model.fetch_release(package_name, release, manifest_media_type(media_type))
return cls._apptuple_to_dict(data)
@classmethod
def all_releases(cls, package_name, media_type=None):
return model.list_releases(package_name, media_type)
@classmethod
def search(cls, query, username=None):
return model.basic_search(query, username=username)
def _save(self, force=False, **kwargs):
user = kwargs['user']
visibility = kwargs['visibility']
model.create_release(self, user, visibility, force)
@classmethod
def _delete(cls, package_name, release, media_type):
model.delete_release(package_name, release, manifest_media_type(media_type))
@classmethod
def isdeleted_release(cls, package, release):
return model.release_exists(package, release)
def channels(self, channel_class, iscurrent=True):
return [
c.name
for c in model.list_release_channels(self.package, self.release, active=iscurrent)
]
@classmethod
def manifests(cls, package, release=None):
return model.list_manifests(package, release)
@classmethod
def dump_all(cls, blob_cls):
raise NotImplementedError
class QuayDB(CnrDB):
""" Wrapper Class to embed all CNR Models """
Channel = Channel
Package = Package
Blob = Blob
@classmethod
def reset_db(cls, force=False):
pass

View file

@ -0,0 +1,52 @@
import logging
from functools import wraps
from data import model
from util.http import abort
logger = logging.getLogger(__name__)
def _raise_unauthorized(repository, scopes):
raise StandardError("Unauthorized acces to %s", repository)
def _get_reponame_kwargs(*args, **kwargs):
return [kwargs['namespace'], kwargs['package_name']]
def disallow_for_image_repository(get_reponame_method=_get_reponame_kwargs):
def wrapper(func):
@wraps(func)
def wrapped(*args, **kwargs):
namespace_name, repo_name = get_reponame_method(*args, **kwargs)
image_repo = model.repository.get_repository(namespace_name, repo_name, kind_filter='image')
if image_repo is not None:
logger.debug('Tried to invoked a CNR method on an image repository')
abort(405, message='Cannot push an application to an image repository with the same name')
return func(*args, **kwargs)
return wrapped
return wrapper
def require_repo_permission(permission_class, scopes=None, allow_public=False,
raise_method=_raise_unauthorized,
get_reponame_method=_get_reponame_kwargs):
def wrapper(func):
@wraps(func)
@disallow_for_image_repository(get_reponame_method=get_reponame_method)
def wrapped(*args, **kwargs):
namespace_name, repo_name = get_reponame_method(*args, **kwargs)
logger.debug('Checking permission %s for repo: %s/%s', permission_class,
namespace_name, repo_name)
permission = permission_class(namespace_name, repo_name)
if (permission.can() or
(allow_public and
model.repository.repository_is_public(namespace_name, repo_name))):
return func(*args, **kwargs)
repository = namespace_name + '/' + repo_name
raise_method(repository, scopes)
return wrapped
return wrapper

View file

@ -0,0 +1,316 @@
from datetime import datetime
import cnr.semver
from cnr.exception import raise_package_not_found, raise_channel_not_found, CnrException
import features
import data.model
from app import storage, authentication
from data import appr_model
from data.database import Repository, MediaType, db_transaction
from data.appr_model.models import NEW_MODELS
from endpoints.appr.models_interface import (
ApplicationManifest, ApplicationRelease, ApplicationSummaryView, AppRegistryDataInterface,
BlobDescriptor, ChannelView, ChannelReleasesView)
from util.audit import track_and_log
from util.morecollections import AttrDict
from util.names import parse_robot_username
class ReadOnlyException(CnrException):
status_code = 405
errorcode = "read-only"
def _strip_sha256_header(digest):
if digest.startswith('sha256:'):
return digest.split('sha256:')[1]
return digest
def _split_package_name(package):
""" Returns the namespace and package-name """
return package.split("/")
def _join_package_name(ns, name):
""" Returns a app-name in the 'namespace/name' format """
return "%s/%s" % (ns, name)
def _timestamp_to_iso(timestamp, in_ms=True):
if in_ms:
timestamp = timestamp / 1000
return datetime.fromtimestamp(timestamp).isoformat()
def _application(package):
ns, name = _split_package_name(package)
repo = data.model.repository.get_app_repository(ns, name)
if repo is None:
raise_package_not_found(package)
return repo
class CNRAppModel(AppRegistryDataInterface):
def __init__(self, models_ref, is_readonly):
self.models_ref = models_ref
self.is_readonly = is_readonly
def log_action(self, event_name, namespace_name, repo_name=None, analytics_name=None,
analytics_sample=1, metadata=None):
metadata = {} if metadata is None else metadata
repo = None
if repo_name is not None:
db_repo = data.model.repository.get_repository(namespace_name, repo_name,
kind_filter='application')
repo = AttrDict({
'id': db_repo.id,
'name': db_repo.name,
'namespace_name': db_repo.namespace_user.username,
'is_free_namespace': db_repo.namespace_user.stripe_id is None,
})
track_and_log(event_name, repo, analytics_name=analytics_name,
analytics_sample=analytics_sample, **metadata)
def list_applications(self, namespace=None, media_type=None, search=None, username=None,
with_channels=False):
""" Lists all repositories that contain applications, with optional filtering to a specific
namespace and view a specific user.
"""
views = []
for repo in appr_model.package.list_packages_query(self.models_ref, namespace, media_type,
search, username=username):
tag_set_prefetch = getattr(repo, self.models_ref.tag_set_prefetch_name)
releases = [t.name for t in tag_set_prefetch]
if not releases:
continue
available_releases = [
str(x) for x in sorted(cnr.semver.versions(releases, False), reverse=True)]
channels = None
if with_channels:
channels = [
ChannelView(name=chan.name, current=chan.linked_tag.name)
for chan in appr_model.channel.get_repo_channels(repo, self.models_ref)]
app_name = _join_package_name(repo.namespace_user.username, repo.name)
manifests = self.list_manifests(app_name, available_releases[0])
view = ApplicationSummaryView(
namespace=repo.namespace_user.username,
name=app_name,
visibility=repo.visibility.name,
default=available_releases[0],
channels=channels,
manifests=manifests,
releases=available_releases,
updated_at=_timestamp_to_iso(tag_set_prefetch[-1].lifetime_start),
created_at=_timestamp_to_iso(tag_set_prefetch[0].lifetime_start),)
views.append(view)
return views
def application_is_public(self, package_name):
"""
Returns:
* True if the repository is public
"""
namespace, name = _split_package_name(package_name)
return data.model.repository.repository_is_public(namespace, name)
def create_application(self, package_name, visibility, owner):
""" Create a new app repository, owner is the user who creates it """
if self.is_readonly:
raise ReadOnlyException('Currently in read-only mode')
ns, name = _split_package_name(package_name)
data.model.repository.create_repository(ns, name, owner, visibility, 'application')
def application_exists(self, package_name):
""" Create a new app repository, owner is the user who creates it """
ns, name = _split_package_name(package_name)
return data.model.repository.get_repository(ns, name, kind_filter='application') is not None
def basic_search(self, query, username=None):
""" Returns an array of matching AppRepositories in the format: 'namespace/name'
Note:
* Only 'public' repositories are returned
Todo:
* Filter results with readeable reposistory for the user (including visibilitys)
"""
return [
_join_package_name(r.namespace_user.username, r.name)
for r in data.model.repository.get_app_search(lookup=query, username=username, limit=50)]
def list_releases(self, package_name, media_type=None):
""" Return the list of all releases of an Application
Example:
>>> get_app_releases('ant31/rocketchat')
['1.7.1', '1.7.0', '1.7.2']
Todo:
* Paginate
"""
return appr_model.release.get_releases(_application(package_name), self.models_ref, media_type)
def list_manifests(self, package_name, release=None):
""" Returns the list of all manifests of an Application.
Todo:
* Paginate
"""
try:
repo = _application(package_name)
return list(appr_model.manifest.get_manifest_types(repo, self.models_ref, release))
except (Repository.DoesNotExist, self.models_ref.Tag.DoesNotExist):
raise_package_not_found(package_name, release)
def fetch_release(self, package_name, release, media_type):
"""
Retrieves an AppRelease from it's repository-name and release-name
"""
repo = _application(package_name)
try:
tag, manifest, blob = appr_model.release.get_app_release(repo, release, media_type,
self.models_ref)
created_at = _timestamp_to_iso(tag.lifetime_start)
blob_descriptor = BlobDescriptor(digest=_strip_sha256_header(blob.digest),
mediaType=blob.media_type.name, size=blob.size, urls=[])
app_manifest = ApplicationManifest(
digest=manifest.digest, mediaType=manifest.media_type.name, content=blob_descriptor)
app_release = ApplicationRelease(release=tag.name, created_at=created_at, name=package_name,
manifest=app_manifest)
return app_release
except (self.models_ref.Tag.DoesNotExist,
self.models_ref.Manifest.DoesNotExist,
self.models_ref.Blob.DoesNotExist,
Repository.DoesNotExist,
MediaType.DoesNotExist):
raise_package_not_found(package_name, release, media_type)
def store_blob(self, cnrblob, content_media_type):
if self.is_readonly:
raise ReadOnlyException('Currently in read-only mode')
fp = cnrblob.packager.io_file
path = cnrblob.upload_url(cnrblob.digest)
locations = storage.preferred_locations
storage.stream_write(locations, path, fp, 'application/x-gzip')
db_blob = appr_model.blob.get_or_create_blob(cnrblob.digest, cnrblob.size, content_media_type,
locations, self.models_ref)
return BlobDescriptor(mediaType=content_media_type,
digest=_strip_sha256_header(db_blob.digest), size=db_blob.size, urls=[])
def create_release(self, package, user, visibility, force=False):
""" Add an app-release to a repository
package is an instance of data.cnr.package.Package
"""
if self.is_readonly:
raise ReadOnlyException('Currently in read-only mode')
manifest = package.manifest()
ns, name = package.namespace, package.name
repo = data.model.repository.get_or_create_repository(ns, name, user, visibility=visibility,
repo_kind='application')
tag_name = package.release
appr_model.release.create_app_release(repo, tag_name, package.manifest(),
manifest['content']['digest'], self.models_ref, force)
def delete_release(self, package_name, release, media_type):
""" Remove/Delete an app-release from an app-repository.
It does not delete the entire app-repository, only a single release
"""
if self.is_readonly:
raise ReadOnlyException('Currently in read-only mode')
repo = _application(package_name)
try:
appr_model.release.delete_app_release(repo, release, media_type, self.models_ref)
except (self.models_ref.Channel.DoesNotExist,
self.models_ref.Tag.DoesNotExist,
MediaType.DoesNotExist):
raise_package_not_found(package_name, release, media_type)
def release_exists(self, package, release):
""" Return true if a release with that name already exist or
have existed (include deleted ones) """
# TODO: Figure out why this isn't implemented.
def channel_exists(self, package_name, channel_name):
""" Returns true if channel exists """
repo = _application(package_name)
return appr_model.tag.tag_exists(repo, channel_name, self.models_ref, "channel")
def delete_channel(self, package_name, channel_name):
""" Delete an AppChannel
Note:
It doesn't delete the AppReleases
"""
if self.is_readonly:
raise ReadOnlyException('Currently in read-only mode')
repo = _application(package_name)
try:
appr_model.channel.delete_channel(repo, channel_name, self.models_ref)
except (self.models_ref.Channel.DoesNotExist, self.models_ref.Tag.DoesNotExist):
raise_channel_not_found(package_name, channel_name)
def list_channels(self, package_name):
""" Returns all AppChannel for a package """
repo = _application(package_name)
channels = appr_model.channel.get_repo_channels(repo, self.models_ref)
return [ChannelView(name=chan.name, current=chan.linked_tag.name) for chan in channels]
def fetch_channel(self, package_name, channel_name, with_releases=True):
""" Returns an AppChannel """
repo = _application(package_name)
try:
channel = appr_model.channel.get_channel(repo, channel_name, self.models_ref)
except (self.models_ref.Channel.DoesNotExist, self.models_ref.Tag.DoesNotExist):
raise_channel_not_found(package_name, channel_name)
if with_releases:
releases = appr_model.channel.get_channel_releases(repo, channel, self.models_ref)
chanview = ChannelReleasesView(
current=channel.linked_tag.name, name=channel.name,
releases=[channel.linked_tag.name] + [c.name for c in releases])
else:
chanview = ChannelView(current=channel.linked_tag.name, name=channel.name)
return chanview
def list_release_channels(self, package_name, release, active=True):
repo = _application(package_name)
try:
channels = appr_model.channel.get_tag_channels(repo, release, self.models_ref, active=active)
return [ChannelView(name=c.name, current=c.linked_tag.name) for c in channels]
except (self.models_ref.Channel.DoesNotExist, self.models_ref.Tag.DoesNotExist):
raise_package_not_found(package_name, release)
def update_channel(self, package_name, channel_name, release):
""" Append a new release to the AppChannel
Returns:
A new AppChannel with the release
"""
if self.is_readonly:
raise ReadOnlyException('Currently in read-only mode')
repo = _application(package_name)
channel = appr_model.channel.create_or_update_channel(repo, channel_name, release,
self.models_ref)
return ChannelView(current=channel.linked_tag.name, name=channel.name)
def get_blob_locations(self, digest):
return appr_model.blob.get_blob_locations(digest, self.models_ref)
# Phase 3: Read and write from new tables.
model = CNRAppModel(NEW_MODELS, features.READONLY_APP_REGISTRY)

View file

@ -0,0 +1,191 @@
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from six import add_metaclass
class BlobDescriptor(namedtuple('Blob', ['mediaType', 'size', 'digest', 'urls'])):
""" BlobDescriptor describes a blob with its mediatype, size and digest.
A BlobDescriptor is used to retrieves the actual blob.
"""
class ChannelReleasesView(namedtuple('ChannelReleasesView', ['name', 'current', 'releases'])):
""" A channel is a pointer to a Release (current).
Releases are the previous tags pointed by channel (history).
"""
class ChannelView(namedtuple('ChannelView', ['name', 'current'])):
""" A channel is a pointer to a Release (current).
"""
class ApplicationSummaryView(
namedtuple('ApplicationSummaryView', [
'name', 'namespace', 'visibility', 'default', 'manifests', 'channels', 'releases',
'updated_at', 'created_at'
])):
""" ApplicationSummaryView is an aggregated view of an application repository.
"""
class ApplicationManifest(namedtuple('ApplicationManifest', ['mediaType', 'digest', 'content'])):
""" ApplicationManifest embed the BlobDescriptor and some metadata around it.
An ApplicationManifest is content-addressable.
"""
class ApplicationRelease(
namedtuple('ApplicationRelease', ['release', 'name', 'created_at', 'manifest'])):
""" The ApplicationRelease associates an ApplicationManifest to a repository and release.
"""
@add_metaclass(ABCMeta)
class AppRegistryDataInterface(object):
""" Interface that represents all data store interactions required by a App Registry.
"""
@abstractmethod
def list_applications(self, namespace=None, media_type=None, search=None, username=None,
with_channels=False):
""" Lists all repositories that contain applications, with optional filtering to a specific
namespace and/or to those visible to a specific user.
Returns: list of ApplicationSummaryView
"""
pass
@abstractmethod
def application_is_public(self, package_name):
"""
Returns true if the application is public
"""
pass
@abstractmethod
def create_application(self, package_name, visibility, owner):
""" Create a new app repository, owner is the user who creates it """
pass
@abstractmethod
def application_exists(self, package_name):
""" Returns true if the application exists """
pass
@abstractmethod
def basic_search(self, query, username=None):
""" Returns an array of matching application in the format: 'namespace/name'
Note:
* Only 'public' repositories are returned
"""
pass
# @TODO: Paginate
@abstractmethod
def list_releases(self, package_name, media_type=None):
""" Returns the list of all releases(names) of an AppRepository
Example:
>>> get_app_releases('ant31/rocketchat')
['1.7.1', '1.7.0', '1.7.2']
"""
pass
# @TODO: Paginate
@abstractmethod
def list_manifests(self, package_name, release=None):
""" Returns the list of all available manifests type of an Application across all releases or
for a specific one.
Example:
>>> get_app_releases('ant31/rocketchat')
['1.7.1', '1.7.0', '1.7.2']
"""
pass
@abstractmethod
def fetch_release(self, package_name, release, media_type):
"""
Returns an ApplicationRelease
"""
pass
@abstractmethod
def store_blob(self, cnrblob, content_media_type):
"""
Upload the blob content to a storage location and creates a Blob entry in the DB.
Returns a BlobDescriptor
"""
pass
@abstractmethod
def create_release(self, package, user, visibility, force=False):
""" Creates and returns an ApplicationRelease
- package is a data.model.Package object
- user is the owner of the package
- visibility is a string: 'public' or 'private'
"""
pass
@abstractmethod
def release_exists(self, package, release):
""" Return true if a release with that name already exist or
has existed (including deleted ones)
"""
pass
@abstractmethod
def delete_release(self, package_name, release, media_type):
""" Remove/Delete an app-release from an app-repository.
It does not delete the entire app-repository, only a single release
"""
pass
@abstractmethod
def list_release_channels(self, package_name, release, active=True):
""" Returns a list of Channel that are/was pointing to a release.
If active is True, returns only active Channel (lifetime_end not null)
"""
pass
@abstractmethod
def channel_exists(self, package_name, channel_name):
""" Returns true if the channel with the given name exists under the matching package """
pass
@abstractmethod
def update_channel(self, package_name, channel_name, release):
""" Append a new release to the Channel
Returns a new Channel with the release as current
"""
pass
@abstractmethod
def delete_channel(self, package_name, channel_name):
""" Delete a Channel, it doesn't delete/touch the ApplicationRelease pointed by the channel """
# @TODO: Paginate
@abstractmethod
def list_channels(self, package_name):
""" Returns all AppChannel for a package """
pass
@abstractmethod
def fetch_channel(self, package_name, channel_name, with_releases=True):
""" Returns an Channel
Raises: ChannelNotFound, PackageNotFound
"""
pass
@abstractmethod
def log_action(self, event_name, namespace_name, repo_name=None, analytics_name=None,
analytics_sample=1, **kwargs):
""" Logs an action to the audit log. """
pass
@abstractmethod
def get_blob_locations(self, digest):
""" Returns a list of strings for the locations in which a Blob is present. """
pass

318
endpoints/appr/registry.py Normal file
View file

@ -0,0 +1,318 @@
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)

View file

@ -0,0 +1,163 @@
import uuid
import pytest
from cnr.tests.conftest import *
from cnr.tests.test_apiserver import BaseTestServer
from cnr.tests.test_models import CnrTestModels
import data.appr_model.blob as appr_blob
from data.database import User
from data.model import organization, user
from endpoints.appr import registry # Needed to register the endpoint
from endpoints.appr.cnr_backend import Channel, Package, QuayDB
from endpoints.appr.models_cnr import model as appr_app_model
from test.fixtures import *
def create_org(namespace, owner):
try:
User.get(username=namespace)
except User.DoesNotExist:
organization.create_organization(namespace, "%s@test.com" % str(uuid.uuid1()), owner)
class ChannelTest(Channel):
@classmethod
def dump_all(cls, package_class=None):
result = []
for repo in appr_app_model.list_applications(with_channels=True):
for chan in repo.channels:
result.append({'name': chan.name, 'current': chan.current, 'package': repo.name})
return result
class PackageTest(Package):
def _save(self, force, **kwargs):
owner = user.get_user('devtable')
create_org(self.namespace, owner)
super(PackageTest, self)._save(force, user=owner, visibility="public")
@classmethod
def create_repository(cls, package_name, visibility, owner):
ns, _ = package_name.split("/")
owner = user.get_user('devtable')
visibility = "public"
create_org(ns, owner)
return super(PackageTest, cls).create_repository(package_name, visibility, owner)
@classmethod
def dump_all(cls, blob_cls):
result = []
for repo in appr_app_model.list_applications(with_channels=True):
package_name = repo.name
for release in repo.releases:
for mtype in cls.manifests(package_name, release):
package = appr_app_model.fetch_release(package_name, release, mtype)
blob = blob_cls.get(package_name, package.manifest.content.digest)
app_data = cls._apptuple_to_dict(package)
app_data.pop('digest')
app_data['channels'] = [
x.name
for x in appr_app_model.list_release_channels(package_name, package.release, False)
]
app_data['blob'] = blob.b64blob
result.append(app_data)
return result
@pytest.fixture(autouse=True)
def quaydb(monkeypatch, app):
monkeypatch.setattr('endpoints.appr.cnr_backend.QuayDB.Package', PackageTest)
monkeypatch.setattr('endpoints.appr.cnr_backend.Package', PackageTest)
monkeypatch.setattr('endpoints.appr.registry.Package', PackageTest)
monkeypatch.setattr('cnr.models.Package', PackageTest)
monkeypatch.setattr('endpoints.appr.cnr_backend.QuayDB.Channel', ChannelTest)
monkeypatch.setattr('endpoints.appr.registry.Channel', ChannelTest)
monkeypatch.setattr('cnr.models.Channel', ChannelTest)
class TestServerQuayDB(BaseTestServer):
DB_CLASS = QuayDB
@property
def token(self):
return "basic ZGV2dGFibGU6cGFzc3dvcmQ="
def test_search_package_match(self, db_with_data1, client):
""" TODO: search cross namespace and package name """
BaseTestServer.test_search_package_match(self, db_with_data1, client)
def test_list_search_package_match(self, db_with_data1, client):
url = self._url_for("api/v1/packages")
res = self.Client(client, self.headers()).get(url, params={'query': 'rocketchat'})
assert res.status_code == 200
assert len(self.json(res)) == 1
def test_list_search_package_no_match(self, db_with_data1, client):
url = self._url_for("api/v1/packages")
res = self.Client(client, self.headers()).get(url, params={'query': 'toto'})
assert res.status_code == 200
assert len(self.json(res)) == 0
@pytest.mark.xfail
def test_push_package_already_exists_force(self, db_with_data1, package_b64blob, client):
""" No force push implemented """
BaseTestServer.test_push_package_already_exists_force(self, db_with_data1, package_b64blob,
client)
@pytest.mark.xfail
def test_delete_channel_release_absent_release(self, db_with_data1, client):
BaseTestServer.test_delete_channel_release_absent_release(self, db_with_data1, client)
@pytest.mark.xfail
def test_get_absent_blob(self, newdb, client):
pass
class TestQuayModels(CnrTestModels):
DB_CLASS = QuayDB
@pytest.mark.xfail
def test_channel_delete_releases(self, db_with_data1):
""" Can't remove a release from the channel, only delete the channel entirely """
CnrTestModels.test_channel_delete_releases(self, db_with_data1)
@pytest.mark.xfail
def test_forbiddeb_db_reset(self, db_class):
pass
@pytest.mark.xfail
def test_db_restore(self, newdb, dbdata1):
# This will fail as long as CNR tests use a mediatype with v1.
pass
def test_push_same_blob(self, db_with_data1):
p = db_with_data1.Package.get("titi/rocketchat", ">1.2", 'kpm')
assert p.package == "titi/rocketchat"
assert p.release == "2.0.1"
assert p.digest == "d3b54b7912fe770a61b59ab612a442eac52a8a5d8d05dbe92bf8f212d68aaa80"
blob = db_with_data1.Blob.get("titi/rocketchat", p.digest)
bdb = appr_blob.get_blob(p.digest, appr_app_model.models_ref)
newblob = db_with_data1.Blob("titi/app2", blob.b64blob)
p2 = db_with_data1.Package("titi/app2", "1.0.0", "helm", newblob)
p2.save()
b2db = appr_blob.get_blob(p2.digest, appr_app_model.models_ref)
assert b2db.id == bdb.id
def test_force_push_different_blob(self, db_with_data1):
p = db_with_data1.Package.get("titi/rocketchat", "2.0.1", 'kpm')
assert p.package == "titi/rocketchat"
assert p.release == "2.0.1"
assert p.digest == "d3b54b7912fe770a61b59ab612a442eac52a8a5d8d05dbe92bf8f212d68aaa80"
blob = db_with_data1.Blob.get(
"titi/rocketchat", "72ed15c9a65961ecd034cca098ec18eb99002cd402824aae8a674a8ae41bd0ef")
p2 = db_with_data1.Package("titi/rocketchat", "2.0.1", "kpm", blob)
p2.save(force=True)
pnew = db_with_data1.Package.get("titi/rocketchat", "2.0.1", 'kpm')
assert pnew.package == "titi/rocketchat"
assert pnew.release == "2.0.1"
assert pnew.digest == "72ed15c9a65961ecd034cca098ec18eb99002cd402824aae8a674a8ae41bd0ef"

View file

@ -0,0 +1,97 @@
import base64
import pytest
from flask import url_for
from data import model
from endpoints.appr.registry import appr_bp, blobs
from endpoints.test.shared import client_with_identity
from test.fixtures import *
BLOB_ARGS = {'digest': 'abcd1235'}
PACKAGE_ARGS = {'release': 'r', 'media_type': 'foo'}
RELEASE_ARGS = {'release': 'r'}
CHANNEL_ARGS = {'channel_name': 'c'}
CHANNEL_RELEASE_ARGS = {'channel_name': 'c', 'release': 'r'}
@pytest.mark.parametrize('resource,method,params,owned_by,is_public,identity,expected', [
('appr.blobs', 'GET', BLOB_ARGS, 'devtable', False, 'public', 403),
('appr.blobs', 'GET', BLOB_ARGS, 'devtable', False, 'devtable', 404),
('appr.blobs', 'GET', BLOB_ARGS, 'devtable', True, 'public', 404),
('appr.blobs', 'GET', BLOB_ARGS, 'devtable', True, 'devtable', 404),
('appr.delete_package', 'DELETE', PACKAGE_ARGS, 'devtable', False, 'public', 403),
('appr.delete_package', 'DELETE', PACKAGE_ARGS, 'devtable', False, 'devtable', 404),
('appr.delete_package', 'DELETE', PACKAGE_ARGS, 'devtable', True, 'public', 403),
('appr.delete_package', 'DELETE', PACKAGE_ARGS, 'devtable', True, 'devtable', 404),
('appr.show_package', 'GET', PACKAGE_ARGS, 'devtable', False, 'public', 403),
('appr.show_package', 'GET', PACKAGE_ARGS, 'devtable', False, 'devtable', 404),
('appr.show_package', 'GET', PACKAGE_ARGS, 'devtable', True, 'public', 404),
('appr.show_package', 'GET', PACKAGE_ARGS, 'devtable', True, 'devtable', 404),
('appr.show_package_releases', 'GET', {}, 'devtable', False, 'public', 403),
('appr.show_package_releases', 'GET', {}, 'devtable', False, 'devtable', 200),
('appr.show_package_releases', 'GET', {}, 'devtable', True, 'public', 200),
('appr.show_package_releases', 'GET', {}, 'devtable', True, 'devtable', 200),
('appr.show_package_release_manifests', 'GET', RELEASE_ARGS, 'devtable', False, 'public', 403),
('appr.show_package_release_manifests', 'GET', RELEASE_ARGS, 'devtable', False, 'devtable', 200),
('appr.show_package_release_manifests', 'GET', RELEASE_ARGS, 'devtable', True, 'public', 200),
('appr.show_package_release_manifests', 'GET', RELEASE_ARGS, 'devtable', True, 'devtable', 200),
('appr.pull', 'GET', PACKAGE_ARGS, 'devtable', False, 'public', 403),
('appr.pull', 'GET', PACKAGE_ARGS, 'devtable', False, 'devtable', 404),
('appr.pull', 'GET', PACKAGE_ARGS, 'devtable', True, 'public', 404),
('appr.pull', 'GET', PACKAGE_ARGS, 'devtable', True, 'devtable', 404),
('appr.push', 'POST', {}, 'devtable', False, 'public', 403),
('appr.push', 'POST', {}, 'devtable', False, 'devtable', 400),
('appr.push', 'POST', {}, 'devtable', True, 'public', 403),
('appr.push', 'POST', {}, 'devtable', True, 'devtable', 400),
('appr.list_channels', 'GET', {}, 'devtable', False, 'public', 403),
('appr.list_channels', 'GET', {}, 'devtable', False, 'devtable', 200),
('appr.list_channels', 'GET', {}, 'devtable', True, 'public', 200),
('appr.list_channels', 'GET', {}, 'devtable', True, 'devtable', 200),
('appr.show_channel', 'GET', CHANNEL_ARGS, 'devtable', False, 'public', 403),
('appr.show_channel', 'GET', CHANNEL_ARGS, 'devtable', False, 'devtable', 404),
('appr.show_channel', 'GET', CHANNEL_ARGS, 'devtable', True, 'public', 404),
('appr.show_channel', 'GET', CHANNEL_ARGS, 'devtable', True, 'devtable', 404),
('appr.delete_channel', 'DELETE', CHANNEL_ARGS, 'devtable', False, 'public', 403),
('appr.delete_channel', 'DELETE', CHANNEL_ARGS, 'devtable', False, 'devtable', 404),
('appr.delete_channel', 'DELETE', CHANNEL_ARGS, 'devtable', True, 'public', 403),
('appr.delete_channel', 'DELETE', CHANNEL_ARGS, 'devtable', True, 'devtable', 404),
('appr.add_channel_release', 'POST', CHANNEL_RELEASE_ARGS, 'devtable', False, 'public', 403),
('appr.add_channel_release', 'POST', CHANNEL_RELEASE_ARGS, 'devtable', False, 'devtable', 404),
('appr.add_channel_release', 'POST', CHANNEL_RELEASE_ARGS, 'devtable', True, 'public', 403),
('appr.add_channel_release', 'POST', CHANNEL_RELEASE_ARGS, 'devtable', True, 'devtable', 404),
('appr.delete_channel_release', 'DELETE', CHANNEL_RELEASE_ARGS, 'devtable', False, 'public', 403),
('appr.delete_channel_release', 'DELETE', CHANNEL_RELEASE_ARGS, 'devtable', False, 'devtable', 404),
('appr.delete_channel_release', 'DELETE', CHANNEL_RELEASE_ARGS, 'devtable', True, 'public', 403),
('appr.delete_channel_release', 'DELETE', CHANNEL_RELEASE_ARGS, 'devtable', True, 'devtable', 404),
])
def test_api_security(resource, method, params, owned_by, is_public, identity, expected, app, client):
app.register_blueprint(appr_bp, url_prefix='/cnr')
with client_with_identity(identity, client) as cl:
owner = model.user.get_user(owned_by)
visibility = 'public' if is_public else 'private'
model.repository.create_repository(owned_by, 'someapprepo', owner, visibility=visibility,
repo_kind='application')
params['namespace'] = owned_by
params['package_name'] = 'someapprepo'
params['_csrf_token'] = '123csrfforme'
url = url_for(resource, **params)
headers = {}
if identity is not None:
headers['authorization'] = 'basic ' + base64.b64encode('%s:password' % identity)
rv = cl.open(url, headers=headers, method=method)
assert rv.status_code == expected

View file

@ -0,0 +1,20 @@
import pytest
from werkzeug.exceptions import HTTPException
from data import model
from endpoints.appr import require_app_repo_read
from test.fixtures import *
def test_require_app_repo_read(app):
called = [False]
# Ensure that trying to read an *image* repository fails.
@require_app_repo_read
def empty(**kwargs):
called[0] = True
with pytest.raises(HTTPException):
empty(namespace='devtable', package_name='simple')
assert not called[0]

View file

@ -0,0 +1,11 @@
import pytest
from endpoints.appr.models_cnr import _strip_sha256_header
@pytest.mark.parametrize('digest,expected', [
('sha256:251b6897608fb18b8a91ac9abac686e2e95245d5a041f2d1e78fe7a815e6480a',
'251b6897608fb18b8a91ac9abac686e2e95245d5a041f2d1e78fe7a815e6480a'),
('251b6897608fb18b8a91ac9abac686e2e95245d5a041f2d1e78fe7a815e6480a',
'251b6897608fb18b8a91ac9abac686e2e95245d5a041f2d1e78fe7a815e6480a'),])
def test_stip_sha256(digest, expected):
assert _strip_sha256_header(digest) == expected

View file

@ -0,0 +1,92 @@
import base64
import json
from mock import patch
import pytest
from flask import url_for
from data import model
from endpoints.appr.registry import appr_bp
from test.fixtures import *
@pytest.mark.parametrize('login_data, expected_code', [
({
"username": "devtable",
"password": "password"
}, 200),
({
"username": "devtable",
"password": "badpass"
}, 401),
({
"username": "devtable+dtrobot",
"password": "badpass"
}, 401),
({
"username": "devtable+dtrobot2",
"password": "badpass"
}, 401),
])
def test_login(login_data, expected_code, app, client):
if "+" in login_data['username'] and login_data['password'] is None:
username, robotname = login_data['username'].split("+")
_, login_data['password'] = model.user.create_robot(robotname, model.user.get_user(username))
url = url_for('appr.login')
headers = {'Content-Type': 'application/json'}
data = {'user': login_data}
rv = client.open(url, method='POST', data=json.dumps(data), headers=headers)
assert rv.status_code == expected_code
@pytest.mark.parametrize('release_name', [
'1.0',
'1',
1,
])
def test_invalid_release_name(release_name, app, client):
params = {
'namespace': 'devtable',
'package_name': 'someapprepo',
}
url = url_for('appr.push', **params)
auth = base64.b64encode('devtable:password')
headers = {'Content-Type': 'application/json', 'Authorization': 'Basic ' + auth}
data = {
'release': release_name,
'media_type': 'application/vnd.cnr.manifest.v1+json',
'blob': 'H4sIAFQwWVoAA+3PMQrCQBAF0Bxlb+Bk143nETGIIEoSC29vMMFOu3TvNb/5DH/Ot8f02jWbiohDremT3ZKR90uuUlty7nKJNmqKtkQuTarbzlo8x+k4zFOu4+lyH4afvbnW93/urH98EwAAAAAAAAAAADb0BsdwExIAKAAA',
}
rv = client.open(url, method='POST', data=json.dumps(data), headers=headers)
assert rv.status_code == 422
@pytest.mark.parametrize('readonly, expected_status', [
(True, 405),
(False, 422),
])
def test_readonly(readonly, expected_status, app, client):
params = {
'namespace': 'devtable',
'package_name': 'someapprepo',
}
url = url_for('appr.push', **params)
auth = base64.b64encode('devtable:password')
headers = {'Content-Type': 'application/json', 'Authorization': 'Basic ' + auth}
data = {
'release': '1.0',
'media_type': 'application/vnd.cnr.manifest.v0+json',
'blob': 'H4sIAFQwWVoAA+3PMQrCQBAF0Bxlb+Bk143nETGIIEoSC29vMMFOu3TvNb/5DH/Ot8f02jWbiohDremT3ZKR90uuUlty7nKJNmqKtkQuTarbzlo8x+k4zFOu4+lyH4afvbnW93/urH98EwAAAAAAAAAAADb0BsdwExIAKAAA',
}
with patch('endpoints.appr.models_cnr.model.is_readonly', readonly):
rv = client.open(url, method='POST', data=json.dumps(data), headers=headers)
assert rv.status_code == expected_status