зеркало из
1
0
Форкнуть 0
This commit is contained in:
Dan Hoerst 2022-06-07 12:17:36 -04:00
Коммит abae8ccf9b
535 изменённых файлов: 16643 добавлений и 0 удалений

83
.github/workflows/acceptance.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,83 @@
name: acceptance
on:
push:
branches:
- main
pull_request:
jobs:
# Detects changes to any of the source files for entitlements-github-plugin
changes:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
has_change: ${{ steps.diff.outputs.has_change}}
steps:
- uses: actions/checkout@v2
- id: fetch-base
if: github.event_name == 'pull_request'
name: fetch the latest commit in the base branch to diff against
run: git fetch --no-tags --prune --depth=1 origin '+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}'
- id: diff
if: github.event_name == 'pull_request'
name: diff against the base branch latest commit for specific paths
run: |
git diff \
origin/${{ github.base_ref }} \
HEAD \
-- \
'bin/**' \
'lib/**' \
'script/**' \
'spec/**' \
'vendor/**' \
'.ruby-version' \
'entitlements-github-plugin.gemspec' \
'Gemfile' \
'Gemfile.lock' \
> diff.txt
# If the diff file is not empty, it has changes.
[ -s diff.txt ] && echo "::set-output name=has_change::true" || echo "::set-output name=has_change::false"
- name: set has_change to true for push to main/master
if: github.event_name == 'push'
run: echo "::set-output name=has_change::true"
acceptance-suite:
needs: changes
runs-on: ubuntu-latest
name: runner / acceptance-tests
permissions:
contents: read
steps:
# If source files were not changed, we don't need the acceptance test suite
- name: bypass
if: ${{ needs.changes.outputs.has_change != 'true' }}
run: |
echo "✅ Bypassing acceptance tests - they are not required for this change"
- name: Check out code
if: ${{ needs.changes.outputs.has_change == 'true' }}
uses: actions/checkout@v2
# Use Docker layer caching for 'docker build' and 'docker-compose build' commands.
# https://github.com/satackey/action-docker-layer-caching/releases/tag/v0.0.11
- uses: satackey/action-docker-layer-caching@46d2c640b1d8ef50d185452ad6fb324e6bd1d052
if: ${{ needs.changes.outputs.has_change == 'true' }}
continue-on-error: true
- name: acceptance tests
if: ${{ needs.changes.outputs.has_change == 'true' }}
run: script/cibuild-entitlements-github-plugin-acceptance
- name: acceptance tests passed
run: echo "✅ The acceptance test suite has passed"

45
.github/workflows/codeql-analysis.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,45 @@
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '25 4 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'ruby' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

27
.github/workflows/lint.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,27 @@
name: lint
on:
push:
branches:
- main
pull_request:
jobs:
rubocop:
name: runner / rubocop
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out code
uses: actions/checkout@v2
# https://github.com/ruby/setup-ruby/releases/tag/v1.87.0
- uses: ruby/setup-ruby@cf1a6dd2d8563b59c7007e381836fd252ab2ac5b
with:
ruby-version: 2.7.5
bundler-cache: true
- name: rubocop
run: bundle exec rubocop -c .rubocop.yml lib/ spec/

27
.github/workflows/test.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,27 @@
name: test
on:
push:
branches:
- main
pull_request:
jobs:
rubocop:
name: runner / rspec
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out code
uses: actions/checkout@v2
# https://github.com/ruby/setup-ruby/releases/tag/v1.87.0
- uses: ruby/setup-ruby@cf1a6dd2d8563b59c7007e381836fd252ab2ac5b
with:
ruby-version: 2.7.5
bundler-cache: true
- name: rspec tests
run: script/test -d

19
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,19 @@
/.bundle
/vendor/gems
# Ignore binstubs
bin/*
!bin/.keep
# There's a place for local caching of container gems to make local builds faster.
# Keep the .keep file but not the gems themselves
vendor/container-gems/*
!vendor/container-gems/.keep
# Coverage reports
coverage/*
.*.swp
# Ignore JetBrains IDEs
.idea

12
.rubocop.yml Normal file
Просмотреть файл

@ -0,0 +1,12 @@
inherit_gem:
rubocop-github:
- config/default.yml
AllCops:
DisplayCopNames: true
TargetRubyVersion: 2.7.5
Exclude:
- 'bin/*'
- 'spec/acceptance/fixtures/**/*'
- 'spec/unit/fixtures/**/*'
- 'vendor/gems/**/*'

1
.ruby-version Normal file
Просмотреть файл

@ -0,0 +1 @@
2.7.5

5
Gemfile Normal file
Просмотреть файл

@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gemspec

138
Gemfile.lock Normal file
Просмотреть файл

@ -0,0 +1,138 @@
PATH
remote: .
specs:
entitlements-github-plugin (0.0.1)
concurrent-ruby (= 1.1.9)
contracts (= 0.16.0)
faraday (>= 0.17.3, < 0.18)
net-ldap (~> 0.17.0)
octokit (~> 4.18)
optimist (= 3.0.0)
GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
ast (2.4.2)
concurrent-ruby (1.1.9)
contracts (0.16.0)
contracts-rspec (0.1.0)
crack (0.4.5)
rexml
diff-lcs (1.4.4)
docile (1.4.0)
entitlements (0.1.5.g0306a452)
concurrent-ruby (= 1.1.9)
contracts (= 0.16.0)
faraday (>= 0.17.3, < 0.18)
net-ldap (~> 0.17.0)
octokit (~> 4.18)
optimist (= 3.0.0)
faraday (0.17.4)
multipart-post (>= 1.2, < 3)
hashdiff (1.0.1)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.5.1)
minitest (5.15.0)
multipart-post (2.1.1)
net-ldap (0.17.0)
octokit (4.21.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
optimist (3.0.0)
parallel (1.22.1)
parser (3.1.2.0)
ast (~> 2.4.1)
public_suffix (4.0.6)
rack (2.2.3)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.4.0)
rexml (3.2.5)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-core (3.8.0)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-mocks (3.8.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.3)
rubocop (1.29.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.17.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.18.0)
parser (>= 3.1.1.0)
rubocop-github (0.17.0)
rubocop
rubocop-performance
rubocop-rails
rubocop-performance (1.13.3)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.14.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.0)
rugged (0.27.5)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
simplecov (0.16.1)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-erb (0.1.1)
simplecov
simplecov-html (0.10.2)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
unicode-display_width (2.1.0)
vcr (4.0.0)
webmock (3.4.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
zeitwerk (2.5.4)
PLATFORMS
ruby
x86_64-darwin-19
DEPENDENCIES
contracts-rspec (= 0.1.0)
entitlements (= 0.1.5.g0306a452)
entitlements-github-plugin!
rake (= 13.0.6)
rspec (= 3.8.0)
rspec-core (= 3.8.0)
rubocop (= 1.29.1)
rubocop-github (= 0.17.0)
rubocop-performance (= 1.13.3)
rugged (= 0.27.5)
simplecov (= 0.16.1)
simplecov-erb (= 0.1.1)
vcr (= 4.0.0)
webmock (= 3.4.2)
BUNDLED WITH
2.2.24

84
README.md Normal file
Просмотреть файл

@ -0,0 +1,84 @@
# entitlements-github-plugin
[![acceptance](https://github.com/github/entitlements-github-plugin/actions/workflows/acceptance.yml/badge.svg)](https://github.com/github/entitlements-github-plugin/actions/workflows/acceptance.yml) [![test](https://github.com/github/entitlements-github-plugin/actions/workflows/test.yml/badge.svg)](https://github.com/github/entitlements-github-plugin/actions/workflows/test.yml) [![lint](https://github.com/github/entitlements-github-plugin/actions/workflows/lint.yml/badge.svg)](https://github.com/github/entitlements-github-plugin/actions/workflows/lint.yml) [![coverage](https://img.shields.io/badge/coverage-100%25-success)](https://img.shields.io/badge/coverage-100%25-success) [![style](https://img.shields.io/badge/code%20style-rubocop--github-blue)](https://github.com/github/rubocop-github)
`entitlements-github-plugin` is an [entitlements-app](https://github.com/github/entitlements-app) plugin allowing entitlements configs to be used to manage membership of GitHub.com Organizations and Teams.
## Usage
Your `entitlements-app` config `config/entitlements.yaml` runs through ERB interpretation automatically. You can extend your entitlements configuration to load plugins like so:
```
<%-
unless ENV['CI_MODE']
begin
require_relative "/data/entitlements/lib/entitlements-and-plugins"
rescue Exception
begin
require_relative "lib/entitlements-and-plugins"
rescue Exception
# We might not have the plugins installed and still want this file to be
# loaded. Don't raise anything but silently fail.
end
end
end
-%>
```
You can then define `lib/entitlements-and-plugins` like so:
```
#!/usr/bin/env ruby
# frozen_string_literal: true
ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", File.dirname(__FILE__))
require "bundler/setup"
require "entitlements"
# require entitlements plugins here
require "entitlements/backend/github_org"
require "entitlements/backend/github_team"
require "entitlements/service/github"
```
Any plugins defined in `lib/entitlements-and-plugins` will be loaded and used at `entitlements-app` runtime.
## Features
### Org Team
`entitlements-github-plugin` manages org team membership to two roles - `admin` and `member`. Your `entitlements-app` config `config/entitlements.yaml` is used to configure the location for the declarations of this membership.
```
github.com/github/org:
addr: <%= ENV["GITHUB_API_BASE"] %>
base: ou=org,ou=github,ou=GitHub,dc=github,dc=com
dir: github.com/github/org
org: github
token: <%= ENV["GITHUB_ORG_TOKEN"] %>
type: "github_org"
```
`entitlements-github-plugin` will look in the defined location above, `github.com/github/org`, for `admin.txt` and `member.txt` defining the respective membership for each role.
### GitHub Teams
`entitlements-github-plugin` manages membership for all teams listed in the defined subfolder. The plugin will use extension-less name of the file as the team name. GitHub Team management can be configured like so:
```
github.com/github/teams:
addr: <%= ENV["GITHUB_API_BASE"] %>
base: ou=teams,ou=github,ou=GitHub,dc=github,dc=com
dir: github.com/github/teams
org: github
token: <%= ENV["GITHUB_ORG_TOKEN"] %>
type: "github_team"
```
For example, if there were a file `github.com/github/teams/new-team.txt` with a single user inside, a GitHub.com Team would be created in the `github` org with the name `new-team`.
#### Metadata
Entitlements configs can contain metadata which the plugin will use to make further configuration decisions.
`metadata_parent_team_name` - when defined in an entitlements config, the defined team will be made the parent team of this GitHub.com Team.

1
VERSION Normal file
Просмотреть файл

@ -0,0 +1 @@
0.0.1

0
bin/.keep Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,35 @@
# frozen_string_literal: true
Gem::Specification.new do |s|
s.name = "entitlements-github-plugin"
s.version = File.read("VERSION").chomp
s.summary = "GitHub dotcom provider for entitlements-app"
s.description = ""
s.authors = ["GitHub, Inc. Security Ops"]
s.email = "security@github.com"
s.license = "MIT"
s.files = Dir.glob("lib/**/*") + %w[VERSION]
s.homepage = "https://github.com/github/entitlements-github-plugin"
s.executables = %w[]
s.add_dependency "concurrent-ruby", "= 1.1.9"
s.add_dependency "contracts", "= 0.16.0"
s.add_dependency "faraday", ">= 0.17.3", "< 0.18"
s.add_dependency "net-ldap", "~> 0.17.0"
s.add_dependency "octokit", "~> 4.18"
s.add_dependency "optimist", "= 3.0.0"
s.add_development_dependency "contracts-rspec", "= 0.1.0"
s.add_development_dependency "entitlements", "0.1.5.g0306a452"
s.add_development_dependency "rake", "= 13.0.6"
s.add_development_dependency "rspec", "= 3.8.0"
s.add_development_dependency "rspec-core", "= 3.8.0"
s.add_development_dependency "rubocop", "= 1.29.1"
s.add_development_dependency "rubocop-github", "= 0.17.0"
s.add_development_dependency "rubocop-performance", "= 1.13.3"
s.add_development_dependency "rugged", "= 0.27.5"
s.add_development_dependency "simplecov", "= 0.16.1"
s.add_development_dependency "simplecov-erb", "= 0.1.1"
s.add_development_dependency "vcr", "= 4.0.0"
s.add_development_dependency "webmock", "3.4.2"
end

Просмотреть файл

@ -0,0 +1,26 @@
# frozen_string_literal: true
module Entitlements
class Backend
class GitHubOrg
include ::Contracts::Core
C = ::Contracts
# There are certain supported roles (which are mutually exclusive): admin, billing manager, member.
# Define these in this one central place to be consumed everywhere.
# The key is the name of the Entitlement, and that data is how this role appears on dotcom.
ORGANIZATION_ROLES = {
"admin" => "ADMIN",
# `billing-manager` is currently not supported
"member" => "MEMBER"
}
# Error classes
class DuplicateUserError < RuntimeError; end
end
end
end
require_relative "github_org/controller"
require_relative "github_org/provider"
require_relative "github_org/service"

Просмотреть файл

@ -0,0 +1,468 @@
# frozen_string_literal: true
# TL;DR: There are multiple shenanigans here, so please read this wall of text.
#
# This controller is different from many of the others because it has 2 mutually-exclusive entitlements that convey
# access to the same thing (a GitHub organization) with a different parameter (role). As such, this controller
# calculates the specific reconciliation actions and passes them along to the service. (Normally, the controller
# passes groups to the service, which figures out changes.) Taking the approach here allows less passing back and
# forth of data structures, such as "the set of all users we've seen".
#
# This controller also supports per-OU feature flags, settable in the configuration. It is possible to disable
# inviting new members to the organization, and removing old members from the organization, if there's already
# a process in place to manage that.
#
# Available features (defined in an array named `features` which can be empty):
# * invite: Invite a non-member to the organization
# * remove: Remove a non-member (or no-longer-known-to-entitlements user) from the organization
#
# If `features` is undefined, all available features will be applied. If you want to include neither of these features,
# set `features` to the empty array (`[]`) in the configuration. Note that moving an existing member of an organization
# from one role to another is always enabled, regardless of feature flag settings.
#
# But wait, there's even more. When a user gets added to a GitHub organization for the first time, they are not actually
# added to the member list right away, but instead they're invited and need to accept an invitation by e-mail before they
# show up in the list. We don't want to add (invite) a member via Entitlements and then have them showing up as a "needs
# to be added" diff on every single deploy until they accept the invitation. So, we will fudge an invited user to be
# "exactly the permissions Entitlements thinks they have" when they show up on the pending list. Unfortunately the pending
# list doesn't show whether they're invited as an admin or a member, so there's a potential gap between when they accept the
# invitation and the next Entitlements deploy where they could have the wrong privilege if they were invited as one thing
# but their role changed in Entitlements before they accepted the invite. This could be addressed by exposing their role
# on the pending member list in the GraphQL API.
#
# The mapping we need to implement looks like this:
#
# +-------------------+----------------+-----------------+----------------+----------------+
# | | Has admin role | Has member role | Pending invite | Does not exist |
# +-------------------+----------------+-----------------+----------------+----------------+
# | In "admin" group | No change | Move | Leave as-is | Invite |
# +-------------------+----------------+-----------------+----------------+----------------+
# | In "member" group | Move | No change | Leave as-is | Invite |
# +-------------------+----------------+-----------------+----------------+----------------+
# | No entitlement | Remove | Remove | Cancel invite | n/a |
# +-------------------+----------------+-----------------+----------------+----------------+
module Entitlements
class Backend
class GitHubOrg
class Controller < Entitlements::Backend::BaseController
# Controller priority and registration
def self.priority
30
end
register
include ::Contracts::Core
C = ::Contracts
AVAILABLE_FEATURES = %w[invite remove]
DEFAULT_FEATURES = %w[invite remove]
ROLES = Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES.keys.freeze
# Constructor. Generic constructor that takes a hash of configuration options.
#
# group_name - Name of the corresponding group in the entitlements configuration file.
# config - Optionally, a Hash of configuration information (configuration is referenced if empty).
Contract String, C::Maybe[C::HashOf[String => C::Any]] => C::Any
def initialize(group_name, config = nil)
super
@provider = Entitlements::Backend::GitHubOrg::Provider.new(config: @config)
end
# Calculation routines.
#
# Takes no arguments.
#
# Returns a list of @actions.
Contract C::None => C::Any
def calculate
@actions = []
validate_github_org_ous! # calls read_all() for the OU
validate_no_dupes! # calls read() for each group
if changes.any?
print_differences(key: group_name, added: [], removed: [], changed: changes, ignored_users: ignored_users)
@actions.concat(changes)
else
logger.debug "UNCHANGED: No GitHub organization changes for #{group_name}"
end
end
# Apply changes.
#
# action - Action array.
#
# Returns nothing.
Contract Entitlements::Models::Action => C::Any
def apply(action)
unless action.existing.is_a?(Entitlements::Models::Group) && action.updated.is_a?(Entitlements::Models::Group)
logger.fatal "#{action.dn}: GitHub entitlements interface does not support creating or removing a GitHub org"
raise RuntimeError, "Invalid Operation"
end
if provider.commit(action)
logger.debug "APPLY: Updating GitHub organization #{action.dn}"
else
logger.warn "DID NOT APPLY: Changes not needed to #{action.dn}"
logger.debug "Old: #{action.existing.inspect}"
logger.debug "New: #{action.updated.inspect}"
end
end
# Validate configuration options.
#
# key - String with the name of the group.
# data - Hash with the configuration data.
#
# Returns nothing.
Contract String, C::HashOf[String => C::Any] => nil
def validate_config!(key, data)
spec = COMMON_GROUP_CONFIG.merge({
"base" => { required: true, type: String },
"addr" => { required: false, type: String },
"org" => { required: true, type: String },
"token" => { required: true, type: String },
"features" => { required: false, type: Array },
"ignore" => { required: false, type: Array }
})
text = "GitHub organization group #{key.inspect}"
Entitlements::Util::Util.validate_attr!(spec, data, text)
# Validate any features against the list of known features.
if data["features"].is_a?(Array)
invalid_flags = data["features"] - AVAILABLE_FEATURES
if invalid_flags.any?
raise "Invalid feature(s) in #{text}: #{invalid_flags.join(', ')}"
end
end
end
def prefetch
existing_groups
end
private
# Utility method to remove repetitive code. From a given hash (`added`, `moved`, `removed`), select
# changes for the specified role, sort by username, and return an array of properly capitalized usernames.
#
# obj - The hash (`added`, `moved`, `removed`)
# role - The role to be selected
#
# Returns an Array of Strings.
Contract C::HashOf[String => { member: String, role: String }], String => C::ArrayOf[String]
def sorted_users_from_hash(obj, role)
obj.select { |_, role_data| role_data[:role] == role }
.sort_by { |username, _| username } # Already downcased by the nature of the array
.map { |_, role_data| role_data[:member] } # Member name with proper case
end
# Validate that each entitlement defines the correct roles (and only the correct roles).
# Raise if this is not the case.
#
# Takes no arguments.
#
# Returns nothing (but, will raise an error if something is broken).
Contract C::None => C::Any
def validate_github_org_ous!
updated = Entitlements::Data::Groups::Calculated.read_all(group_name, config)
# If we are missing an expected role this is a fatal error.
ROLES.each do |role|
role_dn = ["cn=#{role}", config.fetch("base")].join(",")
unless updated.member?(role_dn)
logger.fatal "GitHubOrg: No group definition for #{group_name}:#{role} - abort!"
raise "GitHubOrg must define admin and member roles."
end
end
# If we have an unexpected role that's also an error.
seen_roles = updated.map { |x| Entitlements::Util::Util.first_attr(x) }
unexpected_roles = seen_roles - ROLES
return unless unexpected_roles.any?
logger.fatal "GitHubOrg: Unexpected role(s) in #{group_name}: #{unexpected_roles.join(', ')}"
raise "GitHubOrg unexpected roles."
end
# Validate that within a given GitHub organization, a given person is not assigned to multiple
# roles. Raise if a duplicate user is found.
#
# Takes no arguments.
#
# Returns nothing (but, will raise an error if something is broken).
Contract C::None => C::Any
def validate_no_dupes!
users_seen = Set.new
ROLES.each do |role|
role_dn = ["cn=#{role}", config.fetch("base")].join(",")
group = Entitlements::Data::Groups::Calculated.read(role_dn)
users_set = Set.new(group.member_strings_insensitive)
dupes = users_seen & users_set
if dupes.empty?
users_seen.merge(users_set)
else
message = "Users in multiple roles for #{group_name}: #{dupes.to_a.sort.join(', ')}"
logger.fatal message
raise Entitlements::Backend::GitHubOrg::DuplicateUserError, "Abort due to users in multiple roles"
end
end
end
def existing_groups
@existing_groups ||= begin
ROLES.map do |role|
role_dn = ["cn=#{role}", config.fetch("base")].join(",")
[role, provider.read(role_dn)]
end.to_h
end
end
# For a given OU, calculate the changes.
#
# Takes no arguments.
#
# Returns an array of change actions.
Contract C::None => C::ArrayOf[Entitlements::Models::Action]
def changes
return @changes if @changes
begin
features = Set.new(config["features"] || DEFAULT_FEATURES)
# Populate group membership into groups hash, so that these groups can be mutated later if users
# are being ignored or organization membership is pending.
groups = ROLES.map do |role|
role_dn = ["cn=#{role}", config.fetch("base")].join(",")
[role, Entitlements::Data::Groups::Calculated.read(role_dn)]
end.to_h
# Categorize changes by :added (invite user to organization), :moved (change a user's role), and
# :removed (remove a user from the organization). This operates across all roles.
chg = categorized_changes
# Keep track of any actions needed to make changes.
result = []
# Get the pending members for the organization.
pending = provider.pending_members
# Handle pending members who are not in any entitlements groups (i.e. they were previously invited, but are
# not in entitlements, so we need to cancel their invitation). We don't know from the query whether these users
# are in the 'admin' or 'member' role, so just assign them to the member role. It really doesn't matter except
# for display purposes because the net result is the same -- entitlements will see them as existing in the provider
# but not supposed to exist so it will remove them.
disinvited_users(groups, pending).each do |person_dn|
existing_groups[ROLES.last].add_member(person_dn)
chg[:removed][person_dn.downcase] = { member: person_dn, role: ROLES.last }
end
# For each role:
# - Create actions respecting feature flags
# - Hack changes to calculated membership if invite/remove is disabled by feature flag
# - Calculate actions needed to make changes
ROLES.each do |role|
role_dn = ["cn=#{role}", config.fetch("base")].join(",")
# Respecting feature flags, batch up the additions, move-ins, and removals in separate actions.
# Note that "move-outs" are not tracked because moving in to one role automatically removes from
# the existing role without an explicit API call for the removal.
action = Entitlements::Models::Action.new(role_dn, existing_groups[role], groups[role], group_name)
invited = sorted_users_from_hash(chg[:added], role)
moved_in = sorted_users_from_hash(chg[:moved], role)
removals = sorted_users_from_hash(chg[:removed], role)
# If there are any `invited` members that are also `pending`, remove these from invited, and fake
# them into the groups they are slated to join. This will make Entitlements treat this as a no-op
# to avoid re-inviting these members.
already_invited = remove_pending(invited, pending)
already_invited.each do |person_dn|
# Fake the member into their existing group so this does not show up as a change every time
# that Entitlements runs.
existing_groups[role].add_member(person_dn)
end
# `invited` are users who did not have any role in the organization before. Adding them to the
# organization will generate an invitation that they must accept.
if features.member?("invite")
invited.each do |person_dn|
action.add_implementation({ action: :add, person: person_dn })
end
elsif invited.any?
suppressed = invited.map { |k| Entitlements::Util::Util.first_attr(k) }.sort
targets = [invited.size, invited.size == 1 ? "person:" : "people:", suppressed.join(", ")].join(" ")
logger.debug "GitHubOrg #{group_name}:#{role}: Feature `invite` disabled. Not inviting #{targets}."
invited.each do |person_dn|
# Remove the user from their new group so this does not show up as a change every time
# that Entitlements runs.
groups[role].remove_member(person_dn)
end
end
# `moved_in` are users who exist in the organization but currently have a different role. Adding them
# to the current role will also remove them from their old role (since a person can have exactly one role).
# There is no feature flag to disable this action.
moved_in.each do |person_dn|
action.add_implementation({ action: :add, person: person_dn })
end
# `removals` are users who were in the organization but no longer are assigned to any role.
# The resulting API call will remove the user from the organization.
if features.member?("remove")
removals.each do |person_dn|
action.add_implementation({ action: :remove, person: person_dn })
end
elsif removals.any?
suppressed = removals.map { |k| Entitlements::Util::Util.first_attr(k) }.sort
targets = [removals.size, removals.size == 1 ? "person:" : "people:", suppressed.join(", ")].join(" ")
logger.debug "GitHubOrg #{group_name}:#{role}: Feature `remove` disabled. Not removing #{targets}."
removals.each do |person_dn|
# Add the user back to their group so this does not show up as a change every time
# that Entitlements runs.
groups[role].add_member(person_dn)
end
end
# re-diff with the modified groups to give accurate responses on whether there are changes.
# Also, each move has an addition and a removal, but there's just one API call (the addition),
# but for consistency we want the "diff" to show both the addition and the removal.
diff = provider.diff(groups[role], ignored_users)
if diff[:added].empty? && diff[:removed].empty?
logger.debug "UNCHANGED: No GitHub organization changes for #{group_name}:#{role}"
next
end
# Case-sensitize the existing members, which will be reporting all names in lower case because that's
# how they come from the GitHub provider. If we have seen the member with correct capitalization,
# replace the member entry with the correctly cased one. (There's no need to do this for the newly
# invited members beause those won't show up in the group of existing members.)
all_changes = chg[:moved].merge(chg[:removed])
all_changes.each do |_, data|
action.existing.update_case(data[:member])
end
result << action
end
# If there are changes, determine if the computed `org_members` are based on a predictive cache
# or actual data from the API. If they are based on a predictive cache, then we need to invalidate
# the predictive cache and repeat *all* of this logic with fresh data from the API. (We will just
# call ourselves once the cache is invalidated to repeat.)
if result.any? && provider.github.org_members_from_predictive_cache?
provider.invalidate_predictive_cache
result = changes
else
result
end
end
@changes ||= result
result
end
# For a given OU, translate the entitlement members into `invited`, `removed`, and `moved` hashes.
#
# Takes no arguments.
#
# Returns the structured hash of hashes, with keys :added, :removed, and :moved.
Contract C::None => Hash[
added: C::HashOf[String => Hash],
removed: C::HashOf[String => Hash],
moved: C::HashOf[String => Hash]
]
def categorized_changes
added = {}
removed = {}
moved = {}
ROLES.each do |role|
role_dn = ["cn=#{role}", config.fetch("base")].join(",")
# Read the users calculated by Entitlements for this role.
groups = Entitlements::Data::Groups::Calculated.read(role_dn)
# "diff" makes a call to GitHub API to read the team as it currently exists there.
# Returns a hash { added: Set(members), removed: Set(members) }
diff = provider.diff(groups, ignored_users)
# For comparison purposes we need to downcase the member DNs when populating the
# `added`, `moved` and `removed` hashes. We need to store the original capitalization
# for later reporting.
diff[:added].each do |member|
if removed.key?(member.downcase)
# Already removed from a previous role. Therefore this is a move to a different role.
removed.delete(member.downcase)
moved[member.downcase] = { member: member, role: role }
else
# Not removed from a previous role. Suspect this is an addition to the org (if we later spot a removal
# from a role, then the code below will update that to be a move instead).
added[member.downcase] = { member: member, role: role }
end
end
diff[:removed].each do |member|
if added.key?(member.downcase)
# Already added to a previous role. Therefore this is a move to a different role.
moved[member.downcase] = added[member.downcase]
added.delete(member.downcase)
else
# Not added to a previous role. Suspect this is a removal from the org (if we later spot an addition
# to another role, then the code above will update that to be a move instead).
removed[member.downcase] = { member: member, role: role }
end
end
end
{ added: added, removed: removed, moved: moved }
end
# Admins or members who are both `invited` and `pending` do not need to be re-invited. We're waiting for them
# to accept their invitation but we don't want to re-invite them (and display a diff) over and over again
# while we patiently wait for their acceptance. This method mutates `invited` and returns a set of pending
# user distinguished names.
#
# invited - Set of correct-cased distinguished names (mutated)
# pending - Set of lowercase GitHub usernames of pending members in an organization
#
# Returns a Set of correct-cased distinguished names removed from `invited` because they're pending.
Contract C::ArrayOf[String], C::SetOf[String] => C::SetOf[String]
def remove_pending(invited, pending)
result = Set.new(invited.select { |k| pending.member?(Entitlements::Util::Util.first_attr(k).downcase) })
invited.reject! { |item| result.member?(item) }
result
end
# Given a list of groups and a list of pending members from the provider, determine which pending users are not
# in any of the given groups. Return a list of these pending users (as distinguished names).
#
# groups - Hash of calculated groups: { "role" => Entitlements::Models::Group }
# pending - Set of Strings of pending usernames from GitHub
#
# Returns an Array of Strings with distinguished names.
Contract C::HashOf[String => Entitlements::Models::Group], C::SetOf[String] => C::ArrayOf[String]
def disinvited_users(groups, pending)
all_users = groups.map do |_, grp|
grp.member_strings.map { |ms| Entitlements::Util::Util.first_attr(ms).downcase }
end.compact.flatten
pending.to_a - all_users
end
def ignored_users
@ignored_users ||= begin
ignored_user_list = config["ignore"] || []
Set.new(ignored_user_list.map(&:downcase))
end
end
attr_reader :provider
end
end
end
end

Просмотреть файл

@ -0,0 +1,136 @@
# frozen_string_literal: true
require_relative "service"
require "set"
require "uri"
module Entitlements
class Backend
class GitHubOrg
class Provider < Entitlements::Backend::BaseProvider
include ::Contracts::Core
C = ::Contracts
attr_reader :github
# Constructor.
#
# config - Configuration provided for the controller instantiation
Contract C::KeywordArgs[
config: C::HashOf[String => C::Any],
] => C::Any
def initialize(config:)
@github = Entitlements::Backend::GitHubOrg::Service.new(
org: config.fetch("org"),
addr: config.fetch("addr", nil),
token: config.fetch("token"),
ou: config.fetch("base")
)
@role_cache = {}
end
# Read in a github organization and enumerate its members and their roles. Results are cached
# for future runs. The organization is defined per-entitlement as the `.org` method of the
# github object.
#
# role_identifier - String with the role (a key from Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES) or a group.
#
# Returns a Entitlements::Models::Group object.
Contract C::Or[String, Entitlements::Models::Group] => Entitlements::Models::Group
def read(role_identifier)
role_cn = role_name(role_identifier)
@role_cache[role_cn] ||= role_to_group(role_cn)
end
# Commit changes.
#
# action - An Entitlements::Models::Action object.
#
# Returns true if a change was made, false if no change was made.
Contract Entitlements::Models::Action => C::Bool
def commit(action)
# `false` usually means "What's going on, there are changes but nothing to apply!" Here it is
# more routine that there are removals that are not processed (because adding to one role removes
# from the other), so `true` is more accurate.
return true unless action.implementation
github.sync(action.implementation, role_name(action.updated))
end
# Invalidate the predictive cache.
#
# Takes no arguments.
#
# Returns nothing.
Contract C::None => nil
def invalidate_predictive_cache
@role_cache = {}
github.invalidate_org_members_predictive_cache
nil
end
# Pending members.
#
# Takes no arguments.
#
# Returns Set of usernames.
Contract C::None => C::SetOf[String]
def pending_members
github.pending_members
end
private
# Determine the role name from a string or a group (with validation).
#
# role_identifier - String (a key from Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES) or a group.
#
# Returns a string with the role name.
Contract C::Or[String, Entitlements::Models::Group] => String
def role_name(role_identifier)
role = Entitlements::Util::Util.any_to_cn(role_identifier)
return role if Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES.key?(role)
supported = Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES.keys.join(", ")
message = "Invalid role #{role.inspect}. Supported values: #{supported}."
raise ArgumentError, message
end
# Construct an Entitlements::Models::Group from a given role.
#
# role - A String with the role name.
#
# Returns an Entitlements::Models::Group object.
Contract String => Entitlements::Models::Group
def role_to_group(role)
members = github.org_members.keys.select { |username| github.org_members[username] == role }
Entitlements::Models::Group.new(
dn: role_dn(role),
members: Set.new(members),
description: role_description(role)
)
end
# Default description for a given role.
#
# role - A String with the role name.
#
# Returns a String with the default description for the role.
Contract String => String
def role_description(role)
"Users with role #{role} on organization #{github.org}"
end
# Default distinguished name for a given role.
#
# role - A String with the role name.
#
# Returns a String with the distinguished name for the role.
Contract String => String
def role_dn(role)
"cn=#{role},#{github.ou}"
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,88 @@
# frozen_string_literal: true
require_relative "../../service/github"
module Entitlements
class Backend
class GitHubOrg
class Service < Entitlements::Service::GitHub
include ::Contracts::Core
C = ::Contracts
# Sync the members of an organization in a given role to match the member list.
#
# implementation - An Hash of { action: :add/:remove, person: <person DN> }
# role - A String with the role, matching a key of Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES.
#
# Returns true if it succeeded, false if it did not.
Contract C::ArrayOf[{ action: C::Or[:add, :remove], person: String }], String => C::Bool
def sync(implementation, role)
added_members = []
removed_members = []
implementation.each do |instruction|
username = Entitlements::Util::Util.first_attr(instruction[:person]).downcase
if instruction[:action] == :add
added_members << username if add_user_to_organization(username, role)
else
removed_members << username if remove_user_from_organization(username)
end
end
Entitlements.logger.debug "sync(#{role}): Added #{added_members.count}, removed #{removed_members.count}"
added_members.any? || removed_members.any?
end
private
# Upsert a user with a role to the organization.
#
# user: A String with the (GitHub) username of the person to add or modify.
# role: A String with the role, matching a key of Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES.
#
# Returns true if the user was added to the organization, false otherwise.
Contract String, String => C::Bool
def add_user_to_organization(user, role)
Entitlements.logger.debug "#{identifier} add_user_to_organization(user=#{user}, org=#{org}, role=#{role})"
new_membership = octokit.update_organization_membership(org, user: user, role: role)
# Happy path
if new_membership[:role] == role
if new_membership[:state] == "pending"
pending_members.add(user)
return true
elsif new_membership[:state] == "active"
org_members[user] = role
return true
end
end
Entitlements.logger.debug new_membership.inspect
Entitlements.logger.error "Failed to adjust membership for #{user} in organization #{org} with role #{role}!"
false
end
# Remove a user from the organization.
#
# user: A String with the (GitHub) username of the person to remove.
#
# Returns true if the user was removed, false otherwise.
Contract String => C::Bool
def remove_user_from_organization(user)
Entitlements.logger.debug "#{identifier} remove_user_from_organization(user=#{user}, org=#{org})"
result = octokit.remove_organization_membership(org, user: user)
# If we removed the user, remove them from the cache of members, so that any GitHub team
# operations in this organization will ignore this user.
if result
org_members.delete(user)
pending_members.delete(user)
end
# Return the result, true or false
result
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,6 @@
# frozen_string_literal: true
require_relative "github_team/controller"
require_relative "github_team/models/team"
require_relative "github_team/provider"
require_relative "github_team/service"

Просмотреть файл

@ -0,0 +1,126 @@
# frozen_string_literal: true
module Entitlements
class Backend
class GitHubTeam
class Controller < Entitlements::Backend::BaseController
# Controller priority and registration
def self.priority
40
end
register
include ::Contracts::Core
C = ::Contracts
# Constructor. Generic constructor that takes a hash of configuration options.
#
# group_name - Name of the corresponding group in the entitlements configuration file.
# config - Optionally, a Hash of configuration information (configuration is referenced if empty).
Contract String, C::Maybe[C::HashOf[String => C::Any]] => C::Any
def initialize(group_name, config = nil)
super
@provider = Entitlements::Backend::GitHubTeam::Provider.new(config: @config)
end
def prefetch
teams = Entitlements::Data::Groups::Calculated.read_all(group_name, config)
teams.each do |team_slug|
entitlement_group = Entitlements::Data::Groups::Calculated.read(team_slug)
provider.read(entitlement_group)
end
end
# Calculation routines.
#
# Takes no arguments.
#
# Returns a list of @actions.
Contract C::None => C::Any
def calculate
added = []
changed = []
teams = Entitlements::Data::Groups::Calculated.read_all(group_name, config)
teams.each do |team_slug|
group = Entitlements::Data::Groups::Calculated.read(team_slug)
# Anyone who is not a member of the organization is ignored in the diff calculation.
# This avoids adding an organization membership for someone by virtue of adding them
# to a team, without declaring them as an administrator or a member of the org. Also
# this avoids having a pending member show up in diffs until they accept their invite.
ignored_users = provider.auto_generate_ignored_users(group)
# "diff" includes a call to GitHub API to read the team as it currently exists there.
# Returns a hash { added: Set(members), removed: Set(members) }
diff = provider.diff(group, ignored_users)
if diff[:added].empty? && diff[:removed].empty? && diff[:metadata].nil?
logger.debug "UNCHANGED: No GitHub team changes for #{group_name}:#{team_slug}"
next
end
if diff[:metadata] && diff[:metadata][:create_team]
added << Entitlements::Models::Action.new(team_slug, provider.read(group), group, group_name, ignored_users: ignored_users)
else
changed << Entitlements::Models::Action.new(team_slug, provider.read(group), group, group_name, ignored_users: ignored_users)
end
end
print_differences(key: group_name, added: added, removed: [], changed: changed)
@actions = added + changed
end
# Apply changes.
#
# action - Action array.
#
# Returns nothing.
Contract Entitlements::Models::Action => C::Any
def apply(action)
unless action.updated.is_a?(Entitlements::Models::Group)
logger.fatal "#{action.dn}: GitHub entitlements interface does not support removing a team at this point"
raise RuntimeError, "Invalid Operation"
end
if provider.change_ignored?(action)
logger.debug "SKIP: GitHub team #{action.dn} only changes organization non-members or pending members"
return
end
if provider.commit(action.updated)
logger.debug "APPLY: Updating GitHub team #{action.dn}"
else
logger.warn "DID NOT APPLY: Changes not needed to #{action.dn}"
logger.debug "Old: #{action.existing.inspect}"
logger.debug "New: #{action.updated.inspect}"
end
end
# Validate configuration options.
#
# key - String with the name of the group.
# data - Hash with the configuration data.
#
# Returns nothing.
# :nocov:
Contract String, C::HashOf[String => C::Any] => nil
def validate_config!(key, data)
spec = COMMON_GROUP_CONFIG.merge({
"base" => { required: true, type: String },
"addr" => { required: false, type: String },
"org" => { required: true, type: String },
"token" => { required: true, type: String }
})
text = "GitHub group #{key.inspect}"
Entitlements::Util::Util.validate_attr!(spec, data, text)
end
# :nocov:
private
attr_reader :provider
end
end
end
end

Просмотреть файл

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Entitlements
class Backend
class GitHubTeam
class Models
class Team < Entitlements::Models::Group
include ::Contracts::Core
C = ::Contracts
attr_reader :team_id, :team_name, :team_dn
# Constructor.
#
# team_id - Integer with the team ID
# team_name - String with the team name
# members - Set of String with member UID
# ou - A String with the base OU
Contract C::KeywordArgs[
team_id: Integer,
team_name: String,
members: C::SetOf[String],
ou: String,
metadata: C::Or[C::HashOf[String => C::Any], nil]
] => C::Any
def initialize(team_id:, team_name:, members:, ou:, metadata:)
@team_id = team_id
@team_name = team_name.downcase
@team_dn = ["cn=#{team_name.downcase}", ou].join(",")
super(dn: @team_dn, members: Set.new(members.map { |m| m.downcase }), metadata: metadata)
end
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,208 @@
# frozen_string_literal: true
require_relative "service"
require "set"
require "uri"
module Entitlements
class Backend
class GitHubTeam
class Provider < Entitlements::Backend::BaseProvider
include ::Contracts::Core
C = ::Contracts
# Constructor.
#
# config - Configuration provided for the controller instantiation
Contract C::KeywordArgs[
config: C::HashOf[String => C::Any],
] => C::Any
def initialize(config:)
@github = Entitlements::Backend::GitHubTeam::Service.new(
org: config.fetch("org"),
addr: config.fetch("addr", nil),
token: config.fetch("token"),
ou: config.fetch("base")
)
@github_team_cache = {}
end
# Read in a specific GitHub.com Team and enumerate its members. Results are cached
# for future runs.
#
# team_identifier - Entitlements::Models::Group representing the entitlement
#
# Returns a Entitlements::Models::Group object representing the GitHub group or nil if the GitHub.com Team does not exist
Contract Entitlements::Models::Group => C::Maybe[Entitlements::Models::Group]
def read(entitlement_group)
slug = Entitlements::Util::Util.any_to_cn(entitlement_group.cn.downcase)
return @github_team_cache[slug] if @github_team_cache[slug]
github_team = github.read_team(entitlement_group)
# We should not cache a team which does not exist
return nil if github_team.nil?
Entitlements.logger.debug "Loaded #{github_team.team_dn} (id=#{github_team.team_id}) with #{github_team.member_strings.count} member(s)"
@github_team_cache[github_team.team_name] = github_team
end
# Dry run of committing changes. Returns a list of users added or removed.
#
# group - An Entitlements::Models::Group object.
#
# Returns added / removed hash.
Contract Entitlements::Models::Group, C::Maybe[C::SetOf[String]] => Hash[added: C::SetOf[String], removed: C::SetOf[String]]
def diff(entitlement_group, ignored_users = Set.new)
# The current value of the team from `read` might be based on the predictive cache
# or on an actual API call. At this stage we don't care.
team_identifier = entitlement_group.cn.downcase
github_team_group = read(entitlement_group)
if github_team_group.nil?
github_team_group = create_github_team_group(entitlement_group)
end
result = diff_existing_updated(github_team_group, entitlement_group, ignored_users)
# If there are no differences, return. (If we read from the predictive cache, we just saved ourselves a call
# to the API. Hurray.)
return result unless result[:added].any? || result[:removed].any? || result[:metadata]
# If the group doesn't exist yet, we know we're not using the cache and we can save on any further API calls
unless github_team_group.metadata_fetch_if_exists("team_id") == -999
# There are differences so we don't want to use the predictive cache. Call to `from_predictive_cache?`
# to determine whether our source of "current state" came from the predictive cache or from the API.
# If it returns false, it came from the API, and we should just return what we got
# (since pulling the data from the API again would be pointless).
return result unless github.from_predictive_cache?(entitlement_group)
# If `from_predictive_cache?` returned true, the data came from the predictive cache. We need
# to invalidate the predictive cache entry, clean up the instance variable and re-read the refreshed data.
github.invalidate_predictive_cache(entitlement_group)
@github_team_cache.delete(team_identifier)
github_team_group = read(entitlement_group)
end
# And finally, we have to calculate a new diff, which this time uses the fresh data from the API as
# its basis, rather than the predictive cache.
diff_existing_updated(github_team_group, entitlement_group, ignored_users)
end
# Dry run of committing changes. Returns a list of users added or removed and a hash explaining metadata changes
# Takes an existing and an updated group object, avoiding a lookup in the backend.
#
# existing_group - An Entitlements::Models::Group object.
# group - An Entitlements::Models::Group object.
# ignored_users - Optionally, a Set of lower-case Strings of users to ignore.
Contract Entitlements::Models::Group, Entitlements::Models::Group, C::Maybe[C::SetOf[String]] => Hash[added: C::SetOf[String], removed: C::SetOf[String], metadata: C::Maybe[Hash[]]]
def diff_existing_updated(existing_group, group, ignored_users = Set.new)
diff_existing_updated_metadata(existing_group, group, super)
end
# Determine if a change needs to be ignored. This will return true if the
# user being added or removed is ignored.
#
# action - Entitlements::Models::Action object
#
# Returns true if the change should be ignored, false otherwise.
Contract Entitlements::Models::Action => C::Bool
def change_ignored?(action)
return false if action.existing.nil?
result = diff_existing_updated(action.existing, action.updated, action.ignored_users)
result[:added].empty? && result[:removed].empty? && result[:metadata].nil?
end
# Commit changes.
#
# group - An Entitlements::Models::Group object.
#
# Returns true if a change was made, false if no change was made.
Contract Entitlements::Models::Group => C::Bool
def commit(entitlement_group)
github_team = github.read_team(entitlement_group)
# Create the new team and invalidate the cache
if github_team.nil?
team_name = entitlement_group.cn.downcase
github.create_team(entitlement_group: entitlement_group)
github.invalidate_predictive_cache(entitlement_group)
@github_team_cache.delete(team_name)
github_team = github.read_team(entitlement_group)
end
github.sync_team(entitlement_group, github_team)
end
# Automatically generate ignored users for a group. Find all members listed in the group who are not
# admins or members of the GitHub organization in question.
#
# group - An Entitlements::Models::Group object.
#
# Returns a set of strings with usernames meeting the criteria.
Contract Entitlements::Models::Group => C::SetOf[String]
def auto_generate_ignored_users(entitlement_group)
org_members = github.org_members.keys.map(&:downcase)
group_members = entitlement_group.member_strings.map(&:downcase)
Set.new(group_members - org_members)
end
private
# Construct an Entitlements::Models::Group for a new group and team
#
# group - An Entitlements::Models::Group object representing the defined group
#
# Returns an Entitlements::Models::Group for a new group
Contract Entitlements::Models::Group => Entitlements::Models::Group
def create_github_team_group(entitlement_group)
begin
metadata = entitlement_group.metadata
metadata["team_id"] = -999
rescue Entitlements::Models::Group::NoMetadata
metadata = {"team_id" => -999}
end
Entitlements::Backend::GitHubTeam::Models::Team.new(
team_id: -999,
team_name: entitlement_group.cn.downcase,
members: Set.new,
ou: github.ou,
metadata: metadata
)
end
# Returns a diff hash of group metadata
# Takes an existing and an updated group object, avoiding a lookup in the backend.
#
# existing_group - An Entitlements::Models::Group object.
# group - An Entitlements::Models::Group object.
# base_diff - Hash representing the base diff from diff_existing_updated
Contract Entitlements::Models::Group, Entitlements::Models::Group, Hash[added: C::SetOf[String], removed: C::SetOf[String], metadata: C::Or[Hash[], nil]] => Hash[added: C::SetOf[String], removed: C::SetOf[String], metadata: C::Or[Hash[], nil]]
def diff_existing_updated_metadata(existing_group, group, base_diff)
if existing_group.metadata_fetch_if_exists("team_id") == -999
base_diff[:metadata] = { create_team: true }
end
existing_parent_team = existing_group.metadata_fetch_if_exists("parent_team_name")
changed_parent_team = group.metadata_fetch_if_exists("parent_team_name")
if existing_parent_team != changed_parent_team
if existing_parent_team.nil? && !changed_parent_team.nil?
base_diff[:metadata] = { parent_team: "add" }
Entitlements.logger.info "ADD github_parent_team #{changed_parent_team} to #{existing_group.dn} in #{github.org}"
elsif !existing_parent_team.nil? && changed_parent_team.nil?
base_diff[:metadata] = { parent_team: "remove" }
Entitlements.logger.info "REMOVE (NOOP) github_parent_team #{existing_parent_team} from #{existing_group.dn} in #{github.org}"
else
base_diff[:metadata] = { parent_team: "change" }
Entitlements.logger.info "CHANGE github_parent_team from #{existing_parent_team} to #{changed_parent_team} for #{existing_group.dn} in #{github.org}"
end
end
base_diff
end
attr_reader :github
end
end
end
end

Просмотреть файл

@ -0,0 +1,402 @@
# frozen_string_literal: true
require_relative "models/team"
require_relative "../../service/github"
require "base64"
module Entitlements
class Backend
class GitHubTeam
class Service < Entitlements::Service::GitHub
include ::Contracts::Core
include ::Contracts::Builtin
C = ::Contracts
class TeamNotFound < RuntimeError; end
# Constructor.
#
# addr - Base URL a GitHub Enterprise API (leave undefined to use dotcom)
# org - String with organization name
# token - Access token for GitHub API
# ou - Base OU for fudged DNs
#
# Returns nothing.
Contract C::KeywordArgs[
addr: C::Maybe[String],
org: String,
token: String,
ou: String
] => C::Any
def initialize(addr: nil, org:, token:, ou:)
super
Entitlements.cache[:github_team_members] ||= {}
Entitlements.cache[:github_team_members][org] ||= {}
@team_cache = Entitlements.cache[:github_team_members][org]
end
# Read a single team identified by its slug and return a team object.
# This is aware of the predictive cache and will use it if appropriate.
#
# entitlement_group - Entitlements::Models::Group representing the entitlement being worked on
#
# Returns a Entitlements::Backend::GitHubTeam::Models::Team or nil if the team does not exist
Contract Entitlements::Models::Group => C::Maybe[Entitlements::Backend::GitHubTeam::Models::Team]
def read_team(entitlement_group)
team_identifier = entitlement_group.cn.downcase
@team_cache[team_identifier] ||= begin
dn = "cn=#{team_identifier},#{ou}"
begin
entitlement_metadata = entitlement_group.metadata
rescue Entitlements::Models::Group::NoMetadata
entitlement_metadata = nil
end
if (cached_members = Entitlements::Data::Groups::Cached.members(dn))
Entitlements.logger.debug "Loading GitHub team #{identifier}:#{org}/#{team_identifier} from cache"
cached_metadata = Entitlements::Data::Groups::Cached.metadata(dn)
# If both the cached and entitlement metadata are nil, our team metadata is nil
# If one of the cached or entitlement metadata is nil, we use the other populated metadata hash as-is
# If both cached and entitlement metadata exist, we combine the two hashes with the cached metadata taking precedence
#
# The reason we do this is because an entitlement file should be 1:1 to a GitHub Team. However,
# entitlements files allow for metadata tags and the GitHub.com Team does not have a place to store those.
# Therefore, we must combine any existing entitlement metadata entries into the Team metadata hash
if cached_metadata.nil?
team_metadata = entitlement_metadata
elsif entitlement_metadata.nil?
team_metadata = cached_metadata
else
# Always merge the current state metadata (cached or API call) into the entitlement metadata, so that the current state takes precedent
team_metadata = entitlement_metadata.merge(cached_metadata)
end
team = Entitlements::Backend::GitHubTeam::Models::Team.new(
team_id: -1,
team_name: team_identifier,
members: cached_members,
ou: ou,
metadata: team_metadata
)
{ cache: true, value: team }
else
Entitlements.logger.debug "Loading GitHub team #{identifier}:#{org}/#{team_identifier}"
begin
teamdata = graphql_team_data(team_identifier)
# The entitlement metadata may have GitHub.com Team metadata which it wants to set, so we must
# overwrite that metadata with what we get from the API
if teamdata[:parent_team_name].nil?
team_metadata = entitlement_metadata
else
parent_team_metadata = {
"parent_team_name" => teamdata[:parent_team_name]
}
if entitlement_metadata.nil?
team_metadata = parent_team_metadata
else
# Always merge the current state metadata (cached or API call) into the entitlement metadata, so that the current state takes precedent
team_metadata = entitlement_metadata.merge(parent_team_metadata)
end
end
team = Entitlements::Backend::GitHubTeam::Models::Team.new(
team_id: teamdata[:team_id],
team_name: team_identifier,
members: Set.new(teamdata[:members]),
ou: ou,
metadata: team_metadata
)
rescue TeamNotFound
Entitlements.logger.warn "Team #{team_identifier} does not exist in this GitHub.com organization"
return nil
end
{ cache: false, value: team }
end
end
@team_cache[team_identifier][:value]
end
# Determine whether the most recent entry came from the predictive cache or an actual
# call to the API.
#
# entitlement_group - Entitlements::Models::Group representing the group from the entitlement
#
# Returns true if it came from the cache, or false if it came from the API.
Contract Entitlements::Models::Group => C::Bool
def from_predictive_cache?(entitlement_group)
team_identifier = entitlement_group.cn.downcase
read_team(entitlement_group) unless @team_cache[team_identifier]
(@team_cache[team_identifier] && @team_cache[team_identifier][:cache]) ? true : false
end
# Declare the entry to be invalid for a specific team, and if the prior knowledge
# of that team was from the cache, re-read from the actual data source.
#
# entitlement_group - Entitlements::Models::Group representing the group from the entitlement
#
# Returns nothing.
Contract Entitlements::Models::Group => nil
def invalidate_predictive_cache(entitlement_group)
# If the entry was not from the predictive cache in the first place, just return.
# This really should not get called if that's the case, but regardless, we don't
# want to pointlessly hit the API twice.
return unless from_predictive_cache?(entitlement_group)
# The entry did come from the predictive cache. Clear out all of the local caches
# in this object and re-read the data from the API.
team_identifier = entitlement_group.cn.downcase
dn = "cn=#{team_identifier},#{ou}"
Entitlements.logger.debug "Invalidating cache entry for #{dn}"
Entitlements::Data::Groups::Cached.invalidate(dn)
@team_cache.delete(team_identifier)
read_team(entitlement_group)
nil
end
# Sync a GitHub team. (The team must already exist and its ID must be known.)
#
# data - An Entitlements::Backend::GitHubTeam::Models::Team object with the new members and data.
#
# Returns true if it succeeded, false if it did not.
Contract Entitlements::Models::Group, C::Or[Entitlements::Backend::GitHubTeam::Models::Team, nil] => C::Bool
def sync_team(desired_state, current_state)
begin
desired_metadata = desired_state.metadata
rescue Entitlements::Models::Group::NoMetadata
desired_metadata = {}
end
begin
current_metadata = current_state.metadata
rescue Entitlements::Models::Group::NoMetadata, NoMethodError
current_metadata = {}
end
changed_parent_team = false
unless desired_metadata["parent_team_name"] == current_metadata["parent_team_name"]
# TODO: I'm hard-coding a block for deletes, for now. I'm doing that by making sure we dont set the desired parent_team_id to nil for teams where it is already set
# :nocov:
if desired_metadata["parent_team_name"].nil?
Entitlements.logger.debug "sync_team(team=#{current_state.team_name}): IGNORING GitHub Parent Team DELETE"
else
# :nocov:
Entitlements.logger.debug "sync_team(#{current_state.team_name}=#{current_state.team_id}): Parent team change found - From #{current_metadata["parent_team_name"] || "No Parent Team"} to #{desired_metadata["parent_team_name"]}"
desired_parent_team_id = team_by_name(org_name: org, team_name: desired_metadata["parent_team_name"])[:id]
unless desired_parent_team_id.nil?
# TODO: I'm hard-coding a block for deletes, for now. I'm doing that by making sure we dont set the desired parent_team_id to nil for teams where it is already set
update_team(team: current_state, metadata: { parent_team_id: desired_parent_team_id })
end
changed_parent_team = true
end
end
added_members = desired_state.member_strings.map { |u| u.downcase } - current_state.member_strings.map { |u| u.downcase }
removed_members = current_state.member_strings.map { |u| u.downcase } - desired_state.member_strings.map { |u| u.downcase }
added_members.select! { |username| add_user_to_team(user: username, team: current_state) }
removed_members.select! { |username| remove_user_from_team(user: username, team: current_state) }
Entitlements.logger.debug "sync_team(#{current_state.team_name}=#{current_state.team_id}): Added #{added_members.count}, removed #{removed_members.count}"
added_members.any? || removed_members.any? || changed_parent_team
end
# Create a team
#
# team - String with the desired team name
#
# Returns true if the team was created
Contract C::KeywordArgs[
entitlement_group: Entitlements::Models::Group,
] => C::Bool
def create_team(entitlement_group:)
begin
team_name = entitlement_group.cn.downcase
team_options = { name: team_name, repo_names: [], privacy: "closed" }
begin
entitlement_metadata = entitlement_group.metadata
unless entitlement_metadata["parent_team_name"].nil?
parent_team_data = graphql_team_data(entitlement_metadata["parent_team_name"])
team_options[:parent_team_id] = parent_team_data[:team_id]
Entitlements.logger.debug "create_team(team=#{team_name}) Parent team #{entitlement_metadata["parent_team_name"]} with id #{parent_team_data[:team_id]} found"
end
rescue Entitlements::Models::Group::NoMetadata
Entitlements.logger.debug "create_team(team=#{team_name}) No metadata found"
end
Entitlements.logger.debug "create_team(team=#{team_name})"
octokit.create_team(org, team_options)
true
rescue Octokit::UnprocessableEntity => e
Entitlements.logger.debug "create_team(team=#{team_name}) ERROR - #{e.message}"
false
end
end
# Update a team
#
# team - Entitlements::Backend::GitHubTeam::Models::Team object
#
# Returns true if the team was updated
Contract C::KeywordArgs[
team: Entitlements::Backend::GitHubTeam::Models::Team,
metadata: C::Or[Hash, nil]
] => C::Bool
def update_team(team:, metadata: {})
begin
Entitlements.logger.debug "update_team(team=#{team.team_name})"
options = { name: team.team_name, repo_names: [], privacy: "closed", parent_team_id: metadata[:parent_team_id] }
octokit.update_team(team.team_id, options)
true
rescue Octokit::UnprocessableEntity => e
Entitlements.logger.debug "update_team(team=#{team.team_name}) ERROR - #{e.message}"
false
end
end
# Gets a team by name
#
# team - Entitlements::Backend::GitHubTeam::Models::Team object
#
# Returns true if the team was updated
Contract C::KeywordArgs[
org_name: String,
team_name: String
] => Sawyer::Resource
def team_by_name(org_name:, team_name:)
octokit.team_by_name(org_name, team_name)
end
private
# GraphQL query for the members of a team identified by a slug. (For now
# our GraphQL needs are simple so this is just a hard-coded query. In the
# future if this gets more widely used, consider one of the graphql client
# gems, such as https://github.com/github/graphql-client.)
#
# team_slug - Identifier of the team to retrieve.
#
# Returns a data structure with team data.
Contract String => { members: C::ArrayOf[String], team_id: Integer, parent_team_name: C::Or[String, nil] }
def graphql_team_data(team_slug)
cursor = nil
team_id = nil
result = []
sanity_counter = 0
while sanity_counter < 100
sanity_counter += 1
first_str = cursor.nil? ? "first: #{max_graphql_results}" : "first: #{max_graphql_results}, after: \"#{cursor}\""
query = "{
organization(login: \"#{org}\") {
team(slug: \"#{team_slug}\") {
databaseId
parentTeam {
slug
}
members(#{first_str}, membership: IMMEDIATE) {
edges {
node {
login
}
cursor
}
}
}
}
}".gsub(/\n\s+/, "\n")
response = graphql_http_post(query)
unless response[:code] == 200
Entitlements.logger.fatal "Abort due to GraphQL failure on #{query.inspect}"
raise "GraphQL query failure"
end
team = response[:data].fetch("data").fetch("organization").fetch("team")
if team.nil?
raise TeamNotFound, "Requested team #{team_slug} does not exist in #{org}!"
end
team_id = team.fetch("databaseId")
parent_team_name = team.dig("parentTeam", "slug")
edges = team.fetch("members").fetch("edges")
break unless edges.any?
buffer = edges.map { |e| e.fetch("node").fetch("login").downcase }
result.concat buffer
cursor = edges.last.fetch("cursor")
next if cursor && buffer.size == max_graphql_results
break
end
{ members: result, team_id: team_id, parent_team_name: parent_team_name }
end
# Ensure that the given team ID actually matches up to the team slug on GitHub. This is in place
# because we are relying on something in graphql that we shouldn't be, until the attribute we need
# is added as a first class citizen. Once that happens, this can be removed.
#
# team_id - ID number of the team (Integer)
# team_slug - Slug of the team (String)
#
# Returns nothing but raises if there's a mismatch.
Contract Integer, String => nil
def validate_team_id_and_slug!(team_id, team_slug)
return if team_id == -999
@validation_cache ||= {}
@validation_cache[team_id] ||= begin
Entitlements.logger.debug "validate_team_id_and_slug!(#{team_id}, #{team_slug.inspect})"
team_data = octokit.team(team_id)
team_data[:slug]
end
return if @validation_cache[team_id] == team_slug
raise "validate_team_id_and_slug! mismatch: team_id=#{team_id} expected=#{team_slug.inspect} got=#{@validation_cache[team_id].inspect}"
end
# Add user to team.
#
# user - String with the GitHub username
# team - Entitlements::Backend::GitHubTeam::Models::Team object for the team.
#
# Returns true if the user was added to the team, false if user was already on team.
Contract C::KeywordArgs[
user: String,
team: Entitlements::Backend::GitHubTeam::Models::Team,
] => C::Bool
def add_user_to_team(user:, team:)
return false unless org_members.include?(user.downcase)
Entitlements.logger.debug "#{identifier} add_user_to_team(user=#{user}, org=#{org}, team_id=#{team.team_id})"
validate_team_id_and_slug!(team.team_id, team.team_name)
result = octokit.add_team_membership(team.team_id, user)
result[:state] == "active" || result[:state] == "pending"
end
# Remove user from team.
#
# user - String with the GitHub username
# team - Entitlements::Backend::GitHubTeam::Models::Team object for the team.
#
# Returns true if the user was removed from the team, false if user was not on team.
Contract C::KeywordArgs[
user: String,
team: Entitlements::Backend::GitHubTeam::Models::Team,
] => C::Bool
def remove_user_from_team(user:, team:)
return false unless org_members.include?(user.downcase)
Entitlements.logger.debug "#{identifier} remove_user_from_team(user=#{user}, org=#{org}, team_id=#{team.team_id})"
validate_team_id_and_slug!(team.team_id, team.team_name)
octokit.remove_team_membership(team.team_id, user)
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,394 @@
# frozen_string_literal: true
require "net/http"
require "octokit"
require "uri"
module Entitlements
class Service
class GitHub
include ::Contracts::Core
C = ::Contracts
# This is a limitation of the GitHub API
MAX_GRAPHQL_RESULTS = 100
# Retries to smooth over transient network blips
MAX_GRAPHQL_RETRIES = 3
WAIT_BETWEEN_GRAPHQL_RETRIES = 1
attr_reader :addr, :org, :token, :ou
# Constructor.
#
# addr - Base URL a GitHub Enterprise API (leave undefined to use dotcom)
# org - String with organization name
# token - Access token for GitHub API
# ou - Base OU for fudged DNs
#
# Returns nothing.
Contract C::KeywordArgs[
addr: C::Maybe[String],
org: String,
token: String,
ou: String
] => C::Any
def initialize(addr: nil, org:, token:, ou:)
# Save some parameters for the connection but don't actually connect yet.
@addr = addr
@org = org
@token = token
@ou = ou
# This is a global cache across all invocations of this object. GitHub membership
# need to be obtained only one time per organization, but might be used multiple times.
Entitlements.cache[:github_pending_members] ||= {}
Entitlements.cache[:github_org_members] ||= {}
end
# Return the identifier, either the address specified or otherwise "github.com".
#
# Takes no arguments.
#
# Returns the address.
Contract C::None => String
def identifier
@identifier ||= begin
if addr.nil?
"github.com"
else
u = URI(addr)
u.host
end
end
end
# Read the members of an organization and return a Hash of users with their role.
# This method does not need parameters because the underlying service already
# has the organization available in an `org` method.
#
# Takes no parameters.
#
# Returns Hash of { "username" => "role" }.
Contract C::None => C::HashOf[String => String]
def org_members
Entitlements.cache[:github_org_members][org_signature] ||= begin
roles = Entitlements::Backend::GitHubOrg::ORGANIZATION_ROLES.invert
# Some basic stats are helpful for debugging
data, cache = members_and_roles_from_graphql_or_cache
result = data.map { |username, role| [username, roles.fetch(role)] }.to_h
admin_count = result.count { |_, role| role == "admin" }
member_count = result.count { |_, role| role == "member" }
Entitlements.logger.debug "Currently #{org} has #{admin_count} admin(s) and #{member_count} member(s)"
{ cache: cache, value: result }
end
Entitlements.cache[:github_org_members][org_signature][:value]
end
# Returns true if the github instance is an enterprise server instance
Contract C::None => C::Bool
def enterprise?
meta = octokit.github_meta
meta.key? :installed_version
end
# Read the members of an organization who are in a "pending" role. These users should
# not be re-invited or updated unless and until they have accepted the invitation.
#
# Takes no parameters.
#
# Returns Set of usernames.
Contract C::None => C::SetOf[String]
def pending_members
Entitlements.cache[:github_pending_members][org_signature] ||= begin
# ghes does not support org invites
return Set.new if enterprise?
pm = pending_members_from_graphql
Entitlements.logger.debug "Currently #{org} has #{pm.size} pending member(s)"
pm
end
end
# Determine whether the most recent entry came from the predictive cache or an actual
# call to the API.
#
# Takes no arguments.
#
# Returns true if it came from the cache, or false if it came from the API.
Contract C::None => C::Bool
def org_members_from_predictive_cache?
org_members # Force this to be read if for some reason it has not been yet.
Entitlements.cache[:github_org_members][org_signature][:cache] || false
end
# Invalidate the predictive cache for organization members, and if the prior knowledge
# of that role was from the cache, re-read from the actual data source.
#
# Takes no arguments.
#
# Returns nothing.
Contract C::None => nil
def invalidate_org_members_predictive_cache
# If the entry was not from the predictive cache in the first place, just return.
# This really should not get called if that's the case, but regardless, we don't
# want to pointlessly hit the API twice.
return unless org_members_from_predictive_cache?
# The entry did come from the predictive cache. Invalidate the entry, clear local
# caches, and re-read the data from the API.
Entitlements.logger.debug "Invalidating cache entries for cn=(admin|member),#{ou}"
Entitlements::Data::Groups::Cached.invalidate("cn=admin,#{ou}")
Entitlements::Data::Groups::Cached.invalidate("cn=member,#{ou}")
Entitlements.cache[:github_org_members].delete(org_signature)
org_members
nil
end
private
# The octokit object is initialized the first time it's called.
#
# Takes no arguments.
#
# Returns an Octokit client object.
Contract C::None => Octokit::Client
def octokit
@octokit ||= begin
client = Octokit::Client.new(access_token: token)
client.api_endpoint = addr if addr
client.auto_paginate = true
Entitlements.logger.debug "Setting up GitHub API connection to #{client.api_endpoint}"
client
end
end
# Get data from the predictive updates cache if it's available and valid, or else get it
# from GraphQL API. This is a shim between readers and `members_and_roles_from_graphql`.
#
# Takes no parameters.
#
# Returns Hash of { "username" => "ROLE" } where "ROLE" is from GraphQL Enum.
Contract C::None => [C::HashOf[String => String], C::Bool]
def members_and_roles_from_graphql_or_cache
admin_from_cache = Entitlements::Data::Groups::Cached.members("cn=admin,#{ou}")
member_from_cache = Entitlements::Data::Groups::Cached.members("cn=member,#{ou}")
# If we do not have *both* admins and members, we need to call the API
return [members_and_roles_from_rest, false] unless admin_from_cache && member_from_cache
# Convert the Sets of strings into the expected hash structure.
Entitlements.logger.debug "Loading organization members and roles for #{org} from cache"
result = admin_from_cache.map { |uid| [uid, "ADMIN"] }.to_h
result.merge! member_from_cache.map { |uid| [uid, "MEMBER"] }.to_h
[result, true]
end
# Query GraphQL API to get a list of members and their roles.
#
# Takes no parameters.
#
# Returns Hash of { "username" => "ROLE" } where "ROLE" is from GraphQL Enum.
Contract C::None => C::HashOf[String => String]
def members_and_roles_from_graphql
Entitlements.logger.debug "Loading organization members and roles for #{org}"
cursor = nil
result = {}
sanity_counter = 0
while sanity_counter < 100
sanity_counter += 1
first_str = cursor.nil? ? "first: #{max_graphql_results}" : "first: #{max_graphql_results}, after: \"#{cursor}\""
query = "{
organization(login: \"#{org}\") {
membersWithRole(#{first_str}) {
edges {
node {
login
}
role
cursor
}
}
}
}".gsub(/\n\s+/, "\n")
response = graphql_http_post(query)
unless response[:code] == 200
Entitlements.logger.fatal "Abort due to GraphQL failure on #{query.inspect}"
raise "GraphQL query failure"
end
edges = response[:data].fetch("data").fetch("organization").fetch("membersWithRole").fetch("edges")
break unless edges.any?
edges.each do |edge|
result[edge.fetch("node").fetch("login").downcase] = edge.fetch("role")
end
cursor = edges.last.fetch("cursor")
next if cursor && edges.size == max_graphql_results
break
end
result
end
# Returns Hash of { "username" => "ROLE" } where "ROLE" is ADMIN or MEMBER
Contract C::None => C::HashOf[String => String]
def members_and_roles_from_rest
Entitlements.logger.debug "Loading organization members and roles for #{org}"
result = {}
members = octokit.organization_members(org, { role: "admin" })
members.each do |member|
result[member[:login].downcase] = "ADMIN"
end
octokit.organization_members(org, { role: "member" }).each do |member|
result[member[:login].downcase] = "MEMBER"
end
result
end
# Query GraphQL API to get a list of pending members for the organization.
#
# Takes no parameters.
#
# Returns Set of usernames.
def pending_members_from_graphql
# Since pending members is really a state and not an entitlement, this code does
# not attempt to use a predictive cache. When this is invoked, it contacts the API.
cursor = nil
result = Set.new
sanity_counter = 0
while sanity_counter < 100
sanity_counter += 1
first_str = cursor.nil? ? "first: #{max_graphql_results}" : "first: #{max_graphql_results}, after: \"#{cursor}\""
query = "{
organization(login: \"#{org}\") {
pendingMembers(#{first_str}) {
edges {
node {
login
}
cursor
}
}
}
}".gsub(/\n\s+/, "\n")
response = graphql_http_post(query)
unless response[:code] == 200
Entitlements.logger.fatal "Abort due to GraphQL failure on #{query.inspect}"
raise "GraphQL query failure"
end
edges = response[:data].fetch("data").fetch("organization").fetch("pendingMembers").fetch("edges")
break unless edges.any?
edges.each do |edge|
result.add(edge.fetch("node").fetch("login").downcase)
end
cursor = edges.last.fetch("cursor")
next if cursor && edges.size == max_graphql_results
break
end
result
end
# Helper method: Do the HTTP POST to the GitHub API for GraphQL. This has a retry which is
# intended to avoid a failure due to a network blip.
#
# query - String with the data to be posted.
#
# Returns { code: <Integer>, data: <response data structure> }
Contract String => { code: Integer, data: C::Or[nil, Hash] }
def graphql_http_post(query)
1.upto(MAX_GRAPHQL_RETRIES) do |try_number|
result = graphql_http_post_real(query)
if result[:code] < 500
return result
elsif try_number >= MAX_GRAPHQL_RETRIES
Entitlements.logger.error "Query still failing after #{MAX_GRAPHQL_RETRIES} tries. Giving up."
return result
else
Entitlements.logger.warn "GraphQL failed on try #{try_number} of #{MAX_GRAPHQL_RETRIES}. Will retry."
sleep WAIT_BETWEEN_GRAPHQL_RETRIES * (2 ** (try_number - 1))
end
end
end
# Helper method: Do the HTTP POST to the GitHub API for GraphQL.
#
# query - String with the data to be posted.
#
# Returns { code: <Integer>, data: <response data structure> }
Contract String => { code: Integer, data: C::Or[nil, Hash] }
def graphql_http_post_real(query)
uri = URI.parse(File.join(octokit.api_endpoint, "graphql"))
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
request = Net::HTTP::Post.new(uri)
request.add_field("Authorization", "bearer #{token}")
request.add_field("Content-Type", "application/json")
request.body = JSON.generate("query" => query)
begin
response = http.request(request)
if response.code != "200"
Entitlements.logger.error "Got HTTP #{response.code} POSTing to #{uri}"
Entitlements.logger.error response.body
return { code: response.code.to_i, data: { "body" => response.body } }
end
begin
data = JSON.parse(response.body)
if data.key?("errors")
Entitlements.logger.error "Errors reported: #{data['errors'].inspect}"
return { code: 500, data: data }
end
{ code: response.code.to_i, data: data }
rescue JSON::ParserError => e
Entitlements.logger.error "#{e.class} #{e.message}: #{response.body.inspect}"
{ code: 500, data: { "body" => response.body } }
end
rescue => e
Entitlements.logger.error "Caught #{e.class} POSTing to #{uri}: #{e.message}"
{ code: 500, data: nil }
end
end
# Create a unique signature for this GitHub instance to identify it in a global cache.
#
# Takes no arguments.
#
# Returns a String.
Contract C::None => String
def org_signature
[addr || "", org].join("|")
end
# Get the maximum GraphQL results. This is a method that just returns the constant
# but this way it can be overridden in tests.
#
# Takes no arguments.
#
# Returns an Integer.
# :nocov:
Contract C::None => Integer
def max_graphql_results
MAX_GRAPHQL_RESULTS
end
# :nocov:
end
end
end

15
script/bootstrap Executable file
Просмотреть файл

@ -0,0 +1,15 @@
#!/bin/bash
set -e
set -x
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
cd "$DIR"
export PATH=/usr/share/rbenv/shims:$PATH
export RBENV_VERSION="$(cat .ruby-version)"
rm -rf "${DIR}/.bundle"
# Using Deprecated Flags to avoid pulling from upstream
bundle install --path vendor/gems --local --clean
bundle binstubs rake rspec-core rubocop

7
script/cibuild Executable file
Просмотреть файл

@ -0,0 +1,7 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
echo "Heres some output"
docker build -t entitlements-github-plugin -f spec/acceptance/Dockerfile.entitlements-github-plugin .
docker run -w "/data/entitlements" entitlements-github-plugin bash -c "script/test"

Просмотреть файл

@ -0,0 +1,88 @@
#!/bin/bash
export DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
export APP_NAME="entitlements-github-plugin"
export GITHUB_CONTAINER_NAME="github-server"
export GIT_SERVER_NAME="git-server"
export LDAP_SERVER_NAME="ldap-server"
if [ ! -f "${DIR}/spec/acceptance/Dockerfile.${APP_NAME}" ]; then
echo "No Dockerfile.${APP_NAME} was found"
exit 255
fi
begin_fold() {
local tag="$1"
echo "%%%FOLD {${tag}}%%%" 1>&2
set -x
}
end_fold() {
set +x
echo "%%%END FOLD%%%" 1>&2
}
docker_compose() {
cd "$DIR" && docker-compose -f "$DIR/spec/acceptance/docker-compose.yml" "$@"
}
unset DOCKER_COMPOSE_NEEDS_SHUTDOWN
cleanup() {
if [ -n "$DOCKER_COMPOSE_NEEDS_SHUTDOWN" ]; then
begin_fold "Logs from ${GIT_SERVER_NAME} container"
docker_compose logs --no-color "$GIT_SERVER_NAME" 1>&2
end_fold
begin_fold "Logs from ${LDAP_SERVER_NAME} container"
docker_compose logs --no-color "$LDAP_SERVER_NAME" 1>&2
end_fold
begin_fold "Logs from ${GITHUB_CONTAINER_NAME} container"
docker_compose logs --no-color "$GITHUB_CONTAINER_NAME" 1>&2
end_fold
begin_fold "Shutting down docker-compose"
docker_compose down 1>&2
end_fold
fi
unset DOCKER_COMPOSE_NEEDS_SHUTDOWN
}
trap cleanup EXIT
set -e
begin_fold "Building/updating ${APP_NAME} container"
docker_compose build "${APP_NAME}"
end_fold
export DOCKER_COMPOSE_NEEDS_SHUTDOWN="true"
begin_fold "Starting ${LDAP_SERVER_NAME} container"
docker_compose up --force-recreate -d "${LDAP_SERVER_NAME}"
end_fold
begin_fold "Starting ${GIT_SERVER_NAME} container"
docker_compose up --force-recreate -d "${GIT_SERVER_NAME}"
end_fold
begin_fold "Starting GitHub HTTP container"
docker_compose build "${GITHUB_CONTAINER_NAME}"
docker_compose up --force-recreate -d "${GITHUB_CONTAINER_NAME}"
end_fold
# This is where the tests actually run
echo "Starting ${APP_NAME} acceptance test container" 1>&2
docker_compose run "${APP_NAME}" "/acceptance/support/run-app.sh" && exitcode=$? || exitcode=$?
# Display logs from containers and shut down docker-compose.
cleanup
# We're done!
echo ""
echo "*****************************************************************"
echo "${APP_NAME} acceptance exit status = ${exitcode}"
echo "*****************************************************************"
echo ""
exit $exitcode

10
script/lib/fold.sh Normal file
Просмотреть файл

@ -0,0 +1,10 @@
begin_fold() {
local tag="$1"
echo "%%%FOLD {${tag}}%%%" 1>&2
set -x
}
end_fold() {
set +x
echo "%%%END FOLD%%%" 1>&2
}

76
script/test Executable file
Просмотреть файл

@ -0,0 +1,76 @@
#!/bin/bash
# run script/test -h for help
set -e
function usage()
{
echo -e "\t ================== script/test usage =================="
echo -e "\t-h --help : displays help message"
echo -e "\t-d --disable-bootstrap : disables bootstrap"
echo -e "\n\t Suggested flags for development: script/test -d"
}
while [ "$1" != "" ]; do
PARAM=`echo $1 | awk -F= '{print $1}'`
VALUE=`echo $1 | awk -F= '{print $2}'`
case $PARAM in
-h | --help)
usage
exit
;;
-k | --no-linter)
no_linter=1
;;
-d | --disable-bootstrap)
no_bootstrap=1
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
usage
exit 1
;;
esac
shift
done
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
export PATH=/usr/share/rbenv/shims:$PATH
export RBENV_VERSION="$(cat "${DIR}/.ruby-version")"
TRASHDIR=$(mktemp -d /tmp/cibuild.XXXXXXXXXXXXXXXXXX)
cleanup() {
rm -rf "$TRASHDIR"
}
trap cleanup EXIT
cd "$DIR"
. "${DIR}/script/lib/fold.sh"
if [[ -z $no_bootstrap ]]; then
# bootstrap
begin_fold "Bootstrapping"
./script/bootstrap
end_fold
else
echo -e "\nBypass Bootstrap"
fi
bundle exec rspec spec/unit && rspec_exit=$? || rspec_exit=$?
cat "$DIR/coverage/coverage.txt"
grep -q "You're all set, friend" "$DIR/coverage/coverage.txt" && cov_exit=0 || cov_exit=1
echo ""
echo "---------------------------------------"
echo "Summary Results"
echo "---------------------------------------"
echo ""
echo "rspec: exitcode=${rspec_exit}"
echo "coverage: exitcode=${cov_exit}"
[ $rspec_exit -gt 0 ] && exit 1
[ $cov_exit -gt 0 ] && exit 1
exit 0

