Role and Department-Based Login Location Filtering in O3 — Looking for Community Input

Background

We are building Ethiopia EMR, a national OpenMRS deployment for the Ethiopian Ministry of Health running across multiple federal hospitals. We are looking for community input on how to approach a requirement before we start building.

The Problem

Today every user sees every Login Location in the picker at /login/location regardless of who they are. In a large hospital this creates two problems: providers can accidentally select a location that has nothing to do with their work, and the picker becomes a long noisy list that is hard to navigate.

The Core Concept

In our hospital setup every provider belongs to a department — not a physical room, but a logical grouping of clinical locations. Our departments are:

Internal Medicine, Surgery, Gynecology & Obstetrics, Pediatrics & Child Health, Dermatology, Venereology, ENT, Psychiatry, Family Medicine

Each department contains multiple physical Login Locations beneath it — a triage room, consultation room, OPD, and so on. A Gynecology specialist should only ever see Gyni/Obs locations in the picker. A Pediatrician should only see Pediatrics locations. They are in the same hospital but their worlds in the system should not overlap at login.

Cross-department consultations are handled through a separate consultation workflow and are out of scope here.

What We Want

When a provider logs in, the Login Location picker should show only the locations that belong to their department, filtered by their role. A provider should never see locations from departments they do not belong to, regardless of how many locations exist in the hospital.

What We Found So Far

We brought this up in the #openmrs3-helpme Slack channel and got two very useful responses we want to continue here in a more permanent thread.

@nethmi pointed to the PATH DRC EMR site-based approach where groupings of locations are isolated per facility build. Our use case is different — we need that isolation to happen within a single facility at runtime based on who is logging in, not at build time across facilities.

@ibacher noted that the datafilter module is the only real lever currently available for something like this, but acknowledged there is no standard way today to associate users with specific login locations.

@mseaton shared that PIH is planning to build exactly this kind of user-to-location association in the coming weeks — not at the datafilter level, but as a lighter association between users and particular visit and login locations surfaced in the session location picker. He also raised the open design question of whether to store this as user properties or a new user-location mapping table.

What We Are Asking

We want to understand how PIH is planning to implement this and whether there is an opportunity to align on a shared backend API. Specifically:

  • How is the user-to-location association planned to be stored — user properties, a new mapping table, or something else?

  • Will this be a new REST endpoint or an extension of an existing one?

  • Will the model support parent-to-child location hierarchy — i.e. associating a user with a department-level parent location and implicitly granting access to all login locations beneath it?

We would rather build toward a community standard than ship something in parallel. Any input welcome.

cc: @mseaton @ibacher @nethmi @grace

1 Like

Interesting timing @yonas , as @mseaton mentioned on the Slack thread, we are likely to imminently start work on a similar feature.

Our background is that although we are using some O3 features, we have custom O2 login page.

We generally have a two-level hierarchy of locations, with the top level being a facility and then second level being the department within that facility. And we have build a multi-level login process, where the the first screen is the facility:

… and then second level is the department within that facility:

Our most immediate need to restrict the facilities that people can log in at, though we certainly can have the need/desire to filter at the location level as well.

In our system, the second-level locations are tagged as “Login Locations”, and the top-level locations are tagged as “Visit Locations”. (So, tangential to this discussion, but when a patient starts a visit at any location within “Cange”, for example, the location associated with that Visit in the DB is always “Cange”)

Our current plan is to move forward with user properties for this, I was proposing a user property “allowedVisitLocations”, that would be a comma-separated list of visit uuids that a user could log into. This is meant as a rather light-touch way to ease the log-in process and prevent bad data when users inadvertently log into the wrong location.. I think we will likely move forward with this in the short-term but if we want to codify this at the API level I could see moving forward with some sort of user-to-location mapping system. One thing to think about is that different implementations structure their hierarchies in different ways. Our two site levels generally (though not strictly) are facility and department, while it looks like yours are department and them room/subgroups under those departments. So we want to make a design that is both flexible (ie we for a generic system we probably don’t want a user property such as “allowedVisitLocations”) while still providing a intuitive UI depending on the specific workflows of an implementation.

To your specific questions:

  • How is the user-to-location association planned to be stored — user properties, a new mapping table, or something else?

We are planning on using a user property right now, but I could see setting up a mapping table in a more generic solution.

  • Will this be a new REST endpoint or an extension of an existing one?

TBD, I need to remind myself how our login page works

  • Will the model support parent-to-child location hierarchy — i.e. associating a user with a department-level parent location and implicitly granting access to all login locations beneath it?

Our immediate need is filtering at our parent level (facility in this case), so, yes. But it’s worth discussing the best way to make this work in a generic solution.

How that wasn’t too much and makes sense!

fyi @ibacher @nethmi @grace

1 Like

Thanks @mogoodrich and glad the timing worked out. When @dkibet suggested we bring this to the community, this kind of response is exactly what we were hoping for.

On your point about wanting a generic solution, one alternative we have been thinking about is mapping roles to locations instead of users.

The core idea is that instead of explicitly listing which locations each user can access, you attach location scope to a Role. So Gynecology Triage Nurse becomes a Role that already knows it belongs to the Gynecology department and any user assigned that role automatically sees only Gynecology login locations in the picker. When a provider transfers departments, the admin just swaps the role and everything follows. No per-user location mapping ever needs to be touched.

@mogoodrich, would something like Cange Staff and Mirebalais Staff as roles work for your facility-level filtering?

One open question we have not fully resolved is edge cases like float nurses, temporary coverage, and consultants who need access outside their usual scope. A user-level override on top of the role-based default is one option, but we would love to hear how others have thought about this. What approaches have you seen work in practice?

Is role-based scoping something the community would want to explore?

cc @ibacher @mseaton @nethmi @grace

I can see value in role-based access control for us, but I definitely think that our primary use case is not role-based, and we’d be creating roles like “Cange Staff” and “Mirebalais Staff” just to be able to control location access which doesn’t seem to be the way we want to go?

That being said, it seems like it wouldn’t be difficult to support role-to-location and user-to-location mappings, I’d think.

Also adding @fanderson and @ddesimone .

@yonas do you want to come up with the basic data model/API on how you think this all should work, maybe worth getting a little more into the details as this point.

1 Like

I like the ideas you present @yonas and I definitely hope we can come up with a shared OpenMRS solution to enable this functionality that we can all leverage.

The constructs that @mogoodrich mentions around “Visit Location” and “Login Location” are pretty well established standards, but not in OpenMRS core. These are exposed within the emrapi module and other modules that build upon it, and that same convention is found elsewhere as well. If we are not yet in a position to modify core, evolving something in emrapi could make logical sense.

I think the biggest issue with building this feature around roles is confusion or ambiguity as to how this is handled in terms of actual user privileges checking. The code as-is is not really designed to operate at the role level, but rather at the privilege level. Access is checked via privilege checks, and roles are just logical grouping of privileges to facilitate management and assignment. There isn’t really a great place to bolt location into this mechanism as far as I know.

An alternative could be to build this association against provider accounts, and then build our application layer around the idea that a) all users must be associated with at least one provider account in order to log in via the UI and b) users would choose among their provider accounts (if they have more than one) upon logging in.

We could then build out a new provider_location table in which one could choose to associate providers with the locations at which they provide services.

This also has the side benefit of improving a situation we already do rather poorly - which is ensuring users that will be trying to create encounters have provider accounts, and also allowing users to have multiple provider accounts if they operate with several different provider roles.

Interested in your and others thoughts - @mogoodrich / @ibacher / @dkayiwa / @burke etc

I like this suggestion, but we should still support non-provider logins for purely administrative accounts. But maybe there’s a way we could restrict such users from the clinical views at all (which is probably something desireable).

Alternatively, there’s no reason we couldn’t make this association directly at the user level which may be easier to understand (as you note, we’re kind of hacking around the idea that user’s need provider accounts and it’s messy).

@raff does this, in any way, inpact the work you are currently doing here: Authentication and authorization framework re-work

That’s true, no reason it couldn’t be user_location rather than provider_location. But generally at PIH we have found problems ensue when users are not required to have provider accounts associated with them, even if these are just given non-clinical roles like “General Admin”. I’m actually suggesting that by using provider_location we work to make this concept more universally applied.

@mseaton wanted to share how we were thinking about this on our end since it might be useful context for the discussion.

Our initial thinking of attaching locations to roles was not about privileges but more about admin manageability. The same way roles let admins bundle privileges and assign them once, we thought a role could carry a set of locations so admins don’t have to configure things user by user. A “Paediatrics Clinician” role would just know its locations and any user assigned that role gets the right scope automatically. But once you have role overlap, say a provider working across two departments, you end up creating composite roles just to express that combination and that gets messy quickly. That is why we are now leaning towards user_location instead.

To keep the admin experience manageable, the UI could present locations grouped by department rather than a flat list. An admin selects a department and all child locations get attached to the user automatically, or they can expand a department and pick individual locations if needed. The user_location table stores the resolved associations.

On the provider_location direction, our concern is that it works well for clinical users but leaves administrative users out by design. In larger hospitals you can have multiple cashier stations, registration desks, or billing points across departments. A cashier at the Pediatrics payment window needs location scoping just as much as a clinician does, but they would never have a provider account. user_location could handle scoping at the user level for everyone, with provider_location layered on top for clinical users specifically. That way the logic is universal and admin users are not a special case that falls back to showing everything.

I am perfectly happy with this path.

Sorry, the point I was trying to make above - which is probably just muddying this thread - is that many implementations have moved to requiring that all users are associated with at least one provider account, as even non-clinical users like Registration Clerks may ultimately be responsible for creating encounters in OpenMRS (at our PIH implementations, patient registration results in an encounter with observations). We use Provider Role (added to core in 2.8 and in the providermanagement module for earlier versions) to distinguish specific types of “providers”, which is also used to control various functionality that is available in the system for a given user. So this might be a route we want to consider if this mandated provider-per-user direction is one we want to pursue more concretely across OpenMRS.

2 Likes

This sounds great @yonas , I too think a user_location table works, with a UI working as you described.

@dkayiwa @ibacher @mseaton are we ready to add this to Core? @yonas I’m happy to review any work you do–not sure what your timeframe is.

(In the PIH use case, I have quickly implemented something that uses user_property in our own custom admin and login pages, but happy to adapt to migrate to a new solution when it is ready).

I’m happy for this to live in core. It’s quite clear that, at the very least, associating user’s with locations is a requirement for many community implementations and we have several attempts at providing similar functionality that have mostly been harder to work with precisely because core doesn’t support such a concept.

3 Likes

To move this forward, here is our proposed data model and API.

Data Model

CREATE TABLE user_location (
  user_location_id INT          NOT NULL AUTO_INCREMENT,
  user_id          INT          NOT NULL,
  location_id      INT          NOT NULL,
  creator          INT          NOT NULL,
  date_created     DATETIME     NOT NULL,
  changed_by       INT,
  date_changed     DATETIME,
  voided           TINYINT(1)   NOT NULL DEFAULT 0,
  voided_by        INT,
  date_voided      DATETIME,
  void_reason      VARCHAR(255),
  uuid             CHAR(38)     NOT NULL UNIQUE,
  PRIMARY KEY (user_location_id),
  FOREIGN KEY (user_id)     REFERENCES users(user_id),
  FOREIGN KEY (location_id) REFERENCES location(location_id),
  FOREIGN KEY (creator)     REFERENCES users(user_id),
  FOREIGN KEY (changed_by)  REFERENCES users(user_id),
  FOREIGN KEY (voided_by)   REFERENCES users(user_id)
);

REST API

GET    /ws/rest/v1/user/{uuid}/location?tag=Login+Location
POST   /ws/rest/v1/user/{uuid}/location/{ "location": "uuid" }
DELETE /ws/rest/v1/user/{uuid}/location/{uuid}

Empty GET = no filter, picker shows all locations. Backward compatible.

Frontend

Extend useLoginLocations in esm-login-app: call user-scoped endpoint first, fall back to full list if empty.

Open Questions

  1. Leaf vs parent storage — storing leaf locations keeps the GET response flat and predictable, but any new room added under a department requires re-mapping affected users. Storing parent locations avoids that but the GET API must resolve children at query time. Which tradeoff is acceptable for Core?
  2. Bulk write — individual subresource POST is the primary write pattern. Is a bulk replacement endpoint (POST /user/{uuid}/location/bulk) worth adding for cases like department transfers?
  3. GET default tag — should GET /user/{uuid}/location default to Login Location tag, or return all mapped locations and let the caller filter? Given visit location filtering is also a desired use case, defaulting to Login Location may be too narrow?
  4. Core placement — we plan to ship this in our custom module once we agree on the design. Does this make sense to land in Core so we can align and upstream? @mogoodrich @mseaton @ibacher

Does this really need all of the audit fields? This feels more similar to user_role and user_property than a fully audited domain object?

Tend to agree with @mseaton about not needing the audit data.

Thinking it through a little more, the hierarchical nature of the location and handling leaves and parents does make things a little complicated, say you have the following structure:

  • Internal Medicine
    • Exam Room 1 (Login Location)
    • Exam Room 2 (Login Location)
    • Exam Room 3 (Login Location)
  • Surgery
    • Surgery Room 1 (Login Location)
    • Surgery Room 2 (Login Location)
    • Surgery Room 3 (Login Location)

The “easiest” thing would just be to tag at the leaf level and so when getting login locations you would get all locations with tag “Login Location” that also have an entry in the the user_location table for that user

We could also say “a location is available to a user if it, or any of it’s ancestors, are linked to the user via the user_location table”.

If we need more flexibility (say you want to say "this user can log into all the surgery rooms except surgery room 3) we might want something like this:

  user_location_id INT          NOT NULL AUTO_INCREMENT,
  user_id          INT          NOT NULL,
  location_id      INT    
  allowed          Boolean 

Then, to determine if a user is allowed to choose a specific location, you’d use the following logic:

  • Check to see if there is a user_location entry for the specified location
    • If so, determine whether it is allowed based on the “allowed” boolean
    • If not, check if the location has a parent location, and check the allowed flag on it
    • Repeat until you find an “allowed” flag or reach the top of the hierarchy;
      • If you reach the top with no match, “allow” based on some global configuration boolean

Granted, this is a bit of a brain dump on a Friday afternoon :slight_smile: but, thoughts?

Good point on the audit fields, agreed!, we’ll keep the schema minimal @mseaton

Great brain dump @mogoodrich! The allowed flag discussion actually helped us land on something clearer.

The allowed complexity only arises if we store parent locations, you’d need it to override inherited permissions at the leaf level. But if we go leaf-only, exclusion is just not inserting that row. No flag, no hierarchy resolution on every fetch.

Our proposal: start with leaf-only. Admin UI expands a parent department to its children on assignment, each child stored as its own row. Simple model, simple API.

If a real use case for parent-level assignment surfaces we can revisit later? @mogoodrich @mseaton

We at PIH do already have need for parent-level assignment, @yonas … but that doesn’t mean that we can’t just handle within the Account Management UI (ie, the system administrator may set the parent locations a user can log in at, but behind-the-scenes we just store the leaves), so this may work for us.

Just to confirm, the new functionality would be:

  • For a particular user, the “getLoginLocations” would return the intersection of 1) locations tagged with the “Login Location” tag and 2) those that the user is mapped to via the user_location table?

Thoughts:

  • We’d definitely need some sort of global property to enable/disable this functionality

Question:

  • This is probably a bigger/broader question: are we only using this new “user_location” tag to control where a user can log in. For example, would we also be restricting the encounter location a user can select for an “encounter_location” on a form. (My gut answer is we’d only restrict to login locations, or at least this would be configurable, because I think there are definitely cases where these would be two different things).

yes, that’s exactly how it should be

The empty-table fallback should handle backward compatibility naturally but yes as a safety mechanism we should have it e.g userlocation.enabled (default false)

We could add general-purpose GET /ws/rest/v1/user/{uuid}/location?tag= endpoint isn’t required for now but shipping it alongside the login use case costs little and makes the table immediately reusable, future implementers get any tag-based restriction (e.g. encounter location filtering) without schema or module changes?

I agree with this, and we should not need a separate global property. All implementations will start with an empty table, which should have zero impact.

It’s a good question. I wonder if there is some creative use of Location Tags we could introduce into this, like adding an optional column for location_tag into this table to be able to say “This user is associated with this Location only as it pertains to this particular tag” (and they could have multiple rows to allow saying “They must have all of these tags” or “They must have any of these tags” associated with a given location.