src-auth-perms-sync 0.8.0__tar.gz → 0.8.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 (193) hide show
  1. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/PKG-INFO +2 -2
  2. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/pyproject.toml +1 -1
  3. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/cli.py +35 -17
  4. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/integration/test_cli_entrypoint.py +2 -2
  5. src_auth_perms_sync-0.8.1/tests/live-maps-seed.yaml +80 -0
  6. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/run.py +178 -0
  7. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/setup.yaml +4 -0
  8. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_cli_config.py +131 -0
  9. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/uv.lock +4 -4
  10. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.github/CODEOWNERS +0 -0
  11. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.github/workflows/ci.yml +0 -0
  12. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.github/workflows/release.yml +0 -0
  13. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.github/workflows/validate.yml +0 -0
  14. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.gitignore +0 -0
  15. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.markdownlint-cli2.yaml +0 -0
  16. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/.python-version +0 -0
  17. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/AGENTS.md +0 -0
  18. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/LICENSE +0 -0
  19. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/README.md +0 -0
  20. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/SECURITY.md +0 -0
  21. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/TODO.md +0 -0
  22. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/audit-dead-code.md +0 -0
  23. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/engineering-requests.md +0 -0
  24. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/hooks/pre-commit +0 -0
  25. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/memory-analysis/mapping-efficiency.md +0 -0
  26. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/memory-analysis/memory-efficiency-analyze.py +0 -0
  27. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/memory-analysis/memory-efficiency-generate.py +0 -0
  28. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/memory-analysis/memory-efficiency-monitor-sourcegraph.sh +0 -0
  29. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/memory-analysis/memory-efficiency.md +0 -0
  30. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/dev/update-python-versions.md +0 -0
  31. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/examples/import.py +0 -0
  32. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/examples/maps.yaml +0 -0
  33. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/package-lock.json +0 -0
  34. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/package.json +0 -0
  35. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/renovate.json +0 -0
  36. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/__init__.py +0 -0
  37. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/__main__.py +0 -0
  38. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/orgs/__init__.py +0 -0
  39. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/orgs/command.py +0 -0
  40. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/orgs/queries.py +0 -0
  41. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/orgs/sync.py +0 -0
  42. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/orgs/types.py +0 -0
  43. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/__init__.py +0 -0
  44. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/apply.py +0 -0
  45. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/command.py +0 -0
  46. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/full_set.py +0 -0
  47. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/mapping.py +0 -0
  48. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/maps.py +0 -0
  49. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/queries.py +0 -0
  50. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/restore.py +0 -0
  51. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/snapshot.py +0 -0
  52. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/sourcegraph.py +0 -0
  53. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/types.py +0 -0
  54. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/permissions/workflow.py +0 -0
  55. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/__init__.py +0 -0
  56. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/backups.py +0 -0
  57. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/queries.py +0 -0
  58. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/run_context.py +0 -0
  59. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/saml_groups.py +0 -0
  60. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/site_config.py +0 -0
  61. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/sourcegraph.py +0 -0
  62. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/src/src_auth_perms_sync/shared/types.py +0 -0
  63. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/README.md +0 -0
  64. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/__init__.py +0 -0
  65. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/__init__.py +0 -0
  66. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/case_runner.py +0 -0
  67. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/add-users-by-email-and-list/after.json +0 -0
  68. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/add-users-by-email-and-list/before.json +0 -0
  69. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/add-users-by-email-and-list/maps.yaml +0 -0
  70. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/add-users-preserves-existing/after.json +0 -0
  71. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/add-users-preserves-existing/before.json +0 -0
  72. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/add-users-preserves-existing/maps.yaml +0 -0
  73. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/and-filters-intersect/after.json +0 -0
  74. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/and-filters-intersect/before.json +0 -0
  75. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/and-filters-intersect/maps.yaml +0 -0
  76. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/empty-maps-noop/before.json +0 -0
  77. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/empty-maps-noop/maps.yaml +0 -0
  78. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-dry-run/before.json +0 -0
  79. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-dry-run/maps.yaml +0 -0
  80. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-preserves-pending/after.json +0 -0
  81. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-preserves-pending/before.json +0 -0
  82. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-preserves-pending/maps.yaml +0 -0
  83. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/after.json +0 -0
  84. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/before.json +0 -0
  85. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/maps.yaml +0 -0
  86. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-unions/after.json +0 -0
  87. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-unions/before.json +0 -0
  88. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-unions/maps.yaml +0 -0
  89. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-with-backup/after.json +0 -0
  90. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-with-backup/before.json +0 -0
  91. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/full-overwrite-with-backup/maps.yaml +0 -0
  92. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/get-full-snapshot/before.json +0 -0
  93. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/get-repos-without-explicit-perms/before.json +0 -0
  94. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/get-user-grants/before.json +0 -0
  95. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-bad-regex/before.json +0 -0
  96. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-bad-regex/maps.yaml +0 -0
  97. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-missing-repos-section/before.json +0 -0
  98. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-missing-repos-section/maps.yaml +0 -0
  99. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-restore-wrong-schema-version/before.json +0 -0
  100. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-restore-wrong-schema-version/snapshot.json +0 -0
  101. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-repos-created-after-date/before.json +0 -0
  102. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-repos-created-after-date/maps.yaml +0 -0
  103. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-unknown-repo/before.json +0 -0
  104. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-unknown-repo/maps.yaml +0 -0
  105. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-unknown-user/before.json +0 -0
  106. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-unknown-user/maps.yaml +0 -0
  107. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-users-created-after-date/before.json +0 -0
  108. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-set-users-created-after-date/maps.yaml +0 -0
  109. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-unknown-selector-field/before.json +0 -0
  110. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/invalid-unknown-selector-field/maps.yaml +0 -0
  111. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/match-provider-and-host-fields/after.json +0 -0
  112. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/match-provider-and-host-fields/before.json +0 -0
  113. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/match-provider-and-host-fields/maps.yaml +0 -0
  114. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/no-match-noop/before.json +0 -0
  115. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/no-match-noop/maps.yaml +0 -0
  116. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/regex-filters-scope/after.json +0 -0
  117. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/regex-filters-scope/before.json +0 -0
  118. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/regex-filters-scope/maps.yaml +0 -0
  119. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-applies-snapshot/after.json +0 -0
  120. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-applies-snapshot/before.json +0 -0
  121. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-applies-snapshot/snapshot.json +0 -0
  122. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-dry-run-noop/before.json +0 -0
  123. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-dry-run-noop/snapshot.json +0 -0
  124. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-missing-file/before.json +0 -0
  125. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-restores-pending/after.json +0 -0
  126. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-restores-pending/before.json +0 -0
  127. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/restore-restores-pending/snapshot.json +0 -0
  128. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/saml-group-filter/after.json +0 -0
  129. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/saml-group-filter/before.json +0 -0
  130. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/saml-group-filter/maps.yaml +0 -0
  131. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/saml-group-live/after.json +0 -0
  132. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/saml-group-live/before.json +0 -0
  133. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/saml-group-live/maps.yaml +0 -0
  134. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-full-sync-saml-orgs-dry-run/maps.yaml +0 -0
  135. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-created-after/after.json +0 -0
  136. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-created-after/before.json +0 -0
  137. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-created-after/maps.yaml +0 -0
  138. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-created-after-noop/before.json +0 -0
  139. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-created-after-noop/maps.yaml +0 -0
  140. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
  141. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-filter/after.json +0 -0
  142. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-filter/before.json +0 -0
  143. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-filter/maps.yaml +0 -0
  144. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-sync-saml-orgs-dry-run/maps.yaml +0 -0
  145. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-without-explicit-perms/after.json +0 -0
  146. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-without-explicit-perms/before.json +0 -0
  147. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-without-explicit-perms/maps.yaml +0 -0
  148. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-repos-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
  149. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after/after.json +0 -0
  150. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after/before.json +0 -0
  151. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after/maps.yaml +0 -0
  152. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after-noop/before.json +0 -0
  153. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after-noop/maps.yaml +0 -0
  154. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
  155. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after-temp-user/after.json +0 -0
  156. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after-temp-user/before.json +0 -0
  157. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-created-after-temp-user/maps.yaml +0 -0
  158. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-dry-run/maps.yaml +0 -0
  159. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-noop/after.json +0 -0
  160. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-noop/before.json +0 -0
  161. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-noop/maps.yaml +0 -0
  162. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-scoped/after.json +0 -0
  163. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-scoped/before.json +0 -0
  164. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-sync-saml-orgs-scoped/maps.yaml +0 -0
  165. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-explicit-perms/after.json +0 -0
  166. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-explicit-perms/before.json +0 -0
  167. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-explicit-perms/maps.yaml +0 -0
  168. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch/after.json +0 -0
  169. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch/before.json +0 -0
  170. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch/maps.yaml +0 -0
  171. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/set-users-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
  172. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/sync-saml-orgs-full-cleanup/after.json +0 -0
  173. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/sync-saml-orgs-full-cleanup/before.json +0 -0
  174. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/sync-saml-orgs-users-scoped/after.json +0 -0
  175. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/fixtures/sync-saml-orgs-users-scoped/before.json +0 -0
  176. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/e2e/test_local_cases.py +0 -0
  177. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/integration/__init__.py +0 -0
  178. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/setup.py +0 -0
  179. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/tests.yaml +0 -0
  180. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unicode_scan.py +0 -0
  181. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/__init__.py +0 -0
  182. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_apply.py +0 -0
  183. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_backups.py +0 -0
  184. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_command_additive.py +0 -0
  185. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_command_files.py +0 -0
  186. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_mapping_rules.py +0 -0
  187. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_maps.py +0 -0
  188. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_module_api.py +0 -0
  189. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_permissions_sourcegraph.py +0 -0
  190. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_restore.py +0 -0
  191. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_saml_groups.py +0 -0
  192. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_snapshot.py +0 -0
  193. {src_auth_perms_sync-0.8.0 → src_auth_perms_sync-0.8.1}/tests/unit/test_unicode_scan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: src-auth-perms-sync
