entitlements-github-plugin
This commit is contained in:
Коммит
abae8ccf9b
|
@ -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"
|
|
@ -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
|
|
@ -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/
|
|
@ -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
|
|
@ -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
|
|
@ -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/**/*'
|
|
@ -0,0 +1 @@
|
|||
2.7.5
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gemspec
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
0.0.1
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
||||
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
|
||||
-----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
|
||||
|
||||
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
|
||||
-----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
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче