readysite / website / internal / access / access.go
4.2 KB
access.go
// Package access provides ACL constants and permission checking.
package access

import "github.com/readysite/readysite/website/models"

// User roles
const (
	RoleAdmin  = "admin"
	RoleUser   = "user"
	RoleViewer = "viewer"
)

// Subject types
const (
	SubjectUser   = "user"
	SubjectRole   = "role"
	SubjectPublic = "public"
)

// Resource types
const (
	ResourcePage       = "page"
	ResourceCollection = "collection"
	ResourceDocument   = "document"
)

// Permissions
const (
	PermRead   = "read"
	PermWrite  = "write"
	PermDelete = "delete"
	PermAdmin  = "admin"
)

// IsAdmin returns true if the user has admin role.
func IsAdmin(u *models.User) bool {
	return u.Role == RoleAdmin
}

// Grants returns true if 'have' permission grants 'want' permission.
func Grants(have, want string) bool {
	if have == PermAdmin {
		return true
	}
	if have == PermDelete && (want == PermRead || want == PermWrite) {
		return true
	}
	if have == PermWrite && want == PermRead {
		return true
	}
	return have == want
}

// CheckAccess checks if a user has permission on a resource.
func CheckAccess(user *models.User, resourceType, resourceID, permission string) bool {
	if user != nil && user.Role == RoleAdmin {
		return true
	}

	// Check public rules
	rules, err := models.ACLRules.Search("WHERE SubjectType = ? AND ResourceType = ?",
		SubjectPublic, resourceType)
	if err == nil {
		for _, rule := range rules {
			if matchesResource(rule, resourceID) && Grants(rule.Permission, permission) {
				return true
			}
		}
	}

	if user == nil {
		return false
	}

	// Check user-specific rules
	rules, err = models.ACLRules.Search("WHERE SubjectType = ? AND SubjectID = ? AND ResourceType = ?",
		SubjectUser, user.ID, resourceType)
	if err == nil {
		for _, rule := range rules {
			if matchesResource(rule, resourceID) && Grants(rule.Permission, permission) {
				return true
			}
		}
	}

	// Check role-based rules
	rules, err = models.ACLRules.Search("WHERE SubjectType = ? AND SubjectID = ? AND ResourceType = ?",
		SubjectRole, user.Role, resourceType)
	if err == nil {
		for _, rule := range rules {
			if matchesResource(rule, resourceID) && Grants(rule.Permission, permission) {
				return true
			}
		}
	}

	return false
}

func matchesResource(rule *models.ACLRule, resourceID string) bool {
	return rule.ResourceID == "" || rule.ResourceID == resourceID
}

// GrantAccess creates or updates an ACL rule.
func GrantAccess(subjectType, subjectID, resourceType, resourceID, permission string) error {
	// Check if rule already exists
	existing, err := models.ACLRules.First(
		"WHERE SubjectType = ? AND SubjectID = ? AND ResourceType = ? AND ResourceID = ?",
		subjectType, subjectID, resourceType, resourceID,
	)
	if err == nil && existing != nil {
		// Update existing rule
		existing.Permission = permission
		return models.ACLRules.Update(existing)
	}

	// Create new rule
	rule := &models.ACLRule{
		SubjectType:  subjectType,
		SubjectID:    subjectID,
		ResourceType: resourceType,
		ResourceID:   resourceID,
		Permission:   permission,
	}
	_, err = models.ACLRules.Insert(rule)
	return err
}

// RevokeAccess removes an ACL rule.
func RevokeAccess(subjectType, subjectID, resourceType, resourceID string) error {
	rule, err := models.ACLRules.First(
		"WHERE SubjectType = ? AND SubjectID = ? AND ResourceType = ? AND ResourceID = ?",
		subjectType, subjectID, resourceType, resourceID,
	)
	if err != nil {
		return err
	}
	if rule == nil {
		return nil // Already revoked
	}
	return models.ACLRules.Delete(rule)
}

// GetRules returns all ACL rules for a specific resource.
func GetRules(resourceType, resourceID string) ([]*models.ACLRule, error) {
	return models.ACLRules.Search(
		"WHERE ResourceType = ? AND ResourceID = ? ORDER BY SubjectType, SubjectID",
		resourceType, resourceID,
	)
}

// IsPubliclyAccessible returns true if the resource has public read access.
func IsPubliclyAccessible(resourceType, resourceID string) bool {
	rules, err := models.ACLRules.Search(
		"WHERE SubjectType = ? AND ResourceType = ? AND (ResourceID = ? OR ResourceID = '')",
		SubjectPublic, resourceType, resourceID,
	)
	if err != nil {
		return false
	}
	for _, rule := range rules {
		if Grants(rule.Permission, PermRead) {
			return true
		}
	}
	return false
}
← Back