158 lines
4.2 KiB
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
|
|
}
|