import org.codehaus.groovy.runtime.StackTraceUtils import groovy.xml.XmlSlurper // AGOVaq conversion def maxLoiRoleToCtxClssConvertorMap = [ "level100": "urn:qa.agov.ch:names:tc:ac:classes:100", "level200": "urn:qa.agov.ch:names:tc:ac:classes:200", "level300": "urn:qa.agov.ch:names:tc:ac:classes:300", "level400": "urn:qa.agov.ch:names:tc:ac:classes:400", "level500": "urn:qa.agov.ch:names:tc:ac:classes:500" ] // https://docs.nevis.net/nevisidm/Developer-Guide/SOAP-Interface/Interface-specification/Value-types#enum-value-types def blockingCredentialStates = ['DISABLED', 'EXPIRED', 'LOCKED_TEMPORARY', 'LOCKED', 'ARCHIVED', 'RESET_CODE'] def getUserIdVerificationForRecovery(currentLoaRole) { // 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 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() ?: 'missing' 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, highestRoleLevel) { def result = 'level' 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((highestRoleLevel == 'level400') ? '400' : '300') break default: LOG.warn("unexpected idVerification for recovery on account: ${idVerification}") // safest default, should work in any case result = highestRoleLevel } 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()) : '' } // for autditing def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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' def maxLoi = null // new if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null) { try { def userDto = new XmlSlurper().parseText(session['ch.adnovum.nevisidm.userDto']) def userState = userDto.state def recoveryCode = userDto.'**'.find {node -> node.name() == 'credentials' && node.type.text() == 'CONTEXT_PASSWORD' && node.context.text() == 'RECOVERY'} LOG.debug("Recovery: Dto is '${userDto}") LOG.debug("Recovery: state is '${userState}") LOG.debug("Recovery: RecoveryCode is '${recoveryCode ? recoveryCode : 'none'}'") def session = request.getAuthSession(true) if (userState == 'ACTIVE') { session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery') session.setAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:email') def maxLoiList = userDto.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-Loi' }.collect({ node -> node.name.text() }) maxLoi = (maxLoiList == null || maxLoiList.isEmpty()) ? null : maxLoiList.sort().last() def idVerification = null def agovAqValidFrom = null if (maxLoi) { idVerification = userDto.'**'.find { node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + maxLoi}?.value?.text() idVerification = idVerification ?: 'None' agovAqValidFrom = userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.validFrom?.text() agovAqValidFrom = agovAqValidFrom?: userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.ctlCreDat?.text() } def mustRecover = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'mustRecover' } def hasRecoveryRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recovery' } if (mustRecover) { // attributes are defined over the mustRecover authorization session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover') idVerification = getUserIdVerificationForRecovery(maxLoi ?: 'level100') ?: idVerification agovAqValidFrom = getUserMustRecoverValidFrom() maxLoi = getAqLevelBasedOnIdVerificationForRecovery(idVerification, maxLoi) } LOG.debug("Recovery: MaxLoi is '${maxLoi}'") LOG.debug("Recovery: IdVerification is ${idVerification}") LOG.debug("Recovery: agovAqValidFrom is ${agovAqValidFrom}") LOG.debug("Recovery: mustRecover is '${mustRecover}'") LOG.debug("Recovery: hasRecoveryRole is '${hasRecoveryRole}'") if (maxLoi != null) { if (maxLoiRoleToCtxClssConvertorMap.containsKey(maxLoi)) { LOG.debug("Recovery: MaxLoiMapping is " + maxLoiRoleToCtxClssConvertorMap[maxLoi]) response.setSessionAttribute('agov.recovery.currentAgovAq', '' + maxLoiRoleToCtxClssConvertorMap[maxLoi]) response.setSessionAttribute('agov.recovery.currentIdVerification', '' + idVerification) response.setSessionAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + agovAqValidFrom) if ((maxLoi == 'level100') && (mustRecover == null)) { // AQ100 accounts need to used the recovery code, if they can // check the status of recoveryCode credential if (recoveryCode && !blockingCredentialStates.contains(recoveryCode.state.text())) { LOG.debug("Recovery: emailAndCode") response.setResult('needCode') return } else { LOG.warn("AGOVaq100 recovery: skipped Recovery-Code check '${recoveryCode ? recoveryCode.state.text() : 'MISSING'}'") response.setResult('ok') return } // mustRecover role not set, so code needs to be checked } else { LOG.debug("Recovery: email") response.setResult('ok') return } } else { LOG.error("Recovery: Failed to convert '${maxLoi}' to AGOVaq") response.setResult('error') return } } else { // maxLoi is null LOG.debug("Recovery: no 'AGOV-Loi'-role assigned to user ${user}") if ((hasRecoveryRole != null) && (mustRecover == null)) { response.setResult('notFullyRegistered') return } else { LOG.error("Recovery: no 'AGOV-Loi'-role assigned to user ${user} and no recovery role ") response.setResult('error') return } } } else { // state != ACTIVE and no lasterror should not happen LOG.error("Recovery: state='${userState}' but not lasterror set") response.setNote('lasterror', '9909') response.setNote('lasterrorinfo', 'internal error') response.setResult('error') return } } catch (Exception e) { e = StackTraceUtils.sanitize(e) def affectedLines = e.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" } LOG.error("FATAL: Recovery processing failed (at lines: ${affectedLines})", e) response.setNote('lasterror', '9909') response.setNote('lasterrorinfo', 'internal error') response.setResult('error') return } } LOG.error("Recovery: userDto missing or failure before (lasterror='${notes.getProperty('lasterror', '-')}')") response.setNote('lasterror', '9909') response.setNote('lasterrorinfo', 'internal error') response.setResult('error') return