Creating a new user with role membership from code

Oct 12, 2010 at 7:32 AM

I've managed to create a page that does an auto-login using the current Windows login details (using ServerVariables["LOGON_USER"], which makes me nervous but seems to work), and now I'm extending it to auto-create the user if they don't already exist.

Creating a basic user is simple enough, just get an IMembershipService injected and call the CreateUser() method, my problem is when I get roles from my external source (which has roles matching the defaults), and want to give my new user these roles. The reason I need this is that roles are managed for multiple apps through an external security system, they don't want to manage the roles within Orchard. I'll read in the current role memberships on auto-create, and then have a Sync command that is run via the command-line tool.

From what I understand a UserRolePart is welded on to the User I have created, though I'm struggling to work out how to then manipulate this. I tried looking through the Orchard.Users module, since it lists Roles when you edit a user, but that's all happening through the magic of what's done in Orchard.Roles where it's registering a handler/filter.

Any tips on how I can do this in my custom auto-login module? I don't mind creating a dependency on Orchard.Users and Orchard.Roles to access the Part/PartRecord if needed.

Coordinator
Oct 12, 2010 at 4:20 PM

I think you should shorcut the Roles modules and provide your own implementation instead. This can be done by creating a module depending on Orchard.Roles, or by specifying the class to use as IRoleService in /Config/Host.config (sample.config is an example). Though Orchard should still be able to store the permissions associated with roles, as the different modules will request for them.

Oct 12, 2010 at 10:45 PM

I would rather just be able to add/remove a User from a Role in code. This seems like pretty standard stuff, and would satisfy my needs. I realise it's slightly more complicated due to the Content/Part interaction, but a general pattern of how to edit part properties from code would be a great help.

Is there any existing module that I could use to learn from, creating a Content item, adding a part to it (or getting access to the existing part) and then interacting with that part from code?

How do we access the Parts attached to a Content item?

Coordinator
Oct 12, 2010 at 10:52 PM

I can answer the easy one, to access an attached part:

contentItem.As<ThePart>()

As() is an extension method from Orchard.ContentManagement.

To add/remove a user to/from a specific role, you can check UserRolesPartDriver.cs.

 

Coordinator
Oct 12, 2010 at 11:09 PM

A good place to look for an example of programmatically adding a part to a content type is the SetupService. To see how to add/remove roles on a user, there are good examples in the admin controller of the Orchard.Roles module.

Oct 12, 2010 at 11:37 PM

Thanks guys,

Worked out how to do user.HasPart<IUserRoles>() and user.As<IUserRoles>().Roles.Add(roleName).

var userRoles = user.As<IUserRoles>();
foreach( var role in rolesFromExternalSource )
{
    roles.Roles.Add(role);
}
// Save?
// this didn't seem to work
Services.ContentManager.Publish(user.ContentItem);
// neither did this
Services.ContentManager.Publish(userRoles.ContentItem);
Only issue I'm trying to work out now is saving, digging through the existing modules it all seems to be about using UpdateModel(viewModel). I thought I would post this back for others that might learn from this, I'm still digging right now.

Coordinator
Oct 12, 2010 at 11:40 PM

Actually if you modify a content item, it will be saved automatically at the end of the reques (transaction scope). You don't need to publish again. Publishing is just changing a 'flag' to the content, to show it in the pages.

Oct 12, 2010 at 11:54 PM

I wondered if that was the case (saw lots of Services.TransactionManager.Cancel() calls, which implied that it was Commit by default).

Unfortunately my user isn't being added to the role, could it be because I'm creating them using the IMembershipService.CreateUser() method in the same code, so it's not picking up my change to the roles?

Here's my complete code that does create the user and adds them to the role:

    user = _membershipService.CreateUser(
        new CreateUserParams(
            userName, 
            "NotUsed!", 
            email, 
            null, 
            null,
            true
        )
    );

    // add to roles
    var userRoles = user.As<IUserRoles>();
    userRoles.Roles.Add("Administrator")

Unfortunately the user isn't a member of the Administrator role upon completion, although they do now exist.

Oct 13, 2010 at 1:06 AM

None of my changes to the UserRoles part are being picked up, tried a few different things. Do I need to implement IUpdateModel and use the TryUpdateModel() to trigger the saving of changes?

I'm making my changes inside a controller that's just being hit by a Route (/AutoLogin), which is making changes and then returning a RedirectResult() when it's done.

I looked at the Orchard.Roles AdminController and it's using its RoleService to do all data changes which in turn is using its RoleRepository directly... no help there

