|
| 1 | +Feature: automatic IdP linking for central shadow users |
| 2 | + |
| 3 | + Background: |
| 4 | + * url baseUrl |
| 5 | + * configure cookies = null |
| 6 | + * configure readTimeout = 600000 |
| 7 | + * configure retry = { count: 10, interval: 15000 } |
| 8 | + * configure afterScenario = |
| 9 | + """ |
| 10 | + function(){ |
| 11 | + if (!karate.get('centralTenantId') || !karate.get('memberTenantId')) { |
| 12 | + return; |
| 13 | + } |
| 14 | + karate.call('classpath:eureka/mod-users-keycloak/features/consortia/destroy-data.feature', { |
| 15 | + centralTenantId: karate.get('centralTenantId'), |
| 16 | + memberTenantId: karate.get('memberTenantId') |
| 17 | + }); |
| 18 | + } |
| 19 | + """ |
| 20 | + |
| 21 | + * table modules |
| 22 | + | name | |
| 23 | + | 'mod-users-keycloak' | |
| 24 | + | 'mod-login-keycloak' | |
| 25 | + | 'mod-roles-keycloak' | |
| 26 | + | 'mod-notify' | |
| 27 | + | 'mod-consortia' | |
| 28 | + |
| 29 | + * def setupTenant = read('classpath:common-consortia/eureka/tenant-and-local-admin-setup.feature@SetupTenant') |
| 30 | + * def setupConsortium = read('classpath:common-consortia/eureka/consortium.feature@SetupConsortia') |
| 31 | + * def setupTenantForConsortia = read('classpath:common-consortia/eureka/consortium.feature@SetupTenantForConsortia') |
| 32 | + * def eurekaLogin = read('classpath:common-consortia/eureka/initData.feature@Login') |
| 33 | + * def keycloakMaster = read('classpath:common/eureka/keycloak.feature@getKeycloakMasterToken') |
| 34 | + |
| 35 | + @Positive |
| 36 | + Scenario: creating a staff user in member tenant automatically creates a shadow user in central tenant and links the member tenant IdP |
| 37 | + |
| 38 | + * def suffix = uuid().replace(/-/g, '').substring(0, 10) |
| 39 | + |
| 40 | + * def centralTenant = 'consortium' + suffix |
| 41 | + * def centralTenantId = uuid() |
| 42 | + |
| 43 | + * def memberTenant = 'member' + suffix |
| 44 | + * def memberTenantId = uuid() |
| 45 | + |
| 46 | + * def consortiumId = uuid() |
| 47 | + |
| 48 | + * def consortiaAdmin = { id: '#(uuid())', username: 'consortia_admin', password: 'consortia_admin_password', tenant: '#(centralTenant)' } |
| 49 | + * def memberAdmin = { id: '#(uuid())', username: 'member_admin', password: 'member_admin_password', tenant: '#(memberTenant)' } |
| 50 | + |
| 51 | + * def memberTenantIdpAlias = memberTenant + '-keycloak-oidc' |
| 52 | + |
| 53 | + # Create the central and member tenants. |
| 54 | + * call setupTenant { tenant: '#(centralTenant)', tenantId: '#(centralTenantId)', user: '#(consortiaAdmin)' } |
| 55 | + * call setupTenant { tenant: '#(memberTenant)', tenantId: '#(memberTenantId)', user: '#(memberAdmin)' } |
| 56 | + |
| 57 | + # Register both tenants in the consortium. |
| 58 | + * call eurekaLogin { username: '#(consortiaAdmin.username)', password: '#(consortiaAdmin.password)', tenant: '#(centralTenant)' } |
| 59 | + * configure cookies = null |
| 60 | + * call setupConsortium { tenant: '#(centralTenant)' } |
| 61 | + * call setupTenantForConsortia { tenant: '#(centralTenant)', isCentral: true, code: 'CON' } |
| 62 | + * call setupTenantForConsortia { tenant: '#(memberTenant)', isCentral: false, code: 'MEM' } |
| 63 | + * configure headers = null |
| 64 | + * configure cookies = null |
| 65 | + |
| 66 | + # Create the member-tenant user. |
| 67 | + * def memberToken = karate.call('classpath:common-consortia/eureka/keycloak.feature@getAuthorizationToken', { tenant: memberTenant }).okapitoken |
| 68 | + * def userId = uuid() |
| 69 | + * def memberUsername = 'MemberUser' + nowMillis() |
| 70 | + * def expectedFederatedUsername = memberUsername.toLowerCase() |
| 71 | + |
| 72 | + Given path 'users-keycloak', 'users' |
| 73 | + And headers { 'Content-Type': 'application/json', 'Accept': '*/*', 'x-okapi-tenant': '#(memberTenant)', 'x-okapi-token': '#(memberToken)' } |
| 74 | + And request |
| 75 | + """ |
| 76 | + { |
| 77 | + "id": "#(userId)", |
| 78 | + "username": "#(memberUsername)", |
| 79 | + "active": true, |
| 80 | + "type": "staff", |
| 81 | + "personal": { |
| 82 | + "firstName": "Member", |
| 83 | + "lastName": "User" |
| 84 | + } |
| 85 | + } |
| 86 | + """ |
| 87 | + When method post |
| 88 | + Then status 201 |
| 89 | + And match response.id == userId |
| 90 | + And match response.username == memberUsername |
| 91 | + |
| 92 | + # Wait for consortium processing to create the shadow user in central tenant with the same UUID. |
| 93 | + * def centralToken = karate.call('classpath:common-consortia/eureka/keycloak.feature@getAuthorizationToken', { tenant: centralTenant }).okapitoken |
| 94 | + |
| 95 | + Given path 'users-keycloak', 'users', userId |
| 96 | + And headers { 'Content-Type': 'application/json', 'Accept': '*/*', 'x-okapi-tenant': '#(centralTenant)', 'x-okapi-token': '#(centralToken)' } |
| 97 | + And retry until responseStatus == 200 && response.type == 'shadow' |
| 98 | + When method get |
| 99 | + Then status 200 |
| 100 | + And match response.id == userId |
| 101 | + And match response.type == 'shadow' |
| 102 | + * def originalTenantId = response.customFields.originalTenantId ? response.customFields.originalTenantId : response.customFields.originaltenantid |
| 103 | + * match originalTenantId == memberTenant |
| 104 | + |
| 105 | + # Verify that the shadow user resolves to the real member-tenant user when `overrideUser=true` |
| 106 | + Given path 'users-keycloak', '_self' |
| 107 | + And headers { 'Content-Type': 'application/json', 'Accept': '*/*', 'x-okapi-tenant': '#(centralTenant)', 'x-okapi-token': '#(centralToken)', 'x-okapi-user-id': '#(userId)' } |
| 108 | + And param overrideUser = true |
| 109 | + When method get |
| 110 | + Then status 200 |
| 111 | + And match response.originalTenantId == memberTenant |
| 112 | + And match response.user.id == userId |
| 113 | + And match response.user.username == memberUsername |
| 114 | + And match response.user.type == 'staff' |
| 115 | + |
| 116 | + # Verify that the shadow user is linked to the member tenant IdP in keycloak |
| 117 | + * def keycloakMasterLogin = call keycloakMaster |
| 118 | + * def keycloakMasterToken = keycloakMasterLogin.response.access_token |
| 119 | + |
| 120 | + Given url baseKeycloakUrl |
| 121 | + And path 'admin', 'realms', centralTenant, 'users' |
| 122 | + And header Authorization = 'Bearer ' + keycloakMasterToken |
| 123 | + And param q = 'user_id:' + userId |
| 124 | + And param briefRepresentation = true |
| 125 | + And retry until response.length == 1 |
| 126 | + When method get |
| 127 | + Then status 200 |
| 128 | + * def centralKeycloakUserId = response[0].id |
| 129 | + |
| 130 | + Given url baseKeycloakUrl |
| 131 | + And path 'admin', 'realms', centralTenant, 'users', centralKeycloakUserId, 'federated-identity' |
| 132 | + And header Authorization = 'Bearer ' + keycloakMasterToken |
| 133 | + And retry until karate.filter(response, x => x.identityProvider == memberTenantIdpAlias).length == 1 |
| 134 | + When method get |
| 135 | + Then status 200 |
| 136 | + * def providerLinks = karate.filter(response, x => x.identityProvider == memberTenantIdpAlias) |
| 137 | + * match providerLinks[0] contains |
| 138 | + """ |
| 139 | + { |
| 140 | + "identityProvider": "#(memberTenantIdpAlias)", |
| 141 | + "userId": "#(expectedFederatedUsername)", |
| 142 | + "userName": "#(expectedFederatedUsername)" |
| 143 | + } |
| 144 | + """ |
0 commit comments