mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 02:20:20 +00:00
feat(security): add provider-based external secrets management
This commit is contained in:
committed by
Peter Steinberger
parent
bb60cab76d
commit
4e7a833a24
@@ -25,7 +25,7 @@ describe("auth profile runtime snapshot persistence", () => {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "env", id: "OPENAI_API_KEY" },
|
||||
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -46,7 +46,7 @@ describe("auth profile runtime snapshot persistence", () => {
|
||||
expect(runtimeStore.profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-runtime-openai",
|
||||
keyRef: { source: "env", id: "OPENAI_API_KEY" },
|
||||
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
});
|
||||
|
||||
await markAuthProfileUsed({
|
||||
@@ -61,6 +61,7 @@ describe("auth profile runtime snapshot persistence", () => {
|
||||
expect(persisted.profiles["openai:default"]?.key).toBeUndefined();
|
||||
expect(persisted.profiles["openai:default"]?.keyRef).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -17,13 +17,13 @@ describe("saveAuthProfileStore", () => {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-runtime-value",
|
||||
keyRef: { source: "env", id: "OPENAI_API_KEY" },
|
||||
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
"github-copilot:default": {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: "gh-runtime-token",
|
||||
tokenRef: { source: "env", id: "GITHUB_TOKEN" },
|
||||
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
|
||||
},
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
@@ -45,12 +45,14 @@ describe("saveAuthProfileStore", () => {
|
||||
expect(parsed.profiles["openai:default"]?.key).toBeUndefined();
|
||||
expect(parsed.profiles["openai:default"]?.keyRef).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
});
|
||||
|
||||
expect(parsed.profiles["github-copilot:default"]?.token).toBeUndefined();
|
||||
expect(parsed.profiles["github-copilot:default"]?.tokenRef).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "GITHUB_TOKEN",
|
||||
});
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ describe("resolveApiKeyForProfile secret refs", () => {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "env", id: "OPENAI_API_KEY" },
|
||||
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -217,7 +217,7 @@ describe("resolveApiKeyForProfile secret refs", () => {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: "",
|
||||
tokenRef: { source: "env", id: "GITHUB_TOKEN" },
|
||||
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -236,4 +236,70 @@ describe("resolveApiKeyForProfile secret refs", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves inline ${ENV} api_key values", async () => {
|
||||
const profileId = "openai:inline-env";
|
||||
const previous = process.env.OPENAI_API_KEY;
|
||||
process.env.OPENAI_API_KEY = "sk-openai-inline";
|
||||
try {
|
||||
const result = await resolveApiKeyForProfile({
|
||||
cfg: cfgFor(profileId, "openai", "api_key"),
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "${OPENAI_API_KEY}",
|
||||
},
|
||||
},
|
||||
},
|
||||
profileId,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKey: "sk-openai-inline",
|
||||
provider: "openai",
|
||||
email: undefined,
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves inline ${ENV} token values", async () => {
|
||||
const profileId = "github-copilot:inline-env";
|
||||
const previous = process.env.GITHUB_TOKEN;
|
||||
process.env.GITHUB_TOKEN = "gh-inline-token";
|
||||
try {
|
||||
const result = await resolveApiKeyForProfile({
|
||||
cfg: cfgFor(profileId, "github-copilot", "token"),
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: "${GITHUB_TOKEN}",
|
||||
},
|
||||
},
|
||||
},
|
||||
profileId,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKey: "gh-inline-token",
|
||||
provider: "github-copilot",
|
||||
email: undefined,
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
} else {
|
||||
process.env.GITHUB_TOKEN = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type OAuthProvider,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { loadConfig, type OpenClawConfig } from "../../config/config.js";
|
||||
import { isSecretRef } from "../../config/types.secrets.js";
|
||||
import { coerceSecretRef } from "../../config/types.secrets.js";
|
||||
import { withFileLock } from "../../infra/file-lock.js";
|
||||
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
|
||||
import { resolveSecretRefString, type SecretRefResolveCache } from "../../secrets/resolve.js";
|
||||
@@ -257,14 +257,34 @@ export async function resolveApiKeyForProfile(
|
||||
return null;
|
||||
}
|
||||
|
||||
const refResolveCache: SecretRefResolveCache = { fileSecretsPromise: null };
|
||||
const refResolveCache: SecretRefResolveCache = {};
|
||||
const configForRefResolution = cfg ?? loadConfig();
|
||||
const refDefaults = configForRefResolution.secrets?.defaults;
|
||||
|
||||
if (cred.type === "api_key") {
|
||||
let key = cred.key?.trim();
|
||||
if (!key && isSecretRef(cred.keyRef)) {
|
||||
if (key) {
|
||||
const inlineRef = coerceSecretRef(key, refDefaults);
|
||||
if (inlineRef) {
|
||||
try {
|
||||
key = await resolveSecretRefString(inlineRef, {
|
||||
config: configForRefResolution,
|
||||
env: process.env,
|
||||
cache: refResolveCache,
|
||||
});
|
||||
} catch (err) {
|
||||
log.debug("failed to resolve inline auth profile api_key ref", {
|
||||
profileId,
|
||||
provider: cred.provider,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const keyRef = coerceSecretRef(cred.keyRef, refDefaults);
|
||||
if (!key && keyRef) {
|
||||
try {
|
||||
key = await resolveSecretRefString(cred.keyRef, {
|
||||
key = await resolveSecretRefString(keyRef, {
|
||||
config: configForRefResolution,
|
||||
env: process.env,
|
||||
cache: refResolveCache,
|
||||
@@ -284,9 +304,28 @@ export async function resolveApiKeyForProfile(
|
||||
}
|
||||
if (cred.type === "token") {
|
||||
let token = cred.token?.trim();
|
||||
if (!token && isSecretRef(cred.tokenRef)) {
|
||||
if (token) {
|
||||
const inlineRef = coerceSecretRef(token, refDefaults);
|
||||
if (inlineRef) {
|
||||
try {
|
||||
token = await resolveSecretRefString(inlineRef, {
|
||||
config: configForRefResolution,
|
||||
env: process.env,
|
||||
cache: refResolveCache,
|
||||
});
|
||||
} catch (err) {
|
||||
log.debug("failed to resolve inline auth profile token ref", {
|
||||
profileId,
|
||||
provider: cred.provider,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const tokenRef = coerceSecretRef(cred.tokenRef, refDefaults);
|
||||
if (!token && tokenRef) {
|
||||
try {
|
||||
token = await resolveSecretRefString(cred.tokenRef, {
|
||||
token = await resolveSecretRefString(tokenRef, {
|
||||
config: configForRefResolution,
|
||||
env: process.env,
|
||||
cache: refResolveCache,
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("resolveModelAuthLabel", () => {
|
||||
"github-copilot:default": {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
tokenRef: { source: "env", id: "GITHUB_TOKEN" },
|
||||
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
Reference in New Issue
Block a user