Files
probo/pkg/iam/policy/statement.go
Sacha Al Himdani 8c02c53315 Update copyright headers across all Go files
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-25 17:38:32 +01:00

225 lines
6.0 KiB
Go

// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package policy
import (
"strings"
"go.probo.inc/probo/pkg/gid"
)
// Effect represents whether a statement allows or denies access.
type Effect string
const (
EffectAllow Effect = "allow"
EffectDeny Effect = "deny"
)
// Statement represents a single permission rule within a policy.
// A statement specifies what actions are allowed or denied on what resources,
// with optional conditions for attribute-based access control.
type Statement struct {
// SID is an optional identifier for the statement (useful for debugging).
SID string
// Effect specifies whether this statement allows or denies access.
Effect Effect
// Actions is the list of actions this statement applies to.
// Supports wildcards: "documents:*", "*:*:read", "*"
Actions []string
// Resources defines which resources this statement applies to.
// If empty, applies to all resources.
Resources []ResourcePattern
// Conditions are optional attribute-based constraints.
// All conditions must be satisfied for the statement to apply.
Conditions []Condition
}
// ResourcePattern defines a pattern for matching resources.
// Nil fields act as wildcards (match any value).
type ResourcePattern struct {
// TenantID restricts to a specific tenant. Nil matches any tenant.
TenantID *gid.TenantID
// EntityType restricts to a specific entity type. Nil matches any type.
EntityType *uint16
}
// MatchesResource checks if the pattern matches a given resource GID.
func (p ResourcePattern) MatchesResource(resource gid.GID) bool {
if p.TenantID != nil && *p.TenantID != resource.TenantID() {
return false
}
if p.EntityType != nil && *p.EntityType != resource.EntityType() {
return false
}
return true
}
// Condition represents an attribute-based access control constraint.
// Example: principal.id == resource.owner_id
type Condition struct {
// Operator is the comparison operator.
Operator ConditionOperator
// Key is the attribute path to check (e.g., "principal.id", "resource.owner_id").
Key string
// Values are the values to compare against.
Values []string
}
// ConditionOperator defines how to compare condition values.
type ConditionOperator string
const (
// ConditionEquals checks if the key value equals any of the specified values.
ConditionEquals ConditionOperator = "Equals"
// ConditionNotEquals checks if the key value does not equal any of the specified values.
ConditionNotEquals ConditionOperator = "NotEquals"
// ConditionIn checks if the key value is in the list of values.
ConditionIn ConditionOperator = "In"
// ConditionNotIn checks if the key value is not in the list of values.
ConditionNotIn ConditionOperator = "NotIn"
)
// ConditionContext provides attribute values for condition evaluation.
type ConditionContext struct {
Principal map[string]string
Resource map[string]string
}
// Evaluate checks if the condition is satisfied given the context.
func (c Condition) Evaluate(ctx ConditionContext) bool {
// Resolve the key value from context
value, ok := resolveKey(c.Key, ctx)
if !ok {
// Key not found - condition fails
return false
}
switch c.Operator {
case ConditionEquals:
for _, v := range c.Values {
resolved, ok := resolveValue(v, ctx)
if ok && value == resolved {
return true
}
}
return false
case ConditionNotEquals:
for _, v := range c.Values {
resolved, ok := resolveValue(v, ctx)
if ok && value == resolved {
return false
}
}
return true
case ConditionIn:
for _, v := range c.Values {
resolved, ok := resolveValue(v, ctx)
if !ok {
continue
}
// Support a comma-separated "set" value, e.g.
// principal.organization_ids = "org_1,org_2"
if strings.Contains(resolved, ",") {
for item := range strings.SplitSeq(resolved, ",") {
if value == strings.TrimSpace(item) {
return true
}
}
continue
}
if value == resolved {
return true
}
}
return false
case ConditionNotIn:
for _, v := range c.Values {
resolved, ok := resolveValue(v, ctx)
if !ok {
continue
}
if strings.Contains(resolved, ",") {
for item := range strings.SplitSeq(resolved, ",") {
if value == strings.TrimSpace(item) {
return false
}
}
continue
}
if value == resolved {
return false
}
}
return true
default:
return false
}
}
// resolveKey extracts a value from the context based on a key path.
// Key format: "principal.id", "resource.owner_id", etc.
func resolveKey(key string, ctx ConditionContext) (string, bool) {
// Simple implementation - can be extended for nested paths
if len(key) > 10 && key[:10] == "principal." {
attrKey := key[10:]
val, ok := ctx.Principal[attrKey]
return val, ok
}
if len(key) > 9 && key[:9] == "resource." {
attrKey := key[9:]
val, ok := ctx.Resource[attrKey]
return val, ok
}
return "", false
}
// resolveValue resolves a value, which can be a literal or a reference to context.
func resolveValue(value string, ctx ConditionContext) (string, bool) {
// Check if value is a reference (e.g., "principal.id")
if len(value) > 10 && value[:10] == "principal." {
return resolveKey(value, ctx)
}
if len(value) > 9 && value[:9] == "resource." {
return resolveKey(value, ctx)
}
// Literal value
return value, true
}