Skip to content

Commit

Permalink
Restore auto-add co-owner feature (#6289)
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierdecoster committed Aug 14, 2018
1 parent a9379ee commit ac9e62d
Show file tree
Hide file tree
Showing 45 changed files with 1,633 additions and 229 deletions.
42 changes: 40 additions & 2 deletions src/NuGetGallery.Core/Services/CoreMessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,24 @@ public CoreMessageService(IMailSender mailSender, ICoreMessageServiceConfigurati
public IMailSender MailSender { get; protected set; }
public ICoreMessageServiceConfiguration CoreConfiguration { get; protected set; }

public void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl)
public void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl, IEnumerable<string> warningMessages = null)
{
string subject = $"[{CoreConfiguration.GalleryOwner.DisplayName}] Package published - {package.PackageRegistration.Id} {package.Version}";
bool hasWarnings = warningMessages != null && warningMessages.Any();

string subject;
var warningMessagesPlaceholder = string.Empty;
if (hasWarnings)
{
subject = $"[{CoreConfiguration.GalleryOwner.DisplayName}] Package published with warnings - {package.PackageRegistration.Id} {package.Version}";
warningMessagesPlaceholder = Environment.NewLine + string.Join(Environment.NewLine, warningMessages);
}
else
{
subject = $"[{CoreConfiguration.GalleryOwner.DisplayName}] Package published - {package.PackageRegistration.Id} {package.Version}";
}

string body = $@"The package [{package.PackageRegistration.Id} {package.Version}]({packageUrl}) was recently published on {CoreConfiguration.GalleryOwner.DisplayName} by {package.User.Username}. If this was not intended, please [contact support]({packageSupportUrl}).
{warningMessagesPlaceholder}
-----------------------------------------------
<em style=""font-size: 0.8em;"">
Expand All @@ -54,6 +68,30 @@ [change your email notification settings]({emailSettingsUrl}).
}
}

public void SendPackageAddedWithWarningsNotice(Package package, string packageUrl, string packageSupportUrl, IEnumerable<string> warningMessages)
{
var subject = $"[{CoreConfiguration.GalleryOwner.DisplayName}] Package pushed with warnings - {package.PackageRegistration.Id} {package.Version}";
var warningMessagesPlaceholder = Environment.NewLine + string.Join(Environment.NewLine, warningMessages);

string body = $@"The package [{package.PackageRegistration.Id} {package.Version}]({packageUrl}) was recently pushed to {CoreConfiguration.GalleryOwner.DisplayName} by {package.User.Username}. If this was not intended, please [contact support]({packageSupportUrl}).
{warningMessagesPlaceholder}
";

using (var mailMessage = new MailMessage())
{
mailMessage.Subject = subject;
mailMessage.Body = body;
mailMessage.From = CoreConfiguration.GalleryNoReplyAddress;

AddOwnersSubscribedToPackagePushedNotification(package.PackageRegistration, mailMessage);

if (mailMessage.To.Any())
{
SendMessage(mailMessage);
}
}
}

public void SendPackageValidationFailedNotice(Package package, PackageValidationSet validationSet, string packageUrl, string packageSupportUrl, string announcementsUrl, string twitterUrl)
{
var validationIssues = validationSet.GetValidationIssues();
Expand Down
4 changes: 3 additions & 1 deletion src/NuGetGallery.Core/Services/ICoreMessageService.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// 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.Collections.Generic;
using NuGet.Services.Validation;

namespace NuGetGallery.Services
{
public interface ICoreMessageService
{
void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl);
void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl, IEnumerable<string> warningMessages = null);
void SendPackageAddedWithWarningsNotice(Package package, string packageUrl, string packageSupportUrl, IEnumerable<string> warningMessages);
void SendPackageValidationFailedNotice(Package package, PackageValidationSet validationSet, string packageUrl, string packageSupportUrl, string announcementsUrl, string twitterUrl);
void SendValidationTakingTooLongNotice(Package package, string packageUrl);
}
Expand Down
4 changes: 4 additions & 0 deletions src/NuGetGallery/App_Start/DefaultDependenciesModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,10 @@ protected override void Load(ContainerBuilder builder)

RegisterCookieComplianceService(builder, configuration, diagnosticsService);

builder.RegisterType<MicrosoftTeamSubscription>()
.AsSelf()
.InstancePerLifetimeScope();

// todo: bind all package curators by convention
builder.RegisterType<WebMatrixPackageCurator>()
.AsSelf()
Expand Down
38 changes: 31 additions & 7 deletions src/NuGetGallery/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,13 @@ public async virtual Task<ActionResult> CreatePackageVerificationKeyAsync(string
[ActionName("VerifyPackageKey")]
public async virtual Task<ActionResult> VerifyPackageKeyAsync(string id, string version)
{
var policyResult = await SecurityPolicyService.EvaluateUserPoliciesAsync(SecurityPolicyAction.PackageVerify, HttpContext);
var user = GetCurrentUser();
var policyResult = await SecurityPolicyService.EvaluateUserPoliciesAsync(SecurityPolicyAction.PackageVerify, user, HttpContext);
if (!policyResult.Success)
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, policyResult.ErrorMessage);
}

var user = GetCurrentUser();
var credential = user.GetCurrentApiKeyCredential(User.Identity);

var result = await VerifyPackageKeyInternalAsync(user, credential, id, version);
Expand Down Expand Up @@ -506,15 +506,17 @@ private async Task<ActionResult> CreatePackageInternal()

try
{
var policyResult = await SecurityPolicyService.EvaluateUserPoliciesAsync(SecurityPolicyAction.PackagePush, HttpContext);
var securityPolicyAction = SecurityPolicyAction.PackagePush;

// Get the user
var currentUser = GetCurrentUser();

var policyResult = await SecurityPolicyService.EvaluateUserPoliciesAsync(securityPolicyAction, currentUser, HttpContext);
if (!policyResult.Success)
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, policyResult.ErrorMessage);
}

// Get the user
var currentUser = GetCurrentUser();

using (var packageStream = ReadPackageFromRequest())
{
try
Expand Down Expand Up @@ -654,6 +656,18 @@ await PackageDeleteService.HardDeletePackagesAsync(
packageStreamMetadata,
owner,
currentUser);

var packagePolicyResult = await SecurityPolicyService.EvaluatePackagePoliciesAsync(
securityPolicyAction,
package,
currentUser,
owner,
HttpContext);

if (!packagePolicyResult.Success)
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, packagePolicyResult.ErrorMessage);
}

// Perform validations that require the package already being in the entity context.
var afterValidationResult = await PackageUploadService.ValidateAfterGeneratePackageAsync(
Expand Down Expand Up @@ -702,7 +716,17 @@ await AuditingService.SaveAuditRecordAsync(
MessageService.SendPackageAddedNotice(package,
Url.Package(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false),
Url.ReportPackage(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false),
Url.AccountSettings(relativeUrl: false));
Url.AccountSettings(relativeUrl: false),
packagePolicyResult.WarningMessages);
}
// Emit warning messages if any
else if (packagePolicyResult.HasWarnings)
{
// Notify user of push unless async validation in blocking mode is used
MessageService.SendPackageAddedWithWarningsNotice(package,
Url.Package(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false),
Url.ReportPackage(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false),
packagePolicyResult.WarningMessages);
}

TelemetryService.TrackPackagePushEvent(package, currentUser, User.Identity);
Expand Down
12 changes: 12 additions & 0 deletions src/NuGetGallery/Controllers/PackagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,18 @@ public virtual async Task<JsonResult> VerifyPackage(VerifyPackageRequest formDat

return Json(HttpStatusCode.BadRequest, new[] { ex.Message });
}

var packagePolicyResult = await _securityPolicyService.EvaluatePackagePoliciesAsync(
SecurityPolicyAction.PackagePush,
package,
currentUser,
owner,
HttpContext);

if (!packagePolicyResult.Success)
{
return Json(HttpStatusCode.BadRequest, new[] { packagePolicyResult.ErrorMessage });
}

// Perform validations that require the package already being in the entity context.
var afterValidationResult = await _packageUploadService.ValidateAfterGeneratePackageAsync(
Expand Down
5 changes: 5 additions & 0 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,13 @@
</Compile>
<Compile Include="Security\AutomaticOverwriteRequiredSignerPolicy.cs" />
<Compile Include="Security\ControlRequiredSignerPolicy.cs" />
<Compile Include="Security\MicrosoftTeamSubscription.cs" />
<Compile Include="Security\PackageSecurityPolicyEvaluationContext.cs" />
<Compile Include="Security\PackageSecurityPolicyHandler.cs" />
<Compile Include="Security\RequiredSignerPolicy.cs" />
<Compile Include="Security\RequirePackageMetadataCompliancePolicy.cs" />
<Compile Include="Security\RequireOrganizationTenantPolicy.cs" />
<Compile Include="Security\SecurityPolicyHandler.cs" />
<Compile Include="Services\AccountDeletionOrphanPackagePolicy.cs" />
<Compile Include="Services\ActionOnNewPackageContext.cs" />
<Compile Include="Services\ActionRequiringAccountPermissions.cs" />
Expand Down
16 changes: 15 additions & 1 deletion src/NuGetGallery/Security/ISecurityPolicyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ public interface ISecurityPolicyService
/// Evaluate any security policies that may apply to the current context.
/// </summary>
/// <param name="action">Security policy action.</param>
/// <param name="user">The user for which to evaluate security policies.</param>
/// <param name="httpContext">Http context.</param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="SecurityPolicyResult" />
/// instance.</returns>
Task<SecurityPolicyResult> EvaluateUserPoliciesAsync(SecurityPolicyAction action, HttpContextBase httpContext);
Task<SecurityPolicyResult> EvaluateUserPoliciesAsync(SecurityPolicyAction action, User user, HttpContextBase httpContext);

/// <summary>
/// Evaluate any organization security policies for the specified account.
Expand All @@ -72,5 +73,18 @@ public interface ISecurityPolicyService
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="SecurityPolicyResult" />
/// instance.</returns>
Task<SecurityPolicyResult> EvaluateOrganizationPoliciesAsync(SecurityPolicyAction action, Organization organization, User account);

/// <summary>
/// Evaluate any package security policies that may apply to the current context.
/// </summary>
/// <param name="action">Security policy action.</param>
/// <param name="package">The package to evaluate.</param>
/// <param name="currentUser">The current user.</param>
/// <param name="owner">The package owner.</param>
/// <param name="httpContext">Http context.</param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="SecurityPolicyResult"/>
/// instance.</returns>
Task<SecurityPolicyResult> EvaluatePackagePoliciesAsync(SecurityPolicyAction action, Package package, User currentUser, User owner, HttpContextBase httpContext);
}
}
56 changes: 56 additions & 0 deletions src/NuGetGallery/Security/MicrosoftTeamSubscription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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;

namespace NuGetGallery.Security
{
public class MicrosoftTeamSubscription : IUserSecurityPolicySubscription
{
private Lazy<List<UserSecurityPolicy>> _policies = new Lazy<List<UserSecurityPolicy>>(InitializePoliciesList, isThreadSafe: true);

internal const string MicrosoftUsername = "Microsoft";
internal const string Name = "MicrosoftTeamSubscription";

public string SubscriptionName => Name;

public MicrosoftTeamSubscription()
{
}

public IEnumerable<UserSecurityPolicy> Policies => _policies.Value;

public Task OnSubscribeAsync(UserSecurityPolicySubscriptionContext context)
{
// Todo:
// Maybe we should enumerate through the user's packages and add Microsoft as a package owner if the package passes the metadata requirements when a user is onboarded to this policy.
// We should also unlock the package if it is locked as part of adding Microsoft as co-owner.
return Task.CompletedTask;
}

public Task OnUnsubscribeAsync(UserSecurityPolicySubscriptionContext context)
{
return Task.CompletedTask;
}

private static List<UserSecurityPolicy> InitializePoliciesList()
{
return new List<UserSecurityPolicy>()
{
RequirePackageMetadataCompliancePolicy.CreatePolicy(
Name,
MicrosoftUsername,
allowedCopyrightNotices: new string[]
{
"(c) Microsoft Corporation. All rights reserved.",
"© Microsoft Corporation. All rights reserved."
},
isLicenseUrlRequired: true,
isProjectUrlRequired: true,
errorMessageFormat: Strings.SecurityPolicy_RequireMicrosoftPackageMetadataComplianceForPush)
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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.Web;

namespace NuGetGallery.Security
{
public class PackageSecurityPolicyEvaluationContext : UserSecurityPolicyEvaluationContext
{
public PackageSecurityPolicyEvaluationContext(
IUserService userService,
IPackageOwnershipManagementService packageOwnershipManagementService,
IEnumerable<UserSecurityPolicy> policies,
Package package,
HttpContextBase httpContext)
: base(policies, httpContext)
{
Package = package ?? throw new ArgumentNullException(nameof(package));
UserService = userService ?? throw new ArgumentNullException(nameof(userService));
PackageOwnershipManagementService = packageOwnershipManagementService ?? throw new ArgumentNullException(nameof(packageOwnershipManagementService));
}

public PackageSecurityPolicyEvaluationContext(
IUserService userService,
IPackageOwnershipManagementService packageOwnershipManagementService,
IEnumerable<UserSecurityPolicy> policies,
Package package,
User sourceAccount,
User targetAccount,
HttpContextBase httpContext = null)
: base(policies, sourceAccount, targetAccount, httpContext)
{
Package = package ?? throw new ArgumentNullException(nameof(package));
UserService = userService ?? throw new ArgumentNullException(nameof(userService));
PackageOwnershipManagementService = packageOwnershipManagementService ?? throw new ArgumentNullException(nameof(packageOwnershipManagementService));
}

/// <summary>
/// Package under evaluation.
/// </summary>
public Package Package { get; }

public IUserService UserService { get; }

public IPackageOwnershipManagementService PackageOwnershipManagementService { get; }
}
}
16 changes: 16 additions & 0 deletions src/NuGetGallery/Security/PackageSecurityPolicyHandler.cs
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.

namespace NuGetGallery.Security
{
/// <summary>
/// Policy handler that defines behavior for specific user policy types requiring package policy evaluation.
/// </summary>
public abstract class PackageSecurityPolicyHandler : SecurityPolicyHandler<PackageSecurityPolicyEvaluationContext>
{
public PackageSecurityPolicyHandler(string name, SecurityPolicyAction action)
: base(name, action)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NuGet.Protocol;
using NuGet.Versioning;
Expand Down Expand Up @@ -76,7 +77,7 @@ private NuGetVersion GetProtocolVersion(UserSecurityPolicyEvaluationContext cont
/// <summary>
/// Evaluate if this security policy is met.
/// </summary>
public override SecurityPolicyResult Evaluate(UserSecurityPolicyEvaluationContext context)
public override Task<SecurityPolicyResult> EvaluateAsync(UserSecurityPolicyEvaluationContext context)
{
if (context == null)
{
Expand All @@ -96,11 +97,11 @@ public override SecurityPolicyResult Evaluate(UserSecurityPolicyEvaluationContex

if (protocolVersion == null || protocolVersion < minProtocolVersion)
{
return SecurityPolicyResult.CreateErrorResult(string.Format(CultureInfo.CurrentCulture,
Strings.SecurityPolicy_RequireMinProtocolVersionForPush, minProtocolVersion));
return Task.FromResult(SecurityPolicyResult.CreateErrorResult(string.Format(CultureInfo.CurrentCulture,
Strings.SecurityPolicy_RequireMinProtocolVersionForPush, minProtocolVersion)));
}

return SecurityPolicyResult.SuccessResult;
return Task.FromResult(SecurityPolicyResult.SuccessResult);
}
}
}
Loading

0 comments on commit ac9e62d

Please sign in to comment.