Add schema apply command and policy support

This commit is contained in:
andrew 2026-04-12 04:01:14 +03:00
parent a844e0ba68
commit 92fa3189f7
22 changed files with 1903 additions and 146 deletions

View file

@ -1,6 +1,9 @@
use omnigraph::db::{GraphCommit, MergeOutcome, ReadTarget, RunRecord, Snapshot};
use omnigraph::db::{
GraphCommit, MergeOutcome, ReadTarget, RunRecord, SchemaApplyResult, Snapshot,
};
use omnigraph::error::{MergeConflict, MergeConflictKind};
use omnigraph::loader::{IngestResult, LoadMode};
use omnigraph_compiler::SchemaMigrationStep;
use omnigraph_compiler::result::QueryResult;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -243,6 +246,21 @@ pub struct ChangeRequest {
pub branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaApplyRequest {
pub schema_source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaApplyOutput {
pub uri: String,
pub supported: bool,
pub applied: bool,
pub step_count: usize,
pub manifest_version: u64,
pub steps: Vec<SchemaMigrationStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestRequest {
pub branch: Option<String>,
@ -318,6 +336,17 @@ pub fn snapshot_payload(branch: &str, snapshot: &Snapshot) -> SnapshotOutput {
}
}
pub fn schema_apply_output(uri: &str, result: SchemaApplyResult) -> SchemaApplyOutput {
SchemaApplyOutput {
uri: uri.to_string(),
supported: result.supported,
applied: result.applied,
step_count: result.steps.len(),
manifest_version: result.manifest_version,
steps: result.steps,
}
}
pub fn run_output(run: &RunRecord) -> RunOutput {
RunOutput {
run_id: run.run_id.as_str().to_string(),

View file

@ -13,8 +13,8 @@ use api::{
BranchCreateOutput, BranchCreateRequest, BranchDeleteOutput, BranchListOutput,
BranchMergeOutput, BranchMergeRequest, ChangeOutput, ChangeRequest, CommitListOutput,
CommitListQuery, ErrorCode, ErrorOutput, ExportRequest, HealthOutput, IngestOutput,
IngestRequest, ReadOutput, ReadRequest, RunListOutput, SnapshotQuery, ingest_output,
snapshot_payload,
IngestRequest, ReadOutput, ReadRequest, RunListOutput, SchemaApplyOutput, SchemaApplyRequest,
SnapshotQuery, ingest_output, schema_apply_output, snapshot_payload,
};
use axum::body::{Body, Bytes};
use axum::extract::DefaultBodyLimit;
@ -362,6 +362,7 @@ pub fn build_app(state: AppState) -> Router {
.route("/export", post(server_export))
.route("/read", post(server_read))
.route("/change", post(server_change))
.route("/schema/apply", post(server_schema_apply))
.route(
"/ingest",
post(server_ingest).layer(DefaultBodyLimit::max(INGEST_REQUEST_BODY_LIMIT_BYTES)),
@ -658,6 +659,31 @@ async fn server_change(
}))
}
async fn server_schema_apply(
State(state): State<AppState>,
actor: Option<Extension<AuthenticatedActor>>,
Json(request): Json<SchemaApplyRequest>,
) -> std::result::Result<Json<SchemaApplyOutput>, ApiError> {
let actor_id = actor.as_ref().map(|Extension(actor)| actor.as_str());
authorize_request(
&state,
actor.as_ref().map(|Extension(actor)| actor),
PolicyRequest {
actor_id: actor_id.map(str::to_string).unwrap_or_default(),
action: PolicyAction::SchemaApply,
branch: None,
target_branch: Some("main".to_string()),
},
)?;
let result = {
let mut db = Arc::clone(&state.db).write_owned().await;
db.apply_schema(&request.schema_source)
.await
.map_err(ApiError::from_omni)?
};
Ok(Json(schema_apply_output(state.uri(), result)))
}
async fn server_ingest(
State(state): State<AppState>,
actor: Option<Extension<AuthenticatedActor>>,

View file

@ -19,6 +19,7 @@ pub enum PolicyAction {
Read,
Export,
Change,
SchemaApply,
BranchCreate,
BranchDelete,
BranchMerge,
@ -33,6 +34,7 @@ impl PolicyAction {
Self::Read => "read",
Self::Export => "export",
Self::Change => "change",
Self::SchemaApply => "schema_apply",
Self::BranchCreate => "branch_create",
Self::BranchDelete => "branch_delete",
Self::BranchMerge => "branch_merge",
@ -50,6 +52,7 @@ impl PolicyAction {
matches!(
self,
Self::BranchCreate
| Self::SchemaApply
| Self::BranchDelete
| Self::BranchMerge
| Self::RunPublish
@ -72,6 +75,7 @@ impl FromStr for PolicyAction {
"read" => Ok(Self::Read),
"export" => Ok(Self::Export),
"change" => Ok(Self::Change),
"schema_apply" => Ok(Self::SchemaApply),
"branch_create" => Ok(Self::BranchCreate),
"branch_delete" => Ok(Self::BranchDelete),
"branch_merge" => Ok(Self::BranchMerge),
@ -591,6 +595,7 @@ namespace Omnigraph {
action "read" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
action "export" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
action "change" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
action "schema_apply" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
action "branch_create" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
action "branch_delete" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
action "branch_merge" appliesTo { principal: Actor, resource: Repo, context: RequestContext };
@ -809,4 +814,44 @@ rules:
engine.run_tests(&tests).unwrap();
}
#[test]
fn schema_apply_uses_target_branch_scope() {
let policy: PolicyConfig = serde_yaml::from_str(
r#"
version: 1
groups:
admins: [act-ragnor]
protected_branches: [main]
rules:
- id: admins-schema-apply
allow:
actors: { group: admins }
actions: [schema_apply]
target_branch_scope: protected
"#,
)
.unwrap();
let engine = PolicyCompiler::compile(&policy, "repo").unwrap();
let allow = engine
.authorize(&PolicyRequest {
actor_id: "act-ragnor".to_string(),
action: PolicyAction::SchemaApply,
branch: None,
target_branch: Some("main".to_string()),
})
.unwrap();
assert!(allow.allowed);
let deny = engine
.authorize(&PolicyRequest {
actor_id: "act-ragnor".to_string(),
action: PolicyAction::SchemaApply,
branch: None,
target_branch: Some("feature".to_string()),
})
.unwrap();
assert!(!deny.allowed);
}
}