feat: supply the aws:PrincipalType condition key at the AWS gateway door (#1052)

Bump bluebottle for the new key. requestConditionKeys now maps this
door's internal principal-type spelling to the canonical AWS value
through the bluebottle constants, and omits the key for an
unrecognized type rather than supplying an empty string.
This commit is contained in:
Josh Siviour
2026-09-16 14:36:46 +10:00
committed by GitHub
parent 4eee02fcfd
commit 578185f533
5 changed files with 56 additions and 1 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/klauspost/cpuid/v2 v2.4.0
github.com/miekg/dns v1.1.73
github.com/minio/highwayhash v1.0.4
github.com/mulgadc/bluebottle v1.20.0
github.com/mulgadc/bluebottle v1.20.1-0.20260916040751-5a5ca6f0fb33
github.com/mulgadc/northstar v1.20.0
github.com/mulgadc/predastore v1.20.0
github.com/mulgadc/viperblock v1.20.0
+2
View File
@@ -1329,6 +1329,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mulgadc/bluebottle v1.20.0 h1:Udi+aOA14CgDLfhanw4Rsa5HqkVt+/9e6mxoQWTRCNs=
github.com/mulgadc/bluebottle v1.20.0/go.mod h1:4rjLL3B0xZBPmxacaXPkw6sPOZZgyuCDBFmPSOC81yE=
github.com/mulgadc/bluebottle v1.20.1-0.20260916040751-5a5ca6f0fb33 h1:Rw3mnRF2YrhVZgHpCsOi6La8cOu/V3xnhB000iWcdr8=
github.com/mulgadc/bluebottle v1.20.1-0.20260916040751-5a5ca6f0fb33/go.mod h1:4rjLL3B0xZBPmxacaXPkw6sPOZZgyuCDBFmPSOC81yE=
github.com/mulgadc/northstar v1.20.0 h1:nkWTuJv1aagrJUQiZHruo7xzu9RBtLHEzrQGABElwls=
github.com/mulgadc/northstar v1.20.0/go.mod h1:3GB49VUE9rA2idp88se5jAMv3ql1Oq9kIcT29WAZwDU=
github.com/mulgadc/predastore v1.20.0 h1:BBk02ZWWbCGqy0PkYEXh5Yk5lbOUuDEcMS++voxSc6E=
@@ -25,6 +25,7 @@ var gatewayDoorKeys = []string{
iampolicy.KeyUserID,
iampolicy.KeyPrincipalAccount,
iampolicy.KeySourceIP,
iampolicy.KeyPrincipalType,
}
// Every condition key the evaluator names, so the write-path gate below covers
@@ -36,6 +37,7 @@ var allConditionKeys = []string{
iampolicy.KeyUsername,
iampolicy.KeyPrincipalAccount,
iampolicy.KeyUserID,
iampolicy.KeyPrincipalType,
// Deliberately outside the allowlist: there is no MFA in the stack, so the
// key could never be true. It is here to prove the validator says no.
"aws:MultiFactorAuthPresent",
+28
View File
@@ -40,9 +40,37 @@ func TestRequestConditionKeys_PopulatesAvailableKeys(t *testing.T) {
iampolicy.KeyUsername: "alice",
iampolicy.KeyUserID: "AIDAALICE",
iampolicy.KeyPrincipalAccount: "000000000001",
iampolicy.KeyPrincipalType: iampolicy.PrincipalTypeUser,
}, keys)
}
// Every principal type this door recognizes maps to the canonical AWS spelling,
// and an unrecognized one omits the key rather than supplying an empty string.
func TestRequestConditionKeys_PrincipalType(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/", nil)
tests := []struct {
principalType string
want string
omitted bool
}{
{principalType: principalTypeUser, want: iampolicy.PrincipalTypeUser},
{principalType: principalTypeAssumedRole, want: iampolicy.PrincipalTypeAssumedRole},
{principalType: principalTypeRoot, want: iampolicy.PrincipalTypeAccount},
{principalType: "", omitted: true},
}
for _, tt := range tests {
keys := requestConditionKeys(r, principalContext{
identity: "alice", accountID: "000000000001", principalType: tt.principalType,
})
if tt.omitted {
assert.NotContains(t, keys, iampolicy.KeyPrincipalType, "principalType %q", tt.principalType)
continue
}
assert.Equal(t, tt.want, keys[iampolicy.KeyPrincipalType], "principalType %q", tt.principalType)
}
}
// A role session's aws:userid is the ID STS minted for it. Both halves come from
// the resolved role, so unlike aws:username it can carry a decision.
func TestRequestConditionKeys_UserIDForAssumedRole(t *testing.T) {
+23
View File
@@ -651,9 +651,32 @@ func requestConditionKeys(r *http.Request, principal principalContext) iampolicy
if ip := utils.ClientIP(r.RemoteAddr); ip != "" {
keys[iampolicy.KeySourceIP] = ip
}
// Resolved from the credential record, never from anything the caller
// supplies, so unlike aws:username it is safe for a role session too.
if v, ok := principalTypeCondition(principal.principalType); ok {
keys[iampolicy.KeyPrincipalType] = v
}
return keys
}
// principalTypeCondition maps this gateway's internal principal-type spelling
// to the canonical aws:PrincipalType value AWS documents, so a policy written
// against the documented value applies here and at predastore identically. An
// empty or unrecognized principal type reports false so the door omits the
// key rather than supplying an empty string.
func principalTypeCondition(principalType string) (string, bool) {
switch principalType {
case principalTypeUser:
return iampolicy.PrincipalTypeUser, true
case principalTypeAssumedRole:
return iampolicy.PrincipalTypeAssumedRole, true
case principalTypeRoot:
return iampolicy.PrincipalTypeAccount, true
default:
return "", false
}
}
// principalUserID resolves aws:userid: an IAM user's unique ID, the role ID and
// session name STS minted for a role session, or the account ID for root. Both
// halves of a session's ID come from the resolved role, so unlike aws:username