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

feat(API): add grpc API and service account authentication #33

Merged
merged 5 commits into from
Mar 25, 2021
Merged
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
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-grpc": {
"version": "2.36.0",
"commands": [
"dotnet-grpc"
]
}
}
}
4 changes: 4 additions & 0 deletions .github/workflows/dotnet-pack-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Test
run: ./build.sh --target test --no-logo
pack:
Expand All @@ -26,6 +28,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Pack
run: ./build.sh --no-logo --version 0.0.0-dev.${GITHUB_SHA::8} --release-notes 'This is a package built from master branch.' --target Pack
- uses: actions/upload-artifact@v2
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/dotnet-release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: .NET Release

on: [workflow_dispatch]
on:
push:
branches:
- master
- next

jobs:
test:
Expand All @@ -11,6 +15,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Test
run: ./build.sh --target test --no-logo
semantic-release:
Expand All @@ -24,6 +30,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v2
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Test
run: ./build.sh --target test --no-logo
6 changes: 3 additions & 3 deletions .github/workflows/security-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Code Security Testing

on:
push:
branches:
- master
pull_request:
branches:
- master
Expand Down Expand Up @@ -39,6 +36,9 @@ jobs:
with:
dotnet-version: 5.0.100

- name: Setup dotnet tools
run: dotnet tool restore

- name: Build
run: ./build.sh --target compile --no-logo

Expand Down
16 changes: 13 additions & 3 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"verifyConditions": ["@semantic-release/github"],
"addChannel": ["@semantic-release/github"],
"branches": [
"master",
{
"name": "next",
"prerelease": "prerelease"
}
],
"plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/github"],
"prepare": [
[
"@semantic-release/exec",
Expand All @@ -13,7 +19,11 @@
[
"@semantic-release/github",
{
"assets": [{ "path": "artifacts/*.nupkg" }]
"assets": [
{
"path": "artifacts/*.nupkg"
}
]
}
],
[
Expand Down
14 changes: 14 additions & 0 deletions Zitadel.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Spa.Dev", "tests\Zi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Test", "tests\Zitadel.Test\Zitadel.Test.csproj", "{44543DC1-97C5-4525-8EE4-16E5389FDF4E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Api", "src\Zitadel.Api\Zitadel.Api.csproj", "{0F83D36E-945F-4B70-A6E9-867C21C546EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Api.Access.Dev", "tests\Zitadel.Api.Access.Dev\Zitadel.Api.Access.Dev.csproj", "{B51464E4-58A6-4C41-A0BF-35245F0DD862}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -43,6 +47,8 @@ Global
{9AD0581A-82BA-4E96-A1EE-722D579C26D8} = {2462186B-61A8-476C-9674-176570BBEC35}
{23DE9A2C-E9A0-458A-8878-9224C49FFB3B} = {2462186B-61A8-476C-9674-176570BBEC35}
{44543DC1-97C5-4525-8EE4-16E5389FDF4E} = {2462186B-61A8-476C-9674-176570BBEC35}
{0F83D36E-945F-4B70-A6E9-867C21C546EB} = {47CEB49C-56A9-4BDF-BC66-54E407391D49}
{B51464E4-58A6-4C41-A0BF-35245F0DD862} = {2462186B-61A8-476C-9674-176570BBEC35}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BF13C638-6A5C-4BF6-89FF-1BAA3986F86A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -63,5 +69,13 @@ Global
{44543DC1-97C5-4525-8EE4-16E5389FDF4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44543DC1-97C5-4525-8EE4-16E5389FDF4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44543DC1-97C5-4525-8EE4-16E5389FDF4E}.Release|Any CPU.Build.0 = Release|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Release|Any CPU.Build.0 = Release|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
49 changes: 49 additions & 0 deletions src/Zitadel.Api/ClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using Zitadel.Authentication;
using Zitadel.Authentication.Credentials;

namespace Zitadel.Api
{
/// <summary>
/// Options for an API client.
/// </summary>
public record ClientOptions
{
/// <summary>
/// The API endpoint for the client. This will be the base url for the api calls.
/// </summary>
public string Endpoint { get; init; } = ZitadelDefaults.ZitadelApiEndpoint;

/// <summary>
/// The organizational context in the API. This essentially defines the "x-zitadel-orgid" header value
/// which provides the api with the orgId that the API call will be executed in.
/// This may be overwritten for specific calls.
/// </summary>
public string Organization { get; init; } = string.Empty;

/// <summary>
/// Authentication token for the client. This field may not be used in conjunction with
/// <see cref="ServiceAccountAuthentication"/>. Use this field to explicitly set the
/// Bearer token that will be transmitted to the API. If no authentication method is set,
/// each call must attach the authorization header.
/// </summary>
public string? Token { get; init; }

/// <summary>
/// Service Account authentication method. If this field is set, the API calls are
/// automatically authenticated with a <see cref="ServiceAccount"/> and the corresponding
/// <see cref="ServiceAccount.AuthOptions"/>. This will renew the access token if it is
/// expired.
/// </summary>
public (ServiceAccount Account, ServiceAccount.AuthOptions AuthOptions)? ServiceAccountAuthentication
{
get;
init;
}

/// <summary>
/// List of additional arbitrary headers that are attached to each call.
/// </summary>
public IDictionary<string, string>? AdditionalHeaders { get; init; }
}
}
75 changes: 75 additions & 0 deletions src/Zitadel.Api/Clients.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Net.Http;
using Caos.Zitadel.Admin.Api.V1;
using Caos.Zitadel.Auth.Api.V1;
using Caos.Zitadel.Management.Api.V1;
using Grpc.Core;
using Grpc.Net.Client;
using Zitadel.Authentication;

namespace Zitadel.Api
{
/// <summary>
/// Helper class to instantiate api service clients for the zitadel API with correct settings.
/// </summary>
public static class Clients
{
/// <summary>
/// Create a service client for the auth service.
/// </summary>
/// <param name="options">Options for the client like authorization method.</param>
/// <returns>The <see cref="Caos.Zitadel.Auth.Api.V1.AuthService.AuthServiceClient"/>.</returns>
public static AuthService.AuthServiceClient AuthService(ClientOptions options) =>
GetClient<AuthService.AuthServiceClient>(options);

/// <summary>
/// Create a service client for the admin service.
/// </summary>
/// <param name="options">Options for the client like authorization method.</param>
/// <returns>The <see cref="Caos.Zitadel.Admin.Api.V1.AdminService.AdminServiceClient"/>.</returns>
public static AdminService.AdminServiceClient AdminService(ClientOptions options) =>
GetClient<AdminService.AdminServiceClient>(options);

/// <summary>
/// Create a service client for the management service.
/// </summary>
/// <param name="options">Options for the client like authorization method.</param>
/// <returns>The <see cref="Caos.Zitadel.Management.Api.V1.ManagementService.ManagementServiceClient"/>.</returns>
public static ManagementService.ManagementServiceClient ManagementService(ClientOptions options) =>
GetClient<ManagementService.ManagementServiceClient>(options);

private static TClient GetClient<TClient>(ClientOptions options)
where TClient : ClientBase<TClient>
{
var httpClient = options.Token == null && options.ServiceAccountAuthentication != null
? new HttpClient(
new ServiceAccountHttpHandler(
options.ServiceAccountAuthentication.Value.Account,
options.ServiceAccountAuthentication.Value.AuthOptions))
: new HttpClient();

httpClient.DefaultRequestHeaders.Add(ZitadelDefaults.ZitadelOrgIdHeader, options.Organization);

if (options.Token != null)
{
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", options.Token);
}

if (options.AdditionalHeaders != null)
{
foreach (var (name, value) in options.AdditionalHeaders)
{
httpClient.DefaultRequestHeaders.Add(name, value);
}
}

var channel = GrpcChannel.ForAddress(
options.Endpoint,
new GrpcChannelOptions { HttpClient = httpClient });
var serviceType = typeof(TClient);

return Activator.CreateInstance(serviceType, channel) as TClient ??
throw new($"Could not instantiate type {serviceType}");
}
}
}
49 changes: 49 additions & 0 deletions src/Zitadel.Api/ServiceAccountHttpHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Zitadel.Authentication.Credentials;

namespace Zitadel.Api
{
internal class ServiceAccountHttpHandler : DelegatingHandler
{
private static readonly TimeSpan ServiceTokenLifetime = TimeSpan.FromHours(12);

private readonly ServiceAccount _account;
private readonly ServiceAccount.AuthOptions _options;

private DateTime _tokenExpiryDate;
private string? _token;

public ServiceAccountHttpHandler(ServiceAccount account, ServiceAccount.AuthOptions options)
: base(new HttpClientHandler())
{
_account = account;
_options = options;
}

protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
=> SendAsync(request, cancellationToken).Result;

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Headers.Authorization != null)
{
return await base.SendAsync(request, cancellationToken);
}

// When the token is not fetched or it is expired, re-fetch a service account token.
if (_token == null || _tokenExpiryDate < DateTime.UtcNow)
{
_token = await _account.AuthenticateAsync(_options);
_tokenExpiryDate = DateTime.UtcNow + ServiceTokenLifetime;
}

request.Headers.Authorization = new("Bearer", _token);
return await base.SendAsync(request, cancellationToken);
}
}
}
Loading