Codeunit 1753 Data Classification Mgt. Impl., source in 26

Source242526272829metadata in 26

src/System Application/App/Data Classification/src/DataClassificationMgtImpl.Codeunit.al423 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace System.Privacy;

using System.Reflection;

codeunit 1753 "Data Classification Mgt. Impl."
{
    Access = Internal;
    InherentEntitlements = X;
    InherentPermissions = X;
    Permissions = tabledata "Data Sensitivity" = rimd,
                  tabledata Field = r,
                  tabledata "Table Relations Metadata" = r;

    var
        DataSensitivityOptionStringTxt: Label 'Unclassified,Sensitive,Personal,Company Confidential,Normal', Comment = 'It needs to be translated as the field Data Sensitivity on Page 1751 Data Classification WorkSheet and field Data Sensitivity of Table 1180 Data Privacy Entities';
        LegalDisclaimerTxt: Label 'Microsoft is providing this Data Classification feature as a matter of convenience only. It''s your responsibility to classify the data appropriately and comply with any laws and regulations that are applicable to you. Microsoft disclaims all responsibility towards any claims related to your classification of the data.';
        DataSensitivitySetLbl: Label 'The Data sensitivity value %1 has been set for Company Name %2, Table No %3, Field No %4 by UserSecurityId %5.', Locked = true;

    procedure PopulateDataSensitivityTable()
    var
        "Field": Record "Field";
        DataSensitivity: Record "Data Sensitivity";
        FieldsSyncStatusManagement: Codeunit "Fields Sync Status Management";
    begin
        GetEnabledSensitiveFields(Field);

        if Field.FindSet() then
            repeat
                InsertDataSensitivityForField(Field.TableNo, Field."No.", DataSensitivity."Data Sensitivity"::Unclassified);
            until Field.Next() = 0;

        FieldsSyncStatusManagement.SetLastSyncDate();
    end;

    procedure InsertDataSensitivityForField(TableNo: Integer; FieldNo: Integer; DataSensitivityOption: Option)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        if IsSupportedTable(TableNo) then begin
            DataSensitivity.Init();
            DataSensitivity."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(DataSensitivity."Company Name"));
            DataSensitivity."Table No" := TableNo;
            DataSensitivity."Field No" := FieldNo;
            DataSensitivity."Data Sensitivity" := DataSensitivityOption;
            DataSensitivity.Insert();
            Session.LogAuditMessage(StrSubstNo(DataSensitivitySetLbl, DataSensitivity."Data Sensitivity", DataSensitivity."Company Name",
                DataSensitivity."Table No", DataSensitivity."Field No", UserSecurityId()), SecurityOperationResult::Success, AuditCategory::ApplicationManagement, 3, 0);
        end;
    end;

    procedure SetSensitivities(var DataSensitivity: Record "Data Sensitivity"; Sensitivity: Option)
    var
        Now: DateTime;
    begin
        // MODIFYALL does not result in a bulk query for this table,looping through the records performs faster
        // and eliminates issues with the filters of the record
        Now := CurrentDateTime();
        if DataSensitivity.FindSet() then
            repeat
                DataSensitivity."Data Sensitivity" := Sensitivity;
                DataSensitivity."Last Modified By" := UserSecurityId();
                DataSensitivity."Last Modified" := Now;
                DataSensitivity.Modify();
                Session.LogAuditMessage(StrSubstNo(DataSensitivitySetLbl, DataSensitivity."Data Sensitivity", DataSensitivity."Company Name",
                    DataSensitivity."Table No", DataSensitivity."Field No", UserSecurityId()), SecurityOperationResult::Success, AuditCategory::ApplicationManagement, 3, 0);
            until DataSensitivity.Next() = 0;
    end;

    procedure FindSimilarFieldsInRelatedTables(var DataSensitivity: Record "Data Sensitivity")
    var
        TempDataPrivacyEntities: Record "Data Privacy Entities" temporary;
        RecordRef: RecordRef;
        FieldCaptionFilter: Text;
        TableNoFilter: Text;
    begin
        if DataSensitivity.FindSet() then begin
            // Form a filter using the Field Captions of all the fields that are included in the (filtered) DataSensitivity table
            FieldCaptionFilter := GetFieldCaptionFilterText(DataSensitivity);

            // Get the related tables of all the tables that are included in the (filtered) DataSensitivity table
            GetRelatedTables(DataSensitivity, TempDataPrivacyEntities);

            DataSensitivity.Reset();

            FilterDataSensitivityByFieldCaption(DataSensitivity, FieldCaptionFilter);

            RecordRef.GetTable(TempDataPrivacyEntities);
            TableNoFilter := GetFilterTextForFieldValuesInTable(RecordRef, TempDataPrivacyEntities.FieldNo("Table No."));

            DataSensitivity.SetFilter("Table No", TableNoFilter);

            // Performing a FINDFIRST s.t. the cursor is reset on the Data Sensitivity table
            if DataSensitivity.FindFirst() then;
        end;
    end;

    local procedure GetFieldCaptionFilterText(var DataSensitivity: Record "Data Sensitivity"): Text
    var
        FieldCaptionFilter: Text;
        FieldCaptionFilterLbl: Label '''*%1*''|', Comment = '%1 - Filter', Locked = true;
    begin
        repeat
            DataSensitivity.CalcFields("Field Caption");
            FieldCaptionFilter += StrSubstNo(FieldCaptionFilterLbl, DelChr(DataSensitivity."Field Caption", '=', ''''));
        until DataSensitivity.Next() = 0;

        FieldCaptionFilter := DelChr(FieldCaptionFilter, '>', '|');

        exit(FieldCaptionFilter);
    end;

    local procedure GetRelatedTables(var DataSensitivity: Record "Data Sensitivity"; var TempDataPrivacyEntities: Record "Data Privacy Entities" temporary)
    var
        PrevTableNo: Integer;
    begin
        PrevTableNo := 0;
        repeat
            if PrevTableNo <> DataSensitivity."Table No" then begin
                GetRelatedTablesForTable(TempDataPrivacyEntities, DataSensitivity."Table No");
                PrevTableNo := DataSensitivity."Table No";
            end;
        until DataSensitivity.Next() = 0;
    end;

    procedure GetRelatedTablesForTable(var TempDataPrivacyEntitiesOut: Record "Data Privacy Entities" temporary; TableNo: Integer)
    var
        TableRelationsMetadata: Record "Table Relations Metadata";
        DataPrivacyEntitiesMgt: Codeunit "Data Privacy Entities Mgt.";
    begin
        TableRelationsMetadata.SetRange("Related Table ID", TableNo);
        if TableRelationsMetadata.FindSet() then
            repeat
                DataPrivacyEntitiesMgt.InsertDataPrivacyEntitity(TempDataPrivacyEntitiesOut,
                    TableRelationsMetadata."Table ID", 0, 0, '', 0);
            until TableRelationsMetadata.Next() = 0;
    end;

    local procedure FilterDataSensitivityByFieldCaption(var DataSensitivity: Record "Data Sensitivity"; FieldCaptionFilter: Text)
    begin
        DataSensitivity.SetRange("Company Name", CompanyName());
        DataSensitivity.FilterGroup(2);
        DataSensitivity.SetFilter("Field Caption", FieldCaptionFilter);
    end;

    procedure GetTableNoFilterForTablesWhoseNameContains(Name: Text): Text
    var
        "Field": Record "Field";
        RecordRef: RecordRef;
        TableNameFilterLbl: Label '*%1*', Comment = '%1 - Table name', Locked = true;
    begin
        Field.SetRange(DataClassification, Field.DataClassification::CustomerContent);
        Field.SetFilter(ObsoleteState, '<>%1', Field.ObsoleteState::Removed);
        Field.SetFilter(TableName, StrSubstNo(TableNameFilterLbl, Name));

        RecordRef.GetTable(Field);
        exit(GetFilterTextForFieldValuesInTable(RecordRef, Field.FieldNo(TableNo)));
    end;

    procedure PopulateFieldValue(FieldRef: FieldRef; var FieldContentBuffer: Record "Field Content Buffer")
    var
        FieldValueText: Text;
    begin
        if IsFlowField(FieldRef) then
            FieldRef.CalcField();
        Evaluate(FieldValueText, Format(FieldRef.Value(), 0, 9));
        if FieldValueText <> '' then
            if not FieldContentBuffer.Get(FieldValueText) then begin
                FieldContentBuffer.Init();
                FieldContentBuffer.Value := CopyStr(FieldValueText, 1, 250);
                FieldContentBuffer.Insert();
            end;
    end;

    local procedure IsFlowField(FieldRef: FieldRef): Boolean
    var
        OptionVar: Option Normal,FlowFilter,FlowField;
    begin
        Evaluate(OptionVar, Format(FieldRef.Class()));
        exit(OptionVar = OptionVar::FlowField);
    end;

    procedure SyncAllFields()
    var
        "Field": Record "Field";
        DataSensitivity: Record "Data Sensitivity";
        FieldsSyncStatusManagement: Codeunit "Fields Sync Status Management";
    begin
        DataSensitivity.SetRange("Company Name", CompanyName());
        if DataSensitivity.IsEmpty() then begin
            PopulateDataSensitivityTable();
            exit;
        end;

        Field.SetRange(Class, Field.Class::Normal);
        GetEnabledSensitiveFields(Field);

        UpdateDataSensitivityTable(Field);

        FieldsSyncStatusManagement.SetLastSyncDate();
    end;

    local procedure UpdateDataSensitivityTable(var "Field": Record "Field")
    var
        DataSensitivity: Record "Data Sensitivity";
        TempDataSensitivity: Record "Data Sensitivity" temporary;
    begin
        if Field.FindSet() then begin
            // Read all records from the Data Sensitivity table into a temporary variable
            PopulateTempDataSensitivity(TempDataSensitivity);

            // Go through all the entries in the Field table and if they are present in the temporary variable, then delete them
            // from the temporary variable; otherwise, they are new fields and they are inserted in the Data Sensitivity table
            repeat
                if TempDataSensitivity.Get(CompanyName(), Field.TableNo, Field."No.") then
                    TempDataSensitivity.Delete()
                else
                    InsertDataSensitivityForField(Field.TableNo, Field."No.", DataSensitivity."Data Sensitivity"::Unclassified)
            until Field.Next() = 0;
        end;

        // Now the temporary variable only includes entries that are not present in the Field record, so these should
        // be removed from the Data Sensitivity table if they are not classified
        DeleteUnclasifiedEntriesInDataSensitivity(TempDataSensitivity);
    end;

    local procedure PopulateTempDataSensitivity(var TempDataSensitivity: Record "Data Sensitivity" temporary)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        if DataSensitivity.FindSet() then
            repeat
                TempDataSensitivity.TransferFields(DataSensitivity, true);
                TempDataSensitivity.Insert();
            until DataSensitivity.Next() = 0;
    end;

    local procedure DeleteUnclasifiedEntriesInDataSensitivity(var TempDataSensitivity: Record "Data Sensitivity" temporary)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        repeat
            if TempDataSensitivity."Data Sensitivity" = TempDataSensitivity."Data Sensitivity"::Unclassified then begin
                DataSensitivity.Get(TempDataSensitivity."Company Name", TempDataSensitivity."Table No", TempDataSensitivity."Field No");
                DataSensitivity.Delete();
            end;
        until TempDataSensitivity.Next() = 0;
    end;

    procedure AreAllFieldsClassified(): Boolean
    var
        [SecurityFiltering(SecurityFilter::Ignored)]
        DataSensitivity: Record "Data Sensitivity";
    begin
        if not DataSensitivity.WritePermission() then
            exit;

        DataSensitivity.SetRange("Company Name", CompanyName());
        DataSensitivity.SetRange("Data Sensitivity", DataSensitivity."Data Sensitivity"::Unclassified);

        exit(DataSensitivity.IsEmpty());
    end;

    procedure GetDataSensitivityOptionString(): Text
    begin
        exit(DataSensitivityOptionStringTxt);
    end;

    procedure GetLegalDisclaimerTxt(): Text
    begin
        exit(LegalDisclaimerTxt);
    end;

    procedure SetTableFieldsToNormal(TableNumber: Integer)
    var
        "Field": Record "Field";
    begin
        Field.SetRange(TableNo, TableNumber);
        GetSensitiveFields(Field);

        if Field.FindSet() then
            repeat
                SetFieldToNormal(Field.TableNo, Field."No.");
            until Field.Next() = 0;
    end;

    procedure SetFieldToPersonal(TableNo: Integer; FieldNo: Integer)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        SetDataSensitivityForField(TableNo, FieldNo, DataSensitivity."Data Sensitivity"::Personal);
    end;

    procedure SetFieldToSensitive(TableNo: Integer; FieldNo: Integer)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        SetDataSensitivityForField(TableNo, FieldNo, DataSensitivity."Data Sensitivity"::Sensitive);
    end;

    procedure SetFieldToCompanyConfidential(TableNo: Integer; FieldNo: Integer)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        SetDataSensitivityForField(TableNo, FieldNo, DataSensitivity."Data Sensitivity"::"Company Confidential");
    end;

    procedure SetFieldToNormal(TableNo: Integer; FieldNo: Integer)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        SetDataSensitivityForField(TableNo, FieldNo, DataSensitivity."Data Sensitivity"::Normal);
    end;

    local procedure SetDataSensitivityForField(TableNo: Integer; FieldNo: Integer; DataSensitivityOption: Option Unclassified,Sensitive,Personal,"Company Confidential",Normal)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        if DataSensitivity.Get(CompanyName(), TableNo, FieldNo) then begin
            DataSensitivity."Data Sensitivity" := DataSensitivityOption;
            DataSensitivity.Modify();
        end else
            InsertDataSensitivityForField(TableNo, FieldNo, DataSensitivityOption);
    end;

    procedure IsDataSensitivityEmptyForCurrentCompany(): Boolean
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        DataSensitivity.SetRange("Company Name", CompanyName());
        exit(DataSensitivity.IsEmpty());
    end;

    procedure GetEnabledSensitiveFields(var "Field": Record "Field")
    begin
        Field.SetRange(Enabled, true);
        Field.SetFilter(ObsoleteState, '<>%1', Field.ObsoleteState::Removed);
        GetSensitiveFields(Field);
    end;

    procedure GetSensitiveFields(var "Field": Record "Field")
    var
        DataClassificationFilterLbl: Label '%1|%2|%3', Comment = '%1 - Customer content, %2 - EUII, %3 = EUPI', Locked = true;
    begin
        Field.SetFilter(
          DataClassification,
          StrSubstNo(DataClassificationFilterLbl,
            Field.DataClassification::CustomerContent,
            Field.DataClassification::EndUserIdentifiableInformation,
            Field.DataClassification::EndUserPseudonymousIdentifiers));
    end;

    local procedure GetFilterTextForFieldValuesInTable(var RecordRef: RecordRef; FieldNo: Integer): Text
    var
        FilterText: Text;
        FilterTextOrLbl: Label '%1|%2', Comment = '%1 - Filter text, %2 - Field ref', Locked = true;
    begin
        if RecordRef.FindSet() then begin
            repeat
                FilterText := StrSubstNo(FilterTextOrLbl, FilterText, RecordRef.Field(FieldNo));
            until RecordRef.Next() = 0;

            // remove the first vertical bar from the filter text
            FilterText := DelChr(FilterText, '<', '|');
        end;

        exit(FilterText);
    end;

    procedure RunDataClassificationWorksheetForTableWhoseNameContains(TableNoFilter: Text)
    var
        DataSensitivity: Record "Data Sensitivity";
        DataClassificationMgtImpl: Codeunit "Data Classification Mgt. Impl.";
    begin
        DataSensitivity.SetRange("Company Name", CompanyName());
        DataSensitivity.SetFilter("Table No", DataClassificationMgtImpl.GetTableNoFilterForTablesWhoseNameContains(TableNoFilter));
        Page.Run(Page::"Data Classification Worksheet", DataSensitivity);
    end;

    procedure RunDataClassificationWorksheetForPersonalAndSensitiveDataInTable(TableNo: Integer)
    var
        DataSensitivity: Record "Data Sensitivity";
        DataClassificationMgtImpl: Codeunit "Data Classification Mgt. Impl.";
        DataClassificationFilterLbl: Label '%1|%2', Comment = '%1 - Personal data sensitivity, %2 - Sensitive data sensitivity', Locked = true;
    begin
        DataSensitivity.SetRange("Company Name", CompanyName());
        DataSensitivity.SetRange("Table No", TableNo);
        DataSensitivity.SetFilter("Data Sensitivity", StrSubstNo(DataClassificationFilterLbl,
            DataSensitivity."Data Sensitivity"::Personal,
            DataSensitivity."Data Sensitivity"::Sensitive));
        DataClassificationMgtImpl.FindSimilarFieldsInRelatedTables(DataSensitivity);
        Page.RunModal(Page::"Data Classification Worksheet", DataSensitivity);
    end;

    procedure RunDataClassificationWorksheetForTable(TableNo: Integer)
    var
        DataSensitivity: Record "Data Sensitivity";
    begin
        DataSensitivity.SetRange("Company Name", CompanyName());
        DataSensitivity.FilterGroup(2);
        DataSensitivity.SetRange("Table No", TableNo);
        Page.RunModal(Page::"Data Classification Worksheet", DataSensitivity);
    end;

    procedure IsSupportedTable(TableNo: Integer): Boolean
    var
        TableMetadata: Record "Table Metadata";
    begin
        if TableMetadata.Get(TableNo) then
            if (TableMetadata.ObsoleteState = TableMetadata.ObsoleteState::Removed) or (TableMetadata.TableType <> TableMetadata.TableType::Normal) then
                exit(false)
            else
                exit(true);

        exit(false);
    end;
}