/// <summary> /// Generate the query SQL. Do not actually run it. /// </summary> /// <param name="query">The structured query object to convert.</param> /// <param name="settings">Build-time settings for the conversion.</param> /// <returns>An object structure containing SQL, and other discovered information.</returns> public QueryBuild BuildSql(StructuredQuery query, QuerySqlBuilderSettings settings) { if (query == null) { throw new ArgumentNullException("query"); } // Initialise settings if (settings == null) { settings = new QuerySqlBuilderSettings( ); } // Optimise tree StructuredQuery optimisedQuery = StructuredQueryHelper.PruneQueryTree(query); // Generate SQL QueryBuilder queryBuilder = new QueryBuilder(optimisedQuery, settings); QueryBuild result = queryBuilder.GetSqlInternal( ); // Logging using (MessageContext msg = new MessageContext("Reports")) { msg.Append(() => new string( '-', 50 )); msg.Append(() => "Final structured query:\n" + StructuredQueryHelper.ToXml(optimisedQuery)); msg.Append(() => new string( '-', 50 )); msg.Append(() => "SQL:\n" + result.Sql); msg.Append(() => new string( '-', 50 )); } // Note: identify cache dependencies after getting SQL, so that any calculations are resolved into the structured query. IdentifyCacheDependencies(optimisedQuery, settings); return(result); }
public void Test_Append_TwoLevels() { const string testName = "a"; const string testLine1 = "Line 1"; const string testLine2 = "Line 2"; const string testLine3 = "Line 3"; using (MessageContext outerMessageContext = new MessageContext(testName, MessageContextBehavior.Capturing)) { outerMessageContext.Append(() => testLine1); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1), "Incorrect out message after first append"); using (MessageContext innerMessageContext = new MessageContext(testName, MessageContextBehavior.Capturing)) { innerMessageContext.Append(() => testLine2); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1 + Environment.NewLine + MessageContext.Indent + testLine2), "Incorrect outer message after second append"); Assert.That(innerMessageContext.GetMessage(), Is.EqualTo(testLine2), "Incorrect inner message after second append"); } Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1 + Environment.NewLine + MessageContext.Indent + testLine2), "Incorrect outer message after inner dispose"); outerMessageContext.Append(() => testLine3); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1 + Environment.NewLine + MessageContext.Indent + testLine2 + Environment.NewLine + testLine3), "Incorrect outer message after third append"); } }
public void Test_Sample() { const string testName = "a"; using ( MessageContext messageContext1 = new MessageContext(testName, MessageContextBehavior.Capturing | MessageContextBehavior.New)) { messageContext1.Append(() => "Start:"); using (MessageContext messageContext2 = new MessageContext(testName)) { messageContext2.Append(() => "Addends:"); using (MessageContext messageContext3 = new MessageContext(testName)) { messageContext3.Append(() => "Addend 1: 2"); messageContext3.Append(() => "Addend 2: 3"); } messageContext2.Append(() => "Result:"); using (MessageContext messageContext4 = new MessageContext(testName)) { messageContext4.Append(() => "Sum: 5"); } } Assert.That(messageContext1.GetMessage(), Is.EqualTo(string.Format( @"Start: {0}Addends: {0}{0}Addend 1: 2 {0}{0}Addend 2: 3 {0}Result: {0}{0}Sum: 5", MessageContext.Indent))); } }
private void LogMessage(CachingEntityAccessControlCheckerResult result) { // Force an indent by using two message contexts using (MessageContext outermessageContext = new MessageContext(EntityAccessControlService.MessageName)) { outermessageContext.Append(() => string.Format("Cache '{0}' results:", CacheName)); using (MessageContext innerMessageContext = new MessageContext(EntityAccessControlService.MessageName)) { if (result.CacheResult.Count > 0) { innerMessageContext.Append(() => string.Format( "Allowed: {0}", string.Join(", ", result.CacheResult.Where(kvp => kvp.Value) .Select(kvp => kvp.Key)))); innerMessageContext.Append(() => string.Format( "Denied: {0}", string.Join(", ", result.CacheResult.Where(kvp => !kvp.Value) .Select(kvp => kvp.Key)))); } else { innerMessageContext.Append(() => "No results found in the cache."); } } } }
/// <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> /// Record reason for not caching /// </summary> private static void LogReasonForNonCaching(string reason) { using (MessageContext msg = new MessageContext("Reports")) { msg.Append(() => "Query is uncacheable: " + reason); } EventLog.Application.WriteTrace("Query is uncacheable: " + reason); }
/// <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); }
public void Test_Append_SingleLevel() { const string testName = "a"; const string testLine1 = "Line 1"; const string testLine2 = "Line 2"; using (MessageContext messageContext = new MessageContext(testName, MessageContextBehavior.Capturing)) { Assert.That(messageContext.GetMessage(), Is.Empty, "Incorrect message before first append line"); messageContext.Append(() => testLine1); Assert.That(messageContext.GetMessage(), Is.EqualTo(testLine1), "Incorrect message after first append line"); messageContext.Append(() => testLine2); Assert.That(messageContext.GetMessage(), Is.EqualTo(testLine1 + Environment.NewLine + testLine2), "Incorrect message after second append line"); } }
/// <summary> /// Create a human readable descriptio0n of the result of the security check. /// </summary> /// <param name="result"></param> /// <param name="messageContext"></param> /// <returns></returns> internal void WriteFooterMessage(IDictionary <long, bool> result, MessageContext messageContext) { if (messageContext == null) { throw new ArgumentNullException("messageContext"); } messageContext.Append(() => "Result:"); using (MessageContext innerMessageContext = new MessageContext(MessageName)) { innerMessageContext.Append(() => string.Format("Allowed: {0}", string.Join(", ", result.Where(kvp => kvp.Value) .Select(kvp => kvp.Key)))); innerMessageContext.Append(() => string.Format("Denied: {0}", string.Join(", ", result.Where(kvp => !kvp.Value) .Select(kvp => kvp.Key)))); } }
/// <summary> /// Check the cache. /// Run the continuation callback if necessary. /// Apply presentation formatting. /// Store result in cache if possible. /// </summary> /// <typeparam name="T">Type of result after presentation formatting.</typeparam> /// <param name="reportCompletionData">The completion process for a partially processed report.</param> /// <param name="presentationCallback">Presentation formatting, such as conversion to webapi message.</param> /// <returns></returns> public string GetReportResult(ReportCompletionData reportCompletionData, Func <ServiceResult.ReportResult, string> presentationCallback) { if (reportCompletionData == null) { return(null); } ReportResultCacheKey cacheKey; string result; using (MessageContext msg = new MessageContext("Reports")) { cacheKey = reportCompletionData.ResultCacheKey; // A null cacheKey indicates that the report is uncacheable if (cacheKey == null) { msg.Append(() => "ReportResultCache received no cache key"); // Invoke callback ServiceResult.ReportResult serviceResult = reportCompletionData.PerformRun( ); // And format it result = presentationCallback(serviceResult); } else { // Check cache bool fromCache = TryGetOrAdd(cacheKey, msg, out result, key1 => { string formattedResult; using (CacheContext cacheContext = new CacheContext( )) { // Call completion callback to run report ServiceResult.ReportResult serviceResult = reportCompletionData.PerformRun( ); // Format result formattedResult = presentationCallback(serviceResult); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, cacheKey); if (reportCompletionData.CacheContextDuringPreparation != null) { _cacheInvalidator.AddInvalidations(reportCompletionData.CacheContextDuringPreparation, cacheKey); } } return(formattedResult); }); // Note: No call to AddInvalidationsFor because no-one is listening. } } return(result); }
private bool CheckTypeAccessRelatedTypesImpl(EntityType entityType, EntityRef permission, EntityRef user, List <EntityType> entityTypesToCheck) { bool result = false; using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName)) { IDictionary <long, bool> canAccessTypes; string message; // Allow access if the user has access to any of the related types if (entityTypesToCheck.Count == 0) { messageContext.Append(() => "No entity types found to check."); return(false); } // Check whether the user has access to the given types canAccessTypes = null; SecurityBypassContext.RunAsUser(() => canAccessTypes = Checker.CheckTypeAccess( entityTypesToCheck.ToList( ), permission, user)); message = string.Format( "Checking security relationship(s) to see whether user can create entities of type '{0}': ", entityType.Name ?? entityType.Id.ToString( )); if (canAccessTypes.Any(kvp => kvp.Value)) { messageContext.Append(() => string.Format( "{0} Allowed due to create access to entity type(s) '{1}'", message, canAccessTypes.Select(kvp => entityTypesToCheck.First(et => et.Id == kvp.Key).Name ?? kvp.Key.ToString( )))); result = true; } else { messageContext.Append(() => $"{message} Denied"); } } return(result); }
/// <summary> /// Pre-load report entities. /// </summary> /// <param name="reportId">The ID of the report to preload.</param> public static void PreloadReport(EntityRef reportId) { using (MessageContext messageContext = new MessageContext("Reports")) { messageContext.Append(() => string.Format("Preload report {0}", reportId)); var rq = new EntityRequest(reportId, ReportPreloaderQuery, "Preload report " + reportId.ToString( )); BulkPreloader.Preload(rq); } }
/// <summary> /// Try to get the value from cache, with logging. /// </summary> private bool TryGetOrAdd(CachingQueryRunnerKey key, MessageContext msg, out CachingQueryRunnerValue result, Func <CachingQueryRunnerKey, CachingQueryRunnerValue> valueFactory) { bool foundValue; foundValue = Cache.TryGetOrAdd(key, out result, valueFactory); msg.Append(() => "CachingQueryRunner key:" + key); if (foundValue) { var cacheValue = result; msg.Append(() => "CachingQueryRunner cache hit"); msg.Append(() => $"Entry originally cached at {cacheValue.CacheTime}"); } else { msg.Append(() => "CachingQueryRunner cache miss"); } return(foundValue); }
/// <summary> /// Try to get the value from cache, with logging. /// </summary> private bool TryGetOrAdd(ReportResultCacheKey key, MessageContext msg, out string result, Func <ReportResultCacheKey, string> valueFactory) { bool foundValue; foundValue = Cache.TryGetOrAdd(key, out result, valueFactory); msg.Append(() => "ReportResultCache key:" + key.ToString( )); if (foundValue) { var cacheValue = result; msg.Append(() => "ReportResultCache cache hit"); //msg.Append( ( ) => string.Format( "Entry originally cached at {0}", cacheValue.CacheTime ) ); } else { msg.Append(() => "ReportResultCache cache miss"); } return(foundValue); }
/// <summary> /// Try to get the value from cache, with logging. /// </summary> private bool TryGetValue(CachingReportToQueryConverterKey key, MessageContext msg, out CachingReportToQueryConverterValue result) { msg.Append(() => "CachingReportToQueryConverter key:" + key); CachingReportToQueryConverterValue cacheValue; bool foundValue; if (Cache.TryGetValue(key, out cacheValue)) { msg.Append(() => "CachingReportToQueryConverter cache hit"); msg.Append(() => $"Entry originally cached at {cacheValue.CacheTime}"); result = cacheValue; foundValue = true; } else { msg.Append(() => "CachingReportToQueryConverter cache miss"); result = null; foundValue = false; } return(foundValue); }
/// <summary> /// Check whether the specified <paramref name="permission"/> exists between the <paramref name="subjectId"/> /// and the <paramref name="entities"/> using just a relationship. Used for create permission. /// </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 of the checked entities. /// </param> /// <param name="entities"> /// The checked entities. This cannot be null or contain null. /// </param> /// <param name="result"> /// The map of entity IDs to whether the relationship exists. /// </param> /// <exception cref="ArgumentNullException"> /// No argument can be null. /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="entities"/> cannot contain null. /// </exception> internal void CheckAccessControlByRelationship(long subjectId, EntityRef permission, long entityType, IList <EntityRef> entities, IDictionary <long, bool> result) { if (permission == null) { throw new ArgumentNullException("permission"); } if (entities == null) { throw new ArgumentNullException("entities"); } if (entities.Contains(null)) { throw new ArgumentException(@"Entities cannot contain null", "entities"); } if (result == null) { throw new ArgumentNullException("result"); } Subject subject; IEnumerable <long> allowedEntityTypes; bool containType; using (new SecurityBypassContext()) { subject = Entity.Get <Subject>(new EntityRef(subjectId)); allowedEntityTypes = AllowedEntityTypes(permission, subject); containType = allowedEntityTypes.Contains(entityType); } if (containType) { foreach (EntityRef entity in entities) { result[entity.Id] = true; } using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName)) { messageContext.Append(() => string.Format( "'{0}' access to entities '{1}' allowed", Permissions.GetPermissionByAlias(permission), string.Join(", ", entities.Select(x => x.ToString())))); } } }
public void Append(StringBuilder sb) { if (Context == null) { sb.Append(Reason); } else { Context.Append(sb); } if (DependsOn != null) { sb.Append(" -> "); DependsOn.Append(sb); } }
/// <summary> /// Write the entity member request structure to the security trace. /// </summary> /// <param name="entityMemberRequest"> /// The <see cref="EntityMemberRequest"/> to write out the structure for. This cannot be null. /// </param> /// <param name="visited"> /// (Optional) The set of already traced <see cref="EntityMemberRequest"/>s. /// </param> /// <exception cref="ArgumentNullException"> /// <paramref name="entityMemberRequest"/> cannot be null. /// </exception> internal void TraceEntityMemberRequest(EntityMemberRequest entityMemberRequest, ISet <EntityMemberRequest> visited = null) { if (entityMemberRequest == null) { throw new ArgumentNullException("entityMemberRequest"); } if (visited == null) { visited = new HashSet <EntityMemberRequest>(); } visited.Add(entityMemberRequest); using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName)) { foreach (RelationshipRequest relationshipRequest in entityMemberRequest.Relationships) { // ReSharper disable AccessToForEachVariableInClosure messageContext.Append(() => { Relationship relationship; EntityType target; EntityType source; relationship = Entity.Get <Relationship>(relationshipRequest.RelationshipTypeId.Id); source = relationshipRequest.IsReverse ? relationship.ToType : relationship.FromType; target = relationshipRequest.IsReverse ? relationship.FromType : relationship.ToType; return(string.Format("-> from type '{0}' ({1}) via '{2}' ({3}) to type '{4}' ({5}){6}{7}", source.Name, source.Id, relationship.Name, relationship.Id, target.Name, target.Id, relationshipRequest.IsReverse ? " (Reverse) " : string.Empty, visited.Contains(relationshipRequest.RequestedMembers) ? " (Already listed, see above for relationships from this type)" : string.Empty)); }); // ReSharper restore AccessToForEachVariableInClosure if (!visited.Contains(relationshipRequest.RequestedMembers)) { TraceEntityMemberRequest(relationshipRequest.RequestedMembers, visited); } } } }
public void Test_Append_NotCapturing() { const string testName1 = "a"; const string testLine1 = "Line 1"; bool called; called = false; using (MessageContext messageContext = new MessageContext(testName1, MessageContextBehavior.Default)) { messageContext.Append(() => { called = true; return(testLine1); }); Assert.That(called, Is.False, "Called"); Assert.That(messageContext.GetMessage(), Is.Empty, "Message is not empty"); } }
/// <summary> /// Write out the diagnostic messages when a structured query execution fails. /// </summary> /// <param name="structuredQuery"> /// The <see cref="StructuredQuery"/> that failed. This cannot be null. /// </param> /// <param name="messageContext"> /// (Optional) The <see cref="MessageContext"/> to write the details to. /// </param> /// <param name="ex"> /// The exception thrown when the query ran, or null if no exception was thrown. /// </param> /// <exception cref="ArgumentNullException"> /// <paramref name="structuredQuery"/> cannot be null. /// </exception> public static void WriteInvalidSecurityReportMessage(StructuredQuery structuredQuery, MessageContext messageContext = null, Exception ex = null) { if (structuredQuery == null) { throw new ArgumentNullException("structuredQuery"); } EventLog.Application.WriteWarning( "{0} ignored due to errors when running the report{1}", GetAccessRuleName(structuredQuery), ex == null ? string.Empty : ": " + ex.ToString()); if (messageContext != null) { messageContext.Append( () => string.Format( "{0} ignored due to errors when running the report", GetAccessRuleName(structuredQuery))); } }
public void Test_Append_OuterCapturingOnly() { const string testName1 = "a"; const string testLine1 = "Line 1"; const string testLine2 = "Line 2"; using (MessageContext outerMessageContext = new MessageContext(testName1, MessageContextBehavior.Capturing)) { outerMessageContext.Append(() => testLine1); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1), "Incorrect outer message after first append"); using (MessageContext innerMessageContext = new MessageContext(testName1)) { innerMessageContext.Append(() => testLine2); Assert.That(innerMessageContext.GetMessage(), Is.Empty, "Incorrect inner message after second append"); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1 + Environment.NewLine + MessageContext.Indent + testLine2), "Incorrect outer message after second append"); } } }
/// <summary> /// Get the subjects for a user. /// </summary> /// <param name="user"> /// The user to check. This cannot be null. /// </param> /// <returns> /// The subjects (i.e. user and roles). /// </returns> private ISet <long> GetSubjects(EntityRef user) { if (user == null) { throw new ArgumentNullException("user"); } ISet <long> subjectIds; subjectIds = new HashSet <long>(RoleRepository.GetUserRoles(user.Id)); using (MessageContext messageContext = new MessageContext(EntityAccessControlService.MessageName)) { messageContext.Append(() => string.Format( "Checking access for user '{0}' ({1}) in roles '{2}'", Entity.Get <UserAccount>(user).Name, user.Id, string.Join(", ", subjectIds.ToList().Select(id => string.Format("'{0}' ({1})", Entity.Get <Subject>(id).Name, id))))); } subjectIds.Add(user.Id); return(subjectIds); }
public void Test_NewContext() { const string testName1 = "a"; const string testLine1 = "Line 1"; const string testLine2 = "Line 2"; using (MessageContext outerMessageContext = new MessageContext(testName1, MessageContextBehavior.Capturing)) { outerMessageContext.Append(() => testLine1); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1), "Incorrect outer message after first append"); using (MessageContext innerMessageContext = new MessageContext(testName1, MessageContextBehavior.Capturing | MessageContextBehavior.New)) { innerMessageContext.Append(() => testLine2); Assert.That(outerMessageContext.GetMessage(), Is.EqualTo(testLine1), "Incorrect outer message after second append"); Assert.That(innerMessageContext.GetMessage(), Is.EqualTo(testLine2), "Incorrect inner message after second append"); } } }
/// <summary> /// Create a human readable description of the security check. /// </summary> /// <param name="entities"></param> /// <param name="permissions"></param> /// <param name="messageContext"></param> /// <returns></returns> internal void WriteHeaderMessage(IList <EntityRef> entities, IList <EntityRef> permissions, MessageContext messageContext) { if (messageContext == null) { throw new ArgumentNullException("messageContext"); } messageContext.Append( () => string.Format( "Access control check: Does user '{0}' have '{1}' access to entity(ies) '{2}'?", RequestContext.GetContext().Identity.Name ?? "(null)", permissions != null ? string.Join(", ", permissions.Select(Permissions.GetPermissionByAlias)) : "(null)", entities != null ? string.Join(", ", entities) : "(null)")); // Problem: Need a way to get names without loading the entities. Otherwise, it creates an infinite loop. //messageContext.Append( // () => // { // IList<Resource> entitiesToCheck; // using (new SecurityBypassContext()) // { // entitiesToCheck = Entity.Get<Resource>(entities, new EntityRef("core:name")).ToList(); // } // return string.Format( // "Access control check: Does user '{0}' have '{1}' access to entity(ies) '{2}'?", // RequestContext.GetContext().Identity.Name ?? "(null)", // permissions != null // ? string.Join(", ", permissions.Select(Permissions.GetPermissionAlias)) // : "(null)", // entities != null // ? string.Join(", ", // entitiesToCheck.Select(e => string.Format("'{0}' ({1})", e.Name, e.Id))) // : "(null)"); // }); }
/// <summary> /// Get the structured query, possibly from cache. /// </summary> /// <param name="report">The report to convert.</param> /// <param name="settings">The report run settings.</param> /// <param name="suppressPreload">True if we should suppress preloading.</param> /// <returns>The structured query.</returns> private StructuredQuery GetStructuredQuery(Model.Report report, ReportSettings settings, bool suppressPreload) { using (MessageContext msg = new MessageContext("Reports")) { StructuredQuery immutableStructuredQuery; StructuredQuery structuredQuery; bool useStructuredQueryCache = settings.UseStructuredQueryCache; ReportToQueryConverterSettings converterSettings = new ReportToQueryConverterSettings { SuppressPreload = suppressPreload, RefreshCachedStructuredQuery = settings.RefreshCachedStructuredQuery, SchemaOnly = settings.RequireSchemaMetadata }; if (settings != null && settings.UseStructuredQueryCache) { // don't allow mutations of cached copy immutableStructuredQuery = CachedReportToQueryConverter.Convert(report, converterSettings); } else { // don't allow mutations, just so we can log it correctly immutableStructuredQuery = NonCachedReportToQueryConverter.Convert(report, converterSettings); } structuredQuery = immutableStructuredQuery.DeepCopy( ); // so we can mutate it (in case we need to) // Logging msg.Append(() => new String('-', 50)); msg.Append(() => "GetStructuredQuery"); msg.Append(() => "suppressPreload = " + suppressPreload); msg.Append(() => "useStructuredQueryCache = " + useStructuredQueryCache); msg.Append(() => "Structured Query:\n" + StructuredQueryHelper.ToXml(immutableStructuredQuery)); msg.Append(() => new String('-', 50)); return(structuredQuery); } }
/// <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> /// 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> /// 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> /// 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> /// 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); }