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.
- src_auth_perms_sync/__init__.py +1 -0
- src_auth_perms_sync/__main__.py +6 -0
- src_auth_perms_sync/cli.py +646 -0
- src_auth_perms_sync/orgs/__init__.py +1 -0
- src_auth_perms_sync/orgs/command.py +7 -0
- src_auth_perms_sync/orgs/queries.py +44 -0
- src_auth_perms_sync/orgs/sync.py +1167 -0
- src_auth_perms_sync/orgs/types.py +103 -0
- src_auth_perms_sync/permissions/__init__.py +1 -0
- src_auth_perms_sync/permissions/apply.py +420 -0
- src_auth_perms_sync/permissions/command.py +918 -0
- src_auth_perms_sync/permissions/full_set.py +880 -0
- src_auth_perms_sync/permissions/mapping.py +627 -0
- src_auth_perms_sync/permissions/maps.py +291 -0
- src_auth_perms_sync/permissions/queries.py +180 -0
- src_auth_perms_sync/permissions/restore.py +913 -0
- src_auth_perms_sync/permissions/snapshot.py +1502 -0
- src_auth_perms_sync/permissions/sourcegraph.py +392 -0
- src_auth_perms_sync/permissions/types.py +116 -0
- src_auth_perms_sync/permissions/workflow.py +526 -0
- src_auth_perms_sync/shared/__init__.py +1 -0
- src_auth_perms_sync/shared/backups.py +119 -0
- src_auth_perms_sync/shared/id_codec.py +67 -0
- src_auth_perms_sync/shared/queries.py +65 -0
- src_auth_perms_sync/shared/run_context.py +34 -0
- src_auth_perms_sync/shared/saml_groups.py +267 -0
- src_auth_perms_sync/shared/site_config.py +366 -0
- src_auth_perms_sync/shared/sourcegraph.py +69 -0
- src_auth_perms_sync/shared/types.py +69 -0
- src_auth_perms_sync-0.2.1.dist-info/METADATA +256 -0
- src_auth_perms_sync-0.2.1.dist-info/RECORD +34 -0
- src_auth_perms_sync-0.2.1.dist-info/WHEEL +4 -0
- src_auth_perms_sync-0.2.1.dist-info/entry_points.txt +2 -0
- 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,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."""
|