2023-10-19 13:15:21 +00:00
|
|
|
package iam
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2023-10-30 13:34:53 +00:00
|
|
|
"fmt"
|
2023-10-19 13:15:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
// Policy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
|
|
|
|
Policy struct {
|
|
|
|
Version string `json:"Version,omitempty"`
|
|
|
|
ID string `json:"Id,omitempty"`
|
|
|
|
Statement Statements `json:"Statement"`
|
|
|
|
}
|
|
|
|
|
|
|
|
Statements []Statement
|
|
|
|
|
|
|
|
Statement struct {
|
2023-10-30 13:34:53 +00:00
|
|
|
ID string `json:"Id,omitempty"`
|
|
|
|
SID string `json:"Sid,omitempty"`
|
|
|
|
Principal Principal `json:"Principal,omitempty"`
|
|
|
|
NotPrincipal Principal `json:"NotPrincipal,omitempty"`
|
|
|
|
Effect Effect `json:"Effect"`
|
|
|
|
Action Action `json:"Action,omitempty"`
|
|
|
|
NotAction Action `json:"NotAction,omitempty"`
|
|
|
|
Resource Resource `json:"Resource,omitempty"`
|
|
|
|
NotResource Resource `json:"NotResource,omitempty"`
|
|
|
|
Conditions Conditions `json:"Condition,omitempty"`
|
2023-10-19 13:15:21 +00:00
|
|
|
}
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
Principal map[PrincipalType][]string
|
2023-10-19 13:15:21 +00:00
|
|
|
|
|
|
|
Effect string
|
|
|
|
|
|
|
|
Action []string
|
|
|
|
|
|
|
|
Resource []string
|
|
|
|
|
|
|
|
Conditions map[string]Condition
|
|
|
|
|
|
|
|
Condition map[string][]string
|
2023-10-30 13:34:53 +00:00
|
|
|
|
|
|
|
PolicyType int
|
|
|
|
|
|
|
|
PrincipalType string
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
GeneralPolicyType PolicyType = iota
|
|
|
|
IdentityBasedPolicyType
|
|
|
|
ResourceBasedPolicyType
|
2023-10-19 13:15:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const Wildcard = "*"
|
|
|
|
|
|
|
|
const (
|
|
|
|
AllowEffect Effect = "Allow"
|
|
|
|
DenyEffect Effect = "Deny"
|
|
|
|
)
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
func (e Effect) IsValid() bool {
|
|
|
|
return e == AllowEffect || e == DenyEffect
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
AWSPrincipalType PrincipalType = "AWS"
|
|
|
|
FederatedPrincipalType PrincipalType = "Federated"
|
|
|
|
ServicePrincipalType PrincipalType = "Service"
|
|
|
|
CanonicalUserPrincipalType PrincipalType = "CanonicalUser"
|
|
|
|
)
|
|
|
|
|
|
|
|
func (p PrincipalType) IsValid() bool {
|
|
|
|
return p == AWSPrincipalType || p == FederatedPrincipalType ||
|
|
|
|
p == ServicePrincipalType || p == CanonicalUserPrincipalType
|
|
|
|
}
|
|
|
|
|
2023-10-19 13:15:21 +00:00
|
|
|
func (s *Statements) UnmarshalJSON(data []byte) error {
|
|
|
|
var list []Statement
|
|
|
|
if err := json.Unmarshal(data, &list); err == nil {
|
|
|
|
*s = list
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var elem Statement
|
|
|
|
if err := json.Unmarshal(data, &elem); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*s = []Statement{elem}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Principal) UnmarshalJSON(data []byte) error {
|
|
|
|
*p = make(Principal)
|
|
|
|
|
|
|
|
var str string
|
|
|
|
|
|
|
|
if err := json.Unmarshal(data, &str); err == nil {
|
|
|
|
if str != Wildcard {
|
|
|
|
return errors.New("invalid IAM string principal")
|
|
|
|
}
|
|
|
|
(*p)[Wildcard] = nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
m := make(map[PrincipalType]any)
|
2023-10-19 13:15:21 +00:00
|
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for key, val := range m {
|
|
|
|
element, ok := val.(string)
|
|
|
|
if ok {
|
|
|
|
(*p)[key] = []string{element}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
list, ok := val.([]any)
|
2023-10-19 13:15:21 +00:00
|
|
|
if !ok {
|
|
|
|
return errors.New("invalid principal format")
|
|
|
|
}
|
|
|
|
|
|
|
|
resList := make([]string, len(list))
|
|
|
|
for i := range list {
|
|
|
|
val, ok := list[i].(string)
|
|
|
|
if !ok {
|
|
|
|
return errors.New("invalid principal format")
|
|
|
|
}
|
|
|
|
resList[i] = val
|
|
|
|
}
|
|
|
|
|
|
|
|
(*p)[key] = resList
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Action) UnmarshalJSON(data []byte) error {
|
|
|
|
var list []string
|
|
|
|
if err := json.Unmarshal(data, &list); err == nil {
|
|
|
|
*a = list
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var elem string
|
|
|
|
if err := json.Unmarshal(data, &elem); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*a = []string{elem}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) UnmarshalJSON(data []byte) error {
|
|
|
|
var list []string
|
|
|
|
if err := json.Unmarshal(data, &list); err == nil {
|
|
|
|
*r = list
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var elem string
|
|
|
|
if err := json.Unmarshal(data, &elem); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*r = []string{elem}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Condition) UnmarshalJSON(data []byte) error {
|
|
|
|
*c = make(Condition)
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
m := make(map[string]any)
|
2023-10-19 13:15:21 +00:00
|
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for key, val := range m {
|
|
|
|
element, ok := val.(string)
|
|
|
|
if ok {
|
|
|
|
(*c)[key] = []string{element}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
list, ok := val.([]any)
|
2023-10-19 13:15:21 +00:00
|
|
|
if !ok {
|
|
|
|
return errors.New("invalid principal format")
|
|
|
|
}
|
|
|
|
|
|
|
|
resList := make([]string, len(list))
|
|
|
|
for i := range list {
|
|
|
|
val, ok := list[i].(string)
|
|
|
|
if !ok {
|
|
|
|
return errors.New("invalid principal format")
|
|
|
|
}
|
|
|
|
resList[i] = val
|
|
|
|
}
|
|
|
|
|
|
|
|
(*c)[key] = resList
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-30 13:34:53 +00:00
|
|
|
|
|
|
|
func (p Policy) Validate(typ PolicyType) error {
|
|
|
|
if err := p.validate(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch typ {
|
|
|
|
case IdentityBasedPolicyType:
|
|
|
|
return p.validateIdentityBased()
|
|
|
|
case ResourceBasedPolicyType:
|
|
|
|
return p.validateResourceBased()
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p Policy) validate() error {
|
2023-11-10 14:56:41 +00:00
|
|
|
if len(p.Statement) == 0 {
|
|
|
|
return errors.New("'Statement' is missing")
|
|
|
|
}
|
|
|
|
|
2023-10-30 13:34:53 +00:00
|
|
|
for _, statement := range p.Statement {
|
|
|
|
if !statement.Effect.IsValid() {
|
|
|
|
return fmt.Errorf("unknown effect: '%s'", statement.Effect)
|
|
|
|
}
|
|
|
|
if len(statement.Action) != 0 && len(statement.NotAction) != 0 {
|
|
|
|
return errors.New("'Actions' and 'NotAction' are mutually exclusive")
|
|
|
|
}
|
|
|
|
if statement.Resource != nil && statement.NotResource != nil {
|
|
|
|
return errors.New("'Resources' and 'NotResource' are mutually exclusive")
|
|
|
|
}
|
|
|
|
if len(statement.Resource) == 0 && len(statement.NotResource) == 0 {
|
|
|
|
return errors.New("one of 'Resources'/'NotResource' must be provided")
|
|
|
|
}
|
|
|
|
if len(statement.Principal) != 0 && len(statement.NotPrincipal) != 0 {
|
|
|
|
return errors.New("'Principal' and 'NotPrincipal' are mutually exclusive")
|
|
|
|
}
|
|
|
|
if len(statement.NotPrincipal) != 0 && statement.Effect != DenyEffect {
|
|
|
|
return errors.New("using 'NotPrincipal' with effect 'Allow' is not supported")
|
|
|
|
}
|
|
|
|
|
|
|
|
principal, _ := statement.principal()
|
|
|
|
if err := principal.validate(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p Policy) validateIdentityBased() error {
|
|
|
|
if len(p.ID) != 0 {
|
|
|
|
return errors.New("'Id' is not allowed for identity-based policy")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, statement := range p.Statement {
|
|
|
|
if len(statement.Principal) != 0 || len(statement.NotPrincipal) != 0 {
|
|
|
|
return errors.New("'Principal' and 'NotPrincipal' are not allowed for identity-based policy")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p Policy) validateResourceBased() error {
|
|
|
|
for _, statement := range p.Statement {
|
|
|
|
if len(statement.Principal) == 0 && len(statement.NotPrincipal) == 0 {
|
|
|
|
return errors.New("'Principal' or 'NotPrincipal' must be provided for resource-based policy")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s Statement) principal() (Principal, bool) {
|
|
|
|
if len(s.NotPrincipal) != 0 {
|
|
|
|
return s.NotPrincipal, true
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.Principal, false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s Statement) action() (Action, bool) {
|
|
|
|
if len(s.NotAction) != 0 {
|
|
|
|
return s.NotAction, true
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.Action, false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s Statement) resource() (Resource, bool) {
|
|
|
|
if len(s.NotResource) != 0 {
|
|
|
|
return s.NotResource, true
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.Resource, false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p Principal) validate() error {
|
|
|
|
if _, ok := p[Wildcard]; ok && len(p) == 1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
for key := range p {
|
|
|
|
if !key.IsValid() {
|
|
|
|
return fmt.Errorf("unknown principal type: '%s'", key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|