207 lines
9.3 KiB
Groovy
207 lines
9.3 KiB
Groovy
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
|