Skip to content

Commit 01cc8cd

Browse files
committed
refactor(csa-362): move IamClient into SDK as IdentityEndpoint
- Add IdentityEndpoint.java extending HeraclesEndpoint in core SDK - Add IamGroupRef.java model class for IAM group references - Register client.identity in AtlanClient alongside other endpoints - Remove IamClient.kt from admin-export package - Update Users.kt and AdminExporter.kt to use ctx.client.identity - Update IamClientTest.kt to test IdentityEndpoint via forTesting() - Add branch trigger to merge.yml for CI build
1 parent d6344f3 commit 01cc8cd

8 files changed

Lines changed: 284 additions & 198 deletions

File tree

.github/workflows/merge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ permissions:
66

77
on:
88
push:
9-
branches: [main]
9+
branches: [main, fix/csa-362-admin-export-scale]
1010
workflow_dispatch:
1111
inputs:
1212
branch:

samples/packages/admin-export/src/main/kotlin/com/atlan/pkg/ae/AdminExporter.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,6 @@ object AdminExporter {
4444
val glossaryMap = preloadGlossaryNameMap(ctx)
4545
val connectionMap = preloadConnectionMap(ctx)
4646

47-
// Build IAM client for bulk user/group fetches via Redis-backed identity API.
48-
// Replaces per-user Keycloak calls that fail at high user volumes (60K+).
49-
// Auth: client_credentials grant with client_id=atlan-backend.
50-
// CLIENT_SECRET is injected from argo-client-creds secret into the main container.
51-
val identityBase = "http://heracles-service.heracles.svc.cluster.local"
52-
val bearerToken = ctx.client.impersonate.escalate()
53-
val iamClient = IamClient(baseUrl = identityBase, bearerToken = bearerToken, logger = logger)
5447
val xlsxOutput = ctx.config.fileFormat == "XLSX"
5548

5649
val outputDirectory = validatePathIsSafe(od)
@@ -89,7 +82,7 @@ object AdminExporter {
8982
ExcelWriter(xlsxFileActual.toString()).use { xlsx ->
9083
ctx.config.objectsToInclude.forEach { objectName ->
9184
when (objectName) {
92-
"users" -> Users(ctx, xlsx.createSheet("Users"), logger, iamClient).export()
85+
"users" -> Users(ctx, xlsx.createSheet("Users"), logger).export()
9386
"groups" -> Groups(ctx, xlsx.createSheet("Groups"), logger).export()
9487
"personas" -> Personas(ctx, xlsx.createSheet("Personas"), glossaryMap, connectionMap, logger).export()
9588
"purposes" -> Purposes(ctx, xlsx.createSheet("Purposes"), logger).export()
@@ -101,7 +94,7 @@ object AdminExporter {
10194
} else {
10295
ctx.config.objectsToInclude.forEach { objectName ->
10396
when (objectName) {
104-
"users" -> CSVWriter(usersFileActual.toString()).use { csv -> Users(ctx, csv, logger, iamClient).export() }
97+
"users" -> CSVWriter(usersFileActual.toString()).use { csv -> Users(ctx, csv, logger).export() }
10598
"groups" -> CSVWriter(groupsFileActual.toString()).use { csv -> Groups(ctx, csv, logger).export() }
10699
"personas" -> CSVWriter(personasFileActual.toString()).use { csv -> Personas(ctx, csv, glossaryMap, connectionMap, logger).export() }
107100
"purposes" -> CSVWriter(purposesFileActual.toString()).use { csv -> Purposes(ctx, csv, logger).export() }

samples/packages/admin-export/src/main/kotlin/com/atlan/pkg/ae/IamClient.kt

Lines changed: 0 additions & 132 deletions
This file was deleted.

samples/packages/admin-export/src/main/kotlin/com/atlan/pkg/ae/exports/Users.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ package com.atlan.pkg.ae.exports
44

55
import AdminExportCfg
66
import com.atlan.api.UsersEndpoint
7+
import com.atlan.model.admin.IamGroupRef
78
import com.atlan.model.admin.UserRequest
89
import com.atlan.pkg.PackageContext
9-
import com.atlan.pkg.ae.IamClient
1010
import com.atlan.pkg.serde.TabularWriter
1111
import com.atlan.pkg.serde.cell.TimestampXformer
1212
import mu.KLogger
@@ -16,7 +16,6 @@ class Users(
1616
private val ctx: PackageContext<AdminExportCfg>,
1717
private val writer: TabularWriter,
1818
private val logger: KLogger,
19-
private val iamClient: IamClient,
2019
) {
2120
fun export() {
2221
logger.info { "Exporting all users..." }
@@ -56,20 +55,20 @@ class Users(
5655
logger.info { "Fetched ${allUsers.size} users. Resolving group memberships via IAM API..." }
5756

5857
// Bulk-fetch group memberships — ~120 Redis calls instead of 60K Keycloak calls
59-
val userGroupsMap = iamClient.getUserGroups(allUsers.mapNotNull { it.id })
58+
val userGroupsMap = ctx.client.identity.getUserGroups(allUsers.mapNotNull { it.id })
6059

6160
// Collect all unique group IDs seen across all users, then resolve aliases in bulk
6261
val allGroupIds =
6362
userGroupsMap.values
6463
.flatten()
6564
.map { it.id }
6665
.distinct()
67-
val groupAliasMap = iamClient.getGroupAliases(allGroupIds)
66+
val groupAliasMap = ctx.client.identity.getGroupAliases(allGroupIds)
6867

6968
val ts = Instant.now().toString()
7069
allUsers.forEach { user ->
7170
val personas = user.personas?.joinToString("\n") { it.displayName ?: "" } ?: ""
72-
val groups = userGroupsMap[user.id] ?: emptyList<IamClient.GroupRef>()
71+
val groups = userGroupsMap[user.id] ?: emptyList<IamGroupRef>()
7372
val technicalNames = groups.joinToString("\n") { it.name }
7473
val nontechnicalNames = groups.joinToString("\n") { groupAliasMap[it.id] ?: it.name }
7574
val designation =
Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/* SPDX-License-Identifier: Apache-2.0
22
Copyright 2024 Atlan Pte. Ltd. */
3-
import com.atlan.pkg.ae.IamClient
3+
import com.atlan.api.IdentityEndpoint
44
import com.sun.net.httpserver.HttpServer
5-
import mu.KotlinLogging
65
import org.testng.Assert.assertEquals
76
import org.testng.Assert.assertTrue
87
import org.testng.annotations.AfterClass
@@ -13,14 +12,13 @@ import java.net.URLDecoder
1312
import java.nio.charset.StandardCharsets
1413

1514
/**
16-
* Unit tests for IamClient using a local HTTP stub server.
15+
* Unit tests for IdentityEndpoint using a local HTTP stub server.
1716
* Verifies bulk user-group and group-alias fetching, batching behaviour,
1817
* and graceful handling of empty inputs and missing optional fields.
1918
*/
2019
class IamClientTest {
21-
private val logger = KotlinLogging.logger {}
2220
private lateinit var server: HttpServer
23-
private lateinit var client: IamClient
21+
private lateinit var endpoint: IdentityEndpoint
2422

2523
@BeforeClass
2624
fun setup() {
@@ -34,20 +32,16 @@ class IamClientTest {
3432
query.split("&").firstOrNull { it.startsWith("filter=") }?.removePrefix("filter=") ?: "{}",
3533
StandardCharsets.UTF_8,
3634
)
37-
// Extract IDs from {"id":{"$in":["id1","id2",...]}}
3835
val ids =
3936
Regex("\"([^\"]+)\"")
40-
.findAll(
41-
filter.substringAfter("\$in\":[").substringBefore("]"),
42-
).map { it.groupValues[1] }
37+
.findAll(filter.substringAfter("\$in\":[").substringBefore("]"))
38+
.map { it.groupValues[1] }
4339
.toList()
44-
4540
val body =
4641
ids
4742
.joinToString(",", "[", "]") { id ->
4843
"""{"id":"$id","groups":[{"id":"grp-$id","name":"group-$id"}]}"""
4944
}.toByteArray()
50-
5145
exchange.sendResponseHeaders(200, body.size.toLong())
5246
exchange.responseBody.use { it.write(body) }
5347
}
@@ -62,34 +56,25 @@ class IamClientTest {
6256
)
6357
val ids =
6458
Regex("\"([^\"]+)\"")
65-
.findAll(
66-
filter.substringAfter("\$in\":[").substringBefore("]"),
67-
).map { it.groupValues[1] }
59+
.findAll(filter.substringAfter("\$in\":[").substringBefore("]"))
60+
.map { it.groupValues[1] }
6861
.toList()
69-
7062
val body =
7163
ids
7264
.joinToString(",", "[", "]") { id ->
73-
when {
74-
id.contains("no-alias") -> """{"id":"$id"}"""
75-
76-
// missing attributes entirely
77-
else -> """{"id":"$id","attributes":{"alias":"Alias for $id"}}"""
65+
if (id.contains("no-alias")) {
66+
"""{"id":"$id"}"""
67+
} else {
68+
"""{"id":"$id","attributes":{"alias":"Alias for $id"}}"""
7869
}
7970
}.toByteArray()
80-
8171
exchange.sendResponseHeaders(200, body.size.toLong())
8272
exchange.responseBody.use { it.write(body) }
8373
}
8474

8575
server.start()
8676
val port = server.address.port
87-
client =
88-
IamClient(
89-
baseUrl = "http://localhost:$port",
90-
bearerToken = "test-token",
91-
logger = logger,
92-
)
77+
endpoint = IdentityEndpoint.forTesting("http://localhost:$port", "test-token")
9378
}
9479

9580
@AfterClass
@@ -99,21 +84,20 @@ class IamClientTest {
9984

10085
@Test
10186
fun getUserGroupsEmptyInputReturnsEmptyMap() {
102-
val result = client.getUserGroups(emptyList())
87+
val result = endpoint.getUserGroups(emptyList())
10388
assertTrue(result.isEmpty(), "Expected empty map for empty user ID list")
10489
}
10590

10691
@Test
10792
fun getGroupAliasesEmptyInputReturnsEmptyMap() {
108-
val result = client.getGroupAliases(emptyList())
93+
val result = endpoint.getGroupAliases(emptyList())
10994
assertTrue(result.isEmpty(), "Expected empty map for empty group ID list")
11095
}
11196

11297
@Test
11398
fun getUserGroupsReturnsMappedGroups() {
11499
val userIds = listOf("user-1", "user-2", "user-3")
115-
val result = client.getUserGroups(userIds)
116-
100+
val result = endpoint.getUserGroups(userIds)
117101
assertEquals(result.size, 3)
118102
userIds.forEach { userId ->
119103
val groups = result[userId]
@@ -126,8 +110,7 @@ class IamClientTest {
126110
@Test
127111
fun getGroupAliasesReturnsMappedAliases() {
128112
val groupIds = listOf("grp-user-1", "grp-user-2")
129-
val result = client.getGroupAliases(groupIds)
130-
113+
val result = endpoint.getGroupAliases(groupIds)
131114
assertEquals(result.size, 2)
132115
groupIds.forEach { groupId ->
133116
assertEquals(result[groupId], "Alias for $groupId")
@@ -136,35 +119,23 @@ class IamClientTest {
136119

137120
@Test
138121
fun getGroupAliasesMissingAttributesReturnsEmptyString() {
139-
val result = client.getGroupAliases(listOf("no-alias-grp"))
122+
val result = endpoint.getGroupAliases(listOf("no-alias-grp"))
140123
assertEquals(result["no-alias-grp"], "", "Expected empty string when attributes are missing")
141124
}
142125

143126
@Test
144127
fun getUserGroupsBatchesLargeInputs() {
145-
// 550 users should produce 2 batches (500 + 50) with batchSize=500
146-
val userIds = (1..550).map { "batch-user-$it" }
147-
val requestCount =
148-
java.util.concurrent.atomic
149-
.AtomicInteger(0)
150-
151-
// Wrap client with a smaller batchSize to verify chunking without needing 550 ids
152-
val smallBatchClient =
153-
IamClient(
154-
baseUrl = "http://localhost:${server.address.port}",
155-
bearerToken = "test-token",
156-
logger = logger,
157-
)
158-
val result = smallBatchClient.getUserGroups(userIds.take(7), batchSize = 3)
159-
// 7 users, batchSize=3 → 3 batches: [3, 3, 1]
128+
// 7 users with batchSize=3 → 3 batches: [3, 3, 1]
129+
val userIds = (1..7).map { "user-$it" }
130+
val result = endpoint.getUserGroups(userIds, 3)
160131
assertEquals(result.size, 7, "All 7 users should be present in result")
161132
}
162133

163134
@Test
164135
fun getGroupAliasesBatchesLargeInputs() {
136+
// 7 groups with batchSize=3 → 3 batches: [3, 3, 1]
165137
val groupIds = (1..7).map { "grp-user-$it" }
166-
val result = client.getGroupAliases(groupIds, batchSize = 3)
167-
// 7 groups, batchSize=3 → 3 batches: [3, 3, 1]
138+
val result = endpoint.getGroupAliases(groupIds, 3)
168139
assertEquals(result.size, 7, "All 7 groups should be present in result")
169140
}
170141
}

0 commit comments

Comments
 (0)