1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! Plugin attachment to a hosted mock deployment.
//!
//! Each row says: "plugin P at version V is attached to deployment D
//! with permissions G and config C." The plugin-host fetches the
//! enabled rows for its deployment on boot and on manifest reload.
//!
//! The permission grant payload (`permissions_json`) follows the
//! shape defined in `docs/plugins/security/cloud-trust-permissions-rfc.md`
//! §4.2 — strawman:
//!
//! ```json
//! {
//! "egress": { "allow": ["*.stripe.com"], "deny_all_others": true },
//! "env": { "read": ["MY_PUBLIC_FLAG"] },
//! "request": { "read_body": true, "modify_body": true,
//! "read_headers": ["x-trace-id"],
//! "modify_headers": ["x-rewritten-by"] },
//! "response": { "read_body": true, "modify_body": true,
//! "modify_status": false },
//! "storage": { "kv_namespace": null }
//! }
//! ```
//!
//! Default is deny-all (empty object). The handler that creates the
//! row validates the grant against the manifest's declared
//! capabilities — the runtime enforces `manifest ∩ grant`.
//!
//! Schema: migration 20250101000074_cloud_plugin_attachments.sql.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[cfg(feature = "postgres")]
use sqlx::{FromRow, PgPool};
#[cfg_attr(feature = "postgres", derive(FromRow))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostedMockPlugin {
pub id: Uuid,
pub deployment_id: Uuid,
pub plugin_id: Uuid,
pub plugin_version_id: Uuid,
pub config_json: serde_json::Value,
pub permissions_json: serde_json::Value,
pub enabled: bool,
pub attached_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub attached_by: Option<Uuid>,
}
#[cfg(feature = "postgres")]
pub struct AttachHostedMockPlugin<'a> {
pub deployment_id: Uuid,
pub plugin_id: Uuid,
pub plugin_version_id: Uuid,
pub config_json: &'a serde_json::Value,
pub permissions_json: &'a serde_json::Value,
pub enabled: bool,
pub attached_by: Option<Uuid>,
}
#[cfg(feature = "postgres")]
impl HostedMockPlugin {
/// Attach (or re-attach) a plugin to a deployment. UPSERT on the
/// `(deployment_id, plugin_id)` UNIQUE constraint — re-attach of
/// the same plugin updates the version, config, and grant.
pub async fn attach(pool: &PgPool, input: AttachHostedMockPlugin<'_>) -> sqlx::Result<Self> {
sqlx::query_as::<_, Self>(
r#"
INSERT INTO hosted_mock_plugins (
deployment_id, plugin_id, plugin_version_id,
config_json, permissions_json, enabled, attached_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (deployment_id, plugin_id) DO UPDATE SET
plugin_version_id = EXCLUDED.plugin_version_id,
config_json = EXCLUDED.config_json,
permissions_json = EXCLUDED.permissions_json,
enabled = EXCLUDED.enabled,
attached_by = EXCLUDED.attached_by,
updated_at = NOW()
RETURNING *
"#,
)
.bind(input.deployment_id)
.bind(input.plugin_id)
.bind(input.plugin_version_id)
.bind(input.config_json)
.bind(input.permissions_json)
.bind(input.enabled)
.bind(input.attached_by)
.fetch_one(pool)
.await
}
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<Self>> {
sqlx::query_as::<_, Self>("SELECT * FROM hosted_mock_plugins WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await
}
pub async fn list_by_deployment(pool: &PgPool, deployment_id: Uuid) -> sqlx::Result<Vec<Self>> {
sqlx::query_as::<_, Self>(
r#"
SELECT * FROM hosted_mock_plugins
WHERE deployment_id = $1
ORDER BY attached_at ASC
"#,
)
.bind(deployment_id)
.fetch_all(pool)
.await
}
/// Enabled-only listing. The plugin-host calls this on boot to
/// build its load manifest.
pub async fn list_enabled_by_deployment(
pool: &PgPool,
deployment_id: Uuid,
) -> sqlx::Result<Vec<Self>> {
sqlx::query_as::<_, Self>(
r#"
SELECT * FROM hosted_mock_plugins
WHERE deployment_id = $1 AND enabled = TRUE
ORDER BY attached_at ASC
"#,
)
.bind(deployment_id)
.fetch_all(pool)
.await
}
pub async fn count_active_by_deployment(
pool: &PgPool,
deployment_id: Uuid,
) -> sqlx::Result<i64> {
let row: (Option<i64>,) = sqlx::query_as(
r#"
SELECT COUNT(*)::BIGINT
FROM hosted_mock_plugins
WHERE deployment_id = $1 AND enabled = TRUE
"#,
)
.bind(deployment_id)
.fetch_one(pool)
.await?;
Ok(row.0.unwrap_or(0))
}
/// Soft toggle. Detach (hard delete) is `delete`.
pub async fn set_enabled(pool: &PgPool, id: Uuid, enabled: bool) -> sqlx::Result<Option<Self>> {
sqlx::query_as::<_, Self>(
r#"
UPDATE hosted_mock_plugins
SET enabled = $2,
updated_at = NOW()
WHERE id = $1
RETURNING *
"#,
)
.bind(id)
.bind(enabled)
.fetch_optional(pool)
.await
}
/// Hard detach. Audit trail is preserved separately via
/// `audit_logs` (event type `plugin_detached`); this row goes away.
pub async fn delete(pool: &PgPool, id: Uuid) -> sqlx::Result<bool> {
let rows = sqlx::query("DELETE FROM hosted_mock_plugins WHERE id = $1")
.bind(id)
.execute(pool)
.await?
.rows_affected();
Ok(rows > 0)
}
}