I have to loop through all Rows in a table that contain a user field. I have to retrieve those users and do nasty stuff with them:
private void GetUsrInfo(FieldUserValue user, ClientContext clientContext) {
int id=user.LookupId;
Web web = clientContext.Web;
User spuser = web.GetUserById(id);
clientContext.Load(spuser);
clientContext.ExecuteQuery();
Mail = spuser.Email;
}
This works. However these are "old" entries and a lot of these persons do not even exist anymore. The user-field still contains the data of that now abandoned user, but when I try to retrieve the userdata by GetUserById() I retrieve the following exception:
Microsoft.SharePoint.Client.ServerException: User cannot be found.
at
Microsoft.SharePoint.Client.ClientRequest.ProcessResponseStream(Stream
responseStream) at
Microsoft.SharePoint.Client.ClientRequest.ProcessResponse()
Currently I just catch these Exceptions and proceed to the next user.
But this is bad and very slow.
Is there a more smart way? Anything like "if web.UserExists(id)..."?
EDIT
One possible way to check whether or not the user exists, without throwing an error or creating a new user (as result of the web.EnsureUser(#"domain\username") method) is to load the complete collection of users locally and use a LINQ statement to lookup the user by Id.
For example:
UserCollection collUser = ctx.Web.SiteUsers;
ctx.Load(collUser);
ctx.ExecuteQuery();
var user = collUser.Cast<User>().FirstOrDefault(u => u.Id == 1);
if (null != user)
{
Console.WriteLine("User: {0} Login name: {1} Email: {2}",
user.Title, user.LoginName, user.Email);
}
If there is a record where the ID == 1, it will be returned, if not the return value will be null.
Depending on the number of users in the site, this may have performance concerns, however, based on the number of exceptions you expect to generate checking the user ID, this solution may be feasible.
Reference: Csom or rest to verify user
Related
I'm building a custom workflow where all users that are members of a specific role will receive email notifications depending on certain state changes. I've begun fleshing out e-mail templates via Sitecore items with replaceable tokens, but I'm struggling to find a way to allow the setting of the recipient role in Sitecore. I'd like to avoid having users enter a string representation of the role, so a droplink would be ideal if there were a way to populate it with the various roles defined in sitecore. Bonus points if I can filter the roles that populate the droplink.
I'm aware that users/roles/domains aren't defined as items in the content tree, so how exactly does one go about configuring this droplink?
Sitecore 6.5.
I'm not sure if there is a module for this already made, but you can use this technique: http://newguid.net/sitecore/2013/coded-field-datasources-in-sitecore/
It explains how you can use a class as data source. So you could create a class that lists all user roles.
You might want to take a look at http://sitecorejunkie.com/2012/12/28/have-a-field-day-with-custom-sitecore-fields/ which presents a multilist to allow you to select a list of users.
Also take a look at the Workflow Escaltor Module form which you can borrow the AccountSelector control which allows you to select either individual person or roles.
This is the module I previously used to do this exact thing. The following code gets all the unique email addresses of users and only for those users that have read access to the item (it was a multisite implementation, the roles were restricted to each site but the workflow was shared).
protected override List<string> GetRecipientList(WorkflowPipelineArgs args, Item workflowItem)
{
Field recipientsField = workflowItem.Fields["To"];
Error.Assert((recipientsField != null || !string.IsNullOrEmpty(recipientsField.Value)), "The 'To' field is not specified in the mail action item: " + workflowItem.Paths.FullPath);
List<string> recepients = GetEmailsForUsersAndRoles(recipientsField, args);
if (recepients.Count == 0)
Log.Info("There are no users with valid email addresses to notify for item submission: " + workflowItem.Paths.FullPath);
return recepients;
}
//Returns unique email addresses of users that correspond to the selected list of users/roles
private List<string> GetEmailsForUsersAndRoles(Field field, WorkflowPipelineArgs args)
{
List<string> emails = new List<string>();
List<User> allUsers = new List<User>();
AccountSelectorField accountSelectorField = new AccountSelectorField(field);
List<Account> selectedRoles = accountSelectorField.GetSelectedAccountsByType(AccountType.Role);
List<Account> selectedUsers = accountSelectorField.GetSelectedAccountsByType(AccountType.User);
foreach (var role in selectedRoles)
{
var users = RolesInRolesManager.GetUsersInRole(Role.FromName(role.Name), true).ToList();
if (users.Any())
allUsers.AddRange(users);
}
selectedUsers.ForEach(i => allUsers.Add(Sitecore.Security.Accounts.User.FromName(i.Name, false)));
foreach (var user in allUsers)
{
if (user == null || !args.DataItem.Security.CanRead(user)) continue; //move on if user does not have access to item
if (!emails.Contains(user.Profile.Email.ToLower()))
{
if(user.Profile.Email != null && !string.IsNullOrEmpty(user.Profile.Email.Trim()))
emails.Add(user.Profile.Email.ToLower());
else
Log.Error("No email address setup for user: " + user.Name);
}
}
return emails;
}
I'm using System.DirectoryServices.AccountManagement to query for a user and then find the groups for that user.
var _principalContext = new PrincipalContext(ContextType.Domain, domainAddress, adContainer, adQueryAccount, adQueryAccountPassword);
var user = UserPrincipal.FindByIdentity(_principalContext, IdentityType.SamAccountName, account);
var userGroups = user.GetGroups();
foreach (var group in userGroups.Cast<GroupPrincipal>())
{
//////////////////////////////////////////////////////
// getting the underlying DirectoryEntry shown
// to demonstrate that I can retrieve the underlying
// properties without the exception being thrown
DirectoryEntry directoryEntry = group.GetUnderlyingObject() as DirectoryEntry;
var displayName = directoryEntry.Properties["displayName"];
if (displayName != null && displayName.Value != null)
Console.WriteLine(displayName.Value);
//////////////////////////////////////////////////////
Console.WriteLine(group.DisplayName);// exception thrown here...
}
I can grab the underlying DirectoryEntry object and dump its properties and values but as soon as the GroupPrincipal.DisplayName property (or any property for that matter) is accessed, it throws the following exception:
"System.Runtime.InteropServices.COMException (0x8007200A): The
specified directory service attribute or value does not exist.\r\n\r\n
at System.DirectoryServices.DirectoryEntry.Bind(Boolean
throwIfFail)\r\n at
System.DirectoryServices.DirectoryEntry.Bind()\r\n at
System.DirectoryServices.DirectoryEntry.get_SchemaEntry()\r\n at
System.DirectoryServices.AccountManagement.ADStoreCtx.IsContainer(DirectoryEntry
de)\r\n at
System.DirectoryServices.AccountManagement.ADStoreCtx..ctor(DirectoryEntry
ctxBase, Boolean ownCtxBase, String username, String password,
ContextOptions options)\r\n at
System.DirectoryServices.AccountManagement.PrincipalContext.CreateContextFromDirectoryEntry(DirectoryEntry
entry)\r\n at
System.DirectoryServices.AccountManagement.PrincipalContext.DoLDAPDirectoryInitNoContainer()\r\n
at
System.DirectoryServices.AccountManagement.PrincipalContext.DoDomainInit()\r\n
at
System.DirectoryServices.AccountManagement.PrincipalContext.Initialize()\r\n
at System.DirectoryServices.Account
Management.PrincipalContext.get_QueryCtx()\r\n at
System.DirectoryServices.AccountManagement.Principal.HandleGet[T](T&
currentValue, String name, LoadState& state)\r\n at
System.DirectoryServices.AccountManagement.Principal.get_DisplayName()\r\n
at ConsoleApplication9.Program.Main(String[] args)"
Why would I be able to dump the raw properties of the underlying DirectoryEntry but not be able to call any of the properties directly on the GroupPrincipal? What would cause this exception? Note that this does not happen on the "Domain Users" group but the subsequent groups, it does...
I found the solution. If I pass the context to the GetGroups method, it works.
var user = UserPrincipal.FindByIdentity(_principalContext, IdentityType.SamAccountName, account);
var userGroups = user.GetGroups(_principalContext);
Apparently, this limits the groups retrieved to the domain associated with the context. Although this is not intuitive because the context was used to retrieve the user in the first place!!!
This leads me to believe there must be groups from other domains being returned previously and permissions were as such to prevent accessing that information.
Why are you using the .GetUnderlyingObject() call? Seems totally superfluous... just use the .SamAccountName property of the GroupPrincipal directly...
Try this:
foreach (var group in userGroups.Cast<GroupPrincipal>())
{
Console.WriteLine(group.SamAccountName);
Console.WriteLine(group.DisplayName);
Console.WriteLine(group.IsSecurityGroup);
}
Seems a lot easier - no?
I have a sharepoint field in a list that can be either a user or a group. Using the Server Object Model, I can identify easily whether the user is a group or not.
However, I cannot find a way to achieve this using the Managed Client Object model. Is there a way to know.
I only managed to make it work by looping the list of groups and checking if the there is a group with the name. Howver, this is not exactly correct or efficient. Maybe there is a way to find out using the ListItem of the user. But I did not see any fields that show that user is administrator. I have also tried EnsureUser. This crashes if the user is not a group. So I could work out by using a try/catch but this would be very bad programming.
Thanks,
Joseph
To do this get the list of users from ClientContext.Current.Web.SiteUserInfoList and then check the ContentType of each item that is returned to determine what it is.
Checking the content type is not very direct though, because all you actually get back from each item is a ContentTypeID, which you then have to look-up against the content types of the user list at ClientContext.Current.Web.SiteUserInfoList.ContentTypes. That look-up will return a ContentType object, and you can read from the Name property of that object to see what the list item is.
So an over simplified chunk of code to do this would be:
using Microsoft.SharePoint.Client;
...
ClientContext context = ClientContext.Current;
var q = from i in context.Web.SiteUserInfoList.GetItems(new CamlQuery()) select i;
IEnumerable<ListItem> Items = context.LoadQuery(q);
context.ExecuteQueryAsync((s, e) => {
foreach (ListItem i in Items) {
//This is the important bit:
ContentType contenttype = context.Web.SiteUserInfoList.ContentTypes.GetById(i["ContentTypeId"].ToString());
context.Load(contenttype); //It's another query so we have to load it too
switch (contenttype.Name) {
case "SharePointGroup":
//It's a SharePoint group
break;
case "Person":
//It's a user
break;
case "DomainGroup":
//It's an Active Directory Group or Membership Role
break;
default:
//It's a mystery;
break;
}
}
},
(s, e) => { /* Query failed */ }
);
You didn't specify your platform, but I did all of this in Silverlight using the SharePoint client object model. It stands to reason that the same would be possible in JavaScript as well.
Try Microsoft.SharePoint.Client.Utilities.Utility.SearchPrincipals(...):
var resultPrincipals = Utility.SearchPrincipals(clientContext, clientContext.Web, searchString, PrincipalType.All, PrincipalSource.All, null, maxResults);
The return type, PrincipalInfo, conveniently has a PrincipalType property which you can check for Group.
Assume we have a report called SalesSummary for a large department. This department has many smaller teams for each product. People should be able to see information about their own product, not other teams' products. We also have one domain group for each of these teams.
Copying SalesSummary report for each team and setting the permission is not the best option since we have many products. I was thinking to use a code similar to below on RS, but it doesn't work. Apparently, System.Security.Principal.WindowsPrincipal is disabled by default on RS.
Public Function isPermitted() As Boolean
Dim Principal As New System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent())
If (Principal.IsInRole("group_prod")) Then
Return true
Else
Return false
End If
End Function
I also thought I can send the userID from RS to SQL server, and inside my SP I can use a code similar to below to query active directory. This also doesn't work due to security restriction.
SELECT
*
FROM OPENQUERY(ADSI,'SELECT cn, ADsPath FROM ''LDAP://DC=Fabricam,DC=com'' WHERE objectCategory=''group''')
Is there any easier way to achieve this goal?
Thanks for the help!
The first option you suggested (using embedded code to identify the executing user) will not be reliable. SSRS code is not necessarily executed as the user accessing the report, and may not have access to that users credentials, such as when running a subscription.
Your second approach will work, but requires the appropriate permissions for your SQL server service account to query Active Directory.
Another approach is to maintain a copy of the group membership or user permissions in a SQL table. This table can be updated by hand or with an automated process. Then you can easily incorporate this into both available parameters and core data queries.
So I ended up with this code:
PrincipalContext domain = new PrincipalContext(ContextType.Domain, "AD");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, identityName);
//// if found - grab its groups
if (user != null)
{
PrincipalSearchResult<Principal> _groups = null;
int tries = 0;
//We have this while because GetGroups sometimes fails! Specially if you don't
// mention the domain in PrincipalContext
while (true)
{
try
{
_groups = user.GetGroups();
break;
}
catch (Exception ex)
{
logger.Debug("get groups failed", ex);
if (tries > 5) throw;
tries++;
}
}
// iterate over all groups, just gets groups related to this app
foreach (Principal p in _groups)
{
// make sure to add only group principals
if (p is GroupPrincipal)
{
if (p.Name.StartsWith(GROUP_IDENTIFIER))
{
this.groups.Add((GroupPrincipal)p);
this.groupNames.Add(p.Name);
}
}
}
}
Now, that you have a list of related group you can check the list to authorize the user!
I have an account object that creates a user like so;
public class Account
{
public ICollection<User> Users { get; set; }
public User CreateUser(string email)
{
User user = new User(email);
user.Account = this;
Users.Add(user);
}
}
In my service layer when creating a new user I call this method. However there is a rule that the users email MUST be unique to the account, so where does this go? To me it should go in the CreateUser method with an extra line that just checks that the email is unique to the account.
However if it were to do this then ALL the users for the account would need to be loaded in and that seems like a bit of an overhead to me. It would be better to query the database for the users email - but doing that in the method would require a repository in the account object wouldn't it? Maybe the answer then is when loading the account from the repository instead of doing;
var accountRepository.Get(12);
//instead do
var accountRepository.GetWithUserLoadedOnEmail(12, "someone#example.com");
Then the account object could still check the Users collection for the email and it would have been eagerly loaded in if found.
Does this work? What would you do?
I'm using NHibernate as an ORM.
First off, I do not think you should use exceptions to handle "normal" business logic like checking for duplicate email addresses. This is a well document anti-pattern and is best avoided. Keep the constraint on the DB and handle any duplicate exceptions because they cannot be avoid, but try to keep them to a minimum by checking. I would not recommend locking the table.
Secondly, you've put the DDD tag on this questions, so I'll answer it in a DDD way. It looks to me like you need a domain service or factory. Once you have moved this code in a domain service or factory, you can then inject a UserRepository into it and make a call to it to see if a user already exists with that email address.
Something like this:
public class CreateUserService
{
private readonly IUserRepository userRepository;
public CreateUserService(IUserRepository userRepository)
{
this.userRepository = userRepository;
}
public bool CreateUser(Account account, string emailAddress)
{
// Check if there is already a user with this email address
User userWithSameEmailAddress = userRepository.GetUserByEmailAddress(emailAddress);
if (userWithSameEmailAddress != null)
{
return false;
}
// Create the new user, depending on you aggregates this could be a factory method on Account
User newUser = new User(emailAddress);
account.AddUser(newUser);
return true;
}
}
This allows you to separate the responsiblities a little and use the domain service to coordinate things. Hope that helps!
If you have properly specified the constraints on the users table, the add should throw an exception telling you that there is already a duplicate value. You can either catch that exception in the CreateUser method and return null or some duplicate user status code, or let it flow out and catch it later.
You don't want to test if it exists in your code and then add, because there is a slight possibility that between the test and the add, someone will come along and add the same email with would cause the exception to be thrown anyway...
public User CreateUser(string email)
{
try
{
User user = new User(email);
user.Account = this;
user.Insert();
catch (SqlException e)
{
// It would be best to check for the exception code from your db...
return null;
}
}
Given that "the rule that the users email MUST be unique to the account", then the most important thing is to specify in the database schema that the email is unique, so that the database INSERT will fail if the email is duplicate.
You probably can't prevent two users adding the same email nearly-simultaneously, so the next thing is that the code should handle (gracefully) an INSERT failure cause by the above.
After you've implemented the above, seeing whether the email is unique before you do the insert is just optional.