diff --git a/patterns/1f0702aaabef60a615abf41f_resources/resources.zip b/patterns/1f0702aaabef60a615abf41f_resources/resources.zip index 5b1395f..1c6f45e 100644 Binary files a/patterns/1f0702aaabef60a615abf41f_resources/resources.zip and b/patterns/1f0702aaabef60a615abf41f_resources/resources.zip differ diff --git a/patterns/204c22beaccdfd22727af378_labels/labels.zip b/patterns/204c22beaccdfd22727af378_labels/labels.zip index 9ed6da8..a6c0bff 100644 Binary files a/patterns/204c22beaccdfd22727af378_labels/labels.zip and b/patterns/204c22beaccdfd22727af378_labels/labels.zip differ diff --git a/patterns/204c22beaccdfd22727af378_template/webdata.zip b/patterns/204c22beaccdfd22727af378_template/webdata.zip index 8cd2145..dcfc2c1 100644 Binary files a/patterns/204c22beaccdfd22727af378_template/webdata.zip and b/patterns/204c22beaccdfd22727af378_template/webdata.zip differ diff --git a/patterns/2cdd910036aa06b102863a4f_scriptFile/checkLoa.gy b/patterns/2cdd910036aa06b102863a4f_scriptFile/checkLoa.groovy similarity index 55% rename from patterns/2cdd910036aa06b102863a4f_scriptFile/checkLoa.gy rename to patterns/2cdd910036aa06b102863a4f_scriptFile/checkLoa.groovy index a0e05ac..58cbde7 100644 --- a/patterns/2cdd910036aa06b102863a4f_scriptFile/checkLoa.gy +++ b/patterns/2cdd910036aa06b102863a4f_scriptFile/checkLoa.groovy @@ -1,264 +1,285 @@ -import org.codehaus.groovy.runtime.StackTraceUtils -import groovy.xml.XmlSlurper - -def getUserAGOVLoiRoles() { - // we take the roles from actualRoles - return request.getActualRoles().findAll { role -> role.startsWith('AGOV-Loi.') }.collect({ role -> role.substring(9) }) -} - -def getUserAGOVRecoveryRoles() { - // set attibutes from DTO: -> AGOV - def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - return list.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' }.collect({ node -> node.name.text() }) -} - -def getUserAGOVLoiIdVerification() { - // set attibutes from DTO: -> idVerification - def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text().contains('AGOV-Loi,')}.collect({ node -> node.value.text()}) -} - -def getUserAGOVLoiIdVerification(level) { - // set attibutes from DTO: -> idVerification - def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,level' + level}.collect({ node -> node.value.text()}) -} - -def getUserAGOVLoiValidFrom(level) { - // set attibutes from DTO: -> validFrom - def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validFrom?.text() -} - -def getUserAGOVLoiValidTo(level) { - // set attibutes from DTO: -> validTo - def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validTo?.text() -} - -def getUserIdVerificationForRecovery() { - // application is AGOV-AccountStatus - def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text() - - if (!result) { - // fallback if not explicitly set - def currentLoaRole = getUserAGOVLoiRoles()?.sort()?.last() ?: 'level100' - def chDomicile = list.country.text() == 'ch' - def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text() - switch (currentLoaRole) { - case 'level100': - result = chDomicile ? 'SimpleLetter' : 'Video' - break - case 'level200': - result = chDomicile ? 'Bmid' : 'Video' - break - case 'level300': - case 'level400': - result = chDomicile ? lastIdVerification : 'Video' - break - default: - LOG.warn("unexpected loa on account: ${currentLoaRole}") - // safest default, should work in any case - result = 'Video' - } - LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})") - } - return result -} - -def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber) { - def result = 'urn:qa.agov.ch:names:tc:ac:classes:' - - switch (idVerification) { - case 'None': - result = result.concat('100') - break - case 'SimpleLetter': - result = result.concat('200') - break - case 'Video': - case 'VideoSelfPaid': - case 'Bmid': - case 'BmidSelfPaid': - case 'Counter': - result = result.concat((highestRoleLevelNumber == 400) ? '400' : '300') - break - default: - LOG.warn("unexpected idVerification for recovery on account: ${idVerification}") - // safest default, should work in any case - result = result.concat('' + highestRoleLevelNumber) - } - - return result -} - -def getUserMustRecoverValidFrom() { - // set attibutes from DTO: -> validFrom - def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) - def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'} - return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : '' -} - -// Accounting -def requester = session['ch.nevis.auth.saml.request.scoping.requesterId'] ?: 'unknown' -def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown' -def requestedAq = session['agov.requestedRoleLevel'] ?: 'unknown' -def user = session['ch.adnovum.nevisidm.user.extId'] ?: 'unknown' -def credentialType = session['authenticatedWith'] ?: 'unknown' -def sourceIp = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown' -def userAgent = request.getLoginContext()['connection.HttpHeader.user-agent'] ?: request.getLoginContext()['connection.HttpHeader.User-Agent'] ?: 'unknown' - -try { - // beef - def session = request.getAuthSession(true) - def highestRoleLevelNumber = 0 - def requestedRoleLevelNumber = session.get('agov.requestedRoleLevel').toInteger() - def adressVerificationList = getUserAGOVLoiIdVerification('200') - def adressVerification = 'None' - if (adressVerificationList && !adressVerificationList.isEmpty()) { - adressVerification = adressVerificationList[0] - } - def authenticationMethod = session.get('authenticatedWith') - - LOG.debug('CheckLoa: Requested role level '+ requestedRoleLevelNumber) - LOG.debug('CheckLoa: idVerification: ' + getUserAGOVLoiIdVerification()) - LOG.debug('CheckLoa: adressVerification : ' + adressVerification) - - def idVerificationMethodList = getUserAGOVLoiIdVerification() - - session.setAttribute('idVerification', idVerificationMethodList.isEmpty() ? 'None' : idVerificationMethodList.last()) - session.setAttribute('agov.adressVerification', '' + adressVerification) - - - if (requestedRoleLevelNumber == 0) { - // AuthnFailed_Zero_RoleLvl - response.setResult('error'); - return - } - - if (session.get('ch.adnovum.nevisidm.profileExtId') == '') { - LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='Account without Profile', SourceIp=${sourceIp}, UserAgent='${userAgent}'") - - session.setAttribute('contextClassRefToSet', 'urn:qa.agov.ch:names:tc:ac:classes:100') - - // if the account has no profile, we must not return address or svnr - session.setAttribute('agov.appAddressRequired', 'false') - session.setAttribute('agov.appSvnrAllowed', 'false') - - response.setResult('ok') - return - } - - // Transform sex to number - if(session.get('ch.nevis.idm.User.gender') == 'MALE'){ - session.setAttribute('ch.nevis.idm.User.gender', '1') - } - if(session.get('ch.nevis.idm.User.gender') == 'FEMALE'){ - session.setAttribute('ch.nevis.idm.User.gender', '2') - } - if(session.get('ch.nevis.idm.User.gender') == 'OTHER'){ - session.setAttribute('ch.nevis.idm.User.gender', '3') - } - - for (String role : getUserAGOVLoiRoles()) { - if (role.startsWith('level')) { - def roleLevel = role.substring(5) - int roleLevelNumber = Integer.parseInt(roleLevel) - - if (highestRoleLevelNumber< roleLevelNumber) { - highestRoleLevelNumber=roleLevelNumber - } - } - } - - LOG.debug('CheckLoa: Highest role Level ' + highestRoleLevelNumber.toString() +' contextclassref ' + requestedRoleLevelNumber.toString()) - LOG.debug('CheckLoa: Compare ' + (highestRoleLevelNumber>=requestedRoleLevelNumber)) - - //set attribute Actual Role Level - session.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber) - LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber) - - if (highestRoleLevelNumber > 0) { - // set attribute contextClassRefToSet - session.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:' .concat(highestRoleLevelNumber.toString())) - } else { - // by default 100 - session.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:100' ) - } - - // no login for users with a recovery role - def recoveryRoleList = getUserAGOVRecoveryRoles() - - if (recoveryRoleList.contains('mustRecover')) { - session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover') - session.setAttribute('agov.recovery.authenticatedWith', session.getAttribute('authenticatedWith') ?: 'unknown' ) - - def origIdVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()) ?: 'None' - def idVerification = getUserIdVerificationForRecovery() ?: origIdVerification - session.setAttribute('agov.recovery.currentIdVerification', '' + idVerification ) - - // align currentAgovAq with the method selected for idVerification - def currentAgovAqForRecovery = getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber) - session.setAttribute('agov.recovery.currentAgovAq', '' + currentAgovAqForRecovery) - - def validFrom = getUserMustRecoverValidFrom() ?: '' - session.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + validFrom ) - - LOG.debug("CheckLoa: mustRecover: origIdVerification=${origIdVerification}, idVerification=${idVerification}, currentAgovAqForRecovery=${currentAgovAqForRecovery}") - - response.setResult('exit.2') - return - - } else if (recoveryRoleList.contains('recovery')) { - if (recoveryRoleList.contains('recoveryCascade')) { - session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade') - } else { - session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery') - } - session.setAttribute('agov.recovery.authenticatedWith', session.getAttribute('authenticatedWith') ?: 'unknown') - session.setAttribute('agov.recovery.currentAgovAq', session.getAttribute('contextClassRefToSet') ?: 'urn:qa.agov.ch:names:tc:ac:classes:100' ) - LOG.debug('CheckLoa: idVerification2= '+ getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString())) - def idVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()) - session.setAttribute('agov.recovery.currentIdVerification', (idVerification.isEmpty() ? 'None' : idVerification.first())) - def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString())) ?: '' - session.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', validFrom) - - response.setResult('exit.2') - return - } - - - if (highestRoleLevelNumber>=requestedRoleLevelNumber) { - - // set attribute ValidFrom and ValidTo (only for higher than 100) - if (highestRoleLevelNumber > 100) { - def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString())) - def validTo = getUserAGOVLoiValidTo('level'.concat(highestRoleLevelNumber.toString())) - - LOG.debug('CheckLoa: ValidFrom :' + validFrom) - LOG.debug('CheckLoa: ValidTo :' + validTo) - - if(validFrom != '') { - session.setAttribute('ValidFrom', '' + validFrom) - } - if(validTo != '') { - session.setAttribute('ValidTo', '' + validTo) - } - } - response.setResult('ok') - return; - } else { - // Insufficient_LoaInfo - response.setResult('exit.1'); - return; - } -} catch (Exception ex) { - LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='exception occured: ${ex}', SourceIp=${sourceIp}, UserAgent='${userAgent}'") - ex = StackTraceUtils.sanitize(ex) - def affectedLines = ex.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" } - LOG.error("FATAL: Script failure (at lines: ${affectedLines})", ex) - // AuthnFailed_Zero_RoleLvl - response.setResult('error'); - return; -} \ No newline at end of file +import org.codehaus.groovy.runtime.StackTraceUtils +import groovy.xml.XmlSlurper + +def getUserAGOVLoiRoles() { + // we take the roles from actualRoles + return request.getActualRoles().findAll { role -> role.startsWith('AGOV-Loi.') }.collect({ role -> role.substring(9) }) +} + +def getUserAGOVRecoveryRoles() { + // set attibutes from DTO: -> AGOV + def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + return list.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' }.collect({ node -> node.name.text() }) +} + +def getUserAGOVLoiIdVerification() { + // set attibutes from DTO: -> idVerification + def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text().contains('AGOV-Loi,')}.collect({ node -> node.value.text()}) +} + +def getUserAGOVLoiIdVerification(level) { + // set attibutes from DTO: -> idVerification + def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,level' + level}.collect({ node -> node.value.text()}) +} + +def getUserAGOVLoiValidFrom(level) { + // set attibutes from DTO: -> validFrom + def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validFrom?.text() +} + +def getUserAGOVLoiValidTo(level) { + // set attibutes from DTO: -> validTo + def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validTo?.text() +} + +def getUserIdVerificationForRecovery() { + // application is AGOV-AccountStatus + def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text() + + if (!result) { + // fallback if not explicitly set + def currentLoaRole = getUserAGOVLoiRoles()?.sort()?.last() ?: 'level100' + def chDomicile = list.country.text() == 'ch' + def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text() + switch (currentLoaRole) { + case 'level100': + result = chDomicile ? 'SimpleLetter' : 'Video' + break + case 'level200': + result = chDomicile ? 'Bmid' : 'Video' + break + case 'level300': + case 'level400': + result = chDomicile ? lastIdVerification : 'Video' + break + default: + LOG.warn("unexpected loa on account: ${currentLoaRole}") + // safest default, should work in any case + result = 'Video' + } + LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})") + } + return result +} + +def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber) { + def result = 'urn:qa.agov.ch:names:tc:ac:classes:' + + switch (idVerification) { + case 'None': + result = result.concat('100') + break + case 'SimpleLetter': + result = result.concat('200') + break + case 'Video': + case 'VideoSelfPaid': + case 'Bmid': + case 'BmidSelfPaid': + case 'Counter': + result = result.concat((highestRoleLevelNumber == 400) ? '400' : '300') + break + case 'Eid': + result = result.concat('400') + break + default: + LOG.warn("unexpected idVerification for recovery on account: ${idVerification}") + // safest default, should work in any case + result = result.concat('' + highestRoleLevelNumber) + } + + return result +} + +def getUserMustRecoverValidFrom() { + // set attibutes from DTO: -> validFrom + def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto')) + def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'} + return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : '' +} + +// Accounting +def requester = session['ch.nevis.auth.saml.request.scoping.requesterId'] ?: 'unknown' +def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown' +def requestedAq = session['agov.requestedRoleLevel'] ?: 'unknown' +def user = session['ch.adnovum.nevisidm.user.extId'] ?: 'unknown' +def credentialType = session['authenticatedWith'] ?: 'unknown' +def sourceIp = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown' +def userAgent = request.getLoginContext()['connection.HttpHeader.user-agent'] ?: request.getLoginContext()['connection.HttpHeader.User-Agent'] ?: 'unknown' + +try { + // beef + def s = request.getAuthSession(true) + def highestRoleLevelNumber = 0 + + if (!session.get('agov.requestedRoleLevel')) { + LOG.error("IDP: internal error: agov.requestedRoleLevel not set in session") + response.setResult('error'); + return + } + def requestedRoleLevelNumber = session.get('agov.requestedRoleLevel').toInteger() + + def authenticationMethod = session.get('authenticatedWith') + if (!authenticationMethod) { + LOG.error("IDP: internal error: authenticationMethod not set in session") + response.setResult('error'); + return + } + + // data transformations needed for SAML and OIDC + // Transform sex to number + if(session.get('ch.nevis.idm.User.gender') == 'MALE'){ + s.setAttribute('ch.nevis.idm.User.gender', '1') + } + if(session.get('ch.nevis.idm.User.gender') == 'FEMALE'){ + s.setAttribute('ch.nevis.idm.User.gender', '2') + } + if(s.get('ch.nevis.idm.User.gender') == 'OTHER'){ + session.setAttribute('ch.nevis.idm.User.gender', '3') + } + + + // handle accounts qa attributes, and set them in session + // account itself, only needed if not authenticated with e-ID + if (!'urn:qa.agov.ch:names:tc:authfactor:eid'.equalsIgnoreCase(authenticationMethod)) { + def idVerificationList = getUserAGOVLoiIdVerification() + def idVerification = 'None' + if (idVerificationList && !idVerificationList.isEmpty()) { + idVerification = idVerificationList.last() + } + s.setAttribute('idVerification', idVerification) + + // contextClassRefToSet based on highest level-role assigned to default profile + for (String role : getUserAGOVLoiRoles()) { + if (role.startsWith('level')) { + def roleLevel = role.substring(5) + int roleLevelNumber = Integer.parseInt(roleLevel) + + if (highestRoleLevelNumber< roleLevelNumber) { + highestRoleLevelNumber=roleLevelNumber + } + } + } + + LOG.debug('CheckLoa: Highest role Level ' + highestRoleLevelNumber.toString() +' contextclassref ' + requestedRoleLevelNumber.toString()) + LOG.debug('CheckLoa: Compare ' + (highestRoleLevelNumber>=requestedRoleLevelNumber)) + + //set attribute Actual Role Level + s.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber) + LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber) + + // set attribute ValidFrom and ValidTo (only for higher than 100) + if (highestRoleLevelNumber > 100) { + def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString())) + def validTo = getUserAGOVLoiValidTo('level'.concat(highestRoleLevelNumber.toString())) + + LOG.debug('CheckLoa: ValidFrom :' + validFrom) + LOG.debug('CheckLoa: ValidTo :' + validTo) + + if(validFrom != '') { + s.setAttribute('ValidFrom', '' + validFrom) + } + if(validTo != '') { + s.setAttribute('ValidTo', '' + validTo) + } + } + if (highestRoleLevelNumber > 0) { + // set attribute contextClassRefToSet + s.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:' .concat(highestRoleLevelNumber.toString())) + } else { + // by default 100 + s.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:100' ) + } + } + // address related, needed in any case (also e-ID) + def adressVerificationList = getUserAGOVLoiIdVerification('200') + def adressVerification = 'None' + if (adressVerificationList && !adressVerificationList.isEmpty()) { + adressVerification = adressVerificationList[0] + } + s.setAttribute('agov.adressVerification', '' + adressVerification) + + if (!session.get('ch.adnovum.nevisidm.profileExtId')) { + LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='Account without Profile', SourceIp=${sourceIp}, UserAgent='${userAgent}'") + + // if the account has no profile, we must not return address or svnr + s.setAttribute('agov.appAddressRequired', 'false') + s.setAttribute('agov.appSvnrAllowed', 'false') + + response.setResult('ok') + return + } + + // no login for users with a recovery role (but onyl when not logging in with e-Id) + // TODO/haburger/2025-07-01: automatic recovery if logging in with e-Id + if (!'urn:qa.agov.ch:names:tc:authfactor:eid'.equalsIgnoreCase(authenticationMethod)) { + // no login for users with a recovery role + def recoveryRoleList = getUserAGOVRecoveryRoles() + + if (recoveryRoleList.contains('mustRecover')) { + s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover') + s.setAttribute('agov.recovery.authenticatedWith', session.getAttribute('authenticatedWith') ?: 'unknown' ) + + def origIdVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()) ?: 'None' + def idVerification = getUserIdVerificationForRecovery() ?: origIdVerification + s.setAttribute('agov.recovery.currentIdVerification', '' + idVerification ) + + // align currentAgovAq with the method selected for idVerification + def currentAgovAqForRecovery = getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber) + s.setAttribute('agov.recovery.currentAgovAq', '' + currentAgovAqForRecovery) + + def validFrom = getUserMustRecoverValidFrom() ?: '' + s.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + validFrom ) + + LOG.debug("CheckLoa: mustRecover: origIdVerification=${origIdVerification}, idVerification=${idVerification}, currentAgovAqForRecovery=${currentAgovAqForRecovery}") + + response.setResult('exit.2') + return + + } else if (recoveryRoleList.contains('recovery')) { + if (recoveryRoleList.contains('recoveryCascade')) { + s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade') + } else { + s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery') + } + s.setAttribute('agov.recovery.authenticatedWith', session.getAttribute('authenticatedWith') ?: 'unknown') + s.setAttribute('agov.recovery.currentAgovAq', session.getAttribute('contextClassRefToSet') ?: 'urn:qa.agov.ch:names:tc:ac:classes:100' ) + LOG.debug('CheckLoa: idVerification2= '+ getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString())) + def idVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()) + s.setAttribute('agov.recovery.currentIdVerification', (idVerification.isEmpty() ? 'None' : idVerification.first())) + def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString())) ?: '' + s.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', validFrom) + + response.setResult('exit.2') + return + } + } else { + // authenticated with e-ID, we adjust highestRoleLevelNumber to e-ID login + highestRoleLevelNumber = 500 + s.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber) + LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber) + } + + // verifiy that AQ level is high enough + if (highestRoleLevelNumber>=requestedRoleLevelNumber) { + response.setResult('ok') + return; + } else { + // Insufficient_LoaInfo + response.setResult('exit.1'); + return; + } +} catch (Exception ex) { + LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='exception occured: ${ex}', SourceIp=${sourceIp}, UserAgent='${userAgent}'") + ex = StackTraceUtils.sanitize(ex) + def affectedLines = ex.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" } + LOG.error("FATAL: Script failure (at lines: ${affectedLines})", ex) + // AuthnFailed_Zero_RoleLvl + response.setResult('error'); + return; +} diff --git a/patterns/306ce091fd87bad6174d9e8b_resources/eid_compare_and_update_idm_attributes.groovy b/patterns/306ce091fd87bad6174d9e8b_resources/eid_compare_and_update_idm_attributes.groovy index aab722c..e3df4d6 100644 --- a/patterns/306ce091fd87bad6174d9e8b_resources/eid_compare_and_update_idm_attributes.groovy +++ b/patterns/306ce091fd87bad6174d9e8b_resources/eid_compare_and_update_idm_attributes.groovy @@ -27,6 +27,7 @@ def compareAndUpdateSessionVariables(sess, keys, isProperty){ return updatedKeys } +// TODO/haburger/2025-07-01: we should also set the verificationMethod, etc. of the level400 role String user_update_dto_template = ''' { "name": { diff --git a/patterns/4f15bae09cbda04a7a515158_resources/eid_fetch_linked_accounts.groovy b/patterns/4f15bae09cbda04a7a515158_resources/eid_fetch_linked_accounts.groovy index 361e7a6..6b1141e 100644 --- a/patterns/4f15bae09cbda04a7a515158_resources/eid_fetch_linked_accounts.groovy +++ b/patterns/4f15bae09cbda04a7a515158_resources/eid_fetch_linked_accounts.groovy @@ -33,31 +33,6 @@ def clearEidSession(){ s.removeAttribute('agov.eid.User.origin') } -def updateLoginHistory(idmRestClient, userExtId, credentialExtId) { - try { - def baseUrl = parameters.get("baseUrl") - def clientExtId = parameters.get("clientExtId") - def endpoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId/login-info" - def dto = "{\"success\": true,\"credentialExtId\": \"${credentialExtId}\"}" - - def postRequest = new HTTPRequestWrapper() - postRequest.addToHeaders('Content-Type', ['application/json']) - postRequest.setPayLoad(dto.getBytes('UTF-8')) - postRequest.setPayLoad(dto.getBytes('UTF-8')) - - def result = idmRestClient.postWithResponse(endpoint, postRequest) - if (result.getStatusCode() != 200) { - // best effort, we log only - // TODO/haburger/2025-06-24: context parameters are missing here (also in getAccounts) - LOG.warn("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${userExtId}, CredentialType='E-ID Link', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='failed to update login history for credential ${credentialExtId} (http status: ${result.getStatusCode()})'") - } - } catch (Exception e) { - // best effort, we log only - // TODO/haburger/2025-06-24: context parameters are missing here (also in getAccounts) - LOG.warn("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${userExtId}, CredentialType='E-ID Link', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='failed to update login history for credential ${credentialExtId} (${e})'") - } -} - def getAccounts(json, String svnr) { def idm_users_dto = json["Resources"] def accounts = [:] @@ -126,9 +101,6 @@ if(inargs['submit'] && inargs['login'] && inargs['login'] != ''){ sess.setAttribute('agov.eid.linkingCredentialExtId', account["credentialExtId"]) sess.setAttribute('agov.eid.linkedAccountExtId', account["extId"]) - // update login history - updateLoginHistory(idmRestClient, account["extId"], account["credentialExtId"]) - if(account["firstLogin"]){ response.setResult('firstLogin') return @@ -206,9 +178,6 @@ try { sess.setAttribute('agov.eid.linkingCredentialExtId', account["credentialExtId"]) sess.setAttribute('agov.eid.linkedAccountExtId', account["extId"]) - // update login history - updateLoginHistory(idmRestClient, account["extId"], account["credentialExtId"]) - if(account["firstLogin"]){ response.setResult('firstLogin') return diff --git a/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip b/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip index 9ed6da8..a6c0bff 100644 Binary files a/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip and b/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip differ diff --git a/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip b/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip index 8cd2145..dcfc2c1 100644 Binary files a/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip and b/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip differ diff --git a/patterns/Ask_Mobile_Number_6d83506dfcc430c12d81dfa3.yml b/patterns/Ask_Mobile_Number_6d83506dfcc430c12d81dfa3.yml index 1eb2168..c99db7c 100644 --- a/patterns/Ask_Mobile_Number_6d83506dfcc430c12d81dfa3.yml +++ b/patterns/Ask_Mobile_Number_6d83506dfcc430c12d81dfa3.yml @@ -5,6 +5,7 @@ pattern: name: "Ask_Mobile_Number" properties: authStatesFile: "res://6d83506dfcc430c12d81dfa3#authStatesFile" + parameters: "var://ask_mobile_number-template-parameters" onSuccess: - "pattern://2cdd910036aa06b102863a4f" onFailure: diff --git a/patterns/EId_Compare_And_Update_IDM_Attributes_306ce091fd87bad6174d9e8b.yml b/patterns/EId_Compare_And_Update_IDM_Attributes_306ce091fd87bad6174d9e8b.yml index a8bf504..7d495df 100644 --- a/patterns/EId_Compare_And_Update_IDM_Attributes_306ce091fd87bad6174d9e8b.yml +++ b/patterns/EId_Compare_And_Update_IDM_Attributes_306ce091fd87bad6174d9e8b.yml @@ -8,7 +8,7 @@ pattern: properties: authStatesFile: "res://306ce091fd87bad6174d9e8b#authStatesFile" onSuccess: - - "pattern://b87d0d2b640e8e545ad70234" + - "pattern://2cdd910036aa06b102863a4f" onFailure: - "pattern://4c65de021d362462324a3a5f" nextSteps: diff --git a/variables.yml b/variables.yml index 476a545..ab64113 100644 --- a/variables.yml +++ b/variables.yml @@ -7,6 +7,13 @@ variables: maxAllowed: 1 value: ".agov-d.azure.adnovum.net" requireOverloading: true + ask_mobile_number-template-parameters: + className: "ch.nevis.admin.v4.plugin.base.generation.property.TextProperty" + parameters: + required: false + syntax: "YAML" + value: null + requireOverloading: true auth-session-store-database-host: className: "ch.nevis.admin.v4.plugin.base.generation.property.HostPortProperty" parameters: