예제 #1
0
        private MaskTree Normalize(MaskTree tree)
        {
            tree.Validate(typeof(TEntity), _localizer);
            tree.Normalize(typeof(TEntity));

            return(tree);
        }
예제 #2
0
 /// <summary>
 /// If the user is subject to field-level access control, this method hides all the fields
 /// that the user has no access to and modifies the metadata of the Entities accordingly
 /// </summary>
 protected virtual Task ApplyReadPermissionsMask(
     List <TEntity> resultEntities,
     Query <TEntity> query,
     IEnumerable <AbstractPermission> permissions,
     MaskTree defaultMask)
 {
     // TODO: is there is a solution to this?
     return(Task.CompletedTask);
 }
예제 #3
0
        /// <summary>
        /// Removes from the permissions all permissions that would be violated by the filter or aggregate select, the behavior
        /// of the system here is that when a user orders by a field that she has no full access too, she only sees the
        /// rows where she can see that field, sometimes resulting in a shorter list, this is to prevent the user gaining
        /// any insight over fields she has no access to by filter or order the data
        /// </summary>
        protected IEnumerable <AbstractPermission> FilterViolatedPermissionsForAggregateQuery(IEnumerable <AbstractPermission> permissions, MaskTree defaultMask, FilterExpression filter, AggregateSelectExpression select)
        {
            // Step 1 - Build the "User Mask", i.e the mask containing the fields mentioned in the relevant components of the user query
            var userMask = MaskTree.BasicFieldsMaskTree();

            userMask = UpdateUserMaskAsPerFilter(filter, userMask);
            userMask = UpdateUserMaskAsPerAggregateSelect(select, userMask);

            // Filter out those permissions whose mask does not cover the entire user mask
            return(FilterViolatedPermissionsInner(permissions, defaultMask, userMask));
        }
예제 #4
0
        public void CreateMaskTree_should_work()
        {
            var memoryStream = GeneratePngStream();
            var argbBytes    = PngToArgb.Convert(memoryStream, out _, out _);
            var alpha32Bytes = ExtractAlpha.ArgbToAlpha32(argbBytes);
            var maskTree     = MaskTree.Make(alpha32Bytes, Width, Height);

            Assert.IsTrue(maskTree == 0b11111110);

            memoryStream.Dispose();
        }
예제 #5
0
        private MaskTree UpdateUserMaskAsPerAggregateSelect(AggregateSelectExpression select, MaskTree userMask)
        {
            if (select != null)
            {
                var aggSelectPaths  = select.Select(e => (e.Path, e.Property));
                var aggSelectMask   = MaskTree.GetMaskTree(aggSelectPaths);
                var aggSelectAccess = Normalize(aggSelectMask);

                userMask = userMask.UnionWith(aggSelectAccess);
            }

            return(userMask);
        }
예제 #6
0
        private MaskTree UpdateUserMaskAsPerOrderBy(OrderByExpression orderby, MaskTree userMask)
        {
            if (orderby != null)
            {
                var orderbyPaths  = orderby.Select(e => string.Join("/", e.Path.Union(new string[] { e.Property })));
                var orderbyMask   = MaskTree.GetMaskTree(orderbyPaths);
                var orderbyAccess = Normalize(orderbyMask);

                userMask = userMask.UnionWith(orderbyAccess);
            }

            return(userMask);
        }
예제 #7
0
        private MaskTree UpdateUserMaskAsPerFilter(FilterExpression filter, MaskTree userMask)
        {
            if (filter != null)
            {
                var filterPaths  = filter.Select(e => (e.Path, e.Property));
                var filterMask   = MaskTree.GetMaskTree(filterPaths);
                var filterAccess = Normalize(filterMask);

                userMask = userMask.UnionWith(filterAccess);
            }

            return(userMask);
        }
예제 #8
0
 private IEnumerable <AbstractPermission> FilterViolatedPermissionsInner(IEnumerable <AbstractPermission> permissions, MaskTree defaultMask, MaskTree userMask)
 {
     defaultMask = Normalize(defaultMask);
     return(permissions.Where(e =>
     {
         var permissionMask = string.IsNullOrWhiteSpace(e.Mask) ? defaultMask : Normalize(MaskTree.Parse(e.Mask));
         return permissionMask.Covers(userMask);
     }));
 }
예제 #9
0
        /// <summary>
        /// For each saved entity, determines the applicable mask.
        /// Verifies that the user has sufficient permissions to update the list of entities provided.
        /// </summary>
        protected virtual async Task <Dictionary <TEntityForSave, MaskTree> > GetMasksForSavedEntities(List <TEntityForSave> entities)
        {
            if (entities == null || !entities.Any())
            {
                return(new Dictionary <TEntityForSave, MaskTree>());
            }

            var unrestrictedMask = new MaskTree();
            var permissions      = await UserPermissions(Constants.Update);

            if (!permissions.Any())
            {
                // User has no permissions on this table whatsoever; forbid
                throw new ForbiddenException();
            }
            else if (permissions.Any(e => string.IsNullOrWhiteSpace(e.Criteria) && string.IsNullOrWhiteSpace(e.Mask)))
            {
                // User has unfiltered update permission on the table => proceed
                return(entities.ToDictionary(e => e, e => unrestrictedMask));
            }
            else
            {
                var resultDic = new Dictionary <TEntityForSave, MaskTree>();

                // An array of every criteria and every mask
                var maskAndCriteriaArray = permissions
                                           .Where(e => !string.IsNullOrWhiteSpace(e.Criteria)) // Optimization: a null criteria is satisfied by the entire list of entities
                                           .GroupBy(e => e.Criteria)
                                           .Select(g => new
                {
                    Criteria = g.Key,
                    Mask     = g.Select(e => string.IsNullOrWhiteSpace(e.Mask) ? unrestrictedMask : MaskTree.Parse(e.Mask))
                               .Aggregate((t1, t2) => t1.UnionWith(t2)) // Takes the union of all the mask trees
                }).ToArray();

                var universalPermissions = permissions
                                           .Where(e => string.IsNullOrWhiteSpace(e.Criteria));

                bool hasUniversalPermissions = universalPermissions.Count() > 0;

                // This mask (if exists) applies to every single entity since the criteria is null
                var universalMask = hasUniversalPermissions ? universalPermissions
                                    .Distinct()
                                    .Select(e => MaskTree.Parse(e.Mask))
                                    .Aggregate((t1, t2) => t1.UnionWith(t2)) : null;

                // Every criteria to every index of maskAndCriteriaArray
                var criteriaWithIndexes = maskAndCriteriaArray
                                          .Select((e, index) => new IndexAndCriteria {
                    Criteria = e.Criteria, Index = index
                });

                /////// Part (1) Permissions must allow manipulating the original data before the update

                var existingEntities = entities.Where(e => !0.Equals(e.Id));
                if (existingEntities.Any())
                {
                    // Get the Ids
                    TKey[] existingIds = existingEntities
                                         .Select(e => e.Id).ToArray();

                    // Prepare the query
                    var query = GetRepository()
                                .Query <TEntity>()
                                .FilterByIds(existingIds);

                    // id => index in maskAndCriteriaArray
                    var criteriaMapList = await query
                                          .GetIndexToIdMap <TKey>(criteriaWithIndexes);

                    // id => indices in maskAndCriteriaArray
                    var criteriaMapDictionary = criteriaMapList
                                                .GroupBy(e => e.Id)
                                                .ToDictionary(e => e.Key, e => e.Select(r => r.Index));

                    foreach (var entity in existingEntities)
                    {
                        var      id = entity.Id;
                        MaskTree mask;

                        if (criteriaMapDictionary.ContainsKey(id))
                        {
                            // Those are entities that satisfy one or more non-null Criteria
                            mask = criteriaMapDictionary[id]
                                   .Select(i => maskAndCriteriaArray[i].Mask)
                                   .Aggregate((t1, t2) => t1.UnionWith(t2))
                                   .UnionWith(universalMask);
                        }
                        else
                        {
                            if (hasUniversalPermissions)
                            {
                                // Those are entities that belong to the universal mask of null criteria
                                mask = universalMask;
                            }
                            else
                            {
                                // Cannot update or delete this record, it doesn't satisfy any criteria
                                throw new ForbiddenException();
                            }
                        }

                        resultDic.Add(entity, mask);
                    }
                }


                /////// Part (2) Permissions must work for the new data after the update, only for the modified properties
                {
                    // index in newItems => index in maskAndCriteriaArray
                    var criteriaMapList = await GetAsQuery(entities)
                                          .GetIndexToIndexMap(criteriaWithIndexes);

                    var criteriaMapDictionary = criteriaMapList
                                                .GroupBy(e => e.Id)
                                                .ToDictionary(e => e.Key, e => e.Select(r => r.Index));

                    foreach (var(entity, index) in entities.Select((entity, i) => (entity, i)))
                    {
                        MaskTree mask;

                        if (criteriaMapDictionary.ContainsKey(index))
                        {
                            // Those are entities that satisfy one or more non-null Criteria
                            mask = criteriaMapDictionary[index]
                                   .Select(i => maskAndCriteriaArray[i].Mask)
                                   .Aggregate((t1, t2) => t1.UnionWith(t2))
                                   .UnionWith(universalMask);
                        }
                        else
                        {
                            if (hasUniversalPermissions)
                            {
                                // Those are entities that belong to the universal mask of null criteria
                                mask = universalMask;
                            }
                            else
                            {
                                // Cannot insert or update this record, it doesn't satisfy any criteria
                                throw new ForbiddenException();
                            }
                        }

                        if (resultDic.ContainsKey(entity))
                        {
                            var entityMask = resultDic[entity];
                            resultDic[entity] = resultDic[entity].IntersectionWith(mask);
                        }
                        else
                        {
                            resultDic.Add(entity, mask);
                        }
                    }
                }

                return(resultDic); // preserve the original order
            }
        }
