How to get all LDAP users with LdapTemplate - spring-security

I'm using spring-security and wish to retrieve all users and all groups to be stored in a reference table so I can quickly look up users without having to consult the LDAP directory. I have created a LdapAuthoritiesPopulator implementation mirroring DefaultLdapAuthoritiesPopulator with the following additional method:
public final Collection<GrantedAuthority> getAllAuthorities() {
if (groupSearchBase == null) {
return new HashSet<>();
}
Set<GrantedAuthority> authorities = new HashSet<>();
Set<String> roles = ldapTemplate.searchForSingleAttributeValues(
groupSearchBase,
allAuthorityFilter,
new String[0],
groupRoleAttribute);
for (String role : roles) {
if (convertToUpperCase) {
role = role.toUpperCase();
}
authorities.add(new SimpleGrantedAuthority(rolePrefix + role));
}
return authorities;
}
This now allows me to retrieve all groups, allAuthorityFilter is a property defaulting to (&(objectClass=group)(objectCategory=group)).
I am now trying to achieve the same thing with the users by creating a custom LdapUserSearch based of of FilterBasedLdapUserSearch with the following additional method:
public List<String> findAllUsers() {
SpringSecurityLdapTemplate template
= new SpringSecurityLdapTemplate(contextSource);
template.setSearchControls(searchControls);
List<String> r = template.search(searchBase,
allUsersFilter,
new AttributesMapper() {
#Override
public Object mapFromAttributes(Attributes atrbts)
throws NamingException {
return (String) atrbts.get(userNameAttribute).get();
}
});
return r;
}
There are two problems I have with this:
If the user-list is large I get javax.naming.SizeLimitExceededException which I do not know how to resolve.
I want this method to return something like DirContextOperations similar to how searchForUser(String) works so that my LdapUserDetailsMapper implementation can be reused to return all user properties.
I'm finding the documentation for LdapTemplate a little hairy and having trouble finding the answers I'm after, any assistance would be greatly appreciated.
UPDATE: I have solved point (2) above by
public List<UserDetails> getAllUserDetails(boolean includeAuthorities) {
List<UserDetails> r = new ArrayList<>();
for (DirContextOperations ctx : userSearch.findAllUserOperations()) {
try {
Attribute att = ctx.getAttributes().get(usernameAttribute);
String username = (String) att.get();
r.add(userMapper.mapUserFromContext(
ctx,
username,
includeAuthorities
? authPop.getGrantedAuthorities(ctx, username)
: Collections.<GrantedAuthority>emptySet()));
} catch (NamingException ex) {
LOG.warn("Username attribute " + usernameAttribute + " not found!");
}
}
return r;
}
In my UserSearch implementation I have:
public List<DirContextOperations> findAllUserOperations() {
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
template.setSearchControls(searchControls);
return template.search(searchBase,
allUsersFilter, new ContextMapper() {
#Override
public Object mapFromContext(Object o) {
return (DirContextOperations) o;
}
});
}
However I have not solved point #1. If I need to batch this somehow then that is fine as long as there is a way to tell LdapTemplate where to resume on subsequent calls.

Related

ASP.Net 6 Core CSOM ExecuteQuery when getting a non-available user in SPO

In asp.net core 6 and CSOM library, I'm trying add a permission to a SPO file as following code.
public async Task<IActionResult> AddPermission(Guid guid, String[] emailList)
{
using (var authenticationManager = new AuthenticationManager())
using (var context = authenticationManager.GetContext(_site, _user, _securePwd))
{
File file= context.Web.GetFileById(guid);
SetFilePermission(context, file, emailList);
file.ListItemAllFields.SystemUpdate();
context.Load(file.ListItemAllFields);
await context.ExecuteQueryAsync();
return NoContent();
}
}
private static void SetFilePermission(ClientContext context, File file, string[] emailList)
{
if (emailList != null)
{
file.ListItemAllFields.BreakRoleInheritance(true, false);
var role = new RoleDefinitionBindingCollection(context);
role.Add(context.Web.RoleDefinitions.GetByType(permissionLevel));
foreach (string email in emailList)
{
User user = context.Web.SiteUsers.GetByEmail(email);
file.ListItemAllFields.RoleAssignments.Add(user, role);
}
}
}
This work successfully if only the user is available in SPO, or exception occurs. To avoid non-available user exception, I tried to move Load() and ExecuteQuery() to SetFilePermission method.
public async Task<IActionResult> AddPermission(Guid guid, String[] emailList)
{
using (var authenticationManager = new AuthenticationManager())
using (var context = authenticationManager.GetContext(_site, _user, _securePwd))
{
File file= context.Web.GetFileById(guid);
SetFilePermission(context, file, emailList);
// file.ListItemAllFields.SystemUpdate();
// context.Load(file.ListItemAllFields);
// await context.ExecuteQueryAsync();
return NoContent();
}
}
private static void SetFilePermission(ClientContext context, File file, string[] emailList)
{
if (emailList != null)
{
file.ListItemAllFields.BreakRoleInheritance(true, false);
var role = new RoleDefinitionBindingCollection(context);
role.Add(context.Web.RoleDefinitions.GetByType(permissionLevel));
foreach (string email in emailList)
{
User user = context.Web.SiteUsers.GetByEmail(email);
file.ListItemAllFields.RoleAssignments.Add(user, role);
// Move load and executequery method to here.
file.ListItemAllFields.SystemUpdate();
context.Load(file.ListItemAllFields);
context.ExecuteQueryAsync();
}
}
}
Suddenly, exception disappear even though the user is not available in SPO!
But other available emails in emailList also fail to add permission, just result in return NoContent eventually. Does anyone know the myth behind the process and explain it to me?

How to populate UserInfo object in DNN Authentication providers?

I am trying to create an authentication provider in DotNetNuke 7.4 which supports LinkedId. I have used the source package for the Facebook provider from the DnnPlatform GIT as the base and have modified it for LinkedIn's oAuth. I am able to connect via LinkedIn and get the auth token but the code fails on
OAuthClient.GetCurrentUser<LinkedInUserData>();
due to LinkedInUserData being null. the specific logged error is
DotNetNuke.Services.Exceptions.Exceptions - ~/Default.aspx?tabid=55&error=An unexpected error has occurred
System.ArgumentNullException: Value cannot be null.
Parameter name: value
at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
at DotNetNuke.Services.Authentication.OAuth.OAuthClientBase.GetCurrentUser[TUserData]()
at DotNetNuke.Authentication.LinkedIn.Login.GetCurrentUser() in c:\Websites\dnndev74_2\DesktopModules\AuthenticationServices\LinkedIn\Login.ascx.cs:line 103
Below are the 3 classes that are in play, there is a lot of inheritance going on so I'm having trouble understanding the mechanism for how LinkedInUserData gets populated in the first place. On a note. when I take the facebook codebase from GIT and install it as a provider on my local, and try to register with facebook account, I get the same error. However, if I install the provider through the CMS it runs fine or use the dll that comes with the dnn 7.4 install, facebook works. So I am lead to believe there is something fundamentally flawed with the GIT code..
LinkedInClient.cs
namespace DotNetNuke.Authentication.LinkedIn.Components
{
public class LinkedInClient : OAuthClientBase
{
#region Constructors
public LinkedInClient(int portalId, AuthMode mode)
: base(portalId, mode, "LinkedIn")
{
base.AuthorizationEndpoint = new Uri("https://www.linkedin.com/uas/oauth2/authorization");
base.RequestTokenEndpoint = new Uri("https://api.linkedin.com/uas/oauth/requestToken?scope=r_emailaddress");
base.TokenMethod = HttpMethod.POST;
base.TokenEndpoint = new Uri("https://www.linkedin.com/uas/oauth2/accessToken");
base.MeGraphEndpoint = new Uri("https://api.linkedin.com/v1/people/~:(id,first-name,last-name,email-address,formatted-name,picture-url)?format=json");
base.AuthTokenName = "LinkedInUserToken";
base.OAuthVersion = "2.0";
base.LoadTokenCookie(string.Empty);
}
#endregion
protected override TimeSpan GetExpiry(string responseText)
{
var jsonSerializer = new JavaScriptSerializer();
var tokenDictionary = jsonSerializer.DeserializeObject(responseText) as Dictionary<string, object>;
return new TimeSpan(0, 0, Convert.ToInt32(tokenDictionary["expires_in"]));
}
protected override string GetToken(string responseText)
{
var jsonSerializer = new JavaScriptSerializer();
var tokenDictionary = jsonSerializer.DeserializeObject(responseText) as Dictionary<string, object>;
return Convert.ToString(tokenDictionary["access_token"]);
}
}
}
LinkedInUserData.cs
namespace DotNetNuke.Authentication.LinkedIn.Components
{
[DataContract]
[Serializable]
public class LinkedInUserData : UserData
{
#region Overrides
public override string FirstName
{
get { return LinkedInFirstName; }
set { }
}
public override string LastName
{
get { return LinkedInLastName; }
set { }
}
public override string Email
{
get { return emailAddress; }
set { }
}
public override string ProfileImage
{
get { return LinkedInPictureUrl; }
set { }
}
#endregion
[DataMember(Name = "first-name")]
public string LinkedInFirstName { get; set; }
[DataMember(Name = "last-name")]
public string LinkedInLastName { get; set; }
[DataMember(Name = "picture-url")]
public string LinkedInPictureUrl { get; set; }
[DataMember(Name = "email-address")]
public string emailAddress { set; get; }
}
Login.cs
namespace DotNetNuke.Authentication.LinkedIn
{
public partial class Login : OAuthLoginBase
{
protected override string AuthSystemApplicationName
{
get { return "LinkedIn"; }
}
public override bool SupportsRegistration
{
get { return true; }
}
protected override UserData GetCurrentUser()
{
return OAuthClient.GetCurrentUser<LinkedInUserData>();
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
loginButton.Click += loginButton_Click;
registerButton.Click += loginButton_Click;
OAuthClient = new LinkedInClient(PortalId, Mode);
loginItem.Visible = (Mode == AuthMode.Login);
registerItem.Visible = (Mode == AuthMode.Register);
}
protected override void AddCustomProperties(NameValueCollection properties)
{
base.AddCustomProperties(properties);
properties.Add("LinkedIn", OAuthClient.GetCurrentUser<LinkedInUserData>().Link.ToString());
}
private void loginButton_Click(object sender, EventArgs e)
{
AuthorisationResult result = OAuthClient.Authorize();
if (result == AuthorisationResult.Denied)
{
UI.Skins.Skin.AddModuleMessage(this, Localization.GetString("PrivateConfirmationMessage", Localization.SharedResourceFile), ModuleMessage.ModuleMessageType.YellowWarning);
}
}
}
}
Mark,
I wrote a DNN provider for Linkedin a couple years ago. Comparing my code to yours, the first thing mine does is redirect the user in order to get an access token. The base url for obtaining the access token and permission from the user is: www.linkedin.com/uas/oauth2. I must pass my linked API key, a redirect Url and a few other pieces of data which comes from the provider settings.
My redirect Url is the same as the default portal login page which loads my provider. Once the user allows LinkedIn to allow my application access and verifies he is logged in, the redirect back to my provider will look for the LinkedInAuthToken cookie in the Request.Cookies. Once I verify the token is valid, I can make an additional API call to the /v1/people/ API to get user data to complete any kind of auto-registration or auto profile updates to DNN.
It seems like your provider is immediately attempting the user data lookup API call before obtaining the oauth access cookie.
The code for my LinkedIn provider is not open source, but I suppose I could get permission from my work to make it public. Message me if you are interested in it.

base.AuthorizeCore(httpContext) is always false

I referred to hundreds of posts and no luck yet. The base.AuthorizeCore(httpContext) returns false always.
I am running the MVC application from VS2012 using IIS express. Many people were able to solve this issue with forms based authentication. But i tried that too.
Please help..
Below is the AuthorizeADAttribute is am using.
public class AuthorizeADAttribute : AuthorizeAttribute
{
public string Groups { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (base.AuthorizeCore(httpContext))
{
// var authorized = (httpContext.User.Identity.IsAuthenticated);
/* Return true immediately if the authorization is not
locked down to any particular AD group */
if (String.IsNullOrEmpty(Groups))
return true;
// Get the AD groups
var groups = Groups.Split(',').ToList<string>();
// Verify that the user is in the given AD group (if any)
var context = new PrincipalContext(ContextType.Domain, "MYDOMAIN");
var userPrincipal = UserPrincipal.FindByIdentity(context,
IdentityType.SamAccountName,
httpContext.User.Identity.Name);
foreach (var group in groups)
{
try
{
if (userPrincipal.IsMemberOf(context, IdentityType.Name, group))
return true;
}
catch (NoMatchingPrincipalException exc)
{
var msg = String.Format("While authenticating a user, the operation failed due to the group {0} could not be found in Active Directory.", group);
System.ApplicationException e = new System.ApplicationException(msg, exc);
// ErrorSignal.FromCurrentContext().Raise(e);
return false;
}
catch (Exception exc)
{
var msg = "While authenticating a user, the operation failed.";
System.ApplicationException e = new System.ApplicationException(msg, exc);
//ErrorSignal.FromCurrentContext().Raise(e);
return false;
}
}
}
return false;
}
}
I am passing the group name like this. This works perfect when i run the application from VS2012 using IIS Express. The web.config file is set to
In the IIS settings Forms Based Authentication isenabled. But the URL redirection goes to login.aspx. I dont have any login page in my application
But when I publish the website to IIS. Error page shows up.
[AuthorizeAD(Groups = "DevUsers")]
public ActionResult Index()
{
return View();
}
base.AuthorizeCore(httpContext) works base on roles and group. you may not have the group of 'DevUsers'.
add public string CustomGroups { get; set; } to class and call it like this:
[AuthorizeAD(CustomGroups = "DevUsers")]
public ActionResult Index()
{
return View();
}
in this way which you send the group null then base.AuthorizeCore(httpContext) just does the authenticate part and you do the groups and role in your custom function.
Simply forget about httpcontext getting receive in the method... Stuff can interfere with things...
like this:
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
string Domain = WebConfigurationManager.AppSettings["Domain"];
string AdGroups = WebConfigurationManager.AppSettings["AdGroups"];
/* Return true immediately if the authorization is not
locked down to any particular AD group */
if (String.IsNullOrEmpty(AdGroups))
return true;
// Get the AD groups
//var groups = Groups.Split(',').ToList();
WindowsIdentity CurrentIdentity = WindowsIdentity.GetCurrent();
UserPrincipal userPrincipal = UserPrincipal.Current;
var groups = AdGroups.Split(',').ToList();
List<GroupPrincipal> result = new List<GroupPrincipal>();
PrincipalSearchResult<Principal> groups2 = userPrincipal.GetAuthorizationGroups();
// iterate over all groups
foreach (Principal p in groups2)
{
// make sure to add only group principals
if (p is GroupPrincipal)
{
foreach (var group in groups)
try
{
if (p.ToString().Equals(group.ToString()))
{
return true;
}
}
catch (NoMatchingPrincipalException ex)
{
}
//result.Add((GroupPrincipal)p);
}
}
return false;
}

