views:

409

answers:

4

EDIT - Rewrote my original question to give a bit more information


Background info
At my work I'm working on a ASP.Net web application for our customers. In our implementation we use technologies like Forms authentication with MembershipProviders and RoleProviders. All went well until I ran into some difficulties with configuring the roles, because the roles aren't system-wide, but related to the customer accounts and projects.

I can't name our exact setup/formula, because I think our company wouldn't approve that...

What's a customer / project?
Our company provides management information for our customers on a yearly (or other interval) basis.
In our systems a customer/contract consists of:

  • one Account: information about the Company
  • per Account, one or more Products: the bundle of management information we'll provide
  • per Product, one or more Measurements: a period of time, in which we gather and report the data

Extranet site setup
Eventually we want all customers to be able to access their management information with our online system. The extranet consists of two sites:

  • Company site: provides an overview of Account information and the Products
  • Measurement site: after selecting a Measurement, detailed information on that period of time

The measurement site is the most interesting part of the extranet. We will create submodules for new overviews, reports, managing and maintaining resources that are important for the research.

Our Visual Studio solution consists of a number of projects. One web application named Portal for the basis. The sites and modules are virtual directories within that application (makes it easier to share MasterPages among things).

What kind of roles?
The following users (read: roles) will be using the system:

  • Admins: development users :) (not customer related, full access)
  • Employees: employees of our company (not customer related, full access)
  • Customer SuperUser: top level managers (full access to their account/measurement)
  • Customer ContactPerson: primary contact (full access to their measurement(s))
  • Customer Manager: a department manager (limited access, specific data of a measurement)

What about ASP.Net users?
The system will have many ASP.Net users, let's focus on the customer users:

  • Users are not shared between Accounts
  • SuperUser X automatically has access to all (and new) measurements
  • User Y could be Primary contact for Measurement 1, but have no role for Measurement 2
  • User Y could be Primary contact for Measurement 1, but have a Manager role for Measurement 2
  • The department managers are many individual users (per Measurement), if Manager Z had a login for Measurement 1, we would like to use that login again if he participates in Measurement 2.

URL structure
These are typical urls in our application:

We will also create a document url, where you can request a specific document by it's GUID. The system will have to check if the user has rights to the document. The document is related to a Measurement, the User or specific roles have specific rights to the document.

What's the problem? (finally ;))
Roles aren't enough to determine what a user is allowed to see/access/download a specific item. It's not enough to say that a certain navigation item is accessible to Managers. When the user requests Measurement 1000, we have to check that the user not only has a Manager role, but a Manager role for Measurement 1000.

Summarized:

  1. How can we limit users to their accounts/measurements?
    (remember superusers see all measurements, some managers only specific measurements)

  2. How can we apply roles at a product/measurement level? (user X could be primarycontact for measurement 1, but just a manager for measurement 2)

  3. How can we limit manager access to the reports screen and only to their department's reports?

All with the magic of asp.net classes, perhaps with a custom roleprovider implementation.

Similar Stackoverflow question/problem
http://stackoverflow.com/questions/1367483/asp-net-how-to-manage-users-with-different-types-of-roles

A: 

Hey,

Store a value in the profile potentially. Setup a profile entry in the config file and use that to store the value.

More realistically, you may want to store this outside of the ASP.NET tables for ease of use and for ease of accessing the value (maybe outside of the web environment if you need to)...

Not sure what all your requirements are.

Brian
+3  A: 

What you are seeking from the various posts that I see, is a custom role mechanism or said another way, a custom Authorization mechanism. Authentication can still use the standard SqlMembershipProvider.

I'm not sure that the standard role provider will provide you with what you want as authorization requires that you have the context of the Project. However, you might investigate writing a custom RoleProvider to see if you can create some custom methods that would do that. Still, for the purposes of answering the question, I'm going to assume you cannot use the SqlRoleProvider.

So, here's some potential schema:

Create Table Companies
(
    Id int not null Primary Key
    , ...
)
Create Table Projects
(
    Id int not null Primary Key
    , PrimaryContactUserId uniqueidentifier
    , ...
    , Constraint FK_Projects_aspnet_Users
        Foreign Key ( PrimaryContactUserId )
        References dbo.aspnet_Users ( UserId )
)
Create Table Roles
(
    Name nvarchar(100) not null Primary Key
    , ...
)

Create Table ProjectCompanyRoles
(
    CompanyId int not null
    , ProjectId int not null
    , RoleName nvarchar(100) not null
    , Constraint FK_...
)

As I said before, the reason for including PrimaryContact in the Projects table is to ensure that there is only one for a given project. If you include it as a role, you would have to include a bunch of hoop jumping code to ensure that a project is not assigned more than one PrimaryContact. If that were the case, then take out the PrimaryContactUserId from the Projects table and make it a role.

Authorization checks would entail queries against the ProjectCompanyRoles. Again, the addition of the contexts of Project and Company make using the default role providers problematic. If you wanted to use the .NET mechanism for roles as well as authentication, then you will have to implement your own custom RoleProvider.

Thomas
You are implying a 1:1 relation between user and project which is not the case. A project has many users, one of the being the primary contact, however other users could have admin, edit, x, y, z roles for that specific project.
Zyphrax
Is it the case that each project must have one and only one primary contact? The above structure does not preclude managing the other roles with respect to a given project; it simply provides a means to ensure each project has a primary contact and, in addition to the roles structure, can give someone special permissions.
Thomas
@thomas, I think you may be overthinking this. The problem as I see it is simply providing a central membership authority and distinct role silos.
Sky Sanders
@Thomas - +2 - asshat tax.
Sky Sanders
A: 

This is exactly the kind of scenario that calls for a custom RoleProvider. You design the database schema to support your case (you might want to create a table called ProjectRole and a table called CompanyRole).

Here are a couple of things to get you started (with links to help at the bottom):

Add this section to your web.config:

<roleManager defaultProvider="MyRoleProvider" enabled="true">
    <providers>
        <add name="MyRoleProvider" type="MyNamespace.MyRoleProvider, MyAssembly, Version=1.0.0.0" description="My Custom Role Provider." enableSearchMethods="false" applicationName="MyApplicationName"/>
    </providers>
</roleManager>

Then this is what the MyRoleProvider class looks like (more or less):

(NOTE: your class must inherit from System.Web.Security.RoleProvider)

namespace MyNamespace
{
    ...

    public class MyRoleProvider : System.Web.Security.RoleProvider
    {
        private string _applicationName;

        public MyRoleProvider()
        {
        }

        public override string ApplicationName
        {
            get
            {
                return _applicationName;
            }
            set
            {
                _applicationName = value;
            }
        }

        ...

    }
}

Then you just need to override some methods to provide your application with the information it needs:

At a minimum, I would override these 2 methods:

  • GetRolesForUser
  • IsUserInRole

But you can also override these methods if you want to:

  • AddUsersToRoles
  • RemoveUsersFromRoles
  • FindUsersInRole
  • GetUsersInRole
  • GetAllRoles
  • CreateRole
  • DeleteRole
  • RoleExists

Nor here are the links I promised:

Gabriel McAdams
+1  A: 

DISCLAIMER: Pursuant to the exchange in comments, in which I make a complete asshat of myself, an almost out of the box solution has been arrived at and this answer has been purged of all asshattery and now contains only a tested scenario that may or may not address the OP problem. ;-)

Kudos to Thomas for keeping his cool and not giving up.


Z- tell me if I understand you:

You want a central membership provider for all apps/projects and a distinct role silo for each app/project?

You may not need to implement custom providers. The standard stack may suffice with a minor stored procedure modification. It is always best to try and sweet talk the baked-in systems to do what you want. It leads to less work and more sleep.

The salient facets of the proposed solution:

  • A common database and connection string,
  • A common membership application name,
  • A common machineKey section so that each site will use the common forms ticket.
  • A UNIQUE role provider application name (or projectId, as you say).
  • A modified aspnet_Users_DeleteUser sproc.

The modification to aspnet_Users_DeleteUser involves cleaning up the user references in aspnet_users that are dynamically created by the Roles and Profile providers and carries a condition that a particular aspnet_db instance is owned by the common MembershipProvider, and only the sites that use that common Membership provider should connect to it.

