diff --git a/cli/config.py b/cli/config.py index 2524fb3d..3a7c8a2c 100644 --- a/cli/config.py +++ b/cli/config.py @@ -39,7 +39,6 @@ TOOL_NAME = "sonar-config" -DONT_INLINE_LISTS = "dontInlineLists" FULL_EXPORT = "fullExport" EXPORT_EMPTY = "exportEmpty" @@ -104,14 +103,6 @@ def __parse_args(desc: str) -> object: f"By default the export will show the value as '{utilities.DEFAULT}' " "and the setting will not be imported at import time", ) - parser.add_argument( - f"--{DONT_INLINE_LISTS}", - required=False, - default=False, - action="store_true", - help="By default, sonar-config exports multi-valued settings as comma separated strings instead of arrays (if there is not comma in values). " - "Set this flag if you want to force export multi valued settings as arrays", - ) parser.add_argument( f"--{EXPORT_EMPTY}", required=False, @@ -216,7 +207,7 @@ def export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> Non export_settings = kwargs.copy() export_settings.update( { - "INLINE_LISTS": not kwargs.get(DONT_INLINE_LISTS, False), + "INLINE_LISTS": False, "EXPORT_DEFAULTS": True, "FULL_EXPORT": kwargs.get(FULL_EXPORT, False), "MODE": mode, @@ -271,8 +262,7 @@ def __prep_json_for_write(json_data: types.ObjectJsonRepr, export_settings: type return json_data if not export_settings.get("FULL_EXPORT", False): json_data = utilities.clean_data(json_data, remove_empty=not export_settings.get(EXPORT_EMPTY, False), remove_none=True) - if export_settings.get("INLINE_LISTS", True): - json_data = utilities.inline_lists(json_data, exceptions=("conditions",)) + return json_data diff --git a/sonar/applications.py b/sonar/applications.py index 735c4d18..21c896d6 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -340,7 +340,7 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: "visibility": self.visibility(), # 'projects': self.projects(), "branches": [br.export() for br in self.branches().values()], - "permissions": self.permissions().export(export_settings=export_settings), + "permissions": self.permissions().export(), "tags": self.get_tags(), } ) @@ -521,14 +521,14 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg key_regexp = kwargs.get("key_list", ".*") app_list = {k: v for k, v in get_list(endpoint).items() if not key_regexp or re.match(key_regexp, k)} - apps_settings = {} + apps_export = {} for k, app in app_list.items(): app_json = app.export(export_settings) if write_q: write_q.put(app_json) - apps_settings[k] = app_json + apps_export[k] = app_json write_q and write_q.put(util.WRITE_END) - return dict(sorted(app_json.items())).values() + return dict(sorted(apps_export.items())).values() def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, **kwargs) -> list[problem.Problem]: diff --git a/sonar/branches.py b/sonar/branches.py index 9d7d531e..ebbf3060 100644 --- a/sonar/branches.py +++ b/sonar/branches.py @@ -229,19 +229,16 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: :rtype: str """ log.debug("Exporting %s", str(self)) - data = {settings.NEW_CODE_PERIOD: self.new_code()} + data = {"name": self.name, "project": self.concerned_object.key} if self.is_main(): data["isMain"] = True if self.is_kept_when_inactive() and not self.is_main(): data["keepWhenInactive"] = True if self.new_code(): data[settings.NEW_CODE_PERIOD] = self.new_code() - if export_settings.get("FULL_EXPORT", True): - data.update({"name": self.name, "project": self.concerned_object.key}) if export_settings.get("MODE", "") == "MIGRATION": data.update(self.migration_export(export_settings)) - data = util.remove_nones(data) - return None if len(data) == 0 else data + return util.remove_nones(data) def set_keep_when_inactive(self, keep: bool) -> bool: """Sets whether the branch is kept when inactive diff --git a/sonar/permissions/aggregation_permissions.py b/sonar/permissions/aggregation_permissions.py index 9e4dbf51..b3fad97b 100644 --- a/sonar/permissions/aggregation_permissions.py +++ b/sonar/permissions/aggregation_permissions.py @@ -22,6 +22,7 @@ from __future__ import annotations from sonar.util import types +from sonar import logging as log from sonar.permissions import permissions, project_permissions AGGREGATION_PERMISSIONS = { @@ -49,4 +50,4 @@ def set(self, new_perms: types.JsonPermissions) -> AggregationPermissions: :return: Permissions associated to the aggregation :rtype: self """ - return super().set(permissions.white_list(new_perms, AGGREGATION_PERMISSIONS)) + return super().set(permissions.white_list(permissions.list_to_dict(new_perms), AGGREGATION_PERMISSIONS)) diff --git a/sonar/permissions/global_permissions.py b/sonar/permissions/global_permissions.py index 28fc2895..1dec133c 100644 --- a/sonar/permissions/global_permissions.py +++ b/sonar/permissions/global_permissions.py @@ -93,3 +93,18 @@ def edition_filter(perms: types.JsonPermissions, ed: str) -> types.JsonPermissio log.warning("Can't manage permission '%s' on a %s edition", p, ed) perms.remove(p) return perms + + +def fmt_perms(group_or_user: str, perms: list[str], type_of_perm: str) -> types.JsonPermissions: + """Helper to convert perms to dict""" + return {type_of_perm: group_or_user, "permissions": perms} + + +def group_perms(group: str, perms: list[str]) -> types.JsonPermissions: + """Helper to convert group perms to dict""" + return fmt_perms(group, perms, "groups") + + +def user_perms(user: str, perms: list[str]) -> types.JsonPermissions: + """Helper to convert group perms to dict""" + return fmt_perms(user, perms, "users") diff --git a/sonar/permissions/permission_templates.py b/sonar/permissions/permission_templates.py index 738d8ff1..318661d1 100644 --- a/sonar/permissions/permission_templates.py +++ b/sonar/permissions/permission_templates.py @@ -164,7 +164,7 @@ def to_json(self, export_settings: types.ConfigSettings = None) -> types.ObjectJ "name": self.name, "description": self.description if self.description != "" else None, "pattern": self.project_key_pattern, - "permissions": self.permissions().export(export_settings=export_settings), + "permissions": self.permissions().export(), } defaults = [] @@ -263,7 +263,6 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings) -> type """Exports permission templates as JSON""" log.info("Exporting permission templates") json_data = {pt.name: pt.to_json(export_settings) for pt in get_list(endpoint).values()} - log.info("PT RES = %s", utilities.json_dump(json_data)) return list(dict(sorted(json_data.items())).values()) diff --git a/sonar/permissions/permissions.py b/sonar/permissions/permissions.py index 7ae609f8..8ac3e544 100644 --- a/sonar/permissions/permissions.py +++ b/sonar/permissions/permissions.py @@ -37,18 +37,19 @@ "admin": "Administer System", "gateadmin": "Administer Quality Gates", "profileadmin": "Administer Quality Profiles", - "provisioning": "Create Projects", "scan": "Execute Analysis", + "provisioning": "Create Projects", } -DEVELOPER_GLOBAL_PERMISSIONS = {**COMMUNITY_GLOBAL_PERMISSIONS, **{"applicationcreator": "Create Applications"}} -ENTERPRISE_GLOBAL_PERMISSIONS = {**DEVELOPER_GLOBAL_PERMISSIONS, **{"portfoliocreator": "Create Portfolios"}} + +DEVELOPER_GLOBAL_PERMISSIONS = {**COMMUNITY_GLOBAL_PERMISSIONS, "applicationcreator": "Create Applications"} +ENTERPRISE_GLOBAL_PERMISSIONS = {**DEVELOPER_GLOBAL_PERMISSIONS, "portfoliocreator": "Create Portfolios"} PROJECT_PERMISSIONS = { - "admin": "Administer Project", "user": "Browse", "codeviewer": "See source code", "issueadmin": "Administer Issues", "securityhotspotadmin": "Create Projects", + "admin": "Administer Project", "scan": "Execute Analysis", } @@ -62,7 +63,7 @@ OBJECTS_WITH_PERMISSIONS = (_GLOBAL, _PROJECTS, _TEMPLATES, _QG, _QP, _APPS, _PORTFOLIOS) PERMISSION_TYPES = ("groups", "users") -NO_PERMISSIONS = {"groups": None, "users": None} +NO_PERMISSIONS = {p: {} for p in PERMISSION_TYPES} MAX_PERMS = 100 @@ -81,10 +82,9 @@ def __init__(self, concerned_object: object) -> None: def __str__(self) -> str: return f"permissions of {str(self.concerned_object)}" - def to_json(self, perm_type: Optional[str] = None, csv: bool = False) -> types.JsonPermissions: + def to_json(self, perm_type: Optional[str] = None) -> types.JsonPermissions: """Converts a permission object to JSON""" - if not csv: - return self.permissions.get(perm_type, {}) if is_valid(perm_type) else self.permissions + order = PROJECT_PERMISSIONS if self.concerned_object else ENTERPRISE_GLOBAL_PERMISSIONS perms = [] for p in normalize(perm_type): if p not in self.permissions or len(self.permissions[p]) == 0: @@ -92,18 +92,13 @@ def to_json(self, perm_type: Optional[str] = None, csv: bool = False) -> types.J for k, v in self.permissions.get(p, {}).items(): if not v or len(v) == 0: continue - perms += [{p[:-1]: k, "permissions": encode(v)}] + perms += [{p[:-1]: k, "permissions": encode(v, order)}] return perms if len(perms) > 0 else None - def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: + def export(self) -> types.ObjectJsonRepr: """Exports permissions as JSON""" - inlined = export_settings.get("INLINE_LISTS", True) - perms = self.to_json(csv=inlined) - if not inlined: - perms = {k: v for k, v in perms.items() if len(v) > 0} - if not perms or len(perms) == 0: - return None - return perms + perms = self.to_json() + return None if not perms or len(perms) == 0 else perms @abstractmethod def read(self) -> Permissions: @@ -119,20 +114,6 @@ def set(self, new_perms: types.JsonPermissions) -> Permissions: :param JsonPermissions new_perms: The permissions to set """ - def set_user_permissions(self, user_perms: dict[str, list[str]]) -> Permissions: - """Sets user permissions of an object - - :param dict[str, list[str]] user_perms: The user permissions to apply - """ - return self.set({"users": user_perms}) - - def set_group_permissions(self, group_perms: dict[str, list[str]]) -> Permissions: - """Sets user permissions of an object - - :param dict[str, list[str]] group_perms: The group permissions to apply - """ - return self.set({"groups": group_perms}) - def clear(self) -> Permissions: """Clears all permissions of an object :return: self @@ -140,23 +121,21 @@ def clear(self) -> Permissions: """ return self.set({"users": {}, "groups": {}}) - def users(self) -> dict[str, list[str]]: + def users(self) -> types.JsonPermissions: """ :return: User permissions of an object - :rtype: list (for QualityGate and QualityProfile) or dict (for other objects) """ if self.permissions is None: self.read() - return self.to_json(perm_type="users") + return self.permissions.get("users", {}) - def groups(self) -> dict[str, list[str]]: + def groups(self) -> types.JsonPermissions: """ :return: Group permissions of an object - :rtype: list (for QualityGate and QualityProfile) or dict (for other objects) """ if self.permissions is None: self.read() - return self.to_json(perm_type="groups") + return self.permissions.get("groups", {}) def added_permissions(self, other_perms: types.JsonPermissions) -> types.JsonPermissions: return diff(self.permissions, other_perms) @@ -212,7 +191,7 @@ def __audit_max_users_or_groups_with_permissions(self, audit_settings: types.Con """Audits maximum number of user or groups with permissions""" problems = [] o = self.concerned_object - data = self.to_json() + data = self.permissions for t in PERMISSION_TYPES: max_count = audit_settings.get(f"audit.permissions.max{t.capitalize()}", 5) count = len(data.get(t, {})) @@ -224,7 +203,7 @@ def __audit_max_users_or_groups_with_permissions(self, audit_settings: types.Con def audit_sonar_users_permissions(self, audit_settings: types.ConfigSettings) -> list[Problem]: """Audits that default user group has no sensitive permissions""" __SENSITIVE_PERMISSIONS = ["issueadmin", "scan", "securityhotspotadmin", "admin", "gateadmin", "profileadmin"] - groups = self.to_json(perm_type="groups") + groups = self.permissions.get("groups", {}) if isinstance(groups, list): groups = {u: ["admin"] for u in groups} default_gr = self.endpoint.default_user_group() @@ -234,7 +213,7 @@ def audit_sonar_users_permissions(self, audit_settings: types.ConfigSettings) -> def audit_anyone_permissions(self, audit_settings: types.ConfigSettings) -> list[Problem]: """Audits that Anyone group has no permissions""" - groups = self.to_json(perm_type="groups") + groups = self.permissions.get("groups", {}) if groups and any(gr_name == "Anyone" for gr_name in groups): return [Problem(get_rule(RuleId.PROJ_PERM_ANYONE), self.concerned_object, str(self.concerned_object))] return [] @@ -322,18 +301,12 @@ def _post_api(self, api: str, set_field: str, perms_dict: types.JsonPermissions, return ok -def simplify(perms_dict: dict[str, list[str]]) -> Optional[dict[str, str]]: - """Simplifies permissions by converting to CSV an array""" - if perms_dict is None or len(perms_dict) == 0: - return None - return {k: encode(v) for k, v in perms_dict.items() if len(v) > 0} - - -def encode(perms_array: dict[str, list[str]]) -> dict[str, str]: +def encode(perms_array: dict[str, list[str]], order: list[str]) -> dict[str, str]: """ :meta private: """ - return utilities.list_to_csv(perms_array, ", ", check_for_separator=True) + ordered = utilities.order_list(perms_array, *order) + return utilities.list_to_csv(ordered, ", ", check_for_separator=True) def decode(encoded_perms: dict[str, str]) -> dict[str, list[str]]: @@ -426,15 +399,43 @@ def white_list(perms: types.JsonPermissions, allowed_perms: list[str]) -> types. def black_list(perms: types.JsonPermissions, disallowed_perms: list[str]) -> types.JsonPermissions: """Returns permissions filtered after a black list of disallowed permissions""" resulting_perms = {} - for perm_type, sub_perms in perms.items(): - # if perm_type not in PERMISSION_TYPES: - # continue + for perm_type, sub_perms in list_to_dict(perms).items(): resulting_perms[perm_type] = {} for user_or_group, original_perms in sub_perms.items(): resulting_perms[perm_type][user_or_group] = [p for p in original_perms if p not in disallowed_perms] - return resulting_perms + return dict_to_list(resulting_perms) def convert_for_yaml(json_perms: types.ObjectJsonRepr) -> types.ObjectJsonRepr: """Converts permissions in a format that is more friendly for YAML""" return json_perms + + +def fmt_perms(group_or_user: str, perms: list[str], type_of_perm: str) -> types.JsonPermissions: + """Helper to convert perms to dict""" + return {type_of_perm[:-1]: group_or_user, "permissions": perms} + + +def group_perms(group: str, perms: list[str]) -> types.JsonPermissions: + """Helper to convert group perms to dict""" + return fmt_perms(group, perms, "groups") + + +def user_perms(user: str, perms: list[str]) -> types.JsonPermissions: + """Helper to convert group perms to dict""" + return fmt_perms(user, perms, "users") + + +def list_to_dict(perms: types.JsonPermissions) -> dict[str, dict[str, list[str]]]: + log.info("L2D = %s", utilities.json_dump(perms)) + res = {"users": {p["user"]: p["permissions"] for p in perms if "user" in p}} + res |= {"groups": {p["group"]: p["permissions"] for p in perms if "group" in p}} + return res + + +def dict_to_list(perms: dict[str, dict[str, list[str]]]) -> types.JsonPermissions: + res = [] + for ptype in PERMISSION_TYPES: + for p in perms.get(ptype, {}): + res += [{ptype[:-1]: k, "permissions": v} for k, v in p] + return res diff --git a/sonar/permissions/quality_permissions.py b/sonar/permissions/quality_permissions.py index e3a2bb0e..2989bd8e 100644 --- a/sonar/permissions/quality_permissions.py +++ b/sonar/permissions/quality_permissions.py @@ -62,6 +62,7 @@ def to_json(self, perm_type: Optional[tuple[str, ...]] = None, csv: bool = False dperms = self.permissions.get(p, None) if dperms is not None and len(dperms) > 0: perms[p] = permissions.encode(self.permissions.get(p, None)) + perms = permissions.dict_to_list(perms) return perms if len(perms) > 0 else None def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]: diff --git a/sonar/platform.py b/sonar/platform.py index 2c925a07..d2757384 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -189,7 +189,12 @@ def basics(self) -> dict[str, str]: if self.is_sonarcloud(): return {**data, "organization": self.organization} - return {**data, "version": util.version_to_string(self.version()[:3]), "serverId": self.server_id(), "plugins": self.plugins()} + return { + **data, + "version": util.version_to_string(self.version()[:3]), + "serverId": self.server_id(), + "plugins": util.sort_list_by_key(self.plugins(), "key"), + } def default_user_group(self) -> str: """ @@ -406,7 +411,9 @@ def get_settings(self, settings_list: Optional[list[str]] = None) -> dict[str, a settings_dict = {k: settings.get_object(endpoint=self, key=k) for k in settings_list} platform_settings = {} for v in settings_dict.values(): - platform_settings |= v.to_json() + d = v.to_json() + platform_settings |= {d["key"]: d["value"]} + log.info("PLAT_SETTINGS = %s", util.json_dump(platform_settings)) return platform_settings def __settings(self, settings_list: types.KeyList = None, include_not_set: bool = False) -> dict[str, settings.Setting]: @@ -417,7 +424,7 @@ def __settings(self, settings_list: types.KeyList = None, include_not_set: bool settings_dict[ai_code_fix.key] = ai_code_fix return settings_dict - def get_setting(self, key: str) -> any: + def get_setting(self, key: str) -> Any: """Returns a platform global setting value from its key :param key: Setting key @@ -465,35 +472,41 @@ def webhooks(self) -> dict[str, webhooks.WebHook]: """ return webhooks.get_list(self) - def export(self, export_settings: types.ConfigSettings, full: bool = False) -> types.ObjectJsonRepr: + def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: """Exports the global platform properties as JSON - :param full: Whether to also export properties that cannot be set, defaults to False - :type full: bool, optional + :param bool full: Optional, Whether to also export properties that cannot be set, defaults to False :return: dict of all properties with their values - :rtype: dict """ log.info("Exporting platform global settings") json_data = {} - for s in self.__settings(include_not_set=export_settings.get("EXPORT_DEFAULTS", False)).values(): - if s.is_internal(): - continue - (categ, subcateg) = s.category() + full = export_settings.get("EXPORT_DEFAULTS", False) + to_export = [s for s in self.__settings(include_not_set=full).values() if not s.is_internal() and s.is_global()] + if not full: + to_export = [s for s in to_export if not s.inherited] + langs = {} + for s in to_export: + categ = s.category() if self.is_sonarcloud() and categ == settings.THIRD_PARTY_SETTINGS: # What is reported as 3rd part are SonarQube Cloud internal settings continue - if not s.is_global(): - continue - util.update_json(json_data, categ, subcateg, s.to_json(export_settings.get("INLINE_LISTS", True))) + setting_json = s.to_json() + if not full: + setting_json.pop("isDefault", None) + if lang := s.language(): + langs[lang] = langs.get(lang, []) + langs[lang].append(setting_json) + else: + json_data[categ] = json_data.get(categ, []) + json_data[categ].append(setting_json) + json_data["languages"] = [{"language": k, "settings": util.sort_list_by_key(v, "key")} for k, v in langs.items()] + for k in settings.GENERAL_SETTINGS, settings.ANALYSIS_SCOPE_SETTINGS: + json_data[k] = util.sort_list_by_key(json_data[k], "key") - hooks = {} - for wb in self.webhooks().values(): - j = util.remove_nones(wb.to_json(full)) - j.pop("name", None) - hooks[wb.name] = j + hooks = [wh.to_json(full) for wh in self.webhooks().values()] if len(hooks) > 0: - json_data[settings.GENERAL_SETTINGS].update({"webhooks": hooks}) - json_data["permissions"] = self.global_permissions().export(export_settings=export_settings) + json_data["webhooks"] = util.sort_list_by_key(hooks, "name") + json_data["permissions"] = self.global_permissions().export() json_data["permissionTemplates"] = permission_templates.export(self, export_settings=export_settings) if not self.is_sonarcloud(): json_data[settings.DEVOPS_INTEGRATION] = devops.export(self, export_settings=export_settings) @@ -741,11 +754,13 @@ def _audit_token_max_lifetime(self, audit_settings: types.ConfigSettings) -> lis if lifetime_setting is None: log.info("Token maximum lifetime setting not found, skipping audit") return [] - max_lifetime = util.to_days(self.get_setting(settings.TOKEN_MAX_LIFETIME)) + max_lifetime = util.to_days(lifetime_setting.value) + max_allowed = audit_settings.get("audit.tokens.maxAge", 90) if max_lifetime is None: return [Problem(get_rule(RuleId.TOKEN_LIFETIME_UNLIMITED), self.external_url)] - if max_lifetime > audit_settings.get("audit.tokens.maxAge", 90): - return [Problem(get_rule(RuleId.TOKEN_LIFETIME_TOO_HIGH), self.external_url, max_lifetime, audit_settings.get("audit.tokens.maxAge", 90))] + if max_lifetime > max_allowed: + return [Problem(get_rule(RuleId.TOKEN_LIFETIME_TOO_HIGH), self.external_url, max_lifetime, max_allowed)] + log.info("Maximum token lifetime (%d days) is lower than the audit max (%d days)", max_lifetime, max_allowed) return [] def is_mqr_mode(self) -> bool: diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 361f4042..80e99a51 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -373,7 +373,7 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr subportfolios = self.sub_portfolios() if not self.is_sub_portfolio(): json_data["visibility"] = self._visibility - json_data["permissions"] = self.permissions().export(export_settings=export_settings) + json_data["permissions"] = self.permissions().export() json_data["tags"] = self._tags if subportfolios: json_data["portfolios"] = [s.to_json(export_settings) for s in subportfolios.values()] diff --git a/sonar/projects.py b/sonar/projects.py index 76e767ae..926f50bd 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -35,6 +35,7 @@ from http import HTTPStatus from threading import Lock from requests import HTTPError, RequestException +import traceback import sonar.logging as log import sonar.platform as pf @@ -969,7 +970,7 @@ def __get_branch_export(self, export_settings: types.ConfigSettings) -> Optional # If there is only 1 branch with no specific config except being main, don't return anything if len(branch_data) == 0 or (len(branch_data) == 1 and "main" in branch_data and len(branch_data["main"]) <= 1): return None - return branch_data + return util.sort_list_by_key(list(branch_data.values()), "name", "isMain") def migration_export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: """Produces the data that is exported for SQ to SC migration""" @@ -1009,10 +1010,11 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ json_data[settings.NEW_CODE_PERIOD] = nc json_data["qualityProfiles"] = self.__export_get_qp() json_data["links"] = self.links() - json_data["permissions"] = self.permissions().to_json(csv=export_settings.get("INLINE_LISTS", True)) + json_data["permissions"] = self.permissions().to_json() if self.endpoint.version() >= (10, 7, 0): json_data["aiCodeFix"] = self.ai_code_fix() json_data["branches"] = self.__get_branch_export(export_settings) + _ = [b.pop("project") for b in json_data["branches"]] json_data["tags"] = self.get_tags() json_data["visibility"] = self.visibility() (json_data["qualityGate"], qg_is_default) = self.quality_gate() @@ -1030,7 +1032,18 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ if export_settings.get("MODE", "") == "MIGRATION": json_data.update(self.migration_export(export_settings)) - settings_dict = settings.get_bulk(endpoint=self.endpoint, component=self, settings_list=settings_list, include_not_set=False) + with_inherited = export_settings.get("FULL_EXPORT", False) + log.info("EXP1") + settings_list = settings.get_bulk( + endpoint=self.endpoint, component=self, settings_list=settings_list, include_not_set=with_inherited + ).values() + log.info("EXP2") + settings_list = [s for s in settings_list if not s.is_global()] + settings_list = [s for s in settings_list if s.key not in ("visibility", settings.NEW_CODE_PERIOD)] + log.info("EXP3") + if not with_inherited: + settings_list = [s for s in settings_list if not s.inherited] + # json_data.update({s.to_json() for s in settings_dict.values() if include_inherited or not s.inherited}) contains_ai = False try: @@ -1040,12 +1053,15 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ pass if contains_ai: json_data[_CONTAINS_AI_CODE] = contains_ai - for s in settings_dict.values(): - if not export_settings.get("INCLUDE_INHERITED", False) and s.inherited: - continue - json_data.update(s.to_json()) + log.info("EXP3.2") + json_data["settings"] = util.sort_list_by_key([s.to_json() for s in settings_list], "key") + log.info("EXP4") + if not with_inherited: + _ = [s.pop("isDefault", None) for s in json_data["settings"]] + return json_data except Exception as e: + traceback.print_exc() util.handle_error(e, f"exporting {str(self)}, export of this project interrupted", catch_all=True) json_data["error"] = f"{util.error_msg(e)} while exporting project" log.debug("Exporting %s done, returning %s", str(self), util.json_dump(json_data)) diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index 4af00d3a..39792aa9 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -387,7 +387,7 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr if not full: json_data.pop("isBuiltIn", None) json_data["conditions"] = self.conditions(encoded=True) - json_data["permissions"] = self.permissions().export(export_settings=export_settings) + json_data["permissions"] = self.permissions().export() return util.remove_nones(util.filter_export(json_data, _IMPORTABLE_PROPERTIES, full)) diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index bf92cd17..28fb5373 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -400,7 +400,7 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr if self.rule_has_custom_severities(rule.key): data["impacts"] = self.rule_impacts(rule.key, substitute_with_default=True) json_data["rules"].append({"key": rule.key, **data}) - json_data["permissions"] = self.permissions().export(export_settings) + json_data["permissions"] = self.permissions().export() return util.remove_nones(util.filter_export(json_data, _IMPORTABLE_PROPERTIES, full)) def compare(self, another_qp: QualityProfile) -> dict[str, str]: diff --git a/sonar/settings.py b/sonar/settings.py index 60b341f2..0cbb394b 100644 --- a/sonar/settings.py +++ b/sonar/settings.py @@ -40,6 +40,7 @@ THIRD_PARTY_SETTINGS = "thirdParty" ANALYSIS_SCOPE_SETTINGS = "analysisScope" SAST_CONFIG_SETTINGS = "sastConfig" +SCA_CONFIG_SETTINGS = "sca" TEST_SETTINGS = "tests" CATEGORIES = ( @@ -50,10 +51,60 @@ TEST_SETTINGS, DEVOPS_INTEGRATION, SAST_CONFIG_SETTINGS, + SCA_CONFIG_SETTINGS, LINTER_SETTINGS, THIRD_PARTY_SETTINGS, ) +GLOBAL_SETTINGS = ( + r"sonar\.auth\..+", + r"sonar\.announcement\..+", + r"sonar\.login\..+", + r"provisioning\..+", + r"sonar\.earlyAccess\..+", + r"sonar\.dbCleaner\..+", + r"sonar\.governance\..+", + r"sonar\.sca\..+", + r"sonar\.ai\.codefix\.hidden", + r"sonar\.allowPermissionManagementForProjectAdmin", + r"sonar\.authenticator\.downcase", + r"sonar\.builtInQualityProfiles\.disableNotificationOnUpdate", + r"sonar\.filesize\.limit", + r"sonar\.enforceAzureOpenAiDomainValidation", + r"sonar\.forceAuthentication", + r"sonar\.global\.exclusions", + r"sonar\.governance\.report\.project\.branch\.frequency", + r"sonar\.issues\.sandbox\..*", + r"sonar\.jreAutoProvisioning\.disabled", + r"sonar\.lf\..+", + r"sonar\.multi-quality-mode\.enabled", + r"sonar\.notifications\..+", + r"sonar\.pdf\..+", + r"sonar\.qualityProfiles\.allowDisableInheritedRules", + r"sonar\.scanner\..+", + r"sonar\.technicalDebt\..*", + r"sonar\.validateWebhooks", + r"sonar\.dependencyCheck\..*", +) + +LANGUAGE_SETTING_PATTERN = r"^sonar\.(cpd\.)?(abap|androidLint|ansible|apex|azureresourcemanager|cloudformation|c|cpp|cfamily|\ +cobol|cs|css|dart|docker|eslint|flex|go|html|java|javascript|jcl|json|jsp|kotlin|objc|php|pli|\ +plsql|python|ipynb|rpg|ruby|scala|swift|terraform|text|tsql|typescript|vb|vbnet|xml|yaml|rust|jasmin)\." + +CATEGORY_MAP = { + r"^sonar\.(cpd\.)?(abap|androidLint|ansible|apex|azureresourcemanager|cloudformation|c|cpp|cfamily|cobol|cs|css|dart|docker|" + r"eslint|flex|go|html|java|javascript|jcl|json|jsp|kotlin|objc|php|pli|plsql|python|ipynb|rpg|ruby|scala|swift|" + r"terraform|text|tsql|typescript|vb|vbnet|xml|yaml|rust|jasmin)\.": LANGUAGES_SETTINGS, + r"^.*([lL]int|govet|flake8|checkstyle|pmd|spotbugs|findbugs|phpstan|psalm|detekt|bandit|rubocop|scalastyle|scapegoat).*$": LINTER_SETTINGS, + r"^sonar\.security\.config\..+$": SAST_CONFIG_SETTINGS, + r"^sonar\.sca\..+$": SCA_CONFIG_SETTINGS, + r"^.*\.(exclusions$|inclusions$|issue\..+)$": ANALYSIS_SCOPE_SETTINGS, + r"^.*(\.reports?Paths?$|unit\..*$|cov.*$)": TEST_SETTINGS, + r"^sonar\.forceAuthentication$": AUTH_SETTINGS, + r"^sonar\.dependencyCheck\..*$": THIRD_PARTY_SETTINGS, + r"^(sonar\.|email\.|provisioning\.git).*$": GENERAL_SETTINGS, +} + NEW_CODE_PERIOD = "newCodePeriod" COMPONENT_VISIBILITY = "visibility" PROJECT_DEFAULT_VISIBILITY = "projects.default.visibility" @@ -64,37 +115,43 @@ _GLOBAL_SETTINGS_WITHOUT_DEF = (AI_CODE_FIX, MQR_ENABLED) _SQ_INTERNAL_SETTINGS = ( - "sonaranalyzer", - "sonar.updatecenter", - "sonar.plugins.risk.consent", - "sonar.core.id", - "sonar.core.startTime", - "sonar.plsql.jdbc.driver.class", + r"sonar\.projectCreation\.mainBranchName", + r"sonaranalyzer.*", + r"sonar\.updatecenter\.*", + r"sonar\.plugins\.risk\.consent", + r"sonar\.core\..*", + r"sonar\.plsql\.jdbc\.driver\.class", + r"sonar\.ce\.parallelProjectTasks", + r"sonar\.cs\.analyzer\..+", + r"sonar\.cpd\.cobol\.minimum.+", + r"sonar\.documentation\.+", ) _SC_INTERNAL_SETTINGS = ( - "sonaranalyzer", - "sonar.updatecenter", - "sonar.plugins.risk.consent", - "sonar.core.id", - "sonar.core.startTime", - "sonar.plsql.jdbc.driver.class", - "sonar.dbcleaner", - "sonar.core.serverBaseURL", - "email.", - "sonar.builtIn", - "sonar.issues.defaultAssigneeLogin", - "sonar.filesize.limit", - "sonar.kubernetes.activate", - "sonar.lf", - "sonar.notifications", - "sonar.plugins.loadAll", - "sonar.qualityProfiles.allowDisableInheritedRules", - "sonar.scm.disabled", - "sonar.technicalDebt", - "sonar.issue.", - "sonar.global", - "sonar.forceAuthentication", + r"sonaranalyzer.*", + r"sonar\.updatecenter\.*", + r"sonar\.plugins\.risk\.consent", + r"sonar\.core\..*", + r"sonar\.plsql\.jdbc\.driver\.class", + r"sonar\.ce\.parallelProjectTasks", + r"sonar\.cs\.analyzer\..+", + r"sonar\.cpd\.cobol\.minimum.+", + r"sonar\.documentation\.+", + r"sonar\.dbcleaner\..+", + r"email\..+", + r"sonar\.builtIn.+", + r"sonar\.issues\.defaultAssigneeLogin", + r"sonar\.filesize\.limit", + r"sonar\.kubernetes\.activate", + r"sonar\.lf\..+", + r"sonar\.notifications", + r"sonar\.plugins\.loadAll", + r"sonar\.qualityProfiles\.allowDisableInheritedRules", + r"sonar\.scm\.disabled", + r"sonar\.technicalDebt\..+", + r"sonar\.issue\..+", + r"sonar\.global\..+", + r"sonar\.forceAuthentication", ) _INLINE_SETTINGS = ( @@ -177,28 +234,24 @@ def load(cls, key: str, endpoint: pf.Platform, data: types.ApiPayload, component o.reload(data) return o - def __reload_inheritance(self, data: types.ApiPayload) -> bool: + def __reload_inheritance(self, data: Optional[types.ApiPayload]) -> bool: """Verifies if a setting is inherited from the data returned by SQ""" - if "inherited" in data: - self.inherited = data["inherited"] - elif self.key == NEW_CODE_PERIOD: - self.inherited = False - elif "parentValues" in data or "parentValue" in data or "parentFieldValues" in data: - self.inherited = False - elif "category" in data: - self.inherited = True - elif self.component is not None: - self.inherited = False - else: - self.inherited = True - if self.component is None: + data = data or self.sq_json + self.inherited = None + for key, parent_key in (("values", "parentValues"), ("value", "parentValue")): + if key in data and parent_key in data and data[key] == data[parent_key]: + self.inherited = True + if self.value is None: self.inherited = True + if self.inherited is None: + self.inherited = data.get("inherited", False) return self.inherited def reload(self, data: types.ApiPayload) -> None: """Reloads a Setting with JSON returned from Sonar API""" if not data: return + super().reload(data) self.multi_valued = data.get("multiValues", False) if self.key == NEW_CODE_PERIOD: self.value = new_code_to_string(data) @@ -206,14 +259,15 @@ def reload(self, data: types.ApiPayload) -> None: self.value = data.get("mode", "MQR") != "STANDARD_EXPERIENCE" elif self.key == COMPONENT_VISIBILITY: self.value = data.get("visibility", None) - elif self.key == "sonar.login.message": - self.value: Optional[Any] = None + elif self.key in ("sonar.login.message", "sonar.announcement.message"): + self.value = None if "values" in data and isinstance(data["values"], list) and len(data["values"]) > 0: self.value = data["values"][0] else: self.value = next((data[key] for key in ("fieldValues", "values", "value") if key in data), None) if not self.value and "defaultValue" in data: self.value = util.DEFAULT + self.value = util.convert_string(self.value) self.__reload_inheritance(data) def refresh(self) -> None: @@ -277,19 +331,11 @@ def reset(self) -> bool: else: return ok - def to_json(self, list_as_csv: bool = True) -> types.ObjectJsonRepr: + def to_json(self) -> types.ObjectJsonRepr: val = self.value if self.key == NEW_CODE_PERIOD: val = new_code_to_string(self.value) - elif list_as_csv and isinstance(self.value, list): - for reg in _INLINE_SETTINGS: - if re.match(reg, self.key): - val = util.list_to_csv(val, separator=", ", check_for_separator=True) - break - if val is None: - val = "" - # log.debug("JSON of %s = %s", self, {self.key: val}) - return {self.key: val} + return {"key": self.key, "value": val, "isDefault": self.inherited} def definition(self) -> Optional[dict[str, str]]: """Returns the setting global definition""" @@ -300,7 +346,7 @@ def definition(self) -> Optional[dict[str, str]]: def is_global(self) -> bool: """Returns whether a setting global or specific for one component (project, branch, application, portfolio)""" if self.component: - return False + return any(re.match(regexp, self.key) for regexp in GLOBAL_SETTINGS) if self._is_global is None: self._is_global = self.definition() is not None or self.key in _GLOBAL_SETTINGS_WITHOUT_DEF return self._is_global @@ -310,12 +356,10 @@ def is_internal(self) -> bool: internal_settings = _SQ_INTERNAL_SETTINGS if self.endpoint.is_sonarcloud(): internal_settings = _SC_INTERNAL_SETTINGS - if self.is_global(): - (categ, _) = self.category() - if categ in ("languages", "analysisScope", "tests", "authentication"): - return True + if self.is_global() and self.category() in (LANGUAGES_SETTINGS, ANALYSIS_SCOPE_SETTINGS, TEST_SETTINGS, AUTH_SETTINGS): + return True - return any(self.key.startswith(prefix) for prefix in internal_settings) + return any(re.match(regexp, self.key) for regexp in internal_settings) def is_settable(self) -> bool: """Returns whether a setting can be set""" @@ -325,49 +369,25 @@ def is_settable(self) -> bool: return False return not self.is_internal() - def category(self) -> tuple[str, str]: - """Returns the 2 levels classification of a setting""" - m = re.match( - r"^sonar\.(cpd\.)?(abap|androidLint|ansible|apex|azureresourcemanager|cloudformation|c|cpp|cfamily|cobol|cs|css|dart|docker|" - r"eslint|flex|go|html|java|javascript|jcl|json|jsp|kotlin|objc|php|pli|plsql|python|ipynb|rpg|ruby|scala|swift|" - r"terraform|text|tsql|typescript|vb|vbnet|xml|yaml|rust|jasmin)\.", - self.key, - ) - if m: + def category(self) -> str: + """Returns the setting category""" + for k, v in CATEGORY_MAP.items(): + if re.match(k, self.key): + return v + if self.key in (NEW_CODE_PERIOD, PROJECT_DEFAULT_VISIBILITY, MQR_ENABLED, COMPONENT_VISIBILITY): + return GENERAL_SETTINGS + return THIRD_PARTY_SETTINGS + + def language(self) -> Optional[str]: + """Returns the setting language or None""" + if m := re.match(LANGUAGE_SETTING_PATTERN, self.key): lang = m.group(2) - if lang in ("c", "cpp", "objc", "cfamily"): - lang = "cfamily" - elif lang in ("androidLint"): - lang = "kotlin" - elif lang in ("eslint", "jasmin"): - lang = "javascript" - return (LANGUAGES_SETTINGS, lang) - if re.match( - r"^.*([lL]int|govet|flake8|checkstyle|pmd|spotbugs|findbugs|phpstan|psalm|detekt|bandit|rubocop|scalastyle|scapegoat).*$", - self.key, - ): - return (LINTER_SETTINGS, None) - if re.match(r"^sonar\.security\.config\..+$", self.key): - return (SAST_CONFIG_SETTINGS, None) - if re.match(r"^.*\.(exclusions$|inclusions$|issue\..+)$", self.key): - return (ANALYSIS_SCOPE_SETTINGS, None) - - if re.match(r"^.*(\.reports?Paths?$|unit\..*$|cov.*$)", self.key): - return (TEST_SETTINGS, None) - m = re.match(r"^sonar\.(auth\.|authenticator\.downcase).*$", self.key) - if m: - return (AUTH_SETTINGS, None) - m = re.match(r"^sonar\.forceAuthentication$", self.key) - if m: - return (AUTH_SETTINGS, None) - if re.match(r"^sonar\.dependencyCheck\..*$", self.key): - return ("thirdParty", None) - if self.key in (NEW_CODE_PERIOD, PROJECT_DEFAULT_VISIBILITY, MQR_ENABLED, COMPONENT_VISIBILITY) or re.match( - r"^(sonar\.|email\.|provisioning\.git).*$", - self.key, - ): - return (GENERAL_SETTINGS, None) - return ("thirdParty", None) + lang_map = {("c", "cpp", "objc", "cfamily"): "cfamily", ("androidLint",): "kotlin", ("eslint", "jasmin"): "javascript"} + for k, v in lang_map.items(): + if lang in k: + lang = v + return lang + return None def get_object(endpoint: pf.Platform, key: str, component: Optional[object] = None) -> Setting: @@ -390,6 +410,8 @@ def __get_settings(endpoint: pf.Platform, data: types.ApiPayload, component: Opt log.debug("Looking at %s", setting_type) for s in data.get(setting_type, {}): (key, sdata) = (s, {}) if isinstance(s, str) else (s["key"], s) + if component and any(re.match(regexp, key) for regexp in GLOBAL_SETTINGS): + continue o = Setting(endpoint=endpoint, key=key, component=component, data=None) if o.is_internal(): log.debug("Skipping internal setting %s", s["key"]) diff --git a/sonar/util/types.py b/sonar/util/types.py index 629bf995..1bc8fcbd 100644 --- a/sonar/util/types.py +++ b/sonar/util/types.py @@ -40,4 +40,6 @@ CliParams = Optional[dict[str, Union[str, int, float, list[str]]]] -JsonPermissions = dict[str, dict[str, list[str]]] +PermissionItem = dict[str, Union[str, list[str]]] + +JsonPermissions = list[PermissionItem] diff --git a/sonar/utilities.py b/sonar/utilities.py index 3d75cbd9..ab809b3d 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -237,6 +237,16 @@ def sort_lists(data: Any, redact_tokens: bool = True) -> Any: return data +def sort_list_by_key(list_to_sort: list[dict[str, Any]], key: str, priority_field: Optional[str] = None) -> list[dict[str, Any]]: + """Sorts a lits of dicts by a given key, exception for the priority field that would go first""" + f_elem = None + if priority_field: + f_elem = next((elem for elem in list_to_sort if priority_field in elem), None) + tmp_dict = {elem[key]: elem for elem in list_to_sort if elem != f_elem} + first_elem = [f_elem] if f_elem else [] + return first_elem + list(dict(sorted(tmp_dict.items())).values()) + + def dict_subset(d: dict[str, str], subset_list: list[str]) -> dict[str, str]: """Returns the subset of dict only with subset_list keys""" return {key: d[key] for key in subset_list if key in d} @@ -281,15 +291,13 @@ def list_to_regexp(str_list: list[str]) -> str: return "(" + "|".join(str_list) + ")" if len(str_list) > 0 else "" -def list_to_csv( - array: Union[None, str, int, float, list[str], set[str], tuple[str]], separator: str = ",", check_for_separator: bool = False -) -> Optional[str]: +def list_to_csv(array: Union[None, str, int, float, list[str], set[str], tuple[str]], separator: str = ",", check_for_separator: bool = False) -> Any: """Converts a list of strings to CSV""" if isinstance(array, str): return csv_normalize(array, separator) if " " in array else array if array is None: return None - if isinstance(array, (list, set, tuple)): + if isinstance(array, (list, set, tuple)) and all(isinstance(e, str) for e in array): if check_for_separator: # Don't convert to string if one array item contains the string separator s = separator.strip() @@ -297,7 +305,7 @@ def list_to_csv( if s in item: return array return separator.join([v.strip() for v in array]) - return str(array) + return array def csv_normalize(string: str, separator: str = ",") -> str: @@ -317,10 +325,10 @@ def union(list1: list[any], list2: list[any]) -> list[any]: return list1 + [value for value in list2 if value not in list1] -def difference(list1: list[any], list2: list[any]) -> list[any]: +def difference(list1: list[Any], list2: list[Any]) -> list[Any]: """Computes difference of 2 lists""" # FIXME - This should be sets - return [value for value in list1 if value not in list2] + return list(set(list1) - set(list2)) def quote(string: str, sep: str) -> str: @@ -411,23 +419,6 @@ def convert_string(value: str) -> Union[str, int, float, bool]: return value -def update_json(json_data: dict[str, str], categ: str, subcateg: str, value: Any) -> dict[str, str]: - """Updates a 2 levels JSON""" - if categ not in json_data: - if subcateg is None: - json_data[categ] = value - else: - json_data[categ] = {subcateg: value} - elif subcateg is not None: - if subcateg in json_data[categ]: - json_data[categ][subcateg].update(value) - else: - json_data[categ][subcateg] = value - else: - json_data[categ].update(value) - return json_data - - def nbr_pages(sonar_api_json: dict[str, str], api_version: int = 1) -> int: """Returns nbr of pages of a paginated Sonar API call""" paging = "page" if api_version == 2 else "paging" @@ -595,6 +586,12 @@ def order_dict(d: dict[str, Any], key_order: list[str]) -> dict[str, Any]: return new_d | {k: v for k, v in d.items() if k not in new_d} +def order_list(l: list[str], *key_order) -> list[str]: + """Orders elements of a list in a given order""" + new_l = [k for k in key_order if k in l] + return new_l + [k for k in l if k not in new_l] + + def replace_keys(key_list: list[str], new_key: str, data: dict[str, any]) -> dict[str, any]: """Replace a list of old keys by a new key in a dict""" for k in key_list: diff --git a/test/unit/test_apps.py b/test/unit/test_apps.py index d80f50c0..a8dadbbf 100644 --- a/test/unit/test_apps.py +++ b/test/unit/test_apps.py @@ -29,6 +29,7 @@ from sonar import applications as apps, exceptions from sonar.applications import Application as App import sonar.util.constants as c +from sonar.permissions import permissions EXISTING_KEY = "APP_TEST" EXISTING_KEY_2 = "FE-BE" @@ -122,7 +123,11 @@ def test_permissions_1(get_test_app: Generator[App]) -> None: if not tutil.verify_support(SUPPORTED_EDITIONS, App.create, endpoint=tutil.SQ, name="An app", key=TEST_KEY): return obj = get_test_app - obj.set_permissions({"groups": {tutil.SQ.default_user_group(): ["user", "admin"], "sonar-administrators": ["user", "admin"]}}) + perms = [ + permissions.group_perms(tutil.SQ.default_user_group(), ["user", "admin"]), + permissions.group_perms("sonar-administrators", ["user", "admin"]), + ] + obj.set_permissions(perms) # assert apps.permissions().to_json()["groups"] == {tutil.SQ.default_user_group(): ["user", "admin"], "sonar-administrators": ["user", "admin"]} @@ -131,7 +136,8 @@ def test_permissions_2(get_test_app: Generator[App]) -> None: if not tutil.verify_support(SUPPORTED_EDITIONS, App.create, endpoint=tutil.SQ, name=tutil.TEMP_NAME, key=tutil.TEMP_KEY): return obj = get_test_app - obj.set_permissions({"groups": {tutil.SQ.default_user_group(): ["user"], "sonar-administrators": ["user", "admin"]}}) + perms = [permissions.group_perms(tutil.SQ.default_user_group(), ["user"]), permissions.group_perms("sonar-administrators", ["user", "admin"])] + obj.set_permissions(perms) # assert apps.permissions().to_json()["groups"] == {tutil.SQ.default_user_group(): ["user"], "sonar-administrators": ["user", "admin"]} diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 52f6d221..86bcb45c 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -119,50 +119,31 @@ def test_config_non_existing_project() -> None: assert tutil.run_cmd(config.main, f"{OPTS} -{opt.KEY_REGEXP_SHORT} bad_project") == e.WRONG_SEARCH_CRITERIA -def test_config_inline_lists(json_file: Generator[str]) -> None: - """test_config_inline_lists""" - - assert tutil.run_cmd(config.main, f"{OPTS} --{opt.REPORT_FILE} {json_file}") == e.OK - with open(file=json_file, mode="r", encoding="utf-8") as fh: - json_config = json.loads(fh.read()) - assert isinstance(json_config["globalSettings"]["languages"]["javascript"]["sonar.javascript.file.suffixes"], str) - assert isinstance( - json_config["globalSettings"]["permissionTemplates"][_DEFAULT_TEMPLATE]["permissions"]["groups"][tutil.SQ.default_user_group()], str - ) - assert isinstance(json_config["projects"][tutil.LIVE_PROJECT]["permissions"]["groups"][tutil.SQ.default_user_group()], str) - - if tutil.SQ.edition() not in (c.CE, c.DE): - assert isinstance(json_config["portfolios"]["PORTFOLIO_ALL"]["permissions"]["groups"]["sonar-administrators"], str) - assert isinstance(json_config["portfolios"]["PORTFOLIO-PYTHON"]["projects"]["tags"], str) - # This is a list because there is a comma in one of the branches - if tutil.SQ.version() >= (10, 0, 0): - assert isinstance(json_config["portfolios"]["PORTFOLIO_MULTI_BRANCHES"]["projects"]["manual"]["BANKING-PORTAL"], list) - assert json_config["portfolios"]["All"]["portfolios"]["Banking"]["byReference"] - - # Verify JSON export is in the expected key order - assert __is_ordered_as_expected(list(json_config.keys()), config._SECTIONS_ORDER) - for section in config._SECTIONS_TO_SORT: - assert sorted(json_config.get(section, {}).keys()) == list(json_config.get(section, {}).keys()) - - def test_config_dont_inline_lists(json_file: Generator[str]) -> None: """test_config_dont_inline_lists""" - assert tutil.run_cmd(config.main, f"{OPTS} --{opt.REPORT_FILE} {json_file} --{opt.WHAT} settings,projects,portfolios --dontInlineLists") == e.OK + assert tutil.run_cmd(config.main, f"{OPTS} --{opt.REPORT_FILE} {json_file} --{opt.WHAT} settings,projects,portfolios") == e.OK with open(file=json_file, mode="r", encoding="utf-8") as fh: json_config = json.loads(fh.read()) - assert isinstance(json_config["globalSettings"]["languages"]["javascript"]["sonar.javascript.file.suffixes"], list) - assert isinstance( - json_config["globalSettings"]["permissionTemplates"][_DEFAULT_TEMPLATE]["permissions"]["groups"][tutil.SQ.default_user_group()], list - ) - assert isinstance(json_config["projects"][tutil.LIVE_PROJECT]["permissions"]["groups"][tutil.SQ.default_user_group()], list) + # pset = json_config["globalSettings"] + # assert isinstance(pset["languages"]["javascript"]["sonar.javascript.file.suffixes"], list) + # tpl = next(p for p in pset["permissionTemplates"] if p["name"] == _DEFAULT_TEMPLATE) + # perms = next(p["permissions"] for p in tpl["permissions"] if p.get("group", "") == tutil.SQ.default_user_group()) + # print(f"PERMS X = {perms}") + # assert isinstance(perms, list) + # assert isinstance(json_config["projects"][tutil.LIVE_PROJECT]["permissions"]["groups"][tutil.SQ.default_user_group()], list) if tutil.SQ.edition() not in (c.CE, c.DE): - assert isinstance(json_config["portfolios"]["PORTFOLIO_ALL"]["permissions"]["groups"]["sonar-administrators"], list) - assert isinstance(json_config["portfolios"]["PORTFOLIO-PYTHON"]["projects"]["tags"], list) + pset = next(p for p in json_config["portfolios"] if p["key"] == "PORTFOLIO-PYTHON") + assert isinstance(pset["tags"], list) if tutil.SQ.version() >= (10, 0, 0): - assert isinstance(json_config["portfolios"]["PORTFOLIO_MULTI_BRANCHES"]["projects"]["manual"]["BANKING-PORTAL"], list) + pset = next(p for p in json_config["portfolios"] if p["key"] == "PORTFOLIO_MULTI_BRANCHES") + pset = next(p for p in pset["projects"] if p["key"] == "BANKING-PORTAL") + assert isinstance(pset["branches"], list) if tutil.SQ.edition() != c.CE and tutil.SQ.version() > (10, 0, 0): - assert "sonar.cfamily.ignoreHeaderComments" not in json_config["globalSettings"]["languages"]["cfamily"] - assert "sonar.cfamily.ignoreHeaderComments" in json_config["projects"][tutil.LIVE_PROJECT] + pset = next(p for p in json_config["globalSettings"]["languages"] if p["language"] == "cfamily") + assert "sonar.cfamily.ignoreHeaderComments" not in [p["key"] for p in pset["settings"]] + pset = next(p for p in json_config["projects"] if p["key"] == tutil.LIVE_PROJECT) + pset = [p["key"] for p in pset["settings"]] + assert "sonar.cfamily.ignoreHeaderComments" in pset def test_config_import_portfolios() -> None: