Skip to content

Commit 1eca63c

Browse files
authored
Merge pull request #1295 from okorach:Fix-1288
Fix #1288
2 parents dae54dc + f5aa570 commit 1eca63c

19 files changed

+176
-81
lines changed

cli/config.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,18 +179,14 @@ def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> N
179179
log.info("Exporting configuration from %s completed", kwargs["url"])
180180

181181

182-
def __read_input_file(file: str) -> dict[str, any]:
182+
def __import_config(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
183+
"""Imports a platform configuration from a JSON file"""
184+
log.info("Importing configuration to %s", kwargs[options.URL])
183185
try:
184-
with open(file, "r", encoding="utf-8") as fd:
186+
with open(kwargs[options.REPORT_FILE], "r", encoding="utf-8") as fd:
185187
data = json.loads(fd.read())
186188
except FileNotFoundError as e:
187189
utilities.exit_fatal(f"OS error while reading file: {e}", exit_code=errcodes.OS_ERROR)
188-
return data
189-
190-
191-
def __import_config(endpoint: platform.Platform, what: list[str], data: dict[str, any], **kwargs) -> None:
192-
"""Imports a platform configuration from a JSON file"""
193-
log.info("Importing configuration to %s", kwargs[options.URL])
194190
key_list = kwargs[options.KEYS]
195191

196192
calls = {
@@ -240,7 +236,7 @@ def main() -> None:
240236
if kwargs["import"]:
241237
if kwargs["file"] is None:
242238
utilities.exit_fatal("--file is mandatory to import configuration", errcodes.ARGS_ERROR)
243-
__import_config(endpoint, what, __read_input_file(kwargs[options.REPORT_FILE]), **kwargs)
239+
__import_config(endpoint, what, **kwargs)
244240
utilities.stop_clock(start_time)
245241
sys.exit(0)
246242

doc/sonar-audit.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';'
111111
- More than 3 groups with `create project` permission
112112
- More than 10 groups with any global permissions
113113
- Permission Templates: (if `audit.projects.permissions = yes`, default `yes`)
114+
- Permissions Templates with no permissions granted
114115
- More than `audit.projects.permissions.maxUsers` different users with direct permissions (default 5)
115116
- More than `audit.projects.permissions.maxAdminUsers` users with Project admin permission (default 2)
116117
- 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 ';'
120121
- More than `audit.projects.permissions.maxAdminGroups` groups with project admin permission (default 2)
121122
- `sonar-users` group with elevated project permissions
122123
- `Anyone` group with any project permissions
124+
- No projectKeyPattern for a template that is not a default
125+
- Suspicious projectKeyPattern (a pattern that is likely to not select more than 1 key) for instance:
126+
- `my_favorite_project` (no `.` in pattern)
127+
- `BUXXXX-*` (Likely confusion between wildcards and regexp)
123128
- DB Cleaner: (if `audit.globalSettings = yes`, default `yes`)
124129
- Delay to delete inactive short lived branches (7.9) or branches (8.0+) not between 10 and 60 days
125130
- Delay to delete closed issues not between 10 and 60 days
@@ -195,10 +200,12 @@ sonar-audit --what projects -f projectsAudit.csv --csvSeparator ';'
195200
- Empty portfolios (with no projects) if `audit.portfolios.empty` is `yes`
196201
- Portfolios composed of a single project if `audit.portfolios.singleton` is `yes`
197202
- Last recomputation `FAILED`
203+
- Portfolios with no permissions
198204
- Applications: (if `audit.applications = yes`, default `yes`)
199205
- Empty applications (with no projects) if `audit.applications.empty` is `yes`
200206
- Applications composed of a single project if `audit.applications.singleton` is `yes`
201207
- Last recomputation `FAILED`
208+
- Applications with no permissions
202209
- Users: (if `audit.users = yes`, default `yes`)
203210
- Users that did not login on the platform since `audit.users.maxLoginAge` days (default 180 days)
204211
- Tokens older than `audit.tokens.maxAge` days (default 90 days)

doc/what-is-new.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Next version yet unreleased
22

33
- `sonar-tools` is now available as a docker image
4-
- `sonar-config` export can now export configuration as a YAML file (Only JSON was available previously). Import of YAML is not yet available
4+
- `sonar-config`
5+
- Export can now export configuration as a YAML file (Only JSON was available previously).
6+
Import of YAML is not yet available
7+
- Beta version of config import in SonarCloud
8+
- `sonar-audit` a couple of new audit problems on permission templates (with no permissions, with no or wrong regexp)
59

610
# Version 3.3
711

sonar/aggregations.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
Parent module of applications and portfolios
2323
2424
"""
25+
26+
from typing import Optional
2527
import json
2628

2729
import sonar.logging as log
30+
from sonar.util import types
2831
import sonar.platform as pf
29-
from sonar.util.types import ApiPayload, ApiParams
3032

3133
import sonar.components as comp
3234

@@ -37,7 +39,7 @@
3739
class Aggregation(comp.Component):
3840
"""Parent class of applications and portfolios"""
3941

40-
def __init__(self, endpoint: pf.Platform, key: str, data: ApiPayload = None) -> None:
42+
def __init__(self, endpoint: pf.Platform, key: str, data: types.ApiPayload = None) -> None:
4143
self._nbr_projects = None
4244
self._permissions = None
4345
super().__init__(endpoint=endpoint, key=key)
@@ -90,8 +92,17 @@ def _audit_empty_aggregation(self, broken_rule: object) -> list[Problem]:
9092
def _audit_singleton_aggregation(self, broken_rule: object) -> list[Problem]:
9193
return self._audit_aggregation_cardinality((1, 1), broken_rule)
9294

95+
def permissions(self) -> Optional[object]:
96+
"""Should be implement in child classes"""
97+
return self._permissions
98+
99+
def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
100+
if self.permissions() is None:
101+
return []
102+
return self.permissions().audit(audit_settings)
103+
93104

94-
def count(api: str, endpoint: pf.Platform, params: ApiParams = None) -> int:
105+
def count(api: str, endpoint: pf.Platform, params: types.ApiParams = None) -> int:
95106
"""Returns number of aggregations of a given type (Application OR Portfolio)
96107
:return: number of Apps or Portfolios
97108
:rtype: int

sonar/applications.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,12 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[problem.Problem]:
321321
:rtype: list [Problem]
322322
"""
323323
log.info("Auditing %s", str(self))
324-
return self._audit_empty(audit_settings) + self._audit_singleton(audit_settings) + self._audit_bg_task(audit_settings)
324+
return (
325+
super().audit(audit_settings)
326+
+ self._audit_empty(audit_settings)
327+
+ self._audit_singleton(audit_settings)
328+
+ self._audit_bg_task(audit_settings)
329+
)
325330

326331
def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
327332
"""Exports an application
@@ -346,7 +351,7 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
346351
)
347352
return util.remove_nones(util.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False)))
348353

