SharingCapability SharePoint Online Site Collection c# - c#

I want to set the SharingCapability property after provisioning my site collection. I used the documentation of PnP, as you can find here (External Sharing APIs for SharePoint and OneDrive for Business (Core.ExternalSharing))
When I set the property to 'ExternalUserAndGuestSharing', nothing happens. The site collection is still disabled to share with external.
Here is my code:
public static void SetSharingCapability(string fullWebUrl, ClientContext context)
{
Tenant tenant = new Tenant(context);
SiteProperties siteProp = tenant.GetSitePropertiesByUrl(fullWebUrl, true);
context.Load(siteProp);
context.ExecuteQuery();
siteProp.SharingCapability = SharingCapabilities.ExternalUserAndGuestSharing;
siteProp.Update();
context.ExecuteQuery();
}
Do you habe any suggestions for solving the 'problem'?

Most likely the moment once settings (SharingCapability) are applied, the operation itself is not yet completed and that the reason why updated settings are not yet reflected:
siteProp.SharingCapability = SharingCapabilities.ExternalUserAndGuestSharing;
siteProp.Update();
context.ExecuteQuery(); //<- even though the query is submitted to the server there is no guarantee at this moment the update operation is completed
To ensure the update operation is completed you could consider the following solution:
public static void SetSharingCapability(string fullWebUrl, ClientContext context)
{
Tenant tenant = new Tenant(context);
SiteProperties siteProp = tenant.GetSitePropertiesByUrl(fullWebUrl, true);
siteProp.SharingCapability = SharingCapabilities.Disabled;
siteProp.Update();
context.ExecuteQuery();
siteProp = tenant.GetSitePropertiesByUrl(fullWebUrl, true);
context.Load(siteProp, p => p.Status);
context.ExecuteQuery();
while (siteProp.Status == "Updating")
{
Thread.Sleep(TimeSpan.FromSeconds(1));
siteProp = tenant.GetSitePropertiesByUrl(fullWebUrl, true);
context.Load(siteProp);
context.ExecuteQuery();
}
}

Related

Cached or not thread safe HttpContextAccessor?

I was running into strange user access issues which currently stands resolved. I have this scoped service as below.
services.AddScoped<IJwtService, JwtService>();
The following custom middleware, which has the above service DI through constructor.
app.UseMiddleware<AccessCheckToRoutesMiddleware>();
The following was the original code for the JwtService
public JwtService(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
//Later removed this section of code below to make it work consistently
try
{
_identity = httpContextAccessor.HttpContext.User.Identity as ClaimsIdentity;
}
catch { }
}
private void _getIdentity()
{
if (_identity==null) _identity = httpContextAccessor.HttpContext.User.Identity as ClaimsIdentity;
//Later changed the above section of code as below to make it work consistently
_identity = httpContextAccessor.HttpContext.User.Identity as ClaimsIdentity;
}
public bool IsPrivilegedUser()
{
_getIdentity();
var val = _identity.FindFirst("IsPrivilegedUser")?.Value;
return val.Equals("True", StringComparison.OrdinalIgnoreCase);
}
As explained in the code above, if I don't store the httpContextAccessor.HttpContext.User.Identity into a variable and get it every time straight, it gives strange results (basically the claims gets mixed up between various users accessing the app at that time. Can somebody exlain what is going on here? Is this a thread safety issue or is it some type of caching issue?

How to request Contacts and Calendar Permissions on XamarinMac?

I'm trying to access the ContactStore of MacOS App, did the following implementation
public void Contacts()
{
//Starting
var store = new CNContactStore();
store.RequestAccess(CNEntityType.Contacts, async (bool granted, NSError error) =>
{
if (granted)
{
//Query things
}
});
}
The thing is that the Authorize screen never popup, and the Permission always say denied.
If I go to settings to remove that record, the app is not there.
Can someone please point me to the light?
It works for me with those two lines:
var contactStore = new CNContactStore();
var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts);
It could be also that you need to set NSContactsUsageDescription in info.plist.

