diff --git a/src/Api/Public/Controllers/PoliciesController.cs b/src/Api/Public/Controllers/PoliciesController.cs
new file mode 100644
index 000000000..03e8fc62e
--- /dev/null
+++ b/src/Api/Public/Controllers/PoliciesController.cs
@@ -0,0 +1,135 @@
+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/policies")]
+ [Authorize("Organization")]
+ public class PoliciesController : Controller
+ {
+ private readonly IPolicyRepository _policyRepository;
+ private readonly IPolicyService _policyService;
+ private readonly CurrentContext _currentContext;
+
+ public PoliciesController(
+ IPolicyRepository policyRepository,
+ IPolicyService policyService,
+ CurrentContext currentContext)
+ {
+ _policyRepository = policyRepository;
+ _policyService = policyService;
+ _currentContext = currentContext;
+ }
+
+ ///
+ /// Retrieve a policy.
+ ///
+ ///
+ /// Retrieves the details of an existing policy. You need only supply the unique group identifier
+ /// that was returned upon policy creation.
+ ///
+ /// The identifier of the policy to be retrieved.
+ [HttpGet("{id}")]
+ [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Get(Guid id)
+ {
+ var policy = await _policyRepository.GetByIdAsync(id);
+ if(policy == null || policy.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ var response = new PolicyResponseModel(policy);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// List all policies.
+ ///
+ ///
+ /// Returns a list of your organization's policies.
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)]
+ public async Task List()
+ {
+ var policies = await _policyRepository.GetManyByOrganizationIdAsync(_currentContext.OrganizationId.Value);
+ var policyResponses = policies.Select(p => new PolicyResponseModel(p));
+ var response = new ListResponseModel(policyResponses);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Create a policy.
+ ///
+ ///
+ /// Creates a new policy object.
+ ///
+ /// The request model.
+ [HttpPost]
+ [ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ public async Task Post([FromBody]PolicyCreateRequestModel model)
+ {
+ var policy = model.ToPolicy(_currentContext.OrganizationId.Value);
+ await _policyService.SaveAsync(policy);
+ var response = new PolicyResponseModel(policy);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Update a policy.
+ ///
+ ///
+ /// Updates the specified policy object. If a property is not provided,
+ /// the value of the existing property will be reset.
+ ///
+ /// The identifier of the policy to be updated.
+ /// The request model.
+ [HttpPut("{id}")]
+ [ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Put(Guid id, [FromBody]PolicyUpdateRequestModel model)
+ {
+ var existingPolicy = await _policyRepository.GetByIdAsync(id);
+ if(existingPolicy == null || existingPolicy.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ var updatedPolicy = model.ToPolicy(existingPolicy);
+ await _policyService.SaveAsync(updatedPolicy);
+ var response = new PolicyResponseModel(updatedPolicy);
+ return new JsonResult(response);
+ }
+
+ ///
+ /// Delete a policy.
+ ///
+ ///
+ /// Permanently deletes a policy. This cannot be undone.
+ ///
+ /// The identifier of the policy to be deleted.
+ [HttpDelete("{id}")]
+ [ProducesResponseType((int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task Delete(Guid id)
+ {
+ var policy = await _policyRepository.GetByIdAsync(id);
+ if(policy == null || policy.OrganizationId != _currentContext.OrganizationId)
+ {
+ return new NotFoundResult();
+ }
+ await _policyRepository.DeleteAsync(policy);
+ return new OkResult();
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/PolicyBaseModel.cs b/src/Core/Models/Api/Public/PolicyBaseModel.cs
new file mode 100644
index 000000000..f0bc8f301
--- /dev/null
+++ b/src/Core/Models/Api/Public/PolicyBaseModel.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public abstract class PolicyBaseModel
+ {
+ ///
+ /// Determines if this policy is enabled and enforced.
+ ///
+ [Required]
+ public bool? Enabled { get; set; }
+ ///
+ /// Data for the policy.
+ ///
+ [StringLength(300)]
+ public Dictionary Data { get; set; }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Request/PolicyCreateRequestModel.cs b/src/Core/Models/Api/Public/Request/PolicyCreateRequestModel.cs
new file mode 100644
index 000000000..bc90d1e52
--- /dev/null
+++ b/src/Core/Models/Api/Public/Request/PolicyCreateRequestModel.cs
@@ -0,0 +1,23 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public class PolicyCreateRequestModel : PolicyUpdateRequestModel
+ {
+ ///
+ /// The type of policy.
+ ///
+ [Required]
+ public Enums.PolicyType? Type { get; set; }
+
+ public Policy ToPolicy(Guid orgId)
+ {
+ return ToPolicy(new Policy
+ {
+ OrganizationId = orgId
+ });
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Request/PolicyUpdateRequestModel.cs b/src/Core/Models/Api/Public/Request/PolicyUpdateRequestModel.cs
new file mode 100644
index 000000000..6d508de5e
--- /dev/null
+++ b/src/Core/Models/Api/Public/Request/PolicyUpdateRequestModel.cs
@@ -0,0 +1,15 @@
+using Bit.Core.Models.Table;
+using Newtonsoft.Json;
+
+namespace Bit.Core.Models.Api.Public
+{
+ public class PolicyUpdateRequestModel : PolicyBaseModel
+ {
+ public virtual Policy ToPolicy(Policy existingPolicy)
+ {
+ existingPolicy.Enabled = Enabled.GetValueOrDefault();
+ existingPolicy.Data = Data != null ? JsonConvert.SerializeObject(Data) : null;
+ return existingPolicy;
+ }
+ }
+}
diff --git a/src/Core/Models/Api/Public/Response/PolicyResponseModel.cs b/src/Core/Models/Api/Public/Response/PolicyResponseModel.cs
new file mode 100644
index 000000000..b849f1a79
--- /dev/null
+++ b/src/Core/Models/Api/Public/Response/PolicyResponseModel.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Bit.Core.Models.Table;
+using Newtonsoft.Json;
+
+namespace Bit.Core.Models.Api.Public
+{
+ ///
+ /// A policy.
+ ///
+ public class PolicyResponseModel : PolicyBaseModel, IResponseModel
+ {
+ public PolicyResponseModel(Policy policy)
+ {
+ if(policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ Id = policy.Id;
+ Type = policy.Type;
+ Enabled = policy.Enabled;
+ if(!string.IsNullOrWhiteSpace(policy.Data))
+ {
+ Data = JsonConvert.DeserializeObject>(policy.Data);
+ }
+ }
+
+ ///
+ /// String representing the object's type. Objects of the same type share the same properties.
+ ///
+ /// policy
+ [Required]
+ public string Object => "policy";
+ ///
+ /// The policy's unique identifier.
+ ///
+ /// 539a36c5-e0d2-4cf9-979e-51ecf5cf6593
+ [Required]
+ public Guid Id { get; set; }
+ ///
+ /// The type of policy.
+ ///
+ [Required]
+ public Enums.PolicyType? Type { get; set; }
+ }
+}