- Added ApiClientModel and ApiTokenModel for managing API clients and tokens. - Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management. - Created CredentialSecretModel for storing credential secrets. - Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management. - Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions. - Added TemplateRevisionModel and TemplateVersionModel for versioning templates. - Implemented ApiClientSecretHasher for secure secret hashing. - Created ApiTokenService for generating and validating JWT tokens. - Updated QueueJobService to handle deployment requests with artifacts. - Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
183 lines
6.5 KiB
C#
183 lines
6.5 KiB
C#
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Authentication.Negotiate;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using Microsoft.SelfService.Portal.Core.API.Context;
|
|
using Microsoft.SelfService.Portal.Core.API.Interfaces;
|
|
using Microsoft.SelfService.Portal.Core.API.Repository;
|
|
using Microsoft.SelfService.Portal.Core.API.Services;
|
|
using Microsoft.Extensions.FileProviders;
|
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Text.Json.Serialization;
|
|
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Add services to the container.
|
|
|
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
|
{
|
|
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
|
});
|
|
|
|
builder.Services.AddScoped<IDomainInterface,DomainRepository>();
|
|
builder.Services.AddScoped<IEnvironmentInterface, EnvironmentRepository>();
|
|
builder.Services.AddScoped<ITargetInterface, TargetRepository>();
|
|
builder.Services.AddScoped<IServiceInterface, ServiceRepository>();
|
|
builder.Services.AddScoped<IDeploymentBatchInterface, DeploymentBatchRepository>();
|
|
builder.Services.AddScoped<IDeploymentInterface, DeploymentRepository>();
|
|
builder.Services.AddScoped<ITemplateInterface, TemplateRepository>();
|
|
builder.Services.AddScoped<ITemplateCategoryInterface, TemplateCategoryRepository>();
|
|
builder.Services.AddScoped<IQueueJobService, QueueJobService>();
|
|
builder.Services.AddScoped<ApiTokenService>();
|
|
|
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
builder.Services.AddAutoMapper(_ => { }, AppDomain.CurrentDomain.GetAssemblies());
|
|
|
|
builder.Services.AddDbContext<DataContext>(options =>
|
|
options.UseSqlServer(builder.Configuration.GetConnectionString("Context") ?? throw new InvalidOperationException("Connection string 'Context' not found.")));
|
|
|
|
var tokenService = new ApiTokenService(builder.Configuration);
|
|
|
|
builder.Services.AddAuthentication(options =>
|
|
{
|
|
options.DefaultScheme = "Smart";
|
|
options.DefaultChallengeScheme = "Smart";
|
|
})
|
|
.AddPolicyScheme("Smart", "Negotiate or Bearer", options =>
|
|
{
|
|
options.ForwardDefaultSelector = context =>
|
|
{
|
|
var authorization = context.Request.Headers.Authorization.ToString();
|
|
return authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
|
? JwtBearerDefaults.AuthenticationScheme
|
|
: NegotiateDefaults.AuthenticationScheme;
|
|
};
|
|
})
|
|
.AddNegotiate()
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = true,
|
|
ValidIssuer = tokenService.Issuer,
|
|
ValidateAudience = true,
|
|
ValidAudience = tokenService.Audience,
|
|
ValidateIssuerSigningKey = true,
|
|
IssuerSigningKey = tokenService.GetSigningKey(),
|
|
ValidateLifetime = true,
|
|
ClockSkew = TimeSpan.FromMinutes(2)
|
|
};
|
|
|
|
options.Events = new JwtBearerEvents
|
|
{
|
|
OnTokenValidated = context =>
|
|
{
|
|
var jti = context.Principal?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
|
|
if (string.IsNullOrWhiteSpace(jti))
|
|
{
|
|
context.Fail("Token does not contain a token id.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var dataContext = context.HttpContext.RequestServices.GetRequiredService<DataContext>();
|
|
var token = dataContext.ApiTokens.FirstOrDefault(existing => existing.Jti == jti);
|
|
if (token == null)
|
|
{
|
|
context.Fail("Token is not registered.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
if (token.RevokedAt.HasValue || token.ExpiresAt <= DateTime.UtcNow)
|
|
{
|
|
context.Fail("Token is revoked or expired.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
token.LastUsedAt = DateTime.UtcNow;
|
|
token.Modified = DateTime.UtcNow;
|
|
token.ModifiedBy = "BearerToken";
|
|
dataContext.SaveChanges();
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
});
|
|
|
|
builder.Services.AddHttpContextAccessor();
|
|
|
|
builder.Services.AddAuthorization(options =>
|
|
{
|
|
// By default, all incoming requests will be authorized according to the default policy.
|
|
options.FallbackPolicy = options.DefaultPolicy;
|
|
options.AddPolicy("QueueProcess", policy => policy.RequireClaim("scope", "queue.process"));
|
|
options.AddPolicy("DeploymentRead", policy => policy.RequireClaim("scope", "deployment.read"));
|
|
options.AddPolicy("CredentialResolve", policy => policy.RequireClaim("scope", "credential.resolve"));
|
|
options.AddPolicy("TokenManage", policy => policy.RequireAssertion(context =>
|
|
context.User.HasClaim("scope", "token.manage")
|
|
|| context.User.Identity?.AuthenticationType == NegotiateDefaults.AuthenticationScheme));
|
|
options.AddPolicy("TokenAdmin", policy => policy.RequireAssertion(context =>
|
|
context.User.HasClaim("scope", "token.admin")
|
|
|| context.User.Identity?.AuthenticationType == NegotiateDefaults.AuthenticationScheme));
|
|
});
|
|
|
|
var app = builder.Build();
|
|
var frontendDistPath = Path.GetFullPath(Path.Combine(
|
|
app.Environment.ContentRootPath,
|
|
"..",
|
|
"Microsoft.SelfService.Portal.Web",
|
|
"dist"));
|
|
|
|
// Configure the HTTP request pipeline.
|
|
if (app.Environment.IsDevelopment())
|
|
//{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
//}
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
if (Directory.Exists(frontendDistPath))
|
|
{
|
|
var frontendDistProvider = new PhysicalFileProvider(frontendDistPath);
|
|
|
|
app.UseDefaultFiles(new DefaultFilesOptions
|
|
{
|
|
FileProvider = frontendDistProvider
|
|
});
|
|
|
|
app.UseStaticFiles(new StaticFileOptions
|
|
{
|
|
FileProvider = frontendDistProvider
|
|
});
|
|
}
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.MapControllers();
|
|
|
|
if (Directory.Exists(frontendDistPath))
|
|
{
|
|
app.MapFallback(async context =>
|
|
{
|
|
if (context.Request.Path.StartsWithSegments("/api"))
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
await context.Response.WriteAsJsonAsync(new { message = "API endpoint not found." });
|
|
return;
|
|
}
|
|
|
|
context.Response.ContentType = "text/html";
|
|
await context.Response.SendFileAsync(Path.Combine(frontendDistPath, "index.html"));
|
|
}).AllowAnonymous();
|
|
}
|
|
|
|
app.Run();
|
|
|
|
|
|
|