diff --git a/src/Api/Public/Controllers/GroupsController.cs b/src/Api/Public/Controllers/GroupsController.cs
index c049b7a37b..8333bc7368 100644
--- a/src/Api/Public/Controllers/GroupsController.cs
+++ b/src/Api/Public/Controllers/GroupsController.cs
@@ -1,5 +1,11 @@
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;
@@ -9,23 +15,121 @@ namespace Bit.Api.Public.Controllers
[Authorize("Organization")]
public class GroupsController : Controller
{
- ///
- /// Retrieves a specific product by unique id
- ///
- /// Awesomeness!
- /// Group created
- /// Group has missing/invalid values
- /// Oops! Can't create your product right now
- [HttpGet("{id}")]
- [ProducesResponseType(typeof(GroupResponseModel), 200)]
- public IActionResult Get(Guid id)
+ private readonly IGroupRepository _groupRepository;
+ private readonly IGroupService _groupService;
+ private readonly CurrentContext _currentContext;
+
+ public GroupsController(
+ IGroupRepository groupRepository,
+ IGroupService groupService,
+ CurrentContext currentContext)
{
- return new JsonResult(new GroupResponseModel(new Core.Models.Table.Group
+ _groupRepository = groupRepository;
+ _groupService = groupService;
+ _currentContext = currentContext;
+ }
+
+ ///
+ /// Retrieve a group.
+ ///
+ ///
+ /// Retrieves the details of an existing group. You need only supply the unique group identifier
+ /// that was returned upon group creation.
+ ///
+ /// The identifier of the group to be retrieved.
+ [HttpGet("{id}")]
+ [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Get(Guid id)
+ {
+ var group = await _groupRepository.GetByIdAsync(id);
+ if(group == null || group.OrganizationId != _currentContext.OrganizationId)
{
- Id = id,
- Name = "test",
- OrganizationId = Guid.NewGuid()
- }));
+ return new NotFoundResult();
+ }
+ var response = new GroupResponseModel(group);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// List all groups.
+ ///
+ ///
+ /// Returns a list of your organization's groups.
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)]
+ public async Task List()
+ {
+ var groups = await _groupRepository.GetManyByOrganizationIdAsync(_currentContext.OrganizationId.Value);
+ var groupResponses = groups.Select(g => new GroupResponseModel(g));
+ var response = new ListResponseModel(groupResponses);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Create a group.
+ ///
+ ///
+ /// Creates a new group object.
+ ///
+ [HttpPost]
+ [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ public async Task Post([FromBody]GroupCreateUpdateRequestModel model)
+ {
+ var group = model.ToGroup(_currentContext.OrganizationId.Value);
+ var associations = model.Collections?.Select(c => c.ToSelectionReadOnly());
+ await _groupService.SaveAsync(group, associations);
+ var response = new GroupResponseModel(group);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Update a group.
+ ///
+ ///
+ /// Updates the specified group object. If a property is not provided,
+ /// the value of the existing property will be reset.
+ ///
+ /// The identifier of the group to be updated.
+ [HttpPut("{id}")]
+ [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Put(Guid id, [FromBody]GroupCreateUpdateRequestModel model)
+ {
+ var existingGroup = await _groupRepository.GetByIdAsync(id);
+ if(existingGroup == null || existingGroup.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ var updatedGroup = model.ToGroup(existingGroup);
+ var associations = model.Collections?.Select(c => c.ToSelectionReadOnly());
+ await _groupService.SaveAsync(updatedGroup, associations);
+ var response = new GroupResponseModel(updatedGroup);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Delete a group.
+ ///
+ ///
+ /// Permanently deletes a group. This cannot be undone.
+ ///
+ /// The identifier of the group to be deleted.
+ [HttpDelete("{id}")]
+ [ProducesResponseType((int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Delete(Guid id)
+ {
+ var group = await _groupRepository.GetByIdAsync(id);
+ if(group == null || group.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ await _groupRepository.DeleteAsync(group);
+ return new OkResult();
}
}
}
diff --git a/src/Core/Models/Api/Public/GroupBaseModel.cs b/src/Core/Models/Api/Public/GroupBaseModel.cs
new file mode 100644
index 0000000000..8500341a74
--- /dev/null
+++ b/src/Core/Models/Api/Public/GroupBaseModel.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public abstract class GroupBaseModel
+ {
+ ///
+ /// The name of the group.
+ ///
+ /// Development Team
+ [Required]
+ [StringLength(100)]
+ public string Name { get; set; }
+ ///
+ /// Determines if this group can access all collections within the organization, or only the associated
+ /// collections. If set to true, this option overrides any collection assignments.
+ ///
+ [Required]
+ public bool? AccessAll { get; set; }
+ ///
+ /// External identifier linking this group to another system, such as a user directory.
+ ///
+ /// external_id_123456
+ [StringLength(300)]
+ public string ExternalId { get; set; }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Request/AssociationWithPermissionsRequestModel.cs b/src/Core/Models/Api/Public/Request/AssociationWithPermissionsRequestModel.cs
new file mode 100644
index 0000000000..c8b53d53cf
--- /dev/null
+++ b/src/Core/Models/Api/Public/Request/AssociationWithPermissionsRequestModel.cs
@@ -0,0 +1,31 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Newtonsoft.Json;
+using Bit.Core.Models.Data;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public class AssociationWithPermissionsRequestModel
+ {
+ ///
+ /// The associated object's unique identifier.
+ ///
+ /// bfbc8338-e329-4dc0-b0c9-317c2ebf1a09
+ [Required]
+ public Guid? Id { get; set; }
+ ///
+ /// When true, the read only permission will not allow the user or group to make changes to items.
+ ///
+ [Required]
+ public bool? ReadOnly { get; set; }
+
+ public SelectionReadOnly ToSelectionReadOnly()
+ {
+ return new SelectionReadOnly
+ {
+ Id = Id.Value,
+ ReadOnly = ReadOnly.Value
+ };
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Request/GroupCreateUpdateRequestModel.cs b/src/Core/Models/Api/Public/Request/GroupCreateUpdateRequestModel.cs
new file mode 100644
index 0000000000..87869d070c
--- /dev/null
+++ b/src/Core/Models/Api/Public/Request/GroupCreateUpdateRequestModel.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public class GroupCreateUpdateRequestModel : GroupBaseModel
+ {
+ ///
+ /// The associated collections that this group can access.
+ ///
+ public IEnumerable Collections { get; set; }
+
+ public Group ToGroup(Guid orgId)
+ {
+ return ToGroup(new Group
+ {
+ OrganizationId = orgId
+ });
+ }
+
+ public Group ToGroup(Group existingGroup)
+ {
+ existingGroup.Name = Name;
+ existingGroup.AccessAll = AccessAll.Value;
+ existingGroup.ExternalId = ExternalId;
+ return existingGroup;
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Response/GroupResponseModel.cs b/src/Core/Models/Api/Public/Response/GroupResponseModel.cs
index dd42c69202..321da1841d 100644
--- a/src/Core/Models/Api/Public/Response/GroupResponseModel.cs
+++ b/src/Core/Models/Api/Public/Response/GroupResponseModel.cs
@@ -1,12 +1,15 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Models.Table;
namespace Bit.Core.Models.Api.Public
{
- public class GroupResponseModel : ResponseModel
+ ///
+ /// A user group.
+ ///
+ public class GroupResponseModel : GroupBaseModel, IResponseModel
{
- public GroupResponseModel(Group group, string obj = "group")
- : base(obj)
+ public GroupResponseModel(Group group)
{
if(group == null)
{
@@ -14,16 +17,22 @@ namespace Bit.Core.Models.Api.Public
}
Id = group.Id;
- OrganizationId = group.OrganizationId;
Name = group.Name;
AccessAll = group.AccessAll;
ExternalId = group.ExternalId;
}
+ ///
+ /// String representing the object's type. Objects of the same type share the same properties.
+ ///
+ /// group
+ [Required]
+ public string Object => "group";
+ ///
+ /// The group's unique identifier.
+ ///
+ /// 539a36c5-e0d2-4cf9-979e-51ecf5cf6593
+ [Required]
public Guid Id { get; set; }
- public Guid OrganizationId { get; set; }
- public string Name { get; set; }
- public bool AccessAll { get; set; }
- public string ExternalId { get; set; }
}
}
diff --git a/src/Core/Models/Api/Public/Response/IResponseModel.cs b/src/Core/Models/Api/Public/Response/IResponseModel.cs
new file mode 100644
index 0000000000..55fdcbed44
--- /dev/null
+++ b/src/Core/Models/Api/Public/Response/IResponseModel.cs
@@ -0,0 +1,7 @@
+namespace Bit.Core.Models.Api.Public
+{
+ public interface IResponseModel
+ {
+ string Object { get; }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Response/ListResponseModel.cs b/src/Core/Models/Api/Public/Response/ListResponseModel.cs
new file mode 100644
index 0000000000..f65481b8ef
--- /dev/null
+++ b/src/Core/Models/Api/Public/Response/ListResponseModel.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public class ListResponseModel : IResponseModel where T : IResponseModel
+ {
+ public ListResponseModel(IEnumerable data, string continuationToken = null)
+ {
+ Data = data;
+ ContinuationToken = continuationToken;
+ }
+
+ ///
+ /// String representing the object's type. Objects of the same type share the same properties.
+ ///
+ /// list
+ [Required]
+ public string Object => "list";
+ ///
+ /// An array containing the actual response elements, paginated by any request parameters.
+ ///
+ [Required]
+ public IEnumerable Data { get; set; }
+ ///
+ /// A cursor for use in pagination.
+ ///
+ public string ContinuationToken { get; set; }
+ }
+}