diff --git a/ActiveDirectoryProvider/ActiveDirectoryProvider.cs b/ActiveDirectoryProvider/ActiveDirectoryProvider.cs index 0a9fa9a..f32eca5 100644 --- a/ActiveDirectoryProvider/ActiveDirectoryProvider.cs +++ b/ActiveDirectoryProvider/ActiveDirectoryProvider.cs @@ -4,417 +4,931 @@ using System.DirectoryServices; using System.Linq; using System.Web; using ScrewTurn.Wiki.PluginFramework; +using System.Text; +using System.DirectoryServices.ActiveDirectory; namespace ScrewTurn.Wiki.Plugins.ActiveDirectory { - /// /// Implements a Users Storage Provider for Active Directory. /// - public class ActiveDirectoryProvider { - - private const string HELP_HELP = "Configuration is set with the following parameters:
domain - The domain name (e.g. DOMAIN or DOMAIN.COM)
server - A domain controller to authenticate against (e.g. MYDC1)
admingroup - The AD group that will have admin access to the Wiki
usergroup The AD group that will have user access to the Wiki
username - An AD account username that has at minimum read access to the domain.
password - The password for the above username.

You will also need to adjust your web.config to set Authentication mode to Windows, and turn on NTLM on the server as well."; - private IHostV30 m_Host; - private string m_Server; - private string m_Domain; - private string m_SearchRoot; - private string m_AdminGroup; - private string m_UserGroup; - private string m_Username; - private string m_Password; - private IUsersStorageProviderV30 m_StorageProvider; - - /// - /// Not Implemented - /// - public bool TestAccount(UserInfo user, string password) { - return false; - } - - /// - /// Not Implemented - /// - public UserInfo[] GetUsers() { - return new UserInfo[] { }; - } - - /// - /// Not Implemented - /// - public UserInfo AddUser(string username, string displayName, string password, string email, bool active, DateTime dateTime) { - throw new NotImplementedException(); - } - - /// - /// Not Implemented - /// - public UserInfo ModifyUser(UserInfo user, string newDisplayName, string newPassword, string newEmail, bool newActive) { - throw new NotImplementedException(); - } - - /// - /// Not Implemented - /// - public bool RemoveUser(UserInfo user) { - throw new NotImplementedException(); - } - - /// - /// Not Implemented - /// - public UserGroup[] GetUserGroups() { - return new UserGroup[] { }; - } - - /// - /// Not Implemented - /// - public UserGroup AddUserGroup(string name, string description) { - throw new NotImplementedException(); - } - - /// - /// Not Implemented - /// - public UserGroup ModifyUserGroup(UserGroup group, string description) { - throw new NotImplementedException(); - } - - /// - /// Not Implemented - /// - public bool RemoveUserGroup(UserGroup group) { - throw new NotImplementedException(); - } - - /// - /// Not Implemented - /// - public UserInfo SetUserMembership(UserInfo user, string[] groups) { - throw new NotImplementedException(); - } - - /// - /// Tries to log the user in manually - /// - public UserInfo TryManualLogin(string username, string password) { - - var info = m_StorageProvider.TryManualLogin(username, password); - - if (info != null) - return info; - - try { - using (var rootEntry = new DirectoryEntry(m_SearchRoot, username, password, AuthenticationTypes.Delegation | AuthenticationTypes.ReadonlyServer | AuthenticationTypes.Secure)) { - var nativeObject = rootEntry.NativeObject; - return GetUser(username); - } - } catch { - return null; - } - } - - /// - /// Tries to log the user in automagically - /// - public UserInfo TryAutoLogin(HttpContext context) { - - try { - - if (!context.User.Identity.IsAuthenticated) - return null; - - var username = context.User.Identity.Name.Substring(context.User.Identity.Name.IndexOf("\\") + 1); - return GetUser(username); - - } catch { - return null; - } - } - - /// - /// Gets the user info object for the currently logged in user. - /// - /// - /// - public UserInfo GetUser(string username) { - var user = m_StorageProvider.GetUser(username); - if (user != null) - return user; - - using (var rootEntry = new DirectoryEntry(m_SearchRoot, m_Username, m_Password, AuthenticationTypes.Secure)) { - using (var searcher = new DirectorySearcher(rootEntry) { - Filter = string.Format("(&(objectClass=user)(objectCategory=person)(sAMAccountName={0}))", username) - }) - { - searcher.PropertiesToLoad.Add("objectClass"); - searcher.PropertiesToLoad.Add("member"); - searcher.PropertiesToLoad.Add("sAMAccountName"); - searcher.PropertiesToLoad.Add("sn"); - searcher.PropertiesToLoad.Add("givenName"); - searcher.PropertiesToLoad.Add("department"); - searcher.PropertiesToLoad.Add("title"); - searcher.PropertiesToLoad.Add("mail"); - searcher.PropertiesToLoad.Add("displayName"); - - DirectoryEntry entry; - try { - - entry = searcher.FindOne().GetDirectoryEntry(); - - } catch { - - return null; - - } - - var displayName = string.Empty; - var emailAddress = string.Empty; - var adminGroup = false; - var userGroup = false; - - var values = entry.Properties["displayName"]; - if (values != null && values.Count > 0) - displayName = values[0].ToString(); - - values = entry.Properties["mail"]; - if (values != null && values.Count > 0) - emailAddress = values[0].ToString(); - - var rootPath = entry.Path; - rootPath = rootPath.Substring(0, rootPath.IndexOf("/", 7) + 1); - - for (var i = 0; i < entry.Properties["memberOf"].Count; i++) { - - using (var currentEntry = new DirectoryEntry(rootPath + entry.Properties["memberOf"][i])) { - - if (IsAccountTypeGroup(currentEntry)) { - - values = currentEntry.Properties["sAMAccountName"]; - if (values != null && values.Count > 0) { - - if (values[0].ToString() == m_AdminGroup) - adminGroup = true; - - if (values[0].ToString() == m_UserGroup) - userGroup = true; - - } - - } - - } - - } - - - var userGroups = new List(); - - if (adminGroup) - userGroups.Add("Administrators"); - if (userGroup) - - userGroups.Add("Users"); - - user = m_StorageProvider.AddUser(username, displayName, "jdzs98duadj2918j9sdjasd", emailAddress, true, DateTime.Now); - user = m_StorageProvider.SetUserMembership(user, userGroups.ToArray()); - - return user; - } - } - } - - /// - /// Gets the type of the account. - /// - private static bool IsAccountTypeGroup(DirectoryEntry entry) { - - var objectClass = entry.Properties["objectClass"]; - - for (var i = 0; i < objectClass.Count; i++) - if ((string)objectClass[i] == "group") - return true; - - return false; - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public UserInfo GetUserByEmail(string email) { - - return m_StorageProvider.GetUserByEmail(email); - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public void NotifyCookieLogin(UserInfo user) { - - m_StorageProvider.NotifyCookieLogin(user); - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public void NotifyLogout(UserInfo user) { - - m_StorageProvider.NotifyLogout(user); - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public bool StoreUserData(UserInfo user, string key, string value) { - - return m_StorageProvider.StoreUserData(user, key, value); - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public string RetrieveUserData(UserInfo user, string key) { - - return m_StorageProvider.RetrieveUserData(user, key); - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public IDictionary RetrieveAllUserData(UserInfo user) { - - return m_StorageProvider.RetrieveAllUserData(user); - - } - - /// - /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 - /// - public IDictionary GetUsersWithData(string key) { - - return m_StorageProvider.GetUsersWithData(key); - - } - - /// - /// True, always, we can't write back to AD - /// - public bool UserAccountsReadOnly { - get { return true; } - } - - /// - /// No - /// - public bool UserGroupsReadOnly { - get { return true; } - } - - /// - /// True, always, we can't write back to AD - /// - public bool GroupMembershipReadOnly { - get { return true; } - } - - /// - /// True, always, we can't write back to AD - /// - public bool UsersDataReadOnly { - get { return false; } - } - - /// - /// Inits the settings - /// - public void Init(IHostV30 host, string config) { - m_Host = host; - var provider = (from a in host.GetUsersStorageProviders(true) - where a.Information.Name != this.Information.Name - select a).FirstOrDefault(); - - if (provider == null) - throw new InvalidConfigurationException("This provider require an additional active storage provider for storing of active directory user information."); - - m_StorageProvider = provider; - InitConfig(config); - } - - /// - /// Ignored - /// - public void Shutdown() { - m_StorageProvider.Shutdown(); - } - - /// - /// Plugin Information - /// TODO return and complete - /// - public ComponentInformation Information { - get { return new ComponentInformation("ActiveDirectoryProvider", "Threeplicate Srl", "3.0.1.417", "http://www.screwturn.eu", "http://www.screwturn.eu/Version/ActiveDirectoryProvider.txt"); } - } - - /// - /// Plugin Help Text - /// - public string ConfigHelpHtml { - get { return HELP_HELP; } - } - - /// - /// Configures the plugin based on the configuration settings - /// - /// - private void InitConfig(string config) - { - if (string.IsNullOrEmpty(config)) - throw new InvalidConfigurationException("Config settings must be set before plugin can be used."); - - try { - var configParts = config.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); - foreach (var configPart in configParts) { - var setting = configPart.Substring(0, configPart.IndexOf("=")); - var value = configPart.Substring(configPart.IndexOf("=") + 1); - switch (setting.ToLower().Trim()) - { - case "domain": - m_Domain = value; - break; - case "server": - m_Server = value; - break; - case "admingroup": - m_AdminGroup = value; - break; - case "usergroup": - m_UserGroup = value; - break; - case "username": - m_Username = value; - break; - case "password": - m_Password = value; - break; - } - } - - var ldap = string.Empty; - if (!string.IsNullOrEmpty(m_Server)) - ldap = string.Format("{0}/", m_Server); - - var ldapParts = m_Domain.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - - m_SearchRoot = string.Format("LDAP://{0}DC={1}", ldap, string.Join(",DC=", ldapParts)); - } catch (Exception ex) { - throw new InvalidConfigurationException("The configuration is invalid.", ex); - } - } + public class ActiveDirectoryProvider : IUsersStorageProviderV30 { + private const string HELP_HELP = + "Configuration Settings:
" + + "Map one or more domain groups to wiki groups (Users, Administrators, etc.):" + + "
    " + + "
  • GroupMap=somedomaingroup:somewikigroup[,anotherwikigroup[,...]]
  • " + + "
" + + "Give all users membership in common wiki groups (Users, etc.):" + + "
    " + + "
  • CommonGroups=somewikigroup[,anotherwikigroup[,...]]
  • " + + "
" + + "Give users with no wiki group membership default wiki groups (Users, etc.):" + + "
    " + + "
  • DefaultGroups=somewikigroup[,anotherwikigroup[,...]]
  • " + + "
" + + "Authenticate against a domain the web server is not joined to (optional, choose one):" + + "
    " + + "
  • Domain=some.domain
  • " + + "
  • Server=somedomaincontroller.some.domain
  • " + + "
" + + "Query active directory as a specific user on the domain (optional):" + + "
    " + + "
  • Username=someusername
  • " + + "
  • Password=somepassword
  • " + + "
" + + "Automatic login without a login form (optional):" + + "
    " + + "
  • Set Authentication mode to Windows in Web.config.
  • " + + "
  • Turn on Windows authentication on the web server.
  • " + + "
" + + "In case the user doesn't have an email in his ActiveDirectory profile, sets the email to a predefined value in the form name.surname@example.com (optional):" + + "
    " + + "
  • AutomaticMail=example.com
  • " + + "
" + + "Comments start with a semicolon \";\"."; + + private IHostV30 m_Host; + private IUsersStorageProviderV30 m_StorageProvider; + private Random m_Random; + private Config m_Config; + + private class Config { + public string ServerName; + public string DomainName; + public string Username; + public string Password; + public string AutomaticMail; + + private Dictionary GroupMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Gets the domain specified by the config or else the computer domain. + /// + /// The Domain object. + public Domain GetDomain() { + return GetDomain(Username, Password); + } + + /// + /// Gets the domain specified by the config or else the computer domain using the given credentials (if any). + /// + /// The username. + /// The password. + /// The Domain object. + public Domain GetDomain(string username, string password) { + DirectoryContext context = null; + + if(ServerName != null) { + context = new DirectoryContext(DirectoryContextType.DirectoryServer, ServerName, username, password); + } + else { + context = new DirectoryContext(DirectoryContextType.Domain, DomainName, username, password); + } + + return Domain.GetDomain(context); + } + + /// + /// Determine if the given user credentials are valid. + /// + /// The username. + /// The password. + /// true if credentials are valid otherwise false. + public bool ValidateCredentials(string username, string password) { + try { + using(Domain domain = GetDomain(username, password)) { + return true; + } + } + + catch { + return false; + } + } + + /// + /// Get the wiki groups for the given domain groups, if any. + /// + /// The domain groups. + /// The list of wiki groups. + public List GetWikiGroups(List domainGroups) { + // find all the wiki groups from the given domain groups + var wikiGroups = domainGroups.SelectMany(t => GetGroupMap(t)).ToList(); + + // add groups common to all users + wikiGroups.AddRange(CommonGroups); + + // remove duplicates + wikiGroups = wikiGroups.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList(); + + // return the groups, if any + if(wikiGroups.Count > 0) + return wikiGroups; + + // return the default groups, if any + return DefaultGroups.ToList(); + } + + /// + /// Get the wiki groups for the given domain group, if any. + /// + /// The domain group. + /// The array of string containing the wiki groups. + private string[] GetGroupMap(string domainGroup) { + string[] wikiGroups; + + if(GroupMap.TryGetValue(domainGroup, out wikiGroups)) + return wikiGroups; + + return new string[0]; + } + + /// + /// Adds the given group map. + /// + /// The domain group. + /// The wiki groups. + public void AddGroupMap(string domainGroup, string[] wikiGroups) { + if(wikiGroups == null || wikiGroups.Length == 0) + GroupMap.Remove(domainGroup); + else + GroupMap[domainGroup] = wikiGroups; + } + + /// + /// Returns true if the group map contains at least one entry. + /// + /// + /// true if this instance is group map set; otherwise, false. + /// + public bool IsGroupMapSet { get { return GroupMap.Count > 0; } } + + /// + /// Groups that are common to all users, regardless of their domain group membership. + /// + /// The common groups. + private static readonly string CommonKey = String.Empty; + public string[] CommonGroups { + get { return GetGroupMap(CommonKey); } + set { AddGroupMap(CommonKey, value); } + } + + /// + /// Groups that are used when a user has no other group membership. + /// + /// The default groups. + private static readonly string DefaultKey = ".DEFAULT!"; + public string[] DefaultGroups { + get { return GetGroupMap(DefaultKey); } + set { AddGroupMap(DefaultKey, value); } + } + } + + /// + /// Tests a Password for a User account. + /// + /// The User account. + /// The Password to test. + /// True if the Password is correct. + /// If or are null. + public bool TestAccount(UserInfo user, string password) { + return false; + } + + /// + /// Gets the complete list of Users. + /// + /// All the Users, sorted by username. + public UserInfo[] GetUsers() { + return new UserInfo[] { }; + } + + /// + /// Adds a new User. + /// + /// The Username. + /// The display name (can be null). + /// The Password. + /// The Email address. + /// A value indicating whether the account is active. + /// The Account creation Date/Time. + /// + /// The correct object or null. + /// + /// If , or are null. + /// If , or are empty. + public UserInfo AddUser(string username, string displayName, string password, string email, bool active, DateTime dateTime) { + throw new NotImplementedException(); + } + + /// + /// Modifies a User. + /// + /// The Username of the user to modify. + /// The new display name (can be null). + /// The new Password (null or blank to keep the current password). + /// The new Email address. + /// A value indicating whether the account is active. + /// + /// The correct object or null. + /// + /// If or are null. + /// If is empty. + public UserInfo ModifyUser(UserInfo user, string newDisplayName, string newPassword, string newEmail, bool newActive) { + throw new NotImplementedException(); + } + + /// + /// Removes a User. + /// + /// The User to remove. + /// + /// True if the User has been removed successfully. + /// + /// If is null. + public bool RemoveUser(UserInfo user) { + throw new NotImplementedException(); + } + + /// + /// Gets all the user groups. + /// + /// All the groups, sorted by name. + public UserGroup[] GetUserGroups() { + return new UserGroup[] { }; + } + + /// + /// Adds a new user group. + /// + /// The name of the group. + /// The description of the group. + /// + /// The correct object or null. + /// + /// If or are null. + /// If is empty. + public UserGroup AddUserGroup(string name, string description) { + throw new NotImplementedException(); + } + + /// + /// Modifies a user group. + /// + /// The group to modify. + /// The new description of the group. + /// + /// The correct object or null. + /// + /// If or are null. + public UserGroup ModifyUserGroup(UserGroup group, string description) { + throw new NotImplementedException(); + } + + /// + /// Removes a user group. + /// + /// The group to remove. + /// + /// true if the group is removed, false otherwise. + /// + /// If is null. + public bool RemoveUserGroup(UserGroup group) { + throw new NotImplementedException(); + } + + /// + /// Sets the group memberships of a user account. + /// + /// The user account. + /// The groups the user account is member of. + /// + /// The correct object or null. + /// + /// If or are null. + public UserInfo SetUserMembership(UserInfo user, string[] groups) { + throw new NotImplementedException(); + } + + /// + /// Tries to login a user directly through the provider. + /// + /// The username. + /// The password. + /// + /// The correct UserInfo object, or null. + /// + /// If or are null. + public UserInfo TryManualLogin(string username, string password) { + var info = StorageProvider.TryManualLogin(username, password); + + if(info != null) + return info; + + if(m_Config.ValidateCredentials(username, password)) + return GetUser(username); + + return null; + } + + /// + /// Tries to login a user directly through the provider using + /// the current HttpContext and without username/password. + /// + /// The current HttpContext. + /// + /// The correct UserInfo object, or null. + /// + /// If is null. + public UserInfo TryAutoLogin(HttpContext context) { + try { + if(!context.User.Identity.IsAuthenticated) + return null; + + var username = context.User.Identity.Name.Substring(context.User.Identity.Name.IndexOf(@"\") + 1); + + return GetUser(username); + } + + catch { + return null; + } + } + + /// + /// Creates the primary group SID. + /// http://dunnry.com/blog/DeterminingYourPrimaryGroupInActiveDirectoryUsingNET.aspx + /// + /// The user sid. + /// The primary group ID. + private void CreatePrimaryGroupSID(byte[] userSid, int primaryGroupID) { + // convert the int into a byte array + byte[] rid = BitConverter.GetBytes(primaryGroupID); + + // place the bytes into the user's SID byte array + // overwriting them as necessary + for(int i = 0; i < rid.Length; i++) { + userSid.SetValue(rid[i], new long[] { userSid.Length - (rid.Length - i) }); + } + } + + /// + /// Builds the octet string. + /// http://dunnry.com/blog/DeterminingYourPrimaryGroupInActiveDirectoryUsingNET.aspx + /// + /// The bytes. + /// + private string BuildOctetString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < bytes.Length; i++) { + sb.Append(bytes[i].ToString("X2")); + } + + return sb.ToString(); + } + + private class UserProperties { + public byte[] ObjectSid; + public string Mail; + public string DisplayName; + public List MemberOf; + public int PrimaryGroupID; + } + + /// + /// Gets the user info object for the currently logged in user. + /// + /// The username. + /// + /// The , or null. + /// + /// If is null. + /// If is empty. + public UserInfo GetUser(string username) { + try { + var user = StorageProvider.GetUser(username); + + if(user != null) + return user; + + // Active Directory Attributes + // + // http://msdn.microsoft.com/en-us/library/ms683980(VS.85).aspx + // + // Object-Sid -> objectSid (required, single-value) + // Object-Class -> objectClass (required, multi-value) + // Object-Category -> objectCategory (required, single-value) + // SAM-Account-Name -> sAMAccountName (required, single-value) + // E-mail-Addresses -> mail (optional, single-value) + // Display-Name -> displayName (optional, single-value) + // Is-Member-Of-DL -> memberOf (optional, multi-value) + + using(Domain domain = m_Config.GetDomain()) { + SearchResult result; + + try { + using(DirectoryEntry searchRoot = domain.GetDirectoryEntry()) { + using(var searcher = new DirectorySearcher(searchRoot)) { + searcher.Filter = String.Format("(&(objectClass=user)(objectCategory=person)(sAMAccountName={0}))", username); + + searcher.PropertiesToLoad.Add("objectSid"); + searcher.PropertiesToLoad.Add("mail"); + searcher.PropertiesToLoad.Add("displayName"); + searcher.PropertiesToLoad.Add("memberOf"); + searcher.PropertiesToLoad.Add("primaryGroupID"); + + result = searcher.FindOne(); + } + } + + if(result == null) + return null; + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Unable to complete search for user \"{0}\": {1}", username, ex); + return null; + } + + UserProperties userProperties; + + try { + userProperties = new UserProperties { + ObjectSid = result.Properties["objectSid"].Cast().Single(), + Mail = result.Properties["mail"].Cast().SingleOrDefault(), + DisplayName = result.Properties["displayName"].Cast().SingleOrDefault(), + MemberOf = result.Properties["memberOf"].Cast().ToList(), + PrimaryGroupID = result.Properties["primaryGroupID"].Cast().Single(), + }; + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Unable to access properties for user \"{0}\": {1}", username, ex); + return null; + } + + if(userProperties.Mail == null) { + if(m_Config.AutomaticMail != null && !string.IsNullOrEmpty(userProperties.DisplayName)) { + userProperties.Mail = userProperties.DisplayName.Replace(" ", ".") + "@" + m_Config.AutomaticMail; + } + else { + LogEntry(LogEntryType.Error, "Cannot login user \"{0}\" because they have no email address.", username); + return null; + } + } + + CreatePrimaryGroupSID(userProperties.ObjectSid, userProperties.PrimaryGroupID); + + string primaryGroupPath = String.Format("", BuildOctetString(userProperties.ObjectSid)); + + userProperties.MemberOf.Add(primaryGroupPath); + + var domainGroups = new List(); + + foreach(string memberOfPath in userProperties.MemberOf) { + try { + using(var memberOfEntry = domain.GetDirectoryEntry()) { + string basePath = memberOfEntry.Path.Remove(memberOfEntry.Path.LastIndexOf("/")); + + memberOfEntry.Path = String.Format("{0}/{1}", basePath, memberOfPath); + + var samAccountName = memberOfEntry.Properties["sAMAccountName"].Cast().Single(); + + domainGroups.Add(samAccountName); + } + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Skipping group \"{0}\" due to lookup error: {1}", memberOfPath, ex); + continue; + } + } + + var wikiGroups = m_Config.GetWikiGroups(domainGroups); + + if(wikiGroups.Count == 0) { + LogEntry(LogEntryType.Error, "Refusing to create user \"{0}\" without any groups, please check the GroupMap configuration.", username); + return null; + } + + user = StorageProvider.AddUser(username, userProperties.DisplayName, GeneratePassword(), userProperties.Mail, true, DateTime.Now); + + if(user == null) { + LogEntry(LogEntryType.Error, "Failed to create user \"{0}\" using provider \"{1}\", but no error was given by the provider.", username, StorageProvider.GetType()); + return null; + } + + LogEntry(LogEntryType.General, "Created user \"{0}\" using provider \"{1}\", but no group membership has been set yet.", username, StorageProvider.GetType()); + + user = StorageProvider.SetUserMembership(user, wikiGroups.ToArray()); + + if(user == null) { + LogEntry(LogEntryType.Error, "Failed to set user membership for user \"{0}\" using provider \"{1}\", but no error was given by the provider.", username, StorageProvider.GetType()); + return null; + } + + LogEntry(LogEntryType.General, "Set user membership for user \"{0}\" using provider \"{1}\", user is ready for use.", username, StorageProvider.GetType()); + + return user; + } + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Error looking up user: {0}", ex); + return null; + } + } + + + /// + /// Generate a random password of garbage since a non-zero length password is required + /// + /// The random password. + private string GeneratePassword() { + byte[] bytes = new byte[100]; + m_Random.NextBytes(bytes); + return new String(bytes.Select(t => Convert.ToChar(t)).ToArray()); + } + + + /// + /// Gets a user account. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The email address. + /// + /// The first user found with the specified email address, or null. + /// + /// If is null. + /// If is empty. + public UserInfo GetUserByEmail(string email) { + return StorageProvider.GetUserByEmail(email); + + } + + + /// + /// Notifies the provider that a user has logged in through the authentication cookie. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The user who has logged in. + /// If is null. + public void NotifyCookieLogin(UserInfo user) { + StorageProvider.NotifyCookieLogin(user); + } + + + /// + /// Notifies the provider that a user has logged out. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The user who has logged out. + /// If is null. + public void NotifyLogout(UserInfo user) { + StorageProvider.NotifyLogout(user); + } + + + /// + /// Stores a user data element, overwriting the previous one if present. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The user the data belongs to. + /// The key of the data element (case insensitive). + /// The value of the data element, null for deleting the data. + /// + /// true if the data element is stored, false otherwise. + /// + /// If or are null. + /// If is empty. + public bool StoreUserData(UserInfo user, string key, string value) { + return StorageProvider.StoreUserData(user, key, value); + } + + + /// + /// Gets a user data element, if any. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The user the data belongs to. + /// The key of the data element. + /// + /// The value of the data element, or null if the element is not found. + /// + /// If or are null. + /// If is empty. + public string RetrieveUserData(UserInfo user, string key) { + return StorageProvider.RetrieveUserData(user, key); + } + + + /// + /// Retrieves all the user data elements for a user. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The user. + /// The user data elements (key->value). + /// If is null. + public IDictionary RetrieveAllUserData(UserInfo user) { + return StorageProvider.RetrieveAllUserData(user); + } + + + /// + /// Gets all the users that have the specified element in their data. + /// Not Implemented - Passed Directly to the IUsersStorageProviderV30 + /// + /// The key of the data. + /// The users and the data. + /// If is null. + /// If is empty. + public IDictionary GetUsersWithData(string key) { + return StorageProvider.GetUsersWithData(key); + } + + + /// + /// Gets a value indicating whether user accounts are read-only. + /// + /// True, always, we can't write back to AD + public bool UserAccountsReadOnly { + get { return true; } + } + + + /// + /// Gets a value indicating whether user groups are read-only. If so, the provider + /// should support default user groups as defined in the wiki configuration. + /// + /// True, always, we can't write back to AD + public bool UserGroupsReadOnly { + get { return true; } + } + + /// + /// Gets a value indicating whether group membership is read-only (if + /// is false, then this property must be false). If this property is true, the provider + /// should return membership data compatible with default user groups. + /// + /// True, always, we can't write back to AD + public bool GroupMembershipReadOnly { + get { return true; } + } + + + /// + /// Gets a value indicating whether users' data is read-only. + /// + /// True, always, we can't write back to AD + public bool UsersDataReadOnly { + get { return true; } + } + + /// + /// Initializes the Storage Provider. + /// + /// The Host of the Component. + /// The Configuration data, if any. + /// If or are null. + /// If is not valid or is incorrect. + public void Init(IHostV30 host, string config) { + m_Host = host; + m_Random = new Random(); + + InitConfig(config); + } + + + /// + /// Returns the storage provider. + /// The storage provider is identified the first time it is needed, rather than at init. + /// This avoids a dependency on the storage provider being loaded first, which is not guaranteed + /// + /// The storage provider. + private IUsersStorageProviderV30 StorageProvider { + get { + if(m_StorageProvider == null) { + lock(m_Host) { + if(m_StorageProvider == null) { + m_StorageProvider = (from a in m_Host.GetUsersStorageProviders(true) + where a.Information.Name != this.Information.Name + select a).FirstOrDefault(); + + if(m_StorageProvider == null) { + LogEntry(LogEntryType.Error, "This provider requires an additional active storage provider for storing of active directory user information."); + throw new InvalidConfigurationException("This provider requires an additional active storage provider for storing of active directory user information."); + } + } + } + } + + return m_StorageProvider; + } + } + + + /// + /// Method invoked on shutdown. + /// Ignored + /// + /// This method might not be invoked in some cases. + public void Shutdown() { + StorageProvider.Shutdown(); + } + + + /// + /// Gets the Information about the Provider. + /// + /// The information + public ComponentInformation Information { + get { return new ComponentInformation("Active Directory Provider", "Threeplicate Srl", "3.0.2.509", "http://www.screwturn.eu", "http://www.screwturn.eu/Version/ActiveDirectoryProvider.txt"); } + } + + + /// + /// Gets a brief summary of the configuration string format, in HTML. Returns null if no configuration is needed. + /// + /// + public string ConfigHelpHtml { + get { return HELP_HELP; } + } + + + /// + /// Logs a message from this plugin. + /// + /// Type of the entry. + /// The message. + /// The args. + private void LogEntry(LogEntryType entryType, string message, params object[] args) { + string entry = String.Format(message, args); + m_Host.LogEntry(entry, entryType, null, this); + } + + + /// + /// Configures the plugin based on the configuration settings. + /// + /// The config. + private void InitConfig(string config) { + Config newConfig = ParseConfig(config); + + if(!newConfig.IsGroupMapSet) { + LogEntry(LogEntryType.Error, "No GroupMap entries found. Please make sure at least one valid GroupMap configuration entry exists."); + throw new InvalidConfigurationException("No GroupMap entries found. Please make sure at least one valid GroupMap configuration entry exists."); + } + + if(newConfig.CommonGroups.Length > 0 && newConfig.DefaultGroups.Length > 0) { + LogEntry(LogEntryType.Warning, "DefaultGroups will be ignored because CommonGroups have been configured."); + newConfig.DefaultGroups = null; + } + + if(newConfig.ServerName != null) { + if(newConfig.DomainName != null) { + LogEntry(LogEntryType.Warning, + "Domain and Server config keys are mutually exclusive, but both were given. " + + "The Domain entry will be ignored."); + + newConfig.DomainName = null; + } + + LogEntry(LogEntryType.General, + "Configured to use domain controller \"{0}\".", + newConfig.ServerName); + } + else { + if(newConfig.DomainName == null) { + try { + newConfig.DomainName = Domain.GetComputerDomain().Name; + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Unable to auto-detect the computer domain: {0}", ex); + throw new InvalidConfigurationException("Unable to auto-detect the computer domain.", ex); + } + + LogEntry(LogEntryType.General, + "Domain \"{0}\" was determined through auto-detection.", + newConfig.DomainName); + } + else { + LogEntry(LogEntryType.General, + "Configured to use domain \"{0}\".", + newConfig.DomainName); + } + } + + try { + using(Domain domain = newConfig.GetDomain()) { + } + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Unable to connect to active directory with configured username and password (if any): {0}", ex); + throw new InvalidConfigurationException("Unable to connect to active directory with configured username and password (if any).", ex); + } + + m_Config = newConfig; + } + + + /// + /// Parses the plugin configuration string. + /// + /// The config. + /// A Config object representig the configuration string. + private Config ParseConfig(string config) { + Config newConfig = new Config(); + + try { + string[] configLines = config.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + foreach(string configLine in configLines) { + string remainingConfigLine = configLine.Split(new[] { ';' }).First(); + + if(remainingConfigLine.Length == 0) + continue; + + string[] configEntry = remainingConfigLine.Split(new[] { '=' }, 2); + + if(configEntry.Length != 2) { + LogEntry(LogEntryType.Error, + "Config lines must be in the format \"Key=Value\". " + + "The config line \"{0}\" will be ignored.", + configLine); + continue; + } + + string key = configEntry[0].Trim().ToLower(); + string value = configEntry[1].Trim(); + + switch(key) { + case "server": + newConfig.ServerName = value; + break; + + case "username": + newConfig.Username = value; + break; + + case "password": + newConfig.Password = value; + break; + + case "domain": + newConfig.DomainName = value; + break; + + case "groupmap": + string[] groupMap = value.Split(new[] { ':' }, 2); + + if(groupMap.Length != 2) { + LogEntry(LogEntryType.Error, + "GroupMap entries must be in the format \"GroupMap=DomainGroup:WikiGroup[,...]\". " + + "The config line \"{0}\" will be ignored.", + configLine); + break; + } + + string fromDomainGroup = groupMap[0]; + string[] toWikiGroups = groupMap[1].Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray(); + + newConfig.AddGroupMap(fromDomainGroup, toWikiGroups); + + break; + + case "commongroups": + string[] commonGroups = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray(); + newConfig.CommonGroups = commonGroups; + break; + + case "defaultgroups": + string[] defaultGroups = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray(); + newConfig.DefaultGroups = defaultGroups; + break; + + case "automaticmail": + newConfig.AutomaticMail = value; + break; + + default: + LogEntry(LogEntryType.Error, + "Invalid config key, see help for valid options. " + + "The config line \"{0}\" will be ignored.", + configLine); + break; + } + } + + return newConfig; + } + + catch(Exception ex) { + LogEntry(LogEntryType.Error, "Error parsing the configuration: {0}", ex); + throw new InvalidConfigurationException("Error parsing the configuration.", ex); + } + } } - } diff --git a/ActiveDirectoryProvider/ActiveDirectoryProvider.csproj b/ActiveDirectoryProvider/ActiveDirectoryProvider.csproj index 0437b8d..8ad289a 100644 --- a/ActiveDirectoryProvider/ActiveDirectoryProvider.csproj +++ b/ActiveDirectoryProvider/ActiveDirectoryProvider.csproj @@ -63,6 +63,9 @@ PluginFramework + + +