Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin panel for adding/remove user email address for password authentication. #9468

Merged
merged 17 commits into from
May 12, 2023
7 changes: 4 additions & 3 deletions src/NuGetGallery.Core/Auditing/FeatureFlagsAuditRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NuGet.Services.FeatureFlags;
using NuGetGallery.Auditing.AuditedEntities;
using NuGetGallery.Features;
using NuGetGallery.Shared;

namespace NuGetGallery.Auditing
{
Expand All @@ -13,13 +14,13 @@ public sealed class FeatureFlagsAuditRecord : AuditRecord<AuditedFeatureFlagsAct
public AuditedFeatureFlagFeature[] Features { get; }
public AuditedFeatureFlagFlight[] Flights { get; }
public string ContentId { get; }
public FeatureFlagSaveResult Result { get; }
public ContentSaveResult Result { get; }

public FeatureFlagsAuditRecord(
AuditedFeatureFlagsAction action,
FeatureFlags flags,
string contentId,
FeatureFlagSaveResult result)
string contentId,
ContentSaveResult result)
: base(action)
{
if (flags == null)
Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery.Core/CoreConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,7 @@ public static class Folders
public const string LicenseFileName = "license";

public const string FeatureFlagsFileName = "flags.json";

public const string LoginDiscontinuationConfigFileName = "Login-Discontinuation-Configuration.json";
}
}
5 changes: 5 additions & 0 deletions src/NuGetGallery.Core/CredentialTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,10 @@ public static Credential GetAzureActiveDirectoryCredential(this ICollection<Cred
{
return credentials.SingleOrDefault(c => IsAzureActiveDirectoryAccount(c.Type));
}

public static Credential GetMicrosoftAccountCredential(this ICollection<Credential> credentials)
{
return credentials.FirstOrDefault(c => IsMicrosoftAccount(c.Type));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using NuGet.Services.Entities;
using NuGet.Services.FeatureFlags;
using NuGetGallery.Auditing;
using NuGetGallery.Shared;

namespace NuGetGallery.Features
{
Expand Down Expand Up @@ -69,7 +70,7 @@ public async Task RemoveUserAsync(User user)
f => RemoveUser(f.Value, user)));

var saveResult = await TrySaveAsync(result, reference.ContentId);
if (saveResult == FeatureFlagSaveResult.Ok)
if (saveResult == ContentSaveResult.Ok)
{
return;
}
Expand All @@ -84,7 +85,7 @@ public async Task RemoveUserAsync(User user)
throw new InvalidOperationException($"Unable to remove user from feature flags after {MaxRemoveUserAttempts} attempts");
}

