diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index d5aaef9d89..0f6e643b9c 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -9,6 +9,14 @@
..\..\docker\Docker.dcproj
+
+
+
+
+
+
+
+
diff --git a/src/Api/Properties/launchSettings.json b/src/Api/Properties/launchSettings.json
index d56f373353..b846e65755 100644
--- a/src/Api/Properties/launchSettings.json
+++ b/src/Api/Properties/launchSettings.json
@@ -4,7 +4,7 @@
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:4000",
- "sslPort": 44377
+ "sslPort": 0
}
},
"profiles": {
diff --git a/src/Api/licensing.cer b/src/Api/licensing.cer
new file mode 100644
index 0000000000..0dbb09c3c6
Binary files /dev/null and b/src/Api/licensing.cer differ
diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj
index e2d119fba0..b68cd4f788 100644
--- a/src/Billing/Billing.csproj
+++ b/src/Billing/Billing.csproj
@@ -8,6 +8,10 @@
bitwarden-Billing
+
+
+
+
diff --git a/src/Billing/licensing.cer b/src/Billing/licensing.cer
new file mode 100644
index 0000000000..0dbb09c3c6
Binary files /dev/null and b/src/Billing/licensing.cer differ
diff --git a/src/Core/GlobalSettings.cs b/src/Core/GlobalSettings.cs
index ebcef09f54..b0ef548ab0 100644
--- a/src/Core/GlobalSettings.cs
+++ b/src/Core/GlobalSettings.cs
@@ -7,6 +7,7 @@
public virtual string StripeApiKey { get; set; }
public virtual string ProjectName { get; set; }
public virtual string LogDirectory { get; set; }
+ public virtual string LicenseDirectory { get; set; }
public virtual BaseServiceUriSettings BaseServiceUri { get; set; } = new BaseServiceUriSettings();
public virtual SqlServerSettings SqlServer { get; set; } = new SqlServerSettings();
public virtual MailSettings Mail { get; set; } = new MailSettings();
diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs
new file mode 100644
index 0000000000..5dc8470f1c
--- /dev/null
+++ b/src/Core/Models/Business/ILicense.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Bit.Core.Models.Business
+{
+ public interface ILicense
+ {
+ string LicenseKey { get; set; }
+ int Version { get; set; }
+ DateTime Issued { get; set; }
+ DateTime Expires { get; set; }
+ bool Trial { get; set; }
+ string Signature { get; set; }
+ byte[] GetSignatureData();
+ bool VerifySignature(X509Certificate2 certificate);
+ }
+}
diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs
new file mode 100644
index 0000000000..d42f6b3c17
--- /dev/null
+++ b/src/Core/Models/Business/OrganizationLicense.cs
@@ -0,0 +1,119 @@
+using Bit.Core.Enums;
+using Bit.Core.Models.Table;
+using System;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+
+namespace Bit.Core.Models.Business
+{
+ public class OrganizationLicense : ILicense
+ {
+ public OrganizationLicense()
+ { }
+
+ public OrganizationLicense(Organization org)
+ {
+ LicenseKey = "";
+ Id = org.Id;
+ Name = org.Name;
+ Enabled = org.Enabled;
+ Seats = org.Seats;
+ MaxCollections = org.MaxCollections;
+ UseGroups = org.UseGroups;
+ UseDirectory = org.UseDirectory;
+ UseTotp = org.UseTotp;
+ MaxStorageGb = org.MaxStorageGb;
+ SelfHost = org.SelfHost;
+ Version = 1;
+ }
+
+ public string LicenseKey { get; set; }
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public bool Enabled { get; set; }
+ public string Plan { get; set; }
+ public PlanType PlanType { get; set; }
+ public short? Seats { get; set; }
+ public short? MaxCollections { get; set; }
+ public bool UseGroups { get; set; }
+ public bool UseDirectory { get; set; }
+ public bool UseTotp { get; set; }
+ public short? MaxStorageGb { get; set; }
+ public bool SelfHost { get; set; }
+ public int Version { get; set; }
+ public DateTime Issued { get; set; }
+ public DateTime Expires { get; set; }
+ public bool Trial { get; set; }
+ public string Signature { get; set; }
+ public byte[] SignatureBytes => Convert.FromBase64String(Signature);
+
+ public byte[] GetSignatureData()
+ {
+ string data = null;
+ if(Version == 1)
+ {
+ data = string.Format("organization:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}_{8}_{9}_{10}_{11}_{12}_{13}",
+ Version,
+ Utilities.CoreHelpers.ToEpocMilliseconds(Issued),
+ Utilities.CoreHelpers.ToEpocMilliseconds(Expires),
+ LicenseKey,
+ Id,
+ Enabled,
+ PlanType,
+ Seats,
+ MaxCollections,
+ UseGroups,
+ UseDirectory,
+ UseTotp,
+ MaxStorageGb,
+ SelfHost);
+ }
+ else
+ {
+ throw new NotSupportedException($"Version {Version} is not supported.");
+ }
+
+ return Encoding.UTF8.GetBytes(data);
+ }
+
+ public bool VerifyData(Organization organization)
+ {
+ if(Issued > DateTime.UtcNow)
+ {
+ return false;
+ }
+
+ if(Expires < DateTime.UtcNow)
+ {
+ return false;
+ }
+
+ if(Version == 1)
+ {
+ return
+ organization.LicenseKey.Equals(LicenseKey, StringComparison.InvariantCultureIgnoreCase) &&
+ organization.Enabled == Enabled &&
+ organization.PlanType == PlanType &&
+ organization.Seats == Seats &&
+ organization.MaxCollections == MaxCollections &&
+ organization.UseGroups == UseGroups &&
+ organization.UseDirectory == UseDirectory &&
+ organization.UseTotp == UseTotp &&
+ organization.SelfHost == SelfHost;
+ }
+ else
+ {
+ throw new NotSupportedException($"Version {Version} is not supported.");
+ }
+ }
+
+ public bool VerifySignature(X509Certificate2 certificate)
+ {
+ using(var rsa = certificate.GetRSAPublicKey())
+ {
+ return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ }
+ }
+ }
+}
diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs
new file mode 100644
index 0000000000..3320a19b6a
--- /dev/null
+++ b/src/Core/Models/Business/UserLicense.cs
@@ -0,0 +1,90 @@
+using Bit.Core.Models.Table;
+using System;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+
+namespace Bit.Core.Models.Business
+{
+ public class UserLicense : ILicense
+ {
+ public UserLicense()
+ { }
+
+ public UserLicense(User user)
+ {
+ LicenseKey = "";
+ Id = user.Id;
+ Email = user.Email;
+ Version = 1;
+ }
+
+ public string LicenseKey { get; set; }
+ public Guid Id { get; set; }
+ public string Email { get; set; }
+ public bool Premium { get; set; }
+ public short? MaxStorageGb { get; set; }
+ public int Version { get; set; }
+ public DateTime Issued { get; set; }
+ public DateTime Expires { get; set; }
+ public bool Trial { get; set; }
+ public string Signature { get; set; }
+ public byte[] SignatureBytes => Convert.FromBase64String(Signature);
+
+ public byte[] GetSignatureData()
+ {
+ string data = null;
+ if(Version == 1)
+ {
+ data = string.Format("user:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}",
+ Version,
+ Utilities.CoreHelpers.ToEpocMilliseconds(Issued),
+ Utilities.CoreHelpers.ToEpocMilliseconds(Expires),
+ LicenseKey,
+ Id,
+ Email,
+ Premium,
+ MaxStorageGb);
+ }
+ else
+ {
+ throw new NotSupportedException($"Version {Version} is not supported.");
+ }
+
+ return Encoding.UTF8.GetBytes(data);
+ }
+
+ public bool VerifyData(User user)
+ {
+ if(Issued > DateTime.UtcNow)
+ {
+ return false;
+ }
+
+ if(Expires < DateTime.UtcNow)
+ {
+ return false;
+ }
+
+ if(Version == 1)
+ {
+ return
+ user.LicenseKey.Equals(LicenseKey, StringComparison.InvariantCultureIgnoreCase) &&
+ user.Premium == Premium &&
+ user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase);
+ }
+ else
+ {
+ throw new NotSupportedException($"Version {Version} is not supported.");
+ }
+ }
+
+ public bool VerifySignature(X509Certificate2 certificate)
+ {
+ using(var rsa = certificate.GetRSAPublicKey())
+ {
+ return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ }
+ }
+ }
+}
diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs
index 146ca21c59..e0ba24829e 100644
--- a/src/Core/Models/Table/Organization.cs
+++ b/src/Core/Models/Table/Organization.cs
@@ -19,12 +19,14 @@ namespace Bit.Core.Models.Table
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseTotp { get; set; }
+ public bool SelfHost { get; set; }
public long? Storage { get; set; }
public short? MaxStorageGb { get; set; }
public GatewayType? Gateway { get; set; }
public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
public bool Enabled { get; set; } = true;
+ public string LicenseKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs
index 40faeb3b98..f242bccf8c 100644
--- a/src/Core/Models/Table/User.cs
+++ b/src/Core/Models/Table/User.cs
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Table
public GatewayType? Gateway { get; set; }
public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
+ public string LicenseKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Services/ILicenseVerificationService.cs b/src/Core/Services/ILicenseVerificationService.cs
new file mode 100644
index 0000000000..3a077fa1e0
--- /dev/null
+++ b/src/Core/Services/ILicenseVerificationService.cs
@@ -0,0 +1,10 @@
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Services
+{
+ public interface ILicenseVerificationService
+ {
+ bool VerifyOrganizationPlan(Organization organization);
+ bool VerifyUserPremium(User user);
+ }
+}
diff --git a/src/Core/Services/Implementations/RsaLicenseVerificationService.cs b/src/Core/Services/Implementations/RsaLicenseVerificationService.cs
new file mode 100644
index 0000000000..d684707d97
--- /dev/null
+++ b/src/Core/Services/Implementations/RsaLicenseVerificationService.cs
@@ -0,0 +1,111 @@
+using Bit.Core.Models.Business;
+using Bit.Core.Models.Table;
+using Bit.Core.Utilities;
+using Microsoft.AspNetCore.Hosting;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+
+namespace Bit.Core.Services
+{
+ public class RsaLicenseVerificationService : ILicenseVerificationService
+ {
+ private readonly X509Certificate2 _certificate;
+ private readonly GlobalSettings _globalSettings;
+ private IDictionary _userLicenseCache;
+ private IDictionary _organizationLicenseCache;
+
+ public RsaLicenseVerificationService(
+ IHostingEnvironment environment,
+ GlobalSettings globalSettings)
+ {
+ if(!environment.IsDevelopment() && !globalSettings.SelfHosted)
+ {
+ throw new Exception($"{nameof(RsaLicenseVerificationService)} can only be used for self hosted instances.");
+ }
+
+ _globalSettings = globalSettings;
+ _certificate = CoreHelpers.GetCertificate("licensing.crt", null);
+ if(false && !_certificate.Thumbprint.Equals(""))
+ {
+ throw new Exception("Invalid licensing certificate.");
+ }
+
+ if(!CoreHelpers.SettingHasValue(_globalSettings.LicenseDirectory))
+ {
+ throw new InvalidOperationException("No license directory.");
+ }
+ }
+
+ public bool VerifyOrganizationPlan(Organization organization)
+ {
+ if(_globalSettings.SelfHosted && !organization.SelfHost)
+ {
+ return false;
+ }
+
+ var license = ReadOrganiztionLicense(organization);
+ return license != null && license.VerifyData(organization) && license.VerifySignature(_certificate);
+ }
+
+ public bool VerifyUserPremium(User user)
+ {
+ if(!user.Premium)
+ {
+ return false;
+ }
+
+ var license = ReadUserLicense(user);
+ return license != null && license.VerifyData(user) && license.VerifySignature(_certificate);
+ }
+
+ private UserLicense ReadUserLicense(User user)
+ {
+ if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
+ {
+ return _userLicenseCache[user.LicenseKey];
+ }
+
+ var filePath = $"{_globalSettings.LicenseDirectory}/user/{user.LicenseKey}.json";
+ if(!File.Exists(filePath))
+ {
+ return null;
+ }
+
+ var data = File.ReadAllText(filePath, Encoding.UTF8);
+ var obj = JsonConvert.DeserializeObject(data);
+ if(_userLicenseCache == null)
+ {
+ _userLicenseCache = new Dictionary();
+ }
+ _userLicenseCache.Add(obj.LicenseKey, obj);
+ return obj;
+ }
+
+ private OrganizationLicense ReadOrganiztionLicense(Organization organization)
+ {
+ if(_organizationLicenseCache != null && _organizationLicenseCache.ContainsKey(organization.LicenseKey))
+ {
+ return _organizationLicenseCache[organization.LicenseKey];
+ }
+
+ var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.LicenseKey}.json";
+ if(!File.Exists(filePath))
+ {
+ return null;
+ }
+
+ var data = File.ReadAllText(filePath, Encoding.UTF8);
+ var obj = JsonConvert.DeserializeObject(data);
+ if(_organizationLicenseCache == null)
+ {
+ _organizationLicenseCache = new Dictionary();
+ }
+ _organizationLicenseCache.Add(obj.LicenseKey, obj);
+ return obj;
+ }
+ }
+}
diff --git a/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs b/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs
new file mode 100644
index 0000000000..b88cea21df
--- /dev/null
+++ b/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs
@@ -0,0 +1,29 @@
+using Bit.Core.Models.Table;
+using Microsoft.AspNetCore.Hosting;
+using System;
+
+namespace Bit.Core.Services
+{
+ public class NoopLicenseVerificationService : ILicenseVerificationService
+ {
+ public NoopLicenseVerificationService(
+ IHostingEnvironment environment,
+ GlobalSettings globalSettings)
+ {
+ if(!environment.IsDevelopment() && globalSettings.SelfHosted)
+ {
+ throw new Exception($"{nameof(NoopLicenseVerificationService)} cannot be used for self hosted instances.");
+ }
+ }
+
+ public bool VerifyOrganizationPlan(Organization organization)
+ {
+ return true;
+ }
+
+ public bool VerifyUserPremium(User user)
+ {
+ return user.Premium;
+ }
+ }
+}
diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs
index 4e7c50a61e..571ee4d6bc 100644
--- a/src/Core/Utilities/ServiceCollectionExtensions.cs
+++ b/src/Core/Utilities/ServiceCollectionExtensions.cs
@@ -107,6 +107,15 @@ namespace Bit.Core.Utilities
{
services.AddSingleton();
}
+
+ if(globalSettings.SelfHosted)
+ {
+ services.AddSingleton();
+ }
+ else
+ {
+ services.AddSingleton();
+ }
}
public static void AddNoopServices(this IServiceCollection services)
@@ -117,6 +126,7 @@ namespace Bit.Core.Utilities
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
}
public static IdentityBuilder AddCustomIdentityServices(
diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj
index d8e8b58bad..e962ebd5a8 100644
--- a/src/Identity/Identity.csproj
+++ b/src/Identity/Identity.csproj
@@ -9,6 +9,14 @@
..\..\docker\Docker.dcproj
+
+
+
+
+
+
+
+
diff --git a/src/Identity/Properties/launchSettings.json b/src/Identity/Properties/launchSettings.json
index d822506f3f..2edb5e39ff 100644
--- a/src/Identity/Properties/launchSettings.json
+++ b/src/Identity/Properties/launchSettings.json
@@ -4,7 +4,7 @@
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:33656/",
- "sslPort": 44392
+ "sslPort": 0
}
},
"profiles": {
diff --git a/src/Identity/licensing.cer b/src/Identity/licensing.cer
new file mode 100644
index 0000000000..0dbb09c3c6
Binary files /dev/null and b/src/Identity/licensing.cer differ
diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql
index ce1b843e11..635ffc34f1 100644
--- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql
+++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql
@@ -10,12 +10,14 @@
@UseGroups BIT,
@UseDirectory BIT,
@UseTotp BIT,
+ @SelfHost BIT,
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@Enabled BIT,
+ @LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@@ -35,12 +37,14 @@ BEGIN
[UseGroups],
[UseDirectory],
[UseTotp],
+ [SelfHost],
[Storage],
[MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[Enabled],
+ [LicenseKey],
[CreationDate],
[RevisionDate]
)
@@ -57,12 +61,14 @@ BEGIN
@UseGroups,
@UseDirectory,
@UseTotp,
+ @SelfHost,
@Storage,
@MaxStorageGb,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@Enabled,
+ @LicenseKey,
@CreationDate,
@RevisionDate
)
diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql
index d9df669aea..14ad2c62ce 100644
--- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql
+++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql
@@ -10,12 +10,14 @@
@UseGroups BIT,
@UseDirectory BIT,
@UseTotp BIT,
+ @SelfHost BIT,
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@Enabled BIT,
+ @LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
@@ -36,12 +38,14 @@ BEGIN
[UseGroups] = @UseGroups,
[UseDirectory] = @UseDirectory,
[UseTotp] = @UseTotp,
+ [SelfHost] = @SelfHost,
[Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[Enabled] = @Enabled,
+ [LicenseKey] = @LicenseKey,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE
diff --git a/src/Sql/dbo/Stored Procedures/User_Create.sql b/src/Sql/dbo/Stored Procedures/User_Create.sql
index bb691d1075..ed334f9dd1 100644
--- a/src/Sql/dbo/Stored Procedures/User_Create.sql
+++ b/src/Sql/dbo/Stored Procedures/User_Create.sql
@@ -21,6 +21,7 @@
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
+ @LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@@ -51,6 +52,7 @@ BEGIN
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
+ [LicenseKey],
[CreationDate],
[RevisionDate]
)
@@ -78,6 +80,7 @@ BEGIN
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
+ @LicenseKey,
@CreationDate,
@RevisionDate
)
diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql
index 2c3dc21238..258b8c5af7 100644
--- a/src/Sql/dbo/Stored Procedures/User_Update.sql
+++ b/src/Sql/dbo/Stored Procedures/User_Update.sql
@@ -21,6 +21,7 @@
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
+ @LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@@ -51,6 +52,7 @@ BEGIN
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
+ [LicenseKey] = @LicenseKey,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE
diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql
index 31d0ef077c..44258e20dc 100644
--- a/src/Sql/dbo/Tables/Organization.sql
+++ b/src/Sql/dbo/Tables/Organization.sql
@@ -10,12 +10,14 @@
[UseGroups] BIT NOT NULL,
[UseDirectory] BIT NOT NULL,
[UseTotp] BIT NOT NULL,
+ [SelfHost] BIT NOT NULL,
[Storage] BIGINT NULL,
[MaxStorageGb] SMALLINT NULL,
[Gateway] TINYINT NULL,
[GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL,
[Enabled] BIT NOT NULL,
+ [LicenseKey] VARCHAR (100) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql
index 47a4495445..abc7e53be4 100644
--- a/src/Sql/dbo/Tables/User.sql
+++ b/src/Sql/dbo/Tables/User.sql
@@ -21,6 +21,7 @@
[Gateway] TINYINT NULL,
[GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL,
+ [LicenseKey] VARCHAR (100) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs
index c27b183e84..2c4678e7ac 100644
--- a/util/Setup/Program.cs
+++ b/util/Setup/Program.cs
@@ -260,6 +260,7 @@ globalSettings:attachment:baseDirectory={_outputDir}/core/attachments
globalSettings:attachment:baseUrl={_url}/attachments
globalSettings:dataProtection:directory={_outputDir}/core/aspnet-dataprotection
globalSettings:logDirectory={_outputDir}/core/logs
+globalSettings:licenseDirectory={_outputDir}/core/licenses
globalSettings:duo:aKey={Helpers.SecureRandomString(32, alpha: true, numeric: true)}
globalSettings:yubico:clientId=REPLACE
globalSettings:yubico:REPLACE");
diff --git a/util/SqlUpdate/2017-08-09_00_OrgSelfHost.sql b/util/SqlUpdate/2017-08-09_00_OrgSelfHost.sql
new file mode 100644
index 0000000000..92bfae5c36
--- /dev/null
+++ b/util/SqlUpdate/2017-08-09_00_OrgSelfHost.sql
@@ -0,0 +1,26 @@
+alter table [Organization] add [SelfHost] BIT NULL
+go
+
+
+update [Organization] set [SelfHost] = 0
+go
+
+update [Organization] set [SelfHost] = 1 where PlanType = 4 or PlanType = 5
+go
+
+
+alter table [Organization] alter column [SelfHost] BIT NOT NULL
+go
+
+
+drop view [dbo].[OrganizationView]
+go
+
+CREATE VIEW [dbo].[OrganizationView]
+AS
+SELECT
+ *
+FROM
+ [dbo].[Organization]
+GO
+