isb_apps/app/
db_secrets.rs1use super::Apps;
5use crate::app::{AppSpec, Source, git};
6use crate::error::{Error, Result};
7use crate::org::OrgId;
8
9impl Apps {
10 pub(super) fn database_credentials(
14 &self,
15 org: &OrgId,
16 spec: &AppSpec,
17 db: &crate::app::DatabaseSource,
18 ) -> Result<()> {
19 use crate::app::database as d;
20 let mut names = vec![d::password_secret(&spec.name)];
21 if db.engine.has_root_password() {
22 names.push(d::root_password_secret(&spec.name));
23 }
24 for n in &names {
25 match self.inner.secrets.inspect(org, n) {
26 Ok(_) => {}
27 Err(e) if e.is_not_found() => {
28 self.inner
29 .secrets
30 .set(org, n, git::random_hex(16).as_bytes())?;
31 }
32 Err(e) => return Err(e),
33 }
34 }
35 self.write_urls(org, spec, db)?;
36 Ok(())
37 }
38
39 fn write_urls(
42 &self,
43 org: &OrgId,
44 spec: &AppSpec,
45 db: &crate::app::DatabaseSource,
46 ) -> Result<Vec<String>> {
47 use crate::app::database as d;
48 let (pw, _) = self
49 .inner
50 .secrets
51 .get(org, &d::password_secret(&spec.name))?;
52 let pw = String::from_utf8(pw).map_err(|_| Error::invalid("the password is not text"))?;
53 let url = d::internal_url(spec, db, pw.trim());
54 let wanted = std::iter::once((d::url_secret(&spec.name), url.clone())).chain(
55 db.urls
56 .iter()
57 .map(|(n, q)| (n.clone(), d::with_query(&url, q))),
58 );
59 let mut moved = Vec::new();
60 for (name, value) in wanted {
61 let before = self.inner.secrets.version(org, &name).ok();
62 let m = self.inner.secrets.put(org, &name, value.as_bytes())?;
63 if Some(m.version) != before {
64 moved.push(m.name);
65 }
66 }
67 Ok(moved)
68 }
69
70 pub fn database_password_changed(&self, org: &OrgId, name: &str) -> Result<Vec<String>> {
75 let Some(app) = name
76 .strip_prefix("db.")
77 .and_then(|r| r.strip_suffix(".password"))
78 else {
79 return Ok(vec![]);
80 };
81 let a = match self.get(org, app) {
82 Ok(a) => a,
83 Err(e) if e.is_not_found() => return Ok(vec![]),
84 Err(e) => return Err(e),
85 };
86 let Source::Database(db) = &a.spec.source else {
87 return Ok(vec![]);
88 };
89 self.write_urls(org, &a.spec, db)
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use std::sync::Arc;
96 use std::time::Duration;
97
98 use super::*;
99 use crate::secrets::{Keyring, LocalDriver, Secrets};
100
101 #[test]
102 fn url_secrets_follow_the_password() {
103 let dir = tempfile::tempdir().unwrap();
104 let k = Keyring::new(age::x25519::Identity::generate(), vec![]);
105 let secrets = Arc::new(Secrets::new(LocalDriver::new(dir.path(), Arc::new(k))));
106 let client = crate::client::Client::with_socket("/nonexistent/isb-test/incus.sock");
107 let store = crate::stack::Store::open(dir.path()).unwrap();
108 let ctl = crate::stack::Controller::start(
109 client.clone(),
110 store,
111 Duration::from_secs(60),
112 secrets.clone(),
113 )
114 .unwrap();
115 let ap = Apps::new(dir.path(), client, ctl, secrets.clone());
116 let org = OrgId::default_org();
117 let spec: AppSpec = serde_json::from_value(serde_json::json!({
118 "name": "main-db", "project": "shop",
119 "source": {"database": {"engine": "postgres", "urls": {
120 "dsn.main-db.web": "sslmode=disable", "dsn.main-db.plain": "",
121 }}},
122 }))
123 .unwrap();
124 ap.project_create(&org, "shop", "", &[]).unwrap();
125 ap.create(&org, spec).unwrap();
126 let Source::Database(db) = &ap.get(&org, "main-db").unwrap().spec.source else {
127 panic!("a database")
128 };
129 let value = |n: &str| String::from_utf8(secrets.get(&org, n).unwrap().0).unwrap();
130 ap.database_credentials(&org, &ap.get(&org, "main-db").unwrap().spec, db)
131 .unwrap();
132 let url = value("db.main-db.url");
133 assert_eq!(value("dsn.main-db.web"), format!("{url}?sslmode=disable"));
134 assert_eq!(value("dsn.main-db.plain"), url);
135
136 secrets
137 .set(&org, "db.main-db.password", b"n3w-pass")
138 .unwrap();
139 let mut moved = ap
140 .database_password_changed(&org, "db.main-db.password")
141 .unwrap();
142 moved.sort();
143 assert_eq!(
144 moved,
145 ["db.main-db.url", "dsn.main-db.plain", "dsn.main-db.web"]
146 );
147 assert!(value("dsn.main-db.web").contains(":n3w-pass@"));
148 assert!(value("dsn.main-db.web").ends_with("?sslmode=disable"));
149 assert!(
150 ap.database_password_changed(&org, "db.main-db.password")
151 .unwrap()
152 .is_empty(),
153 "nothing moves twice"
154 );
155 assert!(
156 ap.database_password_changed(&org, "dsn.main-db.web")
157 .unwrap()
158 .is_empty()
159 );
160 }
161}