/// <summary>
        /// Check the cache.
        /// </summary>
        /// <param name="key">Key to look up.</param>
        /// <param name="result">The value result - either from cache or freshly determined.</param>
        /// <param name="valueFactory">A callback that will provide the values.</param>
        /// <returns></returns>

        protected bool TryGetOrAdd(TKey key, out TValue result, Func <TKey, TValue> valueFactory)
        {
            bool fromCache = false;

            // Wrap value-factory to handle cache invalidator
            Func <TKey, TValue> valueFactoryImpl = (k) =>
            {
                TValue innerResult;

                using (CacheContext cacheContext = new CacheContext())
                {
                    innerResult = valueFactory(key);

                    // Add the cache context entries to the appropriate CacheInvalidator
                    _cacheInvalidator.AddInvalidations(cacheContext, key);
                }

                return(innerResult);
            };

            // Check cache
            fromCache = Cache.TryGetOrAdd(key, out result, valueFactoryImpl);
            if (fromCache && CacheContext.IsSet())
            {
                // Add the already stored changes that should invalidate this cache
                // entry to any outer or containing cache contexts.
                using (CacheContext cacheContext = CacheContext.GetContext())
                {
                    cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                }
            }

            return(fromCache);
        }
        /// <summary>
        /// Load the roles recursively for a user or role.
        /// </summary>
        /// <param name="subjectId">
        /// The user to load the roles for. This cannot be negative.
        /// </param>
        /// <returns>
        /// The roles the user is recursively a member of.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="subjectId"/> cannot be negative.
        /// </exception>
        public ISet <long> GetUserRoles(long subjectId)
        {
            ISet <long> roles;

            if (!Cache.TryGetValue(subjectId, out roles))
            {
                using (CacheContext cacheContext = new CacheContext())
                {
                    roles = RoleRepository.GetUserRoles(subjectId);

                    Cache[subjectId] = roles;

                    _cacheInvalidator.AddInvalidations(cacheContext, subjectId);
                }
            }
            else
            {
                // Add the already stored changes that should invalidate this cache
                // entry to any outer or containing cache contexts.
                using (CacheContext cacheContext = CacheContext.GetContext( ))
                {
                    cacheContext.AddInvalidationsFor(_cacheInvalidator, subjectId);
                }
            }

            return(roles);
        }
Example #3
0
        /// <summary>
        /// Identify cache dependencies.
        /// </summary>
        private static void IdentifyCacheDependencies(Report report, ReportToQueryConverterSettings settings, StructuredQuery query)
        {
            // Tell the cache what entities were referenced to return
            // the StructuredQuery result
            using (CacheContext cacheContext = CacheContext.GetContext( ))
            {
                cacheContext.Entities.Add(report.Id);

                if (query.Conditions != null)
                {
                    foreach (var condition in query.Conditions)
                    {
                        cacheContext.Entities.Add(condition.EntityId);

                        if (condition.Expression != null)
                        {
                            cacheContext.Entities.Add(condition.Expression.EntityId);
                        }
                    }
                }

                if (query.OrderBy != null)
                {
                    foreach (var orderBy in query.OrderBy)
                    {
                        if (orderBy.Expression != null)
                        {
                            cacheContext.Entities.Add(orderBy.Expression.EntityId);
                        }
                    }
                }

                if (query.SelectColumns != null)
                {
                    foreach (var column in query.SelectColumns)
                    {
                        cacheContext.Entities.Add(column.EntityId);

                        if (column.Expression != null)
                        {
                            cacheContext.Entities.Add(column.Expression.EntityId);
                        }
                    }
                }

                if (report.ReportOrderBys != null)
                {
                    foreach (var orderBy in report.ReportOrderBys)
                    {
                        if (orderBy != null)
                        {
                            cacheContext.Entities.Add(orderBy.Id);
                        }
                    }
                }
            }

            StructuredQueryHelper.IdentifyStructureCacheDependencies(query, settings.ConditionsOnly);
        }
        /// <summary>
        /// Is there an access rule for the specified type(s) that includes the requested permission? E.g. create.
        /// </summary>
        /// <param name="entityType">
        /// The <see cref="EntityType"/> to check. This cannot be null.
        /// </param>
        /// <param name="permission">The permission being sought.</param>
        /// <param name="user"> The user requesting access. This cannot be null. </param>
        /// <returns>
        /// True if the user can create entities of that type, false if not.
        /// </returns>
        private bool CheckTypeAccess(EntityType entityType, EntityRef permission, EntityRef user)
        {
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }
            if (entityType == null)
            {
                throw new ArgumentNullException(nameof(entityType));
            }

            List <EntityType> entityTypesToCheck;
            bool result;

            using (new SecurityBypassContext())
                using (Profiler.MeasureAndSuppress("SecuresFlagEntityAccessControlChecker.CheckTypeAccess"))
                {
                    // Check if the type itself can be created
                    using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName))
                    {
                        messageContext.Append(() => "Checking access rules first:");

                        IDictionary <long, bool> resultAsDict     = null;
                        List <EntityType>        entityTypeAsList = new List <EntityType> {
                            entityType
                        };
                        SecurityBypassContext.RunAsUser(() =>
                                                        resultAsDict = Checker.CheckTypeAccess(entityTypeAsList, permission, user));

                        result = resultAsDict [entityType.Id];

                        if (result)
                        {
                            return(result);
                        }
                    }

                    // Walk graph to get list of types to check
                    entityTypesToCheck = ReachableTypes(entityType.ToEnumerable( ), true, false)
                                         .Select(walkStep => walkStep.Node)
                                         .Where(et => et != entityType) // skip root type.
                                         .ToList( );

                    result = CheckTypeAccessRelatedTypesImpl(entityType, permission, user, entityTypesToCheck);
                    if (!result)
                    {
                        return(false);
                    }

                    // Register cache invalidations
                    using (CacheContext cacheContext = CacheContext.GetContext( ))
                    {
                        cacheContext.EntityTypes.Add(WellKnownAliases.CurrentTenant.Relationship);
                        cacheContext.FieldTypes.Add(WellKnownAliases.CurrentTenant.SecuresFrom, WellKnownAliases.CurrentTenant.SecuresTo, WellKnownAliases.CurrentTenant.SecuresFromReadOnly, WellKnownAliases.CurrentTenant.SecuresToReadOnly);
                    }
                }

            return(result);
        }
Example #5
0
        /// <summary>
        /// Convert a <see cref="Report" /> to a <see cref="StructuredQuery" />.
        /// </summary>
        /// <param name="report">The <see cref="Report" /> to convert. This cannot be null.</param>
        /// <param name="settings"></param>
        /// <returns>
        /// The converted report.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">report</exception>
        public StructuredQuery Convert(Report report, ReportToQueryConverterSettings settings)
        {
            if (report == null)
            {
                throw new ArgumentNullException("report");
            }
            if (settings == null)
            {
                settings = ReportToQueryConverterSettings.Default;
            }

            StructuredQuery result;
            CachingReportToQueryConverterValue cacheValue;

            CachingReportToQueryConverterKey key = new CachingReportToQueryConverterKey(report, settings);

            using (MessageContext msg = new MessageContext("Reports"))
            {
                // Check cache
                bool doConvert = !TryGetValue(key, msg, out cacheValue);

                // Check for force recalculation
                if (settings.RefreshCachedStructuredQuery)
                {
                    msg.Append(() => "CachingReportToQueryConverter refreshed forced");
                    doConvert = true;
                }

                if (doConvert)
                {
                    lock (_syncRoot)
                    {
                        using (CacheContext cacheContext = new CacheContext( ))
                        {
                            result     = Converter.Convert(report, settings);
                            cacheValue = new CachingReportToQueryConverterValue(result);

                            Cache.Add(key, cacheValue);

                            // Add the cache context entries to the appropriate CacheInvalidator
                            _cacheInvalidator.AddInvalidations(cacheContext, key);
                        }
                    }
                }
                else if (CacheContext.IsSet( ))
                {
                    // Add the already stored changes that should invalidate this cache
                    // entry to any outer or containing cache contexts.
                    using (CacheContext cacheContext = CacheContext.GetContext( ))
                    {
                        cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                    }
                }
            }

            result = cacheValue.StructuredQuery;
            return(result);
        }
        /// <summary>
        /// Gets a value that represents a hash of the IDs of a set of security rules.
        /// That is, if two users have equatable UserRuleSets then the same security rules apply to both.
        /// </summary>
        /// <param name="userId">
        /// The ID of the user <see cref="UserAccount"/>.
        /// This cannot be negative.
        /// </param>
        /// <param name="permission">
        /// The permission to get the query for. This cannot be null and should be one of <see cref="Permissions.Read"/>,
        /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>.
        /// </param>
        public UserRuleSet GetUserRuleSet(long userId, EntityRef permission)
        {
            if (permission == null)
            {
                throw new ArgumentNullException("permission");
            }

            // Explicitly register cache invalidations
            if (CacheContext.IsSet( ))
            {
                using (CacheContext cacheContext = CacheContext.GetContext( ))
                {
                    cacheContext.EntityTypes.Add(
                        new EntityRef("core:accessRule").Id,
                        new EntityRef("core:role").Id);
                    cacheContext.RelationshipTypes.Add(
                        new EntityRef("core:userHasRole").Id,
                        new EntityRef("core:includesRoles").Id,
                        new EntityRef("core:allowAccess").Id);
                }
            }

            using (new SecurityBypassContext( ))
                using (new CacheContext(ContextType.None))  // suppress any inner invalidations.
                {
                    // Calculate all subjects
                    IEnumerable <long> allRoles = _userRoleRepository.GetUserRoles(userId);
                    if (allRoles == null)
                    {
                        throw new InvalidOperationException("userRoleRepository.GetUserRoles returned null.");
                    }

                    IEnumerable <long> allSubjects = allRoles.Concat(userId.ToEnumerable( ));

                    // Determine all applicable rules
                    ISet <long> allRules = new HashSet <long>( );
                    foreach (long subjectId in allSubjects)
                    {
                        ICollection <AccessRule> rules = _ruleRepository.GetAccessRules(subjectId, permission, null);
                        if (rules == null)
                        {
                            throw new InvalidOperationException("ruleRepository.GetAccessRules returned null.");
                        }

                        var ruleIDs = rules.WhereNotNull( ).Select(accessRule => accessRule.Id);

                        allRules.UnionWith(ruleIDs);
                    }

                    // Calculate key
                    return(new UserRuleSet(allRules));
                }
        }
Example #7
0
        public void Test_GetContext_NoContext()
        {
            CacheContext cacheContext;

            using (cacheContext = CacheContext.GetContext( ))
            {
                Assert.That(cacheContext, Has.Property("ContextType").EqualTo(ContextType.Detached));
                Assert.That(cacheContext, Has.Property("Entities").Count.EqualTo(0));
                Assert.That(cacheContext, Has.Property("RelationshipTypes").Count.EqualTo(0));
                Assert.That(cacheContext, Has.Property("FieldTypes").Count.EqualTo(0));
                Assert.That(cacheContext, Has.Property("EntityInvalidatingRelationshipTypes").Count.EqualTo(0));
            }
        }
Example #8
0
        /// <summary>
        /// Build an <see cref="EntityMemberRequest"/> used to look for entities
        /// to perform additional security checks on.
        /// </summary>
        /// <param name="entityType">
        /// The type of entity whose security is being checked. This cannot be null.
        /// </param>
        /// <param name="permissions">The type of permissions required.</param>
        /// <returns>
        /// The <see cref="EntityMemberRequest"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="entityType"/> cannot be null.
        /// </exception>
        public EntityMemberRequest BuildEntityMemberRequest(EntityType entityType, IList <EntityRef> permissions)
        {
            if (entityType == null)
            {
                throw new ArgumentNullException("entityType");
            }

            bool isModify = false;

            if (permissions != null)
            {
                foreach (EntityRef perm in permissions)
                {
                    if (perm.Id == Permissions.Modify.Id || perm.Id == Permissions.Delete.Id)
                    {
                        isModify = true;
                        break;
                    }
                }
            }

            // negative keys for 'modify', +ve keys for read
            // because #27911 popped up just hours before the branch of a feature that got promised to someone
            long cacheKey = isModify ? -entityType.Id : entityType.Id;

            EntityMemberRequest result;

            if (!Cache.TryGetValue(cacheKey, out result))
            {
                using (CacheContext cacheContext = new CacheContext())
                {
                    result = Factory.BuildEntityMemberRequest(entityType, permissions);

                    Cache.Add(cacheKey, result);

                    _cacheInvalidator.AddInvalidations(cacheContext, cacheKey);
                }
            }
            else if (CacheContext.IsSet( ))
            {
                // Add the already stored changes that should invalidate this cache
                // entry to any outer or containing cache contexts.
                using (CacheContext cacheContext = CacheContext.GetContext( ))
                {
                    cacheContext.AddInvalidationsFor(_cacheInvalidator, cacheKey);
                }
            }

            return(result);
        }
        /// <summary>
        /// Gets a value that represents a hash of the IDs of a set of security rules.
        /// That is, if two users have equatable UserRuleSets then the same security rules apply to both.
        /// </summary>
        /// <param name="userId">
        /// The ID of the user <see cref="UserAccount"/>.
        /// This cannot be negative.
        /// </param>
        /// <param name="permission">
        /// The permission to get the query for. This cannot be null and should be one of <see cref="Permissions.Read"/>,
        /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>.
        /// </param>
        public UserRuleSet GetUserRuleSet(long userId, EntityRef permission)
        {
            // Validate
            if (userId <= 0)
            {
                throw new ArgumentNullException("userId");
            }
            if (permission == null)
            {
                throw new ArgumentNullException("permission");
            }
            if (permission.Id <= 0)
            {
                throw new ArgumentException("permission");
            }

            // Create cache key
            CachingUserRuleSetProviderKey key = new CachingUserRuleSetProviderKey(userId, permission.Id);
            UserRuleSet result;

            Func <CachingUserRuleSetProviderKey, UserRuleSet> valueFactory = (k) =>
            {
                UserRuleSet innerResult;

                using (CacheContext cacheContext = new CacheContext())
                {
                    innerResult = InnerProvider.GetUserRuleSet(k.SubjectId, k.PermissionId);

                    // Add the cache context entries to the appropriate CacheInvalidator
                    _cacheInvalidator.AddInvalidations(cacheContext, key);
                }

                return(innerResult);
            };

            // Check cache
            if (Cache.TryGetOrAdd(key, out result, valueFactory) &&
                CacheContext.IsSet())
            {
                // Add the already stored changes that should invalidate this cache
                // entry to any outer or containing cache contexts.
                using (CacheContext cacheContext = CacheContext.GetContext( ))
                {
                    cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                }
            }

            return(result);
        }
Example #10
0
        /// <summary>
        /// /// Get the queries for a given user and permission or operation.
        /// </summary>
        /// <param name="subjectId">
        /// The ID of the <see cref="Subject"/>, that is a <see cref="UserAccount"/> or <see cref="Role"/> instance.
        /// This cannot be negative.
        /// </param>
        /// <param name="permission">
        /// The permission to get the query for. This should be one of <see cref="Permissions.Read"/>,
        /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>. Or null to match all permissions.
        /// </param>
        /// <param name="securableEntityTypes">
        /// The IDs of <see cref="SecurableEntity"/> types being accessed. Or null to match all entity types.
        /// </param>
        /// <returns>
        /// The queries to run.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="subjectId"/> does not exist. Also, <paramref name="permission"/> should
        /// be one of <see cref="Permissions.Read"/>, <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>
        /// </exception>
        public IEnumerable <AccessRuleQuery> GetQueries(long subjectId, [CanBeNull] EntityRef permission, [CanBeNull] IList <long> securableEntityTypes)
        {
            // Get all applicable access rules for this subject/permission/types
            ICollection <AccessRule> accessRules = RuleRepository.GetAccessRules(subjectId, permission, securableEntityTypes);

            IList <AccessRuleQuery> result = new List <AccessRuleQuery>( );

            // Store the enties that, when changed, should invalidate this cache entry.
            using (CacheContext cacheContext = CacheContext.GetContext())
                using (new SecurityBypassContext())
                {
                    foreach (AccessRule allowAccess in accessRules)
                    {
                        Report accessRuleReport = allowAccess.AccessRuleReport;
                        if (accessRuleReport == null)
                        {
                            continue;
                        }
                        if (allowAccess.ControlAccess == null)
                        {
                            continue;
                        }

                        // Load the report query graph
                        Report accessRuleReportGraph = ReportEntityRepository.Get <Report>(allowAccess.AccessRuleReport.Id, ReportHelpers.QueryPreloaderQuery);

                        // Convert the report to a structured query
                        StructuredQuery accessRuleReportQuery = Converter.Convert(accessRuleReportGraph, ConverterSettings);
                        StructuredQueryHelper.OptimiseAuthorisationQuery(accessRuleReportQuery);

                        // Add cache invalidations
                        // Consider using .. StructuredQueryHelper.IdentifyStructureCacheDependencies
                        // See also: cacheContext in EntityAccessControlChecker.CheckAccessControlByQuery
                        cacheContext.Entities.Add(accessRuleReport.Id);

                        // Should this rule be considered for reports?
                        bool ignoreForReports = allowAccess.AccessRuleIgnoreForReports == true;

                        // Create container object
                        AccessRuleQuery accessRuleQuery = new AccessRuleQuery(allowAccess.Id, accessRuleReport.Id, allowAccess.ControlAccess.Id, accessRuleReportQuery, ignoreForReports);

                        result.Add(accessRuleQuery);
                    }
                }

            return(result);
        }
        /// <summary>
        /// Build an <see cref="EntityMemberRequest"/> used to look for entities
        /// to perform additional security checks on.
        /// </summary>
        /// <param name="entityType">The type of entity whose security is being checked.</param>
        /// <param name="permissions">The type of permissions required.</param>
        /// <returns>The <see cref="EntityMemberRequest"/></returns>
        public EntityMemberRequest BuildEntityMemberRequest(EntityType entityType, IList <EntityRef> permissions)
        {
            if (entityType == null)
            {
                throw new ArgumentNullException(nameof(entityType));
            }

            // Should we be following all security relationships, or only ones that convey modify perms.
            bool isModify = false;

            if (permissions != null)
            {
                foreach (EntityRef perm in permissions)
                {
                    if (perm.Id == Permissions.Modify.Id || perm.Id == Permissions.Delete.Id)
                    {
                        isModify = true;
                        break;
                    }
                }
            }

            // Create context
            FactoryContext context = new FactoryContext
            {
                IsModify    = isModify,
                InitialType = entityType
            };

            // Walk graph
            IEqualityComparer <EntityType> comparer = new EntityIdEqualityComparer <EntityType>( );

            Delegates.WalkGraph(entityType, node => VisitNode(node, context), null, comparer).VisitAll( );

            // Register cache invalidations
            using (CacheContext cacheContext = CacheContext.GetContext( ))
            {
                cacheContext.EntityTypes.Add(WellKnownAliases.CurrentTenant.Relationship);
                cacheContext.FieldTypes.Add(WellKnownAliases.CurrentTenant.SecuresFrom, WellKnownAliases.CurrentTenant.SecuresTo, WellKnownAliases.CurrentTenant.SecuresFromReadOnly, WellKnownAliases.CurrentTenant.SecuresToReadOnly);
            }

            return(context.Result);
        }
