diff --git a/.gitignore b/.gitignore index e3200e0..03a0905 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,10 @@ build-iPhoneSimulator/ # Used by RuboCop. Remote config files pulled in from inherit_from directive. # .rubocop-https?--* + +temp/ +output/ +license_key +license_key.pub +result.gitlab-license +features.list diff --git a/.root b/.root new file mode 100644 index 000000000..e69de29 diff --git a/feature.scan.py b/feature.scan.py new file mode 100755 index 000000000..b0fcc87 --- /dev/null +++ b/feature.scan.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import re +import os +import sys + +SCANNING_DIR = sys.argv[1] +OUTPUT_LIST_FILE = sys.argv[2] +print(f"[*] scanning directory: {SCANNING_DIR}") + +# use regex to find the string +# eg ::License.feature_available?(:aaa) || ::Feature.enabled?(:bbb, self) +# make sure +? for shortest match +REGEX_PARTTERN = "License.feature_available\?\(:.+?\)" + +REQUIRED_FILE_SUFFIX = ['rb'] + +scanning_file_list = set() +def build_file_list(input: str): + global scanning_file_list + scanning_file_list.add(input) + if os.path.isdir(input): + for file in os.listdir(input): + build_file_list(os.path.join(input, file)) +build_file_list(SCANNING_DIR) + +print(f"[*] scanning {len(scanning_file_list)} files...") +feature_list=set() +for file in scanning_file_list: + if not os.path.isfile(file): continue + if not file.split(".")[-1] in REQUIRED_FILE_SUFFIX: continue + with open(file, "r") as f: + content = f.read() + all_match = re.findall(REGEX_PARTTERN, content) + all_match = [x.split(":")[1].split(")")[0] for x in all_match] + all_match = [x for x in all_match if x] + feature_list.update(all_match) +print(f"[*] found {len(feature_list)} features") + +feature_list = list(feature_list) +feature_list.sort() + +print(f"[*] writing to {OUTPUT_LIST_FILE}...") +with open(OUTPUT_LIST_FILE, "w") as f: + for feature in feature_list: + f.write(feature + "\n") + +print(f"[*] done") diff --git a/generate_licenses.rb b/generate_licenses.rb index 87e25a9..344b4bc 100644 --- a/generate_licenses.rb +++ b/generate_licenses.rb @@ -1,31 +1,47 @@ -# GitLab License Generator +#!/usr/bin/env ruby +# encoding: utf-8 + +require 'openssl' +require_relative 'lib/license.rb' +puts "[i] lib gitlab-license: #{Gitlab::License::VERSION}" + +OUTPUT_DIR = ARGV[0] +puts "[*] output dir: #{OUTPUT_DIR}" LICENSE_TARGET_PRIVATE_KEY = "license_key" LICENSE_TARGET_PUBLIC_KEY = "license_key.pub" TARGET_LICENSE_FILE = 'result.gitlab-license' +TARGET_PLAIN_LICENSE_FILE = 'result.gitlab-license.json' -puts "[*] Booting generator" +FEATURE_LIST = [] +if ARGV[1].nil? + puts "[i] you can provide a list of feature to be enabled inside license" +else + FEATURE_LIST_FILE = ARGV[1] + File.open(FEATURE_LIST_FILE).each do |line| + FEATURE_LIST.push(line.chomp) + end + FEATURE_LIST.uniq! +end +puts "[*] loaded #{FEATURE_LIST.length} features" -require 'openssl' - -require_relative 'lib/license.rb' -CORE_LIB_VERSION = '2.2.1' - -puts "[i] Using core library version #{CORE_LIB_VERSION}" +Dir.chdir(OUTPUT_DIR) +puts "[*] switching working dir: #{Dir.pwd}" +puts "[*] generating license..." if !File.exist?(LICENSE_TARGET_PRIVATE_KEY) || !File.exist?(LICENSE_TARGET_PUBLIC_KEY) - puts "[*] Generating RSA keys..." + puts "[*] generating rsa key pair..." key = OpenSSL::PKey::RSA.new(2048) File.write(LICENSE_TARGET_PRIVATE_KEY, key.to_pem) File.write(LICENSE_TARGET_PUBLIC_KEY, key.public_key.to_pem) end -puts "[*] Loading RSA keys..." +puts "[*] loading key pair..." public_key = OpenSSL::PKey::RSA.new File.read(LICENSE_TARGET_PUBLIC_KEY) private_key = OpenSSL::PKey::RSA.new File.read(LICENSE_TARGET_PRIVATE_KEY) -puts "[*] Building license..." +puts "[*] building license..." Gitlab::License.encryption_key = private_key @@ -59,20 +75,17 @@ license.restrictions = { # required, just dont overflow } -puts "[*] Calling export..." +if !license.valid? + puts "[E] license validation failed!" + puts "[E] #{license.errors}" + exit 1 +end -puts "" -puts "=====================================================" +puts "[*] exporting license file..." -puts JSON.pretty_generate(JSON.parse(license.to_json)) - -puts "=====================================================" +File.open(TARGET_PLAIN_LICENSE_FILE, "w") { |f| f.write(JSON.pretty_generate(JSON.parse(license.to_json))) } data = license.export File.open(TARGET_LICENSE_FILE, "w") { |f| f.write(data) } -puts "=====================================================" -puts "" - -puts "[*] License generated successfully!" -puts "[*] License file: #{TARGET_LICENSE_FILE}" +puts "[*] done" diff --git a/lib/license.rb b/lib/license.rb index eaac533..c2648b5 100644 --- a/lib/license.rb +++ b/lib/license.rb @@ -15,6 +15,7 @@ module Gitlab class << self attr_reader :encryption_key + attr_reader :fallback_decryption_keys @encryption_key = nil def encryption_key=(key) @@ -24,6 +25,19 @@ module Gitlab @encryptor = nil end + def fallback_decryption_keys=(keys) + unless keys + @fallback_decryption_keys = nil + return + end + + unless keys.is_a?(Enumerable) && keys.all? { |key| key.is_a?(OpenSSL::PKey::RSA) } + raise ArgumentError, 'Invalid fallback RSA encryption keys provided.' + end + + @fallback_decryption_keys = Array(keys) + end + def encryptor @encryptor ||= Encryptor.new(encryption_key) end @@ -33,11 +47,7 @@ module Gitlab data = Boundary.remove_boundary(data) - begin - license_json = encryptor.decrypt(data) - rescue Encryptor::Error - raise ImportError, 'License data could not be decrypted.' - end + license_json = decrypt_with_fallback_keys(data) begin attributes = JSON.parse(license_json) @@ -47,6 +57,20 @@ module Gitlab new(attributes) end + + def decrypt_with_fallback_keys(data) + keys_to_try = Array(encryption_key) + keys_to_try += fallback_decryption_keys if fallback_decryption_keys + + keys_to_try.each do |decryption_key| + decryptor = Encryptor.new(decryption_key) + return decryptor.decrypt(data) + rescue Encryptor::Error + next + end + + raise ImportError, 'License data could not be decrypted.' + end end attr_reader :version @@ -54,7 +78,8 @@ module Gitlab :notify_users_at, :block_changes_at, :last_synced_at, :next_sync_at, :activated_at, :restrictions, :cloud_licensing_enabled, :offline_cloud_licensing_enabled, :auto_renew_enabled, :seat_reconciliation_enabled, - :operational_metrics_enabled, :generated_from_customers_dot + :operational_metrics_enabled, :generated_from_customers_dot, + :generated_from_cancellation alias_method :issued_at, :starts_at alias_method :issued_at=, :starts_at= @@ -65,43 +90,30 @@ module Gitlab def valid? if !licensee || !licensee.is_a?(Hash) || licensee.empty? - puts "Invalid License - licensee is not a hash or is empty" false elsif !starts_at || !starts_at.is_a?(Date) - puts "Invalid License - starts_at is not a date" false elsif !expires_at && !gl_team_license? && !jh_team_license? - puts "Invalid License - expires_at is not a date" false elsif expires_at && !expires_at.is_a?(Date) - puts "Invalid License - expires_at is not a date" false elsif notify_admins_at && !notify_admins_at.is_a?(Date) - puts "Invalid License - notify_admins_at is not a date" false elsif notify_users_at && !notify_users_at.is_a?(Date) - puts "Invalid License - notify_users_at is not a date" false elsif block_changes_at && !block_changes_at.is_a?(Date) - puts "Invalid License - block_changes_at is not a date" false elsif last_synced_at && !last_synced_at.is_a?(DateTime) - puts "Invalid License - last_synced_at is not a datetime" false elsif next_sync_at && !next_sync_at.is_a?(DateTime) - puts "Invalid License - next_sync_at is not a datetime" false elsif activated_at && !activated_at.is_a?(DateTime) - puts "Invalid License - activated_at is not a datetime" false elsif restrictions && !restrictions.is_a?(Hash) - puts "Invalid License - restrictions is not a hash" false elsif !cloud_licensing? && offline_cloud_licensing? - puts "Invalid License - offline_cloud_licensing_enabled is true but cloud_licensing_enabled is false" false else - puts "License is valid" true end end @@ -174,6 +186,10 @@ module Gitlab generated_from_customers_dot == true end + def generated_from_cancellation? + generated_from_cancellation == true + end + def gl_team_license? licensee['Company'].to_s.match?(/GitLab/i) && licensee['Email'].to_s.end_with?('@gitlab.com') end @@ -216,6 +232,7 @@ module Gitlab hash['operational_metrics_enabled'] = operational_metrics? hash['generated_from_customers_dot'] = generated_from_customers_dot? + hash['generated_from_cancellation'] = generated_from_cancellation? hash['restrictions'] = restrictions if restricted? @@ -265,6 +282,7 @@ module Gitlab seat_reconciliation_enabled operational_metrics_enabled generated_from_customers_dot + generated_from_cancellation ].each do |attr_name| public_send("#{attr_name}=", attributes[attr_name] == true) end diff --git a/lib/license/version.rb b/lib/license/version.rb index 3b734b9..c0c6742 100644 --- a/lib/license/version.rb +++ b/lib/license/version.rb @@ -1,5 +1,5 @@ module Gitlab class License - VERSION = '2.2.1'.freeze + VERSION = '2.4.0'.freeze end end diff --git a/license_key b/license_key deleted file mode 100644 index 8ed3b3a..000000000 --- a/license_key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAreEfP/ncA1A5cuxBz7rS0Z9DDxdSymLwt2OUSM5WJa+dVB3z -SpQjinifdNZq+iHVt8toZBZZ02H3unbn8td0rIifoj4oVpLhvnOAVjUn5tZeUX17 -tWMA+yyBpf6w6IFxeYBXFd14WOKEarS05U9B59DjBxNqSm+GzhljHO7vvTKy2xXQ -Q7Fa702DZ7jwr4DJnL87bDXfarnYksuawqtKwQbFHAOvxFj8ghBh1Gshap1abExD -4l7QWxFMTCVOkLJmXiqfOi5KuMiaMsSUsCBNQDE3A5aKvpwLGozsvpGRMy5Tt4Sg -HC7ZbgerBNe75olOoPDxZf7bBt0+O5A/UjK/HwIDAQABAoIBACb3f4hX112KugUu -OyVxidNebKnSIUSn3ahLkayrSRUTASAbwi0he8GJfLqzXrAFqx6QYCml9KVxnBHW -me6LKGOODrBOW73jFuIWgllPeky6F9MNWw7wTAT+GWP46u6AK8z93QZSZqkMwn4j -VzLYiz2HS4mHaVebHMvNVq/iQCnW9ztZnsv9HSoFt2WY2Cm/9UpAtbqrWRQTVnCt -F7E1M9KICUKyM13qOQe+d0sZWx6D8eKrFlPs4KDXATs2SuDsaWpmWj9G8alSeHEW -Ut+2MsS5BYNIVaG0KqDFRKDyTkhXzevz98r5KylFqfAB2bCnaqIE0hdOXfYd+CR0 -wwRAQmECgYEA1CnEO0K+nU8tZUwdTkL3wvo6z2jEnA97Laay9D/fnAjd3q8niTyJ -2DZQJp9omTa51/7EJw6YWhYdk078ZckwebWQPtXsA7MCTXSXL3+sGmL2GohDUovH -G6zdn9sKws+U6tIOoEOMCLivEtmNM7HJXP3PViQr+rOUQV3ig/8v+s8CgYEA0c5c -Or0Ta4apaM8aD6rP2Eilb3VC8AOvSzY36gN38ki/SwVH1ZTw/hbOYlQTsnk+OkXX -205k9tc78+9GrcYSuupjqzEdZVRQSGSbT9qXMMYfM3wK2Z7i37Cehn4Qw4BOOlgR -TvsvBd0FSnzVi2wAkhx0zL1hNUXHHAYnVdOxyrECgYEAwKbkb0NePw4ElLUW71fU -DxKVkHz7+xH7sipq2WueqttKTMkTx4RXTyOSiF+75VRSURYgG68fHL50QK06d1rH -T91UjBpIY9uKvbafChyOtK8j9lfBehU+yZyg6mVGUjuYZ9oyOcjcQZciMqWlmEla -Jby7JudVoCKs/uY3p9BzSvUCgYAF7Pkn44033T7NqgPHa4ChUDPz+PDiDIiX7Dka -D+0EV8+nU8fanXFNC+HaXxuLT+dVCAH3vLgXTK7xzdFGOTDwPIyCGkoFQaNe2BCW -6cqZYw8giiFYUieAP+HKVKcujmInPbOHcoq6dKqglvQFExDVD56w5axoL8dW4Eme -H/OGkQKBgHgQeK29Ntz7LcKlXYhQPkmYn+DWAmEq4J6XjjXyCV82HgEMmhIiAKKI -UURKt4j6c7KSiAhnyITz9JeVRoAFVB3y/tSSc5E+CH3jG/G0YlToW20Itf6o8hwD -XERkPPwsXVoZWR2FcUzcO7Bspm/JvkuaL+4u1fi+eNl7uF7RRaD1 ------END RSA PRIVATE KEY----- diff --git a/license_key.pub b/license_key.pub deleted file mode 100644 index 64eb81d..000000000 --- a/license_key.pub +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAreEfP/ncA1A5cuxBz7rS -0Z9DDxdSymLwt2OUSM5WJa+dVB3zSpQjinifdNZq+iHVt8toZBZZ02H3unbn8td0 -rIifoj4oVpLhvnOAVjUn5tZeUX17tWMA+yyBpf6w6IFxeYBXFd14WOKEarS05U9B -59DjBxNqSm+GzhljHO7vvTKy2xXQQ7Fa702DZ7jwr4DJnL87bDXfarnYksuawqtK -wQbFHAOvxFj8ghBh1Gshap1abExD4l7QWxFMTCVOkLJmXiqfOi5KuMiaMsSUsCBN -QDE3A5aKvpwLGozsvpGRMy5Tt4SgHC7ZbgerBNe75olOoPDxZf7bBt0+O5A/UjK/ -HwIDAQAB ------END PUBLIC KEY----- diff --git a/make.sh b/make.sh index dc2541d..2f7649e 100755 --- a/make.sh +++ b/make.sh @@ -1,4 +1,81 @@ -#!/bin/bash +#!/bin/zsh -cd "$(dirname "$0")" || exit 1 -ruby ./generate_licenses.rb \ No newline at end of file +set -e + +cd "$(dirname "$0")" +if [ ! -f ".root" ]; then + echo "[!] failed to locate project directory, aborting..." + exit 1 +fi +WORKING_DIR=$(pwd) + +mkdir temp || true + +echo "[*] fetching ruby gem version..." +RB_GEM_NAME="gitlab-license" +RB_GEM_LIST_OUTPUT=$(gem list --remote $RB_GEM_NAME) + +RB_GEM_VERSION="" +while IFS= read -r line; do + if [[ $line == "gitlab-license ("* ]]; then + RB_GEM_VERSION=${line#"gitlab-license ("} + RB_GEM_VERSION=${RB_GEM_VERSION%")"} + break + fi +done <<< "$RB_GEM_LIST_OUTPUT" + +echo "[*] gitlab-license version: $RB_GEM_VERSION" +RB_GEM_DOWNLOAD_URL="https://rubygems.org/downloads/gitlab-license-$RB_GEM_VERSION.gem" +RB_GEM_DOWNLOAD_PATH=$(pwd)/temp/gem/gitlab-license.gem +mkdir -p $(dirname $RB_GEM_DOWNLOAD_PATH) +curl -L $RB_GEM_DOWNLOAD_URL -o $RB_GEM_DOWNLOAD_PATH +pushd $(dirname $RB_GEM_DOWNLOAD_PATH) > /dev/null +tar -xzf gitlab-license.gem +tar -xzf data.tar.gz + +if [ ! -f "./lib/gitlab/license.rb" ]; then + echo "[!] failed to locate gem file, aborting..." + exit 1 +fi + +echo "[*] copying gem..." +rm -rf "$WORKING_DIR/lib" || true +mkdir -p "$WORKING_DIR/lib" +cp -r ./lib/gitlab/* $WORKING_DIR/lib +popd > /dev/null + +pushd lib > /dev/null +echo "[*] patching lib requirements gem..." +# replace `require 'gitlab/license/` with `require 'license/` to make it work +find . -type f -exec sed -i '' 's/require '\''gitlab\/license\//require_relative '\''license\//g' {} \; +popd > /dev/null + +echo "[*] updated gem" + +echo "[*] fetching gitlab source code..." +GITLAB_SOURCE_CODE_DIR=$(pwd)/temp/src/ +if [ -d "$GITLAB_SOURCE_CODE_DIR" ]; then + echo "[*] gitlab source code already exists, skipping cloning..." +else + echo "[*] cloning gitlab source code..." + git clone https://gitlab.com/gitlab-org/gitlab.git $GITLAB_SOURCE_CODE_DIR +fi + +echo "[*] updating gitlab source code..." +pushd $GITLAB_SOURCE_CODE_DIR > /dev/null +git clean -fdx -f +git reset --hard +git pull +popd > /dev/null + +echo "[*] scanning features..." +FEATURE_LIST_FILE=$(pwd)/temp/features.txt +rm -f $FEATURE_LIST_FILE || true +./feature.scan.py $GITLAB_SOURCE_CODE_DIR $FEATURE_LIST_FILE + +echo "[*] generating license..." +OUTPUT_DIR=$(pwd)/output +mkdir -p $OUTPUT_DIR +ruby ./generate_licenses.rb $OUTPUT_DIR $FEATURE_LIST_FILE + +echo "[*] done $(basename $0)" \ No newline at end of file diff --git a/result.gitlab-license b/result.gitlab-license deleted file mode 100644 index 6b8ee3a..000000000 --- a/result.gitlab-license +++ /dev/null @@ -1,24 +0,0 @@ -eyJkYXRhIjoiOW5mNE9BaTBjYmxMendXbks4TGdDOWpDeVpNM1QzYzBNdzNs -WS94WGlVR1o3UG9kNDNtWGlmSzJYcDNIXG5pWlc2UEhtVld0VFVUY3RabjF2 -Rk9wWXliZm1XUjM5dkxSQVhkVzJmcFBCQjd2eWwvY3kzaWF5dEdCZkxcblIx -NWk4bmcvRlBpZ1Q2bXI1aFhoT1VKZm9xSHJ5RkE3NXV1b01IQkI0WHI4RGhl -WVpsTEt6RmJueEZJdlxuRVkxZDk0Umd4d0dxcitqSDg0ZWMwKzhWcDFYaFEy -NmhJWnpWMXRXWjY1Q2VVYVJ5TzJ2c2loeGhuRWRzXG5iOTVYWTZVaVJ6TlZs -Ty8xaHByY3A1eklBVzBJQ0lwM2c3anR2SEtCWExuUkxnUDJucTN0UkhWNXM5 -T2dcbllmVkhseUZqZjRvclFpeGc1TEFVZmcwR0R6N0pDck9oc0xtWUZxTXQw -SkZkamQ5YlhGU21OdzhkN2x4M1xuWU12MitXbS9zZ1ZiQzVFa1VJWExSaHlH -bThjRjZNbEoya3N3MCttREVlNDFCem1ncHVWWWpyT01DQlJFXG56My9DSWRV -dnFqY0ljTFFYYUdFajRTNmJtdjVxTXhYQTZiR3Bkd2orTVM4Y0doYjhGVENS -UWVsRVlSVGVcbnZ1Z3lJdnZ6V2dYQWtCZk5NTU9Rd2JmMHFmOVJNK3FyN2lQ -VXkxRWlYU2hDaHloTDhnV3BNc1FvVVNUNVxuSHZDcEE1Y2dPUmV1dC9YS1RH -OHRIMFRJR2RmNjRDd3pFeENQKzN4QWJmM3IrSDJrZlhzaTFJWVhya25CXG5u -aFhGTm83Q1doNUFJVWd1SmdnPVxuIiwia2V5IjoiT3huRG1YTEozcXVOZita -K2lvU3RQMm5wa2U2K2ZMNVBpdEtJSkNkTUhNeTlHTmdhTVNoNnZlbERaUExz -XG5JK3dJaHlEZ09vc2k0OEM4Tmp6Q29oL05mV3gybDJGRmxRSkFQLytRdGd4 -eDJiRnpFRlF5bk0zc0dnRUtcbkZRUkQzUGNTWkpzZGtXdkltWGEzWnlsMEg2 -WEtGelgrTnE0cGhqWktuQ1dneXk3dGU4YWI5Qm5ZNVBlYVxuWUNPL1I0SGpI -MkhKTnhmamd2TWh0YThMNXgrbmt2RXhBODFIRDFsQlVXZzJlQi92RzZjblg0 -YkRPRWF3XG5tVHJpMS9VLzRVcVVoQnRSYklVSnZZeGFCMC9hT3owaStLVkdw -SENSTCs3OXQveHA1NVg2bkRQWlhwdGdcbkE0WXJrbFVHUlF0RnZyMjRRMGxa -TndrS2Z4eU1pOWkrclgrVzVIa1MvQT09XG4iLCJpdiI6Im1FL1RodjE4MWJQ -eGNFaWVwdEhCWnc9PVxuIn0=