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; }