1
|
using System;
|
2
|
using System.Collections.Generic;
|
3
|
using System.Globalization;
|
4
|
using System.Net.NetworkInformation;
|
5
|
using System.Security.Cryptography;
|
6
|
using System.Security.Cryptography.Xml;
|
7
|
using System.ServiceModel;
|
8
|
using System.Threading;
|
9
|
using System.Xml;
|
10
|
//using log4net;
|
11
|
using Rhino.Licensing.Discovery;
|
12
|
|
13
|
namespace Rhino.Licensing
|
14
|
{
|
15
|
/// <summary>
|
16
|
/// Base license validator.
|
17
|
/// </summary>
|
18
|
public abstract class AbstractLicenseValidator
|
19
|
{
|
20
|
/// <summary>
|
21
|
/// License validator logger
|
22
|
/// </summary>
|
23
|
//protected static readonly ILog Log = LogManager.GetLogger(typeof(LicenseValidator));
|
24
|
|
25
|
/// <summary>
|
26
|
/// Standard Time servers
|
27
|
/// </summary>
|
28
|
protected static readonly string[] TimeServers =
|
29
|
{
|
30
|
"time.nist.gov",
|
31
|
"time-nw.nist.gov",
|
32
|
"time-a.nist.gov",
|
33
|
"time-b.nist.gov",
|
34
|
"time-a.timefreq.bldrdoc.gov",
|
35
|
"time-b.timefreq.bldrdoc.gov",
|
36
|
"time-c.timefreq.bldrdoc.gov",
|
37
|
"utcnist.colorado.edu",
|
38
|
"nist1.datum.com",
|
39
|
"nist1.dc.certifiedtime.com",
|
40
|
"nist1.nyc.certifiedtime.com",
|
41
|
"nist1.sjc.certifiedtime.com"
|
42
|
};
|
43
|
|
44
|
private readonly string licenseServerUrl;
|
45
|
private readonly Guid clientId;
|
46
|
private readonly string publicKey;
|
47
|
private readonly Timer nextLeaseTimer;
|
48
|
private bool disableFutureChecks;
|
49
|
private bool currentlyValidatingSubscriptionLicense;
|
50
|
private readonly DiscoveryHost discoveryHost;
|
51
|
private DiscoveryClient discoveryClient;
|
52
|
private Guid senderId;
|
53
|
|
54
|
/// <summary>
|
55
|
/// Fired when license data is invalidated
|
56
|
/// </summary>
|
57
|
public event Action<InvalidationType> LicenseInvalidated;
|
58
|
|
59
|
/// <summary>
|
60
|
/// Fired when license is expired
|
61
|
/// </summary>
|
62
|
public event Action<DateTime> LicenseExpired;
|
63
|
|
64
|
/// <summary>
|
65
|
/// Event that's raised when duplicate licenses are found
|
66
|
/// </summary>
|
67
|
public event EventHandler<DiscoveryHost.ClientDiscoveredEventArgs> MultipleLicensesWereDiscovered;
|
68
|
|
69
|
/// <summary>
|
70
|
/// Disable the <see cref="ExpirationDate"/> validation with the time servers
|
71
|
/// </summary>
|
72
|
public bool DisableTimeServersCheck
|
73
|
{
|
74
|
get; set;
|
75
|
}
|
76
|
|
77
|
/// <summary>
|
78
|
/// Gets the expiration date of the license
|
79
|
/// </summary>
|
80
|
public DateTime ExpirationDate
|
81
|
{
|
82
|
get; private set;
|
83
|
}
|
84
|
|
85
|
/// <summary>
|
86
|
/// Lease timeout
|
87
|
/// </summary>
|
88
|
public TimeSpan LeaseTimeout { get; set; }
|
89
|
|
90
|
/// <summary>
|
91
|
/// How to behave when using the same license multiple times
|
92
|
/// </summary>
|
93
|
public MultipleLicenseUsage MultipleLicenseUsageBehavior { get; set; }
|
94
|
|
95
|
/// <summary>
|
96
|
/// Gets or Sets the endpoint address of the subscription service
|
97
|
/// </summary>
|
98
|
public string SubscriptionEndpoint
|
99
|
{
|
100
|
get; set;
|
101
|
}
|
102
|
|
103
|
/// <summary>
|
104
|
/// Gets the Type of the license
|
105
|
/// </summary>
|
106
|
public LicenseType LicenseType
|
107
|
{
|
108
|
get; private set;
|
109
|
}
|
110
|
|
111
|
/// <summary>
|
112
|
/// Gets the Id of the license holder
|
113
|
/// </summary>
|
114
|
public Guid UserId
|
115
|
{
|
116
|
get; private set;
|
117
|
}
|
118
|
|
119
|
/// <summary>
|
120
|
/// Gets the name of the license holder
|
121
|
/// </summary>
|
122
|
public string Name
|
123
|
{
|
124
|
get; private set;
|
125
|
}
|
126
|
|
127
|
/// <summary>
|
128
|
/// Gets or Sets Floating license support
|
129
|
/// </summary>
|
130
|
public bool DisableFloatingLicenses
|
131
|
{
|
132
|
get; set;
|
133
|
}
|
134
|
|
135
|
/// <summary>
|
136
|
/// Whether the client discovery server is enabled. This detects duplicate licenses used on the same network.
|
137
|
/// </summary>
|
138
|
public bool DiscoveryEnabled { get; private set; }
|
139
|
|
140
|
/// <summary>
|
141
|
/// Gets extra license information
|
142
|
/// </summary>
|
143
|
public IDictionary<string, string> LicenseAttributes
|
144
|
{
|
145
|
get; private set;
|
146
|
}
|
147
|
|
148
|
/// <summary>
|
149
|
/// Gets or Sets the license content
|
150
|
/// </summary>
|
151
|
protected abstract string License
|
152
|
{
|
153
|
get; set;
|
154
|
}
|
155
|
|
156
|
/// <summary>
|
157
|
/// Creates a license validator with specfied public key.
|
158
|
/// </summary>
|
159
|
/// <param name="publicKey">public key</param>
|
160
|
/// <param name="enableDiscovery">Whether to enable the client discovery server to detect duplicate licenses used on the same network.</param>
|
161
|
protected AbstractLicenseValidator(string publicKey, bool enableDiscovery = false)
|
162
|
{
|
163
|
LeaseTimeout = TimeSpan.FromMinutes(5);
|
164
|
LicenseAttributes = new Dictionary<string, string>();
|
165
|
nextLeaseTimer = new Timer(LeaseLicenseAgain);
|
166
|
this.publicKey = publicKey;
|
167
|
|
168
|
DiscoveryEnabled = enableDiscovery;
|
169
|
|
170
|
if (DiscoveryEnabled)
|
171
|
{
|
172
|
senderId = Guid.NewGuid();
|
173
|
discoveryHost = new DiscoveryHost();
|
174
|
discoveryHost.ClientDiscovered += DiscoveryHostOnClientDiscovered;
|
175
|
discoveryHost.Start();
|
176
|
}
|
177
|
}
|
178
|
|
179
|
/// <summary>
|
180
|
/// Creates a license validator using the client information
|
181
|
/// and a service endpoint address to validate the license.
|
182
|
/// </summary>
|
183
|
protected AbstractLicenseValidator(string publicKey, string licenseServerUrl, Guid clientId)
|
184
|
: this(publicKey)
|
185
|
{
|
186
|
this.licenseServerUrl = licenseServerUrl;
|
187
|
this.clientId = clientId;
|
188
|
}
|
189
|
|
190
|
private void LeaseLicenseAgain(object state)
|
191
|
{
|
192
|
var client = discoveryClient;
|
193
|
if (client != null)
|
194
|
client.PublishMyPresence();
|
195
|
|
196
|
if (HasExistingLicense())
|
197
|
return;
|
198
|
|
199
|
RaiseLicenseInvalidated();
|
200
|
}
|
201
|
|
202
|
private void RaiseLicenseInvalidated()
|
203
|
{
|
204
|
var licenseInvalidated = LicenseInvalidated;
|
205
|
if (licenseInvalidated == null)
|
206
|
throw new InvalidOperationException("License was invalidated, but there is no one subscribe to the LicenseInvalidated event");
|
207
|
|
208
|
licenseInvalidated(LicenseType == LicenseType.Floating ? InvalidationType.CannotGetNewLicense :
|
209
|
InvalidationType.TimeExpired);
|
210
|
}
|
211
|
|
212
|
private void RaiseMultipleLicenseDiscovered(DiscoveryHost.ClientDiscoveredEventArgs args)
|
213
|
{
|
214
|
var onMultipleLicensesWereDiscovered = MultipleLicensesWereDiscovered;
|
215
|
if (onMultipleLicensesWereDiscovered != null)
|
216
|
{
|
217
|
onMultipleLicensesWereDiscovered(this, args);
|
218
|
}
|
219
|
}
|
220
|
|
221
|
private void DiscoveryHostOnClientDiscovered(object sender, DiscoveryHost.ClientDiscoveredEventArgs clientDiscoveredEventArgs)
|
222
|
{
|
223
|
if (senderId == clientDiscoveredEventArgs.SenderId) // we got our own notification, ignore it
|
224
|
return;
|
225
|
|
226
|
if (UserId != clientDiscoveredEventArgs.UserId) // another license, we don't care
|
227
|
return;
|
228
|
|
229
|
// same user id, different senders
|
230
|
switch (MultipleLicenseUsageBehavior)
|
231
|
{
|
232
|
case MultipleLicenseUsage.AllowForSameUser:
|
233
|
if (Environment.UserName == clientDiscoveredEventArgs.UserName)
|
234
|
return;
|
235
|
break;
|
236
|
}
|
237
|
|
238
|
RaiseLicenseInvalidated();
|
239
|
RaiseMultipleLicenseDiscovered(clientDiscoveredEventArgs);
|
240
|
}
|
241
|
|
242
|
/// <summary>
|
243
|
/// Validates loaded license
|
244
|
/// </summary>
|
245
|
public virtual void AssertValidLicense()
|
246
|
{
|
247
|
LicenseAttributes.Clear();
|
248
|
if (HasExistingLicense())
|
249
|
{
|
250
|
if (DiscoveryEnabled)
|
251
|
{
|
252
|
discoveryClient = new DiscoveryClient(senderId, UserId, Environment.MachineName, Environment.UserName);
|
253
|
discoveryClient.PublishMyPresence();
|
254
|
}
|
255
|
return;
|
256
|
}
|
257
|
|
258
|
//Log.WarnFormat("Could not validate existing license\r\n{0}", License);
|
259
|
throw new LicenseNotFoundException();
|
260
|
}
|
261
|
|
262
|
private bool HasExistingLicense()
|
263
|
{
|
264
|
try
|
265
|
{
|
266
|
if (TryLoadingLicenseValuesFromValidatedXml() == false)
|
267
|
{
|
268
|
//Log.WarnFormat("Failed validating license:\r\n{0}", License);
|
269
|
return false;
|
270
|
}
|
271
|
//Log.InfoFormat("License expiration date is {0}", ExpirationDate);
|
272
|
|
273
|
bool result;
|
274
|
if (LicenseType == LicenseType.Subscription)
|
275
|
{
|
276
|
result = ValidateSubscription();
|
277
|
}
|
278
|
else
|
279
|
{
|
280
|
result = DateTime.UtcNow < ExpirationDate;
|
281
|
}
|
282
|
|
283
|
if (result &&
|
284
|
!DisableTimeServersCheck)
|
285
|
{
|
286
|
ValidateUsingNetworkTime();
|
287
|
}
|
288
|
|
289
|
if (!result)
|
290
|
{
|
291
|
if (LicenseExpired == null)
|
292
|
throw new LicenseExpiredException("Expiration Date : " + ExpirationDate);
|
293
|
|
294
|
DisableFutureChecks();
|
295
|
LicenseExpired(ExpirationDate);
|
296
|
}
|
297
|
|
298
|
return true;
|
299
|
}
|
300
|
catch (RhinoLicensingException)
|
301
|
{
|
302
|
throw;
|
303
|
}
|
304
|
catch (Exception)
|
305
|
{
|
306
|
return false;
|
307
|
}
|
308
|
}
|
309
|
|
310
|
private bool ValidateSubscription()
|
311
|
{
|
312
|
if ((ExpirationDate - DateTime.UtcNow).TotalDays > 4)
|
313
|
return true;
|
314
|
|
315
|
if (currentlyValidatingSubscriptionLicense)
|
316
|
return DateTime.UtcNow < ExpirationDate;
|
317
|
|
318
|
if (SubscriptionEndpoint == null)
|
319
|
throw new InvalidOperationException("Subscription endpoints are not supported for this license validator");
|
320
|
|
321
|
try
|
322
|
{
|
323
|
TryGettingNewLeaseSubscription();
|
324
|
}
|
325
|
catch (Exception e)
|
326
|
{
|
327
|
//Log.Error("Could not re-lease subscription license", e);
|
328
|
throw new Exception("Could not re-lease subscription license", e);
|
329
|
}
|
330
|
|
331
|
return ValidateWithoutUsingSubscriptionLeasing();
|
332
|
}
|
333
|
|
334
|
private bool ValidateWithoutUsingSubscriptionLeasing()
|
335
|
{
|
336
|
currentlyValidatingSubscriptionLicense = true;
|
337
|
try
|
338
|
{
|
339
|
return HasExistingLicense();
|
340
|
}
|
341
|
finally
|
342
|
{
|
343
|
currentlyValidatingSubscriptionLicense = false;
|
344
|
}
|
345
|
}
|
346
|
|
347
|
private void TryGettingNewLeaseSubscription()
|
348
|
{
|
349
|
var service = ChannelFactory<ISubscriptionLicensingService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(SubscriptionEndpoint));
|
350
|
try
|
351
|
{
|
352
|
var newLicense = service.LeaseLicense(License);
|
353
|
TryOverwritingWithNewLicense(newLicense);
|
354
|
}
|
355
|
finally
|
356
|
{
|
357
|
var communicationObject = service as ICommunicationObject;
|
358
|
if (communicationObject != null)
|
359
|
{
|
360
|
try
|
361
|
{
|
362
|
communicationObject.Close(TimeSpan.FromMilliseconds(200));
|
363
|
}
|
364
|
catch
|
365
|
{
|
366
|
communicationObject.Abort();
|
367
|
}
|
368
|
}
|
369
|
}
|
370
|
}
|
371
|
|
372
|
/// <summary>
|
373
|
/// Loads the license file.
|
374
|
/// </summary>
|
375
|
/// <param name="newLicense"></param>
|
376
|
/// <returns></returns>
|
377
|
protected bool TryOverwritingWithNewLicense(string newLicense)
|
378
|
{
|
379
|
if (string.IsNullOrEmpty(newLicense))
|
380
|
return false;
|
381
|
try
|
382
|
{
|
383
|
var xmlDocument = new XmlDocument();
|
384
|
xmlDocument.LoadXml(newLicense);
|
385
|
}
|
386
|
catch (Exception e)
|
387
|
{
|
388
|
// Log.Error("New license is not valid XML\r\n" + newLicense, e);
|
389
|
System.Diagnostics.Debug.WriteLine("New license is not valid XML\r\n" + newLicense, e);
|
390
|
//throw new Exception("New license is not valid XML\r\n" + newLicense, e);
|
391
|
return false;
|
392
|
}
|
393
|
License = newLicense;
|
394
|
return true;
|
395
|
}
|
396
|
|
397
|
private void ValidateUsingNetworkTime()
|
398
|
{
|
399
|
if (!NetworkInterface.GetIsNetworkAvailable())
|
400
|
return;
|
401
|
|
402
|
var sntp = new SntpClient(GetTimeServers());
|
403
|
sntp.BeginGetDate(time =>
|
404
|
{
|
405
|
if (time > ExpirationDate)
|
406
|
RaiseLicenseInvalidated();
|
407
|
}
|
408
|
, () =>
|
409
|
{
|
410
|
/* ignored */
|
411
|
});
|
412
|
}
|
413
|
|
414
|
/// <summary>
|
415
|
/// Extension point to return different time servers
|
416
|
/// </summary>
|
417
|
/// <returns></returns>
|
418
|
protected virtual string[] GetTimeServers()
|
419
|
{
|
420
|
return TimeServers;
|
421
|
}
|
422
|
|
423
|
/// <summary>
|
424
|
/// Removes existing license from the machine.
|
425
|
/// </summary>
|
426
|
public virtual void RemoveExistingLicense()
|
427
|
{
|
428
|
}
|
429
|
|
430
|
/// <summary>
|
431
|
/// Loads license data from validated license file.
|
432
|
/// </summary>
|
433
|
/// <returns></returns>
|
434
|
public bool TryLoadingLicenseValuesFromValidatedXml()
|
435
|
{
|
436
|
try
|
437
|
{
|
438
|
var doc = new XmlDocument();
|
439
|
doc.LoadXml(License);
|
440
|
|
441
|
if (TryGetValidDocument(publicKey, doc) == false)
|
442
|
{
|
443
|
//Log.WarnFormat("Could not validate xml signature of:\r\n{0}", License);
|
444
|
return false;
|
445
|
}
|
446
|
|
447
|
if (doc.FirstChild == null)
|
448
|
{
|
449
|
//Log.WarnFormat("Could not find first child of:\r\n{0}", License);
|
450
|
return false;
|
451
|
}
|
452
|
|
453
|
if (doc.SelectSingleNode("/floating-license") != null)
|
454
|
{
|
455
|
var node = doc.SelectSingleNode("/floating-license/license-server-public-key/text()");
|
456
|
if (node == null)
|
457
|
{
|
458
|
//Log.WarnFormat("Invalid license, floating license without license server public key:\r\n{0}", License);
|
459
|
throw new InvalidOperationException(
|
460
|
"Invalid license file format, floating license without license server public key");
|
461
|
}
|
462
|
return ValidateFloatingLicense(node.InnerText);
|
463
|
}
|
464
|
|
465
|
var result = ValidateXmlDocumentLicense(doc);
|
466
|
if (result && disableFutureChecks == false)
|
467
|
{
|
468
|
nextLeaseTimer.Change(LeaseTimeout, LeaseTimeout);
|
469
|
}
|
470
|
return result;
|
471
|
}
|
472
|
catch (RhinoLicensingException)
|
473
|
{
|
474
|
throw;
|
475
|
}
|
476
|
catch (Exception e)
|
477
|
{
|
478
|
//Log.Error("Could not validate license", e);
|
479
|
System.Diagnostics.Debug.WriteLine("Could not validate license", e);
|
480
|
return false;
|
481
|
}
|
482
|
}
|
483
|
|
484
|
private bool ValidateFloatingLicense(string publicKeyOfFloatingLicense)
|
485
|
{
|
486
|
if (DisableFloatingLicenses)
|
487
|
{
|
488
|
// Log.Warn("Floating licenses have been disabled");
|
489
|
System.Diagnostics.Debug.WriteLine("Floating licenses have been disabled");
|
490
|
|
491
|
return false;
|
492
|
}
|
493
|
if (licenseServerUrl == null)
|
494
|
{
|
495
|
//Log.Warn("Could not find license server url");
|
496
|
throw new InvalidOperationException("Floating license encountered, but licenseServerUrl was not set");
|
497
|
}
|
498
|
|
499
|
var success = false;
|
500
|
var licensingService = ChannelFactory<ILicensingService>.CreateChannel(new WSHttpBinding(), new EndpointAddress(licenseServerUrl));
|
501
|
try
|
502
|
{
|
503
|
var leasedLicense = licensingService.LeaseLicense(
|
504
|
Environment.MachineName,
|
505
|
Environment.UserName,
|
506
|
clientId);
|
507
|
((ICommunicationObject)licensingService).Close();
|
508
|
success = true;
|
509
|
if (leasedLicense == null)
|
510
|
{
|
511
|
//Log.WarnFormat("Null response from license server: {0}", licenseServerUrl);
|
512
|
throw new FloatingLicenseNotAvailableException();
|
513
|
}
|
514
|
|
515
|
var doc = new XmlDocument();
|
516
|
doc.LoadXml(leasedLicense);
|
517
|
|
518
|
if (TryGetValidDocument(publicKeyOfFloatingLicense, doc) == false)
|
519
|
{
|
520
|
//Log.WarnFormat("Could not get valid license from floating license server {0}", licenseServerUrl);
|
521
|
throw new FloatingLicenseNotAvailableException();
|
522
|
}
|
523
|
|
524
|
var validLicense = ValidateXmlDocumentLicense(doc);
|
525
|
if (validLicense)
|
526
|
{
|
527
|
//setup next lease
|
528
|
var time = (ExpirationDate.AddMinutes(-5) - DateTime.UtcNow);
|
529
|
System.Diagnostics.Debug.WriteLine("Will lease license again at {0}", time);
|
530
|
//Log.DebugFormat("Will lease license again at {0}", time);
|
531
|
if (disableFutureChecks == false)
|
532
|
nextLeaseTimer.Change(time, time);
|
533
|
}
|
534
|
return validLicense;
|
535
|
}
|
536
|
finally
|
537
|
{
|
538
|
if (success == false)
|
539
|
((ICommunicationObject)licensingService).Abort();
|
540
|
}
|
541
|
}
|
542
|
|
543
|
internal bool ValidateXmlDocumentLicense(XmlDocument doc)
|
544
|
{
|
545
|
var id = doc.SelectSingleNode("/license/@id");
|
546
|
if (id == null)
|
547
|
{
|
548
|
//Log.WarnFormat("Could not find id attribute in license:\r\n{0}", License);
|
549
|
return false;
|
550
|
}
|
551
|
|
552
|
UserId = new Guid(id.Value);
|
553
|
|
554
|
var date = doc.SelectSingleNode("/license/@expiration");
|
555
|
if (date == null)
|
556
|
{
|
557
|
//Log.WarnFormat("Could not find expiration in license:\r\n{0}", License);
|
558
|
return false;
|
559
|
}
|
560
|
|
561
|
ExpirationDate = DateTime.ParseExact(date.Value, "yyyy-MM-ddTHH:mm:ss.fffffff", CultureInfo.InvariantCulture);
|
562
|
|
563
|
var licenseType = doc.SelectSingleNode("/license/@type");
|
564
|
if (licenseType == null)
|
565
|
{
|
566
|
//Log.WarnFormat("Could not find license type in {0}", licenseType);
|
567
|
return false;
|
568
|
}
|
569
|
|
570
|
LicenseType = (LicenseType)Enum.Parse(typeof(LicenseType), licenseType.Value);
|
571
|
|
572
|
var name = doc.SelectSingleNode("/license/name/text()");
|
573
|
if (name == null)
|
574
|
{
|
575
|
//Log.WarnFormat("Could not find licensee's name in license:\r\n{0}", License);
|
576
|
return false;
|
577
|
}
|
578
|
|
579
|
Name = name.Value;
|
580
|
|
581
|
var license = doc.SelectSingleNode("/license");
|
582
|
foreach (XmlAttribute attrib in license.Attributes)
|
583
|
{
|
584
|
if (attrib.Name == "type" || attrib.Name == "expiration" || attrib.Name == "id")
|
585
|
continue;
|
586
|
|
587
|
LicenseAttributes[attrib.Name] = attrib.Value;
|
588
|
}
|
589
|
|
590
|
return true;
|
591
|
}
|
592
|
|
593
|
private bool TryGetValidDocument(string licensePublicKey, XmlDocument doc)
|
594
|
{
|
595
|
var rsa = new RSACryptoServiceProvider();
|
596
|
rsa.FromXmlString(licensePublicKey);
|
597
|
|
598
|
var nsMgr = new XmlNamespaceManager(doc.NameTable);
|
599
|
nsMgr.AddNamespace("sig", "http://www.w3.org/2000/09/xmldsig#");
|
600
|
|
601
|
var signedXml = new SignedXml(doc);
|
602
|
var sig = (XmlElement)doc.SelectSingleNode("//sig:Signature", nsMgr);
|
603
|
if (sig == null)
|
604
|
{
|
605
|
//Log.WarnFormat("Could not find this signature node on license:\r\n{0}", License);
|
606
|
return false;
|
607
|
}
|
608
|
signedXml.LoadXml(sig);
|
609
|
|
610
|
return signedXml.CheckSignature(rsa);
|
611
|
}
|
612
|
|
613
|
/// <summary>
|
614
|
/// Disables further license checks for the session.
|
615
|
/// </summary>
|
616
|
public void DisableFutureChecks()
|
617
|
{
|
618
|
disableFutureChecks = true;
|
619
|
nextLeaseTimer.Dispose();
|
620
|
}
|
621
|
|
622
|
/// <summary>
|
623
|
/// Options for detecting multiple licenses
|
624
|
/// </summary>
|
625
|
public enum MultipleLicenseUsage
|
626
|
{
|
627
|
/// <summary>
|
628
|
/// Deny if multiple licenses are used
|
629
|
/// </summary>
|
630
|
Deny,
|
631
|
/// <summary>
|
632
|
/// Only allow if it is running for the same user
|
633
|
/// </summary>
|
634
|
AllowForSameUser
|
635
|
}
|
636
|
}
|
637
|
}
|