148
script/vendor-gem Executable file
Просмотреть файл

@ -0,0 +1,148 @@
#!/bin/sh
#/ Usage: script/vendor-gem [-r <rev>] [-n <gem>] [-d <directory>] <git-url>
#/ Build a gem for the given git repository and stick it in vendor/cache. With -r, build
#/ the gem at the branch, tag, or SHA1 given. With no -r, build the default HEAD.
#/ With -d, build the gem at the given directory within the repository.
#/
#/ This command is used in situations where you'd typically use a :git bundler
#/ source which should not be used in the main github app (even for development gems).
set -e
[[ $TRACE ]] && set -x
# write out compare url for review
[ $# -eq 0 ] && set -- --help
# parse args
rev=master
directory="."
while [ $# -gt 0 ]; do
case "$1" in
-d)
directory=$2
shift 2
;;
-r)
rev=$2
shift 2
;;
-n)
gem=$2
shift 2
;;
-h|--help)
grep ^#/ <"$0" |cut -c4-
exit
;;
*)
url="$1"
shift
;;
esac
done
if [ -z "$url" ]; then
echo "error: no git url given. see $0 --help for usage." 1>&2
exit 1
fi
repo=$(echo "$url" | sed 's@^\(https://github\.com.*\)\.git$@\1@')
if [ -z "$gem" ]; then
gem=$(basename "$url" .git)
fi
# the RAILS_ROOT directory
root=$(cd $(dirname "$0")/.. && pwd)
cd "$root"
gem_directory="$root/tmp/gems/$gem/$directory"
# clone the repo under tmp, clean up on exit
echo "Cloning $url for gem build"
mkdir -p "tmp/gems/$gem"
# go in and build the gem using the HEAD version, clean up this tmp dir on exit
echo "Building $gem"
(
cd "tmp/gems/$gem"
git init -q
git fetch -q -fu "$url" "+refs/*:refs/*"
git reset --hard HEAD
git clean -df
git checkout "$rev"
git submodule update --init
git --no-pager log -n 1
cd "$gem_directory"
gemspec=$(ls -1 *.gemspec | head -1)
echo "Building $gemspec"
gemname=$(basename "$gemspec" .gemspec)
echo $gemname > vendor-gem-name
# tag name + number of commits on top of tag + tree sha
GEM_VERSION=""
# No tags
if [ -z "${GEM_VERSION}" ]
then
gem_version=$(ruby -e "require 'rubygems'; spec=eval(File.read('$gemspec')); print spec.version.to_s")
tree_sha=$(git show --quiet --format=format:%t $rev)
GEM_VERSION="${gem_version}.g${tree_sha}"
fi
if [ -z "${GEM_VERSION}" ]
then
echo "couldn't determine the gem version from \"$gemspec\""
exit 1
fi
export GEM_VERSION
# build a wrapping gemspec that adds the sha1 version to the gem version
# unless the gemspec references the GEM_VERSION environment variable
# in which case we assume this is handled explicitly in the gemspec itself
if ! grep -q "GEM_VERSION" < $gemspec
then
cat <<-RUBY > vendor.gemspec
require 'rubygems'
spec = eval(File.read("$gemspec"))
spec.version = "$GEM_VERSION"
spec
RUBY
gem build vendor.gemspec
else
gem build $gemspec
fi
cd "$root/tmp/gems/$gem"
# Bump gem version in Gemfile (and deal with OS X sed differences)
sed -i -e "s/^gem ['\"]$gemname['\"],\( *\)['\"]\([^'\"]*\)['\"]/gem \"$gemname\",\\1\"$GEM_VERSION\"/" ../../../Gemfile
if [ `uname` = 'Darwin' ]; then
rm -f "../../../Gemfile-e"
fi
)
[ $? -eq 0 ] || exit 1
# get the gem name determined in the subprocess
gemname=$(cat "$gem_directory/vendor-gem-name")
# record old gem ref before deleting
oldref=$(ls vendor/cache/$gemname-*.gem | grep -o -E -e "g[0-9a-f]{7}" | cut -c 2-)
# remove any existing gems and add the newly built gem
if [ -n "$gemname" ]; then
git rm -f vendor/cache/$gemname*.gem 2>/dev/null || true
cp "$gem_directory/$gemname"*.gem vendor/cache
git add vendor/cache/$gemname*
fi
# get new gem ref
newref=$(ls vendor/cache/$gemname-*.gem | grep -o -E -e "g[0-9a-f]{7}" | cut -c 2-)
# write out compare url for review
echo "$repo/compare/$oldref...$newref"
rm -rf "tmp"
bundle update --local $gemname
git add Gemfile Gemfile.lock

