Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Summary

Designing authorization rules within an Authorization OSID Provider can provide visibility in who has access to what and simplify base service implementations. Working from the authorization evaluation perspective can solve a difficult puzzle challenging for experienced architects. This is a case study of a project that tackles this Authorization designs are some of the most difficult puzzles to solve in an enterprise system. Sprinkling authorization data into various services to surface what appears to be simple data can be inflexible and obtuse. A service architect starts with leading requirements and runs into difficulty plowing through a complex problem.

Table of Contents

...

The Student System Project is using the Hold OSID as a means of restricting registration access to students. The registration process uses the Rules.Check OSID as a means of managing what hold Blocks will be checked. 

The product owner understands that one organization may place a Hold on a student while another organization is responsible for expiring it. The However, the Issue has defines a single responsible Resource? . The product owner asks that a list of Organizations, not Resources, who can place the Hold and a list of Organizations who can remove the Hold be added to the Issue.

...

The application programmer scratches his head, and looks to see how he can figure out who belongs to what organization. He looks to the Personnel OSID to answer this question and sees that Persons are related to Organizations via Appointments and Positions. Dismayed at the bizarre complexity of the situation, shovels out the following code:

Code Block
title1. Application Authorization
boolean checkPlaceHold(org.osid.id.Id issueId, org.osid.id.Id agentId)
    throws org.osid.NotFoundException,
           org.osid.OperationFailedException,
           org.osid.PermissionDeniedException {
    org.osid.resource.ResourceAgentSession resourceAgentSession = resourceMgr.getResourceAgentSession();
 
    // I'll assume the resourceId is the same as the personId
    org.osid.id.Id resourceId = resourceAgentSession.getResourceIdByAgent(agentId);
 
    org.osid.hold.IssueLookupSession issueLookupSession = holdMgr.getIssueLookupSession();
    org.osid.hold.Issue issue = issueLookupSession.getIssue(issueId);
    
    // our local org data
    OrganizationIssueRecord record = (OrganizationIssueRecord) issue.getIssueRecord(organizationIssueRecordType);
 
    // could they have made this any more difficult!
    org.osid.personnel.AppointmentLookupSession appointmentLookupSession = personnelMgr.getAppointmentLookupSession();
    appointmentLookupSession.useEffectiveAppointmentView();
    org.osid.personnel.PositionLookupSession positionLookupSession = personnelMgr.getPositionLookupSession();
    positionLookupSession.useEffectivePositionView();
 
    // get the positions of an org - blasted there's no way to get a list of people in an org!
    try (org.osid.id.IdList orgIds = record.getHoldCreatorOrganizationIds()) {
        while (orgIds.hasNext()) {
            org.osid.id.Id orgId = orgIds.getNextId();
            try (org.osid.personnel.PositionList positions = positionLookupSession.getPositionsForOrganization(orgId)) {
                while (positions.hasNext()) {
                    org.osid.personnel.Position position = positions.getNextPosition();
                    try (org.osid.personnel.AppointmentList appointments = appointmentLookupSession.getAppointmentsForPersonAndPosition(resourceId, position.getNextId()) {
                        if (appointments.hasNext()) {
                            return (true);
                        }
                    }
                }
            }
        }
    }
 
    return (false);
}            

...

The service architect is brought in to make it official. She asks why these services have been joined together and she is told that the service design does not meet their needs. She is informed that if this new method was put into the interface, then it would be compliant. However, there isn't much she can do to affect the OSIDs within the time frame of this project's milestone. Not yet knowing the details of the problem she talks in generalities -- "this is usually sign of a factoring issue." This has no impact because as fas as the project is concerned, she was already given the solution to the problem and simply has to executeshould carry it out.

On other projects, the service architect can generally align with one of the project roles to help get the others on board. She can speak to the product owner's vision or simplify the work of an OSID implementation developer. When it comes to performance issues, she is alone to defend a methodology that appears to fly in the face of efficiency.

...

The service architect returns to her think tank to ponder the problem. She traces the problem starting from the requirement of discerning among the organizations who can do what with Holds of various Issues. Issues do constrain Holds and that seems like the place to do it. She eventually arrives at the same place the developers on the project did.

...

The service architect creates a Hold OSID Adapter wrapping the following methods.

Code Block
title2. Authorization In The Hold OSID Provider
public class HoldAdminSession
    extends net.okapia.osid.jamocha.adapter.hold.spi.AbatractAdapterHoldAdminSession
    implements org.osid.hold.HoldAdminSession
 
    private final org.osid.authorization.AuthorizationSession authzSession;
    private static final org.osid.id.Id CREATEHOLD_FUNCTION_ID;
    private static final org.osid.id.Id UPDATEHOLD_FUNCTION_ID;
    private static final org.osid.id.Id DELETEHOLD_FUNCTION_ID;
 
    HoldAdminSession(org.osid.hold.HoldAdminSession session, org.osid.authorization.AuthorizationSession authzSession) {
        super(session);
        this.authzSession = authzSession;
        return;
    }
    public org.osid.hold.HoldForm getHoldFormForCreate(org.osid.id.Id issueId, org.osid.id.Id resourceId, org.osid.type.Type[] recordTypes) {
        if (this.authzSession.isAuthorized(getAuthenticatedAgentId(), CREATEHOLD_FUNCTION_ID, issueId) {
            throw org.osid.PermissionDeniedException();
        }
        
        // wrap the form so we need can get the issueId on the way back in
        return (new HoldFormAdapter(super.getHoldFormForCreate(issueId, resourceId, recordTypes), issueId);
    }
 
    public org.osid.hold.Hold createHold(org.osid.hold.HoldForm form) {
        if (this.authzSession.isAuthorized(getAuthenticatedAgentId(), CREATEHOLD_FUNCTION_ID, getIssueId(form)) {
            throw org.osid.PermissionDeniedException();
        }
 
        return (super.createHold(form));
    }
    public org.osid.hold.HoldForm getHoldFormForUpdate(org.osid.id.Id holdId) {
        if (this.authzSession.isAuthorized(getAuthenticatedAgentId(), UPDATEHOLD_FUNCTION_ID, holdId) {
            throw org.osid.PermissionDeniedException();
        }
    
        // wrap the form so we need can get the issueId on the way back in
        return (new HoldFormAdapter(super.getHoldFormForUpdate(holdId), holdId);
    }
 
    public org.osid.hold.Hold updateHold(org.osid.hold.HoldForm form) {
        if (this.authzSession.isAuthorized(getAuthenticatedAgentId(), UPDATEHOLD_FUNCTION_ID, getIssueIdgetHoldId(form)) {
            throw org.osid.PermissionDeniedException();
        }

        return (super.updateHold(form));
    }
    public void deleteHold(org.osid.id.Id holdId) {
        if (this.authzSession.isAuthorized(getAuthenticatedAgentId(), DELETEHOLD_FUNCTION_ID, holdId) {
            throw org.osid.PermissionDeniedException();
        }
    
        return (super.deleteHold(holdId));
    }
 
    private static org.osid.id.Id getIssueId(org.osid.hold.HoldForm form) {
        if (!(form instance of HoldFormAdapter)) {
            throw new org.osid.InvalidArgumentException("not my form!");
        }
 
        return (((HoldFormAdapter) form).getIssueId());
    }
}

...

With this data the service architect believes she can create the proper Authorizations. But how? 

Code Block
title3. Syncing With The Authorization OSID
public org.osid.hold.Issue createIssue(org.osid.hold.IssueForm form) 
    throws org.osid.NotFoundException,
           org.osid.OperationFailedException,
           org.osid.PermissionDeniedException {    
 
    org.osid.hold.Issue peristIssue(form);
    OrganizationHoldFormRecord record = (OrganizationHoldFormRecord) form.getIssueFormRecord(organizationHoldRecordType);
  
    org.osid.personnel.AppointmentLookupSession appointmentLookupSession = personnelMgr.getAppointmentLookupSession();
    appointmentLookupSession.useEffectiveAppointmentView();
    org.osid.personnel.PositionLookupSession positionLookupSession = personnelMgr.getPositionLookupSession();
    positionLookupSession.useEffectivePositionView();
 
    try (org.osid.id.IdList orgIds = record.getHoldCreatorOrganizationIds()) {
        while (orgIds.hasNext()) {
            org.osid.id.Id orgId = orgIds.getNextId();
            try (org.osid.personnel.PositionList positions = positionLookupSession.getPositionsForOrganization(orgId)) {
                while (positions.hasNext()) {
                    org.osid.personnel.Position position = positions.getNextPosition();
                    if (useThisPosition(position)) {
                        try (org.osid.personnel.AppintmentList appointments = appointmentLookupSession.getAppointmentsForPosition(position.getNextId()) {
                            try (org.osid.id.IdList agentIds = resourceAgentSession.getAgentIdsByResource(position.getResourceId()) {
                                while (agentIds.hasNext()) {
                                    org.osid.authorization.AuthorizationForm authorizationForm = authorizationSession.getAuthorizationFormForCreateForAgent(agentIds.getNextId(), CREATEHOLD_FUNCTION_ID, issue.getId(), new org.osid.type.Type[0]);
                                    authorizationSession.createAuthorization(authorizationForm);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

This doesn't seem right. This is worse than the code from the first iteration Example 1 and something like this needs to occur again for the creation each Hold. Yes, it's now in the Hold OSID Provider but it fixes the membership such that if the organization changes, the authorization service is out of date. 

...

The service architect has seen Qualifier hierarchies based on organizations. She attempts to map these two kinds of Qualifiers and jots down some pseudo-code in an Authorization OSID Adapter:

Code Block
title4. Authorizations In The Authorization OSID
boolean isAuthorized(org.osid.id.Id agentId, org.osid.id.Id functionId, org.osid.id.Id qualifierId)
    throws org.osid.NotFoundException,
           org.osid.OperationFailedException,
           org.osid.PermissionDeniedException { 
    org.osid.id.Id issueId;
    if (functionId.equals(CREATEHOLD_FUNCTION_ID)) {
        issueId = qualifierid;
    } else if (functionId.equals(UPDATEHOLD_FUNCTION_ID)) {
        org.osid.hold.Hold hold = holdLookupSession.getHold(qualifierId);
        issueId = hold.getIssueId();
    } else {
        return (underlyingAuthorizationProvider.isAuthorized(agentId, functionId, qualfiierId));
    }
 
    org.osid.id.Id resourceId = resourceAgentSession.getResourceIdByAgent(agentId);
    org.osid.hold.Issue issue = issueLookupSession.getIssue(issueId);
    OrganizationIssueRecord record = (OrganizationIssueRecord) issue.getIssueRecord(ORGANIZATION_ISSUE_RECORD_TYPE);
    org.osid.id.IdList organizationIds;
 
    if (functionId.equals(CREATEHOLD_FUNCTION_ID)) {
        organizationIds = record.getHoldCreatorOrganizationIds();
    } else if (functionId.equals(UPDATEHOLD_FUNCTION_ID)) {
        organizationIds = record.getHoldUpdaterOrganizationIds();
    }
 
    try {
        while (organizationIds.hasNext()) {
            org.osid.id.Id orgId = organizationIds.getNextId();
            try (org.osid.personnel.PositionList positions = positionLookupSession.getPositionsForOrganization(orgId)) {
                while (positions.hasNext()) {
                    org.osid.personnel.Position position = positions.getNextPosition();
                    try (org.osid.personnel.AppointmentList appointments = appointmentLookupSession.getAppointmentsForPersonAndPosition(resourceId, position.getNextId()) {
                         if (appointments.hasNext()) {
                            return (true);
                        }
                    }
                }
            }
        }
 
        return (false);
    } finally {
        organizationIds.close();
    }
}

...

After a few beers, our service architect equates a Resource with an Organization. While it is the Agent that is creating or updating a Hold, the authorization has been granted to a Resource. 

Code Block
title5. Granting Authorizations To Resources
public org.osid.hold.Issue createIssue(org.osid.hold.IssueForm form) 
    throws org.osid.NotFoundException,
           org.osid.OperationFailedException,
           org.osid.PermissionDeniedException {    
 
    org.osid.hold.Issue peristIssue(form);
    OrganizationHoldFormRecord record = (OrganizationHoldFormRecord) form.getIssueFormRecord(organizationHoldRecordType);
 
    try (org.osid.id.IdList orgIds = record.getHoldCreatorOrganizationIds()) {
        while (orgIds.hasNext()) {
            org.osid.authorization.AuthorizationForm authorizationForm = authorizationSession.getAuthorizationFormForCreateForResource(orgIds.getNextId(), CREATEHOLD_FUNCTION_ID, issue.getId());
            authorizationSession.createAuthorization(authorizationForm);
        }
    }
}

...

Designing around with services is always a choosing with side of the line has ownership of a problem. If the problem of mapping Agents to Resources doesn't belong to the Authorization OSID, then maybe it's a problem for the Resource OSID (as what is implied by its definition). The next iteration on the authorization implementation is:

Code Block
title6. Authorization Iteration 2
boolean isAuthorized(org.osid.id.Id agentId, org.osid.id.Id functionId, org.osid.id.Id qualifierId)
    throws org.osid.NotFoundException,
           org.osid.OperationFailedException,
           org.osid.PermissionDeniedException { 

    org.osid.id.Id issueId;
    if (functionId.equals(CREATEHOLD_FUNCTION_ID)) {
        issueId = qualifierid;
    } else if (functionId.equals(UPDATEHOLD_FUNCTION_ID)) {
        org.osid.hold.Hold hold = holdLookupSession.getHold(qualifierId);
        issueId = hold.getIssueId();
    } else {
        return (underlyingAuthorizationProvider.isAuthorized(agentId, functionId, qualfiierId));
    }
 
    org.osid.hold.Issue issue = issueLookupSession.getIssue(issueId);
    OrganizationIssueRecord record = (OrganizationIssueRecord) issue.getIssueRecord(ORGANIZATION_ISSUE_RECORD_TYPE);
 
    if (functionId.equals(CREATEHOLD_FUNCTION_ID)) {
        if (resourceAgentSession.isAgentForResources(agentId, record.getHoldCreatorOrganizationIds()) {
            return (true);
        }
    } else if (functionId.equals(UPDATEHOLD_FUNCTION_ID)) {
        if (resourceAgentSession.isAgentForResources(agentId, record.getHoldUpdaterOrganizationIds()) {
            return (true);
        }  
    }

    return (false);
}

...

The difference between these two is that a Demographic is a grouping based on an explicit Rule. The service architect tries simple first and works with Resource groups by hard-coding an implicit rule. 

Gliffy
size500
nameStack Service Model

Code Block
title7. Implicit Resource Groups
// ResourceGroupSession - gets athe list of "people" in ana "organization of people who can create holds"
public getResourcesByGroup(org.osid.id.Id groupResourceId) {
    // assumption: groupResourceId is an org Id
 
    java.util.Collection<org.osid.resource.Resource> ret = new java.util.ArrayList<>();
    try (org.osid.personnel.PositionList positions = positionLookupSession.getPositionsForOrganization(groupResourceId)) {
        while (positions.hasNext()) {
            org.osid.personnel.Position position = positions.getNextPosition();
            // can add a filter on a type of position
            try (org.osid.personnel.AppointmentList appointments = appointmentLookupSession.getAppointmentsForPosition(position.getNextId()) {
                 if (appointments.hasNext()) {
                    ret.add(convertPerson2Resource(position.getPerson()));
                }
            }
        }
    }
 
    return (new net.okapia.osid.jamocha.resource.resource.ArrayResourceList(ret));
}

The above method can be used as an assist for evaluating the list of Agents associated with a Resource. There are many valid permutations here, but they all boil down to the question of how to determine the relationship among Resources. All that is needed is to refine the filtering rule so it knows what job positions to look at in order to determine if any of them can create Holds.

Wait a minute...

What Was The Question?

These iterations make an assumption. The assumption is part of the original functional requirement basing authorizations on organizations. It assumes that authorizations are completely derived from job positions and the organizational hierarchy.  If this is the case, then in order to grant access to or remove a person from this authorization either requires changing their job appointments or requires using the Authorization OSID or the Resource OSID directly. 

The service architect poses the question to the product owner who reiterates that the authorizations should be based on organizations. When she asks if there are any exceptions to the organization rule, the product owner reports that these exceptions do exist but the project can iterate on the development of that feature later. However, the service architect wants to nail down the services to which application will interface and contain development iterations to within those boundaries. This is the key.

If there's any part to this system that ought to be stable, it is the line between the end user application and the services it talks to. The stated requirement was to list a bunch of organizations who can create and update Holds. This is a solution, not a requirement. When there's a new authorization rule to which organizations can do something else to a Hold, a new record Type agreement will have to be made. This isn't stable. If this line isn't stable, then it destabilizes everything underneath it. 

The service architect decides to go back to the beginning and looks at the situation from an interoperability and stability perspective.

Solution 1: Ignoring the Organizations

Gliffy
size500
nameApplication Manages Authorization

The service architect starts over from the basic principles. A service is not necessarily an entire OSID. OSIDs are comprised of many services. The OSID packaging is simply a cluster of services around some problem domain. The service of checking Authorizations (AuthorizationSession) and the service of managing Authorizations (AuthorizationAdminSession) can be used in different ways. The checking of Authorizations within the Hold OSID Provider (Example 2) still looks sweet. Perhaps the managing Authorization piece can be moved around.

The responsibility for managing authorizations is moved to the OSID Consumer. For every Issue and Hold created, the application creates Authorizations. In order to accomplish this, the application must:

  • provide a list of people who are authorized for each function
  • understand the Function to be used for each authorization
  • use the correct Qualifier (Hold or Issue) for each Function

In this scenario, checking Authorizations is fully encapsulated under the Hold OSID. However, managing Authorizations is orchestrated alongside the Hold OSID. 

Solution 2: Grouping Agents

Gliffy
size500
nameManaging Authorizations and Resources

Layering in the Resource OSID provides a means of grouping Resources (and thus Agents). In the above scenario, the Application is also taught how to create and manage Resource Groups and the Authorization OSID Provider is taught how to evaluate Resources as was done in an earlier example.

Solution 3: Injecting Organizations

Knowing that the product owner will balk on this manual process, the service architect layers in the Organization code to create implicit groups into the Resource OSID (Example 7). 

Gliffy
size500
nameManaging Authorizations With Resources and Organizations

Now, the application can select Resource Groups that look like Organizations and use those for creating Authorizations. As far as anyone is concerned, the application is prompting for a list of Organizations for each Function but using these Organizations as Resources to manage the Authorizations. When the product owner gets around to iterating amend its exception cases, the Application need not do anything else other than allow administrative users to supplement the Organizations with a list of people.

Sooner or later, they will want to not include certain people coming through the Organization evaluation. It is around this point where the letting go process of Organizational-based authorization begins and will eventually end up back at Solution 2. It's a process.

The service architect feels good. This iterative path is going somewhere. 

Solution 4: Reducing Assumptions

The application has to understand the specific Functions where:

  • Function 1: create Hold
  • Function 2: update Hold

This requires an agreement. On the bright side, an agreement was removed from the Hold OSID with the elimination of the OrganizationIssueRecord. Now, if there was a way to dynamically figure out what Functions exist.

Code Block
title8. Dynamic Functions
try (org.osid.authorization.FunctionList functions = functionLookupSession.getFunctionsByQualifierHierarchy(ISSUE_QUALIFIER_HIERARCHY)) {
    while (functions.hasNext()) {
        org.osid.authorization.Function function = functions.getNextFunction();
        for (org.osid.resource.Resource resource :  getListOfResourcesWhoCanDoThisFunctionOnThisIssue(function, issueId)) {
            org.osid.authorization.AuthroizationForm form = authorizationAdminSession.getAuthorizationFormForCreateForResource(resource, function.getId(), issueId, new org.osid.type.Type[0]);
        }
    }
}
 
org.osid.resource.Resource[] getListOfResourcesWhoCanDoThisFunctionOnThisIssue(org.osid.authorization.Function, org.osid.id.Id issueId) {
    display("please list the People and Organizations who can perform the following function on" + issueId);
    display(function.getDisplayName());
    display(function.getDescription());
    return (getListOfResources());
}

The agreement is reduced to identification of the qualifier hierarchy.

New Functions can be added or removed without any changes to the application code. In the original plan, the OrganizationIssueRecord would be changing with changing authorization requirements. Using an interface to harden authorization rules is not stable as it is the authorization rules that should be encapsulated even if the application is using the Authorization OSID directly.

Retrospective

  1. Know the difference between a functional requirement and a solution. It's easy to be led by the nose into a clumsy solution. Many people think in terms of capturing and moving data. They also believe that any application responsibility means the end-user has to do more work. Service design is a different paradigm. It is about assigning responsibilities among blocks of code.
  2. Gnarly code (Example 1) and poor performance are signs of a factoring problem. Performance can be increased incrementally by breaking things up and leveraging bulk operations. However, more study is warranted when performance is very off base and developers look to merge services together.
  3. Some functional requirements are short sighted. Tightly coupling organizational affiliations with authorizations is not necessary in small environments and never holds up well in an enterprise (changing hold authorization requirements will never cause an institution to do a re-org).
  4. Squirreling through a problem creates new problems to be solved, and solving those creates more problems, and so on. Sometimes it helps to go back to the very beginning and find that one initial assumption on which everything else was based. It's so much easier to do this on a whiteboard rather than wait until the code was written. 
  5. People have trouble with the asymmetry of services. How a consumer uses stuff coming out of a service and how that stuff gets in there are two different problems. In simpler designs, the data object goes in, the data object comes out. In a service paradigm, approach these two aspects independently. 
  6. Generally speaking, encapsulation is good. But here, it made more sense to surface the management of authorizations but in such a way as to carefully manage assumptions between the application and the Authorization OSID.
  7. This case study avoided the use of the word "Role." Roles are often used in other frameworks to manage sets of people on the periphery of an authorization service. A role should describes the action one can perform on something. In the OSID world, it is the Agent, Function, and Qualifier. This is the Authorization itself. A Role is not an attribute of a Person nor is it a group. An Agent doesn't have a role until it is joined with a Function and Qualifier. The slipperiness of Resources allows for creating Authorizations using groups or other rules to cut down on the explicit Authorizations that need to be managed. These composite Resources can be aligned with Functions and Qualifiers ("the group of people who can create Holds in the Bursar's Office") but the role isn't realized until the explicit Authorization is created. This semantic detail helps clarify how the enforcement point works and pays off when applying this principle to Workflow. OSIDs avoid the word altogether. 
  8. A litmus test for a good authorization design is to have the ability to ask the question "who has access to perform this Function on Qualifier?" In order to answer this question, there needs to be the ability to expand explicitly managed authorizations into a list of implicit authorizations based on expansion of the Resources or Qualifier hierarchy. This helps keep authorization logic clear and out in the open.