Example #12
0
        public void Test_GetContext()
        {
            Assert.That(CacheContext.IsSet(), Is.False, "Set initially");

            using (CacheContext newCacheContext = new CacheContext())
            {
                using (CacheContext attachedCacheContext = CacheContext.GetContext())
                {
                    Assert.That(newCacheContext.Entities,
                                Is.EquivalentTo(attachedCacheContext.Entities), "Entities mismatch");
                    Assert.That(attachedCacheContext.ContextType,
                                Is.EqualTo(ContextType.Attached), "Incorrect context type");
                }

                Assert.That(CacheContext.IsSet(), Is.True, "Attached Dispose() removed context");
            }

            Assert.That(CacheContext.IsSet(), Is.False, "New Dispose() did not remove context");
        }
Example #13
0
        /// <summary>
        /// Get the queries for a given user and permission or operation.
        /// </summary>
        /// <param name="subjectId">
        /// The ID of the <see cref="Subject"/>, that is a <see cref="UserAccount"/> or <see cref="Role"/> instance.
        /// This cannot be negative.
        /// </param>
        /// <param name="permission">
        /// The permission to get the query for. This should be one of <see cref="Permissions.Read"/>,
        /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>. Or null to match all permissions.
        /// </param>
        /// <param name="securableEntityTypes">
        /// The IDs of <see cref="SecurableEntity"/> types being accessed. Or null to match all entity types.
        /// </param>
        /// <returns>
        /// The queries to run.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="subjectId"/> does not exist. Also, <paramref name="permission"/> should
        /// be one of <see cref="Permissions.Read"/>, <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// Neither <paramref name="permission"/> nor <paramref name="securableEntityTypes"/> can be null.
        /// </exception>
        public IEnumerable <AccessRuleQuery> GetQueries(long subjectId, [CanBeNull] Model.EntityRef permission, [CanBeNull] IList <long> securableEntityTypes)
        {
            SubjectPermissionTypesTuple   tuple;
            IEnumerable <AccessRuleQuery> result;

            result = null;
            tuple  = new SubjectPermissionTypesTuple(subjectId, permission, securableEntityTypes);

            if (!Cache.TryGetValue(tuple, out result))
            {
                using (CacheContext cacheContext = new CacheContext( ))
                {
                    result = Repository.GetQueries(subjectId, permission, securableEntityTypes).ToList( );
                    try
                    {
                        Cache.Add(tuple, result);

                        // Add the cache context entries to the appropriate CacheInvalidator
                        _cacheInvalidator.AddInvalidations(cacheContext, tuple);
                    }
                    catch (ArgumentException)
                    {
                        Trace.WriteLine("CachingQueryRepository: Entity already in cache");
                        throw;
                    }
                }
            }
            else if (CacheContext.IsSet( ))
            {
                using (CacheContext cacheContext = CacheContext.GetContext( ))
                {
                    // Add the already stored changes that should invalidate this cache
                    // entry to any outer or containing cache contexts.
                    cacheContext.AddInvalidationsFor(_cacheInvalidator, tuple);
                }
            }

            return(result);
        }
        /// <summary>
        /// Convert a <see cref="Report" /> to a <see cref="StructuredQuery" />.
        /// </summary>
        /// <param name="typeIds">The type ids.</param>
        /// <returns>
        /// The converted report.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">report</exception>
        public IEnumerable <Workflow> Fetch(ISet <long> typeIds)
        {
            if (typeIds == null)
            {
                throw new ArgumentNullException("typeIds");
            }


            CachingWorkflowActionsFactoryValue cacheValue;

            var key = new CachingWorkflowActionsFactoryKey(typeIds);

            // Check cache
            bool found = Cache.TryGetOrAdd(key, out cacheValue, (innerKey) =>
            {
                using (CacheContext cacheContext = new CacheContext( ))
                {
                    var result = new CachingWorkflowActionsFactoryValue(_fetcher.Fetch(innerKey.TypeIds));

                    // Add the cache context entries to the appropriate CacheInvalidator
                    _cacheInvalidator.AddInvalidations(cacheContext, key);
                    return(result);
                }
            }
                                           );

            if (found && CacheContext.IsSet( ))
            {
                // Add the already stored changes that should invalidate this cache
                // entry to any outer or containing cache contexts.
                using (CacheContext cacheContext = CacheContext.GetContext( ))
                {
                    cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                }
            }

            return(cacheValue.Workflows);
        }
        /// <summary>
        /// Check whether the queries for the specified <paramref name="permission"/> allow the <paramref name="subjectId"/>
        /// access to <paramref name="entities"/> using the related security queries. Used for read, modify and delete
        /// permissions.
        /// </summary>
        /// <param name="subjectId">
        /// The ID of the subject (user or role).
        /// </param>
        /// <param name="permission">
        /// The permission (operation). This cannot be null.
        /// </param>
        /// <param name="entityType">
        /// The type ID of the entities to check. This cannot be null.
        /// </param>
        /// <param name="entities">
        /// The entities to check. This cannot be null or contain null.
        /// </param>
        /// <param name="allEntities">
        /// All entities.
        /// </param>
        /// <param name="queryResultsCache">
        /// The query results cache.
        /// </param>
        /// <param name="result">
        /// The map of entity IDs to whether the relationship exists.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// No argument can be null.
        /// </exception>
        internal void CheckAccessControlByQuery(long subjectId, EntityRef permission,
                                                long entityType, IList <EntityRef> entities, ISet <long> allEntities,
                                                IDictionary <long, ISet <long> > queryResultsCache, IDictionary <long, bool> result)
        {
            using (Profiler.MeasureAndSuppress("CheckAccessControlByQuery"))
            {
                if (permission == null)
                {
                    throw new ArgumentNullException("permission");
                }
                if (result == null)
                {
                    throw new ArgumentNullException("result");
                }
                if (entities == null)
                {
                    throw new ArgumentNullException("entities");
                }
                if (allEntities == null)
                {
                    throw new ArgumentNullException("allEntities");
                }
                if (queryResultsCache == null)
                {
                    throw new ArgumentNullException("queryResultsCache");
                }
                if (entities.Contains(null))
                {
                    throw new ArgumentException("Cannot check access for null entities", "entities");
                }

                IEnumerable <AccessRuleQuery> queries;

                // Allow access to temporary IDs
                if (AllowAccessToTemporaryIds(result))
                {
                    return;
                }

                using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName))
                {
                    queries = QueryRepository.GetQueries(subjectId, permission, new[] { entityType });
                    QueryResult queryResult;

                    // Check if any queries grant access to all instances of the type
                    StructuredQuery shortCircuitQuery = CheckIfAnyQueryProvideAccessToAllInstancesOfType(entityType, queries, entities, result);
                    if (shortCircuitQuery != null)
                    {
                        messageContext.Append(
                            () => string.Format(
                                "{0} allowed '{1}' access to entities '{2}' because it allows access to all instances of the type.",
                                AccessControlDiagnosticsHelper.GetAccessRuleName(shortCircuitQuery),
                                Permissions.GetPermissionByAlias(permission),
                                string.Join(", ", entities.Select(x => x.ToString()))));
                        return;
                    }

                    long securityOwnerRelId = WellKnownAliases.CurrentTenant.SecurityOwner;

                    foreach (AccessRuleQuery accessRuleQuery in queries)
                    {
                        StructuredQuery structuredQuery = accessRuleQuery.Query;
                        var             allowedEntities = new HashSet <long>();
                        ISet <long>     queryResultSet;

                        if (!queryResultsCache.TryGetValue(accessRuleQuery.ReportId, out queryResultSet))
                        {
                            var querySettings = new QuerySettings
                            {
                                SecureQuery = false,
                                Hint        = "security - " + Name
                            };

                            bool filtered = false;

                            if (allEntities.Count <= MaximumNumberOfFilteredEntities)
                            {
                                filtered = true;
                                querySettings.SupportRootIdFilter = true;
                                querySettings.RootIdFilterList    = allEntities;
                            }

                            queryResult = null;
                            try
                            {
                                using (MessageContext msg = new MessageContext("Reports", MessageContextBehavior.New))
                                {
                                    queryResult = Factory.QueryRunner.ExecuteQuery(structuredQuery, querySettings);
                                }
                            }
                            catch (Exception ex)
                            {
                                AccessControlDiagnosticsHelper.WriteInvalidSecurityReportMessage(structuredQuery, messageContext, ex);
                            }

                            queryResultSet = new HashSet <long>();

                            if (queryResult != null && QueryInspector.IsQueryUndamaged(structuredQuery))
                            {
                                foreach (DataRow dataRow in queryResult.DataTable.Rows)
                                {
                                    var id = dataRow.Field <long>(0);
                                    if (filtered || allEntities.Contains(id))
                                    {
                                        queryResultSet.Add(id);
                                    }
                                }
                            }
                            else
                            {
                                if (queryResult != null)
                                {
                                    AccessControlDiagnosticsHelper.WriteInvalidSecurityReportMessage(structuredQuery, messageContext);
                                }
                            }

                            queryResultsCache[accessRuleQuery.ReportId] = queryResultSet;
                        }

                        foreach (EntityRef entityRef in entities)
                        {
                            if (queryResultSet.Contains(entityRef.Id) &&
                                result.ContainsKey(entityRef.Id))
                            {
                                allowedEntities.Add(entityRef.Id);
                                result[entityRef.Id] = true;
                            }
                        }

                        // ReSharper disable AccessToForEachVariableInClosure
                        // ReSharper disable SpecifyACultureInStringConversionExplicitly
                        if (allowedEntities.Count > 0)
                        {
                            messageContext.Append(
                                () => string.Format(
                                    "{0} allowed '{1}' access to entities '{2}' out of '{3}'",
                                    AccessControlDiagnosticsHelper.GetAccessRuleName(structuredQuery),
                                    Permissions.GetPermissionByAlias(permission),
                                    string.Join(", ",
                                                allowedEntities.Select(x => x.ToString())),
                                    string.Join(", ", entities.Select(x => x.ToString()))));
                        }
                        else
                        {
                            messageContext.Append(
                                () => string.Format(
                                    "{0} returned no results for '{1}' access to entities '{2}'",
                                    AccessControlDiagnosticsHelper.GetAccessRuleName(structuredQuery),
                                    Permissions.GetPermissionByAlias(permission),
                                    string.Join(", ", entities.Select(x => x.ToString()))));
                        }
                        // ReSharper restore AccessToForEachVariableInClosure
                        // ReSharper restore SpecifyACultureInStringConversionExplicitly

                        // Set the cache invalidation information
                        using (CacheContext cacheContext = CacheContext.GetContext())
                        {
                            // ******************* TEMPORARY WORKAROUND ***********************
                            // Until we properly implement filtering the invalidating relationships and fields by type
                            // we will ignore invalidating on the security owner relationship

                            cacheContext.RelationshipTypes.Add(
                                StructuredQueryHelper.GetReferencedRelationships(structuredQuery).Where(er => er.Id != securityOwnerRelId).Select(er => er.Id));
                            cacheContext.FieldTypes.Add(
                                StructuredQueryHelper.GetReferencedFields(structuredQuery, true, true).Select(er => er.Id));
                        }
                    }
                }
            }
        }
        /// <summary>
        /// Check whether the user has access to the entities by following relationships where the
        /// <see cref="Relationship"/> type has the <see cref="Relationship.SecuresTo"/> or
        /// <see cref="Relationship.SecuresFrom"/> flag set.
        /// </summary>
        /// <param name="permissions">
        /// The permissions to check for. This cannot be null or contain null.
        /// </param>
        /// <param name="user">
        /// The user to do the check access for. This cannot be null.
        /// </param>
        /// <param name="entitiesToCheck">
        /// The entities to check. This cannot be null or contain null.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// No argument can be null.
        /// </exception>
        internal ISet <EntityRef> CheckAccessControlByRelationship(IList <EntityRef> permissions, EntityRef user,
                                                                   ISet <EntityRef> entitiesToCheck)
        {
            if (permissions == null)
            {
                throw new ArgumentNullException("permissions");
            }
            if (permissions.Contains(null))
            {
                throw new ArgumentNullException("permissions", "Cannot contain null");
            }
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }
            if (entitiesToCheck == null)
            {
                throw new ArgumentNullException("entitiesToCheck");
            }

            EntityMemberRequest entityMemberRequest;
            IDictionary <long, ISet <EntityRef> > entityTypes;
            IEnumerable <EntityData> entitiesData;
            IDictionary <long, bool> accessToRelatedEntities;
            HashSet <EntityRef>      result;
            IList <long>             relatedAccessibleEntities;
            EntityType entityType;

            using (Profiler.MeasureAndSuppress("SecuresFlagEntityAccessControlChecker.CheckAccessControlByRelationship"))
            {
                result = new HashSet <EntityRef>(EntityRefComparer.Instance);

                entityTypes = TypeRepository.GetEntityTypes(entitiesToCheck);
                using (MessageContext outerMessageContext = new MessageContext(EntityAccessControlService.MessageName))
                    foreach (KeyValuePair <long, ISet <EntityRef> > entitiesType in entityTypes)
                    {
                        outerMessageContext.Append(() =>
                                                   string.Format(
                                                       "Checking relationships for entity(ies) \"{0}\" of type \"{1}\":",
                                                       string.Join(", ", entitiesType.Value.Select(er => string.Format("'{0}' ({1})", er.Entity.As <Resource>().Name, er.Id))),
                                                       string.Join(", ", string.Format("'{0}' ({1})", Entity.Get <Resource>(entitiesType.Key).Name, entitiesType.Key))));

                        using (MessageContext innerMessageContext = new MessageContext(EntityAccessControlService.MessageName))
                        {
                            entityType = Entity.Get <EntityType>(entitiesType.Key);
                            if (entityType != null)
                            {
                                entityMemberRequest = EntityMemberRequestFactory.BuildEntityMemberRequest(entityType, permissions);
                                if (entityMemberRequest.Relationships.Count > 0)
                                {
                                    IList <EntityRef> relatedEntitiesToCheck;

                                    innerMessageContext.Append(() => string.Format("Security relationship structure for entity type '{0}':", entityType.Id));
                                    TraceEntityMemberRequest(entityMemberRequest);

                                    // Get the IDs of entities to check security for
                                    EntityRequest request = new EntityRequest
                                    {
                                        Entities          = entitiesType.Value,
                                        Request           = entityMemberRequest,
                                        IgnoreResultCache = true    // security engine does its own result caching
                                    };
                                    entitiesData = BulkRequestRunner.GetEntitiesData(request).ToList( );

                                    // Do a single security check for all entities related to
                                    // the entities passed in, excluding the original entities
                                    // that failed the security check.
                                    relatedEntitiesToCheck = Delegates
                                                             .WalkGraph(
                                        entitiesData,
                                        entityData =>
                                        entityData.Relationships.SelectMany(relType => relType.Entities))
                                                             .Select(ed => ed.Id)
                                                             .Where(er => !entitiesType.Value.Contains(er, EntityRefComparer.Instance))
                                                             .ToList();
                                    if (relatedEntitiesToCheck.Count > 0)
                                    {
                                        // Add the relationship types to watch for cache invalidations
                                        using (CacheContext cacheContext = CacheContext.GetContext())
                                        {
                                            IList <long> relationshipTypes = Delegates
                                                                             .WalkGraph(entityMemberRequest.Relationships,
                                                                                        rr => rr.RequestedMembers.Relationships)
                                                                             .Select(rr => rr.RelationshipTypeId.Id)
                                                                             .ToList();

                                            cacheContext.RelationshipTypes.Add(relationshipTypes);
                                        }

                                        // ReSharper disable AccessToModifiedClosure
                                        // Do a single access check for all entities for efficiency, then pick the
                                        // important ones for each requested entity below.
                                        accessToRelatedEntities = null;
                                        innerMessageContext.Append(
                                            () => string.Format(
                                                "Checking related entities '{0}':",
                                                string.Join(", ", relatedEntitiesToCheck.Select(er => er.Id))));
                                        SecurityBypassContext.RunAsUser(
                                            () =>
                                            accessToRelatedEntities =
                                                Checker.CheckAccess(relatedEntitiesToCheck, permissions, user));
                                        // ReSharper restore AccessToModifiedClosure

                                        foreach (EntityData entityData in entitiesData)
                                        {
                                            // Get the related entities to check
                                            relatedEntitiesToCheck = Delegates.WalkGraph(
                                                entityData,
                                                ed => ed.Relationships.SelectMany(relType => relType.Entities))
                                                                     .Select(ed => ed.Id)
                                                                     .ToList();

                                            // Remove the start entity for the query, since security has
                                            // already been checked on it.
                                            relatedEntitiesToCheck.Remove(entityData.Id);

                                            // Get the related entities the user has access to
                                            relatedAccessibleEntities = accessToRelatedEntities
                                                                        .Where(kvp => kvp.Value && relatedEntitiesToCheck.Contains(kvp.Key, EntityRefComparer.Instance))
                                                                        .Select(kvp => kvp.Key)
                                                                        .ToList();

                                            // Grant access if the user has access to ANY of the related
                                            // entities.
                                            if (relatedEntitiesToCheck.Count > 0 &&
                                                relatedAccessibleEntities.Count > 0)
                                            {
                                                result.Add(entityData.Id);

                                                // ReSharper disable AccessToModifiedClosure
                                                innerMessageContext.Append(
                                                    () => string.Format(
                                                        "Access to '{0}' granted due to corresponding access to '{1}'",
                                                        string.Join(", ", relatedEntitiesToCheck.Select(id => string.Format("'{0}' ({1})", Entity.Get <Resource>(id).Name, id))),
                                                        string.Join(", ", relatedAccessibleEntities.Select(id => string.Format("'{0}' ({1})", Entity.Get <Resource>(id).Name, id)))));
                                                // ReSharper restore AccessToModifiedClosure
                                            }
                                        }
                                    }
                                }
                                else
                                {
                                    innerMessageContext.Append(() => string.Format("No relationships found to check for entity type '{0}'.", entityType.Id));
                                }
                            }
                            else
                            {
                                EventLog.Application.WriteWarning("Type ID {0} for entities '{1}' is not a type",
                                                                  entitiesType.Key, string.Join(", ", entitiesType.Value));
                            }
                        }
                    }
            }

            return(result);
        }
