1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 17:12:49 -05:00

[AC-1200] Admin Console code ownership - move OrganizationFeatures (#3369)

This commit is contained in:
Thomas Rittson
2023-10-27 07:47:44 +10:00
committed by GitHub
parent 26dd8b0e47
commit ad230fb6a5
66 changed files with 151 additions and 152 deletions

View File

@ -0,0 +1,87 @@
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
[Route("organizations/{orgId}/auth-requests")]
[Authorize("Application")]
[RequireFeature(FeatureFlagKeys.TrustedDeviceEncryption)]
public class OrganizationAuthRequestsController : Controller
{
private readonly IAuthRequestRepository _authRequestRepository;
private readonly ICurrentContext _currentContext;
private readonly IAuthRequestService _authRequestService;
private readonly IUpdateOrganizationAuthRequestCommand _updateOrganizationAuthRequestCommand;
public OrganizationAuthRequestsController(IAuthRequestRepository authRequestRepository,
ICurrentContext currentContext, IAuthRequestService authRequestService,
IUpdateOrganizationAuthRequestCommand updateOrganizationAuthRequestCommand)
{
_authRequestRepository = authRequestRepository;
_currentContext = currentContext;
_authRequestService = authRequestService;
_updateOrganizationAuthRequestCommand = updateOrganizationAuthRequestCommand;
}
[HttpGet("")]
public async Task<ListResponseModel<PendingOrganizationAuthRequestResponseModel>> GetPendingRequests(Guid orgId)
{
await ValidateAdminRequest(orgId);
var authRequests = await _authRequestRepository.GetManyPendingByOrganizationIdAsync(orgId);
var responses = authRequests
.Select(a => new PendingOrganizationAuthRequestResponseModel(a))
.ToList();
return new ListResponseModel<PendingOrganizationAuthRequestResponseModel>(responses);
}
[HttpPost("{requestId}")]
public async Task UpdateAuthRequest(Guid orgId, Guid requestId, [FromBody] AdminAuthRequestUpdateRequestModel model)
{
await ValidateAdminRequest(orgId);
var authRequest =
(await _authRequestRepository.GetManyAdminApprovalRequestsByManyIdsAsync(orgId, new[] { requestId })).FirstOrDefault();
if (authRequest == null || authRequest.OrganizationId != orgId)
{
throw new NotFoundException();
}
await _updateOrganizationAuthRequestCommand.UpdateAsync(authRequest.Id, authRequest.UserId, model.RequestApproved, model.EncryptedUserKey);
}
[HttpPost("deny")]
public async Task BulkDenyRequests(Guid orgId, [FromBody] BulkDenyAdminAuthRequestRequestModel model)
{
await ValidateAdminRequest(orgId);
var authRequests = await _authRequestRepository.GetManyAdminApprovalRequestsByManyIdsAsync(orgId, model.Ids);
foreach (var authRequest in authRequests)
{
await _authRequestService.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId,
new AuthRequestUpdateRequestModel { RequestApproved = false, });
}
}
private async Task ValidateAdminRequest(Guid orgId)
{
if (!await _currentContext.ManageResetPassword(orgId))
{
throw new UnauthorizedAccessException();
}
}
}

View File

