Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 48 Next »

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 problem.

The Student System Project

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 Issue has 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.

Work Begins

The application programmer and OSID implementor collaborate to define an OsidRecord for these extra lists of Organizations. The application populates them from user input on a hold administrative screen. 

organizationIssueRecord

MethodgetHoldCreatorOrganizationIds
DescriptionGets the list of Organization Ids who can create Holds using this Issue.
Returnosid.id.IdListthe list of Organization Ids
CompliancemandatoryThis method must be implemented.
MethodgetHoldCreatorOrganizations
DescriptionGets the list of Organizations who can create Holds using this Issue.
Returnosid.personnel.OrganizationListthe list of Organizations
ErrorsOPERATION_FAILEDunable to complete request
CompliancemandatoryThis method must be implemented.
MethodgetHoldUpdaterOrganizationIds
DescriptionGets the list of Organization Ids who can update Holds of this Issue.
Returnosid.id.IdList the list of Organization Ids
CompliancemandatoryThis method must be implemented.
MethodgetHoldUpdaterOrganizations
DescriptionGets the list of Organizations who can update Holds of this Issue.
Returnosid.personnel.OrganizationList the list of Organizations
ErrorsOPERATION_FAILED unable to complete request
CompliancemandatoryThis method must be implemented.

The product owner tells the application programmer that these organizations should be checked so that only people inside the organization are allowed to perform these operations.

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:

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);
}            

Performance

It may come as a shock but the product owner complains that it takes too long to create a hold. Hundreds of database queries are spawned from these looping calls. Both the application programmer and OSID implementation programmer are in agreement on this one. They complain that these APIs were designed to their requirements. They first consider retrieving all the organization data in bulk and filtering through the results, but this seems wasteful. They conclude that if they can have a simple method:

boolean isAgentInAnyOfTheseOrgs(agentId, orgIds)

then they can optimize for this case in a single call and implement it in a direct SQL query. They put such a method in their own Java interface and wire it to the db via an SQL query that joins the Organization, Appointment, Position, and Resource tables. It's now lightening fast. 

Give Us Our New Method

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 execute.

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.

Back in The Lair

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.

Knowing that hacking up cross-service methods made to look like legititmate service operations is akin to putting lipstick on a pig, she takes a different approach. She decides to work the problem backwards. 

Why are they capturing this data? To enforce a policy.

Authorization

At the end of the day, it's all about saying yes or no to an Agent. The service architect looks at authorization without concerning herself with how it got there. Immediately she notices something disturbing. The application is performing its own authorization in the checkPlaceHold() method and not even allowing the Hold OSID Provider to perform its own authorization enforcement. The enforcement must occur within the provider and the Authorization OSID can be brought in to help it out. 

Authorization Mapping

An Authorization check based on an Agent, Function, and Qualifier. 

  • Agent: the authenticated entity
  • Function: "can create Hold" "can update Hold" "can delete Hold"
  • Qualifier: ???

Qualifiers are the slipperiest of the bunch. They vary by Function.

  • can update Hold? qualifier=the Hold
  • can delete Hold? qualifier=the Hold
  • can create Hold? qualifier=we don't have a Hold yet

Holds are created to an Issue and a Resource (student) in the context of a Oubliette OsidCatalog. The qualifier can only be one of these as this is the only information we have on which to authorize. It's probably more common in OSIDs to align Qualifiers with OsidCatalogs. This doesn't necessarily need to be the case. The project wanted Organizations associated with Issues, so aligning Qualifiers to Issues is a good place to start.

Again, this is not concerning itself about managing Authorizations. The choice of Qualifier for the purpose of checking Authorizations is based on what is known at the time of the check. This narrows it down quite a bit. 

Wiring in Authorization

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

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, getIssueId(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());
    }
}

The Gap

The next step is to note the gap between the above Authorizations and the way the project wants to manage Authorizations. Somehow we need to get from Issues and Holds to Organizations. 

Bouncing Off The Organization Wall

Working from the Organization side, the service architect needs to map Organizations to Issues. While there is an owning Organization in the Issue, it doesn't capture the nuance that separate Organizations have access to place and remove Holds. She is already aware that this translates into create and update operations. Perhaps this can be done using the OsidRecord defined by the team.

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

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 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. 

It appears that the Organization OSID is important but it has no idea about hold authorizations. Resolving the organization above the Hold OSID looks terrible and resolving it within the Hold OSID Provider isn't an improvement. There's only one service left.

It's A Song About Alice

The goal is to manage Authorizations in such a way that the Authorization checks in the adapter work as expected. This stake in the ground for the most common pathway, the checking of authorizations, is the simplest. 

The service architect needs to look deeper into the Authorization OSID. 

Again, the mappings for the Authorization check are:

  • Agent: the authenticated entity
  • Function: "can create Hold" "can update Hold" "can delete Hold"
  • Qualifier: Issue for create, Hold for update and delete

