diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj index 73f4f779fef..36aa5db71be 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj @@ -75,16 +75,11 @@ netcoreapp3.1 Publish - - 2 + + 3 - - - - - diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj index ce7e413169a..2f4d18b3c10 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj @@ -11,12 +11,6 @@ - AzdoTargetFeedPAT : Required token to publish assets to feeds --> - - netcoreapp3.1 - Publish - dnceng - - + + netcoreapp3.1 + Publish + dnceng + internal + public + + + + + AzureDevOpsOrg="$(AzureDevOpsOrg)" + AzureDevOpsProject="$(AzureDevOpsProject)"> + AzureDevOpsOrg="$(AzureDevOpsOrg)" + AzureDevOpsProject="$(AzureDevOpsProject)"> diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/SetupTargetFeeds.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/SetupTargetFeeds.proj deleted file mode 100644 index 25aefb367f7..00000000000 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/SetupTargetFeeds.proj +++ /dev/null @@ -1,333 +0,0 @@ - - - - - Publish - - - - - - - - - - - - - - - - - - - - https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json - https://dotnetfeed.blob.core.windows.net/arcade-validation/index.json - https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore/index.json - https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json - https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json - https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-coreclr/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-sdk/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-tools-internal/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-toolset/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-windowsdesktop/index.json - https://dotnetfeed.blob.core.windows.net/nuget-nugetclient/index.json - https://dotnetfeed.blob.core.windows.net/aspnet-entityframework6/index.json - https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-experimental/index.json - https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsArtifactFeed.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsArtifactFeed.cs new file mode 100644 index 00000000000..200b8adcbe3 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsArtifactFeed.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.DotNet.Build.Tasks.Feed +{ + /// + /// Represents the body of a request sent when creating a new feed. + /// + /// > + /// When creating a new feed, we want to set up permissions based on the org and project. + /// Right now, only dnceng's public and internal projects are supported. + /// New feeds automatically get the feed administrators and project collection administrators as owners, + /// but we want to automatically add some additional permissions so that the build services can push to them, + /// and organization users can read from them. + /// + /// Note that there are two ways of providing read access to the feed: + /// 1. Providing explicit access in the permissions list to the "Project Collection Valid Users" + /// 2. Updating the Local feed view to allow 'collection' users access. + /// + /// The second is probably preferrable from a an AzDO pattern and usage standpoint. BUT the AzDO API has a drawback where + /// the create feed operation cannot create the local view with the appropriate access. Instead, it must be updated after the + /// feed is created. This would be fine except that updating a feed's permissions requires administrative permissions, while + /// creating a feed only requires contributor permissions. This would require passing around a PAT with management permissions, + /// instead of just r/w permissions, which is not ideal. + /// + public class AzureDevOpsArtifactFeed + { + public AzureDevOpsArtifactFeed(string name, string organization, string project) + { + Name = name; + switch (organization) + { + case "dnceng": + switch (project) + { + case "public": + case "internal": + Permissions = new List + { + // Project Collection Build Service + new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:7ea9116e-9fac-403d-b258-b31fcf1bb293", "contributor"), + // internal Build Service + new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:b55de4ed-4b5a-4215-a8e4-0a0a5f71e7d8", "contributor"), + // Project administrators + new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1349140002-2196814402-2899064621-3782482097-0-0-0-0-1", "administrator"), + // Project Collection value users (see class comment for info) + new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-3991166389-1514870082-2833517066-1601300440-0-0-0-0-3", "reader"), + }; + break; + default: + throw new NotImplementedException($"Project '{project}' within organization '{organization}' contains no feed permissions information."); + } + break; + default: + throw new NotImplementedException($"Organization '{organization}' contains no feed permissions information."); + + } + } + + public string Name { get; set; } + + public List Permissions { get; private set; } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsFeedPermission.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsFeedPermission.cs new file mode 100644 index 00000000000..f081ee5eb8e --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsFeedPermission.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Build.Tasks.Feed +{ + public class AzureDevOpsFeedPermission + { + public AzureDevOpsFeedPermission(string identityDescriptor, string role) + { + IdentityDescriptor = identityDescriptor; + Role = role; + } + + public string IdentityDescriptor { get; set; } + + public string Role { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs index 4b6cfd71bfa..49032c55350 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs @@ -23,8 +23,24 @@ public class CreateAzureDevOpsFeed : MSBuild.Task [Output] public string TargetFeedName { get; set; } + /// + /// Organization that the feed should be created in + /// + [Required] + public string AzureDevOpsOrg { get; set; } + + /// + /// Project that that feed should be created in. The public/internal visibility + /// of this project will determine whether the feed is public. + /// + [Required] + public string AzureDevOpsProject { get; set; } + + /// + /// Personal access token used to authorize to the API and create the feed + /// [Required] - public bool IsInternal { get; set; } + public string AzureDevOpsPersonalAccessToken { get; set; } public string RepositoryName { get; set; } @@ -40,12 +56,9 @@ public class CreateAzureDevOpsFeed : MSBuild.Task /// public string ContentIdentifier { get; set; } - [Required] - public string AzureDevOpsPersonalAccessToken { get; set; } - - public string AzureDevOpsFeedsApiVersion { get; set; } = "5.0-preview.1"; + public string AzureDevOpsFeedsApiVersion { get; set; } = "5.1-preview.1"; - public string AzureDevOpsOrg { get; set; } = "dnceng"; + public string LocalViewVisibility { get; set; } = "collection"; /// /// Number of characters from the commit SHA prefix that should be included in the feed name. @@ -86,16 +99,19 @@ private async Task ExecuteAsync() // or contain any of these: @ ~ ; { } ' + = , < > | / \ ? : & $ * " # [ ] % string feedCompatibleRepositoryName = RepositoryName?.Replace('/', '-'); - string accessType = IsInternal ? "internal" : "public"; - string publicSegment = IsInternal ? string.Empty : "public/"; - string accessId = IsInternal ? "int" : "pub"; + // For clarity, and compatibility with existing infrastructure, we include the feed visibility tag. + // This serves two purposes: + // 1. In nuget.config files (and elsewhere), the name at a glance can identify its visibility + // 2. Existing automation has knowledge of "darc-int" and "darc-pub" for purposes of injecting authentication for internal builds + // and managing the isolated feeds within the NuGet.config files. + string accessTag = GetFeedVisibilityTag(AzureDevOpsOrg, AzureDevOpsProject); string extraContentInfo = !string.IsNullOrEmpty(ContentIdentifier) ? $"-{ContentIdentifier}" : ""; - string baseFeedName = FeedName ?? $"darc-{accessId}{extraContentInfo}-{feedCompatibleRepositoryName}-{CommitSha.Substring(0, ShaUsableLength)}"; + string baseFeedName = FeedName ?? $"darc-{accessTag}{extraContentInfo}-{feedCompatibleRepositoryName}-{CommitSha.Substring(0, ShaUsableLength)}"; string versionedFeedName = baseFeedName; bool needsUniqueName = false; int subVersion = 0; - Log.LogMessage(MessageImportance.High, $"Creating the new {accessType} Azure DevOps artifacts feed '{baseFeedName}'..."); + Log.LogMessage(MessageImportance.High, $"Creating the new Azure DevOps artifacts feed '{baseFeedName}'..."); if (baseFeedName.Length > MaxLengthForAzDoFeedNames) { @@ -118,20 +134,24 @@ private async Task ExecuteAsync() "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", AzureDevOpsPersonalAccessToken)))); - AzureDevOpsArtifactFeed newFeed = new AzureDevOpsArtifactFeed(versionedFeedName, AzureDevOpsOrg); + AzureDevOpsArtifactFeed newFeed = new AzureDevOpsArtifactFeed(versionedFeedName, AzureDevOpsOrg, AzureDevOpsProject); - string body = JsonConvert.SerializeObject(newFeed, _serializerSettings); + string createBody = JsonConvert.SerializeObject(newFeed, _serializerSettings); - HttpRequestMessage postMessage = new HttpRequestMessage(HttpMethod.Post, $"{publicSegment}_apis/packaging/feeds"); - postMessage.Content = new StringContent(body, Encoding.UTF8, "application/json"); - HttpResponseMessage response = await client.SendAsync(postMessage); + using HttpRequestMessage createFeedMessage = new HttpRequestMessage(HttpMethod.Post, $"{AzureDevOpsProject}/_apis/packaging/feeds"); + createFeedMessage.Content = new StringContent(createBody, Encoding.UTF8, "application/json"); + using HttpResponseMessage createFeedResponse = await client.SendAsync(createFeedMessage); - if (response.StatusCode == HttpStatusCode.Created) + if (createFeedResponse.StatusCode == HttpStatusCode.Created) { needsUniqueName = false; baseFeedName = versionedFeedName; + + /// This is where we would potentially update the Local feed view with permissions to the organization's + /// valid users. But, see for more info on why this is not + /// done this way. } - else if (response.StatusCode == HttpStatusCode.Conflict) + else if (createFeedResponse.StatusCode == HttpStatusCode.Conflict) { versionedFeedName = $"{baseFeedName}-{++subVersion}"; needsUniqueName = true; @@ -144,12 +164,12 @@ private async Task ExecuteAsync() } else { - throw new Exception($"Feed '{baseFeedName}' was not created. Request failed with status code {response.StatusCode}. Exception: {await response.Content.ReadAsStringAsync()}"); + throw new Exception($"Feed '{baseFeedName}' was not created. Request failed with status code {createFeedResponse.StatusCode}. Exception: {await createFeedResponse.Content.ReadAsStringAsync()}"); } } } while (needsUniqueName); - TargetFeedURL = $"https://pkgs.dev.azure.com/{AzureDevOpsOrg}/{publicSegment}_packaging/{baseFeedName}/nuget/v3/index.json"; + TargetFeedURL = $"https://pkgs.dev.azure.com/{AzureDevOpsOrg}/{AzureDevOpsProject}/_packaging/{baseFeedName}/nuget/v3/index.json"; TargetFeedName = baseFeedName; Log.LogMessage(MessageImportance.High, $"Feed '{TargetFeedURL}' created successfully!"); @@ -161,41 +181,31 @@ private async Task ExecuteAsync() return !Log.HasLoggedErrors; } - } - - public class Permission - { - public Permission(string identityDescriptor, int role) - { - IdentityDescriptor = identityDescriptor; - Role = role; - } - public string IdentityDescriptor { get; set; } - - public int Role { get; set; } - } - - public class AzureDevOpsArtifactFeed - { - public AzureDevOpsArtifactFeed(string name, string organization) + /// + /// Returns a tag for feed visibility that will be added to the feed name + /// + /// Organization containing the feed + /// Project within containing the feed + /// Feed tag + /// + private string GetFeedVisibilityTag(string organization, string project) { - Name = name; - if (organization == "dnceng") + switch (organization) { - Permissions = new List - { - // Mimic the permissions added to a feed when created in the browser - new Permission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:b55de4ed-4b5a-4215-a8e4-0a0a5f71e7d8", 3), // Project Collection Build Service - new Permission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:7ea9116e-9fac-403d-b258-b31fcf1bb293", 3), // internal Build Service - new Permission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1349140002-2196814402-2899064621-3782482097-0-0-0-0-1", 4), // Feed administrators - new Permission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1846651262-2896117056-2992157471-3474698899-1-2052915359-1158038602-2757432096-2854636005", 4) // Feed administrators and contributors - }; + case "dnceng": + switch (project) + { + case "internal": + return "int"; + case "public": + return "pub"; + default: + throw new NotImplementedException($"Project '{project}' within organization '{organization}' has no visibility mapping."); + } + default: + throw new NotImplementedException($"Organization '{organization}' has no visibility mapping."); } } - - public string Name { get; set; } - - public List Permissions { get; private set; } } } diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs index 2296192100b..be97cf2f605 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs @@ -30,6 +30,8 @@ public class SetupTargetFeedConfigV3 : SetupTargetFeedConfigBase public TaskLoggingHelper Log { get; } + public string AzureDevOpsOrg => "dnceng"; + public SetupTargetFeedConfigV3( TargetChannelConfig targetChannelConfig, bool isInternalBuild, @@ -195,7 +197,8 @@ private void CreateStableSymbolsFeedIfNeeded() var symbolsFeedTask = new CreateAzureDevOpsFeed() { BuildEngine = BuildEngine, - IsInternal = IsInternalBuild, + AzureDevOpsOrg = AzureDevOpsOrg, + AzureDevOpsProject = IsInternalBuild ? "internal" : "public", AzureDevOpsPersonalAccessToken = AzureDevOpsFeedsKey, RepositoryName = RepositoryName, CommitSha = CommitSha, @@ -222,7 +225,8 @@ private void CreateStablePackagesFeedIfNeeded() var packagesFeedTask = new CreateAzureDevOpsFeed() { BuildEngine = BuildEngine, - IsInternal = IsInternalBuild, + AzureDevOpsOrg = AzureDevOpsOrg, + AzureDevOpsProject = IsInternalBuild ? "internal" : "public", AzureDevOpsPersonalAccessToken = AzureDevOpsFeedsKey, RepositoryName = RepositoryName, CommitSha = CommitSha