A unit holds credentials for its members, by tool and setting - needed once,
not once per agent, and other units never get them.
- UnitConfig.credentials, protected by ConfigSecrets like every secret.
- OrgChart.ToolSettingsFor: an agent's own settings, completed by the nearest
unit up that holds the value; own non-empty values win; no tool is granted.
Used for runs, tool jobs and preflight probes.
- OrgAdministration.SetUnitCredential, PUT /organisation/units/{id}/credentials;
the view lists held settings, never values.
- Templates never carry unit credentials; they are named instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
4.2 KiB
C#
110 lines
4.2 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.Core.Config;
|
|
|
|
namespace ClawdDotNet.Core.Security;
|
|
|
|
/// <summary>
|
|
/// Weiß, welche Felder einer Konfiguration Zugangsdaten enthalten, und wendet den
|
|
/// <see cref="SecretProtector"/> darauf an.
|
|
///
|
|
/// Beide Richtungen sind gefahrlos mehrfach anwendbar: Schützen überspringt bereits
|
|
/// geschützte Werte, Entschlüsseln gibt Klartext unverändert zurück. Dadurch werden
|
|
/// bestehende Konfigurationen beim ersten Speichern automatisch übernommen.
|
|
/// </summary>
|
|
public static class ConfigSecrets
|
|
{
|
|
/// <summary>
|
|
/// Schlüsselnamen in Tool-Konfigurationen, deren Werte als Zugangsdaten gelten.
|
|
/// Die Tool-Konfiguration ist ein freies Wörterbuch — deshalb wird hier nach
|
|
/// Namen entschieden.
|
|
/// </summary>
|
|
private static readonly HashSet<string> SecretKeys = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"password", "password2fa", "passwort",
|
|
"apikey", "api_key", "xapikey", "openrouterapikey", "apihash", "apisecret",
|
|
"token", "accesstoken", "bottoken", "authtoken", "webuitoken",
|
|
"secret", "clientsecret",
|
|
"connectionstring"
|
|
};
|
|
|
|
public static bool IsSecretKey(string key) => SecretKeys.Contains(key);
|
|
|
|
// ─── Instanz ───
|
|
|
|
public static void Protect(InstanceConfig config) => Apply(config, SecretProtector.Protect);
|
|
|
|
public static void Unprotect(InstanceConfig config) => Apply(config, SecretProtector.Unprotect);
|
|
|
|
private static void Apply(InstanceConfig config, Func<string?, string?> transform)
|
|
{
|
|
config.OpenRouterApiKey = transform(config.OpenRouterApiKey) ?? "";
|
|
|
|
// Das Zugangstoken der Weboberflaeche ist ein Zugangsdatum wie jedes andere.
|
|
// Ohne diese Zeile stuende es im Klartext neben dem geschuetzten
|
|
// OpenRouter-Schluessel — der Name allein in SecretKeys traegt hier nicht, weil
|
|
// InstanceConfig kein freies Woerterbuch ist, sondern feste Felder hat.
|
|
config.WebUiToken = transform(config.WebUiToken) ?? "";
|
|
|
|
if (config.TelegramClient is { } telegram)
|
|
{
|
|
telegram.ApiHash = transform(telegram.ApiHash) ?? "";
|
|
telegram.Password2FA = transform(telegram.Password2FA);
|
|
}
|
|
|
|
if (config.Watchdog is { } watchdog)
|
|
watchdog.AgentToken = transform(watchdog.AgentToken) ?? "";
|
|
|
|
// #712: the Claude Code account — a key with a bill behind it.
|
|
if (config.ClaudeCode is { } claudeCode)
|
|
{
|
|
claudeCode.ApiKey = transform(claudeCode.ApiKey) ?? "";
|
|
claudeCode.OAuthToken = transform(claudeCode.OAuthToken) ?? "";
|
|
}
|
|
|
|
foreach (var agent in config.Agents)
|
|
Apply(agent, transform);
|
|
|
|
// #692: everything a unit holds for its members is a secret, whatever its name.
|
|
foreach (var unit in config.Units)
|
|
foreach (var settings in unit.Credentials?.Values ?? Enumerable.Empty<Dictionary<string, string>>())
|
|
foreach (var key in settings.Keys.ToList())
|
|
if (settings[key] is { Length: > 0 } value)
|
|
settings[key] = transform(value) ?? "";
|
|
}
|
|
|
|
// ─── Agent ───
|
|
|
|
public static void Protect(AgentConfig agent) => Apply(agent, SecretProtector.Protect);
|
|
|
|
public static void Unprotect(AgentConfig agent) => Apply(agent, SecretProtector.Unprotect);
|
|
|
|
private static void Apply(AgentConfig agent, Func<string?, string?> transform)
|
|
{
|
|
foreach (var tool in agent.Tools.Values)
|
|
{
|
|
foreach (var key in tool.Keys.ToList())
|
|
{
|
|
if (!IsSecretKey(key))
|
|
continue;
|
|
|
|
if (ReadString(tool[key]) is not { } current || current.Length == 0)
|
|
continue;
|
|
|
|
tool[key] = transform(current);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Liest einen Zeichenkettenwert. Nach dem Deserialisieren stecken die Werte als
|
|
/// JsonElement im Wörterbuch, nach einer Bearbeitung als einfache Zeichenkette.
|
|
/// </summary>
|
|
private static string? ReadString(object? value) => value switch
|
|
{
|
|
null => null,
|
|
string s => s,
|
|
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
|
|
_ => null
|
|
};
|
|
}
|