.Net Core Identity SignInManager in Console App w/o DI/WebHost

I'm attempting to write a generic .Net Core 2.2 Console Application that allows me to use Identity. Specifically I have a database and am simply tring to call SignInManager.PasswordSignInAsync() to authenticate the username/password against my DB.
If I run this in a full blown .NetCore WebApp, where the HttpContext and DI are all built out, it works fine. If I strip it down and simply call the base services I get the same error every time.
I've been trying variants for a few days now and simply cannot figure out what I'm missing.
Any assistance would be greatly appreciated.
I have a class which manages the buildout of the services available for the console app.
public class FXLoginProvider
{
private readonly IServiceCollection _svccoll;
private UserManager<FXUser> _um = null;
private SignInManager<FXUser> _sm = null;
public UserManager<FXUser> UserMgr
{
get { return _um ?? (_um = _svccoll.BuildServiceProvider().GetService<UserManager<FXUser>>()); }
}
public SignInManager<FXUser> SignInMgr
{
get { return _sm ?? (_sm = _svccoll.BuildServiceProvider().GetService<SignInManager<FXUser>>()); }
}
public FXLoginProvider()
{
string s = "Data Source=.\\SQLEXPRESS;Initial catalog=csNextGen;Integrated Security=True;TrustServerCertificate=True;ApplicationIntent=ReadWrite";
_svccoll = new ServiceCollection();
_svccoll.AddDbContext<FXDataContext>(options => { options.UseSqlServer(s); });
_svccoll.AddIdentity<FXUser, FXUserRole>().AddDefaultTokenProviders();
_svccoll.AddTransient<IUserStore<FXUser>, FXUserStore>();
_svccoll.AddTransient<IRoleStore<FXUserRole>, FXRoleStore>();
_svccoll.AddLogging();
_svccoll.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
}
Then in my main app...
class Program
{
static void Main(string[] args)
{
try
{
FXUser uu = null;
string sUsername = "user";
string sPassword = "P$sSw0rrD#!";
// create the service provider
FXLoginProvider icp = new FXLoginProvider();
// grab the sign in manager
SignInManager<FXUser> sm1 = icp.SignInMgr;
// fetch the user from the db, this works.
uu = icp.UserMgr.FindByNameAsync(sUsername).Result;
// update my security stamp, this works too
sm1.UserManager.UpdateSecurityStampAsync(uu).GetAwaiter().GetResult();
// I was receiving a Null context error, so I added a default context.
sm1.Context = new DefaultHttpContext();
var r = sm1.PasswordSignInAsync(sUsername, sPassword, false, false).GetAwaiter().GetResult();
Console.WriteLine(r);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
and it always throws the same exception:
Value cannot be null.\r\nParameter name: provider
I do see in the StackTrace it is throwing down in DependencyInjection.ServiceProviderServiceExtensions (source code for DI.SPSE) because the IServiceProvider is null; so I guess I'm missing a service in my list?
I was able to figure out the problem with my implementation. My error was simply that I had not completely filled in the default http context properly.
sm1.Context = new DefaultHttpContext();
should have been
sm1.Context = new DefaultHttpContext() { RequestServices = icp._svccoll.BuildServiceProvider() };
Note: I needed to change the access level of the _svccoll too.
With this change in place I was able to use the SignInManager to authenticate against my back end database.
I battled this problem for days so I'm happy to share my solution (solution is available on GitHub). I hope this helps!
SingInManager relies on cookie, you can’t use it in console app. Instead of it use UserManager<> there a method to verify password

SQL Server Tokencache issue

I basically took code from here https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-multitenant-openidconnect/blob/master/TodoListWebApp/DAL/EFADALTokenCache.cs but it is not suitable for my application as I don't need the cache per user as given in the example. Accordingly I removed the constructor that accepted User as a parameter since I wanted the cache to be global. I have came up with this version:
public class EFTestTokenCache : TokenCache
{
private TestEntities _TestEntities = new TestEntities();
private TestTokenCache _cache;
public EFTestTokenCache()
{
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
this.BeforeWrite = BeforeWriteNotification;
}
// clean up the DB
public override void Clear()
{
base.Clear();
foreach (var cacheEntry in _TestEntities.TestTokenCaches)
_TestEntities.TestTokenCaches.Remove(cacheEntry);
_TestEntities.SaveChanges();
}
// Notification raised before ADAL accesses the cache.
// This is your chance to update the in-memory copy from the DB, if the in-memory version is stale
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
if (_cache == null)
{
// first time access
_cache = _TestEntities.TestTokenCaches.FirstOrDefault(c => c.webUserUniqueId == args.DisplayableId);
}
else
{ // retrieve last write from the DB
var status = from e in _TestEntities.TestTokenCaches
where (e.webUserUniqueId == args.DisplayableId)
select new
{
LastWrite = e.LastWrite
};
// if the in-memory copy is older than the persistent copy
if (status.First().LastWrite > _cache.LastWrite)
//// read from from storage, update in-memory copy
{
_cache = _TestEntities.TestTokenCaches.FirstOrDefault(c => c.webUserUniqueId == args.DisplayableId);
}
}
this.Deserialize((_cache == null) ? null : _cache.cacheBits);
}
// Notification raised after ADAL accessed the cache.
// If the HasStateChanged flag is set, ADAL changed the content of the cache
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if state changed
if (this.HasStateChanged)
{
if (_cache != null)
{
_cache.cacheBits = this.Serialize();
_cache.LastWrite = DateTime.Now;
}
else
{
_cache = new TestTokenCache
{
webUserUniqueId = args.DisplayableId,
cacheBits = this.Serialize(),
LastWrite = DateTime.Now
};
}
// update the DB and the lastwrite
_TestEntities.Entry(_cache).State = _cache.EntryId == 0 ? EntityState.Added : EntityState.Modified;
_TestEntities.SaveChanges();
this.HasStateChanged = false;
}
}
void BeforeWriteNotification(TokenCacheNotificationArgs args)
{
// if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
}
}
Do you think this would work fine as a global cache or is it buggy and always has to be user based as given in the example?
Another query is why is the database cleared in Clear(). Does this mean whenever application pool shuts down or so my database would be cleared? That should not happen though.
Any help is appreciated.
If you are trying to implement a global token cache irrespective of the user then I see an issue with your code as code is looking for any existing cache per the sign in user
as code is using webUserUniqueId to filter
_TestEntities.TestTokenCaches.FirstOrDefault(c => c.webUserUniqueId == args.DisplayableId);
In the correct sample code, every user has a set of tokens that are saved in a DB (or as collection), so that when they sign in to the web app they can directly perform their web API calls without having to re-authenticate/repeat consent.
I am not sure of the purpose why you want to do this but in my opinion if you are implementing a custom token cache for a web it would be good to provide the desirable level of isolation between tokens for different users signing in.
Also, Clear() method clears the cache by deleting all the items in db but this method has not been called in the GitHub sample and you need to add a call to authContext.TokenCache.clear() from SignOut() method of AccountController to clear the cache on user signout.

GroupPrincipal.GetMembers and cross-domain members error

I have 2 domains, A and B. The Domain A has the group GroupA which contains users from Domain B.
My code:
using (var context = new PrincipalContext(ContextType.Domain, DomainName, User, Password))
{
using (var groupPrincipal = GroupPrincipal.FindByIdentity(context, IdentityType.SamAccountName,
groupName))
{
if (groupPrincipal == null) return null;
using (var principalSearchResult = groupPrincipal.GetMembers(true))
{
var changedUsersFromGroup =
principalSearchResult
.Where(member => member is UserPrincipal)
.Where(member => IsModifiedUser(member, usnChanged))
.Cast<UserPrincipal>()
.Select(adsUser => new AdsUser(adsUser)).Cast<IAdsUser>()
.ToArray();
return changedUsersFromGroup;
}
}
}
System.DirectoryServices.AccountManagement.PrincipalOperationException:
While trying to resolve a cross-store reference, the target principal
could not be found in the domain indicated by the principal's SID.
But if I add user from here
new PrincipalContext(ContextType.Domain, DomainName, User, Password)
to domain B, it works correctly.
How can I fix it?
At least in .NET 4.7, you can work around it by manually managing the enumerator. This has been tested and has been able to successfully get past the errors.
static System.Guid[] GetGroupMemberGuids(System.DirectoryServices.AccountManagement.GroupPrincipal group)
{
System.Collections.Generic.List<System.Guid> result = new List<Guid>();
if (group == null) return null;
System.DirectoryServices.AccountManagement.PrincipalCollection px = group.Members;
System.Collections.IEnumerator en = px.GetEnumerator();
bool hasMore = true;
int consecFaults = 0;
while (hasMore && consecFaults < 10)
{
System.DirectoryServices.AccountManagement.Principal csr = null;
try
{
hasMore = en.MoveNext();
if (!hasMore) break;
csr = (System.DirectoryServices.AccountManagement.Principal)en.Current;
consecFaults = 0;
}
catch (System.DirectoryServices.AccountManagement.PrincipalOperationException e)
{
Console.Error.WriteLine(" Unable to enumerate a member due to the following error: {0}", e.Message);
consecFaults++;
csr = null;
}
if (csr is System.DirectoryServices.AccountManagement.UserPrincipal)
result.Add(csr.Guid.Value);
}
if (consecFaults >= 10) throw new InvalidOperationException("Too many consecutive errors on retrieval.");
return result.ToArray();
}
Check if it behaves differently when not casting to UserPrincipal, e.g.
var changedUsersFromGroup = principalSearchResult.ToArray();
As per other threads it might be some issue there. Also as per MSDN, using GetMembers(true) returned principal collection that does not contain group objects, only leaf nodes are returned, and so maybe you don't need that casting at all. Next, is to check how many results such search would return. If your AD has many users/nested groups, it might be better to try not to use GetMembers(true) to ensure it works on small groups of users.
It seems you are defining a PrincipalContext of domain A (so you can get the group), but because the users inside the group are defined in domain B the context cannot access them (as it's a domain A context).
You might need to define a second 'PricipalContext` for domain B and run the query against it and filter the objects using maybe the list of SIDs of the users located in the domain A group (you'll need to get the list of SIDs without causing the underlying code to try and resolve them).
Hope it helps!
Unfortunately I currently cannot test it, but maybe you can try this
var contextB = new PrincipalContext(ContextType.Domain, DomainName_B, User_B, Password_B)
[..]
var changedUsersFromGroup =
principalSearchResult
.Where(member => member is UserPrincipal)
.Where(member => IsModifiedUser(member, usnChanged))
.Select(principal => Principal.FindByIdentity(contextB, principal.SamAccountName))
.Cast<UserPrincipal>()
.Select(adsUser => new AdsUser(adsUser)).Cast<IAdsUser>()
.ToArray();
This could work, but only, if all members are in Domain B of course. If they are mixed in different domains you may have to filter it before that and iterate through your domains.
Alternatively you could run your application with a domain account? Then don't pass user/pass and give the required access rights to this account to avoid the error.
Explanation: The domain context will switch for your retrieved principals (you can view this in debugging mode if you comment out the new AdsUser / IAdsUser Cast part). This is the one, that seems to cause the exception. Though exactly this is the part I cannot test, I think the creation of AdsUser creates a new ldap Bind to the target Domain. This fails with the "original" Credentials. Is AdsUser Part of ActiveDS or 3rd Party? I did not find any hint if passing credentials uses basic authentication, but I think it should. Using application credentials uses negotiate and "handling this over" to the new AdsUser(..) should fix the issue as well.
problem found and reported to MS as bug. currently impossible to do it with .net :( but it works via native c++ api via queries

Categories