I looked at the Orchard.Users AdminController and it implements IUpdateModel and has calls to TryUpdateModel() and Services.ContentManager.UpdateEditorModel()... should I be following the same pattern even though I don't have a ViewModel or anything for my Controller?

I feel that I'm very close, just missing some key piece (i.e. making it save my changes!) to get this working.

Oct 13, 2010 at 1:18 AM

I think I've discovered the problem.

When editing a user through the Orchard.Users Admin UI, changes to the UserPart are being picked up fine, but changes to the UserRoles are manually intercepted by the UserRolesPartDriver (which isn't part of my flow), which internally is using the Repository to create/delete rows as needed.

It would seem that if I want to add/remove UserRoles records I will need to do the same.

Coordinator
Oct 13, 2010 at 1:23 AM

True, the modifications applied to parts are handled by the Drivers. They can be seen as controllers for specific portions of the UI.

Oct 13, 2010 at 6:55 AM

Thanks for all the help, here's a working AutoLoginController for those that are interested. It uses Active Directory, validates that the user exists and logs in as them (doesn't use a password, relying on ServerVariables["LOGON_USER"] to do this validation, hopefully this is safe). It checks for AD Groups with a "CMS " prefix (e.g. "CMS Administrator", "CMS Contributor") and will match them to CMS roles without the prefix (e.g. "Administrator", "Contributor").

To use it just create a route that points to the AutoLogin action, and set that route as peoples default page. You might want to get rid of the "Logon" and "Logout" options from your theme too, since you don't want them using the normal logon/logout functionality.

using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;

using Orchard.Data;
using Orchard.Roles.Models;
using Orchard.Roles.Services;
using Orchard.Security;

namespace CompanyName.Security.Controllers
{
    public class AutoLoginController : Controller
    {
        private readonly IAuthenticationService _authenticationService;
        private readonly IRepository<UserRolesPartRecord> _userRolesRepository;
        private readonly IRoleService _roleService;
        private readonly IMembershipService _membershipService;

        public AutoLoginController(
            IAuthenticationService authenticationService,
            IRepository<UserRolesPartRecord> userRolesRepository,
            IRoleService roleService,
            IMembershipService membershipService)
        {
            _authenticationService = authenticationService;
            _userRolesRepository = userRolesRepository;
            _roleService = roleService;
            _membershipService = membershipService;
        }

        public ActionResult AutoLogin()
        {
            var currentUser = _authenticationService.GetAuthenticatedUser();

            if (currentUser == null)
            {
                if (!LoginUsingWindowsCredentials())
                {
                    // login failed, redirect to normal logon page
                    return new RedirectResult("~/Users/Account/Logon");
                }
            }
            return new RedirectResult("~/default.aspx?");
        }

        private bool LoginUsingWindowsCredentials()
        {
            var userIsValid = false;

            // check if we can login using Windows credentials
            var userName = Request.ServerVariables["LOGON_USER"];
            if (!String.IsNullOrEmpty(userName))
            {
                string email;
                IEnumerable<string> roles;
                if (GetUserDetails(userName, out email, out roles))
                {
                    var user = _membershipService.GetUser(userName);

                    if (user == null)
                    {
                        user = _membershipService.CreateUser(
                            new CreateUserParams(
                                userName,
                                "NotUsed!",
                                email,
                                null,
                                null,
                                true
                            )
                        );
                    }

                    UpdateUserRoles(user, roles);

                    // sign in
                    _authenticationService.SignIn(user, false);
                    userIsValid = true;
                }
            }
            return userIsValid;
        }

        private static bool GetUserDetails(string fullUserName, out string email, out IEnumerable<string> roles)
        {
            var domain = SubStringBefore(fullUserName, "\\");
            var userName = SubstringAfter(fullUserName, "\\");

            // default values
            email = null;
            roles = null;

            using (var ctx = new PrincipalContext(ContextType.Domain, domain))
            {
                using (var userPrincipal = UserPrincipal.FindByIdentity(ctx, userName))
                {
                    if (userPrincipal == null )
                    {
                        return false;
                    }

                    email = userPrincipal.EmailAddress;
                    // NOTE: assuming "CMS " prefix on role names here
                    roles = userPrincipal.GetGroups().Where(g => g.Name.StartsWith("CMS ")).Select(g => SubstringAfter(g.Name, "CMS ")).ToList();

                    return true;
                }
            }
        }

