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
}