From 2136bd5d00f7aed5ae722ff8253c2b74ba444972 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 3 May 2022 11:40:24 +0300 Subject: [PATCH] external-idp: add support to manage external IdP objects Fixes: https://pagure.io/freeipa/issue/8804 Fixes: https://pagure.io/freeipa/issue/8803 Signed-off-by: Alexander Bokovoy Signed-off-by: Florence Blanc-Renaud Reviewed-By: Francisco Trivino Reviewed-By: Sumit Bose Reviewed-By: Francisco Trivino Reviewed-By: Sumit Bose --- ACI.txt | 10 + API.txt | 152 +++++++++-- VERSION.m4 | 4 +- ipalib/constants.py | 1 + ipaserver/plugins/baseuser.py | 47 +++- ipaserver/plugins/config.py | 2 +- ipaserver/plugins/host.py | 4 +- ipaserver/plugins/idp.py | 442 ++++++++++++++++++++++++++++++++ ipaserver/plugins/krbtpolicy.py | 14 +- ipaserver/plugins/service.py | 2 +- ipaserver/plugins/stageuser.py | 13 + ipaserver/plugins/user.py | 8 + pylint_plugins.py | 1 + 13 files changed, 670 insertions(+), 30 deletions(-) create mode 100644 ipaserver/plugins/idp.py diff --git a/ACI.txt b/ACI.txt index e985461cd..e575d82a3 100644 --- a/ACI.txt +++ b/ACI.txt @@ -180,6 +180,16 @@ dn: cn=views,cn=accounts,dc=ipa,dc=example aci: (targetattr = "cn || createtimestamp || description || entryusn || gidnumber || ipaanchoruuid || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaGroupOverride)")(version 3.0;acl "permission:System: Read Group ID Overrides";allow (compare,read,search) userdn = "ldap:///all";) dn: cn=views,cn=accounts,dc=ipa,dc=example aci: (targetattr = "createtimestamp || description || entryusn || gecos || gidnumber || homedirectory || ipaanchoruuid || ipaoriginaluid || ipasshpubkey || loginshell || memberof || modifytimestamp || objectclass || uid || uidnumber || usercertificate")(targetfilter = "(objectclass=ipaUserOverride)")(version 3.0;acl "permission:System: Read User ID Overrides";allow (compare,read,search) userdn = "ldap:///all";) +dn: cn=idp,dc=ipa,dc=example +aci: (targetfilter = "(objectclass=ipaidp)")(version 3.0;acl "permission:System: Add External IdP server";allow (add) groupdn = "ldap:///cn=System: Add External IdP server,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=idp,dc=ipa,dc=example +aci: (targetfilter = "(objectclass=ipaidp)")(version 3.0;acl "permission:System: Delete External IdP server";allow (delete) groupdn = "ldap:///cn=System: Delete External IdP server,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=idp,dc=ipa,dc=example +aci: (targetattr = "cn || ipaidpauthendpoint || ipaidpclientid || ipaidpclientsecret || ipaidpdevauthendpoint || ipaidpissuerurl || ipaidpkeysendpoint || ipaidpscope || ipaidpsub || ipaidptokenendpoint || ipaidpuserinfoendpoint || objectclass")(targetfilter = "(objectclass=ipaidp)")(version 3.0;acl "permission:System: Modify External IdP server";allow (write) groupdn = "ldap:///cn=System: Modify External IdP server,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=idp,dc=ipa,dc=example +aci: (targetattr = "cn || createtimestamp || entryusn || ipaidpauthendpoint || ipaidpclientid || ipaidpdevauthendpoint || ipaidpissuerurl || ipaidpkeysendpoint || ipaidpscope || ipaidpsub || ipaidptokenendpoint || ipaidpuserinfoendpoint || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaidp)")(version 3.0;acl "permission:System: Read External IdP server";allow (compare,read,search) groupdn = "ldap:///cn=System: Read External IdP server,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=idp,dc=ipa,dc=example +aci: (targetattr = "cn || createtimestamp || entryusn || ipaidpauthendpoint || ipaidpclientid || ipaidpclientsecret || ipaidpdevauthendpoint || ipaidpissuerurl || ipaidpkeysendpoint || ipaidpscope || ipaidpsub || ipaidptokenendpoint || ipaidpuserinfoendpoint || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaidp)")(version 3.0;acl "permission:System: Read External IdP server client secret";allow (compare,read,search) groupdn = "ldap:///cn=System: Read External IdP server client secret,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=ranges,cn=etc,dc=ipa,dc=example aci: (targetattr = "cn || createtimestamp || entryusn || ipaautoprivategroups || ipabaseid || ipabaserid || ipaidrangesize || ipanttrusteddomainsid || iparangetype || ipasecondarybaserid || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaidrange)")(version 3.0;acl "permission:System: Read ID Ranges";allow (compare,read,search) userdn = "ldap:///all";) dn: cn=views,cn=accounts,dc=ipa,dc=example diff --git a/API.txt b/API.txt index 1f27dcc61..0b540c6ff 100644 --- a/API.txt +++ b/API.txt @@ -1098,7 +1098,7 @@ option: Int('ipasearchrecordslimit?', autofill=False, cli_name='searchrecordslim option: Int('ipasearchtimelimit?', autofill=False, cli_name='searchtimelimit') option: Str('ipaselinuxusermapdefault?', autofill=False) option: Str('ipaselinuxusermaporder?', autofill=False) -option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'disabled']) +option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp', u'disabled']) option: Bool('ipauserdefaultsubordinateid?', autofill=False, cli_name='user_default_subid') option: Str('ipauserobjectclasses*', autofill=False, cli_name='userobjectclasses') option: IA5Str('ipausersearchfields?', autofill=False, cli_name='usersearch') @@ -2477,7 +2477,7 @@ option: Bool('ipakrbokasdelegate?', cli_name='ok_as_delegate') option: Bool('ipakrboktoauthasdelegate?', cli_name='ok_to_auth_as_delegate') option: Bool('ipakrbrequirespreauth?', cli_name='requires_pre_auth') option: Str('ipasshpubkey*', cli_name='sshpubkey') -option: StrEnum('krbprincipalauthind*', cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('krbprincipalauthind*', cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: Str('l?', cli_name='locality') option: Str('macaddress*') option: Flag('no_members', autofill=True, default=False) @@ -2613,7 +2613,7 @@ option: Str('in_netgroup*', cli_name='in_netgroups') option: Str('in_role*', cli_name='in_roles') option: Str('in_sudorule*', cli_name='in_sudorules') option: Str('ipaassignedidview?', autofill=False) -option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: Str('l?', autofill=False, cli_name='locality') option: Str('macaddress*', autofill=False) option: Str('man_by_host*', cli_name='man_by_hosts') @@ -2653,7 +2653,7 @@ option: Bool('ipakrbokasdelegate?', autofill=False, cli_name='ok_as_delegate') option: Bool('ipakrboktoauthasdelegate?', autofill=False, cli_name='ok_to_auth_as_delegate') option: Bool('ipakrbrequirespreauth?', autofill=False, cli_name='requires_pre_auth') option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') -option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: Principal('krbprincipalname*', autofill=False) option: Str('l?', autofill=False, cli_name='locality') option: Str('macaddress*', autofill=False) @@ -3051,6 +3051,96 @@ option: Str('version?') output: Entry('result') output: Output('summary', type=[, ]) output: PrimaryKey('value') +command: idp_add/1 +args: 1,18,3 +arg: Str('cn', cli_name='name') +option: Str('addattr*', cli_name='addattr') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('ipaidpauthendpoint?', alwaysask=False, cli_name='auth_uri') +option: Str('ipaidpbaseurl?', cli_name='base_url') +option: Str('ipaidpclientid', cli_name='client_id') +option: Password('ipaidpclientsecret?', cli_name='secret', confirm=True) +option: Str('ipaidpdevauthendpoint?', alwaysask=False, cli_name='dev_auth_uri') +option: Str('ipaidpissuerurl?', cli_name='issuer_url') +option: Str('ipaidpkeysendpoint?', alwaysask=False, cli_name='keys_uri') +option: Str('ipaidporg?', cli_name='organization') +option: StrEnum('ipaidpprovider?', cli_name='provider', values=[u'google', u'github', u'microsoft', u'okta', u'keycloak']) +option: Str('ipaidpscope?', cli_name='scope') +option: Str('ipaidpsub?', cli_name='idp_user_id') +option: Str('ipaidptokenendpoint?', alwaysask=False, cli_name='token_uri') +option: Str('ipaidpuserinfoendpoint?', alwaysask=False, cli_name='userinfo_uri') +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Str('setattr*', cli_name='setattr') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[, ]) +output: PrimaryKey('value') +command: idp_del/1 +args: 1,2,3 +arg: Str('cn+', cli_name='name') +option: Flag('continue', autofill=True, cli_name='continue', default=False) +option: Str('version?') +output: Output('result', type=[]) +output: Output('summary', type=[, ]) +output: ListOfPrimaryKeys('value') +command: idp_find/1 +args: 1,17,4 +arg: Str('criteria?') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('cn?', autofill=False, cli_name='name') +option: Str('ipaidpauthendpoint?', autofill=False, cli_name='auth_uri') +option: Str('ipaidpclientid?', autofill=False, cli_name='client_id') +option: Password('ipaidpclientsecret?', autofill=False, cli_name='secret', confirm=True) +option: Str('ipaidpdevauthendpoint?', autofill=False, cli_name='dev_auth_uri') +option: Str('ipaidpissuerurl?', autofill=False, cli_name='issuer_url') +option: Str('ipaidpkeysendpoint?', autofill=False, cli_name='keys_uri') +option: Str('ipaidpscope?', autofill=False, cli_name='scope') +option: Str('ipaidpsub?', autofill=False, cli_name='idp_user_id') +option: Str('ipaidptokenendpoint?', autofill=False, cli_name='token_uri') +option: Str('ipaidpuserinfoendpoint?', autofill=False, cli_name='userinfo_uri') +option: Flag('pkey_only?', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Int('sizelimit?', autofill=False) +option: Int('timelimit?', autofill=False) +option: Str('version?') +output: Output('count', type=[]) +output: ListOfEntries('result') +output: Output('summary', type=[, ]) +output: Output('truncated', type=[]) +command: idp_mod/1 +args: 1,18,3 +arg: Str('cn', cli_name='name') +option: Str('addattr*', cli_name='addattr') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('delattr*', cli_name='delattr') +option: Str('ipaidpauthendpoint?', autofill=False, cli_name='auth_uri') +option: Str('ipaidpclientid?', autofill=False, cli_name='client_id') +option: Password('ipaidpclientsecret?', autofill=False, cli_name='secret', confirm=True) +option: Str('ipaidpdevauthendpoint?', autofill=False, cli_name='dev_auth_uri') +option: Str('ipaidpissuerurl?', autofill=False, cli_name='issuer_url') +option: Str('ipaidpkeysendpoint?', autofill=False, cli_name='keys_uri') +option: Str('ipaidpscope?', autofill=False, cli_name='scope') +option: Str('ipaidpsub?', autofill=False, cli_name='idp_user_id') +option: Str('ipaidptokenendpoint?', autofill=False, cli_name='token_uri') +option: Str('ipaidpuserinfoendpoint?', autofill=False, cli_name='userinfo_uri') +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Str('rename?', cli_name='rename') +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[, ]) +output: PrimaryKey('value') +command: idp_show/1 +args: 1,4,3 +arg: Str('cn', cli_name='name') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Flag('rights', autofill=True, default=False) +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[, ]) +output: PrimaryKey('value') command: idrange_add/1 args: 1,13,3 arg: Str('cn', cli_name='name') @@ -3236,16 +3326,18 @@ output: Output('result', type=[]) output: Output('summary', type=[, ]) output: PrimaryKey('value') command: krbtpolicy_mod/1 -args: 1,17,3 +args: 1,19,3 arg: Str('uid?', cli_name='user') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('delattr*', cli_name='delattr') option: Int('krbauthindmaxrenewableage_hardened?', autofill=False, cli_name='hardened_maxrenew') +option: Int('krbauthindmaxrenewableage_idp?', autofill=False, cli_name='idp_maxrenew') option: Int('krbauthindmaxrenewableage_otp?', autofill=False, cli_name='otp_maxrenew') option: Int('krbauthindmaxrenewableage_pkinit?', autofill=False, cli_name='pkinit_maxrenew') option: Int('krbauthindmaxrenewableage_radius?', autofill=False, cli_name='radius_maxrenew') option: Int('krbauthindmaxticketlife_hardened?', autofill=False, cli_name='hardened_maxlife') +option: Int('krbauthindmaxticketlife_idp?', autofill=False, cli_name='idp_maxlife') option: Int('krbauthindmaxticketlife_otp?', autofill=False, cli_name='otp_maxlife') option: Int('krbauthindmaxticketlife_pkinit?', autofill=False, cli_name='pkinit_maxlife') option: Int('krbauthindmaxticketlife_radius?', autofill=False, cli_name='radius_maxlife') @@ -4588,7 +4680,7 @@ option: StrEnum('ipakrbauthzdata*', cli_name='pac_type', values=[u'MS-PAC', u'PA option: Bool('ipakrbokasdelegate?', cli_name='ok_as_delegate') option: Bool('ipakrboktoauthasdelegate?', cli_name='ok_to_auth_as_delegate') option: Bool('ipakrbrequirespreauth?', cli_name='requires_pre_auth') -option: StrEnum('krbprincipalauthind*', cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('krbprincipalauthind*', cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Str('setattr*', cli_name='setattr') @@ -4724,7 +4816,7 @@ arg: Str('criteria?') option: Flag('all', autofill=True, cli_name='all', default=False) option: StrEnum('ipakrbauthzdata*', autofill=False, cli_name='pac_type', values=[u'MS-PAC', u'PAD', u'NONE']) option: Principal('krbcanonicalname?', autofill=False, cli_name='canonical_principal') -option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: Principal('krbprincipalname*', autofill=False, cli_name='principal') option: Str('man_by_host*', cli_name='man_by_hosts') option: Flag('no_members', autofill=True, default=True) @@ -4748,7 +4840,7 @@ option: StrEnum('ipakrbauthzdata*', autofill=False, cli_name='pac_type', values= option: Bool('ipakrbokasdelegate?', autofill=False, cli_name='ok_as_delegate') option: Bool('ipakrboktoauthasdelegate?', autofill=False, cli_name='ok_to_auth_as_delegate') option: Bool('ipakrbrequirespreauth?', autofill=False, cli_name='requires_pre_auth') -option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('krbprincipalauthind*', autofill=False, cli_name='auth_ind', values=[u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: Principal('krbprincipalname*', autofill=False, cli_name='principal') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) @@ -4976,7 +5068,7 @@ output: Entry('result') output: Output('summary', type=[, ]) output: PrimaryKey('value') command: stageuser_add/1 -args: 1,45,3 +args: 1,47,3 arg: Str('uid', cli_name='login') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) @@ -4993,10 +5085,12 @@ option: Int('gidnumber?') option: Str('givenname', cli_name='first') option: Str('homedirectory?', cli_name='homedir') option: Str('initials?', autofill=True) +option: Str('ipaidpconfiglink?', cli_name='idp') +option: Str('ipaidpsub?', cli_name='idp_user_id') option: Str('ipasshpubkey*', cli_name='sshpubkey') option: Str('ipatokenradiusconfiglink?', cli_name='radius') option: Str('ipatokenradiususername?', cli_name='radius_username') -option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: DateTime('krbpasswordexpiration?', cli_name='password_expiration') option: DateTime('krbprincipalexpiration?', cli_name='principal_expiration') option: Principal('krbprincipalname*', autofill=True, cli_name='principal') @@ -5082,7 +5176,7 @@ output: Output('result', type=[]) output: Output('summary', type=[, ]) output: ListOfPrimaryKeys('value') command: stageuser_find/1 -args: 1,60,4 +args: 1,62,4 arg: Str('criteria?') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('carlicense*', autofill=False) @@ -5103,13 +5197,15 @@ option: Str('in_role*', cli_name='in_roles') option: Str('in_subid*', cli_name='in_subids') option: Str('in_sudorule*', cli_name='in_sudorules') option: Str('initials?', autofill=False) +option: Str('ipaidpconfiglink?', autofill=False, cli_name='idp') +option: Str('ipaidpsub?', autofill=False, cli_name='idp_user_id') option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') -option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: DateTime('krbpasswordexpiration?', autofill=False, cli_name='password_expiration') option: DateTime('krbprincipalexpiration?', autofill=False, cli_name='principal_expiration') option: Principal('krbprincipalname*', autofill=False, cli_name='principal') @@ -5149,7 +5245,7 @@ output: ListOfEntries('result') output: Output('summary', type=[, ]) output: Output('truncated', type=[]) command: stageuser_mod/1 -args: 1,51,3 +args: 1,53,3 arg: Str('uid', cli_name='login') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) @@ -5166,6 +5262,8 @@ option: Int('gidnumber?', autofill=False) option: Str('givenname?', autofill=False, cli_name='first') option: Str('homedirectory?', autofill=False, cli_name='homedir') option: Str('initials?', autofill=False) +option: Str('ipaidpconfiglink?', autofill=False, cli_name='idp') +option: Str('ipaidpsub?', autofill=False, cli_name='idp_user_id') option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') @@ -5173,7 +5271,7 @@ option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') -option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: DateTime('krbpasswordexpiration?', autofill=False, cli_name='password_expiration') option: DateTime('krbprincipalexpiration?', autofill=False, cli_name='principal_expiration') option: Principal('krbprincipalname*', autofill=False, cli_name='principal') @@ -6156,7 +6254,7 @@ output: Entry('result') output: Output('summary', type=[, ]) output: PrimaryKey('value') command: user_add/1 -args: 1,46,3 +args: 1,48,3 arg: Str('uid', cli_name='login') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) @@ -6172,10 +6270,12 @@ option: Int('gidnumber?') option: Str('givenname', cli_name='first') option: Str('homedirectory?', cli_name='homedir') option: Str('initials?', autofill=True) +option: Str('ipaidpconfiglink?', cli_name='idp') +option: Str('ipaidpsub?', cli_name='idp_user_id') option: Str('ipasshpubkey*', cli_name='sshpubkey') option: Str('ipatokenradiusconfiglink?', cli_name='radius') option: Str('ipatokenradiususername?', cli_name='radius_username') -option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: DateTime('krbpasswordexpiration?', cli_name='password_expiration') option: DateTime('krbprincipalexpiration?', cli_name='principal_expiration') option: Principal('krbprincipalname*', autofill=True, cli_name='principal') @@ -6278,7 +6378,7 @@ output: Output('result', type=[]) output: Output('summary', type=[, ]) output: PrimaryKey('value') command: user_find/1 -args: 1,63,4 +args: 1,65,4 arg: Str('criteria?') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('carlicense*', autofill=False) @@ -6299,13 +6399,15 @@ option: Str('in_role*', cli_name='in_roles') option: Str('in_subid*', cli_name='in_subids') option: Str('in_sudorule*', cli_name='in_sudorules') option: Str('initials?', autofill=False) +option: Str('ipaidpconfiglink?', autofill=False, cli_name='idp') +option: Str('ipaidpsub?', autofill=False, cli_name='idp_user_id') option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') -option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: DateTime('krbpasswordexpiration?', autofill=False, cli_name='password_expiration') option: DateTime('krbprincipalexpiration?', autofill=False, cli_name='principal_expiration') option: Principal('krbprincipalname*', autofill=False, cli_name='principal') @@ -6348,7 +6450,7 @@ output: ListOfEntries('result') output: Output('summary', type=[, ]) output: Output('truncated', type=[]) command: user_mod/1 -args: 1,52,3 +args: 1,54,3 arg: Str('uid', cli_name='login') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) @@ -6365,6 +6467,8 @@ option: Int('gidnumber?', autofill=False) option: Str('givenname?', autofill=False, cli_name='first') option: Str('homedirectory?', autofill=False, cli_name='homedir') option: Str('initials?', autofill=False) +option: Str('ipaidpconfiglink?', autofill=False, cli_name='idp') +option: Str('ipaidpsub?', autofill=False, cli_name='idp_user_id') option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') @@ -6372,7 +6476,7 @@ option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') -option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'idp']) option: DateTime('krbpasswordexpiration?', autofill=False, cli_name='password_expiration') option: DateTime('krbprincipalexpiration?', autofill=False, cli_name='principal_expiration') option: Principal('krbprincipalname*', autofill=False, cli_name='principal') @@ -7023,6 +7127,12 @@ default: idoverrideuser_find/1 default: idoverrideuser_mod/1 default: idoverrideuser_remove_cert/1 default: idoverrideuser_show/1 +default: idp/1 +default: idp_add/1 +default: idp_del/1 +default: idp_find/1 +default: idp_mod/1 +default: idp_show/1 default: idrange/1 default: idrange_add/1 default: idrange_del/1 @@ -7329,10 +7439,10 @@ default: vaultcontainer_del/1 default: vaultcontainer_remove_owner/1 default: vaultcontainer_show/1 default: whoami/1 +capability: vault_aes_keywrap 2.246 capability: messages 2.52 capability: optional_uid_params 2.54 capability: permissions2 2.69 capability: primary_key_types 2.83 capability: datetime_values 2.84 capability: dns_name_values 2.88 -capability: vault_aes_keywrap 2.246 diff --git a/VERSION.m4 b/VERSION.m4 index 922e68300..1f07faeb7 100644 --- a/VERSION.m4 +++ b/VERSION.m4 @@ -86,8 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000) # # ######################################################## define(IPA_API_VERSION_MAJOR, 2) -# Last change: Add wrapping algorithm to vault archive/retrieve -define(IPA_API_VERSION_MINOR, 246) +# Last change: add idp API +define(IPA_API_VERSION_MINOR, 247) ######################################################## # Following values are auto-generated from values above diff --git a/ipalib/constants.py b/ipalib/constants.py index 681780041..0cabe3628 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -155,6 +155,7 @@ DEFAULT_CONFIG = ( ('container_ca_renewal', DN(('cn', 'ca_renewal'), ('cn', 'ipa'), ('cn', 'etc'))), ('container_subids', DN(('cn', 'subids'), ('cn', 'accounts'))), + ('container_idp', DN(('cn', 'idp'))), # Ports, hosts, and URIs: # Following values do not have any reasonable default. diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py index 14a71b221..684a65242 100644 --- a/ipaserver/plugins/baseuser.py +++ b/ipaserver/plugins/baseuser.py @@ -87,12 +87,21 @@ def validate_nsaccountlock(entry_attrs): raise errors.ValidationError(name='nsaccountlock', error=_('must be TRUE or FALSE')) + def radius_dn2pk(api, entry_attrs): cl = entry_attrs.get('ipatokenradiusconfiglink', None) if cl: pk = api.Object['radiusproxy'].get_primary_key_from_dn(cl[0]) entry_attrs['ipatokenradiusconfiglink'] = [pk] + +def idp_dn2pk(api, entry_attrs): + cl = entry_attrs.get('ipaidpconfiglink', None) + if cl: + pk = api.Object['idp'].get_primary_key_from_dn(cl[0]) + entry_attrs['ipaidpconfiglink'] = [pk] + + def convert_nsaccountlock(entry_attrs): if 'nsaccountlock' not in entry_attrs: entry_attrs['nsaccountlock'] = False @@ -161,7 +170,7 @@ class baseuser(LDAPObject): possible_objectclasses = [ 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', 'ipatokenradiusproxyuser', 'ipacertmapobject', - 'ipantuserattrs', + 'ipantuserattrs', 'ipaidpuser', ] disallow_object_classes = ['krbticketpolicyaux'] permission_filter_objectclasses = ['posixaccount'] @@ -172,6 +181,7 @@ class baseuser(LDAPObject): 'telephonenumber', 'title', 'memberof', 'nsaccountlock', 'memberofindirect', 'ipauserauthtype', 'userclass', 'ipatokenradiusconfiglink', 'ipatokenradiususername', + 'ipaidpconfiglink', 'ipaidpsub', 'krbprincipalexpiration', 'usercertificate;binary', 'krbprincipalname', 'krbcanonicalname', 'ipacertmapdata', 'ipantlogonscript', 'ipantprofilepath', @@ -360,7 +370,8 @@ class baseuser(LDAPObject): cli_name='user_auth_type', label=_('User authentication types'), doc=_('Types of supported user authentication'), - values=(u'password', u'radius', u'otp', u'pkinit', u'hardened'), + values=(u'password', u'radius', u'otp', u'pkinit', u'hardened', + u'idp'), ), Str('userclass*', cli_name='class', @@ -376,6 +387,15 @@ class baseuser(LDAPObject): cli_name='radius_username', label=_('RADIUS proxy username'), ), + Str('ipaidpconfiglink?', + cli_name='idp', + label=_('External IdP configuration'), + ), + Str('ipaidpsub?', + cli_name='idp_user_id', + label=_('External IdP user identifier'), + doc=_('A string that identifies the user at external IdP'), + ), Str('departmentnumber*', label=_('Department Number'), ), @@ -544,6 +564,9 @@ class baseuser_add(LDAPCreate): if entry_attrs.get('ipauserauthtype', None): add_missing_object_class(ldap, u'ipauserauthtypeclass', dn, entry_attrs, update=False) + if entry_attrs.get('ipaidpconfiglink', None): + add_missing_object_class(ldap, 'ipaidpuser', dn, + entry_attrs, update=False) def post_common_callback(self, ldap, dn, entry_attrs, *keys, **options): assert isinstance(dn, DN) @@ -553,6 +576,7 @@ class baseuser_add(LDAPCreate): if 'nsaccountlock' in entry_attrs: convert_nsaccountlock(entry_attrs) radius_dn2pk(self.api, entry_attrs) + idp_dn2pk(self.api, entry_attrs) class baseuser_del(LDAPDelete): @@ -637,7 +661,8 @@ class baseuser_mod(LDAPUpdate): def check_objectclass(self, ldap, dn, entry_attrs): # Some attributes may require additional object classes special_attrs = {'ipasshpubkey', 'ipauserauthtype', 'userclass', - 'ipatokenradiusconfiglink', 'ipatokenradiususername'} + 'ipatokenradiusconfiglink', 'ipatokenradiususername', + 'ipaidpconfiglink'} if special_attrs.intersection(entry_attrs): if 'objectclass' in entry_attrs: obj_classes = entry_attrs['objectclass'] @@ -666,6 +691,15 @@ class baseuser_mod(LDAPUpdate): answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl) entry_attrs['ipatokenradiusconfiglink'] = answer + if 'ipaidpconfiglink' in entry_attrs: + cl = entry_attrs['ipaidpconfiglink'] + if cl: + if 'ipaidpuser' not in obj_classes: + entry_attrs['objectclass'].append('ipaidpuser') + + answer = self.api.Object['idp'].get_dn_if_exists(cl) + entry_attrs['ipaidpconfiglink'] = answer + # Note: we could have used the method add_missing_object_class # but since the data is already fetched and lowercased in # obj_classes, it is more efficient to use the same approach @@ -708,6 +742,7 @@ class baseuser_mod(LDAPUpdate): convert_sshpubkey_post(entry_attrs) remove_sshpubkey_from_output_post(self.context, entry_attrs) radius_dn2pk(self.api, entry_attrs) + idp_dn2pk(self.api, entry_attrs) class baseuser_find(LDAPSearch): """ @@ -732,6 +767,11 @@ class baseuser_find(LDAPSearch): if cl in options: newoptions[cl] = self.api.Object['radiusproxy'].get_dn(options[cl]) + # Ensure that the IdP config link is a dn, not just the name + cl = 'ipaidpconfiglink' + if cl in options: + newoptions[cl] = self.api.Object['idp'].get_dn(options[cl]) + def pre_common_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options): add_sshpubkey_to_attrs_pre(self.context, attrs_list) @@ -761,6 +801,7 @@ class baseuser_show(LDAPRetrieve): convert_sshpubkey_post(entry_attrs) remove_sshpubkey_from_output_post(self.context, entry_attrs) radius_dn2pk(self.api, entry_attrs) + idp_dn2pk(self.api, entry_attrs) class baseuser_add_manager(LDAPAddMember): diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py index 24446beb0..4035f7e8c 100644 --- a/ipaserver/plugins/config.py +++ b/ipaserver/plugins/config.py @@ -269,7 +269,7 @@ class config(LDAPObject): label=_('Default user authentication types'), doc=_('Default types of supported user authentication'), values=(u'password', u'radius', u'otp', - u'pkinit', u'hardened', u'disabled'), + u'pkinit', u'hardened', u'idp', u'disabled'), ), Bool('ipauserdefaultsubordinateid?', cli_name='user_default_subid', diff --git a/ipaserver/plugins/host.py b/ipaserver/plugins/host.py index 41fa933e2..2470ec4fd 100644 --- a/ipaserver/plugins/host.py +++ b/ipaserver/plugins/host.py @@ -601,9 +601,11 @@ class host(LDAPObject): " Use 'pkinit' to allow PKINIT-based 2FA authentications." " Use 'hardened' to allow brute-force hardened password" " authentication by SPAKE or FAST." + " Use 'idp' to allow External Identity Provider" + " authentications." " With no indicator specified," " all authentication mechanisms are allowed."), - values=(u'radius', u'otp', u'pkinit', u'hardened'), + values=(u'radius', u'otp', u'pkinit', u'hardened', u'idp'), ), ) + ticket_flags_params diff --git a/ipaserver/plugins/idp.py b/ipaserver/plugins/idp.py new file mode 100644 index 000000000..2a3cd9f28 --- /dev/null +++ b/ipaserver/plugins/idp.py @@ -0,0 +1,442 @@ +# +# 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 Servers +""") + _(""" +Manage External Identity Provider Servers. +""") + _(""" +IPA supports the use of an external Identity Provider for Oauth2.0 Device Flow +authentication. +""") + _(""" +EXAMPLES: +""") + _(""" + Add a new external Identity Provider server: + 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 server using github predefined endpoints: + ipa idp-add MyIdp --client-id jhkQty13 --provider github --secret +""") + _(""" + Find all external Identity Provider servers whose entries include the string + "test.com": + ipa idp-find test.com +""") + _(""" + Examine the configuration of an external Identity Provider server: + ipa idp-show MyIdP +""") + _(""" + Change the secret: + ipa idp-mod MyIdP --secret +""") + _(""" + Delete an external Identity Provider server: + 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 server') + object_name_plural = _('Identity Provider servers') + 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 servers') + label_singular = _('Identity Provider server') + + takes_params = ( + Str('cn', + cli_name='name', + label=_('Identity Provider server 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 server.') + msg_summary = _('Added Identity Provider server "%(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) + 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 server.') + msg_summary = _('Deleted Identity Provider server "%(value)s"') + + +@register() +class idp_mod(LDAPUpdate): + __doc__ = _('Modify an Identity Provider server.') + msg_summary = _('Modified Identity Provider server "%(value)s"') + + +@register() +class idp_find(LDAPSearch): + __doc__ = _('Search for Identity Provider servers.') + msg_summary = ngettext( + '%(count)d Identity Provider server matched', + '%(count)d Identity Provider servers 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 ' + 'server.') diff --git a/ipaserver/plugins/krbtpolicy.py b/ipaserver/plugins/krbtpolicy.py index b01c44e93..610e1cad7 100644 --- a/ipaserver/plugins/krbtpolicy.py +++ b/ipaserver/plugins/krbtpolicy.py @@ -75,7 +75,7 @@ _default_values = { # These attributes never have non-optional values, so they should be # ignored in post callbacks _option_based_attrs = ('krbauthindmaxticketlife', 'krbauthindmaxrenewableage') -_supported_options = ('otp', 'radius', 'pkinit', 'hardened') +_supported_options = ('otp', 'radius', 'pkinit', 'hardened', 'idp') @register() class krbtpolicy(baseldap.LDAPObject): @@ -187,6 +187,18 @@ class krbtpolicy(baseldap.LDAPObject): label=_('Hardened max renew'), doc=_('Hardened ticket maximum renewable age (seconds)'), minvalue=1), + Int('krbauthindmaxticketlife_idp?', + cli_name='idp_maxlife', + label=_('IdP max life'), + doc=_('External Identity Provider ticket maximum ticket life ' + '(seconds)'), + minvalue=1), + Int('krbauthindmaxrenewableage_idp?', + cli_name='idp_maxrenew', + label=_('IdP max renew'), + doc=_('External Identity Provider ticket maximum renewable ' + 'age (seconds)'), + minvalue=1), ) def get_dn(self, *keys, **kwargs): diff --git a/ipaserver/plugins/service.py b/ipaserver/plugins/service.py index 498f5e444..b4fe542ae 100644 --- a/ipaserver/plugins/service.py +++ b/ipaserver/plugins/service.py @@ -576,7 +576,7 @@ class service(LDAPObject): " authentication by SPAKE or FAST." " With no indicator specified," " all authentication mechanisms are allowed."), - values=(u'radius', u'otp', u'pkinit', u'hardened'), + values=(u'radius', u'otp', u'pkinit', u'hardened', u'idp'), ), ) + ticket_flags_params diff --git a/ipaserver/plugins/stageuser.py b/ipaserver/plugins/stageuser.py index 2c35a8ecc..b3c667937 100644 --- a/ipaserver/plugins/stageuser.py +++ b/ipaserver/plugins/stageuser.py @@ -388,6 +388,19 @@ class stageuser_add(baseuser_add): answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl) entry_attrs['ipatokenradiusconfiglink'] = answer + if 'ipaidpconfiglink' in entry_attrs: + cl = entry_attrs['ipaidpconfiglink'] + if cl: + if 'objectclass' not in entry_attrs: + _entry = ldap.get_entry(dn, ['objectclass']) + entry_attrs['objectclass'] = _entry['objectclass'] + + if 'ipaidpuser' not in entry_attrs['objectclass']: + entry_attrs['objectclass'].append('ipaidpuser') + + answer = self.api.Object['idp'].get_dn_if_exists(cl) + entry_attrs['ipaidpconfiglink'] = answer + self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, **options) diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py index ffca80782..67f3d102e 100644 --- a/ipaserver/plugins/user.py +++ b/ipaserver/plugins/user.py @@ -626,6 +626,14 @@ class user_add(baseuser_add): answer = self.api.Object['radiusproxy'].get_dn_if_exists(rcl) entry_attrs['ipatokenradiusconfiglink'] = answer + rcl = entry_attrs.get('ipaidpconfiglink', None) + if rcl: + if 'ipaidpuser' not in entry_attrs['objectclass']: + entry_attrs['objectclass'].append('ipaidpuser') + + answer = self.api.Object['idp'].get_dn_if_exists(rcl) + entry_attrs['ipaidpconfiglink'] = answer + self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, **options) diff --git a/pylint_plugins.py b/pylint_plugins.py index cc9397427..c352e2e4a 100644 --- a/pylint_plugins.py +++ b/pylint_plugins.py @@ -425,6 +425,7 @@ AstroidBuilder(MANAGER).string_build(textwrap.dedent( api.env.container_hbacservicegroup = DN() api.env.container_host = DN() api.env.container_hostgroup = DN() + api.env.container_idp = DN() api.env.container_locations = DN() api.env.container_masters = DN() api.env.container_netgroup = DN()