terraorg 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +124 -0
- data/bin/terraorg +100 -0
- data/lib/terraorg/model/org.rb +231 -0
- data/lib/terraorg/model/people.rb +44 -0
- data/lib/terraorg/model/person.rb +63 -0
- data/lib/terraorg/model/platoon.rb +98 -0
- data/lib/terraorg/model/platoons.rb +43 -0
- data/lib/terraorg/model/squad.rb +129 -0
- data/lib/terraorg/model/squads.rb +28 -0
- data/lib/terraorg/model/util.rb +49 -0
- data/lib/terraorg/version.rb +3 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8ab3d94ce6dcc408a54773d2922f6939cb3ecb2c1f712c15e6bf38d5b4991262
|
4
|
+
data.tar.gz: 697c67ca943cc95ea5aad8a569968f204d0c6ed54e5747b4e99a55090c8faa6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eea771dfd0e78861b6bc4d40bd6a55a978ee05a7a2739948bfa1ce7890dd26bd59a534395973b28fa9caff76c06d143cf482bb9d22278f89fb4ba9b92cb06172
|
7
|
+
data.tar.gz: a2199d855c507de8ed37a22a62a20d65a36796637ff7ed55003fd205380655ffb2eebcbbc51720eec747ea46252705a00fa9a528581cc4f5756dab5d27bb1f2c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020- Joshua Kwan
|
2
|
+
Copyright 2020 LiveRamp Holdings, Inc.
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
6
|
+
the Software without restriction, including without limitation the rights to
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
9
|
+
so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
[![Build Status](https://travis-ci.com/joshk0/terraorg.svg?token=KqPPGKhNmsFv3uhyxvvf&branch=master)](https://travis-ci.com/joshk0/terraorg)
|
2
|
+
|
3
|
+
# terraorg
|
4
|
+
|
5
|
+
This tool helps define organizational structure using flat JSON files as a
|
6
|
+
source of truth for who is a member of an organization. Traditionally, there
|
7
|
+
are multiple sources of truth that need to be separately maintained. These JSON
|
8
|
+
files are processed into Terraform files which are meant to create nested
|
9
|
+
groups representing the organization that are usable in Okta and G Suite.
|
10
|
+
|
11
|
+
Additionally, the tool facilitates using this source of truth to generate
|
12
|
+
Markdown documentation about the org as well.
|
13
|
+
|
14
|
+
Based on the org that this tool was originally designed for, orgs are expected
|
15
|
+
to have three levels:
|
16
|
+
|
17
|
+
* *squads*: the base unit of team-dom, containing people, who may be in
|
18
|
+
different geographical regions.
|
19
|
+
* *platoons*: a unit which contains squads and exceptional people who are
|
20
|
+
members of the platoon, but not part of any squad
|
21
|
+
* *org*: The whole organization, including its manager, any exceptional squads
|
22
|
+
not organized into a platoon, and any exceptional people not part of any
|
23
|
+
squad at all but still part of the org.
|
24
|
+
|
25
|
+
The tool generates groups for each granular unit of organization in Okta and G
|
26
|
+
Suite in Terraform. With patching, it could be possible for more organizational
|
27
|
+
systems to be supported.
|
28
|
+
|
29
|
+
## How it works
|
30
|
+
|
31
|
+
Firstly, take your entire existing organization and define it using the
|
32
|
+
constructs of squads, platoons and organization. Groups will be generated;
|
33
|
+
*use these groups to assign access to resources* rather than individually
|
34
|
+
assigning access.
|
35
|
+
|
36
|
+
Whenever a person joins your organization, it should be the responsibility of
|
37
|
+
managers in the organization, or their delegates, to update the organizational
|
38
|
+
JSON files. If done correctly, updating the file, re-running terraorg to
|
39
|
+
generate source code, and applying the resulting source code would instantly
|
40
|
+
grant the person access to required services. Similarly, if a person leaves,
|
41
|
+
remove that person from the file and run the tool again.
|
42
|
+
|
43
|
+
## Model definitions
|
44
|
+
|
45
|
+
See `examples/` directory for examples of `squads.json`, `platoons.json` and
|
46
|
+
`org.json`.
|
47
|
+
|
48
|
+
## Running the tool
|
49
|
+
|
50
|
+
### Environment variables
|
51
|
+
|
52
|
+
The tool expects the following environment variables to be defined and fails if
|
53
|
+
they are missing:
|
54
|
+
|
55
|
+
* `OKTA_API_TOKEN`: An API token with read/write admin access to your Okta
|
56
|
+
organization.
|
57
|
+
* `OKTA_ORG_NAME`: Name of the Okta organization.
|
58
|
+
* `OKTA_BASE_URL`: either `okta.com` or `oktapreview.com` depending on your
|
59
|
+
Okta configuration.
|
60
|
+
* `GSUITE_DOMAIN`: The G Suite domain suffix e.g. `yourcompany.com` which
|
61
|
+
is used by Google Groups.
|
62
|
+
|
63
|
+
The following environment variables are optional; if not specified, ensure
|
64
|
+
that your runtime setup matches the defaults.
|
65
|
+
|
66
|
+
* `TERRAORG_SQUADS`: defaulting to `squads.json`, location of the squads
|
67
|
+
definition.
|
68
|
+
* `TERRAORG_PLATOONS`: defaulting to `platoons.json`, location of the platoons
|
69
|
+
definition.
|
70
|
+
* `TERRAORG_ROOT`: defaulting to `org.json`, location of the root
|
71
|
+
organizational unit.
|
72
|
+
* `TERRAORG_OKTA_CACHE`: defaulting to `okta_cache.json`, location of the Okta
|
73
|
+
name, email and user id lookup cache. Delete this file to force a refresh.
|
74
|
+
* `SLACK_DOMAIN`: defaulting to `yourdomain.slack.com`, prefix of your Slack
|
75
|
+
domain. This is only required for doc generation and not for Terraform.
|
76
|
+
|
77
|
+
### Subcommands
|
78
|
+
|
79
|
+
When running `terraorg`, pass a second argument containing a subcommand.
|
80
|
+
Valid subcommands:
|
81
|
+
|
82
|
+
* `fmt`: Idiomatic formatting for all of your org files which will be
|
83
|
+
written back to the JSON files. Similar behavior to `terraform fmt`.
|
84
|
+
`auto.XXXX.tf`.
|
85
|
+
* `generate-platoons-md`: Generate Markdown documentation for Squads.
|
86
|
+
* `generate-squads-md`: Generate Markdown documentation for Squads.
|
87
|
+
* `generate-tf`: Generate Terraform files. Output files will be of the form
|
88
|
+
* `validate`: Ensure the input JSON files represent a valid, self-consistent
|
89
|
+
organization.
|
90
|
+
|
91
|
+
## Combining with terraform
|
92
|
+
|
93
|
+
See `examples/` directory for how to set up your Terraform workspace for plans
|
94
|
+
and applies of the files generated by this tool. At a minimum, you will need to
|
95
|
+
furnish [articulate/terraform-provider-okta] and
|
96
|
+
[DeviaVir/terraform-provider-gsuite], and enable those as providers.
|
97
|
+
|
98
|
+
Please consult the respective Terraform modules' home pages for more
|
99
|
+
information on how to configure the providers.
|
100
|
+
|
101
|
+
[articulate/terraform-provider-okta]: https://github.com/articulate/terraform-provider-okta
|
102
|
+
[DeviaVir/terraform-provider-gsuite]: https://github.com/DeviaVir/terraform-provider-gsuite
|
103
|
+
|
104
|
+
## Suggested process
|
105
|
+
|
106
|
+
At [LiveRamp], a pull request based workflow leveraging [Atlantis] is used to
|
107
|
+
interact with terraorg. The repository containing LiveRamp's engineering
|
108
|
+
organizational structure is locked down and requires approval from
|
109
|
+
organizational leadership for any change.
|
110
|
+
|
111
|
+
Once approved, Atlantis leverages its API access to Okta and G Suite in order
|
112
|
+
to effect the group changes.
|
113
|
+
|
114
|
+
Similar process can be defined using CI tools such as Jenkins and Circle CI.
|
115
|
+
Implementation of such process is left as an exercise to the reader.
|
116
|
+
|
117
|
+
[LiveRamp]: https://github.com/LiveRamp/
|
118
|
+
[Atlantis]: https://www.runatlantis.io/
|
119
|
+
|
120
|
+
## Future
|
121
|
+
|
122
|
+
* terraorg could generate and push GitHub Teams automatically that line up
|
123
|
+
with existing squad structure.
|
124
|
+
* Terraform and Markdown generation could be factored out to ERB.
|
data/bin/terraorg
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'neatjson'
|
5
|
+
require 'oktakit'
|
6
|
+
|
7
|
+
require 'terraorg/model/org'
|
8
|
+
require 'terraorg/model/people'
|
9
|
+
require 'terraorg/model/platoons'
|
10
|
+
require 'terraorg/model/squads'
|
11
|
+
|
12
|
+
ACTIONS = [
|
13
|
+
'fmt',
|
14
|
+
'generate-platoons-md',
|
15
|
+
'generate-squads-md',
|
16
|
+
'generate-tf',
|
17
|
+
'validate'
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
SQUADS_FILE = ENV.fetch('TERRAORG_SQUADS', 'squads.json')
|
21
|
+
PLATOONS_FILE = ENV.fetch('TERRAORG_PLATOONS', 'platoons.json')
|
22
|
+
ORG_FILE = ENV.fetch('TERRAORG_ROOT', 'org.json')
|
23
|
+
CACHE_FILE = ENV.fetch('TERRAORG_OKTA_CACHE', 'okta_cache.json')
|
24
|
+
GSUITE_DOMAIN = ENV.fetch('GSUITE_DOMAIN')
|
25
|
+
SLACK_DOMAIN = ENV.fetch('SLACK_DOMAIN', 'yourcompany.slack.com')
|
26
|
+
OKTA_ORG_NAME = ENV.fetch('OKTA_ORG_NAME')
|
27
|
+
OKTA_API_TOKEN = ENV.fetch('OKTA_API_TOKEN')
|
28
|
+
OKTA_BASE_URL = ENV.fetch('OKTA_BASE_URL', 'okta.com')
|
29
|
+
|
30
|
+
action = ARGV[0]
|
31
|
+
raise "Invalid action: '#{action}' (valid: #{ACTIONS})" unless ACTIONS.member?(action)
|
32
|
+
|
33
|
+
### Actions that can run without Okta
|
34
|
+
case action
|
35
|
+
when 'fmt'
|
36
|
+
# This People object is created with neither an okta cache nor a okta client.
|
37
|
+
# It accepts any user for the purposes of format validation.
|
38
|
+
people = People.new
|
39
|
+
squads_data = File.read(SQUADS_FILE)
|
40
|
+
squads = Squads.new(JSON.parse(squads_data), people, GSUITE_DOMAIN, SLACK_DOMAIN)
|
41
|
+
platoons_data = File.read(PLATOONS_FILE)
|
42
|
+
platoons = Platoons.new(JSON.parse(platoons_data), squads, people, GSUITE_DOMAIN)
|
43
|
+
org_data = File.read(ORG_FILE)
|
44
|
+
org = Org.new(JSON.parse(org_data), platoons, squads, people, GSUITE_DOMAIN)
|
45
|
+
|
46
|
+
options = {wrap: true, sort: true, after_colon: 1, after_comma: 1}
|
47
|
+
new_squads_data = JSON.neat_generate(squads.to_h, **options) + "\n"
|
48
|
+
new_platoons_data = JSON.neat_generate(platoons.to_h, **options) + "\n"
|
49
|
+
new_org_data = JSON.neat_generate(org.to_h, **options) + "\n"
|
50
|
+
|
51
|
+
if new_squads_data != squads_data
|
52
|
+
puts SQUADS_FILE
|
53
|
+
File.write(SQUADS_FILE, new_squads_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
if new_platoons_data != platoons_data
|
57
|
+
puts PLATOONS_FILE
|
58
|
+
File.write(PLATOONS_FILE, new_platoons_data)
|
59
|
+
end
|
60
|
+
|
61
|
+
if new_org_data != org_data
|
62
|
+
puts ORG_FILE
|
63
|
+
File.write(ORG_FILE, new_org_data)
|
64
|
+
end
|
65
|
+
|
66
|
+
exit
|
67
|
+
end
|
68
|
+
|
69
|
+
### Actions that require Okta credentials
|
70
|
+
if OKTA_BASE_URL == 'oktapreview.com'
|
71
|
+
okta = Oktakit.new(token: OKTA_API_TOKEN, api_endpoint: "https://#{OKTA_ORG_NAME}.#{OKTA_BASE_URL}/api/v1")
|
72
|
+
else
|
73
|
+
okta = Oktakit.new(token: OKTA_API_TOKEN, organization: OKTA_ORG_NAME)
|
74
|
+
end
|
75
|
+
|
76
|
+
people = People.new(okta: okta, cache_file: CACHE_FILE)
|
77
|
+
squads_data = File.read(SQUADS_FILE)
|
78
|
+
squads = Squads.new(JSON.parse(squads_data), people, GSUITE_DOMAIN, SLACK_DOMAIN)
|
79
|
+
platoons_data = File.read(PLATOONS_FILE)
|
80
|
+
platoons = Platoons.new(JSON.parse(platoons_data), squads, people, GSUITE_DOMAIN)
|
81
|
+
org_data = File.read(ORG_FILE)
|
82
|
+
org = Org.new(JSON.parse(org_data), platoons, squads, people, GSUITE_DOMAIN)
|
83
|
+
|
84
|
+
org.validate!
|
85
|
+
|
86
|
+
case action
|
87
|
+
when 'generate-squads-md'
|
88
|
+
# Generate CSV file mirroring the 'Engineering Squads' Squads sheet.
|
89
|
+
puts org.get_squads_md
|
90
|
+
when 'generate-platoons-md'
|
91
|
+
# Generate CSV file mirroring the 'Engineering Squads' Platoons sheet.
|
92
|
+
puts org.get_platoons_md
|
93
|
+
when 'generate-tf'
|
94
|
+
# Generate Terraform code to apply to Okta.
|
95
|
+
org.generate_tf
|
96
|
+
when 'validate'
|
97
|
+
# Validation is a prerequisite, so just output success. It would have
|
98
|
+
# failed earlier otherwise
|
99
|
+
puts 'Organization is valid!'
|
100
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'terraorg/model/util'
|
2
|
+
|
3
|
+
class Org
|
4
|
+
MAX_SQUADS_PER_PERSON = 2
|
5
|
+
SCHEMA_VERSION = 'v1'.freeze
|
6
|
+
|
7
|
+
def initialize(parsed_data, platoons, squads, people, gsuite_domain)
|
8
|
+
@people = people
|
9
|
+
@id = parsed_data.fetch('id')
|
10
|
+
@name = parsed_data.fetch('name')
|
11
|
+
@metadata = parsed_data.fetch('metadata', {})
|
12
|
+
@manager = @people.get_or_create!(parsed_data.fetch('manager'))
|
13
|
+
@manager_location = parsed_data.fetch('manager_location')
|
14
|
+
# @member_exception_people is a Hash of Squad::Teams, like the teams attribute in a Squad.
|
15
|
+
@member_exception_people = Hash[parsed_data.fetch('exception_people').map { |p|
|
16
|
+
[p.fetch('location'), Squad::Team.new(p, @people)]
|
17
|
+
}]
|
18
|
+
@member_exception_squad_names = []
|
19
|
+
@member_exception_squads = parsed_data.fetch('exception_squads').map do |s|
|
20
|
+
@member_exception_squad_names.push(s)
|
21
|
+
squads.lookup!(s)
|
22
|
+
end
|
23
|
+
@member_platoon_names = []
|
24
|
+
@member_platoons = parsed_data.fetch('platoons').map do |p|
|
25
|
+
@member_platoon_names.push(p)
|
26
|
+
platoons.lookup!(p)
|
27
|
+
end
|
28
|
+
|
29
|
+
# used to generate a group
|
30
|
+
@gsuite_domain = gsuite_domain
|
31
|
+
|
32
|
+
# used for validate!
|
33
|
+
@platoons = platoons
|
34
|
+
@squads = squads
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate!
|
38
|
+
# Do not allow the JSON files to contain any people who have left.
|
39
|
+
raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
|
40
|
+
|
41
|
+
# Do not allow the org to be totally empty.
|
42
|
+
raise 'Org has no platoons or exception squads' if @member_platoons.size + @member_exception_squads.size == 0
|
43
|
+
|
44
|
+
# Require all platoons to be part of the org.
|
45
|
+
platoon_diff = Set.new(@platoons.all_names) - Set.new(@member_platoon_names)
|
46
|
+
unless platoon_diff.empty?
|
47
|
+
raise "Platoons are not used in the org: #{platoon_diff.to_a.sort}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Require all squads to be used in the org.
|
51
|
+
squad_diff = Set.new(@squads.all_names) - Set.new(@platoons.all_squad_names) - Set.new(@member_exception_squad_names)
|
52
|
+
unless squad_diff.empty?
|
53
|
+
raise "Squad(s) are not used in the org: #{squad_diff.to_a.sort}"
|
54
|
+
end
|
55
|
+
|
56
|
+
all_squads = (@member_platoons.map(&:member_squads) + @member_exception_squads).flatten
|
57
|
+
seen_squads = {}
|
58
|
+
|
59
|
+
# Validate that a squad is not part of more than one platoon
|
60
|
+
all_squads.map(&:id).each do |id|
|
61
|
+
seen_squads[id] = seen_squads.fetch(id, 0) + 1
|
62
|
+
end
|
63
|
+
more_than_one_platoon = seen_squads.select do |squad, count|
|
64
|
+
count > 1
|
65
|
+
end
|
66
|
+
if !more_than_one_platoon.empty?
|
67
|
+
raise "Squads are part of more than one platoon: #{more_than_one_platoon}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Validate that a squad member belongs to at most two squads in the entire org
|
71
|
+
squad_count = {}
|
72
|
+
all_squads.map(&:teams).flatten.map(&:values).flatten.map(&:members).flatten.each do |member|
|
73
|
+
squad_count[member.id] = squad_count.fetch(member.id, 0) + 1
|
74
|
+
end
|
75
|
+
more_than_max_squads = squad_count.select do |member, count|
|
76
|
+
count > MAX_SQUADS_PER_PERSON
|
77
|
+
end
|
78
|
+
if !more_than_max_squads.empty?
|
79
|
+
# TODO(joshk): Enforce after consulting with Sean
|
80
|
+
$stderr.puts "WARNING: Members are part of more than #{MAX_SQUADS_PER_PERSON} squads: #{more_than_max_squads}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Validate that a squad member is not also an org exception
|
84
|
+
exceptions = Set.new(@member_exception_people.map { |_, t| t.members }.flatten.map(&:id))
|
85
|
+
exception_and_squad_member = squad_count.keys.select do |member|
|
86
|
+
exceptions.member? member
|
87
|
+
end
|
88
|
+
if !exception_and_squad_member.empty?
|
89
|
+
raise "Exception members are also squad members: #{exception_and_squad_member}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def members
|
94
|
+
Set.new([@manager] + (@member_platoons + @member_exception_squads).map(&:members).flatten + @member_exception_people.map { |_, t| t.members }.flatten).to_a
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_acl_groups(attr: :id)
|
98
|
+
# Return a LIST_NAME => [MEMBER1, MEMBER2...] hash of ACL groups
|
99
|
+
{ unique_name => members.map(&attr).sort }.
|
100
|
+
merge(get_platoon_acl_groups(attr: attr)).
|
101
|
+
merge(get_exception_squad_acl_groups(attr: attr))
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_platoon_acl_groups(attr: :id)
|
105
|
+
@member_platoons.map { |p| p.get_acl_groups(@id, attr: attr) }.reduce({}, :merge)
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_exception_squad_acl_groups(attr: :id)
|
109
|
+
@member_exception_squads.map { |p| p.get_acl_groups(@id) }.map(&attr).reduce({}, :merge)
|
110
|
+
end
|
111
|
+
|
112
|
+
def unique_name
|
113
|
+
"#{@id}-all"
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_platoons_md
|
117
|
+
# 90 degree rotated version of the Platoons page of the legacy Engineering Squads sheet
|
118
|
+
# Format:
|
119
|
+
# Platoon,Total,Members
|
120
|
+
# [ORG_HUMAN_NAME],[COUNT]
|
121
|
+
# [PLAT1_HUMAN_NAME],[PLAT1_COUNT],[PLAT1_MEMBER1],[PLAT1_MEMBER2],...
|
122
|
+
# [PLAT2_HUMAN_NAME],[PLAT2_COUNT],[PLAT2_MEMBER1],[PLAT2_MEMBER2],...
|
123
|
+
md_lines = [
|
124
|
+
'# Engineering Platoons List',
|
125
|
+
'',
|
126
|
+
'|Platoon|Manager|Total|Members|',
|
127
|
+
'|---|---|---|---|',
|
128
|
+
"|_#{@name} Total_|#{@manager.name}|#{members.size}|",
|
129
|
+
]
|
130
|
+
md_lines += @member_platoons.map(&:get_platoons_psv_row)
|
131
|
+
|
132
|
+
raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
|
133
|
+
md_lines.join("\n")
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_squads_md
|
137
|
+
# Format:
|
138
|
+
# platoon name, squad name, PM, email list, SME, slack, # people, squad manager, eng product owner, members
|
139
|
+
md_lines = [
|
140
|
+
'# Engineering Squads List',
|
141
|
+
'',
|
142
|
+
'|Platoon|Squad|PM|Mailing list|TS SME|Slack|# Engineers|Squad Manager|Eng Product Owner|Members|',
|
143
|
+
'|---|---|---|---|---|---|---|---|---|---|',
|
144
|
+
]
|
145
|
+
md_lines += @member_platoons.map { |s| s.get_squads_psv_rows(@id) }
|
146
|
+
md_lines += @member_exception_squads.map { |s| s.to_md('_No Platoon_', @id) }
|
147
|
+
md_lines.push "|_No Platoon_|_No Squad_|||||#{@member_exception_people.size}|||#{@member_exception_people.values.map(&:to_md).join(' / ')}|"
|
148
|
+
|
149
|
+
raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
|
150
|
+
md_lines.join("\n")
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_tf
|
154
|
+
tf = @member_platoons.map { |p| p.generate_tf(@id) }.join("\n")
|
155
|
+
File.write('auto.platoons.tf', tf)
|
156
|
+
|
157
|
+
tf = @member_exception_squads.map { |s| s.generate_tf(@id) }.join("\n")
|
158
|
+
File.write('auto.exception_squads.tf', tf)
|
159
|
+
|
160
|
+
# Roll all platoons and exception squads into the org.
|
161
|
+
roll_up_to_org = \
|
162
|
+
@member_exception_squads.map { |s| s.unique_name(@id, nil) } + \
|
163
|
+
@member_platoons.map { |p| p.unique_name(@id) }
|
164
|
+
|
165
|
+
# Generate the org, which contains:
|
166
|
+
# - exception people (added manually to group)
|
167
|
+
# - the org manager (added manually to group)
|
168
|
+
# - all the platoons (via rule)
|
169
|
+
# - all exception squads (via rule)
|
170
|
+
org_condition = roll_up_to_org.map {
|
171
|
+
|n| "\\\"${okta_group.#{n}.id}\\\""
|
172
|
+
}.join(',')
|
173
|
+
|
174
|
+
description = "#{@name} organization worldwide members (terraorg)"
|
175
|
+
tf = <<-EOF
|
176
|
+
# Okta for Organization: #{@name}
|
177
|
+
resource "okta_group" "#{unique_name}" {
|
178
|
+
name = "#{unique_name}"
|
179
|
+
description = "#{description}"
|
180
|
+
users = #{Util.persons_tf(members)}
|
181
|
+
}
|
182
|
+
|
183
|
+
#{Util.gsuite_group_tf(unique_name, @gsuite_domain, members, description)}
|
184
|
+
EOF
|
185
|
+
|
186
|
+
# Generate a special group for all org members grouped by country
|
187
|
+
all_squads = (@member_platoons.map(&:member_squads) + @member_exception_squads).flatten
|
188
|
+
all_locations = {}
|
189
|
+
(all_squads.map(&:teams).flatten + [@member_exception_people]).each do |team|
|
190
|
+
team.each do |location, subteam|
|
191
|
+
all_locations[location] = all_locations.fetch(location, Set.new).merge(subteam.members)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Manually add the manager to a specific location
|
196
|
+
all_locations[@manager_location] = all_locations.fetch(@manager_location, Set.new).add(@manager)
|
197
|
+
|
198
|
+
all_locations.each do |l, m|
|
199
|
+
name = "#{unique_name}-#{l.downcase}"
|
200
|
+
tf += <<-EOF
|
201
|
+
resource "okta_group" "#{name}" {
|
202
|
+
name = "#{name}"
|
203
|
+
description = "#{@name} organization members based in #{l} (terraorg)"
|
204
|
+
users = #{Util.persons_tf(m)}
|
205
|
+
}
|
206
|
+
EOF
|
207
|
+
end
|
208
|
+
|
209
|
+
File.write('auto.org.tf', tf)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Output a canonical (sorted, formatted) version of this organization.
|
213
|
+
# - Sort the platoon names lexically
|
214
|
+
# - Sort the exception squad ids lexically
|
215
|
+
# - Sort the exception people ids lexically
|
216
|
+
def to_h
|
217
|
+
obj = { 'version' => SCHEMA_VERSION, 'id' => @id, 'name' => @name, 'manager_location' => @manager_location, 'manager' => @manager.id }
|
218
|
+
obj['platoons'] = @platoons.all.map(&:id).sort
|
219
|
+
if @metadata
|
220
|
+
obj['metadata'] = @metadata
|
221
|
+
end
|
222
|
+
if @member_exception_people
|
223
|
+
obj['exception_people'] = @member_exception_people.values.sort_by(&:location).map(&:to_h)
|
224
|
+
end
|
225
|
+
if @member_exception_squads
|
226
|
+
obj['exception_squads'] = @member_exception_squads.map(&:id).sort
|
227
|
+
end
|
228
|
+
|
229
|
+
obj
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'terraorg/model/person'
|
2
|
+
|
3
|
+
class People
|
4
|
+
attr_accessor :inactive
|
5
|
+
|
6
|
+
def initialize(okta: nil, cache_file: nil)
|
7
|
+
@okta = okta
|
8
|
+
@people = {}
|
9
|
+
@inactive = []
|
10
|
+
@cache_file = cache_file
|
11
|
+
|
12
|
+
load_cache! if @cache_file
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_or_create!(id)
|
16
|
+
p = @people[id]
|
17
|
+
return p if p
|
18
|
+
|
19
|
+
p = Person.new(id, okta: @okta)
|
20
|
+
if p.active?
|
21
|
+
@people[id] = p
|
22
|
+
else
|
23
|
+
@people.delete(id)
|
24
|
+
@inactive.push p
|
25
|
+
end
|
26
|
+
save_cache! if @cache_file
|
27
|
+
return p
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_cache!
|
31
|
+
return unless File.exist?(@cache_file)
|
32
|
+
|
33
|
+
JSON.parse(File.read(@cache_file)).each do |id, cache|
|
34
|
+
@people[id] = Person.new(id, cached: cache)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_cache!
|
39
|
+
# Atomic write cache file
|
40
|
+
cf_new = "#{@cache_file}.new"
|
41
|
+
File.write(cf_new, @people.to_json)
|
42
|
+
File.rename(cf_new, @cache_file)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
class Person
|
4
|
+
ACTIVE_USER_STATUSES = ['ACTIVE', 'PROVISIONED'].freeze
|
5
|
+
|
6
|
+
attr_accessor :id, :name, :okta_id, :email, :status
|
7
|
+
|
8
|
+
def initialize(uid, okta: nil, cached: nil)
|
9
|
+
@id = uid
|
10
|
+
|
11
|
+
if cached
|
12
|
+
@name = cached.fetch('name')
|
13
|
+
@okta_id = cached.fetch('okta_id')
|
14
|
+
@email = cached.fetch('email')
|
15
|
+
@status = cached.fetch('status')
|
16
|
+
|
17
|
+
return
|
18
|
+
elsif !okta
|
19
|
+
# We could just be running in fmt mode, so lie about everything
|
20
|
+
@name = "real name of #{@id}"
|
21
|
+
@okta_id = "fake okta id for #{@id}"
|
22
|
+
@email = "#{@id}@my.domain"
|
23
|
+
@status = 'PROVISIONED'
|
24
|
+
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
# Retrieve from okta
|
29
|
+
tries = 1
|
30
|
+
total_tries = 5
|
31
|
+
|
32
|
+
begin
|
33
|
+
o = okta.get_user(uid)
|
34
|
+
rescue Faraday::ConnectionFailed => e
|
35
|
+
if tries <= total_tries
|
36
|
+
puts "looking up user #{uid}: #{e} (try #{tries}/#{total_tries})"
|
37
|
+
tries += 1
|
38
|
+
retry
|
39
|
+
end
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
|
43
|
+
if tries > 1
|
44
|
+
puts "looking up user #{uid}: success!"
|
45
|
+
end
|
46
|
+
|
47
|
+
# NOTE: allows users in states other than ACTIVE
|
48
|
+
# if you want to check that, do it outside of here
|
49
|
+
obj = o[0].to_hash
|
50
|
+
@name = obj.fetch(:profile).fetch(:displayName)
|
51
|
+
@okta_id = obj.fetch(:id)
|
52
|
+
@email = obj.fetch(:profile).fetch(:email)
|
53
|
+
@status = obj.fetch(:status)
|
54
|
+
end
|
55
|
+
|
56
|
+
def active?
|
57
|
+
ACTIVE_USER_STATUSES.member?(@status)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_json(options = nil)
|
61
|
+
{'id' => @id, 'name' => @name, 'okta_id' => @okta_id, 'email' => @email, 'status' => @status}.to_json
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'terraorg/model/util'
|
2
|
+
|
3
|
+
class Platoon
|
4
|
+
attr_accessor :id, :name, :member_exceptions, :member_squads
|
5
|
+
|
6
|
+
def initialize(parsed_data, squads, people, gsuite_domain)
|
7
|
+
@id = parsed_data.fetch('id')
|
8
|
+
@metadata = parsed_data.fetch('metadata', {})
|
9
|
+
@name = parsed_data.fetch('name')
|
10
|
+
@manager = people.get_or_create!(parsed_data.fetch('manager'))
|
11
|
+
@member_exceptions = parsed_data.fetch('exceptions', []).map do |n|
|
12
|
+
people.get_or_create!(n)
|
13
|
+
end
|
14
|
+
@member_squad_names = []
|
15
|
+
@member_squads = parsed_data.fetch('squads').map do |s|
|
16
|
+
@member_squad_names.push(s)
|
17
|
+
squads.lookup!(s)
|
18
|
+
end
|
19
|
+
@gsuite_domain = gsuite_domain
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
raise 'Platoon has no squads' if @member_squads.size == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def squad_names
|
27
|
+
@member_squad_names
|
28
|
+
end
|
29
|
+
|
30
|
+
def members
|
31
|
+
Set.new([@manager] + @member_squads.map(&:members).flatten + @member_exceptions).to_a
|
32
|
+
end
|
33
|
+
|
34
|
+
def unique_name(org_id)
|
35
|
+
"#{org_id}-platoon-#{@id}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_acl_groups(org_id, platoon: true)
|
39
|
+
if platoon
|
40
|
+
rv = { unique_name(org_id) => {'name' => "#{@name} platoon members worldwide", 'members' => members} }
|
41
|
+
else
|
42
|
+
rv = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
@member_squads.map { |s| s.get_acl_groups(org_id) }.reduce(rv, :merge)
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_platoons_psv_row
|
49
|
+
"|#{@name}|#{@manager.name}|#{members.size}|#{members.map(&:name).sort.join(', ')}|"
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_squads_psv_rows(org_id)
|
53
|
+
@member_squads.map { |s| s.to_md(@name, org_id) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_tf(org_id)
|
57
|
+
tf_id = unique_name(org_id)
|
58
|
+
|
59
|
+
# tf formatted, comma separated references to the group ids for the
|
60
|
+
# squads in this platoon
|
61
|
+
squads_condition = get_acl_groups(org_id, platoon: false).map {
|
62
|
+
|n, _| "\\\"${okta_group.#{n}.id}\\\""
|
63
|
+
}.join(',')
|
64
|
+
|
65
|
+
# tf containing the platoon declaration
|
66
|
+
description = "#{@name} platoon members (terraorg)"
|
67
|
+
rv = <<-EOF
|
68
|
+
# Platoon: #{@name}
|
69
|
+
# Squads: #{squad_names.join(', ')}
|
70
|
+
|
71
|
+
resource "okta_group" "#{tf_id}" {
|
72
|
+
name = "#{tf_id}"
|
73
|
+
description = "#{description}"
|
74
|
+
users = #{Util.persons_tf(members)}
|
75
|
+
}
|
76
|
+
|
77
|
+
#{Util.gsuite_group_tf(tf_id, @gsuite_domain, members, description)}
|
78
|
+
EOF
|
79
|
+
|
80
|
+
# tf containing squads and their members
|
81
|
+
rv += @member_squads.map { |s| s.generate_tf(org_id) }.join("\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Output a canonical (sorted, formatted) version of this object.
|
85
|
+
# - Sort the squad ids lexically
|
86
|
+
# - Sort the exceptions lexically
|
87
|
+
def to_h
|
88
|
+
obj = { 'id' => @id, 'name' => @name, 'manager' => @manager.id, 'squads' => @member_squads.map(&:id) }
|
89
|
+
unless @member_exceptions.empty?
|
90
|
+
obj['exceptions'] = @member_exceptions.map(&:id)
|
91
|
+
end
|
92
|
+
unless @metadata.empty?
|
93
|
+
obj['metadata'] = @metadata
|
94
|
+
end
|
95
|
+
|
96
|
+
obj
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'terraorg/model/platoon'
|
2
|
+
|
3
|
+
class Platoons
|
4
|
+
SCHEMA_VERSION = 'v1'.freeze
|
5
|
+
|
6
|
+
def initialize(parsed_data, squads, people, gsuite_domain)
|
7
|
+
version = parsed_data.fetch('version')
|
8
|
+
raise "Unsupported schema version: #{version}" if version != SCHEMA_VERSION
|
9
|
+
|
10
|
+
@platoons = {}
|
11
|
+
parsed_data.fetch('platoons').each do |platoon_raw|
|
12
|
+
p = Platoon.new(platoon_raw, squads, people, gsuite_domain)
|
13
|
+
@platoons[p.id] = p
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def lookup!(name)
|
18
|
+
@platoons.fetch(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
@platoons.values
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_squad_names
|
29
|
+
@platoons.values.map(&:squad_names).flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_names
|
33
|
+
@platoons.keys
|
34
|
+
end
|
35
|
+
|
36
|
+
def members
|
37
|
+
@platoons.map(&:members).flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_h
|
41
|
+
{ 'version' => SCHEMA_VERSION , 'platoons' => @platoons.values.sort_by(&:id).map(&:to_h) }
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'terraorg/model/people'
|
2
|
+
require 'terraorg/model/util'
|
3
|
+
|
4
|
+
class Squad
|
5
|
+
attr_accessor :id, :name, :metadata, :teams
|
6
|
+
|
7
|
+
class Team
|
8
|
+
attr_accessor :location, :members
|
9
|
+
|
10
|
+
def initialize(parsed_data, people)
|
11
|
+
@location = parsed_data.fetch('location')
|
12
|
+
@members = parsed_data.fetch('members', []).map do |n|
|
13
|
+
people.get_or_create!(n)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Output a canonical (sorted, formatted) version of this Team.
|
18
|
+
# - Sort the members in each team
|
19
|
+
def to_h
|
20
|
+
{ 'location' => @location, 'members' => @members.map(&:id).sort }
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_md
|
24
|
+
"**#{@location}**: #{@members.map(&:name).sort.join(', ')}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(id, parsed_data, people, gsuite_domain, slack_domain)
|
29
|
+
@gsuite_domain = gsuite_domain
|
30
|
+
@slack_domain = slack_domain
|
31
|
+
@id = id
|
32
|
+
@metadata = parsed_data.fetch('metadata', {})
|
33
|
+
@name = parsed_data.fetch('name')
|
34
|
+
@people = people
|
35
|
+
|
36
|
+
teams_arr = parsed_data.fetch('team', []).map do |t|
|
37
|
+
Team.new(t, people)
|
38
|
+
end
|
39
|
+
@teams = Hash[teams_arr.map { |t| [t.location, t] }]
|
40
|
+
end
|
41
|
+
|
42
|
+
def members(location: nil)
|
43
|
+
@teams.select { |l, t|
|
44
|
+
location == nil || l == location
|
45
|
+
}.map { |l, t|
|
46
|
+
t.members
|
47
|
+
}.flatten
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_acl_groups(org_id)
|
51
|
+
# each geographically located subteam
|
52
|
+
groups = Hash[@teams.map { |location, team|
|
53
|
+
[unique_name(org_id, location), {'name' => "#{@name} squad members based in #{location}", 'members' => team.members}]
|
54
|
+
}]
|
55
|
+
|
56
|
+
# combination of all subteams
|
57
|
+
groups[unique_name(org_id, nil)] = {'name' => "#{@name} squad worldwide members", 'members' => members}
|
58
|
+
|
59
|
+
groups
|
60
|
+
end
|
61
|
+
|
62
|
+
def unique_name(org_id, location)
|
63
|
+
if location
|
64
|
+
"#{org_id}-squad-#{@id}-#{location.downcase}"
|
65
|
+
else
|
66
|
+
"#{org_id}-squad-#{@id}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate!
|
71
|
+
raise 'Squad has no members' if members.size == 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_md(platoon_name, org_id)
|
75
|
+
pm = @metadata.fetch('pm', [])
|
76
|
+
pm = pm.map { |p| @people.get_or_create!(p).name }.join(', ')
|
77
|
+
|
78
|
+
sme = @metadata.fetch('sme', '')
|
79
|
+
if !sme.empty?
|
80
|
+
sme = @people.get_or_create!(sme).name
|
81
|
+
end
|
82
|
+
|
83
|
+
epo = @metadata.fetch('epo', '')
|
84
|
+
if !epo.empty?
|
85
|
+
epo = @people.get_or_create!(epo).name
|
86
|
+
end
|
87
|
+
|
88
|
+
manager = @metadata.fetch('manager', '')
|
89
|
+
if !manager.empty?
|
90
|
+
manager = @people.get_or_create!(manager).name
|
91
|
+
end
|
92
|
+
|
93
|
+
subteam_members = @teams.values.map(&:to_md).join(' / ')
|
94
|
+
email = "#{unique_name(org_id, nil)}@#{@gsuite_domain}"
|
95
|
+
slack = @metadata.fetch('slack', '')
|
96
|
+
if slack
|
97
|
+
slack = "[#{slack}](https://#{@slack_domain}/app_redirect?channel=#{slack.gsub(/^#/, '')})"
|
98
|
+
end
|
99
|
+
# platoon name, squad name, PM, email list, SME, slack, # people, squad manager, eng product owner, members
|
100
|
+
"|#{platoon_name}|#{@name}|#{pm}|[#{email}](#{email})|#{sme}|#{slack}|#{members.size}|#{manager}|#{epo}|#{subteam_members}|"
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_tf(org_id)
|
104
|
+
groups = get_acl_groups(org_id)
|
105
|
+
|
106
|
+
groups.map { |id, group|
|
107
|
+
description = "#{group.fetch('name')} (terraorg)"
|
108
|
+
<<-EOF
|
109
|
+
resource "okta_group" "#{id}" {
|
110
|
+
name = "#{id}"
|
111
|
+
description = "#{description}"
|
112
|
+
users = #{Util.persons_tf(group.fetch('members'))}
|
113
|
+
}
|
114
|
+
|
115
|
+
#{Util.gsuite_group_tf(id, @gsuite_domain, group.fetch('members'), description)}
|
116
|
+
EOF
|
117
|
+
}.join("\n\n")
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_h
|
121
|
+
# Output a canonical (sorted, formatted) version of this Squad.
|
122
|
+
# - Subteams are sorted by location lexically
|
123
|
+
obj = { 'id' => @id, 'name' => @name }
|
124
|
+
obj['team'] = @teams.values.sort_by { |t| t.location }.map(&:to_h)
|
125
|
+
obj['metadata'] = @metadata if @metadata
|
126
|
+
|
127
|
+
obj
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'terraorg/model/squad'
|
2
|
+
|
3
|
+
class Squads
|
4
|
+
SCHEMA_VERSION = 'v1'.freeze
|
5
|
+
|
6
|
+
def initialize(parsed_data, people, gsuite_domain, slack_domain)
|
7
|
+
version = parsed_data.fetch('version')
|
8
|
+
raise "Unsupported squads schema version: #{version}" if version != SCHEMA_VERSION
|
9
|
+
|
10
|
+
@squads = {}
|
11
|
+
parsed_data.fetch('squads').each do |squad|
|
12
|
+
id = squad.fetch('id')
|
13
|
+
@squads[id] = Squad.new(id, squad, people, gsuite_domain, slack_domain)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def all_names
|
18
|
+
@squads.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup!(name)
|
22
|
+
@squads.fetch(name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
{ 'version' => SCHEMA_VERSION, 'squads' => @squads.values.sort_by(&:id).map(&:to_h) }
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Util
|
2
|
+
# Take a list of Persons and turn it into a newline delimited, comma
|
3
|
+
# separated array definition suitable for inclusion in terraform. Each line
|
4
|
+
# contains an okta id and a comment indicating the person's name.
|
5
|
+
def self.persons_tf(persons)
|
6
|
+
"[\n" + persons.map { |p| " \"#{p.okta_id}\", # #{p.name}" }.join("\n") + "\n]\n"
|
7
|
+
end
|
8
|
+
|
9
|
+
# Take a list of Persons and generate a gsuite_group containing all of those
|
10
|
+
# members with expected organizational settings.
|
11
|
+
def self.gsuite_group_tf(name, domain, persons, description)
|
12
|
+
email = "#{name}@#{domain}"
|
13
|
+
tf = <<-TERRAFORM
|
14
|
+
# G Suite group for #{email}
|
15
|
+
resource "gsuite_group" "#{name}" {
|
16
|
+
email = "#{email}"
|
17
|
+
name = "#{name}"
|
18
|
+
description = "#{description}"
|
19
|
+
}
|
20
|
+
|
21
|
+
resource "gsuite_group_settings" "#{name}" {
|
22
|
+
email = gsuite_group.#{name}.email
|
23
|
+
who_can_discover_group = "ALL_IN_DOMAIN_CAN_DISCOVER"
|
24
|
+
who_can_view_membership = "ALL_IN_DOMAIN_CAN_VIEW"
|
25
|
+
who_can_leave_group = "NONE_CAN_LEAVE"
|
26
|
+
who_can_join = "INVITED_CAN_JOIN"
|
27
|
+
who_can_post_message = "ALL_IN_DOMAIN_CAN_POST"
|
28
|
+
}
|
29
|
+
|
30
|
+
resource "gsuite_group_members" "#{name}" {
|
31
|
+
group_email = gsuite_group.#{name}.email
|
32
|
+
TERRAFORM
|
33
|
+
|
34
|
+
# Add a member block for everyone
|
35
|
+
# downcase is used as internal G Suite representation is always lowercase
|
36
|
+
# this avoids unnecessary state churn
|
37
|
+
persons.each do |p|
|
38
|
+
tf += <<-TERRAFORM
|
39
|
+
member {
|
40
|
+
email = "#{p.email.downcase}"
|
41
|
+
role = "MEMBER"
|
42
|
+
}
|
43
|
+
TERRAFORM
|
44
|
+
end
|
45
|
+
|
46
|
+
tf += "\n}"
|
47
|
+
tf
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: terraorg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua Kwan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: neatjson
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: oktakit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.2'
|
55
|
+
description: Manage an organizational structure with Okta and G-Suite using Terraform
|
56
|
+
email: joshk@triplehelix.org
|
57
|
+
executables:
|
58
|
+
- terraorg
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- LICENSE.txt
|
63
|
+
- README.md
|
64
|
+
- bin/terraorg
|
65
|
+
- lib/terraorg/model/org.rb
|
66
|
+
- lib/terraorg/model/people.rb
|
67
|
+
- lib/terraorg/model/person.rb
|
68
|
+
- lib/terraorg/model/platoon.rb
|
69
|
+
- lib/terraorg/model/platoons.rb
|
70
|
+
- lib/terraorg/model/squad.rb
|
71
|
+
- lib/terraorg/model/squads.rb
|
72
|
+
- lib/terraorg/model/util.rb
|
73
|
+
- lib/terraorg/version.rb
|
74
|
+
homepage: https://github.com/LiveRamp/terraorg
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '2.3'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubygems_version: 3.0.3
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: terraorg
|
97
|
+
test_files: []
|