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
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)
|
|
}
|