From ff007d23ebd67f0a7984ba0294faedb937f18a2a Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Jun 21 2022 12:36:10 +0000 Subject: [PATCH 1/5] frontend: move the logic about admin using his permissions from templates The logic is a bit too complicated for being written as jinja2 macro, so I am rewriting it in python. Also fixing a bug - the previous implementation was displaying the warning even for group projects that the admin belongs to. --- diff --git a/frontend/coprs_frontend/coprs/filters.py b/frontend/coprs_frontend/coprs/filters.py index c657c3b..fb3c3a0 100644 --- a/frontend/coprs_frontend/coprs/filters.py +++ b/frontend/coprs_frontend/coprs/filters.py @@ -258,3 +258,11 @@ def int_with_commas(number): e.g. display 16,724 instead of 16724, it's more readable. """ return format(int(number), ',d') + + +@app.template_global("being_server_admin") +def being_server_admin(user, copr): + """ + Is Copr maintainer using their special permissions to edit the project? + """ + return helpers.being_server_admin(user, copr) diff --git a/frontend/coprs_frontend/coprs/helpers.py b/frontend/coprs_frontend/coprs/helpers.py index 311fc61..5ba3d9b 100644 --- a/frontend/coprs_frontend/coprs/helpers.py +++ b/frontend/coprs_frontend/coprs/helpers.py @@ -891,3 +891,22 @@ def streamed_json(stream, start_string=None, stop_string=None): ) return _response() + + +def being_server_admin(user, copr): + """ + Is Copr maintainer using their special permissions to edit the project? + """ + if not user: + return False + + if not user.admin: + return False + + if user == copr.user: + return False + + if user.can_edit(copr, ignore_admin=True): + return False + + return True diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py index 2f63b9a..76c0cb7 100644 --- a/frontend/coprs_frontend/coprs/models.py +++ b/frontend/coprs_frontend/coprs/models.py @@ -162,13 +162,20 @@ class User(db.Model, helpers.Serializer): else: return False - def can_edit(self, copr): + def can_edit(self, copr, ignore_admin=False): """ Determine if this user can edit the given copr. """ + # People can obviously edit their personal projects, but the person who + # created a group project doesn't get any exclusive permissions to it. + # We still need to validate their group membership every time. + if not copr.group and copr.user == self: + return True - if copr.user == self or self.admin: + # Copr maintainers can edit every project + if not ignore_admin and self.admin: return True + if (self.permissions_for_copr(copr) and self.permissions_for_copr(copr).copr_admin == helpers.PermissionEnum("approved")): diff --git a/frontend/coprs_frontend/coprs/templates/_helpers.html b/frontend/coprs_frontend/coprs/templates/_helpers.html index f0072fe..d493b12 100644 --- a/frontend/coprs_frontend/coprs/templates/_helpers.html +++ b/frontend/coprs_frontend/coprs/templates/_helpers.html @@ -674,12 +674,11 @@ https://accounts.fedoraproject.org/group/{{name}} {% macro warn_server_admin(copr) %} - {% if g.user and g.user.admin and g.user != copr.user %} - {% if not g.user.permissions_for_copr(copr) or g.user.permissions_for_copr(copr).copr_admin < 2 %} - {% set msg = "As a maintainer of this Copr instance, you have permissions to modify this project, - but if you are acting as a regular user now, be aware that this is not your project." %} - {{ alert(msg, type="warning") }} - {% endif %} + {% if being_server_admin(g.user, copr) %} + {% set msg = "As a maintainer of this Copr instance, you have permissions " + "to modify this project, but if you are acting as a regular " + "user now, be aware that this is not your project." %} + {{ alert(msg, type="warning") }} {% endif %} {% endmacro %} diff --git a/frontend/coprs_frontend/tests/test_helpers.py b/frontend/coprs_frontend/tests/test_helpers.py index 10028d9..80df3c5 100644 --- a/frontend/coprs_frontend/tests/test_helpers.py +++ b/frontend/coprs_frontend/tests/test_helpers.py @@ -9,7 +9,8 @@ from coprs import app from coprs.helpers import parse_package_name, generate_repo_url, \ fix_protocol_for_frontend, fix_protocol_for_backend, pre_process_repo_url, \ parse_repo_params, pagure_html_diff_changed, SubdirMatch, \ - raw_commit_changes, WorkList, pluralize, clone_sqlalchemy_instance + raw_commit_changes, WorkList, pluralize, clone_sqlalchemy_instance, \ + being_server_admin from tests.coprs_test_case import CoprsTestCase @@ -261,6 +262,20 @@ class TestHelpers(CoprsTestCase): assert "fedora-17-x86_64" in \ [m.name for m in target_copr.mock_chroots] + @pytest.mark.usefixtures("f_users", "f_fas_groups", "f_coprs", + "f_group_copr", "f_db") + def test_being_server_admin(self): + assert self.u1.admin + + assert not being_server_admin(self.u1, self.c1) + assert being_server_admin(self.u1, self.c2) + + self.gc1.user = self.u2 + assert not being_server_admin(self.u1, self.gc1) + + self.u1.openid_groups["fas_groups"] = [] + assert being_server_admin(self.u1, self.gc1) + def test_worklist_class(): """ test that all tasks are processed only once """ From 3a74aa351435d63bd5d1f63a43d9bad1fb23c1df Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Jun 21 2022 12:36:10 +0000 Subject: [PATCH 2/5] frontend: don't use main_dir for getting packages Leftover after PR#2205 --- diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py index 9e16018..2cb52a3 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py @@ -244,7 +244,7 @@ def process_save_package(copr, source_type_text, package_name, view, view_method if "reset" in flask.request.form: try: - package = PackagesLogic.get(copr.main_dir.id, package_name)[0] + package = PackagesLogic.get(copr.id, package_name)[0] except IndexError: flask.flash("Package {0} does not exist in copr_dir {1}." .format(package_name, copr.main_dir.full_name)) @@ -263,7 +263,7 @@ def process_save_package(copr, source_type_text, package_name, view, view_method if form.validate_on_submit(): try: if package_name: - package = PackagesLogic.get(copr.main_dir.id, package_name)[0] + package = PackagesLogic.get(copr.id, package_name)[0] else: package = PackagesLogic.add(flask.app.g.user, copr, form.package_name.data) From 1e6d3a232870a9eb14cb0528acdd61e32cc7a220 Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Jun 21 2022 12:36:10 +0000 Subject: [PATCH 3/5] frontend: change logging formatter to show also flask.g.user --- diff --git a/frontend/coprs_frontend/coprs/__init__.py b/frontend/coprs_frontend/coprs/__init__.py index ed5b086..98bc5fc 100644 --- a/frontend/coprs_frontend/coprs/__init__.py +++ b/frontend/coprs_frontend/coprs/__init__.py @@ -146,7 +146,8 @@ from coprs.exceptions import ( from coprs.error_handlers import get_error_handler import coprs.context_processors -setup_log() +with app.app_context(): + setup_log() app.register_blueprint(api_ns.api_ns) app.register_blueprint(apiv3_ns.apiv3_ns) diff --git a/frontend/coprs_frontend/coprs/log.py b/frontend/coprs_frontend/coprs/log.py index b69fa91..fc7519a 100644 --- a/frontend/coprs_frontend/coprs/log.py +++ b/frontend/coprs_frontend/coprs/log.py @@ -1,6 +1,7 @@ import logging from logging import Formatter from logging.handlers import SMTPHandler, WatchedFileHandler +import flask from coprs import app @@ -19,10 +20,36 @@ Message: default_formatter = Formatter( "%(asctime)s [%(levelname)s]" - "[%(pathname)s:%(lineno)d|%(module)s:%(funcName)s] %(message)s" + "[%(pathname)s:%(lineno)d|%(module)s:%(funcName)s][%(user)s] %(message)s" ) +class FlaskUserFilter(logging.Filter): + """ + Define `%(user)s` key for the formatter string + """ + + def filter(self, record): + record.user = self._user() + return True + + def _user(self): + if not flask.g or not hasattr(flask.g, "user"): + return "SERVER" + + if flask.g.user: + return flask.g.user.name + + return self._backend() or "ANON" + + @staticmethod + def _backend(): + auth = flask.request.authorization + if auth and auth.password == app.config["BACKEND_PASSWORD"]: + return "backend: {0}".format(flask.request.remote_addr) + return None + + def setup_log(): if not app.debug: # Send critical message by email @@ -44,6 +71,7 @@ def setup_log(): log_level_text = app.config.get("LOGGING_LEVEL", 'info') log_level = getattr(logging, log_level_text.upper()) handler.setLevel(log_level) + app.logger.addFilter(FlaskUserFilter()) app.logger.setLevel(log_level) app.logger.addHandler(handler) From 872c0b0ff82aa9494c55941c0c94dbe6978a073b Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Jun 21 2022 12:36:10 +0000 Subject: [PATCH 4/5] frontend: use logger from coprs.app instead of creating new ones --- diff --git a/frontend/coprs_frontend/coprs/error_handlers.py b/frontend/coprs_frontend/coprs/error_handlers.py index ea4683d..0867eb4 100644 --- a/frontend/coprs_frontend/coprs/error_handlers.py +++ b/frontend/coprs_frontend/coprs/error_handlers.py @@ -2,8 +2,6 @@ A place for exception-handling logic """ -import logging - import flask from sqlalchemy.exc import SQLAlchemyError from werkzeug.exceptions import ( @@ -12,10 +10,9 @@ from werkzeug.exceptions import ( HTTPException, NotFound, ) +from coprs import app from coprs.exceptions import CoprHttpException -LOG = logging.getLogger(__name__) - def get_error_handler(): """ @@ -42,7 +39,7 @@ class BaseErrorHandler: message = self.message(error) headers = getattr(error, "headers", None) message = self.override_message(message, error) - LOG.error("Response error: %s %s", code, message) + app.logger.error("Response error: %s %s", code, message) return self.render(message, code), code, headers @staticmethod @@ -102,10 +99,10 @@ class BaseErrorHandler: def _log_admin_only_exception(self): # pylint: disable=no-self-use - LOG.exception("Admin-only exception\nRequest: %s %s\nUser: %s\n", - flask.request.method, - flask.request.url, - flask.g.user.name if flask.g.user else None) + app.logger.exception("Admin-only exception\nRequest: %s %s\nUser: %s\n", + flask.request.method, + flask.request.url, + flask.g.user.name if flask.g.user else None) class UIErrorHandler(BaseErrorHandler): diff --git a/frontend/coprs_frontend/coprs/rest_api/common.py b/frontend/coprs_frontend/coprs/rest_api/common.py index 60475ad..94187d5 100644 --- a/frontend/coprs_frontend/coprs/rest_api/common.py +++ b/frontend/coprs_frontend/coprs/rest_api/common.py @@ -2,7 +2,6 @@ import base64 import datetime import functools -from logging import getLogger from flask import url_for import flask @@ -18,8 +17,6 @@ from .schemas import CoprChrootSchema, BuildSchema, ProjectSchema from .util import mm_serialize_one from coprs import app -log = getLogger(__name__) - def render_copr_chroot(chroot): return { @@ -93,7 +90,7 @@ def rest_api_auth_required(f): userstring = base64.b64decode(base64string) (api_login, token) = userstring.decode("utf-8").split(":") except Exception: - log.exception("Failed to get auth token from headers") + app.logger.exception("Failed to get auth token from headers") api_login = token = None token_auth = False diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/project.py b/frontend/coprs_frontend/coprs/rest_api/resources/project.py index 3bc0dec..2fa754c 100644 --- a/frontend/coprs_frontend/coprs/rest_api/resources/project.py +++ b/frontend/coprs_frontend/coprs/rest_api/resources/project.py @@ -1,5 +1,3 @@ -from logging import getLogger - import flask from flask import url_for, make_response from flask_restful import Resource @@ -18,8 +16,6 @@ from ..schemas import ProjectSchema, ProjectCreateSchema from ..exceptions import ObjectAlreadyExists, CannotProcessRequest, AccessForbidden from ..util import mm_deserialize, get_request_parser, arg_bool -log = getLogger(__name__) - class ProjectListR(Resource): diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/project_chroot.py b/frontend/coprs_frontend/coprs/rest_api/resources/project_chroot.py index 00f13e4..58eea86 100644 --- a/frontend/coprs_frontend/coprs/rest_api/resources/project_chroot.py +++ b/frontend/coprs_frontend/coprs/rest_api/resources/project_chroot.py @@ -1,9 +1,5 @@ # coding: utf-8 -import logging - -log = logging.getLogger(__name__) - import flask from flask import url_for, make_response from flask_restful import Resource diff --git a/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py b/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py index 8d5a02f..74c4ec4 100644 --- a/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py +++ b/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py @@ -16,10 +16,6 @@ from coprs.views import misc from coprs.views.backend_ns import backend_ns from sqlalchemy.sql import false, true -import logging - -log = logging.getLogger(__name__) - @backend_ns.after_request def send_frontend_version(response): @@ -308,14 +304,14 @@ def pending_jobs(): def _stream(): args = {"data_type": "for_backend"} - log.info("Generating SRPM builds") + app.logger.info("Generating SRPM builds") for build in BuildsLogic.get_pending_srpm_build_tasks(**args): if not build_ready(build): continue record = get_srpm_build_record(build, for_backend=True) yield record - log.info("Generating RPM builds") + app.logger.info("Generating RPM builds") for build_chroot in BuildsLogic.get_pending_build_tasks(**args): if not build_ready(build_chroot.build): continue @@ -432,7 +428,7 @@ def reschedule_build_chroot(): if task_id == build.task_id: if build.source_status in run_statuses: - log.info("rescheduling source build %s", build.id) + app.logger.info("rescheduling source build %s", build.id) BuildsLogic.update_state_from_dict(build, { "task_id": task_id, "status": StatusEnum("pending") @@ -445,7 +441,8 @@ def reschedule_build_chroot(): else: build_chroot = build.chroots_dict_by_name.get(chroot) if build_chroot and build_chroot.status in run_statuses: - log.info("rescheduling build {} chroot: {}".format(build.id, build_chroot.name)) + app.logger.info("rescheduling build {} chroot: {}" + .format(build.id, build_chroot.name)) BuildsLogic.update_state_from_dict(build, { "task_id": task_id, "chroot": chroot, diff --git a/frontend/coprs_frontend/coprs/views/recent_ns/recent_general.py b/frontend/coprs_frontend/coprs/views/recent_ns/recent_general.py index 427d09d..91ddecc 100644 --- a/frontend/coprs_frontend/coprs/views/recent_ns/recent_general.py +++ b/frontend/coprs_frontend/coprs/views/recent_ns/recent_general.py @@ -4,10 +4,6 @@ from . import recent_ns from coprs.logic import builds_logic from coprs.views.misc import login_required -import logging - -log = logging.getLogger(__name__) - @recent_ns.route("/") @recent_ns.route("/all/") diff --git a/frontend/coprs_frontend/pagure_events.py b/frontend/coprs_frontend/pagure_events.py index 7cc86c6..7da8fc2 100755 --- a/frontend/coprs_frontend/pagure_events.py +++ b/frontend/coprs_frontend/pagure_events.py @@ -4,7 +4,6 @@ import json import pprint import sys import os -import logging import requests import re import munch @@ -27,8 +26,7 @@ SUPPORTED_SOURCE_TYPES = [ helpers.BuildSourceEnum("distgit"), ] -log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) +log = app.logger TOPICS = {} for topic, url in app.config["PAGURE_EVENTS"].items(): From 72a79604608c323aea5261ef98027c0da294463b Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Jun 21 2022 12:36:10 +0000 Subject: [PATCH 5/5] frontend: start logging important events --- diff --git a/frontend/coprs_frontend/coprs/logic/coprs_logic.py b/frontend/coprs_frontend/coprs/logic/coprs_logic.py index 4785f22..5161800 100644 --- a/frontend/coprs_frontend/coprs/logic/coprs_logic.py +++ b/frontend/coprs_frontend/coprs/logic/coprs_logic.py @@ -311,6 +311,10 @@ class CoprsLogic(object): if not user.admin and not copr.auto_prune: raise exceptions.NonAdminCannotDisableAutoPrunning() + if helpers.being_server_admin(user, copr): + app.logger.info("Admin '%s' using their permissions to update " + "project '%s' settings", user.name, copr.full_name) + db.session.add(copr) @classmethod @@ -444,6 +448,12 @@ class CoprPermissionsLogic(object): user, copr, "Only owners and admins may update" " their projects permissions.") + app.logger.info("User '%s' authorized permission change for project '%s'" + " - The '%s' user is now 'builder=%s', 'admin=%s'", + user.name, copr.full_name, copr_permission.user.name, + helpers.PermissionEnum(new_builder), + helpers.PermissionEnum(new_admin)) + (models.CoprPermission.query .filter(models.CoprPermission.copr_id == copr.id) .filter(models.CoprPermission.user_id == copr_permission.user_id) @@ -452,6 +462,13 @@ class CoprPermissionsLogic(object): @classmethod def update_permissions_by_applier(cls, user, copr, copr_permission, new_builder, new_admin): + app.logger.info("User '%s' requests 'builder=%s', 'admin=%s' " + "permissions for project '%s'", + user.name, + helpers.PermissionEnum(new_builder), + helpers.PermissionEnum(new_admin), + copr.full_name) + if copr_permission: # preserve approved permissions if set if (not new_builder or @@ -502,6 +519,11 @@ class CoprPermissionsLogic(object): cls.validate_permission(user, copr, permission, state) + app.logger.info("User '%s' authorized permission change for project '%s'" + " - The '%s' user is now '%s=%s'", + request_user.name, copr.full_name, user.name, + permission, state) + perm_o = models.CoprPermission(user_id=user.id, copr_id=copr.id) perm_o = db.session.merge(perm_o) old_state = perm_o.get_permission(permission) @@ -525,6 +547,9 @@ class CoprPermissionsLogic(object): "expected True or False".format(permission, req_bool)) + app.logger.info("User '%s' requests '%s=%s' permission for project '%s'", + user.name, permission, state, copr.full_name) + cls.validate_permission(user, copr, permission, state) perm_o = models.CoprPermission(user_id=user.id, copr_id=copr.id) perm_o = db.session.merge(perm_o) @@ -919,6 +944,10 @@ class CoprChrootsLogic(object): user, copr_chroot.copr, "Only owners and admins may update their projects.") + if helpers.being_server_admin(user, copr_chroot.copr): + app.logger.info("Admin '%s' using their permissions to update " + "chroot '%s'", user.name, copr_chroot.full_name) + cls._update_chroot(buildroot_pkgs, repos, comps, comps_name, copr_chroot, with_opts, without_opts, delete_after, delete_notify, module_toggle, bootstrap, bootstrap_image, isolation) diff --git a/frontend/coprs_frontend/coprs/logic/packages_logic.py b/frontend/coprs_frontend/coprs/logic/packages_logic.py index 2e0137c..18c0c3b 100644 --- a/frontend/coprs_frontend/coprs/logic/packages_logic.py +++ b/frontend/coprs_frontend/coprs/logic/packages_logic.py @@ -328,3 +328,16 @@ class PackagesLogic(object): builds[build].append(build_chroot) break return builds + + + @classmethod + def log_being_admin(cls, user, package): + """ + Log if a package is being updated by a Copr maintainer + """ + if helpers.being_server_admin(user, package.copr): + app.logger.info("Admin '%s' using their permissions to update " + "package '%s' in project '%s'", + user.name, + package.name, + package.copr.full_name) diff --git a/frontend/coprs_frontend/coprs/logic/users_logic.py b/frontend/coprs_frontend/coprs/logic/users_logic.py index 70f492e..e5e14ea 100644 --- a/frontend/coprs_frontend/coprs/logic/users_logic.py +++ b/frontend/coprs_frontend/coprs/logic/users_logic.py @@ -14,6 +14,7 @@ class UsersLogic(object): @classmethod def get(cls, username): + app.logger.info("Querying user '%s' by username", username) return User.query.filter(User.username == username) @classmethod @@ -40,6 +41,9 @@ class UsersLogic(object): if not user.can_edit(copr): raise exceptions.InsufficientRightsException(message) + app.logger.info("User '%s' allowed to update project '%s'", + user.name, copr.full_name) + @classmethod def raise_if_cant_build_in_copr(cls, user, copr, message): """ @@ -50,6 +54,9 @@ class UsersLogic(object): if not user.can_build_in(copr): raise exceptions.InsufficientRightsException(message) + app.logger.info("User '%s' allowed to build in project '%s'", + user.name, copr.full_name) + @classmethod def raise_if_not_in_group(cls, user, group): if not user.admin and group.fas_name not in user.user_teams: @@ -57,6 +64,9 @@ class UsersLogic(object): "User '{}' doesn't have access to the copr group '{}' (fas_name='{}')" .format(user.username, group.name, group.fas_name)) + app.logger.info("User '%s' allowed to access group '%s' (fas_name='%s')", + user.name, group.name, group.fas_name) + @classmethod def get_group_by_alias(cls, name): return Group.query.filter(Group.name == name) @@ -130,6 +140,7 @@ class UsersLogic(object): "openid_groups": None} for k, v in null.items(): setattr(user, k, v) + app.logger.info("Deleting user '%s' data", user.name) @classmethod def create_user_wrapper(cls, username, email, timezone=None): @@ -149,6 +160,7 @@ class UsersLogic(object): api_token=generate_api_token( app.config["API_TOKEN_LENGTH"]), api_token_expiration=expiration_date_token) + app.logger.info("Creating user '%s <%s>'", user.name, user.mail) return user @@ -157,6 +169,7 @@ class UserDataDumper(object): self.user = user def dumps(self, pretty=False): + app.logger.info("Dumping all user data for '%s'", self.user.name) if pretty: return json.dumps(self.data, indent=2) return json.dumps(self.data) 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 899caaa..e4a6754 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py @@ -268,6 +268,7 @@ def process_package_add_or_edit(copr, source_type_text, package=None, data=None) if "chroot_denylist" in formdata: package.chroot_denylist_raw = form.chroot_denylist.data + PackagesLogic.log_being_admin(flask.g.user, package) db.session.add(package) db.session.commit() else: diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py index 2cb52a3..0169e79 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py @@ -273,6 +273,7 @@ def process_save_package(copr, source_type_text, package_name, view, view_method package.chroot_denylist_raw = form.chroot_denylist.data package.max_builds = form.max_builds.data + PackagesLogic.log_being_admin(flask.g.user, package) db.session.add(package) db.session.commit() except (InsufficientRightsException, IndexError) as e: diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py index 4074b5f..028380c 100644 --- a/frontend/coprs_frontend/coprs/views/misc.py +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -126,6 +126,8 @@ def create_or_login(resp): user = models.User.query.filter( models.User.username == username).first() if not user: # create if not created already + app.logger.info("First login for user '%s', " + "creating a database record", username) user = UsersLogic.create_user_wrapper(username, resp.email, resp.timezone) else: @@ -140,6 +142,10 @@ def create_or_login(resp): flask.flash(u"Welcome, {0}".format(user.name), "success") flask.g.user = user + app.logger.info("%s '%s' logged in", + "Admin" if user.admin else "User", + user.name) + if flask.request.url_root == oid.get_next_url(): return flask.redirect(flask.url_for("coprs_ns.coprs_by_user", username=user.name)) @@ -151,6 +157,8 @@ def create_or_login(resp): @misc.route("/logout/") def logout(): + if flask.g.user: + app.logger.info("User '%s' logging out", flask.g.user.name) flask.session.pop("openid", None) flask.session.pop("krb5_login", None) flask.flash(u"You were signed out") @@ -182,6 +190,9 @@ def api_login_required(f): 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), diff --git a/frontend/coprs_frontend/tests/test_logging.py b/frontend/coprs_frontend/tests/test_logging.py new file mode 100644 index 0000000..4021945 --- /dev/null +++ b/frontend/coprs_frontend/tests/test_logging.py @@ -0,0 +1,261 @@ +# pylint: disable=cyclic-import +# pylint: disable=no-self-use + +from unittest import TestCase +from unittest.mock import patch, MagicMock +import pytest +import flask +from munch import Munch +from tests.coprs_test_case import CoprsTestCase, TransactionDecorator +from coprs import app, db, models, helpers +from coprs.views.misc import create_or_login, logout + +from coprs.logic.users_logic import UsersLogic, UserDataDumper +from coprs.logic.coprs_logic import ( + CoprsLogic, + CoprPermissionsLogic, + CoprChrootsLogic, +) + + +class TestLoggingRequestUser(CoprsTestCase, TestCase): + + @pytest.mark.usefixtures("f_users", "f_coprs", "f_db") + def test_server(self): + with self.assertLogs(app.logger) as cm: + app.logger.info("FOO") + assert cm.records[0].user == "SERVER" + + @pytest.mark.usefixtures("f_u1_ts_client", "f_users", "f_coprs", "f_db") + def test_user(self): + with self.assertLogs(app.logger) as cm: + self.api3.modify_project(self.c2.name, self.c2.owner_name) + assert cm.records[0].user == "user1" + + @pytest.mark.usefixtures("f_users", "f_coprs", "f_db") + def test_anon(self): + with self.assertLogs(app.logger) as cm: + self.tc.get("/coprs/user2", follow_redirects=True) + assert cm.records[0].user == "ANON" + + +class TestLoggingUsersLogic(CoprsTestCase): + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_db") + def test_user_get(self, log): + UsersLogic.get("somebody") + log.info.assert_called_once_with( + "Querying user '%s' by username", "somebody") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_db") + def test_raise_if_cant_update_copr(self, log): + UsersLogic.raise_if_cant_update_copr(self.u2, self.c2, None) + log.info.assert_called_once_with( + "User '%s' allowed to update project '%s'", + "user2", "user2/foocopr") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_db") + def test_raise_if_cant_build_in_copr(self, log): + UsersLogic.raise_if_cant_build_in_copr(self.u2, self.c2, None) + log.info.assert_called_once_with( + "User '%s' allowed to build in project '%s'", + "user2", "user2/foocopr") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_groups", "f_db") + def test_raise_if_not_in_group(self, log): + UsersLogic.raise_if_not_in_group(self.u1, self.g1) + log.info.assert_called_once_with( + "User '%s' allowed to access group '%s' (fas_name='%s')", + "user1", "group1", "fas_1") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_db") + def test_delete_user_data(self, log): + UsersLogic.delete_user_data(self.u2) + log.info.assert_called_once_with("Deleting user '%s' data", "user2") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_db") + def test_create_user_wrapper(self, log): + UsersLogic.create_user_wrapper("somebody", "somebody@example.test") + log.info.assert_called_once_with("Creating user '%s <%s>'", + "somebody", "somebody@example.test") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_db") + def test_user_data_dumper(self, log): + dumper = UserDataDumper(self.u2) + dumper.dumps() + log.info.assert_called_once_with("Dumping all user data for '%s'", + "user2") + + +class TestLoggingUserGeneral(CoprsTestCase): + + @patch("coprs.app.logger", return_value=MagicMock()) + @TransactionDecorator("u2") + @pytest.mark.usefixtures("f_users", "f_db") + def test_delete_user_data(self, log): + self.tc.get("/user/info/download") + log.info.assert_called_once_with("Dumping all user data for '%s'", + "user2") + + +class TestLoggingCoprPermissionsLogic(CoprsTestCase): + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_copr_permissions", "f_db") + def test_update_permissions(self, log): + perm = models.CoprPermission( + copr=self.c2, + user=self.u3, + copr_builder=helpers.PermissionEnum("request"), + copr_admin=helpers.PermissionEnum("request")) + + CoprPermissionsLogic.update_permissions( + self.u2, self.c2, perm, + new_builder=helpers.PermissionEnum("approved"), + new_admin=helpers.PermissionEnum("nothing")) + + log.info.assert_called_with( + "User '%s' authorized permission change for project '%s'" + " - The '%s' user is now 'builder=%s', 'admin=%s'", + "user2", "user2/foocopr", "user3", "approved", "nothing") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_copr_permissions", "f_db") + def test_update_permissions_by_applier(self, log): + CoprPermissionsLogic.update_permissions_by_applier( + self.u2, self.c2, None, + new_builder=helpers.PermissionEnum("request"), + new_admin=helpers.PermissionEnum("nothing")) + + msg = ("User '%s' requests 'builder=%s', 'admin=%s' " + "permissions for project '%s'") + log.info.assert_called_once_with( + msg, "user2", "request", "nothing", "user2/foocopr") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_copr_permissions", "f_db") + def test_set_permissions(self, log): + CoprPermissionsLogic.set_permissions( + self.u2, self.c2, self.u3, "builder", "approved") + log.info.assert_called_with( + "User '%s' authorized permission change for project '%s'" + " - The '%s' user is now '%s=%s'", + "user2", "user2/foocopr", "user3", "builder", "approved") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_db") + def test_request_permission(self, log): + CoprPermissionsLogic.request_permission( + self.c1, self.u2, "builder", True) + log.info.assert_called_once_with( + "User '%s' requests '%s=%s' permission for project '%s'", + "user2", "builder", "request", "user1/foocopr") + + +class TestLoggingAuth(CoprsTestCase): + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_db") + def test_create_or_login(self, log): + resp = Munch({ + "identity_url": 'http://somebody.id.fedoraproject.org/', + "email": "somebody@example.com", + "timezone": "UTC", + "extensions": {}, + }) + + # Log in as user + with app.test_request_context(): + create_or_login(resp) + + log.info.assert_any_call("First login for user '%s', creating " + "a database record", "somebody") + log.info.assert_called_with("%s '%s' logged in", "User", "somebody") + + # Modify user to be an admin + user = models.User.query.filter_by(username="somebody").one() + user.admin = True + db.session.commit() + + # Log in as admin + with app.test_request_context(): + create_or_login(resp) + + log.info.assert_called_with("%s '%s' logged in", "Admin", "somebody") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_db") + def test_logout(self, log): + with app.test_request_context(): + flask.g.user = self.u2 + logout() + log.info.assert_called_with("User '%s' logging out", "user2") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_mock_chroots", "f_db") + def test_api_login_required_invalid(self, log): + headers = self.api3_auth_headers(self.u2) + self.tc.post("/api_3/project/add/user2", headers=headers) + + log.info.assert_called_once_with( + "Attempting to use invalid or expired API login '%s'", "abc") + + +class TestLoggingUsingAdminPermissions(CoprsTestCase): + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_db") + def test_update_copr(self, log): + CoprsLogic.update(self.u1, self.c2) + log.info.assert_called_with( + "Admin '%s' using their permissions to update project '%s' settings", + "user1", "user2/foocopr") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db") + def test_update_copr_chroot(self, log): + CoprChrootsLogic.update_chroot(self.u1, self.c2.copr_chroots[0]) + log.info.assert_called_with( + "Admin '%s' using their permissions to update chroot '%s'", + "user1", "user2/foocopr/fedora-17-x86_64") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_u1_ts_client", "f_coprs", "f_builds", "f_db") + def test_update_package_webui(self, log): + url = "/coprs/{0}/package/{1}/edit/scm".format( + self.c2.full_name, self.p2.name) + + data = { + "clone_url": "https://gitlab.com/zhanggyb/nerd-fonts.git", + "package_name": self.p2.name, + } + + self.tc.post(url, headers=self.auth_header, data=data) + log.info.assert_called_with( + "Admin '%s' using their permissions to update " + "package '%s' in project '%s'", + "user1", "whatsupthere-world", "user2/foocopr") + + @patch("coprs.app.logger", return_value=MagicMock()) + @pytest.mark.usefixtures("f_users", "f_u1_ts_client", "f_coprs", "f_builds", "f_db") + def test_update_package_apiv3(self, log): + url = "/api_3/package/edit/{0}/{1}/scm".format( + self.c2.full_name, self.p2.name) + + data = { + "clone_url": "https://gitlab.com/zhanggyb/nerd-fonts.git", + "package_name": self.p2.name, + } + + self.post_api3_with_auth(url, data, self.u1) + log.info.assert_called_with( + "Admin '%s' using their permissions to update " + "package '%s' in project '%s'", + "user1", "whatsupthere-world", "user2/foocopr")