policy-engine/iam/converter.go
Denis Kirillov 0566a2b058 [#XX] iam: Add converter to native policy
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-11-10 17:56:41 +03:00

386 lines
11 KiB
Go

package iam
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
const (
RequestOwnerProperty = "Owner"
ResourceFullPathProperty = "FullPath"
)
const (
CondKeyAWSPrincipalARN = "aws:PrincipalArn"
CondKeyS3Delimiter = "s3:delimiter"
CondKeyS3Prefix = "s3:prefix"
CondKeyS3VersionID = "s3:VersionId"
)
const (
// String condition operators.
CondStringEquals string = "StringEquals"
CondStringNotEquals string = "StringNotEquals"
CondStringEqualsIgnoreCase string = "StringEqualsIgnoreCase"
CondStringNotEqualsIgnoreCase string = "StringNotEqualsIgnoreCase"
CondStringLike string = "StringLike"
CondStringNotLike string = "StringNotLike"
// Numeric condition operators.
CondNumericEquals string = "NumericEquals"
CondNumericNotEquals string = "NumericNotEquals"
CondNumericLessThan string = "NumericLessThan"
CondNumericLessThanEquals string = "NumericLessThanEquals"
CondNumericGreaterThan string = "NumericGreaterThan"
CondNumericGreaterThanEquals string = "NumericGreaterThanEquals"
// Date condition operators.
CondDateEquals string = "DateEquals"
CondDateNotEquals string = "DateNotEquals"
CondDateLessThan string = "DateLessThan"
CondDateLessThanEquals string = "DateLessThanEquals"
CondDateGreaterThan string = "DateGreaterThan"
CondDateGreaterThanEquals string = "DateGreaterThanEquals"
// Bolean condition operators.
CondBool string = "Bool"
// IP address condition operators.
CondIPAddress string = "IpAddress"
CondNotIPAddress string = "NotIpAddress"
// ARN condition operators.
CondArnEquals string = "ArnEquals"
CondArnLike string = "ArnLike"
CondArnNotEquals string = "ArnNotEquals"
CondArnNotLike string = "ArnNotLike"
)
const (
arnIAMPrefix = "arn:aws:iam::"
s3ResourcePrefix = "arn:aws:s3:::"
)
// ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format.
var ErrInvalidPrincipalFormat = errors.New("invalid principal format")
type ChainType string
const (
S3ChainType ChainType = "s3"
NativeChainType ChainType = "native"
)
type UserResolver interface {
GetUserKey(account, user string) (*keys.PublicKey, error)
}
func (p Policy) ToChain(typ ChainType, resolver UserResolver) (*policyengine.Chain, error) {
if !isValidChainType(typ) {
return nil, fmt.Errorf("unknown chain type '%s'", typ)
}
if err := p.Validate(GeneralPolicyType); err != nil {
return nil, err
}
var chain policyengine.Chain
for _, statement := range p.Statement {
status := policyengine.AccessDenied
if statement.Effect == AllowEffect {
status = policyengine.Allow
}
var principals []string
var op policyengine.ConditionType
statementPrincipal, inverted := statement.principal()
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{Wildcard}
op = policyengine.CondStringLike
} else {
for principalType, principal := range statementPrincipal {
if principalType != AWSPrincipalType {
return nil, fmt.Errorf("unsupported principal type '%s'", principalType)
}
parsedPrincipal, err := formPrincipal(principal, resolver)
if err != nil {
return nil, fmt.Errorf("parse principal '%s': %w", typ, err)
}
principals = append(principals, parsedPrincipal...)
}
op = policyengine.CondStringEquals
if inverted {
op = policyengine.CondStringNotEquals
}
}
var conditions []policyengine.Condition
for _, principal := range principals {
conditions = append(conditions, policyengine.Condition{
Op: op,
Object: policyengine.ObjectRequest,
Key: RequestOwnerProperty,
Value: principal,
})
}
conds, err := statement.Conditions.ToChainCondition(resolver)
if err != nil {
return nil, err
}
conditions = append(conditions, conds...)
action, actionInverted := statement.action()
ruleAction := policyengine.Actions{Inverted: actionInverted, Names: action}
resource, resourceInverted := statement.resource()
names, extraConditions := formResourceNamesAndConditions(typ, resource)
ruleResource := policyengine.Resources{Inverted: resourceInverted, Names: names}
conditions = append(conditions, extraConditions...)
r := policyengine.Rule{
Status: status,
Actions: ruleAction,
Resources: ruleResource,
Any: true,
Condition: conditions,
}
chain.Rules = append(chain.Rules, r)
}
return &chain, nil
}
//nolint:funlen
func (c Conditions) ToChainCondition(resolver UserResolver) ([]policyengine.Condition, error) {
var conditions []policyengine.Condition
var convertValue convertFunction
for op, KVs := range c {
var condType policyengine.ConditionType
switch {
case strings.HasPrefix(op, "String"):
convertValue = noConvertFunction
switch op {
case CondStringEquals:
condType = policyengine.CondStringEquals
case CondStringNotEquals:
condType = policyengine.CondStringNotEquals
case CondStringEqualsIgnoreCase:
condType = policyengine.CondStringEqualsIgnoreCase
case CondStringNotEqualsIgnoreCase:
condType = policyengine.CondStringNotEqualsIgnoreCase
case CondStringLike:
condType = policyengine.CondStringLike
case CondStringNotLike:
condType = policyengine.CondStringNotLike
default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case strings.HasPrefix(op, "Arn"):
convertValue = noConvertFunction
switch op {
case CondArnEquals:
condType = policyengine.CondStringEquals
case CondArnNotEquals:
condType = policyengine.CondStringNotEquals
case CondArnLike:
condType = policyengine.CondStringLike
case CondArnNotLike:
condType = policyengine.CondStringNotLike
default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case strings.HasPrefix(op, "Numeric"):
// TODO
case strings.HasPrefix(op, "Date"):
convertValue = dateConvertFunction
switch op {
case CondDateEquals:
condType = policyengine.CondStringEquals
case CondDateNotEquals:
condType = policyengine.CondStringNotEquals
case CondDateLessThan:
condType = policyengine.CondStringLessThan
case CondDateLessThanEquals:
condType = policyengine.CondStringLessThanEquals
case CondDateGreaterThan:
condType = policyengine.CondStringGreaterThan
case CondDateGreaterThanEquals:
condType = policyengine.CondStringGreaterThanEquals
default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case op == CondBool:
convertValue = noConvertFunction
condType = policyengine.CondStringEqualsIgnoreCase
case op == CondIPAddress:
// todo consider using converters
// "203.0.113.0/24" -> "203.0.113.*",
// "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*"
// or having specific condition type for IP
convertValue = noConvertFunction
condType = policyengine.CondStringLike
case op == CondNotIPAddress:
convertValue = noConvertFunction
condType = policyengine.CondStringNotLike
default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
for key, values := range KVs {
for _, val := range values {
converted, err := convertValue(val)
if err != nil {
return nil, err
}
if key == CondKeyAWSPrincipalARN {
if converted, err = formPrincipalOwner(converted, resolver); err != nil {
return nil, fmt.Errorf("handle %s: %w", CondKeyAWSPrincipalARN, err)
}
key = RequestOwnerProperty
}
conditions = append(conditions, policyengine.Condition{
Op: condType,
Object: policyengine.ObjectRequest,
Key: key,
Value: converted,
})
}
}
}
return conditions, nil
}
type convertFunction func(string) (string, error)
func noConvertFunction(val string) (string, error) {
return val, nil
}
func dateConvertFunction(val string) (string, error) {
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
return val, nil
}
parsed, err := time.Parse(time.RFC3339, val)
if err != nil {
return "", err
}
return strconv.FormatInt(parsed.UTC().Unix(), 10), nil
}
func isValidChainType(chainType ChainType) bool {
return chainType == S3ChainType || chainType == NativeChainType
}
func formPrincipal(principal []string, resolver UserResolver) ([]string, error) {
res := make([]string, len(principal))
var err error
for i := range principal {
if res[i], err = formPrincipalOwner(principal[i], resolver); err != nil {
return nil, err
}
}
return res, nil
}
func formPrincipalOwner(principal string, resolver UserResolver) (string, error) {
account, user, err := parsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
key, err := resolver.GetUserKey(account, user)
if err != nil {
return "", fmt.Errorf("resolve user: %w", err)
}
return key.Address(), nil
}
func parsePrincipalAsIAMUser(principal string) (string, string, error) {
if !strings.HasPrefix(principal, arnIAMPrefix) {
return "", "", ErrInvalidPrincipalFormat
}
// iam arn format arn:aws:iam::<account>:user/<user-name-with-path>
iamResource := strings.TrimPrefix(principal, arnIAMPrefix)
sepIndex := strings.Index(iamResource, ":user/")
if sepIndex < 0 {
return "", "", ErrInvalidPrincipalFormat
}
account := iamResource[:sepIndex]
user := iamResource[sepIndex+6:]
if len(user) == 0 {
return "", "", ErrInvalidPrincipalFormat
}
userNameIndex := strings.LastIndexByte(user, '/')
if userNameIndex > -1 {
user = user[userNameIndex+1:]
if len(user) == 0 {
return "", "", ErrInvalidPrincipalFormat
}
}
return account, user, nil
}
func formResourceNamesAndConditions(chainType ChainType, names []string) ([]string, []policyengine.Condition) {
switch chainType {
case S3ChainType:
return formS3ResourceNamesAndConditions(names)
case NativeChainType:
return formNativeResourceNamesAndConditions(names)
}
panic("unknown chain type") // this must not ever happen
}
func formNativeResourceNamesAndConditions(names []string) ([]string, []policyengine.Condition) {
res := make([]string, len(names))
resCond := make([]policyengine.Condition, len(names))
for i := range names {
// we user ::: instead of :: because of node
// https://git.frostfs.info/TrueCloudLab/frostfs-node/src/commit/78cfb6aea86c01df34a534020dc63cefbad61da0/pkg/services/object/acl/ape_request.go#L42
res[i] = "native:::object/*"
resCond[i] = policyengine.Condition{
Op: policyengine.CondStringLike,
Object: policyengine.ObjectResource,
Key: ResourceFullPathProperty,
Value: strings.TrimPrefix(names[i], s3ResourcePrefix),
}
}
return res, resCond
}
func formS3ResourceNamesAndConditions(names []string) ([]string, []policyengine.Condition) {
res := make([]string, len(names))
for i := range names {
res[i] = strings.TrimPrefix(names[i], s3ResourcePrefix)
}
return res, nil
}