You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

263 lines
8.9 KiB

// Package utils provides utility functions for AWS IAM ARN parsing and role extraction.
package utils
import "strings"
// ARN parsing constants for AWS IAM and STS services
const (
// stsPrefix is the common prefix for all AWS STS ARNs
stsPrefix = "arn:aws:sts::"
// stsAssumedRoleMarker is the marker that identifies assumed role ARNs
stsAssumedRoleMarker = "assumed-role/"
// iamPrefix is the common prefix for all AWS IAM ARNs
iamPrefix = "arn:aws:iam::"
// iamRoleMarker is the marker that identifies IAM role ARNs
iamRoleMarker = "role/"
)
// ARNInfo contains structured information about a parsed AWS ARN.
// This provides more context than a simple string extraction, making it
// easier to debug issues and support different ARN formats.
type ARNInfo struct {
// Original is the original ARN string that was parsed
Original string
// RoleName is the extracted role name (without "role/" prefix)
// May include path components (e.g., "Division/Team/RoleName")
// Empty string indicates an invalid ARN
RoleName string
// AccountID is the AWS account ID if present in the ARN
// Empty string for legacy format ARNs (arn:aws:iam::role/Name)
// Non-empty for standard format ARNs (arn:aws:iam::ACCOUNT:role/Name)
AccountID string
}
// ExtractRoleNameFromPrincipal extracts the role name from an AWS principal ARN.
//
// It handles both STS assumed role and IAM role ARN formats, supporting both
// legacy (without account ID) and standard AWS (with account ID) formats:
// - arn:aws:sts::assumed-role/RoleName/SessionName (legacy STS format)
// - arn:aws:sts::ACCOUNT:assumed-role/RoleName/SessionName (standard STS format)
// - arn:aws:iam::role/RoleName (legacy IAM format)
// - arn:aws:iam::ACCOUNT:role/RoleName (standard IAM format)
//
// For STS assumed-role ARNs, it extracts the role name from the "assumed-role/"
// component, which is the second part of the path (e.g., "Role" from
// "arn:aws:sts::ACCOUNT:assumed-role/Role/Session"). For IAM role ARNs, it
// delegates to ExtractRoleNameFromArn.
//
// Returns an empty string if the principal format is invalid or unrecognized.
//
// Parameters:
// - principal: The AWS principal ARN string to extract the role name from
//
// Returns:
// - The extracted role name (without "role/" prefix)
// - Empty string if the principal is invalid or no role name is found
func ExtractRoleNameFromPrincipal(principal string) string {
// Handle STS assumed role format
if strings.HasPrefix(principal, stsPrefix) {
remainder := principal[len(stsPrefix):]
// Validate ARN structure: should be either "assumed-role/..." or "ACCOUNT:assumed-role/..."
// Split on ':' to separate account ID (if present) from resource type
resourcePart := remainder
if colonIdx := strings.Index(remainder, ":"); colonIdx != -1 {
// Standard format with account ID: "ACCOUNT:assumed-role/..."
resourcePart = remainder[colonIdx+1:]
}
// Verify the resource type is exactly "assumed-role/"
if !strings.HasPrefix(resourcePart, stsAssumedRoleMarker) {
return ""
}
// Extract role name after "assumed-role/"
afterMarker := resourcePart[len(stsAssumedRoleMarker):]
if slash := strings.Index(afterMarker, "/"); slash != -1 {
return afterMarker[:slash]
}
return afterMarker
}
// Handle IAM role format
return ExtractRoleNameFromArn(principal)
}
// ExtractRoleNameFromArn extracts the role name from an AWS IAM role ARN.
//
// It handles both legacy and standard AWS IAM role ARN formats:
// - arn:aws:iam::role/RoleName (legacy format without account ID)
// - arn:aws:iam::ACCOUNT:role/RoleName (standard AWS format with account ID)
//
// The function validates the ARN structure to ensure the resource type is exactly
// "role", preventing security issues where malicious ARNs like
// "arn:aws:iam::123456789012:user/role/malicious" could be accepted.
//
// If the ARN contains a path component (e.g., "role/Division/Team/RoleName"),
// the entire path is returned after "role/".
//
// Returns an empty string if:
// - The ARN does not start with "arn:aws:iam::"
// - The resource type is not exactly "role"
// - The input is empty or invalid
//
// This function is commonly used in STS (Security Token Service) role assumption
// validation to extract the role name from principal ARNs or to validate IAM
// role ARNs during credential assumption checks.
//
// Parameters:
// - roleArn: The IAM role ARN string to extract the role name from
//
// Returns:
// - The extracted role name (without "role/" prefix, may include path)
// - Empty string if the ARN is invalid or no role name is found
func ExtractRoleNameFromArn(roleArn string) string {
if !strings.HasPrefix(roleArn, iamPrefix) {
return ""
}
remainder := roleArn[len(iamPrefix):]
// Validate ARN structure: should be either "role/..." or "ACCOUNT:role/..."
// Split on ':' to separate account ID (if present) from resource type
resourcePart := remainder
if colonIdx := strings.Index(remainder, ":"); colonIdx != -1 {
// Standard format with account ID: "ACCOUNT:role/..."
resourcePart = remainder[colonIdx+1:]
}
// Verify the resource type is exactly "role/"
if !strings.HasPrefix(resourcePart, iamRoleMarker) {
return ""
}
// Extract role name after "role/"
return resourcePart[len(iamRoleMarker):]
}
// ParseRoleARN parses an AWS IAM role ARN and returns structured information.
//
// It handles both legacy and standard AWS IAM role ARN formats:
// - arn:aws:iam::role/RoleName (legacy format without account ID)
// - arn:aws:iam::ACCOUNT:role/RoleName (standard AWS format with account ID)
//
// The function validates the ARN structure to ensure the resource type is exactly
// "role", extracting the role name and account ID (if present).
//
// Parameters:
// - roleArn: The IAM role ARN string to parse
//
// Returns:
// - ARNInfo struct containing parsed information
// - RoleName will be empty if parsing fails or ARN is malformed
func ParseRoleARN(roleArn string) ARNInfo {
info := ARNInfo{
Original: roleArn,
}
if !strings.HasPrefix(roleArn, iamPrefix) {
return info
}
remainder := roleArn[len(iamPrefix):]
// Validate ARN structure: should be either "role/..." or "ACCOUNT:role/..."
// Split on ':' to separate account ID (if present) from resource type
resourcePart := remainder
accountPart := ""
if colonIdx := strings.Index(remainder, ":"); colonIdx != -1 {
// Standard format with account ID: "ACCOUNT:role/..."
accountPart = remainder[:colonIdx]
resourcePart = remainder[colonIdx+1:]
}
// Verify the resource type is exactly "role/"
if !strings.HasPrefix(resourcePart, iamRoleMarker) {
// Invalid resource type
return info
}
// Extract role name (everything after "role/")
info.RoleName = resourcePart[len(iamRoleMarker):]
if info.RoleName == "" {
// Empty role name is invalid
return info
}
// Set account ID if present
info.AccountID = accountPart
return info
}
// ParsePrincipalARN parses an AWS principal ARN (STS or IAM) and returns structured information.
//
// It handles both STS assumed role and IAM role ARN formats:
// - arn:aws:sts::assumed-role/RoleName/SessionName (legacy STS format)
// - arn:aws:sts::ACCOUNT:assumed-role/RoleName/SessionName (standard STS format)
// - arn:aws:iam::role/RoleName (legacy IAM format)
// - arn:aws:iam::ACCOUNT:role/RoleName (standard IAM format)
//
// The function validates the ARN structure to ensure the resource type is exactly
// "assumed-role" for STS or delegates to ParseRoleARN for IAM ARNs.
//
// Parameters:
// - principal: The AWS principal ARN string to parse
//
// Returns:
// - ARNInfo struct containing parsed information
// - RoleName will be empty if parsing fails or ARN is malformed
func ParsePrincipalARN(principal string) ARNInfo {
// Handle STS assumed role format
if strings.HasPrefix(principal, stsPrefix) {
info := ARNInfo{
Original: principal,
}
remainder := principal[len(stsPrefix):]
// Validate ARN structure: should be either "assumed-role/..." or "ACCOUNT:assumed-role/..."
// Split on ':' to separate account ID (if present) from resource type
resourcePart := remainder
accountPart := ""
if colonIdx := strings.Index(remainder, ":"); colonIdx != -1 {
// Standard format with account ID: "ACCOUNT:assumed-role/..."
accountPart = remainder[:colonIdx]
resourcePart = remainder[colonIdx+1:]
}
// Verify the resource type is exactly "assumed-role/"
if !strings.HasPrefix(resourcePart, stsAssumedRoleMarker) {
// Invalid resource type
return info
}
// Extract role name (between "assumed-role/" and next "/")
afterMarker := resourcePart[len(stsAssumedRoleMarker):]
if slash := strings.Index(afterMarker, "/"); slash != -1 {
info.RoleName = afterMarker[:slash]
} else {
info.RoleName = afterMarker
}
// Validate that role name is not empty
if info.RoleName == "" {
return info
}
// Set account ID if present
info.AccountID = accountPart
return info
}
// Handle IAM role format
return ParseRoleARN(principal)
}