Example #17
0
        /// <summary>
        /// Re-used cache checking logic that can be used for caching eithe instances lookups or type lookups.
        /// </summary>
        /// <param name="entities">The entities/types to check. This cannot be null or contain null.</param>
        /// <param name="permissions">The permissions or operations to check. This cannot be null or contain null.</param>
        /// <param name="user">The user requesting access. This cannot be null.</param>
        /// <param name="entityIdCallback">Callback used to determine how to get the ID of TEntity.</param>
        /// <param name="innerCheckAccessCallback">Callback used to perform the uncached check.</param>
        /// <returns>A mapping of each entity ID to whether the user has access (true) or not (false).</returns>
        private IDictionary <long, bool> CachedCheckImpl <TEntity>(IList <TEntity> entities, IList <EntityRef> permissions, EntityRef user,
                                                                   Func <TEntity, long> entityIdCallback,
                                                                   Func <IList <TEntity>, IList <EntityRef>, EntityRef, IDictionary <long, bool> > innerCheckAccessCallback)
        {
            TKey cacheKey;
            CachingEntityAccessControlCheckerResult result;
            IList <TEntity> toCheckEntities;

            long[] permissionIdArray;
            IDictionary <long, bool> innerResult;

            // If SecurityBypassContext is active, avoid the cache. Otherwise,
            // the cache will remember the user had access to entities that
            // they may not have.
            if (EntityAccessControlChecker.SkipCheck(user))
            {
                innerResult = innerCheckAccessCallback(entities, permissions, user);
                return(innerResult);
            }

            result            = new CachingEntityAccessControlCheckerResult();
            toCheckEntities   = null;
            permissionIdArray = permissions.Select(x => x.Id).ToArray();


            // Determine uncached entities
            using (CacheContext cacheContext = CacheContext.IsSet( ) ? CacheContext.GetContext( ) : null)
            {
                foreach (TEntity entity in entities)
                {
                    long entityId = entityIdCallback(entity);

                    cacheKey = CreateKey(user.Id, entityId, permissionIdArray);
                    bool cacheEntry;

                    if (Cache.TryGetValue(cacheKey, out cacheEntry))
                    {
                        result.CacheResult[entityId] = cacheEntry;

                        // Add the already stored changes that should invalidate this cache
                        // entry to any outer or containing cache contexts.
                        if (cacheContext != null)
                        {
                            cacheContext.AddInvalidationsFor(_cacheInvalidator, cacheKey);
                        }
                    }
                    else
                    {
                        if (toCheckEntities == null)
                        {
                            toCheckEntities = new List <TEntity>();
                        }
                        toCheckEntities.Add(entity);
                    }
                }
            }

            LogMessage(result);

            if (toCheckEntities != null)
            {
                using (CacheContext cacheContext = new CacheContext( ))
                {
                    innerResult = innerCheckAccessCallback(toCheckEntities, permissions, user);

                    foreach (KeyValuePair <long, bool> entry in innerResult)
                    {
                        long entityId      = entry.Key;
                        bool accessGranted = entry.Value;

                        result.Add(entityId, accessGranted);

                        // Cache the results
                        cacheKey         = CreateKey(user.Id, entry.Key, permissionIdArray);
                        Cache [cacheKey] = accessGranted;

                        // Add the cache context entries to the appropriate CacheInvalidator
                        _cacheInvalidator.AddInvalidations(cacheContext, cacheKey);
                    }
                }
            }

            // Add the results from the originally cached entities
            foreach (KeyValuePair <long, bool> entry in result.CacheResult)
            {
                result[entry.Key] = entry.Value;
            }

            return(result);
        }
        /// <summary>
        ///     Gets the request context data factory implementation.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>RequestContextData.</returns>
        private IdentityProviderContextCacheValue GetRequestContextDataFactoryImpl(IdentityProviderContextCacheKey key)
        {
            IdentityProviderContextCacheValue cacheValue;

            var isSystem = key.IdentityProviderId == WellKnownAliases.CurrentTenant.ReadiNowIdentityProviderInstance;

            QueryBuild queryResult = GetSql(isSystem);

            using (var dbContext = DatabaseContext.GetContext())
            {
                using (var command = dbContext.CreateCommand(queryResult.Sql))
                {
                    command.AddParameterWithValue("@identityProviderUser", key.IdentityProviderUserName, 500);

                    if (!isSystem)
                    {
                        command.AddParameterWithValue("@identityProviderId", key.IdentityProviderId);
                    }
                    dbContext.AddParameter(command, "@tenant", DbType.Int64, key.TenantId);

                    if (queryResult.SharedParameters != null)
                    {
                        foreach (var parameter in queryResult.SharedParameters)
                        {
                            dbContext.AddParameter(command, parameter.Value, parameter.Key.Type, parameter.Key.Value);
                        }
                    }

                    using (var reader = command.ExecuteReader())
                    {
                        if (!reader.Read())
                        {
                            return(null);
                        }

                        var userAccountId             = reader.GetInt64(0);
                        var userAccountName           = reader.GetString(1);
                        var identityProviderId        = isSystem ? key.IdentityProviderId : reader.GetInt64(2);
                        var identityProviderUserId    = isSystem ? userAccountId : reader.GetInt64(3);
                        var identityProviderTypeAlias = isSystem ? "core:readiNowIdentityProvider" : "core:" + reader.GetString(4);

                        int  accountStatusColumnIndex = isSystem ? 2 : 5;
                        long accountStatusId          = -1;

                        if (!reader.IsDBNull(accountStatusColumnIndex))
                        {
                            accountStatusId = reader.GetInt64(accountStatusColumnIndex);
                        }

                        var identityInfo = new IdentityInfo(userAccountId, userAccountName)
                        {
                            IdentityProviderId        = identityProviderId,
                            IdentityProviderUserId    = identityProviderUserId,
                            IdentityProviderTypeAlias = identityProviderTypeAlias
                        };
                        var tenantInfo = new TenantInfo(key.TenantId);

                        var contextData = new RequestContextData(identityInfo, tenantInfo,
                                                                 CultureHelper.GetUiThreadCulture(CultureType.Specific));

                        cacheValue = new IdentityProviderContextCacheValue(contextData, accountStatusId);

                        if (CacheContext.IsSet())
                        {
                            using (CacheContext cacheContext = CacheContext.GetContext())
                            {
                                cacheContext.Entities.Add(userAccountId);
                                cacheContext.Entities.Add(identityProviderId);
                                if (identityProviderUserId != userAccountId)
                                {
                                    cacheContext.Entities.Add(identityProviderUserId);
                                }
                            }
                        }
                    }
                }
            }

            return(cacheValue);
        }
        /// <summary>
        /// Helper method to fetch multiple entries from cache, then only call the factory for missing entries.
        /// </summary>
        /// <remarks>
        /// Implementation notes:
        /// 1. The result is guaranteed to always be non-null
        /// 2. And contain the same entries as the keys in the same order.
        /// 3. All misses get evaluated in a single round
        /// 4. Caution: because all misses are evaluated together, they get bound together with the same invalidation.
        /// </remarks>
        /// <param name="keys"></param>
        /// <param name="valuesFactory"></param>
        /// <returns></returns>
        protected IReadOnlyCollection <KeyValuePair <TKey, TValue> > GetOrAddMultiple(IEnumerable <TKey> keys, Func <IEnumerable <TKey>, IEnumerable <TValue> > valuesFactory)
        {
            if (keys == null)
            {
                throw new ArgumentNullException("keys");
            }
            if (valuesFactory == null)
            {
                throw new ArgumentNullException("valuesFactory");
            }

            List <KeyValuePair <TKey, TValue> > result = new List <KeyValuePair <TKey, TValue> >();
            KeyValuePair <TKey, TValue>         resultEntry;

            // List of missing entries, including the index position where the result needs to be written
            List <Tuple <TKey, int> > cacheMisses = null;

            using (CacheContext cacheContext = CacheContext.IsSet() ? CacheContext.GetContext() : null)
            {
                // First pass through list
                int pos = 0;
                foreach (TKey key in keys)
                {
                    TValue value;

                    if (Cache.TryGetValue(key, out value))
                    {
                        resultEntry = new KeyValuePair <TKey, TValue>(key, value);

                        // Check cache
                        if (cacheContext != null)
                        {
                            cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                        }
                    }
                    else
                    {
                        if (cacheMisses == null)
                        {
                            cacheMisses = new List <Tuple <TKey, int> >();
                        }

                        cacheMisses.Add(new Tuple <TKey, int>(key, pos));

                        // An entry still gets added to the result to maintain ordering - it will be overridden later
                        // (can't use null .. kvp is a struct)
                        resultEntry = new KeyValuePair <TKey, TValue>(key, default(TValue));
                    }
                    result.Add(resultEntry);
                    pos++;
                }
            }

            // Fill in misses
            if (cacheMisses != null)
            {
                using (CacheContext cacheContext = new CacheContext())
                {
                    IEnumerable <TKey>   missingKeys    = cacheMisses.Select(k => k.Item1);
                    IEnumerable <TValue> missingResults = valuesFactory(missingKeys);

                    Enumerable.Zip(cacheMisses, missingResults, (keyPos, value) =>
                    {
                        TKey key        = keyPos.Item1;
                        int resultIndex = keyPos.Item2;
                        resultEntry     = new KeyValuePair <TKey, TValue>(key, value);

                        // Include in current result
                        result[resultIndex] = resultEntry;

                        // Add to cache
                        Cache.Add(key, value);

                        // Add the cache context entries to the appropriate CacheInvalidator
                        _cacheInvalidator.AddInvalidations(cacheContext, key);

                        return((object)null);
                    }).Last(); // call Last to force evaluate zip
                }
            }

            return(result);
        }
        /// <summary>
        /// Creates the evaluator for a single calculated field.
        /// </summary>
        /// <param name="calculatedField">Entity for the calculated field.</param>
        /// <param name="settings">Settings.</param>
        /// <returns></returns>
        private CalculatedFieldMetadata GetSingleCalculatedFieldMetadata(Field calculatedField, CalculatedFieldSettings settings)
        {
            string          calculation = null;
            IExpression     expression  = null;
            ParseException  exception   = null;
            BuilderSettings builderSettings;

            try
            {
                // Get calculation
                calculation = calculatedField.FieldCalculation;
                if (string.IsNullOrEmpty(calculation))
                {
                    throw new ArgumentException("The field has no calculation script. It may not be a calculated field.");
                }

                // Get settings
                builderSettings = CreateBuilderSettingsForField(calculatedField, settings);

                // Compile
                expression = _expressionCompiler.Compile(calculation, builderSettings);

                // Register cache invalidations
                if (CacheContext.IsSet())
                {
                    CalculationDependencies dependencies = _expressionCompiler.GetCalculationDependencies(expression);

                    using (CacheContext cacheContext = CacheContext.GetContext())
                    {
                        cacheContext.Entities.Add(calculatedField.Id);
                        cacheContext.Entities.Add(dependencies.IdentifiedEntities);
                        cacheContext.Entities.Add(builderSettings.RootContextType.EntityTypeId);
                    }
                }
            }
            catch (InvalidMemberParseException ex)
            {
                exception = ex;

                // If a parse-exception resulted from being unable to look up a member name, then it may be corrected by renaming some arbitrary field or relationship
                // that could not be otherwise detected by dependencies.IdentifiedEntities. So invalidate parse exceptions if any field/relationship changes.
                if (CacheContext.IsSet())
                {
                    using (CacheContext cacheContext = CacheContext.GetContext())
                    {
                        cacheContext.Entities.Add(ex.TypeId);
                        // TODO: ideally just listen for all fields/relationships attached to type
                        var fieldTypes = PerTenantEntityTypeCache.Instance.GetDescendantsAndSelf(new EntityRef("core:field").Id);
                        cacheContext.EntityTypes.Add(fieldTypes);
                        cacheContext.EntityTypes.Add(new EntityRef("core:relationship").Id);
                    }
                }
            }
            catch (ParseException ex)
            {
                exception = ex;
            }

            // Build metadata
            CalculatedFieldMetadata metadata = new CalculatedFieldMetadata(calculatedField.Id, calculation, expression, exception);

            return(metadata);
        }
