mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 01:22:50 -05:00
[SM-394] Secrets Manager (#2164)
Long lived feature branch for Secrets Manager Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com> Co-authored-by: Thomas Avery <tavery@bitwarden.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com>
This commit is contained in:
@ -2,6 +2,9 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -23,8 +26,8 @@ public class ClientStore : IClientStore
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly IApiKeyRepository _apiKeyRepository;
|
||||
|
||||
public ClientStore(
|
||||
IInstallationRepository installationRepository,
|
||||
@ -36,8 +39,8 @@ public class ClientStore : IClientStore
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository)
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
IApiKeyRepository apiKeyRepository)
|
||||
{
|
||||
_installationRepository = installationRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -48,133 +51,219 @@ public class ClientStore : IClientStore
|
||||
_currentContext = currentContext;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
}
|
||||
|
||||
public async Task<Client> FindClientByIdAsync(string clientId)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && clientId.StartsWith("installation."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out Guid id))
|
||||
{
|
||||
var installation = await _installationRepository.GetByIdAsync(id);
|
||||
if (installation != null)
|
||||
{
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"installation.{installation.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(installation.Key.Sha256()) },
|
||||
AllowedScopes = new string[] { "api.push", "api.licensing", "api.installation" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = installation.Enabled,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, installation.Id.ToString())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return await CreateInstallationClientAsync(clientId);
|
||||
}
|
||||
else if (_globalSettings.SelfHosted && clientId.StartsWith("internal.") &&
|
||||
|
||||
if (_globalSettings.SelfHosted && clientId.StartsWith("internal.") &&
|
||||
CoreHelpers.SettingHasValue(_globalSettings.InternalIdentityKey))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1)
|
||||
{
|
||||
var id = idParts[1];
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"internal.{id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "internal" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = true,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, id)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (clientId.StartsWith("organization."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(id);
|
||||
if (org != null)
|
||||
{
|
||||
var orgApiKey = (await _organizationApiKeyRepository
|
||||
.GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default))
|
||||
.First();
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"organization.{org.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "api.organization" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
Enabled = org.Enabled && org.UseApi,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, org.Id.ToString())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (clientId.StartsWith("user."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user != null)
|
||||
{
|
||||
var claims = new Collection<ClientClaim>()
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||
new ClientClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external")
|
||||
};
|
||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
|
||||
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
|
||||
{
|
||||
var upperValue = claim.Value.ToUpperInvariant();
|
||||
var isBool = upperValue == "TRUE" || upperValue == "FALSE";
|
||||
claims.Add(isBool ?
|
||||
new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean) :
|
||||
new ClientClaim(claim.Key, claim.Value)
|
||||
);
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "api" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Claims = claims
|
||||
};
|
||||
}
|
||||
}
|
||||
return CreateInternalClient(clientId);
|
||||
}
|
||||
|
||||
return _staticClientStore.ApiClients.ContainsKey(clientId) ?
|
||||
_staticClientStore.ApiClients[clientId] : null;
|
||||
if (clientId.StartsWith("organization."))
|
||||
{
|
||||
return await CreateOrganizationClientAsync(clientId);
|
||||
}
|
||||
|
||||
if (clientId.StartsWith("user."))
|
||||
{
|
||||
return await CreateUserClientAsync(clientId);
|
||||
}
|
||||
|
||||
if (_staticClientStore.ApiClients.ContainsKey(clientId))
|
||||
{
|
||||
return _staticClientStore.ApiClients[clientId];
|
||||
}
|
||||
|
||||
return await CreateApiKeyClientAsync(clientId);
|
||||
}
|
||||
|
||||
private async Task<Client> CreateApiKeyClientAsync(string clientId)
|
||||
{
|
||||
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(new Guid(clientId));
|
||||
|
||||
if (apiKey == null || apiKey.ExpireAt <= DateTime.Now)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var client = new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(apiKey.ClientSecret.Sha256()) },
|
||||
AllowedScopes = apiKey.GetScopes(),
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Properties = new Dictionary<string, string> {
|
||||
{"encryptedPayload", apiKey.EncryptedPayload},
|
||||
},
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),
|
||||
},
|
||||
};
|
||||
|
||||
switch (apiKey)
|
||||
{
|
||||
case ServiceAccountApiKeyDetails key:
|
||||
client.Claims.Add(new ClientClaim(Claims.Organization, key.ServiceAccountOrganizationId.ToString()));
|
||||
break;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private async Task<Client> CreateUserClientAsync(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var claims = new Collection<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||
new(JwtClaimTypes.AuthenticationMethod, "Application", "external"),
|
||||
};
|
||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
|
||||
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
|
||||
{
|
||||
var upperValue = claim.Value.ToUpperInvariant();
|
||||
var isBool = upperValue is "TRUE" or "FALSE";
|
||||
claims.Add(isBool
|
||||
? new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean)
|
||||
: new ClientClaim(claim.Key, claim.Value)
|
||||
);
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
|
||||
AllowedScopes = new[] { "api" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Claims = claims,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Client> CreateOrganizationClientAsync(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(id);
|
||||
if (org == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var orgApiKey = (await _organizationApiKeyRepository
|
||||
.GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default))
|
||||
.First();
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"organization.{org.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) },
|
||||
AllowedScopes = new[] { ApiScopes.ApiOrganization },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
Enabled = org.Enabled && org.UseApi,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, org.Id.ToString()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private Client CreateInternalClient(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = idParts[1];
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"internal.{id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },
|
||||
AllowedScopes = new[] { ApiScopes.Internal },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = true,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, id),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Client> CreateInstallationClientAsync(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out Guid id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var installation = await _installationRepository.GetByIdAsync(id);
|
||||
if (installation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"installation.{installation.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(installation.Key.Sha256()) },
|
||||
AllowedScopes = new[]
|
||||
{
|
||||
ApiScopes.ApiPush,
|
||||
ApiScopes.ApiLicensing,
|
||||
ApiScopes.ApiInstallation,
|
||||
},
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = installation.Enabled,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, installation.Id.ToString()),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user