349-
def set_permissions(self, data):
354+
def set_permissions(self, data: types.JsonPermissions) -> application_permissions.ApplicationPermissions:
350355
"""Sets an application permissions
351356
352357
:param dict data: dict of permission {"users": [<user1>, <user2>, ...], "groups": [<group1>, <group2>, ...]}

sonar/audit/rules.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@
99
"type": "BAD_PRACTICE",
1010
"message": "{}"
1111
},
12+
"OBJECT_WITH_NO_PERMISSIONS": {
13+
"severity": "MEDIUM",
14+
"type": "OPERATIONS",
15+
"message": "{} has no permissions defined"
16+
},
17+
"OBJECT_WITH_NO_ADMIN_PERMISSION": {
18+
"severity": "MEDIUM",
19+
"type": "OPERATIONS",
20+
"message": "{} has no user or group with admin permission"
21+
},
22+
"TEMPLATE_WITH_NO_PATTERN": {
23+
"severity": "MEDIUM",
24+
"type": "OPERATIONS",
25+
"message": "{} is not a default and has no projectKetPattern defined, it will never be used"
26+
},
27+
"TEMPLATE_WITH_SUSPICIOUS_PATTERN": {
28+
"severity": "HIGH",
29+
"type": "OPERATIONS",
30+
"message": "{} has a suspicious projectKeyPattern '{}'. It should be a regexp that may match several keys"
31+
},
1232
"DUBIOUS_GLOBAL_SETTING": {
1333
"severity": "HIGH",
1434
"type": "BAD_PRACTICE",

sonar/audit/rules.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class RuleId(enum.Enum):
7272
ANYONE_WITH_GLOBAL_PERMS = 151
7373
SONAR_USERS_WITH_ELEVATED_PERMS = 152
7474
FAILED_WEBHOOK = 153
75+
OBJECT_WITH_NO_PERMISSIONS = 154
76+
OBJECT_WITH_NO_ADMIN_PERMISSION = 155
7577

7678
DCE_DIFFERENT_APP_NODES_VERSIONS = 160
7779
DCE_DIFFERENT_APP_NODES_PLUGINS = 161
@@ -161,6 +163,9 @@ class RuleId(enum.Enum):
161163

162164
GROUP_EMPTY = 5200
163165

166+
TEMPLATE_WITH_NO_PATTERN = 5300
167+
TEMPLATE_WITH_SUSPICIOUS_PATTERN = 5301
168+
164169
ERROR_IN_LOGS = 6000
165170
WARNING_IN_LOGS = 6001
166171
DEPRECATION_WARNINGS = 6002

sonar/permissions/global_permissions.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,41 @@
1818
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
#
2020

21+
from __future__ import annotations
2122

2223
import sonar.logging as log
2324
from sonar.permissions import permissions
25+
from sonar.util import types
2426

2527

2628
class GlobalPermissions(permissions.Permissions):
29+
"""Abstraction of SonarQube global permissions"""
30+
2731
API_GET = {"users": "permissions/users", "groups": "permissions/groups"}
2832
API_SET = {"users": "permissions/add_user", "groups": "permissions/add_group"}
2933
API_REMOVE = {"users": "permissions/remove_user", "groups": "permissions/remove_group"}
3034
API_GET_FIELD = {"users": "login", "groups": "name"}
3135
API_SET_FIELD = {"users": "login", "groups": "groupName"}
3236

33-
def __str__(self):
37+
def __init__(self, concerned_object: object) -> None:
38+
self.concerned_object = concerned_object
39+
self.endpoint = concerned_object
40+
self.permissions = None
41+
self.read()
42+
43+
def __str__(self) -> str:
3444
return "global permissions"
3545

36-
def read(self):
46+
def read(self) -> GlobalPermissions:
47+
"""Reads global permissions"""
3748
self.permissions = permissions.NO_PERMISSIONS
3849
for ptype in permissions.PERMISSION_TYPES:
3950
self.permissions[ptype] = self._get_api(
4051
GlobalPermissions.API_GET[ptype], ptype, GlobalPermissions.API_GET_FIELD[ptype], ps=permissions.MAX_PERMS
4152
)
4253
return self
4354

44-
def set(self, new_perms):
55+
def set(self, new_perms: types.JsonPermissions) -> GlobalPermissions:
4556
log.debug("Setting %s to %s", str(self), str(new_perms))
4657
if self.permissions is None:
4758
self.read()
@@ -57,7 +68,8 @@ def set(self, new_perms):
5768
return self.read()
5869

5970

60-
def import_config(endpoint, config_data):
71+
def import_config(endpoint: object, config_data: types.ObjectJsonRepr):
72+
"""Imports global permissions in a SonarQube platform"""
6173
my_permissions = config_data.get("permissions", {})
6274
if len(my_permissions) == 0:
6375
log.info("No global permissions in config, skipping import...")
@@ -67,9 +79,10 @@ def import_config(endpoint, config_data):
6779
global_perms.set(my_permissions)
6880

6981

70-
def edition_filter(perms, ed):
82+
def edition_filter(perms: types.JsonPermissions, ed: str) -> types.JsonPermissions:
83+
"""Filters permissions available in a given edition"""
7184
for p in perms.copy():
7285
if ed == "community" and p in ("portfoliocreator", "applicationcreator") or ed == "developer" and p == "portfoliocreator":
73-
log.warning("Can't remove permission '%s' on a %s edition", p, ed)
86+
log.warning("Can't manage permission '%s' on a %s edition", p, ed)
7487
perms.remove(p)
7588
return perms

sonar/permissions/permission_templates.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
from __future__ import annotations
2222

2323
import json
24+
import re
2425
from requests.exceptions import HTTPError
2526

2627
import sonar.logging as log
2728
from sonar.util import types
2829
from sonar import sqobject, utilities
2930
from sonar.permissions import template_permissions
3031
import sonar.platform as pf
32+
from sonar.audit.rules import get_rule, RuleId
3133
import sonar.audit.problem as pb
3234

3335
_OBJECTS = {}
@@ -176,9 +178,23 @@ def to_json(self, export_settings: types.ConfigSettings = None) -> types.ObjectJ
176178
json_data["lastUpdate"] = utilities.date_to_string(self.last_update)
177179
return utilities.remove_nones(utilities.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False)))
178180

181+
def _audit_pattern(self, audit_settings: types.ConfigSettings) -> list[pb.Problem]:
182+
log.debug("Auditing %s projectKeyPattern ('%s')", str(self.project_key_pattern))
183+
if not self.project_key_pattern or self.project_key_pattern == "":
184+
if not (self.is_applications_default() or self.is_portfolios_default() or self.is_projects_default()):
185+
return [pb.Problem(get_rule(RuleId.TEMPLATE_WITH_NO_PATTERN), self, str(self))]
186+
else:
187+
# Inspect regexp to detect suspicious pattern - Can't determine all bad cases but do our best
188+
# Currently detecting:
189+
# - Absence of '.' in the regexp
190+
# - '*' not preceded by '.' (confusion between wildcard and regexp)
191+
if not re.search(r"(^|[^\\])\.", self.project_key_pattern) or re.search(r"(^|[^.])\*", self.project_key_pattern):
192+
return [pb.Problem(get_rule(RuleId.TEMPLATE_WITH_SUSPICIOUS_PATTERN), self, str(self), self.project_key_pattern)]
193+
return []
194+
179195
def audit(self, audit_settings: types.ConfigSettings) -> list[pb.Problem]:
180196
log.debug("Auditing %s", str(self))
181-
return self.permissions().audit(audit_settings)
197+
return self._audit_pattern(audit_settings) + self.permissions().audit(audit_settings)
182198

183199

184200
def get_object(endpoint: pf.Platform, name: str) -> PermissionTemplate:

sonar/permissions/permissions.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import sonar.logging as log
3232
from sonar import utilities, errcodes
3333
from sonar.util import types
34+
from sonar.audit.rules import get_rule, RuleId
35+
from sonar.audit.problem import Problem
3436

3537
COMMUNITY_GLOBAL_PERMISSIONS = {
3638
"admin": "Administer System",
@@ -71,11 +73,15 @@ class Permissions(ABC):
7173
Abstraction of sonar objects permissions
7274
"""
7375

