src-auth-perms-sync 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. src_auth_perms_sync/__init__.py +1 -0
  2. src_auth_perms_sync/__main__.py +6 -0
  3. src_auth_perms_sync/cli.py +646 -0
  4. src_auth_perms_sync/orgs/__init__.py +1 -0
  5. src_auth_perms_sync/orgs/command.py +7 -0
  6. src_auth_perms_sync/orgs/queries.py +44 -0
  7. src_auth_perms_sync/orgs/sync.py +1167 -0
  8. src_auth_perms_sync/orgs/types.py +103 -0
  9. src_auth_perms_sync/permissions/__init__.py +1 -0
  10. src_auth_perms_sync/permissions/apply.py +420 -0
  11. src_auth_perms_sync/permissions/command.py +918 -0
  12. src_auth_perms_sync/permissions/full_set.py +880 -0
  13. src_auth_perms_sync/permissions/mapping.py +627 -0
  14. src_auth_perms_sync/permissions/maps.py +291 -0
  15. src_auth_perms_sync/permissions/queries.py +180 -0
  16. src_auth_perms_sync/permissions/restore.py +913 -0
  17. src_auth_perms_sync/permissions/snapshot.py +1502 -0
  18. src_auth_perms_sync/permissions/sourcegraph.py +392 -0
  19. src_auth_perms_sync/permissions/types.py +116 -0
  20. src_auth_perms_sync/permissions/workflow.py +526 -0
  21. src_auth_perms_sync/shared/__init__.py +1 -0
  22. src_auth_perms_sync/shared/backups.py +119 -0
  23. src_auth_perms_sync/shared/id_codec.py +67 -0
  24. src_auth_perms_sync/shared/queries.py +65 -0
  25. src_auth_perms_sync/shared/run_context.py +34 -0
  26. src_auth_perms_sync/shared/saml_groups.py +267 -0
  27. src_auth_perms_sync/shared/site_config.py +366 -0
  28. src_auth_perms_sync/shared/sourcegraph.py +69 -0
  29. src_auth_perms_sync/shared/types.py +69 -0
  30. src_auth_perms_sync-0.2.1.dist-info/METADATA +256 -0
  31. src_auth_perms_sync-0.2.1.dist-info/RECORD +34 -0
  32. src_auth_perms_sync-0.2.1.dist-info/WHEEL +4 -0
  33. src_auth_perms_sync-0.2.1.dist-info/entry_points.txt +2 -0
  34. src_auth_perms_sync-0.2.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ """Project package for src-auth-perms-sync."""
@@ -0,0 +1,6 @@
1
+ """Entry point so `python -m src_auth_perms_sync ...` runs the CLI."""
2
+
3
+ from . import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli.main()
@@ -0,0 +1,646 @@
1
+ """
2
+ src-auth-perms-sync uses metadata from auth providers to set:
3
+ - Explicit repo permissions
4
+ - Organizations and memberships
5
+
6
+ See https://github.com/sourcegraph/src-auth-perms-sync/blob/main/README.md for usage instructions
7
+
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import secrets
15
+ import sys
16
+ from collections.abc import Mapping
17
+ from concurrent.futures import ThreadPoolExecutor
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Literal, NoReturn, TypeAlias
21
+
22
+ import src_py_lib as src
23
+
24
+ from .orgs import command as organizations_command
25
+ from .permissions import command as permissions_command
26
+ from .permissions import maps as permissions_maps
27
+ from .permissions import types as permission_types
28
+ from .shared import backups, run_context, site_config
29
+
30
+ log = logging.getLogger(__name__)
31
+
32
+ CommandName: TypeAlias = Literal["get", "set", "restore", "sync_saml_orgs"]
33
+ LogCommandName: TypeAlias = Literal[
34
+ "get",
35
+ "set_full",
36
+ "set_user",
37
+ "set_users_without_explicit_perms",
38
+ "restore",
39
+ "sync_saml_orgs",
40
+ "get_sync_saml_orgs",
41
+ "set_full_sync_saml_orgs",
42
+ "set_user_sync_saml_orgs",
43
+ "set_users_without_explicit_perms_sync_saml_orgs",
44
+ ]
45
+
46
+ SET_COMMAND_LOG_NAMES: dict[permission_types.SetCommandMode, LogCommandName] = {
47
+ "full": "set_full",
48
+ "user": "set_user",
49
+ "users_without_explicit_perms": "set_users_without_explicit_perms",
50
+ }
51
+ SET_COMMAND_ARTIFACT_NAMES: dict[permission_types.SetCommandMode, str] = {
52
+ "full": "set-{run_mode}",
53
+ "user": "set-add-user-{run_mode}",
54
+ "users_without_explicit_perms": "set-add-users-without-explicit-perms-{run_mode}",
55
+ }
56
+ SYNC_SET_COMMAND_LOG_NAMES: dict[permission_types.SetCommandMode, LogCommandName] = {
57
+ "full": "set_full_sync_saml_orgs",
58
+ "user": "set_user_sync_saml_orgs",
59
+ "users_without_explicit_perms": "set_users_without_explicit_perms_sync_saml_orgs",
60
+ }
61
+ SYNC_SET_COMMAND_ARTIFACT_NAMES: dict[permission_types.SetCommandMode, str] = {
62
+ "full": "set-sync-saml-orgs-{run_mode}",
63
+ "user": "set-add-user-sync-saml-orgs-{run_mode}",
64
+ "users_without_explicit_perms": (
65
+ "set-add-users-without-explicit-perms-sync-saml-orgs-{run_mode}"
66
+ ),
67
+ }
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ResolvedCommand:
72
+ """Validated command facts derived from operator config."""
73
+
74
+ name: CommandName
75
+ log_name: LogCommandName
76
+ artifact_name: str
77
+ set_options: permission_types.SetCommandOptions | None = None
78
+ sync_saml_organizations: bool = False
79
+
80
+ @property
81
+ def set_mode(self) -> permission_types.SetCommandMode | None:
82
+ """Return the concrete `--set` mode when this is a set command."""
83
+ if self.set_options is None:
84
+ return None
85
+ return self.set_options.mode
86
+
87
+
88
+ class SrcAuthPermissionsSyncConfig(src.SourcegraphClientConfig, src.LoggingConfig):
89
+ """Config values loaded from defaults, .env, environment, and CLI flags."""
90
+
91
+ get: bool = src.config_field(
92
+ default=False,
93
+ env_var="SRC_AUTH_PERMS_SYNC_GET",
94
+ cli_flag="--get",
95
+ cli_action="store_true",
96
+ help="Query the SG instance and write/refresh auth-providers.yaml and code-hosts.yaml",
97
+ )
98
+ set_path: Path | None = src.config_field(
99
+ default=None,
100
+ env_var="SRC_AUTH_PERMS_SYNC_SET",
101
+ cli_flag="--set",
102
+ cli_nargs="?",
103
+ cli_const="maps.yaml",
104
+ metavar="FILE",
105
+ help=(
106
+ "Read the YAML config file and execute the mapping rules.\n"
107
+ "Defaults to maps.yaml under src-auth-perms-sync-runs/<endpoint>/.\n"
108
+ "Relative paths are resolved from that path."
109
+ ),
110
+ )
111
+ full: bool = src.config_field(
112
+ default=False,
113
+ env_var="SRC_AUTH_PERMS_SYNC_FULL",
114
+ cli_flag="--full",
115
+ cli_action="store_true",
116
+ help="With --set: run the full overwrite reconciliation mode (default)",
117
+ )
118
+ user: str | None = src.config_field(
119
+ default=None,
120
+ env_var="SRC_AUTH_PERMS_SYNC_USER",
121
+ cli_flag="--user",
122
+ metavar="USER",
123
+ help="Process a specific Sourcegraph user by username or email address",
124
+ )
125
+ users_without_explicit_perms: bool = src.config_field(
126
+ default=False,
127
+ env_var="SRC_AUTH_PERMS_SYNC_USERS_WITHOUT_EXPLICIT_PERMS",
128
+ cli_flag="--users-without-explicit-perms",
129
+ cli_action="store_true",
130
+ help="Process Sourcegraph users without explicit permissions",
131
+ )
132
+ created_after: str | None = src.config_field(
133
+ default=None,
134
+ env_var="SRC_AUTH_PERMS_SYNC_CREATED_AFTER",
135
+ cli_flag="--created-after",
136
+ metavar="YYYY-MM-DD",
137
+ pattern=r"^\d{4}-\d{2}-\d{2}$",
138
+ help="Process Sourcegraph users created on or after this date",
139
+ )
140
+ restore_path: Path | None = src.config_field(
141
+ default=None,
142
+ env_var="SRC_AUTH_PERMS_SYNC_RESTORE",
143
+ cli_flag="--restore",
144
+ metavar="FILE",
145
+ help=(
146
+ "Restore explicit-permissions state to match the given snapshot JSON file.\n"
147
+ "Relative paths are resolved under 'src-auth-perms-sync-runs/<endpoint>/.'"
148
+ ),
149
+ )
150
+ sync_saml_organizations: bool = src.config_field(
151
+ default=False,
152
+ env_var="SRC_AUTH_PERMS_SYNC_SYNC_SAML_ORGS",
153
+ cli_flag="--sync-saml-orgs",
154
+ cli_action="store_true",
155
+ help="Create/update Sourcegraph organizations for each discovered SAML group",
156
+ )
157
+ apply: bool = src.config_field(
158
+ default=False,
159
+ env_var="SRC_AUTH_PERMS_SYNC_APPLY",
160
+ cli_flag="--apply",
161
+ cli_action="store_true",
162
+ help="With mutating commands: actually mutate state. Default is dry-run",
163
+ )
164
+ no_backup: bool = src.config_field(
165
+ default=False,
166
+ env_var="SRC_AUTH_PERMS_SYNC_NO_BACKUP",
167
+ cli_flag="--no-backup",
168
+ cli_action="store_true",
169
+ help="With mutating commands: skip before/after snapshots and validation",
170
+ )
171
+ parallelism: int = src.config_field(
172
+ default=16,
173
+ env_var="SRC_AUTH_PERMS_SYNC_PARALLELISM",
174
+ cli_flag="--parallelism",
175
+ metavar="N",
176
+ ge=1,
177
+ help="Concurrent Sourcegraph API worker threads (default: 16)",
178
+ )
179
+ explicit_permissions_batch_size: int = src.config_field(
180
+ default=25,
181
+ env_var="SRC_AUTH_PERMS_SYNC_EXPLICIT_PERMISSIONS_BATCH_SIZE",
182
+ cli_flag="--explicit-permissions-batch-size",
183
+ metavar="N",
184
+ ge=1,
185
+ help=(
186
+ "Users per GraphQL request when capturing explicit repository permissions (default: 25)"
187
+ ),
188
+ )
189
+ max_attempts: int = src.config_field(
190
+ default=5,
191
+ env_var="SRC_AUTH_PERMS_SYNC_MAX_ATTEMPTS",
192
+ cli_flag="--max-attempts",
193
+ metavar="N",
194
+ ge=1,
195
+ help="Max attempts per HTTP request before giving up (default: 5)",
196
+ )
197
+ sample_interval: float = src.config_field(
198
+ default=10.0,
199
+ env_var="SRC_AUTH_PERMS_SYNC_SAMPLE_INTERVAL",
200
+ cli_flag="--sample-interval",
201
+ metavar="SECONDS",
202
+ ge=0,
203
+ help="Seconds between logging compute resource samples; set 0 to disable (default: 10)",
204
+ )
205
+ trace: bool = src.config_field(
206
+ default=False,
207
+ env_var="SRC_AUTH_PERMS_SYNC_TRACE",
208
+ cli_flag="--trace",
209
+ cli_action="store_true",
210
+ help=(
211
+ "Force Sourcegraph trace sampling by sending a sampled traceparent "
212
+ "header on each HTTP request"
213
+ ),
214
+ )
215
+
216
+
217
+ def config_error(message: str) -> NoReturn:
218
+ """Exit with a concise config/argument error."""
219
+ print(f"src-auth-perms-sync: error: {message}", file=sys.stderr)
220
+ raise SystemExit(2)
221
+
222
+
223
+ def validate_config(config: SrcAuthPermissionsSyncConfig) -> None:
224
+ """Validate cross-field CLI/config constraints."""
225
+ validate_command_selection(config)
226
+ validate_user_filter_selection(config)
227
+ validate_set_mode_selection(config)
228
+
229
+
230
+ def validate_command_selection(config: SrcAuthPermissionsSyncConfig) -> None:
231
+ """Validate compatible top-level command flags."""
232
+ if sum((config.get, config.set_path is not None, config.restore_path is not None)) > 1:
233
+ config_error("choose only one of --get, --set, or --restore")
234
+ if config.restore_path is not None and config.sync_saml_organizations:
235
+ config_error("--sync-saml-orgs can run by itself or with --get or --set")
236
+
237
+
238
+ def validate_user_filter_selection(config: SrcAuthPermissionsSyncConfig) -> None:
239
+ """Validate user-scope filters and their compatible commands."""
240
+ user_identifier_filters = sum((config.user is not None, config.users_without_explicit_perms))
241
+ if user_identifier_filters > 1:
242
+ config_error("choose only one of --user or --users-without-explicit-perms")
243
+
244
+ user_filter_selected = user_identifier_filters > 0 or config.created_after is not None
245
+ user_filter_allowed = (
246
+ config.get
247
+ or config.set_path is not None
248
+ or (config.restore_path is None and not config.sync_saml_organizations)
249
+ )
250
+ if user_filter_selected and not user_filter_allowed:
251
+ config_error(
252
+ "--user, --users-without-explicit-perms, and --created-after require --get or --set"
253
+ )
254
+
255
+
256
+ def validate_set_mode_selection(config: SrcAuthPermissionsSyncConfig) -> None:
257
+ """Validate `--set` mode flags."""
258
+ if config.full and config.set_path is None:
259
+ config_error("--full requires --set")
260
+
261
+ if config.set_path is None:
262
+ return
263
+
264
+ if sum((config.full, config.user is not None, config.users_without_explicit_perms)) > 1:
265
+ config_error(
266
+ "with --set, choose at most one of --full, --user, or --users-without-explicit-perms"
267
+ )
268
+
269
+
270
+ def set_command_options(config: SrcAuthPermissionsSyncConfig) -> permission_types.SetCommandOptions:
271
+ """Return the validated `--set` mode options."""
272
+ if config.user is not None:
273
+ return permission_types.SetCommandOptions(
274
+ mode="user",
275
+ user_identifier=config.user,
276
+ user_created_after=config.created_after,
277
+ )
278
+ if config.users_without_explicit_perms:
279
+ return permission_types.SetCommandOptions(
280
+ mode="users_without_explicit_perms",
281
+ user_created_after=config.created_after,
282
+ )
283
+ return permission_types.SetCommandOptions(
284
+ mode="full",
285
+ user_created_after=config.created_after,
286
+ )
287
+
288
+
289
+ def resolve_command(config: SrcAuthPermissionsSyncConfig) -> ResolvedCommand:
290
+ """Return the command execution plan derived from config."""
291
+ run_mode = "apply" if config.apply else "dry-run"
292
+ if config.set_path is not None:
293
+ return resolve_set_command(config, run_mode)
294
+ if config.restore_path is not None:
295
+ return ResolvedCommand(
296
+ name="restore",
297
+ log_name="restore",
298
+ artifact_name=f"restore-{run_mode}",
299
+ )
300
+ if config.get and config.sync_saml_organizations:
301
+ return ResolvedCommand(
302
+ name="get",
303
+ log_name="get_sync_saml_orgs",
304
+ artifact_name=f"get-sync-saml-orgs-{run_mode}",
305
+ sync_saml_organizations=True,
306
+ )
307
+ if config.get:
308
+ return ResolvedCommand(name="get", log_name="get", artifact_name="get")
309
+ if config.sync_saml_organizations:
310
+ return ResolvedCommand(
311
+ name="sync_saml_orgs",
312
+ log_name="sync_saml_orgs",
313
+ artifact_name=f"sync-saml-orgs-{run_mode}",
314
+ sync_saml_organizations=True,
315
+ )
316
+ return ResolvedCommand(name="get", log_name="get", artifact_name="get")
317
+
318
+
319
+ def resolve_set_command(config: SrcAuthPermissionsSyncConfig, run_mode: str) -> ResolvedCommand:
320
+ """Return resolved metadata for the selected `--set` command mode."""
321
+ set_options = set_command_options(config)
322
+ log_names = (
323
+ SYNC_SET_COMMAND_LOG_NAMES if config.sync_saml_organizations else SET_COMMAND_LOG_NAMES
324
+ )
325
+ artifact_names = (
326
+ SYNC_SET_COMMAND_ARTIFACT_NAMES
327
+ if config.sync_saml_organizations
328
+ else SET_COMMAND_ARTIFACT_NAMES
329
+ )
330
+ return ResolvedCommand(
331
+ name="set",
332
+ log_name=log_names[set_options.mode],
333
+ artifact_name=artifact_names[set_options.mode].format(run_mode=run_mode),
334
+ set_options=set_options,
335
+ sync_saml_organizations=config.sync_saml_organizations,
336
+ )
337
+
338
+
339
+ def load_config() -> SrcAuthPermissionsSyncConfig:
340
+ """Parse and validate CLI/environment config."""
341
+ config = src.parse_args(
342
+ SrcAuthPermissionsSyncConfig,
343
+ description=__doc__,
344
+ base_dir=Path("."),
345
+ )
346
+ validate_config(config)
347
+ return config
348
+
349
+
350
+ def endpoint_scoped_config(
351
+ config: SrcAuthPermissionsSyncConfig, endpoint: str
352
+ ) -> SrcAuthPermissionsSyncConfig:
353
+ """Return config with relative operator artifact paths scoped to this endpoint."""
354
+ updates: dict[str, object] = {}
355
+ if config.set_path is not None:
356
+ updates["set_path"] = backups.endpoint_artifact_path(endpoint, config.set_path)
357
+ if config.restore_path is not None:
358
+ updates["restore_path"] = backups.endpoint_artifact_path(endpoint, config.restore_path)
359
+ if not updates:
360
+ return config
361
+ return config.model_copy(update=updates)
362
+
363
+
364
+ def require_set_input_file(config: SrcAuthPermissionsSyncConfig) -> None:
365
+ """Exit with a clear error if the selected maps file is missing."""
366
+ if config.set_path is None:
367
+ return
368
+ if config.set_path.is_file():
369
+ return
370
+ if config.set_path.exists():
371
+ raise SystemExit(f"--set input path is not a file: {config.set_path}")
372
+ raise SystemExit(
373
+ "--set input file does not exist: "
374
+ f"{config.set_path}\n"
375
+ "Run `uv run src-auth-perms-sync --get` to create the default maps.yaml, "
376
+ "or pass a path to an existing maps file."
377
+ )
378
+
379
+
380
+ def run_fields(
381
+ config: SrcAuthPermissionsSyncConfig, command: ResolvedCommand, endpoint: str
382
+ ) -> dict[str, object]:
383
+ """Return run-level fields for structured logging."""
384
+ return {
385
+ "cli_cmd": command.log_name,
386
+ "base_cmd": command.name,
387
+ "set_mode": command.set_mode,
388
+ "sync_saml_orgs_flag": command.sync_saml_organizations,
389
+ "apply_flag": config.apply,
390
+ "endpoint": endpoint,
391
+ "parallelism": config.parallelism,
392
+ "explicit_permissions_batch_size": config.explicit_permissions_batch_size,
393
+ "trace": config.trace,
394
+ "max_attempts": config.max_attempts,
395
+ "no_backup": config.no_backup,
396
+ "sample_interval": config.sample_interval,
397
+ "user_created_after": config.created_after,
398
+ "artifacts_dir": str(backups.endpoint_artifacts_directory(endpoint)),
399
+ "python_version": sys.version.split()[0],
400
+ "pid": os.getpid(),
401
+ }
402
+
403
+
404
+ class TraceSamplingHTTPClient(src.HTTPClient):
405
+ """HTTP client that asks Sourcegraph to retain Jaeger traces for every request."""
406
+
407
+ def request(
408
+ self,
409
+ method: str,
410
+ url: str,
411
+ *,
412
+ headers: Mapping[str, str] | None = None,
413
+ query: Mapping[str, str | int | float | bool | None] | None = None,
414
+ json_body: object | None = None,
415
+ data: bytes | None = None,
416
+ ) -> bytes:
417
+ request_headers = dict(headers or {})
418
+ if not any(name.lower() == "traceparent" for name in request_headers):
419
+ request_headers["traceparent"] = sampled_traceparent()
420
+ return super().request(
421
+ method,
422
+ url,
423
+ headers=request_headers,
424
+ query=query,
425
+ json_body=json_body,
426
+ data=data,
427
+ )
428
+
429
+
430
+ def sampled_traceparent() -> str:
431
+ """Return a W3C traceparent header value with the sampled flag set."""
432
+ return f"00-{nonzero_hex(16)}-{nonzero_hex(8)}-01"
433
+
434
+
435
+ def nonzero_hex(byte_count: int) -> str:
436
+ """Return a random hex string that is not all zeroes."""
437
+ while True:
438
+ value = secrets.token_hex(byte_count)
439
+ if any(character != "0" for character in value):
440
+ return value
441
+
442
+
443
+ def run_with_client(
444
+ config: SrcAuthPermissionsSyncConfig,
445
+ command: ResolvedCommand,
446
+ endpoint: str,
447
+ worker_pool: ThreadPoolExecutor,
448
+ ) -> None:
449
+ """Create a client, run the selected command, and always close HTTP resources."""
450
+ http_class = TraceSamplingHTTPClient if config.trace else src.HTTPClient
451
+ http = http_class(
452
+ user_agent="src-auth-perms-sync/0.1 (+python)",
453
+ max_attempts=config.max_attempts,
454
+ max_connections=config.parallelism,
455
+ )
456
+ client = src.SourcegraphClient(endpoint=endpoint, token=config.src_access_token, http=http)
457
+ try:
458
+ run_command(config, command, client, worker_pool)
459
+ finally:
460
+ client.http.close()
461
+
462
+
463
+ def run_command(
464
+ config: SrcAuthPermissionsSyncConfig,
465
+ command: ResolvedCommand,
466
+ client: src.SourcegraphClient,
467
+ worker_pool: ThreadPoolExecutor,
468
+ ) -> None:
469
+ """Dispatch the selected command."""
470
+ sourcegraph_site_config = site_config.validate_site_config(client)
471
+ command_data = run_context.CommandData()
472
+ if command.name == "get":
473
+ command_data = run_get(config, client, sourcegraph_site_config, worker_pool)
474
+ elif command.name == "set":
475
+ command_data = run_set(config, command, client, sourcegraph_site_config, worker_pool)
476
+ elif command.name == "restore":
477
+ run_restore(config, client, sourcegraph_site_config, worker_pool)
478
+ else:
479
+ run_sync_saml_organizations(
480
+ config,
481
+ client,
482
+ sourcegraph_site_config,
483
+ command_data,
484
+ worker_pool,
485
+ )
486
+ return
487
+
488
+ if command.sync_saml_organizations:
489
+ run_sync_saml_organizations(
490
+ config,
491
+ client,
492
+ sourcegraph_site_config,
493
+ command_data,
494
+ worker_pool,
495
+ )
496
+
497
+
498
+ def run_set(
499
+ config: SrcAuthPermissionsSyncConfig,
500
+ command: ResolvedCommand,
501
+ client: src.SourcegraphClient,
502
+ sourcegraph_site_config: site_config.SiteConfig,
503
+ worker_pool: ThreadPoolExecutor,
504
+ ) -> run_context.CommandData:
505
+ """Run the selected repo-permission sync command."""
506
+ assert config.set_path is not None
507
+ assert command.set_options is not None
508
+ require_set_input_file(config)
509
+ return permissions_command.cmd_set(
510
+ client,
511
+ config.set_path,
512
+ command.set_options,
513
+ dry_run=not config.apply,
514
+ parallelism=config.parallelism,
515
+ explicit_permissions_batch_size=config.explicit_permissions_batch_size,
516
+ bind_id_mode=sourcegraph_site_config.bind_id_mode,
517
+ saml_groups_attribute_name_by_config_id=(
518
+ sourcegraph_site_config.saml_groups_attribute_name_by_config_id
519
+ ),
520
+ do_backup=not config.no_backup,
521
+ retain_saml_group_users=command.sync_saml_organizations,
522
+ worker_pool=worker_pool,
523
+ )
524
+
525
+
526
+ def run_restore(
527
+ config: SrcAuthPermissionsSyncConfig,
528
+ client: src.SourcegraphClient,
529
+ sourcegraph_site_config: site_config.SiteConfig,
530
+ worker_pool: ThreadPoolExecutor,
531
+ ) -> None:
532
+ """Run the selected repo-permission restore command."""
533
+ assert config.restore_path is not None
534
+ permissions_command.cmd_restore(
535
+ client,
536
+ config.restore_path,
537
+ dry_run=not config.apply,
538
+ parallelism=config.parallelism,
539
+ explicit_permissions_batch_size=config.explicit_permissions_batch_size,
540
+ bind_id_mode=sourcegraph_site_config.bind_id_mode,
541
+ do_backup=not config.no_backup,
542
+ worker_pool=worker_pool,
543
+ )
544
+
545
+
546
+ def run_sync_saml_organizations(
547
+ config: SrcAuthPermissionsSyncConfig,
548
+ client: src.SourcegraphClient,
549
+ sourcegraph_site_config: site_config.SiteConfig,
550
+ command_data: run_context.CommandData,
551
+ worker_pool: ThreadPoolExecutor,
552
+ ) -> None:
553
+ """Run the selected SAML organization sync command."""
554
+ organizations_command.cmd_sync_saml_organizations(
555
+ client,
556
+ dry_run=not config.apply,
557
+ parallelism=config.parallelism,
558
+ saml_groups_attribute_name_by_config_id=(
559
+ sourcegraph_site_config.saml_groups_attribute_name_by_config_id
560
+ ),
561
+ do_backup=not config.no_backup,
562
+ command_data=command_data,
563
+ worker_pool=worker_pool,
564
+ )
565
+
566
+
567
+ def run_get(
568
+ config: SrcAuthPermissionsSyncConfig,
569
+ client: src.SourcegraphClient,
570
+ sourcegraph_site_config: site_config.SiteConfig,
571
+ worker_pool: ThreadPoolExecutor,
572
+ ) -> run_context.CommandData:
573
+ """Run the default read-only discovery command."""
574
+ artifacts_directory = backups.endpoint_artifacts_directory(client.endpoint)
575
+ maps_path = artifacts_directory / "maps.yaml"
576
+ maps_created = permissions_maps.create_maps_yaml_if_missing(maps_path)
577
+ if maps_created:
578
+ log.info("maps.yaml missing, created %s with an empty maps list.", maps_path)
579
+ else:
580
+ log.info("Left existing %s unchanged.", maps_path)
581
+
582
+ return permissions_command.cmd_get(
583
+ client,
584
+ artifacts_directory / "code-hosts.yaml",
585
+ artifacts_directory / "auth-providers.yaml",
586
+ maps_path,
587
+ user_identifier=config.user,
588
+ users_without_explicit_perms=config.users_without_explicit_perms,
589
+ user_created_after=config.created_after,
590
+ parallelism=config.parallelism,
591
+ explicit_permissions_batch_size=config.explicit_permissions_batch_size,
592
+ bind_id_mode=sourcegraph_site_config.bind_id_mode,
593
+ saml_groups_attribute_name_by_config_id=(
594
+ sourcegraph_site_config.saml_groups_attribute_name_by_config_id
595
+ ),
596
+ auth_providers_by_config_id=sourcegraph_site_config.auth_providers_by_config_id,
597
+ retain_saml_group_users=config.sync_saml_organizations,
598
+ worker_pool=worker_pool,
599
+ )
600
+
601
+
602
+ def reraise_system_exit_with_logged_error(exception: SystemExit) -> NoReturn:
603
+ """Log string SystemExit messages inside the structured logging context."""
604
+ if isinstance(exception.code, str):
605
+ log.error("%s", exception.code)
606
+ raise SystemExit(1) from exception
607
+ raise exception
608
+
609
+
610
+ def main() -> None:
611
+ config = load_config()
612
+ command = resolve_command(config)
613
+ try:
614
+ endpoint = src.normalize_sourcegraph_endpoint(config.src_endpoint)
615
+ except ValueError as error:
616
+ config_error(str(error))
617
+ config = endpoint_scoped_config(config, endpoint)
618
+ run_timestamp = backups.backup_timestamp()
619
+ run_directory = backups.artifact_run_directory(
620
+ run_timestamp,
621
+ endpoint,
622
+ command.artifact_name,
623
+ )
624
+
625
+ logging_settings = src.logging_settings_from_config(
626
+ config,
627
+ log_file=backups.run_log_path(run_directory),
628
+ logs_dir=None,
629
+ resource_sample_interval_seconds=config.sample_interval,
630
+ )
631
+
632
+ with (
633
+ backups.run_artifacts_context(run_directory, run_timestamp),
634
+ src.logging(
635
+ config,
636
+ command=command.log_name,
637
+ git_cwd=__file__,
638
+ logging_config=logging_settings,
639
+ run_fields=run_fields(config, command, endpoint),
640
+ ),
641
+ run_context.thread_pool(config.parallelism) as worker_pool,
642
+ ):
643
+ try:
644
+ run_with_client(config, command, endpoint, worker_pool)
645
+ except SystemExit as exception:
646
+ reraise_system_exit_with_logged_error(exception)
@@ -0,0 +1 @@
1
+ """Sourcegraph organization sync workflow."""
@@ -0,0 +1,7 @@
1
+ """Sourcegraph organization sync command facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .sync import cmd_sync_saml_organizations
6
+
7
+ __all__ = ["cmd_sync_saml_organizations"]