/
Group.cs
396 lines (328 loc) · 15.5 KB
/
Group.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace System.DirectoryServices.AccountManagement
{
[DirectoryRdnPrefix("CN")]
public class GroupPrincipal : Principal
{
//
// Public constructors
//
public GroupPrincipal(PrincipalContext context)
{
if (context == null)
throw new ArgumentException(SR.NullArguments);
this.ContextRaw = context;
this.unpersisted = true;
}
public GroupPrincipal(PrincipalContext context, string samAccountName) : this(context)
{
if (samAccountName == null)
throw new ArgumentException(SR.NullArguments);
if (Context.ContextType != ContextType.ApplicationDirectory)
this.SamAccountName = samAccountName;
this.Name = samAccountName;
}
//
// Public properties
//
// IsSecurityGroup property
private bool _isSecurityGroup; // the actual property value
private LoadState _isSecurityGroupChanged = LoadState.NotSet; // change-tracking
public Nullable<bool> IsSecurityGroup
{
get
{
// Make sure we're not disposed or deleted. Although HandleGet/HandleSet will check this,
// we need to check these before we do anything else.
CheckDisposedOrDeleted();
// Different stores have different defaults as to the Enabled setting
// (AD: creates disabled by default; SAM: creates enabled by default).
// So if the principal is unpersisted (and thus we may not know what store it's
// going to end up in), we'll just return null unless they previously
// set an explicit value.
if (this.unpersisted && (_isSecurityGroupChanged != LoadState.Changed))
{
GlobalDebug.WriteLineIf(
GlobalDebug.Info,
"Group",
"Enabled: returning null, unpersisted={0}, enabledChanged={1}",
this.unpersisted,
_isSecurityGroupChanged);
return null;
}
return HandleGet<bool>(ref _isSecurityGroup, PropertyNames.GroupIsSecurityGroup, ref _isSecurityGroupChanged);
}
set
{
// Make sure we're not disposed or deleted. Although HandleGet/HandleSet will check this,
// we need to check these before we do anything else.
CheckDisposedOrDeleted();
// We don't want to let them set a null value.
if (!value.HasValue)
throw new ArgumentNullException(nameof(value));
HandleSet<bool>(ref _isSecurityGroup, value.Value, ref _isSecurityGroupChanged, PropertyNames.GroupIsSecurityGroup);
}
}
// GroupScope property
private GroupScope _groupScope = System.DirectoryServices.AccountManagement.GroupScope.Local; // the actual property value
private LoadState _groupScopeChanged = LoadState.NotSet; // change-tracking
public Nullable<GroupScope> GroupScope
{
get
{
// Make sure we're not disposed or deleted. Although HandleGet/HandleSet will check this,
// we need to check these before we do anything else.
CheckDisposedOrDeleted();
// Different stores have different defaults for the GroupScope setting
// (AD: Global; SAM: Local).
// So if the principal is unpersisted (and thus we may not know what store it's
// going to end up in), we'll just return null unless they previously
// set an explicit value.
if (this.unpersisted && (_groupScopeChanged != LoadState.Changed))
{
GlobalDebug.WriteLineIf(
GlobalDebug.Info,
"Group",
"GroupScope: returning null, unpersisted={0}, groupScopeChanged={1}",
this.unpersisted,
_groupScopeChanged);
return null;
}
return HandleGet<GroupScope>(ref _groupScope, PropertyNames.GroupGroupScope, ref _groupScopeChanged);
}
set
{
// Make sure we're not disposed or deleted. Although HandleGet/HandleSet will check this,
// we need to check these before we do anything else.
CheckDisposedOrDeleted();
// We don't want to let them set a null value.
if (!value.HasValue)
throw new ArgumentNullException(nameof(value));
HandleSet<GroupScope>(ref _groupScope, value.Value, ref _groupScopeChanged, PropertyNames.GroupGroupScope);
}
}
// Members property
private PrincipalCollection _members;
public PrincipalCollection Members
{
get
{
// We don't use HandleGet<T> here because we have to load in the PrincipalCollection
// using a special procedure. It's not loaded as part of the regular LoadIfNeeded call.
// Make sure we're not disposed or deleted.
CheckDisposedOrDeleted();
// Check that we actually support this property in our store
//CheckSupportedProperty(PropertyNames.GroupMembers);
if (_members == null)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Members: creating fresh PrincipalCollection");
if (!this.unpersisted)
{
// Retrieve the members from the store.
// QueryCtx because when this group was retrieved, it was
// assigned a _specific_ context for its store
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Members: persisted, querying group membership");
BookmarkableResultSet refs = ContextRaw.QueryCtx.GetGroupMembership(this, false);
_members = new PrincipalCollection(refs, this);
}
else
{
// unpersisted means there's no values to retrieve from the store
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Members: unpersisted, creating empty PrincipalCollection");
_members = new PrincipalCollection(new EmptySet(), this);
}
}
return _members;
}
}
//
// Public methods
//
public static new GroupPrincipal FindByIdentity(PrincipalContext context, string identityValue)
{
return (GroupPrincipal)FindByIdentityWithType(context, typeof(GroupPrincipal), identityValue);
}
public static new GroupPrincipal FindByIdentity(PrincipalContext context, IdentityType identityType, string identityValue)
{
return (GroupPrincipal)FindByIdentityWithType(context, typeof(GroupPrincipal), identityType, identityValue);
}
public PrincipalSearchResult<Principal> GetMembers()
{
return GetMembers(false);
}
public PrincipalSearchResult<Principal> GetMembers(bool recursive)
{
// Make sure we're not disposed or deleted.
CheckDisposedOrDeleted();
if (!this.unpersisted)
{
// Retrieve the members from the store.
// QueryCtx because when this group was retrieved, it was
// assigned a _specific_ context for its store
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetMembers: persisted, querying for members (recursive={0}", recursive);
return new PrincipalSearchResult<Principal>(ContextRaw.QueryCtx.GetGroupMembership(this, recursive));
}
else
{
// unpersisted means there's no values to retrieve from the store
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetMembers: unpersisted, creating empty PrincipalSearchResult");
return new PrincipalSearchResult<Principal>(null);
}
}
private bool _disposed;
public override void Dispose()
{
try
{
if (!_disposed)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Dispose: disposing");
_members?.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}
finally
{
base.Dispose();
}
}
//
// Internal "constructor": Used for constructing Groups returned by a query
//
internal static GroupPrincipal MakeGroup(PrincipalContext ctx)
{
GroupPrincipal g = new GroupPrincipal(ctx);
g.unpersisted = false;
return g;
}
//
// Load/Store implementation
//
//
// Loading with query results
//
internal override void LoadValueIntoProperty(string propertyName, object value)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "LoadValueIntoProperty: name=" + propertyName + " value=" + (value != null ? value.ToString() : "null"));
switch (propertyName)
{
case PropertyNames.GroupIsSecurityGroup:
_isSecurityGroup = (bool)value;
_isSecurityGroupChanged = LoadState.Loaded;
break;
case PropertyNames.GroupGroupScope:
_groupScope = (GroupScope)value;
_groupScopeChanged = LoadState.Loaded;
break;
case PropertyNames.GroupMembers:
Debug.Fail("Group.LoadValueIntoProperty: Trying to load Members, but Members is demand-loaded.");
break;
default:
base.LoadValueIntoProperty(propertyName, value);
break;
}
}
//
// Getting changes to persist (or to build a query from a QBE filter)
//
// Given a property name, returns true if that property has changed since it was loaded, false otherwise.
internal override bool GetChangeStatusForProperty(string propertyName)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetChangeStatusForProperty: name=" + propertyName);
switch (propertyName)
{
case PropertyNames.GroupIsSecurityGroup:
return _isSecurityGroupChanged == LoadState.Changed;
case PropertyNames.GroupGroupScope:
return _groupScopeChanged == LoadState.Changed;
case PropertyNames.GroupMembers:
// If Members was never loaded, it couldn't possibly have changed
if (_members != null)
return _members.Changed;
else
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetChangeStatusForProperty: members was never loaded");
return false;
}
default:
return base.GetChangeStatusForProperty(propertyName);
}
}
// Given a property name, returns the current value for the property.
internal override object GetValueForProperty(string propertyName)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetValueForProperty: name=" + propertyName);
return propertyName switch
{
PropertyNames.GroupIsSecurityGroup => _isSecurityGroup,
PropertyNames.GroupGroupScope => _groupScope,
PropertyNames.GroupMembers => _members,
_ => base.GetValueForProperty(propertyName),
};
}
// Reset all change-tracking status for all properties on the object to "unchanged".
internal override void ResetAllChangeStatus()
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "ResetAllChangeStatus");
_groupScopeChanged = (_groupScopeChanged == LoadState.Changed) ? LoadState.Loaded : LoadState.NotSet;
_isSecurityGroupChanged = (_isSecurityGroupChanged == LoadState.Changed) ? LoadState.Loaded : LoadState.NotSet;
_members?.ResetTracking();
base.ResetAllChangeStatus();
}
/// <summary>
/// if isSmallGroup has a value, it means we already checked if the group is small
/// </summary>
private bool? _isSmallGroup;
/// <summary>
/// cache the search result for the member attribute
/// it will only be set for small groups!
/// </summary>
internal SearchResult SmallGroupMemberSearchResult { get; private set; }
/// <summary>
/// Finds if the group is "small", meaning that it has less than MaxValRange values (usually 1500)
/// The property list for the searcher of a group has "member" attribute. if there are more results than MaxValRange, there will also be a "member;range=..." attribute
/// we can cache the result and don't fear from changes through Add/Remove/Save because the completed/pending lists are looked up before the actual values are
/// </summary>
internal bool IsSmallGroup()
{
if (_isSmallGroup.HasValue)
{
return _isSmallGroup.Value;
}
_isSmallGroup = false;
DirectoryEntry de = (DirectoryEntry)this.UnderlyingObject;
Debug.Assert(de != null);
if (de != null)
{
using (DirectorySearcher ds = new DirectorySearcher(de, "(objectClass=*)", new string[] { "member" }, SearchScope.Base))
{
SearchResult sr = ds.FindOne();
if (sr != null)
{
bool rangePropertyFound = false;
foreach (string propName in sr.Properties.PropertyNames)
{
if (propName.StartsWith("member;range=", StringComparison.OrdinalIgnoreCase))
{
rangePropertyFound = true;
break;
}
}
// we only consider the group "small" if there is a "member" property but no "member;range..." property
if (!rangePropertyFound)
{
_isSmallGroup = true;
SmallGroupMemberSearchResult = sr;
}
}
}
}
return _isSmallGroup.Value;
}
}
}