src-auth-perms-sync 0.5.0__tar.gz → 0.6.0__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 (179) hide show
  1. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/PKG-INFO +15 -1
  2. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/README.md +14 -0
  3. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/PLAN.md +6 -6
  4. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/TODO.md +0 -10
  5. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/__init__.py +2 -0
  6. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/cli.py +61 -9
  7. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/command.py +20 -5
  8. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/full_set.py +3 -2
  9. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/maps.py +22 -0
  10. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/workflow.py +16 -0
  11. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/case_runner.py +27 -4
  12. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/test_local_cases.py +84 -0
  13. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_cli_config.py +5 -0
  14. src_auth_perms_sync-0.6.0/tests/unit/test_mapping_rules.py +116 -0
  15. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_module_api.py +57 -0
  16. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.env.example +0 -0
  17. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.github/CODEOWNERS +0 -0
  18. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.github/workflows/ci.yml +0 -0
  19. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.github/workflows/release.yml +0 -0
  20. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.github/workflows/validate.yml +0 -0
  21. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.gitignore +0 -0
  22. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.markdownlint-cli2.yaml +0 -0
  23. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/.python-version +0 -0
  24. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/AGENTS.md +0 -0
  25. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/LICENSE +0 -0
  26. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/SECURITY.md +0 -0
  27. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/audit-dead-code.md +0 -0
  28. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/engineering-requests.md +0 -0
  29. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/hooks/pre-commit +0 -0
  30. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/memory-analysis/mapping-efficiency.md +0 -0
  31. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/memory-analysis/memory-efficiency-analyze.py +0 -0
  32. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/memory-analysis/memory-efficiency-generate.py +0 -0
  33. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/memory-analysis/memory-efficiency-monitor-sourcegraph.sh +0 -0
  34. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/memory-analysis/memory-efficiency.md +0 -0
  35. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/dev/update-python-versions.md +0 -0
  36. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/maps-example.yaml +0 -0
  37. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/pyproject.toml +0 -0
  38. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/renovate.json +0 -0
  39. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/__main__.py +0 -0
  40. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/orgs/__init__.py +0 -0
  41. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/orgs/command.py +0 -0
  42. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/orgs/queries.py +0 -0
  43. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/orgs/sync.py +0 -0
  44. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/orgs/types.py +0 -0
  45. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/__init__.py +0 -0
  46. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/apply.py +0 -0
  47. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/mapping.py +0 -0
  48. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/queries.py +0 -0
  49. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/restore.py +0 -0
  50. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/snapshot.py +0 -0
  51. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/sourcegraph.py +0 -0
  52. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/permissions/types.py +0 -0
  53. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/__init__.py +0 -0
  54. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/backups.py +0 -0
  55. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/queries.py +0 -0
  56. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/run_context.py +0 -0
  57. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/saml_groups.py +0 -0
  58. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/site_config.py +0 -0
  59. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/sourcegraph.py +0 -0
  60. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/src/src_auth_perms_sync/shared/types.py +0 -0
  61. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/README.md +0 -0
  62. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/__init__.py +0 -0
  63. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/__init__.py +0 -0
  64. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/add-users-by-email-and-list/after.json +0 -0
  65. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/add-users-by-email-and-list/before.json +0 -0
  66. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/add-users-by-email-and-list/maps.yaml +0 -0
  67. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/add-users-preserves-existing/after.json +0 -0
  68. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/add-users-preserves-existing/before.json +0 -0
  69. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/add-users-preserves-existing/maps.yaml +0 -0
  70. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/and-filters-intersect/after.json +0 -0
  71. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/and-filters-intersect/before.json +0 -0
  72. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/and-filters-intersect/maps.yaml +0 -0
  73. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/empty-maps-noop/before.json +0 -0
  74. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/empty-maps-noop/maps.yaml +0 -0
  75. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-dry-run/before.json +0 -0
  76. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-dry-run/maps.yaml +0 -0
  77. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-preserves-pending/after.json +0 -0
  78. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-preserves-pending/before.json +0 -0
  79. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-preserves-pending/maps.yaml +0 -0
  80. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/after.json +0 -0
  81. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/before.json +0 -0
  82. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/maps.yaml +0 -0
  83. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-unions/after.json +0 -0
  84. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-unions/before.json +0 -0
  85. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-unions/maps.yaml +0 -0
  86. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-with-backup/after.json +0 -0
  87. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-with-backup/before.json +0 -0
  88. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/full-overwrite-with-backup/maps.yaml +0 -0
  89. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/get-full-snapshot/before.json +0 -0
  90. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/get-repos-without-explicit-perms/before.json +0 -0
  91. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/get-user-grants/before.json +0 -0
  92. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-bad-regex/before.json +0 -0
  93. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-bad-regex/maps.yaml +0 -0
  94. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-missing-repos-section/before.json +0 -0
  95. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-missing-repos-section/maps.yaml +0 -0
  96. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-restore-wrong-schema-version/before.json +0 -0
  97. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-restore-wrong-schema-version/snapshot.json +0 -0
  98. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-created-after-date/before.json +0 -0
  99. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-created-after-date/maps.yaml +0 -0
  100. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-repos-created-after-date/before.json +0 -0
  101. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-repos-created-after-date/maps.yaml +0 -0
  102. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-unknown-repo/before.json +0 -0
  103. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-unknown-repo/maps.yaml +0 -0
  104. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-unknown-user/before.json +0 -0
  105. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-set-unknown-user/maps.yaml +0 -0
  106. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-unknown-selector-field/before.json +0 -0
  107. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/invalid-unknown-selector-field/maps.yaml +0 -0
  108. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/match-provider-and-host-fields/after.json +0 -0
  109. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/match-provider-and-host-fields/before.json +0 -0
  110. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/match-provider-and-host-fields/maps.yaml +0 -0
  111. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/no-match-noop/before.json +0 -0
  112. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/no-match-noop/maps.yaml +0 -0
  113. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/regex-filters-scope/after.json +0 -0
  114. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/regex-filters-scope/before.json +0 -0
  115. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/regex-filters-scope/maps.yaml +0 -0
  116. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-applies-snapshot/after.json +0 -0
  117. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-applies-snapshot/before.json +0 -0
  118. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-applies-snapshot/snapshot.json +0 -0
  119. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-dry-run-noop/before.json +0 -0
  120. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-dry-run-noop/snapshot.json +0 -0
  121. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-missing-file/before.json +0 -0
  122. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-restores-pending/after.json +0 -0
  123. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-restores-pending/before.json +0 -0
  124. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/restore-restores-pending/snapshot.json +0 -0
  125. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/saml-group-filter/after.json +0 -0
  126. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/saml-group-filter/before.json +0 -0
  127. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/saml-group-filter/maps.yaml +0 -0
  128. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/saml-group-live/after.json +0 -0
  129. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/saml-group-live/before.json +0 -0
  130. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/saml-group-live/maps.yaml +0 -0
  131. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
  132. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-created-after-temp-user/after.json +0 -0
  133. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-created-after-temp-user/before.json +0 -0
  134. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-created-after-temp-user/maps.yaml +0 -0
  135. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-full-sync-saml-orgs-dry-run/maps.yaml +0 -0
  136. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-created-after/after.json +0 -0
  137. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-created-after/before.json +0 -0
  138. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-created-after/maps.yaml +0 -0
  139. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-created-after-noop/before.json +0 -0
  140. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-created-after-noop/maps.yaml +0 -0
  141. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
  142. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-filter/after.json +0 -0
  143. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-filter/before.json +0 -0
  144. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-filter/maps.yaml +0 -0
  145. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-sync-saml-orgs-dry-run/maps.yaml +0 -0
  146. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/after.json +0 -0
  147. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/before.json +0 -0
  148. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/maps.yaml +0 -0
  149. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-repos-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
  150. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-created-after/after.json +0 -0
  151. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-created-after/before.json +0 -0
  152. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-created-after/maps.yaml +0 -0
  153. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-created-after-noop/before.json +0 -0
  154. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-created-after-noop/maps.yaml +0 -0
  155. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-sync-saml-orgs-dry-run/maps.yaml +0 -0
  156. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-explicit-perms/after.json +0 -0
  157. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-explicit-perms/before.json +0 -0
  158. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-explicit-perms/maps.yaml +0 -0
  159. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch/after.json +0 -0
  160. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch/before.json +0 -0
  161. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch/maps.yaml +0 -0
  162. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/e2e/fixtures/set-users-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
  163. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/integration/__init__.py +0 -0
  164. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/integration/test_cli_entrypoint.py +0 -0
  165. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/run.py +0 -0
  166. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/setup.py +0 -0
  167. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/setup.yaml +0 -0
  168. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/tests.yaml +0 -0
  169. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/__init__.py +0 -0
  170. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_apply.py +0 -0
  171. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_backups.py +0 -0
  172. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_command_additive.py +0 -0
  173. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_command_files.py +0 -0
  174. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_maps.py +0 -0
  175. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_permissions_sourcegraph.py +0 -0
  176. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_restore.py +0 -0
  177. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_saml_groups.py +0 -0
  178. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/tests/unit/test_snapshot.py +0 -0
  179. {src_auth_perms_sync-0.5.0 → src_auth_perms_sync-0.6.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: src-auth-perms-sync
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -170,6 +170,20 @@ for provider in get_result.auth_providers:
170
170
  for code_host in get_result.code_hosts:
171
171
  ...
172
172
 
173
+ # Mapping rules can be passed in memory instead of a maps YAML file —
174
+ # same structure and validation as maps.yaml entries:
175
+ rules: list[src.MappingRule] = [
176
+ {
177
+ "name": "Map 1",
178
+ "users": {"usernameRegexes": [".*"]},
179
+ "repos": {"codeHostConnection": {"kind": "GITHUB"}},
180
+ },
181
+ ]
182
+ result = src.Set(config, mapping_rules=rules)
183
+ # When files are enabled, the rules actually used are written into the
184
+ # run directory for auditability. Snapshots still gate apply=True unless
185
+ # no_files=True and no_backup=True are both set explicitly.
186
+
173
187
  # Other command wrappers:
174
188
  # result = src.Restore(config)
175
189
  # result = src.SyncSamlOrgs(config)
@@ -147,6 +147,20 @@ for provider in get_result.auth_providers:
147
147
  for code_host in get_result.code_hosts:
148
148
  ...
149
149
 
150
+ # Mapping rules can be passed in memory instead of a maps YAML file —
151
+ # same structure and validation as maps.yaml entries:
152
+ rules: list[src.MappingRule] = [
153
+ {
154
+ "name": "Map 1",
155
+ "users": {"usernameRegexes": [".*"]},
156
+ "repos": {"codeHostConnection": {"kind": "GITHUB"}},
157
+ },
158
+ ]
159
+ result = src.Set(config, mapping_rules=rules)
160
+ # When files are enabled, the rules actually used are written into the
161
+ # run directory for auditability. Snapshots still gate apply=True unless
162
+ # no_files=True and no_backup=True are both set explicitly.
163
+
150
164
  # Other command wrappers:
151
165
  # result = src.Restore(config)
152
166
  # result = src.SyncSamlOrgs(config)
@@ -1,11 +1,11 @@
1
1
  # PLAN: file handling + observability redesign
2
2
 
3
- > **Status (2026-06-12): implemented**, except Track A Phase A4
4
- > (in-memory mapping rules for `Set`), which the plan marks optional —
5
- > tracked in [TODO.md](./TODO.md). Shipped as src-py-lib v0.3.0 and the
6
- > src-auth-perms-sync `refactor-logging-and-files` PR. Phases were
7
- > compressed into one PR per repo (no ContextVar bridge phase was
8
- > needed); everything else landed as specified.
3
+ > **Status (2026-06-12): fully implemented**, including Track A Phase A4
4
+ > (in-memory mapping rules for `Set`). Shipped as src-py-lib v0.3.0 and
5
+ > src-auth-perms-sync v0.5.0 (`refactor-logging-and-files` PR), with A4
6
+ > following in the `in-memory-mapping-rules` PR. Phases were compressed
7
+ > into one PR per repo (no ContextVar bridge phase was needed);
8
+ > everything landed as specified.
9
9
 
10
10
  Spans both repos (we own both, both greenfield, no external users):
11
11
 
@@ -1,15 +1,5 @@
1
1
  # TODO
2
2
 
3
- ## Follow-up: in-memory mapping rules for Set (PLAN.md Track A Phase A4)
4
-
5
- The rest of [PLAN.md](./PLAN.md) is implemented (src-py-lib v0.3.0 +
6
- the consumer refactor-logging-and-files PR). The one deliberately
7
- deferred piece, marked optional in the plan: let module callers pass
8
- parsed mapping rules to `Set` instead of a maps file, so the full
9
- get → assemble → dry-run loop never touches disk. Snapshots must stay
10
- on disk for `--apply` (reversibility invariant); `no_files` + `apply`
11
- must keep requiring `no_backup`.
12
-
13
3
  ## High priority: Remote trigger on demand
14
4
 
15
5
  - Sourcegraph webhook for new user coming in v7.4.0
@@ -16,6 +16,7 @@ from src_py_lib import (
16
16
  )
17
17
 
18
18
  from .cli import CommandResult, Config, Get, GetResult, Restore, Set, SyncSamlOrgs
19
+ from .permissions.types import MappingRule
19
20
  from .shared.backups import RunPaths
20
21
 
21
22
  __all__ = [
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "GetResult",
29
30
  "InMemoryEventSink",
30
31
  "JSONLEventSink",
32
+ "MappingRule",
31
33
  "NullEventSink",
32
34
  "Restore",
33
35
  "RunPaths",
@@ -26,6 +26,7 @@ from src_py_lib.utils import config as config_utils
26
26
 
27
27
  from .orgs import command as organizations_command
28
28
  from .permissions import command as permissions_command
29
+ from .permissions import mapping as permissions_mapping
29
30
  from .permissions import maps as permissions_maps
30
31
  from .permissions import types as permission_types
31
32
  from .shared import backups, run_context, site_config
@@ -741,6 +742,7 @@ def run_with_client(
741
742
  endpoint: str,
742
743
  run_paths: backups.RunPaths,
743
744
  worker_pool: ThreadPoolExecutor,
745
+ mapping_rules: list[permission_types.MappingRule] | None = None,
744
746
  ) -> run_context.CommandData:
745
747
  """Create a client, run the selected command, and always close HTTP resources."""
746
748
  http = src.HTTPClient(
@@ -756,7 +758,7 @@ def run_with_client(
756
758
  fetch_sg_traces=config.fetch_sg_traces,
757
759
  )
758
760
  try:
759
- return run_command(config, command, client, run_paths, worker_pool)
761
+ return run_command(config, command, client, run_paths, worker_pool, mapping_rules)
760
762
  finally:
761
763
  client.http.close()
762
764
 
@@ -767,6 +769,7 @@ def run_command(
767
769
  client: src.SourcegraphClient,
768
770
  run_paths: backups.RunPaths,
769
771
  worker_pool: ThreadPoolExecutor,
772
+ mapping_rules: list[permission_types.MappingRule] | None = None,
770
773
  ) -> run_context.CommandData:
771
774
  """Dispatch the selected command."""
772
775
  sourcegraph_site_config = site_config.validate_site_config(client)
@@ -775,7 +778,13 @@ def run_command(
775
778
  command_data = run_get(config, client, sourcegraph_site_config, run_paths, worker_pool)
776
779
  elif command.name == "set":
777
780
  command_data = run_set(
778
- config, command, client, sourcegraph_site_config, run_paths, worker_pool
781
+ config,
782
+ command,
783
+ client,
784
+ sourcegraph_site_config,
785
+ run_paths,
786
+ worker_pool,
787
+ mapping_rules=mapping_rules,
779
788
  )
780
789
  elif command.name == "restore":
781
790
  run_restore(config, client, sourcegraph_site_config, run_paths, worker_pool)
@@ -810,10 +819,22 @@ def run_set(
810
819
  sourcegraph_site_config: site_config.SiteConfig,
811
820
  run_paths: backups.RunPaths,
812
821
  worker_pool: ThreadPoolExecutor,
822
+ mapping_rules: list[permission_types.MappingRule] | None = None,
813
823
  ) -> run_context.CommandData:
814
- """Run the selected repo-permission sync command."""
824
+ """Run the selected repo-permission sync command.
825
+
826
+ With in-memory `mapping_rules` (module callers), no maps file is read;
827
+ when files are enabled, the rules actually used are written into the run
828
+ directory so the audit trail stays faithful.
829
+ """
815
830
  assert command.set_options is not None
816
- require_set_input_file(run_paths.maps_path)
831
+ if mapping_rules is None:
832
+ require_set_input_file(run_paths.maps_path)
833
+ elif run_paths.write_files:
834
+ materialized_maps_path = run_paths.input_copy_path(backups.DEFAULT_MAPS_FILE_NAME)
835
+ permissions_maps.dump_mapping_rules_yaml(materialized_maps_path, mapping_rules)
836
+ run_paths = dataclasses.replace(run_paths, maps_path=materialized_maps_path)
837
+ log.info("Wrote in-memory mapping rules for audit: %s", materialized_maps_path)
817
838
  return permissions_command.cmd_set(
818
839
  client,
819
840
  run_paths,
@@ -828,6 +849,7 @@ def run_set(
828
849
  do_backup=not config.no_backup,
829
850
  retain_saml_group_users=command.sync_saml_organizations,
830
851
  worker_pool=worker_pool,
852
+ mapping_rules=mapping_rules,
831
853
  )
832
854
 
833
855
 
@@ -970,9 +992,22 @@ def Get(config: Config, *, event_sink: src.EventSink | None = None) -> GetResult
970
992
  )
971
993
 
972
994
 
973
- def Set(config: Config, *, event_sink: src.EventSink | None = None) -> CommandResult:
974
- """Run repository permission reconciliation."""
975
- succeeded, _, run_paths = _run("set", config, event_sink)
995
+ def Set(
996
+ config: Config,
997
+ *,
998
+ mapping_rules: list[permission_types.MappingRule] | None = None,
999
+ event_sink: src.EventSink | None = None,
1000
+ ) -> CommandResult:
1001
+ """Run repository permission reconciliation.
1002
+
1003
+ `mapping_rules` lets module callers pass parsed rules directly (e.g.
1004
+ assembled from `GetResult` data) instead of a maps YAML file. The rules
1005
+ go through the same structural validation as file-loaded rules. When
1006
+ files are enabled, the rules actually used are written into the run
1007
+ directory for auditability; snapshots still gate `apply` unless
1008
+ `no_files` and `no_backup` are both set explicitly.
1009
+ """
1010
+ succeeded, _, run_paths = _run("set", config, event_sink, mapping_rules=mapping_rules)
976
1011
  return CommandResult(succeeded=succeeded, paths=run_paths)
977
1012
 
978
1013
 
@@ -992,11 +1027,21 @@ def _run(
992
1027
  command_name: CommandName,
993
1028
  config: Config,
994
1029
  event_sink: src.EventSink | None,
1030
+ mapping_rules: list[permission_types.MappingRule] | None = None,
995
1031
  ) -> tuple[bool, run_context.CommandData | None, backups.RunPaths | None]:
996
1032
  """Run a module-mode command, reporting success instead of raising."""
997
1033
  try:
998
- command_data, run_paths = _run_or_raise(command_name, config, event_sink=event_sink)
1034
+ command_data, run_paths = _run_or_raise(
1035
+ command_name,
1036
+ config,
1037
+ event_sink=event_sink,
1038
+ mapping_rules=mapping_rules,
1039
+ )
999
1040
  except SystemExit as exception:
1041
+ if isinstance(exception.code, str):
1042
+ # Module mode swallows the exit; surface the diagnostic through
1043
+ # the package logger so host applications can see why it failed.
1044
+ log.error("%s", exception.code)
1000
1045
  return exception.code in (None, 0), None, None
1001
1046
  except Exception:
1002
1047
  log.exception("src-auth-perms-sync run failed.")
@@ -1035,6 +1080,7 @@ def _run_or_raise(
1035
1080
  *,
1036
1081
  cli_mode: bool = False,
1037
1082
  event_sink: src.EventSink | None = None,
1083
+ mapping_rules: list[permission_types.MappingRule] | None = None,
1038
1084
  ) -> tuple[run_context.CommandData, backups.RunPaths]:
1039
1085
  """Run src-auth-perms-sync, preserving CLI-style exceptions.
1040
1086
 
@@ -1043,6 +1089,10 @@ def _run_or_raise(
1043
1089
  application's logging configuration stays in charge.
1044
1090
  """
1045
1091
  validate_config(command_name, config)
1092
+ if mapping_rules is not None:
1093
+ if command_name != "set":
1094
+ config_error("mapping_rules can only be passed to Set")
1095
+ permissions_mapping.validate_mapping_rules(mapping_rules)
1046
1096
  command = resolve_command(command_name, config)
1047
1097
  try:
1048
1098
  endpoint = src.normalize_sourcegraph_endpoint(config.src_endpoint)
@@ -1102,7 +1152,9 @@ def _run_or_raise(
1102
1152
  )
1103
1153
  worker_pool = stack.enter_context(run_context.thread_pool(config.parallelism))
1104
1154
  try:
1105
- command_data = run_with_client(config, command, endpoint, run_paths, worker_pool)
1155
+ command_data = run_with_client(
1156
+ config, command, endpoint, run_paths, worker_pool, mapping_rules
1157
+ )
1106
1158
  except SystemExit as exception:
1107
1159
  reraise_system_exit_with_logged_error(exception)
1108
1160
  return command_data, run_paths
@@ -26,11 +26,11 @@ from . import types as permission_types
26
26
  from .workflow import (
27
27
  load_discovery,
28
28
  load_mapping_context_discovery,
29
- load_mapping_rules,
30
29
  load_repos_for_mapping_context,
31
30
  load_repository_candidates_by_names,
32
31
  load_repository_candidates_created_on_or_after,
33
32
  parse_cli_date,
33
+ resolve_mapping_rules,
34
34
  sourcegraph_datetime_filter,
35
35
  user_ids_created_on_or_after,
36
36
  write_maps_backup,
@@ -594,8 +594,13 @@ def cmd_set(
594
594
  do_backup: bool,
595
595
  retain_saml_group_users: bool = False,
596
596
  worker_pool: ThreadPoolExecutor | None = None,
597
+ mapping_rules: list[permission_types.MappingRule] | None = None,
597
598
  ) -> run_context.CommandData:
598
- """Dispatch the selected set mode."""
599
+ """Dispatch the selected set mode.
600
+
601
+ `mapping_rules` carries in-memory rules from module callers; when None,
602
+ rules are loaded from `run_paths.maps_path`.
603
+ """
599
604
  options = set_options
600
605
  if options.mode == "full":
601
606
  return permissions_full_set.cmd_set_full(
@@ -613,6 +618,7 @@ def cmd_set(
613
618
  do_backup=do_backup,
614
619
  retain_saml_group_users=retain_saml_group_users,
615
620
  worker_pool=worker_pool,
621
+ mapping_rules=mapping_rules,
616
622
  )
617
623
  if options.mode == "repos":
618
624
  assert options.repository_names
@@ -631,6 +637,7 @@ def cmd_set(
631
637
  do_backup=do_backup,
632
638
  retain_saml_group_users=retain_saml_group_users,
633
639
  worker_pool=worker_pool,
640
+ mapping_rules=mapping_rules,
634
641
  )
635
642
  if options.mode == "repos_without_explicit_perms":
636
643
  return permissions_full_set.cmd_set_full(
@@ -648,6 +655,7 @@ def cmd_set(
648
655
  do_backup=do_backup,
649
656
  retain_saml_group_users=retain_saml_group_users,
650
657
  worker_pool=worker_pool,
658
+ mapping_rules=mapping_rules,
651
659
  )
652
660
  if options.mode == "repos_created_after":
653
661
  assert options.repository_created_after is not None
@@ -666,6 +674,7 @@ def cmd_set(
666
674
  do_backup=do_backup,
667
675
  retain_saml_group_users=retain_saml_group_users,
668
676
  worker_pool=worker_pool,
677
+ mapping_rules=mapping_rules,
669
678
  )
670
679
  if options.mode == "users":
671
680
  assert options.user_identifiers
@@ -680,6 +689,7 @@ def cmd_set(
680
689
  saml_groups_attribute_name_by_config_id,
681
690
  do_backup,
682
691
  worker_pool,
692
+ mapping_rules=mapping_rules,
683
693
  )
684
694
  if options.mode == "users_without_explicit_perms":
685
695
  return cmd_set_additive_users_without_explicit_perms(
@@ -693,6 +703,7 @@ def cmd_set(
693
703
  saml_groups_attribute_name_by_config_id,
694
704
  do_backup,
695
705
  worker_pool,
706
+ mapping_rules=mapping_rules,
696
707
  )
697
708
  if options.mode == "created_after":
698
709
  assert options.user_created_after is not None
@@ -706,6 +717,7 @@ def cmd_set(
706
717
  saml_groups_attribute_name_by_config_id,
707
718
  do_backup,
708
719
  worker_pool,
720
+ mapping_rules=mapping_rules,
709
721
  )
710
722
  return run_context.CommandData()
711
723
 
@@ -721,6 +733,7 @@ def cmd_set_additive_users(
721
733
  saml_groups_attribute_name_by_config_id: dict[str, str],
722
734
  do_backup: bool,
723
735
  worker_pool: ThreadPoolExecutor | None = None,
736
+ mapping_rules: list[permission_types.MappingRule] | None = None,
724
737
  ) -> run_context.CommandData:
725
738
  """Add missing mapped permissions for resolved users."""
726
739
  with src.span(
@@ -732,7 +745,7 @@ def cmd_set_additive_users(
732
745
  parallelism=parallelism,
733
746
  do_backup=do_backup,
734
747
  ):
735
- mapping_rules = load_mapping_rules(run_paths.maps_path)
748
+ mapping_rules = resolve_mapping_rules(mapping_rules, run_paths.maps_path)
736
749
  if not mapping_rules:
737
750
  log.warning("No maps defined in %s — nothing to do.", run_paths.maps_path)
738
751
  return run_context.CommandData()
@@ -844,6 +857,7 @@ def cmd_set_additive_users_without_explicit_perms(
844
857
  saml_groups_attribute_name_by_config_id: dict[str, str],
845
858
  do_backup: bool,
846
859
  worker_pool: ThreadPoolExecutor | None = None,
860
+ mapping_rules: list[permission_types.MappingRule] | None = None,
847
861
  ) -> run_context.CommandData:
848
862
  """Add mapped permissions for users with no explicit API grants."""
849
863
  created_after_filter: str | None = None
@@ -859,7 +873,7 @@ def cmd_set_additive_users_without_explicit_perms(
859
873
  parallelism=parallelism,
860
874
  do_backup=do_backup,
861
875
  ):
862
- mapping_rules = load_mapping_rules(run_paths.maps_path)
876
+ mapping_rules = resolve_mapping_rules(mapping_rules, run_paths.maps_path)
863
877
  if not mapping_rules:
864
878
  log.warning("No maps defined in %s — nothing to do.", run_paths.maps_path)
865
879
  return run_context.CommandData()
@@ -1019,6 +1033,7 @@ def cmd_set_additive_created_after(
1019
1033
  saml_groups_attribute_name_by_config_id: dict[str, str],
1020
1034
  do_backup: bool,
1021
1035
  worker_pool: ThreadPoolExecutor | None = None,
1036
+ mapping_rules: list[permission_types.MappingRule] | None = None,
1022
1037
  ) -> run_context.CommandData:
1023
1038
  """Add missing mapped permissions for users created on or after a date."""
1024
1039
  created_after_filter = sourcegraph_datetime_filter(
@@ -1032,7 +1047,7 @@ def cmd_set_additive_created_after(
1032
1047
  parallelism=parallelism,
1033
1048
  do_backup=do_backup,
1034
1049
  ):
1035
- mapping_rules = load_mapping_rules(run_paths.maps_path)
1050
+ mapping_rules = resolve_mapping_rules(mapping_rules, run_paths.maps_path)
1036
1051
  if not mapping_rules:
1037
1052
  log.warning("No maps defined in %s — nothing to do.", run_paths.maps_path)
1038
1053
  return run_context.CommandData()
@@ -21,12 +21,12 @@ from . import types as permission_types
21
21
  from .workflow import (
22
22
  load_mapping_context_discovery,
23
23
  load_mapping_context_for_rules,
24
- load_mapping_rules,
25
24
  load_repository_candidates_by_names,
26
25
  load_repository_candidates_created_on_or_after,
27
26
  mapping_context_with_repository_candidates,
28
27
  projected_snapshot_shell,
29
28
  render_projected_snapshot_diff,
29
+ resolve_mapping_rules,
30
30
  user_ids_created_on_or_after,
31
31
  validate_post_apply,
32
32
  write_maps_backup,
@@ -968,6 +968,7 @@ def cmd_set_full(
968
968
  do_backup: bool,
969
969
  retain_saml_group_users: bool,
970
970
  worker_pool: ThreadPoolExecutor | None = None,
971
+ mapping_rules: list[permission_types.MappingRule] | None = None,
971
972
  ) -> run_context.CommandData:
972
973
  """Overwrite each mapped repo with the union of users from all rules."""
973
974
  with src.span(
@@ -981,7 +982,7 @@ def cmd_set_full(
981
982
  parallelism=parallelism,
982
983
  do_backup=do_backup,
983
984
  ) as command_event:
984
- mapping_rules = load_mapping_rules(run_paths.maps_path)
985
+ mapping_rules = resolve_mapping_rules(mapping_rules, run_paths.maps_path)
985
986
  if not mapping_rules:
986
987
  _finish_empty_full_set_mapping_rules(
987
988
  client,
@@ -269,6 +269,28 @@ def create_maps_yaml_if_missing(path: Path) -> bool:
269
269
  return True
270
270
 
271
271
 
272
+ def dump_mapping_rules_yaml(
273
+ path: Path,
274
+ mapping_rules: list[permission_types.MappingRule],
275
+ ) -> None:
276
+ """Write in-memory mapping rules as a maps YAML file for run auditability."""
277
+ content = yaml.safe_dump(
278
+ {"maps": mapping_rules},
279
+ sort_keys=False,
280
+ default_flow_style=False,
281
+ )
282
+ with src.span(
283
+ "disk_io",
284
+ level="DEBUG",
285
+ op="write",
286
+ path=str(path),
287
+ file_kind="yaml",
288
+ ) as disk_event:
289
+ path.parent.mkdir(parents=True, exist_ok=True)
290
+ path.write_text(content)
291
+ disk_event["bytes"] = len(content)
292
+
293
+
272
294
  def load_maps_yaml(path: Path) -> permission_types.ConfigFile:
273
295
  with src.span(
274
296
  "disk_io",
@@ -106,6 +106,22 @@ def load_mapping_rules(input_path: Path) -> list[permission_types.MappingRule]:
106
106
  return mapping_rules
107
107
 
108
108
 
109
+ def resolve_mapping_rules(
110
+ provided_rules: list[permission_types.MappingRule] | None,
111
+ maps_path: Path,
112
+ ) -> list[permission_types.MappingRule]:
113
+ """Return validated in-memory mapping rules, or load them from the maps file.
114
+
115
+ In-memory rules go through the same structural validation as rules read
116
+ from YAML, so module callers and CLI operators share one contract.
117
+ """
118
+ if provided_rules is None:
119
+ return load_mapping_rules(maps_path)
120
+ if provided_rules:
121
+ permissions_mapping.validate_mapping_rules(provided_rules)
122
+ return provided_rules
123
+
124
+
109
125
  def load_mapping_context(
110
126
  client: src.SourcegraphClient,
111
127
  input_path: Path,
@@ -28,6 +28,7 @@ import yaml
28
28
  from src_py_lib.utils.config import config_options
29
29
 
30
30
  from src_auth_perms_sync import cli
31
+ from src_auth_perms_sync.permissions import types as permission_types
31
32
  from src_auth_perms_sync.shared import backups
32
33
  from src_auth_perms_sync.shared import types as shared_types
33
34
 
@@ -782,8 +783,20 @@ def cli_input_for_case(
782
783
 
783
784
 
784
785
  def run_fixture_case(
785
- case_name: str, runner: str = "cli", *, no_files: bool = False
786
+ case_name: str,
787
+ runner: str = "cli",
788
+ *,
789
+ no_files: bool = False,
790
+ mapping_rules: list[permission_types.MappingRule] | None = None,
791
+ preserve_artifacts_into: Path | None = None,
786
792
  ) -> FixtureRunResult:
793
+ """Run one fixture case; `mapping_rules` exercises the in-memory Set path.
794
+
795
+ With `mapping_rules` set, the case's maps file is NOT passed to the run
796
+ (maps_path is cleared), proving the rules alone drive the command.
797
+ `preserve_artifacts_into` routes run artifacts to a caller-owned
798
+ directory (not cleaned up here) so tests can inspect their contents.
799
+ """
787
800
  case = load_e2e_cases()[case_name]
788
801
  case_dir = FIXTURES_DIR / case_name
789
802
  before_state = load_state(case_dir / "before.json")
@@ -799,9 +812,16 @@ def run_fixture_case(
799
812
  # Route run artifacts (snapshots, maps copies, generated maps.yaml) into
800
813
  # a per-case temporary directory so local test runs never pollute the
801
814
  # repo's ./src-auth-perms-sync-runs tree; the directory is removed when
802
- # the case finishes.
803
- with tempfile.TemporaryDirectory(prefix="src-auth-perms-sync-case-") as temp_directory:
804
- artifacts_dir = Path(temp_directory)
815
+ # the case finishes (unless the caller supplied its own directory).
816
+ with contextlib.ExitStack() as artifact_stack:
817
+ if preserve_artifacts_into is not None:
818
+ artifacts_dir = preserve_artifacts_into
819
+ else:
820
+ artifacts_dir = Path(
821
+ artifact_stack.enter_context(
822
+ tempfile.TemporaryDirectory(prefix="src-auth-perms-sync-case-")
823
+ )
824
+ )
805
825
  try:
806
826
  cli_input = cli_input_for_case(case, case_name, client.endpoint, runner)
807
827
  # Local runs execute in-process against the in-memory fake, where
@@ -811,6 +831,8 @@ def run_fixture_case(
811
831
  config_updates: dict[str, object] = {"parallelism": 1}
812
832
  if no_files:
813
833
  config_updates["no_files"] = True
834
+ if mapping_rules is not None:
835
+ config_updates["maps_path"] = None
814
836
  local_config = cli_input.config.model_copy(update=config_updates)
815
837
  command = cli.resolve_command(cli_input.command_name, local_config)
816
838
  run_paths = backups.resolve_run_paths(
@@ -827,6 +849,7 @@ def run_fixture_case(
827
849
  cast(src.SourcegraphClient, client),
828
850
  run_paths,
829
851
  worker_pool,
852
+ mapping_rules=mapping_rules,
830
853
  )
831
854
  except SystemExit as exception:
832
855
  command_failure = f"SystemExit: {exception.code!r}"
@@ -8,9 +8,16 @@ structurally validated, including the live/performance ones.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import tempfile
11
12
  import unittest
13
+ from pathlib import Path
14
+ from typing import cast
15
+
16
+ import yaml
12
17
 
13
18
  from src_auth_perms_sync import cli
19
+ from src_auth_perms_sync.permissions import types as permission_types
20
+ from src_auth_perms_sync.permissions.workflow import load_mapping_rules
14
21
  from tests.e2e.case_runner import (
15
22
  FIXTURES_DIR,
16
23
  case_cli_arguments,
@@ -93,6 +100,83 @@ class LocalCaseTests(unittest.TestCase):
93
100
  with self.subTest(case=case_name):
94
101
  self.assertEqual("", run_local_replay_case(case_name))
95
102
 
103
+ def test_in_memory_mapping_rules_match_maps_file(self) -> None:
104
+ """In-memory rules must drive set exactly like the same rules from YAML.
105
+
106
+ The same fixture case runs through the import API three ways: from
107
+ its maps.yaml file, from the equivalent parsed rules in memory (no
108
+ maps file passed at all), and from in-memory rules with no_files.
109
+ All three must plan identical mutations and end in identical
110
+ instance state. The files-enabled in-memory run must write the
111
+ rules it actually used into the run directory as the audit copy,
112
+ and the no_files run must write nothing.
113
+ """
114
+ for case_name in ("full-overwrite-dry-run", "full-overwrite-unions"):
115
+ with self.subTest(case=case_name):
116
+ mapping_rules = load_mapping_rules(FIXTURES_DIR / case_name / "maps.yaml")
117
+ self.assertTrue(mapping_rules, "fixture case must define mapping rules")
118
+
119
+ from_file = run_fixture_case(case_name, "import")
120
+ from_memory = run_fixture_case(case_name, "import", mapping_rules=mapping_rules)
121
+ from_memory_no_files = run_fixture_case(
122
+ case_name,
123
+ "import",
124
+ mapping_rules=mapping_rules,
125
+ no_files=True,
126
+ )
127
+
128
+ self.assertIsNone(from_file.failure)
129
+ self.assertIsNone(from_memory.failure)
130
+ self.assertIsNone(from_memory_no_files.failure)
131
+ self.assertEqual(from_file.actual_mutations, from_memory.actual_mutations)
132
+ self.assertEqual(from_file.actual_state, from_memory.actual_state)
133
+ self.assertEqual(from_file.actual_mutations, from_memory_no_files.actual_mutations)
134
+ self.assertEqual(from_file.actual_state, from_memory_no_files.actual_state)
135
+
136
+ audit_copies = [
137
+ name for name in from_memory.artifact_file_names if name.endswith("maps.yaml")
138
+ ]
139
+ self.assertTrue(
140
+ audit_copies,
141
+ "in-memory run with files enabled must write the rules audit copy",
142
+ )
143
+ self.assertEqual(
144
+ (),
145
+ from_memory_no_files.artifact_file_names,
146
+ "in-memory run with no_files must write nothing",
147
+ )
148
+
149
+ def test_in_memory_mapping_rules_audit_copy_contains_rules_used(self) -> None:
150
+ """The audit maps.yaml must contain exactly the in-memory rules used."""
151
+ case_name = "full-overwrite-dry-run"
152
+ mapping_rules = load_mapping_rules(FIXTURES_DIR / case_name / "maps.yaml")
153
+
154
+ with tempfile.TemporaryDirectory(prefix="rules-audit-") as temp_directory:
155
+ audit_directory = Path(temp_directory)
156
+ result = run_fixture_case(
157
+ case_name,
158
+ "import",
159
+ mapping_rules=mapping_rules,
160
+ preserve_artifacts_into=audit_directory,
161
+ )
162
+ self.assertIsNone(result.failure)
163
+ audit_copies = sorted(audit_directory.rglob("maps.yaml"))
164
+ self.assertEqual(1, len(audit_copies), "expected exactly one rules audit copy")
165
+ audit_content = yaml.safe_load(audit_copies[0].read_text())
166
+ self.assertEqual({"maps": mapping_rules}, audit_content)
167
+
168
+ def test_in_memory_mapping_rules_reject_invalid_structure(self) -> None:
169
+ """Structurally invalid in-memory rules fail fast, before any mutation."""
170
+ case_name = "full-overwrite-unions"
171
+ invalid_rules = [{"name": "Broken", "users": {"unknownField": ["x"]}}]
172
+ result = run_fixture_case(
173
+ case_name,
174
+ "import",
175
+ mapping_rules=cast("list[permission_types.MappingRule]", invalid_rules),
176
+ )
177
+ self.assertIsNotNone(result.failure)
178
+ self.assertEqual(0, result.actual_mutations, "invalid rules must mutate nothing")
179
+
96
180
  def test_no_files_set_dry_run_matches_files_enabled(self) -> None:
97
181
  """no_files must not change a set dry-run's decisions, and writes nothing.
98
182