Example #21
0
        /// <summary>
        /// Get the access rules for a given user and permission or operation.
        /// </summary>
        /// <param name="subjectId">
        /// The ID of the <see cref="Subject"/>, that is a <see cref="UserAccount"/> or <see cref="Role"/> instance.
        /// This cannot be negative.
        /// </param>
        /// <param name="permission">
        /// The permission to get the query for. This may be null or should be one of <see cref="Permissions.Read"/>,
        /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>.
        /// </param>
        /// <param name="securableEntityTypes">
        /// The IDs of <see cref="SecurableEntity"/> types being accessed. This may be null.
        /// </param>
        /// <returns>
        /// The queries to run.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="subjectId"/> does not exist. Also, <paramref name="permission"/> should
        /// be one of <see cref="Permissions.Read"/>, <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>
        /// </exception>
        public ICollection <AccessRule> GetAccessRules(long subjectId, [CanBeNull] EntityRef permission, [CanBeNull] ICollection <long> securableEntityTypes)
        {
            Subject           subject;
            List <AccessRule> accessRules;
            List <AccessRule> result = new List <AccessRule>( );

            subject = Entity.Get <Subject>(new EntityRef(subjectId));
            if (subject == null)
            {
                throw new ArgumentException("Subject not found", "subjectId");
            }

            // Entity model overview:
            //                                                                              +---------------+
            //                                         ------- PermissionAccess ----------> |  Permission   |
            //                                         |                                    +---------------+
            //                                         |
            //  +-------+                    +---------------------+                        +-----------------+
            //  |Subject| -- AllowAccess --> |    AccessRule       | -- ControlAccess -->   | SecurableEntity |
            //  +-------+                    +---------------------+                        +-----------------+
            //                                         |
            //                                         |                                    +---------------+
            //                                         ------- AR to Report  -------------> |    Report     |
            //                                                                              +---------------+
            //
            //  Create ignores any associated report.

            accessRules = new List <AccessRule>();
            accessRules.AddRange(subject.AllowAccess);

            // Store the enties that, when changed, should invalidate this cache entry.
            using (CacheContext cacheContext = CacheContext.GetContext())
                using (new SecurityBypassContext())
                {
                    cacheContext.Entities.Add(subject.Id);

                    foreach (AccessRule allowAccess in accessRules)
                    {
                        if (allowAccess == null)
                        {
                            continue;
                        }

                        cacheContext.Entities.Add(allowAccess.Id);

                        SecurableEntity controlAccess = allowAccess.ControlAccess;

                        if (controlAccess != null)
                        {
                            cacheContext.Entities.Add(controlAccess.Id);
                        }

                        IEnumerable <EntityRef> permissionsRef = allowAccess.PermissionAccess.WhereNotNull().Select(x => new EntityRef(x)).ToList();

                        if ((allowAccess.AccessRuleEnabled ?? false))
                        {
                            if (permission == null || permissionsRef.Any(p => p.Equals(permission)))
                            {
                                if (securableEntityTypes == null || (controlAccess != null && securableEntityTypes.Contains(controlAccess.Id)))
                                {
                                    result.Add(allowAccess);
                                }
                            }
                        }

                        cacheContext.Entities.Add(permissionsRef.Select(p => p.Id));
                        cacheContext.EntityInvalidatingRelationshipTypes.Add(SecurityQueryCacheInvalidatorHelper.SecurityQueryRelationships);
                    }
                }

            return(result);
        }
        /// <summary>
        /// Load the roles recursively for a user.
        /// </summary>
        /// <param name="subjectId">
        /// The user to load the roles for. This cannot be negative.
        /// </param>
        /// <returns>
        /// The roles the user is recursively a member of.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="subjectId"/> cannot be negative.
        /// </exception>
        public ISet <long> GetUserRoles(long subjectId)
        {
            if (subjectId < 0)
            {
                throw new ArgumentException(@"Invalid user ID.", nameof(subjectId));
            }

            EntityRef userHasRoleEntityRef;

            userHasRoleEntityRef = new EntityRef(WellKnownAliases.CurrentTenant.UserHasRole);
            using (new SecurityBypassContext())
            {
                HashSet <long> roles = null;

                // Get the subject
                IEntity subject = Entity.Get(subjectId);

                if (Entity.Is <UserAccount>(subject))
                {
                    // Get the relationships of the 'userHasRole' type.
                    IChangeTracker <IMutableIdKey> relationshipMembers = Entity.GetRelationships(new EntityRef(subjectId), userHasRoleEntityRef, Direction.Forward);
                    if (relationshipMembers != null)
                    {
                        roles = new HashSet <long>(relationshipMembers.Select(pair => pair.Key));

                        GetParentRoles(roles);

                        roles.UnionWith(EveryoneRoles);
                    }
                    else
                    {
                        roles = new HashSet <long>( );
                    }

                    using (CacheContext cacheContext = CacheContext.GetContext( ))
                    {
                        cacheContext.Entities.Add(subjectId);
                        if (roles != null)
                        {
                            cacheContext.Entities.Add(roles);
                        }
                        cacheContext.EntityInvalidatingRelationshipTypes.Add(WellKnownAliases.CurrentTenant.UserHasRole);
                        cacheContext.EntityInvalidatingRelationshipTypes.Add(WellKnownAliases.CurrentTenant.IncludesRoles);
                    }
                }
                else if (Entity.Is <Role>(Entity.Get(subjectId)))
                {
                    // The subject is a role, so include just include itself, then fetch included roles.
                    roles = new HashSet <long>( );
                    roles.Add(subjectId);

                    GetParentRoles(roles);

                    roles.UnionWith(EveryoneRoles);

                    using (CacheContext cacheContext = CacheContext.GetContext( ))
                    {
                        cacheContext.Entities.Add(roles);
                        cacheContext.EntityInvalidatingRelationshipTypes.Add(WellKnownAliases.CurrentTenant.IncludesRoles);
                    }
                }
                else
                {
                    roles = new HashSet <long>( );
                }

                return(roles);
            }
        }
        /// <summary>
        /// Check whether the user has all the specified
        /// <paramref name="permissions">access</paramref> to the specified <paramref name="entities"/>.
        /// </summary>
        /// <param name="entities">
        ///     The entities to check. This cannot be null or contain null.
        /// </param>
        /// <param name="permissions">
        ///     The permissions or operations to check. This cannot be null or contain null.
        /// </param>
        /// <param name="user">
        ///     The user requesting access. This cannot be null.
        /// </param>
        /// <returns>
        /// A mapping of each entity ID to whether the user has access (true) or not (false).
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// No argument can be null.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// Neither <paramref name="entities"/> nor <paramref name="permissions"/> can contain null.
        /// </exception>
        public IDictionary <long, bool> CheckAccess(IList <EntityRef> entities, IList <EntityRef> permissions, EntityRef user)
        {
            if (entities == null)
            {
                throw new ArgumentNullException("entities");
            }
            if (permissions == null)
            {
                throw new ArgumentNullException("permissions");
            }
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }

            IDictionary <long, bool> result;
            ISet <long> subjects;
            IDictionary <long, ISet <EntityRef> >        entityTypes;
            Dictionary <long, IDictionary <long, bool> > permissionToAccess;
            // Dictionary keyed off report id to entities
            Dictionary <long, ISet <long> > queryResultsCache;
            ISet <long> allEntities;

            if (SkipCheck(user))
            {
                result = SetAll(entities.Select(e => e.Id), true);
            }
            else if (entities.Count == 0)
            {
                result = new Dictionary <long, bool>();
            }
            else
            {
                using (new SecurityBypassContext())
                {
                    subjects = GetSubjects(user);

                    using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName))
                    {
                        messageContext.Append(() => "Checking access rules:");

                        entityTypes        = null;
                        permissionToAccess = new Dictionary <long, IDictionary <long, bool> >();
                        queryResultsCache  = new Dictionary <long, ISet <long> >();
                        allEntities        = null;

                        foreach (EntityRef permission in permissions)
                        {
                            permissionToAccess[permission.Id] = SetAll(entities.Select(e => e.Id), false);

                            if (allEntities == null &&
                                !permission.Equals(Permissions.Create))
                            {
                                // Add all the entities to a set
                                allEntities = new HashSet <long>(entities.Select(e => e.Id));
                            }

                            foreach (long subject in subjects)
                            {
                                if (entityTypes == null)
                                {
                                    entityTypes = EntityTypeRepository.GetEntityTypes(entities);
                                }

                                bool accessGrantedToAll = false;

                                // entityType maps a sorted list of type ids to instance entity Refs
                                foreach (KeyValuePair <long, ISet <EntityRef> > entityType in entityTypes)
                                {
                                    using (MessageContext perTypeMessageContext = new MessageContext(EntityAccessControlService.MessageName))
                                    {
                                        perTypeMessageContext.Append(() =>
                                                                     ConstructCheckMessage(entities, subjects, entityTypes, subject, entityType));

                                        using (new MessageContext(EntityAccessControlService.MessageName))
                                        {
                                            // Automatically allow read access (only) to fields, relationships and types
                                            CheckAutomaticAccess(permission, entityType, permissionToAccess);

                                            if (EntityRefComparer.Instance.Equals(permission, Permissions.Create))
                                            {
                                                CheckAccessControlByRelationship(
                                                    subject,
                                                    permission,
                                                    entityType.Key,
                                                    entityType.Value.ToList(),
                                                    permissionToAccess[permission.Id]);
                                            }
                                            else
                                            {
                                                CheckAccessControlByQuery(
                                                    subject,
                                                    permission,
                                                    entityType.Key,
                                                    entityType.Value.ToList(),
                                                    allEntities, queryResultsCache,
                                                    permissionToAccess[permission.Id]);
                                            }

                                            // Skip remaining checks if access is granted to all requested entities
                                            // for the current permission.
                                            if (permissionToAccess[permission.Id].All(kvp => kvp.Value))
                                            {
                                                messageContext.Append(
                                                    () => "Access granted to all entities. Not checking additional access rules.");
                                                accessGrantedToAll = true;
                                                break;
                                            }
                                        }
                                    }
                                }

                                // Skip remaining checks if access is granted to all requested entities
                                // for the current permission.
                                if (accessGrantedToAll || permissionToAccess[permission.Id].All(kvp => kvp.Value))
                                {
                                    if (!accessGrantedToAll)
                                    {
                                        messageContext.Append(() => "Access granted to all entities. Not checking additional access rules.");
                                    }
                                    break;
                                }
                            }
                        }

                        result = CollateAccess(entities, permissionToAccess);
                        result = AllowAccessToTypelessIds(result, entityTypes);

                        // Add all the containing roles to the cache so a role change invalidates this
                        // user's security cache entries.
                        using (CacheContext cacheContext = CacheContext.GetContext())
                        {
                            cacheContext.Entities.Add(subjects);
                        }
                    }
                }
            }

            return(result);
        }