3
- Version: 0.8.0
3
+ Version: 0.8.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
@@ -18,7 +18,7 @@ Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.11
19
19
  Requires-Dist: json5>=0.15.0
20
20
  Requires-Dist: pyyaml>=6.0.3
21
- Requires-Dist: src-py-lib[otel]==0.3.1
21
+ Requires-Dist: src-py-lib[otel]==0.3.2
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  # src-auth-perms-sync
@@ -32,7 +32,7 @@ classifiers = [
32
32
  dependencies = [
33
33
  "json5>=0.15.0",
34
34
  "pyyaml>=6.0.3",
35
- "src-py-lib[otel]==0.3.1",
35
+ "src-py-lib[otel]==0.3.2",
36
36
  ]
37
37
  keywords = [
38
38
  "Sourcegraph"
@@ -263,23 +263,23 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
263
263
  "NOTE: This can be CPU intensive on the database server,\n"
264
264
  "Reduce --parallelism if instance performance is impacted"
265
265
  ),
266
- help_group="Set scope (required: pass --full or filters)",
266
+ help_group="Scope (required: 1 of the following)",
267
267
  )
268
268
  users: tuple[str, ...] = src.config_field(
269
269
  default=(),
270
270
  env_var="SRC_AUTH_PERMS_SYNC_USERS",
271
271
  cli_flag="--users",
272
272
  metavar="USERS",
273
- help="Add perms for a comma-delimited list of Sourcegraph usernames and/or email addresses",
274
- help_group="Set scope (required: pass --full or filters)",
273
+ help="Filter on a comma-delimited list of Sourcegraph usernames and/or email addresses",
274
+ help_group="Scope (required: 1 of the following)",
275
275
  )
