Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/openshell-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ struct CredInjectDef {
struct CredInjectHeaderDef {
header: String,
from_credential: String,
// openlock fork delta: literal prefix prepended to the resolved credential
// value (e.g. "Bearer "). Optional — empty/absent = no prefix (back-compat).
#[serde(default, skip_serializing_if = "String::is_empty")]
value_prefix: String,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -392,6 +396,7 @@ fn to_proto(raw: PolicyFile) -> SandboxPolicy {
.map(|h| CredInjectHeader {
header: h.header,
from_credential: h.from_credential,
value_prefix: h.value_prefix,
})
.collect(),
}),
Expand Down Expand Up @@ -574,6 +579,7 @@ fn from_proto(policy: &SandboxPolicy) -> PolicyFile {
.map(|h| CredInjectHeaderDef {
header: h.header.clone(),
from_credential: h.from_credential.clone(),
value_prefix: h.value_prefix.clone(),
})
.collect(),
}),
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-sandbox/src/l7/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,12 @@ fn get_inject_array(val: &regorus::Value, key: &str) -> Vec<crate::secrets::Cred
.filter_map(|item| {
let header = get_object_str(item, "header")?;
let from_credential = get_object_str(item, "from_credential")?;
// Absent/empty prefix = no prefix (back-compat).
let value_prefix = get_object_str(item, "value_prefix").unwrap_or_default();
Some(crate::secrets::CredInjectDirective {
header,
from_credential,
value_prefix,
})
})
.collect(),
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-sandbox/src/l7/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5124,6 +5124,7 @@ mod tests {
inject: vec![crate::secrets::CredInjectDirective {
header: "x-api-key".to_string(),
from_credential: "ANTHROPIC_API_KEY".to_string(),
value_prefix: String::new(),
}],
};

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-sandbox/src/opa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@ fn proto_to_opa_data_json(proto: &ProtoSandboxPolicy, entrypoint_pid: u32) -> St
serde_json::json!({
"header": h.header,
"from_credential": h.from_credential,
"value_prefix": h.value_prefix,
})
})
.collect();
Expand Down
54 changes: 50 additions & 4 deletions crates/openshell-sandbox/src/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub struct RewriteResult {
pub struct CredInjectDirective {
pub header: String,
pub from_credential: String,
/// Literal prefix prepended to the resolved credential value when composing
/// the injected header (e.g. `"Bearer "`). Empty string = no prefix
/// (back-compat for directives that store a pre-prefixed value).
pub value_prefix: String,
}

/// Result of rewriting a request target for OPA evaluation.
Expand Down Expand Up @@ -1096,14 +1100,14 @@ pub fn apply_cred_inject(
};

// Pre-resolve all inject directives up front (fail-closed).
let mut resolved_injections: Vec<(&str, &str)> = Vec::with_capacity(inject.len());
let mut resolved_injections: Vec<(&str, &str, &str)> = Vec::with_capacity(inject.len());
for directive in inject {
let value = resolver
.resolve_by_env_key(&directive.from_credential)
.ok_or(UnresolvedPlaceholderError {
location: "cred_inject",
})?;
resolved_injections.push((&directive.header, value));
resolved_injections.push((&directive.header, &directive.value_prefix, value));
}

let mut output = Vec::with_capacity(raw.len() + inject.len() * 64);
Expand All @@ -1128,10 +1132,12 @@ pub fn apply_cred_inject(
output.extend_from_slice(b"\r\n");
}

// Append injected headers.
for (header, value) in &resolved_injections {
// Append injected headers. The value_prefix (e.g. "Bearer ") is prepended
// to the resolved credential value; an empty prefix is a no-op (back-compat).
for (header, value_prefix, value) in &resolved_injections {
output.extend_from_slice(header.as_bytes());
output.extend_from_slice(b": ");
output.extend_from_slice(value_prefix.as_bytes());
output.extend_from_slice(value.as_bytes());
output.extend_from_slice(b"\r\n");
}
Expand Down Expand Up @@ -1213,6 +1219,41 @@ mod tests {
);
}

#[test]
fn cred_inject_applies_value_prefix() {
// Provider stores a RAW token (no "Bearer " prefix). cred_inject must
// prepend the literal prefix when composing the injected header so that
// `Authorization: Bearer <token>` is emitted from a raw stored token.
let (_, resolver) = SecretResolver::from_provider_env(
[(
"ANTHROPIC_BEARER_TOKEN".to_string(),
"sk-ant-oat01-REAL".to_string(),
)]
.into_iter()
.collect(),
);
let resolver = resolver.expect("resolver");

let inject = vec![CredInjectDirective {
header: "Authorization".to_string(),
from_credential: "ANTHROPIC_BEARER_TOKEN".to_string(),
value_prefix: "Bearer ".to_string(),
}];

let out = apply_cred_inject(
b"GET / HTTP/1.1\r\nHost: x\r\n\r\n",
&["Authorization".to_string()],
&inject,
&resolver,
)
.expect("should succeed");
let text = String::from_utf8(out).expect("utf8");
assert!(
text.contains("Authorization: Bearer sk-ant-oat01-REAL"),
"got: {text}"
);
}

#[test]
fn rewrites_provider_shaped_alias_header_values() {
let (_, resolver) = SecretResolver::from_provider_env(
Expand Down Expand Up @@ -2274,6 +2315,7 @@ mod tests {
let inject = vec![CredInjectDirective {
header: "x-api-key".to_string(),
from_credential: "ANTHROPIC_API_KEY".to_string(),
value_prefix: String::new(),
}];

let result =
Expand Down Expand Up @@ -2332,6 +2374,7 @@ mod tests {
let inject = vec![CredInjectDirective {
header: "x-api-key".to_string(),
from_credential: "NONEXISTENT_KEY".to_string(),
value_prefix: String::new(),
}];

let result = apply_cred_inject(raw, &[], &inject, &resolver);
Expand Down Expand Up @@ -2378,6 +2421,7 @@ mod tests {
let inject = vec![CredInjectDirective {
header: "x-api-key".into(),
from_credential: "ANTHROPIC_API_KEY".into(),
value_prefix: String::new(),
}];
let result = apply_cred_inject(raw, &[], &inject, &gh_resolver);
assert!(result.is_err(), "gh should not resolve ANTHROPIC_API_KEY");
Expand All @@ -2386,6 +2430,7 @@ mod tests {
let inject_github = vec![CredInjectDirective {
header: "Authorization".into(),
from_credential: "GITHUB_TOKEN".into(),
value_prefix: String::new(),
}];
let result = apply_cred_inject(raw, &[], &inject_github, &gh_resolver);
assert!(result.is_ok());
Expand Down Expand Up @@ -2422,6 +2467,7 @@ mod tests {
let inject = vec![CredInjectDirective {
header: "x-api-key".into(),
from_credential: "ANTHROPIC_API_KEY".into(),
value_prefix: String::new(),
}];
let final_result =
apply_cred_inject(&rewrite_result.rewritten, &strip, &inject, &resolver).unwrap();
Expand Down
4 changes: 4 additions & 0 deletions proto/sandbox.proto
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ message CredInjectHeader {
// Name of the credential value to use as the header value
// (e.g. "ANTHROPIC_API_KEY").
string from_credential = 2;
// openlock fork delta: literal prefix prepended to the resolved credential
// value when composing the injected header (e.g. "Bearer "). Lets cred_inject
// emit `Authorization: Bearer <token>` from a raw stored token.
string value_prefix = 3;
}

// Trust check configuration for package registry endpoints.
Expand Down