Example #24
0
        /// <summary>
        /// Build the SQL, or collect it from cache.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public QueryBuild BuildSql(StructuredQuery query, QuerySqlBuilderSettings settings)
        {
            // Validate
            if (query == null)
            {
                throw new ArgumentNullException("query");
            }
            if (QuerySqlBuilder == null)
            {
                throw new InvalidOperationException("QuerySqlBuilder not set.");
            }
            if (settings == null)
            {
                settings = new QuerySqlBuilderSettings( );
            }

            // Check if query can even participate in cache
            if (!CachingQuerySqlBuilderKey.DoesRequestAllowForCaching(query, settings))
            {
                return(BuildSqlImpl(query, settings));
            }

            // Get a user-set key
            // (Users may share the same report SQL if they have the same set of read-rules)
            UserRuleSet userRuleSet = null;

            if (settings.RunAsUser != 0)
            {
                userRuleSet = UserRuleSetProvider.GetUserRuleSet(settings.RunAsUser, Permissions.Read);
                if (userRuleSet == null)
                {
                    throw new InvalidOperationException("Expected userRuleSet");   // Assert false
                }
            }

            // Create cache key
            CachingQuerySqlBuilderKey   key = new CachingQuerySqlBuilderKey(query, settings, userRuleSet);
            CachingQuerySqlBuilderValue cacheValue;

            using (MessageContext msg = new MessageContext("Reports"))
            {
                // Check for force recalculation
                if (settings.RefreshCachedSql)
                {
                    msg.Append(() => "CachingQuerySqlBuilder refreshed forced");
                    _cacheInvalidator.DebugInvalidations.Add(key);
                    Cache.Remove(key);
                }

                // In some circumstances, a result will be uncacheable, so we just return 'null' in the delegate instead.
                // However, on the first access, we will still be doing the calculation, so store it here.
                CachingQuerySqlBuilderValue calculatedOnThisAccess = null;

                // Check cache
                bool fromCache = TryGetOrAdd(key, msg, out cacheValue, callbackKey =>
                {
                    // This callback is called if we have a cache miss

                    using (CacheContext cacheContext = new CacheContext( ))
                    {
                        QueryBuild queryResult = BuildSqlImpl(query, settings);
                        cacheValue             = new CachingQuerySqlBuilderValue(query, queryResult);
                        calculatedOnThisAccess = cacheValue;

                        if (queryResult.SqlIsUncacheable)
                        {
                            return(null);
                        }
                        else
                        {
                            // Add the cache context entries to the appropriate CacheInvalidator
                            _cacheInvalidator.AddInvalidations(cacheContext, callbackKey);
                            return(cacheValue);
                        }
                    }
                });

                // cacheValue will be null if the result was uncacheable
                if (cacheValue == null)
                {
                    if (calculatedOnThisAccess != null)
                    {
                        // In this path, the result was uncacheable, so the cache returned a 'null',
                        // but it was the initial calculation run anyway, so we can get the value from callbackValue.
                        cacheValue = calculatedOnThisAccess;
                    }
                    else
                    {
                        // In this path, the result was uncacheable, but someone had asked previously, and stored
                        // the null, so we need to actually build the SQL again for this scenario.
                        // Note: don't need to do anything with cache context, because this cache is not participating.
                        // And if there's a parent context set, then the call to BuildSqlImpl will just talk directly to that context.
                        QueryBuild queryResult = BuildSqlImpl(query, settings);
                        cacheValue = new CachingQuerySqlBuilderValue(query, queryResult);
                    }
                }
                else if (fromCache && CacheContext.IsSet( ))
                {
                    // Add the already stored changes that should invalidate this cache
                    // entry to any outer or containing cache contexts.
                    using (CacheContext cacheContext = CacheContext.GetContext( ))
                    {
                        cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                    }
                }
            }

            if (cacheValue == null)
            {
                throw new Exception("Assert false");
            }

            // Mutate returned result to be suitable for current query
            QueryBuild result;

            if (cacheValue.OriginalQuery == query)
            {
                result = cacheValue.QueryResult;
            }
            else
            {
                result = MutateResultToMatchCurrentQuery(cacheValue, query);
            }

            return(result);
        }
        /// <summary>
        /// Runs the report.
        /// </summary>
        /// <param name="report">The report.</param>
        /// <param name="reportSettings">The settings.</param>
        /// <param name="suppressPreload">Pass true if the report has already been preloaded.</param>
        /// <returns>ReportResult.</returns>
        /// <exception cref="System.ArgumentException">@The report identifier resource is not a report.;reportId</exception>
        public ReportCompletionData PrepareReport(Model.Report report, ReportSettings reportSettings, bool suppressPreload = false)
        {
            if (report == null)
            {
                throw new ArgumentNullException("report");
            }
            if (reportSettings == null)
            {
                reportSettings = new ReportSettings( );
            }

            StructuredQuery structuredQuery;
            PreparedQuery   preparedReport;
            PreparedQuery   preparedRollup;

            using (EDC.ReadiNow.Diagnostics.Profiler.Measure("Prepare report run"))
                using (MessageContext messageContext = new MessageContext("Reports"))
                    using (new SecurityBypassContext( ))
                    {
                        // Get the structured query
                        structuredQuery = GetStructuredQuery(report, reportSettings, suppressPreload);

                        // Handle metadata-only request
                        if (reportSettings.RequireSchemaMetadata)
                        {
                            ReportResult reportResult = new ReportResult(report, structuredQuery, null, null, null, reportSettings);
                            return(new ReportCompletionData(reportResult));
                        }

                        // Prepare query settings
                        preparedReport = PrepareReportRun(structuredQuery, reportSettings);
                        preparedReport.QuerySettings.Hint = "Rpt-" + report.Id.ToString( );

                        // Handle rollups
                        preparedRollup = PrepareReportRollupRun(report, preparedReport.StructuredQuery, reportSettings, preparedReport.QuerySettings);
                    }

            Func <ReportResult> resultCallback = () =>
            {
                ReportResult reportResult = null;
                QueryResult  queryResult  = null;
                QueryResult  rollupResult = null;

                using (new SecurityBypassContext( ))
                {
                    // Execute the query
                    queryResult = QueryRunner.ExecuteQuery(preparedReport.StructuredQuery, preparedReport.QuerySettings);

                    // Execute the rollup query
                    if (preparedRollup.StructuredQuery != null)
                    {
                        rollupResult = QueryRunner.ExecuteQuery(preparedRollup.StructuredQuery, preparedRollup.QuerySettings);
                    }

                    // Package up the result.
                    reportResult = new ReportResult(report,
                                                    preparedReport.StructuredQuery, queryResult,
                                                    preparedRollup.ClientAggregate, rollupResult,
                                                    reportSettings);
                }

                return(reportResult);
            };

            // Create cache key (null indicates report is not cacheable)
            IQueryRunnerCacheKey reportCacheKey       = null;
            IQueryRunnerCacheKey rollupCacheKey       = null;
            ReportResultCacheKey reportResultCacheKey = null;

            reportCacheKey = QueryRunnerCacheKeyProvider.CreateCacheKey(preparedReport.StructuredQuery, preparedReport.QuerySettings);
            if (reportCacheKey != null)
            {
                if (preparedRollup.StructuredQuery != null)
                {
                    rollupCacheKey = QueryRunnerCacheKeyProvider.CreateCacheKey(preparedRollup.StructuredQuery, preparedRollup.QuerySettings);
                }
                reportResultCacheKey = new ReportResultCacheKey(reportSettings, reportCacheKey, rollupCacheKey);
            }

            // Create completion result
            ReportCompletionData completionData = new ReportCompletionData( );

            completionData.ResultCallback = resultCallback;
            completionData.ResultCacheKey = reportResultCacheKey;
            completionData.CacheContextDuringPreparation = CacheContext.GetContext( );

            return(completionData);
        }
Example #26
0
        /// <summary>
        /// Build the SQL, or collect it from cache.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public QueryResult ExecuteQuery(StructuredQuery query, QuerySettings settings)
        {
            // Validate
            if (query == null)
            {
                throw new ArgumentNullException("query");
            }
            if (QueryRunner == null)
            {
                throw new InvalidOperationException("QueryRunner not set.");
            }
            if (settings == null)
            {
                settings = new QuerySettings( );
            }

            // Determine if we should cache .. and the cache key
            QueryBuild            builtQuery;
            CachingQueryRunnerKey key;
            CacheContext          queryBuilderCacheContext;

            using (queryBuilderCacheContext = new CacheContext())
            {
                key = CreateCacheKeyAndQuery(query, settings, out builtQuery);
            }

            // A null key means that the ersult should not participate in caching
            if (key == null)
            {
                return(RunQueryImpl(query, settings, builtQuery));
            }

            CachingQueryRunnerValue cacheValue;

            using (MessageContext msg = new MessageContext("Reports"))
            {
                // Check for force recalculation
                if (settings.RefreshCachedResult)
                {
                    msg.Append(() => "CachingQueryRunner refreshed forced");
                    Cache.Remove(key);
                }

                // Run query
                bool fromCache = TryGetOrAdd(key, msg, out cacheValue, callbackKey =>
                {
                    using (CacheContext cacheContext = new CacheContext( ))
                    {
                        QueryResult queryResult = RunQueryImpl(query, settings, builtQuery);
                        cacheValue = new CachingQueryRunnerValue(query, queryResult);

                        // Add the cache context entries to the appropriate CacheInvalidator
                        _cacheInvalidator.AddInvalidations(cacheContext, callbackKey);
                        _cacheInvalidator.AddInvalidations(queryBuilderCacheContext, callbackKey);
                    }

                    return(cacheValue);
                });

                if (fromCache && CacheContext.IsSet())
                {
                    using (CacheContext cacheContext = CacheContext.GetContext( ))
                    {
                        // Add the already stored changes that should invalidate this cache
                        // entry to any outer or containing cache contexts.
                        cacheContext.AddInvalidationsFor(_cacheInvalidator, key);
                    }
                }
            }

            if (cacheValue == null)
            {
                throw new Exception("Assert false");
            }

            // Mutate returned result to be suitable for current query
            QueryResult result;

            if (cacheValue.OriginalQuery == query)
            {
                result = cacheValue.QueryResult;
            }
            else
            {
                result = MutateResultToMatchCurrentQuery(cacheValue, query);
            }

            return(result);
        }