Skip to content

Instantly share code, notes, and snippets.

@MJRichardson
Created January 18, 2016 00:55
Show Gist options
  • Select an option

  • Save MJRichardson/7110d574f38ec7994c27 to your computer and use it in GitHub Desktop.

Select an option

Save MJRichardson/7110d574f38ec7994c27 to your computer and use it in GitHub Desktop.
Octopus Deploy Active Directory Membership
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Octopus.Core.Model.Events;
using Octopus.Core.Model.Users;
using Octopus.Core.RelationalStorage;
using Octopus.Shared.Configuration;
using Octopus.Shared.Diagnostics;
using Octopus.Shared.Util;
namespace Octopus.Server.Web.Infrastructure.Authentication
{
public class ActiveDirectoryMembership : IMembership, IPrincipalAwareMembership
{
const string NTAccountUsernamePrefix = "nt:";
/// <summary>
/// This logon type is intended for high performance servers to authenticate plaintext passwords.
/// The LogonUser function does not cache credentials for this logon type.
/// </summary>
const int LOGON32_LOGON_NETWORK = 3;
/// <summary>
/// Use the standard logon provider for the system.
/// The default security provider is negotiate, unless you pass NULL for the domain name and the user name
/// is not in UPN format. In this case, the default provider is NTLM.
/// NOTE: Windows 2000/NT: The default security provider is NTLM.
/// </summary>
const int LOGON32_PROVIDER_DEFAULT = 0;
readonly IRelationalStore relationalStore;
readonly ILog log;
readonly Lazy<IWebPortalConfiguration> adConfiguration;
readonly Lazy<IImplicitUserCreationPolicy> creationPolicy;
public ActiveDirectoryMembership(IRelationalStore relationalStore, ILog log, Lazy<IWebPortalConfiguration> adConfiguration, Lazy<IImplicitUserCreationPolicy> creationPolicy)
{
this.relationalStore = relationalStore;
this.log = log;
this.adConfiguration = adConfiguration;
this.creationPolicy = creationPolicy;
}
public User ValidateCredentials(string username, string password)
{
if (!adConfiguration.Value.IsFormsAuthAllowed())
{
return null;
}
if (username == null) throw new ArgumentNullException("username");
log.VerboseFormat("Validating credentials provided for '{0}'...", username);
string domain;
NormalizeCredentials(username, out username, out domain);
using (var context = GetContext(domain))
{
var principal = UserPrincipal.FindByIdentity(context, username);
if (principal == null)
{
var searchedContext = domain ?? context.Name ?? context.ConnectedServer;
log.InfoFormat("A principal identifiable by '{0}' was not found in '{1}'", username, searchedContext);
return null;
}
var hToken = IntPtr.Zero;
try
{
var logon = domain == null ? principal.UserPrincipalName : username;
log.VerboseFormat("Calling LogonUser(\"{0}\", \"{1}\", ...)", logon, domain);
if (!LogonUser(logon, domain, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, out hToken))
{
var error = new Win32Exception();
log.WarnFormat(error, "Principal '{0}' (Domain: '{1}') could not be logged on via WIN32: 0x{2:X8}.", logon, domain, error.NativeErrorCode);
return null;
}
}
finally
{
if (hToken != IntPtr.Zero) CloseHandle(hToken);
}
log.VerboseFormat("Credentials for '{0}' validated, mapped to principal '{1}'", username, principal.UserPrincipalName ?? ("(NTAccount)" + principal.Name));
bool unused;
return GetOrCreateUser(principal, username, domain, out unused);
}
}
PrincipalContext GetContext(string domain)
{
var adContainer = adConfiguration.Value.ActiveDirectoryContainer;
adContainer = string.IsNullOrEmpty(adContainer) ? null : adContainer;
return new PrincipalContext(ContextType.Domain, domain, adContainer);
}
public IList<string> GetMemberExternalSecurityGroupIds(string username)
{
if (username == null) throw new ArgumentNullException("username");
log.VerboseFormat("Finding external security groups for '{0}'...", username);
string domain;
NormalizeCredentials(username, out username, out domain);
var groups = new List<string>();
using (var context = GetContext(domain))
{
var principal = UserPrincipal.FindByIdentity(context, username);
if (principal == null)
{
var searchedContext = domain ?? context.Name ?? context.ConnectedServer;
log.TraceFormat("While loading security groups, a principal identifiable by '{0}' was not found in '{1}'", username, searchedContext);
return new List<string>();
}
try
{
// Reads inherited groups - this fails in some situations
ReadAuthorizationGroups(principal, groups);
}
catch (Exception ex)
{
// Don't log it as an Error, it's expected to fail in some situations
log.Verbose(ex);
try
{
// Reads just the groups they are a member of - more reliable but not ideal
ReadUserGroups(principal, groups);
}
catch (Exception ex2)
{
// Only log an error if both methods fail to read the groups
log.Error(ex2);
return groups;
}
}
}
return groups;
}
static void ReadAuthorizationGroups(UserPrincipal principal, ICollection<string> groups)
{
ReadGroups(principal.GetAuthorizationGroups(), groups);
}
static void ReadUserGroups(Principal principal, ICollection<string> groups)
{
ReadGroups(principal.GetGroups(), groups);
}
static void ReadGroups(IEnumerable<Principal> groupPrincipals, ICollection<string> groups)
{
var iterGroup = groupPrincipals.GetEnumerator();
using (iterGroup)
{
while (iterGroup.MoveNext())
{
try
{
var p = iterGroup.Current;
groups.Add(p.Sid.Value);
}
catch (NoMatchingPrincipalException)
{
}
}
}
}
public IList<ExternalSecurityGroup> FindExternalSecurityGroups(string name)
{
var results = new List<ExternalSecurityGroup>();
string domain;
string username;
NormalizeCredentials(name, out username, out domain);
using (var context = GetContext(domain))
{
var searcher = new PrincipalSearcher();
searcher.QueryFilter = new GroupPrincipal(context) {Name = name + "*"};
var iterGroup = searcher.FindAll().GetEnumerator();
using (iterGroup)
{
while (iterGroup.MoveNext())
{
try
{
var p = iterGroup.Current as GroupPrincipal;
if (p == null || !(p.IsSecurityGroup ?? false))
continue;
results.Add(new ExternalSecurityGroup {Id = p.Sid.ToString(), DisplayName = p.Name});
}
catch (NoMatchingPrincipalException)
{
}
}
}
}
return results.OrderBy(o => o.DisplayName).ToList();
}
public User GetOrCreateUser(string username)
{
bool unused;
return GetOrCreateUser(username, out unused);
}
public User GetOrCreateUser(string username, out bool wasCreated)
{
string domain;
NormalizeCredentials(username, out username, out domain);
using (var context = GetContext(domain))
{
var principal = UserPrincipal.FindByIdentity(context, username);
if (principal == null)
{
var searchedContext = domain ?? context.Name ?? context.ConnectedServer;
throw new ArgumentException(string.Format("A principal identifiable by '{0}' was not found in '{1}'", username, searchedContext));
}
return GetOrCreateUser(principal, username, domain ?? Environment.UserDomainName, out wasCreated);
}
}
User GetOrCreateUser(UserPrincipal principal, string fallbackUsername, string fallbackDomain, out bool wasCreated)
{
wasCreated = false;
var name = principal.UserPrincipalName;
if (name == null)
{
log.WarnFormat("The user name (UPN) could not be determined for principal {0} - falling back to NT-style '{1}\\{2}'", principal, fallbackDomain, fallbackUsername);
if (string.IsNullOrWhiteSpace(fallbackDomain))
throw new InvalidOperationException("No fallback domain was provided");
if (string.IsNullOrWhiteSpace(fallbackUsername))
throw new InvalidOperationException("No fallback username was provided");
name = NTAccountUsernamePrefix + fallbackDomain.Trim() + "\\" + fallbackUsername.Trim();
}
using (var transaction = relationalStore.BeginTransaction())
{
var user = transaction.Query<User>().Where("Username = @username").Parameter("username", name).First();
if (user == null)
{
EnsureNotTooManyUsers();
// We implicitly provsion a Windows user for a username
user = new User(name);
user.DisplayName = string.IsNullOrWhiteSpace(principal.DisplayName) ? principal.Name : principal.DisplayName;
user.EmailAddress = principal.EmailAddress;
user.SetPassword(RandomStringGenerator.Generate(16));
transaction.Insert(user);
var team = transaction.Load<Team>(Team.EveryoneTeamId);
team.MemberUserIds.Add(user.Id);
transaction.Update(team);
transaction.Insert(Event.Build(EventCategory.Modified).Append("User ").AppendReference(user.Username, user.Id).Append(" was added to the team ").AppendReference(team.Name, team.Id).By(user.Username, user.Id).Build());
wasCreated = true;
transaction.Commit();
}
return user;
}
}
void EnsureNotTooManyUsers()
{
if (!creationPolicy.Value.CanCreateUser())
{
throw new ArgumentException("Unfortunately this Octopus server license is limited to a specific number of users, and adding a user account for you would put the server beyond this limit. Please contact your Octopus administrator to either disable some users, or increase the license limit.");
}
}
public static void NormalizeCredentials(string username, out string usernamePart, out string domainPart)
{
if (username == null) throw new ArgumentNullException("username");
if (username.StartsWith(NTAccountUsernamePrefix))
username = username.Remove(0, NTAccountUsernamePrefix.Length);
if (!TryParseDownLevelLogonName(username, out usernamePart, out domainPart))
{
usernamePart = username;
domainPart = null;
}
}
// If the return value is true, dlln was a valid down-level logon name, and username/domain
// contain precisely the component username and domain name values. Note, we don't split
// UPNs this way because the suffix part of a UPN is not necessarily a domain, and in
// the default LogonUser case should be passed whole to the function with a null domain.
public static bool TryParseDownLevelLogonName(string dlln, out string username, out string domain)
{
if (dlln == null) throw new ArgumentNullException("dlln");
username = null;
domain = null;
var slash = dlln.IndexOf('\\');
if (slash == -1 || slash == dlln.Length - 1 || slash == 0)
return false;
domain = dlln.Substring(0, slash).Trim();
username = dlln.Substring(slash + 1).Trim();
return !string.IsNullOrWhiteSpace(domain) && !string.IsNullOrWhiteSpace(username);
}
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out IntPtr phToken
);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(IntPtr hObject);
public User GetOrCreateUser(IPrincipal principal)
{
return GetOrCreateUser(principal.Identity.Name);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment