In the new Microsoft Dynamics 365 Finance and Operations release a lot of things have changed in terms of reflection. One of them that we have used frequently for security access validation is gone, DictSecurityKey is deprecated.
You can find a list of still available classes which are mostly used against AOT objects here:
The European Union has introduced strict data protection rules last month, for which companies had to become legally compliant to avoid fines. We have a set of patches to apply to get a GDPR tool for Microsoft Dynamics AX 2012, which has been released to assist us:
KB4056903 Privacy Policy update
KB4074643 DAPIA Security tool
KB4057507 SQM Data collection
The part relevant for us is the tool, which allows capturing which interactive users have logged on to AX, who are using a security role that may access sensitive information.
Unfortunately Microsoft only provides a high-level guideline on what shall be included and provides very little tangible assistance. Due to this I have felt we needed some way to identify what security roles could really be accessing sensitive data, so I came up with an X++ job that does exactly this. You may pass in menu items for forms, reports and also tables that may access details such as Customers, Global Address Book, Vendors, Address and Contact details. The tool is using the Security framework to determine which roles can edit such data, but you may change filter criteria to also include View access.
static void WIK_GDPR_enable_roles(Args _args)
{
#AOT
// List of tables which might contain sensitive data
container tables = [
[menuitemDisplayStr(CustTable), UtilElementType::DisplayTool]
,[menuitemDisplayStr(CustTableListPage), UtilElementType::DisplayTool]
,[menuitemDisplayStr(CustTableEdit), UtilElementType::DisplayTool]
,[menuitemDisplayStr(CustTableDetails), UtilElementType::DisplayTool]
,[menuitemDisplayStr(GlobalAddressBookListPage), UtilElementType::DisplayTool]
,[menuitemDisplayStr(DirPartyTable), UtilElementType::DisplayTool]
,[menuitemDisplayStr(DirPartyTableEdit), UtilElementType::DisplayTool]
];
// Replace role settings?
boolean update = NoYes::Yes;
UtilElementType objectType;
str objectName;
int i = 1;
SysSecFlatDataTable objects;
SysSecFlatDataTable allObjects;
SysUserLogRoleSettings roleSettings;
SecurityRole securityRole;
allObjects.setTmp();
while (i <= conLen(tables))
{
objectName = conPeek(conPeek(tables, i), 1);
objectType = conPeek(conPeek(tables, i), 2);
switch (objectType)
{
// Implemented from \Forms\SysSecObjectsInRole\init
case UtilElementType::DisplayTool:
SysSecObjectsFromEntryPoint::GenerateData(
SysSecObjectsAnalyzeType::SecViewRelatedRoles,
objectName,
enum2int(objectType));
break;
case UtilElementType::OutputTool:
SysSecObjectsFromEntryPoint::GenerateData(
SysSecObjectsAnalyzeType::SecViewRelatedRoles,
objectName,
enum2int(objectType));
break;
case UtilElementType::ActionTool:
SysSecObjectsFromEntryPoint::GenerateData(
SysSecObjectsAnalyzeType::SecViewRelatedRoles,
objectName,
enum2int(objectType));
break;
case UtilElementType::Table:
SysSecObjectsFromSecurableObject::GenerateData(
objectName,
enum2int(objectType));
break;
}
while select objects
{
allObjects.clear();
buf2Buf(objects, allObjects);
allObjects.doInsert();
}
i++;
}
if (update)
{
i = 0;
ttsBegin;
update_recordSet roleSettings
setting HasAccessToSensitiveData = NoYes::No;
// No join for Tmp object, must use nested loop
while select allObjects
group by Role//, IsOverride
where allObjects.IsOverride == NoYes::No
&& ((allObjects.AccessRight != AccessRight::View && allObjects.AccessRight != AccessRight::NoAccess)
&& (allObjects.EntryPointAccess != AccessRight::View && allObjects.EntryPointAccess != AccessRight::NoAccess))
{
select firstOnly forUpdate roleSettings
join RecId from securityRole
where securityRole.AotName == allObjects.Role
&& roleSettings.SecurityRole == securityRole.RecId;
if (roleSettings)
{
roleSettings.HasAccessToSensitiveData = NoYes::Yes;
roleSettings.doUpdate();
i++;
}
}
ttsCommit;
info(strFmt('%1 security roles have been updated', i));
}
while select Role, RoleName
from allObjects
group by RoleName, Role//, AccessRight, EntryPointAccess
where allObjects.IsOverride == NoYes::No
&& ((allObjects.AccessRight != AccessRight::View && allObjects.AccessRight != AccessRight::NoAccess)
&& (allObjects.EntryPointAccess != AccessRight::View && allObjects.EntryPointAccess != AccessRight::NoAccess))
{
info(strFmt('%1 (%2)', allObjects.Role, allObjects.RoleName));
}
}
Many of us might have encountered the below error messages when trying to run a newly created workflow. I would like to provide this neat troubleshooting tip with the below code snippet for investigating workflow security issues.
Stopped (error): X++ Exception: Work item could not be created.
Insufficient rights for user %1.
In most of the cases it is due to the lack of security access for the execution account on a menu item, for which we could add a couple of lines of code to resolve the problem.
Change \AOT\Classes\SysWorfklowDocument\assertAsUser method and for each fork of menu item type you may add the entry point in a security privilege or role, for which you need to provide access for on the user account you have been using. That means for each section of workflowPermission you need to add the appropriate menu item in the error log.
if (workflowPermission.parmDisplayMenuItem())
{
permission = SysWorkflowDocument::assertMenuItem(workflowPermission.parmDisplayMenuItem(), MenuItemType::Display);
if (!permission)
{
error(strFmt('Entry point %1', workflowPermission.parmDisplayMenuItem()));
return [permission, buf2Con(rec)]; // buf2Con() needed to support packing buffers participating in SC/sc hierarchies
}
}
Also you need to add the following pieces to \AOT\Classes\SysWorkflowEventDispatcher\onWorkItemCreate() method:
if (workflowPermission.parmDisplayMenuItem())
{
permission = SysWorkflowDocument::assertMenuItem(workflowPermission.parmDisplayMenuItem(), MenuItemType::Display);
if (!permission)
{
error(strFmt('Entry point %1', workflowPermission.parmDisplayMenuItem()));
return [permission, buf2Con(rec)]; // buf2Con() needed to support packing buffers participating in SC/sc hierarchies
}
}
(...)
if (!SysWorkflowHelper::userHasPermission(
_workItemContext.parmWorkflowCorrelation().parmWorkflowContext(),
stepTable.workflowElementTable().workflowVersionTable().workflowTable().TemplateName,
sysDictWorkflowElement.actionMenuItem(completingOutcome),
menuItemName,
user))
{
error(strFmt('Entry point %1 / Menu item action %2', menuItemName, sysDictWorkflowElement.actionMenuItem(completingOutcome)));
throw error(strfmt("@SYS109561", user));
}
Using the above changes you will be able to set up the missing security pieces easily by taking a look at the workflow execution history, where the infolog in the tracking history will contain the entry points causing the issues.