| 4ac01a73 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
use crate::db::{now_ms, Pool}; |
| 10 |
use crate::levels::{self, Scope}; |
| 11 |
use crate::Shared; |
| 12 |
use sqlx::FromRow; |
| 13 |
|
| 14 |
|
| 15 |
pub const MAX_PER_LEVEL: usize = 100; |
| 16 |
|
| 17 |
pub const MAX_VALUE: usize = 64 * 1024; |
| 18 |
|
| 19 |
const MIN_MASKED: usize = 4; |
| 20 |
|
| 21 |
#[derive(Clone, Debug, FromRow)] |
| 22 |
pub struct Secret { |
| 23 |
pub scope: String, |
| 24 |
pub scope_id: i64, |
| 25 |
pub name: String, |
| 26 |
|
| 27 |
pub value: Vec<u8>, |
| 28 |
pub updated_ms: i64, |
| 29 |
} |
| 30 |
|
| 31 |
|
| 32 |
pub fn name_ok(name: &str) -> bool { |
| 33 |
!name.is_empty() && name.len() <= 64 && name.bytes().all(|b| b.is_ascii_alphanumeric() || b".-_".contains(&b)) |
| 34 |
} |
| 35 |
|
| 36 |
pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result<Vec<Secret>> { |
| 5d90e0e0 Andy McCall |
37 |
sqlx::query_as("SELECT * FROM flow_secrets WHERE scope = $1 AND scope_id = $2 ORDER BY name") |
| 4ac01a73 Andy McCall ↶ |
38 |
.bind(scope.kind()) |
| 39 |
.bind(scope.id()) |
| 40 |
.fetch_all(pool) |
| 41 |
.await |
| 42 |
} |
| 43 |
|
| 44 |
|
| 45 |
pub async fn set(pool: &Pool, scope: Scope, name: &str, sealed: &[u8]) -> sqlx::Result<()> { |
| 46 |
sqlx::query( |
| 5d90e0e0 Andy McCall |
47 |
"INSERT INTO flow_secrets (scope, scope_id, name, value, updated_ms) VALUES ($1, $2, $3, $4, $5) \ |
| 4ac01a73 Andy McCall ↶ |
48 |
ON CONFLICT (scope, scope_id, name) DO UPDATE SET value = excluded.value, updated_ms = excluded.updated_ms", |
| 49 |
) |
| 50 |
.bind(scope.kind()) |
| 51 |
.bind(scope.id()) |
| 52 |
.bind(name) |
| 53 |
.bind(sealed) |
| 54 |
.bind(now_ms()) |
| 55 |
.execute(pool) |
| 56 |
.await?; |
| 57 |
Ok(()) |
| 58 |
} |
| 59 |
|
| 60 |
|
| 61 |
pub async fn delete(pool: &Pool, scope: Scope, name: &str) -> sqlx::Result<bool> { |
| 5d90e0e0 Andy McCall |
62 |
let r = sqlx::query("DELETE FROM flow_secrets WHERE scope = $1 AND scope_id = $2 AND name = $3") |
| 4ac01a73 Andy McCall ↶ |
63 |
.bind(scope.kind()) |
| 64 |
.bind(scope.id()) |
| 65 |
.bind(name) |
| 66 |
.execute(pool) |
| 67 |
.await?; |
| 68 |
Ok(r.rows_affected() == 1) |
| 69 |
} |
| 70 |
|
| 71 |
|
| 72 |
pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> { |
| 5d90e0e0 Andy McCall |
73 |
sqlx::query("DELETE FROM flow_secrets WHERE scope = $1 AND scope_id = $2").bind(scope.kind()).bind(scope.id()).execute(pool).await?; |
| 4ac01a73 Andy McCall ↶ |
74 |
Ok(()) |
| 75 |
} |
| 76 |
|
| 77 |
|
| 78 |
|
| 79 |
#[derive(Clone, Debug)] |
| 80 |
pub struct Seen { |
| 81 |
pub secret: Secret, |
| 82 |
|
| 83 |
pub from: String, |
| 84 |
pub overridden: bool, |
| 85 |
} |
| 86 |
|
| 87 |
|
| 88 |
pub async fn seen(pool: &Pool, scope: Scope, owner: &str) -> sqlx::Result<Vec<Seen>> { |
| 89 |
let mut lvls = levels::above(pool, scope, owner).await?; |
| 90 |
lvls.push((scope, String::new())); |
| 91 |
let mut out: Vec<Seen> = vec![]; |
| 92 |
for (level, from) in lvls { |
| 93 |
for s in list(pool, level).await? { |
| 94 |
for o in out.iter_mut().filter(|o| o.secret.name == s.name) { |
| 95 |
o.overridden = true; |
| 96 |
} |
| 97 |
out.push(Seen { secret: s, from: from.clone(), overridden: false }); |
| 98 |
} |
| 99 |
} |
| 100 |
Ok(out) |
| 101 |
} |
| 102 |
|
| 103 |
|
| 104 |
|
| 105 |
|
| 106 |
pub async fn for_job(app: &Shared, repo: &crate::db::repos::RepoRow, names: &[String]) -> sqlx::Result<Vec<(String, String)>> { |
| 107 |
let all = seen(&app.db, Scope::Repo(repo.id), &repo.owner).await?; |
| 108 |
let mut out = vec![]; |
| 109 |
for name in names { |
| 110 |
if let Some(s) = all.iter().find(|s| !s.overridden && s.secret.name == *name) { |
| 111 |
if let Some(value) = app.secret_key.open(&s.secret.value) { |
| 112 |
out.push((name.clone(), value)); |
| 113 |
} |
| 114 |
} |
| 115 |
} |
| 116 |
Ok(out) |
| 117 |
} |
| 118 |
|
| 119 |
|
| 120 |
|
| 121 |
pub fn mask(text: &str, values: &[String]) -> String { |
| 122 |
let mut out = text.to_string(); |
| 123 |
for v in values { |
| 124 |
if v.len() < MIN_MASKED { |
| 125 |
continue; |
| 126 |
} |
| 127 |
out = out.replace(v.as_str(), "***"); |
| 128 |
let b64 = base64_standard(v.as_bytes()); |
| 129 |
if b64.len() >= MIN_MASKED { |
| 130 |
out = out.replace(&b64, "***"); |
| 131 |
} |
| 132 |
} |
| 133 |
out |
| 134 |
} |
| 135 |
|
| 136 |
fn base64_standard(bytes: &[u8]) -> String { |
| 137 |
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
| 138 |
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); |
| 139 |
for chunk in bytes.chunks(3) { |
| 140 |
let n = chunk.iter().fold(0u32, |acc, b| (acc << 8) | *b as u32) << (8 * (3 - chunk.len())); |
| 141 |
for i in 0..4 { |
| 142 |
if i <= chunk.len() { |
| 143 |
out.push(T[((n >> (18 - 6 * i)) & 63) as usize] as char); |
| 144 |
} else { |
| 145 |
out.push('='); |
| 146 |
} |
| 147 |
} |
| 148 |
} |
| 149 |
out |
| 150 |
} |
| 151 |
|
| 152 |
#[cfg(test)] |
| 153 |
mod tests { |
| 154 |
use super::*; |
| 155 |
|
| 156 |
#[test] |
| 157 |
fn values_and_their_base64_are_masked_in_logs() { |
| 158 |
assert_eq!(base64_standard(b"hunter22"), "aHVudGVyMjI="); |
| 159 |
let line = "token=hunter22 b64=aHVudGVyMjI= short=ab done"; |
| 160 |
assert_eq!(mask(line, &["hunter22".into(), "ab".into()]), "token=*** b64=*** short=ab done"); |
| 161 |
assert!(name_ok("deploy-token") && name_ok("aws.key_1") && !name_ok("") && !name_ok("a b") && !name_ok(&"x".repeat(65))); |
| 162 |
} |
| 163 |
} |