74-
def __init__(self, endpoint: object) -> None:
75-
self.endpoint = endpoint
76+
def __init__(self, concerned_object: object) -> None:
77+
self.concerned_object = concerned_object
78+
self.endpoint = concerned_object.endpoint
7679
self.permissions = None
7780
self.read()
7881

82+
def __str__(self) -> str:
83+
return f"permissions of {str(self.concerned_object)}"
84+
7985
def to_json(self, perm_type: str = None, csv: bool = False) -> types.JsonPermissions:
8086
"""Converts a permission object to JSON"""
8187
if not csv:
@@ -98,10 +104,6 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
98104
return None
99105
return perms
100106

101-
@abstractmethod
102-
def __str__(self) -> str:
103-
pass
104-
105107
@abstractmethod
106108
def read(self) -> Permissions:
107109
"""
@@ -130,21 +132,6 @@ def set_group_permissions(self, group_perms: dict[str, list[str]]) -> Permission
130132
"""
131133
return self.set({"groups": group_perms})
132134

133-
"""
134-
@abstractmethod
135-
def remove_user_permissions(self, user_perms_dict):
136-
pass
137-
138-
@abstractmethod
139-
def remove_group_permissions(self, group_perms_dict):
140-
pass
141-
142-
143-
def remove_permissions(self, perms_dict):
144-
self.remove_user_permissions(perms_dict.get("users", None))
145-
self.remove_group_permissions(perms_dict.get("groups", None))
146-
"""
147-
148135
def clear(self) -> Permissions:
149136
"""Clears all permissions of an object
150137
:return: self
@@ -206,6 +193,18 @@ def _filter_permissions_for_edition(self, perms: types.JsonPermissions) -> types
206193
perms.remove(p)
207194
return perms
208195

196+
def audit_nbr_permissions(self, audit_settings: types.ConfigSettings) -> list[Problem]:
197+
"""Audits that at least one permission is granted to a user or a group
198+
and that at least one group or user has admin permission on the object"""
199+
if self.count() == 0:
200+
return [Problem(get_rule(RuleId.OBJECT_WITH_NO_PERMISSIONS), self.concerned_object, str(self.concerned_object))]
201+
elif self.count(perm_filter=["admin"]) == 0:
202+
return [Problem(get_rule(RuleId.OBJECT_WITH_NO_ADMIN_PERMISSION), self.concerned_object, str(self.concerned_object))]
203+
return []
204+
205+
def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
206+
return self.audit_nbr_permissions(audit_settings)
207+
209208
def count(self, perm_type: Optional[str] = None, perm_filter: Optional[list[str]] = None) -> int:
210209
"""Counts number of permissions of an object
211210
@@ -221,7 +220,6 @@ def count(self, perm_type: Optional[str] = None, perm_filter: Optional[list[str]
221220
if perm_filter is None:
222221
continue
223222
perm_counter += len([1 for p in elem_perms if p in perm_filter])
224-
log.debug("Perm counts = %d", (elem_counter if perm_filter is None else perm_counter))
225223
return elem_counter if perm_filter is None else perm_counter
226224

227225
def _get_api(self, api: str, perm_type: str, ret_field: str, **extra_params) -> types.JsonPermissions:

0 commit comments

Comments
 (0)