SpringSecurityService: Log other user out?

Is there any way, with springSecurityService, to log someone else out?
(For example, if a regular user leaves for the day and forgets to logout, and the manager wants to log their account out.)
I have done in my application where , A admin User can logged-out forcefully any user from the list of all users currently logged-in into the system.
I get all users those are currently logged-in into the system and send to the jsp where list of all logged-in users are shown to the Admin user.
#PreAuthorize("hasRole('Currently_Logged-In_Users')")
#RequestMapping(value = "/getLoggedInUsers", method = RequestMethod.POST)
#ResponseBody
public Map<String, List<?>> getLoggedInUsers(Map<String, Object> map ,Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userName = auth.getName();
List<Object> principals = sessionRegistry.getAllPrincipals();
List<UserInfo> usersInfoList = new ArrayList<UserInfo>();
for (Object principal: principals) {
if (principal instanceof UserInfo) {
if(!((UserInfo) principal).getUsername().equals(userName)){
for(SessionInformation sess :sessionRegistry.getAllSessions(principal, false)){
if(!sess.isExpired()){
usersInfoList.add((UserInfo) sess.getPrincipal());
}
}
}
}
}
Map<String, List<?>> loggedInUserMap = new HashMap<String, List<?>>();
loggedInUserMap.put("loggenInUsers",
usersInfoList);
return loggedInUserMap;
}
Now Admin user can select any user or multiple user by clicking on check box against the users. and call forced Logged-out action.
#PreAuthorize("hasRole('Currently_Logged-In_Users')")
#RequestMapping(value = "/logoutSelectedUsers", method = RequestMethod.POST)
#ResponseBody
public Map<String, String> forcedLoggedOut(#RequestParam("userList[]")ArrayList<String> userList ,Model model ,HttpServletRequest request ) {
Map<String,String> map= new HashMap<String,String>();
try{
String successMessage =null;
List<String> userCodeList = new ArrayList<String>();
for(String userCode :userList ){
userCodeList.add(userCode);
}
List<Object> principals = sessionRegistry.getAllPrincipals();
for (Object principal: principals) {
if (principal instanceof UserInfo) {
if(userCodeList.contains(((UserInfo) principal).getUsername())){
for(SessionInformation sess :sessionRegistry.getAllSessions(principal, false)){
sess.expireNow();
successMessage = "msg.loggedOutSuccessfully";
}
}
}
}
map.put("successmsg", successMessage);
}
catch(Exception e){
map.put("failmsg", "msg.notLoggedOut");
logger.error(e.toString(),e);
}
return map;
}
The springSecurityService itself does not have this capability.
However, nothing is stopping you from creating your own ServletFilter to track session ids and security principals and expose a controller and pages to invalidate the associated session with a login.
Here's how I do it.
Edit: The example below uses the webxml plugin. You can also edit web.xml directly. See this answer for setting the timeout.
// src/groovy/com/example/ExpiringSessionEventListener.groovy:
package com.example
import grails.util.Holders
import javax.servlet.http.HttpSessionListener
import javax.servlet.http.HttpSessionEvent
import org.springframework.security.core.context.SecurityContext
public class ExpiringSessionEventListener implements HttpSessionListener {
#Override
public void sessionCreated(HttpSessionEvent event) {
// Do some logging
}
#Override
public void sessionDestroyed(HttpSessionEvent event) {
SecurityContext securityContext = event.session.getAttribute("SPRING_SECURITY_CONTEXT")
if (securityContext) {
UserService userService = Holders.applicationContext.getBean("userService")
String userName = securityContext.authentication.principal.username
userService.userLoggedOut(userName, event.session.id, Boolean.TRUE)
}
}
}
// grails-app/services/com/example/UserService.groovy:
package com.example
import grails.plugin.springsecurity.annotation.Secured
class UserService {
#Secured(["ROLE_USER"])
def userLoggedOut(String userName, String sessionId, Boolean expired) {
User user = User.findByUsername(userName)
if (expired) {
// Do user cleanup stuff after expired session
} else {
// Do user cleanup stuff after clicking the logout button
}
}
}
Edit:
// grails-app/conf/WebXmlConfig.groovy:
webxml {
sessionConfig.sessionTimeout = 10 // minutes
listener.add = true
listener.classNames = [
"com.example.ExpiringSessionEventListener"
]
}

ASP.NET MVC Web API : Posting a list of objects

I'm trying to post a list of objects from my winforms application to my asp.net mvc 4 website. I've tested posting one object, and it works, but does not work for the list. It returns a 500 (Internal Server Error). Here is my code:
ASP.NET MVC Web API
public class PostTraceController : ApiController
{
public HttpResponseMessage Post(List<WebTrace> list)
{
try
{
// Some code
return Request.CreateResponse(HttpStatusCode.Created);
}
catch (Exception ex)
{
HttpContext.Current.Trace.Write("exception", ex.Message);
return Request.CreateErrorResponse(HttpStatusCode.ServiceUnavailable, ex);
}
}
public HttpResponseMessage Post(WebTrace item)
{
try
{
// Some code
return Request.CreateResponse(HttpStatusCode.Created);
}
catch (Exception ex)
{
HttpContext.Current.Trace.Write("exception", ex.Message);
return Request.CreateErrorResponse(HttpStatusCode.ServiceUnavailable, ex);
}
}
}
Win forms application
public class BaseSender
{
public BaseSender()
{
Client = new HttpClient
{
BaseAddress = new Uri(#"http://localhost/mywebsite/")
};
Client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
public string UserCode { get; set; }
protected readonly HttpClient Client;
public HttpResponseMessage PostAsJsonAsync(string requestUri, object value)
{
var response = Client.PostAsJsonAsync(requestUri, value).Result;
response.EnsureSuccessStatusCode();
return response;
}
}
public class WebTraceSender : BaseSender
{
private const string requestUri = "api/posttrace";
public bool Post(List<ArchiveCptTrace> list)
{
try
{
var listWebTrace = new List<WebTrace>();
foreach (var item in list)
{
listWebTrace.Add(new WebTrace
{
DateStart = item.DatePreparation,
DateEnd = item.DateCloture,
UserStart = item.UserPreparation.UserName,
UserEnd = item.UserCloture.UserName,
AmountStart = item.MontantPreparation,
AmountEnd = item.MontantCloture,
TheoricAmountEnd = item.MontantTheorique,
Difference = item.Ecart,
UserCode = UserCode
});
}
var responce = PostAsJsonAsync(requestUri, listWebTrace);
return responce.IsSuccessStatusCode;
}
catch (Exception e)
{
// TODO : Trace the exception
return false;
}
}
}
EDIT :
I've found out the scenario of the error, which is having two methods in my api controller, even thought they have different signature. If I comment one method, the post work fine (item or a list). Any ideas ?
The methods may have different signatures, but Web API can't tell the difference between them without inspecting the body, which it won't do for performance reasons.
You could do two things - either create a new class which just holds a list of WebTrace objects, and put that in a different API controller, or you could map a custom route to one of your existing methods. You could do that with ActionName attribute, however, I would probably take the first approach.

Resources