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; } + } +}