diff --git a/README.md b/README.md index 656eaf7..817c165 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ snapshots that make `--apply` reversible. ```bash src-auth-perms-sync sync-saml-orgs --users alice,bob - src-auth-perms-sync sync-saml-orgs --created-after 2026-06-01 + src-auth-perms-sync sync-saml-orgs --users-created-after 2026-06-01 src-auth-perms-sync sync-saml-orgs --users-without-explicit-perms ``` @@ -311,7 +311,7 @@ snapshots that make `--apply` reversible. org whose SAML group disappeared has all members removed, but the org itself is kept (its settings survive in case the group comes back). - **Scoped** (user filters on `sync-saml-orgs`, or `set --users` / - `--users-without-explicit-perms` / `--created-after` with + `--users-without-explicit-perms` / `--users-created-after` with `--sync-saml-orgs`): syncs org membership for exactly the selected users - per-user additions AND removals, computed from each user's own SAML assertion and org list. Other users' memberships never change, and no full diff --git a/src/src_auth_perms_sync/cli.py b/src/src_auth_perms_sync/cli.py index 8b5c16e..5434608 100644 --- a/src/src_auth_perms_sync/cli.py +++ b/src/src_auth_perms_sync/cli.py @@ -39,55 +39,47 @@ src.SourcegraphClientConfig, ) COMMON_CONFIG_FIELDS_AFTER = src.config_field_names( - src.LoggingConfig, - src.OpenTelemetryConfig, + "artifacts_dir", + "no_backup", + "no_files", "parallelism", + "explicit_permissions_batch_size", "http_timeout_seconds", "max_attempts", "sample_interval", "fetch_sg_traces", + src.LoggingConfig, + src.OpenTelemetryConfig, ) GET_CONFIG_FIELDS = src.config_field_names( *COMMON_CONFIG_FIELDS_BEFORE, - "maps_path", "users", + "users_created_after", "users_without_explicit_perms", - "created_after", "repos", - "repos_without_explicit_perms", "repos_created_after", - "no_backup", - "artifacts_dir", - "no_files", - "explicit_permissions_batch_size", + "repos_without_explicit_perms", + "maps_path", *COMMON_CONFIG_FIELDS_AFTER, ) SET_CONFIG_FIELDS = src.config_field_names( *COMMON_CONFIG_FIELDS_BEFORE, - "maps_path", "full", "users", + "users_created_after", "users_without_explicit_perms", - "created_after", "repos", - "repos_without_explicit_perms", "repos_created_after", + "repos_without_explicit_perms", "sync_saml_orgs", "apply", - "no_backup", - "artifacts_dir", - "no_files", - "explicit_permissions_batch_size", + "maps_path", *COMMON_CONFIG_FIELDS_AFTER, ) RESTORE_CONFIG_FIELDS = src.config_field_names( *COMMON_CONFIG_FIELDS_BEFORE, "restore_path", "apply", - "no_backup", - "artifacts_dir", - "no_files", - "explicit_permissions_batch_size", *COMMON_CONFIG_FIELDS_AFTER, ) SYNC_SAML_ORGS_CONFIG_FIELDS = src.config_field_names( @@ -95,13 +87,8 @@ "full", "users", "users_without_explicit_perms", - "created_after", + "users_created_after", "apply", - "no_backup", - "artifacts_dir", - "no_files", - "explicit_permissions_batch_size", - "parallelism", *COMMON_CONFIG_FIELDS_AFTER, ) LogCommandName: TypeAlias = Literal[ @@ -109,7 +96,7 @@ "set_full", "set_users", "set_users_without_explicit_perms", - "set_created_after", + "set_users_created_after", "set_repos", "set_repos_without_explicit_perms", "set_repos_created_after", @@ -118,7 +105,7 @@ "set_full_sync_saml_orgs", "set_users_sync_saml_orgs", "set_users_without_explicit_perms_sync_saml_orgs", - "set_created_after_sync_saml_orgs", + "set_users_created_after_sync_saml_orgs", "set_repos_sync_saml_orgs", "set_repos_without_explicit_perms_sync_saml_orgs", "set_repos_created_after_sync_saml_orgs", @@ -128,7 +115,7 @@ "full": "set_full", "users": "set_users", "users_without_explicit_perms": "set_users_without_explicit_perms", - "created_after": "set_created_after", + "users_created_after": "set_users_created_after", "repos": "set_repos", "repos_without_explicit_perms": "set_repos_without_explicit_perms", "repos_created_after": "set_repos_created_after", @@ -137,7 +124,7 @@ "full": "set-{run_mode}", "users": "set-add-users-{run_mode}", "users_without_explicit_perms": "set-add-users-without-explicit-perms-{run_mode}", - "created_after": "set-add-users-created-after-{run_mode}", + "users_created_after": "set-add-users-created-after-{run_mode}", "repos": "set-repos-{run_mode}", "repos_without_explicit_perms": "set-repos-without-explicit-perms-{run_mode}", "repos_created_after": "set-repos-created-after-{run_mode}", @@ -146,7 +133,7 @@ "full": "set_full_sync_saml_orgs", "users": "set_users_sync_saml_orgs", "users_without_explicit_perms": "set_users_without_explicit_perms_sync_saml_orgs", - "created_after": "set_created_after_sync_saml_orgs", + "users_created_after": "set_users_created_after_sync_saml_orgs", "repos": "set_repos_sync_saml_orgs", "repos_without_explicit_perms": "set_repos_without_explicit_perms_sync_saml_orgs", "repos_created_after": "set_repos_created_after_sync_saml_orgs", @@ -155,7 +142,7 @@ "full": "sync-saml-orgs-full-{run_mode}", "users": "sync-saml-orgs-users-{run_mode}", "users_without_explicit_perms": "sync-saml-orgs-users-without-explicit-perms-{run_mode}", - "created_after": "sync-saml-orgs-created-after-{run_mode}", + "users_created_after": "sync-saml-orgs-users-created-after-{run_mode}", } SYNC_SET_COMMAND_ARTIFACT_NAMES: dict[permission_types.SetCommandMode, str] = { "full": "set-sync-saml-orgs-{run_mode}", @@ -163,7 +150,7 @@ "users_without_explicit_perms": ( "set-add-users-without-explicit-perms-sync-saml-orgs-{run_mode}" ), - "created_after": "set-add-users-created-after-sync-saml-orgs-{run_mode}", + "users_created_after": "set-add-users-created-after-sync-saml-orgs-{run_mode}", "repos": "set-repos-sync-saml-orgs-{run_mode}", "repos_without_explicit_perms": ("set-repos-without-explicit-perms-sync-saml-orgs-{run_mode}"), "repos_created_after": "set-repos-created-after-sync-saml-orgs-{run_mode}", @@ -220,7 +207,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo "(default: //maps.yaml)\n" "Relative paths are resolved from the current working directory" ), - help_group="Permission sync", + help_group="Files", ) artifacts_dir: Path | None = src.config_field( default=None, @@ -230,9 +217,18 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo help=( "Directory containing per-endpoint artifact directories\n" "(default: ./src-auth-perms-sync-runs)\n" + "Can set to /tmp/src-auth-perms-sync-runs, so the OS cleans up these files\n" "Relative paths are resolved from the current working directory" ), - help_group="Artifacts", + help_group="Files", + ) + no_backup: bool = src.config_field( + default=False, + env_var="SRC_AUTH_PERMS_SYNC_NO_BACKUP", + cli_flag="--no-backup", + cli_action="store_true", + help="Skip before/after snapshot artifacts and validation", + help_group="Files", ) no_files: bool = src.config_field( default=False, @@ -241,9 +237,9 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo cli_action="store_true", help=( "Write nothing to disk: no generated YAML, snapshots, or log file\n" - "With --apply, also requires --no-backup (explicitly giving up restore)" + "With --apply, also requires --no-backup (explicitly sacrificing restore capabilities)" ), - help_group="Artifacts", + help_group="Files", ) restore_path: Path | None = src.config_field( default=None, @@ -262,51 +258,53 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo cli_flag="--full", cli_action="store_true", help=( - "Full overwrite of all explicit perms for the repos in scope\n" - "Must be passed explicitly when no user or repo filter args are provided" + "Full overwrite mode: process every mapped user and repo\n" + "Use this for initial sync, and to remove perms now out of scope\n" + "NOTE: This can be CPU intensive on the database server,\n" + "Reduce --parallelism if instance performance is impacted" ), - help_group="Permission sync", + help_group="Set scope (required: pass --full or filters)", ) users: tuple[str, ...] = src.config_field( default=(), env_var="SRC_AUTH_PERMS_SYNC_USERS", cli_flag="--users", metavar="USERS", - help="Process a comma-delimited list of Sourcegraph usernames and/or email addresses", - help_group="User filters", + help="Add perms for a comma-delimited list of Sourcegraph usernames and/or email addresses", + help_group="Set scope (required: pass --full or filters)", ) users_without_explicit_perms: bool = src.config_field( default=False, env_var="SRC_AUTH_PERMS_SYNC_USERS_WITHOUT_EXPLICIT_PERMS", cli_flag="--users-without-explicit-perms", cli_action="store_true", - help="Process Sourcegraph users without explicit permissions", - help_group="User filters", + help="Add perms for Sourcegraph users without explicit permissions", + help_group="Set scope (required: pass --full or filters)", ) - created_after: str | None = src.config_field( + users_created_after: str | None = src.config_field( default=None, - env_var="SRC_AUTH_PERMS_SYNC_CREATED_AFTER", - cli_flag="--created-after", + env_var="SRC_AUTH_PERMS_SYNC_USERS_CREATED_AFTER", + cli_flag="--users-created-after", metavar="YYYY-MM-DD", pattern=r"^\d{4}-\d{2}-\d{2}$", - help="Process Sourcegraph users created on or after this date", - help_group="User filters", + help="Add perms for Sourcegraph users created on or after this date", + help_group="Set scope (required: pass --full or filters)", ) repos: tuple[str, ...] = src.config_field( default=(), env_var="SRC_AUTH_PERMS_SYNC_REPOS", cli_flag="--repos", metavar="REPOS", - help="Process a comma-delimited list of Sourcegraph repository names", - help_group="Repo filters", + help="Add perms for a comma-delimited list of Sourcegraph repository names", + help_group="Set scope (required: pass --full or filters)", ) repos_without_explicit_perms: bool = src.config_field( default=False, env_var="SRC_AUTH_PERMS_SYNC_REPOS_WITHOUT_EXPLICIT_PERMS", cli_flag="--repos-without-explicit-perms", cli_action="store_true", - help="Process repositories without explicit permissions", - help_group="Repo filters", + help="Add perms for repositories without explicit permissions", + help_group="Set scope (required: pass --full or filters)", ) repos_created_after: str | None = src.config_field( default=None, @@ -314,8 +312,8 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo cli_flag="--repos-created-after", metavar="YYYY-MM-DD", pattern=r"^\d{4}-\d{2}-\d{2}$", - help="Process repositories cloned to the Sourcegraph instance on or after this date", - help_group="Repo filters", + help="Add perms for repositories cloned to the Sourcegraph instance on or after this date", + help_group="Set scope (required: pass --full or filters)", ) sync_saml_orgs: bool = src.config_field( default=False, @@ -338,8 +336,8 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo env_var="SRC_AUTH_PERMS_SYNC_NO_BACKUP", cli_flag="--no-backup", cli_action="store_true", - help="Skip before/after snapshot artifacts and validation where supported", - help_group="Mutation", + help="Skip before/after snapshot artifacts and validation", + help_group="Files", ) parallelism: int = src.config_field( default=16, @@ -347,8 +345,11 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo cli_flag="--parallelism", metavar="N", ge=1, - help="Concurrent Sourcegraph API worker threads (default: 16)", - help_group="Performance", + help=( + "Concurrent worker threads (default: 16)\n" + "Reduce this number to reduce the CPU load on the pgsql database" + ), + help_group="Performance tuning", ) explicit_permissions_batch_size: int = src.config_field( default=25, @@ -359,7 +360,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo help=( "Users per GraphQL request when capturing explicit repository permissions (default: 25)" ), - help_group="Performance", + help_group="Performance tuning", ) max_attempts: int = src.config_field( default=5, @@ -367,8 +368,8 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo cli_flag="--max-attempts", metavar="N", ge=1, - help="Max attempts per HTTP request before giving up (default: 5)", - help_group="Performance", + help="Max retries per HTTP request before giving up (default: 5)", + help_group="Performance tuning", ) http_timeout_seconds: float = src.config_field( default=300.0, @@ -377,7 +378,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo metavar="SECONDS", gt=0, help="HTTP read timeout per request in seconds (default: 300)", - help_group="Performance", + help_group="Performance tuning", ) sample_interval: float = src.config_field( default=10.0, @@ -386,7 +387,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo metavar="SECONDS", ge=0, help="Seconds between logging compute resource samples; set 0 to disable (default: 10)", - help_group="Performance", + help_group="Performance measurement", ) fetch_sg_traces: bool = src.config_field( default=False, @@ -394,7 +395,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo cli_flag="--fetch-sg-traces", cli_action="store_true", help="Ask Sourcegraph to retain GraphQL traces and return debug trace metadata", - help_group="Performance", + help_group="Performance measurement", ) @@ -471,11 +472,11 @@ def validate_user_filter_selection(command_name: CommandName, config: Config) -> if user_scope_filter_count > 1: config_error("choose only one of --users or --users-without-explicit-perms") - user_filter_selected = user_scope_filter_count > 0 or config.created_after is not None + user_filter_selected = user_scope_filter_count > 0 or config.users_created_after is not None user_filter_allowed = command_name in {"get", "set", "sync_saml_orgs"} if user_filter_selected and not user_filter_allowed: config_error( - "--users, --users-without-explicit-perms, and --created-after " + "--users, --users-without-explicit-perms, and --users-created-after " "require get, set, or sync-saml-orgs" ) @@ -502,7 +503,11 @@ def validate_repository_filter_selection(command_name: CommandName, config: Conf ) user_filter_selected = any( - (bool(config.users), config.users_without_explicit_perms, config.created_after is not None) + ( + bool(config.users), + config.users_without_explicit_perms, + config.users_created_after is not None, + ) ) if repository_filter_selected and user_filter_selected: config_error("choose either user filters or repo filters, not both") @@ -513,9 +518,10 @@ def validate_sync_saml_orgs_mode_selection(command_name: CommandName, config: Co if command_name != "sync_saml_orgs": return - if config.full and config.created_after is not None: + if config.full and config.users_created_after is not None: config_error( - "--full cannot be combined with --created-after; full mode already syncs every user" + "--full cannot be combined with --users-created-after; " + "full mode already syncs every user" ) if config.full and (config.users or config.users_without_explicit_perms): config_error( @@ -527,13 +533,13 @@ def validate_sync_saml_orgs_mode_selection(command_name: CommandName, config: Co config.full, bool(config.users), config.users_without_explicit_perms, - config.created_after is not None, + config.users_created_after is not None, ) ) if not mode_selected: config_error( "sync-saml-orgs requires one of --full, --users, " - "--users-without-explicit-perms, or --created-after" + "--users-without-explicit-perms, or --users-created-after" ) @@ -545,9 +551,9 @@ def validate_set_mode_selection(command_name: CommandName, config: Config) -> No if command_name != "set": return - if config.full and config.created_after is not None: + if config.full and config.users_created_after is not None: config_error( - "--full cannot be combined with --created-after because full mode " + "--full cannot be combined with --users-created-after because full mode " "overwrites mapped repos; omit --full to add grants for new users" ) @@ -575,7 +581,7 @@ def validate_set_mode_selection(command_name: CommandName, config: Config) -> No config.full, bool(config.users), config.users_without_explicit_perms, - config.created_after is not None, + config.users_created_after is not None, bool(config.repos), config.repos_without_explicit_perms, config.repos_created_after is not None, @@ -583,9 +589,10 @@ def validate_set_mode_selection(command_name: CommandName, config: Config) -> No ) if not set_mode_selected: config_error( - "set requires one of --full, --users, --users-without-explicit-perms, " - "--created-after, --repos, --repos-without-explicit-perms, or " - "--repos-created-after" + "set requires an explicit scope: pass --full for a full overwrite, " + "or pass a user/repo filter: --users, --users-without-explicit-perms, " + "--users-created-after, --repos, " + "--repos-without-explicit-perms, or --repos-created-after" ) @@ -595,17 +602,17 @@ def set_command_options(config: Config) -> permission_types.SetCommandOptions: return permission_types.SetCommandOptions( mode="users", user_identifiers=config.users, - user_created_after=config.created_after, + user_created_after=config.users_created_after, ) if config.users_without_explicit_perms: return permission_types.SetCommandOptions( mode="users_without_explicit_perms", - user_created_after=config.created_after, + user_created_after=config.users_created_after, ) - if config.created_after is not None: + if config.users_created_after is not None: return permission_types.SetCommandOptions( - mode="created_after", - user_created_after=config.created_after, + mode="users_created_after", + user_created_after=config.users_created_after, ) if config.repos: return permission_types.SetCommandOptions( @@ -655,8 +662,8 @@ def sync_saml_orgs_mode(config: Config) -> str: return "users" if config.users_without_explicit_perms: return "users_without_explicit_perms" - if config.created_after is not None: - return "created_after" + if config.users_created_after is not None: + return "users_created_after" return "full" @@ -776,8 +783,8 @@ def run_fields( fields["set_mode"] = command.set_mode if command.sync_saml_orgs: fields["sync_saml_orgs"] = True - if config.created_after is not None: - fields["created_after"] = config.created_after + if config.users_created_after is not None: + fields["users_created_after"] = config.users_created_after if config.repos: fields["repos"] = config.repos if config.repos_without_explicit_perms: @@ -959,7 +966,7 @@ def run_sync_saml_orgs( command_data=command_data, user_identifiers=config.users if standalone else (), users_without_explicit_perms=(config.users_without_explicit_perms if standalone else False), - user_created_after=config.created_after if standalone else None, + user_created_after=config.users_created_after if standalone else None, explicit_permissions_batch_size=config.explicit_permissions_batch_size, worker_pool=worker_pool, ) @@ -988,7 +995,7 @@ def run_get( run_paths, user_identifiers=config.users, users_without_explicit_perms=config.users_without_explicit_perms, - user_created_after=config.created_after, + user_created_after=config.users_created_after, repository_names=config.repos, repositories_without_explicit_perms=config.repos_without_explicit_perms, repository_created_after=config.repos_created_after, diff --git a/src/src_auth_perms_sync/permissions/command.py b/src/src_auth_perms_sync/permissions/command.py index 5c1d15b..cc0f33e 100644 --- a/src/src_auth_perms_sync/permissions/command.py +++ b/src/src_auth_perms_sync/permissions/command.py @@ -255,7 +255,7 @@ def cmd_get( if users_without_explicit_perms: cmd_fields["users_without_explicit_perms"] = True if user_created_after is not None: - cmd_fields["created_after"] = user_created_after + cmd_fields["users_created_after"] = user_created_after if repository_names: cmd_fields["repositories"] = repository_names if repositories_without_explicit_perms: @@ -438,7 +438,7 @@ def load_selected_users( created_after_filter: str | None = None if user_created_after is not None: created_after_filter = sourcegraph_datetime_filter( - parse_cli_date(user_created_after, "--created-after") + parse_cli_date(user_created_after, "--users-created-after") ) if users_without_explicit_perms: candidate_selection = ( @@ -749,9 +749,9 @@ def cmd_set( worker_pool=worker_pool, mapping_rules=mapping_rules, ) - if options.mode == "created_after": + if options.mode == "users_created_after": assert options.user_created_after is not None - return cmd_set_additive_created_after( + return cmd_set_additive_users_created_after( client, run_paths, options.user_created_after, @@ -912,7 +912,7 @@ def cmd_set_additive_users_without_explicit_perms( created_after_filter: str | None = None if user_created_after is not None: created_after_filter = sourcegraph_datetime_filter( - parse_cli_date(user_created_after, "--created-after") + parse_cli_date(user_created_after, "--users-created-after") ) with src.span( "cmd_set_additive_users_without_explicit_perms", @@ -1074,7 +1074,7 @@ def cmd_set_additive_users_without_explicit_perms( return _additive_command_data(context, users, retain_saml_group_users) -def cmd_set_additive_created_after( +def cmd_set_additive_users_created_after( client: src.SourcegraphClient, run_paths: backups.RunPaths, user_created_after: str, @@ -1089,10 +1089,10 @@ def cmd_set_additive_created_after( ) -> run_context.CommandData: """Add missing mapped permissions for users created on or after a date.""" created_after_filter = sourcegraph_datetime_filter( - parse_cli_date(user_created_after, "--created-after") + parse_cli_date(user_created_after, "--users-created-after") ) with src.span( - "cmd_set_additive_created_after", + "cmd_set_additive_users_created_after", input_path=str(run_paths.maps_path), user_created_after=user_created_after, dry_run=dry_run, diff --git a/src/src_auth_perms_sync/permissions/full_set.py b/src/src_auth_perms_sync/permissions/full_set.py index 07fb2b6..07910eb 100644 --- a/src/src_auth_perms_sync/permissions/full_set.py +++ b/src/src_auth_perms_sync/permissions/full_set.py @@ -180,7 +180,7 @@ def _filter_full_set_users_by_created_at( users: list[shared_types.User], user_created_after: str | None, ) -> list[shared_types.User]: - """Apply the optional created-after user filter.""" + """Apply the optional users-created-after user filter.""" if user_created_after is None: return users diff --git a/src/src_auth_perms_sync/permissions/sourcegraph.py b/src/src_auth_perms_sync/permissions/sourcegraph.py index fa7a9d4..90e5559 100644 --- a/src/src_auth_perms_sync/permissions/sourcegraph.py +++ b/src/src_auth_perms_sync/permissions/sourcegraph.py @@ -262,14 +262,16 @@ def fetch_batch(batch: Sequence[str]) -> list[shared_types.User | None]: def list_site_user_candidates( client: src.SourcegraphClient, - created_after: str | None, + users_created_after: str | None, *, parallelism: int = 1, worker_pool: ThreadPoolExecutor | None = None, ) -> list[shared_types.SiteUserCandidate]: """Return non-deleted site users, optionally filtered by creation time.""" - created_filter = {"gte": created_after} if created_after is not None else None - created_filter_label = f" created on or after {created_after}" if created_after else "" + created_filter = {"gte": users_created_after} if users_created_after is not None else None + created_filter_label = ( + f" created on or after {users_created_after}" if users_created_after else "" + ) log.info("Querying active Sourcegraph user candidates%s ...", created_filter_label) started = time.perf_counter() first_page, total_count = _site_user_candidate_page( @@ -322,7 +324,7 @@ def fetch_page(offset: int) -> tuple[int, list[shared_types.SiteUserCandidate]]: def list_site_user_candidates_without_explicit_repos( client: src.SourcegraphClient, - created_after: str | None, + users_created_after: str | None, *, batch_size: int, parallelism: int, @@ -337,8 +339,10 @@ def list_site_user_candidates_without_explicit_repos( if batch_size < 1: raise ValueError("batch_size must be at least 1") - created_filter = {"gte": created_after} if created_after is not None else None - created_filter_label = f" created on or after {created_after}" if created_after else "" + created_filter = {"gte": users_created_after} if users_created_after is not None else None + created_filter_label = ( + f" created on or after {users_created_after}" if users_created_after else "" + ) log.info("Querying active Sourcegraph user candidates%s ...", created_filter_label) started = time.perf_counter() first_page, total_count = _site_user_candidate_page( diff --git a/src/src_auth_perms_sync/permissions/types.py b/src/src_auth_perms_sync/permissions/types.py index c764717..fe35dae 100644 --- a/src/src_auth_perms_sync/permissions/types.py +++ b/src/src_auth_perms_sync/permissions/types.py @@ -11,7 +11,7 @@ "full", "users", "users_without_explicit_perms", - "created_after", + "users_created_after", "repos", "repos_without_explicit_perms", "repos_created_after", diff --git a/src/src_auth_perms_sync/permissions/workflow.py b/src/src_auth_perms_sync/permissions/workflow.py index db1fa4e..a840da2 100644 --- a/src/src_auth_perms_sync/permissions/workflow.py +++ b/src/src_auth_perms_sync/permissions/workflow.py @@ -624,7 +624,7 @@ def sourcegraph_datetime_filter(value: datetime.datetime) -> str: def user_ids_created_on_or_after(client: src.SourcegraphClient, value: str) -> set[str]: """Return Sourcegraph user IDs created on or after the given CLI date.""" - filter_value = sourcegraph_datetime_filter(parse_cli_date(value, "--created-after")) + filter_value = sourcegraph_datetime_filter(parse_cli_date(value, "--users-created-after")) candidates = permissions_sourcegraph.list_site_user_candidates(client, filter_value) log.info( "Restricting to %d Sourcegraph user(s) created on or after %s.", diff --git a/tests/README.md b/tests/README.md index 63c438c..c289cf2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -132,7 +132,7 @@ Live cases declare their identity preconditions in tests.yaml: a pointer to setup.py on drift) and `live.temporaryUsers` (the harness creates the named users fresh via `createUser` - `created_at` = now - and hard-deletes them afterwards; `{today}` in a cliCommand resolves to the -run's UTC date, which makes positive `--created-after` selection +run's UTC date, which makes positive `--users-created-after` selection deterministic against the long-pre-existing synthetic users). ## PyPI install smoke (`--install`) diff --git a/tests/e2e/case_runner.py b/tests/e2e/case_runner.py index 6633811..6751ce3 100644 --- a/tests/e2e/case_runner.py +++ b/tests/e2e/case_runner.py @@ -536,15 +536,15 @@ def _repository_candidates(self, variables: dict[str, object]) -> list[dict[str, def _site_users(self, variables: dict[str, object]) -> dict[str, Any]: created_at_filter = variables.get("createdAt") - created_after: str | None = None + users_created_after: str | None = None if isinstance(created_at_filter, dict): created_after_value = cast(dict[str, object], created_at_filter).get("gte") if isinstance(created_after_value, str): - created_after = created_after_value + users_created_after = created_after_value candidates = [ user for user in self._users - if created_after is None or user["createdAt"] >= created_after + if users_created_after is None or user["createdAt"] >= users_created_after ] offset = self._integer_variable(variables, "offset") # Serve pages no wider than SITE_USERS_PAGE_CAP regardless of the diff --git a/tests/e2e/fixtures/invalid-set-created-after-date/before.json b/tests/e2e/fixtures/invalid-set-users-created-after-date/before.json similarity index 100% rename from tests/e2e/fixtures/invalid-set-created-after-date/before.json rename to tests/e2e/fixtures/invalid-set-users-created-after-date/before.json diff --git a/tests/e2e/fixtures/invalid-set-created-after-date/maps.yaml b/tests/e2e/fixtures/invalid-set-users-created-after-date/maps.yaml similarity index 100% rename from tests/e2e/fixtures/invalid-set-created-after-date/maps.yaml rename to tests/e2e/fixtures/invalid-set-users-created-after-date/maps.yaml diff --git a/tests/e2e/fixtures/set-created-after-sync-saml-orgs-dry-run/maps.yaml b/tests/e2e/fixtures/set-users-created-after-sync-saml-orgs-dry-run/maps.yaml similarity index 100% rename from tests/e2e/fixtures/set-created-after-sync-saml-orgs-dry-run/maps.yaml rename to tests/e2e/fixtures/set-users-created-after-sync-saml-orgs-dry-run/maps.yaml diff --git a/tests/e2e/fixtures/set-created-after-temp-user/after.json b/tests/e2e/fixtures/set-users-created-after-temp-user/after.json similarity index 100% rename from tests/e2e/fixtures/set-created-after-temp-user/after.json rename to tests/e2e/fixtures/set-users-created-after-temp-user/after.json diff --git a/tests/e2e/fixtures/set-created-after-temp-user/before.json b/tests/e2e/fixtures/set-users-created-after-temp-user/before.json similarity index 100% rename from tests/e2e/fixtures/set-created-after-temp-user/before.json rename to tests/e2e/fixtures/set-users-created-after-temp-user/before.json diff --git a/tests/e2e/fixtures/set-created-after-temp-user/maps.yaml b/tests/e2e/fixtures/set-users-created-after-temp-user/maps.yaml similarity index 100% rename from tests/e2e/fixtures/set-created-after-temp-user/maps.yaml rename to tests/e2e/fixtures/set-users-created-after-temp-user/maps.yaml diff --git a/tests/integration/test_cli_entrypoint.py b/tests/integration/test_cli_entrypoint.py index 4ad8c42..6ecd88e 100644 --- a/tests/integration/test_cli_entrypoint.py +++ b/tests/integration/test_cli_entrypoint.py @@ -85,7 +85,14 @@ def test_command_help_prints_command_specific_options(self) -> None: self.assertIn("--users USERS", set_help.stdout) self.assertIn("--sync-saml-orgs", set_help.stdout) self.assertNotIn("--restore-path", set_help.stdout) - self.assertIn("Permission sync:", set_help.stdout) + self.assertIn("Set scope (required: pass --full or filters):", set_help.stdout) + self.assertLess( + set_help.stdout.index("\nSet scope"), + set_help.stdout.index("\nOrganization sync:"), + ) + self.assertNotIn("\nUser filters:", set_help.stdout) + self.assertNotIn("\nRepo filters:", set_help.stdout) + self.assertIn("Files:", set_help.stdout) self.assertIn("Organization sync:", set_help.stdout) self.assertIn("Sourcegraph:", set_help.stdout) self.assertIn("Logging:", set_help.stdout) diff --git a/tests/run.py b/tests/run.py index baf29cc..d4b470f 100644 --- a/tests/run.py +++ b/tests/run.py @@ -1883,7 +1883,7 @@ def run_seeded_fixture_apply( self.delete_temporary_user(label, level, username, user_id) def create_temporary_user(self, username: str) -> str | None: - """Create a throwaway user (created_at = now) for created-after cases.""" + """Create a throwaway user (created_at = now) for users-created-after cases.""" try: data = self.graphql( "mutation TestCreateUser($username: String!) {" diff --git a/tests/tests.yaml b/tests/tests.yaml index 23077c2..994e9fe 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -188,7 +188,7 @@ cases: description: dates must match YYYY-MM-DD before any network call. modes: - local - cliCommand: get --created-after 2026-1-01 + cliCommand: get --users-created-after 2026-1-01 expectedExitCode: 2 expectedOutput: - string_pattern_mismatch @@ -305,7 +305,7 @@ cases: cliCommand: set expectedExitCode: 2 expectedOutput: - - set requires one of --full + - "set requires an explicit scope: pass --full" reject-set-full-and-users: # scope: @@ -339,7 +339,7 @@ cases: expectedOutput: - choose only one of --users - reject-set-full-and-created-after: + reject-set-full-and-users-created-after: # scope: # users: 0 # repos: 0 @@ -350,10 +350,10 @@ cases: description: full overwrite cannot be combined with the additive date filter. modes: - local - cliCommand: set --full --created-after 2099-01-01 + cliCommand: set --full --users-created-after 2099-01-01 expectedExitCode: 2 expectedOutput: - - "--full cannot be combined with --created-after" + - "--full cannot be combined with --users-created-after" reject-set-restore-path: # scope: @@ -453,7 +453,7 @@ cases: expectedOutput: - >- sync-saml-orgs requires one of --full, --users, - --users-without-explicit-perms, or --created-after + --users-without-explicit-perms, or --users-created-after reject-sync-saml-orgs-full-and-users: # scope: @@ -471,7 +471,7 @@ cases: expectedOutput: - "choose at most one of --full, --users" - reject-sync-saml-orgs-full-and-created-after: + reject-sync-saml-orgs-full-and-users-created-after: # scope: # users: 0 # repos: 0 @@ -479,13 +479,13 @@ cases: # cost: # bigO: O(1) # note: parse-only -- no instance reads or writes - description: "--full means all users; combining it with --created-after is contradictory." + description: "--full means all users; combining it with --users-created-after is contradictory." modes: - local - cliCommand: sync-saml-orgs --full --created-after 2099-01-01 + cliCommand: sync-saml-orgs --full --users-created-after 2099-01-01 expectedExitCode: 2 expectedOutput: - - "--full cannot be combined with --created-after" + - "--full cannot be combined with --users-created-after" reject-sync-saml-orgs-repos: # scope: @@ -649,7 +649,7 @@ cases: no_backup: true expectedMutations: 2 - set-created-after-temp-user: + set-users-created-after-temp-user: # scope: # users: 1 # repos: 1 @@ -663,9 +663,9 @@ cases: # writes: 1 mutation # note: server-side date filter selects only the temp user description: >- - POSITIVE created-after selection on the real instance: the harness + POSITIVE users-created-after selection on the real instance: the harness creates a fresh temporary user (created today), so - --created-after {today} selects exactly that user out of 10k + --users-created-after {today} selects exactly that user out of 10k pre-existing ones; the mapped grant lands and the canary repo stays empty. The temp user is hard-deleted afterwards. modes: @@ -677,7 +677,7 @@ cases: - test-repo-49912 args: command: set - created_after: "{today}" + users_created_after: "{today}" apply: true no_backup: true expectedMutations: 1 @@ -696,13 +696,13 @@ cases: # writes: 4 mutations # note: server-side date filter description: >- - createdAfter mode additively grants mapped repos to users created + users-created-after mode additively grants mapped repos to users created on/after the date, preserving existing grants. modes: - local args: command: set - created_after: "2026-02-01" + users_created_after: "2026-02-01" apply: true no_backup: true @@ -1127,7 +1127,7 @@ cases: expectedErrors: - unknown users field 'userNames' - invalid-set-created-after-date: + invalid-set-users-created-after-date: # scope: # users: 0 # repos: 0 @@ -1143,10 +1143,10 @@ cases: - live args: command: set - created_after: "2026-02-31" + users_created_after: "2026-02-31" expectedMutations: 0 expectedErrors: - - "--created-after must use YYYY-MM-DD" + - "--users-created-after must use YYYY-MM-DD" invalid-set-repos-created-after-date: # scope: @@ -1470,7 +1470,7 @@ cases: expectedMutations: 1 # -- Live only: real-instance validation and organization sync -- - invalid-created-after-date: + invalid-users-created-after-date: # scope: # users: 0 # repos: 0 @@ -1485,9 +1485,9 @@ cases: - live args: command: get - created_after: "2026-02-31" + users_created_after: "2026-02-31" expectedErrors: - - "--created-after must use YYYY-MM-DD" + - "--users-created-after must use YYYY-MM-DD" invalid-missing-maps-file: # scope: @@ -1511,7 +1511,7 @@ cases: expectedErrors: - set input file does not exist - get-created-after-future: + get-users-created-after-future: # scope: # users: 0 # repos: 0 @@ -1523,15 +1523,15 @@ cases: # userPermScans: 0 # note: server-side date filter selects nobody description: >- - A far-future --created-after selects no users on the real instance. + A far-future --users-created-after selects no users on the real instance. modes: - live - cliCommand: get --created-after 2099-01-01 + cliCommand: get --users-created-after 2099-01-01 expectedExitCode: 0 expectedOutput: - Selected 0 user(s) for get output. - get-user-created-after-future: + get-named-user-users-created-after-future: # scope: # users: 0 # repos: 0 @@ -1543,16 +1543,16 @@ cases: # userPermScans: 0 # note: the user is filtered out by date description: >- - --users combined with a far-future --created-after filters the named + --users combined with a far-future --users-created-after filters the named user out of the selection. modes: - live - cliCommand: get --users {user} --created-after 2099-01-01 + cliCommand: get --users {user} --users-created-after 2099-01-01 expectedExitCode: 0 expectedOutput: - no user metadata selected - get-users-without-perms-created-after-future: + get-users-without-perms-users-created-after-future: # scope: # users: 0 # repos: 0 @@ -1565,10 +1565,10 @@ cases: # note: filtered candidate query selects nobody description: >- --users-without-explicit-perms combined with a far-future - --created-after selects no users. + --users-created-after selects no users. modes: - live - cliCommand: get --users-without-explicit-perms --created-after 2099-01-01 + cliCommand: get --users-without-explicit-perms --users-created-after 2099-01-01 expectedExitCode: 0 expectedOutput: - Selected 0 user(s) for get output. @@ -1631,14 +1631,14 @@ cases: # users: 0 # note: server-side date filter selects nobody -- exits before the repo scan description: >- - A far-future --created-after selects no users on the real instance: + A far-future --users-created-after selects no users on the real instance: zero mutations, seeded state untouched. modes: - live - performance args: command: set - created_after: "2099-01-01" + users_created_after: "2099-01-01" apply: true no_backup: true expectedMutations: 0 @@ -1736,7 +1736,7 @@ cases: expectedOutput: - Dry run complete - set-created-after-sync-saml-orgs-dry-run: + set-users-created-after-sync-saml-orgs-dry-run: # scope: # users: 0 # repos: 0 @@ -1748,11 +1748,11 @@ cases: # writes: none # note: perm phase selects nobody; org phase streams all users description: >- - Combined permission + organization sync dispatch, created-after mode + Combined permission + organization sync dispatch, users-created-after mode (far-future date selects no users), dry run only. modes: - live - cliCommand: set --created-after 2099-01-01 --sync-saml-orgs + cliCommand: set --users-created-after 2099-01-01 --sync-saml-orgs expectedExitCode: 0 expectedOutput: - Dry run complete diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index fa1f76e..8c1f66d 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -209,16 +209,16 @@ def test_set_command_options_match_each_incremental_mode(self) -> None: make_config( maps_path=Path("maps.yaml"), users_without_explicit_perms=True, - created_after="2026-01-01", + users_created_after="2026-01-01", ) ) self.assertEqual("users_without_explicit_perms", users_without_permissions.mode) self.assertEqual("2026-01-01", users_without_permissions.user_created_after) - created_after = cli.set_command_options( - make_config(maps_path=Path("maps.yaml"), created_after="2026-01-01") + users_created_after = cli.set_command_options( + make_config(maps_path=Path("maps.yaml"), users_created_after="2026-01-01") ) - self.assertEqual("created_after", created_after.mode) - self.assertEqual("2026-01-01", created_after.user_created_after) + self.assertEqual("users_created_after", users_created_after.mode) + self.assertEqual("2026-01-01", users_created_after.user_created_after) repos = cli.set_command_options( make_config( maps_path=Path("maps.yaml"), @@ -246,9 +246,9 @@ def test_resolve_command_includes_set_mode_names(self) -> None: "set", make_config(maps_path=Path("maps.yaml"), full=True), ) - created_after_command = cli.resolve_command( + users_created_after_command = cli.resolve_command( "set", - make_config(maps_path=Path("maps.yaml"), created_after="2026-01-01"), + make_config(maps_path=Path("maps.yaml"), users_created_after="2026-01-01"), ) repos_command = cli.resolve_command( "set", @@ -263,12 +263,12 @@ def test_resolve_command_includes_set_mode_names(self) -> None: self.assertEqual("users", users_command.set_mode) self.assertEqual("set_full", full_command.log_name) self.assertEqual("set-dry-run", full_command.artifact_name) - self.assertEqual("set_created_after", created_after_command.log_name) + self.assertEqual("set_users_created_after", users_created_after_command.log_name) self.assertEqual( "set-add-users-created-after-dry-run", - created_after_command.artifact_name, + users_created_after_command.artifact_name, ) - self.assertEqual("created_after", created_after_command.set_mode) + self.assertEqual("users_created_after", users_created_after_command.set_mode) self.assertEqual("set_repos", repos_command.log_name) self.assertEqual("set-repos-dry-run", repos_command.artifact_name) self.assertEqual("repos", repos_command.set_mode) @@ -447,7 +447,7 @@ def test_validate_config_rejects_set_modes_without_set(self) -> None: def test_validate_config_allows_get_user_filters_without_set(self) -> None: cli.validate_config("get", make_config(users=("alice", "bob@example.com"))) cli.validate_config("get", make_config(users_without_explicit_perms=True)) - cli.validate_config("get", make_config(created_after="2026-01-01")) + cli.validate_config("get", make_config(users_created_after="2026-01-01")) def test_validate_config_allows_get_repo_filters_without_set(self) -> None: cli.validate_config("get", make_config(repos=("github.com/sourcegraph/one",))) @@ -497,7 +497,7 @@ def test_validate_config_rejects_set_without_explicit_mode(self) -> None: self.assert_config_error( "set", make_config(maps_path=Path("maps.yaml")), - "set requires one of --full", + "set requires an explicit scope: pass --full", ) def test_validate_config_rejects_sync_saml_orgs_without_explicit_mode(self) -> None: @@ -515,15 +515,15 @@ def test_validate_config_rejects_sync_saml_orgs_full_with_user_filters(self) -> ) self.assert_config_error( "sync_saml_orgs", - make_config(full=True, created_after="2099-01-01"), - "--full cannot be combined with --created-after", + make_config(full=True, users_created_after="2099-01-01"), + "--full cannot be combined with --users-created-after", ) def test_validate_config_allows_sync_saml_orgs_modes(self) -> None: cli.validate_config("sync_saml_orgs", make_config(full=True)) cli.validate_config("sync_saml_orgs", make_config(users=("alice",))) cli.validate_config("sync_saml_orgs", make_config(users_without_explicit_perms=True)) - cli.validate_config("sync_saml_orgs", make_config(created_after="2026-01-01")) + cli.validate_config("sync_saml_orgs", make_config(users_created_after="2026-01-01")) def test_resolve_command_names_sync_saml_orgs_artifacts_by_mode(self) -> None: self.assertEqual( @@ -537,33 +537,33 @@ def test_resolve_command_names_sync_saml_orgs_artifacts_by_mode(self) -> None: ).artifact_name, ) self.assertEqual( - "sync-saml-orgs-created-after-dry-run", + "sync-saml-orgs-users-created-after-dry-run", cli.resolve_command( - "sync_saml_orgs", make_config(created_after="2026-01-01") + "sync_saml_orgs", make_config(users_created_after="2026-01-01") ).artifact_name, ) - def test_created_after_config_accepts_yyyy_mm_dd_date_arguments(self) -> None: - config = load_config_from_env(SRC_AUTH_PERMS_SYNC_CREATED_AFTER="2026-01-01") + def test_users_created_after_config_accepts_yyyy_mm_dd_date_arguments(self) -> None: + config = load_config_from_env(SRC_AUTH_PERMS_SYNC_USERS_CREATED_AFTER="2026-01-01") - self.assertEqual("2026-01-01", config.created_after) - cli.validate_config("get", make_config(created_after="2026-01-01")) + self.assertEqual("2026-01-01", config.users_created_after) + cli.validate_config("get", make_config(users_created_after="2026-01-01")) cli.validate_config( "set", make_config( maps_path=Path("maps.yaml"), users=("alice",), - created_after="2026-01-01", + users_created_after="2026-01-01", ), ) - def test_created_after_config_rejects_values_outside_yyyy_mm_dd_shape(self) -> None: + def test_users_created_after_config_rejects_values_outside_yyyy_mm_dd_shape(self) -> None: for invalid_value in ("2026-1-01", "2026-01-01T00:00:00Z"): with ( self.subTest(invalid_value=invalid_value), self.assertRaisesRegex(shared_config.ConfigError, "String should match pattern"), ): - load_config_from_env(SRC_AUTH_PERMS_SYNC_CREATED_AFTER=invalid_value) + load_config_from_env(SRC_AUTH_PERMS_SYNC_USERS_CREATED_AFTER=invalid_value) def test_explicit_permissions_batch_size_config_is_loaded_from_env(self) -> None: config = load_config_from_env(SRC_AUTH_PERMS_SYNC_EXPLICIT_PERMISSIONS_BATCH_SIZE="50") @@ -661,11 +661,11 @@ def test_validate_config_rejects_multiple_set_modes(self) -> None: "choose at most one", ) - def test_validate_config_rejects_full_created_after(self) -> None: + def test_validate_config_rejects_full_users_created_after(self) -> None: self.assert_config_error( "set", - make_config(maps_path=Path("maps.yaml"), full=True, created_after="2026-01-01"), - "--full cannot be combined with --created-after", + make_config(maps_path=Path("maps.yaml"), full=True, users_created_after="2026-01-01"), + "--full cannot be combined with --users-created-after", ) def test_require_set_input_file_reports_missing_maps_file(self) -> None: @@ -727,7 +727,7 @@ def test_run_fields_omit_irrelevant_false_flags(self) -> None: self.assertNotIn("no_backup", fields) self.assertNotIn("set_mode", fields) self.assertNotIn("sync_saml_orgs", fields) - self.assertNotIn("created_after", fields) + self.assertNotIn("users_created_after", fields) def test_run_fields_include_no_backup_only_when_set(self) -> None: configuration = make_config(no_backup=True)