src-auth-perms-sync 0.3.0__tar.gz → 0.3.1__tar.gz

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 (70) hide show
  1. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/PKG-INFO +1 -1
  2. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/TODO.md +0 -16
  3. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/memory-efficiency-generate.py +1 -1
  4. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/memory-efficiency.md +40 -1
  5. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/test-end-to-end.py +9 -4
  6. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/cli.py +154 -16
  7. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/orgs/sync.py +129 -109
  8. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/apply.py +119 -149
  9. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/command.py +736 -110
  10. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/full_set.py +180 -13
  11. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/mapping.py +29 -0
  12. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/maps.py +1 -1
  13. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/queries.py +95 -26
  14. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/restore.py +13 -12
  15. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/snapshot.py +261 -164
  16. src_auth_perms_sync-0.3.1/src/src_auth_perms_sync/permissions/sourcegraph.py +897 -0
  17. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/types.py +6 -0
  18. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/workflow.py +129 -6
  19. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/queries.py +17 -9
  20. src_auth_perms_sync-0.3.1/src/src_auth_perms_sync/shared/run_context.py +265 -0
  21. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/saml_groups.py +2 -2
  22. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/site_config.py +10 -7
  23. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/sourcegraph.py +10 -2
  24. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/integration/test_cli_entrypoint.py +1 -1
  25. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_cli_config.py +214 -13
  26. src_auth_perms_sync-0.3.1/tests/unit/test_command_additive.py +280 -0
  27. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_maps.py +72 -0
  28. src_auth_perms_sync-0.3.1/tests/unit/test_permissions_sourcegraph.py +272 -0
  29. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_restore.py +4 -8
  30. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_snapshot.py +84 -18
  31. src_auth_perms_sync-0.3.0/src/src_auth_perms_sync/permissions/sourcegraph.py +0 -405
  32. src_auth_perms_sync-0.3.0/src/src_auth_perms_sync/shared/run_context.py +0 -34
  33. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.env.example +0 -0
  34. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.github/CODEOWNERS +0 -0
  35. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.github/workflows/ci.yml +0 -0
  36. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.github/workflows/release.yml +0 -0
  37. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.github/workflows/validate.yml +0 -0
  38. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.gitignore +0 -0
  39. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.markdownlint-cli2.yaml +0 -0
  40. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/.python-version +0 -0
  41. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/AGENTS.md +0 -0
  42. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/LICENSE +0 -0
  43. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/README.md +0 -0
  44. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/SECURITY.md +0 -0
  45. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/audit-dead-code.md +0 -0
  46. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/hooks/pre-commit +0 -0
  47. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/mapping-efficiency.md +0 -0
  48. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/memory-efficiency-analyze.py +0 -0
  49. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/memory-efficiency-monitor-sourcegraph.sh +0 -0
  50. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/dev/update-python-versions.md +0 -0
  51. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/maps-example.yaml +0 -0
  52. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/pyproject.toml +0 -0
  53. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/renovate.json +0 -0
  54. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/__init__.py +0 -0
  55. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/__main__.py +0 -0
  56. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/orgs/__init__.py +0 -0
  57. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/orgs/command.py +0 -0
  58. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/orgs/queries.py +0 -0
  59. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/orgs/types.py +0 -0
  60. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/permissions/__init__.py +0 -0
  61. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/__init__.py +0 -0
  62. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/backups.py +0 -0
  63. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/src/src_auth_perms_sync/shared/types.py +0 -0
  64. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/__init__.py +0 -0
  65. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/integration/__init__.py +0 -0
  66. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/__init__.py +0 -0
  67. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_apply.py +0 -0
  68. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_backups.py +0 -0
  69. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/tests/unit/test_saml_groups.py +0 -0
  70. {src_auth_perms_sync-0.3.0 → src_auth_perms_sync-0.3.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: src-auth-perms-sync
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Set Sourcegraph permissions from authentication provider data
5
5
  Project-URL: Homepage, https://github.com/sourcegraph/src-auth-perms-sync
6
6
  Project-URL: Issues, https://github.com/sourcegraph/src-auth-perms-sync/issues
@@ -1,21 +1,5 @@
1
1
  # TODO
2
2
 
3
- ## High priority: Sync modes
4
-
5
- ### Fast
6
-
7
- - Additive modes, to add new users’ perms quickly,
8
- without the extraneous load on the database of a full sync
9
- - Take a list of usernames and/or email addresses as input,
10
- query users on the instance for these,
11
- then trigger a perms sync for found users
12
- - Query the instance for all new users, which do not yet have explicit perms
13
- - Query the instance for all new repos, which do not yet have explicit perms
14
-
15
- ### Full: Overwrite all perms
16
-
17
- - Separate full sync mode with an arg
18
-
19
3
  ## High priority: Remote trigger on demand
20
4
 
21
5
  - Sourcegraph webhook for new user coming in v7.4.0
@@ -551,7 +551,7 @@ def list_external_services(client: src.SourcegraphClient) -> list[ExternalServic
551
551
  )
552
552
  )
