chore: Introduce enterprise edition license (#3209)

- Initialize an "enterprise" folder that is copyrighted.
- You can remove this folder and the system will continue functioning normally, in case you want a purely MIT licensed product.
- Enable limit on the number of user accounts in enterprise code.
- Use enterprise edition injector methods (inspired from Gitlab).
- SaaS software would run enterprise edition software always.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose 2021-12-09 12:07:48 +05:30 committed by GitHub
parent 2f63ebb8a6
commit b1eea7f7d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 253 additions and 3 deletions

View file

@ -1,7 +1,11 @@
The MIT License (MIT)
Copyright (c) 2017-2021 Chatwoot Inc. Copyright (c) 2017-2021 Chatwoot Inc.
Portions of this software are licensed as follows:
* All content that resides under the "enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "enterprise/LICENSE".
* All third party components incorporated into the Chatwoot Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View file

@ -2,6 +2,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index] before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization before_action :check_authorization
before_action :find_user, only: [:create] before_action :find_user, only: [:create]
before_action :validate_limit, only: [:create]
before_action :create_user, only: [:create] before_action :create_user, only: [:create]
before_action :save_account_user, only: [:create] before_action :save_account_user, only: [:create]
@ -69,4 +70,8 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
def agents def agents
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] }) @agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
end end
def validate_limit
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
end
end end

View file

@ -31,6 +31,10 @@ module RequestExceptionHandler
render json: { error: message }, status: :unprocessable_entity render json: { error: message }, status: :unprocessable_entity
end end
def render_payment_required(message)
render json: { error: message }, status: :payment_required
end
def render_internal_server_error(message) def render_internal_server_error(message)
render json: { error: message }, status: :internal_server_error render json: { error: message }, status: :internal_server_error
end end

View file

@ -6,6 +6,7 @@
# auto_resolve_duration :integer # auto_resolve_duration :integer
# domain :string(100) # domain :string(100)
# feature_flags :integer default(0), not null # feature_flags :integer default(0), not null
# limits :jsonb
# locale :integer default("en") # locale :integer default("en")
# name :string not null # name :string not null
# settings_flags :integer default(0), not null # settings_flags :integer default(0), not null
@ -19,6 +20,7 @@ class Account < ApplicationRecord
include FlagShihTzu include FlagShihTzu
include Reportable include Reportable
include Featurable include Featurable
prepend_mod_with('Account')
DEFAULT_QUERY_SETTING = { DEFAULT_QUERY_SETTING = {
flag_query_mode: :bit_operator, flag_query_mode: :bit_operator,
@ -107,6 +109,13 @@ class Account < ApplicationRecord
super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>') super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
end end
def usage_limits
{
agents: ChatwootApp.max_limit,
inboxes: ChatwootApp.max_limit
}
end
private private
def notify_creation def notify_creation

View file

@ -13,8 +13,11 @@ module Chatwoot
# Initialize configuration defaults for originally generated Rails version. # Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0 config.load_defaults 6.0
config.autoload_paths << Rails.root.join('lib')
config.eager_load_paths << Rails.root.join('lib') config.eager_load_paths << Rails.root.join('lib')
config.eager_load_paths << Rails.root.join('enterprise/lib')
# rubocop:disable Rails/FilePath
config.eager_load_paths += Dir["#{Rails.root}/enterprise/app/**"]
# rubocop:enable Rails/FilePath
# Settings in config/environments/* take precedence over those specified here. # Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers # Application configuration can go into files in config/initializers

View file

@ -0,0 +1,87 @@
# frozen_string_literal: true
# original Authors: Gitlab
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/initializers/0_inject_enterprise_edition_module.rb
#
### Ref: https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073
# Ancestors chain : it holds a list of constant names which are its ancestors
# example, by calling ancestors on the String class,
# String.ancestors => [String, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]
#
# Include: Ruby will insert the module into the ancestors chain of the class, just after its superclass
# ancestor chain : [OriginalClass, IncludedModule, ...]
#
# Extend: class will actually import the module methods as class methods
#
# Prepend: Ruby will look into the module methods before looking into the class.
# ancestor chain : [PrependedModule, OriginalClass, ...]
########
require 'active_support/inflector'
module InjectEnterpriseEditionModule
def prepend_mod_with(constant_name, namespace: Object, with_descendants: false)
each_extension_for(constant_name, namespace) do |constant|
prepend_module(constant, with_descendants)
end
end
def extend_mod_with(constant_name, namespace: Object)
# rubocop:disable Performance/MethodObjectAsBlock
each_extension_for(
constant_name,
namespace,
&method(:extend)
)
# rubocop:enable Performance/MethodObjectAsBlock
end
def include_mod_with(constant_name, namespace: Object)
# rubocop:disable Performance/MethodObjectAsBlock
each_extension_for(
constant_name,
namespace,
&method(:include)
)
# rubocop:enable Performance/MethodObjectAsBlock
end
def prepend_mod(with_descendants: false)
prepend_mod_with(name, with_descendants: with_descendants)
end
def extend_mod
extend_mod_with(name)
end
def include_mod
include_mod_with(name)
end
private
def prepend_module(mod, with_descendants)
prepend(mod)
descendants.each { |descendant| descendant.prepend(mod) } if with_descendants
end
def each_extension_for(constant_name, namespace)
ChatwootApp.extensions.each do |extension_name|
extension_namespace =
const_get_maybe_false(namespace, extension_name.camelize)
extension_module =
const_get_maybe_false(extension_namespace, constant_name)
yield(extension_module) if extension_module
end
end
def const_get_maybe_false(mod, name)
mod&.const_defined?(name, false) && mod&.const_get(name, false)
end
end
Module.prepend(InjectEnterpriseEditionModule)

View file

@ -0,0 +1,5 @@
class AddLimitsToAccounts < ActiveRecord::Migration[6.1]
def change
add_column :accounts, :limits, :jsonb, default: {}
end
end

View file

@ -52,6 +52,7 @@ ActiveRecord::Schema.define(version: 2021_12_08_085931) do
t.integer "settings_flags", default: 0, null: false t.integer "settings_flags", default: 0, null: false
t.integer "feature_flags", default: 0, null: false t.integer "feature_flags", default: 0, null: false
t.integer "auto_resolve_duration" t.integer "auto_resolve_duration"
t.jsonb "limits", default: {}
end end
create_table "action_mailbox_inbound_emails", force: :cascade do |t| create_table "action_mailbox_inbound_emails", force: :cascade do |t|

36
enterprise/LICENSE Normal file
View file

@ -0,0 +1,36 @@
The Chatwoot Enterprise license (the “Enterprise License”)
Copyright (c) 2017-2021 Chatwoot Inc
With regard to the Chatwoot Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Chatwoot Subscription Terms of Service, available
at https://www.chatwoot.com/terms-of-service/ (the “Enterprise Terms”), or other
agreement governing the use of the Software, as agreed by you and Chatwoot,
and otherwise have a valid Chatwoot Enterprise License for for the
correct number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Chatwoot
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Chatwoot Enterprise subscription for the correct
number of user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Chatwoot and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Chatwoot Software, those
components are licensed under the original license provided by the owner of the
applicable component.

View file

@ -0,0 +1,15 @@
module Enterprise::Account
def usage_limits
{
agents: get_limits(:agents),
inboxes: get_limits(:inboxes)
}
end
private
def get_limits(limit_name)
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
self[:limits][limit_name.to_s] || GlobalConfig.get(config_name)[config_name] || ChatwootApp.max_limit
end
end

View file

@ -0,0 +1,2 @@
module Enterprise
end

33
lib/chatwoot_app.rb Normal file
View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'pathname'
module ChatwootApp
def self.root
Pathname.new(File.expand_path('..', __dir__))
end
def self.max_limit
100_000
end
def self.enterprise?
return if ENV.fetch('DISABLE_ENTERPRISE', false)
@enterprise ||= root.join('enterprise').exist?
end
def self.custom?
@custom ||= root.join('custom').exist?
end
def self.extensions
if custom?
%w[enterprise custom]
elsif enterprise?
%w[enterprise]
else
%w[]
end
end
end

View file

@ -28,4 +28,10 @@ module CustomExceptions::Account
I18n.t 'errors.signup.failed' I18n.t 'errors.signup.failed'
end end
end end
class PlanUpgradeRequired < CustomExceptions::Base
def message
I18n.t 'errors.plan_upgrade_required.failed'
end
end
end end

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Account do
describe 'usage_limits' do
before do
create(:installation_config, name: 'ACCOUNT_AGENTS_LIMIT', value: 20)
end
let!(:account) { create(:account) }
it 'returns max limits from global config when enterprise version' do
expect(account.usage_limits).to eq(
{
agents: 20,
inboxes: ChatwootApp.max_limit
}
)
end
it 'returns max limits from account when enterprise version' do
account.update(limits: { agents: 10 })
expect(account.usage_limits).to eq(
{
agents: 10,
inboxes: ChatwootApp.max_limit
}
)
end
end
end

View file

@ -21,4 +21,12 @@ RSpec.describe Account do
it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) } it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) }
it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) } it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) }
it { is_expected.to have_many(:teams).dependent(:destroy_async) } it { is_expected.to have_many(:teams).dependent(:destroy_async) }
describe 'usage_limits' do
let(:account) { create(:account) }
it 'returns ChatwootApp.max limits' do
expect(account.usage_limits).to eq({ agents: ChatwootApp.max_limit, inboxes: ChatwootApp.max_limit })
end
end
end end