diff --git a/bearer/bearer.go b/bearer/bearer.go index aaea6c3..e419d54 100644 --- a/bearer/bearer.go +++ b/bearer/bearer.go @@ -6,7 +6,9 @@ import ( "fmt" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" + apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" + apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" @@ -33,9 +35,85 @@ type Token struct { sigSet bool sig refs.Signature + apeOverrideSet bool + apeOverride APEOverride + impersonate bool } +// APEOverride is the list of APE chains defined for a target. +// These chains are meant to serve as overrides to the already defined (or even undefined) +// APE chains for the target (see contract `Policy`). +// +// The server-side processing of the bearer token with set APE overrides must verify if a client is permitted +// to override chains for the target, preventing unauthorized access through the APE mechanism. +type APEOverride struct { + // Target for which chains are applied. + Target apeSDK.ChainTarget + + // The list of APE chains. + Chains []apeSDK.Chain +} + +// Marshal marshals APEOverride into a protobuf binary form. +func (c *APEOverride) Marshal() ([]byte, error) { + return c.ToV2().StableMarshal(nil), nil +} + +// Unmarshal unmarshals protobuf binary representation of APEOverride. +func (c *APEOverride) Unmarshal(data []byte) error { + overrideV2 := new(acl.APEOverride) + if err := overrideV2.Unmarshal(data); err != nil { + return err + } + + return c.FromV2(overrideV2) +} + +// MarshalJSON encodes APEOverride to protobuf JSON format. +func (c *APEOverride) MarshalJSON() ([]byte, error) { + return c.ToV2().MarshalJSON() +} + +// UnmarshalJSON decodes APEOverride from protobuf JSON format. +func (c *APEOverride) UnmarshalJSON(data []byte) error { + overrideV2 := new(acl.APEOverride) + if err := overrideV2.UnmarshalJSON(data); err != nil { + return err + } + + return c.FromV2(overrideV2) +} + +func (c *APEOverride) FromV2(tokenAPEChains *acl.APEOverride) error { + c.Target.FromV2(tokenAPEChains.GetTarget()) + if chains := tokenAPEChains.GetChains(); len(chains) > 0 { + c.Chains = make([]apeSDK.Chain, len(chains)) + for i := range chains { + if err := c.Chains[i].ReadFromV2(chains[i]); err != nil { + return fmt.Errorf("invalid APE chain: %w", err) + } + } + } + return nil +} + +func (c *APEOverride) ToV2() *acl.APEOverride { + if c == nil { + return nil + } + + apeOverride := new(acl.APEOverride) + apeOverride.SetTarget(c.Target.ToV2()) + chains := make([]*apeV2.Chain, len(c.Chains)) + for i := range c.Chains { + chains[i] = c.Chains[i].ToV2() + } + apeOverride.SetChains(chains) + + return apeOverride +} + // reads Token from the acl.BearerToken message. If checkFieldPresence is set, // returns an error on absence of any protocol-required field. func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error { @@ -48,10 +126,11 @@ func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error { b.impersonate = body.GetImpersonate() + apeOverrides := body.GetAPEOverride() eaclTable := body.GetEACL() if b.eaclTableSet = eaclTable != nil; b.eaclTableSet { b.eaclTable = *eacl.NewTableFromV2(eaclTable) - } else if checkFieldPresence && !b.impersonate { + } else if checkFieldPresence && !b.impersonate && apeOverrides == nil { return errors.New("missing eACL table") } @@ -72,6 +151,14 @@ func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error { return errors.New("missing token lifetime") } + if b.apeOverrideSet = apeOverrides != nil; b.apeOverrideSet { + if err = b.apeOverride.FromV2(apeOverrides); err != nil { + return err + } + } else if checkFieldPresence && !b.impersonate && !b.eaclTableSet { + return errors.New("missing APE override") + } + sig := m.GetSignature() if b.sigSet = sig != nil; sig != nil { b.sig = *sig @@ -90,7 +177,7 @@ func (b *Token) ReadFromV2(m acl.BearerToken) error { } func (b Token) fillBody() *acl.BearerTokenBody { - if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet && !b.impersonate { + if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet && !b.impersonate && !b.apeOverrideSet { return nil } @@ -116,6 +203,10 @@ func (b Token) fillBody() *acl.BearerTokenBody { body.SetLifetime(&lifetime) } + if b.apeOverrideSet { + body.SetAPEOverride(b.apeOverride.ToV2()) + } + body.SetImpersonate(b.impersonate) return &body @@ -214,6 +305,25 @@ func (b Token) EACLTable() eacl.Table { return eacl.Table{} } +// SetAPEOverride sets APE override to the bearer token. +// +// See also: APEOverride. +func (b *Token) SetAPEOverride(v APEOverride) { + b.apeOverride = v + b.apeOverrideSet = true +} + +// APEOverride returns APE override set by SetAPEOverride. +// +// Zero Token has zero APEOverride. +func (b *Token) APEOverride() APEOverride { + if b.apeOverrideSet { + return b.apeOverride + } + + return APEOverride{} +} + // SetImpersonate mark token as impersonate to consider token signer as request owner. // If this field is true extended EACLTable in token body isn't processed. func (b *Token) SetImpersonate(v bool) { diff --git a/bearer/bearer_test.go b/bearer/bearer_test.go index 3c0e946..341c4f2 100644 --- a/bearer/bearer_test.go +++ b/bearer/bearer_test.go @@ -3,6 +3,7 @@ package bearer_test import ( "bytes" "math/rand" + "reflect" "testing" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" @@ -81,6 +82,58 @@ func TestToken_SetEACLTable(t *testing.T) { require.True(t, isEqualEACLTables(eaclTable, val.EACLTable())) } +func TestToken_SetAPEOverrides(t *testing.T) { + var val bearer.Token + var m acl.BearerToken + filled := bearertest.Token() + + val.WriteToV2(&m) + require.Zero(t, m.GetBody()) + + val2 := filled + + require.NoError(t, val2.Unmarshal(val.Marshal())) + require.Zero(t, val2.APEOverride()) + + val2 = filled + + jd, err := val.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, val2.UnmarshalJSON(jd)) + require.Zero(t, val2.APEOverride()) + + // set value + + tApe := bearertest.APEOverride() + + val.SetAPEOverride(tApe) + require.Equal(t, tApe, val.APEOverride()) + + val.WriteToV2(&m) + require.NotNil(t, m.GetBody().GetAPEOverride()) + require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), m.GetBody().GetAPEOverride())) + + val2 = filled + + require.NoError(t, val2.Unmarshal(val.Marshal())) + apeOverride := val2.APEOverride() + require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), apeOverride.ToV2())) + + val2 = filled + + jd, err = val.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, val2.UnmarshalJSON(jd)) + apeOverride = val.APEOverride() + require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), apeOverride.ToV2())) +} + +func tokenAPEOverridesEqual(lhs, rhs *acl.APEOverride) bool { + return reflect.DeepEqual(lhs, rhs) +} + func TestToken_ForUser(t *testing.T) { var val bearer.Token var m acl.BearerToken diff --git a/bearer/test/generate.go b/bearer/test/generate.go index 4fce6a4..0ea3936 100644 --- a/bearer/test/generate.go +++ b/bearer/test/generate.go @@ -1,6 +1,7 @@ package bearertest import ( + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test" usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test" @@ -15,6 +16,17 @@ func Token() (t bearer.Token) { t.SetIat(1) t.ForUser(usertest.ID()) t.SetEACLTable(*eacltest.Table()) + t.SetAPEOverride(APEOverride()) return t } + +func APEOverride() bearer.APEOverride { + return bearer.APEOverride{ + Target: ape.ChainTarget{ + TargetType: ape.TargetTypeContainer, + Name: "F8JsMnChywiPvbDvpxMbjTjx5KhWHHp6gCDt8BhzL9kF", + }, + Chains: []ape.Chain{{Raw: []byte("{}")}}, + } +}