ops design and app

add more comments feature, more tags feature, and other frontend updates
This commit is contained in:
2026-06-09 18:27:59 -04:00
parent 1e939f307d
commit 04a1f2bb6d
33 changed files with 3276 additions and 113 deletions

View File

@@ -169,3 +169,7 @@ func hasScope(scopes []Scope, required Scope) bool {
}
return false
}
func HasScope(scopes []Scope, required Scope) bool {
return hasScope(scopes, required)
}

View File

@@ -6,6 +6,8 @@ import (
"encoding/json"
"errors"
"fmt"
"path"
"strings"
"time"
"github.com/go-webauthn/webauthn/webauthn"
@@ -284,6 +286,349 @@ func (r *Repository) UpdateUserAccess(ctx context.Context, updates []UserAccessU
return nil
}
type resourceMatch struct {
resourceType ResourceType
resourceID string
}
func (r *Repository) ListResourcePermissions(ctx context.Context, resourceType ResourceType, resourceID string) ([]ResourcePermission, error) {
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, `
SELECT p.id, p.user_id, COALESCE(u.email, ''), COALESCE(u.display_name, ''), p.resource_type, COALESCE(p.resource_id, ''), p.permission, COALESCE(p.granted_by, ''), p.created_at
FROM permissions p
LEFT JOIN users u ON u.id = p.user_id
WHERE p.resource_type = ? AND COALESCE(p.resource_id, '') = ?
ORDER BY u.email ASC, p.created_at ASC
`, string(resourceType), resourceID)
if err != nil {
return nil, fmt.Errorf("list resource permissions: %w", err)
}
defer rows.Close()
var permissions []ResourcePermission
for rows.Next() {
permission, err := scanResourcePermission(rows)
if err != nil {
return nil, err
}
permissions = append(permissions, permission)
}
return permissions, rows.Err()
}
func (r *Repository) ReplaceResourcePermissions(ctx context.Context, resourceType ResourceType, resourceID string, actorID string, updates []ResourcePermissionUpdate) error {
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return err
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin resource permissions update: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
DELETE FROM permissions
WHERE resource_type = ? AND COALESCE(resource_id, '') = ?
`, string(resourceType), resourceID); err != nil {
return fmt.Errorf("clear resource permissions: %w", err)
}
now := time.Now().UTC().Format(time.RFC3339)
for _, update := range updates {
update.UserID = strings.TrimSpace(update.UserID)
if update.UserID == "" || update.Permission == PermissionNone {
continue
}
if !validPermission(update.Permission) {
return fmt.Errorf("invalid permission")
}
var exists int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE id = ? AND disabled = 0`, update.UserID).Scan(&exists); err != nil {
return fmt.Errorf("check permission user: %w", err)
}
if exists == 0 {
return fmt.Errorf("user %s does not exist", update.UserID)
}
idPart, err := randomToken(18)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO permissions (id, user_id, resource_type, resource_id, permission, granted_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, "perm:"+idPart, update.UserID, string(resourceType), nullString(resourceID), string(update.Permission), nullString(actorID), now); err != nil {
return fmt.Errorf("insert resource permission: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit resource permissions update: %w", err)
}
return nil
}
func (r *Repository) EnsureResourcePermission(ctx context.Context, resourceType ResourceType, resourceID string, actorID string, update ResourcePermissionUpdate) error {
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return err
}
update.UserID = strings.TrimSpace(update.UserID)
if update.UserID == "" {
return fmt.Errorf("user id is required")
}
if !validPermission(update.Permission) || update.Permission == PermissionNone {
return fmt.Errorf("invalid permission")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin resource permission ensure: %w", err)
}
defer func() { _ = tx.Rollback() }()
var exists int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE id = ? AND disabled = 0`, update.UserID).Scan(&exists); err != nil {
return fmt.Errorf("check permission user: %w", err)
}
if exists == 0 {
return fmt.Errorf("user %s does not exist", update.UserID)
}
best := PermissionNone
rows, err := tx.QueryContext(ctx, `
SELECT permission
FROM permissions
WHERE user_id = ? AND resource_type = ? AND COALESCE(resource_id, '') = ?
`, update.UserID, string(resourceType), resourceID)
if err != nil {
return fmt.Errorf("lookup existing resource permission: %w", err)
}
for rows.Next() {
var raw string
if err := rows.Scan(&raw); err != nil {
rows.Close()
return fmt.Errorf("scan existing resource permission: %w", err)
}
best = maxPermission(best, Permission(raw))
}
if err := rows.Close(); err != nil {
return err
}
if PermissionAllows(best, update.Permission) {
return tx.Commit()
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM permissions
WHERE user_id = ? AND resource_type = ? AND COALESCE(resource_id, '') = ?
`, update.UserID, string(resourceType), resourceID); err != nil {
return fmt.Errorf("clear user resource permission: %w", err)
}
idPart, err := randomToken(18)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO permissions (id, user_id, resource_type, resource_id, permission, granted_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, "perm:"+idPart, update.UserID, string(resourceType), nullString(resourceID), string(update.Permission), nullString(actorID), time.Now().UTC().Format(time.RFC3339)); err != nil {
return fmt.Errorf("insert resource permission: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit resource permission ensure: %w", err)
}
return nil
}
func (r *Repository) EffectiveResourcePermission(ctx context.Context, principal Principal, resourceType ResourceType, resourceID string, collectionIDs []string) (Permission, error) {
if principal.UserID == "" {
return PermissionNone, nil
}
if principal.Role == RoleAdmin {
return PermissionAdmin, nil
}
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return PermissionNone, err
}
matches := []resourceMatch{{resourceType: ResourceGlobal, resourceID: ""}}
switch resourceType {
case ResourceDocument:
for _, folder := range folderMatchesForDocument(resourceID) {
matches = append(matches, resourceMatch{resourceType: ResourceFolder, resourceID: folder})
}
for _, collectionID := range collectionIDs {
normalized := normalizeCollectionID(collectionID)
if normalized != "" {
matches = append(matches, resourceMatch{resourceType: ResourceCollection, resourceID: normalized})
}
}
matches = append(matches, resourceMatch{resourceType: ResourceDocument, resourceID: resourceID})
case ResourceFolder:
for _, folder := range folderMatches(resourceID) {
matches = append(matches, resourceMatch{resourceType: ResourceFolder, resourceID: folder})
}
case ResourceCollection:
matches = append(matches, resourceMatch{resourceType: ResourceCollection, resourceID: resourceID})
default:
matches = append(matches, resourceMatch{resourceType: resourceType, resourceID: resourceID})
}
best := PermissionNone
for _, match := range matches {
rows, err := r.db.QueryContext(ctx, `
SELECT permission
FROM permissions
WHERE user_id = ? AND resource_type = ? AND COALESCE(resource_id, '') = ?
`, principal.UserID, string(match.resourceType), match.resourceID)
if err != nil {
return PermissionNone, fmt.Errorf("lookup resource permission: %w", err)
}
for rows.Next() {
var raw string
if err := rows.Scan(&raw); err != nil {
rows.Close()
return PermissionNone, fmt.Errorf("scan resource permission: %w", err)
}
best = maxPermission(best, Permission(raw))
}
if err := rows.Close(); err != nil {
return PermissionNone, err
}
}
return best, nil
}
func scanResourcePermission(rows *sql.Rows) (ResourcePermission, error) {
var (
permission ResourcePermission
resourceType string
resourceID string
rawPermission string
created string
)
if err := rows.Scan(&permission.ID, &permission.UserID, &permission.UserEmail, &permission.UserName, &resourceType, &resourceID, &rawPermission, &permission.GrantedBy, &created); err != nil {
return ResourcePermission{}, fmt.Errorf("scan resource permission: %w", err)
}
permission.ResourceType = ResourceType(resourceType)
permission.ResourceID = resourceID
permission.Permission = Permission(rawPermission)
createdAt, err := time.Parse(time.RFC3339, created)
if err != nil {
return ResourcePermission{}, fmt.Errorf("parse resource permission created_at: %w", err)
}
permission.CreatedAt = createdAt
return permission, nil
}
func normalizeResource(resourceType ResourceType, resourceID string) (ResourceType, string, error) {
resourceType = ResourceType(strings.TrimSpace(strings.ToLower(string(resourceType))))
switch resourceType {
case ResourceGlobal:
return resourceType, "", nil
case ResourceDocument:
resourceID = normalizeDocumentPath(resourceID)
if resourceID == "" {
return "", "", fmt.Errorf("document resource id is required")
}
return resourceType, resourceID, nil
case ResourceFolder:
return resourceType, normalizeFolderPath(resourceID), nil
case ResourceCollection:
resourceID = normalizeCollectionID(resourceID)
if resourceID == "" {
return "", "", fmt.Errorf("collection resource id is required")
}
return resourceType, resourceID, nil
default:
return "", "", fmt.Errorf("invalid resource type")
}
}
func normalizeDocumentPath(raw string) string {
clean := normalizeFolderPath(raw)
if clean == "" {
return ""
}
if !strings.HasSuffix(strings.ToLower(clean), ".md") {
clean += ".md"
}
return clean
}
func normalizeFolderPath(raw string) string {
raw = strings.TrimSpace(strings.Trim(raw, "/"))
if raw == "" {
return ""
}
clean := path.Clean(strings.ReplaceAll(raw, "\\", "/"))
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
return ""
}
return strings.Trim(clean, "/")
}
func normalizeCollectionID(raw string) string {
return strings.Trim(strings.TrimSpace(raw), "#/")
}
func folderMatchesForDocument(documentPath string) []string {
folder := path.Dir(documentPath)
if folder == "." {
return []string{""}
}
return folderMatches(folder)
}
func folderMatches(folder string) []string {
folder = normalizeFolderPath(folder)
matches := []string{""}
if folder == "" {
return matches
}
parts := strings.Split(folder, "/")
for index := range parts {
matches = append(matches, strings.Join(parts[:index+1], "/"))
}
return matches
}
func validPermission(permission Permission) bool {
switch permission {
case PermissionNone, PermissionRead, PermissionWrite, PermissionAdmin:
return true
default:
return false
}
}
func maxPermission(left Permission, right Permission) Permission {
if permissionRank(right) > permissionRank(left) {
return right
}
return left
}
func permissionRank(permission Permission) int {
switch permission {
case PermissionRead:
return 1
case PermissionWrite:
return 2
case PermissionAdmin:
return 3
default:
return 0
}
}
func (r *Repository) DeleteUser(ctx context.Context, userID string) error {
result, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE id = ?`, userID)
if err != nil {

View File

@@ -409,6 +409,96 @@ func (s *Service) UpdateUserAccess(ctx context.Context, actor Principal, updates
return users, nil
}
func (s *Service) ListResourcePermissions(ctx context.Context, actor Principal, resourceType ResourceType, resourceID string) ([]ResourcePermission, error) {
if actor.UserID == "" {
return nil, fmt.Errorf("authentication required")
}
if actor.Role != RoleAdmin {
permission, err := s.repo.EffectiveResourcePermission(ctx, actor, resourceType, resourceID, nil)
if err != nil {
return nil, err
}
if !PermissionAllows(permission, PermissionAdmin) {
return nil, fmt.Errorf("admin permission required")
}
}
return s.repo.ListResourcePermissions(ctx, resourceType, resourceID)
}
func (s *Service) UpdateResourcePermissions(ctx context.Context, actor Principal, resourceType ResourceType, resourceID string, updates []ResourcePermissionUpdate) ([]ResourcePermission, error) {
if actor.UserID == "" {
return nil, fmt.Errorf("authentication required")
}
if actor.Role != RoleAdmin {
permission, err := s.repo.EffectiveResourcePermission(ctx, actor, resourceType, resourceID, nil)
if err != nil {
return nil, err
}
if !PermissionAllows(permission, PermissionAdmin) {
return nil, fmt.Errorf("admin permission required")
}
}
normalized := make([]ResourcePermissionUpdate, 0, len(updates))
seen := make(map[string]struct{}, len(updates))
for _, update := range updates {
update.UserID = strings.TrimSpace(update.UserID)
if update.UserID == "" {
continue
}
if _, ok := seen[update.UserID]; ok {
return nil, fmt.Errorf("duplicate user permission")
}
seen[update.UserID] = struct{}{}
update.Permission = normalizePermission(update.Permission)
if !validPermission(update.Permission) {
return nil, fmt.Errorf("invalid permission")
}
normalized = append(normalized, update)
}
if err := s.repo.ReplaceResourcePermissions(ctx, resourceType, resourceID, actor.UserID, normalized); err != nil {
return nil, err
}
s.repo.Audit(ctx, actor.UserID, "auth.resource.permissions.update", string(resourceType), resourceID, "", "", map[string]any{
"updates": len(normalized),
})
return s.repo.ListResourcePermissions(ctx, resourceType, resourceID)
}
func (s *Service) EnsureResourcePermission(ctx context.Context, actor Principal, resourceType ResourceType, resourceID string, update ResourcePermissionUpdate) error {
if actor.UserID == "" {
return fmt.Errorf("authentication required")
}
update.UserID = strings.TrimSpace(update.UserID)
if update.UserID == "" {
return fmt.Errorf("user id is required")
}
update.Permission = normalizePermission(update.Permission)
if !validPermission(update.Permission) || update.Permission == PermissionNone {
return fmt.Errorf("invalid permission")
}
if update.UserID != actor.UserID && actor.Role != RoleAdmin {
permission, err := s.repo.EffectiveResourcePermission(ctx, actor, resourceType, resourceID, nil)
if err != nil {
return err
}
if !PermissionAllows(permission, PermissionAdmin) {
return fmt.Errorf("admin permission required")
}
}
if err := s.repo.EnsureResourcePermission(ctx, resourceType, resourceID, actor.UserID, update); err != nil {
return err
}
s.repo.Audit(ctx, actor.UserID, "auth.resource.permissions.ensure", string(resourceType), resourceID, "", "", map[string]any{
"userID": update.UserID,
"permission": update.Permission,
})
return nil
}
func (s *Service) EffectiveResourcePermission(ctx context.Context, principal Principal, resourceType ResourceType, resourceID string, collectionIDs []string) (Permission, error) {
return s.repo.EffectiveResourcePermission(ctx, principal, resourceType, resourceID, collectionIDs)
}
func (s *Service) SendPasswordReset(ctx context.Context, actor Principal, userID string) error {
if !Allows(actor, ScopeAdmin) {
return fmt.Errorf("admin role required")
@@ -715,6 +805,23 @@ func RoleAllows(role Role, required Scope) bool {
}
}
func PermissionAllows(actual Permission, required Permission) bool {
return permissionRank(actual) >= permissionRank(required)
}
func normalizePermission(permission Permission) Permission {
switch Permission(strings.ToLower(strings.TrimSpace(string(permission)))) {
case "view":
return PermissionRead
case "edit":
return PermissionWrite
case PermissionRead, PermissionWrite, PermissionAdmin, PermissionNone:
return Permission(strings.ToLower(strings.TrimSpace(string(permission))))
default:
return Permission(strings.ToLower(strings.TrimSpace(string(permission))))
}
}
func Allows(principal Principal, required Scope) bool {
if principal.UserID == "" {
return false

View File

@@ -343,3 +343,82 @@ func TestInitialSetupCanDisablePublicRegistration(t *testing.T) {
t.Fatalf("public signup role = %s, want viewer", viewer.Role)
}
}
func TestResourcePermissionsArePrivateByDefault(t *testing.T) {
service := setupAuthTestService(t)
ctx := context.Background()
admin := setupInitialAdmin(t, service, true)
viewer, err := service.repo.UpsertUser(ctx, User{
Email: "viewer@example.com",
DisplayName: "Viewer",
Role: RoleViewer,
})
if err != nil {
t.Fatalf("create viewer: %v", err)
}
viewerPrincipal := principalFromUser(viewer, "session", "sess:viewer", "", nil, time.Now().Add(time.Hour))
permission, err := service.EffectiveResourcePermission(ctx, viewerPrincipal, ResourceDocument, "private/page.md", nil)
if err != nil {
t.Fatalf("EffectiveResourcePermission() error = %v", err)
}
if permission != PermissionNone {
t.Fatalf("viewer permission = %q, want no access", permission)
}
adminPrincipal := principalFromUser(admin, "session", "sess:admin", "", nil, time.Now().Add(time.Hour))
permission, err = service.EffectiveResourcePermission(ctx, adminPrincipal, ResourceDocument, "private/page.md", nil)
if err != nil {
t.Fatalf("EffectiveResourcePermission() admin error = %v", err)
}
if permission != PermissionAdmin {
t.Fatalf("admin permission = %q, want admin", permission)
}
}
func TestResourcePermissionsUseMostPermissiveGrant(t *testing.T) {
service := setupAuthTestService(t)
ctx := context.Background()
admin := setupInitialAdmin(t, service, true)
viewer, err := service.repo.UpsertUser(ctx, User{
Email: "viewer@example.com",
DisplayName: "Viewer",
Role: RoleViewer,
})
if err != nil {
t.Fatalf("create viewer: %v", err)
}
adminPrincipal := principalFromUser(admin, "session", "sess:admin", "", nil, time.Now().Add(time.Hour))
viewerPrincipal := principalFromUser(viewer, "session", "sess:viewer", "", nil, time.Now().Add(time.Hour))
if _, err := service.UpdateResourcePermissions(ctx, adminPrincipal, ResourceDocument, "shared/page.md", []ResourcePermissionUpdate{
{UserID: viewer.ID, Permission: PermissionRead},
}); err != nil {
t.Fatalf("grant document read: %v", err)
}
if _, err := service.UpdateResourcePermissions(ctx, adminPrincipal, ResourceFolder, "shared", []ResourcePermissionUpdate{
{UserID: viewer.ID, Permission: PermissionWrite},
}); err != nil {
t.Fatalf("grant folder write: %v", err)
}
permission, err := service.EffectiveResourcePermission(ctx, viewerPrincipal, ResourceDocument, "shared/page.md", nil)
if err != nil {
t.Fatalf("EffectiveResourcePermission() folder error = %v", err)
}
if permission != PermissionWrite {
t.Fatalf("folder/document combined permission = %q, want write", permission)
}
if _, err := service.UpdateResourcePermissions(ctx, adminPrincipal, ResourceCollection, "team", []ResourcePermissionUpdate{
{UserID: viewer.ID, Permission: PermissionAdmin},
}); err != nil {
t.Fatalf("grant collection admin: %v", err)
}
permission, err = service.EffectiveResourcePermission(ctx, viewerPrincipal, ResourceDocument, "other/page.md", []string{"team"})
if err != nil {
t.Fatalf("EffectiveResourcePermission() collection error = %v", err)
}
if permission != PermissionAdmin {
t.Fatalf("collection permission = %q, want admin", permission)
}
}

View File

@@ -24,6 +24,24 @@ const (
ScopeAdmin Scope = "admin"
)
type Permission string
const (
PermissionNone Permission = ""
PermissionRead Permission = "read"
PermissionWrite Permission = "write"
PermissionAdmin Permission = "admin"
)
type ResourceType string
const (
ResourceGlobal ResourceType = "global"
ResourceCollection ResourceType = "collection"
ResourceDocument ResourceType = "document"
ResourceFolder ResourceType = "folder"
)
type Principal struct {
UserID string `json:"userId"`
Email string `json:"email"`
@@ -54,6 +72,23 @@ type UserAccessUpdate struct {
Disabled bool `json:"disabled"`
}
type ResourcePermission struct {
ID string `json:"id"`
UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"`
ResourceType ResourceType `json:"resourceType"`
ResourceID string `json:"resourceId"`
Permission Permission `json:"permission"`
GrantedBy string `json:"grantedBy,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
type ResourcePermissionUpdate struct {
UserID string `json:"userId"`
Permission Permission `json:"permission"`
}
type PasswordResetToken struct {
ID string
UserID string