time-tracker/docu/permissions_plan.md
2025-03-31 19:07:30 +00:00

6.0 KiB

Berechtigungssystem Plan

Dieser Plan beschreibt die Implementierung eines scope-basierten Berechtigungssystems für das TimeTracker-Projekt.

Grundkonzept

  • Ein Benutzer kann eine Rolle annehmen, aber immer nur eine ist aktiv.
  • Eine Rolle besteht aus mehreren Policies.
  • Eine Policy hat einen Namen und eine Map, die Scopes (z. B. items/books) einem Berechtigungsschlüssel (Bitflag) zuordnet.
  • Berechtigungsschlüssel sind Bitflags, die Permissions wie read, write, create, list, delete, moderate, superadmin usw. repräsentieren.
  • Scopes können Wildcards enthalten, z. B. items/*, das auf items/books vererbt wird.
  • Ziel: Berechtigungen sowohl im Go-Backend (für API-Sicherheit) als auch im TypeScript-Frontend (für UI-Anpassung) evaluieren.

Implementierung im Go-Backend

1. Ordnerstruktur

  • Neuer Ordner: backend/internal/permissions
  • Dateien:
    • permissions.go: Permission-Konstanten (Bitflags).
    • policy.go: Policy-Struktur.
    • role.go: Role-Struktur.
    • user.go: Erweiterung der User-Struktur.
    • matching.go: matchScope-Funktion.
    • evaluator.go: EffectivePermissions- und HasPermission-Funktionen.

2. Go-Strukturen

  • permissions.go:
package permissions

type Permission uint64

const (
    PermRead Permission = 1 << iota // 1
    PermWrite                       // 2
    PermCreate                      // 4
    PermList                        // 8
    PermDelete                      // 16
    PermModerate                    // 32
    PermSuperadmin                  // 64
)
  • policy.go:
package permissions

type Policy struct {
    Name   string
    Scopes map[string]Permission
}
  • role.go:
package permissions

type Role struct {
    Name     string
    Policies []Policy
}
  • user.go:
package permissions

import "github.com/your-org/your-project/backend/internal/models" // Pfad anpassen

type User struct {
    models.User // Einbettung
    ActiveRole *Role
}

3. Funktionen

  • matching.go:
package permissions

import "strings"

func MatchScope(pattern, scope string) bool {
    if strings.HasSuffix(pattern, "/*") {
        prefix := strings.TrimSuffix(pattern, "/*")
        return strings.HasPrefix(scope, prefix)
    }
    return pattern == scope
}
  • evaluator.go:
package permissions

func (u *User) EffectivePermissions(scope string) Permission {
    if u.ActiveRole == nil {
        return 0
    }
    var perm Permission
    for _, policy := range u.ActiveRole.Policies {
        for pat, p := range policy.Scopes {
            if MatchScope(pat, scope) {
                perm |= p
            }
        }
    }
    return perm
}

func (u *User) HasPermission(scope string, requiredPerm Permission) bool {
    effective := u.EffectivePermissions(scope)
    return (effective & requiredPerm) == requiredPerm
}

4. Integration in die API-Handler

  • Anpassung der jwt_auth.go Middleware.
  • Verwendung von HasPermission in den API-Handlern.

Persistierung (Datenbank)

1. Datenbankmodell

  • Zwei neue Tabellen: roles und policies.
  • roles:
    • id (ULID, Primärschlüssel)
    • name (VARCHAR, eindeutig)
  • policies:
    • id (ULID, Primärschlüssel)
    • name (VARCHAR, eindeutig)
    • role_id (ULID, Fremdschlüssel, der auf roles.id verweist)
    • scopes (JSONB oder TEXT, speichert die map[string]Permission als JSON)
  • Beziehung: 1:n zwischen roles und policies.

2. Go-Strukturen (Anpassungen)

  • role.go:
package permissions

import (
	"github.com/your-org/your-project/backend/internal/types" // Pfad anpassen
)

type Role struct {
    ID       types.ULID `gorm:"primaryKey;type:bytea"`
    Name     string     `gorm:"unique;not null"`
    Policies []Policy   `gorm:"foreignKey:RoleID"`
}
  • policy.go:
package permissions

import (
	"encoding/json"
    "database/sql/driver"
    "errors"
    "fmt"

	"github.com/your-org/your-project/backend/internal/types" // Pfad anpassen
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type Policy struct {
    ID      types.ULID      `gorm:"primaryKey;type:bytea"`
    Name    string          `gorm:"not null"`
    RoleID  types.ULID      `gorm:"type:bytea"` //Fremdschlüssel
    Scopes  Scopes          `gorm:"type:jsonb;not null"` // JSONB-Spalte
}

//Scopes type to handle JSON marshalling
type Scopes map[string]Permission

// Scan scan value into Jsonb, implements sql.Scanner interface
func (j *Scopes) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
	}

	var scopes map[string]Permission
	if err := json.Unmarshal(bytes, &scopes); err != nil {
		return err
	}
	*j = scopes
	return nil
}

// Value return json value, implement driver.Valuer interface
func (j Scopes) Value() (driver.Value, error) {
	return json.Marshal(j)
}

3. Migration

  • Verwendung des vorhandenen Migrations-Frameworks (backend/cmd/migrate/main.go).

4. Seed-Daten

  • Optionale Seed-Daten (backend/cmd/seed/main.go).

5. Anpassung der Funktionen

  • Anpassung von EffectivePermissions und HasPermission in evaluator.go für Datenbankzugriff.

Mermaid Diagramm

graph LR
    subgraph Benutzer
        U[User] --> AR(ActiveRole)
    end
    subgraph Rolle
        AR --> R(Role)
        R --> P1(Policy 1)
        R --> P2(Policy 2)
        R --> Pn(Policy n)
    end
    subgraph Policy
        P1 --> S1(Scope 1: Permissions)
        P1 --> S2(Scope 2: Permissions)
        P2 --> S3(Scope 3: Permissions)
        Pn --> Sm(Scope m: Permissions)
    end

    S1 -- Permissions --> PR(PermRead)
    S1 -- Permissions --> PW(PermWrite)
    S2 -- Permissions --> PL(PermList)
    Sm -- Permissions --> PD(PermDelete)
    
    style U fill:#f9f,stroke:#333,stroke-width:2px
    style R fill:#ccf,stroke:#333,stroke-width:2px
    style P1,P2,Pn fill:#ddf,stroke:#333,stroke-width:2px
    style S1,S2,S3,Sm fill:#eef,stroke:#333,stroke-width:1px
    style PR,PW,PL,PD fill:#ff9,stroke:#333,stroke-width:1px