diff --git a/src/isn/IDataProtector.cs b/src/isn/IDataProtector.cs deleted file mode 100644 index d91ea23..0000000 --- a/src/isn/IDataProtector.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace isn -{ - public interface IDataProtector - { - string Protect(string data); - string UnProtect(string data); - } - - public class DefaultDataProtector : IDataProtector - { - private byte delta = 145; - - public DefaultDataProtector() - { - - } - - public string Protect(string data) - { - List protd = new List(); - StringBuilder sb = new StringBuilder(); - foreach (byte c in Encoding.UTF8.GetBytes(data)) - { - protd.Add((byte) (c ^ delta)); - } - return System.Convert.ToBase64String(protd.ToArray()); - } - - public string UnProtect(string data) - { - if (data==null) return null; - StringBuilder sb = new StringBuilder(); - List unps = new List(); - - foreach (byte c in System.Convert.FromBase64CharArray(data.ToCharArray(),0,data.Length)) - { - unps.Add((byte) (c ^ delta)); - } - return Encoding.UTF8.GetString(unps.ToArray()); - } - } - -} \ No newline at end of file diff --git a/src/isn/Settings.cs b/src/isn/Settings.cs index 98c6eee..3324c8b 100644 --- a/src/isn/Settings.cs +++ b/src/isn/Settings.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; +using System.Security.Cryptography; +using System.Text; using Newtonsoft.Json; namespace isn { public class SourceSettings { + private RSA rsa; + /// /// Protected API Key /// @@ -22,19 +25,25 @@ namespace isn /// public string Alias { get; set; } + public SourceSettings() + { + rsa = RSA.Create(); + } public string GetClearApiKey() { if (!string.IsNullOrEmpty(ApiKey)) return ApiKey; - return ProtectedApiKey = Protector.UnProtect(ApiKey); + return + Encoding.UTF8.GetString( + rsa.Decrypt(Encoding.UTF8.GetBytes(ProtectedApiKey), + RSAEncryptionPadding.Pkcs1)); } public void SetApiKey(string key) { - ApiKey = key; - ProtectedApiKey = Protector.Protect(key); + ApiKey = Encoding.UTF8.GetString( + rsa.Encrypt(Encoding.UTF8.GetBytes(key), + RSAEncryptionPadding.Pkcs1)); } - - public static IDataProtector Protector { get; private set ; } = new DefaultDataProtector(); } public class Settings diff --git a/src/isnd/Controllers/ApiKeysController.cs b/src/isnd/Controllers/ApiKeysController.cs index 968c924..d2d8ea9 100644 --- a/src/isnd/Controllers/ApiKeysController.cs +++ b/src/isnd/Controllers/ApiKeysController.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Options; using isnd.Data; using isnd.Entities; using isnd.Data.ApiKeys; +using isnd.Interfaces; namespace isnd.Controllers @@ -23,17 +24,20 @@ namespace isnd.Controllers private readonly ApplicationDbContext dbContext; private readonly IsndSettings isndSettings; private readonly UserManager _userManager; - + private readonly IApiKeyProvider apiKeyProvider; private readonly IDataProtector protector; public ApiKeysController(ApplicationDbContext dbContext, IOptions isndSettingsOptions, IDataProtectionProvider provider, - UserManager userManager) + UserManager userManager, + IApiKeyProvider apiKeyProvider + ) { this.dbContext = dbContext; this.isndSettings = isndSettingsOptions.Value; protector = provider.CreateProtector(isndSettings.ProtectionTitle); _userManager = userManager; + this.apiKeyProvider = apiKeyProvider; } [HttpGet] @@ -57,17 +61,17 @@ namespace isnd.Controllers [HttpPost] public async Task Create(CreateModel model) { - string userid = User.FindFirstValue(ClaimTypes.NameIdentifier); - IQueryable userKeys = GetUserKeys(); + string userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + IQueryable userKeys = apiKeyProvider.GetUserKeys(User.Identity.Name); if (userKeys.Count() >= isndSettings.MaxUserKeyCount) { ModelState.AddModelError(null, "Maximum key count reached"); return View(); } - ApiKey newKey = new ApiKey { UserId = userid, Name = model.Name, - CreationDate = DateTimeOffset.Now.ToUniversalTime() }; - _ = dbContext.ApiKeys.Add(newKey); - _ = await dbContext.SaveChangesAsync(); + model.UserId = userId; + + ApiKey newKey = await apiKeyProvider.CreateApiKeyAsync(model); + return View("Details", new DetailModel { Name = newKey.Name, ProtectedValue = protector.Protect(newKey.Id), ApiKey = newKey }); @@ -79,7 +83,6 @@ namespace isnd.Controllers string userid = User.FindFirstValue(ClaimTypes.NameIdentifier); ApiKey key = await dbContext.ApiKeys.FirstOrDefaultAsync(k => k.Id == id && k.UserId == userid); return View(new DeleteModel { ApiKey = key }); - } [HttpPost] diff --git a/src/isnd/Data/ApiKeys/CreateModel.cs b/src/isnd/Data/ApiKeys/CreateModel.cs index af6b909..41496bf 100644 --- a/src/isnd/Data/ApiKeys/CreateModel.cs +++ b/src/isnd/Data/ApiKeys/CreateModel.cs @@ -9,6 +9,8 @@ namespace isnd.Data.ApiKeys [Display(Name = "Key Name")] public string Name { get; set; } public string UserId { get; set; } + + public int ValidityPeriodInDays { get; set; } } } \ No newline at end of file diff --git a/src/isnd/Interfaces/IApiKeyProvider.cs b/src/isnd/Interfaces/IApiKeyProvider.cs new file mode 100644 index 0000000..f1d1d2d --- /dev/null +++ b/src/isnd/Interfaces/IApiKeyProvider.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Threading.Tasks; +using isnd.Data.ApiKeys; + + +namespace isnd.Interfaces +{ + public interface IApiKeyProvider + { + Task CreateApiKeyAsync(CreateModel model); + IQueryable GetUserKeys(string identityName); + } +} \ No newline at end of file diff --git a/src/isnd/Services/ApiKeyProvider.cs b/src/isnd/Services/ApiKeyProvider.cs new file mode 100644 index 0000000..4f226db --- /dev/null +++ b/src/isnd/Services/ApiKeyProvider.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using isnd.Data; +using isnd.Data.ApiKeys; +using isnd.Entities; +using isnd.Interfaces; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace isnd; + + +public class ApiKeyProvider : IApiKeyProvider +{ + private readonly IsndSettings isndSettings; + private readonly IDataProtector protector; + private readonly ApplicationDbContext dbContext; + + public ApiKeyProvider( + ApplicationDbContext dbContext, + IDataProtectionProvider dataProtectionProvider, IOptions isndSettingsOptions) + { + this.dbContext = dbContext; + isndSettings = isndSettingsOptions.Value; + protector = dataProtectionProvider.CreateProtector(isndSettings.ProtectionTitle); + } + + public async Task CreateApiKeyAsync(CreateModel model) + { + var newKey = new ApiKey{ + UserId = model.UserId, + CreationDate = DateTime.Now, + Name = model.Name, + ValidityPeriodInDays = model.ValidityPeriodInDays + }; + + _ = dbContext.ApiKeys.Add(newKey); + _ = await dbContext.SaveChangesAsync(); + return newKey; + } + + public IQueryable GetUserKeys(string identityName) + { + return dbContext.ApiKeys.Include(k => k.User).Where(k => k.User.UserName == identityName); + } +} \ No newline at end of file diff --git a/src/isnd/Startup.cs b/src/isnd/Startup.cs index d6ff1e0..20cdfb4 100644 --- a/src/isnd/Startup.cs +++ b/src/isnd/Startup.cs @@ -19,6 +19,7 @@ using System; using Microsoft.OpenApi.Models; using System.IO; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.DataProtection; namespace isnd { @@ -76,9 +77,9 @@ namespace isnd .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSingleton(); - services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { diff --git a/test/isn.tests/PushTest.cs b/test/isn.tests/PushTest.cs index 7e910b4..11e0826 100644 --- a/test/isn.tests/PushTest.cs +++ b/test/isn.tests/PushTest.cs @@ -12,20 +12,6 @@ namespace isn.tests { public class Tests { - - [Fact] - public void HaveADefaultDataProtector() - { - string pass = "a lame and big pass"; - isn.IDataProtector _protector = new isn.DefaultDataProtector(); - string protectedPassword = _protector.Protect(pass); - string unprotectedPassword = _protector.UnProtect(protectedPassword); - Console.WriteLine(protectedPassword); - Assert.Equal(pass, unprotectedPassword); - Assert.True(protectedPassword != null); - Assert.True(protectedPassword.Length > 0); - } - [Fact] public async Task TestHttpClient() { diff --git a/test/isnd.tests/UnitTestWebHost.cs b/test/isnd.tests/UnitTestWebHost.cs index 36b901b..d02baf8 100644 --- a/test/isnd.tests/UnitTestWebHost.cs +++ b/test/isnd.tests/UnitTestWebHost.cs @@ -114,7 +114,7 @@ namespace isnd.host.tests } public string SPIIndexURI { - get => server.Addresses.First() + "/v3/index"; + get => server.Addresses.First() + "/v3/index.json"; } [Fact] @@ -170,5 +170,40 @@ namespace isnd.host.tests } } + + [Fact] + public async Task TestPackagePush() + { + var logger = new TestLogger(); + SourceRepository repository = Repository.Factory.GetCoreV3(SPIIndexURI); + PackageUpdateResource pushRes = await repository.GetResourceAsync(); + SymbolPackageUpdateResourceV3 symbolPackageResource = await repository.GetResourceAsync(); + + await pushRes.Push(new List{ "../../../../../src/isnd/bin/Release/isnd.1.1.4.nupkg" }, null, + 5000, false, GetApiKey, GetSymbolsApiKey, false, false, symbolPackageResource, logger); + } + + private string GetSymbolsApiKey(string apiUrl) + { + return GetApiKey(apiUrl); + } + + private string GetApiKey(string apiUrl) + { + return server.ProtectedTestingApiKey; + } + } + + internal class TestLogger : NuGet.Common.LoggerBase + { + public override void Log(ILogMessage message) + { + Console.WriteLine(message.Message); + } + + public async override Task LogAsync(ILogMessage message) + { + Log(message); + } } } diff --git a/test/isnd.tests/WebServerFixture.cs b/test/isnd.tests/WebServerFixture.cs index 4c711ff..76b2c3e 100644 --- a/test/isnd.tests/WebServerFixture.cs +++ b/test/isnd.tests/WebServerFixture.cs @@ -1,10 +1,20 @@ using System; using System.Collections.Generic; +using System.Linq; +using isn; +using isnd.Data; +using isnd.Entities; +using isnd.Interfaces; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Identity; +using Microsoft.CodeAnalysis.Options; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Xunit; namespace isnd.tests @@ -15,6 +25,13 @@ namespace isnd.tests { public IWebHost Host { get; private set;} public List Addresses { get; private set; } = new List(); + public Microsoft.Extensions.Logging.ILogger Logger { get; internal set; } + + private IsndSettings siteSettings; + + public IDataProtector DataProtector { get; private set; } + public string ProtectedTestingApiKey { get; internal set; } + public ApplicationUser TestingUser { get; private set; } public WebServerFixture() { @@ -40,14 +57,60 @@ namespace isnd.tests config.AddJsonFile("appsettings.Development.json", false); }); + + Host = webhostBuilder.Build(); + + var logFactory = Host.Services.GetRequiredService(); + Logger = logFactory.CreateLogger(); + + Host.Start(); //Starts listening on the configured addresses. var server = Host.Services.GetRequiredService(); + + var addressFeature = server.Features.Get(); + foreach (var address in addressFeature.Addresses) { Addresses.Add(address); } + siteSettings = Host.Services.GetRequiredService>().Value; + + DataProtector = Host.Services.GetRequiredService() + .CreateProtector(siteSettings.ProtectionTitle); + + var dbContext = Host.Services.GetRequiredService(); + string testingUserName = "Tester"; + TestingUser = dbContext.Users.FirstOrDefault(u=>u.UserName==testingUserName); + if (TestingUser==null) + { + var userManager = Host.Services.GetRequiredService>(); + TestingUser = new ApplicationUser + { + UserName=testingUserName + }; + + var result = userManager.CreateAsync(TestingUser).Result; + + Assert.True(result.Succeeded); + TestingUser = dbContext.Users.FirstOrDefault(u=>u.UserName==testingUserName); + } + var testKey = dbContext.ApiKeys.FirstOrDefault(k=>k.UserId==TestingUser.Id); + if (testKey == null) + { + var keyProvider = Host.Services.GetService(); + var apiKeyQuery = new Data.ApiKeys.CreateModel + { + Name = "Testing Key", + UserId = TestingUser.Id, + ValidityPeriodInDays = 1 + }; + + testKey = keyProvider.CreateApiKeyAsync(apiKeyQuery).Result; + + } + ProtectedTestingApiKey = DataProtector.Protect(testKey.Id); } } } \ No newline at end of file