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