diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al new file mode 100644 index 0000000..48b7185 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al @@ -0,0 +1,47 @@ +codeunit 50000 MyCodeunit +{ + + trigger OnRun() + var + MyTable: Record MyTable; + begin + // read + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|MyTable.Get(1);|] + [|if MyTable.IsEmpty() then;|] + + // insert + [|MyTable.Insert();|] + + // modify + [|MyTable.Modify();|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Rename(1);|] + + // delete + [|MyTable.Delete();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al new file mode 100644 index 0000000..d8ad897 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al @@ -0,0 +1,33 @@ +query 50000 MyQuery +{ + + elements + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al new file mode 100644 index 0000000..5f0dbbe --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al @@ -0,0 +1,34 @@ +report 50000 MyReport +{ + ProcessingOnly = true; + + dataset + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al new file mode 100644 index 0000000..d76362f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al @@ -0,0 +1,93 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + Permissions = tabledata MyTable = r; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} +xmlport 50001 MyXmlport2 +{ + Direction = Export; + Permissions = tabledata MyTable = m; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} +xmlport 50002 MyXmlport3 +{ + Direction = Both; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} + +xmlport 50003 MyXmlport4 +{ + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al new file mode 100644 index 0000000..151fad6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al @@ -0,0 +1,10 @@ +codeunit 50000 MyCodeunit +{ + trigger OnRun() + var + Integer: Record Integer; + begin + [|Integer.FindFirst();|] + end; +} + diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al new file mode 100644 index 0000000..1f61cb9 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al @@ -0,0 +1,31 @@ +codeunit 50000 Test +{ + Permissions = + tabledata MyTableOne = r, + tabledata MyTableTwo = i; + + procedure Test() + var + MyTableOne: Record MyTableOne; + MyTableTwo: Record MyTableTwo; + begin + [|MyTableOne.FindFirst();|] + [|MyTableTwo.Insert();|] + end; +} + +table 50000 MyTableOne +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50001 MyTableTwo +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al new file mode 100644 index 0000000..c0327d4 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al @@ -0,0 +1,25 @@ +pageextension 50000 MyPageExtension extends MyPage +{ + trigger OnOpenPage() + var + MyTable: Record MyTable; + begin + [|MyTable.FindFirst();|] + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Delete();|] + end; +} + +page 50000 MyPage +{ + SourceTable = MyTable; +} + +table 50000 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al new file mode 100644 index 0000000..4c6fa81 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al @@ -0,0 +1,52 @@ +page 50000 MyPage +{ + PageType = Card; + ApplicationArea = All; + UsageCategory = Administration; + SourceTable = MyTable; + + layout + { + area(Content) + { + group(GroupName) + { + field(Name; MyField) + { + ApplicationArea = All; + + } + } + } + } + + trigger OnOpenPage() + var + MyTable: Record MyTable; + begin + [|Rec.FindFirst();|] + [|MyTable.FindFirst();|] + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Delete();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al new file mode 100644 index 0000000..c95f832 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al @@ -0,0 +1,32 @@ +codeunit 50000 CommentTestCodeunit +{ + Permissions = + tabledata MyTableOne = r, + // single line comment + tabledata MyTableTwo = r; + + trigger OnRun() + var + MyTableOne: Record MyTableOne; + MyTableTwo: Record MyTableTwo; + begin + MyTableOne.FindFirst(); + [|MyTableTwo.FindFirst();|] + end; +} + +table 50000 MyTableOne +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50001 MyTableTwo +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al new file mode 100644 index 0000000..c3e587f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al @@ -0,0 +1,45 @@ +codeunit 50000 PragmaTestCodeunit +{ + // example from issue #923 (pragma in permissions property) + + Permissions = + tabledata MyTableOne = r, +#pragma warning disable AA0123 + tabledata MyTableTwo = r, +#pragma warning restore AA0123 + tabledata MyTableThree = r; + + trigger OnRun() + var + MyTableOne: Record MyTableOne; + MyTableTwo: Record MyTableTwo; + MyTableThree: Record MyTableThree; + begin + MyTableOne.FindFirst(); + [|MyTableTwo.FindFirst();|] + [|MyTableThree.FindFirst();|] + end; +} +table 50000 MyTableOne +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50001 MyTableTwo +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50002 MyTableThree +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al new file mode 100644 index 0000000..6fe0ee5 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al @@ -0,0 +1,19 @@ +codeunit 50000 MyCodeunit +{ + Permissions = tabledata 50000 = r; + + trigger OnRun() + var + MyTable: Record MyTable; + begin + [|MyTable.FindFirst();|] + end; +} + +table 50000 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al new file mode 100644 index 0000000..14d4e16 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al @@ -0,0 +1,41 @@ +codeunit 50000 MyCodeunit +{ + + [InherentPermissions(PermissionObjectType::TableData, Database::MyTable, 'RIMD')] + local procedure Test() + var + MyTable: Record MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al new file mode 100644 index 0000000..87ae3c6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al @@ -0,0 +1,41 @@ +codeunit 50000 MyCodeunit +{ + + trigger OnRun() + var + MyTable: Record MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = rimd; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al new file mode 100644 index 0000000..0205912 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al @@ -0,0 +1,42 @@ +codeunit 50000 MyCodeunit +{ + + Permissions = tabledata MyTable = rimd; + + trigger OnRun() + var + MyTable: Record MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al new file mode 100644 index 0000000..f55d0b6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al @@ -0,0 +1,44 @@ +namespace MyNameSpace; + +codeunit 50000 MyCodeunit +{ + + Permissions = tabledata MyNameSpace.MyTable = rimd; + + trigger OnRun() + var + MyTable: Record MyNameSpace.MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al new file mode 100644 index 0000000..bc2aee6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al @@ -0,0 +1,33 @@ +query 50000 MyQuery +{ + elements + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = r; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al new file mode 100644 index 0000000..67648ae --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al @@ -0,0 +1,34 @@ +query 50000 MyQuery +{ + Permissions = tabledata MyTable = r; + + elements + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al new file mode 100644 index 0000000..56d50b3 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al @@ -0,0 +1,35 @@ +report 50000 MyReport +{ + ProcessingOnly = true; + + dataset + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = r; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al new file mode 100644 index 0000000..1dbd82b --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al @@ -0,0 +1,35 @@ +report 50000 MyReport +{ + ProcessingOnly = true; + Permissions = tabledata MyTable = r; + + dataset + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al new file mode 100644 index 0000000..ed61e05 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al @@ -0,0 +1,41 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + AutoReplace = false; // modify permissions + AutoSave = false; // insert permissions + AutoUpdate = false; //modify permissions + + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al new file mode 100644 index 0000000..d7076ed --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al @@ -0,0 +1,92 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50001 MyXmlport2 +{ + Direction = Export; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50002 MyXmlport3 +{ + Direction = Both; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +xmlport 50003 MyXmlport4 +{ + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = rim; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al new file mode 100644 index 0000000..c385b2d --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al @@ -0,0 +1,96 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + Permissions = tabledata MyTable = im; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50001 MyXmlport2 +{ + Direction = Export; + Permissions = tabledata MyTable = r; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50002 MyXmlport3 +{ + Direction = Both; + Permissions = tabledata MyTable = rim; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +xmlport 50003 MyXmlport4 +{ + + Permissions = tabledata MyTable = rim; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs new file mode 100644 index 0000000..77b1cea --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs @@ -0,0 +1,92 @@ +using ALCops.ApplicationCop.CodeFixes; +using RoslynTestKit; + +namespace ALCops.ApplicationCop.Test +{ + public class TableDataAccessRequiresPermissions : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private static readonly Analyzers.TableDataAccessRequiresPermissions _analyzer = new(); + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(TableDataAccessRequiresPermissions))); + } + + [Test] + [TestCase("ProcedureCalls")] + [TestCase("XmlPorts")] + [TestCase("Queries")] + [TestCase("Reports")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.TableDataAccessRequiresPermissions); + } + + [Test] + [TestCase("ProcedureCallsPermissionsProperty")] + [TestCase("XmlPortPermissionsProperty")] + [TestCase("QueryPermissionsProperty")] + [TestCase("XmlPortInherentPermissions")] + [TestCase("QueryInherentPermissions")] + [TestCase("ReportPermissionsProperty")] + [TestCase("ReportInherentPermissions")] + [TestCase("ProcedureCallsInherentPermissionsProperty")] + [TestCase("ProcedureCallsInherentPermissionsAttribute")] + [TestCase("PageSourceTable")] + [TestCase("PageExtensionSourceTable")] + [TestCase("ProcedureCallsPermissionsPropertyFullyQualified")] + // [TestCase("IntegerTable")] + [TestCase("XMLPortWithTableElementProps")] + [TestCase("PermissionsAsObjectId")] + [TestCase("PermissionPropertyWithPragma")] + [TestCase("PermissionPropertyWithComment")] + [TestCase("MultiplePermissionsDifferentType")] + public async Task NoDiagnostic(string testCase) + { + SkipTestIfVersionIsTooLow( + ["PageExtensionSourceTable"], + testCase, + "13.0", + "No support for tableextensions when target itself is already declared in the same module"); + + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.TableDataAccessRequiresPermissions); + } + + // [Test] + // [TestCase("PageRunModelPageIdentifierAndRecord")] + // [TestCase("PageRunModelPageIdentifierAndRecordWithPageFIeld")] + // [TestCase("PageRunPageIdentifierAndRecord")] + // [TestCase("PageRunPageIdentifierAndRecordWithPageField")] + // [TestCase("PageRunZeroIdentifierAndRecord")] + // public async Task HasFix(string testCase) + // { + // var currentCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "current.al")) + // .ConfigureAwait(false); + + // var expectedCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "expected.al")) + // .ConfigureAwait(false); + + // var fixture = RoslynFixtureFactory.Create( + // new CodeFixTestFixtureConfig + // { + // AdditionalAnalyzers = [_analyzer] + // }); + + // fixture.TestCodeFix(currentCode, expectedCode, DiagnosticDescriptors.NotBlankRequiredOnPrimaryKeyField); + // } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs index d14da45..551aaf8 100644 --- a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs +++ b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs @@ -759,6 +759,42 @@ internal static string RunPageImplementPageManagementTitle { } } + /// + /// Looks up a localized string similar to ALCops: Add missing permissions. + /// + internal static string TableDataAccessRequiresPermissionsCodeAction { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsCodeAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code that reads from or writes to table data must declare the required Permissions on the containing object. This ensures the code can run using indirect permissions, which is required when table access is restricted by the user's license. . + /// + internal static string TableDataAccessRequiresPermissionsDescription { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The object does not declare permission "{0}" for tabledata "{1}". + /// + internal static string TableDataAccessRequiresPermissionsMessageFormat { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Table data access requires explicit object permissions. + /// + internal static string TableDataAccessRequiresPermissionsTitle { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to In projects where company data isolation matters, table objects must explicitly define the DataPerCompany property. Relying on implicit defaults makes the data scope unclear and can lead to incorrect assumptions about whether data is shared across companies or stored per company.. /// diff --git a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx index d9d7f39..9ab6f5f 100644 --- a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx +++ b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx @@ -351,6 +351,18 @@ ALCops: Refactor to use "Page Management" codeunit + + Table data access requires explicit object permissions + + + The object does not declare permission "{0}" for tabledata "{1}" + + + Code that reads from or writes to table data must declare the required Permissions on the containing object. This ensures the code can run using indirect permissions, which is required when table access is restricted by the user's license. + + + ALCops: Add missing permissions + DataPerCompany must be explicitly set on table objects diff --git a/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs b/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs new file mode 100644 index 0000000..ccce8f5 --- /dev/null +++ b/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs @@ -0,0 +1,292 @@ +using System.Collections.Immutable; +using ALCops.Common.Extensions; +using ALCops.Common.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; + +namespace ALCops.ApplicationCop.Analyzers; + +[DiagnosticAnalyzer] +public class TableDataAccessRequiresPermissions : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.TableDataAccessRequiresPermissions); + + private static readonly ImmutableDictionary MethodPermissionMap = + ImmutableDictionary.CreateRange( + StringComparer.OrdinalIgnoreCase, + [ + // read + new KeyValuePair("Find", 'r'), + new KeyValuePair("FindFirst", 'r'), + new KeyValuePair("FindLast", 'r'), + new KeyValuePair("FindSet", 'r'), + new KeyValuePair("Get", 'r'), + new KeyValuePair("IsEmpty", 'r'), + + // insert + new KeyValuePair("Insert", 'i'), + + // modify + new KeyValuePair("Modify", 'm'), + new KeyValuePair("ModifyAll", 'm'), + new KeyValuePair("Rename", 'm'), + + // delete + new KeyValuePair("Delete", 'd'), + new KeyValuePair("DeleteAll", 'd'), + ]); + + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction( + AnalyzeInvocation, + EnumProvider.OperationKind.InvocationExpression); + + context.RegisterSymbolAction( + CheckReportDataItemObjectPermission, + EnumProvider.SymbolKind.ReportDataItem); + + context.RegisterSymbolAction( + CheckQueryDataItemObjectPermission, + EnumProvider.SymbolKind.QueryDataItem); + + context.RegisterSymbolAction( + CheckXmlportNodeObjectPermission, + EnumProvider.SymbolKind.XmlPortNode); + } + + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Operation is not IInvocationExpression invocation) + return; + + if (invocation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod) + return; + + if (invocation.Instance?.Type is not IRecordTypeSymbol recordType || recordType.Temporary) + return; + + if (recordType.OriginalDefinition is not ITableTypeSymbol tableType) + return; + + if (TargetTableIsPageSourceTable(ctx, tableType)) + return; + + var permission = GetRequiredPermission(invocation.TargetMethod.Name); + if (permission is null) + return; + + var inherentPermissions = GetInherentPermissionsAttributes(ctx); + if (ProcedureHasInherentPermission(inherentPermissions, recordType, permission.Value)) + return; + + var objectPermissions = ctx.ContainingSymbol + .GetContainingApplicationObjectTypeSymbol() + ?.GetProperty(EnumProvider.PropertyKind.Permissions); + + CheckProcedureInvocation( + objectPermissions, + recordType, + permission.Value, + ctx.ReportDiagnostic, + invocation.Syntax.GetLocation(), + tableType); + } + + + + private void CheckXmlportNodeObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete()) + return; + + if (((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).SourceTypeKind != EnumProvider.XmlPortSourceTypeKind.Table) return; + + string direction = ""; + + IXmlPortTypeSymbol xmlPort = (IXmlPortTypeSymbol)ctx.Symbol.GetContainingObjectTypeSymbol(); + + IPropertySymbol? objectPermissions = xmlPort.GetProperty(EnumProvider.PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).GetTypeSymbol(); + var directionProperty = xmlPort.Properties.FirstOrDefault(property => property.Name == "Direction"); + + if (directionProperty is null) + direction = EnumProvider.DirectionKind.Both.ToString(); + else + direction = directionProperty.ValueText; + + bool? AutoReplace = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == EnumProvider.PropertyKind.AutoReplace)?.Value; // modify permissions + bool? AutoUpdate = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == EnumProvider.PropertyKind.AutoUpdate)?.Value; // modify permissions + bool? AutoSave = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == EnumProvider.PropertyKind.AutoSave)?.Value; // insert permissions + + AutoReplace ??= true; + AutoUpdate ??= true; + AutoSave ??= true; + + direction = direction.ToLowerInvariant(); + + if (direction == "import" || direction == "both") + { + if (AutoReplace == true || AutoUpdate == true) + CheckProcedureInvocation(objectPermissions, targetSymbol, 'm', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + if (AutoSave == true) + CheckProcedureInvocation(objectPermissions, targetSymbol, 'i', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + if (direction == "export" || direction == "both") + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + + private void CheckQueryDataItemObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete()) return; + + IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(EnumProvider.PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IQueryDataItemSymbol)ctx.Symbol).GetTypeSymbol(); + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + + private void CheckReportDataItemObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete()) return; + if (ctx.Symbol.GetBooleanPropertyValue(EnumProvider.PropertyKind.UseTemporary) == true) return; + if (((ITableTypeSymbol)((IRecordTypeSymbol)((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol()).OriginalDefinition).TableType == EnumProvider.TableTypeKind.Temporary) return; + + IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(EnumProvider.PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol(); + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + + + + private static bool ProcedureHasInherentPermission(IEnumerable inherentPermissions, ITypeSymbol variableType, char requestedPermission) + { + //[InherentPermissions(PermissionObjectType::TableData, Database::"SomeTable", 'r'),InherentPermissions(PermissionObjectType::TableData, Database::"SomeOtherTable", 'w')] + + if (inherentPermissions is null || inherentPermissions.Count() == 0) return false; + + foreach (var inherentPermission in inherentPermissions) + { + var inherentPermissionAsString = inherentPermission.DeclaringSyntaxReference?.GetSyntax().ToString(); + + var permissions = inherentPermissionAsString?.Split(new[] { '[', ']', '(', ')', ',' }, StringSplitOptions.RemoveEmptyEntries); + if (permissions?[1].Trim() != "PermissionObjectType::TableData") continue; + + var typeAndObjectName = permissions[2].Trim(); + var permissionValue = permissions[3].Trim().Trim(new[] { '\'', ' ' }).ToLowerInvariant(); + + var typeParts = typeAndObjectName.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries); + if (typeParts.Length < 2) continue; + + var objectName = typeParts[1].Trim().Trim('"'); + if (objectName.ToLowerInvariant() != variableType.Name.ToLowerInvariant()) + if (objectName.UnquoteIdentifier().ToLowerInvariant() != (variableType.OriginalDefinition.ContainingNamespace?.QualifiedName.ToLowerInvariant() + "." + variableType.Name.ToLowerInvariant())) + continue; + + if (permissionValue.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) + { + return true; + } + } + return false; + } + + private void CheckProcedureInvocation(IPropertySymbol? objectPermissions, ITypeSymbol variableType, char requestedPermission, Action ReportDiagnostic, Microsoft.Dynamics.Nav.CodeAnalysis.Text.Location location, ITableTypeSymbol targetTable) + { + if (targetTable.Id > 2000000000) + return; + + if (TableHasInherentPermission(targetTable, requestedPermission)) + return; + + if (objectPermissions is null) + { + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TableDataAccessRequiresPermissions, location, requestedPermission, variableType.Name)); + return; + } + + bool permissionContainRequestedObject = false; + var permissions = objectPermissions.GetPropertyValueSyntax(); + foreach (var permission in permissions.PermissionProperties) + { + if (!permission.ObjectType.IsKind(EnumProvider.SyntaxKind.TableDataKeyword)) + continue; // ensure permission is tabledata + + var identifier = permission.ObjectReference.Identifier; + switch (identifier.Kind) + { + case var _ when identifier.Kind == EnumProvider.SyntaxKind.IdentifierName: + string? name = ((IdentifierNameSyntax)identifier).Identifier.ValueText?.UnquoteIdentifier(); + if (name is not null && name.Equals(variableType.Name, StringComparison.OrdinalIgnoreCase)) + permissionContainRequestedObject = true; + break; + case var _ when identifier.Kind == EnumProvider.SyntaxKind.ObjectId: + int objectId = Convert.ToInt32(((ObjectIdSyntax)identifier).Value.ValueText); + if (objectId == targetTable.Id) + permissionContainRequestedObject = true; + break; + case var _ when identifier.Kind == EnumProvider.SyntaxKind.QualifiedName: + string qualifier = ((QualifiedNameSyntax)identifier).Left.GetText().ToString(); + string? onlyName = ((QualifiedNameSyntax)identifier).Right.Identifier.ValueText?.UnquoteIdentifier(); + if (onlyName is not null && qualifier.Equals(variableType.OriginalDefinition.ContainingNamespace?.QualifiedName, StringComparison.OrdinalIgnoreCase) && onlyName.Equals(variableType.Name, StringComparison.OrdinalIgnoreCase)) + permissionContainRequestedObject = true; + break; + } + if (permissionContainRequestedObject) + { + var permissionsText = permission.Permissions.ValueText; + if (permissionsText is null || !permissionsText.ToLowerInvariant().Contains(requestedPermission)) + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TableDataAccessRequiresPermissions, location, requestedPermission, variableType.Name)); + break; // analysed the permissions for the requested object, break the foreach loop + } + } + if (!permissionContainRequestedObject) + { + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TableDataAccessRequiresPermissions, location, requestedPermission, variableType.Name)); + } + } + + private static bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPermission) + { + IPropertySymbol? permissionProperty = table.GetProperty(EnumProvider.PropertyKind.InherentPermissions); + // InherentPermissions = RIMD; + char[]? permissions = permissionProperty?.Value.ToString()?.ToLowerInvariant().Split(new[] { '=' }, 2)[0].Trim().ToCharArray(); + + if (permissions is not null && permissions.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) + return true; + + return false; + } + + private static char? GetRequiredPermission(string methodName) + { + return MethodPermissionMap.TryGetValue(methodName, out var p) ? p : null; + } + + private static bool TargetTableIsPageSourceTable(OperationAnalysisContext ctx, ITableTypeSymbol targetTable) + { + IPageBaseTypeSymbol? page = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol() switch + { + IPageBaseTypeSymbol p => p, + IApplicationObjectExtensionTypeSymbol ext => ext.Target?.OriginalDefinition as IPageBaseTypeSymbol, + _ => null + }; + + if (page is null || page.RelatedTable is null) + return false; + + return page.RelatedTable.OriginalDefinition.Equals(targetTable); + } + + private static IEnumerable GetInherentPermissionsAttributes(OperationAnalysisContext ctx) + { + if (ctx.ContainingSymbol is not IMethodSymbol method) + return Enumerable.Empty(); + + return method.Attributes.Where(a => a.AttributeKind == EnumProvider.AttributeKind.InherentPermissions); + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs b/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs index 1894497..7f094c5 100644 --- a/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs +++ b/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs @@ -3,6 +3,7 @@ using ALCops.Common.Reflection; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; namespace ALCops.ApplicationCop.Analyzers; @@ -33,9 +34,13 @@ private void AnalyzeInvocation(OperationAnalysisContext ctx) if (ctx.IsObsolete() || ctx.Operation is not IInvocationExpression invocation) return; - if (invocation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod || - invocation.Instance?.Type.OriginalDefinition.Kind != EnumProvider.SymbolKind.Table || - !DatabaseReadMethods.Contains(invocation.TargetMethod.Name)) + if (invocation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod) + return; + + if (invocation.Instance?.Type is not IRecordTypeSymbol) + return; + + if (!DatabaseReadMethods.Contains(invocation.TargetMethod.Name)) return; if (ctx.Operation.Syntax.Parent.Kind == EnumProvider.SyntaxKind.ExpressionStatement) diff --git a/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs b/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs index 2567b25..d30860f 100644 --- a/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs +++ b/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs @@ -225,6 +225,16 @@ public static class DiagnosticDescriptors description: ApplicationCopAnalyzers.RunPageImplementPageManagementDescription, helpLinkUri: GetHelpUri(DiagnosticIds.RunPageImplementPageManagement)); + public static readonly DiagnosticDescriptor TableDataAccessRequiresPermissions = new( + id: DiagnosticIds.TableDataAccessRequiresPermissions, + title: ApplicationCopAnalyzers.TableDataAccessRequiresPermissionsTitle, + messageFormat: ApplicationCopAnalyzers.TableDataAccessRequiresPermissionsMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: ApplicationCopAnalyzers.TableDataAccessRequiresPermissionsDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.TableDataAccessRequiresPermissions)); + public static readonly DiagnosticDescriptor TableDataPerCompanyDeclaration = new( id: DiagnosticIds.TableDataPerCompanyDeclaration, title: ApplicationCopAnalyzers.TableDataPerCompanyDeclarationTitle, diff --git a/src/ALCops.ApplicationCop/DiagnosticIds.cs b/src/ALCops.ApplicationCop/DiagnosticIds.cs index 65efa6c..1d5f3c5 100644 --- a/src/ALCops.ApplicationCop/DiagnosticIds.cs +++ b/src/ALCops.ApplicationCop/DiagnosticIds.cs @@ -32,4 +32,5 @@ public static class DiagnosticIds public static readonly string TableFieldToolTipShouldBeDefined = "AC0028"; public static readonly string DuplicateToolTipBetweenPageAndTable = "AC0029"; public static readonly string UseReturnValueForDatabaseReadMethods = "AC0030"; + public static readonly string TableDataAccessRequiresPermissions = "AC0031"; } \ No newline at end of file diff --git a/src/ALCops.Common/Reflection/EnumProvider.cs b/src/ALCops.Common/Reflection/EnumProvider.cs index 3b7b0b6..11ddd07 100644 --- a/src/ALCops.Common/Reflection/EnumProvider.cs +++ b/src/ALCops.Common/Reflection/EnumProvider.cs @@ -150,6 +150,8 @@ public static class AttributeKind new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.FilterPageHandler))); private static readonly Lazy _hyperlinkHandler = new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.HyperlinkHandler))); + private static readonly Lazy _inherentPermissions = + new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.InherentPermissions))); private static readonly Lazy _integrationEvent = new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.IntegrationEvent))); private static readonly Lazy _internalEvent = @@ -183,6 +185,7 @@ public static class AttributeKind public static NavCodeAnalysis.InternalSyntax.AttributeKind EventSubscriber => _eventSubscriber.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind FilterPageHandler => _filterPageHandler.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind HyperlinkHandler => _hyperlinkHandler.Value; + public static NavCodeAnalysis.InternalSyntax.AttributeKind InherentPermissions => _inherentPermissions.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind IntegrationEvent => _integrationEvent.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind InternalEvent => _internalEvent.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind MessageHandler => _messageHandler.Value; @@ -327,6 +330,12 @@ public static class DirectionKind { public static readonly Lazy> CanonicalNames = CreateEnumDictionary(); + + + private static readonly Lazy _both = + new(() => ParseEnum(nameof(NavCodeAnalysis.DirectionKind.Both))); + + public static NavCodeAnalysis.DirectionKind Both => _both.Value; } /// @@ -880,6 +889,12 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ApplicationArea))); private static readonly Lazy _autoIncrement = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoIncrement))); + private static readonly Lazy _autoReplace = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoReplace))); + private static readonly Lazy _autoSave = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoSave))); + private static readonly Lazy _autoUpdate = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoUpdate))); private static readonly Lazy _caption = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.Caption))); private static readonly Lazy _captionClass = @@ -908,6 +923,8 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ObsoleteState))); private static readonly Lazy _oDataKeyFields = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ODataKeyFields))); + private static readonly Lazy _permissions = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.Permissions))); private static readonly Lazy _scope = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.Scope))); private static readonly Lazy _showAs = @@ -916,6 +933,8 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ShowCaption))); private static readonly Lazy _singleInstance = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.SingleInstance))); + private static readonly Lazy _sourceTable = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.SourceTable))); private static readonly Lazy _sourceTableTemporary = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.SourceTableTemporary))); private static readonly Lazy _subtype = @@ -924,6 +943,8 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.TableRelation))); private static readonly Lazy _toolTip = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ToolTip))); + private static readonly Lazy _useTemporary = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.UseTemporary))); public static NavCodeAnalysis.PropertyKind Access => _access.Value; @@ -931,6 +952,9 @@ public static class PropertyKind public static NavCodeAnalysis.PropertyKind AllowInCustomizations => _allowInCustomizations.Value; public static NavCodeAnalysis.PropertyKind ApplicationArea => _applicationArea.Value; public static NavCodeAnalysis.PropertyKind AutoIncrement => _autoIncrement.Value; + public static NavCodeAnalysis.PropertyKind AutoReplace => _autoReplace.Value; + public static NavCodeAnalysis.PropertyKind AutoSave => _autoSave.Value; + public static NavCodeAnalysis.PropertyKind AutoUpdate => _autoUpdate.Value; public static NavCodeAnalysis.PropertyKind Caption => _caption.Value; public static NavCodeAnalysis.PropertyKind CaptionClass => _captionClass.Value; public static NavCodeAnalysis.PropertyKind CaptionML => _captionMl.Value; @@ -945,14 +969,17 @@ public static class PropertyKind public static NavCodeAnalysis.PropertyKind NotBlank => _notBlank.Value; public static NavCodeAnalysis.PropertyKind ObsoleteState => _obsoleteState.Value; public static NavCodeAnalysis.PropertyKind ODataKeyFields => _oDataKeyFields.Value; + public static NavCodeAnalysis.PropertyKind Permissions => _permissions.Value; public static NavCodeAnalysis.PropertyKind Scope => _scope.Value; public static NavCodeAnalysis.PropertyKind ShowAs => _showAs.Value; public static NavCodeAnalysis.PropertyKind ShowCaption => _showCaption.Value; public static NavCodeAnalysis.PropertyKind SingleInstance => _singleInstance.Value; + public static NavCodeAnalysis.PropertyKind SourceTable => _sourceTable.Value; public static NavCodeAnalysis.PropertyKind SourceTableTemporary => _sourceTableTemporary.Value; public static NavCodeAnalysis.PropertyKind Subtype => _subtype.Value; public static NavCodeAnalysis.PropertyKind TableRelation => _tableRelation.Value; public static NavCodeAnalysis.PropertyKind ToolTip => _toolTip.Value; + public static NavCodeAnalysis.PropertyKind UseTemporary => _useTemporary.Value; } /// @@ -1209,6 +1236,20 @@ public static class UsageCategoryKind CreateEnumDictionary(); } + /// + /// XmlPortSourceTypeKind enum values + /// + public static class XmlPortSourceTypeKind + { + public static readonly Lazy> CanonicalNames = + CreateEnumDictionary(); + + private static readonly Lazy _table = + new(() => ParseEnum(nameof(NavCodeAnalysis.XmlPortSourceTypeKind.Table))); + + public static NavCodeAnalysis.XmlPortSourceTypeKind Table => _table.Value; + } + /// /// XmlVersionNoKind enum values /// @@ -1268,8 +1309,12 @@ public static class SymbolKind new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.ProfileExtension))); private static readonly Lazy _query = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.Query))); + private static readonly Lazy _queryDataItem = + new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.QueryDataItem))); private static readonly Lazy _report = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.Report))); + private static readonly Lazy _reportDataItem = + new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.ReportDataItem))); private static readonly Lazy _reportExtension = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.ReportExtension))); private static readonly Lazy _table = @@ -1280,6 +1325,8 @@ public static class SymbolKind new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.Undefined))); private static readonly Lazy _xmlPort = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.XmlPort))); + private static readonly Lazy _xmlPortNode = + new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.XmlPortNode))); public static NavCodeAnalysis.SymbolKind Action => _action.Value; public static NavCodeAnalysis.SymbolKind Class => _class.Value; @@ -1302,12 +1349,15 @@ public static class SymbolKind public static NavCodeAnalysis.SymbolKind Profile => _profile.Value; public static NavCodeAnalysis.SymbolKind ProfileExtension => _profileExtension.Value; public static NavCodeAnalysis.SymbolKind Query => _query.Value; + public static NavCodeAnalysis.SymbolKind QueryDataItem => _queryDataItem.Value; public static NavCodeAnalysis.SymbolKind Report => _report.Value; + public static NavCodeAnalysis.SymbolKind ReportDataItem => _reportDataItem.Value; public static NavCodeAnalysis.SymbolKind ReportExtension => _reportExtension.Value; public static NavCodeAnalysis.SymbolKind Table => _table.Value; public static NavCodeAnalysis.SymbolKind TableExtension => _tableExtension.Value; public static NavCodeAnalysis.SymbolKind Undefined => _undefined.Value; public static NavCodeAnalysis.SymbolKind XmlPort => _xmlPort.Value; + public static NavCodeAnalysis.SymbolKind XmlPortNode => _xmlPortNode.Value; } /// @@ -1580,6 +1630,8 @@ public static class SyntaxKind new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.SubtypedDataType))); private static readonly Lazy _systemKeyword = new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.SystemKeyword))); + private static readonly Lazy _tableDataKeyword = + new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.TableDataKeyword))); private static readonly Lazy _tableExtensionObject = new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.TableExtensionObject))); private static readonly Lazy _tableKeyword = @@ -1689,7 +1741,7 @@ public static class SyntaxKind public static NavCodeAnalysis.SyntaxKind NotEqualsToken => _notEqualsToken.Value; public static NavCodeAnalysis.SyntaxKind NotKeyword => _notKeyword.Value; public static NavCodeAnalysis.SyntaxKind ObjectNameReference => _objectId.Value; - public static NavCodeAnalysis.SyntaxKind ObjectId => _objectNameReference.Value; + public static NavCodeAnalysis.SyntaxKind ObjectId => _objectId.Value; public static NavCodeAnalysis.SyntaxKind ObjectReference => _objectReference.Value; public static NavCodeAnalysis.SyntaxKind OpenBraceToken => _openBraceToken.Value; public static NavCodeAnalysis.SyntaxKind OpenParenToken => _openParenToken.Value; @@ -1740,6 +1792,7 @@ public static class SyntaxKind public static NavCodeAnalysis.SyntaxKind SingleLineDocumentationCommentTrivia => _singleLineDocumentationCommentTrivia.Value; public static NavCodeAnalysis.SyntaxKind SubtypedDataType => _subtypedDataType.Value; public static NavCodeAnalysis.SyntaxKind SystemKeyword => _systemKeyword.Value; + public static NavCodeAnalysis.SyntaxKind TableDataKeyword => _tableDataKeyword.Value; public static NavCodeAnalysis.SyntaxKind TableExtensionObject => _tableExtensionObject.Value; public static NavCodeAnalysis.SyntaxKind TableKeyword => _tableKeyword.Value; public static NavCodeAnalysis.SyntaxKind TableObject => _tableObject.Value; diff --git a/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs b/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs index a72bae6..f72e626 100644 --- a/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs +++ b/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs @@ -47,7 +47,7 @@ private void AnalyzeCountInvocation(OperationAnalysisContext ctx) invocation.TargetMethod.ContainingSymbol?.Name != "Table") return; - if (invocation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) + if (invocation.Instance?.Type is not IRecordTypeSymbol recordTypeSymbol || recordTypeSymbol.Temporary) return; if (invocation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression)