diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs
new file mode 100644
index 0000000000..da646f01d2
--- /dev/null
+++ b/src/Api/Public/Controllers/CollectionsController.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Bit.Core;
+using Bit.Core.Models.Api.Public;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Bit.Api.Public.Controllers
+{
+ [Route("public/collections")]
+ [Authorize("Organization")]
+ public class CollectionsController : Controller
+ {
+ private readonly ICollectionRepository _collectionRepository;
+ private readonly ICollectionService _collectionService;
+ private readonly CurrentContext _currentContext;
+
+ public CollectionsController(
+ ICollectionRepository collectionRepository,
+ ICollectionService collectionService,
+ CurrentContext currentContext)
+ {
+ _collectionRepository = collectionRepository;
+ _collectionService = collectionService;
+ _currentContext = currentContext;
+ }
+
+ ///
+ /// Retrieve a collection.
+ ///
+ ///
+ /// Retrieves the details of an existing collection. You need only supply the unique collection identifier
+ /// that was returned upon collection creation.
+ ///
+ /// The identifier of the collection to be retrieved.
+ [HttpGet("{id}")]
+ [ProducesResponseType(typeof(CollectionResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Get(Guid id)
+ {
+ var collectionWithGroups = await _collectionRepository.GetByIdWithGroupsAsync(id);
+ var collection = collectionWithGroups?.Item1;
+ if(collection == null || collection.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ var response = new CollectionResponseModel(collection, collectionWithGroups.Item2);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// List all collections.
+ ///
+ ///
+ /// Returns a list of your organization's collections.
+ /// Collection objects listed in this call do not include information about their associated groups.
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)]
+ public async Task List()
+ {
+ var collections = await _collectionRepository.GetManyByOrganizationIdAsync(
+ _currentContext.OrganizationId.Value);
+ // TODO: Get all CollectionGroup associations for the organization and marry them up here for the response.
+ var collectionResponses = collections.Select(c => new CollectionResponseModel(c, null));
+ var response = new ListResponseModel(collectionResponses);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Update a collection.
+ ///
+ ///
+ /// Updates the specified collection object. If a property is not provided,
+ /// the value of the existing property will be reset.
+ ///
+ /// The identifier of the collection to be updated.
+ /// The request model.
+ [HttpPut("{id}")]
+ [ProducesResponseType(typeof(CollectionResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Put(Guid id, [FromBody]CollectionCreateUpdateRequestModel model)
+ {
+ var existingCollection = await _collectionRepository.GetByIdAsync(id);
+ if(existingCollection == null || existingCollection.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ var updatedCollection = model.ToCollection(existingCollection);
+ var associations = model.Groups?.Select(c => c.ToSelectionReadOnly());
+ await _collectionService.SaveAsync(updatedCollection, associations);
+ var response = new CollectionResponseModel(updatedCollection, associations);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Delete a collection.
+ ///
+ ///
+ /// Permanently deletes a collection. This cannot be undone.
+ ///
+ /// The identifier of the collection to be deleted.
+ [HttpDelete("{id}")]
+ [ProducesResponseType((int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Delete(Guid id)
+ {
+ var collection = await _collectionRepository.GetByIdAsync(id);
+ if(collection == null || collection.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ await _collectionRepository.DeleteAsync(collection);
+ return new OkResult();
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Request/CollectionCreateUpdateRequestModel.cs b/src/Core/Models/Api/Public/Request/CollectionCreateUpdateRequestModel.cs
new file mode 100644
index 0000000000..3d877108bf
--- /dev/null
+++ b/src/Core/Models/Api/Public/Request/CollectionCreateUpdateRequestModel.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public class CollectionCreateUpdateRequestModel : GroupBaseModel
+ {
+ ///
+ /// The associated groups that this collection is assigned to.
+ ///
+ public IEnumerable Groups { get; set; }
+
+ public Collection ToCollection(Collection existingCollection)
+ {
+ // TODO
+ // existingCollection.ExternalId = ExternalId;
+ return existingCollection;
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Response/CollectionResponseModel.cs b/src/Core/Models/Api/Public/Response/CollectionResponseModel.cs
new file mode 100644
index 0000000000..25155bc996
--- /dev/null
+++ b/src/Core/Models/Api/Public/Response/CollectionResponseModel.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Models.Api.Public
+{
+ ///
+ /// A collection.
+ ///
+ public class CollectionResponseModel : IResponseModel
+ {
+ public CollectionResponseModel(Collection collection, IEnumerable groups)
+ {
+ if(collection == null)
+ {
+ throw new ArgumentNullException(nameof(collection));
+ }
+
+ Id = collection.Id;
+ // ExternalId = group.ExternalId; TODO: Add external is for referencing purposes
+ Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c));
+ }
+
+ ///
+ /// String representing the object's type. Objects of the same type share the same properties.
+ ///
+ /// collection
+ [Required]
+ public string Object => "collection";
+ ///
+ /// The collection's unique identifier.
+ ///
+ /// 539a36c5-e0d2-4cf9-979e-51ecf5cf6593
+ [Required]
+ public Guid Id { get; set; }
+ ///
+ /// The associated groups that this collection is assigned to.
+ ///
+ public IEnumerable Groups { get; set; }
+ }
+}