Personal tools

ACL Fine Granular Control

From OpenEMR Project Wiki

(Difference between revisions)
Jump to: navigation, search
Line 92: Line 92:
*"'lab' => array(xl('Check Lab Results')  , 0, 'orders/lab_exchange.php')": '''Lab Exchange in Administration, Globals, Connectors'''.
*"'lab' => array(xl('Check Lab Results')  , 0, 'orders/lab_exchange.php')": '''Lab Exchange in Administration, Globals, Connectors'''.
*"'dem' => array(xl('Patient')  , 1,  "patient_file/summary/demographics.php")": '''Demographics section of Patient Summary'''.
*"'dem' => array(xl('Patient')  , 1,  "patient_file/summary/demographics.php")": '''Patient Summary, otherwise referred to as "Demographics"'''.
*"'his' => array(xl('History')  , 1, 'patient_file/history/history.php')": '''History tab of Patient Summary'''.
*"'his' => array(xl('History')  , 1, 'patient_file/history/history.php')": '''History tab of Patient Summary'''.

Latest revision as of 21:01, 17 March 2016



A practice, with staff performing a multitude of tasks, at some point, would wish to delineate the degree of access to the various parts of OpenEMR. Because the Navigation Menu is the nexus of all modules, it is this part of the application that users seek to customize access for staff, commensurate with their duties.

Ideally changing permissions should be as simple as described in the Wiki article, but that is not the case. Because permissions tend to overlap and because each permission may encompass more functions than its stated title, fine granular control is difficult to achieve.

Increasing the complexity is the fact that new snippets need to be added to the relevant .php file. Forum members are all too aware that, even with the help of developers, at least 2 pages of discussion are usually required to accomplish one single task. Fine tuning is possible with a healthy dose of patience.

There is an excellent explanation of ACL by the authors of the module. It can be found by navigating from Administration->ACL-> (Advanced) link in the heading->Manual tab. "Star Wars" aficionados will appreciate the analogy.


Two important concepts are ARO or Access Request Objects (the Group to whom access is granted) and ACO or Access Control Objects (functions to which access is being granted).

If a practice has many groups of ARO's beyond the default, it may be easier to start with the Front Office because it has the most limited access. From that set of ACO's the practice can configure additional layers of ACO's.


  • Experiment on a test copy.
  • Backup or create a system image before attempting on the production copy. White screens tend to make even the most hardy amongst us faint with fear.


The user must become familiar with the various aspects of the Menu, the Array and the Disallowed sections of openemr/interface/main/left_nav.php that govern the Menu, Globals settings, the Access Control Administration (Advanced) and the .php files that generate the frames in question.


Navigation Menu provides an in-depth look at the left side bar.


This .php file that governs Left Navigation holds one of the two keys to denial of access.


The Array section, shown below, describes most of what is listed in the Navigation Menu; but not all of it and at times beyond it. Not everything in the Menu can be found here and, at the same time, Array addresses non-Menu functions. Our task is to find the correct primary document (if available) for hiding the part of the Menu that is of interest to us.


Each of the documents has a short descriptor, a more detailed descriptor, information regarding reference point (whether global or about patients or encounters) and generally the location of the pertinent .php file. Some of the documents are self-evident, while others are more opaque. All will have comments in bold.

  • "'ros' => array(xl('Roster') , 0, 'reports/players_report.php?embed=1')": Roster of players, for Athletic teams; probably not relevant to most practices.
  • "'cal' => array(xl('Calendar') , 0, 'main/main_info.php')": Calendar for scheduling appointments.
  • "'app' => array(xl('Portal Activity') , 0, '../myportal/index.php')": Patient Portal, if enabled.
  • "'msg' => array(xl('Messages') , 0, 'main/messages/messages.php?form_active=1')": Messages Center.
  • "'pwd' => array(xl('Password') , 0, 'usergroup/user_info.php')": Password, under Miscellaneous.
  • "'prf' => array(xl('Preferences') , 0, 'super/edit_globals.php?mode=user')": Preferences, under Miscellaneous.
  • "'adm' => array(xl('Admin') , 0, 'usergroup/admin_frameset.php')": Users, under Administration.
  • "'rep' => array(xl('Reports') , 0, 'reports/index.php')": Reports, for Clients, Clinic, Visits and Financial.
  • "'ono' => array(xl('Ofc Notes') , 0, 'main/onotes/office_comments.php')": Ofc Notes, under Miscellaneous.
  • "'fax' => array(xl('Fax/Scan') , 0, 'fax/faxq.php')": Administration, Globals, Miscellaneous tab.
  • "'adb' => array(xl('Addr Bk') , 0, 'usergroup/addrbook_list.php')": Address Book under Miscellaneous.
  • "'orl' => array(xl('Proc Prov') , 0, 'orders/procedure_provider_list.php')": Providers under Procedures.
  • "'ort' => array(xl('Proc Cat') , 0, 'orders/types.php')": Categories of tests in Configuration under Procedures.
  • "'orc' => array(xl('Proc Load') , 0, 'orders/load_compendium.php')": Load Compendium under Procedures.
  • "'orb' => array(xl('Proc Bat') , 0, 'orders/orders_results.php?batch=1')": Batch Results under Procedures.
  • "'ore' => array(xl('E-Reports') , 0, 'orders/list_reports.php')": Electronic Reports under Procedures.
  • "'ppo' => array(xl('CMS Portal'), 0, 'cmsportal/list_requests.php')": CMS Portal.
  • "'cht' => array(xl('Chart Trk') , 0, '../custom/chart_tracker.php')": Chart Tracker under Miscellaneous.
  • "'imp' => array(xl('Import') , 0, '../custom/import.php')": New Documents for import, under Miscellaneous.
  • "'bil' => array(xl('Billing') , 0, 'billing/billing_report.php')": Billing module under Fees.
  • "'sup' => array(xl('Superbill') , 0, 'patient_file/encounter/superbill_custom_full.php')": Superbill under Visits, Reports.
  • "'aun' => array(xl('Authorizations'), 0, 'main/authorizations/authorizations.php')": Authorization under Miscellaneous.
  • "'new' => array(xl('New Pt') , 0, 'new/new.php')": New/Search under Patients/Clients.
  • "'ped' => array(xl('Patient Education'), 0, 'reports/patient_edu_web_lookup.php')": Patient Education under Miscellaneous.
  • "'lab' => array(xl('Check Lab Results') , 0, 'orders/lab_exchange.php')": Lab Exchange in Administration, Globals, Connectors.
  • "'dem' => array(xl('Patient') , 1, "patient_file/summary/demographics.php")": Patient Summary, otherwise referred to as "Demographics".
  • "'his' => array(xl('History') , 1, 'patient_file/history/history.php')": History tab of Patient Summary.
  • "'ens' => array(xl('Visit History'), 1, 'patient_file/history/encounters.php')": Visit History under Visits,Patients/Clients.
  • "'nen' => array(xl('Create Visit'), 1, 'forms/newpatient/new.php?autoloaded=1&calenc=')": Create Visit under Visits, Patients/Clients.
  • "'pre' => array(xl('Rx') , 1, 'patient_file/summary/rx_frameset.php')": Prescription module.
  • "'iss' => array(xl('Issues') , 1, 'patient_file/summary/stats_full.php?active=all')": Issues tab of Patient Summary.
  • "'imm' => array(xl('Immunize') , 1, 'patient_file/summary/immunizations.php')": Immunizations module of Patient Summary.
  • "'doc' => array(xl('Documents') , 1, '../controller.php?document&list&patient_id={PID}')": Documents tab of Patient Summary.
  • "'orp' => array(xl('Proc Pending Rev'), 1, 'orders/orders_results.php?review=1')": Pending Review under Procedures.
  • "'orr' => array(xl('Proc Res') , 1, 'orders/orders_results.php')": Patient Results under Procedures.
  • "'lda' => array(xl('Lab overview') , 1, 'patient_file/summary/labdata.php')": Lab Overview under Procedures.
  • "'tan' => array(xl('Configure Tracks') , 0, 'forms/track_anything/create.php')": Track Anything module, if enabled.
  • "'prp' => array(xl('Pt Report') , 1, 'patient_file/report/patient_report.php')": Report tab of Patient Summary.
  • "'prq' => array(xl('Pt Rec Request') , 1, 'patient_file/transaction/record_request.php')": Record Request in Transaction tab of Patient Summary.
  • "'pno' => array(xl('Pt Notes') , 1, 'patient_file/summary/pnotes.php')": Notes in Patient Summary.
  • "'tra' => array(xl('Transact') , 1, 'patient_file/transaction/transactions.php')": Transaction tab of Patient Summary.
  • "'sum' => array(xl('Summary') , 1, 'patient_file/summary/summary_bottom.php')": Summary under Patients/Clients.
  • "'enc' => array(xl('Encounter') , 2, 'patient_file/encounter/encounter_top.php')": New Encounter from Encounter History drop down menu.
  • "'erx' => array(xl('e-Rx') , 1, 'eRx.php')": E-Prescribe if enabled.
  • "'err' => array(xl('e-Rx Renewal') , 1, 'eRx.php?page=status')": E-Prescription renewal.
  • "'pay' => array(xl('Payment') , 1, '../patient_file/front_payment.php')": Payment under Fees.
  • "'edi' => array(xl('EDI History') , 0, 'billing/edih_view.php')": EDI History under Fees.
  • "'dld' => array(xl('Display Documents'), 0, 'main/display_documents.php')": Display from Documents tab of Patient Summary.


If we can work from the Disallowed section, it would be helpful.



Because the Globals section of Administration in Left Navigation regulates many of the Menu items, settings from there are used to refine access. Become familiar with these settings.


If the user cannot find an appropriate ACO, chances are that it must be created de novo. This task can be accomplished from Access Control List Administration, (Advanced) link in the heading. This will be described in more details below in the example about Reports.


Fine granular control of these functions is the main goal of this exercise. Fine tuning cannot be achieved without knowing the path to the specific .php file. To find the .php for the frame that is to be hidden, right click in the frame -> choose "This Frame" -> "Open In Another Tab". The URL will give the path to the .php file.


There are essentially two ways to hide Menu items. The first involves snippets from Globals, Array, Disallowed, an ACO and the specific .php file. The second requires only a related ACO and the specific .php file. By far the latter is the easier of the two, because less code changes and therefore less testing are needed.

An example of the first method is described in hiding Calendar and Reports. The second method is used for the other examples.

An exception to the above is the restriction of clinical notes from non-attending physicians as discussed below in the section on Encounter. The only action required of the user is to move the restricted ACO into the Inactive column. No snippet insertion is necessary because the revelant .php file has the ACL checks built-in.


The following examples of access denial will be arranged alphabetically for easy reference instead of the order in the Menu.


Access Control is fully functional for the various groups of ARO's, therefore fine granular control is available presently. Should a new situation arise, wherein more control is requested by the community, we will elaborate at that time.


There exist practices that don't want the Front Office to view the Calendar. It is a rare occurrence because the Front Office is predominately responsible for booking appointments. We will use this example because the exercise is fairly straightforward.

  • Move the ACO to the right.

  • Note that that the Array section has a "cal" document,"'cal' => array(xl('Calendar'), ..."; so we have a fortuitous start.
  • Note also the path is at the top of the screenshot, which helps to orient the user.
  • Because the Calendar can be disabled from Globals, a statement must address this fact. The second statement asks if the ARO has permission to view the Calendar.
  • Insert this snippet into Line 189:

$disallowed['cal'] = !(GLOBALS['disable_calendar'] || acl_check('patients','appointments','', 'write'));


  • The openemr/interface/main/calendar/index.php handles the Calendar.
  • Insert the 2 snippets:

if (!acl_check('patients','appointment','','write')) die("Access Denied.");


Should the Administrator decide that because Physicians are not involved in billing, this entire section of the Menu will be off-limits to that group. One or more of the subsections can be hidden. If the user understands how to hide one subsection, then he is capable of hiding the balance.

  • Move the ACO to the right.
No fee1.png


  • In the script for the Billing Manager, insert the following:

if (!acl_check('encounters','coding_a')) die("Access Denied.");


Unless the practice consists only of the physician, most offices will find the Message Center useful. Method two is deployed to hide it. It is extremely difficult to use the more elaborate technique because there is no Global setting which can act as a "handle" for hiding.

  • Choose an ACO from Administration, which is generally reserved for Administrators. It is unnecessary for the ACO to bear a direct relationship to the Message Center, merely one which is unlikely to be granted to the other ARO's.
  • Insert this snippet:

if (!acl_check('admin','database')) die("Access Denied.");


Use the same technique to hide these items as deployed in Messages.

Patient Education

  • Insert this snippet:

if (!acl_check('admin','database')) die("Access Denied.");


Modules have not been implemented; but when that has been achieved, use the simpler hiding technique.


Every practice will need access to Patients/Clients, but there will be situations wherein some modules should have restricted access.


It is not possible to hide an individual Category of the Documents Tab of the Patient Summary. It is an all-or-none situation.

  • Be certain that the ARO does not have the ACO, Patients, Documents.

  • Into openemr/controller.php insert this snippet:

if (!acl_check('patients','documents')) die("Access Denied.");


Should a user require sole access to his own clinical notes, but deny access for other users; the ACO, Encounters, Notes - my encounters, should be in the Active column; while Encounters, Notes - any encounters, must be in the Inactive column. No snippet needs to be inserted.


Encounter History

New Encounter and clinical notes in past encounters will be off-limits if this ACO has not been granted. Because the Administrative and Clinical tabs are still available, it may be necessary to hide Encounter History completely.

  • Check that the ACO is in the Inactive column.

  • Into openemr/interface/main/left_nav.php, insert:

$disallowed['enc'] = !(acl_check('encounters','notes_a','','write'));

  • Into openemr/interface/patient_files/encounter/forms.php, insert:

if (!acl_check('encounters','notes_a','','write')) die("Access Denied.");


If Physicians have no responsibilities for billing, it would be unnecessary for this ARO to make entries in the EOB Posting Invoice. A good choice for an appropriate ACO would be Encounters, Coding - anything. Navigating to the frame of interest and opening that frame in another tab will inform the user that the path is openemr/interface/patient_file/history/encounters.php.

  • Move the ACO to the right.

  • Insert this snippet:

if (!acl_check('encounters','coding_a')) die("Access Denied.");


Despite the fact that the Front Office should not have access to forms like Review of Systems and SOAP, it does.

  • The public document, 'enc', exists in the Array section; permitting us to insert the following:

$disallowed['enc'] = !(acl_check('encounters','notes','','write'));
No form.png

  • Into each of script for the forms, insert these 2 snippets:

if (!acl_check('encounters','notes_a','','write')) die("Access Denied.");

  • Note that this technique is similar to that employed in Encounter History, but specificity was achieved by the insertion of the second set of snippets to the targeted forms.


This example deployed the easier method for hiding one subsection.


  • Insert this snippet:

if (!acl_check('admin','database')) die("Access Denied.");


This task tends to be more difficult because there are no ACO's except Financial Reporting as vehicles. Although the listing of various reports will be visible, but access will be denied.

The more elaborate technique is demonstrated to provide a mini-tutorial on adding ACO's. Fine granular control can be had with the other way, but each of the 34 .php files will require snippet insertions. On balance the "difficult" technique turns out to be more facile in this case.

  • N.B. The process of creating new ACO's does not permit deletions in ACL Admin. Any deletion will cause the loss of the ACL module entirely. Additionally if the Disallowed section is configured incorrectly, Administration in Left Navigation will malfunction. It is strongly advised that a system image be procured beforehand, should these two mishaps occur in a production copy.


Go to Aministration->ACL, click the (Advanced) link in the header.

  • We need to create a new section first. This is done by navigating from the ACL Admin tab and clicking the Edit button in Sections.

  • Make the appropriate entries and click Submit.

  • Click the Edit button under Access Control Objects.

  • Make the appropriate entries and click Submit.

  • At this point no ARO has the new set of Reports ACO's, so they must be granted. Move the ACO's to the left for Administrators.

  • In this example Physicians will be denied. Ensure that they have no access to Financial and other Reports.


Despite the fact that there exists a public document, rep, in Array; a snippet cannot simply be inserted, because all users will be denied access to Reports. The workaround is to employ a Globals statement. Because many modules are regulated by Globals, it can be used to refine access.

  • Check that both "Enable CQM Reporting" and "Enable AMC Reporting" are selected in Administration.Globals.
  • Insert into /openemr/interface/main/left_nav.php Lines 189 to 194:

$disallowed['rep'] = !($GLOBALS['enable cqm reporting'] || $GLOBALS['enable amc reporting'] ||
acl_check('accounting', 'financial_reporting_m') || acl_check('accounting', 'financial_reporting_a') ||
acl_check('reports', 'clients') || acl_check('reports', 'clinic') || acl_check('reports', 'visits') ||
acl_check('reports', 'procedures') || acl_check('reports', 'insurance') ||
acl_check('reports', 'blank_forms') || acl_check('reports', 'services'));



Mercifully it is not necessary to insert snippets into all 34 .php files under the Reports subsections, only to the three Blank Forms.

  • Insert into openemr/interface/patient_file/summary/demographics_print.php, openemr/interface/patient_file/printed_fee_sheet.php and openemr/interface/patient_file/transaction/print_referral.php the following:

if (!acl_check('reports','blank')) die("Access Denied.");


This section will give examples of restoring access that had been previously hidden.


In general the Front Office has access to the Patient Summary but not to all the forms in Encounters. It should not have access to such forms as SOAP and ROS.


If the Front Office is responsible for entering charges and answering questions about a patient's ledger; the fact that the Code portion of the Visit History is hidden, renders the staff member's work inefficient.


  • To grant access; the ACO, coding_a, must be moved to the Active column.

  • Delete Lines 465 to 470 in openemr/interface/patient_file/history/encounters.php.

  • Change Line 518 to the following:
if ($auth_coding_a) {



Stephen Waite of Case Management Solutions whose solutions are the exemplars of simplicity with elegance.

El Jefe whose single snippet ("special sauce") and support laid the foundation for this article.

If your head is not spinning after all these code changes, three cheers for you!!!