276
276
  users_without_explicit_perms: bool = src.config_field(
277
277
  default=False,
278
278
  env_var="SRC_AUTH_PERMS_SYNC_USERS_WITHOUT_EXPLICIT_PERMS",
279
279
  cli_flag="--users-without-explicit-perms",
280
280
  cli_action="store_true",
281
- help="Add perms for Sourcegraph users without explicit permissions",
282
- help_group="Set scope (required: pass --full or filters)",
281
+ help="Filter on Sourcegraph users without explicit permissions",
282
+ help_group="Scope (required: 1 of the following)",
283
283
  )
284
284
  users_created_after: str | None = src.config_field(
285
285
  default=None,
@@ -287,24 +287,24 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
287
287
  cli_flag="--users-created-after",
288
288
  metavar="YYYY-MM-DD",
289
289
  pattern=r"^\d{4}-\d{2}-\d{2}$",
290
- help="Add perms for Sourcegraph users created on or after this date",
291
- help_group="Set scope (required: pass --full or filters)",
290
+ help="Filter on Sourcegraph users created on or after this date",
291
+ help_group="Scope (required: 1 of the following)",
292
292
  )
293
293
  repos: tuple[str, ...] = src.config_field(
294
294
  default=(),
295
295
  env_var="SRC_AUTH_PERMS_SYNC_REPOS",
296
296
  cli_flag="--repos",
297
297
  metavar="REPOS",
298
- help="Add perms for a comma-delimited list of Sourcegraph repository names",
299
- help_group="Set scope (required: pass --full or filters)",
298
+ help="Filter on a comma-delimited list of Sourcegraph repository names",
299
+ help_group="Scope (required: 1 of the following)",
300
300
  )
301
301
  repos_without_explicit_perms: bool = src.config_field(
302
302
  default=False,
303
303
  env_var="SRC_AUTH_PERMS_SYNC_REPOS_WITHOUT_EXPLICIT_PERMS",
304
304
  cli_flag="--repos-without-explicit-perms",
305
305
  cli_action="store_true",
306
- help="Add perms for repositories without explicit permissions",
307
- help_group="Set scope (required: pass --full or filters)",
306
+ help="Filter on repositories without explicit permissions",
307
+ help_group="Scope (required: 1 of the following)",
308
308
  )
309
309
  repos_created_after: str | None = src.config_field(
310
310
  default=None,
@@ -312,8 +312,8 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
312
312
  cli_flag="--repos-created-after",
313
313
  metavar="YYYY-MM-DD",
314
314
  pattern=r"^\d{4}-\d{2}-\d{2}$",
315
- help="Add perms for repositories cloned to the Sourcegraph instance on or after this date",
316
- help_group="Set scope (required: pass --full or filters)",
315
+ help="Filter on repositories cloned to the Sourcegraph instance on or after this date",
316
+ help_group="Scope (required: 1 of the following)",
317
317
  )
318
318
  sync_saml_orgs: bool = src.config_field(
319
319
  default=False,
@@ -404,28 +404,40 @@ CLI_COMMANDS: tuple[CliCommand, ...] = (
404
404
  argument_name="get",
405
405
  command_name="get",
406
406
  help="Discover auth providers and code hosts",
407
- description="Gather auth providers, code hosts, users, and permissions.",
407
+ description=(
408
+ "Gather auth providers, code hosts, users, and permissions.\n"
409
+ "NOTE: All args can be set via cli args or environment variables"
410
+ ),
408
411
  config_fields=GET_CONFIG_FIELDS,
409
412
  ),
410
413
  CliCommand(
411
414
  argument_name="set",
412
415
  command_name="set",
413
416
  help="Reconcile repo permissions from maps.yaml",
414
- description="Reconcile Sourcegraph explicit repo permissions from maps.yaml.",
417
+ description=(
418
+ "Reconcile Sourcegraph explicit repo permissions from maps.yaml\n"
419
+ "NOTE: All args can be set via cli args or environment variables"
420
+ ),
415
421
  config_fields=SET_CONFIG_FIELDS,
416
422
  ),
417
423
  CliCommand(
418
424
  argument_name="restore",
419
425
  command_name="restore",
420
426
  help="Restore repo permissions from a snapshot",
421
- description="Restore Sourcegraph explicit repo permissions from a snapshot JSON file.",
427
+ description=(
428
+ "Restore Sourcegraph explicit repo permissions from a snapshot JSON file.\n"
429
+ "NOTE: All args can be set via cli args or environment variables"
430
+ ),
422
431
  config_fields=RESTORE_CONFIG_FIELDS,
423
432
  ),
424
433
  CliCommand(
425
434
  argument_name="sync-saml-orgs",
426
435
  command_name="sync_saml_orgs",
427
436
  help="Sync orgs from SAML groups",
428
- description="Create/update Sourcegraph organizations and memberships from SAML groups.",
437
+ description=(
438
+ "Create/update Sourcegraph organizations and memberships from SAML groups.\n"
439
+ "NOTE: All args can be set via cli args or environment variables"
440
+ ),
429
441
  config_fields=SYNC_SAML_ORGS_CONFIG_FIELDS,
430
442
  ),
431
443
  )
@@ -690,6 +702,12 @@ def load_cli(argv: Sequence[str] | None = None) -> CliInput:
690
702
  formatter_class=argparse.RawDescriptionHelpFormatter,
691
703
  allow_abbrev=False,
