This commit is contained in:
Paul Schneider 2021-05-09 13:45:55 +01:00
commit 6a59f776d5
223 changed files with 75 additions and 8 deletions

View file

@ -0,0 +1,236 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using nuget_host.Models;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace nuget_host.Controllers
{
[AllowAnonymous]
public class AccountController : Controller
{
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AccountController(
IAuthenticationSchemeProvider schemeProvider,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager)
{
_schemeProvider = schemeProvider;
_signInManager = signInManager;
_userManager = userManager;
}
/// <summary>
/// Entry point into the login workflow
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (vm.IsExternalLoginOnly)
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
}
return View(vm);
}
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// the user clicked the "cancel" button
if (button != "login")
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
if (ModelState.IsValid)
{
// validate username/password
var user = await _userManager.FindByNameAsync(model.Username);
var signResult = await _signInManager.CheckPasswordSignInAsync(user, model.Password, true);
if (signResult.Succeeded)
{
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
await _signInManager.SignInAsync(user, model.RememberLogin && AccountOptions.AllowRememberLogin);
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
/// <summary>
/// Show logout page
/// </summary>
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
// build a model so the logout page knows what to display
var vm = await BuildLogoutViewModelAsync(logoutId);
if (vm.ShowLogoutPrompt == false)
{
// if the request for logout was properly authenticated from IdentityServer, then
// we don't need to show the prompt and can just log the user out directly.
return await Logout(vm);
}
return View(vm);
}
/// <summary>
/// Handle logout page postback
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
[HttpGet]
public IActionResult AccessDenied()
{
return new BadRequestObjectResult(403);
}
/*****************************************/
/* helper APIs for the AccountController */
/*****************************************/
private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
{
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null)
.Select(x => new ExternalProvider
{
DisplayName = x.DisplayName ?? x.Name,
AuthenticationScheme = x.Name
}).ToList();
var allowLocal = true;
return new LoginViewModel
{
AllowRememberLogin = AccountOptions.AllowRememberLogin,
EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
ReturnUrl = returnUrl,
ExternalProviders = providers.ToArray()
};
}
private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
{
var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
vm.Username = model.Username;
vm.RememberLogin = model.RememberLogin;
return vm;
}
private async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt };
if (User?.Identity.IsAuthenticated != true)
{
// if the user is not authenticated, then just show logged out page
vm.ShowLogoutPrompt = false;
return vm;
}
// show the logout prompt. this prevents attacks where the user
// is automatically signed out by another malicious web page.
return vm;
}
private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
{
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
LogoutId = logoutId
};
return vm;
}
}
}

View file

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using nuget_host.Data;
using nuget_host.Entities;
using nuget_host.Models;
using nuget_host.Models.ApiKeys;
namespace nuget_host.Controllers
{
[Authorize]
public class ApiKeysController : Controller
{
private readonly ApplicationDbContext dbContext;
private readonly NugetSettings nugetSettings;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IDataProtector protector;
public ApiKeysController(ApplicationDbContext dbContext,
IOptions<NugetSettings> nugetSettingsOptions,
IDataProtectionProvider provider,
UserManager<ApplicationUser> userManager)
{
this.dbContext = dbContext;
this.nugetSettings = nugetSettingsOptions.Value;
protector = provider.CreateProtector(nugetSettings.ProtectionTitle);
_userManager = userManager;
}
[HttpGet]
public async Task<ActionResult> Index()
{
string userid = User.FindFirstValue(ClaimTypes.NameIdentifier);
System.Collections.Generic.List<ApiKey> index = GetUserKeys().ToList();
IndexModel model = new IndexModel { ApiKey = index };
ViewData["Title"] = "Index";
return View("Index", model);
}
[HttpGet]
public async Task<ActionResult> Create()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var username = User.Identity.Name;
var user = await _userManager.FindByIdAsync(userId);
ViewBag.UserId = new SelectList(new List<ApplicationUser> { user });
return View(new CreateModel{ });
}
[HttpPost]
public async Task<ActionResult> Create(CreateModel model)
{
string userid = User.FindFirstValue(ClaimTypes.NameIdentifier);
IQueryable<ApiKey> userKeys = GetUserKeys();
if (userKeys.Count() >= nugetSettings.MaxUserKeyCount)
{
ModelState.AddModelError(null, "Maximum key count reached");
return View();
}
ApiKey newKey = new ApiKey { UserId = userid, Name = model.Name,
CreationDate = DateTime.Now };
_ = dbContext.ApiKeys.Add(newKey);
_ = await dbContext.SaveChangesAsync();
return View("Details", new DetailModel { Name = newKey.Name,
ProtectedValue = protector.Protect(newKey.Id),
ApiKey = newKey });
}
[HttpGet]
public async Task<ActionResult> Delete(string id)
{
string userid = User.FindFirstValue(ClaimTypes.NameIdentifier);
ApiKey key = dbContext.ApiKeys.FirstOrDefault(k => k.Id == id && k.UserId == userid);
return View(new DeleteModel { ApiKey = key });
}
[HttpPost]
public async Task<ActionResult> Delete(DeleteModel model)
{
string userid = User.FindFirstValue(ClaimTypes.NameIdentifier);
ApiKey key = dbContext.ApiKeys.FirstOrDefault(k => k.Id == model.ApiKey.Id && k.UserId == userid);
if (key == null)
{
ModelState.AddModelError(null, "Key not found");
return View();
}
_ = dbContext.ApiKeys.Remove(key);
_ = await dbContext.SaveChangesAsync();
return View("Index", new IndexModel { ApiKey = GetUserKeys().ToList() } );
}
public async Task<ActionResult> Details(string id)
{
string userid = User.FindFirstValue(ClaimTypes.NameIdentifier);
ApiKey key = await dbContext.ApiKeys.FirstOrDefaultAsync(k => k.Id == id && k.UserId == userid);
if (key == null)
{
ModelState.AddModelError(null, "Key not found");
return View();
}
return View("Details", new DetailModel { ApiKey = key, Name = key.Name, ProtectedValue = protector.Protect(key.Id)});
}
public async Task<ActionResult> Edit(string id)
{
EditModel edit = new EditModel();
string userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId);
edit.ApiKey = await GetUserKeys().SingleOrDefaultAsync(k =>
k.UserId == userId && k.Id == id);
ViewBag.UserId = new SelectList(new List<ApplicationUser> { user });
return View(edit);
}
[HttpPost]
public async Task<ActionResult> Edit(EditModel model)
{
string userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var apiKey = await dbContext.ApiKeys.SingleOrDefaultAsync(k => k.UserId == userId && k.Id == model.ApiKey.Id);
apiKey.Name = model.ApiKey.Name;
apiKey.ValidityPeriodInDays = model.ApiKey.ValidityPeriodInDays;
await dbContext.SaveChangesAsync();
return View("Details", new DetailModel { ApiKey = apiKey });
}
public IQueryable<ApiKey> GetUserKeys()
{
return dbContext.ApiKeys.Include(k => k.User).Where(k => k.User.UserName == User.Identity.Name);
}
}
}

View file

@ -0,0 +1,55 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using nuget_host.Entities;
using nuget_host.Models;
namespace nuget_host.Controllers
{
public class HomeController : Controller
{
private readonly ILogger _logger;
readonly IHostingEnvironment _environment;
public SmtpSettings _smtpSettings { get; } //set only via Secret Manager
public HomeController(
IOptions<SmtpSettings> smtpSettings,
IHostingEnvironment environment,
ILogger<HomeController> logger)
{
_environment = environment;
_logger = logger;
_smtpSettings = smtpSettings.Value;
}
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Privacy()
{
ViewData["Message"] = "Your Privacy page.";
return Ok(ViewData);
}
}
}

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using nuget_host.Data;
using nuget_host.Entities;
using nuget_host.Helpers;
namespace nuget_host.Controllers
{
[AllowAnonymous]
public class PackagesController : Controller
{
private readonly ILogger<PackagesController> logger;
private readonly IDataProtector protector;
private readonly NugetSettings nugetSettings;
ApplicationDbContext dbContext;
public PackagesController(
ILoggerFactory loggerFactory,
IDataProtectionProvider provider,
IOptions<NugetSettings> nugetOptions,
ApplicationDbContext dbContext)
{
logger = loggerFactory.CreateLogger<PackagesController>();
nugetSettings = nugetOptions.Value;
protector = provider.CreateProtector(nugetSettings.ProtectionTitle);
this.dbContext = dbContext;
}
[HttpPut("packages/{*spec}")]
public IActionResult Put(string spec)
{
string path = null;
if (string.IsNullOrEmpty(spec))
{
var clientVersionId = Request.Headers["X-NuGet-Client-Version"];
var apiKey = Request.Headers["X-NuGet-ApiKey"];
ViewData["nuget client"] = "nuget {clientVersionId}";
var clearkey = protector.Unprotect(apiKey);
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var apikey = dbContext.ApiKeys.SingleOrDefault(k => k.Id == clearkey);
if (apikey == null)
return new BadRequestObjectResult(new {error = "api-key"});
foreach (var file in Request.Form.Files)
{
string initpath = "package.nupkg";
using (FileStream fw = new FileStream(initpath, FileMode.Create))
{
file.CopyTo(fw);
}
using (FileStream fw = new FileStream(initpath, FileMode.Open))
{
var archive = new System.IO.Compression.ZipArchive(fw);
foreach (var entry in archive.Entries)
{
if (entry.FullName.EndsWith(".nuspec"))
{
// var entry = archive.GetEntry(filename);
var specstr = entry.Open();
NuGet.Packaging.Core.NuspecCoreReader reader = new NuspecCoreReader(specstr);
string pkgdesc = reader.GetDescription();
string pkgid = reader.GetId();
var version = reader.GetVersion();
path = Path.Combine(nugetSettings.PackagesRootDir,
Path.Combine(pkgid,
Path.Combine(version.Version.ToString()),
$"{pkgid}-{version}.nupkg"));
var source = new FileInfo(initpath);
var dest = new FileInfo(path);
var destdir = new DirectoryInfo(dest.DirectoryName);
if (dest.Exists)
return BadRequest(new {error = "existant"});
if (!destdir.Exists) destdir.Create();
source.MoveTo(path);
}
}
}
}
}
else
{
return new BadRequestObjectResult(ViewData);
}
return Ok(ViewData);
}
[HttpGet("Packages/{spec}")]
public IActionResult Index(string spec)
{
if (string.IsNullOrEmpty(spec))
{
ViewData["warn"] = "no spec";
}
else
{
ViewData["spec"] = spec;
// TODO Assert valid sem ver spec
var filelst = new DirectoryInfo(nugetSettings.PackagesRootDir);
var fi = new FileInfo(spec);
var lst = filelst.GetDirectories(spec);
ViewData["lst"] = lst.Select(entry => entry.Name);
}
return Ok(ViewData);
}
[Authorize]
[HttpGet("api/get-key/{*apikey}")]
public IActionResult GetApiKey(string apiKey)
{
return Ok(protector.Protect(apiKey));
}
}
}