Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add configuration list to determine which users have a discontinued p…
…assword login (#5414)
  • Loading branch information
Scott Bommarito committed Feb 9, 2018
1 parent 6bac355 commit 084311c
Show file tree
Hide file tree
Showing 24 changed files with 414 additions and 113 deletions.
@@ -0,0 +1,11 @@
{
"DiscontinuedForEmailAddresses": [
"cannotUsePassword@canUsePassword.com"
],
"DiscontinuedForDomains": [
"cannotUsePassword.com"
],
"ExceptionsForEmailAddresses": [
"exception@cannotUsePassword.com"
]
}
6 changes: 6 additions & 0 deletions src/NuGetGallery/App_Start/DefaultDependenciesModule.cs
Expand Up @@ -252,6 +252,12 @@ protected override void Load(ContainerBuilder builder)

builder.RegisterType<RequireSecurePushForCoOwnersPolicy>()
.SingleInstance();

builder.RegisterType<ContentObjectService>()
.AsSelf()
.As<IContentObjectService>()
.SingleInstance();
HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await DependencyResolver.Current.GetService<IContentObjectService>().Refresh());

var mailSenderThunk = new Lazy<IMailSender>(
() =>
Expand Down
14 changes: 8 additions & 6 deletions src/NuGetGallery/Authentication/AuthenticationService.cs
Expand Up @@ -29,8 +29,8 @@ public class AuthenticationService
private readonly ICredentialBuilder _credentialBuilder;
private readonly ICredentialValidator _credentialValidator;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IContentObjectService _contentObjectService;
private readonly ITelemetryService _telemetryService;
private readonly IUserService _userService;

/// <summary>
/// This ctor is used for test only.
Expand All @@ -46,7 +46,7 @@ protected AuthenticationService()
IEntitiesContext entities, IAppConfiguration config, IDiagnosticsService diagnostics,
IAuditingService auditing, IEnumerable<Authenticator> providers, ICredentialBuilder credentialBuilder,
ICredentialValidator credentialValidator, IDateTimeProvider dateTimeProvider, ITelemetryService telemetryService,
IUserService userService)
IContentObjectService contentObjectService)
{
InitCredentialFormatters();

Expand All @@ -59,7 +59,7 @@ protected AuthenticationService()
_credentialValidator = credentialValidator ?? throw new ArgumentNullException(nameof(credentialValidator));
_dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider));
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_contentObjectService = contentObjectService ?? throw new ArgumentNullException(nameof(contentObjectService));
}

public IEntitiesContext Entities { get; private set; }
Expand Down Expand Up @@ -236,7 +236,7 @@ private async Task<AuthenticatedUser> AuthenticateInternal(Func<Credential, Cred
public virtual async Task CreateSessionAsync(IOwinContext owinContext, AuthenticatedUser user)
{
// Create a claims identity for the session
ClaimsIdentity identity = CreateIdentity(user.User, AuthenticationTypes.LocalUser, GetDiscontinuedLoginClaims(user));
ClaimsIdentity identity = CreateIdentity(user.User, AuthenticationTypes.LocalUser, await GetDiscontinuedLoginClaims(user));

// Issue the session token and clean up the external token if present
owinContext.Authentication.SignIn(identity);
Expand All @@ -247,9 +247,11 @@ public virtual async Task CreateSessionAsync(IOwinContext owinContext, Authentic
new UserAuditRecord(user.User, AuditedUserAction.Login, user.CredentialUsed));
}

private Claim[] GetDiscontinuedLoginClaims(AuthenticatedUser user)
private async Task<Claim[]> GetDiscontinuedLoginClaims(AuthenticatedUser user)
{
return user.CredentialUsed.IsPassword() && _userService.AreOrganizationsEnabledForAccount(user.User) ?
await _contentObjectService.Refresh();

return _contentObjectService.LoginDiscontinuationConfiguration.IsLoginDiscontinued(user) ?
new[] { new Claim(NuGetClaims.DiscontinuedLogin, NuGetClaims.DiscontinuedLoginValue) } :
new Claim[0];
}
Expand Down
4 changes: 0 additions & 4 deletions src/NuGetGallery/Configuration/AppConfiguration.cs
Expand Up @@ -68,10 +68,6 @@ public class AppConfiguration : IAppConfiguration
public bool AsynchronousPackageValidationEnabled { get; set; }

public bool BlockingAsynchronousPackageValidationEnabled { get; set; }

[DefaultValue(null)]
[TypeConverter(typeof(StringArrayConverter))]
public string[] OrganizationsEnabledForDomains { get; set; }

/// <summary>
/// Gets the URI to the search service
Expand Down
5 changes: 0 additions & 5 deletions src/NuGetGallery/Configuration/IAppConfiguration.cs
Expand Up @@ -93,11 +93,6 @@ public interface IAppConfiguration : ICoreMessageServiceConfiguration
/// </summary>
bool BlockingAsynchronousPackageValidationEnabled { get; set; }

/// <summary>
/// Whitelist of domains for which the Organizations feature is enabled.
/// </summary>
string[] OrganizationsEnabledForDomains { get; set; }

/// <summary>
/// Gets the URI to the search service
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/Constants.cs
Expand Up @@ -84,6 +84,7 @@ public static class ContentNames
public static readonly string TermsOfUse = "Terms-Of-Use";
public static readonly string PrivacyPolicy = "Privacy-Policy";
public static readonly string Team = "Team";
public static readonly string LoginDiscontinuationConfiguration = "Login-Discontinuation-Configuration";
}

public static class StatisticsDimensions
Expand Down
6 changes: 3 additions & 3 deletions src/NuGetGallery/Controllers/AccountsController.cs
Expand Up @@ -54,7 +54,7 @@ public class ViewMessages
protected internal abstract ViewMessages Messages { get; }

[HttpGet]
[UIAuthorize]
[UIAuthorize(allowDiscontinuedLogins: true)]
public virtual ActionResult ConfirmationRequired(string accountName = null)
{
var account = GetAccount(accountName);
Expand All @@ -70,7 +70,7 @@ public virtual ActionResult ConfirmationRequired(string accountName = null)
return View(model);
}

[UIAuthorize]
[UIAuthorize(allowDiscontinuedLogins: true)]
[HttpPost]
[ActionName("ConfirmationRequired")]
[ValidateAntiForgeryToken]
Expand Down Expand Up @@ -106,7 +106,7 @@ public virtual ActionResult ConfirmationRequiredPost(string accountName = null)
return View(model);
}

