PageRenderTime 54ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/OpenIdProvider/Models/DBContext.Restrictions.cs

https://bitbucket.org/sparktree/stackexchange.stackid
C# | 275 lines | 197 code | 39 blank | 39 comment | 16 complexity | 327f085176abaaca5ae763fdab27e630 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.ComponentModel.DataAnnotations;
  6. using System.Reflection;
  7. using System.Data.Linq;
  8. namespace OpenIdProvider.Models
  9. {
  10. /// <summary>
  11. /// When placed on a class, denotes that instances of this class can be *inserted* as a result of a non-POST request.
  12. /// </summary>
  13. [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
  14. class AllowNonPostInsertAttribute : Attribute { }
  15. /// <summary>
  16. /// When placed on a class, denotes that instances of this class can be *updated* as a result of a non-POST request.
  17. /// </summary>
  18. [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
  19. class AllowNonPostUpdateAttribute : Attribute { }
  20. /// <summary>
  21. /// When placed on a property, denotes that the value must be one of the explicitly permitted ids if its row is being updated.
  22. ///
  23. /// Implicitly ops the class that declares the property to allow updates.
  24. ///
  25. /// These ids are generally those of newly inserted rows, or the currently logged in user's id.
  26. ///
  27. /// Example:
  28. /// Changing CreationDate on a User of Id = 2, you'd need to provide '2' because User.Id has this property.
  29. /// </summary>
  30. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
  31. class RestrictIdsOnNonPostAttribute : Attribute { }
  32. /// <summary>
  33. /// When placed on a field of an object with one of the Allow*NonPost attributes,
  34. /// it restricts updates to that field *anyway*.
  35. /// </summary>
  36. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
  37. class ForbidNonPostUpdateAttribute : Attribute { }
  38. /// <summary>
  39. /// When placed on a field of an object that does *not* have one of the Allow*NonPost attributes,
  40. /// it allows that single field to be updated modified as part ofa non-POST request.
  41. /// </summary>
  42. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
  43. class AllowNonPostColumnUpdate : Attribute { }
  44. /// <summary>
  45. /// Models and checks update restriction on a DataContext.
  46. ///
  47. /// See the above attributes for what restrictions are supported on
  48. /// linq model objects.
  49. /// </summary>
  50. public class Restrictions
  51. {
  52. public class ShadowModifiedMember
  53. {
  54. public MemberInfo Member { get; set; }
  55. public object CurrentValue { get; set; }
  56. public object OriginalValue { get; set; }
  57. }
  58. private HashSet<Type> InsertsAllowed;
  59. private HashSet<Type> UpdatesAllowed;
  60. private Dictionary<Type, HashSet<MemberInfo>> ForbiddenColumns;
  61. private Dictionary<Type, HashSet<MemberInfo>> AllowedColumns;
  62. private Dictionary<Type, HashSet<MemberInfo>> RestrictedColumns;
  63. public Restrictions()
  64. {
  65. var asm = GetType().Assembly;
  66. var withMetaData =
  67. asm.GetTypes().Where(
  68. a => a.GetCustomAttributes(typeof(MetadataTypeAttribute), false).Count() > 0
  69. ).Select(p => new { Type = p, Attribute = (MetadataTypeAttribute)p.GetCustomAttributes(typeof(MetadataTypeAttribute), false).Single() }
  70. ).ToDictionary(p => p.Attribute.MetadataClassType, p => p.Type);
  71. InsertsAllowed = new HashSet<Type>(GetTypeAttributes<AllowNonPostInsertAttribute>(asm));
  72. UpdatesAllowed = new HashSet<Type>(GetTypeAttributes<AllowNonPostUpdateAttribute>(asm));
  73. var forbiddenColumns = GetPropertyAttributes<ForbidNonPostUpdateAttribute>(asm);
  74. var allowedColumns = GetPropertyAttributes<AllowNonPostColumnUpdate>(asm);
  75. var restrictedColumns = GetPropertyAttributes<RestrictIdsOnNonPostAttribute>(asm);
  76. var fcGrouped = forbiddenColumns.GroupBy(g => g.DeclaringType);
  77. var acGrouped = allowedColumns.GroupBy(g => g.DeclaringType);
  78. var rcGrouped = restrictedColumns.GroupBy(g => g.DeclaringType);
  79. ForbiddenColumns = fcGrouped.ToDictionary(g => g.Key, h => new HashSet<MemberInfo>(h.AsEnumerable()));
  80. AllowedColumns = acGrouped.ToDictionary(g => g.Key, h => new HashSet<MemberInfo>(h.AsEnumerable()));
  81. RestrictedColumns = rcGrouped.ToDictionary(g => g.Key, h => new HashSet<MemberInfo>(h.AsEnumerable()));
  82. ForbiddenColumns = MapMetaData(ForbiddenColumns, withMetaData);
  83. AllowedColumns = MapMetaData(AllowedColumns, withMetaData);
  84. RestrictedColumns = MapMetaData(RestrictedColumns, withMetaData);
  85. }
  86. private static Dictionary<Type, HashSet<MemberInfo>> MapMetaData(Dictionary<Type, HashSet<MemberInfo>> toMap, Dictionary<Type, Type> metaDataToReal)
  87. {
  88. var ret = new Dictionary<Type, HashSet<MemberInfo>>();
  89. foreach (var k in toMap.Keys)
  90. {
  91. Type mapTo;
  92. if (!metaDataToReal.TryGetValue(k, out mapTo))
  93. {
  94. ret[k] = toMap[k];
  95. continue;
  96. }
  97. var copy = new HashSet<MemberInfo>();
  98. foreach (var m in toMap[k])
  99. {
  100. copy.Add(mapTo.GetProperty(m.Name));
  101. }
  102. ret[mapTo] = copy;
  103. }
  104. return ret;
  105. }
  106. private static IEnumerable<MemberInfo> GetPropertyAttributes<T>(Assembly asm)
  107. where T : Attribute
  108. {
  109. foreach (Type type in asm.GetTypes())
  110. {
  111. foreach (PropertyInfo p in type.GetProperties())
  112. {
  113. if (p.GetCustomAttributes(typeof(T), true).Length > 0)
  114. {
  115. yield return p;
  116. }
  117. }
  118. }
  119. }
  120. // See: http://stackoverflow.com/questions/607178/c-how-enumerate-all-classes-with-custom-class-attribute/607204#607204
  121. private static IEnumerable<Type> GetTypeAttributes<T>(Assembly asm)
  122. where T : Attribute
  123. {
  124. foreach (Type type in asm.GetTypes())
  125. {
  126. if (type.GetCustomAttributes(typeof(T), true).Length > 0)
  127. {
  128. yield return type;
  129. }
  130. }
  131. }
  132. public bool IsValidChangeSet(Dictionary<object, ShadowModifiedMember[]> changes, IList<object> deletes, IList<object> inserts, IList<object> updated, List<int> permittedIds, out string error)
  133. {
  134. // Absolutely *no* deletes in restricted mode
  135. if (deletes.Count > 0)
  136. {
  137. error = "illegal hard delete";
  138. return false;
  139. }
  140. if (inserts.Count > 0)
  141. {
  142. foreach (var i in inserts)
  143. {
  144. var t = i.GetType();
  145. // Only explicitly allowed inserts are valid
  146. if (!InsertsAllowed.Contains(t))
  147. {
  148. error = "illegal insert of " + t.Name;
  149. return false;
  150. }
  151. // Even in explicitly allowed inserts, id restrictions must be honored
  152. HashSet<MemberInfo> restricted;
  153. if (RestrictedColumns.TryGetValue(t, out restricted))
  154. {
  155. foreach (var m in restricted.Cast<PropertyInfo>())
  156. {
  157. var check = (int)m.GetValue(i, null);
  158. if (!permittedIds.Contains(check))
  159. {
  160. error = "illegal id placed into " + m.Name + " of " + t.Name + ", " + check;
  161. return false;
  162. }
  163. }
  164. }
  165. }
  166. }
  167. if (updated.Count > 0)
  168. {
  169. foreach (var i in updated)
  170. {
  171. var t = i.GetType();
  172. var modified = changes[i];
  173. // Any columns that have explicit "do not update" attributes trigger a rejection
  174. HashSet<MemberInfo> explicitForbid;
  175. if (ForbiddenColumns.TryGetValue(t, out explicitForbid))
  176. {
  177. if (modified.Any(m => explicitForbid.Contains(m.Member)))
  178. {
  179. error = "illegal update (" + i + ") of type " + t.Name;
  180. return false;
  181. }
  182. }
  183. // Any columns with an id constraint need to be checked too
  184. HashSet<MemberInfo> restricted;
  185. if (RestrictedColumns.TryGetValue(t, out restricted))
  186. {
  187. foreach (var m in restricted)
  188. {
  189. var value = ((PropertyInfo)m).GetValue(i, null);
  190. if (!permittedIds.Contains((int)value))
  191. {
  192. error = "illegal id placed in " + m.Name + " of " + t.Name + ", " + value;
  193. return false;
  194. }
  195. }
  196. // Passed the checks, so equivalent to an [AllowNonPostUpdate]
  197. continue;
  198. }
  199. // If the whole *row* is good to update, carry on
  200. if (UpdatesAllowed.Contains(t)) continue;
  201. // Check if any the changeset is covered by single column permissions
  202. HashSet<MemberInfo> explicitAllow;
  203. if (AllowedColumns.TryGetValue(t, out explicitAllow))
  204. {
  205. if (modified.All(m => explicitAllow.Contains(m.Member)))
  206. continue;
  207. }
  208. error = "illegal update to (" + i + ") of type " + t.Name;
  209. return false;
  210. }
  211. }
  212. error = null;
  213. return true;
  214. }
  215. public bool IsValidChangeSet(DataContext context, List<int> permittedIds, out string error)
  216. {
  217. var changes = context.GetChangeSet();
  218. var updates = new Dictionary<object, ShadowModifiedMember[]>();
  219. foreach (var m in changes.Updates)
  220. {
  221. var modified = context.GetTable(m.GetType()).GetModifiedMembers(m);
  222. updates[m] =
  223. modified.Select(
  224. x =>
  225. new ShadowModifiedMember
  226. {
  227. Member = x.Member,
  228. CurrentValue = x.CurrentValue,
  229. OriginalValue = x.OriginalValue
  230. }
  231. ).ToArray();
  232. }
  233. return IsValidChangeSet(updates, changes.Deletes, changes.Inserts, changes.Updates, permittedIds, out error);
  234. }
  235. }
  236. }