I’ve written in the past about different security features that are native to D365FO, but in most cases these features require elevated privileges or even the System Administrator role to be accessible. But is there a way to grant these security features to normal users?

Page Options -> Security Diagnostics

The Security Diagnostics feature on a form allows for a user to determine which roles, duties, and privileges give access to this particular page.

Normally a user would not have this option in the Options tab -> Page Options area of a form:

The first thing I did was try and determine which securable object actually controls this option and found that it was the SysSecDiagnostics menu item display. By default, there is no security role in the system that has access to this object. So what I did was to create a custom privilege granting read access to this object and assigned it to the System User role. Now every user in the environment will have access to this feature.

Note: Users will not be able to assign roles to a user, duties to a role, or privileges to a duty with this access they will get a permissions error if they try and do so.

Administration tab on Form Information

Getting more information about a particular form, including the data source and data field where data is coming from is a very helpful feature. This can be found by right clicking on a form and then going to form information. Normally this is what a user would see when they perform this:

They do not have access to the Administration area which contains the information we are looking for. In this case, it is not as easy as just finding the securable object that controls access to this area. The first thing I did in this case was to try and find the form that displays this information and found that it was FormControlPersonalization form. The code on this form shows why this Administration area is missing:

When this dialog is loading it checks to see if the user is assigned the System Administration role, if the user is not assigned this role the tab is marked as not visible.

So what can we do to handle this scenario? My solution was to create a post handler for this loadControlsState() method which means my code will execute after this method has been successfully executed. In this code I do two things:

  • Make the Administration tab visible
  • Load the correct values into the fields that are in the Administration tab

The first step is fairly straight forward as you can get the FormRun information and then easily get the control and mark it as visible.

The second step is slightly more involved for the simple fact that the loadAdminInformation() method is a private method so is not accessible from a separate class. So we have to basically recreate what this method does. I created a new class called FpFormControlPersonalization_Ext and added this code to it:

[PostHandlerFor(formStr(FormControlPersonalization), formMethodStr(FormControlPersonalization, loadControlsState))]
public static void FormControlPersonalization_Post_loadConstrolsState(XppPrePostArgs args)
{
  FpFormControlPersonalization_Ext ext = new FpFormControlPersonalization_Ext();
  FormRun fr = args.getThis();
  FormTabPageControl adminControl = fr.design().controlName(formControlStr(FormControlPersonalization, AdministrationTab)) as FormTabPageControl;
  adminControl.visible(true);

  FormControl personalizedControl;
  personalizedControl = fr.args().parmObject();

  FormRun callingForm;
  callingForm = fr.args().caller();

  FormStringControl formName = fr.design().controlName(formControlStr(FormControlPersonalization, FormName)) as FormStringControl;
  formName.text(callingForm.name());

  FormStringControl controlName = fr.design().controlName(formControlStr(FormControlPersonalization, ControlName)) as FormStringControl;
  FormStringControl controlType = fr.design().controlName(formControlStr(FormControlPersonalization, ControlType)) as FormStringControl;
  FormStringControl dataSource = fr.design().controlName(formControlStr(FormControlPersonalization, DataSource)) as FormStringControl;
  FormStringControl dataField = fr.design().controlName(formControlStr(FormControlPersonalization, DataField)) as FormStringControl;
  FormStringControl menuItemName = fr.design().controlName(formControlStr(FormControlPersonalization, MenuItemName)) as FormStringControl;
  FormStringControl menuItemType = fr.design().controlName(formControlStr(FormControlPersonalization, MenuItemType)) as FormStringControl;
  FormStringControl queryName = fr.design().controlName(formControlStr(FormControlPersonalization, QueryName)) as FormStringControl;
  FormStringControl queryString = fr.design().controlName(formControlStr(FormControlPersonalization, QueryString)) as FormStringControl;
  FormStringControl callerName = fr.design().controlName(formControlStr(FormControlPersonalization, CallerName)) as FormStringControl;
  FormStringControl callerType = fr.design().controlName(formControlStr(FormControlPersonalization, CallerType)) as FormStringControl;

  if(personalizedControl)
  {
    ControlName.text(personalizedControl.name());
    System.Type controlDotNetType = any2Object(personalizedControl).GetType();
    ControlType.text(controlDotNetType.Name);
    DataSource.text(personalizedControl.dataSourceObject()? strfmt('%1 (%2)', personalizedControl.dataSourceObject().name(), tableId2Name(personalizedControl.dataSourceObject().table())) : '');
    DataField.text(personalizedControl.fieldBinding()? personalizedControl.fieldBinding().fieldName() : '');
  }
  else
  {
    controlName.text('');
    controlType.text('');
    dataSource.text('');
    dataField.text('');
  }

  if (callingForm.args())
  {
    Args callingFormArgs = callingForm.args();
    MenuItemName.text(callingFormArgs.menuItemName());
    MenuItemType.text(enum2Symbol(enumNum(MenuItemType), callingFormArgs.menuItemType()));
    QueryName.text(callingFormArgs.initialQuery() ? callingFormArgs.initialQuery().modeledQueryName() : '');
    QueryString.text('');
    ext.setDataSourceField(callingForm, queryString);
    CallerName.text(callingFormArgs.callerName());
    CallerType.text(enum2Symbol(enumNum(UtilElementType), callingFormArgs.callerType()));
  }
  else
  {
    MenuItemName.text('');
    MenuItemType.text('');
    QueryName.text('');
    ext.setDataSourceField(callingForm, queryString);
    CallerName.text('');
    CallerType.text('');
  }
}

private void setDataSourceField(FormRun callingForm, FormStringControl queryString)
{
  if (callingForm.dataSource(1))
  {
    FormDataSource fds = callingForm.dataSource(1);
    QueryString.text(fds.queryRun() ? fds.queryRun().query().toString() : fds.query().toString());
  }
}

Once this code has been added, any user can utilize this right click -> Form Information and get the Administration information:

 

Conclusion

One thing to note here is that in both cases I made it so that every user in the environment would be able to access this information, you could also very easily add these features to only a subset of users in your environment if needed for example to only certain roles or even certain user Ids.

Hopefully this helps with not having to grant SysAdmin access to a user just to perform some security functionality.