dealspace/lib/rbac.go

158 lines
4.2 KiB
Go

package lib
import (
"errors"
"strings"
)
var (
ErrAccessDenied = errors.New("access denied")
ErrInsufficientOps = errors.New("insufficient permissions")
)
// CheckAccess verifies that actorID has the required operation on the given project/entry.
// op is one of "r", "w", "d", "m" (read, write, delete, manage).
// Returns the matching Access grant or an error.
func CheckAccess(db *DB, actorID, projectID string, workstreamID string, op string) (*Access, error) {
grants, err := getUserAccess(db, actorID, projectID)
if err != nil {
return nil, err
}
for _, g := range grants {
if g.RevokedAt != nil {
continue
}
// Grant matches if it covers all workstreams or the specific one
if g.WorkstreamID != "" && g.WorkstreamID != workstreamID && workstreamID != "" {
continue
}
if strings.Contains(g.Ops, op) {
return &g, nil
}
}
return nil, ErrAccessDenied
}
// CheckAccessRead is a convenience for read operations.
func CheckAccessRead(db *DB, actorID, projectID, workstreamID string) error {
_, err := CheckAccess(db, actorID, projectID, workstreamID, "r")
return err
}
// CheckAccessWrite is a convenience for write operations.
func CheckAccessWrite(db *DB, actorID, projectID, workstreamID string) error {
_, err := CheckAccess(db, actorID, projectID, workstreamID, "w")
return err
}
// CheckAccessDelete is a convenience for delete operations.
func CheckAccessDelete(db *DB, actorID, projectID, workstreamID string) error {
_, err := CheckAccess(db, actorID, projectID, workstreamID, "d")
return err
}
// IsBuyerRole returns true if the role is a buyer role.
// Buyers cannot see pre_dataroom entries.
func IsBuyerRole(role string) bool {
return role == RoleBuyerAdmin || role == RoleBuyerMember
}
// CanGrantRole checks whether the granting role can grant the target role.
// A role can only grant roles at or below its own hierarchy level.
func CanGrantRole(granterRole, targetRole string) bool {
granterLevel, ok1 := RoleHierarchy[granterRole]
targetLevel, ok2 := RoleHierarchy[targetRole]
if !ok1 || !ok2 {
return false
}
return granterLevel >= targetLevel
}
// ResolveWorkstreamID walks up the entry tree to find the workstream ancestor.
// Returns empty string if the entry is at project level (depth 0).
func ResolveWorkstreamID(db *DB, entry *Entry) (string, error) {
if entry.Type == TypeWorkstream {
return entry.EntryID, nil
}
if entry.Type == TypeProject || entry.Depth == 0 {
return "", nil
}
// Walk up to find workstream
current := entry
for current.Depth > 1 && current.ParentID != "" {
parent, err := entryReadSystem(db, current.ParentID)
if err != nil {
return "", err
}
if parent == nil {
return "", nil
}
if parent.Type == TypeWorkstream {
return parent.EntryID, nil
}
current = parent
}
return current.EntryID, nil
}
// getUserAccess retrieves all active access grants for a user on a project.
func getUserAccess(db *DB, userID, projectID string) ([]Access, error) {
rows, err := db.Conn.Query(
`SELECT id, project_id, workstream_id, user_id, role, ops, can_grant,
granted_by, granted_at, revoked_at, revoked_by
FROM access
WHERE user_id = ? AND project_id = ? AND revoked_at IS NULL`,
userID, projectID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var grants []Access
for rows.Next() {
var a Access
var wsID *string
var revokedAt *int64
var revokedBy *string
err := rows.Scan(&a.ID, &a.ProjectID, &wsID, &a.UserID, &a.Role,
&a.Ops, &a.CanGrant, &a.GrantedBy, &a.GrantedAt, &revokedAt, &revokedBy)
if err != nil {
return nil, err
}
if wsID != nil {
a.WorkstreamID = *wsID
}
a.RevokedAt = revokedAt
if revokedBy != nil {
a.RevokedBy = *revokedBy
}
grants = append(grants, a)
}
return grants, rows.Err()
}
// GetUserHighestRole returns the highest-privilege role a user has on a project.
func GetUserHighestRole(db *DB, userID, projectID string) (string, error) {
grants, err := getUserAccess(db, userID, projectID)
if err != nil {
return "", err
}
bestRole := ""
bestLevel := -1
for _, g := range grants {
if level, ok := RoleHierarchy[g.Role]; ok && level > bestLevel {
bestLevel = level
bestRole = g.Role
}
}
if bestRole == "" {
return "", ErrAccessDenied
}
return bestRole, nil
}