okta_group_maker.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. #!/usr/bin/env python
  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. class OktaAuth(AuthBase):
  36. """
  37. Adds Okta API expected auth header
  38. """
  39. def __init__(self, api_key):
  40. self.api_key = api_key
  41. def __call__(self, r):
  42. r.headers['Authorization'] = 'SSWS {0}'.format(self.api_key)
  43. return r
  44. def main(args):
  45. """
  46. The main
  47. """
  48. logging.basicConfig(stream=sys.stderr,
  49. level=LOGLEVEL,
  50. format='%(asctime)s %(levelname)s %(funcName)s %(message)s')
  51. for group in MASTER_GROUPS:
  52. process_group(group)
  53. def process_group(group):
  54. """
  55. Process a group obviously
  56. """
  57. log = logging.getLogger(__name__)
  58. payload = {
  59. 'q' : group.get('group_name')
  60. }
  61. log.debug("Processing Group %s", group.get('group_name'))
  62. r = requests.get('{0}/api/v1/groups'.format(API_URL),
  63. auth=OktaAuth(API_KEY),
  64. params=payload)
  65. log.debug("Response code %d", r.status_code)
  66. my_group = None
  67. # we should "always" get a 200 even if we get an empty-ish response
  68. # a basic [ ] json doc.
  69. if r.status_code == 200:
  70. data = r.json()
  71. if data:
  72. for rec in data:
  73. if rec.get('profile').get('name') == group.get('group_name'):
  74. my_group = rec
  75. # Our group does not exist, we need to make it
  76. payload = {
  77. 'profile': {
  78. 'name' : group.get('group_name'),
  79. 'description': 'AWS SAML Role'
  80. }
  81. }
  82. headers = {
  83. 'Accept': 'application/json',
  84. 'Content-Type': 'application/json'
  85. }
  86. if my_group is None:
  87. log.info("Creating Group %s", group.get('group_name'))
  88. r = requests.post('{0}/api/v1/groups'.format(API_URL),
  89. auth=OktaAuth(API_KEY),
  90. data=json.dumps(payload),
  91. headers=headers)
  92. log.debug("Response code: %d", r.status_code)
  93. my_group = r.json()
  94. log.info("Master group id=%s", my_group.get('id'))
  95. subgroups = find_subgroups(group.get('subgroup_regex'))
  96. log.info("Valid Subgroups = %s", repr(subgroups))
  97. create_subgroup_rule(group.get('group_name'), my_group.get('id'), subgroups)
  98. def create_subgroup_rule(rule_name, master_group_id, subgroups):
  99. """
  100. Create a rule to assign a user to all of the subgroups
  101. when they are assigned to the master group. If a rule
  102. by the same name exists, it will be destroyed and
  103. re-created. (Okta API artifact)
  104. """
  105. log = logging.getLogger(__name__)
  106. log.debug("Checking for existing rule named %s", rule_name)
  107. payload = {
  108. 'q': rule_name
  109. }
  110. existing_rule_id = None
  111. r = requests.get('{0}/api/v1/groups/rules'.format(API_URL),
  112. auth=OktaAuth(API_KEY),
  113. params=payload)
  114. log.debug("Response code %d", r.status_code)
  115. if r.status_code == 200:
  116. rules = r.json()
  117. for rule in rules:
  118. if rule.get('name') == rule_name:
  119. existing_rule_id = rule.get('id')
  120. else:
  121. raise
  122. # Need to remove the existing rule to add a new one
  123. if existing_rule_id is not None:
  124. remove_group_rule(existing_rule_id)
  125. # Now let's make a new rule whee
  126. new_rule = {
  127. 'type' : 'group_rule',
  128. 'name' : rule_name,
  129. 'conditions': {
  130. 'expression': {
  131. 'type' : 'urn:okta:expression:1.0',
  132. 'value': 'isMemberOfAnyGroup("{}")'.format(master_group_id)
  133. }
  134. },
  135. 'actions' : {
  136. 'assignUserToGroups': {
  137. 'groupIds': subgroups
  138. }
  139. }
  140. }
  141. # First deactivate the rule, per Okta API
  142. url = '{0}/api/v1/groups/rules'.format(API_URL)
  143. headers = {
  144. 'Accept': 'application/json',
  145. 'Content-Type': 'application/json'
  146. }
  147. r = requests.post(url,
  148. auth=OktaAuth(API_KEY),
  149. headers=headers,
  150. data=json.dumps(new_rule))
  151. log.debug("Response code: %d", r.status_code)
  152. if r.status_code != 200:
  153. log.error("Response code %d trying to create group rule id=%s", r.rule_name)
  154. raise
  155. new_rule_response=r.json()
  156. url = '{0}/api/v1/groups/rules/{1}/lifecycle/activate'.format(
  157. API_URL,
  158. new_rule_response.get('id'))
  159. r = requests.post(url,
  160. auth=OktaAuth(API_KEY),
  161. headers=headers,
  162. data=json.dumps(new_rule))
  163. log.debug("Response code: %d", r.status_code)
  164. def remove_group_rule(rule_id):
  165. log = logging.getLogger(__name__)
  166. # First deactivate the rule, per Okta API
  167. url = '{0}/api/v1/groups/rules/{1}/lifecycle/deactivate'.format(
  168. API_URL,
  169. rule_id)
  170. headers = {
  171. 'Accept': 'application/json',
  172. 'Content-Type': 'application/json'
  173. }
  174. r = requests.post(url,
  175. auth=OktaAuth(API_KEY),
  176. headers=headers)
  177. log.debug("Response code: %d", r.status_code)
  178. if r.status_code != 204:
  179. log.error("Response code %d trying to deactivate group rule id=%s", r.status_code, rule_id)
  180. raise
  181. # Now let's try to delete it
  182. url = '{0}/api/v1/groups/rules/{1}'.format(
  183. API_URL,
  184. rule_id)
  185. headers = {
  186. 'Accept': 'application/json',
  187. 'Content-Type': 'application/json'
  188. }
  189. r = requests.delete(url,
  190. auth=OktaAuth(API_KEY),
  191. headers=headers)
  192. log.debug("Response code: %d", r.status_code)
  193. if r.status_code != 202:
  194. log.error("Response code %d trying to delete group rule id=%s", r.status_code, rule_id)
  195. raise
  196. def find_subgroups(regex):
  197. """
  198. Finds all the groups matching a given regex, so that
  199. we can attach them to a rule.
  200. """
  201. log = logging.getLogger(__name__)
  202. log.debug("Looking for groups matching regex '%s'", regex)
  203. payload = {
  204. 'q' : 'aws'
  205. }
  206. r = requests.get('{0}/api/v1/groups'.format(API_URL),
  207. auth=OktaAuth(API_KEY),
  208. params=payload)
  209. log.debug("Response code %d", r.status_code)
  210. groups = []
  211. # we should "always" get a 200 even if we get an empty-ish response
  212. # a basic [ ] json doc.
  213. if r.status_code == 200:
  214. data = r.json()
  215. if data:
  216. for rec in data:
  217. name = rec.get('profile').get('name')
  218. groupid = rec.get('id')
  219. if re.match(regex, name) is not None:
  220. groups.append(groupid)
  221. return groups
  222. if __name__ == "__main__":
  223. sys.exit(main(sys.argv))