553
553
  if not services:
554
- raise SystemExit("No external services found on the Sourcegraph instance")
554
+ raise SystemExit("No code hosts found on the Sourcegraph instance")
555
555
  return services
556
556
 
557
557
 
@@ -298,6 +298,39 @@ Important requirements:
298
298
  Expected benefit: replace hundreds or thousands of per-repo resolver SQL spans
299
299
  per request with one indexed `user_repo_permissions` join per user batch.
300
300
 
301
+ The `get --users-without-explicit-perms` path also needs a cheaper presence
302
+ check. Today it has to ask
303
+ `User.permissionsInfo.repositories(source: API, first: 1)` for every candidate
304
+ user, in aliased batches. Recent test runs show the client can parallelize
305
+ those batches, but the Sourcegraph frontend / load balancer can still return
306
+ 502/503s under that resolver load. Add one or both direct APIs:
307
+
308
+ ```graphql
309
+ type ExplicitRepositoryPermissionPresence {
310
+ userID: ID!
311
+ hasExplicitRepositoryPermissions: Boolean!
312
+ }
313
+
314
+ extend type Query {
315
+ explicitRepositoryPermissionPresenceForUsers(
316
+ userIDs: [ID!]!
317
+ source: PermissionSource = API
318
+ ): [ExplicitRepositoryPermissionPresence!]!
319
+
320
+ usersWithoutExplicitRepositoryPermissions(
321
+ createdAt: DateTimeFilter
322
+ source: PermissionSource = API
323
+ first: Int
324
+ after: String
325
+ ): UserConnection!
326
+ }
327
+ ```
328
+
329
+ Expected benefit: `src-auth-perms-sync get --users-without-explicit-perms`
330
+ can either check explicit-permission presence for candidate users in one indexed
331
+ batch query, or ask Sourcegraph for the filtered user set directly instead of
332
+ probing every user through the expensive permissions connection resolver.
333
+
301
334
  The stress profile also needs attention on the write path. A purpose-built
302
335
  bulk overwrite API that accepts many repo/user edges at once, streams or stages
303
336
  the input server-side, and avoids repeated per-repo permission reconciliation
@@ -324,7 +357,10 @@ Request: add a bulk explicit-permissions read API that accepts many user IDs and
324
357
  returns compact permission edges (`userID`, `repositoryID`, `repositoryName`,
325
358
  `updatedAt`) for `source: API`, without resolving full `Repository` GraphQL
326
359
  objects. A single indexed query over `user_repo_permissions` joined to `repo`
327
- should be enough for each user batch.
360
+ should be enough for each user batch. Also add a cheaper presence/filter path
361
+ for `get --users-without-explicit-perms`: either `userID -> has explicit API
362
+ repo permissions` for many users, or a direct query for users without explicit
363
+ API repo permissions, optionally filtered by `createdAt`.
328
364
 
329
365
  Acceptance criteria:
330
366
 
@@ -336,6 +372,9 @@ Acceptance criteria:
336
372
  latency visible.
337
373
  - `src-auth-perms-sync` can replace its aliased
338
374
  `User.permissionsInfo.repositories(source: API)` calls with this API.
375
+ - `src-auth-perms-sync get --users-without-explicit-perms` can stop probing
376
+ every candidate user through `User.permissionsInfo.repositories(source: API,
377
+ first: 1)`.
339
378
  - Follow-up: evaluate a bulk overwrite API for large full-set applies. The
340
379
  stress run planned roughly 10 million grants and observed
341
380
  `permsStore.upsertUserRepoPermissions-range1` averaging about 2.5s per call.
