/// <summary> /// Runs a request. Caches it. Secures it. /// </summary> /// <param name="request">The entity member request string.</param> /// <returns></returns> public static IEnumerable <EntityData> GetEntitiesData(EntityRequest request) { if (request == null) { throw new ArgumentNullException("request"); } if (request.Entities == null) { throw new ArgumentNullException("request", "request.Entities cannot be null"); } if (!request.EntityIDs.Any()) { return(Enumerable.Empty <EntityData>()); } // Check cache and/or get unsecured result BulkRequestResult unsecuredResult = BulkResultCache.GetBulkResult(request); // Bail out if we're not interested in cached results. if (request.DontProcessResultIfFromCache && request.ResultFromCache) { return(Enumerable.Empty <EntityData>()); } // Secure the result IEnumerable <EntityData> results = BulkRequestResultConverter.BuildAndSecureResults(unsecuredResult, request.EntityIDs, request.QueryType == QueryType.BasicWithDemand ? SecurityOption.DemandAll : SecurityOption.SkipDenied); return(results); }
/// <summary> /// Constructor /// </summary> public CachingBulkRequestRunnerValue(BulkRequestResult bulkRequestResult) { if (bulkRequestResult == null) { throw new ArgumentNullException("bulkRequestResult"); } BulkRequestResult = bulkRequestResult; }
/// <summary> /// Generates the query. /// </summary> /// <param name="request"></param> /// <returns></returns> private BulkRequestResult CreateResult(EntityRequest request) { // Get/create a query object BulkSqlQuery query = BulkSqlQueryCache.GetBulkSqlQuery(request); // Run query on SQL server BulkRequestResult unsecuredResult = BulkRequestSqlRunner.RunQuery(query, request.EntityIDsCanonical); return(unsecuredResult); }
/// <summary> /// Read field values from the database result /// </summary> private static void ReadFieldValues(IDataReader reader, BulkSqlQuery query, BulkRequestResult result) { // select d.EntityId, d.FieldId, d.Data, [d.Namespace] while (reader.Read()) { // Caution: We're in a database-read context, so don't touch the entity model or things will crash. // Load row long entityId = reader.GetInt64(0); long fieldId = reader.GetInt64(1); object data = reader.GetValue(2); // Get alias namespace if (data != null && reader.FieldCount == 4) { string aliasNamespace = reader.GetString(3); data = aliasNamespace + ":" + data; } // Get/convert the type info for the field FieldInfo fieldInfo; if (!query.FieldTypes.TryGetValue(fieldId, out fieldInfo)) { throw new InvalidOperationException("Assert false: encountered a field type in the result that was not part of the request."); } if (fieldInfo.IsWriteOnly) { data = null; } // Prepare field value var typedValue = new TypedValue(DateTimeKind.Utc); typedValue.Type = fieldInfo.DatabaseType; typedValue.Value = data; var fieldValue = new FieldValue(data, typedValue); // Add to dictionary var key = new FieldKey(entityId, fieldId); result.FieldValues [key] = fieldValue; } }
/// <summary> /// Creates an entity ref for the ID to be checked. /// </summary> /// <remarks> /// The BulkRequestResult graph typically already has type information for the entities being loaded. /// The security engine requires type information to determine applicable rules. /// So instead of having it attempt to activate the entities (additional DB trips) to determine the types, just /// pass the type information along instead. /// This is done by creating a EntityTypeOnly implementation of IEntity, and packing it into the EntityRef that we pass. /// </remarks> /// <param name="data"></param> /// <param name="entityId"></param> /// <returns></returns> private static EntityRef CreateEntityRefForSecurityCheck(BulkRequestResult data, long entityId) { EntityRef result; long isOfTypeRelId = WellKnownAliases.CurrentTenant.IsOfType; // Look up type information RelationshipKey key = new RelationshipKey(entityId, isOfTypeRelId); List <long> typeIds; if (data.Relationships.TryGetValue(key, out typeIds)) { IEntity entity = new EntityTypeOnly(entityId, typeIds); result = new EntityRef(entity); } else { result = new EntityRef(entityId); } return(result); }
/// <summary> /// Remove duplicate entries from relationship lists. /// (that could result if two different nodes point to the same node along the same relationship) /// </summary> /// <param name="result"></param> private static void RemoveDuplicateRelationshipEntries(BulkRequestResult result) { // Note: this is a bit messy // Ideally we could remove the duplicates during the main ReadRelationships loop. // However, I don't want to return the result sorted, as it will unnecessarily slow down the SQL. // And I don't want to store individual HashSets in the object if I can help it. foreach (var pair in result.Relationships) { RelationshipKey key = pair.Key; List <long> targetEntityIds = pair.Value; // Only consolidate if there are multiple entries // (And do so, without recreating the list, otherwise we'll break our enumeration) if (targetEntityIds.Count > 1) { HashSet <long> distinct = new HashSet <long>(targetEntityIds); targetEntityIds.Clear( ); targetEntityIds.AddRange(distinct); } } }
/// <summary> /// Converts an unsecured BulkRequestResult to a secured EntityData structure. /// </summary> public static IEnumerable <EntityData> BuildAndSecureResults(BulkRequestResult data, IEnumerable <long> entities, SecurityOption securityOption) { // Get readability (security) for all entities in the unsecured result set var readability = BulkRequestResultSecurityHelper.GetEntityReadability(Factory.EntityAccessControlService, data); // Prepare for recursive walk var context = new Context { Readability = readability, RawData = data, BulkSqlQuery = data.BulkSqlQuery }; if (securityOption == SecurityOption.DemandAll) { if (context.RawData.RootEntitiesList.Any(id => !context.Readability(id))) { throw new PlatformSecurityException( RequestContext.GetContext().Identity.Name, new [] { Permissions.Read }, context.RawData.RootEntitiesList .Where(id => !context.Readability(id)) .Select(id => new EntityRef(id))); } } else if (securityOption != SecurityOption.SkipDenied) { throw new ArgumentException( string.Format("Unknown security option {0}", securityOption), "securityOption"); } IEnumerable <EntityData> result = GetRootEntities(context, entities); return(result); }
/// <summary> /// Pre-fetches the can-read security for the list of entities. /// </summary> /// <param name="entityAccessControlService">The security service to use.</param> /// <param name="data">The data.</param> /// <returns> /// A predicate backed by a dictionary that can quickly return whether an entity is readable. /// </returns> public static Predicate <long> GetEntityReadability(IEntityAccessControlService entityAccessControlService, BulkRequestResult data) { if (entityAccessControlService == null) { throw new ArgumentNullException("entityAccessControlService"); } Predicate <long> predicate; if (SecurityBypassContext.IsActive) { predicate = (long entityId) => true; } else { // Get readable entities List <EntityRef> entitiesToExplicitlyCheck = data.AllEntities .Where(pair => !pair.Value.ImplicitlySecured) .Select(pair => CreateEntityRefForSecurityCheck(data, pair.Key)) // Stop! If you change .Check to take longs, then discuss with Pete first. .ToList(); IDictionary <long, bool> readableEntities = entityAccessControlService.Check(entitiesToExplicitlyCheck, new [] { Permissions.Read }); // Lookup predicate predicate = (long entityId) => { // Check if implicitly secured by relationship EntityValue ev; if (!data.AllEntities.TryGetValue(entityId, out ev)) { return(false); // assert false } if (ev.ImplicitlySecured) { return(true); } // Check if explicitly secured bool canRead; if (readableEntities.TryGetValue(entityId, out canRead)) { return(canRead); } return(false); }; } return(predicate); }
/// <summary> /// Extract the list of relationship types referenced in the query. /// </summary> /// <param name="result"></param> /// <returns></returns> private IEnumerable <long> GetRelationshipTypesUsed(BulkRequestResult result) { return(result.BulkSqlQuery.Relationships.Keys.Select(Math.Abs)); }
/// <summary> /// Execute the query held in the BulkSqlQuery object for the specified entities, and capture the results in a fairly raw format. /// </summary> /// <param name="query">The query object.</param> /// <param name="entities">IDs of root-level entities to load. Must not contain duplicates.</param> /// <returns></returns> /// <exception cref="System.ArgumentNullException"> /// query /// or /// entities /// </exception> /// <exception cref="System.ArgumentException">No entities were loaded;entities</exception> public static BulkRequestResult RunQuery(BulkSqlQuery query, IEnumerable <long> entities) { if (query == null) { throw new ArgumentNullException("query"); } if (entities == null) { throw new ArgumentNullException("entities"); } var entitiesList = entities.ToList( ); if (entitiesList.Count <= 0) { throw new ArgumentException("No entities were loaded", "entities"); } var result = new BulkRequestResult(); result.BulkSqlQuery = query; ///// // HACK:TODO: 'LastLogin' is handled differently to all other fields on an entity. See UserAccountValidator ///// long lastLogonId = WellKnownAliases.CurrentTenant.LastLogon; if (query.FieldTypes.ContainsKey(lastLogonId)) { using (CacheContext cacheContext = new CacheContext( )) { cacheContext.Entities.Add(lastLogonId); } } using (DatabaseContext ctx = DatabaseContext.GetContext()) using (IDbCommand command = ctx.CreateCommand()) { // If single entity, then pass via parameter (to allow SQL to cache execution plan) if (entitiesList.Count == 1) { ctx.AddParameter(command, "@entityId", DbType.Int64, entitiesList[0]); } else { command.AddIdListParameter("@entityIds", entitiesList); } ctx.AddParameter(command, "@tenantId", DbType.Int64, RequestContext.TenantId); foreach (KeyValuePair <string, DataTable> tvp in query.TableValuedParameters) { command.AddTableValuedParameter(tvp.Key, tvp.Value); } command.CommandText = "dbo.spExecBulkRequest"; command.CommandType = CommandType.StoredProcedure; using (IDataReader reader = command.ExecuteReader()) { if (reader != null) { ReadRelationships(reader, query, result); while (reader.NextResult()) { ReadFieldValues(reader, query, result); } } } } return(result); }
/// <summary> /// Read relationships, and top level entities, from the database result. /// </summary> private static void ReadRelationships(IDataReader reader, BulkSqlQuery query, BulkRequestResult result) { // select distinct EntityId, RelSrcId, RelTypeId from #process while (reader.Read()) { // Caution: We're in a database-read context, so don't touch the entity model or things will crash. long toId = reader.GetInt64(0); int nodeTag = reader.GetInt32(1); // tag of the request node that returned this entity long fromId = reader.GetInt64(2); // zero for root-level entities long typeIdWithNeg = reader.GetInt64(3); // relationship type-id, with reverse being indicated with negative values // Root result entity if (fromId == 0) { result.RootEntities.Add(toId); } else { // Add to dictionary var key = new RelationshipKey(fromId, typeIdWithNeg); var value = toId; List <long> list; if (!result.Relationships.TryGetValue(key, out list)) { list = new List <long>(); result.Relationships[key] = list; } list.Add(value); } // Implicit relationship security bool implicitlySecured = false; if (fromId != 0) { var relInfo = query.Relationships[typeIdWithNeg]; implicitlySecured = relInfo.ImpliesSecurity; } // Store entity EntityValue ev; if (!result.AllEntities.TryGetValue(toId, out ev)) { ev = new EntityValue { ImplicitlySecured = implicitlySecured }; result.AllEntities[toId] = ev; } else { ev.ImplicitlySecured = ev.ImplicitlySecured && implicitlySecured; } // Store the request node that specified members to load for this entity RequestNodeInfo requestNode = query.RequestNodes [nodeTag]; ev.Nodes.Add(requestNode); } #if DEBUG if (result.RootEntitiesList.Count != 0) { throw new InvalidOperationException("Assert false .. expected RootEntityList to be empty."); } #endif result.RootEntitiesList.AddRange(result.RootEntities); RemoveDuplicateRelationshipEntries(result); }