        private static string SubStringBefore(string source, string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return value;
            }
            CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;
            int index = compareInfo.IndexOf(source, value, CompareOptions.Ordinal);
            if (index < 0)
            {
                //No such substring
                return string.Empty;
            }
            return source.Substring(0, index);
        }

        public static string SubstringAfter(string source, string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return source;
            }
            CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;
            int index = compareInfo.IndexOf(source, value, CompareOptions.Ordinal);
            if (index < 0)
            {
                //No such substring
                return string.Empty;
            }
            return source.Substring(index + value.Length);
        }

        private void UpdateUserRoles(IUser user, IEnumerable<string> roles)
        {
            var currentUserRoleRecords = _userRolesRepository.Fetch(x => x.UserId == user.Id);
            var currentRoleRecords = currentUserRoleRecords.Select(x => x.Role);
            var targetRoleRecords = _roleService.GetRoles().Where(x => roles.Contains(x.Name));

            foreach (var addingRole in targetRoleRecords.Where(x => !currentRoleRecords.Contains(x)))
            {
                _userRolesRepository.Create(new UserRolesPartRecord { UserId = user.Id, Role = addingRole });
            }

            foreach (var removingRole in currentUserRoleRecords.Where(x => !targetRoleRecords.Contains(x.Role)))
            {
                _userRolesRepository.Delete(removingRole);
            }
        }
    }
}

Coordinator
Oct 13, 2010 at 7:15 AM

Create a CodePlex project. Publish it on the gallery.

Oct 15, 2010 at 1:12 PM
bertrandleroy wrote:

Create a CodePlex project. Publish it on the gallery.

 Where is the gallery? Missing it somehow.... looking at the codeplex site and the http://www.orchardproject.net/ site.

Oct 15, 2010 at 1:41 PM

Found it in the docs:

"By default, a single feed is exposed from http://orchardproject.net/gallery/feed. You can also add additional feeds (note that a reference implementation for exposing a gallery feed is included in the Orchard source tree on Orchard.CodePlex.com). If you are interested in publishing to the default Orchard gallery feed, please contact us!"

http://www.orchardproject.net/docs/Module%20gallery%20feeds.ashx

Coordinator
Oct 15, 2010 at 4:53 PM

Yes, send me e-mail at bleroy at microsoft and I'll et that up for you.

Mar 22, 2011 at 4:16 PM

Yes, I think its a great idea to have Security Groups for users with common resource access and perhaps also distribution groups for emails.

Jun 9, 2011 at 8:00 PM

This is a great solution Timothy...I didn't see that it had been published to the gallery, so I thought I'd ask a couple quick questions regarding implementing this.

1. Where should the AutoLoginController.cs file be placed in the directory structure? Orchard.Web, Orchard.Core, Orchard.Framework, one of the Modules, custom Module?

2. Where would I create the route to make sure that users are directed to this action first?

Thanks!

Jun 10, 2011 at 12:58 AM
Hi Psenechal,

We ended up dropping Orchard and choosing Umbraco for our project,
we'll have another look at it when it has another major release or two
maybe. I know the Auto-Login required some dirty hacks in newer
versions of IIS (replacing one of the IIS DLL's with one from a
Microsoft Employee's blog). I didn't feel very comfortable with it at
all.

If you're still interested I could see if I still have the old code
lying around.

Timothy.

On Fri, Jun 10, 2011 at 5:00 AM, [email removed] wrote:
> From: psenechal
>
> This is a great solution Timothy...I didn't see that it had been published
> to the gallery, so I thought I'd ask a couple quick questions regarding
> implementing this.
>
> 1. Where should the AutoLoginController.cs file be placed in the directory
> structure? Orchard.Web, Orchard.Core, Orchard.Framework, one of the Modules,
> custom Module?
>
> 2. Where would I create the route to make sure that users are directed to
> this action first?
>
> Thanks!
>
> Read the full discussion online.
>
> To add a post to this discussion, reply to this email
> ([email removed])
>
> To start a new discussion for this project, email
> [email removed]
>
> You are receiving this email because you subscribed to this discussion on
> CodePlex. You can unsubscribe on CodePlex.com.
>
> Please note: Images and attachments will be removed from emails. Any posts
> to this discussion will also be available online at CodePlex.com
Jun 10, 2011 at 5:48 AM

Hi Timothy...thanks for the response. Dirty hacks are probably something I want to avoid on a production server =) Thanks for the explanation of your modification...I guess we'll stick with Sitefinity for a while longer as well.

Jan 24, 2012 at 3:18 PM

Hi, 

Please how a can get the autologincontroller for to be implemented in AD infrastructure? Anybody can help me? Thanks.