feat: User-Berechtigungen für Agent-Erstellung und CLI-Nutzung
- Neue Felder CanCreateAgents und CanUseCli im UserRecord (Default: true) - DB-Migration mit Abwärtskompatibilität (bestehende User behalten alle Rechte) - Admin-UI: Checkboxen für beide Berechtigungen pro User - AgentEndpoint: Prüfung auf CanCreateAgents beim Erstellen - ChatEndpoint: CLI-Fallback nur wenn CanUseCli erlaubt - Frontend: Neuer-Agent-Button und Agent-Page bei fehlender Berechtigung gesperrt - Chat: Proaktive Warnung wenn kein API-Key und keine CLI-Berechtigung - Bug Fix: is_public fehlte im Agent-INSERT (Public Agents erst nach erneutem Speichern sichtbar) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
500c00f948
commit
f03fbd298e
13 files changed
+100
-16
No files matched your search
@@ -38,6 +38,8 @@ public static class AdminEndpoint
|
||||
displayName = u.DisplayName,
|
||||
isAdmin = u.IsAdmin,
|
||||
defaultPath = u.DefaultPath,
|
||||
canCreateAgents = u.CanCreateAgents,
|
||||
canUseCli = u.CanUseCli,
|
||||
createdAt = u.CreatedAt,
|
||||
updatedAt = u.UpdatedAt
|
||||
});
|
||||
@@ -60,6 +62,8 @@ public static class AdminEndpoint
|
||||
var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null;
|
||||
var isAdmin = body.TryGetProperty("isAdmin", out var ia) && ia.GetBoolean();
|
||||
var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : null;
|
||||
var canCreateAgents = !body.TryGetProperty("canCreateAgents", out var cca) || cca.GetBoolean();
|
||||
var canUseCli = !body.TryGetProperty("canUseCli", out var cuc) || cuc.GetBoolean();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
@@ -84,7 +88,7 @@ public static class AdminEndpoint
|
||||
}
|
||||
|
||||
var hash = auth.HashPassword(password);
|
||||
var user = await db.CreateUser(username, hash, displayName, isAdmin, defaultPath);
|
||||
var user = await db.CreateUser(username, hash, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli);
|
||||
|
||||
ctx.Response.StatusCode = 201;
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
@@ -94,6 +98,8 @@ public static class AdminEndpoint
|
||||
displayName = user.DisplayName,
|
||||
isAdmin = user.IsAdmin,
|
||||
defaultPath = user.DefaultPath,
|
||||
canCreateAgents = user.CanCreateAgents,
|
||||
canUseCli = user.CanUseCli,
|
||||
createdAt = user.CreatedAt
|
||||
});
|
||||
}
|
||||
@@ -120,6 +126,8 @@ public static class AdminEndpoint
|
||||
var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : user.DisplayName;
|
||||
var isAdmin = body.TryGetProperty("isAdmin", out var ia) ? ia.GetBoolean() : user.IsAdmin;
|
||||
var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : user.DefaultPath;
|
||||
var canCreateAgents = body.TryGetProperty("canCreateAgents", out var cca) ? cca.GetBoolean() : user.CanCreateAgents;
|
||||
var canUseCli = body.TryGetProperty("canUseCli", out var cuc) ? cuc.GetBoolean() : user.CanUseCli;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
@@ -137,7 +145,7 @@ public static class AdminEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
await db.UpdateUserAdmin(userId, username, displayName, isAdmin, defaultPath);
|
||||
await db.UpdateUserAdmin(userId, username, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli);
|
||||
var updated = await db.GetUser(userId);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
@@ -147,6 +155,8 @@ public static class AdminEndpoint
|
||||
displayName = updated.DisplayName,
|
||||
isAdmin = updated.IsAdmin,
|
||||
defaultPath = updated.DefaultPath,
|
||||
canCreateAgents = updated.CanCreateAgents,
|
||||
canUseCli = updated.CanUseCli,
|
||||
createdAt = updated.CreatedAt,
|
||||
updatedAt = updated.UpdatedAt
|
||||
});
|
||||
|
||||
@@ -100,6 +100,14 @@ public static class AgentEndpoint
|
||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
var user = await db.GetUser(userId);
|
||||
if (user != null && !user.CanCreateAgents && !user.IsAdmin)
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Keine Berechtigung zum Erstellen von Agents" });
|
||||
return;
|
||||
}
|
||||
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
|
||||
var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null;
|
||||
|
||||
@@ -95,7 +95,9 @@ public static class AuthEndpoint
|
||||
username = user.Username,
|
||||
displayName = user.DisplayName,
|
||||
isAdmin = user.IsAdmin,
|
||||
defaultPath = user.DefaultPath
|
||||
defaultPath = user.DefaultPath,
|
||||
canCreateAgents = user.CanCreateAgents,
|
||||
canUseCli = user.CanUseCli
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -243,6 +243,15 @@ public static class ChatEndpoint
|
||||
// Kein API-Key → Claude CLI als Fallback verwenden
|
||||
if (string.IsNullOrEmpty(anthropicToken))
|
||||
{
|
||||
// CLI-Berechtigung prüfen
|
||||
var chatUser = await db.GetUser(userId);
|
||||
if (chatUser != null && !chatUser.CanUseCli && !chatUser.IsAdmin)
|
||||
{
|
||||
await SseHelper.TryWriteSse(res, "error", new { error = "Kein API-Key zugewiesen und keine CLI-Berechtigung. Bitte einen Key zuweisen." }, sseCt);
|
||||
await SseHelper.TryWriteSse(res, "done", new { chainId = ctx.ChainId }, sseCt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (anthropic.IsCliAvailable)
|
||||
{
|
||||
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, workCt: workCt, sseCt: sseCt);
|
||||
|
||||
@@ -14,6 +14,8 @@ public class UserRecord
|
||||
public string Settings { get; set; }
|
||||
public string DefaultPath { get; set; }
|
||||
public string SelectedApiKeyId { get; set; }
|
||||
public bool CanCreateAgents { get; set; }
|
||||
public bool CanUseCli { get; set; }
|
||||
|
||||
public string GetEffectivePersona()
|
||||
{
|
||||
|
||||
@@ -19,8 +19,8 @@ public partial class DatabaseService
|
||||
agent.CreatedAt = now;
|
||||
agent.UpdatedAt = now;
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
|
||||
VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
|
||||
INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_public, is_deleted, created_at, updated_at)
|
||||
VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @IsPublic, 0, @CreatedAt, @UpdatedAt)
|
||||
", agent);
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -159,24 +159,24 @@ public partial class DatabaseService
|
||||
new { userId, settings, now = DateTime.UtcNow.ToString("o") });
|
||||
}
|
||||
|
||||
public async Task<UserRecord> CreateUser(string username, string passwordHash, string displayName, bool isAdmin, string defaultPath = null)
|
||||
public async Task<UserRecord> CreateUser(string username, string passwordHash, string displayName, bool isAdmin, string defaultPath = null, bool canCreateAgents = true, bool canUseCli = true)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
var userId = Guid.NewGuid().ToString("N");
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO users (user_id, username, password_hash, display_name, is_admin, default_path, created_at, updated_at)
|
||||
VALUES (@userId, @username, @passwordHash, @displayName, @isAdmin, @defaultPath, @now, @now)",
|
||||
new { userId, username, passwordHash, displayName, isAdmin, defaultPath, now });
|
||||
INSERT INTO users (user_id, username, password_hash, display_name, is_admin, default_path, can_create_agents, can_use_cli, created_at, updated_at)
|
||||
VALUES (@userId, @username, @passwordHash, @displayName, @isAdmin, @defaultPath, @canCreateAgents, @canUseCli, @now, @now)",
|
||||
new { userId, username, passwordHash, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli, now });
|
||||
return await GetUser(userId);
|
||||
}
|
||||
|
||||
public async Task UpdateUserAdmin(string userId, string username, string displayName, bool isAdmin, string defaultPath)
|
||||
public async Task UpdateUserAdmin(string userId, string username, string displayName, bool isAdmin, string defaultPath, bool canCreateAgents, bool canUseCli)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE users SET username = @username, display_name = @displayName, is_admin = @isAdmin, default_path = @defaultPath, updated_at = @now WHERE user_id = @userId",
|
||||
new { userId, username, displayName, isAdmin, defaultPath, now = DateTime.UtcNow.ToString("o") });
|
||||
"UPDATE users SET username = @username, display_name = @displayName, is_admin = @isAdmin, default_path = @defaultPath, can_create_agents = @canCreateAgents, can_use_cli = @canUseCli, updated_at = @now WHERE user_id = @userId",
|
||||
new { userId, username, displayName, isAdmin, defaultPath, canCreateAgents, canUseCli, now = DateTime.UtcNow.ToString("o") });
|
||||
}
|
||||
|
||||
public async Task DeleteUser(string userId)
|
||||
|
||||
@@ -367,6 +367,10 @@ public partial class DatabaseService
|
||||
// users: ausgewählter API-Key (legacy, wird durch agent_api_keys ersetzt)
|
||||
TryAddColumn(conn, "users", "selected_api_key_id", "TEXT");
|
||||
|
||||
// users: Berechtigungen (Default: true für Abwärtskompatibilität)
|
||||
TryAddColumn(conn, "users", "can_create_agents", "INTEGER NOT NULL DEFAULT 1");
|
||||
TryAddColumn(conn, "users", "can_use_cli", "INTEGER NOT NULL DEFAULT 1");
|
||||
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS agent_api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -677,6 +677,18 @@
|
||||
<span>Administrator</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-checkbox-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="adminCanCreateAgents" checked>
|
||||
<span>Agents erstellen</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-checkbox-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="adminCanUseCli" checked>
|
||||
<span>CLI verwenden (ohne API-Key)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="adminFormSaveBtn">Erstellen</button>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,8 @@ registerPage('admin', {
|
||||
var defaultPathInput = document.getElementById('adminDefaultPath');
|
||||
var defaultPathBrowse = document.getElementById('adminDefaultPathBrowse');
|
||||
var isAdminCheckbox = document.getElementById('adminIsAdmin');
|
||||
var canCreateAgentsCheckbox = document.getElementById('adminCanCreateAgents');
|
||||
var canUseCliCheckbox = document.getElementById('adminCanUseCli');
|
||||
var saveBtn = document.getElementById('adminFormSaveBtn');
|
||||
|
||||
function renderUsers() {
|
||||
@@ -42,6 +44,10 @@ registerPage('admin', {
|
||||
var metaParts = ['@' + user.username];
|
||||
if (user.isAdmin) metaParts.push('Administrator');
|
||||
if (user.defaultPath) metaParts.push(user.defaultPath);
|
||||
var perms = [];
|
||||
if (user.canCreateAgents === false) perms.push('keine Agents');
|
||||
if (user.canUseCli === false) perms.push('keine CLI');
|
||||
if (perms.length) metaParts.push(perms.join(', '));
|
||||
metaSpan.textContent = metaParts.join(' · ');
|
||||
|
||||
info.appendChild(nameSpan);
|
||||
@@ -107,6 +113,8 @@ registerPage('admin', {
|
||||
passwordInput.required = false;
|
||||
defaultPathInput.value = user.defaultPath || '';
|
||||
isAdminCheckbox.checked = user.isAdmin;
|
||||
canCreateAgentsCheckbox.checked = user.canCreateAgents !== false;
|
||||
canUseCliCheckbox.checked = user.canUseCli !== false;
|
||||
saveBtn.textContent = 'Speichern';
|
||||
userForm.hidden = false;
|
||||
usernameInput.focus();
|
||||
@@ -199,6 +207,8 @@ registerPage('admin', {
|
||||
passwordInput.required = true;
|
||||
defaultPathInput.value = '';
|
||||
isAdminCheckbox.checked = false;
|
||||
canCreateAgentsCheckbox.checked = true;
|
||||
canUseCliCheckbox.checked = true;
|
||||
saveBtn.textContent = 'Erstellen';
|
||||
userForm.hidden = false;
|
||||
usernameInput.focus();
|
||||
@@ -212,7 +222,9 @@ registerPage('admin', {
|
||||
username: usernameInput.value.trim(),
|
||||
displayName: displayNameInput.value.trim(),
|
||||
defaultPath: defaultPathInput.value.trim() || null,
|
||||
isAdmin: isAdminCheckbox.checked
|
||||
isAdmin: isAdminCheckbox.checked,
|
||||
canCreateAgents: canCreateAgentsCheckbox.checked,
|
||||
canUseCli: canUseCliCheckbox.checked
|
||||
};
|
||||
await apiAdminUpdateUser(editingUserId, data);
|
||||
|
||||
@@ -234,7 +246,9 @@ registerPage('admin', {
|
||||
password: password,
|
||||
displayName: displayNameInput.value.trim(),
|
||||
defaultPath: defaultPathInput.value.trim() || null,
|
||||
isAdmin: isAdminCheckbox.checked
|
||||
isAdmin: isAdminCheckbox.checked,
|
||||
canCreateAgents: canCreateAgentsCheckbox.checked,
|
||||
canUseCli: canUseCliCheckbox.checked
|
||||
});
|
||||
|
||||
showToast('Benutzer erstellt');
|
||||
|
||||
@@ -9,6 +9,11 @@ registerPage('agent', {
|
||||
const isNew = !agentId;
|
||||
let agent = null;
|
||||
|
||||
if (isNew && currentUser && currentUser.canCreateAgents === false && !currentUser.isAdmin) {
|
||||
showToast('Keine Berechtigung zum Erstellen von Agents', 'error');
|
||||
return navigateTo('dashboard');
|
||||
}
|
||||
|
||||
if (!isNew) {
|
||||
agent = await apiGetAgent(agentId);
|
||||
if (!agent) return navigateTo('dashboard');
|
||||
|
||||
@@ -82,6 +82,16 @@ async function _initChatUsageBadge() {
|
||||
}
|
||||
}
|
||||
|
||||
async function _checkChatPermissions() {
|
||||
if (!currentUser || currentUser.isAdmin) return;
|
||||
if (currentUser.canUseCli !== false) return; // CLI erlaubt → kein Problem
|
||||
try {
|
||||
var selectedKey = await apiGetSelectedKey(getCurrentAgentId());
|
||||
if (selectedKey && selectedKey.selected) return; // Key vorhanden → kein Problem
|
||||
addWarningBubble('Kein API-Key zugewiesen und keine CLI-Berechtigung. Nachrichten können nicht verarbeitet werden. Bitte wende dich an einen Admin.');
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
async function _refreshChatUsageBadge() {
|
||||
if (!currentUser) return;
|
||||
var container = document.getElementById('chatUsageBadge');
|
||||
@@ -619,6 +629,9 @@ registerPage('chat', {
|
||||
// Usage Badge laden (Admin only)
|
||||
_initChatUsageBadge();
|
||||
|
||||
// Warnung anzeigen wenn kein Key und keine CLI-Berechtigung
|
||||
_checkChatPermissions();
|
||||
|
||||
// Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern)
|
||||
document.fonts.ready.then(() => {
|
||||
requestAnimationFrame(() => scrollDown());
|
||||
|
||||
@@ -149,8 +149,13 @@ registerPage('dashboard', {
|
||||
agentGrid.appendChild(card);
|
||||
}
|
||||
|
||||
// Neuer Agent Button
|
||||
document.getElementById('newAgentBtn').addEventListener('click', () => navigateTo('agent'));
|
||||
// Neuer Agent Button (nur wenn Berechtigung vorhanden)
|
||||
var newAgentBtn = document.getElementById('newAgentBtn');
|
||||
if (currentUser && currentUser.canCreateAgents === false && !currentUser.isAdmin) {
|
||||
newAgentBtn.hidden = true;
|
||||
} else {
|
||||
newAgentBtn.addEventListener('click', () => navigateTo('agent'));
|
||||
}
|
||||
|
||||
// Cronjobs
|
||||
const cronjobGrid = document.getElementById('cronjobCards');
|
||||
|
||||
Reference in new issue
Block a user