mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 07:33:27 -06:00
69e4397421
Use of 'ipa idp-add --provider' was supposed to allow override scope and other IdP options. The defaults are provided by the IdP template and were actually not overridden. Fix this. Fixes: https://pagure.io/freeipa/issue/9421 Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
447 lines
16 KiB
Python
447 lines
16 KiB
Python
#
|
|
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
|
|
#
|
|
import logging
|
|
import string
|
|
from urllib.parse import urlparse
|
|
|
|
from .baseldap import (
|
|
LDAPObject,
|
|
LDAPCreate,
|
|
LDAPDelete,
|
|
LDAPUpdate,
|
|
LDAPSearch,
|
|
LDAPRetrieve)
|
|
from ipalib import api, errors, Password, Str, StrEnum, _, ngettext
|
|
from ipalib.plugable import Registry
|
|
from ipapython.dn import DN
|
|
from ipapython.ipautil import template_str
|
|
from copy import deepcopy
|
|
from itertools import chain
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
__doc__ = _("""
|
|
External Identity Provider References
|
|
""") + _("""
|
|
Manage External Identity Provider References.
|
|
""") + _("""
|
|
IPA supports the use of an external Identity Provider for OAuth2.0 Device Flow
|
|
authentication.
|
|
""") + _("""
|
|
EXAMPLES:
|
|
""") + _("""
|
|
Add a new external Identity Provider reference:
|
|
ipa idp-add MyIdP --client-id jhkQty13 \
|
|
--auth-uri https://oauth2.idp.com/auth \
|
|
--token-uri https://oauth2.idp.com/token --secret
|
|
""") + _("""
|
|
Add a new external Identity Provider reference using github predefined
|
|
endpoints:
|
|
ipa idp-add MyIdp --client-id jhkQty13 --provider github --secret
|
|
""") + _("""
|
|
Find all external Identity Provider references whose entries include the string
|
|
"test.com":
|
|
ipa idp-find test.com
|
|
""") + _("""
|
|
Examine the configuration of an external Identity Provider reference:
|
|
ipa idp-show MyIdP
|
|
""") + _("""
|
|
Change the secret:
|
|
ipa idp-mod MyIdP --secret
|
|
""") + _("""
|
|
Delete an external Identity Provider reference:
|
|
ipa idp-del MyIdP
|
|
""")
|
|
|
|
register = Registry()
|
|
|
|
|
|
def normalize_baseurl(url):
|
|
if url.startswith('https://'):
|
|
return url[len('https://'):]
|
|
return url
|
|
|
|
|
|
def validate_uri(ugettext, uri):
|
|
try:
|
|
parsed = urlparse(uri, 'https')
|
|
except Exception:
|
|
return _('Invalid URI: not an https scheme')
|
|
|
|
if not parsed.netloc:
|
|
return _('Invalid URI: missing netloc')
|
|
|
|
return None
|
|
|
|
|
|
@register()
|
|
class idp(LDAPObject):
|
|
"""
|
|
Identity Provider object.
|
|
"""
|
|
container_dn = api.env.container_idp
|
|
object_name = _('Identity Provider reference')
|
|
object_name_plural = _('Identity Provider references')
|
|
object_class = ['ipaidp']
|
|
default_attributes = [
|
|
'cn', 'ipaidpauthendpoint', 'ipaidpdevauthendpoint',
|
|
'ipaidpuserinfoendpoint', 'ipaidpkeysendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpissuerurl',
|
|
'ipaidpclientid', 'ipaidpscope', 'ipaidpsub',
|
|
]
|
|
search_attributes = [
|
|
'cn', 'ipaidpauthendpoint', 'ipaidpdevauthendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpuserinfoendpoint',
|
|
'ipaidpkeysendpoint', 'ipaidpscope', 'ipaidpsub',
|
|
]
|
|
allow_rename = True
|
|
label = _('Identity Provider references')
|
|
label_singular = _('Identity Provider reference')
|
|
|
|
takes_params = (
|
|
Str('cn',
|
|
cli_name='name',
|
|
label=_('Identity Provider reference name'),
|
|
primary_key=True,
|
|
),
|
|
Str('ipaidpauthendpoint?',
|
|
validate_uri,
|
|
cli_name='auth_uri',
|
|
label=_('Authorization URI'),
|
|
doc=_('OAuth 2.0 authorization endpoint'),
|
|
),
|
|
Str('ipaidpdevauthendpoint?',
|
|
validate_uri,
|
|
cli_name='dev_auth_uri',
|
|
label=_('Device authorization URI'),
|
|
doc=_('Device authorization endpoint'),
|
|
),
|
|
Str('ipaidptokenendpoint?',
|
|
validate_uri,
|
|
cli_name='token_uri',
|
|
label=_('Token URI'),
|
|
doc=_('Token endpoint'),
|
|
),
|
|
Str('ipaidpuserinfoendpoint?',
|
|
validate_uri,
|
|
cli_name='userinfo_uri',
|
|
label=_('User info URI'),
|
|
doc=_('User information endpoint'),
|
|
),
|
|
Str('ipaidpkeysendpoint?',
|
|
validate_uri,
|
|
cli_name='keys_uri',
|
|
label=_('JWKS URI'),
|
|
doc=_('JWKS endpoint'),
|
|
),
|
|
Str('ipaidpissuerurl?',
|
|
cli_name='issuer_url',
|
|
label=_('OIDC URL'),
|
|
doc=_(
|
|
'The Identity Provider OIDC URL'),
|
|
),
|
|
Str('ipaidpclientid',
|
|
cli_name='client_id',
|
|
label=_('Client identifier'),
|
|
doc=_(
|
|
'OAuth 2.0 client identifier'),
|
|
),
|
|
Password('ipaidpclientsecret?',
|
|
cli_name='secret',
|
|
label=_('Secret'),
|
|
doc=_('OAuth 2.0 client secret'),
|
|
confirm=True,
|
|
flags={'no_display'},
|
|
),
|
|
Str('ipaidpscope?',
|
|
cli_name='scope',
|
|
label=_('Scope'),
|
|
doc=_('OAuth 2.0 scope. Multiple scopes separated by space'),
|
|
),
|
|
Str('ipaidpsub?',
|
|
cli_name='idp_user_id',
|
|
label=_('External IdP user identifier attribute'),
|
|
doc=_('Attribute for user identity in OAuth 2.0 userinfo'),
|
|
),
|
|
)
|
|
|
|
permission_filter_objectclasses = ['ipaidp']
|
|
managed_permissions = {
|
|
'System: Add External IdP server': {
|
|
'ipapermright': {'add'},
|
|
'ipapermlocation': DN(container_dn, api.env.basedn),
|
|
'ipapermtargetfilter': {
|
|
'(objectclass=ipaidp)'},
|
|
'default_privileges': {'External IdP server Administrators'}
|
|
},
|
|
'System: Read External IdP server': {
|
|
'ipapermright': {'read', 'search', 'compare'},
|
|
'ipapermdefaultattr': {
|
|
'cn', 'objectclass', 'ipaidpauthendpoint',
|
|
'ipaidpdevauthendpoint', 'ipaidpuserinfoendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpkeysendpoint',
|
|
'ipaidpissuerurl', 'ipaidpclientid', 'ipaidpscope',
|
|
'ipaidpsub',
|
|
},
|
|
'ipapermlocation': DN(container_dn, api.env.basedn),
|
|
'ipapermtargetfilter': {
|
|
'(objectclass=ipaidp)'},
|
|
'default_privileges': {'External IdP server Administrators'}
|
|
},
|
|
'System: Modify External IdP server': {
|
|
'ipapermright': {'write'},
|
|
'ipapermlocation': DN(container_dn, api.env.basedn),
|
|
'ipapermdefaultattr': {
|
|
'cn', 'objectclass', 'ipaidpauthendpoint',
|
|
'ipaidpdevauthendpoint', 'ipaidpuserinfoendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpkeysendpoint',
|
|
'ipaidpissuerurl', 'ipaidpclientid', 'ipaidpscope',
|
|
'ipaidpclientsecret', 'ipaidpsub',
|
|
},
|
|
'default_privileges': {'External IdP server Administrators'}
|
|
},
|
|
'System: Delete External IdP server': {
|
|
'ipapermright': {'delete'},
|
|
'ipapermlocation': DN(container_dn, api.env.basedn),
|
|
'ipapermtargetfilter': {
|
|
'(objectclass=ipaidp)'},
|
|
'default_privileges': {'External IdP server Administrators'}
|
|
},
|
|
'System: Read External IdP server client secret': {
|
|
'ipapermright': {'read', 'search', 'compare'},
|
|
'ipapermlocation': DN(container_dn, api.env.basedn),
|
|
'ipapermdefaultattr': {
|
|
'cn', 'objectclass', 'ipaidpauthendpoint',
|
|
'ipaidpdevauthendpoint', 'ipaidpuserinfoendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpissuerurl',
|
|
'ipaidpkeysendpoint', 'ipaidpclientid', 'ipaidpscope',
|
|
'ipaidpclientsecret', 'ipaidpsub',
|
|
},
|
|
'ipapermtargetfilter': {
|
|
'(objectclass=ipaidp)'},
|
|
}
|
|
}
|
|
|
|
|
|
@register()
|
|
class idp_add(LDAPCreate):
|
|
__doc__ = _('Add a new Identity Provider reference.')
|
|
msg_summary = _('Added Identity Provider reference "%(value)s"')
|
|
|
|
# List of pre-populated idp endpoints
|
|
# key = provider,
|
|
# value = dictionary of overidden attributes
|
|
idp_providers = {
|
|
'google': {
|
|
'ipaidpauthendpoint':
|
|
'https://accounts.google.com/o/oauth2/auth',
|
|
'ipaidpdevauthendpoint':
|
|
'https://oauth2.googleapis.com/device/code',
|
|
'ipaidptokenendpoint':
|
|
'https://oauth2.googleapis.com/token',
|
|
'ipaidpuserinfoendpoint':
|
|
'https://openidconnect.googleapis.com/v1/userinfo',
|
|
'ipaidpkeysendpoint':
|
|
'https://www.googleapis.com/oauth2/v3/certs',
|
|
'ipaidpscope': 'openid email',
|
|
'ipaidpsub': 'email'},
|
|
'github': {
|
|
'ipaidpauthendpoint':
|
|
'https://github.com/login/oauth/authorize',
|
|
'ipaidpdevauthendpoint':
|
|
'https://github.com/login/device/code',
|
|
'ipaidptokenendpoint':
|
|
'https://github.com/login/oauth/access_token',
|
|
'ipaidpuserinfoendpoint':
|
|
'https://api.github.com/user',
|
|
'ipaidpscope': 'user',
|
|
'ipaidpsub': 'login'},
|
|
'microsoft': {
|
|
'ipaidpauthendpoint':
|
|
'https://login.microsoftonline.com/${ipaidporg}/oauth2/v2.0/'
|
|
'authorize',
|
|
'ipaidpdevauthendpoint':
|
|
'https://login.microsoftonline.com/${ipaidporg}/oauth2/v2.0/'
|
|
'devicecode',
|
|
'ipaidptokenendpoint':
|
|
'https://login.microsoftonline.com/${ipaidporg}/oauth2/v2.0/'
|
|
'token',
|
|
'ipaidpuserinfoendpoint':
|
|
'https://graph.microsoft.com/oidc/userinfo',
|
|
'ipaidpkeysendpoint':
|
|
'https://login.microsoftonline.com/common/discovery/v2.0/keys',
|
|
'ipaidpscope': 'openid email',
|
|
'ipaidpsub': 'email',
|
|
},
|
|
'okta': {
|
|
'ipaidpauthendpoint':
|
|
'https://${ipaidpbaseurl}/oauth2/v1/authorize',
|
|
'ipaidpdevauthendpoint':
|
|
'https://${ipaidpbaseurl}/oauth2/v1/device/authorize',
|
|
'ipaidptokenendpoint':
|
|
'https://${ipaidpbaseurl}/oauth2/v1/token',
|
|
'ipaidpuserinfoendpoint':
|
|
'https://${ipaidpbaseurl}/oauth2/v1/userinfo',
|
|
'ipaidpscope': 'openid email',
|
|
'ipaidpsub': 'email'},
|
|
'keycloak': {
|
|
'ipaidpauthendpoint':
|
|
'https://${ipaidpbaseurl}/realms/${ipaidporg}/protocol/'
|
|
'openid-connect/auth',
|
|
'ipaidpdevauthendpoint':
|
|
'https://${ipaidpbaseurl}/realms/${ipaidporg}/protocol/'
|
|
'openid-connect/auth/device',
|
|
'ipaidptokenendpoint':
|
|
'https://${ipaidpbaseurl}/realms/${ipaidporg}/protocol/'
|
|
'openid-connect/token',
|
|
'ipaidpuserinfoendpoint':
|
|
'https://${ipaidpbaseurl}/realms/${ipaidporg}/protocol/'
|
|
'openid-connect/userinfo',
|
|
'ipaidpscope': 'openid email',
|
|
'ipaidpsub': 'email'},
|
|
}
|
|
|
|
takes_options = LDAPCreate.takes_options + (
|
|
StrEnum(
|
|
'ipaidpprovider?',
|
|
cli_name='provider',
|
|
label=_('IdP provider template'),
|
|
doc=_('Choose a pre-defined template to use'),
|
|
flags={'virtual_attribute', 'no_create', 'no_update', 'nosearch'},
|
|
values=tuple(idp_providers.keys()),
|
|
),
|
|
Str('ipaidporg?',
|
|
cli_name='organization',
|
|
label=_('Organization'),
|
|
doc=_('Organization ID or Realm name for IdP provider templates'),
|
|
flags={'virtual_attribute', 'no_create', 'no_update', 'nosearch'}),
|
|
Str('ipaidpbaseurl?',
|
|
cli_name='base_url',
|
|
label=_('Base URL'),
|
|
doc=_('Base URL for IdP provider templates'),
|
|
normalizer=normalize_baseurl,
|
|
flags={'virtual_attribute', 'no_create', 'no_update', 'nosearch'}),
|
|
)
|
|
|
|
def _convert_provider_to_endpoints(self, entry_attrs,
|
|
provider=None, elements=None):
|
|
"""
|
|
Converts provider options to auth-uri and token-uri
|
|
"""
|
|
if provider:
|
|
if provider not in self.idp_providers:
|
|
raise errors.ValidationError(
|
|
name='provider',
|
|
error=_('unknown provider')
|
|
)
|
|
# For each string in the template check if a variable
|
|
# is required, it is provided as an option
|
|
points = deepcopy(self.idp_providers[provider])
|
|
r = string.Template.pattern
|
|
for (k,v) in points.items():
|
|
# build list of variables to be replaced
|
|
subs = list(chain.from_iterable(
|
|
(filter(None, s) for s in r.findall(v))))
|
|
if subs:
|
|
for s in subs:
|
|
if s not in elements:
|
|
raise errors.ValidationError(
|
|
name=self.options[s].cli_name,
|
|
error=_('value is missing'))
|
|
points[k] = template_str(v, elements)
|
|
elif k in elements:
|
|
points[k] = elements[k]
|
|
|
|
entry_attrs.update(points)
|
|
|
|
def get_options(self):
|
|
# Some URIs are not mandatory as they can be built from the value of
|
|
# provider.
|
|
for option in super(idp_add, self).get_options():
|
|
if option.name in ('ipaidpauthendpoint', 'ipaidpdevauthendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpuserinfoendpoint',
|
|
'ipaidpkeysendpoint'):
|
|
yield option.clone(required=False, alwaysask=False)
|
|
else:
|
|
yield option
|
|
|
|
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
|
|
**options):
|
|
# The valid calls are
|
|
# ipa idp-add --provider provider IDP [client details]
|
|
# ipa idp-add --dev-auth-uri auth --token-uri token-uri IDP
|
|
auth = options.get('ipaidpauthendpoint')
|
|
devauth = options.get('ipaidpdevauthendpoint')
|
|
token = options.get('ipaidptokenendpoint')
|
|
userinfo = options.get('ipaidpuserinfoendpoint')
|
|
jwks = options.get('ipaidpkeysendpoint')
|
|
provider = options.get('ipaidpprovider')
|
|
|
|
# If the provider is supplied, reject individual endpoints
|
|
if any([devauth, auth, token, userinfo, jwks]):
|
|
if provider:
|
|
raise errors.MutuallyExclusiveError(
|
|
reason=_('cannot specify both individual endpoints '
|
|
'and IdP provider'))
|
|
|
|
# If there is no --provider, individual endpoints required
|
|
if not provider and not devauth:
|
|
raise errors.RequirementError(name='dev-auth-uri or provider')
|
|
if not provider and not auth:
|
|
raise errors.RequirementError(name='auth-uri or provider')
|
|
if not provider and not token:
|
|
raise errors.RequirementError(name='token-uri or provider')
|
|
if not provider and not userinfo:
|
|
raise errors.RequirementError(name='userinfo-uri or provider')
|
|
|
|
# if the command is called with --provider we need to add
|
|
# ipaidpdevauthendpoint, ipaidpauthendpoint, and ipaidptokenendpoint
|
|
# to the attrs list in order to display the resulting value in
|
|
# the command output
|
|
for endpoint in ['ipaidpauthendpoint', 'ipaidpdevauthendpoint',
|
|
'ipaidptokenendpoint', 'ipaidpuserinfoendpoint',
|
|
'ipaidpkeysendpoint']:
|
|
if endpoint not in attrs_list:
|
|
attrs_list.append(endpoint)
|
|
|
|
self._convert_provider_to_endpoints(entry_attrs,
|
|
provider=provider,
|
|
elements=options)
|
|
return dn
|
|
|
|
|
|
@register()
|
|
class idp_del(LDAPDelete):
|
|
__doc__ = _('Delete an Identity Provider reference.')
|
|
msg_summary = _('Deleted Identity Provider reference "%(value)s"')
|
|
|
|
|
|
@register()
|
|
class idp_mod(LDAPUpdate):
|
|
__doc__ = _('Modify an Identity Provider reference.')
|
|
msg_summary = _('Modified Identity Provider reference "%(value)s"')
|
|
|
|
|
|
@register()
|
|
class idp_find(LDAPSearch):
|
|
__doc__ = _('Search for Identity Provider references.')
|
|
msg_summary = ngettext(
|
|
'%(count)d Identity Provider reference matched',
|
|
'%(count)d Identity Provider references matched', 0
|
|
)
|
|
|
|
def get_options(self):
|
|
# do not propose --client-id or --secret in ipa idp-find
|
|
for option in super(idp_find, self).get_options():
|
|
if option.name in ('ipaidpclientsecret', 'ipaidpclientid'):
|
|
option = option.clone(flags={'no_option'})
|
|
|
|
yield option
|
|
|
|
|
|
@register()
|
|
class idp_show(LDAPRetrieve):
|
|
__doc__ = _('Display information about an Identity Provider '
|
|
'reference.')
|