Initial commit
This commit is contained in:
commit
243ed398a0
22 changed files with 512 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/.bundle/
|
||||
/.yardoc
|
||||
/_yardoc/
|
||||
/coverage/
|
||||
/doc/
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
/vendor/
|
||||
|
||||
# rspec failure tracking
|
||||
.rspec_status
|
||||
|
||||
# install data
|
||||
/Gemfile.lock
|
||||
/config.yml
|
||||
|
||||
10
.gitlab-ci.yml
Normal file
10
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
default:
|
||||
image: ruby:3.2.9
|
||||
|
||||
before_script:
|
||||
- gem install bundler -v 2.6.9
|
||||
- bundle install
|
||||
|
||||
example_job:
|
||||
script:
|
||||
- bundle exec rake
|
||||
3
.rspec
Normal file
3
.rspec
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
--format documentation
|
||||
--color
|
||||
--require spec_helper
|
||||
8
.rubocop.yml
Normal file
8
.rubocop.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
AllCops:
|
||||
TargetRubyVersion: 3.1
|
||||
|
||||
Style/StringLiterals:
|
||||
EnforcedStyle: double_quotes
|
||||
|
||||
Style/StringLiteralsInInterpolation:
|
||||
EnforcedStyle: double_quotes
|
||||
16
Containerfile
Normal file
16
Containerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FROM docker.io/library/ruby
|
||||
|
||||
ENV APP_HOME /app
|
||||
ENV RACK_ENV production
|
||||
WORKDIR /app
|
||||
|
||||
ADD Gemfile /app/
|
||||
ADD *gemspec /app/
|
||||
ADD config.ru /app/
|
||||
ADD lib /app/lib/
|
||||
|
||||
RUN bundle config set without 'development' && \
|
||||
bundle install
|
||||
|
||||
EXPOSE 9292/tcp
|
||||
CMD ["bundle", "exec", "rackup"]
|
||||
6
Gemfile
Normal file
6
Gemfile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Specify your gem's dependencies in infisical_license_server.gemspec
|
||||
gemspec
|
||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2026 Alexander Olofsson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of 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.
|
||||
16
README.md
Normal file
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
Infisical License Server
|
||||
========================
|
||||
|
||||
A simple implementation of the Infisical license server API, which supports on-prem online licenses.
|
||||
|
||||
## Usage
|
||||
|
||||
Create a `config.yml` file following the example in [`config.yml.example`](config.yml.example), then run the application using `rackup`.
|
||||
|
||||
The configuration values that need to be set in Infisical to use the server are;
|
||||
- `LICENSE_KEY=<.>` - should be set to the `key` value for the plan you want to use
|
||||
- `LICENSE_SERVER_URL=<.>` - should be set to the URL where this server is running
|
||||
|
||||
## License
|
||||
|
||||
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
||||
12
Rakefile
Normal file
12
Rakefile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/gem_tasks"
|
||||
require "rspec/core/rake_task"
|
||||
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
|
||||
require "rubocop/rake_task"
|
||||
|
||||
RuboCop::RakeTask.new
|
||||
|
||||
task default: %i[spec rubocop]
|
||||
19
config.ru
Normal file
19
config.ru
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/infisical_license_server'
|
||||
|
||||
# Ensure config can be loaded
|
||||
InfisicalLicenseServer.config
|
||||
|
||||
map '/api/auth/v1' do
|
||||
run InfisicalLicenseServer::API::AuthV1
|
||||
end
|
||||
map '/api/license/v1' do
|
||||
run InfisicalLicenseServer::API::LicenseV1
|
||||
end
|
||||
map '/api/license-server/v1' do
|
||||
run InfisicalLicenseServer::API::LicenseServerV1
|
||||
end
|
||||
map '/' do
|
||||
run InfisicalLicenseServer::Server
|
||||
end
|
||||
8
config.yml.example
Normal file
8
config.yml.example
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
plans:
|
||||
'Some plan name':
|
||||
key: 'Some API KEY'
|
||||
# https://github.com/Infisical/infisical/blob/main/backend/src/ee/services/license/license-types.ts#L34
|
||||
features:
|
||||
identityLimit: 10
|
||||
samlSSO: true
|
||||
46
infisical_license_server.gemspec
Normal file
46
infisical_license_server.gemspec
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "lib/infisical_license_server/version"
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "infisical_license_server"
|
||||
spec.version = InfisicalLicenseServer::VERSION
|
||||
spec.authors = ["Alexander Olofsson"]
|
||||
spec.email = ["me@ananace.dev"]
|
||||
|
||||
spec.summary = "TODO: Write a short summary, because RubyGems requires one."
|
||||
spec.description = "TODO: Write a longer description or delete this line."
|
||||
spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
||||
spec.license = "MIT"
|
||||
spec.required_ruby_version = ">= 3.1.0"
|
||||
|
||||
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
||||
|
||||
spec.metadata["homepage_uri"] = spec.homepage
|
||||
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
||||
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
||||
|
||||
# Specify which files should be added to the gem when it is released.
|
||||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
||||
gemspec = File.basename(__FILE__)
|
||||
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
||||
ls.readlines("\x0", chomp: true).reject do |f|
|
||||
(f == gemspec) ||
|
||||
f.start_with?(*%w[bin/ test/ spec/ features/ .git .gitlab-ci.yml appveyor Gemfile])
|
||||
end
|
||||
end
|
||||
spec.bindir = "exe"
|
||||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
# For more information and examples about making a new gem, check out our
|
||||
# guide at: https://bundler.io/guides/creating_gem.html
|
||||
spec.add_dependency 'rackup'
|
||||
spec.add_dependency 'sinatra'
|
||||
spec.add_dependency 'webrick'
|
||||
|
||||
spec.add_development_dependency 'rake'
|
||||
spec.add_development_dependency 'rspec'
|
||||
spec.add_development_dependency 'rubocop'
|
||||
spec.add_development_dependency 'sinatra-contrib'
|
||||
end
|
||||
20
lib/infisical_license_server.rb
Normal file
20
lib/infisical_license_server.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "infisical_license_server/version"
|
||||
|
||||
module InfisicalLicenseServer
|
||||
class Error < StandardError; end
|
||||
|
||||
module API
|
||||
autoload :AuthV1, 'infisical_license_server/api/auth'
|
||||
autoload :LicenseV1, 'infisical_license_server/api/license'
|
||||
autoload :LicenseServerV1, 'infisical_license_server/api/license_server'
|
||||
end
|
||||
|
||||
autoload :Config, 'infisical_license_server/config'
|
||||
autoload :Server, 'infisical_license_server/server'
|
||||
|
||||
def self.config
|
||||
@config ||= InfisicalLicenseServer::Config.new
|
||||
end
|
||||
end
|
||||
38
lib/infisical_license_server/api/auth.rb
Normal file
38
lib/infisical_license_server/api/auth.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'base64'
|
||||
require 'sinatra/base'
|
||||
|
||||
module InfisicalLicenseServer::API
|
||||
class AuthV1 < Sinatra::Base
|
||||
configure :development do
|
||||
require 'sinatra/reloader'
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
enable :logging
|
||||
|
||||
post '/license-login' do
|
||||
key = request.get_header('HTTP_X_API_KEY')
|
||||
region = request.get_header('HTTP_X_REGION')
|
||||
|
||||
error 400, 'Missing API key' unless key
|
||||
|
||||
plan = InfisicalLicenseServer.config.plan_for key
|
||||
error 401 unless plan
|
||||
|
||||
region = " (region #{region})" if region
|
||||
logger.info "New auth for #{plan['plan']}#{region}"
|
||||
|
||||
{
|
||||
token: Base64.strict_encode64(
|
||||
{
|
||||
auth: Time.now.to_i,
|
||||
key: plan['key'],
|
||||
plan: plan['plan']
|
||||
}.to_json
|
||||
)
|
||||
}.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
44
lib/infisical_license_server/api/license.rb
Normal file
44
lib/infisical_license_server/api/license.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'sinatra/base'
|
||||
|
||||
module InfisicalLicenseServer::API
|
||||
class LicenseV1 < Sinatra::Base
|
||||
configure :development do
|
||||
require 'sinatra/reloader'
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
enable :logging
|
||||
|
||||
get '/plan' do
|
||||
auth = request.get_header('HTTP_AUTHORIZATION')
|
||||
error 401 unless InfisicalLicenseServer.config.auth? auth
|
||||
|
||||
plan = InfisicalLicenseServer.config.plan_for(auth)
|
||||
|
||||
{
|
||||
currentPlan: plan['merged'].compact
|
||||
}.to_json
|
||||
end
|
||||
|
||||
patch '/license' do
|
||||
auth = request.get_header('HTTP_AUTHORIZATION')
|
||||
error 401 unless InfisicalLicenseServer.config.auth? auth
|
||||
|
||||
plan = InfisicalLicenseServer.config.plan_for(auth)
|
||||
error 500 unless plan
|
||||
|
||||
#request.body.rewind
|
||||
data = JSON.parse(request.body.read)
|
||||
|
||||
logger.info "Plan #{plan['plan']} uses #{data['usedSeats']} seats and #{data['usedIdentitySeats']} identities"
|
||||
|
||||
# Cache these for next plan request
|
||||
plan['features']['membersUsed'] = data['usedSeats']
|
||||
plan['features']['identitiesUsed'] = data['usedIdentitySeats']
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
54
lib/infisical_license_server/api/license_server.rb
Normal file
54
lib/infisical_license_server/api/license_server.rb
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'sinatra/base'
|
||||
|
||||
module InfisicalLicenseServer::API
|
||||
class LicenseServerV1 < Sinatra::Base
|
||||
# https://github.com/Infisical/infisical/blob/main/backend/src/ee/services/license/licence-enums.ts
|
||||
HEADERS = %w[Allowed Used].freeze
|
||||
ROWS = [
|
||||
{ name: "Organization identity limit", field: "identityLimit" },
|
||||
{ name: "Project limit", field: "workspaceLimit" },
|
||||
{ name: "Environment limit", field: "environmentLimit" },
|
||||
{ name: "Secret versioning", field: "secretVersioning" },
|
||||
{ name: "Point in time recovery", field: "pitRecovery" },
|
||||
{ name: "RBAC", field: "rbac" },
|
||||
{ name: "Custom rate limits", field: "customRateLimits" },
|
||||
{ name: "Custom alerts", field: "customAlerts" },
|
||||
{ name: "Audit logs", field: "auditLogs" },
|
||||
{ name: "SAML SSO", field: "samlSSO" },
|
||||
{ name: "SSH Host Groups", field: "sshHostGroups" },
|
||||
{ name: "Hardware Security Module (HSM)", field: "hsm" },
|
||||
{ name: "OIDC SSO", field: "oidcSSO" },
|
||||
{ name: "Secret approvals", field: "secretApproval" },
|
||||
{ name: "Secret rotation", field: "secretRotation" },
|
||||
{ name: "Instance User Management", field: "instanceUserManagement" },
|
||||
{ name: "External KMS", field: "externalKms" }
|
||||
].freeze
|
||||
|
||||
configure :development do
|
||||
require 'sinatra/reloader'
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
enable :logging
|
||||
|
||||
get '/customers/on-prem-plan/table' do
|
||||
auth = request.get_header('HTTP_AUTHORIZATION')
|
||||
error 401 unless InfisicalLicenseServer.config.auth? auth
|
||||
|
||||
plan = InfisicalLicenseServer.config.plan_for(auth)
|
||||
error 500 unless plan
|
||||
|
||||
{
|
||||
head: HEADERS.map { |head| { name: head } },
|
||||
rows: ROWS.map do |row|
|
||||
{
|
||||
name: row[:name],
|
||||
allowed: row[:field].end_with?('Limit') ? true : (plan['merged'][row[:field]] || false),
|
||||
}
|
||||
end
|
||||
}.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
121
lib/infisical_license_server/config.rb
Normal file
121
lib/infisical_license_server/config.rb
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'psych'
|
||||
|
||||
require_relative 'util'
|
||||
|
||||
class InfisicalLicenseServer::Config
|
||||
AUTH_TIME = 1 * 24 * 60 * 60
|
||||
|
||||
attr_reader :data
|
||||
|
||||
def initialize(file = 'config.yml')
|
||||
raise 'Missing configuration file' unless File.exist?(file)
|
||||
|
||||
@data = Psych.load_file(file)
|
||||
end
|
||||
|
||||
def auth?(token)
|
||||
Time.now - Time.at(decode_auth(token)['auth']) <= AUTH_TIME
|
||||
rescue StandardError => e
|
||||
puts "Failed to read auth token - #{e.class}: #{e}"
|
||||
puts e.backtrace
|
||||
false
|
||||
end
|
||||
|
||||
def valid_key?(key)
|
||||
data['plans'].any? { |_, plan| plan['key'] == key }
|
||||
end
|
||||
|
||||
def plan_for(key)
|
||||
if key.start_with? 'Bearer '
|
||||
key = decode_auth(key)['key']
|
||||
end
|
||||
|
||||
plan_name, plan = data['plans'].find { |_, search| search['key'] == key }
|
||||
return nil unless plan
|
||||
|
||||
plan['merged'] = default_plan.merge(_id: plan_name, slug: 'enterprise').deep_merge(plan['features'])
|
||||
plan['plan'] = plan_name
|
||||
plan
|
||||
end
|
||||
|
||||
def decode_auth(token)
|
||||
raise 'Missing auth' unless token
|
||||
|
||||
token = token[7..] if token.start_with? 'Bearer '
|
||||
|
||||
JSON.parse(Base64.decode64(token))
|
||||
end
|
||||
|
||||
# https://github.com/Infisical/infisical/blob/main/backend/src/ee/services/license/license-fns.ts#L55
|
||||
def self.default_plan
|
||||
{
|
||||
_id: nil,
|
||||
slug: nil,
|
||||
tier: -1,
|
||||
workspaceLimit: nil,
|
||||
workspacesUsed: 0,
|
||||
memberLimit: nil,
|
||||
membersUsed: 0,
|
||||
environmentLimit: nil,
|
||||
environmentsUsed: 0,
|
||||
identityLimit: nil,
|
||||
identitiesUsed: 0,
|
||||
dynamicSecret: false,
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: false,
|
||||
githubOrgSync: false,
|
||||
customRateLimits: false,
|
||||
subOrganization: false,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
enforceGoogleSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
status: nil,
|
||||
trial_end: nil,
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretRotation: false,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false,
|
||||
rateLimits: {
|
||||
readLimit: 60,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 40
|
||||
},
|
||||
pkiEst: false,
|
||||
pkiAcme: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false,
|
||||
sshHostGroups: false,
|
||||
secretScanning: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseCertificateSyncs: false,
|
||||
enterpriseAppConnections: false,
|
||||
fips: false,
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false,
|
||||
pkiLegacyTemplates: false,
|
||||
secretShareExternalBranding: false
|
||||
}
|
||||
end
|
||||
|
||||
def default_plan
|
||||
self.class.default_plan
|
||||
end
|
||||
end
|
||||
18
lib/infisical_license_server/server.rb
Normal file
18
lib/infisical_license_server/server.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'sinatra/base'
|
||||
|
||||
module InfisicalLicenseServer
|
||||
class Server < Sinatra::Base
|
||||
configure :development do
|
||||
require 'sinatra/reloader'
|
||||
register Sinatra::Reloader
|
||||
|
||||
enable :logging
|
||||
end
|
||||
|
||||
get '/healthz' do
|
||||
'Ok'
|
||||
end
|
||||
end
|
||||
end
|
||||
8
lib/infisical_license_server/util.rb
Normal file
8
lib/infisical_license_server/util.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ::Hash
|
||||
def deep_merge(second)
|
||||
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
||||
self.merge(second, &merger)
|
||||
end
|
||||
end
|
||||
5
lib/infisical_license_server/version.rb
Normal file
5
lib/infisical_license_server/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module InfisicalLicenseServer
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
7
spec/infisical_license_server_spec.rb
Normal file
7
spec/infisical_license_server_spec.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe InfisicalLicenseServer do
|
||||
it "has a version number" do
|
||||
expect(InfisicalLicenseServer::VERSION).not_to be nil
|
||||
end
|
||||
end
|
||||
15
spec/spec_helper.rb
Normal file
15
spec/spec_helper.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "infisical_license_server"
|
||||
|
||||
RSpec.configure do |config|
|
||||
# Enable flags like --only-failures and --next-failure
|
||||
config.example_status_persistence_file_path = ".rspec_status"
|
||||
|
||||
# Disable RSpec exposing methods globally on `Module` and `main`
|
||||
config.disable_monkey_patching!
|
||||
|
||||
config.expect_with :rspec do |c|
|
||||
c.syntax = :expect
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue