diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf159d2..5ecda69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,17 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Setup .NET 3.1.x - uses: actions/setup-dotnet@v1 + - name: Setup .NET 10.x + uses: actions/setup-dotnet@v4 with: - dotnet-version: 3.1.x - - - name: Setup .NET 6.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.x.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore src/service/Microsoft.FeatureFlighting.sln diff --git a/assets/pipeline/cd-feature-flights-management-build.yml b/assets/pipeline/cd-feature-flights-management-build.yml new file mode 100644 index 0000000..8e9830f --- /dev/null +++ b/assets/pipeline/cd-feature-flights-management-build.yml @@ -0,0 +1,188 @@ +resources: + repositories: + - repository: FeatureFlightingGithub + type: github + endpoint: github.com + name: microsoft/FeatureFlightingManagement + trigger: + - none + - repository: FxpTools + type: git + name: CE-FS-FExP-FxpTools + +variables: + BuildPlatform: 'any cpu' + BuildConfiguration: 'release' +stages: +- stage: Build + jobs: + - job: Build + displayName: "Build & Publish Artifact" + pool: + vmImage: windows-2019 + steps: + - checkout: FeatureFlightingGithub + - checkout: self + - task: NuGetToolInstaller@1 + displayName: Use NuGet 4.3.0 + inputs: + versionSpec: 4.3.0 + checkLatest: true + - task: UseDotNet@2 + displayName: Use .Net Core sdk 3.1.x + inputs: + version: 3.1.x + includePreviewVersions: true + - task: UseDotNet@2 + displayName: Use .Net Core sdk 6.0.x + inputs: + version: 6.0.x + includePreviewVersions: true + - task: DotNetCoreCLI@2 + displayName: Restore API (Microsoft.FeatureFlighting.sln) + inputs: + command: restore + projects: '**/Microsoft.FeatureFlighting.sln' + selectOrConfig: config + feedRestore: c94f6383-c859-4345-ad90-73d2e3bcb70f + nugetConfigPath: NuGet.Config + - task: DotNetCoreCLI@2 + displayName: Build API (Microsoft.FeatureFlighting.sln) + inputs: + projects: '**/Microsoft.FeatureFlighting.sln' + - task: DotNetCoreCLI@2 + displayName: Test API (*.Tests.csproj) + continueOnError: false + inputs: + command: test + projects: '**/*Tests.csproj' + - task: DotNetCoreCLI@2 + displayName: Publish API (Microsoft.FeatureFlighting.sln) + inputs: + command: publish + publishWebProjects: false + projects: '**/Microsoft.FeatureFlighting.API.csproj' + arguments: --output $(Build.artifactstagingdirectory) + - task: DotNetCoreCLI@2 + displayName: Restore Functional Tests (Microsoft.FeatureFlighting.Tests.Functional.sln) + inputs: + command: restore + projects: '**/Microsoft.FeatureFlighting.Tests.Functional.sln' + selectOrConfig: config + feedRestore: c94f6383-c859-4345-ad90-73d2e3bcb70f + nugetConfigPath: NuGet.Config + - task: DotNetCoreCLI@2 + displayName: Build Functional Tests (Microsoft.FeatureFlighting.Tests.Functional.sln) + inputs: + projects: '**/Microsoft.FeatureFlighting.Tests.Functional.sln' + - task: DotNetCoreCLI@2 + displayName: Publish Functional Tests (Microsoft.FeatureFlighting.Tests.Functional.sln) + inputs: + command: publish + publishWebProjects: false + projects: FeatureFlightingManagement/tests/functional/Tests/Microsoft.FeatureFlighting.Tests.Functional.csproj + arguments: --output $(Build.artifactstagingdirectory) + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: drop' + +- stage: Pre_Prod_Perf_Deploy + dependsOn: + - Build + condition: succeeded('Build') + jobs: + - deployment: Pre_Prod_Perf_Deploy_EUS + displayName: Performance Environment Deployment EUS + pool: + name: Azure Pipelines + vmImage: windows-2019 + demands: vstest + variables: + KeyVault-EUS: 'kv-exp-ppe-eus' + AppService-EUS: 'feature-flights-management-api-perf-eus' + AppService-EUS-Staging: 'feature-flights-management-api-perf-eus-staging' + ResourceGroup: 'RG-FieldExperiencePlatform-Uat-01' + Subscription: '05a315f7-744f-4692-b9dd-1aed7c6cee64' + environment: 'PS-GCM-FeXP-Services-Pre-Prod' + strategy: + runOnce: + deploy: + steps: + - checkout: FeatureFlightingGithub + - checkout: self + - powershell: | + [string] $branchName = "refs/heads/master" + [string] $mainBranchName = "refs/heads/main" + [string] $branchNameHotfix = "hotfix" + if ($env:BUILD_SOURCEBRANCH -eq $branchName -Or $env:BUILD_SOURCEBRANCH -contains $mainBranchName -Or $env:BUILD_SOURCEBRANCH -contains $branchNameHotfix) + { + Write-Host $branchName "branch check passed. Release triggered" from $env:BUILD_SOURCEBRANCH branch. + } + else + { + [string] $text1 = "##vso[task.logissue type=error;]" + [string] $text2 = "Branch Policy Violation:" + [string] $text3 = " !=" + [string] $text4 = " | " + [string] $text5 = "branch. Deployment stopped from" + [string] $text6 = "branch." + + Write-Host "$text1 $text2 $env:BUILD_SOURCEBRANCH $text3 $branchName $text4 $mainBranchName $text5 $env:BUILD_SOURCEBRANCH $text6" + + exit 1 + } + displayName: 'Stop Release if not from master | main branch' + enabled: false + - task: AzurePowerShell@5 + displayName: "Add Az IP Agent on UAT EUS KeyVault" + inputs: + azureSubscription: "FXP-Azure-Subscription (SPA)" + ScriptType: InlineScript + azurePowerShellVersion: "LatestVersion" + Inline: | + $VerbosePreference = "SilentlyContinue" + Set-AzContext -SubscriptionId '$(Subscription)' + # Installing MSIdentityTools Module + Write-Host "Installing MSIdentityTools Module to fetch the Azure published IP ranges" + Install-Module -Name MSIdentityTools -Scope CurrentUser -Force + # Allowed IP address. + Write-Host "Getting IP address of Azure DevOps agent" + $IPAddress = (Invoke-WebRequest myexternalip.com/raw -UseBasicParsing).Content + $IPAddressRange = '{0}/32' -f $IPAddress + # Allow current public IP address. + Add-AzKeyVaultNetworkRule -ResourceId '/subscriptions/$(Subscription)/resourceGroups/$(ResourceGroup)/providers/Microsoft.KeyVault/vaults/$(KeyVault-EUS)' -IpAddressRange $IPAddressRange -SubscriptionId '05a315f7-744f-4692-b9dd-1aed7c6cee64' + Write-Host "Successfully appended the IP Address for Azure DevOps" -ForegroundColor Green + FailOnStandardError: true + - task: AzureKeyVault@1 + displayName: 'Azure Key Vault: Download Secrets' + inputs: + azureSubscription: 'FXP-Azure-Subscription (SPA)' + KeyVaultName: '$(KeyVault-EUS)' + SecretsFilter: 'Authentication-Secret,MS-Graph-Secret' + - template: templates/app-service-deploy.yml + parameters: + KeyVault: $(KeyVault-EUS) + AppService: $(AppService-EUS) + ResourceGroup: $(ResourceGroup) + FunctionalTestRunSettingsFile: 'PreProduction.runsettings' + AppServiceStaging: $(AppService-EUS-Staging) + DeploymentAppSettings: '-AppConfiguration:ConfigurationEnvLabel Feature-Flight-Management-Config-PPE -AppConfiguration:FeatureFlightsLabel PPE -Keyvault:EndpointUrl https://kv-exp-ppe-eus.vault.azure.net/ -FeatureManagement:* true' + - task: AzurePowerShell@5 + displayName: "Delete Az IP Agent Address on UAT EUS KeyVault" + inputs: + azureSubscription: "FXP-Azure-Subscription (SPA)" + ScriptType: InlineScript + azurePowerShellVersion: "LatestVersion" + Inline: | + $VerbosePreference = "SilentlyContinue" + Set-AzContext -SubscriptionId '$(Subscription)' + # Installing MSIdentityTools Module + Write-Host "Installing MSIdentityTools Module to fetch the Azure published IP ranges" + Install-Module -Name MSIdentityTools -Scope CurrentUser -Force + # Allowed IP address. + Write-Host "Getting IP address of Azure DevOps agent" + $IPAddress = (Invoke-WebRequest myexternalip.com/raw -UseBasicParsing).Content + $IPAddressRange = '{0}/32' -f $IPAddress + # Delete current public IP address. + Remove-AzKeyVaultNetworkRule -ResourceId '/subscriptions/$(Subscription)/resourceGroups/$(ResourceGroup)/providers/Microsoft.KeyVault/vaults/$(KeyVault-EUS)' -IpAddressRange $IPAddressRange -SubscriptionId '05a315f7-744f-4692-b9dd-1aed7c6cee64' + Write-Host "Successfully deleted the IP Address for Azure DevOps" -ForegroundColor Green + FailOnStandardError: true diff --git a/assets/pipeline/cd-feature-flights-management.yml b/assets/pipeline/cd-feature-flights-management.yml index 3572567..48a863e 100644 --- a/assets/pipeline/cd-feature-flights-management.yml +++ b/assets/pipeline/cd-feature-flights-management.yml @@ -26,9 +26,9 @@ stages: versionSpec: 4.3.0 checkLatest: true - task: UseDotNet@2 - displayName: Use .Net Core sdk 3.1.x + displayName: Use .NET sdk 10.0.x inputs: - version: 3.1.x + version: 10.0.x includePreviewVersions: true - task: DotNetCoreCLI@2 displayName: Restore API (Microsoft.FeatureFlighting.sln) diff --git a/assets/pipeline/ci-feature-flighting.yml b/assets/pipeline/ci-feature-flighting.yml new file mode 100644 index 0000000..8878320 --- /dev/null +++ b/assets/pipeline/ci-feature-flighting.yml @@ -0,0 +1,61 @@ +resources: + repositories: + - repository: FeatureFlightingGithub + type: github + endpoint: github.com + name: microsoft/FeatureFlightingManagement + trigger: + - main + +trigger: none +name: Feature-Flighting-$(Date:yyyyMMdd)$(Rev:.rr) +jobs: +- job: Build + displayName: Agent job 1 + pool: + vmImage: 'windows-2019' + steps: + - checkout: FeatureFlightingGithub + - task: NuGetToolInstaller@1 + displayName: Use NuGet 4.3.0 + inputs: + versionSpec: 4.3.0 + checkLatest: true + - task: UseDotNet@2 + displayName: Use .Net Core sdk 3.1.x + inputs: + version: 3.1.x + includePreviewVersions: true + - task: DotNetCoreCLI@2 + displayName: Restore API (Microsoft.FeatureFlighting.sln) + inputs: + command: restore + projects: '**/Microsoft.FeatureFlighting.sln' + - task: DotNetCoreCLI@2 + displayName: Build API (Microsoft.FeatureFlighting.sln) + inputs: + projects: '**/Microsoft.FeatureFlighting.sln' + - task: DotNetCoreCLI@2 + displayName: Test API (Microsoft.FeatureFlighting.sln) + enabled: false + inputs: + command: test + projects: '**/*Tests.csproj' + arguments: --collect "Code coverage" + workingDirectory: src/service/ + - task: DotNetCoreCLI@2 + displayName: Restore Functional Tests (Microsoft.FXP.Flighting.Tests.FunctionalTest.csproj) + enabled: false + inputs: + command: restore + projects: tests/Microsoft.FXP.Experimentation.Tests/Microsoft.FXP.Flighting.Tests.FunctionalTest/Microsoft.FXP.Flighting.Tests.FunctionalTest.csproj + selectOrConfig: config + feedRestore: c94f6383-c859-4345-ad90-73d2e3bcb70f + nugetConfigPath: src/Microsoft.PS.FlightingService/NuGet.Config + - task: DotNetCoreCLI@2 + enabled: false + displayName: Build Functional Tests (Microsoft.FXP.Flighting.Tests.FunctionalTest.csproj) + inputs: + projects: tests/Microsoft.FXP.Experimentation.Tests/Microsoft.FXP.Flighting.Tests.FunctionalTest/Microsoft.FXP.Flighting.Tests.FunctionalTest.csproj + + diff --git a/assets/pipeline/templates/app-service-deploy.yml b/assets/pipeline/templates/app-service-deploy.yml index 4ad7dde..63a6442 100644 --- a/assets/pipeline/templates/app-service-deploy.yml +++ b/assets/pipeline/templates/app-service-deploy.yml @@ -28,9 +28,9 @@ steps: Folder: '$(System.ArtifactsDirectory)/drop/FunctionalTests' - task: UseDotNet@2 - displayName: 'Use .Net Core sdk 3.1.x' + displayName: 'Use .NET sdk 10.0.x' inputs: - version: 3.1.x + version: 10.0.x - task: AzureKeyVault@1 displayName: 'Azure Key Vault: Download Secrets' diff --git a/src/service/API/Extensions/ServicesExtensions.cs b/src/service/API/Extensions/ServicesExtensions.cs index a3a7360..f141abb 100644 --- a/src/service/API/Extensions/ServicesExtensions.cs +++ b/src/service/API/Extensions/ServicesExtensions.cs @@ -2,7 +2,6 @@ using System.IO; using System.Linq; using System.Reflection; -using Microsoft.OpenApi.Models; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.FeatureManagement; @@ -15,8 +14,8 @@ using Microsoft.FeatureFlighting.Api.ExceptionHandler; using AppInsights.EnterpriseTelemetry.Web.Extension.Middlewares; using Microsoft.IdentityModel.Validators; -using Microsoft.Identity.ServiceEssentials.Extensions.AspNetCoreMiddleware; -using Microsoft.IdentityModel.S2S.Extensions.AspNetCore; +using Microsoft.Identity.ServiceEssentials; +using Microsoft.Identity.ServiceEssentials.Configuration; using System.Configuration; @@ -29,80 +28,57 @@ internal static class ServicesExtensions /// public static void AddAuthentication(this IServiceCollection services, IConfiguration configuration) { - services.AddAuthentication(S2SAuthenticationDefaults.AuthenticationScheme) - .AddMiseWithDefaultAuthentication(configuration, options => - { - var primaryAudience = configuration["Authentication:Audience"]; - IList validAudiences = !string.IsNullOrWhiteSpace(configuration["Authentication:AdditionalAudiences"]) - ? configuration["Authentication:AdditionalAudiences"].Split(',').ToList() - : new List(); - validAudiences.Add(primaryAudience); - options.Authority= configuration["Authentication:Authority"]; - foreach (var audience in validAudiences) - { - options.Audiences.Add(audience); - } - options.ClientId = configuration["ClientInfo:ClientId"]; - options.Instance = configuration["InstanceInfo:Instance"]; - options.TenantId = configuration["TenantInfo:Tenant"]; - }); + var primaryAudience = configuration["Authentication:Audience"]; + var clientId = configuration["ClientInfo:ClientId"]; + var tenantId = configuration["TenantInfo:Tenant"]; + var instance = configuration["InstanceInfo:Instance"]; - } + // Accept the configured audience and its api:// URI form (v1.0 and v2.0 tokens), plus any + // extra audiences from Authentication:AdditionalAudiences (comma-separated), each in raw and api:// forms. + var additionalAudiences = !string.IsNullOrWhiteSpace(configuration["Authentication:AdditionalAudiences"]) + ? configuration["Authentication:AdditionalAudiences"].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + : Array.Empty(); + var validAudiences = new[] { primaryAudience, clientId, $"api://{primaryAudience}", $"api://{clientId}" } + .Concat(additionalAudiences) + .Concat(additionalAudiences.Select(a => $"api://{a}")) + .Where(a => !string.IsNullOrWhiteSpace(a)) + .Distinct() + .ToArray(); - /// - /// Adds Swagger documenation - /// - /// - /// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - /// - public static void AddSwagger(this IServiceCollection services) - { - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo + // Inbound token validation via MISE 2.x with the default module set. + services.AddAuthentication(MiseAuthenticationDefaults.AuthenticationScheme) + .AddMiseWithDefaultModules(configuration, miseOptions => { - Title = "Flighting Service", - Version = "v2", - Contact = new OpenApiContact + miseOptions.AzureAd ??= new MiseAuthenticationOptions(); + miseOptions.AzureAd.Instance = instance; + miseOptions.AzureAd.TenantId = tenantId; + miseOptions.AzureAd.ClientId = clientId; + miseOptions.AzureAd.Audience = primaryAudience; + miseOptions.AzureAd.Audiences = validAudiences; + + // Explicit inbound policy accepting both app and user Bearer tokens. + // Required for MISE 2.x to validate inbound tokens (the default policy alone rejects them). + var inboundPolicy = new MiseInboundPolicyOptions { - Email = "fxpswe@microsoft.com", - Name = "Field Experience Engineering Team", - Url = new System.Uri("https://aka.ms/fxpdocs") - }, - Description = "APIs for managing and evaluating feature flags. Powered by Azure Configuration (Feature Management)" - }); - c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - In = ParameterLocation.Header, - Description = "Please insert JWT with Bearer into field", - Name = "Authorization", - Type = SecuritySchemeType.ApiKey - }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement { + Label = "FeatureFlightingInbound", + Instance = instance, + TenantId = tenantId, + Audiences = validAudiences, + }; + + var bearerTokenTypeOptions = new TokenTypeOptions { - new OpenApiSecurityScheme() - { - Reference = new OpenApiReference() - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - System.Array.Empty() - } - }); - try - { - string documentationFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - string documentationPath = Path.Combine(AppContext.BaseDirectory, documentationFile); - c.IncludeXmlComments(documentationPath); - } - catch - { - // Do nothing if documentation fails - } - }); - services.AddEndpointsApiExplorer(); + AppToken = true, + UserToken = true, + AllowMissingIdTypeClaim = true, + }; + var bearerProtocol = new ProtocolOptions(); + bearerProtocol.TokenTypes["AccessToken"] = bearerTokenTypeOptions; + bearerProtocol.TokenTypes["AppToken"] = bearerTokenTypeOptions; + inboundPolicy.Protocols[Microsoft.Identity.ServiceEssentials.Authentication.Protocol.BearerConstants.ProtocolName] = bearerProtocol; + + miseOptions.AzureAd.InboundPolicies = new List { inboundPolicy }; + }, MiseAuthenticationDefaults.AuthenticationScheme); } /// diff --git a/src/service/API/Microsoft.FeatureFlighting.API.csproj b/src/service/API/Microsoft.FeatureFlighting.API.csproj index f342293..6c70716 100644 --- a/src/service/API/Microsoft.FeatureFlighting.API.csproj +++ b/src/service/API/Microsoft.FeatureFlighting.API.csproj @@ -1,7 +1,7 @@  - net6.0 + net10.0 InProcess True @@ -19,29 +19,24 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/service/API/Middlewares/MSALMiddleware.cs b/src/service/API/Middlewares/MSALMiddleware.cs index 11a8d96..fd234ef 100644 --- a/src/service/API/Middlewares/MSALMiddleware.cs +++ b/src/service/API/Middlewares/MSALMiddleware.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; -using Microsoft.IdentityModel.S2S.Extensions.AspNetCore; +using Microsoft.Identity.ServiceEssentials; namespace Microsoft.FeatureFlighting.Api.Middlewares { @@ -17,7 +17,7 @@ public MSALMiddleware(RequestDelegate next) public async Task Invoke(HttpContext httpContext) { - var result = await httpContext.AuthenticateAsync(S2SAuthenticationDefaults.AuthenticationScheme); + var result = await httpContext.AuthenticateAsync(MiseAuthenticationDefaults.AuthenticationScheme); if (result.Succeeded || httpContext.Request.Path.Value == "/api/probe/ping") { httpContext.User = result.Principal; diff --git a/src/service/API/Startup.cs b/src/service/API/Startup.cs index 63737b2..0fc9afe 100644 --- a/src/service/API/Startup.cs +++ b/src/service/API/Startup.cs @@ -16,8 +16,7 @@ using Microsoft.FeatureFlighting.API.Controllers; using AppInsights.EnterpriseTelemetry.Web.Extension; using AppInsights.EnterpriseTelemetry.Web.Extension.Filters; -using Microsoft.Identity.ServiceEssentials.Extensions.AspNetCoreMiddleware; - + namespace Microsoft.PS.Services.FlightingService.Api { /// @@ -45,7 +44,6 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(Configuration); - services.AddSwagger(); services.AddTelememtry(Configuration); AddMvc(services); services.AddHttpClients(Configuration); @@ -61,12 +59,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Flighting Service V2"); - c.RoutePrefix = string.Empty; - }); app.UseDeveloperExceptionPage(); } else @@ -83,7 +75,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); - app.UseMise(); app.UseEndpoints(endpoints => endpoints.MapControllers()); } diff --git a/src/service/Cache/Microsoft.FeatureFlighting.Cache.csproj b/src/service/Cache/Microsoft.FeatureFlighting.Cache.csproj index 00b2e64..612fb4d 100644 --- a/src/service/Cache/Microsoft.FeatureFlighting.Cache.csproj +++ b/src/service/Cache/Microsoft.FeatureFlighting.Cache.csproj @@ -1,11 +1,11 @@ - netstandard2.1 + net10.0 - + diff --git a/src/service/Common/Microsoft.FeatureFlighting.Common.csproj b/src/service/Common/Microsoft.FeatureFlighting.Common.csproj index a9be514..004ee61 100644 --- a/src/service/Common/Microsoft.FeatureFlighting.Common.csproj +++ b/src/service/Common/Microsoft.FeatureFlighting.Common.csproj @@ -1,16 +1,16 @@  - netstandard2.1 + net10.0 Preview - - + + - + diff --git a/src/service/Domain/FeatureFilters/BaseFilter.cs b/src/service/Domain/FeatureFilters/BaseFilter.cs index ffd4715..2356aec 100644 --- a/src/service/Domain/FeatureFilters/BaseFilter.cs +++ b/src/service/Domain/FeatureFilters/BaseFilter.cs @@ -15,7 +15,6 @@ using Microsoft.FeatureFlighting.Core.Operators; using Microsoft.FeatureFlighting.Common.AppExceptions; using static Microsoft.FeatureFlighting.Common.Constants; -using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; namespace Microsoft.FeatureFlighting.Core.FeatureFilters { diff --git a/src/service/Domain/Microsoft.FeatureFlighting.Core.csproj b/src/service/Domain/Microsoft.FeatureFlighting.Core.csproj index b68389c..34010d2 100644 --- a/src/service/Domain/Microsoft.FeatureFlighting.Core.csproj +++ b/src/service/Domain/Microsoft.FeatureFlighting.Core.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + net10.0 Preview @@ -12,14 +12,14 @@ - - - - - - - - + + + + + + + + diff --git a/src/service/Infrastructure/AppConfig/AzureConfigurationClientProvider.cs b/src/service/Infrastructure/AppConfig/AzureConfigurationClientProvider.cs index 36678cb..45780fa 100644 --- a/src/service/Infrastructure/AppConfig/AzureConfigurationClientProvider.cs +++ b/src/service/Infrastructure/AppConfig/AzureConfigurationClientProvider.cs @@ -4,7 +4,6 @@ using Azure.Identity; using Microsoft.Extensions.Configuration; using Microsoft.FeatureFlighting.Common; -using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; namespace Microsoft.FeatureFlighting.Infrastructure.AppConfig { diff --git a/src/service/Infrastructure/Graph/GraphGroupVerificationService.cs b/src/service/Infrastructure/Graph/GraphGroupVerificationService.cs index 0ea44d5..30e23ad 100644 --- a/src/service/Infrastructure/Graph/GraphGroupVerificationService.cs +++ b/src/service/Infrastructure/Graph/GraphGroupVerificationService.cs @@ -2,6 +2,9 @@ using System.Linq; using System.Text; using Microsoft.Graph; +using Microsoft.Graph.Models; +using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Authentication; using System.Threading.Tasks; using System.Net.Http.Headers; using Microsoft.Identity.Client; @@ -25,7 +28,7 @@ namespace Microsoft.FeatureFlighting.Infrastructure.Graph /// /> internal class GraphGroupVerificationService : IGroupVerificationService, IBackgroundCacheable> { - private readonly IGraphServiceClient _graphServiceClient; + private readonly GraphServiceClient _graphServiceClient; private readonly ICacheFactory _cacheFactory; private readonly bool _isCachingEnabled; private readonly int _cacheInterval; @@ -126,7 +129,7 @@ private async Task> GetGroupMembers(string securityGroupId, Logger return groupMembers ?? new(); } - private IGraphServiceClient CreateGraphClient(IConfiguration configuration) + private GraphServiceClient CreateGraphClient(IConfiguration configuration) { try { @@ -161,15 +164,8 @@ private IGraphServiceClient CreateGraphClient(IConfiguration configuration) _cache.Add(confidentialAppCacheKey, client); #endif - IGraphServiceClient graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) => - { - AuthenticationResult authResult = await client - .AcquireTokenForClient(scopes) - .ExecuteAsync(); - - requestMessage.Headers.Authorization = - new AuthenticationHeaderValue("Bearer", authResult.AccessToken); - })); + GraphServiceClient graphServiceClient = new GraphServiceClient( + new MsalConfidentialClientAuthenticationProvider(client, scopes)); return graphServiceClient; } catch (Exception ex) @@ -234,23 +230,26 @@ public async Task>> CreateCacheableObject string cacheKey = cacheParameters.CacheKey; string groupId = cacheParameters.ObjectId; - var transitiveMembers = await _graphServiceClient.Groups[groupId] - .TransitiveMembers - .Request() - .GetAsync() - .ConfigureAwait(false); - - var groupMembers = transitiveMembers?.ToList() ?? new(); - while (transitiveMembers != null && transitiveMembers.NextPageRequest != null) - { - transitiveMembers = await transitiveMembers.NextPageRequest.GetAsync().ConfigureAwait(false); - if (transitiveMembers != null) - groupMembers.AddRange(transitiveMembers?.ToList() ?? new()); - } - - List userPrincipalNames = groupMembers - .Where(member => member is User)? - .Select(member => ((User)member).UserPrincipalName)? + var transitiveMembersResponse = await _graphServiceClient.Groups[groupId] + .TransitiveMembers + .GetAsync() + .ConfigureAwait(false); + + var groupMembers = new List(); + if (transitiveMembersResponse?.Value != null) + { + var pageIterator = PageIterator + .CreatePageIterator(_graphServiceClient, transitiveMembersResponse, (member) => + { + groupMembers.Add(member); + return true; + }); + await pageIterator.IterateAsync().ConfigureAwait(false); + } + + List userPrincipalNames = groupMembers + .Where(member => member is User)? + .Select(member => ((User)member).UserPrincipalName)? .ToList() ?? new(); BackgroundCacheableObject> cacheableGroupMembers = new() @@ -261,11 +260,33 @@ public async Task>> CreateCacheableObject return cacheableGroupMembers; } - public async Task RebuildCache(BackgroundCacheParameters cacheParameters, LoggerTrackingIds trackingIds) - { + public async Task RebuildCache(BackgroundCacheParameters cacheParameters, LoggerTrackingIds trackingIds) + { var cacheableObject = await CreateCacheableObject(cacheParameters, trackingIds).ConfigureAwait(false); if (cacheableObject.Object != null && cacheableObject.Object.Any()) - await SetCacheObject(cacheableObject, trackingIds).ConfigureAwait(false); - } + await SetCacheObject(cacheableObject, trackingIds).ConfigureAwait(false); + } + + private sealed class MsalConfidentialClientAuthenticationProvider : IAuthenticationProvider + { + private readonly IConfidentialClientApplication _client; + private readonly string[] _scopes; + + public MsalConfidentialClientAuthenticationProvider(IConfidentialClientApplication client, string[] scopes) + { + _client = client; + _scopes = scopes; + } + + public async Task AuthenticateRequestAsync(RequestInformation request, Dictionary additionalAuthenticationContext = null, CancellationToken cancellationToken = default) + { + AuthenticationResult authResult = await _client + .AcquireTokenForClient(_scopes) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + request.Headers.Add("Authorization", $"Bearer {authResult.AccessToken}"); + } + } } } diff --git a/src/service/Infrastructure/Microsoft.FeatureFlighting.Infrastructure.csproj b/src/service/Infrastructure/Microsoft.FeatureFlighting.Infrastructure.csproj index 2e49800..8588646 100644 --- a/src/service/Infrastructure/Microsoft.FeatureFlighting.Infrastructure.csproj +++ b/src/service/Infrastructure/Microsoft.FeatureFlighting.Infrastructure.csproj @@ -1,21 +1,21 @@  - netstandard2.1 + net10.0 enable Preview - - - - - - - - - + + + + + + + + + diff --git a/src/service/Services/Microsoft.FeatureFlighting.Services.csproj b/src/service/Services/Microsoft.FeatureFlighting.Services.csproj index 7c66b46..7145b36 100644 --- a/src/service/Services/Microsoft.FeatureFlighting.Services.csproj +++ b/src/service/Services/Microsoft.FeatureFlighting.Services.csproj @@ -1,16 +1,16 @@  - netstandard2.1 + net10.0 - - - - - - + + + + + + diff --git a/src/service/Tests/Api.Tests/Microsoft.FeatureFlighting.API.Tests.csproj b/src/service/Tests/Api.Tests/Microsoft.FeatureFlighting.API.Tests.csproj index 591d90f..b3fdb94 100644 --- a/src/service/Tests/Api.Tests/Microsoft.FeatureFlighting.API.Tests.csproj +++ b/src/service/Tests/Api.Tests/Microsoft.FeatureFlighting.API.Tests.csproj @@ -1,27 +1,25 @@ - net6.0 + net10.0 false - - - - - - - - - - - + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/service/Tests/Common.Tests/Microsoft.FeatureFlighting.Common.Tests.csproj b/src/service/Tests/Common.Tests/Microsoft.FeatureFlighting.Common.Tests.csproj index 39c9352..7fac0fa 100644 --- a/src/service/Tests/Common.Tests/Microsoft.FeatureFlighting.Common.Tests.csproj +++ b/src/service/Tests/Common.Tests/Microsoft.FeatureFlighting.Common.Tests.csproj @@ -1,21 +1,21 @@  - net6.0 + net10.0 false - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/service/Tests/Domain.Tests/FilterTests/CountryFilterTests.cs b/src/service/Tests/Domain.Tests/FilterTests/CountryFilterTests.cs index edd5c2e..3b08d4d 100644 --- a/src/service/Tests/Domain.Tests/FilterTests/CountryFilterTests.cs +++ b/src/service/Tests/Domain.Tests/FilterTests/CountryFilterTests.cs @@ -11,7 +11,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.FeatureFlighting.Common.AppExceptions; using Microsoft.FeatureFlighting.Core.FeatureFilters; -using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; namespace Microsoft.FeatureFlighting.Core.Tests.FilterTests { @@ -252,12 +251,10 @@ public async Task Feature_Filter_Must_Evaluate_To_False_For_StageId_Being_Begati } [TestMethod] - [ExpectedException(typeof(EvaluationException))] public async Task Feature_Filter_GetFilterSettings_Must_Evaluate_To_False() { CountryFilter countryFilter = new CountryFilter(configMock.Object, httpContextAccessorMockInDefinedInt.Object, loggerMock.Object, failureMockEvaluatorStrategy.Object); - var featureFlagStatus = await countryFilter.EvaluateAsync(null); - Assert.IsFalse(featureFlagStatus); + await Assert.ThrowsExactlyAsync(async () => await countryFilter.EvaluateAsync(null)); } [TestMethod] diff --git a/src/service/Tests/Domain.Tests/Microsoft.PS.FlightingService.Core.Tests.csproj b/src/service/Tests/Domain.Tests/Microsoft.PS.FlightingService.Core.Tests.csproj index 9afd482..c1dca22 100644 --- a/src/service/Tests/Domain.Tests/Microsoft.PS.FlightingService.Core.Tests.csproj +++ b/src/service/Tests/Domain.Tests/Microsoft.PS.FlightingService.Core.Tests.csproj @@ -1,26 +1,24 @@  - net6.0 + net10.0 false Preview - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + + + + + + + diff --git a/src/service/Tests/Domain.Tests/OperatorTests/MemberOfSecurityGroupEvaluatorTests.cs b/src/service/Tests/Domain.Tests/OperatorTests/MemberOfSecurityGroupEvaluatorTests.cs index 51371cf..ddbfea8 100644 --- a/src/service/Tests/Domain.Tests/OperatorTests/MemberOfSecurityGroupEvaluatorTests.cs +++ b/src/service/Tests/Domain.Tests/OperatorTests/MemberOfSecurityGroupEvaluatorTests.cs @@ -170,7 +170,6 @@ public async Task evaluate_sg_operator_returns_false_for_value_not_inSG_for_alai Assert.AreEqual(evaluationResult.Result, false); } [TestMethod] - [ExpectedException(typeof(GraphException))] public async Task evaluate_sg_operator_throws_exception_whenfor_value_not_inSG_for_alais() { //Arrange @@ -184,8 +183,7 @@ public async Task evaluate_sg_operator_throws_exception_whenfor_value_not_inSG_f }; evaluator = new MemberOfSecurityGroupOperator(mockGraphApiProviderWithUpnAliasInSGException.Object, mockConfig.Object); //Act - var evaluationResult = await evaluator.Evaluate(configuredValue, contextValue, filterType, trackingIds); - + await Assert.ThrowsExactlyAsync(async () => await evaluator.Evaluate(configuredValue, contextValue, filterType, trackingIds)); } } } diff --git a/src/service/Tests/Services.Tests/AuthorizationServiceTests.cs b/src/service/Tests/Services.Tests/AuthorizationServiceTests.cs index 9d5173e..8efe4b1 100644 --- a/src/service/Tests/Services.Tests/AuthorizationServiceTests.cs +++ b/src/service/Tests/Services.Tests/AuthorizationServiceTests.cs @@ -45,12 +45,11 @@ public void Must_Authorize_When_Admin_Claims_Are_Present() authService.EnsureAuthorized("TestApp", "TestOp", "CorrId"); } - [ExpectedException(typeof(AccessForbiddenException))] [TestMethod] public void Must_Authorize_When_Claims_Are_Not_Present() { IAuthorizationService authService = new AuthorizationService(httpAccessorMockWithoutPermissions.Object, _tenantConfigurationProvider, _mockConfiguration); - authService.EnsureAuthorized("TestApp", "TestOp", "CorrId"); + Assert.ThrowsExactly(() => authService.EnsureAuthorized("TestApp", "TestOp", "CorrId")); } [TestMethod] diff --git a/src/service/Tests/Services.Tests/Microsoft.FeatureFlighting.Infrastructure.Tests.csproj b/src/service/Tests/Services.Tests/Microsoft.FeatureFlighting.Infrastructure.Tests.csproj index 86edc68..2eb02b2 100644 --- a/src/service/Tests/Services.Tests/Microsoft.FeatureFlighting.Infrastructure.Tests.csproj +++ b/src/service/Tests/Services.Tests/Microsoft.FeatureFlighting.Infrastructure.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net10.0 false Preview @@ -19,20 +19,18 @@ - - - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/tests/functional/Tests/Microsoft.FeatureFlighting.Tests.Functional.csproj b/tests/functional/Tests/Microsoft.FeatureFlighting.Tests.Functional.csproj index d48f059..501fb7c 100644 --- a/tests/functional/Tests/Microsoft.FeatureFlighting.Tests.Functional.csproj +++ b/tests/functional/Tests/Microsoft.FeatureFlighting.Tests.Functional.csproj @@ -1,22 +1,22 @@  - netcoreapp3.1 + net10.0 Preview false - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - +