Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private feed implementation #156

Merged
merged 35 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b29393e
Private feed implementation
tomasfil Jun 17, 2024
3967c2b
case insensitive username
tomasfil Jun 17, 2024
42908d3
Cleanup
tomasfil Jun 17, 2024
50e8448
Cleanup
tomasfil Jun 17, 2024
0c596a6
Implement multi auth
tomasfil Jun 18, 2024
0871aa2
Prevent null exception.
tomasfil Jun 18, 2024
2452fa2
Requested changes
tomasfil Jun 19, 2024
66d0016
fix comment
tomasfil Jun 19, 2024
0faf0e1
Documentation
tomasfil Jun 19, 2024
19ce37b
Update config docs
tomasfil Jun 19, 2024
b01e8e8
Update apikeys docs
tomasfil Jun 19, 2024
4c7c179
feedback impl
tomasfil Jun 27, 2024
9ff7a0a
Tests
tomasfil Jun 27, 2024
2685f4c
Integration tests for basic authentication
tomasfil Jun 27, 2024
b2e467e
CodeMaid
tomasfil Jun 27, 2024
8af4c64
Remove redundant call
tomasfil Jun 27, 2024
ddfac3e
Move auth handler and tests
tomasfil Jun 27, 2024
1463c75
ApiKey to object
tomasfil Jun 27, 2024
15319b2
Move auth config to extend BaGetterApplication
tomasfil Jun 27, 2024
13b9312
Merge branch 'main' into main
tomasfil Jun 27, 2024
31b2b22
Api keys move into auth
tomasfil Jun 28, 2024
1ba8428
docs
tomasfil Jun 28, 2024
351b491
formatting
tomasfil Jun 28, 2024
818c578
VS Package manager console guide
tomasfil Jul 1, 2024
6379f24
remove extra space
tomasfil Jul 1, 2024
c9fef19
Unify line style
tomasfil Jul 1, 2024
6e4bf54
Allow auth policies config
tomasfil Sep 5, 2024
d7eb09b
Using
tomasfil Sep 5, 2024
b1d06d9
Move auth config to Bagetter project
tomasfil Sep 5, 2024
cb65543
Remove sealed class
tomasfil Sep 5, 2024
901d731
Auth options comments
tomasfil Sep 5, 2024
075db6d
Fix tests
tomasfil Sep 5, 2024
6b63d79
Merge branch 'main' into main
seriouz Sep 7, 2024
59071b9
Fix build
tomasfil Oct 21, 2024
1a3b3ea
Merge branch 'main' into main
tomasfil Oct 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ To do so, you can insert the desired API key in the `ApiKey` field.
}
```

You can also use the `ApiKeys` array in order to manage multiple API keys for multiple teams/developers.

```json
{
"Authentication": {
"ApiKeys": [
{
"Key" : "NUGET-SERVER-API-KEY-1"
},
{
"Key" : "NUGET-SERVER-API-KEY-2"
}
]
...
}
...
}
```

Both `ApiKey` and `ApiKeys` work in conjunction additively eg.: `or` `||` logical operator.

Users will now have to provide the API key to push packages:

```shell
Expand Down Expand Up @@ -200,11 +221,54 @@ Pushing a package with a pre-release version like "3.1.0-SNAPSHOT" will overwrit

A private feed requires users to authenticate before accessing packages.

:::warning
You can require that users provide a username and password to access the nuget feed.
To do so, you can insert the credentials in the `Authentication` section.