Просмотреть файл

@ -0,0 +1,26 @@
FROM ruby:2.7.5-buster
LABEL maintainer="GitHub Security Ops <opensource+entitlements-app@github.com>"
ENV HOME /root
ENV RELEASE=buster
ENV container docker
WORKDIR /data/entitlements
# Install dependency packages for bootstrapping and running...
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \
cmake \
ldap-utils
# Install bundler
RUN gem install bundler
# Bootstrap files and caching for speed
COPY "vendor/cache/" "/data/entitlements/vendor/cache/"
COPY "script/" "/data/entitlements/script/"
COPY [".rubocop.yml", ".ruby-version", "entitlements-github-plugin.gemspec", "Gemfile", "Gemfile.lock", "VERSION", "/data/entitlements/"]
RUN ./script/bootstrap
# Source Files
COPY "bin/" "/data/entitlements/bin/"
COPY "lib/" "/data/entitlements/lib/"
COPY "spec/" "/data/entitlements/spec/"

Просмотреть файл

@ -0,0 +1,3 @@
# CA for acceptance
Password for every key: `kittens` (what else? :smile_cat:)

Просмотреть файл

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGJDCCBAygAwIBAgIJAKHrJtknH6gDMA0GCSqGSIb3DQEBCwUAMIGeMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEVMBMGA1UECgwMR2l0SHViLCBJbmMuMSwwKgYDVQQLDCNlbnRpdGxlbWVu
dHMgYXBwIGFjY2VwdGFuY2UgdGVzdCBDQTEdMBsGA1UEAwwUYWNjZXB0YW5jZS1j
YS5naXRodWIwHhcNMTgwMzAxMjE0MDAzWhcNMzgwMjI0MjE0MDAzWjCBnjELMAkG
A1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFu
Y2lzY28xFTATBgNVBAoMDEdpdEh1YiwgSW5jLjEsMCoGA1UECwwjZW50aXRsZW1l
bnRzIGFwcCBhY2NlcHRhbmNlIHRlc3QgQ0ExHTAbBgNVBAMMFGFjY2VwdGFuY2Ut
Y2EuZ2l0aHViMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA55O7ULc/
WQySiMxs9Y8EZVr6hGSJZEUUm7gk42dUSejvj/gCE52AhYUUR5ZQGAd5hZLKNvqH
qunLlRzFqziVo9uQ+EBnEalepO58ohhpzVlzSl4hxlPs9lO6L9owN28xH52DVnsx
G1GzOlZnnhI7nmxui+G9v0J0h5JVotqsYq7PKniovgP7XIg42EvrqYjhaCw53k7y
cgQL1OCxQF+ytiFFNnFrYlHqIZthOLAEP/OHGYZAidZuFzNJso6wEdCe0sYUZtHt
8aX1mWC6CqnaWoAAzyj7h4rwqXo/SqF25G1EKG3AjVUAl33Ai4J1PSJVdETYCV5w
ARcD6gIacDFO1a2bN54nQ/Oke8Yb5nAVuNrMFe5/gNZCQ2WTKYHUm8G8FbdsKs98
8guxfrewGRblQLUg3NRZQXupPXYi5Wy+d+TEtbYTAZi/mq+S16aOW32MALM6z/85
B8qCQUEM00K5GcpZAn3GnuWv1Pr79MQ2Fj5h40lJU4u3zDTKF1xNE4XOaIKldZlS
YJuJ0JKGF47MYxxa2ywxIWKM1+ukFMsc7XSlWXpvecCmWXvEbq4QkUHvGs//tWMW
6+hXc7zT3I6HxtHw+dllvGvkdP2KkS/SdJ4VDI+sK33dV0PqZs/O5c1GwpiCyXeY
mZpcOktXyjAe1iutVemYoNowNU4tzbqse8sCAwEAAaNjMGEwHQYDVR0OBBYEFEHP
qR0y/D7SMWwxSnZ7sh60TUvqMB8GA1UdIwQYMBaAFEHPqR0y/D7SMWwxSnZ7sh60
TUvqMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
CwUAA4ICAQBWxFhxvqpnel7AGhiB/kk1B792TZ83WB7QIDEXjV21eylvdQTCF5tM
3U3U5DsbxsyDW7pG5TykQOc83Z1F4AGUB2YyjMmsU5702RX3aBoH9J8wJ4dVoDWn
eevNXgR1WNpXsk3XOmunGIHEFSfEbZK9KAKOCAIP3CVUvKzS1rjlfpU+OS+qhrq8
EIIDSH4Twz25NHKTp/1Qq7YOeDrShkWKC5EWIXi3K85NKbJ6R7ulluDsv+alfAd5
0Y4RLKkDmrDQfBnQcMFmsgZmZ4/NfZPO/1siDsyTPIm/58w0pXKEyhQBi9pd/8PM
h8kzktldJFP5cPTSMz0hGMxUft5uFzHA4dAsuAUKo/zshpukjTYJW4Q8fykQ33tn
bqxheuLZO45rJo4jP1Nq/7g8yVXRae5JoxoYKEeciWW+ept2ygrQb/bUmoSWabQI
qX/TUzheRPB9TDBuRfQSHrxZb7EHTbB7E5GKbEEmFDsoB9vMcvg2k+OAmooNVtGO
s351Irsm2EBinlRBKIbej1am2pn2F96aVw5T1UJxlFGBPc6WgEOsWXtelwN2MOOs
7dyLMwjhEa775vXdcFwN15rX4MPOA2nGK+jpNrreHexv3r5dXSnOlESOC0sxMDgu
vLRJaJ2JnnuoDAvhaw6AOwzbwVGDHYl2ClUbKz7Iy5YwvF25khLswQ==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1 @@
V 371116214418Z 1000 unknown /C=US/ST=California/O=GitHub, Inc./OU=entitlements app acceptance test CA/CN=intermediate-ca.github

Просмотреть файл

@ -0,0 +1 @@
unique_subject = yes

Просмотреть файл

Просмотреть файл

@ -0,0 +1,70 @@
-----BEGIN CERTIFICATE-----
MIIGCjCCA/KgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZ4xCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUw
EwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAg
YWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRhY2NlcHRhbmNlLWNhLmdpdGh1
YjAeFw0xODAzMDEyMTQ0MThaFw0zNzExMTYyMTQ0MThaMIGIMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR2l0SHViLCBJbmMuMSww
KgYDVQQLDCNlbnRpdGxlbWVudHMgYXBwIGFjY2VwdGFuY2UgdGVzdCBDQTEfMB0G
A1UEAwwWaW50ZXJtZWRpYXRlLWNhLmdpdGh1YjCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAJZfDROyejD2AKheJUyAonf44Li8cAtbr6vCZ4/RyvoPiYD/
uJBBkV09/y3ldkQgHBt9zr7SE4BKjJI/w+X1bo26bD7RvRaC8gxUrT5ym5cJAXaf
1/QXoLiORxlK4F1TZKb8anFvNtS5nZ8Bzyxewm65cDUlyZJhMTBWTSs8qIXHSL0D
PaqKytnQCmtGCQYdb0qPV/aQpmlw8oW1mkdQTiJy5getBh/eDKQcC+owdl+ZvDOd
sFpcYWY5VgxdUTmnNQRRLUW55SMJJBppdxveLcMQOb43W9FBNL4pkSZ2yC+0ES/2
0FI7ydixucqeu5HPpS3eRPTkr3ATtO685LMsAAfn0AlLlnQ2fLMNZWD7bYtoNWSd
uxpZrVJhsAqTFHd/bx/aSPRdYDwoS9bIYin8uEUx7LQNX9/viO7NDcS6Kor7Lpu4
kqy7HvSSgoY6bDuGm/mexzOL9IljHac9ztuoeCDa/Gca57asu4pp5mWZnBgHfIu9
U9fxiqadTmHHYgXgEB2SXaSM0GIwy7014L3Bpsdlp/4ywTaOpxsiJntHlZLfw5Jw
LJA696xq6pQl7vkdkDm5UF2BH+dk3wuRX1MCCbim1CwGtwws4UR+NQH6A5CKfBbL
VTOLO4nqe7X7/u+s7p1McI+LPuMnfM1eu5yFblmvebNggcLpKOPea+ClqIIlAgMB
AAGjZjBkMB0GA1UdDgQWBBSw2FmWIrihGNPV0KKeQO6t/R9L8zAfBgNVHSMEGDAW
gBRBz6kdMvw+0jFsMUp2e7IetE1L6jASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1Ud
DwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA3mt0BFljOxA2HuXV/vXdGBQY
ErR2vGZNvS0DMBhBBZcWaILesFeBEiHxLXlDcSisvsK2LsN6s3N8AYPY9s6f6XQ1
amSG9osfTouS2y/JqO3bbJSjL/pjz1/s1x/d9PwYPlEFeCniihAbwpQiYoyec0wh
ihQ1vDs4wQBLD+Gj+gYhnUf35OWhBjVw+iu0/oIxjdmQEQS1Qe/zimk+CpmdGf5u
rNWKDxs5TSskvCIwBdzh/hCvVuzk7fATc1pj5IaGi8I5o5070O4OJ5O2vLnKuwQw
pr7AYRGvew08xaMKVXhnBa9rNyynwb6u6vOeks8C6WQF14i+0WPPk/vcyfQZv6t6
lpD8kalzV94MbIM27HFY0dgboIhYFYJ31RGiC8lq1JYKdeBY2Z8eKRbmwrfvDiYU
CYAVkYH2QHD3a/H5fuB0JR20Tt82+NKKsH+fZO+kCMRDRpmRzG/H7Aiahs4gwjqq
29jTFg2NGYyoBfyQELuHSM/iW7reHdg/F1WqUW7AGmoAtf0Jk1PGfIJbczRAGYqS
FWh1X9nHvh51jg8arHPaRiYeLuSsjkYvnQGVhNnb+0q6M1H7tHZfKahuuUKeem4+
5l2eDeY3FlijQ6jtHOF4HsfJMlDTFLb6Adtl46Quh6zFJjkApldl6L9IuAbSffHV
Kpfjbv8qMtY2qx/pYI0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGJDCCBAygAwIBAgIJAKHrJtknH6gDMA0GCSqGSIb3DQEBCwUAMIGeMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEVMBMGA1UECgwMR2l0SHViLCBJbmMuMSwwKgYDVQQLDCNlbnRpdGxlbWVu
dHMgYXBwIGFjY2VwdGFuY2UgdGVzdCBDQTEdMBsGA1UEAwwUYWNjZXB0YW5jZS1j
YS5naXRodWIwHhcNMTgwMzAxMjE0MDAzWhcNMzgwMjI0MjE0MDAzWjCBnjELMAkG
A1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFu
Y2lzY28xFTATBgNVBAoMDEdpdEh1YiwgSW5jLjEsMCoGA1UECwwjZW50aXRsZW1l
bnRzIGFwcCBhY2NlcHRhbmNlIHRlc3QgQ0ExHTAbBgNVBAMMFGFjY2VwdGFuY2Ut
Y2EuZ2l0aHViMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA55O7ULc/
WQySiMxs9Y8EZVr6hGSJZEUUm7gk42dUSejvj/gCE52AhYUUR5ZQGAd5hZLKNvqH
qunLlRzFqziVo9uQ+EBnEalepO58ohhpzVlzSl4hxlPs9lO6L9owN28xH52DVnsx
G1GzOlZnnhI7nmxui+G9v0J0h5JVotqsYq7PKniovgP7XIg42EvrqYjhaCw53k7y
cgQL1OCxQF+ytiFFNnFrYlHqIZthOLAEP/OHGYZAidZuFzNJso6wEdCe0sYUZtHt
8aX1mWC6CqnaWoAAzyj7h4rwqXo/SqF25G1EKG3AjVUAl33Ai4J1PSJVdETYCV5w
ARcD6gIacDFO1a2bN54nQ/Oke8Yb5nAVuNrMFe5/gNZCQ2WTKYHUm8G8FbdsKs98
8guxfrewGRblQLUg3NRZQXupPXYi5Wy+d+TEtbYTAZi/mq+S16aOW32MALM6z/85
B8qCQUEM00K5GcpZAn3GnuWv1Pr79MQ2Fj5h40lJU4u3zDTKF1xNE4XOaIKldZlS
YJuJ0JKGF47MYxxa2ywxIWKM1+ukFMsc7XSlWXpvecCmWXvEbq4QkUHvGs//tWMW
6+hXc7zT3I6HxtHw+dllvGvkdP2KkS/SdJ4VDI+sK33dV0PqZs/O5c1GwpiCyXeY
mZpcOktXyjAe1iutVemYoNowNU4tzbqse8sCAwEAAaNjMGEwHQYDVR0OBBYEFEHP
qR0y/D7SMWwxSnZ7sh60TUvqMB8GA1UdIwQYMBaAFEHPqR0y/D7SMWwxSnZ7sh60
TUvqMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
CwUAA4ICAQBWxFhxvqpnel7AGhiB/kk1B792TZ83WB7QIDEXjV21eylvdQTCF5tM
3U3U5DsbxsyDW7pG5TykQOc83Z1F4AGUB2YyjMmsU5702RX3aBoH9J8wJ4dVoDWn
eevNXgR1WNpXsk3XOmunGIHEFSfEbZK9KAKOCAIP3CVUvKzS1rjlfpU+OS+qhrq8
EIIDSH4Twz25NHKTp/1Qq7YOeDrShkWKC5EWIXi3K85NKbJ6R7ulluDsv+alfAd5
0Y4RLKkDmrDQfBnQcMFmsgZmZ4/NfZPO/1siDsyTPIm/58w0pXKEyhQBi9pd/8PM
h8kzktldJFP5cPTSMz0hGMxUft5uFzHA4dAsuAUKo/zshpukjTYJW4Q8fykQ33tn
bqxheuLZO45rJo4jP1Nq/7g8yVXRae5JoxoYKEeciWW+ept2ygrQb/bUmoSWabQI
qX/TUzheRPB9TDBuRfQSHrxZb7EHTbB7E5GKbEEmFDsoB9vMcvg2k+OAmooNVtGO
s351Irsm2EBinlRBKIbej1am2pn2F96aVw5T1UJxlFGBPc6WgEOsWXtelwN2MOOs
7dyLMwjhEa775vXdcFwN15rX4MPOA2nGK+jpNrreHexv3r5dXSnOlESOC0sxMDgu
vLRJaJ2JnnuoDAvhaw6AOwzbwVGDHYl2ClUbKz7Iy5YwvF25khLswQ==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,40 @@
-----BEGIN CERTIFICATE-----
MIIG8zCCBNugAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAq
BgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR8wHQYD
VQQDDBZpbnRlcm1lZGlhdGUtY2EuZ2l0aHViMB4XDTE4MDgwNDE2MDE1OFoXDTI4
MDgwMTE2MDE1OFowgYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtHaXRIdWIgSW5jLjEZ
MBcGA1UECwwQRW50aXRsZW1lbnRzIEFwcDEUMBIGA1UEAwwLZ2l0aHViLmZha2Uw
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDTMuTl+HsK08zKp4VuuxSN
BjAl2dsBkWy0cu6hdFg6x0RHVbVaYquSUSJJQ9P/YHZ/J0E0pZqCZ1DbVpm1HIhM
S4A2mFwt8URAACOMAIXU2sRcujimaqd3zlKmLa8E7g9lrgqKwnMyZfUjs+69G/ao
oLVv8np3XiJCKlSxezIEDnZjVKftb98wJzZEfvIS+Qw9/wKUt3Ou4SZQuytFoN+c
5KT4ebnxP9VFoeBexyVsus/ijCKafsqoEj8XvoApEP2FN8CpBIGxyotdLW3KIKz9
zg/t/IXtpIgp3gtiWp4WUpBX1bchULQ78HGxAq66/oY3ZZbKUuOhm2th0QsubgGc
s63djNYTcEhKMprjEO2BbGdgqkKkRlZQzX49hAMmFN632y0renArjOY3jGaVwEdF
S+eFsMzavVkwGT6XmUMn6d3e9cXTdI+gxAgqGGjZ81LLbHFtXBq2ZVkMqe4oA7Lw
SH8D+7tFFVlFzes3QFF84abCzAmMSlr2C+Q1G7PsoD4FfEfu3wfFDHI2K6bNmKoo
Mhwga96TYzJATmPOWA+wrAY41yiKr3k/F4xNWDGRKedX7RBORouZ3dAJKoDm/8kw
DmjXU7Ry3M15O2NHz2A0uAy6AWC3KOHXd9kSbuHJsBzCtTB6Nko4Sr0Pb6dy8OUg
i9DmRA7IRONenHv1jYVtMwIDAQABo4IBajCCAWYwCQYDVR0TBAIwADARBglghkgB
hvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIFNl
cnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxVJuZ7xgP0URnJW1d3Vk8bGTvMw
gcwGA1UdIwSBxDCBwYAUsNhZliK4oRjT1dCinkDurf0fS/OhgaSkgaEwgZ4xCzAJ
BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsMI2VudGl0bGVt
ZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRhY2NlcHRhbmNl
LWNhLmdpdGh1YoICEAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMA0GCSqGSIb3DQEBCwUAA4ICAQA1aUG7IlEcvSyM727yW/jXbhBLZSsbqRws
lcrGa3QtvbxjhdERDmOuEsm5nxok8UJiix//0Ew3DDF3FmiyEiM1rKkYakntwIbP
JmxLAXX2RXUqklJzvF+3CpU82XF31a34hDyoc2/DM64Ha9RiM17gSlwUfrRoOSmK
REa33T6mQWid6VljcYzFabYHdTQkXDU7TiCwKOiZHgnrMPA1W+atvSIh2JWgVJdp
w/dxxpOuE3dkL6WpntlWfSQHQToHbiyjDLhl918W9YHKQSRE2RHA3YoGE2gEdCz3
HG/4Rrfp4WFRPUe1lscnm/5x6STQekHSLl+d56f+KBKQYl78VMUaDwbFrGaOok5u
XUCyhRWsOnHVRhQWe6Y6j/ScBWuog1tKeYE18vxplREMjW9Zij6pnpuF7APaSks7
KUfv3xrqSSCDpxXFU2W27DzdWeQHKTh+efunk1r22XdxgMA+b6xUyt/zMlfxuf9C
HigcjjD7UFgw10bdV09ZjEZ43dwH+AhBVCcyf81gdiL3OOpGRW5QSSBwncyNxB8c
n7Xo1FaF2Ao3X5Bwb5nYjdIbGHOSAfVcK4iTrltjxtVhPdvKs6aguircnol867nV
WS0w/Mvt2gzbh06OCMOG2YoQvvUJuMtzKTnxLGOFNSLitcbkK5A3Y08a1gT+8bwa
9i4cSDAvaw==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCjCCA/KgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZ4xCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUw
EwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAg
YWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRhY2NlcHRhbmNlLWNhLmdpdGh1
YjAeFw0xODAzMDEyMTQ0MThaFw0zNzExMTYyMTQ0MThaMIGIMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR2l0SHViLCBJbmMuMSww
KgYDVQQLDCNlbnRpdGxlbWVudHMgYXBwIGFjY2VwdGFuY2UgdGVzdCBDQTEfMB0G
A1UEAwwWaW50ZXJtZWRpYXRlLWNhLmdpdGh1YjCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAJZfDROyejD2AKheJUyAonf44Li8cAtbr6vCZ4/RyvoPiYD/
uJBBkV09/y3ldkQgHBt9zr7SE4BKjJI/w+X1bo26bD7RvRaC8gxUrT5ym5cJAXaf
1/QXoLiORxlK4F1TZKb8anFvNtS5nZ8Bzyxewm65cDUlyZJhMTBWTSs8qIXHSL0D
PaqKytnQCmtGCQYdb0qPV/aQpmlw8oW1mkdQTiJy5getBh/eDKQcC+owdl+ZvDOd
sFpcYWY5VgxdUTmnNQRRLUW55SMJJBppdxveLcMQOb43W9FBNL4pkSZ2yC+0ES/2
0FI7ydixucqeu5HPpS3eRPTkr3ATtO685LMsAAfn0AlLlnQ2fLMNZWD7bYtoNWSd
uxpZrVJhsAqTFHd/bx/aSPRdYDwoS9bIYin8uEUx7LQNX9/viO7NDcS6Kor7Lpu4
kqy7HvSSgoY6bDuGm/mexzOL9IljHac9ztuoeCDa/Gca57asu4pp5mWZnBgHfIu9
U9fxiqadTmHHYgXgEB2SXaSM0GIwy7014L3Bpsdlp/4ywTaOpxsiJntHlZLfw5Jw
LJA696xq6pQl7vkdkDm5UF2BH+dk3wuRX1MCCbim1CwGtwws4UR+NQH6A5CKfBbL
VTOLO4nqe7X7/u+s7p1McI+LPuMnfM1eu5yFblmvebNggcLpKOPea+ClqIIlAgMB
AAGjZjBkMB0GA1UdDgQWBBSw2FmWIrihGNPV0KKeQO6t/R9L8zAfBgNVHSMEGDAW
gBRBz6kdMvw+0jFsMUp2e7IetE1L6jASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1Ud
DwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA3mt0BFljOxA2HuXV/vXdGBQY
ErR2vGZNvS0DMBhBBZcWaILesFeBEiHxLXlDcSisvsK2LsN6s3N8AYPY9s6f6XQ1
amSG9osfTouS2y/JqO3bbJSjL/pjz1/s1x/d9PwYPlEFeCniihAbwpQiYoyec0wh
ihQ1vDs4wQBLD+Gj+gYhnUf35OWhBjVw+iu0/oIxjdmQEQS1Qe/zimk+CpmdGf5u
rNWKDxs5TSskvCIwBdzh/hCvVuzk7fATc1pj5IaGi8I5o5070O4OJ5O2vLnKuwQw
pr7AYRGvew08xaMKVXhnBa9rNyynwb6u6vOeks8C6WQF14i+0WPPk/vcyfQZv6t6
lpD8kalzV94MbIM27HFY0dgboIhYFYJ31RGiC8lq1JYKdeBY2Z8eKRbmwrfvDiYU
CYAVkYH2QHD3a/H5fuB0JR20Tt82+NKKsH+fZO+kCMRDRpmRzG/H7Aiahs4gwjqq
29jTFg2NGYyoBfyQELuHSM/iW7reHdg/F1WqUW7AGmoAtf0Jk1PGfIJbczRAGYqS
FWh1X9nHvh51jg8arHPaRiYeLuSsjkYvnQGVhNnb+0q6M1H7tHZfKahuuUKeem4+
5l2eDeY3FlijQ6jtHOF4HsfJMlDTFLb6Adtl46Quh6zFJjkApldl6L9IuAbSffHV
Kpfjbv8qMtY2qx/pYI0=
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGDDCCA/SgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAq
BgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR8wHQYD
VQQDDBZpbnRlcm1lZGlhdGUtY2EuZ2l0aHViMB4XDTE4MDMwMTIxNDc0N1oXDTI4
MDIyNzIxNDc0N1owgZoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4x
LDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMRkw
FwYDVQQDDBBsZGFwLXNlcnZlci5mYWtlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxOfyPfi1TmXKIaeTNvK8i0HTpnt1/VTYAUVJRFLvZbKvhcz6qvO9
3XVNQej+ZacGuSxh7NOKCOUGBc5K9P//Y1xAyH+FrvdP8Q73fY8yGcAWWvCAM9Z/
Hw22aPvSiiG4s1Lro4IRW2ubZcfkZAhkUTVLJCVqrXczxJ2KhqgbKdmTPa+xGhcd
tTHvX6NnKQHpYqfSH3OSabLx2YM6WYsC97OMGDFUpN3vObF6/YvgdU8qDKln7cB3
Q3B2FaBxAccZEhmUvV+fsYLhNjVPMs/VvIWK6xD7TLaLnE2O19tRWUAGl4woaeby
lwNsyqefJP0HrodnzYxvMEVNydNDKtCcYQIDAQABo4IBajCCAWYwCQYDVR0TBAIw
ADARBglghkgBhvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2Vu
ZXJhdGVkIFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUuYHcULNnfwrZHwYk
ImOnE0CUa5wwgcwGA1UdIwSBxDCBwYAUsNhZliK4oRjT1dCinkDurf0fS/OhgaSk
gaEwgZ4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
DA1TYW4gRnJhbmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsM
I2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRh
Y2NlcHRhbmNlLWNhLmdpdGh1YoICEAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQM
MAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4ICAQBQPyaEjFKfTMYe2bSrCLt+
UOrYfFdsEVxK4rtsKwLtSpHQXfD/tF/3XV0jOU/yP/bB40leQ8AzGBnZLKPbh0P2
oaKqazBwd2aaCePUUXy831pM5v+NpRi2jX9gKjT1cAHLDjAYPt9Cd3D9IycWeand
8QWU5CzXCZrjDhDC8T7rplyO3bA2TwWnXt/BqsgKAIoGt/6EUR/YmQSL2K7ykU8Z
UAN6Kx6P3dRNkQ/5RCXx65Tum2ogXUkE/w8GWNYB21VGWMPIp1v8i8uBESg9aHRU
pQg6xOdAU1XekdFrau5JsNoYupPj125aAiKgLxvvz/xOpaQh0jQyzmlGH63i0nMv
uF2c1CQRVpjEqPW5Ok5fDQLPJQ7PP49k/5Fl+uDgAEEmq3lpF0yYFmDr8gTMZ8mf
sPahN6xZQCsIN3BlPkpuf0harHWhwyr1CTT1vSrcKdUGxeOxMPrjhZIYgDrpMAt8
B1n7BGfLJqQJ7shjOpaqCv67qqDLWcbhMMKZYbdDgoQyhT1GjFNVcIgNFgOj9vCj
9GF6eInT0LuNQEfnXJ4V6JlswFS7A6L9Us5vAgg1HmAGqQ339Ljy6Py/nN5QwUGp
cqVcWDioSZq0HSkPo753/NDTAfn/AeIS+Wn3BXHZpoXhmZplp3JL3MfP9Aww+ZnL
xRYlA4mOUC6bNM9DVzw3AA==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1 @@
1000

