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

DEV-303 - Client certificate authentication for users #274

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 25 additions & 3 deletions .github/workflows/base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ on:
docker-tag:
required: true
type: string
docker-registry:
required: false
type: string
default: docker.eventstore.com/eventstore-ce/eventstoredb-ce
build-matrix:
required: false
type: string
default: '["Streams", "PersistentSubscriptions", "Operations", "UserManagement", "ProjectionManagement"]'
test-matrix:
required: false
type: string
default: '["Streams", "PersistentSubscriptions", "Operations", "UserManagement", "ProjectionManagement"]'

jobs:
test:
Expand All @@ -15,7 +27,8 @@ jobs:
matrix:
framework: [ net6.0, net7.0, net8.0 ]
os: [ ubuntu-latest ]
test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ]
build: ${{fromJson(inputs.build-matrix)}}
test: ${{fromJson(inputs.test-matrix)}}
configuration: [ release ]
runs-on: ${{ matrix.os }}
name: EventStore.Client.${{ matrix.test }}/${{ matrix.os }}/${{ matrix.framework }}/${{ inputs.docker-tag }}
Expand All @@ -25,10 +38,18 @@ jobs:
- shell: bash
run: |
git fetch --prune --unshallow

- name: Login to Cloudsmith
uses: docker/login-action@v3
with:
registry: docker.eventstore.com
username: ${{ secrets.CLOUDSMITH_CICD_USER }}
password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }}

- name: Pull EventStore Image
shell: bash
run: |
docker pull ghcr.io/eventstore/eventstore:${{ inputs.docker-tag }}
docker pull ${{ inputs.docker-registry }}:${{ inputs.docker-tag }}
- name: Install dotnet SDKs
uses: actions/setup-dotnet@v3
with:
Expand All @@ -39,11 +60,12 @@ jobs:
- name: Compile
shell: bash
run: |
dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client.${{ matrix.test }}
dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client.${{ matrix.build }}
- name: Run Tests
shell: bash
env:
ES_DOCKER_TAG: ${{ inputs.docker-tag }}
ES_DOCKER_REGISTRY: ${{ inputs.docker-registry }}
run: |
sudo ./gencert.sh
dotnet test --configuration ${{ matrix.configuration }} --blame \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ jobs:
uses: ./.github/workflows/base.yml
with:
docker-tag: ci
secrets: inherit
1 change: 1 addition & 0 deletions .github/workflows/dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ jobs:
uses: ./.github/workflows/base.yml
with:
docker-tag: ${{ inputs.version }}
secrets: inherit
18 changes: 18 additions & 0 deletions .github/workflows/ee.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Test EE

on:
pull_request:
push:
branches:
- master
tags:
- v*

jobs:
test:
uses: ./.github/workflows/base.yml
with:
docker-tag: 24.2.0-jammy
docker-registry: docker.eventstore.com/eventstore-ee/eventstoredb-commercial
test-matrix: '["Plugins"]'
secrets: inherit
1 change: 1 addition & 0 deletions .github/workflows/lts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ jobs:
uses: ./.github/workflows/base.yml
with:
docker-tag: lts
secrets: inherit
1 change: 1 addition & 0 deletions .github/workflows/previous-lts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ jobs:
uses: ./.github/workflows/base.yml
with:
docker-tag: previous-lts
secrets: inherit
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ jobs:
framework: [ net8.0 ]
services:
esdb:
image: ghcr.io/eventstore/eventstore:lts
image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts
credentials:
username: ${{ secrets.CLOUDSMITH_CICD_USER }}
password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }}
env:
EVENTSTORE_INSECURE: true
EVENTSTORE_MEM_DB: false
Expand Down
7 changes: 7 additions & 0 deletions EventStore.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManag
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests.Common", "test\EventStore.Client.Tests.Common\EventStore.Client.Tests.Common.csproj", "{E326832D-DE52-4DE4-9E54-C800908B75F3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Plugins.Tests", "test\EventStore.Client.Plugins.Tests\EventStore.Client.Plugins.Tests.csproj", "{315B38AF-4574-4E25-992B-CA0D24C95884}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -94,6 +96,10 @@ Global
{E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.Build.0 = Debug|Any CPU
{E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.ActiveCfg = Release|Any CPU
{E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.Build.0 = Release|Any CPU
{315B38AF-4574-4E25-992B-CA0D24C95884}.Debug|x64.ActiveCfg = Debug|Any CPU
{315B38AF-4574-4E25-992B-CA0D24C95884}.Debug|x64.Build.0 = Debug|Any CPU
{315B38AF-4574-4E25-992B-CA0D24C95884}.Release|x64.ActiveCfg = Release|Any CPU
{315B38AF-4574-4E25-992B-CA0D24C95884}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D3744A86-DD35-4104-AAEE-84B79062C4A2} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8}
Expand All @@ -109,5 +115,6 @@ Global
{6CEB731F-72E1-461F-A6B3-54DBF3FD786C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
{22634CEE-4F7B-4679-A48D-38A2A8580ECA} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
{E326832D-DE52-4DE4-9E54-C800908B75F3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
{315B38AF-4574-4E25-992B-CA0D24C95884} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
EndGlobalSection
EndGlobal
18 changes: 11 additions & 7 deletions gencert.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ Write-Host ">> Generating certificate..."
New-Item -ItemType Directory -Path .\certs -Force

# Set permissions for the directory
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F"

# Pull the Docker image
docker pull eventstore/es-gencert-cli:1.0.2
docker pull ghcr.io/eventstore/es-gencert-cli:1.3

# Create CA certificate
docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca
docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-ca -out /tmp/ca

# Create node certificate
docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost
docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost

# Create admin user
docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin

# Create an invalid user
docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid

# Set permissions recursively for the directory
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F"

Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root
10 changes: 7 additions & 3 deletions gencert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ mkdir -p certs

chmod 0755 ./certs

docker pull eventstore/es-gencert-cli:1.0.2
docker pull ghcr.io/eventstore/es-gencert-cli:1.3

docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca
docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-ca -out /tmp/ca

docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost
docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost

docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin

docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid

chmod -R 0755 ./certs

Expand Down
7 changes: 4 additions & 3 deletions src/EventStore.Client.Common/EventStoreCallOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public static CallOptions CreateNonStreaming(
static CallOptions Create(
EventStoreClientSettings settings, TimeSpan? deadline,
UserCredentials? userCredentials, CancellationToken cancellationToken
) =>
new(
) {
return new(
cancellationToken: cancellationToken,
deadline: DeadlineAfter(deadline),
headers: new() {
Expand All @@ -64,11 +64,12 @@ static CallOptions Create(
}
)
);
}

static DateTime? DeadlineAfter(TimeSpan? timeoutAfter) =>
!timeoutAfter.HasValue
? new DateTime?()
: timeoutAfter.Value == TimeSpan.MaxValue || timeoutAfter.Value == InfiniteTimeSpan
? DateTime.MaxValue
: DateTime.UtcNow.Add(timeoutAfter.Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,48 +118,92 @@ private static CreateReq.Types.AllOptions AllOptionsForCreateProto(Position posi
/// Creates a persistent subscription.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public async Task CreateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings,
TimeSpan? deadline = null, UserCredentials? userCredentials = null,
CancellationToken cancellationToken = default) =>
await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials,
cancellationToken)
public async Task CreateToStreamAsync(
string streamName, string groupName, PersistentSubscriptionSettings settings,
TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) =>
await CreateInternalAsync(
streamName,
groupName,
null,
settings,
deadline,
userCredentials,
userCertificate,
cancellationToken
)
.ConfigureAwait(false);

/// <summary>
/// Creates a persistent subscription.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
[Obsolete("CreateAsync is no longer supported. Use CreateToStreamAsync instead.", false)]
public async Task CreateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings,
TimeSpan? deadline = null, UserCredentials? userCredentials = null,
CancellationToken cancellationToken = default) =>
await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials,
cancellationToken)
public async Task CreateAsync(
string streamName, string groupName, PersistentSubscriptionSettings settings,
TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) =>
await CreateInternalAsync(
streamName,
groupName,
null,
settings,
deadline,
userCredentials,
userCertificate,
cancellationToken
)
.ConfigureAwait(false);

/// <summary>
/// Creates a filtered persistent subscription to $all.
/// </summary>
public async Task CreateToAllAsync(string groupName, IEventFilter eventFilter,
public async Task CreateToAllAsync(
string groupName, IEventFilter eventFilter,
PersistentSubscriptionSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null,
CancellationToken cancellationToken = default) =>
await CreateInternalAsync(SystemStreams.AllStream, groupName, eventFilter, settings, deadline,
userCredentials, cancellationToken)
UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) =>
await CreateInternalAsync(
SystemStreams.AllStream,
groupName,
eventFilter,
settings,
deadline,
userCredentials,
userCertificate,
cancellationToken
)
.ConfigureAwait(false);

/// <summary>
/// Creates a persistent subscription to $all.
/// </summary>
public async Task CreateToAllAsync(string groupName, PersistentSubscriptionSettings settings,
TimeSpan? deadline = null, UserCredentials? userCredentials = null,
CancellationToken cancellationToken = default) =>
await CreateInternalAsync(SystemStreams.AllStream, groupName, null, settings, deadline, userCredentials,
cancellationToken)
public async Task CreateToAllAsync(
string groupName, PersistentSubscriptionSettings settings,
TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) =>
await CreateInternalAsync(
SystemStreams.AllStream,
groupName,
null,
settings,
deadline,
userCredentials,
userCertificate,
cancellationToken
)
.ConfigureAwait(false);

private async Task CreateInternalAsync(string streamName, string groupName, IEventFilter? eventFilter,
private async Task CreateInternalAsync(
string streamName, string groupName, IEventFilter? eventFilter,
PersistentSubscriptionSettings settings, TimeSpan? deadline, UserCredentials? userCredentials,
CancellationToken cancellationToken) {
UserCertificate? userCertificate,
CancellationToken cancellationToken
) {
if (streamName is null) {
throw new ArgumentNullException(nameof(streamName));
}
Expand Down Expand Up @@ -198,7 +242,7 @@ private async Task CreateInternalAsync(string streamName, string groupName, IEve
"The specified consumer strategy is not supported, specify one of the SystemConsumerStrategies");
}

var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false);
var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false);

if (streamName == SystemStreams.AllStream &&
!channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ partial class EventStorePersistentSubscriptionsClient {
/// Deletes a persistent subscription.
/// </summary>
[Obsolete("DeleteAsync is no longer supported. Use DeleteToStreamAsync instead.", false)]
public Task DeleteAsync(string streamName, string groupName, TimeSpan? deadline = null,
UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) =>
DeleteToStreamAsync(streamName, groupName, deadline, userCredentials, cancellationToken);
public Task DeleteAsync(
string streamName, string groupName, TimeSpan? deadline = null,
UserCredentials? userCredentials = null, UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) =>
DeleteToStreamAsync(streamName, groupName, deadline, userCredentials, userCertificate, cancellationToken);

/// <summary>
/// Deletes a persistent subscription.
/// </summary>
public async Task DeleteToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null,
UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) {
var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false);
public async Task DeleteToStreamAsync(
string streamName, string groupName, TimeSpan? deadline = null,
UserCredentials? userCredentials = null, UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) {
var channelInfo =
await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false);

if (streamName == SystemStreams.AllStream &&
!channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) {
Expand Down Expand Up @@ -47,9 +54,19 @@ public async Task DeleteToStreamAsync(string streamName, string groupName, TimeS
/// <summary>
/// Deletes a persistent subscription to $all.
/// </summary>
public async Task DeleteToAllAsync(string groupName, TimeSpan? deadline = null,
UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) =>
await DeleteToStreamAsync(SystemStreams.AllStream, groupName, deadline, userCredentials, cancellationToken)
public async Task DeleteToAllAsync(
string groupName, TimeSpan? deadline = null,
UserCredentials? userCredentials = null, UserCertificate? userCertificate = null,
CancellationToken cancellationToken = default
) =>
await DeleteToStreamAsync(
SystemStreams.AllStream,
groupName,
deadline,
userCredentials,
userCertificate,
cancellationToken
)
.ConfigureAwait(false);
}
}
Loading
Loading