security: API-Key Permissions + Customer-Scoping
- DB Migration: api_keys bekommt 'permissions' + 'customer_id' Spalten - Permissions: agent | read | write | admin - agent: nur /api/v1/agent/ws + /api/v1/agent/heartbeat - read: GET-Endpoints (optional customer-scoped) - write: alles ausser Terminal - admin: voll (Terminal bleibt JWT-only) - Bestehende Keys werden auf 'admin' migriert (Abwaertskompatibilitaet) - Frontend: API-Key Verwaltung zeigt Permissions-Badge + Kunden-Scope - API-Client: createAPIKey nimmt jetzt permissions + customer_id
This commit is contained in:
+56
-11
@@ -201,6 +201,11 @@ func (d *Database) migrate() error {
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used TIMESTAMPTZ
|
||||
)`)
|
||||
// Migration: customer_id + permissions Spalten hinzufuegen falls nicht vorhanden
|
||||
d.db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL`)
|
||||
d.db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS permissions TEXT NOT NULL DEFAULT 'admin'`)
|
||||
// Bestehende Keys auf 'admin' setzen (Abwaertskompatibilitaet)
|
||||
d.db.Exec(`UPDATE api_keys SET permissions = 'admin' WHERE permissions = ''`)
|
||||
|
||||
// Installer-Pakete (install.sh + Plugin als ZIP pro Plattform)
|
||||
d.db.Exec(`CREATE TABLE IF NOT EXISTS installers (
|
||||
@@ -1135,16 +1140,23 @@ func (d *Database) GetAgentStatus(agentID string) string {
|
||||
|
||||
// ==================== API-Keys ====================
|
||||
|
||||
// Permissions: "agent" | "read" | "write" | "admin"
|
||||
// - agent: nur /api/v1/agent/ws + /api/v1/agent/heartbeat (fuer Agents auf Kundenservern)
|
||||
// - read: GET-Endpoints, optional auf customer_id beschraenkt
|
||||
// - write: alles ausser Terminal (fuer externe Integrationen)
|
||||
// - admin: voll (kein Terminal — das ist JWT-only)
|
||||
type APIKey struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed *time.Time `json:"last_used,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
Permissions string `json:"permissions"`
|
||||
CustomerID *int `json:"customer_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed *time.Time `json:"last_used,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Database) ListAPIKeys() ([]APIKey, error) {
|
||||
rows, err := d.db.Query("SELECT id, name, key, created_at, last_used FROM api_keys ORDER BY created_at")
|
||||
rows, err := d.db.Query("SELECT id, name, key, permissions, customer_id, created_at, last_used FROM api_keys ORDER BY created_at")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1154,12 +1166,17 @@ func (d *Database) ListAPIKeys() ([]APIKey, error) {
|
||||
for rows.Next() {
|
||||
var k APIKey
|
||||
var lastUsed sql.NullTime
|
||||
if err := rows.Scan(&k.ID, &k.Name, &k.Key, &k.CreatedAt, &lastUsed); err != nil {
|
||||
var custID sql.NullInt64
|
||||
if err := rows.Scan(&k.ID, &k.Name, &k.Key, &k.Permissions, &custID, &k.CreatedAt, &lastUsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lastUsed.Valid {
|
||||
k.LastUsed = &lastUsed.Time
|
||||
}
|
||||
if custID.Valid {
|
||||
id := int(custID.Int64)
|
||||
k.CustomerID = &id
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
if keys == nil {
|
||||
@@ -1168,15 +1185,24 @@ func (d *Database) ListAPIKeys() ([]APIKey, error) {
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
func (d *Database) CreateAPIKey(name, key string) (*APIKey, error) {
|
||||
func (d *Database) CreateAPIKey(name, key, permissions string, customerID *int) (*APIKey, error) {
|
||||
var k APIKey
|
||||
var custID sql.NullInt64
|
||||
if customerID != nil {
|
||||
custID = sql.NullInt64{Int64: int64(*customerID), Valid: true}
|
||||
}
|
||||
var scannedCustID sql.NullInt64
|
||||
err := d.db.QueryRow(
|
||||
"INSERT INTO api_keys (name, key) VALUES ($1, $2) RETURNING id, name, key, created_at",
|
||||
name, key,
|
||||
).Scan(&k.ID, &k.Name, &k.Key, &k.CreatedAt)
|
||||
"INSERT INTO api_keys (name, key, permissions, customer_id) VALUES ($1, $2, $3, $4) RETURNING id, name, key, permissions, customer_id, created_at",
|
||||
name, key, permissions, custID,
|
||||
).Scan(&k.ID, &k.Name, &k.Key, &k.Permissions, &scannedCustID, &k.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if scannedCustID.Valid {
|
||||
id := int(scannedCustID.Int64)
|
||||
k.CustomerID = &id
|
||||
}
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
@@ -1192,6 +1218,25 @@ func (d *Database) DeleteAPIKey(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAPIKeyInfo gibt Key + Permissions + CustomerID zurueck (fuer Middleware)
|
||||
func (d *Database) GetAPIKeyInfo(key string) (*APIKey, error) {
|
||||
var k APIKey
|
||||
var custID sql.NullInt64
|
||||
var lastUsed sql.NullTime
|
||||
err := d.db.QueryRow(
|
||||
"SELECT id, name, key, permissions, customer_id, created_at, last_used FROM api_keys WHERE key = $1",
|
||||
key,
|
||||
).Scan(&k.ID, &k.Name, &k.Key, &k.Permissions, &custID, &k.CreatedAt, &lastUsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if custID.Valid {
|
||||
id := int(custID.Int64)
|
||||
k.CustomerID = &id
|
||||
}
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetAllAPIKeys() ([]string, error) {
|
||||
rows, err := d.db.Query("SELECT key FROM api_keys")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user