Describing University via software I faced an auth issue.
Previously I had only a Headmaster
role, which can do and access anything.
But as for now, I need to integrate a Teacher
role.
The Teacher
role should have an option to access certain features, which could be easily restricted by Authorize
attribute. But in some cases, I want to reduce the number of data allowed to access for this role, e.g. not all students of the universe, but the ones who study Teacher's
Subject
.
All of this is already described in EF, (e.g. teacher-subject, subject-students relationships). But now I struggle to refuse (return 403) request for subject or students which are not allowed to access by Teacher
.
I thought about a Specification pattern usage for my Services, so the resulting data will be reduced with a Specification's filter, as it helps reduce the data amount, sometimes to no-data, but didn't help to fully refuse a request.
Could you kindly provide me a link or an architectural idea to satisfy expectations for both use-cases specified above?
// entity models
class Subject {
...
public Teacher Teacher { get; set; }
public List<Students> { get; set; }
...
}
class Teacher {
...
public List<Subject> Subjects { get; set; }
...
}
class Student {
...
public List<Subject> StudiedSubjects {get; set; }
...
}
// first use-case I want to solve
public ActionResult<List<Student>> GetStudent()
{
// previously I just did
return Ok(_studentsService.GetStudents());
// but as for now in case of Teacher role accessed the method I want to
// reduce the number of returned students
}
// second use-case I want to solve
public ActionResult<Subject> GetSubjectDetails(int subjectId)
{
// previously I just did
return Ok(_subjectService.GetSubject(subjectId);
// but as for now in case of Teacher role I need to check whether its
// allowed to get the subject and return Access denied in case its not
}
For your first case, because the Action have no parameters at all, it'll make more sense to return Students that are accessible for a Teacher, or no Students at all if no one take all the subjects of certain Teacher, so 403 are not needed in this case. You could pass the User
from controller or inject HttpContextAssessor to StudentService and use it for filtering.
as for your second case, its a perfect situation to return 403 if the SubjectId
is not related to the Teacher
in context. If you dont mind getting data from database for each request, you can use Requirement combined AuthorizationHandler in a Policy-Based Authorization by retrieving any data you need for authorization from database thus determine if the teacher has access to certain Subject(s). Steps to achieve it:
First setup the policy for the Teachers-Subjects relation and the handlers in Startup.ConfigureServices
:
services.AddAuthorization(options =>
{
options.AddPolicy("TeacherSubject", policy => policy.Requirements.Add( new TeacherSubjectRequirement() ));
});
services.AddScoped<IAuthorizationHandler, TeacherSubjectHandler>();
next create the AuthorizationHandler for that policy:
public class TeacherSubjectHandler : AuthorizationHandler<TeacherSubjectRequirement>
{
readonly IHttpContextAccessor _contextAccessor;
readonly UserManager<AppUser> _usermanager;
readonly UserToTeacherService _userToTeacherService;
public ThePolicyAuthorizationHandler(IHttpContextAccessor c, UserManager<AppUser> u, _userToTeacherService s)
{
_contextAccessor = c;
_userManager = u;
_userToTeacherService = s;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, TeacherSubjectRequirement requirement)
{
var user = _userManager.GetUserAsync(_contextAccessor.HttpContext.User);
var teacher = _userToTeacherService(user); //I assume this service will also retrieve teacher's subjects
var subjectIds = teacher.Subjects.Select(s => s.SubjectId).ToList();
if (context.Resource is AuthorizationFilterContext filterContext)
{
var subjectIdStr = filterContext.RouteData.Values["id"].ToString();
if ( int.TryParse(subjectIdStr, out var subjectId) && subjectIds.Contains(subjectId) )
{
context.Succeed(requirement);
}
}
}
}
as for the Requirement class, its just an empty class:
public class TeacherSubjectRequirement: IAuthorizationRequirement
{
}
Since we're doing the authorization mechanism in AuthorizationHandler, we can leave this class empty. But it will still needed for the policy-based authorization to works.
And then for the policy to take effect, add attribute to the controller
[Authorize(Policy = "TeacherSubject")]
public ActionResult<Subject> GetSubjectDetails(int subjectId)
{
//existing code
}
But to be honest, I haven't tried putting policy-based attribute in an Action. If this doesn't work, putting the attribute in controller will surely works.
Hope this helps.
Your scenario is very practical which makes the question very interesting. But It would be nice for you to look at this documentation.
Use claims to give the teach access to the information they must be able to access. You can save a claim as .i.e. "TeacherName-TargetInforNameTheyCanAccess"; you can have as many claims as possible depending on how much information should be accessible by the teacher.
This goes the same for Students as well. You can create claims for Students that are under that Lecturers Class. Like say: "StudentName-LectureName" an you can then base your authentication by checking if the student is claimed to be under a specific lecturers class.