We're building an ASP.NET app, and have a requirement to use the corporate LDAP system (Siteminder) for authentication (upside: no login dialogs). Roles are created in the LDAP tool, and users are assigned to the roles by userland managers (read: the structure has to be easily understood). Currently, all apps that use the system use a dual-entry process whereby the roles identified in the app are hand-entered into the LDAP system and users are assigned, then app functions are assigned to their role mirrors in an app-based control panel. This works, but it bothers me that dual-entry is required.
What I would like to achieve is something where the app queries the LDAP system to get a list of roles that are assigned to the app (which is identified in the LDAP system) and populate the role:function control panel with them. This part seems really straightforward. However, I lose clarity when it comes to figuring out what to put in the Authorize attribute:
[Authorize(Roles = "Admin, Moderator")]
would become... what?
[Authorize(LoadedRoles(r => r.FindAll("some expression that describes the roles that have a particular permission")))]
I'm seriously into blue sky territory here. I read this question, and liked - from an architectural standpoint - the answer that suggested making the permissions the roles. But that might not be acceptable to the userland managers that needed to manage users. On the other hand, this question turns things into non-string resources, but I can't conceive of how to translate that into "roles that have this sort of function included".
Any suggestions?
Update:
Based on the advice of #venerik below, I've made some progress. For the time being, I'm encapsulating everything in the [AuthorizeFunctionAttribute], and will farm the individual pieces out where they belong later. To that end, I created three variables:
private IList<KeyValuePair<long, string>> Roles;
private IList<KeyValuePair<long, string>> Functions;
private IList<RoleFunction> RoleFunctions;
...then put static data in them:
Roles = new ICollection<KeyValuePair<long, string>>();
Roles.Add(KeyValuePair<long, string>(1, "Basic User"));
Roles.Add(KeyValuePair<long, string>(2, "Administrator"));
Functions = new ICollection<KeyValuePair<long, string>>();
Functions.Add(KeyValuePair<long,string>(1,"List Things"));
Functions.Add(KeyValuePair<long,string>(2,"Add Or Edit Things"));
Functions.Add(KeyValuePair<long,string>(3,"Delete Things"));
...and finally bound them together (in a complicated manner that lays the groundwork for the future):
RoleFunctions = new IList<RoleFunction>();
RoleFunctions.Add(
new RoleFunction
{
RoleId = Roles.Where( r => r.Value == "Basic User").FirstOrDefault().Key,
FunctionId = Functions.Where( f => f.Value == "List Things" ).FirstOrDefault().Key,
isAuthorized = true
},
new RoleFunction
{
RoleId = Roles.Where( r => r.Value == "Administrator").FirstOrDefault().Key,
FunctionId = Functions.Where( f => f.Value == "Add or Edit Things" ).FirstOrDefault().Key,
isAuthorized = true
},
// More binding...
);
I feel good about this so far. So I went researching AuthorizeCore to see what I needed to do there. However, per the comment at the bottom of the page, it's not very helpful. I more or less get that at the end, the method needs to return a bool value. And I get that I need to check that one of the User.Roles array fits the permission that's passed in through [AuthorizeFunction("List Things")].
Update (again):
I've got the following code, which seems like it will do what I need (one method needs fleshing out):
/// <summary>An authorization attribute that takes "function name" as a parameter
/// and checks to see if the logged-in user is authorized to use that function.
/// </summary>
public class AuthorizeFunctionAttribute : AuthorizeAttribute
{
private IList<KeyValuePair<long, string>> Roles;
private IList<KeyValuePair<long, string>> Functions;
private IList<RoleFunction> RoleFunctions;
public string Function { get; private set; }
public AuthorizeFunctionAttribute(string FunctionName)
{
Function = FunctionName;
Roles = SetApplicationRoles();
Functions = SetApplicationFunctions();
RoleFunctions = SetRoleFunctions();
}
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
bool userIsAuthorized = false;
foreach (string ur in GetUserRoles(httpContext.Current.Request.Headers["SM_USER"]))
{
long roleId = Roles.Where( sr => sr.Value == ur )
.First().Key;
long functionId = Functions.Where( sf => sf.Value == Function )
.First().Key;
// If any role is authorized for this function, set userIsAuthorized to true.
// DO NOT set userIsAuthorized to false within this loop.
if (RoleFunctions.Where(rf => rf.RoleId == roleId && rf.FunctionId == functionId)
.First().isAuthorized)
{
userIsAuthorized = true;
}
}
return userIsAuthorized;
}
Previously I didn't know enough about the underlying bits of creating a custom attribute to get out of my own way. However, this MSDN article told me what should have been obvious to me in the beginning: build it yourself. So, once I get the GetUserRoles() method put together, I should be underway.
I think you can solve this using a custom AuthorizeAttribute. In a project I worked close to they used that to access Active Directory (as described in this answer).
In your case it would look something like:
public class AuthorizeWithLDAPAttribute(string functionName) : AuthorizeAttribute
{
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
// check LDAP to verify that user has
// a role that's linked to `functionName`
}
}
Next you can use this attribute on your controllers and/or methods:
[AuthorizeWithLDAP("functionName1")]
public class BlogController : Controller
{
....
[AuthorizeWithLDAP("functionName2")]
public ViewResult Index()
{
return View();
}
}
The controller is now only accessible to users whose role are linked to functionName1 and the method is only accessible to users whose role are linked to functionName1 and functionName2
Related
I have a question about Claims, JWT, and ASP.Net Core. Again... (Greetings Chris). So...
I have my JWT with Claim:
"Authorization": "CanEditUnit,CanBrowseUnit,CanCreateUnit,CanDeleteUnit,CanSeeUnitDetails,CanBrowseRole,CanEditRole,CanCreateRole,CanDeleteRole,CanSeeRoleDetails,CanBrowseUser,CanSeeUserDetails,CanDeleteUser,CanEditUser,CanRegisterNewUser"
etc.
This Claim has all privileges, that user contains (for example: If the user has CanEditUnit in a database set to True, CanEditUnit is saved in Authorization Claim, but if something is set to False it simply doesn't appear in that Claim.
Then I want to check if user has that Privilages in Policies like that:
options.AddPolicy("CanEditUnit", policy => policy.RequireClaim("Authorization", "CanEditUnit"));
But it probably checks if Authorization Claim is equal to CanEditUnit.
Is there a way to check policies with Contains instead of Equal? If not, what should I do them?
I found this in docs, but I don't know how to use it.
As you've suggested in your question, it looks like RequireAssertion has the ability to handle this for you. Here's an example:
policy.RequireAssertion(ctx =>
{
var authorizationClaim = ctx.User.FindFirstValue("Authorization");
if (authorizationClaim == null)
return false;
return authorizationClaim.Split(",").Contains("CanEditUnit");
});
This simply looks for an Authorization claim and, if it exists, splits it by , and checks for the existence of a CanEditUnit value.
If you want something a little more reusable, you can create a custom AssertionRequirement class of your own. Here's an example of what that might look like:
public class CustomAssertionRequirement : AssertionRequirement
{
public CustomAssertionRequirement(string requiredValue)
: base(ctx => HandleRequirement(ctx, requiredValue)) { }
private static bool HandleRequirement(AuthorizationHandlerContext ctx, string requiredValue)
{
var authorizationClaim = ctx.User.FindFirstValue("Authorization");
if (authorizationClaim == null)
return false;
return authorizationClaim.Split(",").Contains(requiredValue);
}
}
In order to use this new class, you can add it as a requirement to the AuthorizationPolicyBuilder (instead of using RequireAssertion), like so:
policy.AddRequirements(new CustomAssertionRequirement("CanEditUnit"));
How does one get the results of a "Saved Search" of Type "Deleted Record" in NetSuite? Other search types are obvious(CustomerSearchAdvanced, ItemSearchAdvanced, etc...) but this one seems to have no reference online, just documentation around deleting records, not running saved searches on them.
Update 1
I should clarify a little bit more what I'm trying to do. In NetSuite you can run(and Save) Saved Search's on the record type "Deleted Record", I believe you are able to access at least 5 columns(excluding user defined ones) through this process from the web interface:
Date Deleted
Deleted By
Context
Record Type
Name
You are also able to setup search criteria as part of the "Saved Search". I would like to access a series of these "Saved Search's" already present in my system utilizing their already setup search criteria and retrieving data from all 5 of their displayed columns.
The Deleted Record record isn't supported in SuiteTalk as of version 2016_2 which means you can't run a Saved Search and pull down the results.
This is not uncommon when integrating with NetSuite. :(
What I've always done in these situations is create a RESTlet (NetSuite's wannabe RESTful API framework) SuiteScript that will run the search (or do whatever is possible with SuiteScript and not possible with SuiteTalk) and return the results.
From the documentation:
You can deploy server-side scripts that interact with NetSuite data
following RESTful principles. RESTlets extend the SuiteScript API to
allow custom integrations with NetSuite. Some benefits of using
RESTlets include the ability to:
Find opportunities to enhance usability and performance, by
implementing a RESTful integration that is more lightweight and
flexible than SOAP-based web services. Support stateless communication
between client and server. Control client and server implementation.
Use built-in authentication based on token or user credentials in the
HTTP header. Develop mobile clients on platforms such as iPhone and
Android. Integrate external Web-based applications such as Gmail or
Google Apps. Create backends for Suitelet-based user interfaces.
RESTlets offer ease of adoption for developers familiar with
SuiteScript and support more behaviors than NetSuite's SOAP-based web
services, which are limited to those defined as SuiteTalk operations.
RESTlets are also more secure than Suitelets, which are made available
to users without login. For a more detailed comparison, see RESTlets
vs. Other NetSuite Integration Options.
In your case this would be a near trivial script to create, it would gather the results and return JSON encoded (easiest) or whatever format you need.
You will likely spend more time getting the Token Based Authentication (TBA) working than you will writing the script.
[Update] Adding some code samples related to what I mentioned in the comments below:
Note that the SuiteTalk proxy object model is frustrating in that it
lacks inheritance that it could make such good use of. So you end with
code like your SafeTypeCastName(). Reflection is one of the best tools
in my toolbox when it comes to working with SuiteTalk proxies. For
example, all *RecordRef types have common fields/props so reflection
saves you type checking all over the place to work with the object you
suspect you have.
public static TType GetProperty<TType>(object record, string propertyID)
{
PropertyInfo pi = record.GetType().GetProperty(propertyID);
return (TType)pi.GetValue(record, null);
}
public static string GetInternalID(Record record)
{
return GetProperty<string>(record, "internalId");
}
public static string GetInternalID(BaseRef recordRef)
{
PropertyInfo pi = recordRef.GetType().GetProperty("internalId");
return (string)pi.GetValue(recordRef, null);
}
public static CustomFieldRef[] GetCustomFieldList(Record record)
{
return GetProperty<CustomFieldRef[]>(record, CustomFieldPropertyName);
}
Credit to #SteveK for both his revised and final answer. I think long term I'm going to have to implement what is suggested, short term I tried implementing his first solution("getDeleted") and I'd like to add some more detail on this in case anyone needs to use this method in the future:
//private NetSuiteService nsService = new DataCenterAwareNetSuiteService("login");
//private TokenPassport createTokenPassport() { ... }
private IEnumerable<DeletedRecord> DeletedRecordSearch()
{
List<DeletedRecord> results = new List<DeletedRecord>();
int totalPages = Int32.MaxValue;
int currentPage = 1;
while (currentPage <= totalPages)
{
//You may need to reauthenticate here
nsService.tokenPassport = createTokenPassport();
var queryResults = nsService.getDeleted(new GetDeletedFilter
{
//Add any filters here...
//Example
/*
deletedDate = new SearchDateField()
{
#operator = SearchDateFieldOperator.after,
operatorSpecified = true,
searchValue = DateTime.Now.AddDays(-49),
searchValueSpecified = true,
predefinedSearchValueSpecified = false,
searchValue2Specified = false
}
*/
}, currentPage);
currentPage++;
totalPages = queryResults.totalPages;
results.AddRange(queryResults.deletedRecordList);
}
return results;
}
private Tuple<string, string> SafeTypeCastName(
Dictionary<string, string> customList,
BaseRef input)
{
if (input.GetType() == typeof(RecordRef)) {
return new Tuple<string, string>(((RecordRef)input).name,
((RecordRef)input).type.ToString());
}
//Not sure why "Last Sales Activity Record" doesn't return a type...
else if (input.GetType() == typeof(CustomRecordRef)) {
return new Tuple<string, string>(((CustomRecordRef)input).name,
customList.ContainsKey(((CustomRecordRef)input).internalId) ?
customList[((CustomRecordRef)input).internalId] :
"Last Sales Activity Record"));
}
else {
return new Tuple<string, string>("", "");
}
}
public Dictionary<string, string> GetListCustomTypeName()
{
//You may need to reauthenticate here
nsService.tokenPassport = createTokenPassport();
return
nsService.search(new CustomListSearch())
.recordList.Select(a => (CustomList)a)
.ToDictionary(a => a.internalId, a => a.name);
}
//Main code starts here
var results = DeletedRecordSearch();
var customList = GetListCustomTypeName();
var demoResults = results.Select(a => new
{
DeletedDate = a.deletedDate,
Type = SafeTypeCastName(customList, a.record).Item2,
Name = SafeTypeCastName(customList, a.record).Item1
}).ToList();
I have to apply all the filters API side, and this only returns three columns:
Date Deleted
Record Type(Not formatted in the same way as the Web UI)
Name
Users of my site have experienced some strange behaviour yesterday (first time I've seen this issue), and unfortunately I don't have much in the way of error logs to try to figure out what's going on. The site had a higher-than-normal number of people online at once, albeit not a large number in the grand scheme of things (maybe 50 to 100 users all trying to perform similar functions). I can't recreate the issue in my development environment, haven't seen it before, and don't really know why it is happening.
The crux of the problem is that users can register or log on successfully, but a small number of them could see other users' data.
The site is ASP.NET MVC 3.
Users are logging on and I set an authentication cookie - here's the LogOn action:
[HttpPost]
public ActionResult LogOn(AccountLogOnViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (!Membership.ValidateUser(model.UserName, model.Password))
{
ModelState.AddModelError("login-message", "Incorrect username or password");
}
}
if (ModelState.IsValid)
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
Session.Remove("MenuItems");
return Redirect(returnUrl ?? Url.Action("Index", "Home"));
}
else
{
model.ReturnUrl = returnUrl;
return View(model);
}
}
AccountLogOnViewModel is a simple object with two string properties, UserName and Password.
From what I can gather, this is fine - if you log in as NickW then doing something like User.Identity.Name correctly gives you "NickW" (when users were seeing other users' data, they reported that that "Welcome, NickW" text on screen was showing them the correct value - this is written out using User.Identity.Name)
The site also uses a custom membership provider. It overrides the ValidateLogin method, and the GetUser method. ValidateLogin appears to be working just fine so I'm not concerned about it.
The overridden GetUser method is as follows:
public override MembershipUser GetUser(string username, bool userIsOnline)
{
User user = _userRepository.Users.FirstOrDefault(u => u.UserName == username);
MembershipUser membershipUser = null;
if (user == null)
return membershipUser;
membershipUser = new MembershipUser(this.Name,
user.UserName,
user.Id,
user.Email,
null,
user.Comments,
user.IsActivated,
user.IsLockedOut,
user.CreatedDate,
user.LastLoginDate,
user.LastLoginDate,
user.LastModifiedDate,
Convert.ToDateTime(user.LastLockedOutDate));
return membershipUser;
}
So I'm attempting to retrieve a User object from my database, and using that to create a new MembershipUser object. My database User table has additional columns on top of those required by the membership provider - e.g. name, address, phone number etc.
At various points in the rest of the website (for example if you go to the Profile page), I retrieve a user object from the database and use it to populate the screen. The line I use to retrieve the User object is:
User user = userRepository.Users.FirstOrDefault(u => u.UserName == Membership.GetUser().UserName);
Here is a cut down version of the userRepository (i.e. just removing unrelated code).
public class SqlUserRepository : IUserRepository
{
private Table<User> usersTable;
private string _connectionString;
public SqlUserRepository(string connectionString)
{
_connectionString = connectionString;
usersTable = (new DataContext(connectionString)).GetTable<User>();
}
public IQueryable<User> Users
{
get { return usersTable; }
}
public void CreateUser(AccountRegisterViewModel user)
{
User newUser = new User();
newUser.UserName = user.UserName;
newUser.Salutation = user.Salutation;
newUser.PhoneNumber = user.PhoneNumber;
newUser.SecondaryPhoneNumber = user.SecondaryPhoneNumber;
newUser.FirstName = user.FirstName;
newUser.LastName = user.LastName;
newUser.PasswordSalt = CreateSalt();
newUser.Password = CreatePasswordHash(user.Password, newUser.PasswordSalt);
newUser.Email = user.Email;
newUser.CreatedDate = DateTime.UtcNow;
newUser.Comments = "Created from web registration";
newUser.LastModifiedDate = DateTime.UtcNow;
newUser.LastLoginDate = DateTime.UtcNow;
newUser.IsActivated = true;
newUser.IsLockedOut = false;
newUser.MayContact = user.MayContact;
usersTable.InsertOnSubmit(newUser);
usersTable.Context.SubmitChanges();
}
}
So it appears to me as if the auth cookie I'm setting is fine, but either:
When I first go in to the membership provider's GetUser() method, it retrieves the wrong record from the database and therefore sets up a MembershipUser object with the wrong username; subsequently when I look in the database for "this" user I'm actually looking for the wrong username.
Or: Intermittently when I do userRepository.FirstOrDefault(x => x.UserName == Membership.GetUser().Name) it retrieves the wrong record.
Or: something else is going wrong that I haven't thought of.
As I say, this seems to be a problem when the site was under load, so I'm wondering if it's some sort of caching issue somewhere? But I really don't know.
One thought I had was to change the way I retrieve the user in case the problem lies with the membership provider, and use this instead:
userRepository.FirstOrDefault(x => x.UserName == User.Identity.Name)
// or HttpContext.Current.User.Identity.Name if not within a controller
But really I'm not even sure what's going on so have no idea whether this will resolve the issue. Could it be a caching problem somewhere? It appears (but I can't be 100% certain) that when user A could see user B's details, it was always the case that user B was also active in the system (or had been within the previous 20 minutes).
I know it's a long shot, but does anyone have any idea how this could happen? Obviously it's a major concern and needs to be fixed urgently, but without knowing why it's happening I can't fix it!
Thanks in advance for any help,
Nick
Some things to consider:
Instead of using FirstOrDefault, use SingleOrDefault. FirstOrDefault assumes there will be more than 1 record of data matching your query. Since you are querying by username, there should only be 1 matching row, correct? In that case, use SingleOrDefault instead. When there are multiple rows that match the query, SingleOrDefault will throw an exception.
To get the username, instead of invoking Membership.GetUser().UserName, use User.Identity.Name. The User property on an MVC controller references an IPrincipal that should match the user's forms authentication cookie value. Since you have a custom membership provider, this should help eliminate its methods as a source of the problem.
There could be a caching issue if you have caching set up for the MVC project. Do you use the OutputCacheAttribute ([OutputCache]) on any controllers or action methods? Do you have it set up as a global filter in the global.asax file? Or do you think there may be some kind of SQL-based caching going on?
Looking at your overridden GetUser method, I see it should take 2 parameters: string username and bool isOnline. However, when you invoke it with Membership.GetUser().UserName, you are passing no parameters. Do you have another overridden overload of this method that also takes no parameters? What does it look like? Does it use System.Threading.CurrentPrincipal.Identity.Name to sniff out the current username when none is passed?
I would appreciate some pointers regarding data access/control in a MVC based multi tenant site:
Is there a better/more secure/elegant way to make sure that in a multi tenant site the user can handle only its own data.
There are number of tenants using same app: firstTenant.myapp.com, secondTenant.myapp.com...
//
// GET: /Customer/
// show this tenant's customer info only
public ViewResult Index()
{
//get TenantID from on server cache
int TenantID = Convert.ToInt16( new AppSettings()["TenantID"]);
return View(context.Customers.ToList().Where(c => c.TenantID == TenantID));
}
If a user logs in for the first time and there is no server side cache for this tenant/user- AppSettings checks in db and stores TenantID in the cache.
Each table in database contains the field TenantID and is used to limit access to data only to appropriate Tenant.
So, to come to the point, instead of checking in each action in each controller if data belong to current tenant, can I do something more 'productive'?
Example:
When firstTenant admin tries editing some info for user 4, url has:
http://firstTenant.myapp.com/User/Edit/4
Let's say that user with ID 2 belongs to secondTenant. Admin from firstTenant puts
http://firstTenant.myapp.com/User/Edit/2 in url, and tries getting info which is not owned by his company.
In order to prevent this in the controller I check if the info being edited is actually owned by current tenant.
//
// GET: /User/Edit/
public ActionResult Edit(int id)
{
//set tennant ID
int TenanatID = Convert.ToInt32(new AppSettings()["TenantID"]);
//check if asked info is actually owned by this tennant
User user = context.Userss.Where(u => u.TenantID == TenantID).SingleOrDefault(u => u.UserID == id);
//in case this tenant doesn't have this user ID, ie.e returned User == null
//something is wrong, so handle bad request
//
return View(user);
}
Basically this sort of setneeds to be placed in every controller where there is an access to any data. Is there (and how) a better way to handle this? (Filters, attributes...)
I choose to use action filters to do this. It may not be the most elegant solution, but it is the cleanest of the solutions we've tried so far.
I keep the tenant (in our case, it's a team) in the URL like this: https://myapp.com/{team}/tasks/details/1234
I use custom bindings to map {team} into an actual Team object so my action methods look like this:
[AjaxAuthorize, TeamMember, TeamTask("id")]
public ActionResult Details(Team team, Task id)
The TeamMember attribute verifies that the currently logged in user actually belongs to the team. It also verifies that the team actually exists:
public class TeamMemberAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var httpContext = filterContext.RequestContext.HttpContext;
Team team = filterContext.ActionParameters["team"] as Team;
long userId = long.Parse(httpContext.User.Identity.Name);
if (team == null || team.Members.Where(m => m.Id == userId).Count() == 0)
{
httpContext.Response.StatusCode = 403;
ViewResult insufficientPermssions = new ViewResult();
insufficientPermssions.ViewName = "InsufficientPermissions";
filterContext.Result = insufficientPermssions;
}
}
}
Similarly, the TeamTask attribute ensures that the task in question actually belongs to the team.
Since my app is using subdomains (sub1.app.com, sub2.app.com.....) I basically choose to:
a) use something like the following code to cache info about tenants and
b) to call an action filter on each controller as suggested by Ragesh & Doc:
(Following code is from the blog on : http://www.developer.com/design/article.php/10925_3801931_2/Introduction-to-Multi-Tenant-Architecture.htm )
// <summary>
// This class is used to manage the Cached AppSettings
// from the Database
// </summary>
public class AppSettings
{
// <summary>
// This indexer is used to retrieve AppSettings from Memory
// </summary>
public string this[string Name]
{
get
{
//See if we have an AppSettings Cache Item
if (HttpContext.Current.Cache["AppSettings"] == null)
{
int? TenantID = 0;
//Look up the URL and get the Tenant Info
using (ApplContext dc =
new ApplContext())
{
Site result =
dc.Sites
.Where(a => a.Host ==
HttpContext.Current.Request.Url.
Host.ToLower())
.FirstOrDefault();
if (result != null)
{
TenantID = result.SiteID;
}
}
AppSettings.LoadAppSettings(TenantID);
}
Hashtable ht =
(Hashtable)HttpContext.Current.Cache["AppSettings"];
if (ht.ContainsKey(Name))
{
return ht[Name].ToString();
}
else
{
return string.Empty;
}
}
}
// <summary>
// This Method is used to load the app settings from the
// database into memory
// </summary>
public static void LoadAppSettings(int? TenantID)
{
Hashtable ht = new Hashtable();
//Now Load the AppSettings
using (ShoelaceContext dc =
new ShoelaceContext())
{
//settings are turned off
// no specific settings per user needed currently
//var results = dc.AppSettings.Where(a =>
// a.in_Tenant_Id == TenantID);
//foreach (var appSetting in results)
//{
// ht.Add(appSetting.vc_Name, appSetting.vc_Value);
//}
ht.Add("TenantID", TenantID);
}
//Add it into Cache (Have the Cache Expire after 1 Hour)
HttpContext.Current.Cache.Add("AppSettings",
ht, null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(1, 0, 0),
System.Web.Caching.CacheItemPriority.NotRemovable, null);
}
}
If you want to execute common code like this on every Action in the Controller, you can do this:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// do your magic here, you can check the session and/or call the database
}
We have developed a multi tenant application using ASP.NET MVC as well and including the tenant ID in every query is a completely acceptable and really necessary thing to do. I'm not sure where you are hosting your application but if you can use SQL Azure they have a new product called Federations that allows you to easily manage multi tenant data. One nice feature is that when you open the connection you can specify the tenant ID and all queries executed thereafter will only effect that tenants data. It is essentially just including their tenant ID in every request for you so you don't have to do it manually. (Note that federating data is not a new concept, Microsoft just released their own implementation of it recently)
I'm working on some Role-based security for our app and I essentially want to do customized verison MVC's AuthorizeAttribute - but only at the business logic layer, where we don't link to MVC.
I've looked at PrincipalPermissionAttribute but it seems it doesn't have a way to customize it as it's sealed. I just want to create a custom version where I can check for membership in any of a list of roles without using multiple attributes, and also define where to look for the role membership.
Is there anything like this in .Net that I'm missing? Or does anybody have some insight on how to do this without reimplementing ASP.Net's AuthorizeAttribute/RoleProvider/etc?
EDIT
I currently have a imperative version running, but I'd rather have a declarative-attribute version, as it's easier to see it above the method/class.
Right now I have the following in an abstract base class for my business layer:
protected void EnsureEditorLevelAccess()
{
var allowedRoles = new[]
{
Roles.Administrator,
Roles.Editor,
};
var roles = GetAccountRoles(GetCurrentUsername());
if (roles.Any(role => allowedRoles.Contains(role)))
{
return;
}
throw new SecurityException("You do not have sufficient privileges for this operation.");
}
I like being able to use Roles.Administrator etc because the role names are hideous (Active Directory group based...), so I was thinking of wrapping those details up in the constructor of a custom attribute that I can just plop on top of classes/methods.
GetAccountRoles is just a facade over an injectable role-provider property, which I can set to use either AD or a testing version that uses the database.
I could subclass Attribute, but not sure how it would kick off the security check.
You can create a new attribute that uses the existing PrincipalPermission if that would be sufficient for your needs. If your existing imperative implementation uses PrincipalPermission, then this should be the case. However, if your imperative version does something else, you may need to consider implementing both a custom permission and a corresponding attribute. If you're not sure whether this is necessary, perhaps you could share some details regarding your current imperative approach...
After question update...
It's actually possible to use "any" logic with PrincipalPermission, although it requires unioning of multiple instances, which is not particularly practical to work with in an attribute. This makes it much more reasonable to create a custom attribute, which might look something like the following:
[Serializable]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class AnyRolePermissionAttribute : CodeAccessSecurityAttribute
{
public AnyRolePermissionAttribute(SecurityAction action)
: base(action)
{
}
public string Roles { get; set; }
public override IPermission CreatePermission()
{
IList<string> roles = (this.Roles ?? string.Empty).Split(',', ';')
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.Distinct()
.ToList();
IPermission result;
if (roles.Count == 0)
{
result = new PrincipalPermission(null, null, true);
}
else
{
result = new PrincipalPermission(null, roles[0]);
for (int i = 1; i < roles.Count; i++)
{
result = result.Union(new PrincipalPermission(null, roles[i]));
}
}
return result;
}
}
Unfortunately, you can't use arrays in security attributes, so the role list has to be represented as a string. e.g.:
[AnyRolePermission(SecurityAction.Demand, Roles = "Foo, Bar")]
You could use it with your constants via design-time concatenation. e.g.:
[AnyRolePermission(SecurityAction.Demand, Roles = Roles.Administrator + ", " + Roles.Editor)]
As for your custom role provider, the appropriate place to use it is in the thread principal, not the permission or attribute. For example, if you're currently using a GenericPrincipal, you could replace it with a custom principal that uses your custom role provider to retrieve the target identity's roles.
You could derive your own CodeAccessSecurityAttribute and implement your logic around the Thread.CurrentPrincipal (http://msdn.microsoft.com/en-us/library/system.security.permissions.codeaccesssecurityattribute.aspx).
essentially, you'd want to verify allowedRoles.Any(r => Thread.CurrentPrincipal.IsInRole(r))