To map this solution to the OP scenario:

Each Account/Company would have a distinct aspnet_db instance and the 'ProjectId' would be mapped to the applicationName attribute of the RoleManager provider element.

As projects are 'migrated' they are assigned a new ProjectId (applicationName) and in doing so, the companies users can authenticate against the migrated project by virtue of the common membership provider but the roles from the original project do not carry over by virtue of distinct role providers.

All standard membership management strategies, e.g. AspNet configuration tool, Login controls, createuser wizards, Membership functions (especially Membership.DeleteUser() - thank you Thomas) will behave as expected with no modifications.

Profiles may be implemented in either direction, using the applicationId of the Membership provider will allow profile data to follow a user to any of the associated projects. Using the distinct ProjectId (applicationName) of the Role provider will allow seperate profiles for each user in each project.

Some more detail and the tests are here.

The salient configuration sections are listed below and the modified sproc follows.

Web.config

<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <add name="testDb" providerName="System.Data.SqlClient" connectionString="Data Source=(local);Initial Catalog=__SingleAuthMultiRole;Integrated Security=True"/>
  </connectionStrings>
  <system.web>
    <compilation debug="true"/>

    <!-- this key is common all your apps - generate a new one @ http://www.developmentnow.com/articles/machinekey_generator.aspx -->
    <machineKey validationKey="841FEF8E55CD7963CE9EAFED329724667D62F4412F635815DFDDBE7D2D8D15819AE0FDF70CEF8F72792DBD7BF661F163B01134092CBCB80D7D71EAA42DFBF0A9" decryptionKey="FC9B0626224B0CF0DA68C558577F3E37723BB09AACE795498C4069A490690669" validation="SHA1" decryption="AES"/>

    <authorization>
      <deny users="?"/>
    </authorization>

    <authentication mode="Forms" />

    <membership defaultProvider="SqlProvider" userIsOnlineTimeWindow="15">
      <providers>
        <clear/>
        <add name="SqlProvider"
             type="System.Web.Security.SqlMembershipProvider"
             connectionStringName="testDb"
             applicationName="Common"  /> <!-- membership applicationName is common to all projects  -->
      </providers>
    </membership>

    <roleManager enabled="true" defaultProvider="SqlRoleManager" cacheRolesInCookie="true">
      <providers>
        <add name="SqlRoleManager"
             type="System.Web.Security.SqlRoleProvider"
             connectionStringName="testDb"
             applicationName="WebApplication1"/> <!-- roleManager applicationName is unique to each projects  -->
      </providers>
    </roleManager>

  </system.web>
</configuration>

Usage: After provisioning your Aspnet_db with aspnet_regsql.exe, run this script to modify the aspnet_Users_DeleteUser sproc.

/*************************************************************/
/*************************************************************/
--- Modified DeleteUser SP

IF (EXISTS (SELECT name
              FROM sysobjects
             WHERE (name = N'aspnet_Users_DeleteUser')
               AND (type = 'P')))
DROP PROCEDURE [dbo].aspnet_Users_DeleteUser
GO
CREATE PROCEDURE [dbo].[aspnet_Users_DeleteUser]
    @ApplicationName  nvarchar(256),
    @UserName         nvarchar(256),
    @TablesToDeleteFrom int,
    @NumTablesDeletedFrom int OUTPUT    

AS
BEGIN
    -- holds all user id for username
    DECLARE @UserIds TABLE(UserId UNIQUEIDENTIFIER)
    SELECT  @NumTablesDeletedFrom = 0

    DECLARE @TranStarted   bit
    SET @TranStarted = 0

    IF( @@TRANCOUNT = 0 )
    BEGIN
        BEGIN TRANSACTION
        SET @TranStarted = 1
    END
    ELSE
    SET @TranStarted = 0

    DECLARE @ErrorCode   int
    DECLARE @RowCount    int

    SET @ErrorCode = 0
    SET @RowCount  = 0

    -- get all userid for username
    INSERT INTO @UserIds
    SELECT  UserId
    FROM    dbo.aspnet_Users 
    WHERE   LoweredUserName = LOWER(@UserName)

DECLARE @tmp int
SELECT @tmp = COUNT(*) FROM @UserIds
    IF NOT EXISTS(SELECT * FROM @UserIds)
        GOTO Cleanup

    -- Delete from Membership table if (@TablesToDeleteFrom & 1) is set
    IF ((@TablesToDeleteFrom & 1) <> 0 AND
        (EXISTS (SELECT name FROM sysobjects WHERE (name = N'vw_aspnet_MembershipUsers') AND (type = 'V'))))
    BEGIN
        DELETE FROM dbo.aspnet_Membership WHERE UserId IN (SELECT UserId from @UserIds)

        SELECT @ErrorCode = @@ERROR,
               @RowCount = @@ROWCOUNT

        IF( @ErrorCode <> 0 )
            GOTO Cleanup

        IF (@RowCount <> 0)
            SELECT  @NumTablesDeletedFrom = @NumTablesDeletedFrom + 1
    END

    -- Delete from aspnet_UsersInRoles table if (@TablesToDeleteFrom & 2) is set
    IF ((@TablesToDeleteFrom & 2) <> 0  AND
        (EXISTS (SELECT name FROM sysobjects WHERE (name = N'vw_aspnet_UsersInRoles') AND (type = 'V'))) )
    BEGIN
        DELETE FROM dbo.aspnet_UsersInRoles WHERE UserId IN (SELECT UserId from @UserIds)

        SELECT @ErrorCode = @@ERROR,
                @RowCount = @@ROWCOUNT

        IF( @ErrorCode <> 0 )
            GOTO Cleanup

        IF (@RowCount <> 0)
            SELECT  @NumTablesDeletedFrom = @NumTablesDeletedFrom + 1
    END

    -- Delete from aspnet_Profile table if (@TablesToDeleteFrom & 4) is set
    IF ((@TablesToDeleteFrom & 4) <> 0  AND
        (EXISTS (SELECT name FROM sysobjects WHERE (name = N'vw_aspnet_Profiles') AND (type = 'V'))) )
    BEGIN
        DELETE FROM dbo.aspnet_Profile WHERE UserId IN (SELECT UserId from @UserIds)

        SELECT @ErrorCode = @@ERROR,
                @RowCount = @@ROWCOUNT

        IF( @ErrorCode <> 0 )
            GOTO Cleanup

        IF (@RowCount <> 0)
            SELECT  @NumTablesDeletedFrom = @NumTablesDeletedFrom + 1
    END

    -- Delete from aspnet_PersonalizationPerUser table if (@TablesToDeleteFrom & 8) is set
    IF ((@TablesToDeleteFrom & 8) <> 0  AND
        (EXISTS (SELECT name FROM sysobjects WHERE (name = N'vw_aspnet_WebPartState_User') AND (type = 'V'))) )
    BEGIN
        DELETE FROM dbo.aspnet_PersonalizationPerUser WHERE UserId IN (SELECT UserId from @UserIds)

        SELECT @ErrorCode = @@ERROR,
                @RowCount = @@ROWCOUNT

        IF( @ErrorCode <> 0 )
            GOTO Cleanup

        IF (@RowCount <> 0)
            SELECT  @NumTablesDeletedFrom = @NumTablesDeletedFrom + 1
    END

    -- Delete from aspnet_Users table if (@TablesToDeleteFrom & 1,2,4 & 8) are all set
    IF ((@TablesToDeleteFrom & 1) <> 0 AND
        (@TablesToDeleteFrom & 2) <> 0 AND
        (@TablesToDeleteFrom & 4) <> 0 AND
        (@TablesToDeleteFrom & 8) <> 0 AND
        (EXISTS (SELECT UserId FROM dbo.aspnet_Users WHERE UserId IN (SELECT UserId from @UserIds))))
    BEGIN
        DELETE FROM dbo.aspnet_Users WHERE UserId IN (SELECT UserId from @UserIds)

        SELECT @ErrorCode = @@ERROR,
                @RowCount = @@ROWCOUNT

        IF( @ErrorCode <> 0 )
            GOTO Cleanup

        IF (@RowCount <> 0)
            SELECT  @NumTablesDeletedFrom = @NumTablesDeletedFrom + 1
    END

    IF( @TranStarted = 1 )
    BEGIN
        SET @TranStarted = 0
        COMMIT TRANSACTION
    END

    RETURN 0

Cleanup:
    SET @NumTablesDeletedFrom = 0

    IF( @TranStarted = 1 )
    BEGIN
        SET @TranStarted = 0
        ROLLBACK TRANSACTION
    END

    RETURN @ErrorCode

END
GO
Sky Sanders
Thnx, but there are many projects not applications. I would like to define the roles per ProjectID
Zyphrax
This looks like it ought to work however it won't work in practice. E.g., FindUsersInRole, GetUsersInRole will only return users associated with the SqlRoleProvider's application. Thus, if your central users are associated with a different app, you will not get any returned. To make this work, I found that I had to write a custom RoleProvider that adhered to the ApplicationName on the SqlMembershipProvider.
Thomas
-1: This is a hack. Take the time to write it correctly, and you wont be burned later when you need to make some unforeseen change.
Gabriel McAdams
@Gabriel, would you care to elaborate? I am not sure how you could describe this as a hack, I am just configuring existing systems to deliver the requirements as state. @Thomas, the requirements stated, as I read them, require the behaviour that you are writing custom code to avoid..
Sky Sanders
@Sky: A hack - You're taking a system that was designed to do one thing, and using it to do something else. There is no reason to do what you are suggesting, because a system exists to do precisely what the OP needs (a custom role provider).
Gabriel McAdams
@Gabriel. Two points: I am using the system EXACTLY as it was designed to be used. Take a step back and show me where, except in your opinion, this usage (with no code, nonetheless) , is described as unsuitable. And please tell me what aspect of my suggestion is a hack. Second: a custom role provider does not 'exists', you must design, code and maintain a custom provider. I think you are misunderstanding the requirements of the OP and characterising my suggestion as a hack exposes a defect of perception. just sayin...
Sky Sanders
The structure you provided will not work in practice! When you query against each app's SqlRoleProvider, it will use the **role provider's** ApplicationName attribute **not** the SqlMembershipProvider's ApplicationName. You will not be able to query for users in roles for example using the schema you provided. So when you create a user "BOB" to the application "Common", you will not be able to find roles for "BOB" using a RoleProvider whose ApplicationName is set to WebApp2. I've personally run up against this very problem and had to write a custom RoleProvider.
Thomas
@Sky: The ApplicationName attribute is for designating `applications` and not something else as you are suggesting. I never said that your suggestion was unsuitable. I said it was a hack (going around the design in order to achieve an objective). ASP.Net allows developers to write custom providers because of situations just like this.
Gabriel McAdams
@Gabriel, what 'something else' am I suggesting? Do you not see 3 web.configs? that means 3 applications, thus 3 application names.
Sky Sanders
@ALL - I think there is a HUGE disconnect happening here due to some ambiguity in the OP's question. I read: He has some 'projects', whether they are applications or just aspects of a single application, in which certain users should have roles. These projects are destined to be redeployed and upon redeployment the role assignements should be specific to the extranet instance, not the source/staging whatever - all while maintaining a central authentication store. That is what I read and that is the problem that my suggestion solves with no coding, using the framework as intended.
Sky Sanders
@Sky. There is no solution using built-in classes that allows you to have multiple applications with independent roles with a single authentication store using forms authentication. It does not exist. You must write a custom RoleProvider. The SqlMembershipProvider and SqlRoleProvider will not work this way. The moment you use the SqlRoleProvider to get roles, you will use the SqlRoleProvider's ApplicationName and the users attached to the SqlMembershipProvider's ApplicationName will be ignored.
Thomas
@Sky: I now think I understand the disconnect. The OP is not talking about project the same way you are. `project` in this sense is not a portion of an application, but a designation within the application. Think of it as if it were called `location`
Gabriel McAdams
@Thomas - EXACTLY! read what you just wrote. This is exactly what makes this solution work OOB. And you are distinctly mistaken in your assertion that UsersInRole will not return the expected results. There is a link to some code that disproves this assertion. simply add `var users = Roles.GetUsersInRole("webApp3Role");` to default page_load of app3 to see what I am talking about.
Sky Sanders
@Gabriel, either way, it still works. I am using the term 'project' in an ambiguous manner as I am not able to determine the OP perception/implementation of 'project'. I feel safe doing so because in either of the scenarios , a location or an app, the solution works perfectly as it was designed to. ;-)
Sky Sanders
@Gabriel, Dude, Roles has no way of knowing the guid of a user, and has no knowledge of the membership provider and does not interact with the membership provider/tables/nothing in any way. It simply takes the USERNAME of the thread principal. I am telling you it works. I have provided a working solution to prove it. Can the three of us just take a look at the tangibles here?
Sky Sanders
@Sky: I agree. You did provide a working solution. In my opinion, though, it is not the best solution. I have stated my reasons for this.
Gabriel McAdams
@All, i could take the time to re-implement the solution given using a 'location' based scenario and dump users/roles on every default page but that would be gilding the lily. If this concept is still confusing there are other issues to be addressed. And let me just try to cover my ass and say that if I am mistunderstanding something here I am profoundly sorry but I am pretty confident I understand the requirements and the need for a custom provider is not indicated.
Sky Sanders
@Gabriel: I understand your objection is that I am subverting the provider stack. Is this correct?
Sky Sanders
@Sky-You are doubly wrong. **ANY** call against the RP will use the RP's AppName. Roles.GetUsersInRole will use the RP which again will use the RP's AppName not the MP's AppName! Here's the proof: in this system, the User's AppId will be different than the Role's AppId correct? Notice that the stored proc aspnet_UsersInRoles_GetRolesForUser only takes a single parameter for AppName? How can it possibly return users from one app and roles from another app with only a single parameter? It can't. It only uses one AppName and that is the app name of the RoleProvider.
Thomas
@Thomas - we are still not seeing the problem the same way. What I read is that OP wants roles to be specific to the application instance, but the authentication to be common to all. Can we agree on that?
Sky Sanders
@Thomas, AND I did not say you were wrong. I stated that your description of the behavior of providers is absolutely correct. But your perception of the problem domain is what is causing you to perceive this behavior as defective. I maintain that it is the desired behavior.
Sky Sanders
@Thomas, the USER has NO appId. The user has a USERNAME. The providers have AppId. That is what make the solution appropriate for the requirements. The provider stack was designed to be abstract in that each component needs not know the implementation details of the other. The common api is string UserName - which is common to the authentication provider. The user cannot access the site without authenticating meaning that the roles defined with the "other" appId on RP will be associated with the proper user.
Sky Sanders
@Thomas - the bottom line is that the USER is not married to any provider except the MP. It is the MP job to ensure that the current principal is authenticated. The only member of principal that is used in RP or PP is Name. And OP wants to know if current user is in a role in the current app AND wants to use a single authentication store.
Sky Sanders
@Gabriel: You downvoted a working solution, called it a hack, and asserted that the provider stack is not meant to be used this way. Do you have anything other than your opinion to support these assertions? I have written many complete provider stacks, aggregating an unbelievably diverse range of information silos, and understand the need to implement custom providers. I have also learned that if the provider stack can provide the functionality you need via configuration only, then that is ALWAYS your best bet.
Sky Sanders
@Sky: I agree that the idea is having a central auth store with different role lists per app. I've built something that does that very thing and the first thing I tried was to use the Sql providers in the way you describe. It doesn't work because when the apps make calls to get the list of users in roles or visa versa, they assume all the users have the same app as the role.
Thomas
@Sky: RE: "Bottom Line" The User and the Roles are both married to a specific AppId/AppName. When you query from the MP it will use the MP's AppName. When you query from the RP it will use the RP's AppName. The problem is anything that asks to cross boundaries won't work. So, "Roles.GetUsersInRole" translates to "Use the RP to get all users tied to the RP's AppName that belong to a role with name = X and that Role's AppName = the RP's AppName"
Thomas
@Thomas - again what you are saying is correct. What you are not understanding is that the OP requirements are to segregate roles, not aggregate roles. 'However when a new project is exported to the extranet, he might no longer be primary contact. And lose this role for the Account and new project. However still has this role for the first project.' - e.g. when the 'project', whatever that may mean, is migrated, a new appid is assigned and the appropriate roles are assigned. The MP does not change and needs no additional configuration.
Sky Sanders
@Sky I think we're talking apples and oranges. I completely agree that the spec on the OP vague. I suspect Project != Application as others have said. That said, if you want multiple apps with indep., segregated roles and a single auth store, the config you posted will not work (unless you never tie users to roles of course).
Thomas
@Thomas, now we are getting somewhere. We have agreed on the problem domain. Now, whether 'project' = application or not (that is a technical semantic that is moot in this scenario) I fully maintain that the solution I have posted *can* work and *does* work to provide a central authentication authority and separate role silos. The linked solution has a common user in app specific role for each application. Log in to one, you are logged into them all. Which ever app you are in, app specific roles are applied. Did you look at the demo or are you basing your opinion on a past experience?
Sky Sanders
@Thomas - for perspective, in the linked solution think of WebApp1 as the internal 'project' and WebApp2, WebApp3 as extranet instances of WebApp1. Btw UID:CommonUser, PWD:CommonUser!
Sky Sanders
@Sky. Worse than not working, it that it will look like it works. (from WebApp1):var user = Membership.CreateUser( "test", "12345", "[email protected]" );Roles.CreateRole( "TestRole" );//this line creates a second User record with a different AppId!Roles.AddUserToRole( "test", "TestRole" );user = Membership.GetUser( "test" );Membership.DeleteUser( "test" );var users = Roles.GetUsersInRole( "TestRole" );Debug.Assert( users.Length == 0 );
Thomas
@Sky I understand the structure. As I said, I built something to do just this and I had to write a custom RoleProvider to get around the fact that if the MP's AppName and RP's AppName are different it creates all kinds of problems.
Thomas
BTW, the line that creates a second user is Roles.AddUserToRole. Basically, the RP says "Hey I don't have a user so I'll a new one with my AppId and then drop that user into my role."
Thomas
@Thomas, I cannot explain the strengths and subtle oddities of the provider stack in comments. See updated answer. If you want to discuss this further, send me feedback from my blog so I can get your email address and we can take it offline. Peace.
Sky Sanders
@Sky We certainly can. I'll try to connect with you later this wk. I'm quite versed in the provider stack and especially the SqlProvider as I'm using in production. I've been down the road of dealing with its oddities. At the end of the day, MS designed the SqlProviders (MP and RP) so that you could have multiple applications each with their own user and role list. Once you look a the code it uses, it becomes clear that it was never really designed to handle multiple applications with their own roles but a single auth store.
Thomas
@all - i have called in the big guns on this one. Lets wait and see how the chips fall.
Sky Sanders
@Thomas, first you proclaim loudly, multiple times, that the (working) solution I presented will not work, then, using a faulty test using incorrect procedure as support, proclaim loudly that it does not work, and now when it is proven that it does work you fall back on a personal opinion that it was not designed that way. All in the face of empirical evidence the it works fine. It is becoming quite clear that your position is more valuable to you than the facts. There is nothing further I can do here except point you to http://bobsutton.typepad.com/my_weblog/2006/07/strong_opinions.html
Sky Sanders
@Thomas , and http://www.codinghorror.com/blog/2008/05/strong-opinions-weakly-held.html peace.
Sky Sanders
Actually, it does not work and provided test code to prove it!
Thomas
As I have said *many* times, any code called against the Role provider will use the ROLE PROVIDER's App name. Worse, the SqlRoleProvider, will create a user, with a different AppId. Run through the example I provide against your solution btw. You will before you call Membership.DeleteUser, you will have TWO users and one Role. I never professed to be an expert but I have dealt with this exact issue (and I stayed at a Holiday Inn last night).
Thomas
Worse than not working, it that it will look like it works. (from WebApp1): <pre><code>var user = Membership.CreateUser( "test", "12345", "[email protected]" ); Roles.CreateRole( "TestRole" ); //this line creates a second User record with a different AppId! Roles.AddUserToRole( "test", "TestRole" ); user = Membership.GetUser( "test" ); Membership.DeleteUser( "test" ); var users = Roles.GetUsersInRole( "TestRole" ); Debug.Assert( users.Length == 0 );</code></pre>
Thomas
Crap. Can't format in comments. Either way, if you run this code you get an error on the Assert because your role will still think that user is part of the "test" role.
Thomas
By the way, let me add that if you want to provide the ability for users to change their username, which granted in not part of the SqlMembershipProvider, but is commonly implemented and you use your structure, it will completely hose all of your results. If I change username "BOB" to "ROBERT", calls to GetUsersInRole will return a username that does not exist.
Thomas
@Thomas - You are right in saying that this solution does not work out of the box. I noticed myself getting hot focusing solely on some, in my opinion, inconsistencies in the logic of your objections and took a step back, wrote a shitload of tests, and found that indeed the MP.DeleteUser, *even* when I utilized the RP to remove the user from roles, still left a mess for the other apps that may have roles defined for the deleted user. Still clinging to my position, I made a simple mod to the aspnet_Users_DeleteUser sproc that addresses all of those concerns. And then it dawned on me......
Sky Sanders
@Thomas - what an asshat I have been. I want to apologize, if I can and restate my position. But before I do, I need to make something clear that does seem to be. The records in aspnet_users are not *users*, they are *usernames*. RP does not create users, it simply takes your word for it when you say a username is valid and creates a 'user' row, a username if you will, for it's own use if it doesn't find one. It needs one to hang stuff off of. That is all. If the mess left by user deletion is taken care of the scenario is golden. Still not Out Of The Box, but golden.
Sky Sanders
@Thomas: - All membership related is still spread between the aspnet_Membership common user row and aspnet_Users common user row. Roles and Profiles etc care nothing of these properties. So, in this scenario, deletion aside, those multiple aspnet_user rows are what makes this solution work. So - if you are interested, the sproc change is added to the answer with an apology. I still think that a minor sproc change is a much simpler, cleaner, easier to maintain (no maintenance) than writing, testing and maintaining a new provider stack. There, I said it. Peace.
Sky Sanders
@Thomas - find some more detail and the tests here: http://skysanders.net/subtext/archive/2010/03/05/multiple-asp.net-apps-with-common-sqlmembershipprovider-and-unique-sqlroleprovidersqlprofileprovider.aspx , I think you will find it interesting.
Sky Sanders
Hey sky. I posted a comment to the link on your blog. Nice to know I'm not crazy :D. Check the the use case of changing a username. That also leaves a mess. In the end I found I had to write a custom role provider.
Thomas
@thomas, sure - an out of band functionality like that may require a bit more work. A short sproc should do the trick nicely. No modification to the stock provider stack necessary. Next objection? lol.
Sky Sanders
Yes, you can change the stored procs to correct the issue. I felt that to be problematic. If I only changed the sp's, it wouldn't be clear to someone looking at the schema or config files that I altered them. In fact, it would look like I used the out-of-box functionality. I created a separate RoleProvider and separate procs to make it clear both in config and the db that I had deviated from the built-in functionality.
Thomas
@Thomas - I really don't know how to respond to that. So you are saying that you felt compelled to reimplement working code and sprocs to aleviate some sort of ambiguity crisis? Wouldn't a bold preface to docs or a read me suffice? Who is it that you are worried about being confused? If I balance this concern against the benefits of a minor change (8 lines modified) in a stock sproc and a 4 line out-of-band username change sproc I would tend to lean towards no compiled and deployed code with minor sproc changes and proper docs.
Sky Sanders
I had a couple of concerns in the system in question. The first, is that I wanted to ensure that when I handed the system off to other developers, that many years down the line it would be clear that there was some custom work done on the provider and it was not an out-of-the-box solution. Second, this system (with 2-5 web applications) was installed at dozens of clients. I was concerned that some knucklehead admin would run regsql to regen the provider stored procs and tables and lose my custom stored proc work.
Thomas