diff --git a/runtime/ast/attachment.go b/runtime/ast/attachment.go index 397e1fa181..b0cbe2b67b 100644 --- a/runtime/ast/attachment.go +++ b/runtime/ast/attachment.go @@ -29,13 +29,12 @@ import ( // AttachmentDeclaration type AttachmentDeclaration struct { - Access Access - Identifier Identifier - BaseType *NominalType - Conformances []*NominalType - RequiredEntitlements []*NominalType - Members *Members - DocString string + Access Access + Identifier Identifier + BaseType *NominalType + Conformances []*NominalType + Members *Members + DocString string Range } @@ -50,7 +49,6 @@ func NewAttachmentDeclaration( identifier Identifier, baseType *NominalType, conformances []*NominalType, - requiredEntitlements []*NominalType, members *Members, docString string, declarationRange Range, @@ -58,14 +56,13 @@ func NewAttachmentDeclaration( common.UseMemory(memoryGauge, common.AttachmentDeclarationMemoryUsage) return &AttachmentDeclaration{ - Access: access, - Identifier: identifier, - BaseType: baseType, - Conformances: conformances, - RequiredEntitlements: requiredEntitlements, - Members: members, - DocString: docString, - Range: declarationRange, + Access: access, + Identifier: identifier, + BaseType: baseType, + Conformances: conformances, + Members: members, + DocString: docString, + Range: declarationRange, } } @@ -113,15 +110,9 @@ func (d *AttachmentDeclaration) ConformanceList() []*NominalType { return d.Conformances } -func (d *AttachmentDeclaration) RequiredEntitlementsToAttach() []*NominalType { - return d.RequiredEntitlements -} - const attachmentStatementDoc = prettier.Text("attachment") const attachmentStatementForDoc = prettier.Text("for") const attachmentConformancesSeparatorDoc = prettier.Text(":") -const attachmentEntitlementDoc = prettier.Text("entitlement") -const attachmentRequireDoc = prettier.Text("require") var attachmentConformanceSeparatorDoc prettier.Doc = prettier.Concat{ prettier.Text(","), @@ -151,31 +142,7 @@ func (d *AttachmentDeclaration) Doc() prettier.Doc { ) var membersDoc prettier.Concat - if d.RequiredEntitlements != nil && len(d.RequiredEntitlements) > 0 { - membersDoc = append(membersDoc, membersStartDoc) - for _, entitlement := range d.RequiredEntitlements { - var entitlementRequiredDoc = prettier.Indent{ - Doc: prettier.Concat{ - attachmentRequireDoc, - prettier.Space, - attachmentEntitlementDoc, - prettier.Space, - entitlement.Doc(), - }, - } - membersDoc = append( - membersDoc, - prettier.HardLine{}, - entitlementRequiredDoc, - ) - } - if len(d.Members.declarations) > 0 { - membersDoc = append(membersDoc, prettier.HardLine{}, d.Members.docWithNoBraces()) - } - membersDoc = append(membersDoc, prettier.HardLine{}, membersEndDoc) - } else { - membersDoc = append(membersDoc, prettier.Line{}, d.Members.Doc()) - } + membersDoc = append(membersDoc, prettier.Line{}, d.Members.Doc()) if len(d.Conformances) > 0 { conformancesDoc := prettier.Concat{ @@ -244,10 +211,9 @@ func (d *AttachmentDeclaration) String() string { // AttachExpression type AttachExpression struct { - Base Expression - Attachment *InvocationExpression - Entitlements []*NominalType - StartPos Position `json:"-"` + Base Expression + Attachment *InvocationExpression + StartPos Position `json:"-"` } var _ Element = &AttachExpression{} @@ -270,16 +236,14 @@ func NewAttachExpression( gauge common.MemoryGauge, base Expression, attachment *InvocationExpression, - entitlements []*NominalType, startPos Position, ) *AttachExpression { common.UseMemory(gauge, common.AttachExpressionMemoryUsage) return &AttachExpression{ - Base: base, - Attachment: attachment, - Entitlements: entitlements, - StartPos: startPos, + Base: base, + Attachment: attachment, + StartPos: startPos, } } @@ -289,14 +253,9 @@ func (e *AttachExpression) String() string { const attachExpressionDoc = prettier.Text("attach") const attachExpressionToDoc = prettier.Text("to") -const attachExpressionWithDoc = prettier.Text("with") -const attachExpressionCommaDoc = prettier.Text(",") func (e *AttachExpression) Doc() prettier.Doc { - var doc prettier.Concat - - doc = append( - doc, + return prettier.Concat{ attachExpressionDoc, prettier.Space, e.Attachment.Doc(), @@ -304,19 +263,7 @@ func (e *AttachExpression) Doc() prettier.Doc { attachExpressionToDoc, prettier.Space, e.Base.Doc(), - ) - if len(e.Entitlements) > 0 { - entitlementsLen := len(e.Entitlements) - doc = append(doc, prettier.Space, attachExpressionWithDoc, prettier.Space, openParenthesisDoc) - for i, entitlement := range e.Entitlements { - doc = append(doc, entitlement.Doc()) - if i < entitlementsLen-1 { - doc = append(doc, attachExpressionCommaDoc, prettier.Space) - } - } - doc = append(doc, closeParenthesisDoc) } - return doc } func (e *AttachExpression) StartPosition() Position { @@ -324,9 +271,6 @@ func (e *AttachExpression) StartPosition() Position { } func (e *AttachExpression) EndPosition(memoryGauge common.MemoryGauge) Position { - if len(e.Entitlements) > 0 { - return e.Entitlements[len(e.Entitlements)-1].EndPosition(memoryGauge) - } return e.Base.EndPosition(memoryGauge) } diff --git a/runtime/ast/attachment_test.go b/runtime/ast/attachment_test.go index 65d6faf50b..35d27e119a 100644 --- a/runtime/ast/attachment_test.go +++ b/runtime/ast/attachment_test.go @@ -56,22 +56,6 @@ func TestAttachmentDeclaration_MarshallJSON(t *testing.T) { ), }, }, - RequiredEntitlements: []*NominalType{ - { - Identifier: NewIdentifier( - nil, - "X", - Position{Offset: 1, Line: 2, Column: 3}, - ), - }, - { - Identifier: NewIdentifier( - nil, - "Y", - Position{Offset: 1, Line: 2, Column: 3}, - ), - }, - }, Members: NewMembers(nil, []Declaration{}), DocString: "test", Range: Range{ @@ -118,28 +102,6 @@ func TestAttachmentDeclaration_MarshallJSON(t *testing.T) { "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, "EndPos": {"Offset": 3, "Line": 2, "Column": 5} } - ], - "RequiredEntitlements": [ - { - "Type": "NominalType", - "Identifier": { - "Identifier": "X", - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - { - "Type": "NominalType", - "Identifier": { - "Identifier": "Y", - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - } ], "Members": { "Declarations": [] @@ -179,22 +141,6 @@ func TestAttachmentDeclaration_Doc(t *testing.T) { ), }, }, - RequiredEntitlements: []*NominalType{ - { - Identifier: NewIdentifier( - nil, - "X", - Position{Offset: 1, Line: 2, Column: 3}, - ), - }, - { - Identifier: NewIdentifier( - nil, - "Y", - Position{Offset: 1, Line: 2, Column: 3}, - ), - }, - }, Members: NewMembers(nil, []Declaration{}), DocString: "test", Range: Range{ @@ -225,29 +171,8 @@ func TestAttachmentDeclaration_Doc(t *testing.T) { Doc: prettier.Concat{ prettier.Line{}, prettier.Concat{ - prettier.Text("{"), - prettier.HardLine{}, - prettier.Indent{ - Doc: prettier.Concat{ - prettier.Text("require"), - prettier.Text(" "), - prettier.Text("entitlement"), - prettier.Text(" "), - prettier.Text("X"), - }, - }, - prettier.HardLine{}, - prettier.Indent{ - Doc: prettier.Concat{ - prettier.Text("require"), - prettier.Text(" "), - prettier.Text("entitlement"), - prettier.Text(" "), - prettier.Text("Y"), - }, - }, - prettier.HardLine{}, - prettier.Text("}"), + prettier.Line{}, + prettier.Text("{}"), }, }, }, @@ -260,10 +185,7 @@ func TestAttachmentDeclaration_Doc(t *testing.T) { require.Equal(t, `access(all) -attachment Foo for Bar: Baz { -require entitlement X -require entitlement Y -}`, +attachment Foo for Bar: Baz {}`, decl.String(), ) } @@ -296,24 +218,6 @@ func TestAttachExpressionMarshallJSON(t *testing.T) { Position{Offset: 1, Line: 2, Column: 3}, Position{Offset: 1, Line: 2, Column: 3}, ), - Entitlements: []*NominalType{ - NewNominalType(nil, - NewIdentifier( - nil, - "X", - Position{Offset: 1, Line: 2, Column: 3}, - ), - []Identifier{}, - ), - NewNominalType(nil, - NewIdentifier( - nil, - "Y", - Position{Offset: 1, Line: 2, Column: 3}, - ), - []Identifier{}, - ), - }, StartPos: Position{Offset: 1, Line: 2, Column: 3}, } @@ -326,7 +230,7 @@ func TestAttachExpressionMarshallJSON(t *testing.T) { { "Type": "AttachExpression", "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3}, + "EndPos": {"Offset": 3, "Line": 2, "Column": 5}, "Base": { "Type": "IdentifierExpression", "Identifier": { @@ -354,29 +258,7 @@ func TestAttachExpressionMarshallJSON(t *testing.T) { "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, "ArgumentsStartPos": {"Offset": 1, "Line": 2, "Column": 3}, "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - "Entitlements": [ - { - "Type": "NominalType", - "Identifier": { - "Identifier": "X", - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - { - "Type": "NominalType", - "Identifier": { - "Identifier": "Y", - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - }, - "StartPos": {"Offset": 1, "Line": 2, "Column": 3}, - "EndPos": {"Offset": 1, "Line": 2, "Column": 3} - } - ] + } } `, string(actual), @@ -411,24 +293,6 @@ func TestAttachExpression_Doc(t *testing.T) { Position{Offset: 1, Line: 2, Column: 3}, Position{Offset: 1, Line: 2, Column: 3}, ), - Entitlements: []*NominalType{ - NewNominalType(nil, - NewIdentifier( - nil, - "X", - Position{Offset: 1, Line: 2, Column: 3}, - ), - []Identifier{}, - ), - NewNominalType(nil, - NewIdentifier( - nil, - "Y", - Position{Offset: 1, Line: 2, Column: 3}, - ), - []Identifier{}, - ), - }, StartPos: Position{Offset: 1, Line: 2, Column: 3}, } @@ -445,20 +309,11 @@ func TestAttachExpression_Doc(t *testing.T) { prettier.Text("to"), prettier.Text(" "), prettier.Text("foo"), - prettier.Text(" "), - prettier.Text("with"), - prettier.Text(" "), - prettier.Text("("), - prettier.Text("X"), - prettier.Text(","), - prettier.Text(" "), - prettier.Text("Y"), - prettier.Text(")"), }, decl.Doc(), ) - require.Equal(t, "attach bar() to foo with (X, Y)", decl.String()) + require.Equal(t, "attach bar() to foo", decl.String()) } func TestRemoveStatement_MarshallJSON(t *testing.T) { diff --git a/runtime/contract_update_validation_test.go b/runtime/contract_update_validation_test.go index 0617b62b6e..dcf8c5259a 100644 --- a/runtime/contract_update_validation_test.go +++ b/runtime/contract_update_validation_test.go @@ -1869,17 +1869,6 @@ func assertConformanceMismatchError( assert.Equal(t, erroneousDeclName, conformanceMismatchError.DeclName) } -func assertEntitlementRequirementMismatchError( - t *testing.T, - err error, - erroneousDeclName string, -) { - var entitlementMismatchError *stdlib.RequiredEntitlementMismatchError - require.ErrorAs(t, err, &entitlementMismatchError) - - assert.Equal(t, erroneousDeclName, entitlementMismatchError.DeclName) -} - func assertEnumCaseMismatchError(t *testing.T, err error, expectedEnumCase string, foundEnumCase string) { var enumMismatchError *stdlib.EnumCaseMismatchError require.ErrorAs(t, err, &enumMismatchError) @@ -2108,130 +2097,6 @@ func TestRuntimeContractUpdateConformanceChanges(t *testing.T) { require.NoError(t, err) }) - t.Run("removing required entitlement", func(t *testing.T) { - - t.Parallel() - - const oldCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement X - require entitlement Y - } - } - ` - - const newCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement X - } - } - ` - - err := testDeployAndUpdate(t, "Test", oldCode, newCode) - require.NoError(t, err) - }) - - t.Run("reordering required entitlement", func(t *testing.T) { - - t.Parallel() - - const oldCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement X - require entitlement Y - } - } - ` - - const newCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement Y - require entitlement X - } - } - ` - - err := testDeployAndUpdate(t, "Test", oldCode, newCode) - require.NoError(t, err) - }) - - t.Run("renaming required entitlement", func(t *testing.T) { - - t.Parallel() - - const oldCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement Y - } - } - ` - - const newCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement X - } - } - ` - - err := testDeployAndUpdate(t, "Test", oldCode, newCode) - RequireError(t, err) - - cause := getSingleContractUpdateErrorCause(t, err, "Test") - - assertEntitlementRequirementMismatchError(t, cause, "Foo") - }) - - t.Run("adding required entitlement", func(t *testing.T) { - - t.Parallel() - - const oldCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement X - } - } - ` - - const newCode = ` - access(all) contract Test { - access(all) entitlement X - access(all) entitlement Y - access(all) attachment Foo for AnyStruct { - require entitlement X - require entitlement Y - } - } - ` - - err := testDeployAndUpdate(t, "Test", oldCode, newCode) - RequireError(t, err) - - cause := getSingleContractUpdateErrorCause(t, err, "Test") - - assertEntitlementRequirementMismatchError(t, cause, "Foo") - }) - t.Run("missing comma in parameter list of old contract", func(t *testing.T) { t.Parallel() diff --git a/runtime/entitlements_test.go b/runtime/entitlements_test.go index 048d179412..bafe338e25 100644 --- a/runtime/entitlements_test.go +++ b/runtime/entitlements_test.go @@ -217,7 +217,7 @@ func TestRuntimeAccountEntitlementSaveAndLoadFail(t *testing.T) { require.ErrorAs(t, err, &interpreter.ForceCastTypeMismatchError{}) } -func TestRuntimeAccountEntitlementAttachmentMap(t *testing.T) { +func TestRuntimeAccountEntitlementAttachment(t *testing.T) { t.Parallel() storage := NewTestLedger(nil, nil) @@ -226,16 +226,13 @@ func TestRuntimeAccountEntitlementAttachmentMap(t *testing.T) { deployTx := DeploymentTransaction("Test", []byte(` access(all) contract Test { - access(all) entitlement X access(all) entitlement Y - access(all) entitlement mapping M { - X -> Y - } - - access(all) resource R {} + access(all) resource R { + access(Y) fun foo() {} + } - access(mapping M) attachment A for R { + access(all) attachment A for R { access(Y) fun foo() {} } @@ -252,7 +249,7 @@ func TestRuntimeAccountEntitlementAttachmentMap(t *testing.T) { prepare(signer: auth(Storage, Capabilities) &Account) { let r <- Test.createRWithA() signer.storage.save(<-r, to: /storage/foo) - let cap = signer.capabilities.storage.issue(/storage/foo) + let cap = signer.capabilities.storage.issue(/storage/foo) signer.capabilities.publish(cap, at: /public/foo) } } @@ -263,7 +260,7 @@ func TestRuntimeAccountEntitlementAttachmentMap(t *testing.T) { transaction { prepare(signer: &Account) { - let ref = signer.capabilities.borrow(/public/foo)! + let ref = signer.capabilities.borrow(/public/foo)! ref[Test.A]!.foo() } } diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index d7ad5afe68..1b208f31f3 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -42,8 +42,6 @@ import ( "github.com/onflow/cadence/runtime/sema" ) -// - type getterSetter struct { target Value // allowMissing may be true when the got value is nil. @@ -1003,18 +1001,25 @@ func (interpreter *Interpreter) evaluateDefaultDestroyEvent( var self MemberAccessibleValue = containingResourceComposite if containingResourceComposite.Kind == common.CompositeKindAttachment { var base *EphemeralReferenceValue + // in evaluation of destroy events, base and self are fully entitled, as the value must be owned + entitlementSupportingType, ok := interpreter.MustSemaTypeOfValue(containingResourceComposite).(sema.EntitlementSupportingType) + if !ok { + panic(errors.NewUnreachableError()) + } + supportedEntitlements := entitlementSupportingType.SupportedEntitlements() + access := sema.NewAccessFromEntitlementSet(supportedEntitlements, sema.Conjunction) locationRange := LocationRange{ Location: interpreter.Location, HasPosition: eventDecl, } - base, self = attachmentBaseAndSelfValues(declarationInterpreter, containingResourceComposite, locationRange) + base, self = attachmentBaseAndSelfValues(declarationInterpreter, access, containingResourceComposite, locationRange) declarationInterpreter.declareVariable(sema.BaseIdentifier, base) } declarationInterpreter.declareVariable(sema.SelfIdentifier, self) for _, parameter := range parameters { // "lazily" evaluate the default argument expressions. - // This "lazy" with respect to the event's declaration: + // This is "lazy" with respect to the event's declaration: // if we declare a default event `ResourceDestroyed(foo: Int = self.x)`, // `self.x` is evaluated in the context that exists when the event is destroyed, // not the context when it is declared. This function is only called after the destroy @@ -1352,19 +1357,27 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( var self MemberAccessibleValue = value if declaration.Kind() == common.CompositeKindAttachment { - var auth Authorization = UnauthorizedAccess attachmentType := interpreter.MustSemaTypeOfValue(value).(*sema.CompositeType) - // Self's type in the constructor is codomain of the attachment's entitlement map, since + // Self's type in the constructor is fully entitled, since // the constructor can only be called when in possession of the base resource - // if the attachment is declared with access(all) access, then self is unauthorized - if attachmentType.AttachmentEntitlementAccess != nil { - auth = ConvertSemaAccessToStaticAuthorization(interpreter, attachmentType.AttachmentEntitlementAccess.Codomain()) - } + + auth := ConvertSemaAccessToStaticAuthorization( + interpreter, + sema.NewAccessFromEntitlementSet(attachmentType.SupportedEntitlements(), sema.Conjunction), + ) + self = NewEphemeralReferenceValue(interpreter, auth, value, attachmentType, locationRange) // set the base to the implicitly provided value, and remove this implicit argument from the list implicitArgumentPos := len(invocation.Arguments) - 1 invocation.Base = invocation.Arguments[implicitArgumentPos].(*EphemeralReferenceValue) + + var ok bool + value.base, ok = invocation.Base.Value.(*CompositeValue) + if !ok { + panic(errors.NewUnreachableError()) + } + invocation.Arguments[implicitArgumentPos] = nil invocation.Arguments = invocation.Arguments[:implicitArgumentPos] invocation.ArgumentTypes[implicitArgumentPos] = nil @@ -4780,25 +4793,25 @@ func (interpreter *Interpreter) ReportComputation(compKind common.ComputationKin } } -func (interpreter *Interpreter) getAccessOfMember(self Value, identifier string) *sema.Access { +func (interpreter *Interpreter) getAccessOfMember(self Value, identifier string) sema.Access { typ, err := interpreter.ConvertStaticToSemaType(self.StaticType(interpreter)) // some values (like transactions) do not have types that can be looked up this way. These types // do not support entitled members, so their access is always unauthorized if err != nil { - return &sema.UnauthorizedAccess + return sema.UnauthorizedAccess } member, hasMember := typ.GetMembers()[identifier] // certain values (like functions) have builtin members that are not present on the type // in such cases the access is always unauthorized if !hasMember { - return &sema.UnauthorizedAccess + return sema.UnauthorizedAccess } - return &member.Resolve(interpreter, identifier, ast.EmptyRange, func(err error) {}).Access + return member.Resolve(interpreter, identifier, ast.EmptyRange, func(err error) {}).Access } func (interpreter *Interpreter) mapMemberValueAuthorization( self Value, - memberAccess *sema.Access, + memberAccess sema.Access, resultValue Value, resultingType sema.Type, locationRange LocationRange, @@ -4808,7 +4821,7 @@ func (interpreter *Interpreter) mapMemberValueAuthorization( return resultValue } - if mappedAccess, isMappedAccess := (*memberAccess).(*sema.EntitlementMapAccess); isMappedAccess { + if mappedAccess, isMappedAccess := (memberAccess).(*sema.EntitlementMapAccess); isMappedAccess { var auth Authorization switch selfValue := self.(type) { case AuthorizedValue: diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index 87d6978718..0c986bfefa 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -1184,8 +1184,17 @@ func (interpreter *Interpreter) VisitCastingExpression(expression *ast.CastingEx switch expression.Operation { case ast.OperationFailableCast, ast.OperationForceCast: - valueStaticType := value.StaticType(interpreter) - valueSemaType := interpreter.MustConvertStaticToSemaType(valueStaticType) + // if the value itself has a mapped entitlement type in its authorization + // (e.g. if it is a reference to `self` or `base` in an attachment function with mapped access) + // substitution must also be performed on its entitlements + // + // we do this here (as opposed to in `IsSubTypeOfSemaType`) because casting is the only way that + // an entitlement can "traverse the boundary", so to speak, between runtime and static types, and + // thus this is the only place where it becomes necessary to "instantiate" the result of a map to its + // concrete outputs. In other places (e.g. interface conformance checks) we want to leave maps generic, + // so we don't substitute them. + valueSemaType := interpreter.substituteMappedEntitlements(interpreter.MustSemaTypeOfValue(value)) + valueStaticType := ConvertSemaToStaticType(interpreter, valueSemaType) isSubType := interpreter.IsSubTypeOfSemaType(valueStaticType, expectedType) switch expression.Operation { @@ -1417,16 +1426,13 @@ func (interpreter *Interpreter) VisitAttachExpression(attachExpression *ast.Atta // set it on the attachment's `CompositeValue` yet, because the value does not exist. // Instead, we create an implicit constructor argument containing a reference to the base. - var auth Authorization = UnauthorizedAccess - attachmentType := interpreter.Program.Elaboration.AttachTypes(attachExpression) + // within the constructor, the attachment's base and self references should be fully entitled, + // as the constructor of the attachment is only callable by the owner of the base + baseType := interpreter.MustSemaTypeOfValue(base).(sema.EntitlementSupportingType) + baseAccess := sema.NewAccessFromEntitlementSet(baseType.SupportedEntitlements(), sema.Conjunction) + auth := ConvertSemaAccessToStaticAuthorization(interpreter, baseAccess) - if attachmentType.RequiredEntitlements.Len() > 0 { - baseAccess := sema.EntitlementSetAccess{ - SetKind: sema.Conjunction, - Entitlements: attachmentType.RequiredEntitlements, - } - auth = ConvertSemaAccessToStaticAuthorization(interpreter, baseAccess) - } + attachmentType := interpreter.Program.Elaboration.AttachTypes(attachExpression) var baseValue Value = NewEphemeralReferenceValue( interpreter, @@ -1454,6 +1460,8 @@ func (interpreter *Interpreter) VisitAttachExpression(attachExpression *ast.Atta nil, ).(*CompositeValue) + attachment.setBaseValue(interpreter, base) + // we enforce this in the checker if !ok { panic(errors.NewUnreachableError()) diff --git a/runtime/interpreter/interpreter_statement.go b/runtime/interpreter/interpreter_statement.go index 9da8956e82..00cfd6eac2 100644 --- a/runtime/interpreter/interpreter_statement.go +++ b/runtime/interpreter/interpreter_statement.go @@ -446,7 +446,7 @@ func (interpreter *Interpreter) VisitRemoveStatement(removeStatement *ast.Remove if attachment.IsResourceKinded(interpreter) { // this attachment is no longer attached to its base, but the `base` variable is still available in the destructor - attachment.setBaseValue(interpreter, base, locationRange) + attachment.setBaseValue(interpreter, base) attachment.Destroy(interpreter, locationRange) } diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index f37dea78fa..664386b822 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16323,7 +16323,7 @@ type CompositeValue struct { // 2) When a resource `r`'s destructor is invoked, all of `r`'s attachments' destructors will also run, and // have their `base` fields set to `&r` // 3) When a value is transferred, this field is copied between its attachments - base *EphemeralReferenceValue + base *CompositeValue QualifiedIdentifier string Kind common.CompositeKind isDestroyed bool @@ -16633,7 +16633,7 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio // destroy every nested resource in this composite; note that this iteration includes attachments v.ForEachField(interpreter, func(_ string, fieldValue Value) bool { if compositeFieldValue, ok := fieldValue.(*CompositeValue); ok && compositeFieldValue.Kind == common.CompositeKindAttachment { - compositeFieldValue.setBaseValue(interpreter, v, locationRange) + compositeFieldValue.setBaseValue(interpreter, v) } maybeDestroy(interpreter, locationRange, fieldValue) return true @@ -16802,7 +16802,27 @@ func (v *CompositeValue) GetFunction(interpreter *Interpreter, locationRange Loc var base *EphemeralReferenceValue var self MemberAccessibleValue = v if v.Kind == common.CompositeKindAttachment { - base, self = attachmentBaseAndSelfValues(interpreter, v, locationRange) + functionAccess := interpreter.getAccessOfMember(v, name) + + // with respect to entitlements, any access inside an attachment that is not an entitlement access + // does not provide any entitlements to base and self + // E.g. consider: + // + // access(E) fun foo() {} + // access(self) fun bar() { + // self.foo() + // } + // access(all) fun baz() { + // self.bar() + // } + // + // clearly `bar` should be callable within `baz`, but we cannot allow `foo` + // to be callable within `bar`, or it will be possible to access `E` entitled + // methods on `base` + if functionAccess.IsPrimitiveAccess() { + functionAccess = sema.UnauthorizedAccess + } + base, self = attachmentBaseAndSelfValues(interpreter, functionAccess, v, locationRange) } return NewBoundFunctionValue(interpreter, function, &self, base, nil) } @@ -17178,7 +17198,7 @@ func (v *CompositeValue) ConformsToStaticType( } if compositeType.Kind == common.CompositeKindAttachment { - base := v.getBaseValue().Value + base := v.getBaseValue(interpreter, UnauthorizedAccess, locationRange).Value if base == nil || !base.ConformsToStaticType(interpreter, locationRange, results) { return false } @@ -17381,7 +17401,7 @@ func (v *CompositeValue) Transfer( if compositeValue, ok := value.(*CompositeValue); ok && compositeValue.Kind == common.CompositeKindAttachment { - compositeValue.setBaseValue(interpreter, v, locationRange) + compositeValue.setBaseValue(interpreter, v) } value = value.Transfer( @@ -17687,11 +17707,11 @@ func NewEnumCaseValue( return v } -func (v *CompositeValue) getBaseValue() *EphemeralReferenceValue { - return v.base -} - -func (v *CompositeValue) setBaseValue(interpreter *Interpreter, base *CompositeValue, locationRange LocationRange) { +func (v *CompositeValue) getBaseValue( + interpreter *Interpreter, + functionAuthorization Authorization, + locationRange LocationRange, +) *EphemeralReferenceValue { attachmentType, ok := interpreter.MustSemaTypeOfValue(v).(*sema.CompositeType) if !ok { panic(errors.NewUnreachableError()) @@ -17705,8 +17725,11 @@ func (v *CompositeValue) setBaseValue(interpreter *Interpreter, base *CompositeV baseType = ty } - authorization := attachmentBaseAuthorization(interpreter, v) - v.base = NewEphemeralReferenceValue(interpreter, authorization, base, baseType, locationRange) + return NewEphemeralReferenceValue(interpreter, functionAuthorization, v.base, baseType, locationRange) +} + +func (v *CompositeValue) setBaseValue(interpreter *Interpreter, base *CompositeValue) { + v.base = base } func attachmentMemberName(ty sema.Type) string { @@ -17776,53 +17799,15 @@ func (v *CompositeValue) forEachAttachmentFunction(interpreter *Interpreter, loc ) } -func attachmentReferenceAuthorization( - interpreter *Interpreter, - attachmentType *sema.CompositeType, - baseAccess sema.Access, -) (Authorization, error) { - // Map the entitlements of the accessing reference through the attachment's entitlement map to get the authorization of this reference - var attachmentReferenceAuth Authorization = UnauthorizedAccess - if attachmentType.AttachmentEntitlementAccess == nil { - return attachmentReferenceAuth, nil - } - attachmentReferenceAccess, err := attachmentType.AttachmentEntitlementAccess.Image(baseAccess, func() ast.Range { return ast.EmptyRange }) - if err != nil { - return nil, err - } - return ConvertSemaAccessToStaticAuthorization(interpreter, attachmentReferenceAccess), nil -} - -func attachmentBaseAuthorization( - interpreter *Interpreter, - attachment *CompositeValue, -) Authorization { - var auth Authorization = UnauthorizedAccess - attachmentType := interpreter.MustSemaTypeOfValue(attachment).(*sema.CompositeType) - if attachmentType.RequiredEntitlements.Len() > 0 { - baseAccess := sema.EntitlementSetAccess{ - SetKind: sema.Conjunction, - Entitlements: attachmentType.RequiredEntitlements, - } - auth = ConvertSemaAccessToStaticAuthorization(interpreter, baseAccess) - } - return auth -} - func attachmentBaseAndSelfValues( interpreter *Interpreter, + fnAccess sema.Access, v *CompositeValue, locationRange LocationRange, ) (base *EphemeralReferenceValue, self *EphemeralReferenceValue) { - base = v.getBaseValue() - - attachmentType := interpreter.MustSemaTypeOfValue(v).(*sema.CompositeType) - - var attachmentReferenceAuth Authorization = UnauthorizedAccess - if attachmentType.AttachmentEntitlementAccess != nil { - attachmentReferenceAuth = ConvertSemaAccessToStaticAuthorization(interpreter, attachmentType.AttachmentEntitlementAccess.Codomain()) - } + attachmentReferenceAuth := ConvertSemaAccessToStaticAuthorization(interpreter, fnAccess) + base = v.getBaseValue(interpreter, attachmentReferenceAuth, locationRange) // in attachment functions, self is a reference value self = NewEphemeralReferenceValue(interpreter, attachmentReferenceAuth, v, interpreter.MustSemaTypeOfValue(v), locationRange) @@ -17858,7 +17843,7 @@ func (v *CompositeValue) forEachAttachment(interpreter *Interpreter, locationRan // attachments is added that takes a `fun (&Attachment): Void` callback, the `f` provided here // should convert the provided attachment value into a reference before passing it to the user // callback - attachment.setBaseValue(interpreter, v, locationRange) + attachment.setBaseValue(interpreter, v) f(attachment) } } @@ -17875,16 +17860,17 @@ func (v *CompositeValue) getTypeKey( return Nil } attachmentType := keyType.(*sema.CompositeType) - // dynamically set the attachment's base to this composite, but with authorization based on the requested access on that attachment - attachment.setBaseValue(interpreter, v, locationRange) - - // Map the entitlements of the accessing reference through the attachment's entitlement map to get the authorization of this reference - attachmentReferenceAuth, err := attachmentReferenceAuthorization(interpreter, attachmentType, baseAccess) - if err != nil { - return Nil - } + // dynamically set the attachment's base to this composite + attachment.setBaseValue(interpreter, v) - attachmentRef := NewEphemeralReferenceValue(interpreter, attachmentReferenceAuth, attachment, attachmentType, locationRange) + // The attachment reference has the same entitlements as the base access + attachmentRef := NewEphemeralReferenceValue( + interpreter, + ConvertSemaAccessToStaticAuthorization(interpreter, baseAccess), + attachment, + attachmentType, + locationRange, + ) return NewSomeValueNonCopying(interpreter, attachmentRef) } @@ -17896,8 +17882,8 @@ func (v *CompositeValue) GetTypeKey( ) Value { var access sema.Access = sema.UnauthorizedAccess attachmentTyp, isAttachmentType := ty.(*sema.CompositeType) - if isAttachmentType && attachmentTyp.AttachmentEntitlementAccess != nil { - access = attachmentTyp.AttachmentEntitlementAccess.Domain() + if isAttachmentType { + access = sema.NewAccessFromEntitlementSet(attachmentTyp.SupportedEntitlements(), sema.Conjunction) } return v.getTypeKey(interpreter, locationRange, ty, access) } diff --git a/runtime/parser/declaration.go b/runtime/parser/declaration.go index 5a3353f397..c3de31689c 100644 --- a/runtime/parser/declaration.go +++ b/runtime/parser/declaration.go @@ -1344,47 +1344,6 @@ func parseCompositeOrInterfaceDeclaration( } } -func parseRequiredEntitlement(p *parser) (*ast.NominalType, error) { - if !p.isToken(p.current, lexer.TokenIdentifier, KeywordRequire) { - return nil, p.syntaxError( - "expected 'require', got %s", - p.current.Type, - ) - } - - // skip the `require` keyword - p.nextSemanticToken() - - if !p.isToken(p.current, lexer.TokenIdentifier, KeywordEntitlement) { - return nil, p.syntaxError( - "expected 'entitlement', got %s", - p.current.Type, - ) - } - - // skip the `entitlement` keyword - p.nextSemanticToken() - - return rejectAccessKeywords(p, func() (*ast.NominalType, error) { - return parseNominalType(p, lowestBindingPower) - }) -} - -func parseRequiredEntitlements(p *parser) ([]*ast.NominalType, error) { - var requiredEntitlements []*ast.NominalType - - for p.isToken(p.current, lexer.TokenIdentifier, KeywordRequire) { - requiredEntitlement, err := parseRequiredEntitlement(p) - if err != nil { - return nil, err - } - requiredEntitlements = append(requiredEntitlements, requiredEntitlement) - p.skipSpaceAndComments() - } - - return requiredEntitlements, nil -} - func parseAttachmentDeclaration( p *parser, access ast.Access, @@ -1450,13 +1409,6 @@ func parseAttachmentDeclaration( p.skipSpaceAndComments() - requiredEntitlements, err := parseRequiredEntitlements(p) - if err != nil { - return nil, err - } - - p.skipSpaceAndComments() - members, err := parseMembersAndNestedDeclarations(p, lexer.TokenBraceClose) if err != nil { return nil, err @@ -1481,7 +1433,6 @@ func parseAttachmentDeclaration( identifier, baseNominalType, conformances, - requiredEntitlements, members, docString, declarationRange, diff --git a/runtime/parser/declaration_test.go b/runtime/parser/declaration_test.go index 7f9795c4af..27da105bc2 100644 --- a/runtime/parser/declaration_test.go +++ b/runtime/parser/declaration_test.go @@ -4174,120 +4174,17 @@ func TestParseAttachmentDeclaration(t *testing.T) { ) }) - t.Run("required entitlements", func(t *testing.T) { - - t.Parallel() - - result, errs := testParseDeclarations(`access(all) attachment E for S { - require entitlement X - require entitlement Y - }`) - require.Empty(t, errs) - - utils.AssertEqualWithDiff(t, - []ast.Declaration{ - &ast.AttachmentDeclaration{ - Access: ast.AccessAll, - Identifier: ast.Identifier{ - Identifier: "E", - Pos: ast.Position{ - Offset: 23, - Line: 1, - Column: 23, - }, - }, - BaseType: &ast.NominalType{ - Identifier: ast.Identifier{ - Identifier: "S", - Pos: ast.Position{ - Offset: 29, - Line: 1, - Column: 29, - }, - }, - }, - RequiredEntitlements: []*ast.NominalType{ - { - Identifier: ast.Identifier{ - Identifier: "X", - Pos: ast.Position{ - Offset: 56, - Line: 2, - Column: 23, - }, - }, - }, - { - Identifier: ast.Identifier{ - Identifier: "Y", - Pos: ast.Position{ - Offset: 81, - Line: 3, - Column: 23, - }, - }, - }, - }, - Members: ast.NewUnmeteredMembers(nil), - Range: ast.Range{ - StartPos: ast.Position{ - Offset: 0, - Line: 1, - Column: 0, - }, - EndPos: ast.Position{ - Offset: 85, - Line: 4, - Column: 2, - }, - }, - }, - }, - result, - ) - }) - - t.Run("required entitlements error no identifier", func(t *testing.T) { + t.Run("required entitlements error", func(t *testing.T) { t.Parallel() _, errs := testParseDeclarations(`access(all) attachment E for S { - require entitlement - }`) - utils.AssertEqualWithDiff(t, []error{ - &SyntaxError{ - Pos: ast.Position{Line: 3, Column: 3, Offset: 60}, - Message: "unexpected token in type: '}'", - }, - }, errs) - }) - - t.Run("required entitlements error no entitlement", func(t *testing.T) { - - t.Parallel() - - _, errs := testParseDeclarations(`access(all) attachment E for S { - require X - }`) - utils.AssertEqualWithDiff(t, []error{ - &SyntaxError{ - Pos: ast.Position{Line: 2, Column: 11, Offset: 44}, - Message: "expected 'entitlement', got identifier", - }, - }, errs) - }) - - t.Run("required entitlements error non-nominal type", func(t *testing.T) { - - t.Parallel() - - _, errs := testParseDeclarations(`access(all) attachment E for S { - require entitlement [X] + require entitlement X }`) utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Pos: ast.Position{Line: 2, Column: 26, Offset: 59}, - Message: "unexpected non-nominal type: [X]", + Pos: ast.Position{Line: 2, Column: 3, Offset: 36}, + Message: "unexpected identifier", }, }, errs) }) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index b9d1b5c8e5..9d06d9def6 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -971,37 +971,7 @@ func parseAttachExpressionRemainder(p *parser, token lexer.Token) (*ast.AttachEx p.skipSpaceAndComments() - var entitlements []*ast.NominalType - if p.isToken(p.current, lexer.TokenIdentifier, KeywordWith) { - // consume the `with` token - p.nextSemanticToken() - - _, err = p.mustOne(lexer.TokenParenOpen) - if err != nil { - return nil, err - } - - entitlements, _, err = parseNominalTypes(p, lexer.TokenParenClose, lexer.TokenComma) - for _, entitlement := range entitlements { - _, err = rejectAccessKeywords(p, func() (*ast.NominalType, error) { - return entitlement, nil - }) - if err != nil { - return nil, err - } - } - if err != nil { - return nil, err - } - - _, err = p.mustOne(lexer.TokenParenClose) - if err != nil { - return nil, err - } - p.skipSpaceAndComments() - } - - return ast.NewAttachExpression(p.memoryGauge, base, attachment, entitlements, token.StartPos), nil + return ast.NewAttachExpression(p.memoryGauge, base, attachment, token.StartPos), nil } // Invocation Expression Grammar: diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index ac382ca8bb..b3c27caa96 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -2843,93 +2843,12 @@ func TestParseAttach(t *testing.T) { t.Parallel() - result, errs := testParseExpression("attach E() to r with (X, Y)") - require.Empty(t, errs) - - utils.AssertEqualWithDiff(t, - &ast.AttachExpression{ - Base: &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "r", - Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, - }, - }, - Attachment: &ast.InvocationExpression{ - InvokedExpression: &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "E", - Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, - }, - }, - ArgumentsStartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, - EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, - }, - Entitlements: []*ast.NominalType{ - ast.NewNominalType( - nil, - ast.Identifier{ - Identifier: "X", - Pos: ast.Position{Line: 1, Column: 22, Offset: 22}, - }, - nil, - ), - ast.NewNominalType( - nil, - ast.Identifier{ - Identifier: "Y", - Pos: ast.Position{Line: 1, Column: 25, Offset: 25}, - }, - nil, - ), - }, - StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, - }, - result, - ) - }) - - t.Run("with provided entitlements not closed", func(t *testing.T) { - - t.Parallel() - - _, errs := testParseExpression("attach E() to r with (X, Y") + _, errs := testParseExpression("attach E() to r with (X)") utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Message: "invalid end of input, expected ')'", - Pos: ast.Position{Offset: 26, Line: 1, Column: 26}, - }, - }, - errs, - ) - }) - - t.Run("with provided entitlements extra comma", func(t *testing.T) { - - t.Parallel() - - _, errs := testParseExpression("attach E() to r with (X, Y,)") - utils.AssertEqualWithDiff(t, - []error{ - &SyntaxError{ - Message: "missing type after separator", - Pos: ast.Position{Offset: 27, Line: 1, Column: 27}, - }, - }, - errs, - ) - }) - - t.Run("with provided entitlements unopened", func(t *testing.T) { - - t.Parallel() - - _, errs := testParseExpression("attach E() to r with X, Y)") - utils.AssertEqualWithDiff(t, - []error{ - &SyntaxError{ - Message: "expected token '('", - Pos: ast.Position{Offset: 21, Line: 1, Column: 21}, + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 16, Line: 1, Column: 16}, }, }, errs, diff --git a/runtime/parser/keyword.go b/runtime/parser/keyword.go index 00e7dc9413..605b4e9e32 100644 --- a/runtime/parser/keyword.go +++ b/runtime/parser/keyword.go @@ -69,7 +69,6 @@ const ( keywordAttach = "attach" keywordRemove = "remove" keywordTo = "to" - KeywordWith = "with" KeywordRequire = "require" KeywordStatic = "static" KeywordNative = "native" @@ -122,7 +121,6 @@ var allKeywords = []string{ KeywordDefault, KeywordEnum, KeywordView, - KeywordWith, KeywordMapping, KeywordRequire, keywordAttach, diff --git a/runtime/sema/access.go b/runtime/sema/access.go index b936f4495d..3c05b4673a 100644 --- a/runtime/sema/access.go +++ b/runtime/sema/access.go @@ -32,6 +32,7 @@ import ( type Access interface { isAccess() + IsPrimitiveAccess() bool ID() TypeID String() string QualifiedString() string @@ -72,8 +73,26 @@ func NewEntitlementSetAccess( } } +func NewAccessFromEntitlementSet( + set *EntitlementOrderedSet, + setKind EntitlementSetKind, +) Access { + if set.Len() == 0 { + return UnauthorizedAccess + } + + return EntitlementSetAccess{ + Entitlements: set, + SetKind: setKind, + } +} + func (EntitlementSetAccess) isAccess() {} +func (EntitlementSetAccess) IsPrimitiveAccess() bool { + return false +} + func (e EntitlementSetAccess) ID() TypeID { entitlementTypeIDs := make([]TypeID, 0, e.Entitlements.Len()) e.Entitlements.Foreach(func(entitlement *EntitlementType, _ struct{}) { @@ -266,6 +285,10 @@ func NewEntitlementMapAccess(mapType *EntitlementMapType) *EntitlementMapAccess func (*EntitlementMapAccess) isAccess() {} +func (*EntitlementMapAccess) IsPrimitiveAccess() bool { + return false +} + func (e *EntitlementMapAccess) ID() TypeID { return e.Type.ID() } @@ -435,6 +458,10 @@ var _ Access = PrimitiveAccess(0) func (PrimitiveAccess) isAccess() {} +func (PrimitiveAccess) IsPrimitiveAccess() bool { + return true +} + func (PrimitiveAccess) ID() TypeID { panic(errors.NewUnreachableError()) } diff --git a/runtime/sema/check_attach_expression.go b/runtime/sema/check_attach_expression.go index 9dd401c8bc..e2b70676cf 100644 --- a/runtime/sema/check_attach_expression.go +++ b/runtime/sema/check_attach_expression.go @@ -21,7 +21,6 @@ package sema import ( "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" - "github.com/onflow/cadence/runtime/common/orderedmap" ) func (checker *Checker) VisitAttachExpression(expression *ast.AttachExpression) Type { @@ -107,38 +106,5 @@ func (checker *Checker) VisitAttachExpression(expression *ast.AttachExpression) checker.Elaboration.SetAttachTypes(expression, attachmentCompositeType) - // compute the set of all the entitlements provided to this attachment - providedEntitlements := orderedmap.New[EntitlementOrderedSet](len(expression.Entitlements)) - for _, entitlement := range expression.Entitlements { - nominalType := checker.convertNominalType(entitlement) - if entitlementType, isEntitlement := nominalType.(*EntitlementType); isEntitlement { - _, present := providedEntitlements.Set(entitlementType, struct{}{}) - if present { - checker.report(&DuplicateEntitlementProvidedError{ - Range: ast.NewRangeFromPositioned(checker.memoryGauge, entitlement), - Entitlement: entitlementType, - }) - } - continue - } - checker.report(&InvalidNonEntitlementProvidedError{ - Range: ast.NewRangeFromPositioned(checker.memoryGauge, entitlement), - InvalidType: nominalType, - }) - } - - // if the attachment requires entitlements, check that they are provided as requested - if attachmentCompositeType.RequiredEntitlements != nil { - attachmentCompositeType.RequiredEntitlements.Foreach(func(key *EntitlementType, _ struct{}) { - if !providedEntitlements.Contains(key) { - checker.report(&RequiredEntitlementNotProvidedError{ - Range: ast.NewRangeFromPositioned(checker.memoryGauge, expression), - AttachmentType: attachmentCompositeType, - RequiredEntitlement: key, - }) - } - }) - } - return baseType } diff --git a/runtime/sema/check_composite_declaration.go b/runtime/sema/check_composite_declaration.go index 8267d69b88..d87bf49b7f 100644 --- a/runtime/sema/check_composite_declaration.go +++ b/runtime/sema/check_composite_declaration.go @@ -66,68 +66,54 @@ func (checker *Checker) checkAttachmentBaseType(attachmentType *CompositeType, a }) } +func (checker *Checker) checkAttachmentMemberAccess( + attachmentType *CompositeType, + member *Member, + baseType Type, + supportedBaseEntitlements *EntitlementOrderedSet, +) { + var requestedEntitlements *EntitlementOrderedSet = &orderedmap.OrderedMap[*EntitlementType, struct{}]{} + switch memberAccess := member.Access.(type) { + case EntitlementSetAccess: + requestedEntitlements = memberAccess.Entitlements + // if the attachment field/function is declared with mapped access, the domain of the map must be a + // subset of the supported entitlements on the base. This is because the attachment reference + // will never be able to possess any entitlements other than these, so any map relations that map + // from other entitlements will be unreachable + case *EntitlementMapAccess: + requestedEntitlements = memberAccess.Domain().Entitlements + } + + requestedEntitlements.Foreach(func(entitlement *EntitlementType, _ struct{}) { + if !supportedBaseEntitlements.Contains(entitlement) { + checker.report(&InvalidAttachmentEntitlementError{ + Attachment: attachmentType, + BaseType: baseType, + InvalidEntitlement: entitlement, + Pos: member.Identifier.Pos, + }) + } + }) +} + func (checker *Checker) checkAttachmentMembersAccess(attachmentType *CompositeType) { - // all the access modifiers for attachment members must be elements of the - // codomain of the attachment's entitlement map. This is because the codomain - // of the attachment's declared map specifies all the entitlements one can possibly - // have to that attachment, since the only way to obtain an attachment reference - // is to access it off of a base (and hence through the map). - // --------------------------------------------------- - // entitlement map M { - // E -> F - // X -> Y - // U -> V - // } - // - // access(M) attachment A for R { - // access(F) fun foo() {} - // access(Y | F) fun bar() {} - // access(V & Y) fun baz() {} - // - // access(V | Q) fun qux() {} - // } - // --------------------------------------------------- - // - // in this example, the only entitlements one can ever obtain to an &A reference are - // `F`, `Y` and `V`, and as such these are the only entitlements that may be used - // in `A`'s definition. Thus the definitions of `foo`, `bar`, and `baz` are valid, - // while the definition of `qux` is not. - var attachmentAccess Access = UnauthorizedAccess - if attachmentType.AttachmentEntitlementAccess != nil { - attachmentAccess = attachmentType.AttachmentEntitlementAccess - } - - if attachmentAccess, ok := attachmentAccess.(*EntitlementMapAccess); ok { - codomain := attachmentAccess.Codomain() - attachmentType.Members.Foreach(func(_ string, member *Member) { - if memberAccess, ok := member.Access.(EntitlementSetAccess); ok { - memberAccess.Entitlements.Foreach(func(entitlement *EntitlementType, _ struct{}) { - if !codomain.Entitlements.Contains(entitlement) { - checker.report(&InvalidAttachmentEntitlementError{ - Attachment: attachmentType, - AttachmentAccessModifier: attachmentAccess, - InvalidEntitlement: entitlement, - Pos: member.Identifier.Pos, - }) - } - }) - } - }) - return + // all the access modifiers for attachment members must be valid entitlements for the base type + var supportedBaseEntitlements *EntitlementOrderedSet = &orderedmap.OrderedMap[*EntitlementType, struct{}]{} + baseType := attachmentType.GetBaseType() + switch base := attachmentType.GetBaseType().(type) { + case EntitlementSupportingType: + supportedBaseEntitlements = base.SupportedEntitlements() } - // if the attachment's access is public, its members may not have entitlement access - attachmentType.Members.Foreach(func(_ string, member *Member) { - if _, ok := member.Access.(PrimitiveAccess); ok { - return - } - checker.report(&InvalidAttachmentEntitlementError{ - Attachment: attachmentType, - AttachmentAccessModifier: attachmentAccess, - Pos: member.Identifier.Pos, + attachmentType.EffectiveInterfaceConformanceSet().ForEach(func(intf *InterfaceType) { + intf.Members.Foreach(func(_ string, member *Member) { + checker.checkAttachmentMemberAccess(attachmentType, member, baseType, supportedBaseEntitlements) }) + }) + attachmentType.Members.Foreach(func(_ string, member *Member) { + checker.checkAttachmentMemberAccess(attachmentType, member, baseType, supportedBaseEntitlements) }) } @@ -630,35 +616,7 @@ func (checker *Checker) declareNestedDeclarations( func (checker *Checker) declareAttachmentType(declaration *ast.AttachmentDeclaration) *CompositeType { composite := checker.declareCompositeType(declaration) - composite.baseType = checker.convertNominalType(declaration.BaseType) - - attachmentAccess := checker.accessFromAstAccess(declaration.Access) - if attachmentAccess, ok := attachmentAccess.(*EntitlementMapAccess); ok { - composite.AttachmentEntitlementAccess = attachmentAccess - } - - // add all the required entitlements to a set for this attachment - requiredEntitlements := orderedmap.New[EntitlementOrderedSet](len(declaration.RequiredEntitlements)) - for _, entitlement := range declaration.RequiredEntitlements { - nominalType := checker.convertNominalType(entitlement) - if entitlementType, isEntitlement := nominalType.(*EntitlementType); isEntitlement { - _, present := requiredEntitlements.Set(entitlementType, struct{}{}) - if present { - checker.report(&DuplicateEntitlementRequirementError{ - Range: ast.NewRangeFromPositioned(checker.memoryGauge, entitlement), - Entitlement: entitlementType, - }) - } - continue - } - checker.report(&InvalidNonEntitlementRequirement{ - Range: ast.NewRangeFromPositioned(checker.memoryGauge, entitlement), - InvalidType: nominalType, - }) - } - composite.RequiredEntitlements = requiredEntitlements - return composite } @@ -1619,7 +1577,7 @@ func (checker *Checker) memberSatisfied( // Check access effectiveInterfaceMemberAccess := checker.effectiveInterfaceMemberAccess(interfaceMember.Access) - effectiveCompositeMemberAccess := checker.effectiveCompositeMemberAccess(compositeMember.Access) + effectiveCompositeMemberAccess := checker.EffectiveCompositeMemberAccess(compositeMember.Access) return !effectiveCompositeMemberAccess.IsLessPermissiveThan(effectiveInterfaceMemberAccess) } @@ -1951,7 +1909,7 @@ func (checker *Checker) enumMembersAndOrigins( // Enum cases must be effectively public enumAccess := checker.accessFromAstAccess(enumCase.Access) - if !checker.effectiveCompositeMemberAccess(enumAccess).Equal(PrimitiveAccess(ast.AccessAll)) { + if !checker.EffectiveCompositeMemberAccess(enumAccess).Equal(PrimitiveAccess(ast.AccessAll)) { checker.report( &InvalidAccessModifierError{ DeclarationKind: enumCase.DeclarationKind(), @@ -2069,16 +2027,19 @@ func (checker *Checker) checkDefaultDestroyParamExpressionKind( func (checker *Checker) checkDefaultDestroyEventParam( param Parameter, astParam *ast.Parameter, - containerType ContainerType, + containerType EntitlementSupportingType, containerDeclaration ast.Declaration, ) { paramType := param.TypeAnnotation.Type paramDefaultArgument := astParam.DefaultArgument // make `self` and `base` available when checking default arguments so the fields of the composite are available - checker.declareSelfValue(containerType, containerDeclaration.DeclarationDocString()) + // as this event is emitted when the resource is destroyed, these values should be fully entitled + fullyEntitledAccess := NewAccessFromEntitlementSet(containerType.SupportedEntitlements(), Conjunction) + checker.declareSelfValue(fullyEntitledAccess, containerType, containerDeclaration.DeclarationDocString()) if compositeContainer, isComposite := containerType.(*CompositeType); isComposite && compositeContainer.Kind == common.CompositeKindAttachment { checker.declareBaseValue( + fullyEntitledAccess, compositeContainer.baseType, compositeContainer, compositeContainer.baseTypeDocString) @@ -2105,7 +2066,7 @@ func (checker *Checker) checkDefaultDestroyEventParam( func (checker *Checker) checkDefaultDestroyEvent( eventType *CompositeType, eventDeclaration ast.CompositeLikeDeclaration, - containerType ContainerType, + containerType EntitlementSupportingType, containerDeclaration ast.Declaration, ) { @@ -2213,9 +2174,10 @@ func (checker *Checker) checkSpecialFunction( checker.enterValueScope() defer checker.leaveValueScope(specialFunction.EndPosition, checkResourceLoss) - fnAccess := checker.effectiveMemberAccess(checker.accessFromAstAccess(specialFunction.FunctionDeclaration.Access), containerKind) + // initializers and destructors are considered fully entitled to their container type + fnAccess := NewAccessFromEntitlementSet(containerType.SupportedEntitlements(), Conjunction) - checker.declareSelfValue(containerType, containerDocString) + checker.declareSelfValue(fnAccess, containerType, containerDocString) if containerType.GetCompositeKind() == common.CompositeKindAttachment { // attachments cannot be interfaces, so this cast must succeed attachmentType, ok := containerType.(*CompositeType) @@ -2223,6 +2185,7 @@ func (checker *Checker) checkSpecialFunction( panic(errors.NewUnreachableError()) } checker.declareBaseValue( + fnAccess, attachmentType.baseType, attachmentType, attachmentType.baseTypeDocString) @@ -2279,9 +2242,16 @@ func (checker *Checker) checkCompositeFunctions( checker.enterValueScope() defer checker.leaveValueScope(function.EndPosition, true) - checker.declareSelfValue(selfType, selfDocString) + fnAccess := checker.effectiveMemberAccess(checker.accessFromAstAccess(function.Access), ContainerKindComposite) + // all non-entitlement functions produce unauthorized references in attachments + if fnAccess.IsPrimitiveAccess() { + fnAccess = UnauthorizedAccess + } + + checker.declareSelfValue(fnAccess, selfType, selfDocString) if selfType.GetCompositeKind() == common.CompositeKindAttachment { checker.declareBaseValue( + fnAccess, selfType.baseType, selfType, selfType.baseTypeDocString, @@ -2338,45 +2308,24 @@ func (checker *Checker) declareLowerScopedValue( } } -func (checker *Checker) declareSelfValue(selfType Type, selfDocString string) { +func (checker *Checker) declareSelfValue(fnAccess Access, selfType Type, selfDocString string) { // inside of an attachment, self is a reference to the attachment's type, because // attachments are never first class values, they must always exist inside references if typedSelfType, ok := selfType.(*CompositeType); ok && typedSelfType.Kind == common.CompositeKindAttachment { - // the `self` value in an attachment is considered fully-entitled to that attachment, or - // equivalently the entire codomain of the attachment's map - var selfAccess Access = UnauthorizedAccess - if typedSelfType.AttachmentEntitlementAccess != nil { - selfAccess = typedSelfType.AttachmentEntitlementAccess.Codomain() - } - selfType = NewReferenceType(checker.memoryGauge, selfAccess, typedSelfType) + // the `self` value in an attachment is entitled to the same entitlements required by the containing function + selfType = NewReferenceType(checker.memoryGauge, fnAccess, typedSelfType) } checker.declareLowerScopedValue(selfType, selfDocString, SelfIdentifier, common.DeclarationKindSelf) } -func (checker *Checker) declareBaseValue(baseType Type, attachmentType *CompositeType, superDocString string) { +func (checker *Checker) declareBaseValue(fnAccess Access, baseType Type, attachmentType *CompositeType, superDocString string) { if typedBaseType, ok := baseType.(*InterfaceType); ok { // we can't actually have a value of an interface type I, so instead we create a value of {I} // to be referenced by `base` baseType = NewIntersectionType(checker.memoryGauge, []*InterfaceType{typedBaseType}) } - // the `base` value in an attachment function has the set of entitlements defined by the required entitlements specified in the attachment's declaration - // ------------------------------- - // entitlement E - // entitlement F - // access(all) attachment A for R { - // require entitlement E - // access(all) fun foo() { ... } - // } - // ------------------------------- - // within the body of `foo`, the `base` value will be entitled to `E` but not `F`, because only `E` was required in the attachment's declaration - var baseAccess Access = UnauthorizedAccess - if attachmentType.RequiredEntitlements.Len() > 0 { - baseAccess = EntitlementSetAccess{ - Entitlements: attachmentType.RequiredEntitlements, - SetKind: Conjunction, - } - } - base := NewReferenceType(checker.memoryGauge, baseAccess, baseType) + // the `base` value in an attachment is entitled to the same entitlements required by the containing function + base := NewReferenceType(checker.memoryGauge, fnAccess, baseType) checker.declareLowerScopedValue(base, superDocString, BaseIdentifier, common.DeclarationKindBase) } diff --git a/runtime/sema/check_interface_declaration.go b/runtime/sema/check_interface_declaration.go index 7dbf61930e..241c3d92ae 100644 --- a/runtime/sema/check_interface_declaration.go +++ b/runtime/sema/check_interface_declaration.go @@ -224,7 +224,9 @@ func (checker *Checker) checkInterfaceFunctions( checker.enterValueScope() defer checker.leaveValueScope(function.EndPosition, false) - checker.declareSelfValue(selfType, selfDocString) + fnAccess := checker.effectiveMemberAccess(checker.accessFromAstAccess(function.Access), ContainerKindInterface) + + checker.declareSelfValue(fnAccess, selfType, selfDocString) mustExit := false checkResourceLoss := false diff --git a/runtime/sema/check_member_expression.go b/runtime/sema/check_member_expression.go index 27dd302112..d5b2821ff1 100644 --- a/runtime/sema/check_member_expression.go +++ b/runtime/sema/check_member_expression.go @@ -375,7 +375,9 @@ func (checker *Checker) visitMember(expression *ast.MemberExpression, isAssignme // in the current location of the checker, along with the authorzation with which the result can be used func (checker *Checker) isReadableMember(accessedType Type, member *Member, resultingType Type, accessRange func() ast.Range) (bool, Access) { if checker.Config.AccessCheckMode.IsReadableAccess(member.Access) || - checker.containerTypes[member.ContainerType] { + // only allow references unrestricted access to members in their own container that are not entitled + // this prevents rights escalation attacks on entitlements + (member.Access.IsPrimitiveAccess() && checker.containerTypes[member.ContainerType]) { if mappedAccess, isMappedAccess := member.Access.(*EntitlementMapAccess); isMappedAccess { return checker.mapAccess(mappedAccess, accessedType, resultingType, accessRange) diff --git a/runtime/sema/check_transaction_declaration.go b/runtime/sema/check_transaction_declaration.go index f6be79dabe..345300cdcd 100644 --- a/runtime/sema/check_transaction_declaration.go +++ b/runtime/sema/check_transaction_declaration.go @@ -57,7 +57,7 @@ func (checker *Checker) VisitTransactionDeclaration(declaration *ast.Transaction checker.enterValueScope() defer checker.leaveValueScope(declaration.EndPosition, true) - checker.declareSelfValue(transactionType, "") + checker.declareSelfValue(UnauthorizedAccess, transactionType, "") if declaration.ParameterList != nil { checker.checkTransactionParameters(declaration, transactionType.Parameters) diff --git a/runtime/sema/checker.go b/runtime/sema/checker.go index 71ebba16ad..4dcce1b959 100644 --- a/runtime/sema/checker.go +++ b/runtime/sema/checker.go @@ -1915,12 +1915,7 @@ func (checker *Checker) checkEntitlementMapAccess( containerKind *common.CompositeKind, startPos ast.Position, ) { - // attachments may be declared with an entitlement map access - if declarationKind == common.DeclarationKindAttachment { - return - } - - // otherwise, mapped entitlements may only be used in structs, resources and attachments + // mapped entitlements may only be used in structs, resources and attachments if containerKind == nil || (*containerKind != common.CompositeKindResource && *containerKind != common.CompositeKindStructure && @@ -1933,7 +1928,17 @@ func (checker *Checker) checkEntitlementMapAccess( return } - // mapped entitlement fields must be, one of: + // due to potential security issues, entitlement mappings are disabled on attachments for now + if *containerKind == common.CompositeKindAttachment { + checker.report( + &InvalidAttachmentMappedEntitlementMemberError{ + Pos: startPos, + }, + ) + return + } + + // mapped entitlement fields must be one of: // 1) An [optional] reference that is authorized to the same mapped entitlement. // 2) A function that return an [optional] reference authorized to the same mapped entitlement. // 3) A container - So if the parent is a reference, entitlements can be granted to the resulting field reference. @@ -2375,7 +2380,7 @@ func (checker *Checker) TypeActivationDepth() int { func (checker *Checker) effectiveMemberAccess(access Access, containerKind ContainerKind) Access { switch containerKind { case ContainerKindComposite: - return checker.effectiveCompositeMemberAccess(access) + return checker.EffectiveCompositeMemberAccess(access) case ContainerKindInterface: return checker.effectiveInterfaceMemberAccess(access) default: @@ -2391,7 +2396,7 @@ func (checker *Checker) effectiveInterfaceMemberAccess(access Access) Access { } } -func (checker *Checker) effectiveCompositeMemberAccess(access Access) Access { +func (checker *Checker) EffectiveCompositeMemberAccess(access Access) Access { if !access.Equal(PrimitiveAccess(ast.AccessNotSpecified)) { return access } diff --git a/runtime/sema/errors.go b/runtime/sema/errors.go index 44fe1f89b4..5f6a639c17 100644 --- a/runtime/sema/errors.go +++ b/runtime/sema/errors.go @@ -4209,6 +4209,30 @@ func (e *InvalidMappedEntitlementMemberError) EndPosition(common.MemoryGauge) as return e.Pos } +// InvalidAttachmentMappedEntitlementMemberError +type InvalidAttachmentMappedEntitlementMemberError struct { + Pos ast.Position +} + +var _ SemanticError = &InvalidAttachmentMappedEntitlementMemberError{} +var _ errors.UserError = &InvalidAttachmentMappedEntitlementMemberError{} + +func (*InvalidAttachmentMappedEntitlementMemberError) isSemanticError() {} + +func (*InvalidAttachmentMappedEntitlementMemberError) IsUserError() {} + +func (e *InvalidAttachmentMappedEntitlementMemberError) Error() string { + return "entitlement mapped members are not yet supported on attachments" +} + +func (e *InvalidAttachmentMappedEntitlementMemberError) StartPosition() ast.Position { + return e.Pos +} + +func (e *InvalidAttachmentMappedEntitlementMemberError) EndPosition(common.MemoryGauge) ast.Position { + return e.Pos +} + // InvalidNonEntitlementAccessError type InvalidNonEntitlementAccessError struct { ast.Range @@ -4393,94 +4417,6 @@ func (e *CyclicEntitlementMappingError) Error() string { ) } -type DuplicateEntitlementRequirementError struct { - Entitlement *EntitlementType - ast.Range -} - -var _ SemanticError = &DuplicateEntitlementRequirementError{} -var _ errors.UserError = &DuplicateEntitlementRequirementError{} - -func (*DuplicateEntitlementRequirementError) isSemanticError() {} - -func (*DuplicateEntitlementRequirementError) IsUserError() {} - -func (e *DuplicateEntitlementRequirementError) Error() string { - return fmt.Sprintf("entitlement %s is already required by this attachment", e.Entitlement.QualifiedString()) -} - -type DuplicateEntitlementProvidedError struct { - Entitlement *EntitlementType - ast.Range -} - -var _ SemanticError = &DuplicateEntitlementProvidedError{} -var _ errors.UserError = &DuplicateEntitlementProvidedError{} - -func (*DuplicateEntitlementProvidedError) isSemanticError() {} - -func (*DuplicateEntitlementProvidedError) IsUserError() {} - -func (e *DuplicateEntitlementProvidedError) Error() string { - return fmt.Sprintf("entitlement %s is already provided to this attachment", e.Entitlement.QualifiedString()) -} - -// InvalidNonEntitlementRequirement -type InvalidNonEntitlementRequirement struct { - InvalidType Type - ast.Range -} - -var _ SemanticError = &InvalidNonEntitlementRequirement{} -var _ errors.UserError = &InvalidNonEntitlementRequirement{} - -func (*InvalidNonEntitlementRequirement) isSemanticError() {} - -func (*InvalidNonEntitlementRequirement) IsUserError() {} - -func (e *InvalidNonEntitlementRequirement) Error() string { - return fmt.Sprintf("cannot use %s as an entitlement requirement", e.InvalidType.QualifiedString()) -} - -// InvalidNonEntitlementRequirement -type InvalidNonEntitlementProvidedError struct { - InvalidType Type - ast.Range -} - -var _ SemanticError = &InvalidNonEntitlementProvidedError{} -var _ errors.UserError = &InvalidNonEntitlementProvidedError{} - -func (*InvalidNonEntitlementProvidedError) isSemanticError() {} - -func (*InvalidNonEntitlementProvidedError) IsUserError() {} - -func (e *InvalidNonEntitlementProvidedError) Error() string { - return fmt.Sprintf("cannot provide %s as an entitlement to this attachment", e.InvalidType.QualifiedString()) -} - -// InvalidNonEntitlementRequirement -type RequiredEntitlementNotProvidedError struct { - RequiredEntitlement *EntitlementType - AttachmentType *CompositeType - ast.Range -} - -var _ SemanticError = &RequiredEntitlementNotProvidedError{} -var _ errors.UserError = &RequiredEntitlementNotProvidedError{} - -func (*RequiredEntitlementNotProvidedError) isSemanticError() {} - -func (*RequiredEntitlementNotProvidedError) IsUserError() {} - -func (e *RequiredEntitlementNotProvidedError) Error() string { - return fmt.Sprintf( - "attachment type `%s` requires entitlement `%s` to be provided when attaching", - e.AttachmentType.QualifiedString(), - e.RequiredEntitlement.QualifiedString(), - ) -} - // InvalidBaseTypeError type InvalidBaseTypeError struct { @@ -4647,10 +4583,10 @@ func (e *AttachmentsNotEnabledError) Error() string { // InvalidAttachmentEntitlementError type InvalidAttachmentEntitlementError struct { - Attachment *CompositeType - AttachmentAccessModifier Access - InvalidEntitlement *EntitlementType - Pos ast.Position + Attachment *CompositeType + BaseType Type + InvalidEntitlement *EntitlementType + Pos ast.Position } var _ SemanticError = &InvalidAttachmentEntitlementError{} @@ -4672,15 +4608,10 @@ func (e *InvalidAttachmentEntitlementError) Error() string { } func (e *InvalidAttachmentEntitlementError) SecondaryError() string { - switch access := e.AttachmentAccessModifier.(type) { - case PrimitiveAccess: - return "attachments declared with `access(all)` access do not support entitlements on their members" - case *EntitlementMapAccess: - return fmt.Sprintf("`%s` must appear in the output of the entitlement mapping `%s`", - e.InvalidEntitlement.QualifiedIdentifier(), - access.Type.QualifiedIdentifier()) - } - return "" + return fmt.Sprintf("`%s` must appear in the base type `%s`", + e.InvalidEntitlement.QualifiedIdentifier(), + e.BaseType.String(), + ) } func (e *InvalidAttachmentEntitlementError) StartPosition() ast.Position { diff --git a/runtime/sema/type.go b/runtime/sema/type.go index 33b666c31a..e99b80c536 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -304,6 +304,7 @@ func TypeActivationNestedType(typeActivation *VariableActivation, qualifiedIdent // CompositeKindedType is a type which has a composite kind type CompositeKindedType interface { Type + EntitlementSupportingType GetCompositeKind() common.CompositeKind } @@ -4293,10 +4294,8 @@ type CompositeType struct { // in a language with support for algebraic data types, // we would implement this as an argument to the CompositeKind type constructor. // Alas, this is Go, so for now these fields are only non-nil when Kind is CompositeKindAttachment - baseType Type - baseTypeDocString string - RequiredEntitlements *EntitlementOrderedSet - AttachmentEntitlementAccess *EntitlementMapAccess + baseType Type + baseTypeDocString string DefaultDestroyEvent *CompositeType @@ -4528,6 +4527,14 @@ func (t *CompositeType) SupportedEntitlements() (set *EntitlementOrderedSet) { set.SetAll(it.SupportedEntitlements()) }) + // attachments support at least the entitlements supported by their base, + // and we must ensure there is no recursive case + if entitlementSupportingBase, isEntitlementSupportingBase := + t.GetBaseType().(EntitlementSupportingType); isEntitlementSupportingBase && entitlementSupportingBase != t { + + set.SetAll(entitlementSupportingBase.SupportedEntitlements()) + } + t.supportedEntitlements = set return set } @@ -4686,10 +4693,9 @@ func (t *CompositeType) TypeIndexingElementType(indexingType Type, _ func() ast. var access Access = UnauthorizedAccess switch attachment := indexingType.(type) { case *CompositeType: - attachmentEntitlementAccess := attachment.AttachmentEntitlementAccess - if attachmentEntitlementAccess != nil { - access = attachmentEntitlementAccess.Codomain() - } + // when accessed on an owned value, the produced attachment reference is entitled to all the + // entitlements it supports + access = NewAccessFromEntitlementSet(attachment.SupportedEntitlements(), Conjunction) } return &OptionalType{ @@ -6037,6 +6043,9 @@ func (t *ReferenceType) String() string { if t.Authorization != UnauthorizedAccess { authorization = t.Authorization.String() } + if _, isMapping := t.Authorization.(*EntitlementMapAccess); isMapping { + authorization = "mapping " + authorization + } return formatReferenceType(" ", authorization, t.Type.String()) } @@ -6048,6 +6057,9 @@ func (t *ReferenceType) QualifiedString() string { if t.Authorization != UnauthorizedAccess { authorization = t.Authorization.QualifiedString() } + if _, isMapping := t.Authorization.(*EntitlementMapAccess); isMapping { + authorization = "mapping " + authorization + } return formatReferenceType(" ", authorization, t.Type.QualifiedString()) } @@ -6158,15 +6170,11 @@ func (t *ReferenceType) TypeIndexingElementType(indexingType Type, astRange func } var access Access = UnauthorizedAccess - switch attachment := indexingType.(type) { + switch indexingType.(type) { case *CompositeType: - if attachment.AttachmentEntitlementAccess != nil { - var err error - access, err = attachment.AttachmentEntitlementAccess.Image(t.Authorization, astRange) - if err != nil { - return nil, err - } - } + // attachment access on a composite reference yields a reference to the attachment entitled to the same + // entitlements as that reference + access = t.Authorization } return &OptionalType{ @@ -7240,9 +7248,9 @@ func (t *IntersectionType) TypeIndexingElementType(indexingType Type, _ func() a var access Access = UnauthorizedAccess switch attachment := indexingType.(type) { case *CompositeType: - if attachment.AttachmentEntitlementAccess != nil { - access = attachment.AttachmentEntitlementAccess.Codomain() - } + // when accessed on an owned value, the produced attachment reference is entitled to all the + // entitlements it supports + access = NewAccessFromEntitlementSet(attachment.SupportedEntitlements(), Conjunction) } return &OptionalType{ diff --git a/runtime/sema/type_test.go b/runtime/sema/type_test.go index 3256778d60..3308e027ef 100644 --- a/runtime/sema/type_test.go +++ b/runtime/sema/type_test.go @@ -2166,7 +2166,7 @@ func TestReferenceType_String(t *testing.T) { referenceType := NewReferenceType(nil, access, IntType) assert.Equal(t, - "auth(M) &Int", + "auth(mapping M) &Int", referenceType.String(), ) }) @@ -2220,7 +2220,7 @@ func TestReferenceType_QualifiedString(t *testing.T) { referenceType := NewReferenceType(nil, access, IntType) assert.Equal(t, - "auth(M) &Int", + "auth(mapping M) &Int", referenceType.QualifiedString(), ) }) @@ -2256,7 +2256,7 @@ func TestReferenceType_QualifiedString(t *testing.T) { referenceType := NewReferenceType(nil, access, IntType) assert.Equal(t, - "auth(C.M) &Int", + "auth(mapping C.M) &Int", referenceType.QualifiedString(), ) }) diff --git a/runtime/stdlib/contract_update_validation.go b/runtime/stdlib/contract_update_validation.go index 39f0d5edff..4c7502a27e 100644 --- a/runtime/stdlib/contract_update_validation.go +++ b/runtime/stdlib/contract_update_validation.go @@ -147,12 +147,6 @@ func (validator *ContractUpdateValidator) checkDeclarationUpdatability( validator.checkConformances(oldDecl, newDecl) } } - - if newDecl, ok := newDeclaration.(*ast.AttachmentDeclaration); ok { - if oldDecl, ok := oldDeclaration.(*ast.AttachmentDeclaration); ok { - validator.checkRequiredEntitlements(oldDecl, newDecl) - } - } } func (validator *ContractUpdateValidator) checkFields(oldDeclaration ast.Declaration, newDeclaration ast.Declaration) { @@ -338,47 +332,6 @@ func (validator *ContractUpdateValidator) checkEnumCases(oldDeclaration ast.Decl } } -func (validator *ContractUpdateValidator) checkRequiredEntitlements( - oldDecl *ast.AttachmentDeclaration, - newDecl *ast.AttachmentDeclaration, -) { - oldEntitlements := oldDecl.RequiredEntitlements - newEntitlements := newDecl.RequiredEntitlements - - // updates cannot add new entitlement requirements, or equivalently, - // the new entitlements must all be present in the old entitlements list - // Adding new entitlement requirements has to be prohibited because it would - // be a security vulnerability. If your attachment previously only requires X access to the base, - // people who might be okay giving an attachment X access to their resource would be willing to attach it. - // If the author could later add a requirement to the attachment declaration asking for Y access as well, - // then they would be able to access Y-entitled values on existing attached bases without ever having - // received explicit permission from the resource owners to access that entitlement. - - for _, newEntitlement := range newEntitlements { - found := false - for index, oldEntitlement := range oldEntitlements { - err := oldEntitlement.CheckEqual(newEntitlement, validator) - if err == nil { - found = true - - // Remove the matched entitlement, so we don't have to check it again. - // i.e: optimization - oldEntitlements = append(oldEntitlements[:index], oldEntitlements[index+1:]...) - break - } - } - - if !found { - validator.report(&RequiredEntitlementMismatchError{ - DeclName: newDecl.Identifier.Identifier, - Range: ast.NewUnmeteredRangeFromPositioned(newDecl.Identifier), - }) - - return - } - } -} - func (validator *ContractUpdateValidator) checkConformances( oldDecl *ast.CompositeDeclaration, newDecl *ast.CompositeDeclaration, @@ -595,21 +548,6 @@ func (e *ConformanceMismatchError) Error() string { return fmt.Sprintf("conformances does not match in `%s`", e.DeclName) } -// RequiredEntitlementMismatchError is reported during a contract update, when the required entitlements of the new attachment -// does not match the existing one. -type RequiredEntitlementMismatchError struct { - DeclName string - ast.Range -} - -var _ errors.UserError = &RequiredEntitlementMismatchError{} - -func (*RequiredEntitlementMismatchError) IsUserError() {} - -func (e *RequiredEntitlementMismatchError) Error() string { - return fmt.Sprintf("required entitlements do not match in `%s`", e.DeclName) -} - // EnumCaseMismatchError is reported during an enum update, when an updated enum case // does not match the existing enum case. type EnumCaseMismatchError struct { diff --git a/runtime/tests/checker/attachments_test.go b/runtime/tests/checker/attachments_test.go index ac40b40701..9aab821faa 100644 --- a/runtime/tests/checker/attachments_test.go +++ b/runtime/tests/checker/attachments_test.go @@ -1330,6 +1330,29 @@ func TestCheckAttachmentSelfTyping(t *testing.T) { require.NoError(t, err) }) + + t.Run("self access restricted", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + resource R { + access(E) fun foo() {} + } + attachment Test for R { + access(E) fun bar() {} + fun foo(t: &Test) { + t.bar() + } + }`, + ) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.InvalidAccessError{}, errs[0]) + }) } func TestCheckAttachmentType(t *testing.T) { @@ -3801,13 +3824,11 @@ func TestCheckAttachmentsExternalMutation(t *testing.T) { _, err := ParseAndCheck(t, ` - access(all) resource R {} - - entitlement mapping M { - Mutate -> Insert + access(all) resource R { + access(Mutate) fun foo() {} } - access(mapping M) attachment A for R { + access(all) attachment A for R { access(mapping Identity) let x: [String] init() { self.x = ["x"] @@ -3822,7 +3843,8 @@ func TestCheckAttachmentsExternalMutation(t *testing.T) { `, ) - require.NoError(t, err) + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) }) t.Run("in base", func(t *testing.T) { @@ -3833,6 +3855,7 @@ func TestCheckAttachmentsExternalMutation(t *testing.T) { ` access(all) resource R { access(all) fun foo() { + // this only works because A supports all the entitlements of R self[A]!.x.append("y") } } @@ -3875,23 +3898,19 @@ func TestCheckAttachmentsExternalMutation(t *testing.T) { ) }) - t.Run("in base, with entitlements", func(t *testing.T) { + t.Run("identity mapping in attachment", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - entitlement mapping M { - Mutate -> Insert - } - access(all) resource R { - access(all) fun foo() { + access(Insert) fun foo() { var xRef = self[A]!.x xRef.append("y") } } - access(mapping M) attachment A for R { + access(all) attachment A for R { access(mapping Identity) let x: [String] init() { self.x = ["x"] @@ -3900,7 +3919,8 @@ func TestCheckAttachmentsExternalMutation(t *testing.T) { `, ) - require.NoError(t, err) + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) }) t.Run("in self, through base", func(t *testing.T) { @@ -4030,6 +4050,289 @@ func TestCheckAttachmentBaseNonMember(t *testing.T) { require.NoError(t, err) }) + + t.Run("entitlement mapped field", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement G + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) fun foo() {} + access(G) fun bar() {} + } + access(all) attachment A for R { + access(mapping M) let x: [String] + init() { + self.x = ["x"] + } + } + fun foo() { + let r <- attach A() to <- create R() + let a = r[A]! + let x: auth(F) &[String] = a.x + destroy r + } + `, + ) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }) + + t.Run("entitlement mapped function self value cast", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) fun foo() {} + access(F) fun bar() {} + } + access(all) attachment A for R { + access(F) let x: Int + init() { + self.x = 3 + } + access(mapping M) fun foo(): auth(mapping M) &Int { + if let concreteSelf = self as? auth(F) &A { + return &concreteSelf.x + } + return &1 + } + } + fun foo(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, + ) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }) + + t.Run("entitlement mapped function self value cast invalid access", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) fun foo() {} + access(F) fun bar() {} + } + access(all) attachment A for R { + access(E) let x: Int + init() { + self.x = 3 + } + access(mapping M) fun foo(): auth(mapping M) &Int { + if let concreteSelf = self as? auth(F) &A { + return &concreteSelf.x + } + return &1 + } + } + fun foo(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, + ) + + errs := RequireCheckerErrors(t, err, 2) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + require.IsType(t, &sema.InvalidAccessError{}, errs[1]) + }) + + t.Run("entitlement mapped function base value cast", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(F) let x: Int + init() { + self.x = 3 + } + access(E) fun bar() {} + } + access(all) attachment A for R { + access(mapping M) fun foo(): auth(mapping M) &Int { + if let concreteBase = base as? auth(F) &R { + return &concreteBase.x + } + return &1 + } + } + fun foo(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, + ) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }) + + t.Run("entitlement mapped function base value cast invalid access", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) let x: Int + init() { + self.x = 3 + } + access(F) fun bar() {} + } + access(all) attachment A for R { + access(mapping M) fun foo(): auth(mapping M) &Int { + if let concreteBase = base as? auth(F) &R { + return &concreteBase.x + } + return &1 + } + } + fun foo(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, + ) + + errs := RequireCheckerErrors(t, err, 2) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + assert.IsType(t, &sema.InvalidAccessError{}, errs[1]) + }) + + t.Run("entitlement mapped function self value access", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) fun foo() {} + access(F) fun bar() {} + } + access(all) attachment A for R { + access(E) let x: Int + init() { + self.x = 3 + } + access(mapping M) fun foo(): auth(mapping M) &Int { + return &self.x + } + } + fun foo(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, + ) + + errs := RequireCheckerErrors(t, err, 2) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + assert.IsType(t, &sema.InvalidAccessError{}, errs[1]) + }) + + t.Run("entitlement mapped function base value access", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) let x: Int + init() { + self.x = 3 + } + access(F) fun bar() {} + } + access(all) attachment A for R { + access(mapping M) fun foo(): auth(mapping M) &Int { + return &base.x + } + } + fun foo(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, + ) + + errs := RequireCheckerErrors(t, err, 2) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + assert.IsType(t, &sema.InvalidAccessError{}, errs[1]) + }) } func TestCheckAttachmentsResourceReference(t *testing.T) { @@ -4451,16 +4754,15 @@ func TestCheckAttachmentForEachAttachment(t *testing.T) { ` entitlement F entitlement E - entitlement mapping M { - E -> F - } fun bar (attachmentRef: &AnyResourceAttachment) { if let a = attachmentRef as? auth(F) &A { a.foo() } } - resource R {} - access(mapping M) attachment A for R { + resource R { + access(F) fun foo() {} + } + access(all) attachment A for R { access(F) fun foo() {} } access(all) fun foo(s: @R) { @@ -4664,3 +4966,33 @@ func TestCheckAttachmentPurity(t *testing.T) { assert.IsType(t, &sema.PurityError{}, errs[0]) }) } + +func TestCheckAccessOnNonEntitlementSupportingBaseCreatesUnauthorizedReference(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + access(all) contract Test { + access(all) resource R {} + access(all) attachment A for R {} + access(all) fun makeRWithA(): @R { + return <- attach A() to <-create R() + } + } + access(all) fun main(): &Test.A? { + let r <- Test.makeRWithA() + var a = r[Test.A] + + a = returnSameRef(a) + + destroy r + return a + } + + access(all) fun returnSameRef(_ ref: &Test.A?): &Test.A? { + return ref + } + `) + + require.NoError(t, err) +} diff --git a/runtime/tests/checker/entitlements_test.go b/runtime/tests/checker/entitlements_test.go index f95dab90fa..3a65ede707 100644 --- a/runtime/tests/checker/entitlements_test.go +++ b/runtime/tests/checker/entitlements_test.go @@ -969,7 +969,7 @@ func TestCheckBasicEntitlementMappingAccess(t *testing.T) { typeMismatchError.ExpectedType.QualifiedString(), ) assert.Equal(t, - "auth(M) &Int", + "auth(mapping M) &Int", typeMismatchError.ActualType.QualifiedString(), ) }) @@ -1310,7 +1310,7 @@ func TestCheckBasicEntitlementMappingAccess(t *testing.T) { var typeMismatchErr *sema.TypeMismatchError require.ErrorAs(t, errs[0], &typeMismatchErr) assert.Equal(t, - "auth(NM) &Int", + "auth(mapping NM) &Int", typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, @@ -1397,7 +1397,7 @@ func TestCheckBasicEntitlementMappingAccess(t *testing.T) { var typeMismatchErr *sema.TypeMismatchError require.ErrorAs(t, errs[0], &typeMismatchErr) assert.Equal(t, - "auth(NM) &Int", + "auth(mapping NM) &Int", typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, @@ -2995,98 +2995,114 @@ func TestCheckEntitlementInheritance(t *testing.T) { assert.NoError(t, err) }) - t.Run("attachment default function entitlements", func(t *testing.T) { + t.Run("attachment inherited default entitled function entitlements on base", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` entitlement E entitlement F - entitlement G - entitlement mapping M { - E -> F - } - entitlement mapping N { - G -> E - } struct interface I { - access(mapping M) fun foo(): auth(mapping M) &Int { - return &1 as auth(mapping M) &Int + access(E) fun foo(): auth(F) &Int { + return &1 as auth(F) &Int } } - struct S {} - access(mapping N) attachment A for S: I {} + struct interface I2: I {} + struct S { + access(E) fun foo() {} + } + access(all) attachment A for S: I2 {} fun test() { let s = attach A() to S() - let ref = &s as auth(G) &S - let i: auth(F) &Int = s[A]!.foo() + let ref = &s as auth(E) &S + let attachmentRef: auth(E) &A = s[A]! + let i: auth(F) &Int = attachmentRef.foo() } `) assert.NoError(t, err) }) - t.Run("attachment inherited default function entitlements", func(t *testing.T) { + t.Run("attachment inherited default mapped function entitlements on base", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` entitlement E entitlement F - entitlement G entitlement mapping M { E -> F } - entitlement mapping N { - G -> E - } struct interface I { access(mapping M) fun foo(): auth(mapping M) &Int { return &1 as auth(mapping M) &Int } } struct interface I2: I {} - struct S {} - access(mapping N) attachment A for S: I2 {} + struct S { + access(E) fun foo() {} + } + access(all) attachment A for S: I2 {} fun test() { let s = attach A() to S() - let ref = &s as auth(G) &S - let i: auth(F) &Int = s[A]!.foo() + let ref = &s as auth(E) &S + let attachmentRef: auth(E) &A = s[A]! + let i: auth(F) &Int = attachmentRef.foo() } `) assert.NoError(t, err) }) - t.Run("attachment default function entitlements no attachment mapping", func(t *testing.T) { + t.Run("attachment inherited default mapped function entitlements not on base", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` entitlement E entitlement F - entitlement G entitlement mapping M { E -> F } - entitlement mapping N { - G -> E - } struct interface I { access(mapping M) fun foo(): auth(mapping M) &Int { return &1 as auth(mapping M) &Int } } + struct interface I2: I {} struct S {} - attachment A for S: I {} + access(all) attachment A for S: I2 {} fun test() { let s = attach A() to S() - let ref = &s as auth(G) &S - let i: auth(F) &Int = s[A]!.foo() // mismatch + let ref = &s as auth(E) &S + let i: auth(F) &Int = s[A]!.foo() } `) errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentEntitlementError{}, errs[0]) + }) - // because A is declared with no mapping, all its references are unentitled - require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + t.Run("attachment inherited default entitled function entitlements not on base", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement E + entitlement F + struct interface I { + access(E) fun foo(): auth(F) &Int { + return &1 as auth(F) &Int + } + } + struct interface I2: I {} + struct S {} + access(all) attachment A for S: I2 {} + fun test() { + let s = attach A() to S() + let ref = &s as auth(E) &S + let i: auth(F) &Int = s[A]!.foo() + } + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentEntitlementError{}, errs[0]) }) } @@ -3583,7 +3599,7 @@ func TestCheckAttachmentEntitlementAccessAnnotation(t *testing.T) { t.Parallel() - t.Run("mapping allowed", func(t *testing.T) { + t.Run("mapping not allowed", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` @@ -3592,7 +3608,9 @@ func TestCheckAttachmentEntitlementAccessAnnotation(t *testing.T) { access(mapping E) attachment A for AnyStruct {} `) - assert.NoError(t, err) + errs := RequireCheckerErrors(t, err, 1) + + require.IsType(t, &sema.InvalidMappedEntitlementMemberError{}, errs[0]) }) t.Run("entitlement set not allowed", func(t *testing.T) { @@ -3611,7 +3629,7 @@ func TestCheckAttachmentEntitlementAccessAnnotation(t *testing.T) { require.IsType(t, &sema.InvalidEntitlementAccessError{}, errs[0]) }) - t.Run("mapping allowed in contract", func(t *testing.T) { + t.Run("mapping not allowed in contract", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` @@ -3624,12 +3642,14 @@ func TestCheckAttachmentEntitlementAccessAnnotation(t *testing.T) { X -> Y } access(mapping E) attachment A for AnyStruct { - access(Y) fun foo() {} + access(all) fun foo() {} } } `) - assert.NoError(t, err) + errs := RequireCheckerErrors(t, err, 1) + + require.IsType(t, &sema.InvalidMappedEntitlementMemberError{}, errs[0]) }) t.Run("entitlement set not allowed in contract", func(t *testing.T) { @@ -4604,11 +4624,10 @@ func TestCheckAttachmentEntitlements(t *testing.T) { _, err := ParseAndCheck(t, ` entitlement X entitlement Y - entitlement mapping M { - X -> Y + struct S { + access(Y) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(Y) fun entitled() { let a: auth(Y) &A = self let b: &S = base @@ -4629,7 +4648,7 @@ func TestCheckAttachmentEntitlements(t *testing.T) { typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, - "auth(Y) &A", + "&A", typeMismatchErr.ActualType.QualifiedString(), ) @@ -4650,12 +4669,8 @@ func TestCheckAttachmentEntitlements(t *testing.T) { _, err := ParseAndCheck(t, ` entitlement X entitlement Y - entitlement mapping M { - X -> Y - } struct S {} - access(mapping M) attachment A for S { - require entitlement X + access(all) attachment A for S { access(all) fun unentitled() { let b: &S = base } @@ -4674,43 +4689,30 @@ func TestCheckAttachmentEntitlements(t *testing.T) { typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, - "auth(X) &S", + "&S", typeMismatchErr.ActualType.QualifiedString(), ) }) - t.Run("base type with no requirements", func(t *testing.T) { + t.Run("base type with sufficient requirements", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` entitlement X - entitlement Y - entitlement mapping M { - X -> Y + struct S { + access(X) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(all) fun unentitled() { let b: &S = base } - access(all) fun entitled() { + access(X) fun entitled() { let b: auth(X) &S = base } } `) - errs := RequireCheckerErrors(t, err, 1) - - var typeMismatchErr *sema.TypeMismatchError - require.ErrorAs(t, errs[0], &typeMismatchErr) - assert.Equal(t, - typeMismatchErr.ExpectedType.QualifiedString(), - "auth(X) &S", - ) - assert.Equal(t, - "&S", - typeMismatchErr.ActualType.QualifiedString(), - ) + assert.NoError(t, err) }) t.Run("base type", func(t *testing.T) { @@ -4719,17 +4721,15 @@ func TestCheckAttachmentEntitlements(t *testing.T) { _, err := ParseAndCheck(t, ` entitlement X entitlement Y - entitlement mapping M { - X -> Y + struct S { + access(X) fun foo() {} + access(Y) fun bar() {} } - struct S {} - access(mapping M) attachment A for S { - require entitlement X - require entitlement Y + access(all) attachment A for S { access(all) fun unentitled() { let b: &S = base } - access(all) fun entitled() { + access(X, Y) fun entitled() { let b: auth(X, Y) &S = base } } @@ -4738,46 +4738,80 @@ func TestCheckAttachmentEntitlements(t *testing.T) { assert.NoError(t, err) }) - t.Run("multiple mappings", func(t *testing.T) { + t.Run("base and self in mapped functions", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` entitlement X entitlement Y - entitlement E - entitlement F entitlement mapping M { X -> Y - E -> F } struct S { - access(E, X) fun foo() {} + access(X) fun foo() {} } - access(mapping M) attachment A for S { - access(F, Y) fun entitled() { - let a: auth(F, Y) &A = self - } - access(all) fun unentitled() { - let a: auth(F, Y, E) &A = self // err + access(all) attachment A for S { + access(mapping M) fun foo(): auth(mapping M) &Int { + let b: auth(mapping M) &S = base + let a: auth(mapping M) &A = self + + return &1 } } `) errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }) + + t.Run("invalid base and self in mapped functions", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement X + entitlement Y + entitlement mapping M { + X -> Y + } + struct S { + access(X) fun foo() {} + } + access(all) attachment A for S { + access(mapping M) fun foo(): auth(mapping M) &Int { + let b: auth(Y) &S = base + let a: auth(Y) &A = self + + return &1 + } + } + `) + + errs := RequireCheckerErrors(t, err, 3) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) var typeMismatchErr *sema.TypeMismatchError - require.ErrorAs(t, errs[0], &typeMismatchErr) + require.ErrorAs(t, errs[1], &typeMismatchErr) + assert.Equal(t, + "auth(Y) &S", + typeMismatchErr.ExpectedType.QualifiedString(), + ) + assert.Equal(t, + "auth(mapping M) &S", + typeMismatchErr.ActualType.QualifiedString(), + ) + + require.ErrorAs(t, errs[2], &typeMismatchErr) assert.Equal(t, - "auth(F, Y, E) &A", + "auth(Y) &A", typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, - "auth(Y, F) &A", + "auth(mapping M) &A", typeMismatchErr.ActualType.QualifiedString(), ) }) - t.Run("missing in codomain", func(t *testing.T) { + t.Run("missing in S", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` @@ -4785,12 +4819,14 @@ func TestCheckAttachmentEntitlements(t *testing.T) { entitlement Y entitlement Z entitlement E - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(X) fun foo() {} + access(Y | Z) let bar: Int + init() { + self.bar = 1 + } } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(E) fun entitled() {} } `) @@ -4805,20 +4841,17 @@ func TestCheckAttachmentEntitlements(t *testing.T) { ) }) - t.Run("missing in codomain in set", func(t *testing.T) { + t.Run("missing in set", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - entitlement X entitlement Y entitlement Z entitlement E - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(Y, Z) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(Y | E | Z) fun entitled() {} } `) @@ -4840,11 +4873,10 @@ func TestCheckAttachmentEntitlements(t *testing.T) { entitlement X entitlement E entitlement F - entitlement mapping M { - E -> F + struct S { + access(F) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(F, X, E) fun entitled() {} } `) @@ -4875,9 +4907,9 @@ func TestCheckAttachmentEntitlements(t *testing.T) { X -> Y } struct S { - access(Y) fun foo() {} + access(X) fun foo() {} } - access(mapping M) attachment A for S { + access(all) attachment A for S { access(mapping M) let x: auth(mapping M) &S init() { self.x = &S() as auth(Y) &S @@ -4885,7 +4917,8 @@ func TestCheckAttachmentEntitlements(t *testing.T) { } `) - assert.NoError(t, err) + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) }) t.Run("access(all) decl", func(t *testing.T) { @@ -4964,12 +4997,10 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { entitlement X entitlement Y entitlement Z - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(Y, Z) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(Y, Z) fun foo() {} } let s = attach A() to S() @@ -4979,6 +5010,27 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { assert.NoError(t, err) }) + t.Run("basic owned fully entitled missing X", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement X + entitlement Y + entitlement Z + struct S { + access(Y, Z) fun foo() {} + } + access(all) attachment A for S { + access(Y, Z) fun foo() {} + } + let s = attach A() to S() + let a: auth(X, Y, Z) &A = s[A]! + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) + t.Run("basic owned intersection fully entitled", func(t *testing.T) { t.Parallel() @@ -4986,13 +5038,13 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { entitlement X entitlement Y entitlement Z - entitlement mapping M { - X -> Y - X -> Z + struct interface I { + access(Y, Z) fun foo() + } + struct S: I { + access(Y, Z) fun foo() {} } - struct interface I {} - struct S: I {} - access(mapping M) attachment A for I { + access(all) attachment A for I { access(Y, Z) fun foo() {} } let s: {I} = attach A() to S() @@ -5002,33 +5054,51 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { assert.NoError(t, err) }) - t.Run("basic reference mapping", func(t *testing.T) { + t.Run("basic owned intersection fully entitled missing X", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` entitlement X entitlement Y - entitlement E - entitlement F - entitlement mapping M { - X -> Y - E -> F + entitlement Z + struct interface I { + access(Y, Z) fun foo() } + struct S: I { + access(Y, Z) fun foo() {} + } + access(all) attachment A for I { + access(Y, Z) fun foo() {} + } + let s: {I} = attach A() to S() + let a: auth(X, Y, Z) &A = s[A]! + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) + + t.Run("basic reference mapping", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement X + entitlement E struct S { access(X, E) fun foo() {} } - access(mapping M) attachment A for S { - access(Y, F) fun foo() {} + access(all) attachment A for S { + access(X, E) fun foo() {} } let s = attach A() to S() - let yRef = &s as auth(X) &S - let fRef = &s as auth(E) &S + let xRef = &s as auth(X) &S + let eRef = &s as auth(E) &S let bothRef = &s as auth(X, E) &S - let a1: auth(Y) &A = yRef[A]! - let a2: auth(F) &A = fRef[A]! - let a3: auth(F) &A = yRef[A]! // err - let a4: auth(Y) &A = fRef[A]! // err - let a5: auth(Y, F) &A = bothRef[A]! + let a1: auth(X) &A = xRef[A]! + let a2: auth(E) &A = eRef[A]! + let a3: auth(E) &A = xRef[A]! // err + let a4: auth(X) &A = eRef[A]! // err + let a5: auth(X, E) &A = bothRef[A]! `) errs := RequireCheckerErrors(t, err, 2) @@ -5036,21 +5106,21 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { var typeMismatchErr *sema.TypeMismatchError require.ErrorAs(t, errs[0], &typeMismatchErr) assert.Equal(t, - "auth(F) &A?", + "auth(E) &A?", typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, - "auth(Y) &A?", + "auth(X) &A?", typeMismatchErr.ActualType.QualifiedString(), ) require.ErrorAs(t, errs[1], &typeMismatchErr) assert.Equal(t, - "auth(Y) &A?", + "auth(X) &A?", typeMismatchErr.ExpectedType.QualifiedString(), ) assert.Equal(t, - "auth(F) &A?", + "auth(E) &A?", typeMismatchErr.ActualType.QualifiedString(), ) }) @@ -5059,13 +5129,11 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - entitlement X entitlement Y - entitlement mapping M { - X -> Y + struct S { + access(Y) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(Y) fun foo() {} } let s = attach A() to S() @@ -5087,47 +5155,13 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { ) }) - t.Run("entitled access access(all) attachment", func(t *testing.T) { + t.Run("access(all) access access(all) attachment", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - entitlement X - entitlement Y - entitlement mapping M { - X -> Y - } - struct S { - access(X) fun foo() {} - } - access(all) attachment A for S { - access(all) fun foo() {} - } - let s = attach A() to S() - let ref = &s as auth(X) &S - let a1: auth(Y) &A = ref[A]! - `) + entitlement X - errs := RequireCheckerErrors(t, err, 1) - - var typeMismatchErr *sema.TypeMismatchError - require.ErrorAs(t, errs[0], &typeMismatchErr) - assert.Equal(t, - "auth(Y) &A?", - typeMismatchErr.ExpectedType.QualifiedString(), - ) - assert.Equal(t, - "&A?", - typeMismatchErr.ActualType.QualifiedString(), - ) - }) - - t.Run("access(all) access access(all) attachment", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement X - - entitlement Y + entitlement Y entitlement mapping M { X -> Y @@ -5156,41 +5190,65 @@ func TestCheckAttachmentAccessEntitlements(t *testing.T) { ) }) - t.Run("unrepresentable access mapping", func(t *testing.T) { + t.Run("mapped function in attachment", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - entitlement X - entitlement Y - entitlement Z - entitlement E - entitlement F - entitlement G + entitlement X + entitlement Y + entitlement mapping M { + X -> Y + } + struct S { + access(X) fun foo() {} + } + access(all) attachment A for S { + access(mapping M) fun foo(): auth(mapping M) &Int { + let s: auth(mapping M) &A = base[A]! - entitlement mapping M { - X -> Y - X -> Z - E -> F - E -> G - } + return &1 + } + } + `) - struct S { - access(X, E) fun foo() {} - } + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }) - access(mapping M) attachment A for S { - access(Y, Z, F, G) fun foo() {} - } + t.Run("invalid base attachment access in mapped function", func(t *testing.T) { + t.Parallel() - let s = attach A() to S() - let ref = (&s as auth(X) &S) as auth(X | E) &S - let a1 = ref[A]! + _, err := ParseAndCheck(t, ` + entitlement X + entitlement Y + entitlement mapping M { + X -> Y + } + struct S { + access(X) fun foo() {} + } + access(all) attachment A for S { + access(mapping M) fun foo(): auth(mapping M) &Int { + let s: auth(Y) &A? = base[A] + + return &1 + } + } `) errs := RequireCheckerErrors(t, err, 2) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) - require.IsType(t, &sema.UnrepresentableEntitlementMapOutputError{}, errs[0]) - require.IsType(t, &sema.InvalidTypeIndexingError{}, errs[1]) + var typeMismatchErr *sema.TypeMismatchError + require.ErrorAs(t, errs[1], &typeMismatchErr) + assert.Equal(t, + "auth(Y) &A?", + typeMismatchErr.ExpectedType.QualifiedString(), + ) + assert.Equal(t, + "auth(mapping M) &A?", + typeMismatchErr.ActualType.QualifiedString(), + ) }) } @@ -5782,400 +5840,6 @@ func TestCheckEntitledWriteAndMutateNotAllowed(t *testing.T) { }) } -func TestCheckAttachmentRequireEntitlements(t *testing.T) { - t.Parallel() - - t.Run("entitlements allowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement F - attachment A for AnyStruct { - require entitlement E - require entitlement F - } - `) - - assert.NoError(t, err) - }) - - t.Run("entitlement mapping disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement mapping M {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("event disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - event M() - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("struct disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - struct M {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("struct interface disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - struct interface M {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("resource disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - resource M {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("resource interface disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - resource interface M {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("attachment disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - attachment M for AnyResource {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("enum disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - enum M: UInt8 {} - attachment A for AnyStruct { - require entitlement E - require entitlement M - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("int disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - attachment A for AnyStruct { - require entitlement E - require entitlement Int - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.InvalidNonEntitlementRequirement{}, errs[0]) - }) - - t.Run("duplicates disallowed", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - attachment A for AnyStruct { - require entitlement E - require entitlement E - } - `) - - errs := RequireCheckerErrors(t, err, 1) - - require.IsType(t, &sema.DuplicateEntitlementRequirementError{}, errs[0]) - }) -} - -func TestCheckAttachProvidedEntitlements(t *testing.T) { - t.Parallel() - - t.Run("all provided", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement F - struct S {} - attachment A for S { - require entitlement E - require entitlement F - } - fun foo() { - let s = attach A() to S() with (E, F) - } - - `) - assert.NoError(t, err) - }) - - t.Run("extra provided", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement F - entitlement G - struct S {} - attachment A for S { - require entitlement E - require entitlement F - } - fun foo() { - let s = attach A() to S() with (E, F, G) - } - - `) - assert.NoError(t, err) - }) - - t.Run("one missing", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement F - struct S {} - attachment A for S { - require entitlement E - require entitlement F - } - fun foo() { - let s = attach A() to S() with (E) - } - - `) - errs := RequireCheckerErrors(t, err, 1) - - var requiredEntitlementNotProvidedErr *sema.RequiredEntitlementNotProvidedError - require.ErrorAs(t, errs[0], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "F", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - }) - - t.Run("one missing with extra provided", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement F - entitlement G - struct S {} - attachment A for S { - require entitlement E - require entitlement F - } - fun foo() { - let s = attach A() to S() with (E, G) - } - - `) - errs := RequireCheckerErrors(t, err, 1) - - var requiredEntitlementNotProvidedErr *sema.RequiredEntitlementNotProvidedError - require.ErrorAs(t, errs[0], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "F", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - }) - - t.Run("two missing", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement F - struct S {} - attachment A for S { - require entitlement E - require entitlement F - } - fun foo() { - let s = attach A() to S() - } - - `) - errs := RequireCheckerErrors(t, err, 2) - - var requiredEntitlementNotProvidedErr *sema.RequiredEntitlementNotProvidedError - require.ErrorAs(t, errs[0], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "E", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - - require.ErrorAs(t, errs[1], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "F", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - }) - - t.Run("mapping provided", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - entitlement mapping M {} - struct S {} - attachment A for S { - require entitlement E - } - fun foo() { - let s = attach A() to S() with (M) - } - - `) - errs := RequireCheckerErrors(t, err, 2) - - require.IsType(t, &sema.InvalidNonEntitlementProvidedError{}, errs[0]) - - var requiredEntitlementNotProvidedErr *sema.RequiredEntitlementNotProvidedError - require.ErrorAs(t, errs[1], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "E", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - }) - - t.Run("int provided", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - struct S {} - attachment A for S { - require entitlement E - } - fun foo() { - let s = attach A() to S() with (UInt8) - } - - `) - errs := RequireCheckerErrors(t, err, 2) - - require.IsType(t, &sema.InvalidNonEntitlementProvidedError{}, errs[0]) - - var requiredEntitlementNotProvidedErr *sema.RequiredEntitlementNotProvidedError - require.ErrorAs(t, errs[1], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "E", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - }) - - t.Run("struct provided", func(t *testing.T) { - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - struct S {} - attachment A for S { - require entitlement E - } - fun foo() { - let s = attach A() to S() with (S) - } - - `) - errs := RequireCheckerErrors(t, err, 2) - - require.IsType(t, &sema.InvalidNonEntitlementProvidedError{}, errs[0]) - - var requiredEntitlementNotProvidedErr *sema.RequiredEntitlementNotProvidedError - require.ErrorAs(t, errs[1], &requiredEntitlementNotProvidedErr) - assert.Equal(t, - "E", - requiredEntitlementNotProvidedErr.RequiredEntitlement.Identifier, - ) - }) -} - func TestCheckBuiltinEntitlements(t *testing.T) { t.Parallel() diff --git a/runtime/tests/checker/events_test.go b/runtime/tests/checker/events_test.go index 6d3ae0bae6..08469e5e0d 100644 --- a/runtime/tests/checker/events_test.go +++ b/runtime/tests/checker/events_test.go @@ -1202,30 +1202,6 @@ func TestCheckDefaultEventParamChecking(t *testing.T) { } } `) - errs := RequireCheckerErrors(t, err, 1) - assert.IsType(t, &sema.InvalidAccessError{}, errs[0]) - }) - - t.Run("attachment with entitled base allowed", func(t *testing.T) { - - t.Parallel() - - _, err := ParseAndCheck(t, ` - entitlement E - - attachment A for R { - require entitlement E - event ResourceDestroyed(name: Int = base.field) - } - - resource R { - access(E) let field : Int - - init() { - self.field = 3 - } - } - `) require.NoError(t, err) }) @@ -1236,11 +1212,7 @@ func TestCheckDefaultEventParamChecking(t *testing.T) { _, err := ParseAndCheck(t, ` entitlement E - entitlement mapping M { - E -> E - } - - access(mapping M) attachment A for R { + access(all) attachment A for R { access(E) let field : Int event ResourceDestroyed(name: Int = self.field) init() { @@ -1248,7 +1220,9 @@ func TestCheckDefaultEventParamChecking(t *testing.T) { } } - resource R {} + resource R { + access(E) fun foo() {} + } `) require.NoError(t, err) }) diff --git a/runtime/tests/interpreter/attachments_test.go b/runtime/tests/interpreter/attachments_test.go index 678634da7e..9426619add 100644 --- a/runtime/tests/interpreter/attachments_test.go +++ b/runtime/tests/interpreter/attachments_test.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/onflow/cadence/runtime/tests/checker" . "github.com/onflow/cadence/runtime/tests/utils" ) @@ -1949,6 +1950,146 @@ func TestInterpretAttachmentDefensiveCheck(t *testing.T) { }) } +func TestInterpretAttachmentSelfAccessMembers(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) resource R{ + access(all) fun baz() {} + } + access(all) attachment A for R{ + access(all) fun foo() {} + access(self) fun qux1() { + self.foo() + base.baz() + } + access(contract) fun qux2() { + self.foo() + base.baz() + } + access(account) fun qux3() { + self.foo() + base.baz() + } + access(all) fun bar() { + self.qux1() + self.qux2() + self.qux3() + } + } + + access(all) fun main() { + var r <- attach A() to <- create R() + r[A]!.bar() + destroy r + } + `) + + _, err := inter.Invoke("main") + require.NoError(t, err) +} + +func TestInterpretAttachmentMappedMembers(t *testing.T) { + + t.Parallel() + + t.Run("mapped self cast", func(t *testing.T) { + + t.Parallel() + + inter, _ := parseCheckAndInterpretWithOptions(t, ` + entitlement E + entitlement F + entitlement G + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(E) fun foo() {} + access(F) fun bar() {} + } + access(all) attachment A for R { + access(F) let x: Int + init() { + self.x = 3 + } + access(mapping M) fun foo(): auth(mapping M) &Int { + if let concreteSelf = self as? auth(F) &A { + return &concreteSelf.x + } + return &1 + } + } + fun test(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, ParseCheckAndInterpretOptions{ + HandleCheckerError: func(err error) { + errs := checker.RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + + _, err := inter.Invoke("test") + require.ErrorAs(t, err, &interpreter.ValueTransferTypeError{}) + }) + + t.Run("mapped base cast", func(t *testing.T) { + + t.Parallel() + + inter, _ := parseCheckAndInterpretWithOptions(t, ` + entitlement E + entitlement F + entitlement mapping M { + E -> F + } + + access(all) resource R { + access(F) let x: Int + init() { + self.x = 3 + } + access(E) fun bar() {} + } + access(all) attachment A for R { + access(mapping M) fun foo(): auth(mapping M) &Int { + if let concreteBase = base as? auth(F) &R { + return &concreteBase.x + } + return &1 + } + } + fun test(): &Int { + let r <- attach A() to <- create R() + let a = r[A]! + let i = a.foo() + destroy r + return i + } + `, ParseCheckAndInterpretOptions{ + HandleCheckerError: func(err error) { + errs := checker.RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAttachmentMappedEntitlementMemberError{}, errs[0]) + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + + _, err := inter.Invoke("test") + require.ErrorAs(t, err, &interpreter.ValueTransferTypeError{}) + }) +} + func TestInterpretForEachAttachment(t *testing.T) { t.Parallel() @@ -2046,32 +2187,23 @@ func TestInterpretForEachAttachment(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - entitlement E entitlement F - entitlement X entitlement Y - entitlement mapping M { - E -> F - } - entitlement mapping N { - X -> Y - } - entitlement mapping O { - E -> Y + struct S { + access(F, Y) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(F) fun foo(_ x: Int): Int { return 7 + x } } - access(mapping N) attachment B for S { + access(all) attachment B for S { access(Y) fun foo(): Int { return 10 } } - access(mapping O) attachment C for S { + access(all) attachment C for S { access(Y) fun foo(_ x: Int): Int { return 8 + x } } fun test(): Int { var s = attach C() to attach B() to attach A() to S() - let ref = &s as auth(E) &S + let ref = &s as auth(F, Y) &S var i = 0 ref.forEachAttachment(fun(attachmentRef: &AnyStructAttachment) { if let a = attachmentRef as? auth(F) &A { @@ -2093,6 +2225,49 @@ func TestInterpretForEachAttachment(t *testing.T) { AssertValuesEqual(t, inter, interpreter.NewUnmeteredIntValueFromInt64(0), value) }) + t.Run("bound function", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement F + + access(all) struct S { + access(F) let x: Int + init() { + self.x = 3 + } + } + access(all) attachment A for S { + access(F) var funcPtr: fun(): auth(F) ∬ + init() { + self.funcPtr = self.foo + } + access(F) fun foo(): auth(F) &Int { + return &base.x + } + } + fun test(): &Int { + let r = attach A() to S() + let rRef = &r as auth(F) &S + let a = rRef[A]! + let i = a.foo() + return i + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + require.IsType(t, &interpreter.EphemeralReferenceValue{}, value) + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredIntValueFromInt64(3), + value.(*interpreter.EphemeralReferenceValue).Value, + ) + }) + t.Run("access fields", func(t *testing.T) { t.Parallel() diff --git a/runtime/tests/interpreter/entitlements_test.go b/runtime/tests/interpreter/entitlements_test.go index 09e1740a01..4c0833e2c7 100644 --- a/runtime/tests/interpreter/entitlements_test.go +++ b/runtime/tests/interpreter/entitlements_test.go @@ -2166,15 +2166,12 @@ func TestInterpretEntitledAttachments(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - entitlement X entitlement Y entitlement Z - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(Y, Z) fun foo() {} } - struct S {} - access(mapping M) attachment A for S {} + access(all) attachment A for S {} fun test(): auth(Y, Z) &A { let s = attach A() to S() return s[A]! @@ -2200,20 +2197,17 @@ func TestInterpretEntitledAttachments(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - entitlement X entitlement Y entitlement Z - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(Y | Z) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { - access(Y | Z) fun entitled(): auth(Y, Z) &A { + access(all) attachment A for S { + access(Y | Z) fun entitled(): auth(Y | Z) &A { return self } } - fun test(): auth(Y, Z) &A { + fun test(): auth(Y | Z) &A { let s = attach A() to S() return s[A]!.entitled() } @@ -2228,7 +2222,7 @@ func TestInterpretEntitledAttachments(t *testing.T) { nil, func() []common.TypeID { return []common.TypeID{"S.test.Y", "S.test.Z"} }, 2, - sema.Conjunction, + sema.Disjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) @@ -2238,20 +2232,17 @@ func TestInterpretEntitledAttachments(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - entitlement X entitlement Y entitlement Z - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(Y | Z) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { - access(Y | Z) fun entitled(): &S { + access(all) attachment A for S { + access(Y | Z) fun entitled(): auth(Y | Z) &S { return base } } - fun test(): &S { + fun test(): auth(Y | Z) &S { let s = attach A() to S() return s[A]!.entitled() } @@ -2262,32 +2253,70 @@ func TestInterpretEntitledAttachments(t *testing.T) { require.True( t, - interpreter.UnauthorizedAccess.Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), + interpreter.NewEntitlementSetAuthorization( + nil, + func() []common.TypeID { return []common.TypeID{"S.test.Y", "S.test.Z"} }, + 2, + sema.Disjunction, + ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) - t.Run("basic call return authorized base", func(t *testing.T) { + t.Run("basic call unbound method", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - entitlement X entitlement Y entitlement Z - entitlement mapping M { - X -> Y - X -> Z + struct S { + access(Y | Z) fun foo() {} + } + access(all) attachment A for S { + access(Y | Z) fun entitled(): auth(Y | Z) &A { + return self + } + } + fun test(): auth(Y | Z) &A { + let s = attach A() to S() + let foo = s[A]!.entitled + return foo() + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + require.True( + t, + interpreter.NewEntitlementSetAuthorization( + nil, + func() []common.TypeID { return []common.TypeID{"S.test.Y", "S.test.Z"} }, + 2, + sema.Disjunction, + ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), + ) + }) + + t.Run("basic call unbound method base", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement Y + entitlement Z + struct S { + access(Y | Z) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { - require entitlement X - access(Y | Z) fun entitled(): auth(X) &S { + access(all) attachment A for S { + access(Y | Z) fun entitled(): auth(Y | Z) &S { return base } } - fun test(): auth(X) &S { - let s = attach A() to S() with (X) - return s[A]!.entitled() + fun test(): auth(Y | Z) &S { + let s = attach A() to S() + let foo = s[A]!.entitled + return foo() } `) @@ -2298,9 +2327,9 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.X"} }, - 1, - sema.Conjunction, + func() []common.TypeID { return []common.TypeID{"S.test.Y", "S.test.Z"} }, + 2, + sema.Disjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) @@ -2311,21 +2340,13 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter := parseCheckAndInterpret(t, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + struct S { + access(X, E, G) fun foo() {} } - struct S {} - access(mapping M) attachment A for S {} - fun test(): auth(F, G) &A { + access(all) attachment A for S {} + fun test(): auth(E) &A { let s = attach A() to S() let ref = &s as auth(E) &S return ref[A]! @@ -2339,8 +2360,8 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.F", "S.test.G"} }, - 2, + func() []common.TypeID { return []common.TypeID{"S.test.E"} }, + 1, sema.Conjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) @@ -2352,25 +2373,17 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter := parseCheckAndInterpret(t, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + struct S { + access(X, E, G) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { - access(F | Z) fun entitled(): auth(Y, Z, F, G) &A { + access(all) attachment A for S { + access(E | G) fun entitled(): auth(E | G) &A { return self } } - fun test(): auth(Y, Z, F, G) &A { + fun test(): auth(E | G) &A { let s = attach A() to S() let ref = &s as auth(E) &S return ref[A]!.entitled() @@ -2384,40 +2397,32 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.F", "S.test.G", "S.test.Y", "S.test.Z"} }, - 4, - sema.Conjunction, + func() []common.TypeID { return []common.TypeID{"S.test.E", "S.test.G"} }, + 2, + sema.Disjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) - t.Run("basic ref call return base", func(t *testing.T) { + t.Run("basic ref call conjunction", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + struct S { + access(X, E, G) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { - access(F | Z) fun entitled(): &S { - return base + access(all) attachment A for S { + access(E) fun entitled(): auth(E) &A { + return self } } - fun test(): &S { + fun test(): auth(E) &A { let s = attach A() to S() - let ref = &s as auth(E) &S + let ref = &s as auth(E, G) &S return ref[A]!.entitled() } `) @@ -2427,37 +2432,33 @@ func TestInterpretEntitledAttachments(t *testing.T) { require.True( t, - interpreter.UnauthorizedAccess.Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), + interpreter.NewEntitlementSetAuthorization( + nil, + func() []common.TypeID { return []common.TypeID{"S.test.E"} }, + 1, + sema.Conjunction, + ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) - t.Run("basic ref call return entitled base", func(t *testing.T) { + t.Run("basic ref call return base", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + struct S { + access(X, E, G) fun foo() {} } - struct S {} - access(mapping M) attachment A for S { - require entitlement E - access(F | Z) fun entitled(): auth(E) &S { + access(all) attachment A for S { + access(X | E) fun entitled(): auth(X | E) &S { return base } } - fun test(): auth(E) &S { - let s = attach A() to S() with(E) + fun test(): auth(X | E) &S { + let s = attach A() to S() let ref = &s as auth(E) &S return ref[A]!.entitled() } @@ -2470,9 +2471,9 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.E"} }, - 1, - sema.Conjunction, + func() []common.TypeID { return []common.TypeID{"S.test.E", "S.test.X"} }, + 2, + sema.Disjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) @@ -2483,24 +2484,18 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter := parseCheckAndInterpret(t, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + struct S: I { + access(X, E) fun foo() {} } - struct S: I {} - struct interface I {} - access(mapping M) attachment A for I {} - fun test(): auth(F, G) &A { + struct interface I { + access(X, E, G) fun foo() + } + access(all) attachment A for I {} + fun test(): auth(G) &A { let s = attach A() to S() - let ref = &s as auth(E) &{I} + let ref = &s as auth(G) &{I} return ref[A]! } `) @@ -2512,8 +2507,8 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.F", "S.test.G"} }, - 2, + func() []common.TypeID { return []common.TypeID{"S.test.G"} }, + 1, sema.Conjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) @@ -2527,21 +2522,13 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter, _ := testAccount(t, address, true, nil, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + resource R { + access(X, E, G) fun foo() {} } - resource R {} - access(mapping M) attachment A for R {} - fun test(): auth(F, G) &A { + access(all) attachment A for R {} + fun test(): auth(E) &A { let r <- attach A() to <-create R() account.storage.save(<-r, to: /storage/foo) let ref = account.storage.borrow(from: /storage/foo)! @@ -2558,8 +2545,8 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.F", "S.test.G"} }, - 2, + func() []common.TypeID { return []common.TypeID{"S.test.E"} }, + 1, sema.Conjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) @@ -2573,28 +2560,20 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter, _ := testAccount(t, address, true, nil, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + resource R { + access(X, E, G) fun foo() {} } - resource R {} - access(mapping M) attachment A for R { - access(F | Z) fun entitled(): auth(F, G, Y, Z) &A { + access(all) attachment A for R { + access(X, E) fun entitled(): auth(X, E) &A { return self } } - fun test(): auth(F, G, Y, Z) &A { + fun test(): auth(X, E) &A { let r <- attach A() to <-create R() account.storage.save(<-r, to: /storage/foo) - let ref = account.storage.borrow(from: /storage/foo)! + let ref = account.storage.borrow(from: /storage/foo)! return ref[A]!.entitled() } `, sema.Config{ @@ -2608,8 +2587,8 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.F", "S.test.G", "S.test.Y", "S.test.Z"} }, - 4, + func() []common.TypeID { return []common.TypeID{"S.test.X", "S.test.E"} }, + 2, sema.Conjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) @@ -2623,29 +2602,20 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter, _ := testAccount(t, address, true, nil, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + resource R { + access(X, E, G) fun foo() {} } - resource R {} - access(mapping M) attachment A for R { - require entitlement X - access(F | Z) fun entitled(): auth(X) &R { + access(all) attachment A for R { + access(X) fun entitled(): auth(X) &R { return base } } fun test(): auth(X) &R { - let r <- attach A() to <-create R() with (X) + let r <- attach A() to <-create R() account.storage.save(<-r, to: /storage/foo) - let ref = account.storage.borrow(from: /storage/foo)! + let ref = account.storage.borrow(from: /storage/foo)! return ref[A]!.entitled() } `, sema.Config{ @@ -2672,29 +2642,19 @@ func TestInterpretEntitledAttachments(t *testing.T) { inter := parseCheckAndInterpret(t, ` entitlement X - entitlement Y - entitlement Z entitlement E - entitlement F entitlement G - entitlement mapping M { - X -> Y - X -> Z - E -> F - X -> F - E -> G + resource R { + access(X, E, G) fun foo() {} } - resource R {} - access(mapping M) attachment A for R { - require entitlement E - require entitlement X + access(all) attachment A for R { init() { - let x = self as! auth(Y, Z, F, G) &A - let y = base as! auth(X, E) &R + let x = self as! auth(X, E, G) &A + let y = base as! auth(X, E, G) &R } } fun test() { - let r <- attach A() to <-create R() with (E, X) + let r <- attach A() to <-create R() destroy r } `) @@ -2703,28 +2663,20 @@ func TestInterpretEntitledAttachments(t *testing.T) { require.NoError(t, err) }) - t.Run("composed mapped attachment access", func(t *testing.T) { + t.Run("composed attachment access", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - entitlement X - entitlement Y entitlement Z - entitlement E - entitlement F - entitlement G - entitlement mapping M { - X -> Y - E -> F + entitlement Y + struct S { + access(Y) fun foo() {} } - entitlement mapping N { - Z -> X - G -> F + struct T { + access(Z) fun foo() {} } - struct S {} - struct T {} - access(mapping M) attachment A for S { + access(all) attachment A for S { access(self) let t: T init(t: T) { self.t = t @@ -2733,10 +2685,10 @@ func TestInterpretEntitledAttachments(t *testing.T) { return &self.t as auth(Z) &T } } - access(mapping N) attachment B for T {} - fun test(): auth(X) &B { + access(all) attachment B for T {} + fun test(): auth(Z) &B { let s = attach A(t: attach B() to T()) to S() - let ref = &s as auth(X) &S + let ref = &s as auth(Y) &S return ref[A]!.getT()[B]! } `) @@ -2748,7 +2700,7 @@ func TestInterpretEntitledAttachments(t *testing.T) { t, interpreter.NewEntitlementSetAuthorization( nil, - func() []common.TypeID { return []common.TypeID{"S.test.X"} }, + func() []common.TypeID { return []common.TypeID{"S.test.Z"} }, 1, sema.Conjunction, ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization),