diff --git a/cli/config.py b/cli/config.py index 7c5b89de1..80b626c9b 100644 --- a/cli/config.py +++ b/cli/config.py @@ -179,18 +179,14 @@ def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> N log.info("Exporting configuration from %s completed", kwargs["url"]) -def __read_input_file(file: str) -> dict[str, any]: +def __import_config(endpoint: platform.Platform, what: list[str], **kwargs) -> None: + """Imports a platform configuration from a JSON file""" + log.info("Importing configuration to %s", kwargs[options.URL]) try: - with open(file, "r", encoding="utf-8") as fd: + with open(kwargs[options.REPORT_FILE], "r", encoding="utf-8") as fd: data = json.loads(fd.read()) except FileNotFoundError as e: utilities.exit_fatal(f"OS error while reading file: {e}", exit_code=errcodes.OS_ERROR) - return data - - -def __import_config(endpoint: platform.Platform, what: list[str], data: dict[str, any], **kwargs) -> None: - """Imports a platform configuration from a JSON file""" - log.info("Importing configuration to %s", kwargs[options.URL]) key_list = kwargs[options.KEYS] calls = { @@ -240,7 +236,7 @@ def main() -> None: if kwargs["import"]: if kwargs["file"] is None: utilities.exit_fatal("--file is mandatory to import configuration", errcodes.ARGS_ERROR) - __import_config(endpoint, what, __read_input_file(kwargs[options.REPORT_FILE]), **kwargs) + __import_config(endpoint, what, **kwargs) utilities.stop_clock(start_time) sys.exit(0) diff --git a/doc/sonar-audit.md b/doc/sonar-audit.md index f53088984..601e9d5bf 100644 --- a/doc/sonar-audit.md +++ b/doc/sonar-audit.md @@ -111,6 +111,7 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';' - More than 3 groups with `create project` permission - More than 10 groups with any global permissions - Permission Templates: (if `audit.projects.permissions = yes`, default `yes`) + - Permissions Templates with no permissions granted - More than `audit.projects.permissions.maxUsers` different users with direct permissions (default 5) - More than `audit.projects.permissions.maxAdminUsers` users with Project admin permission (default 2) - More than `audit.projects.permissions.maxGroups` different groups with permissions on project (default 5) @@ -120,6 +121,10 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';' - More than `audit.projects.permissions.maxAdminGroups` groups with project admin permission (default 2) - `sonar-users` group with elevated project permissions - `Anyone` group with any project permissions + - No projectKeyPattern for a template that is not a default + - Suspicious projectKeyPattern (a pattern that is likely to not select more than 1 key) for instance: + - `my_favorite_project` (no `.` in pattern) + - `BUXXXX-*` (Likely confusion between wildcards and regexp) - DB Cleaner: (if `audit.globalSettings = yes`, default `yes`) - Delay to delete inactive short lived branches (7.9) or branches (8.0+) not between 10 and 60 days - Delay to delete closed issues not between 10 and 60 days @@ -195,10 +200,12 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';' - Empty portfolios (with no projects) if `audit.portfolios.empty` is `yes` - Portfolios composed of a single project if `audit.portfolios.singleton` is `yes` - Last recomputation `FAILED` + - Portfolios with no permissions - Applications: (if `audit.applications = yes`, default `yes`) - Empty applications (with no projects) if `audit.applications.empty` is `yes` - Applications composed of a single project if `audit.applications.singleton` is `yes` - Last recomputation `FAILED` + - Applications with no permissions - Users: (if `audit.users = yes`, default `yes`) - Users that did not login on the platform since `audit.users.maxLoginAge` days (default 180 days) - Tokens older than `audit.tokens.maxAge` days (default 90 days) diff --git a/doc/what-is-new.md b/doc/what-is-new.md index c867a1a33..a06fcd0b5 100644 --- a/doc/what-is-new.md +++ b/doc/what-is-new.md @@ -1,7 +1,11 @@ # Next version yet unreleased - `sonar-tools` is now available as a docker image -- `sonar-config` export can now export configuration as a YAML file (Only JSON was available previously). Import of YAML is not yet available +- `sonar-config` + - Export can now export configuration as a YAML file (Only JSON was available previously). + Import of YAML is not yet available + - Beta version of config import in SonarCloud +- `sonar-audit` a couple of new audit problems on permission templates (with no permissions, with no or wrong regexp) # Version 3.3 diff --git a/sonar/aggregations.py b/sonar/aggregations.py index 626afc2b4..1810ee741 100644 --- a/sonar/aggregations.py +++ b/sonar/aggregations.py @@ -22,11 +22,13 @@ Parent module of applications and portfolios """ + +from typing import Optional import json import sonar.logging as log +from sonar.util import types import sonar.platform as pf -from sonar.util.types import ApiPayload, ApiParams import sonar.components as comp @@ -37,7 +39,7 @@ class Aggregation(comp.Component): """Parent class of applications and portfolios""" - def __init__(self, endpoint: pf.Platform, key: str, data: ApiPayload = None) -> None: + def __init__(self, endpoint: pf.Platform, key: str, data: types.ApiPayload = None) -> None: self._nbr_projects = None self._permissions = None super().__init__(endpoint=endpoint, key=key) @@ -90,8 +92,17 @@ def _audit_empty_aggregation(self, broken_rule: object) -> list[Problem]: def _audit_singleton_aggregation(self, broken_rule: object) -> list[Problem]: return self._audit_aggregation_cardinality((1, 1), broken_rule) + def permissions(self) -> Optional[object]: + """Should be implement in child classes""" + return self._permissions + + def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: + if self.permissions() is None: + return [] + return self.permissions().audit(audit_settings) + -def count(api: str, endpoint: pf.Platform, params: ApiParams = None) -> int: +def count(api: str, endpoint: pf.Platform, params: types.ApiParams = None) -> int: """Returns number of aggregations of a given type (Application OR Portfolio) :return: number of Apps or Portfolios :rtype: int diff --git a/sonar/applications.py b/sonar/applications.py index 2b9d9039f..fbc56b52d 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -321,7 +321,12 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[problem.Problem]: :rtype: list [Problem] """ log.info("Auditing %s", str(self)) - return self._audit_empty(audit_settings) + self._audit_singleton(audit_settings) + self._audit_bg_task(audit_settings) + return ( + super().audit(audit_settings) + + self._audit_empty(audit_settings) + + self._audit_singleton(audit_settings) + + self._audit_bg_task(audit_settings) + ) def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: """Exports an application @@ -346,7 +351,7 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: ) return util.remove_nones(util.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False))) - def set_permissions(self, data): + def set_permissions(self, data: types.JsonPermissions) -> application_permissions.ApplicationPermissions: """Sets an application permissions :param dict data: dict of permission {"users": [, , ...], "groups": [, , ...]} diff --git a/sonar/audit/rules.json b/sonar/audit/rules.json index 7c5a289d7..7ddbbfdec 100644 --- a/sonar/audit/rules.json +++ b/sonar/audit/rules.json @@ -9,6 +9,26 @@ "type": "BAD_PRACTICE", "message": "{}" }, + "OBJECT_WITH_NO_PERMISSIONS": { + "severity": "MEDIUM", + "type": "OPERATIONS", + "message": "{} has no permissions defined" + }, + "OBJECT_WITH_NO_ADMIN_PERMISSION": { + "severity": "MEDIUM", + "type": "OPERATIONS", + "message": "{} has no user or group with admin permission" + }, + "TEMPLATE_WITH_NO_PATTERN": { + "severity": "MEDIUM", + "type": "OPERATIONS", + "message": "{} is not a default and has no projectKetPattern defined, it will never be used" + }, + "TEMPLATE_WITH_SUSPICIOUS_PATTERN": { + "severity": "HIGH", + "type": "OPERATIONS", + "message": "{} has a suspicious projectKeyPattern '{}'. It should be a regexp that may match several keys" + }, "DUBIOUS_GLOBAL_SETTING": { "severity": "HIGH", "type": "BAD_PRACTICE", diff --git a/sonar/audit/rules.py b/sonar/audit/rules.py index d24019739..e3914703f 100644 --- a/sonar/audit/rules.py +++ b/sonar/audit/rules.py @@ -72,6 +72,8 @@ class RuleId(enum.Enum): ANYONE_WITH_GLOBAL_PERMS = 151 SONAR_USERS_WITH_ELEVATED_PERMS = 152 FAILED_WEBHOOK = 153 + OBJECT_WITH_NO_PERMISSIONS = 154 + OBJECT_WITH_NO_ADMIN_PERMISSION = 155 DCE_DIFFERENT_APP_NODES_VERSIONS = 160 DCE_DIFFERENT_APP_NODES_PLUGINS = 161 @@ -161,6 +163,9 @@ class RuleId(enum.Enum): GROUP_EMPTY = 5200 + TEMPLATE_WITH_NO_PATTERN = 5300 + TEMPLATE_WITH_SUSPICIOUS_PATTERN = 5301 + ERROR_IN_LOGS = 6000 WARNING_IN_LOGS = 6001 DEPRECATION_WARNINGS = 6002 diff --git a/sonar/permissions/global_permissions.py b/sonar/permissions/global_permissions.py index 00568243c..fb0500b50 100644 --- a/sonar/permissions/global_permissions.py +++ b/sonar/permissions/global_permissions.py @@ -18,22 +18,33 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # +from __future__ import annotations import sonar.logging as log from sonar.permissions import permissions +from sonar.util import types class GlobalPermissions(permissions.Permissions): + """Abstraction of SonarQube global permissions""" + API_GET = {"users": "permissions/users", "groups": "permissions/groups"} API_SET = {"users": "permissions/add_user", "groups": "permissions/add_group"} API_REMOVE = {"users": "permissions/remove_user", "groups": "permissions/remove_group"} API_GET_FIELD = {"users": "login", "groups": "name"} API_SET_FIELD = {"users": "login", "groups": "groupName"} - def __str__(self): + def __init__(self, concerned_object: object) -> None: + self.concerned_object = concerned_object + self.endpoint = concerned_object + self.permissions = None + self.read() + + def __str__(self) -> str: return "global permissions" - def read(self): + def read(self) -> GlobalPermissions: + """Reads global permissions""" self.permissions = permissions.NO_PERMISSIONS for ptype in permissions.PERMISSION_TYPES: self.permissions[ptype] = self._get_api( @@ -41,7 +52,7 @@ def read(self): ) return self - def set(self, new_perms): + def set(self, new_perms: types.JsonPermissions) -> GlobalPermissions: log.debug("Setting %s to %s", str(self), str(new_perms)) if self.permissions is None: self.read() @@ -57,7 +68,8 @@ def set(self, new_perms): return self.read() -def import_config(endpoint, config_data): +def import_config(endpoint: object, config_data: types.ObjectJsonRepr): + """Imports global permissions in a SonarQube platform""" my_permissions = config_data.get("permissions", {}) if len(my_permissions) == 0: log.info("No global permissions in config, skipping import...") @@ -67,9 +79,10 @@ def import_config(endpoint, config_data): global_perms.set(my_permissions) -def edition_filter(perms, ed): +def edition_filter(perms: types.JsonPermissions, ed: str) -> types.JsonPermissions: + """Filters permissions available in a given edition""" for p in perms.copy(): if ed == "community" and p in ("portfoliocreator", "applicationcreator") or ed == "developer" and p == "portfoliocreator": - log.warning("Can't remove permission '%s' on a %s edition", p, ed) + log.warning("Can't manage permission '%s' on a %s edition", p, ed) perms.remove(p) return perms diff --git a/sonar/permissions/permission_templates.py b/sonar/permissions/permission_templates.py index 137c6543e..548ad343a 100644 --- a/sonar/permissions/permission_templates.py +++ b/sonar/permissions/permission_templates.py @@ -21,6 +21,7 @@ from __future__ import annotations import json +import re from requests.exceptions import HTTPError import sonar.logging as log @@ -28,6 +29,7 @@ from sonar import sqobject, utilities from sonar.permissions import template_permissions import sonar.platform as pf +from sonar.audit.rules import get_rule, RuleId import sonar.audit.problem as pb _OBJECTS = {} @@ -176,9 +178,23 @@ def to_json(self, export_settings: types.ConfigSettings = None) -> types.ObjectJ json_data["lastUpdate"] = utilities.date_to_string(self.last_update) return utilities.remove_nones(utilities.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False))) + def _audit_pattern(self, audit_settings: types.ConfigSettings) -> list[pb.Problem]: + log.debug("Auditing %s projectKeyPattern ('%s')", str(self.project_key_pattern)) + if not self.project_key_pattern or self.project_key_pattern == "": + if not (self.is_applications_default() or self.is_portfolios_default() or self.is_projects_default()): + return [pb.Problem(get_rule(RuleId.TEMPLATE_WITH_NO_PATTERN), self, str(self))] + else: + # Inspect regexp to detect suspicious pattern - Can't determine all bad cases but do our best + # Currently detecting: + # - Absence of '.' in the regexp + # - '*' not preceded by '.' (confusion between wildcard and regexp) + if not re.search(r"(^|[^\\])\.", self.project_key_pattern) or re.search(r"(^|[^.])\*", self.project_key_pattern): + return [pb.Problem(get_rule(RuleId.TEMPLATE_WITH_SUSPICIOUS_PATTERN), self, str(self), self.project_key_pattern)] + return [] + def audit(self, audit_settings: types.ConfigSettings) -> list[pb.Problem]: log.debug("Auditing %s", str(self)) - return self.permissions().audit(audit_settings) + return self._audit_pattern(audit_settings) + self.permissions().audit(audit_settings) def get_object(endpoint: pf.Platform, name: str) -> PermissionTemplate: diff --git a/sonar/permissions/permissions.py b/sonar/permissions/permissions.py index c1f0de29f..7f2c46cea 100644 --- a/sonar/permissions/permissions.py +++ b/sonar/permissions/permissions.py @@ -31,6 +31,8 @@ import sonar.logging as log from sonar import utilities, errcodes from sonar.util import types +from sonar.audit.rules import get_rule, RuleId +from sonar.audit.problem import Problem COMMUNITY_GLOBAL_PERMISSIONS = { "admin": "Administer System", @@ -71,11 +73,15 @@ class Permissions(ABC): Abstraction of sonar objects permissions """ - def __init__(self, endpoint: object) -> None: - self.endpoint = endpoint + def __init__(self, concerned_object: object) -> None: + self.concerned_object = concerned_object + self.endpoint = concerned_object.endpoint self.permissions = None self.read() + def __str__(self) -> str: + return f"permissions of {str(self.concerned_object)}" + def to_json(self, perm_type: str = None, csv: bool = False) -> types.JsonPermissions: """Converts a permission object to JSON""" if not csv: @@ -98,10 +104,6 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: return None return perms - @abstractmethod - def __str__(self) -> str: - pass - @abstractmethod def read(self) -> Permissions: """ @@ -130,21 +132,6 @@ def set_group_permissions(self, group_perms: dict[str, list[str]]) -> Permission """ return self.set({"groups": group_perms}) - """ - @abstractmethod - def remove_user_permissions(self, user_perms_dict): - pass - - @abstractmethod - def remove_group_permissions(self, group_perms_dict): - pass - - - def remove_permissions(self, perms_dict): - self.remove_user_permissions(perms_dict.get("users", None)) - self.remove_group_permissions(perms_dict.get("groups", None)) - """ - def clear(self) -> Permissions: """Clears all permissions of an object :return: self @@ -206,6 +193,18 @@ def _filter_permissions_for_edition(self, perms: types.JsonPermissions) -> types perms.remove(p) return perms + def audit_nbr_permissions(self, audit_settings: types.ConfigSettings) -> list[Problem]: + """Audits that at least one permission is granted to a user or a group + and that at least one group or user has admin permission on the object""" + if self.count() == 0: + return [Problem(get_rule(RuleId.OBJECT_WITH_NO_PERMISSIONS), self.concerned_object, str(self.concerned_object))] + elif self.count(perm_filter=["admin"]) == 0: + return [Problem(get_rule(RuleId.OBJECT_WITH_NO_ADMIN_PERMISSION), self.concerned_object, str(self.concerned_object))] + return [] + + def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: + return self.audit_nbr_permissions(audit_settings) + def count(self, perm_type: Optional[str] = None, perm_filter: Optional[list[str]] = None) -> int: """Counts number of permissions of an object @@ -221,7 +220,6 @@ def count(self, perm_type: Optional[str] = None, perm_filter: Optional[list[str] if perm_filter is None: continue perm_counter += len([1 for p in elem_perms if p in perm_filter]) - log.debug("Perm counts = %d", (elem_counter if perm_filter is None else perm_counter)) return elem_counter if perm_filter is None else perm_counter def _get_api(self, api: str, perm_type: str, ret_field: str, **extra_params) -> types.JsonPermissions: diff --git a/sonar/permissions/project_permissions.py b/sonar/permissions/project_permissions.py index d2ce8c223..f7434fad8 100644 --- a/sonar/permissions/project_permissions.py +++ b/sonar/permissions/project_permissions.py @@ -47,13 +47,6 @@ class ProjectPermissions(permissions.Permissions): API_GET_FIELD = {"users": "login", "groups": "name"} API_SET_FIELD = {"users": "login", "groups": "groupName"} - def __init__(self, concerned_object: object) -> None: - self.concerned_object = concerned_object - super().__init__(concerned_object.endpoint) - - def __str__(self) -> str: - return f"permissions of {str(self.concerned_object)}" - def read(self) -> ProjectPermissions: """Reads permissions in SonarQube""" self.permissions = permissions.NO_PERMISSIONS.copy() @@ -71,7 +64,7 @@ def read(self) -> ProjectPermissions: return self def _set_perms( - self, new_perms: types.JsonPermissions, apis: dict[str, str], field: dict[str, str], diff_func: Callable, **kwargs + self, new_perms: types.JsonPermissions, apis: dict[str, dict[str, str]], field: dict[str, str], diff_func: Callable, **kwargs ) -> ProjectPermissions: log.debug("Setting %s with %s", str(self), str(new_perms)) if self.permissions is None: @@ -102,7 +95,7 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: log.debug("Auditing project permissions is disabled by configuration, skipping") return [] log.debug("Auditing %s", str(self)) - return self.__audit_user_permissions(audit_settings) + self.__audit_group_permissions(audit_settings) + return super().audit(audit_settings) + self.__audit_user_permissions(audit_settings) + self.__audit_group_permissions(audit_settings) def __audit_user_permissions(self, audit_settings: types.ConfigSettings) -> list[Problem]: """Audits project user permissions""" @@ -113,7 +106,7 @@ def __audit_user_permissions(self, audit_settings: types.ConfigSettings) -> list problems.append(Problem(get_rule(RuleId.PROJ_PERM_MAX_USERS), self, str(self.concerned_object), user_count)) max_admins = audit_settings.get("audit.projects.permissions.maxAdminUsers", 2) - admin_count = self.count("users", ("admin")) + admin_count = self.count("users", ("admin",)) if admin_count > max_admins: rule = get_rule(RuleId.PROJ_PERM_MAX_ADM_USERS) problems.append(Problem(rule, self, str(self.concerned_object), admin_count, max_admins)) diff --git a/sonar/permissions/quality_permissions.py b/sonar/permissions/quality_permissions.py index f1ccda54a..7d1b22b37 100644 --- a/sonar/permissions/quality_permissions.py +++ b/sonar/permissions/quality_permissions.py @@ -20,9 +20,13 @@ """Parent permissions class for quality gates and quality profiles permissions subclasses""" +from __future__ import annotations +from typing import Optional + import json from http import HTTPStatus +from sonar.util import types import sonar.logging as log from sonar import utilities, errcodes from sonar.permissions import permissions @@ -31,14 +35,12 @@ class QualityPermissions(permissions.Permissions): - def __init__(self, concerned_object): - self.concerned_object = concerned_object - super().__init__(concerned_object.endpoint) - - def __str__(self): - return f"permissions of {str(self.concerned_object)}" + """ + Abstractions of QP and QG permissions + """ - def _post_api(self, api, set_field, perms_dict, **extra_params): + def _post_api(self, api: str, set_field: str, perms_dict: types.JsonPermissions, **extra_params) -> bool: + """Runs a post on QG or QP permissions""" if perms_dict is None: return True result = False @@ -49,7 +51,8 @@ def _post_api(self, api, set_field, perms_dict, **extra_params): result = result and r.ok return result - def to_json(self, perm_type=None, csv=False): + def to_json(self, perm_type: Optional[tuple[str, ...]] = None, csv: bool = False) -> types.ObjectJsonRepr: + """Returns the JSON representation of permissions""" if not csv: return self.permissions[perm_type] if permissions.is_valid(perm_type) else self.permissions perms = {} @@ -61,7 +64,7 @@ def to_json(self, perm_type=None, csv=False): perms[p] = permissions.encode(self.permissions.get(p, None)) return perms if len(perms) > 0 else None - def _get_api(self, api, perm_type, ret_field, **extra_params): + def _get_api(self, api: str, perm_type: tuple[str, ...], ret_field: str, **extra_params) -> list[str]: perms = [] params = extra_params.copy() params["ps"] = MAX_PERMS @@ -80,11 +83,12 @@ def _get_api(self, api, perm_type, ret_field, **extra_params): page, nbr_pages = page + 1, utilities.nbr_pages(data) return perms - def _set_perms(self, new_perms, apis, field, diff_func, **kwargs): + def _set_perms(self, new_perms: types.ObjectJsonRepr, apis: dict[str, dict[str, str]], field: str, diff_func: callable, **kwargs) -> bool: + """Sets permissions of a QG or QP""" if self.concerned_object.is_built_in: log.debug("Can't set %s because it's built-in", str(self)) self.permissions = {p: [] for p in permissions.PERMISSION_TYPES} - return self + return False log.debug("Setting %s with %s", str(self), str(new_perms)) if self.permissions is None: self.read() @@ -99,7 +103,8 @@ def _set_perms(self, new_perms, apis, field, diff_func, **kwargs): self.read() return True - def _read_perms(self, apis, field, **kwargs): + def _read_perms(self, apis: dict[str, dict[str, str]], field: str, **kwargs) -> types.ObjectJsonRepr: + """Reads permissions of a QP or QG""" self.permissions = {p: [] for p in permissions.PERMISSION_TYPES} if self.endpoint.is_sonarcloud(): log.debug("No permissions for %s because it's SonarCloud", str(self)) diff --git a/sonar/permissions/qualitygate_permissions.py b/sonar/permissions/qualitygate_permissions.py index 2ff13f5ee..81f54eed0 100644 --- a/sonar/permissions/qualitygate_permissions.py +++ b/sonar/permissions/qualitygate_permissions.py @@ -20,12 +20,18 @@ """Quality gates permissions class""" +from __future__ import annotations + import sonar.logging as log from sonar import exceptions from sonar.permissions import permissions, quality_permissions class QualityGatePermissions(quality_permissions.QualityPermissions): + """ + Abstraction of quality gates permissions + """ + APIS = { "get": {"users": "qualitygates/search_users", "groups": "qualitygates/search_groups"}, "add": {"users": "qualitygates/add_user", "groups": "qualitygates/add_group"}, @@ -34,17 +40,15 @@ class QualityGatePermissions(quality_permissions.QualityPermissions): API_GET_FIELD = {"users": "login", "groups": "name"} API_SET_FIELD = {"users": "login", "groups": "groupName"} - def __str__(self): - return f"permissions of {str(self.concerned_object)}" - - def read(self): + def read(self) -> QualityGatePermissions: if not self.endpoint.is_sonarcloud() and self.endpoint.version() < (9, 2, 0): log.debug("Can't read %s on SonarQube < 9.2", str(self)) return self self._read_perms(QualityGatePermissions.APIS, QualityGatePermissions.API_GET_FIELD, gateName=self.concerned_object.name) return self - def set(self, new_perms): + def set(self, new_perms: dict[str, any]) -> QualityGatePermissions: + """Sets permissions of a quality gate""" if not self.endpoint.is_sonarcloud() and self.endpoint.version() < (9, 2, 0): raise exceptions.UnsupportedOperation(f"Can't set {str(self)} on SonarQube < 9.2") diff --git a/sonar/permissions/qualityprofile_permissions.py b/sonar/permissions/qualityprofile_permissions.py index 9417d675d..585ced7b6 100644 --- a/sonar/permissions/qualityprofile_permissions.py +++ b/sonar/permissions/qualityprofile_permissions.py @@ -20,11 +20,17 @@ """Quality profiles permissions class""" +from __future__ import annotations + import sonar.logging as log +from sonar.util import types from sonar.permissions import permissions, quality_permissions class QualityProfilePermissions(quality_permissions.QualityPermissions): + """ + Abtraction of quality profiles permissions + """ APIS = { "get": {"users": "qualityprofiles/search_users", "groups": "qualityprofiles/search_groups"}, @@ -34,7 +40,7 @@ class QualityProfilePermissions(quality_permissions.QualityPermissions): API_GET_FIELD = {"users": "login", "groups": "name"} API_SET_FIELD = {"users": "login", "groups": "group"} - def read(self): + def read(self) -> QualityProfilePermissions: if self.endpoint.version() < (9, 2, 0): log.debug("Can't read %s on SonarQube < 9.2", str(self)) return self @@ -46,7 +52,7 @@ def read(self): ) return self - def set(self, new_perms): + def set(self, new_perms: types.JsonPermissions) -> bool: if self.endpoint.version() < (6, 6, 0): log.debug("Can set %s on SonarQube < 6.6", str(self)) return self diff --git a/sonar/permissions/template_permissions.py b/sonar/permissions/template_permissions.py index 662c520f7..99ad479f3 100644 --- a/sonar/permissions/template_permissions.py +++ b/sonar/permissions/template_permissions.py @@ -20,18 +20,25 @@ """Permissions templates permissions class""" +from __future__ import annotations import sonar.logging as log +from sonar.util import types from sonar.permissions import permissions, project_permissions class TemplatePermissions(project_permissions.ProjectPermissions): + """ + Abstraction of the permission templates permissions + """ + API_GET = {"users": "permissions/template_users", "groups": "permissions/template_groups"} API_SET = {"users": "permissions/add_user_to_template", "groups": "permissions/add_group_to_template"} API_REMOVE = {"users": "permissions/remove_user_from_template", "groups": "permissions/remove_group_from_template"} API_GET_FIELD = {"users": "login", "groups": "name"} API_SET_FIELD = {"users": "login", "groups": "groupName"} - def read(self): + def read(self) -> TemplatePermissions: + """Reads permissions of a permission template""" self.permissions = permissions.NO_PERMISSIONS for p in permissions.PERMISSION_TYPES: self.permissions[p] = self._get_api( @@ -46,8 +53,9 @@ def read(self): self.white_list(tuple(project_permissions.PROJECT_PERMISSIONS.keys())) return self - def set(self, new_perms): - log.debug("Setting %s with %s", str(self), str(new_perms)) + def set(self, new_perms: types.JsonPermissions) -> TemplatePermissions: + """Sets permissions of a permission template""" + log.info("Setting %s with %s", str(self), str(new_perms)) if self.permissions is None: self.read() for p in permissions.PERMISSION_TYPES: diff --git a/sonar/platform.py b/sonar/platform.py index 6a3b504ce..34fba5f9b 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -512,7 +512,6 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: + self._audit_admin_password() + self._audit_lta_latest() + sif.Sif(pf_sif, self).audit(audit_settings) - + permission_templates.audit(self, audit_settings) ) return problems @@ -723,7 +722,7 @@ def _audit_setting_set( def _audit_maintainability_rating_range(value: float, range: tuple[float, float], rating_letter: str, url: str) -> list[Problem]: """Audits a maintainability rating grid level range""" - log.info( + log.debug( "Checking that maintainability rating threshold %.1f%% for '%s' is within recommended range [%.1f%%-%.1f%%]", value * 100, rating_letter, diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 2a9962ba1..faaed9688 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -331,7 +331,12 @@ def _audit_singleton(self, audit_settings: types.ConfigSettings) -> list[problem def audit(self, audit_settings: types.ConfigSettings) -> list[problem.Problem]: """Audits a portfolio""" log.info("Auditing %s", str(self)) - return self._audit_empty(audit_settings) + self._audit_singleton(audit_settings) + self._audit_bg_task(audit_settings) + return ( + super().audit(audit_settings) + + self._audit_empty(audit_settings) + + self._audit_singleton(audit_settings) + + self._audit_bg_task(audit_settings) + ) def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: """Returns the portfolio representation as JSON""" diff --git a/sonar/projects.py b/sonar/projects.py index d5994d622..a35eadfd0 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -560,7 +560,7 @@ def scanner(self) -> str: def __audit_scanner(self, audit_settings: types.ConfigSettings) -> list[Problem]: proj_type, scanner = self.get_type(), self.scanner() - log.debug("%s is of type %s and uses scanner %s", proj_type, scanner) + log.debug("%s is of type %s and uses scanner %s", str(self), proj_type, scanner) if proj_type == "UNKNOWN": log.info("%s project type can't be identified, skipping check", str(self)) return [] diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index 7775c3d6e..e6085fc01 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -473,7 +473,7 @@ def audit(self, audit_settings: types.ConfigSettings = None) -> list[Problem]: :return: List of problems found, or empty list :rtype: list[Problem] """ - log.debug("Auditing %s", str(self)) + log.info("Auditing %s", str(self)) if self.is_built_in: log.info("%s is built-in, skipping audit", str(self)) return []