/// <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); }
/// <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); }
/// <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)); } }
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)); } }
/// <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); }
/// <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); }
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"); }
/// <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); }
/// <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); }
/// <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); }
/// <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); }
/// <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); }