Repository navigation
feat: add authz schema pipeline and load_authz_schema command #446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3ef73d2
feat: add authz schema pipeline and load_authz_schema command
rodmgwgu 94ecba7
squash!: Fix rebase issues
rodmgwgu 85d55a8
squash!: Deduplicate distribution resolution warnings
rodmgwgu d260fef
squash!: Correct package naming
rodmgwgu 58d672e
squash!: Reformat outputs
rodmgwgu 08ffa84
squash!: Output improvements
rodmgwgu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| """End-to-end orchestration of the authz schema lifecycle (ADR 0018). | ||
|
|
||
| :class:`SchemaPipeline` wires the steps together: | ||
|
|
||
| discover -> load -> validate -> compile -> render -> (plan) -> apply | ||
|
|
||
| The Casbin-free steps (discover..compile) live in :mod:`openedx_authz.engine.schema`; | ||
| render/apply live in :mod:`openedx_authz.engine.renderer`. This orchestrator is | ||
| the single entry point used by the deployment management command and by tests. | ||
|
|
||
| Deployment runs discover-through-apply before the application serves traffic | ||
| (ADR 0018 §2). CI/local runs may stop after ``plan`` for a dry run, or pass | ||
| explicit resources. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
|
|
||
| from openedx_authz.engine.renderer import ( | ||
| ApplyResult, | ||
| ChangePlan, | ||
| PolicyRenderer, | ||
| SchemaApplier, | ||
| ) | ||
| from openedx_authz.engine.schema.compilation import SchemaCompiler | ||
| from openedx_authz.engine.schema.discovery import SchemaDiscovery | ||
| from openedx_authz.engine.schema.exceptions import SchemaValidationError | ||
| from openedx_authz.engine.schema.loading import SchemaLoader | ||
| from openedx_authz.engine.schema.types import CompiledSchema | ||
| from openedx_authz.engine.schema.validation import SchemaValidator, ValidationIssue | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class SchemaPipeline: | ||
| """Runs the schema lifecycle from discovery through apply. | ||
|
|
||
| Components are injected for testability; each defaults to its standard | ||
| implementation. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| discovery: SchemaDiscovery | None = None, | ||
| loader: SchemaLoader | None = None, | ||
| validator: SchemaValidator | None = None, | ||
| compiler: SchemaCompiler | None = None, | ||
| renderer: PolicyRenderer | None = None, | ||
| applier: SchemaApplier | None = None, | ||
| ): | ||
| self._discovery = discovery or SchemaDiscovery() | ||
| self._loader = loader or SchemaLoader() | ||
| self._validator = validator or SchemaValidator() | ||
| self._compiler = compiler or SchemaCompiler() | ||
| self._renderer = renderer or PolicyRenderer() | ||
| self._applier = applier or SchemaApplier() | ||
|
|
||
| def compile(self) -> CompiledSchema: | ||
| """Run discover -> load -> validate -> compile and return the result. | ||
|
|
||
| Validation gates twice: once on the loaded documents, then again on the | ||
| compiled schema, because extensions and priority resolution can only be | ||
| checked after they are applied (ADR 0017 §4). | ||
|
|
||
| Raises: | ||
| SchemaValidationError: If either validation pass finds error-level | ||
| issues. | ||
| SchemaCompileError: On an unresolvable conflict. | ||
| """ | ||
| resources = self._discovery.discover() | ||
| documents = self._loader.load(resources) | ||
|
|
||
| self._gate(self._validator.validate(documents)) | ||
| schema = self._compiler.compile(documents) | ||
| self._gate(self._validator.validate_compiled(schema)) | ||
|
|
||
| return schema | ||
|
|
||
| def _gate(self, issues: list[ValidationIssue]) -> None: | ||
| """Report every issue, then stop the run if any is error-level. | ||
|
|
||
| Warnings are logged and the run continues; errors are logged and raised | ||
| together so the deployment report lists all of them at once. | ||
| """ | ||
| for issue in issues: | ||
| log = logger.error if issue.is_error else logger.warning | ||
| log("authz schema %s: %s [%s]", issue.level, issue.message, issue.source_id or "-") | ||
| if self._validator.has_errors(issues): | ||
| raise SchemaValidationError([i for i in issues if i.is_error]) | ||
|
|
||
| def plan(self) -> ChangePlan: | ||
| """Run through render and produce the change report without writing. | ||
|
|
||
| Used for dry-run / CI review (ADR 0018 §6). | ||
| """ | ||
| schema = self.compile() | ||
| rendered = self._renderer.render(schema) | ||
| return self._applier.plan(rendered, schema) | ||
|
|
||
| def apply(self, *, force: bool = False) -> ApplyResult: | ||
| """Run the full lifecycle and persist the result transactionally. | ||
|
|
||
| Args: | ||
| force: Allow removal of roles that still have assignments (ADR 0018). | ||
| """ | ||
| schema = self.compile() | ||
| rendered = self._renderer.render(schema) | ||
| return self._applier.apply(rendered, schema, force=force) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would happen if two commands with different schemas (let's say one it's out of date) are executed at the same time? I guess the atomic in the previous PR would take care of that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the previous PR ensures that applying the actual change is atomic, and if two commands run at the same time, the last one finishing will win.
However there is nothing that would prevent this from happening, but given how this command is meant to be run (on deployment, usually via tutor), I don't see this happening easily.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see it more as two operators running the command from different shells at the same time, rather than necessarily two deployments, since the command can also be executed manually.
I don't think we need to prevent that from happening, but we should at least make it visible when another execution is already in progress.