diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/__init__.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/__init__.py index 0b1ac425d..0391c1238 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/__init__.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/__init__.py @@ -95,39 +95,6 @@ def query_params_wrapper(*args, **kwargs): return query_params_decorator -def _shared_pagination_wrapper(**kwargs): - form = PaginationForm(flask.request.args) - if not form.validate(): - raise CoprHttpException(form.errors) - kwargs.update(form.data) - return kwargs - - -def pagination(): - def pagination_decorator(f): - @wraps(f) - def pagination_wrapper(*args, **kwargs): - kwargs = _shared_pagination_wrapper(**kwargs) - return f(*args, **kwargs) - return pagination_wrapper - return pagination_decorator - - -def _shared_file_upload_wrapper(): - data = json.loads(flask.request.files["json"].read()) or {} - flask.request.form = ImmutableMultiDict(list(data.items())) - -def file_upload(): - def file_upload_decorator(f): - @wraps(f) - def file_upload_wrapper(*args, **kwargs): - if "json" in flask.request.files: - _shared_file_upload_wrapper() - return f(*args, **kwargs) - return file_upload_wrapper - return file_upload_decorator - - class PaginationForm(wtforms.Form): limit = wtforms.IntegerField("Limit", validators=[wtforms.validators.Optional()]) offset = wtforms.IntegerField("Offset", validators=[wtforms.validators.Optional()]) @@ -249,27 +216,6 @@ def get(self): return objects[self.offset : limit] -def _check_if_user_can_edit_copr(ownername, projectname): - copr = get_copr(ownername, projectname) - if not flask.g.user.can_edit(copr): - raise AccessRestricted( - "User '{0}' can not see permissions for project '{1}' " \ - "(missing admin rights)".format( - flask.g.user.name, - '/'.join([ownername, projectname]) - ) - ) - return copr - - -def editable_copr(f): - @wraps(f) - def wrapper(ownername, projectname): - copr = _check_if_user_can_edit_copr(ownername, projectname) - return f(copr) - return wrapper - - def set_defaults(formdata, form_class): """ Take a `formdata` which can be `flask.request.form` or an output from @@ -481,42 +427,55 @@ def call_deprecated_endpoint_method(endpoint_method): return call_deprecated_endpoint_method -def restx_editable_copr(endpoint_method): +def editable_copr(endpoint_method): """ Raises an exception if user don't have permissions for editing Copr repo. Order matters! If flask.g.user is None then this will fail! If used with @api_login_required it has to be called after it: @api_login_required - @restx_editable_copr + @editable_copr ... """ @wraps(endpoint_method) def editable_copr_getter(self, ownername, projectname): - copr = _check_if_user_can_edit_copr(ownername, projectname) + copr = get_copr(ownername, projectname) + if not flask.g.user.can_edit(copr): + raise AccessRestricted( + "User '{0}' can not see permissions for project '{1}' " \ + "(missing admin rights)".format( + flask.g.user.name, + '/'.join([ownername, projectname]) + ) + ) + return endpoint_method(self, copr) return editable_copr_getter -def restx_pagination(endpoint_method): +def pagination(endpoint_method): """ Validates pagination arguments and converts pagination parameters from query to kwargs. """ @wraps(endpoint_method) def create_pagination(self, *args, **kwargs): - kwargs = _shared_pagination_wrapper(**kwargs) + form = PaginationForm(flask.request.args) + if not form.validate(): + raise CoprHttpException(form.errors) + kwargs.update(form.data) return endpoint_method(self, *args, **kwargs) return create_pagination -def restx_file_upload(endpoint_method): +def file_upload(endpoint_method): """ Allow uploading a file to a form via endpoint by using this function as an endpoint decorator. """ @wraps(endpoint_method) def inner(self, *args, **kwargs): if "json" in flask.request.files: - _shared_file_upload_wrapper() + data = json.loads(flask.request.files["json"].read()) or {} + flask.request.form = ImmutableMultiDict(list(data.items())) return endpoint_method(self, *args, **kwargs) return inner diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_build_chroots.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_build_chroots.py index df881f519..2301ff461 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_build_chroots.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_build_chroots.py @@ -18,7 +18,7 @@ build_chroot_config_model, nevra_packages_model, ) -from coprs.views.apiv3_ns import restx_pagination +from coprs.views.apiv3_ns import pagination from . import Paginator @@ -83,7 +83,7 @@ def get(self, build_id, chrootname): doc={"deprecated": True, "description": "Use query parameters instead"}, ) class BuildChrootList(Resource): - @restx_pagination + @pagination @query_to_parameters @apiv3_bchroots_ns.doc(params=build_id_params | pagination_params) @apiv3_bchroots_ns.marshal_list_with(pagination_build_chroot_model) diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py index 8e5c365b4..e4d49e3ca 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py @@ -14,8 +14,11 @@ from coprs import db, forms, models from coprs.exceptions import (BadRequest, AccessRestricted) from coprs.views.misc import api_login_required -from coprs.views.apiv3_ns import apiv3_ns, api, rename_fields_helper -from coprs.views.apiv3_ns.schema.schemas import build_model +from coprs.views.apiv3_ns import api, rename_fields_helper, deprecated_route_method_type +from coprs.views.apiv3_ns.schema.schemas import build_model, pagination_build_model, source_chroot_model, \ + source_build_config_model, list_build_params, create_build_url_input_model, create_build_upload_input_model, \ + create_build_scm_input_model, create_build_distgit_input_model, create_build_pypi_input_model, \ + create_build_rubygems_input_model, create_build_custom_input_model, delete_builds_input_model, list_build_model from coprs.views.apiv3_ns.schema.docs import get_build_docs from coprs.logic.complex_logic import ComplexLogic from coprs.logic.builds_logic import BuildsLogic @@ -23,15 +26,11 @@ from . import ( get_copr, - file_upload, - query_params, - pagination, SubqueryPaginator, json2form, - GET, - POST, - PUT, - DELETE, + query_to_parameters, + pagination, + file_upload, ) from .json2form import get_form_compatible_data @@ -84,13 +83,44 @@ def rename_fields(input_dict): }) -def render_build(build): - return flask.jsonify(to_dict(build)) +def process_creating_new_build(copr, form, create_new_build): + if not form.validate_on_submit(): + raise BadRequest("Bad request parameters: {0}".format(form.errors)) + + if not flask.g.user.can_build_in(copr): + raise AccessRestricted("User {} is not allowed to build in the copr: {}" + .format(flask.g.user.username, copr.full_name)) + form.isolation.data = "unchanged" if form.isolation.data is None else form.isolation.data + + generic_build_options = { + 'chroot_names': form.selected_chroots, + 'background': form.background.data, + 'copr_dirname': form.project_dirname.data, + 'timeout': form.timeout.data, + 'bootstrap': form.bootstrap.data, + 'isolation': form.isolation.data, + 'after_build_id': form.after_build_id.data, + 'with_build_id': form.with_build_id.data, + 'packit_forge_project': form.packit_forge_project.data + } + + if form.enable_net.data is not None: + generic_build_options['enable_net'] = form.enable_net.data + + # From URLs it can be created multiple builds at once + # so it can return a list + build = create_new_build(generic_build_options) + db.session.commit() + + if type(build) == list: + builds = [build] if type(build) != list else build + return {"items": [to_dict(b) for b in builds], "meta": {}} + + return to_dict(build) @apiv3_builds_ns.route("/") class GetBuild(Resource): - @apiv3_builds_ns.doc(params=get_build_docs) @apiv3_builds_ns.marshal_with(build_model) def get(self, build_id): @@ -101,6 +131,7 @@ def get(self, build_id): build = ComplexLogic.get_build(build_id) result = to_dict(build) + # TODO: I think this workaround is bad usage of models... check it later # Workaround - `marshal_with` needs the input `build_id` to be present # in the returned dict. Don't worry, it won't get to the end user, it # will be stripped away. @@ -108,304 +139,414 @@ def get(self, build_id): return result -@apiv3_ns.route("/build/list/", methods=GET) -@pagination() -@query_params() -def get_build_list(ownername, projectname, packagename=None, status=None, **kwargs): - copr = get_copr(ownername, projectname) - - # WORKAROUND - # We can't filter builds by status directly in the database, because we - # use a logic in Build.status property to determine a build status. - # Therefore if we want to filter by `status`, we need to query all builds - # and filter them in the application and then return the desired number. - limit = kwargs["limit"] - paginator_limit = None if status else kwargs["limit"] - del kwargs["limit"] - - # Loading relationships straight away makes running `to_dict` somewhat - # faster, which adds up over time, and brings a significant speedup for - # large projects - query = BuildsLogic.get_multiple() - query = query.options( - joinedload(models.Build.build_chroots), - joinedload(models.Build.package), - joinedload(models.Build.copr), - ) - - subquery = query.filter(models.Build.copr == copr) - if packagename: - subquery = BuildsLogic.filter_by_package_name(subquery, packagename) - - paginator = SubqueryPaginator(query, subquery, models.Build, limit=paginator_limit, **kwargs) - - builds = paginator.map(to_dict) - - if status: - builds = [b for b in builds if b["state"] == status][:limit] - paginator.limit = limit - - return flask.jsonify(items=builds, meta=paginator.meta) +@apiv3_builds_ns.route("/list") +class ListBuild(Resource): + @pagination + @query_to_parameters + @apiv3_builds_ns.doc(params=list_build_params) + @apiv3_builds_ns.marshal_with(pagination_build_model) + def get(self, ownername, projectname, packagename=None, status=None, **kwargs): + """ + List builds + List all builds in a Copr project. + """ + copr = get_copr(ownername, projectname) + + # WORKAROUND + # We can't filter builds by status directly in the database, because we + # use a logic in Build.status property to determine a build status. + # Therefore if we want to filter by `status`, we need to query all builds + # and filter them in the application and then return the desired number. + limit = kwargs["limit"] + paginator_limit = None if status else kwargs["limit"] + del kwargs["limit"] + + # Loading relationships straight away makes running `to_dict` somewhat + # faster, which adds up over time, and brings a significant speedup for + # large projects + query = BuildsLogic.get_multiple() + query = query.options( + joinedload(models.Build.build_chroots), + joinedload(models.Build.package), + joinedload(models.Build.copr), + ) + subquery = query.filter(models.Build.copr == copr) + if packagename: + subquery = BuildsLogic.filter_by_package_name(subquery, packagename) -@apiv3_ns.route("/build/source-chroot//", methods=GET) -def get_source_chroot(build_id): - build = ComplexLogic.get_build(build_id) - return flask.jsonify(to_source_chroot(build)) + paginator = SubqueryPaginator(query, subquery, models.Build, limit=paginator_limit, **kwargs) + builds = paginator.map(to_dict) -@apiv3_ns.route("/build/source-build-config//", methods=GET) -def get_source_build_config(build_id): - build = ComplexLogic.get_build(build_id) - return flask.jsonify(to_source_build_config(build)) + if status: + builds = [b for b in builds if b["state"] == status][:limit] + paginator.limit = limit + return {"items": builds, "meta": paginator.meta} -@apiv3_ns.route("/build/built-packages//", methods=GET) -def get_build_built_packages(build_id): - """ - Return built packages (NEVRA dicts) for a given build - """ - build = ComplexLogic.get_build(build_id) - return flask.jsonify(build.results_dict) +@apiv3_builds_ns.route("/source-log/") +class SourceChroot(Resource): + @apiv3_builds_ns.doc(params=get_build_docs) + @apiv3_builds_ns.marshal_with(source_chroot_model) + def get(self, build_id): + """ + Get source chroot + Get source chroot for a build. + """ + build = ComplexLogic.get_build(build_id) + return to_source_chroot(build) -@apiv3_ns.route("/build/cancel/", methods=PUT) -@api_login_required -def cancel_build(build_id): - build = ComplexLogic.get_build(build_id) - BuildsLogic.cancel_build(flask.g.user, build) - db.session.commit() - return render_build(build) - - -@apiv3_ns.route("/build/create/url", methods=POST) -@api_login_required -def create_from_url(): - copr = get_copr() - data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) - form = forms.BuildFormUrlFactory(copr.active_chroots)(data, meta={'csrf': False}) - - def create_new_build(options): - # create separate build for each package - pkgs = form.pkgs.data.split("\n") - return [BuildsLogic.create_new_from_url( - flask.g.user, copr, - url=pkg, - **options, - ) for pkg in pkgs] - return process_creating_new_build(copr, form, create_new_build) - - -@apiv3_ns.route("/build/create/upload", methods=POST) -@api_login_required -@file_upload() -def create_from_upload(): - copr = get_copr() - data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) - form = forms.BuildFormUploadFactory(copr.active_chroots)(data, meta={'csrf': False}) - - def create_new_build(options): - return BuildsLogic.create_new_from_upload( - flask.g.user, copr, - form.pkgs, - orig_filename=secure_filename(form.pkgs.data.filename), - **options, - ) - return process_creating_new_build(copr, form, create_new_build) +@apiv3_builds_ns.route("/source-build-config/") +class SourceBuildConfig(Resource): + @apiv3_builds_ns.doc(params=get_build_docs) + @apiv3_builds_ns.marshal_with(source_build_config_model) + def get(self, build_id): + """ + Get source build config + Get source build config for a build. + """ + build = ComplexLogic.get_build(build_id) + return to_source_build_config(build) -@apiv3_ns.route("/build/check-before-build", methods=POST) -@api_login_required -def check_before_build(): - """ - Check if a build can be submitted (if the project exists, you have - permissions, the chroot exists, etc). This is useful before trying to - upload a large SRPM and failing to do so. - """ - data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) - # Raises an exception if project doesn't exist - copr = get_copr() +@apiv3_builds_ns.route("/built-packages/") +class BuildPackages(Resource): + @apiv3_builds_ns.doc( + params=get_build_docs, + # not marshalable b/c the dict key is dynamic + responses={200: '{"chroot_name": any_result_dict_or_value}'} + ) + def get(self, build_id): + """ + Get built packages + Get built packages (NEVRA dicts) for a given build + """ + build = ComplexLogic.get_build(build_id) + return build.results_dict - # Raises an exception if CoprDir doesn't exist - if data.get("project_dirname"): - CoprDirsLogic.get_or_validate(copr, data["project_dirname"]) - # Permissions check - if not flask.g.user.can_build_in(copr): - msg = ("User '{0}' is not allowed to build in '{1}'" - .format(flask.g.user.name, copr.full_name)) - raise AccessRestricted(msg) - - # Validation, i.e. check if chroot names are valid - # pylint: disable=not-callable - factory = forms.BuildFormCheckFactory(copr.active_chroots) - form = factory(data, meta={'csrf': False}) - if not form.validate_on_submit(): - raise BadRequest("Bad request parameters: {0}".format(form.errors)) +@apiv3_builds_ns.route("/cancel/") +class CancelBuild(Resource): + @staticmethod + def _common(build_id): + build = ComplexLogic.get_build(build_id) + BuildsLogic.cancel_build(flask.g.user, build) + db.session.commit() + return to_dict(build) - return {"message": "It should be safe to submit a build like this"} - - -@apiv3_ns.route("/build/create/scm", methods=POST) -@api_login_required -def create_from_scm(): - copr = get_copr() - data = rename_fields(get_form_compatible_data(preserve=["chroots", "exclude_chroots"])) - form = forms.BuildFormScmFactory(copr.active_chroots)(data, meta={'csrf': False}) - - def create_new_build(options): - return BuildsLogic.create_new_from_scm( - flask.g.user, - copr, - scm_type=form.scm_type.data, - clone_url=form.clone_url.data, - committish=form.committish.data, - subdirectory=form.subdirectory.data, - spec=form.spec.data, - srpm_build_method=form.srpm_build_method.data, - **options, - ) - return process_creating_new_build(copr, form, create_new_build) - -@apiv3_ns.route("/build/create/distgit", methods=POST) -@api_login_required -def create_from_distgit(): - """ - route for v3.proxies.create_from_distgit() call - """ - copr = get_copr() - data = rename_fields(get_form_compatible_data(preserve=["chroots", "exclude_chroots"])) - # pylint: disable=not-callable - form = forms.BuildFormDistGitSimpleFactory(copr.active_chroots)(data, meta={'csrf': False}) - - def create_new_build(options): - return BuildsLogic.create_new_from_distgit( - flask.g.user, - copr, - package_name=form.package_name.data, - distgit_name=form.distgit.data, - distgit_namespace=form.namespace.data, - committish=form.committish.data, - **options, - ) - return process_creating_new_build(copr, form, create_new_build) - -@apiv3_ns.route("/build/create/pypi", methods=POST) -@api_login_required -def create_from_pypi(): - copr = get_copr() - data = MultiDict(json2form.without_empty_fields(json2form.get_input())) - form = forms.BuildFormPyPIFactory(copr.active_chroots)(data, meta={'csrf': False}) - - # TODO: automatically prepopulate all form fields with their defaults - if not form.python_versions.data: - form.python_versions.data = form.python_versions.default - - def create_new_build(options): - return BuildsLogic.create_new_from_pypi( - flask.g.user, - copr, - form.pypi_package_name.data, - form.pypi_package_version.data, - form.spec_generator.data, - form.spec_template.data, - form.python_versions.data, - **options, - ) - return process_creating_new_build(copr, form, create_new_build) - - -@apiv3_ns.route("/build/create/rubygems", methods=POST) -@api_login_required -def create_from_rubygems(): - copr = get_copr() - data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) - form = forms.BuildFormRubyGemsFactory(copr.active_chroots)(data, meta={'csrf': False}) - - def create_new_build(options): - return BuildsLogic.create_new_from_rubygems( - flask.g.user, - copr, - form.gem_name.data, - **options, - ) - return process_creating_new_build(copr, form, create_new_build) - - -@apiv3_ns.route("/build/create/custom", methods=POST) -@api_login_required -def create_from_custom(): - copr = get_copr() - data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) - form = forms.BuildFormCustomFactory(copr.active_chroots)(data, meta={'csrf': False}) - - def create_new_build(options): - return BuildsLogic.create_new_from_custom( - flask.g.user, - copr, - form.script.data, - form.chroot.data, - form.builddeps.data, - form.resultdir.data, - form.repos.data, - **options, - ) - return process_creating_new_build(copr, form, create_new_build) + @api_login_required + @apiv3_builds_ns.doc(params=get_build_docs) + @apiv3_builds_ns.marshal_with(build_model) + def put(self, build_id): + """ + Cancel a build + Cancel a build by its id. + """ + return self._common(build_id) + @api_login_required + @apiv3_builds_ns.doc(params=get_build_docs) + @apiv3_builds_ns.marshal_with(build_model) + @deprecated_route_method_type(apiv3_builds_ns, "POST", "PUT") + def post(self, build_id): + """ + Cancel a build + Cancel a build by its id. + """ + return self._common(build_id) -def process_creating_new_build(copr, form, create_new_build): - if not form.validate_on_submit(): - raise BadRequest("Bad request parameters: {0}".format(form.errors)) - if not flask.g.user.can_build_in(copr): - raise AccessRestricted("User {} is not allowed to build in the copr: {}" - .format(flask.g.user.username, copr.full_name)) - form.isolation.data = "unchanged" if form.isolation.data is None else form.isolation.data +@apiv3_builds_ns.route("/create/url") +class CreateFromUrl(Resource): + @api_login_required + @apiv3_builds_ns.expect(create_build_url_input_model) + @apiv3_builds_ns.marshal_with(pagination_build_model) + def post(self): + """ + Create a build from URL + Create a build from a URL. + """ + copr = get_copr() + data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) + # pylint: disable-next=not-callable + form = forms.BuildFormUrlFactory(copr.active_chroots)(data, meta={'csrf': False}) + + def create_new_build(options): + # create separate build for each package + pkgs = form.pkgs.data.split("\n") + return [BuildsLogic.create_new_from_url( + flask.g.user, copr, + url=pkg, + **options, + ) for pkg in pkgs] + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/create/upload") +class CreateFromUpload(Resource): + @file_upload + @api_login_required + @apiv3_builds_ns.expect(create_build_upload_input_model) + @apiv3_builds_ns.marshal_with(build_model) + def post(self): + """ + Create a build from upload + Create a build from an uploaded file. + """ + copr = get_copr() + data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) + # pylint: disable-next=not-callable + form = forms.BuildFormUploadFactory(copr.active_chroots)(data, meta={'csrf': False}) + + def create_new_build(options): + return BuildsLogic.create_new_from_upload( + flask.g.user, copr, + form.pkgs, + orig_filename=secure_filename(form.pkgs.data.filename), + **options, + ) + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/create/scm") +class CreateFromScm(Resource): + @api_login_required + @apiv3_builds_ns.expect(create_build_scm_input_model) + @apiv3_builds_ns.marshal_with(build_model) + def post(self): + """ + Create a build from SCM + Create a build from a source code management system. + """ + copr = get_copr() + data = rename_fields(get_form_compatible_data(preserve=["chroots", "exclude_chroots"])) + # pylint: disable-next=not-callable + form = forms.BuildFormScmFactory(copr.active_chroots)(data, meta={'csrf': False}) + + def create_new_build(options): + return BuildsLogic.create_new_from_scm( + flask.g.user, + copr, + scm_type=form.scm_type.data, + clone_url=form.clone_url.data, + committish=form.committish.data, + subdirectory=form.subdirectory.data, + spec=form.spec.data, + srpm_build_method=form.srpm_build_method.data, + **options, + ) + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/create/distgit") +class CreateFromDistGit(Resource): + @api_login_required + @apiv3_builds_ns.expect(create_build_distgit_input_model) + @apiv3_builds_ns.marshal_with(build_model) + def post(self): + """ + Create a build from DistGit + Create a build from a DistGit repository. + """ + copr = get_copr() + data = rename_fields(get_form_compatible_data(preserve=["chroots", "exclude_chroots"])) + # pylint: disable-next=not-callable + form = forms.BuildFormDistGitSimpleFactory(copr.active_chroots)(data, meta={'csrf': False}) + + def create_new_build(options): + return BuildsLogic.create_new_from_distgit( + flask.g.user, + copr, + package_name=form.package_name.data, + distgit_name=form.distgit.data, + distgit_namespace=form.namespace.data, + committish=form.committish.data, + **options, + ) + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/create/pypi") +class CreateFromPyPi(Resource): + @api_login_required + @apiv3_builds_ns.expect(create_build_pypi_input_model) + @apiv3_builds_ns.marshal_with(build_model) + def post(self): + """ + Create a build from PyPi + Create a build from a PyPi package. + """ + copr = get_copr() + data = MultiDict(json2form.without_empty_fields(json2form.get_input())) + # pylint: disable-next=not-callable + form = forms.BuildFormPyPIFactory(copr.active_chroots)(data, meta={'csrf': False}) + + # TODO: automatically prepopulate all form fields with their defaults + if not form.python_versions.data: + form.python_versions.data = form.python_versions.default + + def create_new_build(options): + return BuildsLogic.create_new_from_pypi( + flask.g.user, + copr, + form.pypi_package_name.data, + form.pypi_package_version.data, + form.spec_generator.data, + form.spec_template.data, + form.python_versions.data, + **options, + ) + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/create/rubygems") +class CreateFromRubyGems(Resource): + @api_login_required + @apiv3_builds_ns.expect(create_build_rubygems_input_model) + @apiv3_builds_ns.marshal_with(build_model) + def post(self): + """ + Create a build from RubyGems + Create a build from a RubyGems package. + """ + copr = get_copr() + data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) + # pylint: disable-next=not-callable + form = forms.BuildFormRubyGemsFactory(copr.active_chroots)(data, meta={'csrf': False}) + + def create_new_build(options): + return BuildsLogic.create_new_from_rubygems( + flask.g.user, + copr, + form.gem_name.data, + **options, + ) + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/create/custom") +class CreateCustom(Resource): + @api_login_required + @apiv3_builds_ns.expect(create_build_custom_input_model) + @apiv3_builds_ns.marshal_with(build_model) + def post(self): + """ + Create a build using custom method + Create a build using a custom method. + """ + copr = get_copr() + data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) + # pylint: disable-next=not-callable + form = forms.BuildFormCustomFactory(copr.active_chroots)(data, meta={'csrf': False}) + + def create_new_build(options): + return BuildsLogic.create_new_from_custom( + flask.g.user, + copr, + form.script.data, + form.chroot.data, + form.builddeps.data, + form.resultdir.data, + form.repos.data, + **options, + ) + + return process_creating_new_build(copr, form, create_new_build) + + +@apiv3_builds_ns.route("/delete/") +class DeleteBuild(Resource): + @api_login_required + @apiv3_builds_ns.doc(params=get_build_docs) + @apiv3_builds_ns.marshal_with(build_model) + def delete(self, build_id): + """ + Delete a build + Delete a build by its id. + """ + build = ComplexLogic.get_build(build_id) + build_dict = to_dict(build) + BuildsLogic.delete_build(flask.g.user, build) + db.session.commit() + return build_dict + + +@apiv3_builds_ns.route("/delete/list") +class DeleteBuilds(Resource): + @staticmethod + def _common(): + data = get_form_compatible_data(preserve=["builds"]) + build_ids = data["builds"] + BuildsLogic.delete_builds(flask.g.user, build_ids) + db.session.commit() + return {"builds": build_ids} + + @api_login_required + @apiv3_builds_ns.expect(delete_builds_input_model) + @apiv3_builds_ns.marshal_with(list_build_model) + @deprecated_route_method_type(apiv3_builds_ns, "POST", "DELETE") + def post(self): + """ + Delete builds + Delete builds specified by a list of IDs. + """ + return self._common() - generic_build_options = { - 'chroot_names': form.selected_chroots, - 'background': form.background.data, - 'copr_dirname': form.project_dirname.data, - 'timeout': form.timeout.data, - 'bootstrap': form.bootstrap.data, - 'isolation': form.isolation.data, - 'after_build_id': form.after_build_id.data, - 'with_build_id': form.with_build_id.data, - 'packit_forge_project': form.packit_forge_project.data - } + @api_login_required + @apiv3_builds_ns.expect(delete_builds_input_model) + @apiv3_builds_ns.marshal_with(list_build_model) + def delete(self): + """ + Delete builds + Delete builds specified by a list of IDs. + """ + return self._common() - if form.enable_net.data is not None: - generic_build_options['enable_net'] = form.enable_net.data - # From URLs it can be created multiple builds at once - # so it can return a list - build = create_new_build(generic_build_options) - db.session.commit() +@apiv3_builds_ns.route("/check-before-build") +# this endoint is not meant to be used by the end user +@apiv3_builds_ns.hide +class CheckBeforeBuild(Resource): + @api_login_required + @apiv3_builds_ns.doc( + responses={200: {"message": "It should be safe to submit a build like this"}} + ) + def post(self): + """ + Check before build + Check if a build can be submitted (if the project exists, you have + permissions, the chroot exists, etc). This is useful before trying to + upload a large SRPM and failing to do so. + """ + data = get_form_compatible_data(preserve=["chroots", "exclude_chroots"]) - if type(build) == list: - builds = [build] if type(build) != list else build - return flask.jsonify(items=[to_dict(b) for b in builds], meta={}) - return flask.jsonify(to_dict(build)) + # Raises an exception if project doesn't exist + copr = get_copr() + # Raises an exception if CoprDir doesn't exist + if data.get("project_dirname"): + CoprDirsLogic.get_or_validate(copr, data["project_dirname"]) -@apiv3_ns.route("/build/delete/", methods=DELETE) -@api_login_required -def delete_build(build_id): - build = ComplexLogic.get_build(build_id) - build_dict = to_dict(build) - BuildsLogic.delete_build(flask.g.user, build) - db.session.commit() - return flask.jsonify(build_dict) + # Permissions check + if not flask.g.user.can_build_in(copr): + msg = ("User '{0}' is not allowed to build in '{1}'" + .format(flask.g.user.name, copr.full_name)) + raise AccessRestricted(msg) + # Validation, i.e. check if chroot names are valid + # pylint: disable=not-callable + factory = forms.BuildFormCheckFactory(copr.active_chroots) + form = factory(data, meta={'csrf': False}) + if not form.validate_on_submit(): + raise BadRequest("Bad request parameters: {0}".format(form.errors)) -@apiv3_ns.route("/build/delete/list", methods=POST) -@api_login_required -def delete_builds(): - """ - Delete builds specified by a list of IDs. - """ - build_ids = flask.request.json["builds"] - BuildsLogic.delete_builds(flask.g.user, build_ids) - db.session.commit() - return flask.jsonify({"builds": build_ids}) + return {"message": "It should be safe to submit a build like this"} diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py index 38eb9b504..7836ac91b 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py @@ -12,7 +12,7 @@ from coprs import app, oid, db from coprs.views.apiv3_ns import api from coprs.exceptions import AccessRestricted -from coprs.views.misc import restx_api_login_required +from coprs.views.misc import api_login_required from coprs.auth import UserAuth @@ -64,7 +64,7 @@ def krb_straighten_username(krb_remote_user): @apiv3_general_ns.route("/auth-check") class AuthCheck(Resource): - @restx_api_login_required + @api_login_required def get(self): """ Check if the user is authenticated diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_modules.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_modules.py index 277393b10..3bbf61e95 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_modules.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_modules.py @@ -10,10 +10,10 @@ from wtforms import ValidationError from coprs import forms, db_session_scope -from coprs.views.apiv3_ns import api, get_copr, restx_file_upload +from coprs.views.apiv3_ns import api, get_copr, file_upload from coprs.views.apiv3_ns.schema.schemas import (module_build_model, fullname_params, module_add_input_model) -from coprs.views.misc import restx_api_login_required +from coprs.views.misc import api_login_required from coprs.exceptions import DuplicateException, BadRequest, InvalidForm from coprs.logic.modules_logic import ModuleProvider, ModuleBuildFacade @@ -30,8 +30,8 @@ def to_dict(module): @apiv3_module_ns.route("/build//") class Module(Resource): - @restx_api_login_required - @restx_file_upload + @api_login_required + @file_upload @apiv3_module_ns.doc(params=fullname_params) @apiv3_module_ns.expect(module_add_input_model) @apiv3_module_ns.marshal_with(module_build_model) diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py index de17534b4..c729f7c36 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py @@ -15,13 +15,13 @@ UnknownSourceTypeException, InvalidForm, ) -from coprs.views.misc import restx_api_login_required +from coprs.views.misc import api_login_required from coprs import db, models, forms, helpers from coprs.views.apiv3_ns import ( api, rename_fields_helper, query_to_parameters, - restx_pagination, + pagination, deprecated_route_method_type, ) from coprs.views.apiv3_ns.schema.schemas import ( @@ -140,7 +140,7 @@ def get(self, ownername, projectname, packagename, with_latest_build=False, @apiv3_packages_ns.route("/list") class PackageGetList(Resource): - @restx_pagination + @pagination @query_to_parameters @apiv3_packages_ns.doc(params=package_get_list_params) @apiv3_packages_ns.marshal_with(pagination_package_model) @@ -178,7 +178,7 @@ def get(self, ownername, projectname, with_latest_build=False, @apiv3_packages_ns.route("/add////") class PackageAdd(Resource): - @restx_api_login_required + @api_login_required @apiv3_packages_ns.doc(params=add_package_docs) @apiv3_packages_ns.expect(package_add_input_model) @apiv3_packages_ns.marshal_with(package_model) @@ -200,7 +200,7 @@ def post(self, ownername, projectname, package_name, source_type_text): @apiv3_packages_ns.route("/edit////") @apiv3_packages_ns.route("/edit////") class PackageEdit(Resource): - @restx_api_login_required + @api_login_required @apiv3_packages_ns.doc(params=edit_package_docs) @apiv3_packages_ns.expect(package_edit_input_model) @apiv3_packages_ns.marshal_with(package_model) @@ -245,7 +245,7 @@ def _common(): db.session.commit() return to_dict(package) - @restx_api_login_required + @api_login_required @apiv3_packages_ns.marshal_with(package_model) @apiv3_packages_ns.expect(base_package_input_model) def put(self): @@ -256,7 +256,7 @@ def put(self): return self._common() @deprecated_route_method_type(apiv3_packages_ns, "POST", "PUT") - @restx_api_login_required + @api_login_required @apiv3_packages_ns.marshal_with(package_model) @apiv3_packages_ns.expect(base_package_input_model) def post(self): @@ -269,7 +269,7 @@ def post(self): @apiv3_packages_ns.route("/build") class PackageBuild(Resource): - @restx_api_login_required + @api_login_required @apiv3_packages_ns.marshal_with(build_model) def post(self): """ @@ -321,7 +321,7 @@ def _common(): db.session.commit() return to_dict(package) - @restx_api_login_required + @api_login_required @apiv3_packages_ns.marshal_with(package_model) @apiv3_packages_ns.expect(base_package_input_model) def delete(self): @@ -332,7 +332,7 @@ def delete(self): return self._common() @deprecated_route_method_type(apiv3_packages_ns, "POST", "DELETE") - @restx_api_login_required + @api_login_required @apiv3_packages_ns.marshal_with(package_model) @apiv3_packages_ns.expect(base_package_input_model) def post(self): diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_permissions.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_permissions.py index 0ea4d1568..d21ba1e81 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_permissions.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_permissions.py @@ -6,8 +6,8 @@ from flask_restx import Namespace, Resource -from coprs.views.apiv3_ns import api, restx_editable_copr, get_copr, deprecated_route_method_type -from coprs.views.misc import restx_api_login_required +from coprs.views.apiv3_ns import api, editable_copr, get_copr, deprecated_route_method_type +from coprs.views.misc import api_login_required from coprs.exceptions import ObjectNotFound, BadRequest from coprs.helpers import PermissionEnum from coprs.logic.coprs_logic import CoprPermissionsLogic @@ -44,8 +44,8 @@ def get(self, who, ownername, projectname): @apiv3_permissions_ns.route("/get//") class GetPermissions(Resource): - @restx_api_login_required - @restx_editable_copr + @api_login_required + @editable_copr @apiv3_permissions_ns.doc(params=fullname_params) @apiv3_permissions_ns.response(HTTPStatus.OK.value, HTTPStatus.OK.description) @apiv3_permissions_ns.response( @@ -112,8 +112,8 @@ def _common(copr): return {'updated': list(updated.keys())} - @restx_api_login_required - @restx_editable_copr + @api_login_required + @editable_copr @apiv3_permissions_ns.doc(params=fullname_params) @apiv3_permissions_ns.response(HTTPStatus.OK.value, HTTPStatus.OK.description) @apiv3_permissions_ns.response( @@ -126,8 +126,8 @@ def post(self, copr): """ return self._common(copr) - @restx_api_login_required - @restx_editable_copr + @api_login_required + @editable_copr @apiv3_permissions_ns.doc(params=fullname_params) @apiv3_permissions_ns.response(HTTPStatus.OK.value, HTTPStatus.OK.description) @apiv3_permissions_ns.response( @@ -171,7 +171,7 @@ def _common(ownername, projectname): return {'updated': bool(permission_dict)} @deprecated_route_method_type(apiv3_permissions_ns, "POST", "PUT") - @restx_api_login_required + @api_login_required @apiv3_permissions_ns.doc(params=fullname_params) @apiv3_permissions_ns.response(HTTPStatus.OK.value, HTTPStatus.OK.description) @apiv3_permissions_ns.response( @@ -184,7 +184,7 @@ def post(self, ownername, projectname): """ return self._common(ownername, projectname) - @restx_api_login_required + @api_login_required @apiv3_permissions_ns.doc(params=fullname_params) @apiv3_permissions_ns.response(HTTPStatus.OK.value, HTTPStatus.OK.description) @apiv3_permissions_ns.response( diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_project_chroots.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_project_chroots.py index d6b3b7627..f314ddfcf 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_project_chroots.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_project_chroots.py @@ -4,7 +4,7 @@ import flask from flask_restx import Namespace, Resource -from coprs.views.misc import restx_api_login_required +from coprs.views.misc import api_login_required from coprs.views.apiv3_ns import ( api, rename_fields_helper, @@ -24,7 +24,7 @@ get_copr, str_to_list, reset_to_defaults, - restx_file_upload, + file_upload, ) from .json2form import get_form_compatible_data @@ -150,8 +150,8 @@ def _common(self, ownername, projectname, chrootname): return to_dict(chroot) @deprecated_route_method_type(apiv3_project_chroots_ns, "POST", "PUT") - @restx_file_upload - @restx_api_login_required + @file_upload + @api_login_required @apiv3_project_chroots_ns.doc(params=project_chroot_get_params) @apiv3_project_chroots_ns.marshal_with(project_chroot_model) def post(self, ownername, projectname, chrootname): @@ -161,8 +161,8 @@ def post(self, ownername, projectname, chrootname): """ return self._common(ownername, projectname, chrootname) - @restx_file_upload - @restx_api_login_required + @file_upload + @api_login_required @apiv3_project_chroots_ns.doc(params=project_chroot_get_params) @apiv3_project_chroots_ns.marshal_with(project_chroot_model) def put(self, ownername, projectname, chrootname): diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py index e1949f5e9..681016197 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py @@ -8,15 +8,15 @@ from coprs.views.apiv3_ns import ( get_copr, - restx_pagination, + pagination, Paginator, set_defaults, deprecated_route_method_type, - restx_editable_copr, + editable_copr, ) from coprs.views.apiv3_ns.json2form import get_form_compatible_data, get_input_dict from coprs import db, models, forms, db_session_scope -from coprs.views.misc import restx_api_login_required +from coprs.views.misc import api_login_required from coprs.views.apiv3_ns import rename_fields_helper, api, query_to_parameters from coprs.views.apiv3_ns.schema.schemas import ( project_model, @@ -137,7 +137,7 @@ def get(self, ownername, projectname): @apiv3_projects_ns.route("/list") class ProjectList(Resource): - @restx_pagination + @pagination @query_to_parameters @apiv3_projects_ns.doc(params=project_params | pagination_params) @apiv3_projects_ns.marshal_list_with(pagination_project_model) @@ -159,7 +159,7 @@ def get(self, ownername=None, **kwargs): @apiv3_projects_ns.route("/search") class ProjectSearch(Resource): - @restx_pagination + @pagination @query_to_parameters @apiv3_projects_ns.doc(params=query_docs) @apiv3_projects_ns.marshal_list_with(pagination_project_model) @@ -183,7 +183,7 @@ def get(self, query, **kwargs): @apiv3_projects_ns.route("/add/") class ProjectAdd(Resource): - @restx_api_login_required + @api_login_required @query_to_parameters @apiv3_projects_ns.doc(params=project_params) @apiv3_projects_ns.marshal_with(project_model) @@ -312,7 +312,7 @@ def _common(ownername, projectname): return to_dict(copr) - @restx_api_login_required + @api_login_required @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.expect(project_edit_input_model) @@ -327,7 +327,7 @@ def put(self, ownername, projectname): """ return self._common(ownername, projectname) - @restx_api_login_required + @api_login_required @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.expect(project_edit_input_model) @@ -394,7 +394,7 @@ def _common(ownername, projectname): return to_dict(fcopr) - @restx_api_login_required + @api_login_required @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.expect(project_fork_input_model) @@ -409,7 +409,7 @@ def post(self, ownername, projectname): """ return self._common(ownername, projectname) - @restx_api_login_required + @api_login_required @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.expect(project_fork_input_model) @@ -446,7 +446,7 @@ def _common(ownername, projectname): raise InvalidForm(form) return copr_dict - @restx_api_login_required + @api_login_required @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.expect(project_delete_input_model) @@ -461,7 +461,7 @@ def delete(self, ownername, projectname): """ return self._common(ownername, projectname) - @restx_api_login_required + @api_login_required @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.expect(project_delete_input_model) @@ -487,8 +487,8 @@ def _common(copr): return to_dict(copr) - @restx_api_login_required - @restx_editable_copr + @api_login_required + @editable_copr @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.response( @@ -500,8 +500,8 @@ def put(self, copr): """ return self._common(copr) - @restx_api_login_required - @restx_editable_copr + @api_login_required + @editable_copr @apiv3_projects_ns.doc(params=fullname_params) @apiv3_projects_ns.marshal_with(project_model) @apiv3_projects_ns.response( diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_webhooks.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_webhooks.py index 7cb82f287..8ebf6d64d 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_webhooks.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_webhooks.py @@ -10,8 +10,8 @@ from flask_restx import Namespace, Resource from coprs import db -from coprs.views.misc import restx_api_login_required -from coprs.views.apiv3_ns import api, restx_editable_copr +from coprs.views.misc import api_login_required +from coprs.views.apiv3_ns import api, editable_copr from coprs.views.apiv3_ns.schema.schemas import fullname_params, webhook_secret_model @@ -35,8 +35,8 @@ def to_dict(copr): @apiv3_webhooks_ns.route("/generate//") class WebhookSecret(Resource): - @restx_api_login_required - @restx_editable_copr + @api_login_required + @editable_copr @apiv3_webhooks_ns.doc(params=fullname_params) @apiv3_webhooks_ns.marshal_with(webhook_secret_model) @apiv3_webhooks_ns.response(HTTPStatus.OK.value, "Webhook secret created") diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/fields.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/fields.py index 92634ade8..2c0f7f9e0 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/fields.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/fields.py @@ -456,6 +456,52 @@ def __init__(self, example=None, **kwargs): modulemd = Raw(example="YAML file", description="Modulemd YAML file") +status = String( + example="succeeded", + description="Status of the build", +) + +chroot_names = List( + String, + description="List of chroot names", + example=["fedora-37-x86_64", "fedora-rawhide-x86_64"], +) + +background = is_background + +copr_dirname = project_dirname + +after_build_id = Integer( + description="Build after the batch containing the Build ID build", + example=123, +) + +with_build_id = Integer( + description="Build in the same batch with the Build ID build", + example=123, +) + +packit_forge_project = String( + description="Forge project name that Packit passes", + example="github.com/fedora-copr/copr", + # hide this so we don't confuse our users in the API docs + # packit uses this internally to check whether given packit build is allowed + # to build from the source upstream project into tis copr project + # packit_forge_project in packit_forge_projects_allowed + mask=True, +) + +distgit = String( + description="Dist-git URL we build against", + example="fedora", +) + +namespace = String( + description="DistGit namescape", + example="@copr/copr", +) + + # TODO: these needs description chroot_repos = Raw() @@ -468,10 +514,10 @@ def __init__(self, example=None, **kwargs): memory_limit = Integer() -distgit = String() - scmurl = Url() +result_url = Url() + # TODO: specify those only in Repo schema? baseurl = Url() diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/schemas.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/schemas.py index a529725a0..ae587ab6a 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/schemas.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/schema/schemas.py @@ -85,7 +85,14 @@ def schema_attrs_from_fields(cls) -> dict[str, Any]: return result_schema @staticmethod - def _convert_schema_class_dict_to_schema(d: dict) -> dict: + def _should_be_item_candidate_to_delete(key: str, value: Any) -> bool: + return (key.startswith("_") or not isinstance(value, Raw)) or ( + # masking feature is missing in marshaling + hasattr(value, "mask") and value.mask + ) + + @classmethod + def _convert_schema_class_dict_to_schema(cls, d: dict) -> dict: """ Returns the same dictionary that was passed as param, doesn't create copy of it. """ @@ -103,7 +110,7 @@ def _convert_schema_class_dict_to_schema(d: dict) -> dict: keys_to_delete = [] for key, value in d.items(): - if key.startswith("_") or not isinstance(value, Raw): + if cls._should_be_item_candidate_to_delete(key, value): keys_to_delete.append(key) for key_to_delete in keys_to_delete: @@ -594,6 +601,111 @@ class Monitor(Schema): packages: List = List(Nested(_module_package_model)) +@dataclass +class SourceChroot(Schema): + state: String + result_url: Url + + +@dataclass +class SourceBuildConfig(Schema): + source_type: String + source_dict: Raw + memory_limit: Integer + timeout: Integer + is_background: Boolean + + +@dataclass +class ListBuild(ParamsSchema): + ownername: String + projectname: String + packagename: String + status: String + + @property + def required_attrs(self) -> list: + return [self.ownername, self.projectname] + + +@dataclass +class _GenericBuildOptions: + chroot_names: List + background: Boolean + timeout: Integer + bootstrap: String + isolation: String + after_build_id: Integer + with_build_id: Integer + packit_forge_project: String + enable_net: Boolean + + +@dataclass +class _BuildDataCommon: + ownername: String + projectname: String + + +@dataclass +class CreateBuildUrl(_BuildDataCommon, _GenericBuildOptions, InputSchema): + project_dirname: String + pkgs: List = List( + Url, + description="List of urls to build from", + example=["https://example.com/some.src.rpm"], + ) + + +@dataclass +class CreateBuildUpload(_BuildDataCommon, _GenericBuildOptions, InputSchema): + project_dirname: String + pkgs: List = List(Raw, description="application/x-rpm files to build from") + + +@dataclass +class CreateBuildSCM(_BuildDataCommon, _GenericBuildOptions, _SourceDictScmFields, InputSchema): + project_dirname: String + scm_type: String + source_build_method: String + + +@dataclass +class CreateBuildDistGit(_BuildDataCommon, _GenericBuildOptions, InputSchema): + distgit: String + namespace: String + package_name: String + committish: String + project_dirname: String + + +@dataclass +class CreateBuildPyPI(_BuildDataCommon, _GenericBuildOptions, SourceDictPyPI, InputSchema): + project_dirname: String + + +@dataclass +class CreateBuildRubyGems(_BuildDataCommon, _GenericBuildOptions, InputSchema): + project_dirname: String + gem_name: String + + +@dataclass +class CreateBuildCustom(_BuildDataCommon, _GenericBuildOptions, InputSchema): + script: String + chroot: String + builddeps: String + resultdir: String + project_dirname: String + repos: List = List(Nested(_repo_model)) + + +@dataclass +class DeleteBuilds(InputSchema): + build_ids: List = List(Integer, description="List of build ids to delete") + + + # OUTPUT MODELS project_chroot_model = ProjectChroot.get_cls().model() project_chroot_build_config_model = ProjectChrootBuildConfig.get_cls().model() @@ -608,10 +720,14 @@ class Monitor(Schema): webhook_secret_model = WebhookSecret.get_cls().model() monitor_model = Monitor.get_cls().model() can_build_in_model = CanBuildSchema.get_cls().model() +source_chroot_model = SourceChroot.get_cls().model() +source_build_config_model = SourceBuildConfig.get_cls().model() +list_build_model = DeleteBuilds.get_cls().model() pagination_project_model = Pagination(items=List(Nested(project_model))).model() pagination_build_chroot_model = Pagination(items=List(Nested(build_chroot_model))).model() pagination_package_model = Pagination(items=List(Nested(package_model))).model() +pagination_build_model = Pagination(items=List(Nested(_build_model))).model() source_package_model = _source_package_model build_model = _build_model @@ -630,6 +746,15 @@ class Monitor(Schema): project_delete_input_model = ProjectDelete.get_cls().input_model() module_add_input_model = ModuleAdd.get_cls().input_model() +create_build_url_input_model = CreateBuildUrl.get_cls().input_model() +create_build_upload_input_model = CreateBuildUpload.get_cls().input_model() +create_build_scm_input_model = CreateBuildSCM.get_cls().input_model() +create_build_distgit_input_model = CreateBuildDistGit.get_cls().input_model() +create_build_pypi_input_model = CreateBuildPyPI.get_cls().input_model() +create_build_rubygems_input_model = CreateBuildRubyGems.get_cls().input_model() +create_build_custom_input_model = CreateBuildCustom.get_cls().input_model() +delete_builds_input_model = DeleteBuilds.get_cls().input_model() + # PARAMETER SCHEMAS package_get_params = PackageGet.get_cls().params_schema() @@ -642,3 +767,4 @@ class Monitor(Schema): build_chroot_params = BuildChrootParams.get_cls().params_schema() build_id_params = {"build_id": build_chroot_params["build_id"]} can_build_params = CanBuildParams.get_cls().params_schema() +list_build_params = ListBuild.get_cls().params_schema() diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py index 772345979..e413ba2ad 100644 --- a/frontend/coprs_frontend/coprs/views/misc.py +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -157,53 +157,6 @@ def logout(): return UserAuth.logout() -def _shared_api_login_required_wrapper(): - token = None - api_login = None - if "Authorization" in flask.request.headers: - base64string = flask.request.headers["Authorization"] - base64string = base64string.split()[1].strip() - userstring = base64.b64decode(base64string) - (api_login, token) = userstring.decode("utf-8").split(":") - token_auth = False - if token and api_login: - user = UsersLogic.get_by_api_login(api_login).first() - if (user and user.api_token == token and - user.api_token_expiration >= datetime.date.today()): - token_auth = True - flask.g.user = user - if not token_auth: - url = 'https://' + app.config["PUBLIC_COPR_HOSTNAME"] - url = helpers.fix_protocol_for_frontend(url) - - msg = "Attempting to use invalid or expired API login '%s'" - app.logger.info(msg, api_login) - - output = { - "output": "notok", - "error": "Login invalid/expired. Please visit {0}/api to get or renew your API token.".format(url), - } - jsonout = flask.jsonify(output) - jsonout.status_code = 401 - return jsonout - return None - - -def api_login_required(f): - @functools.wraps(f) - def decorated_function(*args, **kwargs): - # flask.g.user can be already set in case a user is using gssapi auth, - # in that case before_request was called and the user is known. - if flask.g.user is not None: - return f(*args, **kwargs) - retval = _shared_api_login_required_wrapper() - if retval is not None: - return retval - - return f(*args, **kwargs) - return decorated_function - - def login_required(role=RoleEnum("user")): def view_wrapper(f): @functools.wraps(f) @@ -324,7 +277,38 @@ def wrapper(*args, **kwargs): # is done -def restx_api_login_required(endpoint_method): +def _check_api_login(): + token = None + api_login = None + if "Authorization" in flask.request.headers: + base64string = flask.request.headers["Authorization"] + base64string = base64string.split()[1].strip() + userstring = base64.b64decode(base64string) + (api_login, token) = userstring.decode("utf-8").split(":") + token_auth = False + if token and api_login: + user = UsersLogic.get_by_api_login(api_login).first() + if (user and user.api_token == token and + user.api_token_expiration >= datetime.date.today()): + token_auth = True + flask.g.user = user + if not token_auth: + url = 'https://' + app.config["PUBLIC_COPR_HOSTNAME"] + url = helpers.fix_protocol_for_frontend(url) + + msg = "Attempting to use invalid or expired API login '%s'" + app.logger.info(msg, api_login) + + output = { + "output": "notok", + "error": "Login invalid/expired. Please visit {0}/api to get or renew your API token.".format(url), + } + return output, 401 + + return None + + +def api_login_required(endpoint_method): """ Checks whether API login is required for an endpoint which is decorated by this decorator. @@ -338,9 +322,10 @@ def check_if_api_login_is_required(self, *args, **kwargs): # in that case before_request was called and the user is known. if flask.g.user is not None: return endpoint_method(self, *args, **kwargs) - retval = _shared_api_login_required_wrapper() + + retval = _check_api_login() if retval is not None: - return retval.json, retval.status_code + return retval return endpoint_method(self, *args, **kwargs) return check_if_api_login_is_required