feat: add live GitHub OIDC end-to-end test for AssumeRoleWithWebIdentity

Add IAMAssumeRoleWithWebIdentity_github_oidc_live, the only web-identity test that exercises AssumeRoleWithWebIdentity against a real external OIDC provider end-to-end: GitHub Actions' own issuer, with real discovery-document fetch, JWKS fetch, RS256 signature verification, claims mapping, and session credential issuance. Every other web-identity test in the suite uses a fake token that never reaches real signature verification. The test registers a throwaway OIDC provider and trust role scoped to this repo (via a distinct test audience and repo-scoped sub condition), fetches a real ID token from GitHub's runtime endpoint, assumes the role, and confirms the issued session credentials work with a follow-up GetCallerIdentity call. It cleans up the role and provider unconditionally and skips itself when run outside a GitHub Actions job with id-token: write permission (e.g. local runs or fork PRs, where GitHub downgrades OIDC permissions to read-only).

Add functional-iam-oidc.yml to run this test in CI on push to main and on same-repo pull_request runs, isolated from the full iam suite since it's the only test needing id-token: write.

Add a SKIP counter and skipF() alongside the existing runF/passF/failF, and report it in the final RAN/PASS/FAIL summary, so a test opting out via skipF() (as this one does when OIDC env vars aren't present) is visible instead of silently absent from the count.
This commit is contained in:
niksis02
2026-08-25 01:07:33 +04:00
parent ab2b816633
commit 2e22a42324
5 changed files with 439 additions and 100 deletions
+96 -94
View File
@@ -1474,6 +1474,7 @@ func TestIAMAssumeRoleWithWebIdentity(ts *TestState) {
ts.Run(IAMAssumeRoleWithWebIdentity_oaud_condition_mismatch)
ts.Run(IAMAssumeRoleWithWebIdentity_issuer_trailing_slash_mismatch)
ts.Run(IAMAssumeRoleWithWebIdentity_issuer_scheme_mismatch)
ts.Run(IAMAssumeRoleWithWebIdentity_github_oidc_live)
}
func TestIAMGetCallerIdentity(ts *TestState) {
@@ -2177,6 +2178,101 @@ func GetIntTests() IntTests {
"IAMUpdateOpenIDConnectProviderThumbprint_non_existing_provider": IAMUpdateOpenIDConnectProviderThumbprint_non_existing_provider,
"IAMUpdateOpenIDConnectProviderThumbprint_success": IAMUpdateOpenIDConnectProviderThumbprint_success,
"IAMUpdateOpenIDConnectProviderThumbprint_boundary_max_thumbprints": IAMUpdateOpenIDConnectProviderThumbprint_boundary_max_thumbprints,
"IAMAssumeRoleWithWebIdentity_missing_role_arn": IAMAssumeRoleWithWebIdentity_missing_role_arn,
"IAMAssumeRoleWithWebIdentity_role_arn_too_short": IAMAssumeRoleWithWebIdentity_role_arn_too_short,
"IAMAssumeRoleWithWebIdentity_malformed_duration": IAMAssumeRoleWithWebIdentity_malformed_duration,
"IAMAssumeRoleWithWebIdentity_wrong_version_is_invalid_action": IAMAssumeRoleWithWebIdentity_wrong_version_is_invalid_action,
"IAMAssumeRoleWithWebIdentity_malformed_token": IAMAssumeRoleWithWebIdentity_malformed_token,
"IAMAssumeRoleWithWebIdentity_duration_exceeds_role_max": IAMAssumeRoleWithWebIdentity_duration_exceeds_role_max,
"IAMAssumeRoleWithWebIdentity_nonexistent_role": IAMAssumeRoleWithWebIdentity_nonexistent_role,
"IAMAssumeRoleWithWebIdentity_no_matching_principal": IAMAssumeRoleWithWebIdentity_no_matching_principal,
"IAMAssumeRoleWithWebIdentity_no_issuer_match": IAMAssumeRoleWithWebIdentity_no_issuer_match,
"IAMAssumeRoleWithWebIdentity_condition_failed": IAMAssumeRoleWithWebIdentity_condition_failed,
"IAMAssumeRoleWithWebIdentity_explicit_deny": IAMAssumeRoleWithWebIdentity_explicit_deny,
"IAMAssumeRoleWithWebIdentity_audience_not_in_client_id_list": IAMAssumeRoleWithWebIdentity_audience_not_in_client_id_list,
"IAMAssumeRoleWithWebIdentity_empty_client_id_list": IAMAssumeRoleWithWebIdentity_empty_client_id_list,
"IAMAssumeRoleWithWebIdentity_idp_communication_error": IAMAssumeRoleWithWebIdentity_idp_communication_error,
"IAMAssumeRoleWithWebIdentity_role_arn_path_mismatch": IAMAssumeRoleWithWebIdentity_role_arn_path_mismatch,
"IAMAssumeRoleWithWebIdentity_policy_arns_rejected": IAMAssumeRoleWithWebIdentity_policy_arns_rejected,
"IAMAssumeRoleWithWebIdentity_provider_id_rejected": IAMAssumeRoleWithWebIdentity_provider_id_rejected,
"IAMAssumeRoleWithWebIdentity_session_policy_too_large": IAMAssumeRoleWithWebIdentity_session_policy_too_large,
"IAMAssumeRoleWithWebIdentity_session_policy_invalid": IAMAssumeRoleWithWebIdentity_session_policy_invalid,
"IAMAssumeRoleWithWebIdentity_oaud_condition_matches": IAMAssumeRoleWithWebIdentity_oaud_condition_matches,
"IAMAssumeRoleWithWebIdentity_oaud_condition_mismatch": IAMAssumeRoleWithWebIdentity_oaud_condition_mismatch,
"IAMAssumeRoleWithWebIdentity_issuer_trailing_slash_mismatch": IAMAssumeRoleWithWebIdentity_issuer_trailing_slash_mismatch,
"IAMAssumeRoleWithWebIdentity_issuer_scheme_mismatch": IAMAssumeRoleWithWebIdentity_issuer_scheme_mismatch,
"IAMAssumeRoleWithWebIdentity_github_oidc_live": IAMAssumeRoleWithWebIdentity_github_oidc_live,
"IAMGetCallerIdentity_root_success": IAMGetCallerIdentity_root_success,
"IAMGetCallerIdentity_user_success": IAMGetCallerIdentity_user_success,
"IAMGetCallerIdentity_unknown_access_key": IAMGetCallerIdentity_unknown_access_key,
"IAMGetCallerIdentity_no_auth": IAMGetCallerIdentity_no_auth,
"IAMGetCallerIdentity_wrong_version_is_invalid_action": IAMGetCallerIdentity_wrong_version_is_invalid_action,
"IAMGetCallerIdentity_incorrect_service_scope": IAMGetCallerIdentity_incorrect_service_scope,
"IAMAccessControl_ImplicitDenyNoMatchingPolicy": IAMAccessControl_ImplicitDenyNoMatchingPolicy,
"IAMAccessControl_AllowGrantsMatchingRequest": IAMAccessControl_AllowGrantsMatchingRequest,
"IAMAccessControl_NonMatchingStatementDoesNotGrant": IAMAccessControl_NonMatchingStatementDoesNotGrant,
"IAMAccessControl_ExplicitDenyOverridesAllow": IAMAccessControl_ExplicitDenyOverridesAllow,
"IAMAccessControl_MultipleStatementsEvaluatedIndependently": IAMAccessControl_MultipleStatementsEvaluatedIndependently,
"IAMAccessControl_MultipleInlinePoliciesCombinedAllow": IAMAccessControl_MultipleInlinePoliciesCombinedAllow,
"IAMAccessControl_MultipleInlinePoliciesExplicitDenyWins": IAMAccessControl_MultipleInlinePoliciesExplicitDenyWins,
"IAMAccessControl_EffectNonMatchingAllowStillImplicitlyDenies": IAMAccessControl_EffectNonMatchingAllowStillImplicitlyDenies,
"IAMAccessControl_EffectNonMatchingDenyDoesNotBlockUnrelatedAllow": IAMAccessControl_EffectNonMatchingDenyDoesNotBlockUnrelatedAllow,
"IAMAccessControl_ActionMatchingVariants": IAMAccessControl_ActionMatchingVariants,
"IAMAccessControl_ActionAllowOneDenyAnotherByOmission": IAMAccessControl_ActionAllowOneDenyAnotherByOmission,
"IAMAccessControl_ActionExplicitDenySubsetOfWildcardAllow": IAMAccessControl_ActionExplicitDenySubsetOfWildcardAllow,
"IAMAccessControl_NotActionAllowGrantsEverythingExceptExcluded": IAMAccessControl_NotActionAllowGrantsEverythingExceptExcluded,
"IAMAccessControl_NotActionDenyBlocksEverythingExceptExcluded": IAMAccessControl_NotActionDenyBlocksEverythingExceptExcluded,
"IAMAccessControl_ResourceMatchingVariants": IAMAccessControl_ResourceMatchingVariants,
"IAMAccessControl_ResourceOneAllowedOneDeniedSameAction": IAMAccessControl_ResourceOneAllowedOneDeniedSameAction,
"IAMAccessControl_ResourceWildcardRequiredForListAction": IAMAccessControl_ResourceWildcardRequiredForListAction,
"IAMAccessControl_ResourceExplicitDenyOverridesBroaderAllow": IAMAccessControl_ResourceExplicitDenyOverridesBroaderAllow,
"IAMAccessControl_NotResourceExcludesTarget": IAMAccessControl_NotResourceExcludesTarget,
"IAMAccessControl_NotResourceMultipleExcludedResources": IAMAccessControl_NotResourceMultipleExcludedResources,
"IAMAccessControl_NotResourceWildcardExclusion": IAMAccessControl_NotResourceWildcardExclusion,
"IAMAccessControl_ConditionStringOperators": IAMAccessControl_ConditionStringOperators,
"IAMAccessControl_ConditionStringMultipleExpectedValuesOR": IAMAccessControl_ConditionStringMultipleExpectedValuesOR,
"IAMAccessControl_ConditionArnOperators": IAMAccessControl_ConditionArnOperators,
"IAMAccessControl_ConditionIpAddressRealSourceIp": IAMAccessControl_ConditionIpAddressRealSourceIp,
"IAMAccessControl_ConditionIpAddressExplicitDenyOverridesBroaderAllow": IAMAccessControl_ConditionIpAddressExplicitDenyOverridesBroaderAllow,
"IAMAccessControl_ConditionMultipleContextKeysANDed": IAMAccessControl_ConditionMultipleContextKeysANDed,
"IAMAccessControl_ConditionAllowMatchesDenyConditionDoesNotApply": IAMAccessControl_ConditionAllowMatchesDenyConditionDoesNotApply,
"IAMAccessControl_ConditionAllowAndDenyBothMatchDenyWins": IAMAccessControl_ConditionAllowAndDenyBothMatchDenyWins,
"IAMAccessControl_ConditionOneFailedConditionVoidsStatement": IAMAccessControl_ConditionOneFailedConditionVoidsStatement,
"IAMAccessControl_ConditionNullPrincipalTag": IAMAccessControl_ConditionNullPrincipalTag,
"IAMAccessControl_ConditionIfExistsPrincipalTag": IAMAccessControl_ConditionIfExistsPrincipalTag,
"IAMAccessControl_ConditionResourceTagOnTarget": IAMAccessControl_ConditionResourceTagOnTarget,
"IAMAccessControl_ConditionRequestTagOnCreateUser": IAMAccessControl_ConditionRequestTagOnCreateUser,
"IAMAccessControl_ConditionCurrentTimeBroadWindow": IAMAccessControl_ConditionCurrentTimeBroadWindow,
"IAMAccessControl_ConditionNumericOperators": IAMAccessControl_ConditionNumericOperators,
"IAMAccessControl_ConditionDateOperators": IAMAccessControl_ConditionDateOperators,
"IAMAccessControl_ConditionBoolOperator": IAMAccessControl_ConditionBoolOperator,
"IAMAccessControl_ConditionNullOperatorClaim": IAMAccessControl_ConditionNullOperatorClaim,
"IAMAccessControl_ConditionBinaryEqualsOperator": IAMAccessControl_ConditionBinaryEqualsOperator,
"IAMAccessControl_ConditionForAnyValueOperator": IAMAccessControl_ConditionForAnyValueOperator,
"IAMAccessControl_ConditionForAllValuesOperator": IAMAccessControl_ConditionForAllValuesOperator,
"IAMAccessControl_ConditionIfExistsTrustClaim": IAMAccessControl_ConditionIfExistsTrustClaim,
"IAMAccessControl_ConditionMultipleOperatorBlocksANDedTrust": IAMAccessControl_ConditionMultipleOperatorBlocksANDedTrust,
"IAMAccessControl_TrustPolicyFederatedExactMatchAllowed": IAMAccessControl_TrustPolicyFederatedExactMatchAllowed,
"IAMAccessControl_TrustPolicyFederatedWrongProviderDenied": IAMAccessControl_TrustPolicyFederatedWrongProviderDenied,
"IAMAccessControl_TrustPolicyFederatedArrayMatchesAny": IAMAccessControl_TrustPolicyFederatedArrayMatchesAny,
"IAMAccessControl_TrustPolicyNonFederatedPrincipalsIgnored": IAMAccessControl_TrustPolicyNonFederatedPrincipalsIgnored,
"IAMAccessControl_TrustPolicyStringEqualsSubjectExactAllowed": IAMAccessControl_TrustPolicyStringEqualsSubjectExactAllowed,
"IAMAccessControl_TrustPolicyStringEqualsSubjectMismatchDenied": IAMAccessControl_TrustPolicyStringEqualsSubjectMismatchDenied,
"IAMAccessControl_TrustPolicyStringLikeBranchWildcardAllowed": IAMAccessControl_TrustPolicyStringLikeBranchWildcardAllowed,
"IAMAccessControl_TrustPolicyStringLikeTagSubjectDenied": IAMAccessControl_TrustPolicyStringLikeTagSubjectDenied,
"IAMAccessControl_TrustPolicyAudienceCorrectAllowed": IAMAccessControl_TrustPolicyAudienceCorrectAllowed,
"IAMAccessControl_TrustPolicyAudienceIncorrectDenied": IAMAccessControl_TrustPolicyAudienceIncorrectDenied,
"IAMAccessControl_TrustPolicyMultipleAudiencesArrayAllowed": IAMAccessControl_TrustPolicyMultipleAudiencesArrayAllowed,
"IAMAccessControl_TrustPolicyAudienceAndSubjectBothMustMatch": IAMAccessControl_TrustPolicyAudienceAndSubjectBothMustMatch,
"IAMAccessControl_TrustPolicyExplicitDenyStatement": IAMAccessControl_TrustPolicyExplicitDenyStatement,
"IAMAccessControl_TrustPolicyMultipleStatementsSecondGrants": IAMAccessControl_TrustPolicyMultipleStatementsSecondGrants,
"IAMAccessControl_TrustPolicyMissingRequiredClaimDenied": IAMAccessControl_TrustPolicyMissingRequiredClaimDenied,
"IAMAccessControl_UserInlinePolicyWorkflow": IAMAccessControl_UserInlinePolicyWorkflow,
"IAMAccessControl_UserPathScopedResourceGrantsOnlyMatchingPath": IAMAccessControl_UserPathScopedResourceGrantsOnlyMatchingPath,
"IAMAccessControl_RolePermissionPolicyDoesNotAffectAssumptionDecision": IAMAccessControl_RolePermissionPolicyDoesNotAffectAssumptionDecision,
"IAMAccessControl_RoleTrustDenialIndependentOfPermissionPolicy": IAMAccessControl_RoleTrustDenialIndependentOfPermissionPolicy,
"IAMAccessControl_CrossIdentity_UnrelatedRoleCannotBeAssumedViaWrongIssuer": IAMAccessControl_CrossIdentity_UnrelatedRoleCannotBeAssumedViaWrongIssuer,
"IAMAccessControl_CrossIdentity_AssumeRoleWithWebIdentityHasNoCallerIdentityCheck": IAMAccessControl_CrossIdentity_AssumeRoleWithWebIdentityHasNoCallerIdentityCheck,
"PresignedAuth_security_token_not_supported": PresignedAuth_security_token_not_supported,
"PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm,
"PresignedAuth_ECDSA_not_supported": PresignedAuth_ECDSA_not_supported,
@@ -2989,99 +3085,5 @@ func GetIntTests() IntTests {
"PostObject_multiple_checksum_headers": PostObject_multiple_checksum_headers,
"PostObject_checksums_success": PostObject_checksums_success,
"PostObject_success_double_dash_boundary": PostObject_success_double_dash_boundary,
"IAMAssumeRoleWithWebIdentity_missing_role_arn": IAMAssumeRoleWithWebIdentity_missing_role_arn,
"IAMAssumeRoleWithWebIdentity_role_arn_too_short": IAMAssumeRoleWithWebIdentity_role_arn_too_short,
"IAMAssumeRoleWithWebIdentity_malformed_duration": IAMAssumeRoleWithWebIdentity_malformed_duration,
"IAMAssumeRoleWithWebIdentity_wrong_version_is_invalid_action": IAMAssumeRoleWithWebIdentity_wrong_version_is_invalid_action,
"IAMAssumeRoleWithWebIdentity_malformed_token": IAMAssumeRoleWithWebIdentity_malformed_token,
"IAMAssumeRoleWithWebIdentity_duration_exceeds_role_max": IAMAssumeRoleWithWebIdentity_duration_exceeds_role_max,
"IAMAssumeRoleWithWebIdentity_nonexistent_role": IAMAssumeRoleWithWebIdentity_nonexistent_role,
"IAMAssumeRoleWithWebIdentity_no_matching_principal": IAMAssumeRoleWithWebIdentity_no_matching_principal,
"IAMAssumeRoleWithWebIdentity_no_issuer_match": IAMAssumeRoleWithWebIdentity_no_issuer_match,
"IAMAssumeRoleWithWebIdentity_condition_failed": IAMAssumeRoleWithWebIdentity_condition_failed,
"IAMAssumeRoleWithWebIdentity_explicit_deny": IAMAssumeRoleWithWebIdentity_explicit_deny,
"IAMAssumeRoleWithWebIdentity_audience_not_in_client_id_list": IAMAssumeRoleWithWebIdentity_audience_not_in_client_id_list,
"IAMAssumeRoleWithWebIdentity_empty_client_id_list": IAMAssumeRoleWithWebIdentity_empty_client_id_list,
"IAMAssumeRoleWithWebIdentity_idp_communication_error": IAMAssumeRoleWithWebIdentity_idp_communication_error,
"IAMAssumeRoleWithWebIdentity_role_arn_path_mismatch": IAMAssumeRoleWithWebIdentity_role_arn_path_mismatch,
"IAMAssumeRoleWithWebIdentity_policy_arns_rejected": IAMAssumeRoleWithWebIdentity_policy_arns_rejected,
"IAMAssumeRoleWithWebIdentity_provider_id_rejected": IAMAssumeRoleWithWebIdentity_provider_id_rejected,
"IAMAssumeRoleWithWebIdentity_session_policy_too_large": IAMAssumeRoleWithWebIdentity_session_policy_too_large,
"IAMAssumeRoleWithWebIdentity_session_policy_invalid": IAMAssumeRoleWithWebIdentity_session_policy_invalid,
"IAMAssumeRoleWithWebIdentity_oaud_condition_matches": IAMAssumeRoleWithWebIdentity_oaud_condition_matches,
"IAMAssumeRoleWithWebIdentity_oaud_condition_mismatch": IAMAssumeRoleWithWebIdentity_oaud_condition_mismatch,
"IAMAssumeRoleWithWebIdentity_issuer_trailing_slash_mismatch": IAMAssumeRoleWithWebIdentity_issuer_trailing_slash_mismatch,
"IAMAssumeRoleWithWebIdentity_issuer_scheme_mismatch": IAMAssumeRoleWithWebIdentity_issuer_scheme_mismatch,
"IAMGetCallerIdentity_root_success": IAMGetCallerIdentity_root_success,
"IAMGetCallerIdentity_user_success": IAMGetCallerIdentity_user_success,
"IAMGetCallerIdentity_unknown_access_key": IAMGetCallerIdentity_unknown_access_key,
"IAMGetCallerIdentity_no_auth": IAMGetCallerIdentity_no_auth,
"IAMGetCallerIdentity_wrong_version_is_invalid_action": IAMGetCallerIdentity_wrong_version_is_invalid_action,
"IAMGetCallerIdentity_incorrect_service_scope": IAMGetCallerIdentity_incorrect_service_scope,
"IAMAccessControl_ImplicitDenyNoMatchingPolicy": IAMAccessControl_ImplicitDenyNoMatchingPolicy,
"IAMAccessControl_AllowGrantsMatchingRequest": IAMAccessControl_AllowGrantsMatchingRequest,
"IAMAccessControl_NonMatchingStatementDoesNotGrant": IAMAccessControl_NonMatchingStatementDoesNotGrant,
"IAMAccessControl_ExplicitDenyOverridesAllow": IAMAccessControl_ExplicitDenyOverridesAllow,
"IAMAccessControl_MultipleStatementsEvaluatedIndependently": IAMAccessControl_MultipleStatementsEvaluatedIndependently,
"IAMAccessControl_MultipleInlinePoliciesCombinedAllow": IAMAccessControl_MultipleInlinePoliciesCombinedAllow,
"IAMAccessControl_MultipleInlinePoliciesExplicitDenyWins": IAMAccessControl_MultipleInlinePoliciesExplicitDenyWins,
"IAMAccessControl_EffectNonMatchingAllowStillImplicitlyDenies": IAMAccessControl_EffectNonMatchingAllowStillImplicitlyDenies,
"IAMAccessControl_EffectNonMatchingDenyDoesNotBlockUnrelatedAllow": IAMAccessControl_EffectNonMatchingDenyDoesNotBlockUnrelatedAllow,
"IAMAccessControl_ActionMatchingVariants": IAMAccessControl_ActionMatchingVariants,
"IAMAccessControl_ActionAllowOneDenyAnotherByOmission": IAMAccessControl_ActionAllowOneDenyAnotherByOmission,
"IAMAccessControl_ActionExplicitDenySubsetOfWildcardAllow": IAMAccessControl_ActionExplicitDenySubsetOfWildcardAllow,
"IAMAccessControl_NotActionAllowGrantsEverythingExceptExcluded": IAMAccessControl_NotActionAllowGrantsEverythingExceptExcluded,
"IAMAccessControl_NotActionDenyBlocksEverythingExceptExcluded": IAMAccessControl_NotActionDenyBlocksEverythingExceptExcluded,
"IAMAccessControl_ResourceMatchingVariants": IAMAccessControl_ResourceMatchingVariants,
"IAMAccessControl_ResourceOneAllowedOneDeniedSameAction": IAMAccessControl_ResourceOneAllowedOneDeniedSameAction,
"IAMAccessControl_ResourceWildcardRequiredForListAction": IAMAccessControl_ResourceWildcardRequiredForListAction,
"IAMAccessControl_ResourceExplicitDenyOverridesBroaderAllow": IAMAccessControl_ResourceExplicitDenyOverridesBroaderAllow,
"IAMAccessControl_NotResourceExcludesTarget": IAMAccessControl_NotResourceExcludesTarget,
"IAMAccessControl_NotResourceMultipleExcludedResources": IAMAccessControl_NotResourceMultipleExcludedResources,
"IAMAccessControl_NotResourceWildcardExclusion": IAMAccessControl_NotResourceWildcardExclusion,
"IAMAccessControl_ConditionStringOperators": IAMAccessControl_ConditionStringOperators,
"IAMAccessControl_ConditionStringMultipleExpectedValuesOR": IAMAccessControl_ConditionStringMultipleExpectedValuesOR,
"IAMAccessControl_ConditionArnOperators": IAMAccessControl_ConditionArnOperators,
"IAMAccessControl_ConditionIpAddressRealSourceIp": IAMAccessControl_ConditionIpAddressRealSourceIp,
"IAMAccessControl_ConditionIpAddressExplicitDenyOverridesBroaderAllow": IAMAccessControl_ConditionIpAddressExplicitDenyOverridesBroaderAllow,
"IAMAccessControl_ConditionMultipleContextKeysANDed": IAMAccessControl_ConditionMultipleContextKeysANDed,
"IAMAccessControl_ConditionAllowMatchesDenyConditionDoesNotApply": IAMAccessControl_ConditionAllowMatchesDenyConditionDoesNotApply,
"IAMAccessControl_ConditionAllowAndDenyBothMatchDenyWins": IAMAccessControl_ConditionAllowAndDenyBothMatchDenyWins,
"IAMAccessControl_ConditionOneFailedConditionVoidsStatement": IAMAccessControl_ConditionOneFailedConditionVoidsStatement,
"IAMAccessControl_ConditionNullPrincipalTag": IAMAccessControl_ConditionNullPrincipalTag,
"IAMAccessControl_ConditionIfExistsPrincipalTag": IAMAccessControl_ConditionIfExistsPrincipalTag,
"IAMAccessControl_ConditionResourceTagOnTarget": IAMAccessControl_ConditionResourceTagOnTarget,
"IAMAccessControl_ConditionRequestTagOnCreateUser": IAMAccessControl_ConditionRequestTagOnCreateUser,
"IAMAccessControl_ConditionCurrentTimeBroadWindow": IAMAccessControl_ConditionCurrentTimeBroadWindow,
"IAMAccessControl_ConditionNumericOperators": IAMAccessControl_ConditionNumericOperators,
"IAMAccessControl_ConditionDateOperators": IAMAccessControl_ConditionDateOperators,
"IAMAccessControl_ConditionBoolOperator": IAMAccessControl_ConditionBoolOperator,
"IAMAccessControl_ConditionNullOperatorClaim": IAMAccessControl_ConditionNullOperatorClaim,
"IAMAccessControl_ConditionBinaryEqualsOperator": IAMAccessControl_ConditionBinaryEqualsOperator,
"IAMAccessControl_ConditionForAnyValueOperator": IAMAccessControl_ConditionForAnyValueOperator,
"IAMAccessControl_ConditionForAllValuesOperator": IAMAccessControl_ConditionForAllValuesOperator,
"IAMAccessControl_ConditionIfExistsTrustClaim": IAMAccessControl_ConditionIfExistsTrustClaim,
"IAMAccessControl_ConditionMultipleOperatorBlocksANDedTrust": IAMAccessControl_ConditionMultipleOperatorBlocksANDedTrust,
"IAMAccessControl_TrustPolicyFederatedExactMatchAllowed": IAMAccessControl_TrustPolicyFederatedExactMatchAllowed,
"IAMAccessControl_TrustPolicyFederatedWrongProviderDenied": IAMAccessControl_TrustPolicyFederatedWrongProviderDenied,
"IAMAccessControl_TrustPolicyFederatedArrayMatchesAny": IAMAccessControl_TrustPolicyFederatedArrayMatchesAny,
"IAMAccessControl_TrustPolicyNonFederatedPrincipalsIgnored": IAMAccessControl_TrustPolicyNonFederatedPrincipalsIgnored,
"IAMAccessControl_TrustPolicyStringEqualsSubjectExactAllowed": IAMAccessControl_TrustPolicyStringEqualsSubjectExactAllowed,
"IAMAccessControl_TrustPolicyStringEqualsSubjectMismatchDenied": IAMAccessControl_TrustPolicyStringEqualsSubjectMismatchDenied,
"IAMAccessControl_TrustPolicyStringLikeBranchWildcardAllowed": IAMAccessControl_TrustPolicyStringLikeBranchWildcardAllowed,
"IAMAccessControl_TrustPolicyStringLikeTagSubjectDenied": IAMAccessControl_TrustPolicyStringLikeTagSubjectDenied,
"IAMAccessControl_TrustPolicyAudienceCorrectAllowed": IAMAccessControl_TrustPolicyAudienceCorrectAllowed,
"IAMAccessControl_TrustPolicyAudienceIncorrectDenied": IAMAccessControl_TrustPolicyAudienceIncorrectDenied,
"IAMAccessControl_TrustPolicyMultipleAudiencesArrayAllowed": IAMAccessControl_TrustPolicyMultipleAudiencesArrayAllowed,
"IAMAccessControl_TrustPolicyAudienceAndSubjectBothMustMatch": IAMAccessControl_TrustPolicyAudienceAndSubjectBothMustMatch,
"IAMAccessControl_TrustPolicyExplicitDenyStatement": IAMAccessControl_TrustPolicyExplicitDenyStatement,
"IAMAccessControl_TrustPolicyMultipleStatementsSecondGrants": IAMAccessControl_TrustPolicyMultipleStatementsSecondGrants,
"IAMAccessControl_TrustPolicyMissingRequiredClaimDenied": IAMAccessControl_TrustPolicyMissingRequiredClaimDenied,
"IAMAccessControl_UserInlinePolicyWorkflow": IAMAccessControl_UserInlinePolicyWorkflow,
"IAMAccessControl_UserPathScopedResourceGrantsOnlyMatchingPath": IAMAccessControl_UserPathScopedResourceGrantsOnlyMatchingPath,
"IAMAccessControl_RolePermissionPolicyDoesNotAffectAssumptionDecision": IAMAccessControl_RolePermissionPolicyDoesNotAffectAssumptionDecision,
"IAMAccessControl_RoleTrustDenialIndependentOfPermissionPolicy": IAMAccessControl_RoleTrustDenialIndependentOfPermissionPolicy,
"IAMAccessControl_CrossIdentity_UnrelatedRoleCannotBeAssumedViaWrongIssuer": IAMAccessControl_CrossIdentity_UnrelatedRoleCannotBeAssumedViaWrongIssuer,
"IAMAccessControl_CrossIdentity_AssumeRoleWithWebIdentityHasNoCallerIdentityCheck": IAMAccessControl_CrossIdentity_AssumeRoleWithWebIdentityHasNoCallerIdentityCheck,
}
}
@@ -0,0 +1,242 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package integration
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const (
// githubOIDCIssuerURL is GitHub Actions' own OIDC token issuer: a real,
// publicly reachable HTTPS endpoint with a CA-issued certificate.
githubOIDCIssuerURL = "https://token.actions.githubusercontent.com"
// githubOIDCTestAudience is deliberately distinct from GitHub's default
// audience (which is the caller's own server URL). If this org ever
// configures a real cloud-provider role trusting
// token.actions.githubusercontent.com for this repo (e.g. for
// publishing/deploys), a leaked test token must not be replayable
// against that unrelated trust relationship - binding the throwaway
// role's trust policy to this audience (instead of GitHub's default)
// is what prevents that.
githubOIDCTestAudience = "versitygw-integration-tests"
)
// IAMAssumeRoleWithWebIdentity_github_oidc_live exercises
// AssumeRoleWithWebIdentity against a REAL external OIDC identity provider —
// GitHub Actions' own OIDC issuer — end-to-end: discovery-document fetch,
// JWKS fetch, real RS256 signature verification, claims mapping, and
// session credential issuance. It's the only web-identity test that does
// this; every other one in this package uses a fake token that never
// reaches real signature verification.
func IAMAssumeRoleWithWebIdentity_github_oidc_live(s *S3Conf) error {
testName := "IAMAssumeRoleWithWebIdentity_github_oidc_live"
reqURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL")
reqToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
if reqURL == "" || reqToken == "" {
skipF("%v: ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN not set "+
"(expected outside a GitHub Actions job with id-token: write permission)", testName)
return nil
}
return iamActionHandler(s, testName, func(client *iam.Client) error {
repo := os.Getenv("GITHUB_REPOSITORY")
if repo == "" {
return fmt.Errorf("GITHUB_REPOSITORY is not set, but ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN are - unexpected environment")
}
roleName, roleArn, cleanup, err := createGitHubOIDCTrust(client, repo)
if err != nil {
return err
}
defer cleanup()
token, err := fetchGitHubIDToken(reqURL, reqToken, githubOIDCTestAudience)
if err != nil {
return err
}
const sessionName = "github-oidc-live"
assumeOut, err := assumeRoleWithWebIdentity(s, roleArn, sessionName, token, 0)
if err != nil {
// checkIAMApiErr-style wrapping isn't used here since a live
// AssumeRoleWithWebIdentity SDK error carries no token material
// of its own to guard against - it's the request we build
// (never printed) and GitHub's response (never printed either,
// see fetchGitHubIDToken) that could leak the token.
return fmt.Errorf("AssumeRoleWithWebIdentity: %w", err)
}
if assumeOut.Credentials == nil {
return fmt.Errorf("expected Credentials in AssumeRoleWithWebIdentity response")
}
accessKeyID := aws.ToString(assumeOut.Credentials.AccessKeyId)
secretAccessKey := aws.ToString(assumeOut.Credentials.SecretAccessKey)
sessionToken := aws.ToString(assumeOut.Credentials.SessionToken)
if accessKeyID == "" || secretAccessKey == "" || sessionToken == "" {
return fmt.Errorf("expected a full AccessKeyId/SecretAccessKey/SessionToken triple in AssumeRoleWithWebIdentity response")
}
wantArn := fmt.Sprintf("arn:aws:sts::000000000000:assumed-role/%s/%s", roleName, sessionName)
if aws.ToString(assumeOut.AssumedRoleUser.Arn) != wantArn {
return fmt.Errorf("expected AssumedRoleUser.Arn %q, instead got %q", wantArn, aws.ToString(assumeOut.AssumedRoleUser.Arn))
}
// A follow-up call authenticated with the session credentials
// AssumeRoleWithWebIdentity just issued proves the whole chain -
// discovery, JWKS, signature verification, claims mapping, and
// session creds - actually works, not just that a 200 came back.
callerOut, err := getCallerIdentityWithSessionCreds(*s, accessKeyID, secretAccessKey, sessionToken)
if err != nil {
return fmt.Errorf("GetCallerIdentity with assumed-role session credentials: %w", err)
}
if aws.ToString(callerOut.Arn) != wantArn {
return fmt.Errorf("GetCallerIdentity: expected Arn %q, instead got %q", wantArn, aws.ToString(callerOut.Arn))
}
return nil
})
}
// createGitHubOIDCTrust registers a throwaway OIDC provider for GitHub
// Actions' own issuer (ThumbprintList omitted, exercising
// CreateOpenIDConnectProvider's autofetch-and-CA-verify path against a real
// publicly reachable HTTPS endpoint instead of thumbprint pinning) and a
// throwaway role trusting it, returning the role's name, its ARN, and a
// cleanup func that removes both unconditionally.
//
// The trust policy's Condition requires both:
// - the effective audience to equal githubOIDCTestAudience (not GitHub's
// default audience - see that constant's doc comment), and
// - the sub claim to match "repo:<repo>:*".
//
// The sub match is a repo-wide wildcard rather than pinning an exact
// ref/event suffix: GitHub's sub claim differs by trigger and branch (e.g.
// "repo:o/r:pull_request" for a pull_request event vs.
// "repo:o/r:ref:refs/heads/main" for a push to main), and pinning one exact
// form would make this test fail depending on how it was triggered. That
// tradeoff only holds because this role is created and deleted within a
// single test run - the same repo-wide wildcard left in a real production
// trust policy would grant every workflow run in the repo, on any branch,
// the same trust, which is far too broad outside this throwaway context.
func createGitHubOIDCTrust(client *iam.Client, repo string) (roleName, roleArn string, cleanup func(), err error) {
out, err := createOIDCProvider(client, &iam.CreateOpenIDConnectProviderInput{
Url: aws.String(githubOIDCIssuerURL),
ClientIDList: []string{githubOIDCTestAudience},
})
if err != nil {
return "", "", nil, fmt.Errorf("create GitHub OIDC provider: %w", err)
}
providerArn := aws.ToString(out.OpenIDConnectProviderArn)
host := trimProviderScheme(githubOIDCIssuerURL)
roleName = "github-oidc-" + genRandString(12)
trust := fmt.Sprintf(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":%q},"Action":"sts:AssumeRoleWithWebIdentity",`+
`"Condition":{"StringEquals":{"%s:aud":%q},"StringLike":{"%s:sub":%q}}}]}`,
providerArn, host, githubOIDCTestAudience, host, "repo:"+repo+":*")
if _, err := createIAMRole(client, &iam.CreateRoleInput{RoleName: &roleName, AssumeRolePolicyDocument: &trust}); err != nil {
deleteOIDCProvider(client, providerArn)
return "", "", nil, fmt.Errorf("create GitHub OIDC trust role: %w", err)
}
roleArn = "arn:aws:iam::000000000000:role/" + roleName
cleanup = func() {
deleteIAMRole(client, roleName)
deleteOIDCProvider(client, providerArn)
}
return roleName, roleArn, cleanup, nil
}
// githubIDTokenResponse is the JSON body GitHub's runtime ID-token endpoint
// returns: {"value": "<jwt>", "count": <n>}. Only value is needed here.
type githubIDTokenResponse struct {
Value string `json:"value"`
}
// fetchGitHubIDToken fetches a real, signed OIDC ID token for audience from
// GitHub Actions' runtime token endpoint (requestURL/requestToken are
// ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN, only present
// inside a GitHub Actions job with id-token: write permission).
//
// The returned token is a real, unmasked bearer credential - unlike a
// secrets.* value, GitHub does not scrub it from logs automatically since it
// never appears in the workflow YAML. Every error path here is deliberately
// built from fixed strings and status codes only, never from the response
// body or the request's Authorization header, so a failure here can never
// leak the token into CI output.
func fetchGitHubIDToken(requestURL, requestToken, audience string) (string, error) {
parsed, err := url.Parse(requestURL)
if err != nil {
return "", fmt.Errorf("parse ACTIONS_ID_TOKEN_REQUEST_URL: invalid URL")
}
q := parsed.Query()
q.Set("audience", audience)
parsed.RawQuery = q.Encode()
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
if err != nil {
return "", fmt.Errorf("build GitHub OIDC token request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+requestToken)
req.Header.Set("Accept", "application/json; api-version=2.0")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("fetch GitHub OIDC token: request failed")
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", fmt.Errorf("read GitHub OIDC token response: failed after status %d", resp.StatusCode)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub OIDC token endpoint returned status %d", resp.StatusCode)
}
var out githubIDTokenResponse
if err := json.Unmarshal(body, &out); err != nil {
return "", fmt.Errorf("parse GitHub OIDC token response: malformed JSON")
}
if out.Value == "" {
return "", fmt.Errorf("GitHub OIDC token endpoint returned an empty token value")
}
return out.Value, nil
}
// getCallerIdentityWithSessionCreds calls GetCallerIdentity authenticated
// with a full access/secret/session-token triple.
func getCallerIdentityWithSessionCreds(cfg S3Conf, access, secret, token string) (*sts.GetCallerIdentityOutput, error) {
cfg.awsID = access
cfg.awsSecret = secret
stsCfg := cfg.Config()
stsCfg.Credentials = credentials.NewStaticCredentialsProvider(access, secret, token)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
defer cancel()
return sts.NewFromConfig(stsCfg).GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
}
+11 -4
View File
@@ -20,16 +20,18 @@ import (
)
var (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorCyan = "\033[36m"
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorCyan = "\033[36m"
colorYellow = "\033[33m"
)
var (
RunCount atomic.Uint32
PassCount atomic.Uint32
FailCount atomic.Uint32
SkipCount atomic.Uint32
)
func runF(format string, a ...any) {
@@ -46,3 +48,8 @@ func passF(format string, a ...any) {
PassCount.Add(1)
fmt.Printf(colorGreen+"PASS "+colorReset+format+"\n", a...)
}
func skipF(format string, a ...any) {
SkipCount.Add(1)
fmt.Printf(colorYellow+"SKIP "+colorReset+format+"\n", a...)
}