Private feeds are not supported at this time! See [this pull request](https://github.com/loic-sharma/BaGet/pull/69) for more information.
```json
{
"Authentication": {
"Credentials": [
{
"Username": "username",
"Password": "password"
}
]
...
}
...
}
```

:::
Users will now have to provide the username and password to fetch and download packages.
Regenhardt marked this conversation as resolved.
Show resolved Hide resolved

How to add private nuget feed:

1. Download the latest NuGet executable.
2. Open a Command Prompt and change the path to the nuget.exe location.
3. The command from the example below stores a token in the %AppData%\NuGet\NuGet.config file. Your original credentials cannot be obtained from this token.


```shell
NuGet Sources Add -Name "localhost" -Source "http://localhost:5000/v3/index.json" -UserName "username" -Password "password"
```

If you are unable to connect to the feed by using encrypted credentials, store your credentials in clear text:

```shell
NuGet Sources Add -Name "localhost" -Source "http://localhost:5000/v3/index.json" -UserName "username" -Password "password" -StorePasswordInClearText
```

If you have already stored a token instead of storing the credentials as clear text, update the definition in the %AppData%\NuGet\NuGet.config file by using the following command:

```shell
NuGet Sources Update -Name "localhost" -Source "http://localhost:5000/v3/index.json" -UserName "username" -Password "password" -StorePasswordInClearText
```

The commands are slightly different when using the Package Manager console in Visual Studio:

```shell
dotnet nuget add source "http://localhost:5000/v3/index.json" --name "bagetter" --username "username" --password "password"
```

## Database configuration

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Core.Configuration;
using Microsoft.Extensions.Options;

namespace BaGetter.Core;

public class ApiKeyAuthenticationService : IAuthenticationService
{
private readonly string _apiKey;
private readonly ApiKey[] _apiKeys;

public ApiKeyAuthenticationService(IOptionsSnapshot<BaGetterOptions> options)
{
ArgumentNullException.ThrowIfNull(options);

_apiKey = string.IsNullOrEmpty(options.Value.ApiKey) ? null : options.Value.ApiKey;
_apiKeys = options.Value.Authentication?.ApiKeys ?? [];
}

public Task<bool> AuthenticateAsync(string apiKey, CancellationToken cancellationToken)
Expand All @@ -22,8 +26,8 @@ public Task<bool> AuthenticateAsync(string apiKey, CancellationToken cancellatio
private bool Authenticate(string apiKey)
{
// No authentication is necessary if there is no required API key.
if (_apiKey == null) return true;
if (_apiKey == null && (_apiKeys.Length == 0)) return true;

return _apiKey == apiKey;
return _apiKey == apiKey || _apiKeys.Any(x => x.Key.Equals(apiKey));
}
}
12 changes: 12 additions & 0 deletions src/BaGetter.Core/Authentication/AuthenticationConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BaGetter.Authentication;
public static class AuthenticationConstants
{
public const string NugetBasicAuthenticationScheme = "NugetBasicAuthentication";
public const string NugetUserPolicy = "NuGetUserPolicy";
}
11 changes: 11 additions & 0 deletions src/BaGetter.Core/Configuration/ApiKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BaGetter.Core.Configuration;
public class ApiKey
{
public string Key { get; set; }
}
6 changes: 5 additions & 1 deletion src/BaGetter.Core/Configuration/BaGetterOptions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using BaGetter.Core.Configuration;

namespace BaGetter.Core;

public class BaGetterOptions
{
/// <summary>
/// The API Key required to authenticate package
/// operations. If empty, package operations do not require authentication.
/// operations. If <see cref="ApiKeys"/> and <see cref="ApiKey"/> are not set, package operations do not require authentication.

Check warning on line 9 in src/BaGetter.Core/Configuration/BaGetterOptions.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

XML comment has cref attribute 'ApiKeys' that could not be resolved
/// </summary>
public string ApiKey { get; set; }

Expand Down Expand Up @@ -64,4 +66,6 @@
public HealthCheckOptions HealthCheck { get; set; }

public StatisticsOptions Statistics { get; set; }

public NugetAuthenticationOptions Authentication { get; set; }
}
16 changes: 16 additions & 0 deletions src/BaGetter.Core/Configuration/NugetAuthenticationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using BaGetter.Core.Configuration;

namespace BaGetter.Core;

public sealed class NugetAuthenticationOptions
{
/// <summary>
/// Username and password credentials for downloading packages.
/// </summary>
public NugetCredentials[] Credentials { get; set; }

/// <summary>
/// Api keys for pushing packages into the feed.
/// </summary>
public ApiKey[] ApiKeys { get; set; }
tomasfil marked this conversation as resolved.
Show resolved Hide resolved
}
8 changes: 8 additions & 0 deletions src/BaGetter.Core/Configuration/NugetCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BaGetter.Core;

public sealed class NugetCredentials
{
public string Username { get; set; }

public string Password { get; set; }
}
4 changes: 2 additions & 2 deletions src/BaGetter.Core/Extensions/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace BaGetter.Core;

public static partial class DependencyInjectionExtensions
{
public static IServiceCollection AddBaGetterApplication(
public static BaGetterApplication AddBaGetterApplication(
this IServiceCollection services,
Action<BaGetterApplication> configureAction)
{
Expand All @@ -29,7 +29,7 @@ public static IServiceCollection AddBaGetterApplication(

services.AddFallbackServices();

return services;
return app;
}

/// <summary>
Expand Down
100 changes: 100 additions & 0 deletions src/BaGetter.Web/Authentication/NugetBasicAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text;
using System.Threading.Tasks;
using System;
using BaGetter.Core;
using System.Linq;

namespace BaGetter.Web.Authentication;

public class NugetBasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IOptions<BaGetterOptions> bagetterOptions;

public NugetBasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IOptions<BaGetterOptions> bagetterOptions)
: base(options, logger, encoder)
{
this.bagetterOptions = bagetterOptions;
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (IsAnonymousAllowed())
{
return CreateAnonymousAuthenticatonResult();
}

if (!Request.Headers.TryGetValue("Authorization", out var auth))
return Task.FromResult(AuthenticateResult.NoResult());

string username = null;
string password = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(auth);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split([':'], 2);
username = credentials[0];
password = credentials[1];
}
catch
{
return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
}

if (!ValidateCredentials(username, password))
return Task.FromResult(AuthenticateResult.Fail("Invalid Username or Password"));

return CreateUserAuthenticatonResult(username);
}

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers.WWWAuthenticate = "Basic realm=\"NuGet Server\"";
await base.HandleChallengeAsync(properties);
}

private Task<AuthenticateResult> CreateAnonymousAuthenticatonResult()
{
Claim[] claims = [new Claim(ClaimTypes.Anonymous, string.Empty)];
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

return Task.FromResult(AuthenticateResult.Success(ticket));
}

private Task<AuthenticateResult> CreateUserAuthenticatonResult(string username)
{
Claim[] claims = [new Claim(ClaimTypes.Name, username)];
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

return Task.FromResult(AuthenticateResult.Success(ticket));
}

private bool IsAnonymousAllowed()
{
return bagetterOptions.Value.Authentication is null ||
bagetterOptions.Value.Authentication.Credentials is null ||
bagetterOptions.Value.Authentication.Credentials.Length == 0 ||
bagetterOptions.Value.Authentication.Credentials.All(a => string.IsNullOrWhiteSpace(a.Username) && string.IsNullOrWhiteSpace(a.Password));
}

private bool ValidateCredentials(string username, string password)
{
return bagetterOptions.Value.Authentication.Credentials.Any(a => a.Username.Equals(username, StringComparison.OrdinalIgnoreCase) && a.Password == password);
}
}
4 changes: 4 additions & 0 deletions src/BaGetter.Web/Controllers/PackageContentController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NuGet.Versioning;

Expand All @@ -12,6 +14,8 @@ namespace BaGetter.Web;
/// The Package Content resource, used to download content from packages.
/// See: https://docs.microsoft.com/nuget/api/package-base-address-resource
/// </summary>

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class PackageContentController : Controller
{
private readonly IPackageContentService _content;
Expand Down
4 changes: 4 additions & 0 deletions src/BaGetter.Web/Controllers/PackageMetadataController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NuGet.Versioning;

Expand All @@ -12,6 +14,8 @@ namespace BaGetter.Web;
/// The Package Metadata resource, used to fetch packages' information.
/// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
/// </summary>

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class PackageMetadataController : Controller
{
private readonly IPackageMetadataService _metadata;
Expand Down
3 changes: 3 additions & 0 deletions src/BaGetter.Web/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BaGetter.Web;

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class SearchController : Controller
{
private readonly ISearchService _searchService;
Expand Down
4 changes: 4 additions & 0 deletions src/BaGetter.Web/Controllers/ServiceIndexController.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BaGetter.Web;

/// <summary>
/// The NuGet Service Index. This aids NuGet client to discover this server's services.
/// </summary>

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
tomasfil marked this conversation as resolved.
Show resolved Hide resolved
public class ServiceIndexController : Controller
{
private readonly IServiceIndexService _serviceIndex;
Expand Down
Loading
Loading