Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import dev.thunderid.android.ThunderIDFetcher
import dev.thunderid.android.ThunderIDHttpRequest
import dev.thunderid.android.ThunderIDHttpResponse
import dev.thunderid.android.api.management.Agent
import dev.thunderid.android.api.management.AgentOwner
import dev.thunderid.android.api.management.Application
import dev.thunderid.android.api.management.BasicAgent
import dev.thunderid.android.api.management.BasicApplication
Expand Down Expand Up @@ -238,7 +239,7 @@ object AdminSamples {
type = "assistant",
display = "Billing assistant",
description = "Answers billing questions",
owner = "usr-1",
owner = AgentOwner(id = "usr-1"),
clientId = "billing-assistant",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,33 @@ data class AgentLoginConsentConfig(
val validityPeriod: Int? = null,
)

/** What an agent owner's [AgentOwner.id] holds. [value] is the wire value. */
enum class AgentOwnerIdType(
val value: String,
) {
@SerializedName("uuid")
UUID("uuid"),

@SerializedName("email")
EMAIL("email"),

@SerializedName("mobile")
MOBILE("mobile"),
}

/**
* The person accountable for an agent. A local owner is an entity of this deployment, named by its entity ID.
* An owner managed in another identity system is not looked up.
*
* @property idType What [id] holds. The server defaults it to [AgentOwnerIdType.UUID]. A local owner must be a UUID.
* @property isLocal Whether the owner is an entity of this deployment. The server treats `null` as `true`.
*/
data class AgentOwner(
val id: String,
val idType: AgentOwnerIdType? = null,
val isLocal: Boolean? = null,
)

/**
* An agent registered in ThunderID. [authFlowId], [registrationFlowId] and [isRegistrationFlowEnabled]
* are populated only when the agent has an inbound client.
Expand All @@ -123,7 +150,7 @@ data class Agent(
val ouHandle: String? = null,
val description: String? = null,
val logoUrl: String? = null,
val owner: String? = null,
val owner: AgentOwner? = null,
val clientId: String? = null,
val attributes: Map<String, Any?>? = null,
val allowedUserTypes: List<String>? = null,
Expand Down Expand Up @@ -175,7 +202,7 @@ data class CreateAgentRequest(
val type: String,
val description: String? = null,
val logoUrl: String? = null,
val owner: String? = null,
val owner: AgentOwner? = null,
val attributes: Map<String, Any?>? = null,
val inboundAuthConfig: List<AgentInboundAuthConfig>? = null,
val projectId: String? = null,
Expand All @@ -187,7 +214,7 @@ data class UpdateAgentRequest(
val type: String? = null,
val description: String? = null,
val logoUrl: String? = null,
val owner: String? = null,
val owner: AgentOwner? = null,
val attributes: Map<String, Any?>? = null,
val allowedUserTypes: List<String>? = null,
val allowedAgentTypes: List<String>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import dev.thunderid.android.api.management.Agent
import dev.thunderid.android.api.management.AgentOwner
import dev.thunderid.android.api.management.CreateAgentRequest
import dev.thunderid.compose.LocalThunderID
import dev.thunderid.compose.management.rememberCreateAgent
Expand Down Expand Up @@ -117,15 +118,15 @@ fun BaseAgentCreate(
* project's root organization unit, or, with no project chosen, to the signed-in user's organization unit. Offers
* the agent types the server lists.
*
* @param owner The identifier of the agent's owner, if any.
* @param owner The person accountable for the agent, sent as given. The server chooses one when it is `null`.
* @param onCreated Receives the agent as the server stored it.
* @param content Renders custom UI in place of the default form. See [BaseAgentCreate].
*/
@Composable
fun AgentCreate(
modifier: Modifier = Modifier,
ouId: String? = null,
owner: String? = null,
owner: AgentOwner? = null,
onCreated: ((Agent) -> Unit)? = null,
onCancel: (() -> Unit)? = null,
title: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ fun BaseAgentDetails(
val details =
listOfNotNull(
AdminDetailItem(i18n.resolve("admin.agents.details.type"), agent.type),
AdminDetailItem(i18n.resolve("admin.agents.details.owner"), agent.owner ?: i18n.resolve("admin.agents.details.no_owner")),
AdminDetailItem(
i18n.resolve("admin.agents.details.owner"),
agent.owner?.id ?: i18n.resolve("admin.agents.details.no_owner"),
),
AdminDetailItem(i18n.resolve("admin.common.columns.organization_unit"), agent.ouHandle ?: agent.ouId),
agent.clientId?.let { AdminDetailItem(i18n.resolve("admin.agents.list.columns.client_id"), it, copyable = true) },
accessStatusLabel(i18n, agent.status?.value)?.let { AdminDetailItem(i18n.resolve("admin.common.columns.status"), it) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ class AdministrationApiTest {
resource =
"""{"id":"a-1","clientId":"client","display":"Bot","attributes":{"name":"Bot"},""" +
""""ouHandle":"default","ouId":"ou-1","type":"bot",""" +
""""owner":{"id":"jane@example.com","idType":"email","isLocal":false},""" +
""""inboundAuthConfig":[{"type":"oauth2","config":{"grantTypes":["client_credentials"]}}]}""",
)

Expand All @@ -595,6 +596,7 @@ class AdministrationApiTest {
assertEquals(
json(
"""{"ouId":"ou-1","type":"bot","attributes":{"name":"Bot"},"delegated":false,""" +
""""owner":{"id":"jane@example.com","idType":"email","isLocal":false},""" +
""""inboundAuthConfig":[{"type":"oauth2",""" +
""""config":{"grantTypes":["client_credentials"],"clientSecret":"${result.clientSecret}"}}]}""",
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,61 @@ class ManagementApiTest {
assertEquals("Calendar Agent", resolveAgentName(agent))
}

@Test
fun `agent create sends the owner as an object`() =
runTest {
val recorder = RecordingFetcher()
val client = client(recorder)
recorder.respond("""{"id":"ag-1","ouId":"ou","type":"default"}""")
val owner = AgentOwner(id = "jane@example.com", idType = AgentOwnerIdType.EMAIL, isLocal = false)

client.agents.create(CreateAgentRequest(ouId = "ou", type = "default", owner = owner))

@Suppress("UNCHECKED_CAST")
val body = Gson().fromJson(recorder.requests.single().body, Map::class.java) as Map<String, Any?>
assertEquals(mapOf("id" to "jane@example.com", "idType" to "email", "isLocal" to false), body["owner"])
}

@Test
fun `agent create leaves out the owner fields it was not given`() =
runTest {
val recorder = RecordingFetcher()
val client = client(recorder)
recorder.respond("""{"id":"ag-1","ouId":"ou","type":"default"}""")

client.agents.create(CreateAgentRequest(ouId = "ou", type = "default", owner = AgentOwner(id = "usr-1")))

@Suppress("UNCHECKED_CAST")
val body = Gson().fromJson(recorder.requests.single().body, Map::class.java) as Map<String, Any?>
assertEquals(mapOf("id" to "usr-1"), body["owner"])
}

@Test
fun `agent reads parse the owner object`() =
runTest {
val recorder = RecordingFetcher()
val client = client(recorder)
recorder.respond(
"""{"id":"ag-1","ouId":"ou","type":"default",""" +
""""owner":{"id":"jane@example.com","idType":"email","isLocal":false}}""",
)

val agent = client.agents.get("ag-1")

assertEquals("jane@example.com", agent.owner?.id)
assertEquals(AgentOwnerIdType.EMAIL, agent.owner?.idType)
assertEquals(false, agent.owner?.isLocal)
}

@Test
fun `agent reads leave the owner null when the agent has none`() =
runTest {
val recorder = RecordingFetcher()
recorder.respond("""{"id":"ag-1","ouId":"ou","type":"default"}""")

assertNull(client(recorder).agents.get("ag-1").owner)
}

@Test
fun `agent reads take the name from display`() =
runTest {
Expand Down
Loading