okta_group_maker.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. #!/usr/bin/env python3
  2. """
  3. Makes the Okta groups and group rules needed to support the Okta + AWS integration.
  4. A master group has a group rule associated with it. The group rule auto-assigns
  5. membership in several "subgroups" - each of which maps to a given role in a given
  6. AWS account. All of this is so that if we assign to you the "foo-role" group in
  7. Okta, you'll be able to assume "foo-role" in each of the many AWS accounts we have.
  8. """
  9. from __future__ import print_function
  10. import json
  11. import logging
  12. import os
  13. import sys
  14. import re
  15. import requests
  16. from requests.auth import AuthBase
  17. # Configuration:
  18. # * a list of groups that should exist
  19. # * a regex for matching up 'child groups'
  20. # Maybe this should be a configuration file? Perhaps one day
  21. # but for now I'm happy with it in here.
  22. LOGLEVEL = logging.DEBUG
  23. API_URL = 'https://mdr-multipass.okta.com'
  24. API_KEY = os.environ.get('OKTA_API_TOKEN')
  25. MASTER_GROUPS = [
  26. {
  27. 'group_name': 'AWS - MDR_Engineer-Readonly Role',
  28. 'subgroup_regex': r'^aws(?:-us-gov)?#[^#]+#mdr_engineer_readonly#\d+$'
  29. },
  30. {
  31. 'group_name': 'AWS - Cyber Range / A&I',
  32. 'subgroup_regex': r'^aws(?:-us-gov)?#afs-mdr-common-services(?:-gov)?#mdr_developer_readonly#\d+$'
  33. },
  34. {
  35. 'group_name': 'AWS - Feed Management',
  36. 'subgroup_regex': r'^aws(?:-us-gov)?#afs-mdr-common-services(?:-gov)?#mdr_feedmgmt_readonly#\d+$'
  37. },
  38. ]
  39. class OktaAuth(AuthBase):
  40. """
  41. Adds Okta API expected auth header
  42. """
  43. def __init__(self, api_key):
  44. self.api_key = api_key
  45. def __call__(self, r):
  46. r.headers['Authorization'] = 'SSWS {0}'.format(self.api_key)
  47. return r
  48. def main(args):
  49. """
  50. The main
  51. """
  52. logging.basicConfig(stream=sys.stderr,
  53. level=LOGLEVEL,
  54. format='%(asctime)s %(levelname)s %(funcName)s %(message)s')
  55. if API_KEY is None:
  56. logging.fatal("No OKTA_API_TOKEN environment variable set")
  57. return 1
  58. for group in MASTER_GROUPS:
  59. process_group(group)
  60. def process_group(group):
  61. """
  62. Process a group obviously
  63. """
  64. log = logging.getLogger(__name__)
  65. payload = {
  66. 'q' : group.get('group_name')
  67. }
  68. log.debug("Processing Group %s", group.get('group_name'))
  69. r = requests.get('{0}/api/v1/groups'.format(API_URL),
  70. auth=OktaAuth(API_KEY),
  71. params=payload)
  72. log.debug("Response code %d", r.status_code)
  73. my_group = None
  74. # we should "always" get a 200 even if we get an empty-ish response
  75. # a basic [ ] json doc.
  76. if r.status_code == 200:
  77. data = r.json()
  78. if data:
  79. for rec in data:
  80. if rec.get('profile').get('name') == group.get('group_name'):
  81. my_group = rec
  82. # Our group does not exist, we need to make it
  83. payload = {
  84. 'profile': {
  85. 'name' : group.get('group_name'),
  86. 'description': 'AWS SAML Role'
  87. }
  88. }
  89. headers = {
  90. 'Accept': 'application/json',
  91. 'Content-Type': 'application/json'
  92. }
  93. if my_group is None:
  94. log.info("Creating Group %s", group.get('group_name'))
  95. r = requests.post('{0}/api/v1/groups'.format(API_URL),
  96. auth=OktaAuth(API_KEY),
  97. data=json.dumps(payload),
  98. headers=headers)
  99. log.debug("Response code: %d", r.status_code)
  100. my_group = r.json()
  101. log.info("Master group id=%s", my_group.get('id'))
  102. subgroups = find_subgroups(group.get('subgroup_regex'))
  103. log.info("Valid Subgroups = %s", repr(subgroups))
  104. create_subgroup_rule(group.get('group_name'), my_group.get('id'), subgroups)
  105. def create_subgroup_rule(rule_name, master_group_id, subgroups):
  106. """
  107. Create a rule to assign a user to all of the subgroups
  108. when they are assigned to the master group. If a rule
  109. by the same name exists, it will be destroyed and
  110. re-created. (Okta API artifact)
  111. """
  112. log = logging.getLogger(__name__)
  113. log.debug("Checking for existing rule named %s", rule_name)
  114. payload = {
  115. 'q': rule_name
  116. }
  117. existing_rule_id = None
  118. r = requests.get('{0}/api/v1/groups/rules'.format(API_URL),
  119. auth=OktaAuth(API_KEY),
  120. params=payload)
  121. log.debug("Response code %d", r.status_code)
  122. if r.status_code == 200:
  123. rules = r.json()
  124. for rule in rules:
  125. if rule.get('name') == rule_name:
  126. existing_rule_id = rule.get('id')
  127. else:
  128. raise
  129. # Need to remove the existing rule to add a new one
  130. if existing_rule_id is not None:
  131. remove_group_rule(existing_rule_id)
  132. # Now let's make a new rule whee
  133. new_rule = {
  134. 'type' : 'group_rule',
  135. 'name' : rule_name,
  136. 'conditions': {
  137. 'expression': {
  138. 'type' : 'urn:okta:expression:1.0',
  139. 'value': 'isMemberOfAnyGroup("{}")'.format(master_group_id)
  140. }
  141. },
  142. 'actions' : {
  143. 'assignUserToGroups': {
  144. 'groupIds': subgroups
  145. }
  146. }
  147. }
  148. # First deactivate the rule, per Okta API
  149. url = '{0}/api/v1/groups/rules'.format(API_URL)
  150. headers = {
  151. 'Accept': 'application/json',
  152. 'Content-Type': 'application/json'
  153. }
  154. r = requests.post(url,
  155. auth=OktaAuth(API_KEY),
  156. headers=headers,
  157. data=json.dumps(new_rule))
  158. log.debug("Response code: %d", r.status_code)
  159. if r.status_code != 200:
  160. log.error("Response code %d trying to create group rule id=%s", r.rule_name)
  161. raise
  162. new_rule_response=r.json()
  163. url = '{0}/api/v1/groups/rules/{1}/lifecycle/activate'.format(
  164. API_URL,
  165. new_rule_response.get('id'))
  166. r = requests.post(url,
  167. auth=OktaAuth(API_KEY),
  168. headers=headers,
  169. data=json.dumps(new_rule))
  170. log.debug("Response code: %d", r.status_code)
  171. def remove_group_rule(rule_id):
  172. log = logging.getLogger(__name__)
  173. # First deactivate the rule, per Okta API
  174. url = '{0}/api/v1/groups/rules/{1}/lifecycle/deactivate'.format(
  175. API_URL,
  176. rule_id)
  177. headers = {
  178. 'Accept': 'application/json',
  179. 'Content-Type': 'application/json'
  180. }
  181. r = requests.post(url,
  182. auth=OktaAuth(API_KEY),
  183. headers=headers)
  184. log.debug("Response code: %d", r.status_code)
  185. if r.status_code != 204:
  186. log.error("Response code %d trying to deactivate group rule id=%s", r.status_code, rule_id)
  187. raise
  188. # Now let's try to delete it
  189. url = '{0}/api/v1/groups/rules/{1}'.format(
  190. API_URL,
  191. rule_id)
  192. headers = {
  193. 'Accept': 'application/json',
  194. 'Content-Type': 'application/json'
  195. }
  196. r = requests.delete(url,
  197. auth=OktaAuth(API_KEY),
  198. headers=headers)
  199. log.debug("Response code: %d", r.status_code)
  200. if r.status_code != 202:
  201. log.error("Response code %d trying to delete group rule id=%s", r.status_code, rule_id)
  202. raise
  203. def find_subgroups(regex):
  204. """
  205. Finds all the groups matching a given regex, so that
  206. we can attach them to a rule.
  207. """
  208. log = logging.getLogger(__name__)
  209. log.debug("Looking for groups matching regex '%s'", regex)
  210. payload = {
  211. 'q' : 'aws'
  212. }
  213. r = requests.get('{0}/api/v1/groups'.format(API_URL),
  214. auth=OktaAuth(API_KEY),
  215. params=payload)
  216. log.debug("Response code %d", r.status_code)
  217. groups = []
  218. # we should "always" get a 200 even if we get an empty-ish response
  219. # a basic [ ] json doc.
  220. if r.status_code == 200:
  221. data = r.json()
  222. if data:
  223. for rec in data:
  224. name = rec.get('profile').get('name')
  225. groupid = rec.get('id')
  226. if re.match(regex, name) is not None:
  227. groups.append(groupid)
  228. return groups
  229. if __name__ == "__main__":
  230. sys.exit(main(sys.argv))