Skip to content

Commit 367b50e

Browse files
committed
Overall code structure for assignment sync
1 parent 28ef3a6 commit 367b50e

File tree

1 file changed

+267
-0
lines changed
  • ansible_base/resource_registry/tasks

1 file changed

+267
-0
lines changed

ansible_base/resource_registry/tasks/sync.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,29 @@ def __iter__(self):
6666
return iter((self.status, self.item))
6767

6868

69+
@dataclass
70+
class AssignmentTuple:
71+
"""Represents an assignment as a 3-tuple for comparison"""
72+
73+
actor_ansible_id: str # user_ansible_id or team_ansible_id
74+
object_id: str | None # object_id or object_ansible_id (None for global)
75+
role_definition_name: str
76+
assignment_type: str # 'user' or 'team'
77+
78+
def __hash__(self):
79+
return hash((self.actor_ansible_id, self.object_id, self.role_definition_name, self.assignment_type))
80+
81+
def __eq__(self, other):
82+
if not isinstance(other, AssignmentTuple):
83+
return False
84+
return (
85+
self.actor_ansible_id == other.actor_ansible_id
86+
and self.object_id == other.object_id
87+
and self.role_definition_name == other.role_definition_name
88+
and self.assignment_type == other.assignment_type
89+
)
90+
91+
6992
def create_api_client() -> ResourceAPIClient:
7093
"""Factory for pre-configured ResourceAPIClient."""
7194
params = {"raise_if_bad_request": False}
@@ -107,6 +130,193 @@ def fetch_manifest(
107130
return [ManifestItem(**row) for row in csv_reader]
108131

109132

133+
def get_remote_assignments(api_client: ResourceAPIClient) -> set[AssignmentTuple]:
134+
"""Fetch remote assignments from the resource server and convert to tuples."""
135+
assignments = set()
136+
137+
# Fetch user assignments
138+
try:
139+
user_resp = api_client.list_user_assignments()
140+
if user_resp.status_code == 200:
141+
user_data = user_resp.json()
142+
for assignment in user_data.get('results', []):
143+
# Handle both object_id and object_ansible_id
144+
object_id = assignment.get('object_id') or assignment.get('object_ansible_id')
145+
assignments.add(
146+
AssignmentTuple(
147+
actor_ansible_id=assignment['user_ansible_id'],
148+
object_id=object_id,
149+
role_definition_name=assignment['role_definition'],
150+
assignment_type='user',
151+
)
152+
)
153+
except Exception as e:
154+
logger.warning(f"Failed to fetch remote user assignments: {e}")
155+
156+
# Fetch team assignments
157+
try:
158+
team_resp = api_client.list_team_assignments()
159+
if team_resp.status_code == 200:
160+
team_data = team_resp.json()
161+
for assignment in team_data.get('results', []):
162+
# Handle both object_id and object_ansible_id
163+
object_id = assignment.get('object_id') or assignment.get('object_ansible_id')
164+
assignments.add(
165+
AssignmentTuple(
166+
actor_ansible_id=assignment['team_ansible_id'],
167+
object_id=object_id,
168+
role_definition_name=assignment['role_definition'],
169+
assignment_type='team',
170+
)
171+
)
172+
except Exception as e:
173+
logger.warning(f"Failed to fetch remote team assignments: {e}")
174+
175+
return assignments
176+
177+
178+
def get_local_assignments() -> set[AssignmentTuple]:
179+
"""Get local assignments and convert to tuples."""
180+
from ansible_base.rbac.models.role import RoleTeamAssignment, RoleUserAssignment
181+
182+
assignments = set()
183+
184+
# Get user assignments
185+
for assignment in RoleUserAssignment.objects.select_related('user', 'role_definition').all():
186+
try:
187+
user_resource = Resource.get_resource_for_object(assignment.user)
188+
user_ansible_id = user_resource.ansible_id
189+
190+
# Handle both object-scoped and global assignments
191+
object_id = assignment.object_id
192+
if object_id and assignment.content_type:
193+
# For object-scoped assignments, try to get the object's ansible_id
194+
try:
195+
object_resource = Resource.objects.filter(object_id=object_id, content_type=assignment.content_type).first()
196+
if object_resource:
197+
object_id = object_resource.ansible_id
198+
except Exception:
199+
# If we can't get ansible_id, keep the original object_id
200+
pass
201+
202+
assignments.add(
203+
AssignmentTuple(
204+
actor_ansible_id=str(user_ansible_id),
205+
object_id=str(object_id) if object_id else None,
206+
role_definition_name=assignment.role_definition.name,
207+
assignment_type='user',
208+
)
209+
)
210+
except (Resource.DoesNotExist, AttributeError):
211+
# Skip assignments where the user doesn't have a resource
212+
continue
213+
214+
# Get team assignments
215+
for assignment in RoleTeamAssignment.objects.select_related('team', 'role_definition').all():
216+
try:
217+
team_resource = Resource.get_resource_for_object(assignment.team)
218+
team_ansible_id = team_resource.ansible_id
219+
220+
# Handle both object-scoped and global assignments
221+
object_id = assignment.object_id
222+
if object_id and assignment.content_type:
223+
# For object-scoped assignments, try to get the object's ansible_id
224+
try:
225+
object_resource = Resource.objects.filter(object_id=object_id, content_type=assignment.content_type).first()
226+
if object_resource:
227+
object_id = object_resource.ansible_id
228+
except Exception:
229+
# If we can't get ansible_id, keep the original object_id
230+
pass
231+
232+
assignments.add(
233+
AssignmentTuple(
234+
actor_ansible_id=str(team_ansible_id),
235+
object_id=str(object_id) if object_id else None,
236+
role_definition_name=assignment.role_definition.name,
237+
assignment_type='team',
238+
)
239+
)
240+
except (Resource.DoesNotExist, AttributeError):
241+
# Skip assignments where the team doesn't have a resource
242+
continue
243+
244+
return assignments
245+
246+
247+
def delete_local_assignment(assignment_tuple: AssignmentTuple) -> bool:
248+
"""Delete a local assignment based on the tuple."""
249+
from ansible_base.rbac.models.role import RoleDefinition, RoleTeamAssignment, RoleUserAssignment
250+
251+
try:
252+
role_definition = RoleDefinition.objects.get(name=assignment_tuple.role_definition_name)
253+
254+
# Get the actor (user or team)
255+
resource = Resource.objects.get(ansible_id=assignment_tuple.actor_ansible_id)
256+
actor = resource.content_object
257+
258+
# Get the object if it's not a global assignment
259+
content_object = None
260+
if assignment_tuple.object_id:
261+
try:
262+
# Try to find by ansible_id first
263+
object_resource = Resource.objects.get(ansible_id=assignment_tuple.object_id)
264+
content_object = object_resource.content_object
265+
except Resource.DoesNotExist:
266+
# Fall back to finding by object_id (for cases where we stored the raw object_id)
267+
if assignment_tuple.assignment_type == 'user':
268+
assignment = RoleUserAssignment.objects.filter(user=actor, role_definition=role_definition, object_id=assignment_tuple.object_id).first()
269+
else:
270+
assignment = RoleTeamAssignment.objects.filter(team=actor, role_definition=role_definition, object_id=assignment_tuple.object_id).first()
271+
272+
if assignment:
273+
assignment.delete()
274+
return True
275+
return False
276+
277+
# Use the role definition's remove methods
278+
if content_object:
279+
role_definition.remove_permission(actor, content_object)
280+
else:
281+
role_definition.remove_global_permission(actor)
282+
283+
return True
284+
285+
except Exception as e:
286+
logger.error(f"Failed to delete assignment {assignment_tuple}: {e}")
287+
return False
288+
289+
290+
def create_local_assignment(assignment_tuple: AssignmentTuple) -> bool:
291+
"""Create a local assignment based on the tuple."""
292+
from ansible_base.rbac.models.role import RoleDefinition
293+
294+
try:
295+
role_definition = RoleDefinition.objects.get(name=assignment_tuple.role_definition_name)
296+
297+
# Get the actor (user or team)
298+
resource = Resource.objects.get(ansible_id=assignment_tuple.actor_ansible_id)
299+
actor = resource.content_object
300+
301+
# Get the object if it's not a global assignment
302+
content_object = None
303+
if assignment_tuple.object_id:
304+
object_resource = Resource.objects.get(ansible_id=assignment_tuple.object_id)
305+
content_object = object_resource.content_object
306+
307+
# Use the role definition's give methods
308+
if content_object:
309+
role_definition.give_permission(actor, content_object)
310+
else:
311+
role_definition.give_global_permission(actor)
312+
313+
return True
314+
315+
except Exception as e:
316+
logger.error(f"Failed to create assignment {assignment_tuple}: {e}")
317+
return False
318+
319+
110320
def get_orphan_resources(
111321
resource_type_name: str,
112322
manifest_list: list[ManifestItem],
@@ -330,6 +540,7 @@ class SyncExecutor:
330540
deleted_count: int = 0
331541
asyncio: bool = False
332542
results: dict = field(default_factory=lambda: defaultdict(list))
543+
sync_assignments: bool = True # New flag to enable/disable assignment sync
333544

334545
def write(self, text: str = ""):
335546
"""Write to assigned IO or simply ignores the text."""
@@ -449,6 +660,57 @@ def _dispatch_sync_process(self, manifest_list: list[ManifestItem]):
449660
self.write()
450661
self._process_manifest_list(manifest_list)
451662

663+
def _sync_assignments(self):
664+
"""Synchronize role assignments between local and remote systems."""
665+
if not self.sync_assignments:
666+
return
667+
668+
self.write(">>> Syncing role assignments")
669+
670+
try:
671+
# Get remote and local assignments
672+
remote_assignments = get_remote_assignments(self.api_client)
673+
local_assignments = get_local_assignments()
674+
675+
# Calculate differences
676+
to_delete = local_assignments - remote_assignments
677+
to_create = remote_assignments - local_assignments
678+
679+
deleted_count = 0
680+
created_count = 0
681+
error_count = 0
682+
683+
# Delete local assignments that don't exist remotely
684+
for assignment_tuple in to_delete:
685+
if delete_local_assignment(assignment_tuple):
686+
deleted_count += 1
687+
self.write(
688+
f"DELETED assignment {assignment_tuple.assignment_type} {assignment_tuple.actor_ansible_id} -> {assignment_tuple.role_definition_name} on {assignment_tuple.object_id or 'global'}"
689+
)
690+
else:
691+
error_count += 1
692+
693+
# Create local assignments that exist remotely but not locally
694+
for assignment_tuple in to_create:
695+
if create_local_assignment(assignment_tuple):
696+
created_count += 1
697+
self.write(
698+
f"CREATED assignment {assignment_tuple.assignment_type} {assignment_tuple.actor_ansible_id} -> {assignment_tuple.role_definition_name} on {assignment_tuple.object_id or 'global'}"
699+
)
700+
else:
701+
error_count += 1
702+
703+
self.write(f"Assignment sync completed: Created {created_count} | Deleted {deleted_count} | Errors {error_count}")
704+
705+
# Store results for reporting
706+
self.results["assignment_created"] = created_count
707+
self.results["assignment_deleted"] = deleted_count
708+
self.results["assignment_errors"] = error_count
709+
710+
except Exception as e:
711+
self.write(f"Assignment sync failed: {e}")
712+
logger.error(f"Assignment sync failed: {e}")
713+
452714
def run(self):
453715
"""Run the sync workflow.
454716
@@ -457,6 +719,7 @@ def run(self):
457719
3. Cleanup orphaned resources (deleted remotely).
458720
4. Process the sync for each item in the manifest.
459721
5. Handle retries.
722+
6. Sync role assignments.
460723
"""
461724
self.write("----- RESOURCE SYNC STARTED -----")
462725
self.write()
@@ -479,4 +742,8 @@ def run(self):
479742

480743
self.write()
481744

745+
# Sync assignments after all resources are synced
746+
self._sync_assignments()
747+
self.write()
748+
482749
self.write("----- RESOURCE SYNC FINISHED -----")

0 commit comments

Comments
 (0)