The Qualifier as an Organization

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:

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();
    }
}

What's been accomplished to is to bury both the organizational affiliations and the mappings from Issues & Holds to the Organizations. It's certainly encapsulated but that's all this solution has going for it. It's hairy, has lots of service dependencies, and is probably slow. Also, this solution deals with only one aspect of authorization. In order to be able to audit who has access to what this logic would have to be replicated in such a way to support the return of implicit authorizations. 

The Authorization Two-Step

It is the Agent that performs and action therefore Authorizations are checked using Agents. However, they can be created using Resources as well. What's a Resource? Anything you need it to be.

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. 

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);
        }
    }
}

Better. However, the service architect feels she is kicking the can down the street. The can arrived at the Resource OSID.

Organizational Resources

Fun With Resources

A Resource is a generic dimension referenced throughout the OSIDs. In typical models, the entity that is managed is the same entity that is referenced for other purposes. For example, Starbucks has an inventory model for things it sells in its store. It manages coffee beans, city mugs, and muffins. Each of these items is managed to track unique information. Some of this information is expressed in the model such as the country or countries from which the coffee originates. 

A Customer may purchase any of these items. If an OrderItem directly references each of these items, its structure is dependent on what is going on in the inventory service. When Starbucks begins to sell tasty pastries, the OrderItem must be taught how to deal with them. These interfaces are coupled.

To decouple the services, the OrderItem must reference something that doesn't vary with the inventory. This is a good use of Resources.

 

The OrderItem directly references the Resource (the Resource Id is part of the OrderItem). However, the Resource does not directly reference Muffins. In fact, the order entry system doesn't care about distinguishing among these items at an entity level. It would be happy knowing the Id, name, and price of the Resource. Only the application that manages coffee beans cares about operating on Coffee entities. 

Different applications care about different things. Tightly wiring a model together gets unwieldy and one of the symptoms is the endless stream of ORs in the entity (it could be this OR it could be that OR it could be something else). 

The relation between Resource and the items is not explicit, it is assumed. In the case a Mug is purchased, it appears in the service environment as both a Mug and a Resource. This implicit assumption is part of the orchestration agreement between the Starbucks Inventory OSID and the Resource OSID.

Perhaps this can be applied to the authorization dilemma.

The Authorization Perspective

The last iteration of createIssue() showed how an Authorization can be created using a Resource. However, the checking of the Authorization requires an Agent. The service architect has to get from Agent to Resource. 

If a Resource represented a Person that this seems like a simple query. This is based on a model such as the one below:

 

However, Resources can be Groups, Organizations, and coffee mugs. From the Authorization OSID viewpoint, the model now looks like:

If the service architect uses the Resource OSID and extracts all the Agents associated with a Resource, these Agents can be stashed into the authorization database for fast retrieval. Again, this creates the problem of how to synchronize changes in the Resource OSID with the Authorization OSID Provider. Perhaps it's too soon to design for performance. 

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:

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);
}

This looks better. This implementation of an Authorization OSID Adapter is simpler because it pushes the organizational logic out of authorization into the Resource OSID. This logic has to live somewhere so is the services architect just moving the problem around? 

Crazy Eights

Another service architect posed the question: what is one-half of 8? Most people immediately respond "four." However, this answer assumes the question was an arithmetic one. "3" is also a correct answer. As is "o," "S," "w," and "m."  "5," and "Z" could work if you squint hard enough, but this might feel like forcing a square peg into a round hole. Another answer was 100. I'll leave that one as a homework exercise.

Looking at the picture, rotating it, flipping it over, and slicing it up is helpful to see all the possibilities. If we look at all the OSIDs that are in play, it takes on different shapes depending on where responsibilities are assigned. 

Moving the responsibilities around produces different solutions. Not all of the solutions may be good but some of them may be answers to questions no one anyone knew to ask at the outset. Permutating around the solution space is a good exercise to do when you don't have a pat answer in your back pocket. 

Our service architect marches on.

Deeper into the Resource Can

The service architect needs a place to put the logic to determine whether or not someone belongs to a set of Resources who are authorized to create or update Holds. Although it's all part of the authorization rule, she is attempting to delegate the concept of belonging to a set of Resources to the Resource OSID. A Resource is both singular and plural. In other words, a Resource can represent a single identity or a group of other Resources. This allows an Authorization to reference a single Resource while the Resource OSID understands it to be any one of multiple Resources. Since authorizations are always performed on the Agent, the Resource OSID just needs to answer the question if an Agent is associated with a Resource.

There are two ways to go about managing sets of Resources.

  1. Resource Groups
  2. Demographics

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. 

// ResourceGroupSession - gets the list of "people" in a "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 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?

 

 

  • No labels