[UIAuthorize]
[UIAuthorize(allowDiscontinuedLogins: true)]
public virtual async Task<ActionResult> Confirm(string username, string token)
{
// We don't want Login to go to this page as a return URL
Expand Down
2 changes: 1 addition & 1 deletion src/NuGetGallery/Controllers/UsersController.cs
Expand Up @@ -321,7 +321,7 @@ private ApiKeyOwnerViewModel CreateApiKeyOwnerViewModel(User user, bool canPushN
}

[HttpGet]
[UIAuthorize]
[UIAuthorize(allowDiscontinuedLogins: true)]
public virtual ActionResult Thanks()
{
// No need to redirect here after someone logs in...
Expand Down
4 changes: 4 additions & 0 deletions src/NuGetGallery/NuGetGallery.csproj
Expand Up @@ -849,7 +849,9 @@
<Compile Include="Services\IActionRequiringEntityPermissions.cs" />
<Compile Include="Services\IDeleteAccountService.cs" />
<Compile Include="Services\IFileStorageService.cs" />
<Compile Include="Services\IContentObjectService.cs" />
<Compile Include="Services\ImmediatePackageValidator.cs" />
<Compile Include="Services\ContentObjectService.cs" />
<Compile Include="Services\PackageOwnershipManagementService.cs" />
<Compile Include="Services\IPackageOwnershipManagementService.cs" />
<Compile Include="Services\IPackageValidationInitiator.cs" />
Expand Down Expand Up @@ -933,6 +935,7 @@
<Compile Include="Services\PackageUploadService.cs" />
<Compile Include="Services\IReservedNamespaceService.cs" />
<Compile Include="Services\IPackageUploadService.cs" />
<Compile Include="Services\LoginDiscontinuationConfiguration.cs" />
<Compile Include="Services\PermissionsCheckResult.cs" />
<Compile Include="Services\PermissionsRequirement.cs" />
<Compile Include="Services\ReservedNamespaceService.cs" />
Expand Down Expand Up @@ -1981,6 +1984,7 @@
<Content Include="Areas\Admin\Views\Delete\_ReflowBulk.cshtml" />
<Content Include="Areas\Admin\Views\Delete\_ReflowBulkConfirm.cshtml" />
<Content Include="Areas\Admin\Views\LockPackage\Index.cshtml" />
<Content Include="App_Data\Files\Content\Login-Discontinuation-Configuration.json" />
<None Include="Scripts\jquery-1.11.0.intellisense.js" />
<Content Include="Scripts\jquery-1.11.0.js" />
<Content Include="Scripts\jquery-1.11.0.min.js" />
Expand Down
46 changes: 46 additions & 0 deletions src/NuGetGallery/Services/ContentObjectService.cs
@@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace NuGetGallery
{
public class ContentObjectService : IContentObjectService
{
private const int RefreshIntervalHours = 1;

private readonly IContentService _contentService;

public ContentObjectService(IContentService contentService)
{
_contentService = contentService;

LoginDiscontinuationConfiguration =
new LoginDiscontinuationConfiguration(
Enumerable.Empty<string>(), Enumerable.Empty<string>(), Enumerable.Empty<string>());
}

public ILoginDiscontinuationConfiguration LoginDiscontinuationConfiguration { get; set; }

public async Task Refresh()
{
LoginDiscontinuationConfiguration =
await Refresh<LoginDiscontinuationConfiguration>(Constants.ContentNames.LoginDiscontinuationConfiguration);
}

private async Task<T> Refresh<T>(string contentName)
where T : class
{
var configString = (await _contentService.GetContentItemAsync(contentName, TimeSpan.FromHours(RefreshIntervalHours))).ToString();
if (string.IsNullOrEmpty(configString))
{
return null;
}

return JsonConvert.DeserializeObject<T>(configString);
}
}
}
14 changes: 14 additions & 0 deletions src/NuGetGallery/Services/IContentObjectService.cs
@@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;

namespace NuGetGallery
{
public interface IContentObjectService
{
ILoginDiscontinuationConfiguration LoginDiscontinuationConfiguration { get; }

Task Refresh();
}
}
2 changes: 0 additions & 2 deletions src/NuGetGallery/Services/IUserService.cs
Expand Up @@ -36,8 +36,6 @@ public interface IUserService

bool CanTransformUserToOrganization(User accountToTransform, out string errorReason);

bool AreOrganizationsEnabledForAccount(User account);

bool CanTransformUserToOrganization(User accountToTransform, User adminUser, out string errorReason);

Task RequestTransformToOrganizationAccount(User accountToTransform, User adminUser);
Expand Down
52 changes: 52 additions & 0 deletions src/NuGetGallery/Services/LoginDiscontinuationConfiguration.cs
@@ -0,0 +1,52 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NuGetGallery.Authentication;

namespace NuGetGallery
{
public class LoginDiscontinuationConfiguration : ILoginDiscontinuationConfiguration
{
public HashSet<string> DiscontinuedForEmailAddresses { get; }
public HashSet<string> DiscontinuedForDomains { get; }
public HashSet<string> ExceptionsForEmailAddresses { get; }

[JsonConstructor]
public LoginDiscontinuationConfiguration(
IEnumerable<string> discontinuedForEmailAddresses,
IEnumerable<string> discontinuedForDomains,
IEnumerable<string> exceptionsForEmailAddresses)
{
DiscontinuedForEmailAddresses = new HashSet<string>(discontinuedForEmailAddresses);
DiscontinuedForDomains = new HashSet<string>(discontinuedForDomains);
ExceptionsForEmailAddresses = new HashSet<string>(exceptionsForEmailAddresses);
}

public bool IsLoginDiscontinued(AuthenticatedUser authUser)
{
var email = authUser.User.ToMailAddress();
return
authUser.CredentialUsed.IsPassword() &&
AreOrganizationsSupportedForUser(authUser.User) &&
!ExceptionsForEmailAddresses.Contains(email.Address);
}

public bool AreOrganizationsSupportedForUser(User user)
{
var email = user.ToMailAddress();
return
DiscontinuedForDomains.Contains(email.Host, StringComparer.OrdinalIgnoreCase) ||
DiscontinuedForEmailAddresses.Contains(email.Address);
}
}

public interface ILoginDiscontinuationConfiguration
{
bool IsLoginDiscontinued(AuthenticatedUser authUser);
bool AreOrganizationsSupportedForUser(User user);
}
}
12 changes: 4 additions & 8 deletions src/NuGetGallery/Services/UserService.cs
Expand Up @@ -26,6 +26,7 @@ public class UserService : IUserService
public IAuditingService Auditing { get; protected set; }

public IEntitiesContext EntitiesContext { get; protected set; }
public IContentObjectService ContentObjectService { get; protected set; }

public ISecurityPolicyService SecurityPolicyService { get; set; }

Expand All @@ -37,6 +38,7 @@ public class UserService : IUserService
IEntityRepository<Credential> credentialRepository,
IAuditingService auditing,
IEntitiesContext entitiesContext,
IContentObjectService contentObjectService,
ISecurityPolicyService securityPolicyService)
: this()
{
Expand All @@ -45,6 +47,7 @@ public class UserService : IUserService
CredentialRepository = credentialRepository;
Auditing = auditing;
EntitiesContext = entitiesContext;
ContentObjectService = contentObjectService;
SecurityPolicyService = securityPolicyService;
}

Expand Down Expand Up @@ -310,7 +313,7 @@ public bool CanTransformUserToOrganization(User accountToTransform, out string e
{
errorReason = Strings.TransformAccount_AccountHasMemberships;
}
else if (!AreOrganizationsEnabledForAccount(accountToTransform))
else if (!ContentObjectService.LoginDiscontinuationConfiguration.AreOrganizationsSupportedForUser(accountToTransform))
{
errorReason = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_FailedReasonNotInDomainWhitelist, accountToTransform.Username);
Expand All @@ -319,13 +322,6 @@ public bool CanTransformUserToOrganization(User accountToTransform, out string e
return errorReason == null;
}

public bool AreOrganizationsEnabledForAccount(User account)
{
var enabledDomains = Config.OrganizationsEnabledForDomains;
return enabledDomains != null &&
enabledDomains.Contains(account.ToMailAddress().Host, StringComparer.OrdinalIgnoreCase);
}

public bool CanTransformUserToOrganization(User accountToTransform, User adminUser, out string errorReason)
{
if (!CanTransformUserToOrganization(accountToTransform, out errorReason))
Expand Down
1 change: 0 additions & 1 deletion src/NuGetGallery/Web.config
Expand Up @@ -41,7 +41,6 @@

<add key="Gallery.AsynchronousPackageValidationEnabled" value="false" />
<add key="Gallery.BlockingAsynchronousPackageValidationEnabled" value="false" />
<add key="Gallery.OrganizationsEnabledForDomains" value="" />

<add key="AzureServiceBus.Validation.ConnectionString" value="" />
<add key="AzureServiceBus.Validation.TopicName" value="" />
Expand Down

0 comments on commit 084311c

Please sign in to comment.