Просмотреть файл

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIExzCCAq8CAQAwgYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtHaXRIdWIgSW5jLjEZ
MBcGA1UECwwQRW50aXRsZW1lbnRzIEFwcDEUMBIGA1UEAwwLZ2l0aHViLmZha2Uw
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDTMuTl+HsK08zKp4VuuxSN
BjAl2dsBkWy0cu6hdFg6x0RHVbVaYquSUSJJQ9P/YHZ/J0E0pZqCZ1DbVpm1HIhM
S4A2mFwt8URAACOMAIXU2sRcujimaqd3zlKmLa8E7g9lrgqKwnMyZfUjs+69G/ao
oLVv8np3XiJCKlSxezIEDnZjVKftb98wJzZEfvIS+Qw9/wKUt3Ou4SZQuytFoN+c
5KT4ebnxP9VFoeBexyVsus/ijCKafsqoEj8XvoApEP2FN8CpBIGxyotdLW3KIKz9
zg/t/IXtpIgp3gtiWp4WUpBX1bchULQ78HGxAq66/oY3ZZbKUuOhm2th0QsubgGc
s63djNYTcEhKMprjEO2BbGdgqkKkRlZQzX49hAMmFN632y0renArjOY3jGaVwEdF
S+eFsMzavVkwGT6XmUMn6d3e9cXTdI+gxAgqGGjZ81LLbHFtXBq2ZVkMqe4oA7Lw
SH8D+7tFFVlFzes3QFF84abCzAmMSlr2C+Q1G7PsoD4FfEfu3wfFDHI2K6bNmKoo
Mhwga96TYzJATmPOWA+wrAY41yiKr3k/F4xNWDGRKedX7RBORouZ3dAJKoDm/8kw
DmjXU7Ry3M15O2NHz2A0uAy6AWC3KOHXd9kSbuHJsBzCtTB6Nko4Sr0Pb6dy8OUg
i9DmRA7IRONenHv1jYVtMwIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBAGyN0ijX
wCXs63bG1q9LuhAxtzE3oK9jzxoLr4ObHS2hf8Ix5Vlgw8TSPam/z/P3cQxdfItS
0U8pRbmPa+Cmuv2bx9SAz9PRMGZXUklh6RF81maHgJt/EdjXIAOdSqSTuilJvTT6
jo2BDOkv0G+iInSKbBY/Q5M7a36Xy4u1WbGabd+wIkQTnNmCUPTV13DbmMkVEGhC
qq5/S/oMnBs9IbfcqOYR50tyI1T2Y7vMBVq2oKIPmqNGB7qDO9unUt+rF1JJJ++E
2SoDAJpVD/VzNOkAdWv+Br7WeIlFGAQAygU1T+dYLM3bW6m6JNhI3GdxMYl1xber
1oND/5OU8amM9b2p47ziQHhR+ZbpEbYcRFqKy236/4uCzNnAsJaAFHGQDRJ5TrIk
nRnZvcEdaUcID96qUeFhmkuhO/zcA6cXTbPXkr/QPP+C6YBPIk+UcOfrvGgchje7
VUYRI7JZD7a9EflQVTvnms2oNPqOE7GLhsfk3pLwoxVe7rC35E07YOD6thSURS7s
GligtIWNines48XQ8tDynPf2AQI4VJLHqZ8+RgSW3S45M+spBlkGTkH/LtPIjE5F
O3IZJBqaafeSbSwlahDY7gCFp/ol1BsGMhGnSK4zKEF++SdP4x+tsN4j0gDSAYPg
Y3zSxqShCe0+62u3hQGJH++uytshC/Guctfv
-----END CERTIFICATE REQUEST-----

Просмотреть файл

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIE5jCCAs4CAQAwgaAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4x
LDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR8w
HQYDVQQDDBZpbnRlcm1lZGlhdGUtY2EuZ2l0aHViMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAll8NE7J6MPYAqF4lTICid/jguLxwC1uvq8Jnj9HK+g+J
gP+4kEGRXT3/LeV2RCAcG33OvtITgEqMkj/D5fVujbpsPtG9FoLyDFStPnKblwkB
dp/X9BeguI5HGUrgXVNkpvxqcW821LmdnwHPLF7CbrlwNSXJkmExMFZNKzyohcdI
vQM9qorK2dAKa0YJBh1vSo9X9pCmaXDyhbWaR1BOInLmB60GH94MpBwL6jB2X5m8
M52wWlxhZjlWDF1ROac1BFEtRbnlIwkkGml3G94twxA5vjdb0UE0vimRJnbIL7QR
L/bQUjvJ2LG5yp67kc+lLd5E9OSvcBO07rzksywAB+fQCUuWdDZ8sw1lYPtti2g1
ZJ27GlmtUmGwCpMUd39vH9pI9F1gPChL1shiKfy4RTHstA1f3++I7s0NxLoqivsu
m7iSrLse9JKChjpsO4ab+Z7HM4v0iWMdpz3O26h4INr8Zxrntqy7imnmZZmcGAd8
i71T1/GKpp1OYcdiBeAQHZJdpIzQYjDLvTXgvcGmx2Wn/jLBNo6nGyIme0eVkt/D
knAskDr3rGrqlCXu+R2QOblQXYEf52TfC5FfUwIJuKbULAa3DCzhRH41AfoDkIp8
FstVM4s7iep7tfv+76zunUxwj4s+4yd8zV67nIVuWa95s2CBwuko495r4KWogiUC
AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQBPrKarbvuT7jtdlA//OgVOOxBIVuPf
99eG3khYuVtiAUdCFvDcNjfqDrxtcTyoFhlX2nj71pPVvzzt8N5K/TmXfbQ/du5v
EeoTSIf8dwOB5huymV4zrl10+MZyshth5X3d27zZh/i0A1Pxgrn/nrDyTNVCxV7x
OSTr4ihuehxuINut463YyoZUDrBZnQKmy78HpnPLDCg+g2cLPqKT2HgEfq/3mLRu
oVvbTyVYn8E0xLiPKsoCXKSPH2+aoXAss4mLMqMDzeyXG3DmS8Nx5RjVPtGxmvBQ
2GPBdEio12pctupeFznVzhxyt60aoZ+UsjJQcFvIZQJtDJm5Fy1O6hUz+JJomZ7p
6Tg9tw7VVHNMcx3Qe1fI+4aVq0ZWpRockGpclztNYo3P7CPnMV6fjMGo7vwRhSE0
+wS+hV4C0vmgTcfHVdLt6QDjYYCteH+5u68AMtpBDjGPpRhIBCnw89D91/TZijYm
Hsk90SrIQ5y0IArT/+pDdRy6I/A/qAadGyNi14DjVexsNFFH4vrkVRWX3QdcGxvX
vkMPLlv1OQ9BP47ROWMJgUK3Nc++o+hg2lc8GmlAYZ4o2daRX5Pt3qaH/rxRob7D
ZDKJLmrKoLmtckZJtofncsbum7qOeAmFszBTNs6e/Qv37VaZrMUxHWVZeZL6SLdG
8sbc6ngSu9zUyw==
-----END CERTIFICATE REQUEST-----

Просмотреть файл

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC4DCCAcgCAQAwgZoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4x
LDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMRkw
FwYDVQQDDBBsZGFwLXNlcnZlci5mYWtlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxOfyPfi1TmXKIaeTNvK8i0HTpnt1/VTYAUVJRFLvZbKvhcz6qvO9
3XVNQej+ZacGuSxh7NOKCOUGBc5K9P//Y1xAyH+FrvdP8Q73fY8yGcAWWvCAM9Z/
Hw22aPvSiiG4s1Lro4IRW2ubZcfkZAhkUTVLJCVqrXczxJ2KhqgbKdmTPa+xGhcd
tTHvX6NnKQHpYqfSH3OSabLx2YM6WYsC97OMGDFUpN3vObF6/YvgdU8qDKln7cB3
Q3B2FaBxAccZEhmUvV+fsYLhNjVPMs/VvIWK6xD7TLaLnE2O19tRWUAGl4woaeby
lwNsyqefJP0HrodnzYxvMEVNydNDKtCcYQIDAQABoAAwDQYJKoZIhvcNAQELBQAD
ggEBAJGIcuQvJw6PANXkpVYJoRd09A37MLUPwIW2QJUtqezjwxa6y6YO3e/Y0f+3
5ILXsH/uUIq6YGs4tjS3K0Dob3TOksDNef++oTizmbFqCJt18a2dg395p//QT1u0
WAt5eNb7dNcENNu5wwON6vf4DWq2u5H494bTVCh6WiDmK9sdDwx6tufX2FQUz98s
xA1SPguRAqBBwDwhfX9gGi0vAi2TXVyKjoU7Ks7kr8KxJsF4YQIPSBXDPzmfZmjO
WLzdJhrIBKua2E5F1rPZdRRXIEdEw/SvOFfrC4H09sAH590hl1j2G4MSHdikITvf
OxmZPzUIcr4VSWDh+UE9yEkQp0c=
-----END CERTIFICATE REQUEST-----

Просмотреть файл

@ -0,0 +1,2 @@
V 280227214747Z 1000 unknown /C=US/ST=California/L=San Francisco/O=GitHub, Inc./OU=entitlements app acceptance test CA/CN=ldap-server.fake
V 280801160158Z 1001 unknown /C=US/ST=California/L=San Francisco/O=GitHub Inc./OU=Entitlements App/CN=github.fake

Просмотреть файл

@ -0,0 +1 @@
unique_subject = yes

Просмотреть файл

@ -0,0 +1 @@
unique_subject = yes

Просмотреть файл

@ -0,0 +1 @@
V 280227214747Z 1000 unknown /C=US/ST=California/L=San Francisco/O=GitHub, Inc./OU=entitlements app acceptance test CA/CN=ldap-server.fake

Просмотреть файл

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGDDCCA/SgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAq
BgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR8wHQYD
VQQDDBZpbnRlcm1lZGlhdGUtY2EuZ2l0aHViMB4XDTE4MDMwMTIxNDc0N1oXDTI4
MDIyNzIxNDc0N1owgZoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4x
LDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMRkw
FwYDVQQDDBBsZGFwLXNlcnZlci5mYWtlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxOfyPfi1TmXKIaeTNvK8i0HTpnt1/VTYAUVJRFLvZbKvhcz6qvO9
3XVNQej+ZacGuSxh7NOKCOUGBc5K9P//Y1xAyH+FrvdP8Q73fY8yGcAWWvCAM9Z/
Hw22aPvSiiG4s1Lro4IRW2ubZcfkZAhkUTVLJCVqrXczxJ2KhqgbKdmTPa+xGhcd
tTHvX6NnKQHpYqfSH3OSabLx2YM6WYsC97OMGDFUpN3vObF6/YvgdU8qDKln7cB3
Q3B2FaBxAccZEhmUvV+fsYLhNjVPMs/VvIWK6xD7TLaLnE2O19tRWUAGl4woaeby
lwNsyqefJP0HrodnzYxvMEVNydNDKtCcYQIDAQABo4IBajCCAWYwCQYDVR0TBAIw
ADARBglghkgBhvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2Vu
ZXJhdGVkIFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUuYHcULNnfwrZHwYk
ImOnE0CUa5wwgcwGA1UdIwSBxDCBwYAUsNhZliK4oRjT1dCinkDurf0fS/OhgaSk
gaEwgZ4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
DA1TYW4gRnJhbmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsM
I2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRh
Y2NlcHRhbmNlLWNhLmdpdGh1YoICEAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQM
MAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4ICAQBQPyaEjFKfTMYe2bSrCLt+
UOrYfFdsEVxK4rtsKwLtSpHQXfD/tF/3XV0jOU/yP/bB40leQ8AzGBnZLKPbh0P2
oaKqazBwd2aaCePUUXy831pM5v+NpRi2jX9gKjT1cAHLDjAYPt9Cd3D9IycWeand
8QWU5CzXCZrjDhDC8T7rplyO3bA2TwWnXt/BqsgKAIoGt/6EUR/YmQSL2K7ykU8Z
UAN6Kx6P3dRNkQ/5RCXx65Tum2ogXUkE/w8GWNYB21VGWMPIp1v8i8uBESg9aHRU
pQg6xOdAU1XekdFrau5JsNoYupPj125aAiKgLxvvz/xOpaQh0jQyzmlGH63i0nMv
uF2c1CQRVpjEqPW5Ok5fDQLPJQ7PP49k/5Fl+uDgAEEmq3lpF0yYFmDr8gTMZ8mf
sPahN6xZQCsIN3BlPkpuf0harHWhwyr1CTT1vSrcKdUGxeOxMPrjhZIYgDrpMAt8
B1n7BGfLJqQJ7shjOpaqCv67qqDLWcbhMMKZYbdDgoQyhT1GjFNVcIgNFgOj9vCj
9GF6eInT0LuNQEfnXJ4V6JlswFS7A6L9Us5vAgg1HmAGqQ339Ljy6Py/nN5QwUGp
cqVcWDioSZq0HSkPo753/NDTAfn/AeIS+Wn3BXHZpoXhmZplp3JL3MfP9Aww+ZnL
xRYlA4mOUC6bNM9DVzw3AA==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,40 @@
-----BEGIN CERTIFICATE-----
MIIG8zCCBNugAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAq
BgNVBAsMI2VudGl0bGVtZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR8wHQYD
VQQDDBZpbnRlcm1lZGlhdGUtY2EuZ2l0aHViMB4XDTE4MDgwNDE2MDE1OFoXDTI4
MDgwMTE2MDE1OFowgYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtHaXRIdWIgSW5jLjEZ
MBcGA1UECwwQRW50aXRsZW1lbnRzIEFwcDEUMBIGA1UEAwwLZ2l0aHViLmZha2Uw
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDTMuTl+HsK08zKp4VuuxSN
BjAl2dsBkWy0cu6hdFg6x0RHVbVaYquSUSJJQ9P/YHZ/J0E0pZqCZ1DbVpm1HIhM
S4A2mFwt8URAACOMAIXU2sRcujimaqd3zlKmLa8E7g9lrgqKwnMyZfUjs+69G/ao
oLVv8np3XiJCKlSxezIEDnZjVKftb98wJzZEfvIS+Qw9/wKUt3Ou4SZQuytFoN+c
5KT4ebnxP9VFoeBexyVsus/ijCKafsqoEj8XvoApEP2FN8CpBIGxyotdLW3KIKz9
zg/t/IXtpIgp3gtiWp4WUpBX1bchULQ78HGxAq66/oY3ZZbKUuOhm2th0QsubgGc
s63djNYTcEhKMprjEO2BbGdgqkKkRlZQzX49hAMmFN632y0renArjOY3jGaVwEdF
S+eFsMzavVkwGT6XmUMn6d3e9cXTdI+gxAgqGGjZ81LLbHFtXBq2ZVkMqe4oA7Lw
SH8D+7tFFVlFzes3QFF84abCzAmMSlr2C+Q1G7PsoD4FfEfu3wfFDHI2K6bNmKoo
Mhwga96TYzJATmPOWA+wrAY41yiKr3k/F4xNWDGRKedX7RBORouZ3dAJKoDm/8kw
DmjXU7Ry3M15O2NHz2A0uAy6AWC3KOHXd9kSbuHJsBzCtTB6Nko4Sr0Pb6dy8OUg
i9DmRA7IRONenHv1jYVtMwIDAQABo4IBajCCAWYwCQYDVR0TBAIwADARBglghkgB
hvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIFNl
cnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxVJuZ7xgP0URnJW1d3Vk8bGTvMw
gcwGA1UdIwSBxDCBwYAUsNhZliK4oRjT1dCinkDurf0fS/OhgaSkgaEwgZ4xCzAJ
BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsMI2VudGl0bGVt
ZW50cyBhcHAgYWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRhY2NlcHRhbmNl
LWNhLmdpdGh1YoICEAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMA0GCSqGSIb3DQEBCwUAA4ICAQA1aUG7IlEcvSyM727yW/jXbhBLZSsbqRws
lcrGa3QtvbxjhdERDmOuEsm5nxok8UJiix//0Ew3DDF3FmiyEiM1rKkYakntwIbP
JmxLAXX2RXUqklJzvF+3CpU82XF31a34hDyoc2/DM64Ha9RiM17gSlwUfrRoOSmK
REa33T6mQWid6VljcYzFabYHdTQkXDU7TiCwKOiZHgnrMPA1W+atvSIh2JWgVJdp
w/dxxpOuE3dkL6WpntlWfSQHQToHbiyjDLhl918W9YHKQSRE2RHA3YoGE2gEdCz3
HG/4Rrfp4WFRPUe1lscnm/5x6STQekHSLl+d56f+KBKQYl78VMUaDwbFrGaOok5u
XUCyhRWsOnHVRhQWe6Y6j/ScBWuog1tKeYE18vxplREMjW9Zij6pnpuF7APaSks7
KUfv3xrqSSCDpxXFU2W27DzdWeQHKTh+efunk1r22XdxgMA+b6xUyt/zMlfxuf9C
HigcjjD7UFgw10bdV09ZjEZ43dwH+AhBVCcyf81gdiL3OOpGRW5QSSBwncyNxB8c
n7Xo1FaF2Ao3X5Bwb5nYjdIbGHOSAfVcK4iTrltjxtVhPdvKs6aguircnol867nV
WS0w/Mvt2gzbh06OCMOG2YoQvvUJuMtzKTnxLGOFNSLitcbkK5A3Y08a1gT+8bwa
9i4cSDAvaw==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,132 @@
# OpenSSL root CA configuration file.
# https://jamielinux.com/docs/openssl-certificate-authority/_downloads/root-config.txt
[ ca ]
# `man ca`
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = ./intermediate
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
# The root key and root certificate.
private_key = $dir/private/intermediate.key.pem
certificate = $dir/certs/intermediate.cert.pem
# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/ca.crl.pem
crl_extensions = crl_ext
default_crl_days = 30
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 3650
preserve = no
policy = policy_loose
[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
# Extension to add when the -x509 option is used.
x509_extensions = v3_ca
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
# Optionally, specify some defaults.
countryName_default = GB
stateOrProvinceName_default = England
localityName_default =
0.organizationName_default = Alice Ltd
organizationalUnitName_default =
emailAddress_default =
[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always
[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

Просмотреть файл

@ -0,0 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,CFB07176D5D8B7B07E48974FEB8AD84C
7sYf/zN0GLFaV7YtBxh6IrDNf/nW0VMUz1rUuiUb3/0HrQnFh4xsm6KkNlDgKjH1
Ee5sHOhNeGTuQSHT8CZdCjc5OR2XfKzNuaVaSHY0FF2D1lg529Jut/aDMNZQbXKu
tlx1qRixuEYxYrH/Da16MDyykPIU7YYGqwwuSDfhDK2/z+yu8NLYm24gf+a/n0Jb
zBP03jdAS21e1DSsoNdxzPGDXQmCxv4PXmcpKNAyQV3dSSxiwAdXFqRVKfmiAK4Q
+HT9PDrSdWxBZgUeXhsRkB8v7AmHGPbHTLX2z4fYRy59edrxhD2c6zHmXrP1fPur
MIrBQzkFIcZ3fZH4v1lVnYsy2n40fx4sHi1tU3AzYtKkFnH6Z5VFMCmwUJr65IHk
RRvNDWs4CWbsgacqNlAFpRThNb4Rc+aght6TSCmCwxe2snOPtPIh6FKfHT3jYXES
H91/gzTx6LdCG/qmo2xxdL2Y4LXihN9DkT5MBTioPT3ruDLnJvmYv4x32y+a/dz/
1uTpUBjdDBjLkpOLk6zTUrb24yuBk4cgnb86ZSXYt+42BEr7GuYnBZMM5yR+fzEO
UNf+lJ2rMpb+TmTXamQRV3ijxnt00HnHvpk32pd9Kku7vELybr8IeemIKL/oVfcE
n44zxS15uKolJpjjF7KdnHPwVycW5cuagHNmmRSXXiSO7uzDhbZHh1Dg4Fyka1AJ
Dacc7yP9hVnSjhRCebm8N7b5JgoUGQw5z5Hh2bB8fF2QljDNDftQ2Iyhc1gVxIzI
6LA7NmGUEzDE5irUYEFW2+E1mw8zx0OIfyQ5+K0VpgFlZWZwOFo4jIbDZxnyEmTm
CrgIux7Mx96kI9e8b7fVhTl4G5YeSBLmNeQU/+DWFOCIugg17qPpj/VAvlQbRWrN
lfOQkIAox69ADCH6cJLDl4ZSE54Nl0nXifgM362PJDf9ZMirpDrAUcVifLFrtr23
QG8lOUKXtQ5xL+hJ9CA9xCzWrQl6jByDYc1L5BI9VrgpoRySJJk5cWRtpAZKWuzv
qaEnfLgq1tW77STQPw/BFSKWbBq2mLCMrYW+LJO0CV5CoNZkSRPn+7VMRNh/UtRF
82LENqMCJIDPNp3dt8hOSIXbeK686ET5Z/9QByVbPpcpLeKOGPaXI3vzOjAiHr+q
gtNh7elbGV8nNLtew1HjWmXWTqO08rUqyGYMLEoLI2n+SSH74k/nyPVIzgbxH2EP
lpanO0IlTt87gg9tatbhdaIyo8juBrtNEz2ogAsTefdH9AS4aRjM1lifRsTS6FqI
pFOzzNAPPUqYpzCWSU7q4Po87Zkr53UaBzvBoSlFm1AHEXaMB3dGFZkxkD6E5J9Z
VM3kWH2HO93pbg3NIkL+/H2L4upN8v0LsKJ7a9YCWcYFRE4MHHjxrEc/poEhLasR
dPhCDpnJwVE43QHuerKIpMuVUvkeO4Dn8vn0sQr24pvwJf9dyqaxxIM0tIBMtetu
0SRd3qvutgeg8/JR9QJ1KBJB80hXKqJ8qaCnxXA3ExMBDd8fX7TONCrM0sQgnak4
fnn4u1Y6d3byuDcHS9LRh4Nwr4dIpuO6mLonIyO9Xz7PqjU8iP4wdfPWiFUiPxG9
CMBh4xeSlW3MqjTCPBYtplzCQ5ZMELp3pQq0DLMl/ZAurgGff+wn8RYoXx7TaaI9
PBqk63NYyerQIdRAjvJW/z5mskU4/SmtCbZKNHjxpon5F3gBDLGWZF86hymAIYkH
FIRnvYTP0g246vVul31kFwY7pKgSyFyQBN/3icDHEcsP5+t0k02kIC4wxmmiaWnl
FEUnL6tMZZZujpGd8/3+x3K/A2OahkoXJVDW1zL9mQsgy6qinNyiEHLeaOYXasC5
Ur4dhn3VfN0oxLPvxFzsKsWUsay+JKW8qPFlpDzw5AaeuAdahPKBTuw3TgLfHVyL
F09Hy7VPz/TfoprKJmoVCWvRUpAsXch2x6F+xn5Rbb/hL00QdkuCPs8Ek7JTp+qC
1mqiAITUyuyDsQFgn/pgyUdLsr43IaJeOQNqPKm+7thd0SO89fy06DvCjvXb3A0O
E9A2jTRd+jwGT1QtX7mmFZUb2ZUYqQd+DIaPHrQCAtvjqr1rybBCdxnbJa6Yp4y4
U6v4b/4esTyl7LdmJfbs9PLT2ctMHZsgpNtAI75sWq8iM+HIY0kwrbziL8Kfx5SB
BixcDZ4qBPeUwrijWYK6MiOARdfnwzbQoY+jPhnCizss7EUq7Y9Gt6Cia9X3a1VM
GxqxOjzuPO3ijaH74w+V9YtOzurnjZ/C4nCdztutTbQYpX23M2E3AD1fqMiV83pl
TvrDsK+gsXdDKhF30CgndIBjmiud1t1hHKPMfesQWq/SzKLozxSyEQi/WA0HEO5n
EzMcLiNjR2p7Q5/aXO0KrrALiQsvxK7MNRF7hos1nGWytPW4CJdbRJ2mfkgGdRHG
/z1fik9BQv7RrQv4tKvORlZnQrLxHU9SgEEpvjnS0aDCk2B5ki97W6vBzDPIUOnQ
E3/lFsWjBQGMhINVxh7no0otLNKZaq4ZH+zw2Y+ILjATLbI4UppWAhpo1t08EYHz
mgkER+qN6qNLan1uh3B0hIt3aYwYCjptey8tDRp1JfyUU+DaFtqDZWOOKkKDKUtT
2ecnSarm02G92FOnp7IDn5CvaFj/c9eF9UdZQkwZolcypbMvZi7w6xgtaFpkYuGZ
ND7/7VOwh5fVefWP2t4fH+54NzRrLMsVQnIkcfVtVWGxLGRL260WSnNF8X3+yQ3m
cj5HhQMn5+dTzwDCA/MSu8UlU1wMogG3IhTZ5vvbjuWM9Od5wm91ViMUTU4Alt0E
5kSq04X0ilQ9745gzuyCFL6fiMSiYVkzG4bfVn9eVGeqT5KgV8gyth05ByxfaWf5
SHTGOKNqKhnSeWS+eN0NCDmDn6sGWZpSOFXDjNhjztugS/EE8qHUQqXxEhl0sCNp
Jz4Fgdct62o/xW7iNYuJolN51iwdNIohdzbgdoqwEGnHcliEu93lxp79YdSY0IYN
/QEhmQdK0ZhSOz2x/IY60oCM5W9+QNHq+Z5O61JnlHdElaEB6aN7jyq+jZiINrDP
1mFTeBtDJVVvESfmrHFJCZJ9H+u5LHhnZuiJRyAK6wIbgFoYutT71QaQToi33ZLd
-----END RSA PRIVATE KEY-----

Просмотреть файл

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxOfyPfi1TmXKIaeTNvK8i0HTpnt1/VTYAUVJRFLvZbKvhcz6
qvO93XVNQej+ZacGuSxh7NOKCOUGBc5K9P//Y1xAyH+FrvdP8Q73fY8yGcAWWvCA
M9Z/Hw22aPvSiiG4s1Lro4IRW2ubZcfkZAhkUTVLJCVqrXczxJ2KhqgbKdmTPa+x
GhcdtTHvX6NnKQHpYqfSH3OSabLx2YM6WYsC97OMGDFUpN3vObF6/YvgdU8qDKln
7cB3Q3B2FaBxAccZEhmUvV+fsYLhNjVPMs/VvIWK6xD7TLaLnE2O19tRWUAGl4wo
aebylwNsyqefJP0HrodnzYxvMEVNydNDKtCcYQIDAQABAoIBAQC1jAnsm4hHHVDN
0NzJ9+phFzNWt1RlW7nBCinwZes2QBbXbdPclNkE++dckr8j6GkpiENNgQkvBEKW
TcsMs4+8A83zTqhqVroNTzhfOwz9cv7pn/8ETItujU0W5MAM/eR26kg6vGzWdpOT
t0bb+GTN/y4lyJ3gDBzd9kdHum05GjTxeMbj38xJmsj6mdp8j9Q4RUC4v6kaNV/n
jkHwtD90B/Ke1XLl8WozSaPWrh0DxSQ2TNB9uGIikRR10a5/dQhzER7O574slaPk
ybnOyoFnKzWC3aeBlibZ62Rnwy89UPr/dfzyOGkZEvQBzxtVIRUSVFnyD3uuWav4
EAUKQ4gBAoGBAOmbcs4zWDRt+wxdjQIy+LAIuT6ptd3+phvLBB2cbRCNxo4FwmG/
uacCTJr4kvvUp87YR/mWhpH4mMVskOBlrwl5yNKOt9Ym/9xT+lPoMf7nrH1Whp9H
bvvw4AWCT7yA7qp7lH+VRmI3qAD9i7TJa9e6fJ6dA/OHu1S8Cby9eZ4hAoGBANfH
4KLFxe5CP6zjMLQiS4hqVZLLda6l/Q3boWsp5YbV3IJVOEJJ0xYBOY4CVCon5DLw
A3eTCjEIAiQqMHPkobxYILCgHFq0WX1FT3ryvzkk+jb3wyRMkSVWdNoPC3Xupfbq
MfrGbbxFSwDzEaqYUay72pJSuJzQrPDdkvggBLZBAoGBAOcGycItOwUW41l2R1au
bA1Dg1f6ZJb4GPSRkS9rnNzrKTsZbQRYpNUKzBHS1SHiTFaexIeMGufPb96HILwb
M8DuRPIfHKXYid8u2fkTQCZ0nbySzq5E6fiVXid0mUC2TdIiR1jpmuLVYfwgw4J2
VIXTsxz18YttZ5FEKWkUBWuhAoGAOmR9lq7chEBrSNbmTCjntc/IvoPOoeeLVl+0
C/L3iQiAtshnBBFETTUhpRteOVY4O8yVs5sX6/LV2YGNQR+C4RwcJI//fFBH002Z
V5UvSECIca2UolMSCD+gOY5OtKhTQ0FXKxzdf1BlHqmogzjbCVqAqI44JT+XPUtI
xVqsy8ECgYB2IHRozu2VK/N4myWe6X0MbbMiQ0WVv71sOVa/Eb/WQOqpo89o3tI8
51gunM2hJZkkcdau76+LdMXs9cyUY5VYeYPIGFz6JAjp+H9P0LuhxaI2pppPQ6t9
tMorvd5QK4u3xlJVS10OLTAlRU1xsRTtRqb9KglKWw+xLAvguwTvlg==
-----END RSA PRIVATE KEY-----

Просмотреть файл

@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,B87F38A2BC95F7921446E32017040B8C
7v6YRnTkFRf2xFSSiSt/MYghO6S+MtiuJ4zmYiww1N/nb+B9lj/elxAwvTy6c8vM
e0hNOhOmzwWrOruTwTfpgKiq75ZA915t6c5DrP8ZpKAy+lAKeWFCl5Tpv/DGvwYv
id15yGwWb6mmhEYKjR83uM2S8enhWQb2l2jokols+7AEMAdnTafM+fbSKs0GeXrX
0og8tEqBC7Eax8REmVpVeg3tLwXeRB8RJl7KsLk2Y8kUsUs0RAl8WRsEazhGEuJv
rFbsd0XjcRDSXJxnGEPjrscpc5nxE3lOgbI5xWWvBNZoFn0YF+qDuzyIbpM0I6e0
lYnphYiEoGas+BjHmF5cGKa7fHuF68R4phxcI81XfDJXGj4R+eNSJOqkMFG2CvuH
waqQRLgqE3xhewV2YY/FXJpFwFtlYz53yrAAlLFfyM56psTEyy2/zjazzV2yOVeh
3B791tLk0sIPQ4p9W36PFlvTqP3Eu7ZNHAD7X7ElluhuRLa5yuAhP5tACqRis/qu
uB7OJ0iwH2eNoQO6Mkta/KVTek51ZDTN/cW+fyovJIEGS7hqsGzTtQ2ZLksrROIq
nBPaiUaHP0Do4FBBgN4ebBSmoq+xdlZGJO+rPp9O2UYZx9LeKqh/ibCCwru01Pnb
c+OYEUU3pMKN6NmWRfjw32IKaimv8IM+d7q0I18w2ZoSVE2mAorPqFi7009bl2XD
WUL7LO4N7cHDeXtWWDEw4K/AQzb/0hCpEjpTnMPDBPGZmFxxWza6zTIEYFAcyURa
tKPD4STYXvYrRi2cIpXzAsxxDK+Mfr/+GZMaQQD4DbdO9sTgvWCRwY8mvGkdNTUp
Do+xgLy3argP3iMKgAMLXDzN1tu3/obKCp/JTTrp/DEtR/Sd8kNr2+xQ7IydezZs
6yLps7l89g/M2Apo4hEGEX3K+l+2dfmogv6zCmJQHIE1llJCXkdpvtuuI6eVP8U4
ZNKPHQRJ/jJJHhizI2e2gfdbuWSdizNKOygVT9/Rk0RO846EqHBoEL+pavEthzq6
vMAWqD/TN8KrLcnl98Q2d1myMjeyd49eVZ7trNLregk92qu5i7Twx3WsOArhV0Ke
TybvGaoGmnA53MA3xKUaW8ImqSOMCH0sOtLA/5y7HBnPZtrDRP40Xz0hzozz2XgT
qHt1hsqeLnBLvSzgkZDmHfDzUu/ssWnO03SmS51/umTFeDH5Sdoivo7mAhlobL4G
moT4CPk1fVIWGF+c3b7MHbWVIW1FZbi2mcUqYyziR0kElzPe8X40+3Io0xccB6Ly
tHA+oVm8u96l+K472ndWDsA3ggTs5GomVX3LIbFZl0veXl/2SGTBaCId9QaPPdP0
uV38w4dndSSw7ZX76gPSlcxFEi383WSWobhgv7qdMyUe1G1Q+s/2U3O/uvZQO410
BzJiz1XcPxCzHuH3TN7F+ZaHliB+oIM+eKUyNxTahZtnoWxCR7zOw9epxPoGHyWt
eCzjDHxLpDas24z48Yi6YJuZwGxKrOB4af0IysmsePorDv9dv10NTnZ7TQepy/6P
a/F83vqymcxO0HgKBVBYwlXBsg5OXEFXcnfVpl91vMlUQidvW4Q7IpdrctrgRUAT
-----END RSA PRIVATE KEY-----

Просмотреть файл

@ -0,0 +1 @@
1002

Просмотреть файл

@ -0,0 +1 @@
1001

Просмотреть файл

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCjCCA/KgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZ4xCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUw
EwYDVQQKDAxHaXRIdWIsIEluYy4xLDAqBgNVBAsMI2VudGl0bGVtZW50cyBhcHAg
YWNjZXB0YW5jZSB0ZXN0IENBMR0wGwYDVQQDDBRhY2NlcHRhbmNlLWNhLmdpdGh1
YjAeFw0xODAzMDEyMTQ0MThaFw0zNzExMTYyMTQ0MThaMIGIMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR2l0SHViLCBJbmMuMSww
KgYDVQQLDCNlbnRpdGxlbWVudHMgYXBwIGFjY2VwdGFuY2UgdGVzdCBDQTEfMB0G
A1UEAwwWaW50ZXJtZWRpYXRlLWNhLmdpdGh1YjCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAJZfDROyejD2AKheJUyAonf44Li8cAtbr6vCZ4/RyvoPiYD/
uJBBkV09/y3ldkQgHBt9zr7SE4BKjJI/w+X1bo26bD7RvRaC8gxUrT5ym5cJAXaf
1/QXoLiORxlK4F1TZKb8anFvNtS5nZ8Bzyxewm65cDUlyZJhMTBWTSs8qIXHSL0D
PaqKytnQCmtGCQYdb0qPV/aQpmlw8oW1mkdQTiJy5getBh/eDKQcC+owdl+ZvDOd
sFpcYWY5VgxdUTmnNQRRLUW55SMJJBppdxveLcMQOb43W9FBNL4pkSZ2yC+0ES/2
0FI7ydixucqeu5HPpS3eRPTkr3ATtO685LMsAAfn0AlLlnQ2fLMNZWD7bYtoNWSd
uxpZrVJhsAqTFHd/bx/aSPRdYDwoS9bIYin8uEUx7LQNX9/viO7NDcS6Kor7Lpu4
kqy7HvSSgoY6bDuGm/mexzOL9IljHac9ztuoeCDa/Gca57asu4pp5mWZnBgHfIu9
U9fxiqadTmHHYgXgEB2SXaSM0GIwy7014L3Bpsdlp/4ywTaOpxsiJntHlZLfw5Jw
LJA696xq6pQl7vkdkDm5UF2BH+dk3wuRX1MCCbim1CwGtwws4UR+NQH6A5CKfBbL
VTOLO4nqe7X7/u+s7p1McI+LPuMnfM1eu5yFblmvebNggcLpKOPea+ClqIIlAgMB
AAGjZjBkMB0GA1UdDgQWBBSw2FmWIrihGNPV0KKeQO6t/R9L8zAfBgNVHSMEGDAW
gBRBz6kdMvw+0jFsMUp2e7IetE1L6jASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1Ud
DwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA3mt0BFljOxA2HuXV/vXdGBQY
ErR2vGZNvS0DMBhBBZcWaILesFeBEiHxLXlDcSisvsK2LsN6s3N8AYPY9s6f6XQ1
amSG9osfTouS2y/JqO3bbJSjL/pjz1/s1x/d9PwYPlEFeCniihAbwpQiYoyec0wh
ihQ1vDs4wQBLD+Gj+gYhnUf35OWhBjVw+iu0/oIxjdmQEQS1Qe/zimk+CpmdGf5u
rNWKDxs5TSskvCIwBdzh/hCvVuzk7fATc1pj5IaGi8I5o5070O4OJ5O2vLnKuwQw
pr7AYRGvew08xaMKVXhnBa9rNyynwb6u6vOeks8C6WQF14i+0WPPk/vcyfQZv6t6
lpD8kalzV94MbIM27HFY0dgboIhYFYJ31RGiC8lq1JYKdeBY2Z8eKRbmwrfvDiYU
CYAVkYH2QHD3a/H5fuB0JR20Tt82+NKKsH+fZO+kCMRDRpmRzG/H7Aiahs4gwjqq
29jTFg2NGYyoBfyQELuHSM/iW7reHdg/F1WqUW7AGmoAtf0Jk1PGfIJbczRAGYqS
FWh1X9nHvh51jg8arHPaRiYeLuSsjkYvnQGVhNnb+0q6M1H7tHZfKahuuUKeem4+
5l2eDeY3FlijQ6jtHOF4HsfJMlDTFLb6Adtl46Quh6zFJjkApldl6L9IuAbSffHV
Kpfjbv8qMtY2qx/pYI0=
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,132 @@
# OpenSSL root CA configuration file.
# https://jamielinux.com/docs/openssl-certificate-authority/_downloads/root-config.txt
[ ca ]
# `man ca`
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = .
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
# The root key and root certificate.
private_key = $dir/private/ca.key.pem
certificate = $dir/certs/ca.cert.pem
# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/ca.crl.pem
crl_extensions = crl_ext
default_crl_days = 30
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 3650
preserve = no
policy = policy_strict
[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
# Extension to add when the -x509 option is used.
x509_extensions = v3_ca
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
# Optionally, specify some defaults.
countryName_default = GB
stateOrProvinceName_default = England
localityName_default =
0.organizationName_default = Alice Ltd
organizationalUnitName_default =
emailAddress_default =
[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always
[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

Просмотреть файл

@ -0,0 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,E318106249072FADE86EF6CF5D191E4B
VIUd2hIF6GO+5pEb7FKPILDgmWSDR7HDsydf/4wKhOLNfZBfuQ8zULoHEglq+vyb
PfATA4g5fDbFHZc3bExyIYu/p8jrp+55V2UFcBdickZWW86A13ciSemweIlpL/9M
OP0pXK6zI7Tqq3r96gEtrwMmmp0UMqUQwCCm5VQvi6PwkFMUrtuQBHADHbDsU2zD
7wDgHJcAg8v+Rrvw/QYJ6HlOwEAsxWp03ID5qLP1KaTRG7+2yfegP/cNLAqwkzO3
+gYAdPxYw+sVOUcU+hmrIAvGkvtv8LAbyLaYzMKL6PD7ItZ0L8m65QD8gnpLiHSJ
a48q+O4i63sDb7Qu4lbNjdF2+LqU6+zyS9SW/1C+b66Xu4nwMtXhdLMGunnTyj/U
GHmbbgmYu3sA/JsXCpZ8gIBwWB/ZWktYD2GNhbBTAyE3yfk0rJKiZ7/8E8/ckxyH
fadv7oaZ6wkT/OweL1Yk07/LgEPTiodi8gCKUUEUKztgTuD0U3DBhIT62UOXAywg
cBlQq6fcCt8e8joVS2BttBsFsptb3xhZxfYkHZ6PelxFhQRelIsNq4c+cPqxidZU
DbDfS0J1h3S8O8W7H9H12kw83nkeyds/wRck5F5HTWVjWNZoA1sBR57b/5SXKdPY
FTqr2jeYxRFP0k1AnJri/8CIggAn3vDI3k5KM7AULz4GD27Z6pOTZIeFtb+kk7c9
PVhYCZ35Kl6H023cZHJ9ssq1hvpQ5l4az1wHXMHIp9DnBgyXJ8bXMysmUhlzHr9L
JuEMoX7+i82a23Yzf09ao7cMgHsp+cj/kESCWl5Wn8B1hazW1CyyZnUbyqlCwy7P
0bDWERnlwbzrDKbBHYsiL2ce872wq4aCf31RgE8j/Uy29hZHLKivFgdG0kjjEhOp
/3YdRDfVlZwv5cAL+wEAb+E5NQ7A4p32zz/ej0goltbLRsrU0xg0A24Fu0lvsegf
2s+NTc2REu8RGGPrnRLg+1JS2QDoQOQa8Q7addwnTRvjOoc6VebnLm/L7SoU/3u6
W7kdu62PQPaXsP8mltIMAFNO16dVridHHgBShXLzCUZ4xms5x5NZijvNYdMlHBnb
H9acSkYEAUBHuU3b6WCdTPxpiLGhHit3zlB2FC4An485bfj4HoYjLj2CIi6wxama
FLsVnrceCctGqoUb1Bg5uiy5EgvSPqfxVtD92BHZt3yefyh6bixVc+um8zagBXRR
dSIfTzgIvTF1f7+J5IDAr4cNJ59Q8vBAMfYoodioiQ3m0vB0pHopTlXWMpufG1Pp
ecdSZU3L5LrA+PIjoNgNxT99NaITyB8eBlhQTw9XKv8U1IuQr+/GIqdFTLHq9dZ9
AlFhJaZ2jIJXwQpiqfsKojCnZZJcQuurbQRmkwe87rDbOmWnOIcxhCxCuMYwJIyF
7KKPlzzW1mVx+z+oGfMN0OhSHrpk1jKrCfw6IgaBXNi3tOHIyI9SIclV0BNLmWnm
U1wTFI2DSuXSZsTRwSFl11F7ua2f/j2co3ioizkAwT0sV8UhIU5bpbVx258tV1Ke
CiH42s8EkuGH8dTLZmXCrnLrioECLEO+HoxyzR8oz5k3rL0JJ6IAXlmM87nHEo46
K7sBuvs1e/X6AoTS5mtUsIZMQwFVixMLWleyKj+SjI/+lyK07M+EY4b+vZWmkLIL
4gskDdNjBbxeFs0r3x4vFPLNcnyti4uPAkh3S9cSF4ya9wn/q5pfW0AX4HPXjyHI
Aie36cBmO4ape81VBIXshLdXwKURT/EQ+UWu/ob409RFdX2/QjKJbwmWjHQcy2km
1GgRAyINOTvtvurJJ9Zd17ja9GFL+6LvPfFL2ULBZ385Mr7HZSrfNL9YGGx0ZvIe
/E3FZcgejq5EkFuIJ9O1vWHQfCqU+nUNRR+q4T/a8sB0eCbPxfYkj7xvlVJoVKX2
/btmo3BTxPk8Eo37d7nNGkgedC6aTdc9uKNUkIuFW3FffVugVIg5/nwSB1A2B0e/
ClDapMs7Lgw5Rcnb0/C0eviNRPp5k+gJr8ZEqlTCQi0OGjDTz+1cnYkqmcbIywu3
bF0OXj1BUywefoC5zpgXn3na2ykZ3d0PIF65P5Bt0EG+FB0smUYJ56L1iWWm8thp
6PB8NA9+OgdTWTTLsnLuRCzJk9JD+S+1gyECpK/PfZDg3h/ZEFs8H5xQBSpWULSA
qfJaXiL9jOgai3jsBhwW30iIBYZUcYCqhS/RDsUFse3P+92/D+YzCj5qND19ZHSR
3iq5wEdqiQWuqV9X161Ivk9a7Xh+x+5E8ia5Gw+1sUCnJzXgi4ZOG/BCCceCQJw8
aW3WxsEz9THbCrFrRJYIn8R80Z00h9cbqgH6ehCUu3UUrdhACEjPsCoIg7RG+mUc
pU9kuQtsEcIFh3KxIEOKr88v8ox5wHdI7x87+GZGoudlPoTC8uONcMEUbggThOjZ
TDpYovyUZw2F0Qvg4stgANxRjJvWDK1SODkT06ynRVZLG3eL3UKGnqXm8t6OP9AY
XBDlAgefrvkQfm4L6KNMs8ZwzXJCc982VDsKwO4m0iHk8Ys3zDla51XoCpaQ0V0v
dU35zy+dbyjG0DzuhkzJGjM3pH+8UmyXw3fnBKOaVCjhfHwAgrbX8j8Za15Dgt7R
a7Js+kshtrGtlT+pZ8C5u/pmx4kzSHUzUAE1TKNoA6DMJVLJdL2/L6L3/Mym+EML
V5zHIFVp2/8sRoz4hOYDinNw3ZzW9Gd8GN/8BElzhrnj9O8ybWeVb7LFe4NLFVck
+G9khwZT/2/O7KK+gk/cCf7+5zWQMXdWTQz3OQOGLP7vjVuzWjxMhQUc+66WoqJo
OdKQA4cG8tyBB3fq7HS4qu5b8tuWlQp2WUYxXsgcO/qXrbGvAgMJxAYih85fnofp
RZavv5LQEfH+keTW0Gvd584SyqE/N1BQOsUAzhaLYPjGVd+90pYog+JBF1qtcjo/
Oru5HOdBqytNjFPMcdCek4JdjDSUta8o5bR51BpEQ6KGl3Q3Gch0VT6ACENd5sb9
9ejcbUmdDusbDJn2LEqieNeyOsQUT6naCmwJCQY3nR5D1MGj0zJ1+FCCU6XW5eDW
SiaClZ8rGsXqJabtOoXC4JiDidvDUR6nxJ863yt+Pxi9m/CHAwOKxv/J8ZD6LzBm
-----END RSA PRIVATE KEY-----

Просмотреть файл

@ -0,0 +1 @@
1001

Просмотреть файл

@ -0,0 +1 @@
1000

Просмотреть файл

@ -0,0 +1,55 @@
version: '2'
networks:
ldap-network:
internal: true
services:
entitlements-github-plugin:
build:
context: "${DIR}"
dockerfile: "spec/acceptance/Dockerfile.entitlements-github-plugin"
networks:
ldap-network:
aliases:
- entitlements-github-plugin.fake
volumes:
- "${DIR}/spec/acceptance:/acceptance:ro"
- "${DIR}/vendor/container-gems:/data/entitlements/vendor/gems:rw"
git-server:
entrypoint: /acceptance/git-server/run-server.sh
image: jkarlos/git-server-docker
networks:
ldap-network:
aliases:
- git-server.fake
ports:
- "127.0.0.1:22:22"
volumes:
- "${DIR}/spec/acceptance:/acceptance:ro"
- "${DIR}/spec/acceptance/git-server/keys:/git-server/keys:ro"
ldap-server:
entrypoint: /acceptance/ldap-server/run-server.sh
image: osixia/openldap:1.2.2
networks:
ldap-network:
aliases:
- ldap-server.fake
ports:
- "127.0.0.1:636:636"
volumes:
- "${DIR}/spec/acceptance:/acceptance:ro"
github-server:
build:
context: "${DIR}/spec/acceptance/github-server"
dockerfile: "Dockerfile"
networks:
ldap-network:
aliases:
- github.fake
ports:
- "127.0.0.1:443:443"
volumes:
- "${DIR}/spec/acceptance:/acceptance:ro"
ports:
- "127.0.0.1:80:80"

Просмотреть файл

Просмотреть файл

@ -0,0 +1,19 @@
---
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
groups: {}
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

Просмотреть файл

@ -0,0 +1,3 @@
description = Listing of contractors
username = pixiebob
username = serengeti

Просмотреть файл

@ -0,0 +1,2 @@
description = Listing of pre-hires
username = chartreux

Просмотреть файл

@ -0,0 +1,109 @@
---
nebelung:
status:
- employee
manager: ojosazules
pixiebob:
status:
- contractor
manager: balinese
balinese:
status:
- employee
manager: peterbald
serengeti:
status:
- contractor
manager: balinese
germanrex:
status:
- employee
manager: bengal
khaomanee:
status:
- employee
manager: ojosazules
napoleon:
status:
- employee
manager: napoleon
blackmanx:
status:
- employee
manager: mainecoon
chausie:
status:
- employee
manager: ojosazules
orientallonghair:
status:
- employee
manager: napoleon
korat:
status:
- employee
manager: balinese
oregonrex:
status:
- employee
manager: donskoy
donskoy:
status:
- employee
manager: foldex
russianblue:
status:
- employee
manager: mainecoon
ragamuffin:
status:
- employee
manager: mainecoon
foldex:
status:
- employee
manager: orientallonghair
cheetoh:
status:
- employee
manager: ojosazules
peterbald:
status:
- employee
manager: orientallonghair
mainecoon:
status:
- employee
manager: balinese
cyprus:
status:
- employee
manager: ojosazules
desertlynx:
status:
- employee
manager: bengal
minskin:
status:
- employee
manager: balinese
chartreux:
status:
- pre-hire
manager: bengal
ojosazules:
status:
- employee
manager: donskoy
bobtail:
status:
- employee
manager: bengal
bengal:
status:
- employee
manager: balinese
abyssinian:
status:
- former
manager: balinese

Просмотреть файл

@ -0,0 +1,32 @@
<%-
require "/data/entitlements/lib/entitlements/backend/github_org"
require "/data/entitlements/lib/entitlements/backend/github_team"
require "/data/entitlements/lib/entitlements/service/github"
-%>
---
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
groups:
github-org:
addr: https://github.fake
base: ou=meowsister-org,ou=GitHub,dc=github,dc=fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_org
features:
- invite
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

@ -0,0 +1,8 @@
username = blackmanx
username = mainecoon
# korat is invited
username = korat
# cyprus moves in from the member role
username = cyprus

Просмотреть файл

@ -0,0 +1,18 @@
username = ojosazules
username = chausie
username = khaomanee
# cheetoh returns from lockout and will be re-invited
username = cheetoh
# russianblue / donskoy are not known to the organization and will be invited
username = rUssianblue
username = DONSKoy
# peterbald moves in from the admin role
username = peterbald
# Previous members to simulate removal
#username = ragamuffin
#username = minskin
#username = nebelung

Просмотреть файл

@ -0,0 +1,31 @@
<%-
require "/data/entitlements/lib/entitlements/backend/github_org"
require "/data/entitlements/lib/entitlements/backend/github_team"
require "/data/entitlements/lib/entitlements/service/github"
-%>
---
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
groups:
github-org:
addr: https://github.fake
base: ou=meowsister-org,ou=GitHub,dc=github,dc=fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_org
features: []
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

@ -0,0 +1,8 @@
username = blackmanx
username = mainecoon
# korat is invited
username = korat
# cyprus moves in from the member role
username = cyprus

Просмотреть файл

@ -0,0 +1,18 @@
username = ojosazules
username = chausie
username = khaomanee
# cheetoh returns from lockout and will be re-invited
username = cheetoh
# russianblue / donskoy are not known to the organization and will be invited
username = rUssianblue
username = DONSKoy
# peterbald moves in from the admin role
username = peterbald
# Previous members to simulate removal
#username = ragamuffin
#username = minskin
#username = nebelung

Просмотреть файл

@ -0,0 +1,32 @@
<%-
require "/data/entitlements/lib/entitlements/backend/github_org"
require "/data/entitlements/lib/entitlements/backend/github_team"
require "/data/entitlements/lib/entitlements/service/github"
-%>
---
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
groups:
github-org:
addr: https://github.fake
base: ou=meowsister-org,ou=GitHub,dc=github,dc=fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_org
features:
- remove
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

@ -0,0 +1,8 @@
username = blackmanx
username = mainecoon
# korat is invited
username = korat
# cyprus moves in from the member role
username = cyprus

Просмотреть файл

@ -0,0 +1,18 @@
username = ojosazules
username = chausie
username = khaomanee
# cheetoh returns from lockout and will be re-invited
username = cheetoh
# russianblue / donskoy are not known to the organization and will be invited
username = rUssianblue
username = DONSKoy
# peterbald moves in from the admin role
username = peterbald
# Previous members to simulate removal
#username = ragamuffin
#username = minskin
#username = nebelung

Просмотреть файл

@ -0,0 +1,35 @@
<%-
require "/data/entitlements/lib/entitlements/backend/github_org"
require "/data/entitlements/lib/entitlements/backend/github_team"
require "/data/entitlements/lib/entitlements/service/github"
-%>
---
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
groups:
github-org:
addr: https://github.fake
base: ou=meowsister-org,ou=GitHub,dc=github,dc=fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_org
ignore:
- korat
- monalisa
- peterbald
- minskin
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

@ -0,0 +1,8 @@
username = blackmanx
username = mainecoon
# korat is invited
username = korat
# cyprus moves in from the member role
username = cyprus

Просмотреть файл

@ -0,0 +1,18 @@
username = ojosazules
username = chausie
username = khaomanee
# cheetoh returns from lockout and will be re-invited
username = cheetoh
# russianblue / donskoy are not known to the organization and will be invited
username = rUssianblue
username = DONSKoy
# peterbald moves in from the admin role
username = peterbald
# Previous members to simulate removal
#username = ragamuffin
#username = minskin
#username = nebelung

Просмотреть файл

@ -0,0 +1,36 @@
<%-
require "/data/entitlements/lib/entitlements/backend/github_org"
require "/data/entitlements/lib/entitlements/backend/github_team"
require "/data/entitlements/lib/entitlements/service/github"
-%>
---
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
groups:
github:
addr: https://github.fake
base: ou=meowsister,ou=GitHub,dc=github,dc=fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_team
github-org:
addr: https://github.fake
base: ou=meowsister-org,ou=GitHub,dc=github,dc=fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_org
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

@ -0,0 +1,12 @@
# these are existing admins
username = blackmanx
username = mainecoon
# ragamuffin was previously a member
username = ragamuffin
# chausie is pending already
username = chausie
# korat is not invited yet and needs to be invited now
username = korat

Просмотреть файл

@ -0,0 +1,12 @@
# Already existing members
username = khaomanee
# Pending members
username = ojosazules
username = cyprus
# Formerly an admin
username = peterbald
# existing person who accepted an invite but was not in this role
username = minskin

Просмотреть файл

@ -0,0 +1,32 @@
# existing admins
username = blackmanx
# former member, now admin
username = ragamuffin
# pending admins
username = chausie
# admin that is being invited
username = korat
# existing members
username = khaomanee
# pending members
username = ojosazules
username = cyprus
# former admin, now member
username = peterbald
# not member, not admin, on team
username = BALINESE
username = desertlynx
username = DwelF
# offboarded from organization
# username = nebelung
# offboarded from organization during this run
username = napoleon

Просмотреть файл

@ -0,0 +1,96 @@
<%-
require "/data/entitlements/lib/entitlements/backend/github_org"
require "/data/entitlements/lib/entitlements/backend/github_team"
require "/data/entitlements/lib/entitlements/service/github"
-%>
---
backends:
dockerized-ldap:
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
type: ldap
github-dot-fake-org-meowsister:
addr: https://github.fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_org
github-dot-fake-meowsister:
addr: https://github.fake
org: meowsister
token: meowmeowmeowmeowmeow
type: github_team
configuration_path: ./ldap-config
extras:
ldap_group: {}
orgchart:
manager_map_file: ../common/manager-map.yaml
filters:
contractors:
class: Entitlements::Data::Groups::Calculated::Filters::MemberOfGroup
config:
group: internal/contractors
pre-hires:
class: Entitlements::Data::Groups::Calculated::Filters::MemberOfGroup
config:
group: internal/pre-hires
groups:
dummy-ou:
type: dummy
dummy-ou-renamed:
type: dummy
dir: dummy-ou-special
entitlements:
backend: dockerized-ldap
base: ou=Entitlements,ou=Groups,dc=kittens,dc=net
entitlements/foo-bar-app:
backend: dockerized-ldap
base: ou=foo-bar-app,ou=Entitlements,ou=Groups,dc=kittens,dc=net
create_if_missing: true
entitlements/groupofnames:
backend: dockerized-ldap
base: ou=GroupOfNames,ou=Entitlements,ou=Groups,dc=kittens,dc=net
plugin:
file: group_of_names.rb
class: GroupOfNames
create_if_missing: true
entitlements/mirror:
backend: dockerized-ldap
base: ou=Mirror,ou=Entitlements,ou=Groups,dc=kittens,dc=net
mirror: entitlements/groupofnames
create_if_missing: true
plugin:
file: posix_group.rb
class: PosixGroup
github:
backend: github-dot-fake-meowsister
base: ou=meowsister,ou=GitHub,dc=github,dc=fake
github-org:
backend: github-dot-fake-org-meowsister
base: ou=meowsister-org,ou=GitHub,dc=github,dc=fake
internal:
type: dummy
dir: ../../common/internal
memberof:
memberof_attribute: shellentitlements
backend: dockerized-ldap
type: member_of
ou:
- entitlements/groupofnames
pizza_teams:
backend: dockerized-ldap
base: ou=Pizza_Teams,ou=Groups,dc=kittens,dc=net
people:
ldap:
type: ldap
config:
base: ou=People,dc=kittens,dc=net
ldap_binddn: <%= ENV["LDAP_BINDDN"] %>
ldap_bindpw: <%= ENV["LDAP_BINDPW"] %>
ldap_uri: <%= ENV["LDAP_URI"] %>
person_dn_format: uid=%KEY%,ou=People,dc=kittens,dc=net
additional_attributes:
- shellentitlements
people_data_source: ldap

Просмотреть файл

@ -0,0 +1 @@
username = maiNecOON

Просмотреть файл

@ -0,0 +1 @@
username = RagaMuffin

Просмотреть файл

@ -0,0 +1 @@
username = ojosazules

Просмотреть файл

@ -0,0 +1,10 @@
---
description: Admin access to primary AWS account
rules:
or:
- entitlements_group: pizza_teams/grumpy-cat
- group: pizza_teams/colonel-meow
expiration: "2050-01-01"
- group: pizza_teams/keyboard-cat
expiration: "2001-01-01"
expiration: "2043-01-01"

Просмотреть файл

@ -0,0 +1,3 @@
description = Group for testing empty membership
username = RAGAMUFFIn
entitlements_group = pizza_teams/empty-but-ok

Просмотреть файл

@ -0,0 +1,4 @@
description = Group for testing empty membership
username = RAGAMUFFIn
username = BlackManx
group != pizza_teams/empty-but-ok

Просмотреть файл

@ -0,0 +1,2 @@
description = Group for testing empty membership
group = pizza_teams/empty-but-ok

Просмотреть файл

@ -0,0 +1,6 @@
description: Group for testing entitlement expiration
expiration: 2043-01-01
rules:
or:
- username: blackmanx
- username: nebelung

Просмотреть файл

@ -0,0 +1,4 @@
description = Group for testing entitlement expiration
username = blackmanx; expiration = 2001-01-01
username = nebelung; expiration = 2001-01-01
expiration = 2001-01-01

Просмотреть файл

@ -0,0 +1,4 @@
description = Group for testing entitlement expiration
username = blackmanx
username = nebelung
expiration = 2001-01-01

Просмотреть файл

@ -0,0 +1,5 @@
description = This is in a sub-ou
username = RUssianblue; expiration = 2050-01-01
username = BlackManx; expiration = 2050-01-01
username = mainecoon; expiration = 2001-01-01
expiration = 2043-01-01

Просмотреть файл

@ -0,0 +1,4 @@
description = This is in a sub-ou
username = russianblue
username = BLACKMANX
metadata_gid_number = 12345

Просмотреть файл

@ -0,0 +1,3 @@
description = This is in a sub-ou
group = dummy-ou/ragamuffin
metadata_gid_number = 23456

Просмотреть файл

@ -0,0 +1,3 @@
username = blackmanx
username = ragamuffin
username = mainecoon

Просмотреть файл

@ -0,0 +1,3 @@
entitlements_group = github/*
entitlements_group != github-org/admin
username = donskoy

Просмотреть файл

@ -0,0 +1,4 @@
description = Fake team on fake GitHub
metadata_parent_team_name = employees
username = blackmanx
username = donskoy

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше