This version of the Ed-Fi Dashboards is no longer supported. See the Ed-Fi Technology Version Index for a link to the latest version.

 

Developers' Guide - Security Overview

Security Design


The security system in the Ed-Fi Dashboards application is implemented using a combination of claims-based security and ownership-based permissions. Claims-based security is implemented using Windows Identity Foundation (WIF), while ownership-based security makes use of dashboard data to determine the relationship a user has to the data they attempting to view. The security implementation uses software development techniques such as Inversion of Control (IoC) and Aspect-Oriented Programming (AOP), which are discussed in detail in the Architectural Pattern Overview section of this documentation.

Claims-Based Security

Claims-based security is used to restrict access to the data a user may retrieve based on the claims they have been issued by an Identity Provider. Teachers will generally be issued claims enabling them only to see certain pages and information, while district superintendents will be able to see everything at their district.

When a user logs on, they are issued claims by the Security Token Service Identity Provider (STS-IP). Some of these claims are identity claims (e.g., name, email address), while others are used to identify the user’s ability to view specific metrics and students within the organizational hierarchy. The code snippet below shows all the Ed-Fi-specific claims in use at the time of this writing.

/// <summary>
/// Defines the claim types used by the EdFi dashboards application.
/// </summary>
public static class EdFiClaimTypes
{
    public const string _BaseNamespace = "http://edfi.org/dashboards/identity/claims/";
    public const string _OrgClaimNamespace = _BaseNamespace + "org/";
    public const string StaffUSI = _BaseNamespace + "staffUSI";                        
    public const string LocalEducationAgencyId = _BaseNamespace + "localEducationAgencyId"; 
    public const string FullName = _BaseNamespace + "fullName";                        
    public const string ViewAllMetrics = _OrgClaimNamespace + "viewAllMetrics";
    public const string ViewMyMetrics = _OrgClaimNamespace + "viewMyMetrics";
    public const string ViewAllStudents = _OrgClaimNamespace + "viewAllStudents";
    public const string ViewMyStudents = _OrgClaimNamespace + "viewMyStudents";
    public const string ViewAllTeachers = _OrgClaimNamespace + "viewAllTeachers";
    public const string ViewOperationalDashboard = _OrgClaimNamespace + "viewOperationalDashboard";
    public const string AdministerDashboard = _OrgClaimNamespace + "administerDashboard";
    public const string ManageGoals = _OrgClaimNamespace + "manageGoals";
    public const string AccessOrganization = _OrgClaimNamespace + "AccessOrganization";
    public const string Impersonating = _BaseNamespace + "Impersonating";
}

Ownership-Based Security

While a user may have the ability to view information for some students, they may not be able to view information about all students. This is where ownership-based security comes into play. Generally speaking, teachers are able to see students currently enrolled in their classes, principals are able to see students currently enrolled in their schools, and superintendents can see any student in the district. There are also other roles in the system, such as district administrators, who are unable to see any student-level information. In the Ed-Fi Dashboards application, we have employed AOP techniques to add call-level parameter validation to verify user’s data “ownership,” and to filter student lists so that users only see their own students when drilling down into campus metric student lists.

To support AOP, the Castle Windsor container provides an IInterceptor interface which enables method-level call interception, providing a mechanism for writing code that will run before and/or after the target method. When we identify the StudentInformationService as the desired implementation for the IStudentInformationService interface, we also tell the container that we want the ApplyViewPermissionsInterceptor to be used to intercept the method calls so that we can inspect the parameters plus their values and then, based on the current user’s information, decide whether to allow the call to proceed or not.

With the interceptor now “registered” with the container, when the SomeController class is requested of the container, which depends on an IStudentInformationService instance, the container no longer simply returns an instance of StudentInformationService. Rather, it dynamically creates an IStudentInformationServiceProxy class containing code to instantiate our ApplyViewPermissionsInterceptor class and invoke the Intercept method.

The diagram below shows the classes used in performing method-level interception/validation:

The ApplyViewPermissionsByClaim interceptor then iterates through all the claims that can be used to authorize access to the service method being invoked to see if the user has the claim. For each claim, it then invokes a corresponding {ClaimName}ClaimAuthorization class which will in turn call into claim-specific chain of validator classes (see the Chain of Responsibility pattern). Each claim validator implementation is responsible for validating a specific method signature (a distinct combination of method parameters).

In the dashboards application, all these components are registered with the container using conventions based on names. For example, a service method that can be authorized by a ViewMyStudents claim will be matched to the ViewMyStudentsClaimAuthorization class, which will in turn call into a chain of ViewMyStudentsClaimValidator{Xxxxx} classes, each of which validates a particular method signature.

The Chain of Responsibility pattern provides an extensible mechanism for handling requests. In this case, we are attempting to handle a request to validate a user’s ability to make a method call with a particular set of parameter values. Although not shown on the diagrams above, the Get service method actually takes a request with three arguments: a local education agency ID (int), a school ID (int), and a student ID (int). In our processing for a ViewMyStudents claim, this particular invocation will end up getting handled by the ViewMyStudentsClaimValidatorSchoolStudent class:

public class ViewMyStudentsClaimValidatorSchoolStudent : ViewMyStudentsClaimValidator
{
    public ViewMyStudentsClaimValidatorSchoolStudent(ISecurityAssertionProvider securityAssertionProvider, IClaimValidator next) : base(securityAssertionProvider, next)
    {
        HandledSignatureKey = ClaimValidatorRequest.SchoolStudentSignature;
    }
    protected override object HandleRequest(ClaimValidatorRequest request)
    {
        ValidateClaimSchoolStudentByStudentListAssociation(request, ClaimType);
        return null;
    }
}

ViewMyStudentsClaimValidatorSchoolStudent Class

public object ValidateClaimSchoolStudentByStudentListAssociation(ClaimValidatorRequest request, string claimType)
{
    var schoolId = request.GetSchoolId();
    var staff = UserInformation.Current.StaffUSI;
    var studentUSI = request.GetStudentUSI();
 
    SecurityAssertionProvider.CurrentUserMustHaveClaimWithinEducationOrganizationHierarchy(schoolId, claimType);
    SecurityAssertionProvider.StaffMemberMustHaveLinkToStudent(staff, studentUSI);
    ValidateLocalEducationAgencySchoolIfPresent(request);
    return null;
}

You’ll notice in the “helper” method ValidateClaimSchoolStudentByStudentListAssociation, there are two assertions made against the arguments.  First, the user must have a claim associated with the school in question. If the user has no ViewMyStudents claim associated with the school, the method call is rejected. Second, the staff member (not necessarily the user) must be associated with the student through a current enrollment or a cohort relationship. If this is not true, the method call is rejected. Finally, a consistency check is made between the local education agency and the school.

If the invocation is handled, and no exceptions are thrown, the interceptor will allow the call to proceed.

Secure Assumptions

The security infrastructure was developed to be “secure by default.” Thus, if a method is added to the services layer which contains a new and as-of-yet unhandled method signature, the security infrastructure will reject invocation attempts on this method. Additionally, the code base includes unit tests to identify potential gaps in the parameter validation implementations before invocation is ever attempted. For issues identified by the unit test, developers must either address the scenario (by implementing a new claim validator for the particular claim type), or explicitly identify it as a method that does not need to be authorized (some methods do not expose data that is sensitive in nature).

Another scenario that sometimes occurs is where a method might have a request that includes a couple of identifiers, followed by another parameter which only serves to further restrict the returned data, but does not represent a possible security violation. Consider the following request:  

public class HistoricalChartRequest
{
    public int SchoolId { get; set; }
    public int MetricId { get; set; }
        
    [AuthIgnore("PeriodId does not affect the results of the request in a way requiring it to be secured.")]
    public int? PeriodId { get; set; }
 
    ...
}

In this case, we can identify that the PeriodId value will never result in sensitive data being exposed by using the AuthIgnore attribute. The security infrastructure will consider this parameter to be “safe,” and ignore it while trying to find a claim validator matching the method “signature”.

More Information about Dashboard Security

This documentation includes the following detailed security topics: