diff --git a/src/NuGetGallery/Authentication/ApiScopeEvaluationResult.cs b/src/NuGetGallery/Authentication/ApiScopeEvaluationResult.cs
new file mode 100644
index 0000000000..3c481b1b2d
--- /dev/null
+++ b/src/NuGetGallery/Authentication/ApiScopeEvaluationResult.cs
@@ -0,0 +1,44 @@
+// 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.Authentication
+{
+
+ ///
+ /// The result of evaluating the current user's scopes by using .
+ ///
+ public class ApiScopeEvaluationResult
+ {
+ ///
+ /// True IFF any scope's subject () and allowed action () match the subject being acted upon and the action being performed.
+ ///
+ public bool ScopesAreValid { get; }
+
+ ///
+ /// If is true, the returned by checking the permission of the scope's owner ().
+ /// Otherwise, .
+ ///
+ public PermissionsCheckResult PermissionsCheckResult { get; }
+
+ ///
+ /// The owner of the scope as acquired from .
+ ///
+ public User Owner { get; }
+
+ public ApiScopeEvaluationResult(User owner, PermissionsCheckResult permissionsCheckResult, bool scopesAreValid)
+ {
+ ScopesAreValid = scopesAreValid;
+ PermissionsCheckResult = permissionsCheckResult;
+ Owner = owner;
+ }
+
+ ///
+ /// Returns whether or not this represents a successful authentication.
+ /// If this does not represent a successful authentication, the current user should be denied from performing the action they are attempting to perform.
+ ///
+ public bool IsSuccessful()
+ {
+ return ScopesAreValid && PermissionsCheckResult == PermissionsCheckResult.Allowed;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/Authentication/ApiScopeEvaluator.cs b/src/NuGetGallery/Authentication/ApiScopeEvaluator.cs
new file mode 100644
index 0000000000..4ad2267468
--- /dev/null
+++ b/src/NuGetGallery/Authentication/ApiScopeEvaluator.cs
@@ -0,0 +1,81 @@
+// 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;
+
+namespace NuGetGallery.Authentication
+{
+ public class ApiScopeEvaluator : IApiScopeEvaluator
+ {
+ private readonly IUserService _userService;
+
+ public ApiScopeEvaluator(IUserService userService)
+ {
+ _userService = userService ?? throw new ArgumentNullException(nameof(userService));
+ }
+
+ public ApiScopeEvaluationResult Evaluate(
+ User currentUser,
+ IEnumerable scopes,
+ IActionRequiringEntityPermissions action,
+ PackageRegistration packageRegistration,
+ params string[] requestedActions)
+ {
+ return Evaluate(currentUser, scopes, action, packageRegistration, pr => pr.Id, requestedActions);
+ }
+
+ public ApiScopeEvaluationResult Evaluate(
+ User currentUser,
+ IEnumerable scopes,
+ IActionRequiringEntityPermissions action,
+ ActionOnNewPackageContext context,
+ params string[] requestedActions)
+ {
+ return Evaluate(currentUser, scopes, action, context, c => c.PackageId, requestedActions);
+ }
+
+ /// This method is internal because it is tested directly.
+ internal ApiScopeEvaluationResult Evaluate(
+ User currentUser,
+ IEnumerable scopes,
+ IActionRequiringEntityPermissions action,
+ TEntity entity,
+ Func getSubjectFromEntity,
+ params string[] requestedActions)
+ {
+ User ownerInScope = null;
+
+ if (scopes == null || !scopes.Any())
+ {
+ // Legacy V1 API key without scopes.
+ // Evaluate it as if it has an unlimited scope.
+ scopes = new[] { new Scope(ownerKey: null, subject: NuGetPackagePattern.AllInclusivePattern, allowedAction: NuGetScopes.All) };
+ }
+
+ // Check that all scopes provided have the same owner scope.
+ var ownerScopes = scopes.Select(s => s.OwnerKey);
+ var ownerScope = ownerScopes.FirstOrDefault();
+ if (ownerScopes.Any(o => o != ownerScope))
+ {
+ throw new ArgumentException("All scopes provided must have the same owner scope.");
+ }
+
+ var matchingScope = scopes
+ .FirstOrDefault(scope =>
+ scope.AllowsSubject(getSubjectFromEntity(entity)) &&
+ scope.AllowsActions(requestedActions));
+
+ ownerInScope = ownerScope.HasValue ? _userService.FindByKey(ownerScope.Value) : currentUser;
+
+ if (matchingScope == null)
+ {
+ return new ApiScopeEvaluationResult(ownerInScope, PermissionsCheckResult.Unknown, scopesAreValid: false);
+ }
+
+ var isActionAllowed = action.CheckPermissions(currentUser, ownerInScope, entity);
+ return new ApiScopeEvaluationResult(ownerInScope, isActionAllowed, scopesAreValid: true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/Authentication/IApiScopeEvaluator.cs b/src/NuGetGallery/Authentication/IApiScopeEvaluator.cs
new file mode 100644
index 0000000000..34b753ccb9
--- /dev/null
+++ b/src/NuGetGallery/Authentication/IApiScopeEvaluator.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+
+namespace NuGetGallery.Authentication
+{
+ public interface IApiScopeEvaluator
+ {
+ ///
+ /// Evaluates the whether or not an action is allowed given a set of , an , and the .
+ ///
+ /// The current user attempting to do the action with the given .
+ /// The scopes being evaluated.
+ /// The action that the scopes being evaluated are checked for permission to do.
+ /// The that the scopes being evaluated are checked for permission to on.
+ /// A list of actions that the scopes must match.
+ /// A that describes the evaluation of the s.
+ ApiScopeEvaluationResult Evaluate(
+ User currentUser,
+ IEnumerable scopes,
+ IActionRequiringEntityPermissions action,
+ PackageRegistration packageRegistration,
+ params string[] requestedActions);
+
+ ///
+ /// Evaluates the whether or not an action is allowed given a set of , an , and the .
+ ///
+ /// The current user attempting to do the action with the given .
+ /// The scopes being evaluated.
+ /// The action that the scopes being evaluated are checked for permission to do.
+ /// The that the scopes being evaluated are checked for permission to on.
+ /// A list of actions that the scopes must match.
+ /// A that describes the evaluation of the s.
+ ApiScopeEvaluationResult Evaluate(
+ User currentUser,
+ IEnumerable scopes,
+ IActionRequiringEntityPermissions action,
+ ActionOnNewPackageContext context,
+ params string[] requestedActions);
+ }
+}
diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs
index 69c0ef9746..30dc532e48 100644
--- a/src/NuGetGallery/Controllers/ApiController.cs
+++ b/src/NuGetGallery/Controllers/ApiController.cs
@@ -32,6 +32,7 @@ namespace NuGetGallery
public partial class ApiController
: AppController
{
+ public IApiScopeEvaluator ApiScopeEvaluator { get; set; }
public IEntitiesContext EntitiesContext { get; set; }
public INuGetExeDownloaderService NugetExeDownloaderService { get; set; }
public IPackageFileService PackageFileService { get; set; }
@@ -59,6 +60,7 @@ protected ApiController()
}
public ApiController(
+ IApiScopeEvaluator apiScopeEvaluator,
IEntitiesContext entitiesContext,
IPackageService packageService,
IPackageFileService packageFileService,
@@ -79,6 +81,7 @@ public ApiController(
IReservedNamespaceService reservedNamespaceService,
IPackageUploadService packageUploadService)
{
+ ApiScopeEvaluator = apiScopeEvaluator;
EntitiesContext = entitiesContext;
PackageService = packageService;
PackageFileService = packageFileService;
@@ -102,6 +105,7 @@ public ApiController(
}
public ApiController(
+ IApiScopeEvaluator apiScopeEvaluator,
IEntitiesContext entitiesContext,
IPackageService packageService,
IPackageFileService packageFileService,
@@ -122,9 +126,9 @@ public ApiController(
ISecurityPolicyService securityPolicies,
IReservedNamespaceService reservedNamespaceService,
IPackageUploadService packageUploadService)
- : this(entitiesContext, packageService, packageFileService, userService, nugetExeDownloaderService, contentService,
+ : this(apiScopeEvaluator, entitiesContext, packageService, packageFileService, userService, nugetExeDownloaderService, contentService,
indexingService, searchService, autoCuratePackage, statusService, messageService, auditingService,
- configurationService, telemetryService, authenticationService, credentialBuilder, securityPolicies,
+ configurationService, telemetryService, authenticationService, credentialBuilder, securityPolicies,
reservedNamespaceService, packageUploadService)
{
StatisticsService = statisticsService;
@@ -292,22 +296,21 @@ private async Task VerifyPackageKeyInternalAsync(U
// Write an audit record
await AuditingService.SaveAuditRecordAsync(
new PackageAuditRecord(package, AuditedPackageAction.Verify));
-
+
+ string[] requestedActions;
if (CredentialTypes.IsPackageVerificationApiKey(credential.Type))
{
- // Secure path: verify that verification key matches package scope.
- if (!HasAnyScopeThatAllows(package.PackageRegistration, NuGetScopes.PackageVerify))
- {
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized);
- }
+ requestedActions = new[] { NuGetScopes.PackageVerify };
}
else
{
- // Insecure path: verify that API key is legacy or matches package scope.
- if (!HasAnyScopeThatAllows(package.PackageRegistration, NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion))
- {
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized);
- }
+ requestedActions = new[] { NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion };
+ }
+
+ var apiScopeEvaluationResult = EvaluateApiScope(ActionsRequiringPermissions.VerifyPackage, package.PackageRegistration, requestedActions);
+ if (!apiScopeEvaluationResult.IsSuccessful())
+ {
+ return GetHttpResultFromFailedApiScopeEvaluation(apiScopeEvaluationResult, id, version);
}
return null;
@@ -340,7 +343,7 @@ private async Task CreatePackageInternal()
}
// Get the user
- var user = GetCurrentUser();
+ var currentUser = GetCurrentUser();
using (var packageStream = ReadPackageFromRequest())
{
@@ -400,42 +403,39 @@ private async Task CreatePackageInternal()
nuspec.GetMinClientVersion()));
}
+ User owner;
+
// Ensure that the user can push packages for this partialId.
var id = nuspec.GetId();
+ var version = nuspec.GetVersion();
var packageRegistration = PackageService.FindPackageRegistrationById(id);
if (packageRegistration == null)
{
- // Check if API key allows pushing a new package id
- if (!HasAnyScopeThatAllowsPushNew(id))
+ // Check if the current user's scopes allow pushing a new package ID
+ var apiScopeEvaluationResult = EvaluateApiScope(ActionsRequiringPermissions.UploadNewPackageId, new ActionOnNewPackageContext(id, ReservedNamespaceService), NuGetScopes.PackagePush);
+ owner = apiScopeEvaluationResult.Owner;
+ if (!apiScopeEvaluationResult.IsSuccessful())
{
- // User cannot push a new package ID as the API key scope does not allow it
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Unauthorized, Strings.ApiKeyNotAuthorized);
- }
-
- // For a new package id verify that the user is allowed to push to the matching namespaces, if any.
- if (ActionsRequiringPermissions.UploadNewPackageId.CheckPermissionsOnBehalfOfAnyAccount(
- user, new ActionOnNewPackageContext(id, ReservedNamespaceService)) != PermissionsCheckResult.Allowed)
- {
- var version = nuspec.GetVersion().ToNormalizedString();
- TelemetryService.TrackPackagePushNamespaceConflictEvent(id, version, user, User.Identity);
-
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.UploadPackage_IdNamespaceConflict);
+ // User cannot push a new package ID as the current user's scopes does not allow it
+ return GetHttpResultFromFailedApiScopeEvaluationForPush(apiScopeEvaluationResult, id, version);
}
}
else
{
- // Check if API key allows pushing the current package id
- if (!HasAnyScopeThatAllows(packageRegistration, NuGetScopes.PackagePushVersion, NuGetScopes.PackagePush))
+ // Check if the current user's scopes allow pushing a new version of an existing package ID
+ var apiScopeEvaluationResult = EvaluateApiScope(ActionsRequiringPermissions.UploadNewPackageVersion, packageRegistration, NuGetScopes.PackagePushVersion, NuGetScopes.PackagePush);
+ owner = apiScopeEvaluationResult.Owner;
+ if (!apiScopeEvaluationResult.IsSuccessful())
{
+ // User cannot push a package as the current user's scopes does not allow it
await AuditingService.SaveAuditRecordAsync(
new FailedAuthenticatedOperationAuditRecord(
- user.Username,
+ currentUser.Username,
AuditedAuthenticatedOperationAction.PackagePushAttemptByNonOwner,
attemptedPackage: new AuditedPackageIdentifier(
- id, nuspec.GetVersion().ToNormalizedStringSafe())));
+ id, version.ToNormalizedStringSafe())));
- // User cannot push a package as the API key scope does not allow it
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Unauthorized, Strings.ApiKeyNotAuthorized);
+ return GetHttpResultFromFailedApiScopeEvaluationForPush(apiScopeEvaluationResult, id, version);
}
if (packageRegistration.IsLocked)
@@ -446,7 +446,7 @@ await AuditingService.SaveAuditRecordAsync(
}
// Check if a particular Id-Version combination already exists. We eventually need to remove this check.
- string normalizedVersion = nuspec.GetVersion().ToNormalizedString();
+ string normalizedVersion = version.ToNormalizedString();
bool packageExists =
packageRegistration.Packages.Any(
p => string.Equals(
@@ -474,8 +474,8 @@ await AuditingService.SaveAuditRecordAsync(
id,
packageToPush,
packageStreamMetadata,
- owner: user,
- currentUser: user);
+ owner,
+ currentUser);
await AutoCuratePackage.ExecuteAsync(package, packageToPush, commitChanges: false);
@@ -487,7 +487,7 @@ await AuditingService.SaveAuditRecordAsync(
package,
uploadStream.AsSeekableStream());
}
-
+
switch (commitResult)
{
case PackageCommitResult.Success:
@@ -515,7 +515,7 @@ await AuditingService.SaveAuditRecordAsync(
Url.AccountSettings(relativeUrl: false));
}
- TelemetryService.TrackPackagePushEvent(package, user, User.Identity);
+ TelemetryService.TrackPackagePushEvent(package, currentUser, User.Identity);
if (package.SemVerLevelKey == SemVerLevelKey.SemVer2)
{
@@ -564,11 +564,11 @@ public virtual async Task DeletePackage(string id, string version)
HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
}
- // Check if API key allows listing/unlisting the current package id
- var user = GetCurrentUser();
- if (!HasAnyScopeThatAllows(package.PackageRegistration, NuGetScopes.PackageUnlist))
+ // Check if the current user's scopes allow listing/unlisting the current package ID
+ var apiScopeEvaluationResult = EvaluateApiScope(ActionsRequiringPermissions.UnlistOrRelistPackage, package.PackageRegistration, NuGetScopes.PackageUnlist);
+ if (!apiScopeEvaluationResult.IsSuccessful())
{
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized);
+ return GetHttpResultFromFailedApiScopeEvaluation(apiScopeEvaluationResult, id, version);
}
if (package.PackageRegistration.IsLocked)
@@ -596,11 +596,11 @@ public virtual async Task PublishPackage(string id, string version
HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
}
- // Check if API key allows listing/unlisting the current package id
- User user = GetCurrentUser();
- if (!HasAnyScopeThatAllows(package.PackageRegistration, NuGetScopes.PackageUnlist))
+ // Check if the current user's scopes allow listing/unlisting the current package ID
+ var apiScopeEvaluationResult = EvaluateApiScope(ActionsRequiringPermissions.UnlistOrRelistPackage, package.PackageRegistration, NuGetScopes.PackageUnlist);
+ if (!apiScopeEvaluationResult.IsSuccessful())
{
- return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized);
+ return GetHttpResultFromFailedApiScopeEvaluation(apiScopeEvaluationResult, id, version);
}
if (package.PackageRegistration.IsLocked)
@@ -729,42 +729,60 @@ public virtual async Task GetStatsDownloads(int? count)
return new HttpStatusCodeResult(HttpStatusCode.NotFound);
}
- private bool HasAnyScopeThatAllows(PackageRegistration package, params string[] requestedActions)
+ private HttpStatusCodeWithBodyResult GetHttpResultFromFailedApiScopeEvaluation(ApiScopeEvaluationResult evaluationResult, string id, string version)
{
- var scopes = User.Identity.GetScopesFromClaim();
- if (scopes != null)
- {
- if (!scopes.Any(s => s.AllowsSubject(package.Id) && s.AllowsActions(requestedActions)))
- {
- // Subject (package id) or action scopes do not match.
- return false;
- }
-
- if (scopes.Any(s => s.HasOwnerScope()))
- {
- // ApiKeyHandler has already verified that the current user matches the owner scope.
- // Do not need to check organization role (IsAdmin) which is covered by the action scope.
- return GetCurrentUser().IsOwnerOrMemberOfOrganizationOwner(package);
- }
- }
+ return GetHttpResultFromFailedApiScopeEvaluation(evaluationResult, id, NuGetVersion.Parse(version));
+ }
- // Legacy V1 API key (no scopes), or Legacy V2 API key (no owner scope).
- // Must verify that the current user is the package owner or admin for an organization owner.
- return GetCurrentUser().IsOwnerOrMemberOfOrganizationOwner(package);
+ private HttpStatusCodeWithBodyResult GetHttpResultFromFailedApiScopeEvaluation(ApiScopeEvaluationResult result, string id, NuGetVersion version)
+ {
+ return GetHttpResultFromFailedApiScopeEvaluationHelper(result, id, version, HttpStatusCode.Forbidden);
+ }
+
+ ///
+ /// Push returns instead of for failures not related to reserved namespaces.
+ /// This is inconsistent with both the rest of our API and the HTTP standard, but it is an existing behavior that we must support.
+ ///
+ private HttpStatusCodeWithBodyResult GetHttpResultFromFailedApiScopeEvaluationForPush(ApiScopeEvaluationResult result, string id, NuGetVersion version)
+ {
+ return GetHttpResultFromFailedApiScopeEvaluationHelper(result, id, version, HttpStatusCode.Unauthorized);
}
- private bool HasAnyScopeThatAllowsPushNew(string packageId)
+ private HttpStatusCodeWithBodyResult GetHttpResultFromFailedApiScopeEvaluationHelper(ApiScopeEvaluationResult result, string id, NuGetVersion version, HttpStatusCode statusCodeOnFailure)
{
- // Package owners not populated yet, so only verify the scope subject and action.
- var scopes = User.Identity.GetScopesFromClaim();
- if (scopes != null)
+ if (result.IsSuccessful())
{
- // Return true if scope allows action for subject (package).
- return scopes.Any(s => s.AllowsActions(NuGetScopes.PackagePush) && s.AllowsSubject(packageId));
+ throw new ArgumentException($"{nameof(result)} is not a failed evaluation!", nameof(result));
}
- // Legacy V1 API key (no scopes).
- return true;
+ if (result.PermissionsCheckResult == PermissionsCheckResult.ReservedNamespaceFailure)
+ {
+ // We return a special error code for reserved namespace failures.
+ TelemetryService.TrackPackagePushNamespaceConflictEvent(id, version.ToNormalizedString(), GetCurrentUser(), User.Identity);
+ return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.UploadPackage_IdNamespaceConflict);
+ }
+
+ return new HttpStatusCodeWithBodyResult(statusCodeOnFailure, Strings.ApiKeyNotAuthorized);
+ }
+
+ private ApiScopeEvaluationResult EvaluateApiScope(IActionRequiringEntityPermissions action, PackageRegistration packageRegistration, params string[] requestedActions)
+ {
+ return ApiScopeEvaluator.Evaluate(
+ GetCurrentUser(),
+ User.Identity.GetScopesFromClaim(),
+ action,
+ packageRegistration,
+ requestedActions);
+ }
+
+ private ApiScopeEvaluationResult EvaluateApiScope(IActionRequiringEntityPermissions action, ActionOnNewPackageContext context, params string[] requestedActions)
+ {
+ return ApiScopeEvaluator.Evaluate(
+ GetCurrentUser(),
+ User.Identity.GetScopesFromClaim(),
+ action,
+ context,
+ requestedActions);
}
}
}
diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj
index 3eb23a8ec4..8c93995f5d 100644
--- a/src/NuGetGallery/NuGetGallery.csproj
+++ b/src/NuGetGallery/NuGetGallery.csproj
@@ -725,8 +725,11 @@
+
+
+
diff --git a/src/NuGetGallery/Services/ActionsRequiringPermissions.cs b/src/NuGetGallery/Services/ActionsRequiringPermissions.cs
index 081484cc1c..3aa0e72814 100644
--- a/src/NuGetGallery/Services/ActionsRequiringPermissions.cs
+++ b/src/NuGetGallery/Services/ActionsRequiringPermissions.cs
@@ -40,6 +40,14 @@ public static class ActionsRequiringPermissions
accountOnBehalfOfPermissionsRequirement: RequireOwnerOrOrganizationMember,
packageRegistrationPermissionsRequirement: PermissionsRequirement.Owner);
+ ///
+ /// The action of verify a package verification key.
+ ///
+ public static ActionRequiringPackagePermissions VerifyPackage =
+ new ActionRequiringPackagePermissions(
+ accountOnBehalfOfPermissionsRequirement: RequireOwnerOrOrganizationMember,
+ packageRegistrationPermissionsRequirement: PermissionsRequirement.Owner);
+
///
/// The action of editing an existing version of an existing package ID.
///
diff --git a/src/NuGetGallery/Services/IUserService.cs b/src/NuGetGallery/Services/IUserService.cs
index 07dad25772..1a4a0164a5 100644
--- a/src/NuGetGallery/Services/IUserService.cs
+++ b/src/NuGetGallery/Services/IUserService.cs
@@ -18,6 +18,8 @@ public interface IUserService
User FindByUsername(string username);
+ User FindByKey(int key);
+
Task ConfirmEmailAddress(User user, string token);
Task ChangeEmailAddress(User user, string newEmailAddress);
diff --git a/src/NuGetGallery/Services/PermissionsCheckResult.cs b/src/NuGetGallery/Services/PermissionsCheckResult.cs
index 891ae51376..1f63beef6e 100644
--- a/src/NuGetGallery/Services/PermissionsCheckResult.cs
+++ b/src/NuGetGallery/Services/PermissionsCheckResult.cs
@@ -24,12 +24,12 @@ public enum PermissionsCheckResult
AccountFailure,
///
- /// The current user does not have permissions to perform the action on the on behalf of another .
+ /// The current user does not have permissions to perform the action on the on behalf of another .
///
PackageRegistrationFailure,
///
- /// The current user does not have permissions to perform the action on the on behalf of another .
+ /// The current user does not have permissions to perform the action on the on behalf of another .
///
ReservedNamespaceFailure
}
diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs
index 9f470d169d..baec9a5569 100644
--- a/src/NuGetGallery/Services/UserService.cs
+++ b/src/NuGetGallery/Services/UserService.cs
@@ -94,6 +94,14 @@ public virtual User FindByUsername(string username)
.SingleOrDefault(u => u.Username == username);
}
+ public virtual User FindByKey(int key)
+ {
+ return UserRepository.GetAll()
+ .Include(u => u.Roles)
+ .Include(u => u.Credentials)
+ .SingleOrDefault(u => u.Key == key);
+ }
+
public async Task ChangeEmailAddress(User user, string newEmailAddress)
{
var existingUsers = FindAllByEmailAddress(newEmailAddress);
@@ -213,4 +221,4 @@ public async Task TransformUserToOrganization(User accountToTransform, Use
return await EntitiesContext.TransformUserToOrganization(accountToTransform, adminUser, token);
}
}
-}
+}
\ No newline at end of file
diff --git a/tests/NuGetGallery.Facts/Authentication/ApiScopeEvaluationResultFacts.cs b/tests/NuGetGallery.Facts/Authentication/ApiScopeEvaluationResultFacts.cs
new file mode 100644
index 0000000000..107f3b624e
--- /dev/null
+++ b/tests/NuGetGallery.Facts/Authentication/ApiScopeEvaluationResultFacts.cs
@@ -0,0 +1,47 @@
+// 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 NuGetGallery.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace NuGetGallery.Authentication
+{
+ public class ApiScopeEvaluationResultFacts
+ {
+ public class TheIsSuccessfulMethod
+ {
+ public static IEnumerable