692
704
  )
705
+ parser.add_argument(
706
+ "--version",
707
+ action="version",
708
+ version=f"src-auth-perms-sync {_package_version()}",
709
+ help="Show the installed src-auth-perms-sync version and exit",
710
+ )
693
711
  subparsers = parser.add_subparsers(
694
712
  title="commands",
695
713
  metavar="COMMAND",
@@ -85,9 +85,9 @@ class CliEntrypointTests(unittest.TestCase):
85
85
  self.assertIn("--users USERS", set_help.stdout)
86
86
  self.assertIn("--sync-saml-orgs", set_help.stdout)
87
87
  self.assertNotIn("--restore-path", set_help.stdout)
88
- self.assertIn("Set scope (required: pass --full or filters):", set_help.stdout)
88
+ self.assertIn("Scope (required: 1 of the following):", set_help.stdout)
89
89
  self.assertLess(
90
- set_help.stdout.index("\nSet scope"),
90
+ set_help.stdout.index("\nScope (required:"),
91
91
  set_help.stdout.index("\nOrganization sync:"),
92
92
  )
93
93
  self.assertNotIn("\nUser filters:", set_help.stdout)
@@ -0,0 +1,80 @@
1
+ # Synthetic-corpus mapping rules used to bootstrap live test runs.
2
+ #
3
+ # The live harness (tests/run.py --live) writes this file to
4
+ # src-auth-perms-sync-runs/<endpoint>/maps.yaml when that path is missing or
5
+ # still holds the empty default template, so a fresh worktree can run the
6
+ # mutating permission cycles. An operator-maintained maps.yaml with real
7
+ # rules is left untouched.
8
+ #
9
+ # It targets the corpus that tests/setup.yaml provisions on the test
10
+ # instance:
11
+ # - users: test_user_00001 through test_user_10000
12
+ # - repos: test-repo-00001 through test-repo-50000
13
+ #
14
+ # Default active shape:
15
+ # - one large rectangular non-overlap rule: 10k users x 1k repos = 10M grants
16
+ # - two small overlapping rules near repo 49k to validate union correctness
17
+ #
18
+ # The 50k-repo all-repo selector below is intentionally not active by default:
19
+ # 10k users x 50k repos would produce 500M planned grants and very large
20
+ # dry-run after/diff files. Use it only for a deliberate long stress run.
21
+
22
+ x-test-users-all: &test_users_all
23
+ - '^test_user_[0-9]{5}$'
24
+
25
+ x-test-users-00001-00100: &test_users_00001_00100
26
+ - '^test_user_(0000[1-9]|000[1-9][0-9]|00100)$'
27
+
28
+ x-test-users-00101-00200: &test_users_00101_00200
29
+ - '^test_user_(0010[1-9]|001[1-9][0-9]|00200)$'
30
+
31
+ x-stress-code-host: &stress_code_host
32
+ kind: OTHER
33
+ displayName: 'OTHER #1'
34
+ url: http://src-serve-git:3434/
35
+
36
+ x-test-repos-00001-01000: &test_repos_00001_01000
37
+ - '^test-repo-(00[0-9]{3}|01000)$'
38
+
39
+ x-test-repos-49001-49100: &test_repos_49001_49100
40
+ - '^test-repo-(4900[1-9]|490[1-9][0-9]|49100)$'
41
+
42
+ x-test-repos-49051-49150: &test_repos_49051_49150
43
+ - '^test-repo-(4905[1-9]|490[6-9][0-9]|491[0-4][0-9]|49150)$'
44
+
45
+ x-test-repos-all-50000: &test_repos_all_50000
46
+ - '^test-repo-[0-4][0-9]{4}$'
47
+ - '^test-repo-50000$'
48
+
49
+ maps:
50
+
51
+ - name: MEMORY phase1 rectangle 10k users to first 1000 test repos
52
+ users:
53
+ usernameRegexes: *test_users_all
54
+ repos:
55
+ codeHostConnection: *stress_code_host
56
+ nameRegexes: *test_repos_00001_01000
57
+
58
+ - name: MEMORY overlap A first 100 users to repos 49001-49100
59
+ users:
60
+ usernameRegexes: *test_users_00001_00100
61
+ repos:
62
+ codeHostConnection: *stress_code_host
63
+ nameRegexes: *test_repos_49001_49100
64
+
65
+ - name: MEMORY overlap B next 100 users to repos 49051-49150
66
+ users:
67
+ usernameRegexes: *test_users_00101_00200
68
+ repos:
69
+ codeHostConnection: *stress_code_host
70
+ nameRegexes: *test_repos_49051_49150
71
+
72
+ # Deliberate heavy stress profile; uncomment as the only active map for a
73
+ # 500M-grant planning run. Expect very large dry-run artifact files.
74
+ #
75
+ # - name: MEMORY max rectangle 10k users to all 50000 test repos
76
+ # users:
77
+ # usernameRegexes: *test_users_all
78
+ # repos:
79
+ # codeHostConnection: *stress_code_host
80
+ # nameRegexes: *test_repos_all_50000
@@ -73,6 +73,19 @@ if TYPE_CHECKING:
73
73
  from tests.e2e.case_runner import FixtureRunResult, FixtureState
74
74
 
75
75
  FIXTURES_DIR = ROOT / "tests" / "e2e" / "fixtures"
76
+ # Canonical synthetic-corpus maps.yaml the live harness seeds into a fresh
77
+ # worktree's endpoint runs directory so the mutating permission cycles have a
78
+ # valid maps file to apply. See ensure_live_maps_seed().
79
+ LIVE_MAPS_SEED_PATH = ROOT / "tests" / "live-maps-seed.yaml"
80
+ # Live checks that reach the pgsql pod over `kubectl exec` (and so need a
81
+ # valid AWS SSO session). check_database_connectivity() probes the pod when
82
+ # any of these is selected and skips them up front when it is unreachable.
83
+ DATABASE_BACKED_LIVE_CHECKS = ("live: perms follow saml group change",)
84
+ # When the pod is unreachable and the run is interactive, the harness offers
85
+ # to refresh AWS SSO, then re-probes on this cadence until it succeeds.
86
+ AWS_SSO_LOGIN_TIMEOUT_SECONDS = 300
87
+ DATABASE_PROBE_RETRY_INTERVAL_SECONDS = 10
88
+ DATABASE_PROBE_RETRY_TIMEOUT_SECONDS = 600
76
89
  TEST_LOGS_DIR = ROOT / "logs"
77
90
  LOG_PATH_PATTERN = re.compile(r"Writing log events to (.+?/log\.json)\.")
78
91
  STRUCTURED_EVENT_LINE_PATTERN = re.compile(r"^[.]*event=\S+\s*$")
@@ -880,6 +893,10 @@ class TestSuite:
880
893
  test_user: str = ""
881
894
  skipped_check_names: list[str] = field(default_factory=list[str])
882
895
  filter_matched_count: int = 0
896
+ # Set by check_database_connectivity(); None until the pgsql pod is probed.
897
+ # False means DB-backed live tests are skipped (the recorded probe failure
898
+ # still makes the run exit non-zero).
899
+ database_available: bool | None = None
883
900
 
884
901
  # -- bookkeeping --------------------------------------------------------
885
902
 
@@ -1436,6 +1453,7 @@ class TestSuite:
1436
1453
  return
1437
1454
  self.record("live prerequisites", "live", True, 0.0)
1438
1455
 
1456
+ self.check_database_connectivity()
1439
1457
  self.check_live_hygiene()
1440
1458
  if self.select("wheel install smoke"):
1441
1459
  self.run_wheel_install_smoke()
@@ -1445,6 +1463,110 @@ class TestSuite:
1445
1463
  self.run_live_permission_cycles(environment)
1446
1464
  self.check_live_hygiene()
1447
1465
 
1466
+ def check_database_connectivity(self) -> None:
1467
+ """Probe the pgsql pod once, up front, so an expired AWS SSO session
1468
+ is reported as a single early failure instead of crashing a later
1469
+ DB-backed test mid-run.
1470
+
1471
+ Only DB-backed live tests need the pod (via `kubectl exec` against
1472
+ EKS, which requires a valid AWS SSO session), so the probe is skipped
1473
+ when none of them are selected. When the probe fails on an interactive
1474
+ run, the harness refreshes AWS SSO and re-probes (see
1475
+ recover_database_access). On unrecovered failure, DB-backed tests skip
1476
+ themselves and the run still exits non-zero via this recorded failure.
1477
+ """
1478
+ label = "live: database connectivity"
1479
+ if not self.test_selected(label, *DATABASE_BACKED_LIVE_CHECKS):
1480
+ return
1481
+ started = time.monotonic()
1482
+ error = self.probe_database()
1483
+ if error is not None and sys.stdin.isatty():
1484
+ error = self.recover_database_access(error)
1485
+ if error is None:
1486
+ self.database_available = True
1487
+ self.record(label, "live", True, time.monotonic() - started)
1488
+ else:
1489
+ self.database_available = False
1490
+ self.record(
1491
+ label, "live", False, time.monotonic() - started, database_error_detail(error)
1492
+ )
1493
+
1494
+ def probe_database(self) -> str | None:
1495
+ """Run `SELECT 1` on the pgsql pod; return None on success, else the error."""
1496
+ from tests import setup as instance_setup
1497
+
1498
+ try:
1499
+ kubectl_config = cast("dict[str, Any]", load_setup_config()["kubectl"])
1500
+ instance_setup.run_sql(kubectl_config, "SELECT 1;", timeout_seconds=30)
1501
+ except Exception as exception:
1502
+ return str(exception)
1503
+ return None
1504
+
1505
+ def recover_database_access(self, error: str) -> str | None:
1506
+ """Refresh AWS SSO interactively, then re-probe until the pod responds.
1507
+
1508
+ Runs `aws sso login` once, then retries the probe every 10s for up to
1509
+ 10 minutes. Returns None once the pod is reachable, or the latest
1510
+ error if SSO login could not run or the pod stayed unreachable.
1511
+ """
1512
+ log.warning("Database probe failed: %s", database_error_detail(error))
1513
+ if not self.run_aws_sso_login():
1514
+ return error
1515
+ deadline = time.monotonic() + DATABASE_PROBE_RETRY_TIMEOUT_SECONDS
1516
+ while True:
1517
+ probe_error = self.probe_database()
1518
+ if probe_error is None:
1519
+ log.info("Database reachable after AWS SSO refresh.")
1520
+ return None
1521
+ if time.monotonic() >= deadline:
1522
+ return probe_error
1523
+ log.info(
1524
+ "Database still unreachable; retrying in %ds (up to 10 min total)...",
1525
+ DATABASE_PROBE_RETRY_INTERVAL_SECONDS,
1526
+ )
1527
+ time.sleep(DATABASE_PROBE_RETRY_INTERVAL_SECONDS)
1528
+
1529
+ def resolve_aws_profile(self) -> str | None:
1530
+ """AWS profile for SSO login: AWS_PROFILE, then setup.yaml
1531
+ kubectl.awsProfile, else None (the default profile)."""
1532
+ profile = os.environ.get("AWS_PROFILE")
1533
+ if profile:
1534
+ return profile
1535
+ kubectl_config = cast("dict[str, Any]", load_setup_config().get("kubectl") or {})
1536
+ configured = kubectl_config.get("awsProfile")
1537
+ return str(configured) if configured else None
1538
+
1539
+ def run_aws_sso_login(self) -> bool:
1540
+ """Run `aws sso login` for the resolved profile; return True if it ran ok."""
1541
+ command = ["aws", "sso", "login"]
1542
+ profile = self.resolve_aws_profile()
1543
+ if profile:
1544
+ command += ["--profile", profile]
1545
+ log.warning("Running `%s` - complete the browser prompt to continue...", " ".join(command))
1546
+ try:
1547
+ completed = subprocess.run(command, timeout=AWS_SSO_LOGIN_TIMEOUT_SECONDS, check=False)
1548
+ except FileNotFoundError:
1549
+ log.error("aws CLI not found; cannot refresh AWS SSO automatically")
1550
+ return False
1551
+ except subprocess.TimeoutExpired:
1552
+ log.error("`aws sso login` timed out after %ds", AWS_SSO_LOGIN_TIMEOUT_SECONDS)
1553
+ return False
1554
+ if completed.returncode != 0:
1555
+ log.error("`aws sso login` failed (exit %d)", completed.returncode)
1556
+ return False
1557
+ return True
1558
+
1559
+ def skip_if_database_unavailable(self, label: str) -> bool:
1560
+ """Log and return True when a DB-backed test must skip the pgsql pod."""
1561
+ if self.database_available is False:
1562
+ log.warning(
1563
+ "SKIP [live] %s - database unavailable "
1564
+ "(see 'live: database connectivity'); run `aws sso login`",
1565
+ label,
1566
+ )
1567
+ return True
1568
+ return False
1569
+
1448
1570
  def check_live_hygiene(self) -> None:
1449
1571
  """Cheap small-state guard: no pending bindIDs should ever persist.
1450
1572
 
@@ -2178,6 +2300,8 @@ class TestSuite:
2178
2300
  label = "live: perms follow saml group change"
2179
2301
  if not self.select(label):
2180
2302
  return
2303
+ if self.skip_if_database_unavailable(label):
2304
+ return
2181
2305
  log.info("\n--- Live: permissions follow a SAML group change ---")
2182
2306
  import yaml
2183
2307
 
@@ -2428,6 +2552,32 @@ class TestSuite:
2428
2552
  "; ".join(mismatches) if mismatches else f"{len(expected_pending)} repo(s) match",
2429
2553
  )
2430
2554
 
2555
+ def live_maps_path(self) -> Path:
2556
+ """Endpoint root maps.yaml the permission cycles apply (CLI default)."""
2557
+ from src_auth_perms_sync.shared import backups
2558
+
2559
+ return (
2560
+ Path(backups.ARTIFACTS_DIR_NAME)
2561
+ / backups.endpoint_directory_name(self.endpoint)
2562
+ / backups.DEFAULT_MAPS_FILE_NAME
2563
+ )
2564
+
2565
+ def ensure_live_maps_seed(self) -> None:
2566
+ """Seed a valid maps.yaml so a fresh worktree can run the cycles.
2567
+
2568
+ The permission cycles apply the endpoint root maps.yaml. On a fresh
2569
+ worktree that file does not exist, so the baseline `get` writes the
2570
+ empty default template, which then fails `set` validation. Seed the
2571
+ synthetic-corpus maps when the file is missing or unusable; leave an
2572
+ operator-maintained maps.yaml with real rules untouched.
2573
+ """
2574
+ maps_path = self.live_maps_path()
2575
+ if maps_file_has_usable_rule(maps_path):
2576
+ return
2577
+ maps_path.parent.mkdir(parents=True, exist_ok=True)
2578
+ maps_path.write_text(LIVE_MAPS_SEED_PATH.read_text(encoding="utf-8"), encoding="utf-8")
2579
+ log.info("Seeded live maps file from %s -> %s", LIVE_MAPS_SEED_PATH, maps_path)
2580
+
2431
2581
  def run_live_permission_cycles(self, environment: dict[str, str]) -> None:
2432
2582
  # The baseline get is a prerequisite for both cycles, so it runs when
2433
2583
  # any of them is selected.
@@ -2445,6 +2595,7 @@ class TestSuite:
2445
2595
  if not want_baseline:
2446
2596
  return
2447
2597
  log.info("\n--- Live: permission cycles with independent read-back ---")
2598
+ self.ensure_live_maps_seed()
2448
2599
  baseline = self.run_cli_case(
2449
2600
  CliCase(
2450
2601
  "live: get user baseline",
@@ -3557,6 +3708,33 @@ def load_setup_config() -> dict[str, Any]:
3557
3708
  return cast("dict[str, Any]", yaml.safe_load(SETUP_CONFIG_PATH.read_text(encoding="utf-8")))
3558
3709
 
3559
3710
 
3711
+ def database_error_detail(error: str) -> str:
3712
+ """Summarize a pgsql/kubectl failure, calling out expired AWS SSO."""
3713
+ lowered = error.lower()
3714
+ if "sso session" in lowered and ("expired" in lowered or "invalid" in lowered):
3715
+ return "pgsql pod unreachable: AWS SSO session expired - run `aws sso login`"
3716
+ first_line = next((line for line in error.splitlines() if line.strip()), "unknown error")
3717
+ return f"pgsql pod unreachable: {first_line.strip()}"
3718
+
3719
+
3720
+ def maps_file_has_usable_rule(path: Path) -> bool:
3721
+ """Return True if the maps file has a rule with both users and repos.
3722
+
3723
+ A `set` run needs at least one rule carrying both sections; the empty
3724
+ default template (a lone `- name: Map 1`) fails validation, so it is not
3725
+ usable.
3726
+ """
3727
+ import yaml
3728
+
3729
+ if not path.is_file():
3730
+ return False
3731
+ loaded = cast("dict[str, Any]", yaml.safe_load(path.read_text(encoding="utf-8")) or {})
3732
+ for rule in cast("list[dict[str, Any]]", loaded.get("maps") or []):
3733
+ if rule.get("users") and rule.get("repos"):
3734
+ return True
3735
+ return False
3736
+
3737
+
3560
3738
  def fixture_grants(case_name: str, file_name: str) -> dict[str, set[str]] | None:
3561
3739
  """Return {repo name: usernames} from one fixture state file."""
3562
3740
  path = FIXTURES_DIR / case_name / file_name
@@ -11,6 +11,10 @@ kubectl:
11
11
  databaseUser: sg
12
12
  database: sg
13
13
  tenantID: 1
14
+ # Optional. AWS SSO profile the live harness refreshes when the pgsql pod
15
+ # is unreachable on an interactive run. Precedence: AWS_PROFILE env var,
16
+ # then this key, then the default profile. Omit to use the default.
17
+ # awsProfile: my-sso-profile
14
18
 
15
19
  users:
16
20
  # Synthetic users are pre-provisioned in bulk; setup verifies the count
@@ -5,6 +5,7 @@ import io
5
5
  import os
6
6
  import tempfile
7
7
  import unittest
8
+ from collections.abc import Sequence
8
9
  from concurrent.futures import ThreadPoolExecutor
9
10
  from pathlib import Path
10
11
  from types import SimpleNamespace
@@ -429,6 +430,97 @@ class CliConfigTests(unittest.TestCase):
429
430
  self.assertIn("--artifacts-dir", help_text)
430
431
  self.assertIn("--no-files", help_text)
431
432
 
433
+ def test_set_help_lists_set_specific_options(self) -> None:
434
+ help_text = self.load_cli_help_text("set", "--help")
435
+
436
+ self.assertIn("--full", help_text)
437
+ self.assertIn("--maps-path", help_text)
438
+ self.assertIn("--apply", help_text)
439
+
440
+ def test_restore_help_lists_restore_specific_options(self) -> None:
441
+ help_text = self.load_cli_help_text("restore", "--help")
442
+
443
+ self.assertIn("--restore-path", help_text)
444
+ self.assertIn("--apply", help_text)
445
+ self.assertNotIn("--maps-path", help_text)
446
+
447
+ def test_sync_saml_orgs_help_lists_mode_options(self) -> None:
448
+ help_text = self.load_cli_help_text("sync-saml-orgs", "--help")
449
+
450
+ self.assertIn("--full", help_text)
451
+ self.assertIn("--users", help_text)
452
+ self.assertNotIn("--maps-path", help_text)
453
+
454
+ def test_version_flag_reports_package_version(self) -> None:
455
+ captured_stdout = io.StringIO()
456
+
457
+ with (
458
+ contextlib.redirect_stdout(captured_stdout),
459
+ self.assertRaises(SystemExit) as exit_context,
460
+ ):
461
+ cli.load_cli(["--version"])
462
+
463
+ self.assertEqual(0, exit_context.exception.code)
464
+ self.assertIn("src-auth-perms-sync", captured_stdout.getvalue())
465
+
466
+ def test_parallelism_config_is_loaded_from_env(self) -> None:
467
+ config = load_config_from_env(SRC_AUTH_PERMS_SYNC_PARALLELISM="8")
468
+
469
+ self.assertEqual(8, config.parallelism)
470
+
471
+ def test_parallelism_rejects_values_below_one(self) -> None:
472
+ with self.assertRaisesRegex(shared_config.ConfigError, "greater than or equal to 1"):
473
+ load_config_from_env(SRC_AUTH_PERMS_SYNC_PARALLELISM="0")
474
+
475
+ def test_max_attempts_config_is_loaded_from_env(self) -> None:
476
+ config = load_config_from_env(SRC_AUTH_PERMS_SYNC_MAX_ATTEMPTS="3")
477
+
478
+ self.assertEqual(3, config.max_attempts)
479
+
480
+ def test_max_attempts_rejects_values_below_one(self) -> None:
481
+ with self.assertRaisesRegex(shared_config.ConfigError, "greater than or equal to 1"):
482
+ load_config_from_env(SRC_AUTH_PERMS_SYNC_MAX_ATTEMPTS="0")
483
+
484
+ def test_sample_interval_config_is_loaded_from_env(self) -> None:
485
+ config = load_config_from_env(SRC_AUTH_PERMS_SYNC_SAMPLE_INTERVAL="2.5")
486
+
487
+ self.assertEqual(2.5, config.sample_interval)
488
+
489
+ def test_sample_interval_allows_zero_to_disable_sampling(self) -> None:
490
+ config = load_config_from_env(SRC_AUTH_PERMS_SYNC_SAMPLE_INTERVAL="0")
491
+
492
+ self.assertEqual(0, config.sample_interval)
493
+
494
+ def test_sample_interval_rejects_negative_values(self) -> None:
495
+ with self.assertRaisesRegex(shared_config.ConfigError, "greater than or equal to 0"):
496
+ load_config_from_env(SRC_AUTH_PERMS_SYNC_SAMPLE_INTERVAL="-5")
497
+
498
+ def test_users_config_drops_empty_and_whitespace_only_entries(self) -> None:
499
+ config = load_config_from_env(SRC_AUTH_PERMS_SYNC_USERS=" , ,, ")
500
+
501
+ self.assertEqual((), config.users)
502
+
503
+ def test_load_cli_rejects_restore_path_env_var_on_set(self) -> None:
504
+ self.assert_load_cli_rejects_env(
505
+ ["set", "--full"],
506
+ {"SRC_AUTH_PERMS_SYNC_RESTORE_PATH": "snapshot.json"},
507
+ "--restore-path requires the restore command",
508
+ )
509
+
510
+ def test_load_cli_rejects_maps_path_env_var_on_restore(self) -> None:
511
+ self.assert_load_cli_rejects_env(
512
+ ["restore", "--restore-path", "snapshot.json"],
513
+ {"SRC_AUTH_PERMS_SYNC_MAPS_PATH": "maps.yaml"},
514
+ "--maps-path requires the get or set command",
515
+ )
516
+
517
+ def test_load_cli_rejects_full_env_var_on_get(self) -> None:
518
+ self.assert_load_cli_rejects_env(
519
+ ["get"],
520
+ {"SRC_AUTH_PERMS_SYNC_FULL": "true"},
521
+ "--full requires the set or sync-saml-orgs command",
522
+ )
523
+
432
524
  def test_validate_config_rejects_restore_without_restore_path(self) -> None:
433
525
  self.assert_config_error("restore", make_config(), "restore requires --restore-path")
434
526
 
@@ -953,3 +1045,42 @@ class CliConfigTests(unittest.TestCase):
953
1045
  cli.validate_config(command_name, config)
954
1046
  self.assertEqual(2, exit_context.exception.code)
955
1047
  self.assertIn(expected_message, captured_stderr.getvalue())
1048
+
1049
+ def load_cli_help_text(self, *argv: str) -> str:
1050
+ captured_stdout = io.StringIO()
1051
+ with (
1052
+ contextlib.redirect_stdout(captured_stdout),
1053
+ self.assertRaises(SystemExit) as exit_context,
1054
+ ):
1055
+ cli.load_cli([*argv])
1056
+ self.assertEqual(0, exit_context.exception.code)
1057
+ return captured_stdout.getvalue()
1058
+
1059
+ def assert_load_cli_rejects_env(
1060
+ self,
1061
+ argv: Sequence[str],
1062
+ env: dict[str, str],
1063
+ expected_message: str,
1064
+ ) -> None:
1065
+ with (
1066
+ tempfile.TemporaryDirectory() as directory,
1067
+ mock.patch.dict(
1068
+ os.environ,
1069
+ {
1070
+ "SRC_ENDPOINT": "https://sourcegraph.example.com",
1071
+ "SRC_ACCESS_TOKEN": "secret",
1072
+ **env,
1073
+ },
1074
+ clear=True,
1075
+ ),
1076
+ ):
1077
+ env_file = Path(directory) / ".env"
1078
+ env_file.write_text("")
1079
+ captured_stderr = io.StringIO()
1080
+ with (
1081
+ contextlib.redirect_stderr(captured_stderr),
1082
+ self.assertRaises(SystemExit) as exit_context,
1083
+ ):
1084
+ cli.load_cli([*argv, "--env-file", str(env_file)])
1085
+ self.assertEqual(2, exit_context.exception.code)
1086
+ self.assertIn(expected_message, captured_stderr.getvalue())
@@ -548,7 +548,7 @@ dev = [
548
548
  requires-dist = [
549
549
  { name = "json5", specifier = ">=0.15.0" },
550
550
  { name = "pyyaml", specifier = ">=6.0.3" },
551
- { name = "src-py-lib", extras = ["otel"], specifier = "==0.3.1" },
551
+ { name = "src-py-lib", extras = ["otel"], specifier = "==0.3.2" },
552
552
  ]
553
553
 
554
554
  [package.metadata.requires-dev]
@@ -560,7 +560,7 @@ dev = [
560
560
 
561
561
  [[package]]
562
562
  name = "src-py-lib"
563
- version = "0.3.1"
563
+ version = "0.3.2"
564
564
  source = { registry = "https://pypi.org/simple" }
565
565
  dependencies = [
566
566
  { name = "httpx" },
@@ -568,9 +568,9 @@ dependencies = [
568
568
  { name = "pydantic" },
569
569
  { name = "python-dotenv" },
570
570
  ]
571
- sdist = { url = "https://files.pythonhosted.org/packages/da/bb/65afd84cf2c730a50d5b995236c5cf7700c35d397f375b23517f44f97d72/src_py_lib-0.3.1.tar.gz", hash = "sha256:e2fb46269fb0a493275a2e7701b7ae34532c11f7167f98481c09370abb3ab41f", size = 97312, upload-time = "2026-06-26T06:51:23.265Z" }
571
+ sdist = { url = "https://files.pythonhosted.org/packages/34/b5/e711949b9c27b3cbadfcc90cf56c43db308b319fd0732f12c0a4722bc9f1/src_py_lib-0.3.2.tar.gz", hash = "sha256:5675f2ee4eb7cde8daca066550f9bf684c94e76b3496618261794dcb765e85bc", size = 97377, upload-time = "2026-06-27T19:20:46.334Z" }
572
572
  wheels = [
573
- { url = "https://files.pythonhosted.org/packages/93/a1/cfc286ec004747082fa9f6d17058dcb4c80e45d68d29a4e9ebf456be9a70/src_py_lib-0.3.1-py3-none-any.whl", hash = "sha256:f507a4cf3738275fd5dc6b7efd954b1ed420d3c934ed53f21739f46464fd2705", size = 54382, upload-time = "2026-06-26T06:51:21.855Z" },
573
+ { url = "https://files.pythonhosted.org/packages/38/1e/a41a634803bb330ba7cfdaa2d2757ae129e3d66bf35f90e1796663edfdc0/src_py_lib-0.3.2-py3-none-any.whl", hash = "sha256:9cd81d2a98d32137fc6e799a5b4decd61b72f836b5d049b21a70247dac0f7bd5", size = 54434, upload-time = "2026-06-27T19:20:44.937Z" },
574
574
  ]
575
575
 
576
576
  [package.optional-dependencies]