@@ -1575,6 +1575,12 @@ def invalid_configuration_cases(config: EndToEndConfig) -> list[CommandCase]:
1575
1575
  expected_exit_code=2,
1576
1576
  must_contain=("choose at most one",),
1577
1577
  ),
1578
+ CommandCase(
1579
+ name="invalid-set-full-and-created-after",
1580
+ arguments=("set", "--full", "--created-after", config.future_date),
1581
+ expected_exit_code=2,
1582
+ must_contain=("--full cannot be combined with --created-after",),
1583
+ ),
1578
1584
  CommandCase(
1579
1585
  name="invalid-user-filter-conflict",
1580
1586
  arguments=("get", "--users", config.user, "--users-without-explicit-perms"),
@@ -1682,10 +1688,9 @@ def read_only_cases(config: EndToEndConfig) -> list[CommandCase]:
1682
1688
  def run_safe_set_cases(config: EndToEndConfig, runner: CommandPermutationRunner) -> None:
1683
1689
  runner.run(
1684
1690
  CommandCase(
1685
- name="set-explicit-full-no-op-apply",
1691
+ name="set-created-after-no-op-apply",
1686
1692
  arguments=(
1687
1693
  "set",
1688
- "--full",
1689
1694
  "--created-after",
1690
1695
  config.future_date,
1691
1696
  "--apply",
@@ -1693,8 +1698,8 @@ def run_safe_set_cases(config: EndToEndConfig, runner: CommandPermutationRunner)
1693
1698
  "--parallelism",
1694
1699
  str(config.parallelism),
1695
1700
  ),
1696
- expected_log_command="set_full",
1697
- must_contain=("No repos resolved across any mapping",),
1701
+ expected_log_command="set_created_after",
1702
+ must_contain=("No users selected",),
1698
1703
  )
1699
1704
  )
1700
1705
 
@@ -47,6 +47,10 @@ GET_CONFIG_FIELDS = src.config_field_names(
47
47
  "users",
48
48
  "users_without_explicit_perms",
49
49
  "created_after",
50
+ "repos",
51
+ "repos_without_explicit_perms",
52
+ "repos_created_after",
53
+ "no_backup",
50
54
  "explicit_permissions_batch_size",
51
55
  *COMMON_CONFIG_FIELDS,
52
56
  )
@@ -56,6 +60,9 @@ SET_CONFIG_FIELDS = src.config_field_names(
56
60
  "users",
57
61
  "users_without_explicit_perms",
58
62
  "created_after",
63
+ "repos",
64
+ "repos_without_explicit_perms",
65
+ "repos_created_after",
59
66
  "sync_saml_organizations",
60
67
  "apply",
61
68
  "no_backup",
@@ -79,27 +86,47 @@ LogCommandName: TypeAlias = Literal[
79
86
  "set_full",
80
87
  "set_users",
81
88
  "set_users_without_explicit_perms",
89
+ "set_created_after",
90
+ "set_repos",
91
+ "set_repos_without_explicit_perms",
92
+ "set_repos_created_after",
82
93
  "restore",
83
94
  "sync_saml_orgs",
84
95
  "set_full_sync_saml_orgs",
85
96
  "set_users_sync_saml_orgs",
86
97
  "set_users_without_explicit_perms_sync_saml_orgs",
98
+ "set_created_after_sync_saml_orgs",
99
+ "set_repos_sync_saml_orgs",
100
+ "set_repos_without_explicit_perms_sync_saml_orgs",
101
+ "set_repos_created_after_sync_saml_orgs",
87
102
  ]
88
103
 
89
104
  SET_COMMAND_LOG_NAMES: dict[permission_types.SetCommandMode, LogCommandName] = {
90
105
  "full": "set_full",
91
106
  "users": "set_users",
92
107
  "users_without_explicit_perms": "set_users_without_explicit_perms",
108
+ "created_after": "set_created_after",
109
+ "repos": "set_repos",
110
+ "repos_without_explicit_perms": "set_repos_without_explicit_perms",
111
+ "repos_created_after": "set_repos_created_after",
93
112
  }
94
113
  SET_COMMAND_ARTIFACT_NAMES: dict[permission_types.SetCommandMode, str] = {
95
114
  "full": "set-{run_mode}",
96
115
  "users": "set-add-users-{run_mode}",
97
116
  "users_without_explicit_perms": "set-add-users-without-explicit-perms-{run_mode}",
117
+ "created_after": "set-add-users-created-after-{run_mode}",
118
+ "repos": "set-repos-{run_mode}",
119
+ "repos_without_explicit_perms": "set-repos-without-explicit-perms-{run_mode}",
120
+ "repos_created_after": "set-repos-created-after-{run_mode}",
98
121
  }
99
122
  SYNC_SET_COMMAND_LOG_NAMES: dict[permission_types.SetCommandMode, LogCommandName] = {
100
123
  "full": "set_full_sync_saml_orgs",
101
124
  "users": "set_users_sync_saml_orgs",
102
125
  "users_without_explicit_perms": "set_users_without_explicit_perms_sync_saml_orgs",
126
+ "created_after": "set_created_after_sync_saml_orgs",
127
+ "repos": "set_repos_sync_saml_orgs",
128
+ "repos_without_explicit_perms": "set_repos_without_explicit_perms_sync_saml_orgs",
129
+ "repos_created_after": "set_repos_created_after_sync_saml_orgs",
103
130
  }
104
131
  SYNC_SET_COMMAND_ARTIFACT_NAMES: dict[permission_types.SetCommandMode, str] = {
105
132
  "full": "set-sync-saml-orgs-{run_mode}",
@@ -107,6 +134,10 @@ SYNC_SET_COMMAND_ARTIFACT_NAMES: dict[permission_types.SetCommandMode, str] = {
107
134
  "users_without_explicit_perms": (
108
135
  "set-add-users-without-explicit-perms-sync-saml-orgs-{run_mode}"
109
136
  ),
137
+ "created_after": "set-add-users-created-after-sync-saml-orgs-{run_mode}",
138
+ "repos": "set-repos-sync-saml-orgs-{run_mode}",
139
+ "repos_without_explicit_perms": ("set-repos-without-explicit-perms-sync-saml-orgs-{run_mode}"),
140
+ "repos_created_after": "set-repos-created-after-sync-saml-orgs-{run_mode}",
110
141
  }
111
142
 
112
143
 
@@ -178,7 +209,10 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
178
209
  env_var="SRC_AUTH_PERMS_SYNC_FULL",
179
210
  cli_flag="--full",
180
211
  cli_action="store_true",
181
- help="With the set command: run the full overwrite reconciliation mode (default)",
212
+ help=(
213
+ "With the set command: run full overwrite reconciliation "
214
+ "(default only when no user filter is set)"
215
+ ),
182
216
  help_group="Permission sync",
183
217
  )
184
218
  users: tuple[str, ...] = src.config_field(
@@ -206,6 +240,31 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
206
240
  help="Process Sourcegraph users created on or after this date",
207
241
  help_group="User filters",
208
242
  )
243
+ repos: tuple[str, ...] = src.config_field(
244
+ default=(),
245
+ env_var="SRC_AUTH_PERMS_SYNC_REPOS",
246
+ cli_flag="--repos",
247
+ metavar="REPOS",
248
+ help="Process comma-delimited Sourcegraph repository names",
249
+ help_group="Repo filters",
250
+ )
251
+ repos_without_explicit_perms: bool = src.config_field(
252
+ default=False,
253
+ env_var="SRC_AUTH_PERMS_SYNC_REPOS_WITHOUT_EXPLICIT_PERMS",
254
+ cli_flag="--repos-without-explicit-perms",
255
+ cli_action="store_true",
256
+ help="Process Sourcegraph repositories without explicit permissions",
257
+ help_group="Repo filters",
258
+ )
259
+ repos_created_after: str | None = src.config_field(
260
+ default=None,
261
+ env_var="SRC_AUTH_PERMS_SYNC_REPOS_CREATED_AFTER",
262
+ cli_flag="--repos-created-after",
263
+ metavar="YYYY-MM-DD",
264
+ pattern=r"^\d{4}-\d{2}-\d{2}$",
265
+ help="Process Sourcegraph repositories created on or after this date",
266
+ help_group="Repo filters",
267
+ )
209
268
  sync_saml_organizations: bool = src.config_field(
210
269
  default=False,
211
270
  env_var="SRC_AUTH_PERMS_SYNC_SYNC_SAML_ORGS",
@@ -227,7 +286,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
227
286
  env_var="SRC_AUTH_PERMS_SYNC_NO_BACKUP",
228
287
  cli_flag="--no-backup",
229
288
  cli_action="store_true",
230
- help="With mutating commands: skip before/after snapshots and validation",
289
+ help="Skip before/after snapshot artifacts and validation where supported",
231
290
  help_group="Mutation",
232
291
  )
233
292
  parallelism: int = src.config_field(
@@ -329,6 +388,7 @@ def validate_config(command_name: CommandName, config: Config) -> None:
329
388
  """Validate cross-field CLI/config constraints."""
330
389
  validate_command_options(command_name, config)
331
390
  validate_user_filter_selection(command_name, config)
391
+ validate_repository_filter_selection(command_name, config)
332
392
  validate_set_mode_selection(command_name, config)
333
393
 
334
394
 
@@ -336,8 +396,6 @@ def validate_command_options(command_name: CommandName, config: Config) -> None:
336
396
  """Validate options that only make sense with specific commands."""
337
397
  if command_name == "get" and config.apply:
338
398
  config_error("--apply cannot be used with the read-only get command")
339
- if command_name == "get" and config.no_backup:
340
- config_error("--no-backup cannot be used with the read-only get command")
341
399
  if config.sync_saml_organizations and command_name != "set":
342
400
  config_error("--sync-saml-orgs can only be combined with set")
343
401
  if command_name == "restore" and config.restore_path is None:
@@ -360,6 +418,34 @@ def validate_user_filter_selection(command_name: CommandName, config: Config) ->
360
418
  )
361
419
 
362
420
 
421
+ def validate_repository_filter_selection(command_name: CommandName, config: Config) -> None:
422
+ """Validate repo-scope filters and their compatible commands."""
423
+ repository_filter_count = sum(
424
+ (
425
+ bool(config.repos),
426
+ config.repos_without_explicit_perms,
427
+ config.repos_created_after is not None,
428
+ )
429
+ )
430
+ if repository_filter_count > 1:
431
+ config_error(
432
+ "choose only one of --repos, --repos-without-explicit-perms, or --repos-created-after"
433
+ )
434
+
435
+ repository_filter_selected = repository_filter_count > 0
436
+ repository_filter_allowed = command_name in {"get", "set"}
437
+ if repository_filter_selected and not repository_filter_allowed:
438
+ config_error(
439
+ "--repos, --repos-without-explicit-perms, and --repos-created-after require get or set"
440
+ )
441
+
442
+ user_filter_selected = any(
443
+ (bool(config.users), config.users_without_explicit_perms, config.created_after is not None)
444
+ )
445
+ if repository_filter_selected and user_filter_selected:
446
+ config_error("choose either user filters or repo filters, not both")
447
+
448
+
363
449
  def validate_set_mode_selection(command_name: CommandName, config: Config) -> None:
364
450
  """Validate set command mode flags."""
365
451
  if config.full and command_name != "set":
@@ -368,9 +454,29 @@ def validate_set_mode_selection(command_name: CommandName, config: Config) -> No
368
454
  if command_name != "set":
369
455
  return
370
456
 
371
- if sum((config.full, bool(config.users), config.users_without_explicit_perms)) > 1:
457
+ if config.full and config.created_after is not None:
458
+ config_error(
459
+ "--full cannot be combined with --created-after because full mode "
460
+ "overwrites mapped repos; omit --full to add grants for new users"
461
+ )
462
+
463
+ if (
464
+ sum(
465
+ (
466
+ config.full,
467
+ bool(config.users),
468
+ config.users_without_explicit_perms,
469
+ bool(config.repos),
470
+ config.repos_without_explicit_perms,
471
+ config.repos_created_after is not None,
472
+ )
473
+ )
474
+ > 1
475
+ ):
372
476
  config_error(
373
- "with set, choose at most one of --full, --users, or --users-without-explicit-perms"
477
+ "with set, choose at most one of --full, --users, "
478
+ "--users-without-explicit-perms, --repos, "
479
+ "--repos-without-explicit-perms, or --repos-created-after"
374
480
  )
375
481
 
376
482
 
@@ -387,9 +493,27 @@ def set_command_options(config: Config) -> permission_types.SetCommandOptions:
387
493
  mode="users_without_explicit_perms",
388
494
  user_created_after=config.created_after,
389
495
  )
496
+ if config.created_after is not None:
497
+ return permission_types.SetCommandOptions(
498
+ mode="created_after",
499
+ user_created_after=config.created_after,
500
+ )
501
+ if config.repos:
502
+ return permission_types.SetCommandOptions(
503
+ mode="repos",
504
+ repository_names=config.repos,
505
+ )
506
+ if config.repos_without_explicit_perms:
507
+ return permission_types.SetCommandOptions(
508
+ mode="repos_without_explicit_perms",
509
+ )
510
+ if config.repos_created_after is not None:
511
+ return permission_types.SetCommandOptions(
512
+ mode="repos_created_after",
513
+ repository_created_after=config.repos_created_after,
514
+ )
390
515
  return permission_types.SetCommandOptions(
391
516
  mode="full",
392
- user_created_after=config.created_after,
393
517
  )
394
518
 
395
519
 
@@ -507,12 +631,7 @@ def require_set_input_file(maps_path: Path) -> None:
507
631
 
508
632
  def run_fields(config: Config, command: ResolvedCommand, endpoint: str) -> dict[str, object]:
509
633
  """Return run-level fields for structured logging."""
510
- return {
511
- "cli_cmd": command.log_name,
512
- "base_cmd": command.name,
513
- "set_mode": command.set_mode,
514
- "sync_saml_orgs_flag": command.sync_saml_organizations,
515
- "apply_flag": config.apply,
634
+ fields: dict[str, object] = {
516
635
  "endpoint": endpoint,
517
636
  "parallelism": config.parallelism,
518
637
  "explicit_permissions_batch_size": config.explicit_permissions_batch_size,
@@ -520,13 +639,28 @@ def run_fields(config: Config, command: ResolvedCommand, endpoint: str) -> dict[
520
639
  "open_telemetry": config.open_telemetry,
521
640
  "max_attempts": config.max_attempts,
522
641
  "http_timeout_seconds": config.http_timeout_seconds,
523
- "no_backup": config.no_backup,
524
642
  "sample_interval": config.sample_interval,
525
- "user_created_after": config.created_after,
526
643
  "artifacts_dir": str(backups.endpoint_artifacts_directory(endpoint)),
527
644
  "python_version": sys.version.split()[0],
528
645
  "pid": os.getpid(),
529
646
  }
647
+ if command.name != "get":
648
+ fields["apply"] = config.apply
649
+ if config.no_backup:
650
+ fields["no_backup"] = True
651
+ if command.set_mode is not None:
652
+ fields["set_mode"] = command.set_mode
653
+ if command.sync_saml_organizations:
654
+ fields["sync_saml_orgs"] = True
655
+ if config.created_after is not None:
656
+ fields["created_after"] = config.created_after
657
+ if config.repos:
658
+ fields["repos"] = config.repos
659
+ if config.repos_without_explicit_perms:
660
+ fields["repos_without_explicit_perms"] = True
661
+ if config.repos_created_after is not None:
662
+ fields["repos_created_after"] = config.repos_created_after
663
+ return fields
530
664
 
531
665
 
532
666
  def run_with_client(
@@ -683,6 +817,9 @@ def run_get(
683
817
  user_identifiers=config.users,
684
818
  users_without_explicit_perms=config.users_without_explicit_perms,
685
819
  user_created_after=config.created_after,
820
+ repository_names=config.repos,
821
+ repositories_without_explicit_perms=config.repos_without_explicit_perms,
822
+ repository_created_after=config.repos_created_after,
686
823
  parallelism=config.parallelism,
687
824
  explicit_permissions_batch_size=config.explicit_permissions_batch_size,
688
825
  bind_id_mode=sourcegraph_site_config.bind_id_mode,
@@ -690,6 +827,7 @@ def run_get(
690
827
  sourcegraph_site_config.saml_groups_attribute_name_by_config_id
691
828
  ),
692
829
  auth_providers_by_config_id=sourcegraph_site_config.auth_providers_by_config_id,
830
+ do_backup=not config.no_backup,
693
831
  retain_saml_group_users=False,
694
832
  worker_pool=worker_pool,
695
833
  )
@@ -767,7 +905,7 @@ def _run_or_raise(command_name: CommandName, config: Config) -> None:
767
905
  backups.run_artifacts_context(run_directory, run_timestamp),
768
906
  src.logging(
769
907
  config,
770
- command=command.log_name,
908
+ command=command.name,
771
909
  git_cwd=__file__,
772
910
  logging_config=logging_settings,
773
911
  run_fields=run_fields(config, command, endpoint),