Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
63c3167
Fix gregression
okorach-sonar Oct 31, 2025
0dd3cf0
Remove unwanted log
okorach-sonar Oct 31, 2025
db29b99
Remove unneeded function
okorach-sonar Oct 31, 2025
bb1c455
Change to_json() to return dict
okorach-sonar Oct 31, 2025
05b7dee
Fixes #2041
okorach-sonar Oct 31, 2025
c3267c5
Fixes #2038
okorach-sonar Oct 31, 2025
a7b868b
Set lang as a category
okorach-sonar Oct 31, 2025
2643e3e
Add SCA category
okorach-sonar Oct 31, 2025
b019fbd
Split category and language
okorach-sonar Oct 31, 2025
7ef6cc4
Settings keys regexp as data, not code
okorach-sonar Oct 31, 2025
bd1b6af
Improve docstring
okorach-sonar Oct 31, 2025
dc24ba3
Fix sonar.announcement.message setting value
okorach-sonar Oct 31, 2025
df0e280
Handle full export properly
okorach-sonar Oct 31, 2025
03d8f70
Fix setting inherited
okorach-sonar Oct 31, 2025
ae37895
Convert settings values "true" and "false"
okorach-sonar Oct 31, 2025
8b31326
Remove list inlining
okorach-sonar Oct 31, 2025
b222598
Suppress permissions inlining
okorach-sonar Oct 31, 2025
057b1cd
Remove test on inline lists
okorach-sonar Oct 31, 2025
8dc0fa2
Fix handling of false project settings that are actually only globals
okorach-sonar Nov 1, 2025
01f260b
Fix full project settings export
okorach-sonar Nov 1, 2025
1b3a6e9
Fix branch export as list
okorach-sonar Nov 1, 2025
a31b38f
Add sort_list_by_key()
okorach-sonar Nov 1, 2025
bd87183
Sort project branches and settings
okorach-sonar Nov 1, 2025
43c878c
Sort global settings
okorach-sonar Nov 1, 2025
dcd8516
Sort plugin list
okorach-sonar Nov 1, 2025
faf8755
Sort webhooks
okorach-sonar Nov 1, 2025
4c2af50
Add order_list()
okorach-sonar Nov 1, 2025
a84441a
Order permissions
okorach-sonar Nov 1, 2025
833e4bd
Use strings for array settings
okorach-sonar Nov 1, 2025
64885e9
Add permission order
okorach-sonar Nov 1, 2025
6134045
Remove unused function
okorach-sonar Nov 1, 2025
7b8c0df
Redefine permission type
okorach-sonar Nov 1, 2025
2c5c1cc
Quality pass
okorach-sonar Nov 1, 2025
2bb698a
Quality pass
okorach-sonar Nov 1, 2025
5932bfd
Change permission format
okorach-sonar Nov 1, 2025
7471d08
Improve difference()
okorach-sonar Nov 1, 2025
6d29633
Update with new perms format
okorach-sonar Nov 1, 2025
e5a73f7
Restore old JSON perms
okorach-sonar Nov 1, 2025
66b5ef3
Revert to old permissions
okorach-sonar Nov 1, 2025
e9e510d
Remove log
okorach-sonar Nov 1, 2025
bcf508d
Fix for new perms format
okorach-sonar Nov 1, 2025
7aab834
Fixes for new perms format
okorach-sonar Nov 1, 2025
d6591f4
Fix change of setting format
okorach-sonar Nov 1, 2025
996f34d
Last adjustments
okorach-sonar Nov 1, 2025
9e0ed8d
Adapt to new config export format
okorach-sonar Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@

TOOL_NAME = "sonar-config"

DONT_INLINE_LISTS = "dontInlineLists"
FULL_EXPORT = "fullExport"
EXPORT_EMPTY = "exportEmpty"

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down
8 changes: 4 additions & 4 deletions sonar/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
)
Expand Down Expand Up @@ -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]:
Expand Down
7 changes: 2 additions & 5 deletions sonar/branches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion sonar/permissions/aggregation_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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))
15 changes: 15 additions & 0 deletions sonar/permissions/global_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
3 changes: 1 addition & 2 deletions sonar/permissions/permission_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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())


Expand Down
107 changes: 54 additions & 53 deletions sonar/permissions/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand All @@ -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

Expand All @@ -81,29 +82,23 @@ 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:
continue
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:
Expand All @@ -119,44 +114,28 @@ 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
:rtype: 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)
Expand Down Expand Up @@ -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, {}))
Expand All @@ -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()
Expand All @@ -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 []
Expand Down Expand Up @@ -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]]:
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions sonar/permissions/quality_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
Loading