feat: pkg/security/ — tipos puros y resolución ACL (issue 0024a)
Crea el paquete puro pkg/security/ con los tipos base del sistema
centralizado de permisos y la función ResolveACL.
Cambios:
- pkg/acl/config.go: añade FromRoles([]Role) ACL como constructor directo
- pkg/security/groups.go: UserGroup, AgentGroup
- pkg/security/policy.go: Permission, AgentPolicy, SecurityPolicy
- pkg/security/resolver.go: ResolveACL(agentID, SecurityPolicy) → acl.ACL
* soporte wildcard de agente ("*") y de usuario ("*")
* políticas acumulativas: unión de permisos entre grupos
* referencia directa por agentID sin definir grupo
- pkg/security/security_test.go: 7 tests cubriendo todos los casos del issue
El paquete es pure core: cero I/O, cero side effects.
Mergeado con feature flag centralized-security-groups = false (no wired).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package security
|
||||
|
||||
import "github.com/enmanuel/agents/pkg/acl"
|
||||
|
||||
// ResolveACL computes the ACL for a given agentID from a SecurityPolicy.
|
||||
//
|
||||
// Resolution rules:
|
||||
// - An AgentPolicy applies to agentID if its AgentGroup field is:
|
||||
// (a) a group name in p.AgentGroups whose Agents list contains agentID or "*", or
|
||||
// (b) directly equal to agentID (individual assignment without a named group).
|
||||
// - If multiple AgentPolicies apply, their permissions are accumulated (union).
|
||||
// - For each Permission, the UserGroup is resolved to members in p.UserGroups.
|
||||
// A UserGroup with Members = ["*"] grants the actions to all users.
|
||||
// - If no policy applies, an empty ACL is returned (open access per acl semantics).
|
||||
func ResolveACL(agentID string, p SecurityPolicy) acl.ACL {
|
||||
// Build a lookup: group name → members.
|
||||
userGroupMembers := make(map[string][]string, len(p.UserGroups))
|
||||
for _, ug := range p.UserGroups {
|
||||
userGroupMembers[ug.Name] = ug.Members
|
||||
}
|
||||
|
||||
// Collect all roles from every AgentPolicy that applies to this agent.
|
||||
var roles []acl.Role
|
||||
for _, ap := range p.Policies {
|
||||
if !agentPolicyApplies(agentID, ap.AgentGroup, p.AgentGroups) {
|
||||
continue
|
||||
}
|
||||
for _, perm := range ap.Permissions {
|
||||
members := resolveMembers(perm.UserGroup, userGroupMembers)
|
||||
roles = append(roles, acl.Role{
|
||||
Name: perm.UserGroup,
|
||||
Users: members,
|
||||
Actions: perm.Actions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return acl.FromRoles(roles)
|
||||
}
|
||||
|
||||
// agentPolicyApplies returns true if an AgentPolicy with the given agentGroupRef
|
||||
// should apply to agentID.
|
||||
func agentPolicyApplies(agentID, agentGroupRef string, groups []AgentGroup) bool {
|
||||
// Try to find a named group first.
|
||||
for _, ag := range groups {
|
||||
if ag.Name != agentGroupRef {
|
||||
continue
|
||||
}
|
||||
for _, a := range ag.Agents {
|
||||
if a == "*" || a == agentID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false // group found but agent not in it
|
||||
}
|
||||
// No matching group found — treat agentGroupRef as a direct agent ID.
|
||||
return agentGroupRef == agentID
|
||||
}
|
||||
|
||||
// resolveMembers returns the member list for a user group name.
|
||||
// If the group is not defined, it falls back to treating the name as a literal user ID.
|
||||
func resolveMembers(userGroupName string, lookup map[string][]string) []string {
|
||||
if members, ok := lookup[userGroupName]; ok {
|
||||
return members
|
||||
}
|
||||
// Fallback: treat as a direct user ID.
|
||||
return []string{userGroupName}
|
||||
}
|
||||
Reference in New Issue
Block a user