ops design and app
add more comments feature, more tags feature, and other frontend updates
This commit is contained in:
@@ -169,3 +169,7 @@ func hasScope(scopes []Scope, required Scope) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasScope(scopes []Scope, required Scope) bool {
|
||||
return hasScope(scopes, required)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user