@ -0,0 +1,215 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
[Authorize("Application")]
[Route("organizations/connections")]
public class OrganizationConnectionsController : Controller
{
private readonly ICreateOrganizationConnectionCommand _createOrganizationConnectionCommand;
private readonly IUpdateOrganizationConnectionCommand _updateOrganizationConnectionCommand;
private readonly IDeleteOrganizationConnectionCommand _deleteOrganizationConnectionCommand;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings;
private readonly ILicensingService _licensingService;
public OrganizationConnectionsController(
ICreateOrganizationConnectionCommand createOrganizationConnectionCommand,
IUpdateOrganizationConnectionCommand updateOrganizationConnectionCommand,
IDeleteOrganizationConnectionCommand deleteOrganizationConnectionCommand,
IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
IGlobalSettings globalSettings,
ILicensingService licensingService)
{
_createOrganizationConnectionCommand = createOrganizationConnectionCommand;
_updateOrganizationConnectionCommand = updateOrganizationConnectionCommand;
_deleteOrganizationConnectionCommand = deleteOrganizationConnectionCommand;
_organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
_globalSettings = globalSettings;
_licensingService = licensingService;
}
[HttpGet("enabled")]
public bool ConnectionsEnabled()
{
return _globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication;
}
[HttpPost]
public async Task<OrganizationConnectionResponseModel> CreateConnection([FromBody] OrganizationConnectionRequestModel model)
{
if (!await HasPermissionAsync(model?.OrganizationId))
{
throw new BadRequestException($"You do not have permission to create a connection of type {model.Type}.");
}
if (await HasConnectionTypeAsync(model, null, model.Type))
{
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
}
switch (model.Type)
{
case OrganizationConnectionType.CloudBillingSync:
return await CreateOrUpdateOrganizationConnectionAsync<BillingSyncConfig>(null, model, ValidateBillingSyncConfig);
case OrganizationConnectionType.Scim:
return await CreateOrUpdateOrganizationConnectionAsync<ScimConfig>(null, model);
default:
throw new BadRequestException($"Unknown Organization connection Type: {model.Type}");
}
}
[HttpPut("{organizationConnectionId}")]
public async Task<OrganizationConnectionResponseModel> UpdateConnection(Guid organizationConnectionId, [FromBody] OrganizationConnectionRequestModel model)
{
if (model == null)
{
throw new NotFoundException();
}
var existingOrganizationConnection = await _organizationConnectionRepository.GetByIdOrganizationIdAsync(organizationConnectionId, model.OrganizationId);
if (existingOrganizationConnection == null)
{
throw new NotFoundException();
}
if (!await HasPermissionAsync(model?.OrganizationId, model?.Type))
{
throw new BadRequestException("You do not have permission to update this connection.");
}
if (await HasConnectionTypeAsync(model, organizationConnectionId, model.Type))
{
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
}
switch (model.Type)
{
case OrganizationConnectionType.CloudBillingSync:
return await CreateOrUpdateOrganizationConnectionAsync<BillingSyncConfig>(organizationConnectionId, model, ValidateBillingSyncConfig);
case OrganizationConnectionType.Scim:
return await CreateOrUpdateOrganizationConnectionAsync<ScimConfig>(organizationConnectionId, model);
default:
throw new BadRequestException($"Unknown Organization connection Type: {model.Type}");
}
}
[HttpGet("{organizationId}/{type}")]
public async Task<OrganizationConnectionResponseModel> GetConnection(Guid organizationId, OrganizationConnectionType type)
{
if (!await HasPermissionAsync(organizationId, type))
{
throw new BadRequestException($"You do not have permission to retrieve a connection of type {type}.");
}
var connections = await GetConnectionsAsync(organizationId, type);
var connection = connections.FirstOrDefault(c => c.Type == type);
switch (type)
{
case OrganizationConnectionType.CloudBillingSync:
if (!_globalSettings.SelfHosted)
{
throw new BadRequestException($"Cannot get a {type} connection outside of a self-hosted instance.");
}
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
case OrganizationConnectionType.Scim:
return new OrganizationConnectionResponseModel(connection, typeof(ScimConfig));
default:
throw new BadRequestException($"Unknown Organization connection Type: {type}");
}
}
[HttpDelete("{organizationConnectionId}")]
[HttpPost("{organizationConnectionId}/delete")]
public async Task DeleteConnection(Guid organizationConnectionId)
{
var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId);
if (connection == null)
{
throw new NotFoundException();
}
if (!await HasPermissionAsync(connection.OrganizationId, connection.Type))
{
throw new BadRequestException($"You do not have permission to remove this connection of type {connection.Type}.");
}
await _deleteOrganizationConnectionCommand.DeleteAsync(connection);
}
private async Task<ICollection<OrganizationConnection>> GetConnectionsAsync(Guid organizationId, OrganizationConnectionType type) =>
await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, type);
private async Task<bool> HasConnectionTypeAsync(OrganizationConnectionRequestModel model, Guid? connectionId,
OrganizationConnectionType type)
{
var existingConnections = await GetConnectionsAsync(model.OrganizationId, type);
return existingConnections.Any(c => c.Type == model.Type && (!connectionId.HasValue || c.Id != connectionId.Value));
}
private async Task<bool> HasPermissionAsync(Guid? organizationId, OrganizationConnectionType? type = null)
{
if (!organizationId.HasValue)
{
return false;
}
return type switch
{
OrganizationConnectionType.Scim => await _currentContext.ManageScim(organizationId.Value),
_ => await _currentContext.OrganizationOwner(organizationId.Value),
};
}
private async Task ValidateBillingSyncConfig(OrganizationConnectionRequestModel<BillingSyncConfig> typedModel)
{
if (!_globalSettings.SelfHosted)
{
throw new BadRequestException($"Cannot create a {typedModel.Type} connection outside of a self-hosted instance.");
}
var license = await _licensingService.ReadOrganizationLicenseAsync(typedModel.OrganizationId);
if (!_licensingService.VerifyLicense(license))
{
throw new BadRequestException("Cannot verify license file.");
}
typedModel.ParsedConfig.CloudOrganizationId = license.Id;
}
private async Task<OrganizationConnectionResponseModel> CreateOrUpdateOrganizationConnectionAsync<T>(
Guid? organizationConnectionId,
OrganizationConnectionRequestModel model,
Func<OrganizationConnectionRequestModel<T>, Task> validateAction = null)
where T : IConnectionConfig
{
var typedModel = new OrganizationConnectionRequestModel<T>(model);
if (validateAction != null)
{
await validateAction(typedModel);
}
var data = typedModel.ToData(organizationConnectionId);
var connection = organizationConnectionId.HasValue
? await _updateOrganizationConnectionCommand.UpdateAsync(data)
: await _createOrganizationConnectionCommand.CreateAsync(data);
return new OrganizationConnectionResponseModel(connection, typeof(T));
}
}

View File

@ -0,0 +1,150 @@
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
[Route("organizations")]
[Authorize("Application")]
public class OrganizationDomainController : Controller
{
private readonly ICreateOrganizationDomainCommand _createOrganizationDomainCommand;
private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;
private readonly IDeleteOrganizationDomainCommand _deleteOrganizationDomainCommand;
private readonly IGetOrganizationDomainByIdOrganizationIdQuery _getOrganizationDomainByIdAndOrganizationIdQuery;
private readonly IGetOrganizationDomainByOrganizationIdQuery _getOrganizationDomainByOrganizationIdQuery;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationDomainRepository _organizationDomainRepository;
public OrganizationDomainController(
ICreateOrganizationDomainCommand createOrganizationDomainCommand,
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
IDeleteOrganizationDomainCommand deleteOrganizationDomainCommand,
IGetOrganizationDomainByIdOrganizationIdQuery getOrganizationDomainByIdAndOrganizationIdQuery,
IGetOrganizationDomainByOrganizationIdQuery getOrganizationDomainByOrganizationIdQuery,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
_createOrganizationDomainCommand = createOrganizationDomainCommand;
_verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;
_deleteOrganizationDomainCommand = deleteOrganizationDomainCommand;
_getOrganizationDomainByIdAndOrganizationIdQuery = getOrganizationDomainByIdAndOrganizationIdQuery;
_getOrganizationDomainByOrganizationIdQuery = getOrganizationDomainByOrganizationIdQuery;
_currentContext = currentContext;
_organizationRepository = organizationRepository;
_organizationDomainRepository = organizationDomainRepository;
}
[HttpGet("{orgId}/domain")]
public async Task<ListResponseModel<OrganizationDomainResponseModel>> Get(Guid orgId)
{
await ValidateOrganizationAccessAsync(orgId);
var domains = await _getOrganizationDomainByOrganizationIdQuery
.GetDomainsByOrganizationIdAsync(orgId);
var response = domains.Select(x => new OrganizationDomainResponseModel(x)).ToList();
return new ListResponseModel<OrganizationDomainResponseModel>(response);
}
[HttpGet("{orgId}/domain/{id}")]
public async Task<OrganizationDomainResponseModel> Get(Guid orgId, Guid id)
{
await ValidateOrganizationAccessAsync(orgId);
var organizationDomain = await _getOrganizationDomainByIdAndOrganizationIdQuery
.GetOrganizationDomainByIdOrganizationIdAsync(id, orgId);
if (organizationDomain is null)
{
throw new NotFoundException();
}
return new OrganizationDomainResponseModel(organizationDomain);
}
[HttpPost("{orgId}/domain")]
public async Task<OrganizationDomainResponseModel> Post(Guid orgId,
[FromBody] OrganizationDomainRequestModel model)
{
await ValidateOrganizationAccessAsync(orgId);
var organizationDomain = new OrganizationDomain
{
OrganizationId = orgId,
Txt = model.Txt,
DomainName = model.DomainName.ToLower()
};
organizationDomain = await _createOrganizationDomainCommand.CreateAsync(organizationDomain);
return new OrganizationDomainResponseModel(organizationDomain);
}
[HttpPost("{orgId}/domain/{id}/verify")]
public async Task<OrganizationDomainResponseModel> Verify(Guid orgId, Guid id)
{
await ValidateOrganizationAccessAsync(orgId);
var organizationDomain = await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, orgId);
if (organizationDomain is null)
{
throw new NotFoundException();
}
organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain);
return new OrganizationDomainResponseModel(organizationDomain);
}
[HttpDelete("{orgId}/domain/{id}")]
[HttpPost("{orgId}/domain/{id}/remove")]
public async Task RemoveDomain(Guid orgId, Guid id)
{
await ValidateOrganizationAccessAsync(orgId);
var domain = await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, orgId);
if (domain is null)
{
throw new NotFoundException();
}
await _deleteOrganizationDomainCommand.DeleteAsync(domain);
}
[AllowAnonymous]
[HttpPost("domain/sso/details")] // must be post to accept email cleanly
public async Task<OrganizationDomainSsoDetailsResponseModel> GetOrgDomainSsoDetails(
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
{
var ssoResult = await _organizationDomainRepository.GetOrganizationDomainSsoDetailsAsync(model.Email);
if (ssoResult is null)
{
throw new NotFoundException("Claimed org domain not found");
}
return new OrganizationDomainSsoDetailsResponseModel(ssoResult);
}
private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)
{
if (!await _currentContext.ManageSso(orgIdGuid))
{
throw new UnauthorizedAccessException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
}
}

View File

@ -2,6 +2,7 @@
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -10,7 +11,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;

View File

@ -11,6 +11,7 @@ using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
@ -20,7 +21,6 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;