예제 #10
0
        /// <summary>
        /// If the user is subject to field-level access control, this method hides all the fields
        /// that the user has no access to and modifies the metadata of the Entities accordingly
        /// </summary>
        protected override async Task ApplyReadPermissionsMask(
            List <TEntity> resultEntities,
            Query <TEntity> query,
            IEnumerable <AbstractPermission> permissions,
            MaskTree defaultMask)
        {
            bool defaultMaskIsUnrestricted            = defaultMask == null || defaultMask.IsUnrestricted;
            bool allPermissionMasksAreEmpty           = permissions.All(e => string.IsNullOrWhiteSpace(e.Mask));
            bool anEmptyCriteriaIsPairedWithEmptyMask =
                permissions.Any(e => string.IsNullOrWhiteSpace(e.Mask) && string.IsNullOrWhiteSpace(e.Criteria));

            if ((allPermissionMasksAreEmpty || anEmptyCriteriaIsPairedWithEmptyMask) && defaultMaskIsUnrestricted)
            {
                // Optimization: if all masks are unrestricted, or an empty criteria is paired with an empty mask then we can skip this whole ordeal
                return;
            }
            else
            {
                // Maps every Entity to its list of masks
                var maskedEntities       = new Dictionary <Entity, HashSet <string> >();
                var unrestrictedEntities = new HashSet <Entity>();

                // Marks the Entity and all Entities reachable from it as unrestricted
                void MarkUnrestricted(Entity entity, Type entityType)
                {
                    if (entity == null)
                    {
                        return;
                    }

                    if (maskedEntities.ContainsKey(entity))
                    {
                        maskedEntities.Remove(entity);
                    }

                    if (!unrestrictedEntities.Contains(entity))
                    {
                        unrestrictedEntities.Add(entity);
                        foreach (var key in entity.EntityMetadata.Keys)
                        {
                            var prop = entityType.GetProperty(key);
                            if (prop.PropertyType.IsList())
                            {
                                // This is a navigation collection, iterate over the rows
                                var collection = prop.GetValue(entity);
                                if (collection != null)
                                {
                                    var collectionType = prop.PropertyType.CollectionType();
                                    foreach (var row in collection.Enumerate <Entity>())
                                    {
                                        MarkUnrestricted(row, collectionType);
                                    }
                                }
                            }
                            else
                            {
                                // This is a normal navigation property
                                var propValue = prop.GetValue(entity) as Entity;
                                var propType  = prop.PropertyType;
                                MarkUnrestricted(propValue, propType);
                            }
                        }
                    }
                }

                // Goes over this entity and every entity reachable from it and marks each one with the accessible fields
                void MarkMask(Entity entity, Type entityType, MaskTree mask)
                {
                    if (entity == null)
                    {
                        return;
                    }

                    if (mask.IsUnrestricted)
                    {
                        MarkUnrestricted(entity, entityType);
                    }
                    else
                    {
                        if (unrestrictedEntities.Contains(entity))
                        {
                            // Nothing to mask in an unrestricted Entity
                            return;
                        }
                        else
                        {
                            if (!maskedEntities.ContainsKey(entity))
                            {
                                // All entities will have their basic fields accessible
                                var accessibleFields = new HashSet <string>();
                                foreach (var basicField in entityType.AlwaysAccessibleFields())
                                {
                                    accessibleFields.Add(basicField.Name);
                                }

                                maskedEntities[entity] = accessibleFields;
                            }

                            {
                                var accessibleFields = maskedEntities[entity];
                                foreach (var requestedField in entity.EntityMetadata.Keys)
                                {
                                    var prop = entityType.GetProperty(requestedField);
                                    if (mask.ContainsKey(requestedField))
                                    {
                                        // If the field is included in the mask, make it accessible
                                        if (!accessibleFields.Contains(requestedField))
                                        {
                                            accessibleFields.Add(requestedField);
                                        }

                                        if (prop.PropertyType.IsList())
                                        {
                                            // This is a navigation collection, iterate over the rows and apply the mask subtree
                                            var collection = prop.GetValue(entity);
                                            if (collection != null)
                                            {
                                                var collectionType = prop.PropertyType.CollectionType();
                                                foreach (var row in collection.Enumerate <Entity>())
                                                {
                                                    MarkMask(row, collectionType, mask[requestedField]);
                                                }
                                            }
                                        }
                                        else
                                        {
                                            var foreignKeyNameAtt = prop.GetCustomAttribute <ForeignKeyAttribute>();
                                            if (foreignKeyNameAtt != null)
                                            {
                                                // Make sure if the navigation property is included that its foreign key is included as well
                                                var foreignKeyName = foreignKeyNameAtt.Name;
                                                if (!string.IsNullOrWhiteSpace(foreignKeyName) && !accessibleFields.Contains(foreignKeyName))
                                                {
                                                    accessibleFields.Add(foreignKeyName);
                                                }

                                                // Use recursion to update the rest of the tree
                                                var propValue = prop.GetValue(entity) as Entity;
                                                var propType  = prop.PropertyType;
                                                MarkMask(propValue, propType, mask[requestedField]);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                if (permissions.All(e => string.IsNullOrWhiteSpace(e.Criteria)))
                {
                    // Having no criteria is a very common case that can be optimized by skipping the database call
                    var addDefault = permissions.Any(p => string.IsNullOrWhiteSpace(p.Mask));
                    var masks      = permissions.Select(e => e.Mask).Where(e => !string.IsNullOrWhiteSpace(e));
                    var maskTrees  = masks.Select(mask => MaskTree.Parse(mask)).ToList();
                    if (addDefault)
                    {
                        maskTrees.Add(defaultMask);
                    }

                    // Calculate the union of all the mask fields
                    var maskUnion = maskTrees.Aggregate(MaskTree.BasicFieldsMaskTree(), (t1, t2) => t1.UnionWith(t2));

                    // Mark all the entities
                    var entityType = typeof(TEntity);
                    foreach (var item in resultEntities)
                    {
                        MarkMask(item, entityType, maskUnion);
                    }
                }
                else
                {
                    // an array of every criteria and every mask
                    var maskAndCriteriaArray = permissions
                                               .Where(e => !string.IsNullOrWhiteSpace(e.Criteria)) // Optimization: a null criteria is satisfied by the entire list of entities
                                               .GroupBy(e => e.Criteria)
                                               .Select(g => new
                    {
                        Criteria = g.Key,
                        Mask     = g.Select(e => string.IsNullOrWhiteSpace(e.Mask) ? defaultMask : MaskTree.Parse(e.Mask))
                                   .Aggregate((t1, t2) => t1.UnionWith(t2)) // takes the union of all the mask trees
                    }).ToArray();

                    // This mask applies to every single entity since the criteria is null
                    var universalMask = permissions
                                        .Where(e => string.IsNullOrWhiteSpace(e.Criteria))
                                        .Distinct()
                                        .Select(e => string.IsNullOrWhiteSpace(e.Mask) ? defaultMask : MaskTree.Parse(e.Mask))
                                        .Aggregate(MaskTree.BasicFieldsMaskTree(), (t1, t2) => t1.UnionWith(t2)); // we use a seed here since if the collection is empty this will throw an error

                    var criteriaWithIndexes = maskAndCriteriaArray
                                              .Select((e, index) => new IndexAndCriteria {
                        Criteria = e.Criteria, Index = index
                    });

                    var criteriaMapList = await query.GetIndexToIdMap <TKey>(criteriaWithIndexes);

                    // Go over the Ids in the result and apply all relevant masks to said entity
                    var entityType            = typeof(TEntity);
                    var criteriaMapDictionary = criteriaMapList
                                                .GroupBy(e => e.Id)
                                                .ToDictionary(e => e.Key, e => e.ToList());

                    foreach (var entity in resultEntities)
                    {
                        var      id = entity.Id;
                        MaskTree mask;

                        if (criteriaMapDictionary.ContainsKey(id))
                        {
                            // Those are entities that satisfy one or more non-null Criteria
                            mask = criteriaMapDictionary[id]
                                   .Select(e => maskAndCriteriaArray[e.Index].Mask)
                                   .Aggregate((t1, t2) => t1.UnionWith(t2))
                                   .UnionWith(universalMask);
                        }
                        else
                        {
                            // Those are entities that belong to the universal mask of null criteria
                            mask = universalMask;
                        }

                        MarkMask(entity, entityType, mask);
                    }
                }

                // This where field-level security is applied, we read all masked entities and apply the
                // masks on them by setting the field to null and adjusting the metadata accordingly
                foreach (var pair in maskedEntities)
                {
                    var entity           = pair.Key;
                    var accessibleFields = pair.Value;

                    List <Action> updates = new List <Action>(entity.EntityMetadata.Keys.Count);
                    foreach (var requestedField in entity.EntityMetadata.Keys)
                    {
                        if (!accessibleFields.Contains(requestedField))
                        {
                            // Mark the field as restricted (we delay the call to avoid the dreadful "collection-was-modified" Exception)
                            updates.Add(() => entity.EntityMetadata[requestedField] = FieldMetadata.Restricted);

                            // Set the field to null
                            var prop = entity.GetType().GetProperty(requestedField);
                            try
                            {
                                prop.SetValue(entity, null);
                            }
                            catch (Exception ex)
                            {
                                if (prop.PropertyType.IsValueType && Nullable.GetUnderlyingType(prop.PropertyType) == null)
                                {
                                    // Programmer mistake
                                    throw new InvalidOperationException($"Entity field {prop.Name} has a non nullable type, all Entity fields must have a nullable type");
                                }
                                else
                                {
                                    throw ex;
                                }
                            }
                        }
                    }

                    updates.ForEach(a => a());
                }
            }
        }