-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstate.rs
More file actions
180 lines (159 loc) · 6.28 KB
/
Copy pathstate.rs
File metadata and controls
180 lines (159 loc) · 6.28 KB
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
180
//! On-disk org-mode enrollment record.
//!
//! Persisted at `~/.aperion-shield/orgmode.json` with mode 0600. Stores
//! everything `aperion-shield` needs to continue talking to Smartflow
//! across restarts: the virtual key (treat as bearer secret), device id,
//! policy group, and the smartflow base URL.
use std::path::PathBuf;
use anyhow::{anyhow, Context};
use serde::{Deserialize, Serialize};
/// Filename relative to the user's `~/.aperion-shield/` directory.
pub const ORG_STATE_FILE: &str = "orgmode.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrgState {
/// Base URL of the Smartflow control plane, e.g.
/// `https://smartflow.langsmart.app`. Used for every REST call.
pub smartflow_url: String,
/// Virtual key issued by `enterprise_device_api::token_enroll`.
/// Sent as `Authorization: Bearer <vkey>` on every request.
pub vkey: String,
/// Server-assigned device id (uuid v4).
pub device_id: String,
/// Policy group this device is bound to. Used to fetch the right
/// shieldset from `/api/enterprise/shield/shieldset/<group>`.
pub policy_group: String,
/// Original enrolling user email (informational; the dashboard
/// shows it in the fleet view).
#[serde(default)]
pub owner_email: Option<String>,
/// RFC 3339 timestamp of when this device was enrolled.
pub enrolled_at: String,
/// Device platform string sent at enrollment time -- "macos",
/// "linux", or "windows". Drives policy group resolution on the
/// server.
pub platform: String,
/// Friendly device name shown in the fleet view. Defaults to the
/// machine's hostname.
pub device_name: String,
/// Hashed device fingerprint -- prevents the server from issuing
/// two records for the same physical machine if the user re-enrolls.
pub device_fingerprint: String,
}
impl OrgState {
/// Resolve `~/.aperion-shield/orgmode.json`. Honour the
/// `APERION_SHIELD_HOME` env override so tests don't write into
/// the real user home.
pub fn default_path() -> anyhow::Result<PathBuf> {
let dir = if let Ok(custom) = std::env::var("APERION_SHIELD_HOME") {
PathBuf::from(custom)
} else {
let mut home = dirs::home_dir()
.ok_or_else(|| anyhow!("could not resolve home directory"))?;
home.push(".aperion-shield");
home
};
std::fs::create_dir_all(&dir).context("create ~/.aperion-shield/")?;
Ok(dir.join(ORG_STATE_FILE))
}
/// Load if present; `Ok(None)` means "not enrolled" (the normal
/// standalone path).
pub fn load() -> anyhow::Result<Option<Self>> {
let path = Self::default_path()?;
if !path.exists() {
return Ok(None);
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let state: OrgState =
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
Ok(Some(state))
}
/// Persist atomically with mode 0600 on Unix.
pub fn save(&self) -> anyhow::Result<()> {
let path = Self::default_path()?;
let tmp = path.with_extension("json.tmp");
let json = serde_json::to_string_pretty(self)?;
std::fs::write(&tmp, json).with_context(|| format!("write {}", tmp.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&tmp)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(&tmp, perms)?;
}
std::fs::rename(&tmp, &path).with_context(|| format!("rename {}", path.display()))?;
Ok(())
}
/// Remove the on-disk file. Used by `aperion-shield disenroll`.
pub fn remove() -> anyhow::Result<()> {
let path = Self::default_path()?;
if path.exists() {
std::fs::remove_file(&path)
.with_context(|| format!("remove {}", path.display()))?;
}
Ok(())
}
/// Derive a fingerprint that's stable across re-enrolls on the
/// same physical machine but doesn't leak anything sensitive.
/// SHA-256 of `<hostname>|<os-name>|<machine-id-if-available>`.
pub fn fingerprint() -> String {
use sha2::{Digest, Sha256};
let hostname = hostname_string();
let os = std::env::consts::OS.to_string();
let machine_id = machine_id_string();
let mut hasher = Sha256::new();
hasher.update(format!("{}|{}|{}", hostname, os, machine_id).as_bytes());
hex::encode(hasher.finalize())
}
}
fn hostname_string() -> String {
// Fallback chain: HOSTNAME env, then uname-style read, then "unknown".
std::env::var("HOSTNAME")
.ok()
.or_else(|| std::env::var("COMPUTERNAME").ok())
.or_else(|| {
std::process::Command::new("hostname")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| "unknown".to_string())
}
fn machine_id_string() -> String {
// Best-effort. On Linux /etc/machine-id is universal; on macOS we
// hash IOPlatformUUID; on Windows we use the registry's MachineGuid
// (skip Windows for now -- not deployed).
if let Ok(s) = std::fs::read_to_string("/etc/machine-id") {
return s.trim().to_string();
}
if cfg!(target_os = "macos") {
if let Ok(out) = std::process::Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
if let Ok(s) = String::from_utf8(out.stdout) {
for line in s.lines() {
if let Some(idx) = line.find("IOPlatformUUID") {
if let Some(uuid) = line[idx..].split('"').nth(3) {
return uuid.to_string();
}
}
}
}
}
}
"unknown".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_stable() {
let a = OrgState::fingerprint();
let b = OrgState::fingerprint();
assert_eq!(a, b);
assert_eq!(a.len(), 64);
}
}