public async Task<FeatureFlagSaveResult> TrySaveAsync(FeatureFlags flags, string contentId)
public async Task<ContentSaveResult> TrySaveAsync(FeatureFlags flags, string contentId)
{
var result = await TrySaveInternalAsync(flags, contentId);
await _auditing.SaveAuditRecordAsync(
zhhyu marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -97,7 +98,7 @@ await _auditing.SaveAuditRecordAsync(
return result;
}

private async Task<FeatureFlagSaveResult> TrySaveInternalAsync(FeatureFlags flags, string contentId)
private async Task<ContentSaveResult> TrySaveInternalAsync(FeatureFlags flags, string contentId)
{
var accessCondition = AccessConditionWrapper.GenerateIfMatchCondition(contentId);

Expand All @@ -113,12 +114,12 @@ private async Task<FeatureFlagSaveResult> TrySaveInternalAsync(FeatureFlags flag

await _storage.SaveFileAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, stream, accessCondition);

return FeatureFlagSaveResult.Ok;
return ContentSaveResult.Ok;
}
}
catch (StorageException e) when (e.IsPreconditionFailedException())
{
return FeatureFlagSaveResult.Conflict;
return ContentSaveResult.Conflict;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using NuGet.Services.Entities;
using NuGet.Services.FeatureFlags;
using NuGetGallery.Shared;

namespace NuGetGallery.Features
{
Expand All @@ -23,7 +24,7 @@ public interface IEditableFeatureFlagStorageService : IFeatureFlagStorageService
/// <param name="flags">The feature flags.</param>
/// <param name="contentId">The feature flag's ETag.</param>
/// <returns>The result of the save operation.</returns>
Task<FeatureFlagSaveResult> TrySaveAsync(FeatureFlags flags, string contentId);
Task<ContentSaveResult> TrySaveAsync(FeatureFlags flags, string contentId);

/// <summary>
/// Remove the user from the feature flags if needed. This may throw on failure.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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.IO;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage;
using Newtonsoft.Json;
using NuGetGallery.Shared;

namespace NuGetGallery.Login
lyndaidaii marked this conversation as resolved.
Show resolved Hide resolved
{
public class EditableLoginConfigurationFileStorageService : LoginDiscontinuationFileStorageService, IEditableLoginConfigurationFileStorageService
{
private const int MaxAttempts = 3;
private readonly ILogger<EditableLoginConfigurationFileStorageService> _logger;

public EditableLoginConfigurationFileStorageService(
ICoreFileStorageService storage,
ILogger<EditableLoginConfigurationFileStorageService> logger) : base(storage)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<LoginDiscontinuationReference> GetReferenceAsync()
{
var reference = await _storage.GetFileReferenceAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.LoginDiscontinuationConfigFileName);

return new LoginDiscontinuationReference(
ReadLoginDiscontinuationFromStream(reference.OpenRead()),
reference.ContentId);
}

public async Task AddUserEmailAddressforPasswordAuthenticationAsync(string emailAddress, bool add)
lyndaidaii marked this conversation as resolved.
Show resolved Hide resolved
{
lyndaidaii marked this conversation as resolved.
Show resolved Hide resolved
for (var attempt = 0; attempt < MaxAttempts; attempt++)
{
var reference = await _storage.GetFileReferenceAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.LoginDiscontinuationConfigFileName);

LoginDiscontinuation logins;
using (var stream = reference.OpenRead())
using (var streamReader = new StreamReader(stream))
using (var reader = new JsonTextReader(streamReader))
{
logins = _serializer.Deserialize<LoginDiscontinuation>(reader);
}

var exceptionsForEmailAddresses = logins.ExceptionsForEmailAddresses;
if (add)
{

if (logins.ExceptionsForEmailAddresses.Contains(emailAddress))
lyndaidaii marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}
exceptionsForEmailAddresses.Add(emailAddress);
}
else
{
if (!logins.ExceptionsForEmailAddresses.Contains(emailAddress))

{
return;
}
exceptionsForEmailAddresses.Remove(emailAddress);
}

var result = new LoginDiscontinuation(
logins.DiscontinuedForEmailAddresses,
logins.DiscontinuedForDomains,
exceptionsForEmailAddresses,
logins.ForceTransformationToOrganizationForEmailAddresses,
logins.EnabledOrganizationAadTenants,
logins.IsPasswordDiscontinuedForAll);

var saveResult = await TrySaveAsync(result, reference.ContentId);
if (saveResult == ContentSaveResult.Ok)
{
return;
}

var operation = add ? "add" : "remove";
_logger.LogWarning(
"Failed to {operation} emailAddress from exception list, attempt {Attempt} of {MaxAttempts}...",
operation,
attempt + 1,
MaxAttempts);
}

throw new InvalidOperationException($"Unable to add/remove emailAddress from exception list after {MaxAttempts} attempts");
}

public async Task<ContentSaveResult> TrySaveAsync(LoginDiscontinuation loginDiscontinuation, string contentId)
{
var result = await TrySaveInternalAsync(loginDiscontinuation, contentId);

return result;
}

public async Task<IReadOnlyList<string>> GetListOfExceptionEmailList()
{
var loginDiscontinuation = await GetAsync();

return loginDiscontinuation.ExceptionsForEmailAddresses.ToList();
}

private async Task<ContentSaveResult> TrySaveInternalAsync(LoginDiscontinuation loginDiscontinuationConfig, string contentId)
{
var accessCondition = AccessConditionWrapper.GenerateIfMatchCondition(contentId);

try
{
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
using (var jsonWriter = new JsonTextWriter(writer))
lyndaidaii marked this conversation as resolved.
Show resolved Hide resolved
{
_serializer.Serialize(jsonWriter, loginDiscontinuationConfig);
jsonWriter.Flush();
stream.Position = 0;

await _storage.SaveFileAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.LoginDiscontinuationConfigFileName, stream, accessCondition);

return ContentSaveResult.Ok;
}
}
catch (StorageException e) when (e.IsPreconditionFailedException())
{
return ContentSaveResult.Conflict;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.Threading.Tasks;
using NuGetGallery.Shared;

namespace NuGetGallery.Login
{
public interface IEditableLoginConfigurationFileStorageService: ILoginDiscontinuationFileStorageService
{

/// <summary>
/// Get a reference to the raw content of <see cref="LoginDiscontinuation"/>.
/// </summary>
/// <returns>A snapshot of the loginDiscontinuation's content and ETag.</returns>
Task<LoginDiscontinuationReference> GetReferenceAsync();

/// <summary>
/// Add or Remove an user email address to the exception email list on <see cref="LoginDiscontinuation"/>.
/// </summary>
/// <param name="emailAddress">The user email address.</param>
/// <param name="add">Indicate remove or add email address.</param>
Task AddUserEmailAddressforPasswordAuthenticationAsync(string emailAddress, bool add);

/// <summary>
/// Get an exception email list on <see cref="LoginDiscontinuation"/>.
/// </summary>
lyndaidaii marked this conversation as resolved.
Show resolved Hide resolved
/// <returns>the exception email list on loginDiscontinuation.</returns>
Task<IReadOnlyList<string>> GetListOfExceptionEmailList();

/// <summary>
/// Try to update the <see cref="LoginDiscontinuation"/>.
/// </summary>
/// <param name="loginDiscontinuation">The log in discontinuation configuration.</param>
/// <param name="contentId">The loginDiscontinuation's ETag.</param>
/// <returns>The result of the save operation.</returns>
Task<ContentSaveResult> TrySaveAsync(LoginDiscontinuation loginDiscontinuation, string contentId);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 System.Text;
using System.Threading.Tasks;

namespace NuGetGallery.Login
{
public interface ILoginDiscontinuationFileStorageService
{
Task<LoginDiscontinuation> GetAsync();
}
}
Loading