I've build a simple intranet application using ASP.NET MVC4 that allows users inside network to view some resources. Application is build mostly in ExtJS and WebAPI controllers.
My authorization is based on Active Directory.
Debugging my code I noticed that I'm doing AD calls multiple times.
Here is my AD helper class:
public class AD
{
public static string CurrentUser = "";
public static string GetCurrentUserLogin()
{
return HttpContext.Current.User.Identity.Name.Split('\\')[1];
}
public static Employee GetCurrentUser()
{
var login = CurrentUser == "" ? GetCurrentUserLogin() : CurrentUser ;
return GetUser(login);
}
public static Employee GetCurrentUser(string login)
{
return GetUser(login);
}
public static Employee GetUser(Guid guid)
{
using (var entry = new DirectoryEntry("LDAP://wawa.firma.pl/OU=IT,DC=wawa,DC=firma,DC=pl", #"AD_client", "xyzabc"))
{
using (var searcher = new DirectorySearcher(entry))
{
byte[] bytes = guid.ToByteArray();
var sb = new StringBuilder();
foreach (byte b in bytes)
{
sb.Append(string.Format(#"\{0}", b.ToString("X2")));
}
searcher.Filter = String.Format("(&(objectClass=User)(objectCategory=Person)(objectguid={0}))", sb);
searcher.PropertiesToLoad.Add("description");
searcher.PropertiesToLoad.Add("objectguid");
searcher.PropertiesToLoad.Add("SN");
searcher.PropertiesToLoad.Add("givenName");
searcher.PropertiesToLoad.Add("name");
searcher.PropertiesToLoad.Add("manager");
searcher.PropertiesToLoad.Add("mail");
searcher.PropertiesToLoad.Add("sAMAccountName");
searcher.PropertiesToLoad.Add("telephoneNumber");
searcher.PropertiesToLoad.Add("directReports");
searcher.SizeLimit = 1;
searcher.SearchScope = SearchScope.Subtree;
try
{
SearchResult result = searcher.FindOne();
if (result == null) return null;
var emp = ParseSearchResult(result);
emp.fullAccess = true;
return emp;
}
catch (Exception)
{
return null;
}
}
}
}
}
This works fine, but every time I must get current user id and I do call like this:
var user_id = AD.GetCurrentUser().Id;
My problem is that every time I request some property for user that is stored in AD i must do new AD call.
Ideally I would like to do simple call once (for current user) and store it, then every time I need something I will get it from local object.
I read some articles about session in WebAPI controllers, for example: http://stick2basic.wordpress.com/2013/04/18/how-to-access-session-in-web-api-controller-in-mvc4/, but I found also greate example of using Cache: https://github.com/filipw/AspNetWebApi-OutputCache
But Session is per User and Cache per Application.
What would be best way of storing ActiveDirectory results per User? How should I modify my AD class to hold those variables.
The easiest way would be to store result in session as so:
public static Employee GetUser(Guid guid)
{
var e = HttpContext.Current.Session[guid.ToString()] as Employee;
if (e == null)
{
//get user from AD
//store in session
}
return e;
}
This way I'll be able to query AD only once, but I won't be able to requery every n-minutes (cache has timeout).
How can I make this easier/better? Are there other ways to do this?
You need some memory cache. Have a look at: MemoryCache Class
Store the DirectoryEntry object in the cache with the path as key.
Related
I am building a ASP.NET Core website, where the user signs in, and is able to save things like the theme of the page, which then triggers some js code:
var data = {
userName: userName,
key: "theme",
value: localStorage.getItem("theme")
}
var pendingRequest;
if (pendingRequest) {
pendingRequest.abort()
pendingRequest = null;
}
pendingRequest = $.post('/Account/setExtension', data, function (data) {
pendingRequest = null;
$('#settingsModal').modal('hide');
});
which calls the controller:
[HttpPost]
public void setExtension(string userName, string key, string value)
{
Dictionary<string, object> addData = new Dictionary<string, object>
{
{key, value }
};
GraphHelper.updateExtension(userName, "com.user.roamingSettings", addData).ConfigureAwait(false);
}
which calls the checks if the extension exists, and then decides to create it, or update the existing extension:
public static async Task<bool> extensionExistsAsync(string userName, string extensionName)
{
try
{
var graphClient = GetAuthenticatedClient();
// if extension doesnt exist
bool extensionFound = false;
var extensions = await graphClient.Users[userName].Extensions.Request().GetAsync();
foreach (var extension in extensions)
{
if (extension.Id == extensionName)
{
extensionFound = true;
}
}
return extensionFound;
}
catch (Exception e)
{
Debug.WriteLine(e.Message.ToString());
throw;
}
}
The problem is, the code just stops running on this line:
var extensions = await graphClient.Users[userName].Extensions.Request().GetAsync();
It doesn't throw or anything. Stepping through it line by line, it returns all the way to the assignment, and the output window is empty when it stops. Why is this? How can I get an extension by name, or get all extensions to see which ones exist, either by graph api, or calls?
When ever you use the below call
var extensions = await graphClient.Users[userName].Extensions.Request().GetAsync();
you will be getting UserExtensionsCollectionPage object which gives the list of extensions of a user.
This page doesn't have Id property, the extension objects that are present in this UserExtensionsCollectionPage object have it. So use the below code to print the id and type of the Extensions.
var extensions = await graphClient.Users["a1ee289f-4cab-4fc3-885f-d4cbefb48895"].Extensions.Request().GetAsync();
foreach(var data in extensions.CurrentPage)
{
Console.WriteLine("Id: " + data.Id + "Type: " + data.ODataType );
}
This will give you the data as below.
I'm using Domain PrincipalContext to find users groups. And I got it. But when I'm trying to work with group collection I get System.OutOfMemoryException. All Principal objects are disposable. And there is using section in my code. I had tried to use Dispose() method and GC.Collect() But it does not help.
Here is code:
using (var ctx = new PrincipalContext(ContextType.Domain, _domain, _user, _password))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(ctx,IdentityType.SamAccountName, sAMAccountName))
{
PrincipalSearchResult<Principal> userGroups = user.GetGroups();
using (userGroups)
{
foreach (Principal p in userGroups)
{
using (p)
{
result.Add(p.Guid == null ? Guid.Empty : (Guid)p.Guid);
}
}
}
}
}
foreach loop return exception. Even foreach is empty loop.
I find that the System.DirectoryServices.AccountManagement namespace (UserPrincipal, etc.) does waste a lot of memory. For example, every time you create a UserPrincipal or GroupPrincipal, it asks AD for every attribute that has a value - even if you only ever use one of them.
If the user is a member of many, many groups, that could be the cause, although I am still surprised. Maybe your computer just doesn't have the available memory to load all that.
You can do the same thing and use less memory (and probably less time) by using the System.DirectoryServices namespace directly (which is what the AccountManagement namespace uses in the background anyway).
Here is an example that will look at the memberOf attribute of the user to find the groups and pull the Guid. This does have some limitations, which I describe an article I wrote: Finding all of a user’s groups (this example is modified from one in that article). However, in most cases (especially if you only have one domain in your environment and no trusted domains) it'll be fine.
public static IEnumerable<Guid> GetUserMemberOf(DirectoryEntry de) {
var groups = new List<Guid>();
//retrieve only the memberOf attribute from the user
de.RefreshCache(new[] {"memberOf"});
while (true) {
var memberOf = de.Properties["memberOf"];
foreach (string group in memberOf) {
using (var groupDe = new DirectoryEntry($"LDAP://{group.Replace("/", "\\/")}") {
groupDe.RefreshCache(new[] {"objectGUID"});
groups.Add(new Guid((byte[]) groupDe.Properties["objectGUID"].Value));
}
}
//AD only gives us 1000 or 1500 at a time (depending on the server version)
//so if we've hit that, go see if there are more
if (memberOf.Count != 1500 && memberOf.Count != 1000) break;
try {
de.RefreshCache(new[] {$"memberOf;range={groups.Count}-*"});
} catch (COMException e) {
if (e.ErrorCode == unchecked((int) 0x80072020)) break; //no more results
throw;
}
}
return groups;
}
You need to feed it a DirectoryEntry object of the user. If you know the distinguishedName beforehand, you can use that (e.g. new DirectoryEntry($"LDAP://{distinguishedName}")). But if not, you can search for it:
var ds = new DirectorySearcher(
new DirectoryEntry($"LDAP://{_domain}"),
$"(&(objectClass=user)(sAMAccountName={sAMAccountName}))");
ds.PropertiesToLoad.Add("distinguishedName"); //add at least one attribute so it doesn't return everything
var result = ds.FindOne();
var userDe = result.GetDirectoryEntry();
I notice you are also passing the username and password to PrincipalContext. If that's needed here, the constructor for DirectoryEntry does accept a username and password, so you can update this code to include that every time you create a new DirectoryEntry.
I want to hide a certain page from menu, if the current session IP is in Israel. Here's what I've tried, but in fact the menu-item doesn't appear anywhere.
I tested the GeoIP provider and it seems to be working, what am I doing wrong?
Here's how I the menu is created and how I try to skip the items I don't want in the menu:
public class PagesDynamicNodeProvider
: DynamicNodeProviderBase
{
private static readonly Guid KeyGuid = Guid.NewGuid();
private const string IsraelOnlyItemsPageKey = "publications-in-hebrew";
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode siteMapNode)
{
using (var context = new Context())
{
var pages = context.Pages
.Include(p => p.Language)
.Where(p => p.IsPublished)
.OrderBy(p => p.SortOrder)
.ThenByDescending(p => p.PublishDate)
.ToArray();
foreach (var page in pages)
{
//*********************************************************
//Is it the right way to 'hide' the page in current session
if (page.MenuKey == IsraelOnlyItemsPageKey && !Constants.IsIsraeliIp)
continue;
var node = new DynamicNode(
key: page.MenuKey,
parentKey: page.MenuParentKey,
title: page.MenuTitle,
description: page.Title,
controller: "Home",
action: "Page");
node.RouteValues.Add("id", page.PageId);
node.RouteValues.Add("pagetitle", page.MenuKey);
yield return node;
}
}
}
}
Here's how I determine and cache whether the IP is from Israel:
private const string IsIsraeliIpCacheKey = "5522EDE1-0E22-4FDE-A664-7A5A594D3992";
private static bool? _IsIsraeliIp;
/// <summary>
/// Gets a value indicating wheather the current request IP is from Israel
/// </summary>
public static bool IsIsraeliIp
{
get
{
if (!_IsIsraeliIp.HasValue)
{
var value = HttpContext.Current.Session[IsIsraeliIpCacheKey];
if (value != null)
_IsIsraeliIp = (bool)value;
else
HttpContext.Current.Session[IsIsraeliIpCacheKey] = _IsIsraeliIp = GetIsIsraelIpFromServer() == true;
}
return _IsIsraeliIp.Value;
}
}
private static readonly Func<string, string> FormatIpWithGeoIpServerAddress = (ip) => #"http://www.telize.com/geoip/" + ip;
private static bool? GetIsIsraelIpFromServer()
{
var ip = HttpContext.Current.Request.UserHostAddress;
var address = FormatIpWithGeoIpServerAddress(ip);
string jsonResult = null;
using (var client = new WebClient())
{
try
{
jsonResult = client.DownloadString(address);
}
catch
{
return null;
}
}
if (jsonResult != null)
{
var obj = JObject.Parse(jsonResult);
var countryCode = obj["country_code"];
if (countryCode != null)
return string.Equals(countryCode.Value<string>(), "IL", StringComparison.OrdinalIgnoreCase);
}
return null;
}
Is the DynamicNodeProvider cached? If yes, maybe this is what's causing the issue? How can I make it cache per session, so each sessions gets its specific menu?
Is it right to cache the IP per session?
Any other hints on tracking down the issue?
The reason why your link doesn't appear anywhere is because the SiteMap is cached and shared between all if its users. Whatever the state of the user request that builds the cache is what all of your users will see.
However without caching the performance of looking up the node hierarchy would be really expensive for each request. In general, the approach of using a session per SiteMap is supported (with external DI), but not recommended for performance and scalability reasons.
The recommended approach is to always load all of your anticipated nodes for every user into the SiteMap's cache (or to fake it by forcing a match). Then use one of the following approaches to show and/or hide the nodes as appropriate.
Security Trimming
Built-in or custom visibility providers
Customized HTML helper templates (in the /Views/Shared/DisplayTemplates/ folder)
A custom HTML helper
It is best to think of the SiteMap as a hierarchical database. You do little more than set up the data structure, and that data structure applies to every user of the application. Then you make per-request queries against that shared data (the SiteMap object) that can be filtered as desired.
Of course, if none of the above options cover your use case, please answer my open question as to why anyone would want to cache per user, as it pretty much defeats the purpose of making a site map.
Here is how you might set up a visibility provider to do your filtering in this case.
public class IsrealVisibilityProvider : SiteMapNodeVisibilityProviderBase
{
public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
{
return Constants.IsIsraeliIp;
}
}
Then remove the conditional logic from your DynamicNodeProvider and add the visibility provider to each node where it applies.
public class PagesDynamicNodeProvider
: DynamicNodeProviderBase
{
private const string IsraelOnlyItemsPageKey = "publications-in-hebrew";
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode siteMapNode)
{
using (var context = new Context())
{
var pages = context.Pages
.Include(p => p.Language)
.Where(p => p.IsPublished)
.OrderBy(p => p.SortOrder)
.ThenByDescending(p => p.PublishDate)
.ToArray();
foreach (var page in pages)
{
var node = new DynamicNode(
key: page.MenuKey,
parentKey: page.MenuParentKey,
title: page.MenuTitle,
description: page.Title,
controller: "Home",
action: "Page");
// Add the visibility provider to each node that has the condition you want to check
if (page.MenuKey == IsraelOnlyItemsPageKey)
{
node.VisibilityProvider = typeof(IsraelVisibilityProvider).AssemblyQualifiedName;
}
node.RouteValues.Add("id", page.PageId);
node.RouteValues.Add("pagetitle", page.MenuKey);
yield return node;
}
}
}
}
For a more complex visibility scheme, you might want to make a parent visibility provider that calls child visibility providers based on your own custom logic and then set the parent visibility provider as the default in web.config.
<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MyNamespace.ParentVisibilityProvider, MyAssembly"/>
Or, using external DI, you would set the default value in the constructor of SiteMapNodeVisibilityProviderStrategy.
// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
.Ctor<string>("defaultProviderName").Is("MyNamespace.ParentVisibilityProvider, MyAssembly");
I am not sure which version of MVCSiteMapProvider you are using, but the latest version is very extensible as it allows using internal/external DI(depenedency injection).
In your case it is easy to configure cache per session, by using sliding cache expiration set to session time out.
Link
// Setup cache
SmartInstance<CacheDetails> cacheDetails;
this.For<System.Runtime.Caching.ObjectCache>()
.Use(s => System.Runtime.Caching.MemoryCache.Default);
this.For<ICacheProvider<ISiteMap>>().Use<RuntimeCacheProvider<ISiteMap>>();
var cacheDependency =
this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
.Ctor<string>("fileName").Is(absoluteFileName);
cacheDetails =
this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(cacheDependency);
If you are using Older Version, you can try to implement GetCacheDescription method in IDynamicNodeProvider
public interface IDynamicNodeProvider
{
IEnumerable<DynamicNode> GetDynamicNodeCollection();
CacheDescription GetCacheDescription();
}
Here are the details of CacheDescription structure.
Link
I am using C# to get the Role from Azman Manager by login User.
This is my code,
internal string AzRoleCheck(string strUserName)
{
string strRoleName = string.Empty;
try
{
WindowsIdentity userIdentity = new WindowsIdentity(strUserName);
clientContext = azApplication.InitializeClientContextFromToken((ulong)userIdentity.Token, null);
foreach (IAzRole Azrole in azApplication.Roles)
{
strRoleName = Azrole.Name;
foreach (object member in (object[])Azrole.MembersName)
{
string strMemberName = member.ToString();
string[] str1 = Regex.Split(strMemberName, "#");
if (string.Equals(str1[0],strUserName,StringComparison.CurrentCultureIgnoreCase))
return strRoleName;
}
}
}
catch (Exception)
{
throw;
}
return strRoleName;
}
When the ad-user login to my application, call this above method with 'strUserName' parameter. Here, i am checking the all user roles from azman by using foreach & within this foreach getting membername based on role & then checking all this members is available under the role or not. If the member is available under the role means return the userrole as string.
It's working fine, but my question is , Is it a correct method to get the role based on user???
I have a question, i added a AD-User group to Azman, Now i am logging in through userName, how do i get the Azman role ?
Thanks in advance...
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)