7
7
# https://docs.python.org/3/whatsnew/3.11.html#pep-563-may-not-be-the-future
8
8
from __future__ import annotations
9
9
10
- import concurrent . futures
10
+ import concurrent
11
11
import json
12
12
import logging
13
13
from functools import lru_cache
@@ -93,7 +93,6 @@ def raise_for_status(self, *args, **kwargs):
93
93
raise
94
94
95
95
get_server_info = instrumented_method (Jira .get_server_info )
96
- get_permissions = instrumented_method (Jira .get_permissions )
97
96
get_project_components = instrumented_method (Jira .get_project_components )
98
97
projects = instrumented_method (Jira .projects )
99
98
update_issue = instrumented_method (Jira .update_issue )
@@ -103,6 +102,19 @@ def raise_for_status(self, *args, **kwargs):
103
102
create_issue = instrumented_method (Jira .create_issue )
104
103
get_project = instrumented_method (Jira .get_project )
105
104
105
+ @instrumented_method
106
+ def permitted_projects (self , permissions : Iterable ):
107
+ """Fetches projects that the user has the required permissions for
108
+
109
+ https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-permissions/#api-rest-api-2-permissions-project-post
110
+ """
111
+
112
+ response = self .post (
113
+ "/rest/api/2/permissions/project" ,
114
+ json = {"permissions" : list (JIRA_REQUIRED_PERMISSIONS )},
115
+ )
116
+ return response ["projects" ]
117
+
106
118
107
119
class JiraService :
108
120
"""Used by action workflows to perform action-specific Jira tasks"""
@@ -124,16 +136,24 @@ def check_health(self, actions: Actions) -> ServiceHealth:
124
136
health : ServiceHealth = {
125
137
"up" : is_up ,
126
138
"all_projects_are_visible" : is_up and self ._all_projects_visible (actions ),
127
- "all_projects_have_permissions" : self ._all_projects_permissions (actions ),
128
- "all_projects_components_exist" : is_up
129
- and self ._all_projects_components_exist (actions ),
139
+ "all_projects_have_permissions" : is_up
140
+ and self ._all_projects_permissions (actions ),
141
+ "all_project_custom_components_exist" : is_up
142
+ and self ._all_project_custom_components_exist (actions ),
130
143
"all_projects_issue_types_exist" : is_up
131
144
and self ._all_project_issue_types_exist (actions ),
132
145
}
133
146
return health
134
147
135
148
def _all_projects_visible (self , actions : Actions ) -> bool :
136
- visible_projects = {project ["key" ] for project in self .fetch_visible_projects ()}
149
+ try :
150
+ visible_projects = {
151
+ project ["key" ] for project in self .fetch_visible_projects ()
152
+ }
153
+ except requests .HTTPError :
154
+ logger .exception ("Error fetching visible Jira projects" )
155
+ return False
156
+
137
157
missing_projects = actions .configured_jira_projects_keys - visible_projects
138
158
if missing_projects :
139
159
logger .error (
@@ -142,97 +162,96 @@ def _all_projects_visible(self, actions: Actions) -> bool:
142
162
)
143
163
return not missing_projects
144
164
145
- def _all_projects_permissions (self , actions : Actions ):
165
+ def _all_projects_permissions (self , actions : Actions ) -> bool :
146
166
"""Fetches and validates that required permissions exist for the configured projects"""
147
- all_projects_perms = self ._fetch_project_permissions (actions )
148
- return self ._validate_permissions (all_projects_perms )
167
+ try :
168
+ projects = self .client .permitted_projects (JIRA_REQUIRED_PERMISSIONS )
169
+ except requests .HTTPError :
170
+ logger .exception (
171
+ "Encountered error when trying to fetch permitted projects"
172
+ )
173
+ return False
149
174
150
- def _fetch_project_permissions (self , actions : Actions ):
151
- """Fetches permissions for the configured projects"""
175
+ projects_with_required_perms = {project ["key" ] for project in projects }
176
+ missing_perms = (
177
+ actions .configured_jira_projects_keys - projects_with_required_perms
178
+ )
179
+ if missing_perms :
180
+ logger .warning (
181
+ "Missing permissions for projects %s" , ", " .join (missing_perms )
182
+ )
183
+ return False
152
184
153
- all_projects_perms = {}
154
- # Query permissions for all configured projects in parallel threads.
185
+ return True
186
+
187
+ def _all_project_custom_components_exist (self , actions : Actions ):
155
188
with concurrent .futures .ThreadPoolExecutor (max_workers = 4 ) as executor :
156
- futures_to_projects = {
157
- executor .submit (
158
- self .client .get_permissions ,
159
- project_key = project_key ,
160
- permissions = "," .join (JIRA_REQUIRED_PERMISSIONS ),
161
- ): project_key
162
- for project_key in actions .configured_jira_projects_keys
189
+ futures = {
190
+ executor .submit (self ._check_project_components , action ): action
191
+ for action in actions
192
+ if action .parameters .jira_components .set_custom_components
163
193
}
164
- # Obtain futures' results unordered.
165
- for future in concurrent .futures .as_completed (futures_to_projects ):
166
- project_key = futures_to_projects [future ]
167
- response = future .result ()
168
- all_projects_perms [project_key ] = response ["permissions" ]
169
- return all_projects_perms
170
-
171
- def _validate_permissions (self , all_projects_perms ):
172
- """Validates permissions for the configured projects"""
173
- misconfigured = []
174
- for project_key , obtained_perms in all_projects_perms .items ():
175
- missing_required_perms = JIRA_REQUIRED_PERMISSIONS - set (
176
- obtained_perms .keys ()
177
- )
178
- not_given = set (
179
- entry ["key" ]
180
- for entry in obtained_perms .values ()
181
- if not entry ["havePermission" ]
194
+
195
+ success = True
196
+ for future in concurrent .futures .as_completed (futures ):
197
+ success = success and future .result ()
198
+ return success
199
+
200
+ def _check_project_components (self , action ):
201
+ project_key = action .parameters .jira_project_key
202
+ specified_components = set (
203
+ action .parameters .jira_components .set_custom_components
204
+ )
205
+
206
+ try :
207
+ all_project_components = self .client .get_project_components (project_key )
208
+ except requests .HTTPError :
209
+ logger .exception ("Error checking project components for %s" , project_key )
210
+ return False
211
+
212
+ try :
213
+ all_components_names = set (comp ["name" ] for comp in all_project_components )
214
+ except KeyError :
215
+ logger .exception (
216
+ "Unexpected get_project_components response for %s" ,
217
+ action .whiteboard_tag ,
182
218
)
183
- if missing_permissions := missing_required_perms .union (not_given ):
184
- misconfigured .append ((project_key , missing_permissions ))
185
- for project_key , missing in misconfigured :
219
+ return False
220
+
221
+ unknown = specified_components - all_components_names
222
+ if unknown :
186
223
logger .error (
187
- "Configured credentials don't have permissions %s on Jira project %s" ,
188
- "," .join (missing ),
224
+ "Jira project %s does not have components %s" ,
189
225
project_key ,
190
- extra = {
191
- "jira" : {
192
- "project" : project_key ,
193
- }
194
- },
226
+ unknown ,
195
227
)
196
- return not misconfigured
197
-
198
- def _all_projects_components_exist (self , actions : Actions ):
199
- components_by_project = {
200
- action .parameters .jira_project_key : action .parameters .jira_components .set_custom_components
201
- for action in actions
202
- }
203
- success = True
204
- for project , specified_components in components_by_project .items ():
205
- all_project_components = self .client .get_project_components (project )
206
- all_components_names = set (comp ["name" ] for comp in all_project_components )
207
- unknown = set (specified_components ) - all_components_names
208
- if unknown :
209
- logger .error (
210
- "Jira project %s does not have components %s" ,
211
- project ,
212
- unknown ,
213
- )
214
- success = False
228
+ return False
215
229
216
- return success
230
+ return True
217
231
218
232
def _all_project_issue_types_exist (self , actions : Actions ):
233
+ projects = self .client .projects (included_archived = None , expand = "issueTypes" )
219
234
issue_types_by_project = {
220
- action .parameters .jira_project_key : set (
221
- action .parameters .issue_type_map .values ()
222
- )
223
- for action in actions
235
+ project ["key" ]: {issue_type ["name" ] for issue_type in project ["issueTypes" ]}
236
+ for project in projects
224
237
}
225
- success = True
226
- for project , specified_issue_types in issue_types_by_project .items ():
227
- response = self .client .get_project (project )
228
- all_issue_types_names = set (it ["name" ] for it in response ["issueTypes" ])
229
- unknown = set (specified_issue_types ) - all_issue_types_names
230
- if unknown :
231
- logger .error (
232
- "Jira project %s does not have issue type %s" , project , unknown
233
- )
234
- success = False
235
- return success
238
+ missing_issue_types_by_project = {}
239
+ for action in actions :
240
+ action_issue_types = set (action .parameters .issue_type_map .values ())
241
+ project_issue_types = issue_types_by_project .get (
242
+ action .jira_project_key , set ()
243
+ )
244
+ if missing_issue_types := action_issue_types - project_issue_types :
245
+ missing_issue_types_by_project [
246
+ action .jira_project_key
247
+ ] = missing_issue_types
248
+ if missing_issue_types_by_project :
249
+ logger .warning (
250
+ "Jira projects with missing issue types" ,
251
+ extra = missing_issue_types_by_project ,
252
+ )
253
+ return False
254
+ return True
236
255
237
256
def get_issue (self , context : ActionContext , issue_key ):
238
257
"""Return the Jira issue fields or `None` if not found."""
0 commit comments