public enum ObjectType { // May grow in the future Document } public enum Permission { None = 0, Read = 1, Write = 2, Delete = 3 } public enum Role { Administrator, User }
CREATE TRIGGER [dbo].[TR_Documents_Insert] ON [dbo].[Documents] FOR INSERT AS BEGIN INSERT INTO Permissions(ObjectId, ObjectType, UserId, Permission) SELECT inserted.Id, 1, -- ObjectType.Document inserted.CreatedBy, 3 -- Permission.Delete FROM inserted END
CREATE TRIGGER [dbo].[TR_Documents_Delete] on [dbo].[Documents] FOR DELETE AS BEGIN DELETE FROM Permissions WHERE ObjectId IN (SELECT ID FROM deleted) AND ObjectType = 1 END
public class Document { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } public int CreatedBy { get; set; } public string Source { get; set; } } public class UserPermission { [Key] [Column(Order = 1)] public long ObjectId { get; set; } [Key] [Column(Order = 2)] public byte ObjectType { get; set; } [Key] [Column(Order = 3)] public int UserId { get; set; } public byte Permission { get; set; } } public interface IModel { IQueryable<Document> Documents { get; } IQueryable<UserPermission> Permissions { get; } } public class MyDbContext : DbContext, IModel { public MyDbContext() { } public MyDbContext(string connectString) : base(connectString) { #if DEBUG Database.Log = x => Trace.WriteLine(x); #endif } public DbSet<Document> Documents { get; set; } public DbSet<UserPermission> Permissions { get; set; } #region Explicit IModel interface implementations IQueryable<Document> IModel.Documents => Documents; IQueryable<UserPermission> IModel.Permissions => Permissions; #endregion }
internal class DbContextStub : IModel { public List<Document> Documents { get; } = new List<Document>(); public List<UserPermission> Permissions { get; } = new List<UserPermission>(); #region Explicit Interface Implementations IQueryable<Document> IModel.Documents => Documents.AsQueryable(); IQueryable<UserPermission> IModel.Permissions => Permissions.AsQueryable(); #endregion }
public interface IAccessor { IQueryable<T> GetQuery<T>() where T : class, IAuthorizedObject; Permission GetPermission<T>(long objectId) where T : class, IAuthorizedObject; bool HasPermission<T>(long objectId, Permission permission) where T : class, IAuthorizedObject; }
public interface IAuthorizedObject { long Id { get; } }
public class Document : IAuthorizedObject
public abstract class UserBase : IAccessor { protected readonly IModel Model; protected readonly int Id; private readonly Dictionary<Type, IQueryable> _typeToQuery = new Dictionary<Type, IQueryable>(); private readonly Dictionary<Type, ObjectType> _typeToEnum = new Dictionary<Type, ObjectType>(); protected UserBase(IModel model, int userId) { Model = model; Id = userId; AppendAuthorizedObject(Auth.ObjectType.Document, Model.Documents); // Append new authorized objects here... } private void AppendAuthorizedObject<T>(ObjectType type, IQueryable<T> source) where T : class, IAuthorizedObject { _typeToQuery.Add(typeof(T), source); _typeToEnum.Add(typeof(T), type); } protected IQueryable<T> Query<T>() where T : class, IAuthorizedObject { IQueryable query; if (!_typeToQuery.TryGetValue(typeof(T), out query)) throw new InvalidOperationException( $"Unsupported object type {typeof(T)}"); return query as IQueryable<T>; } protected byte ObjectType<T>() where T : class, IAuthorizedObject { ObjectType type; if (!_typeToEnum.TryGetValue(typeof(T), out type)) throw new InvalidOperationException( $"Unsupported object type {typeof(T)}"); return (byte)type; } protected Permission GetPermission<T>(int userId, long objectId) where T : class, IAuthorizedObject { var entities = Query<T>(); var objectType = ObjectType<T>(); var query = from obj in entities from p in Model.Permissions where p.ObjectType == objectType && p.ObjectId == objectId && obj.Id == p.ObjectId && p.UserId == userId select p.Permission; return (Permission) query.FirstOrDefault(); } public abstract IQueryable<T> GetQuery<T>() where T : class, IAuthorizedObject; public abstract Permission GetPermission<T>(long objectId) where T : class, IAuthorizedObject; public abstract bool HasPermission<T>(long objectId, Permission permission) where T : class, IAuthorizedObject; }
public class Administrator : UserBase { public Administrator(IModel model, int userId) : base(model, userId) { } public override IQueryable<T> GetQuery<T>() { return Query<T>(); } public override bool HasPermission<T>(long objectId, Permission permission) { return permission != Permission.None; } public override Permission GetPermission<T>(long objectId) { return Permission.Delete; } }
public class User : UserBase { public User(IModel model, int userId) : base(model, userId) { } public override IQueryable<T> GetQuery<T>() { var entities = Query<T>(); var objectType = ObjectType<T>(); return from obj in entities from p in Model.Permissions where p.ObjectType == objectType && p.UserId == Id && obj.Id == p.ObjectId select obj; } public override bool HasPermission<T>(long objectId, Permission permission) { return permission == Permission.None ? GetPermission<T>(objectId) == Permission.None : GetPermission<T>(objectId) >= permission; } public override Permission GetPermission<T>(long objectId) { return GetPermission<T>(Id, objectId); } }
public class AdvancedUser : UserBase { public AdvancedUser(IModel model, int userId) : base(model, userId) { } public override IQueryable<T> GetQuery<T>() { // Advanced user can see all resources return Query<T>(); } public override bool HasPermission<T>(long objectId, Permission permission) { if (permission == Permission.None) return false; return GetPermission<T>(objectId) >= permission; } public override Permission GetPermission<T>(long objectId) { // Return own permission if exists or Permission.Read return Max(GetPermission<T>(Id, objectId), Permission.Read); } private static Permission Max(Permission perm1, Permission perm2) { return (Permission) Math.Max((int) perm1, (int) perm2); } }
public static class Factory { public static IAccessor CreateAccessor(IPrincipal principal, IModel model) { if( IsAdministrator(principal)) return new Administrator(model, GetUserId(principal)); else return new User(model, GetUserId(principal)); } private static bool IsAdministrator(IPrincipal principal) { return principal.IsInRole("SYSTEM_ADMINISTRATE"); } private static int GetUserId(IPrincipal principal) { var id = 0; // TODO: Obtain user id from Thread.CurrentPrincipal here... return id; } }
[RoutePrefix("documents")] public class DocumentsController : ApiController { private readonly MyDbContext _db = new MyDbContext(); private IAccessor Accessor => Factory.CreateAccessor(Thread.CurrentPrincipal, _db); [HttpGet] [Route("", Name = "GetDocuments")] [ResponseType(typeof(IQueryable<Document>))] public IHttpActionResult GetDocuments() { var query = Accessor.GetQuery<Document>(); return Ok(query); } [HttpGet] [Route("{id:long}", Name = "GetDocumentById")] [ResponseType(typeof(Document))] public IHttpActionResult GetDocumentById(long id) { if (!Accessor.HasPermission<Document>(id, Permission.Read)) return NotFound(); var document = _db.Documents.FirstOrDefault(e => e.Id == id); if (document == null) return NotFound(); return Ok(document); } [HttpPost] [Route("", Name = "CreateDocument")] [ResponseType(typeof(Document))] public IHttpActionResult CreateDocument(Document document) { if (!ModelState.IsValid) return BadRequest(ModelState); _db.Documents.Add(document); _db.SaveChanges(); return CreatedAtRoute("CreateDocument", new { id = document.Id }, document); } [HttpDelete] [Route("{id:long}", Name = "DeleteDocument")] [ResponseType(typeof(Document))] public IHttpActionResult DeleteDocument(long id) { if (Accessor.HasPermission<Document>(id, Permission.Delete)) return NotFound(); var document = _db.Documents.FirstOrDefault(e => e.Id == id); if (document == null) return NotFound(); _db.Documents.Remove(document); _db.SaveChanges(); return Ok(document); } protected override void Dispose(bool disposing) { if (disposing) _db.Dispose(); base.Dispose(disposing); } }
[RoutePrefix("documents")] public class DocumentPermissionsController : ApiController { private readonly MyDbContext _db = new MyDbContext(); private readonly DocumentPermissionService _service = new DocumentPermissionService(); private IAccessor Accessor => Factory.CreateAccessor(Thread.CurrentPrincipal, _db); [HttpGet] [Route("{id:long}/permissions", Name = "GetPermissions")] [ResponseType(typeof(IQueryable<UserPermission>))] public IHttpActionResult GetPermissions(long id) { if (!Accessor.HasPermission<Document>(id, Permission.Write)) return NotFound(); var permissions = _service.GetPermissions(id); return Ok(permissions); } [HttpPatch] [Route("{id:long}/permissions", Name = "SetPermissions")] public HttpResponseMessage SetPermissions( long id, IList<PermissionDto> permissions) { if (!Accessor.HasPermission<Document>(id, Permission.Write)) return Request.CreateResponse(HttpStatusCode.NotFound); string err; var validationCode = _service.ValidatePermissions(permissions, out err); if (validationCode != HttpStatusCode.OK) return Request.CreateResponse(validationCode, err); _service.SetPermissions(id, permissions); return Request.CreateResponse(HttpStatusCode.OK); } [Route("{id:long}/permissions/{userId:int}", Name = "DeletePermission")] [HttpDelete] public IHttpActionResult DeletePermission(long id, int userId) { if (!Accessor.HasPermission<Document>(id, Permission.Write)) return NotFound(); var isDeleted = _service.DeletePermission(id, userId); return isDeleted ? (IHttpActionResult) Ok() : NotFound(); } protected override void Dispose(bool disposing) { if (disposing) _db.Dispose(); base.Dispose(disposing); } }
AppendAuthorizedObject(ObjectType.Image, Model.Image);
Source: https://habr.com/ru/post/342000/