openshift_master.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # vim: expandtab:tabstop=4:shiftwidth=4
  4. '''
  5. Custom filters for use in openshift-master
  6. '''
  7. import copy
  8. import sys
  9. import yaml
  10. from ansible import errors
  11. from ansible.runner.filter_plugins.core import bool as ansible_bool
  12. class IdentityProviderBase(object):
  13. """ IdentityProviderBase
  14. Attributes:
  15. name (str): Identity provider Name
  16. login (bool): Is this identity provider a login provider?
  17. challenge (bool): Is this identity provider a challenge provider?
  18. provider (dict): Provider specific config
  19. _idp (dict): internal copy of the IDP dict passed in
  20. _required (list): List of lists of strings for required attributes
  21. _optional (list): List of lists of strings for optional attributes
  22. _allow_additional (bool): Does this provider support attributes
  23. not in _required and _optional
  24. Args:
  25. api_version(str): OpenShift config version
  26. idp (dict): idp config dict
  27. Raises:
  28. AnsibleFilterError:
  29. """
  30. # disabling this check since the number of instance attributes are
  31. # necessary for this class
  32. # pylint: disable=too-many-instance-attributes
  33. def __init__(self, api_version, idp):
  34. if api_version not in ['v1']:
  35. raise errors.AnsibleFilterError("|failed api version {0} unknown".format(api_version))
  36. self._idp = copy.deepcopy(idp)
  37. if 'name' not in self._idp:
  38. raise errors.AnsibleFilterError("|failed identity provider missing a name")
  39. if 'kind' not in self._idp:
  40. raise errors.AnsibleFilterError("|failed identity provider missing a kind")
  41. self.name = self._idp.pop('name')
  42. self.login = ansible_bool(self._idp.pop('login', False))
  43. self.challenge = ansible_bool(self._idp.pop('challenge', False))
  44. self.provider = dict(apiVersion=api_version, kind=self._idp.pop('kind'))
  45. self._required = [['mappingMethod', 'mapping_method']]
  46. self._optional = []
  47. self._allow_additional = True
  48. @staticmethod
  49. def validate_idp_list(idp_list):
  50. ''' validates a list of idps '''
  51. login_providers = [x.name for x in idp_list if x.login]
  52. if len(login_providers) > 1:
  53. raise errors.AnsibleFilterError("|failed multiple providers are "
  54. "not allowed for login. login "
  55. "providers: {0}".format(', '.join(login_providers)))
  56. names = [x.name for x in idp_list]
  57. if len(set(names)) != len(names):
  58. raise errors.AnsibleFilterError("|failed more than one provider configured with the same name")
  59. for idp in idp_list:
  60. idp.validate()
  61. def validate(self):
  62. ''' validate an instance of this idp class '''
  63. valid_mapping_methods = ['add', 'claim', 'generate', 'lookup']
  64. if self.provider['mappingMethod'] not in valid_mapping_methods:
  65. raise errors.AnsibleFilterError("|failed unkown mapping method "
  66. "for provider {0}".format(self.__class__.__name__))
  67. @staticmethod
  68. def get_default(key):
  69. ''' get a default value for a given key '''
  70. if key == 'mappingMethod':
  71. return 'claim'
  72. else:
  73. return None
  74. def set_provider_item(self, items, required=False):
  75. ''' set a provider item based on the list of item names provided. '''
  76. for item in items:
  77. provider_key = items[0]
  78. if item in self._idp:
  79. self.provider[provider_key] = self._idp.pop(item)
  80. break
  81. else:
  82. default = self.get_default(provider_key)
  83. if default is not None:
  84. self.provider[provider_key] = default
  85. elif required:
  86. raise errors.AnsibleFilterError("|failed provider {0} missing "
  87. "required key {1}".format(self.__class__.__name__, provider_key))
  88. def set_provider_items(self):
  89. ''' set the provider items for this idp '''
  90. for items in self._required:
  91. self.set_provider_item(items, True)
  92. for items in self._optional:
  93. self.set_provider_item(items)
  94. if self._allow_additional:
  95. for key in self._idp.keys():
  96. self.set_provider_item([key])
  97. else:
  98. if len(self._idp) > 0:
  99. raise errors.AnsibleFilterError("|failed provider {0} "
  100. "contains unknown keys "
  101. "{1}".format(self.__class__.__name__, ', '.join(self._idp.keys())))
  102. def to_dict(self):
  103. ''' translate this idp to a dictionary '''
  104. return dict(name=self.name, challenge=self.challenge,
  105. login=self.login, provider=self.provider)
  106. class LDAPPasswordIdentityProvider(IdentityProviderBase):
  107. """ LDAPPasswordIdentityProvider
  108. Attributes:
  109. Args:
  110. api_version(str): OpenShift config version
  111. idp (dict): idp config dict
  112. Raises:
  113. AnsibleFilterError:
  114. """
  115. def __init__(self, api_version, idp):
  116. IdentityProviderBase.__init__(self, api_version, idp)
  117. self._allow_additional = False
  118. self._required += [['attributes'], ['url'], ['insecure']]
  119. self._optional += [['ca'],
  120. ['bindDN', 'bind_dn'],
  121. ['bindPassword', 'bind_password']]
  122. self._idp['insecure'] = ansible_bool(self._idp.pop('insecure', False))
  123. if 'attributes' in self._idp and 'preferred_username' in self._idp['attributes']:
  124. pref_user = self._idp['attributes'].pop('preferred_username')
  125. self._idp['attributes']['preferredUsername'] = pref_user
  126. def validate(self):
  127. ''' validate this idp instance '''
  128. IdentityProviderBase.validate(self)
  129. if not isinstance(self.provider['attributes'], dict):
  130. raise errors.AnsibleFilterError("|failed attributes for provider "
  131. "{0} must be a dictionary".format(self.__class__.__name__))
  132. attrs = ['id', 'email', 'name', 'preferredUsername']
  133. for attr in attrs:
  134. if attr in self.provider['attributes'] and not isinstance(self.provider['attributes'][attr], list):
  135. raise errors.AnsibleFilterError("|failed {0} attribute for "
  136. "provider {1} must be a list".format(attr, self.__class__.__name__))
  137. unknown_attrs = set(self.provider['attributes'].keys()) - set(attrs)
  138. if len(unknown_attrs) > 0:
  139. raise errors.AnsibleFilterError("|failed provider {0} has unknown "
  140. "attributes: {1}".format(self.__class__.__name__, ', '.join(unknown_attrs)))
  141. class KeystonePasswordIdentityProvider(IdentityProviderBase):
  142. """ KeystoneIdentityProvider
  143. Attributes:
  144. Args:
  145. api_version(str): OpenShift config version
  146. idp (dict): idp config dict
  147. Raises:
  148. AnsibleFilterError:
  149. """
  150. def __init__(self, api_version, idp):
  151. IdentityProviderBase.__init__(self, api_version, idp)
  152. self._allow_additional = False
  153. self._required += [['url'], ['domainName', 'domain_name']]
  154. self._optional += [['ca'], ['certFile', 'cert_file'], ['keyFile', 'key_file']]
  155. class RequestHeaderIdentityProvider(IdentityProviderBase):
  156. """ RequestHeaderIdentityProvider
  157. Attributes:
  158. Args:
  159. api_version(str): OpenShift config version
  160. idp (dict): idp config dict
  161. Raises:
  162. AnsibleFilterError:
  163. """
  164. def __init__(self, api_version, idp):
  165. IdentityProviderBase.__init__(self, api_version, idp)
  166. self._allow_additional = False
  167. self._required += [['headers']]
  168. self._optional += [['challengeURL', 'challenge_url'],
  169. ['loginURL', 'login_url'],
  170. ['clientCA', 'client_ca']]
  171. def validate(self):
  172. ''' validate this idp instance '''
  173. IdentityProviderBase.validate(self)
  174. if not isinstance(self.provider['headers'], list):
  175. raise errors.AnsibleFilterError("|failed headers for provider {0} "
  176. "must be a list".format(self.__class__.__name__))
  177. class AllowAllPasswordIdentityProvider(IdentityProviderBase):
  178. """ AllowAllPasswordIdentityProvider
  179. Attributes:
  180. Args:
  181. api_version(str): OpenShift config version
  182. idp (dict): idp config dict
  183. Raises:
  184. AnsibleFilterError:
  185. """
  186. def __init__(self, api_version, idp):
  187. IdentityProviderBase.__init__(self, api_version, idp)
  188. self._allow_additional = False
  189. class DenyAllPasswordIdentityProvider(IdentityProviderBase):
  190. """ DenyAllPasswordIdentityProvider
  191. Attributes:
  192. Args:
  193. api_version(str): OpenShift config version
  194. idp (dict): idp config dict
  195. Raises:
  196. AnsibleFilterError:
  197. """
  198. def __init__(self, api_version, idp):
  199. IdentityProviderBase.__init__(self, api_version, idp)
  200. self._allow_additional = False
  201. class HTPasswdPasswordIdentityProvider(IdentityProviderBase):
  202. """ HTPasswdPasswordIdentity
  203. Attributes:
  204. Args:
  205. api_version(str): OpenShift config version
  206. idp (dict): idp config dict
  207. Raises:
  208. AnsibleFilterError:
  209. """
  210. def __init__(self, api_version, idp):
  211. IdentityProviderBase.__init__(self, api_version, idp)
  212. self._allow_additional = False
  213. self._required += [['file', 'filename', 'fileName', 'file_name']]
  214. @staticmethod
  215. def get_default(key):
  216. if key == 'file':
  217. return '/etc/origin/htpasswd'
  218. else:
  219. return IdentityProviderBase.get_default(key)
  220. class BasicAuthPasswordIdentityProvider(IdentityProviderBase):
  221. """ BasicAuthPasswordIdentityProvider
  222. Attributes:
  223. Args:
  224. api_version(str): OpenShift config version
  225. idp (dict): idp config dict
  226. Raises:
  227. AnsibleFilterError:
  228. """
  229. def __init__(self, api_version, idp):
  230. IdentityProviderBase.__init__(self, api_version, idp)
  231. self._allow_additional = False
  232. self._required += [['url']]
  233. self._optional += [['ca'], ['certFile', 'cert_file'], ['keyFile', 'key_file']]
  234. class IdentityProviderOauthBase(IdentityProviderBase):
  235. """ IdentityProviderOauthBase
  236. Attributes:
  237. Args:
  238. api_version(str): OpenShift config version
  239. idp (dict): idp config dict
  240. Raises:
  241. AnsibleFilterError:
  242. """
  243. def __init__(self, api_version, idp):
  244. IdentityProviderBase.__init__(self, api_version, idp)
  245. self._allow_additional = False
  246. self._required += [['clientID', 'client_id'], ['clientSecret', 'client_secret']]
  247. def validate(self):
  248. ''' validate this idp instance '''
  249. IdentityProviderBase.validate(self)
  250. if self.challenge:
  251. raise errors.AnsibleFilterError("|failed provider {0} does not "
  252. "allow challenge authentication".format(self.__class__.__name__))
  253. class OpenIDIdentityProvider(IdentityProviderOauthBase):
  254. """ OpenIDIdentityProvider
  255. Attributes:
  256. Args:
  257. api_version(str): OpenShift config version
  258. idp (dict): idp config dict
  259. Raises:
  260. AnsibleFilterError:
  261. """
  262. def __init__(self, api_version, idp):
  263. IdentityProviderOauthBase.__init__(self, api_version, idp)
  264. self._required += [['claims'], ['urls']]
  265. self._optional += [['ca'],
  266. ['extraScopes'],
  267. ['extraAuthorizeParameters']]
  268. if 'claims' in self._idp and 'preferred_username' in self._idp['claims']:
  269. pref_user = self._idp['claims'].pop('preferred_username')
  270. self._idp['claims']['preferredUsername'] = pref_user
  271. if 'urls' in self._idp and 'user_info' in self._idp['urls']:
  272. user_info = self._idp['urls'].pop('user_info')
  273. self._idp['urls']['userInfo'] = user_info
  274. if 'extra_scopes' in self._idp:
  275. self._idp['extraScopes'] = self._idp.pop('extra_scopes')
  276. if 'extra_authorize_parameters' in self._idp:
  277. self._idp['extraAuthorizeParameters'] = self._idp.pop('extra_authorize_parameters')
  278. if 'extraAuthorizeParameters' in self._idp:
  279. if 'include_granted_scopes' in self._idp['extraAuthorizeParameters']:
  280. val = ansible_bool(self._idp['extraAuthorizeParameters'].pop('include_granted_scopes'))
  281. self._idp['extraAuthorizeParameters']['include_granted_scopes'] = val
  282. def validate(self):
  283. ''' validate this idp instance '''
  284. IdentityProviderOauthBase.validate(self)
  285. if not isinstance(self.provider['claims'], dict):
  286. raise errors.AnsibleFilterError("|failed claims for provider {0} "
  287. "must be a dictionary".format(self.__class__.__name__))
  288. if 'extraScopes' not in self.provider['extraScopes'] and not isinstance(self.provider['extraScopes'], list):
  289. raise errors.AnsibleFilterError("|failed extraScopes for provider "
  290. "{0} must be a list".format(self.__class__.__name__))
  291. if ('extraAuthorizeParameters' not in self.provider['extraAuthorizeParameters']
  292. and not isinstance(self.provider['extraAuthorizeParameters'], dict)):
  293. raise errors.AnsibleFilterError("|failed extraAuthorizeParameters "
  294. "for provider {0} must be a dictionary".format(self.__class__.__name__))
  295. required_claims = ['id']
  296. optional_claims = ['email', 'name', 'preferredUsername']
  297. all_claims = required_claims + optional_claims
  298. for claim in required_claims:
  299. if claim in required_claims and claim not in self.provider['claims']:
  300. raise errors.AnsibleFilterError("|failed {0} claim missing "
  301. "for provider {1}".format(claim, self.__class__.__name__))
  302. for claim in all_claims:
  303. if claim in self.provider['claims'] and not isinstance(self.provider['claims'][claim], list):
  304. raise errors.AnsibleFilterError("|failed {0} claims for "
  305. "provider {1} must be a list".format(claim, self.__class__.__name__))
  306. unknown_claims = set(self.provider['claims'].keys()) - set(all_claims)
  307. if len(unknown_claims) > 0:
  308. raise errors.AnsibleFilterError("|failed provider {0} has unknown "
  309. "claims: {1}".format(self.__class__.__name__, ', '.join(unknown_claims)))
  310. if not isinstance(self.provider['urls'], dict):
  311. raise errors.AnsibleFilterError("|failed urls for provider {0} "
  312. "must be a dictionary".format(self.__class__.__name__))
  313. required_urls = ['authorize', 'token']
  314. optional_urls = ['userInfo']
  315. all_urls = required_urls + optional_urls
  316. for url in required_urls:
  317. if url not in self.provider['urls']:
  318. raise errors.AnsibleFilterError("|failed {0} url missing for "
  319. "provider {1}".format(url, self.__class__.__name__))
  320. unknown_urls = set(self.provider['urls'].keys()) - set(all_urls)
  321. if len(unknown_urls) > 0:
  322. raise errors.AnsibleFilterError("|failed provider {0} has unknown "
  323. "urls: {1}".format(self.__class__.__name__, ', '.join(unknown_urls)))
  324. class GoogleIdentityProvider(IdentityProviderOauthBase):
  325. """ GoogleIdentityProvider
  326. Attributes:
  327. Args:
  328. api_version(str): OpenShift config version
  329. idp (dict): idp config dict
  330. Raises:
  331. AnsibleFilterError:
  332. """
  333. def __init__(self, api_version, idp):
  334. IdentityProviderOauthBase.__init__(self, api_version, idp)
  335. self._optional += [['hostedDomain', 'hosted_domain']]
  336. class GitHubIdentityProvider(IdentityProviderOauthBase):
  337. """ GitHubIdentityProvider
  338. Attributes:
  339. Args:
  340. api_version(str): OpenShift config version
  341. idp (dict): idp config dict
  342. Raises:
  343. AnsibleFilterError:
  344. """
  345. pass
  346. class FilterModule(object):
  347. ''' Custom ansible filters for use by the openshift_master role'''
  348. @staticmethod
  349. def translate_idps(idps, api_version):
  350. ''' Translates a list of dictionaries into a valid identityProviders config '''
  351. idp_list = []
  352. if not isinstance(idps, list):
  353. raise errors.AnsibleFilterError("|failed expects to filter on a list of identity providers")
  354. for idp in idps:
  355. if not isinstance(idp, dict):
  356. raise errors.AnsibleFilterError("|failed identity providers must be a list of dictionaries")
  357. cur_module = sys.modules[__name__]
  358. idp_class = getattr(cur_module, idp['kind'], None)
  359. idp_inst = idp_class(api_version, idp) if idp_class is not None else IdentityProviderBase(api_version, idp)
  360. idp_inst.set_provider_items()
  361. idp_list.append(idp_inst)
  362. IdentityProviderBase.validate_idp_list(idp_list)
  363. return yaml.safe_dump([idp.to_dict() for idp in idp_list], default_flow_style=False)
  364. def filters(self):
  365. ''' returns a mapping of filters to methods '''
  366. return {"translate_idps": self.translate_idps}