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,