From fae4a335dc906b23c1afc6e61a2d0739ae6d1ecd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Apr 2020 11:29:19 -0400 Subject: [PATCH] public API for organization import (#707) --- .../Controllers/OrganizationController.cs | 59 ++++++++++ .../Request/OrganizationImportRequestModel.cs | 104 ++++++++++++++++++ src/Core/Services/IOrganizationService.cs | 2 +- .../Implementations/OrganizationService.cs | 2 +- 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/Api/Public/Controllers/OrganizationController.cs create mode 100644 src/Core/Models/Api/Public/Request/OrganizationImportRequestModel.cs diff --git a/src/Api/Public/Controllers/OrganizationController.cs b/src/Api/Public/Controllers/OrganizationController.cs new file mode 100644 index 000000000..996e26f13 --- /dev/null +++ b/src/Api/Public/Controllers/OrganizationController.cs @@ -0,0 +1,59 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Bit.Core; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Public; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Public.Controllers +{ + [Route("public/organization")] + [Authorize("Organization")] + public class OrganizationController : Controller + { + private readonly IOrganizationService _organizationService; + private readonly CurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; + + public OrganizationController( + IOrganizationService organizationService, + CurrentContext currentContext, + GlobalSettings globalSettings) + { + _organizationService = organizationService; + _currentContext = currentContext; + _globalSettings = globalSettings; + } + + /// + /// Import members and groups. + /// + /// + /// Import members and groups from an external system. + /// + /// The request model. + [HttpPost("import")] + [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] + public async Task Import([FromBody]OrganizationImportRequestModel model) + { + if (!_globalSettings.SelfHosted && + (model.Groups.Count() > 200 || model.Members.Count(u => !u.Deleted) > 1000)) + { + throw new BadRequestException("You cannot import this much data at once."); + } + + await _organizationService.ImportAsync( + _currentContext.OrganizationId.Value, + null, + model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)), + model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()), + model.Members.Where(u => u.Deleted).Select(u => u.ExternalId), + model.OverwriteExisting.GetValueOrDefault()); + return new OkResult(); + } + } +} diff --git a/src/Core/Models/Api/Public/Request/OrganizationImportRequestModel.cs b/src/Core/Models/Api/Public/Request/OrganizationImportRequestModel.cs new file mode 100644 index 000000000..45e140303 --- /dev/null +++ b/src/Core/Models/Api/Public/Request/OrganizationImportRequestModel.cs @@ -0,0 +1,104 @@ +using Bit.Core.Models.Business; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api.Public +{ + public class OrganizationImportRequestModel + { + /// + /// Groups to import. + /// + public OrganizationImportGroupRequestModel[] Groups { get; set; } + /// + /// Members to import. + /// + public OrganizationImportMemberRequestModel[] Members { get; set; } + /// + /// Determines if the data in this request should overwrite or append to the existing organization data. + /// + [Required] + public bool? OverwriteExisting { get; set; } + + public class OrganizationImportGroupRequestModel + { + /// + /// The name of the group. + /// + /// Development Team + [Required] + [StringLength(100)] + public string Name { get; set; } + /// + /// External identifier for reference or linking this group to another system, such as a user directory. + /// + /// external_id_123456 + [Required] + [StringLength(300)] + public string ExternalId { get; set; } + /// + /// The associated external ids for members in this group. + /// + public IEnumerable MemberExternalIds { get; set; } + + public ImportedGroup ToImportedGroup(Guid organizationId) + { + var importedGroup = new ImportedGroup + { + Group = new Table.Group + { + OrganizationId = organizationId, + Name = Name, + ExternalId = ExternalId + }, + ExternalUserIds = new HashSet(MemberExternalIds) + }; + + return importedGroup; + } + } + + public class OrganizationImportMemberRequestModel : IValidatableObject + { + /// + /// The member's email address. Required for non-deleted users. + /// + /// jsmith@example.com + [EmailAddress] + [StringLength(50)] + public string Email { get; set; } + /// + /// External identifier for reference or linking this member to another system, such as a user directory. + /// + /// external_id_123456 + [Required] + [StringLength(300)] + public string ExternalId { get; set; } + /// + /// Determines if this member should be removed from the organization during import. + /// + public bool Deleted { get; set; } + + public ImportedOrganizationUser ToImportedOrganizationUser() + { + var importedUser = new ImportedOrganizationUser + { + Email = Email.ToLowerInvariant(), + ExternalId = ExternalId + }; + + return importedUser; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(Email) && !Deleted) + { + yield return new ValidationResult("Email is required for enabled members.", + new string[] { nameof(Email) }); + } + } + } + } +} diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 872d25018..6b3d64027 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -45,7 +45,7 @@ namespace Bit.Core.Services Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds); Task GenerateLicenseAsync(Guid organizationId, Guid installationId); Task GenerateLicenseAsync(Organization organization, Guid installationId); - Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable groups, + Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting); Task RotateApiKeyAsync(Organization organization); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index db0d3fcc3..e3a9658e8 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1214,7 +1214,7 @@ namespace Bit.Core.Services } public async Task ImportAsync(Guid organizationId, - Guid importingUserId, + Guid? importingUserId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds,