diff --git a/Untitled b/Untitled new file mode 100644 index 0000000..088eda4 --- /dev/null +++ b/Untitled @@ -0,0 +1 @@ +version diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizations/Organizations.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizations/Organizations.kt index a648654..3fa7856 100644 --- a/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizations/Organizations.kt +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizations/Organizations.kt @@ -16,6 +16,9 @@ import com.stytch.java.b2b.models.organizations.ConnectedAppsRequestOptions import com.stytch.java.b2b.models.organizations.ConnectedAppsResponse import com.stytch.java.b2b.models.organizations.CreateRequest import com.stytch.java.b2b.models.organizations.CreateResponse +import com.stytch.java.b2b.models.organizations.DeleteExternalIdRequest +import com.stytch.java.b2b.models.organizations.DeleteExternalIdRequestOptions +import com.stytch.java.b2b.models.organizations.DeleteExternalIdResponse import com.stytch.java.b2b.models.organizations.DeleteRequest import com.stytch.java.b2b.models.organizations.DeleteRequestOptions import com.stytch.java.b2b.models.organizations.DeleteResponse @@ -270,6 +273,22 @@ public interface Organizations { data: GetConnectedAppRequest, methodOptions: GetConnectedAppRequestOptions? = null, ): CompletableFuture> + + public suspend fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions? = null, + ): StytchResult + + public fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions? = null, + callback: (StytchResult) -> Unit, + ) + + public fun deleteExternalIdCompletable( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions? = null, + ): CompletableFuture> } internal class OrganizationsImpl( @@ -514,4 +533,36 @@ internal class OrganizationsImpl( .async { getConnectedApp(data, methodOptions) }.asCompletableFuture() + + override suspend fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions?, + ): StytchResult = + withContext(Dispatchers.IO) { + var headers = emptyMap() + methodOptions?.let { + headers = methodOptions.addHeaders(headers) + } + + httpClient.delete("/v1/b2b/organizations/${data.organizationId}/external_id", headers) + } + + override fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions?, + callback: (StytchResult) -> Unit, + ) { + coroutineScope.launch { + callback(deleteExternalId(data, methodOptions)) + } + } + + override fun deleteExternalIdCompletable( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions?, + ): CompletableFuture> = + coroutineScope + .async { + deleteExternalId(data, methodOptions) + }.asCompletableFuture() } diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizationsmembers/OrganizationsMembers.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizationsmembers/OrganizationsMembers.kt index 4a6a9d5..b6c2f57 100644 --- a/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizationsmembers/OrganizationsMembers.kt +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/api/organizationsmembers/OrganizationsMembers.kt @@ -17,6 +17,9 @@ import com.stytch.java.b2b.models.organizationsmembers.CreateRequest import com.stytch.java.b2b.models.organizationsmembers.CreateRequestOptions import com.stytch.java.b2b.models.organizationsmembers.CreateResponse import com.stytch.java.b2b.models.organizationsmembers.DangerouslyGetRequest +import com.stytch.java.b2b.models.organizationsmembers.DeleteExternalIdRequest +import com.stytch.java.b2b.models.organizationsmembers.DeleteExternalIdRequestOptions +import com.stytch.java.b2b.models.organizationsmembers.DeleteExternalIdResponse import com.stytch.java.b2b.models.organizationsmembers.DeleteMFAPhoneNumberRequest import com.stytch.java.b2b.models.organizationsmembers.DeleteMFAPhoneNumberRequestOptions import com.stytch.java.b2b.models.organizationsmembers.DeleteMFAPhoneNumberResponse @@ -566,6 +569,22 @@ public interface Members { methodOptions: GetConnectedAppsRequestOptions? = null, ): CompletableFuture> + public suspend fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions? = null, + ): StytchResult + + public fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions? = null, + callback: (StytchResult) -> Unit, + ) + + public fun deleteExternalIdCompletable( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions? = null, + ): CompletableFuture> + /** * Creates a Member. An `organization_id` and `email_address` are required. */ @@ -1000,6 +1019,38 @@ internal class MembersImpl( getConnectedApps(data, methodOptions) }.asCompletableFuture() + override suspend fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions?, + ): StytchResult = + withContext(Dispatchers.IO) { + var headers = emptyMap() + methodOptions?.let { + headers = methodOptions.addHeaders(headers) + } + + httpClient.delete("/v1/b2b/organizations/${data.organizationId}/members/${data.memberId}/external_id", headers) + } + + override fun deleteExternalId( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions?, + callback: (StytchResult) -> Unit, + ) { + coroutineScope.launch { + callback(deleteExternalId(data, methodOptions)) + } + } + + override fun deleteExternalIdCompletable( + data: DeleteExternalIdRequest, + methodOptions: DeleteExternalIdRequestOptions?, + ): CompletableFuture> = + coroutineScope + .async { + deleteExternalId(data, methodOptions) + }.asCompletableFuture() + override suspend fun create( data: CreateRequest, methodOptions: CreateRequestOptions?, diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbac/RBAC.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbac/RBAC.kt index 145631c..53dd7bd 100644 --- a/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbac/RBAC.kt +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbac/RBAC.kt @@ -9,6 +9,8 @@ package com.stytch.java.b2b.api.rbac import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import com.stytch.java.b2b.api.rbacorganizations.Organizations +import com.stytch.java.b2b.api.rbacorganizations.OrganizationsImpl import com.stytch.java.b2b.models.rbac.PolicyRequest import com.stytch.java.b2b.models.rbac.PolicyResponse import com.stytch.java.common.InstantAdapter @@ -23,6 +25,8 @@ import kotlinx.coroutines.withContext import java.util.concurrent.CompletableFuture public interface RBAC { + public val organizations: Organizations + /** * Get the active RBAC Policy for your current Stytch Project. An RBAC Policy is the canonical document that stores all * defined Resources and Roles within your RBAC permissioning model. @@ -87,6 +91,8 @@ internal class RBACImpl( ) : RBAC { private val moshi = Moshi.Builder().add(InstantAdapter()).build() + override val organizations: Organizations = OrganizationsImpl(httpClient, coroutineScope) + override suspend fun policy(data: PolicyRequest): StytchResult = withContext(Dispatchers.IO) { var headers = emptyMap() diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbacorganizations/RBACOrganizations.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbacorganizations/RBACOrganizations.kt new file mode 100644 index 0000000..d8cb5e7 --- /dev/null +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/api/rbacorganizations/RBACOrganizations.kt @@ -0,0 +1,233 @@ +package com.stytch.java.b2b.api.rbacorganizations + +// !!! +// WARNING: This file is autogenerated +// Only modify code within MANUAL() sections +// or your changes may be overwritten later! +// !!! + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.stytch.java.b2b.models.rbacorganizations.GetOrgPolicyRequest +import com.stytch.java.b2b.models.rbacorganizations.GetOrgPolicyResponse +import com.stytch.java.b2b.models.rbacorganizations.SetOrgPolicyRequest +import com.stytch.java.b2b.models.rbacorganizations.SetOrgPolicyResponse +import com.stytch.java.common.InstantAdapter +import com.stytch.java.common.StytchResult +import com.stytch.java.http.HttpClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.CompletableFuture + +public interface Organizations { + /** + * Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains + * the roles that have been defined specifically for that organization, allowing for organization-specific permissioning + * models. + * + * This endpoint returns the organization-scoped roles that supplement the project-level RBAC policy. Organization + * policies allow you to define custom roles that are specific to individual organizations within your project. + * + * When using the backend SDKs, the RBAC Policy will be cached to allow for local evaluations, eliminating the need for an + * extra request to Stytch. The policy will be refreshed if an authorization check is requested and the RBAC policy was + * last updated more than 5 minutes ago. + * + * Organization-specific roles can be created and managed through this API endpoint, providing fine-grained control over + * permissions at the organization level. + * + * Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC + * permissioning model and organization-scoped policies. + */ + public suspend fun getOrgPolicy(data: GetOrgPolicyRequest): StytchResult + + /** + * Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains + * the roles that have been defined specifically for that organization, allowing for organization-specific permissioning + * models. + * + * This endpoint returns the organization-scoped roles that supplement the project-level RBAC policy. Organization + * policies allow you to define custom roles that are specific to individual organizations within your project. + * + * When using the backend SDKs, the RBAC Policy will be cached to allow for local evaluations, eliminating the need for an + * extra request to Stytch. The policy will be refreshed if an authorization check is requested and the RBAC policy was + * last updated more than 5 minutes ago. + * + * Organization-specific roles can be created and managed through this API endpoint, providing fine-grained control over + * permissions at the organization level. + * + * Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC + * permissioning model and organization-scoped policies. + */ + public fun getOrgPolicy( + data: GetOrgPolicyRequest, + callback: (StytchResult) -> Unit, + ) + + /** + * Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains + * the roles that have been defined specifically for that organization, allowing for organization-specific permissioning + * models. + * + * This endpoint returns the organization-scoped roles that supplement the project-level RBAC policy. Organization + * policies allow you to define custom roles that are specific to individual organizations within your project. + * + * When using the backend SDKs, the RBAC Policy will be cached to allow for local evaluations, eliminating the need for an + * extra request to Stytch. The policy will be refreshed if an authorization check is requested and the RBAC policy was + * last updated more than 5 minutes ago. + * + * Organization-specific roles can be created and managed through this API endpoint, providing fine-grained control over + * permissions at the organization level. + * + * Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC + * permissioning model and organization-scoped policies. + */ + public fun getOrgPolicyCompletable(data: GetOrgPolicyRequest): CompletableFuture> + + /** + * Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to + * define roles that are specific to that organization, providing fine-grained control over permissions at the + * organization level. + * + * This endpoint allows you to create, update, or replace the organization-scoped roles for a given organization. + * Organization policies supplement the project-level RBAC policy with additional roles that are only applicable within + * the context of that specific organization. + * + * The organization policy consists of roles, where each role defines: + * - A unique `role_id` to identify the role + * - A human-readable `description` of the role's purpose + * - A set of `permissions` that specify which actions can be performed on which resources + * + * When you set an organization policy, it will replace any existing organization-specific roles for that organization. + * The project-level RBAC policy remains unchanged. + * + * Organization-specific roles are useful for scenarios where different organizations within your project require + * different permission structures, such as: + * - Multi-tenant applications with varying access levels per tenant + * - Organizations with custom approval workflows + * - Different organizational hierarchies requiring unique role definitions + * + * Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC + * permissioning model and organization-scoped policies. + */ + public suspend fun setOrgPolicy(data: SetOrgPolicyRequest): StytchResult + + /** + * Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to + * define roles that are specific to that organization, providing fine-grained control over permissions at the + * organization level. + * + * This endpoint allows you to create, update, or replace the organization-scoped roles for a given organization. + * Organization policies supplement the project-level RBAC policy with additional roles that are only applicable within + * the context of that specific organization. + * + * The organization policy consists of roles, where each role defines: + * - A unique `role_id` to identify the role + * - A human-readable `description` of the role's purpose + * - A set of `permissions` that specify which actions can be performed on which resources + * + * When you set an organization policy, it will replace any existing organization-specific roles for that organization. + * The project-level RBAC policy remains unchanged. + * + * Organization-specific roles are useful for scenarios where different organizations within your project require + * different permission structures, such as: + * - Multi-tenant applications with varying access levels per tenant + * - Organizations with custom approval workflows + * - Different organizational hierarchies requiring unique role definitions + * + * Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC + * permissioning model and organization-scoped policies. + */ + public fun setOrgPolicy( + data: SetOrgPolicyRequest, + callback: (StytchResult) -> Unit, + ) + + /** + * Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to + * define roles that are specific to that organization, providing fine-grained control over permissions at the + * organization level. + * + * This endpoint allows you to create, update, or replace the organization-scoped roles for a given organization. + * Organization policies supplement the project-level RBAC policy with additional roles that are only applicable within + * the context of that specific organization. + * + * The organization policy consists of roles, where each role defines: + * - A unique `role_id` to identify the role + * - A human-readable `description` of the role's purpose + * - A set of `permissions` that specify which actions can be performed on which resources + * + * When you set an organization policy, it will replace any existing organization-specific roles for that organization. + * The project-level RBAC policy remains unchanged. + * + * Organization-specific roles are useful for scenarios where different organizations within your project require + * different permission structures, such as: + * - Multi-tenant applications with varying access levels per tenant + * - Organizations with custom approval workflows + * - Different organizational hierarchies requiring unique role definitions + * + * Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC + * permissioning model and organization-scoped policies. + */ + public fun setOrgPolicyCompletable(data: SetOrgPolicyRequest): CompletableFuture> +} + +internal class OrganizationsImpl( + private val httpClient: HttpClient, + private val coroutineScope: CoroutineScope, +) : Organizations { + private val moshi = Moshi.Builder().add(InstantAdapter()).build() + + override suspend fun getOrgPolicy(data: GetOrgPolicyRequest): StytchResult = + withContext(Dispatchers.IO) { + var headers = emptyMap() + + val asJson = moshi.adapter(GetOrgPolicyRequest::class.java).toJson(data) + val type = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java) + val adapter: JsonAdapter> = moshi.adapter(type) + val asMap = adapter.fromJson(asJson) ?: emptyMap() + httpClient.get("/v1/b2b/rbac/organizations/${data.organizationId}", asMap, headers) + } + + override fun getOrgPolicy( + data: GetOrgPolicyRequest, + callback: (StytchResult) -> Unit, + ) { + coroutineScope.launch { + callback(getOrgPolicy(data)) + } + } + + override fun getOrgPolicyCompletable(data: GetOrgPolicyRequest): CompletableFuture> = + coroutineScope + .async { + getOrgPolicy(data) + }.asCompletableFuture() + + override suspend fun setOrgPolicy(data: SetOrgPolicyRequest): StytchResult = + withContext(Dispatchers.IO) { + var headers = emptyMap() + + val asJson = moshi.adapter(SetOrgPolicyRequest::class.java).toJson(data) + httpClient.put("/v1/b2b/rbac/organizations/${data.organizationId}", asJson, headers) + } + + override fun setOrgPolicy( + data: SetOrgPolicyRequest, + callback: (StytchResult) -> Unit, + ) { + coroutineScope.launch { + callback(setOrgPolicy(data)) + } + } + + override fun setOrgPolicyCompletable(data: SetOrgPolicyRequest): CompletableFuture> = + coroutineScope + .async { + setOrgPolicy(data) + }.asCompletableFuture() +} diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizations/Organizations.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizations/Organizations.kt index 410d449..b6c2a1a 100644 --- a/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizations/Organizations.kt +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizations/Organizations.kt @@ -126,6 +126,47 @@ public data class ConnectedAppsRequestOptions } } +@JsonClass(generateAdapter = true) +public data class CustomRole + @JvmOverloads + constructor( + @Json(name = "role_id") + val roleId: String, + @Json(name = "description") + val description: String, + @Json(name = "permissions") + val permissions: List, + ) + +@JsonClass(generateAdapter = true) +public data class CustomRolePermission + @JvmOverloads + constructor( + @Json(name = "resource_id") + val resourceId: String, + @Json(name = "actions") + val actions: List, + ) + +public data class DeleteExternalIdRequestOptions + @JvmOverloads + constructor( + /** + * Optional authorization object. + * Pass in an active Stytch Member session token or session JWT and the request + * will be run using that member's permissions. + */ + val authorization: Authorization? = null, + ) { + internal fun addHeaders(headers: Map = emptyMap()): Map { + var res = mapOf() + if (authorization != null) { + res = authorization.addHeaders(res) + } + return res + headers + } + } + public data class DeleteRequestOptions @JvmOverloads constructor( @@ -832,6 +873,8 @@ public data class Organization */ @Json(name = "allowed_third_party_connected_apps") val allowedThirdPartyConnectedApps: List, + @Json(name = "custom_roles") + val customRoles: List, /** * An arbitrary JSON object for storing application-specific data or identity-provider-specific data. */ @@ -1344,6 +1387,26 @@ public data class CreateResponse val statusCode: Int, ) +@JsonClass(generateAdapter = true) +public data class DeleteExternalIdRequest + @JvmOverloads + constructor( + @Json(name = "organization_id") + val organizationId: String, + ) + +@JsonClass(generateAdapter = true) +public data class DeleteExternalIdResponse + @JvmOverloads + constructor( + @Json(name = "request_id") + val requestId: String, + @Json(name = "organization") + val organization: Organization, + @Json(name = "status_code") + val statusCode: Int, + ) + /** * Request type for `Organizations.delete`. */ diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizationsmembers/OrganizationsMembers.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizationsmembers/OrganizationsMembers.kt index 748b7b6..008f742 100644 --- a/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizationsmembers/OrganizationsMembers.kt +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/models/organizationsmembers/OrganizationsMembers.kt @@ -59,6 +59,25 @@ public data class CreateRequestOptions } } +public data class DeleteExternalIdRequestOptions + @JvmOverloads + constructor( + /** + * Optional authorization object. + * Pass in an active Stytch Member session token or session JWT and the request + * will be run using that member's permissions. + */ + val authorization: Authorization? = null, + ) { + internal fun addHeaders(headers: Map = emptyMap()): Map { + var res = mapOf() + if (authorization != null) { + res = authorization.addHeaders(res) + } + return res + headers + } + } + public data class DeleteMFAPhoneNumberRequestOptions @JvmOverloads constructor( @@ -387,6 +406,32 @@ public data class DangerouslyGetRequest val includeDeleted: Boolean? = null, ) +@JsonClass(generateAdapter = true) +public data class DeleteExternalIdRequest + @JvmOverloads + constructor( + @Json(name = "organization_id") + val organizationId: String, + @Json(name = "member_id") + val memberId: String, + ) + +@JsonClass(generateAdapter = true) +public data class DeleteExternalIdResponse + @JvmOverloads + constructor( + @Json(name = "request_id") + val requestId: String, + @Json(name = "member_id") + val memberId: String, + @Json(name = "member") + val member: Member, + @Json(name = "organization") + val organization: Organization, + @Json(name = "status_code") + val statusCode: Int, + ) + /** * Request type for `Members.deleteMFAPhoneNumber`. */ diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbac/RBAC.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbac/RBAC.kt index 7933c58..94c2e59 100644 --- a/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbac/RBAC.kt +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbac/RBAC.kt @@ -9,6 +9,17 @@ package com.stytch.java.b2b.models.rbac import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) +public data class OrgPolicy + @JvmOverloads + constructor( + /** + * An array of [Role objects](https://stytch.com/docs/b2b/api/rbac-role-object). + */ + @Json(name = "roles") + val roles: List, + ) + @JsonClass(generateAdapter = true) public data class Policy @JvmOverloads diff --git a/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbacorganizations/RBACOrganizations.kt b/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbacorganizations/RBACOrganizations.kt new file mode 100644 index 0000000..b9cdcd6 --- /dev/null +++ b/stytch/src/main/kotlin/com/stytch/java/b2b/models/rbacorganizations/RBACOrganizations.kt @@ -0,0 +1,103 @@ +package com.stytch.java.b2b.models.rbacorganizations + +// !!! +// WARNING: This file is autogenerated +// Only modify code within MANUAL() sections +// or your changes may be overwritten later! +// !!! + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.stytch.java.b2b.models.rbac.OrgPolicy + +/** +* Request type for `Organizations.getOrgPolicy`. +*/ +@JsonClass(generateAdapter = true) +public data class GetOrgPolicyRequest + @JvmOverloads + constructor( + /** + * Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations + * on an Organization, so be sure to preserve this value. You may also use the organization_slug or + * organization_external_id here as a convenience. + */ + @Json(name = "organization_id") + val organizationId: String, + ) + +/** +* Response type for `Organizations.getOrgPolicy`. +*/ +@JsonClass(generateAdapter = true) +public data class GetOrgPolicyResponse + @JvmOverloads + constructor( + /** + * Globally unique UUID that is returned with every API call. This value is important to log for debugging purposes; we + * may ask for this value to help identify a specific API call when helping you debug an issue. + */ + @Json(name = "request_id") + val requestId: String, + /** + * The organization-specific RBAC Policy that contains roles defined for this organization. Organization policies + * supplement the project-level RBAC policy with additional roles that are specific to the organization. + */ + @Json(name = "org_policy") + val orgPolicy: OrgPolicy, + /** + * The HTTP status code of the response. Stytch follows standard HTTP response status code patterns, e.g. 2XX values + * equate to success, 3XX values are redirects, 4XX are client errors, and 5XX are server errors. + */ + @Json(name = "status_code") + val statusCode: Int, + ) + +/** +* Request type for `Organizations.setOrgPolicy`. +*/ +@JsonClass(generateAdapter = true) +public data class SetOrgPolicyRequest + @JvmOverloads + constructor( + /** + * Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations + * on an Organization, so be sure to preserve this value. You may also use the organization_slug or + * organization_external_id here as a convenience. + */ + @Json(name = "organization_id") + val organizationId: String, + /** + * The organization-specific RBAC Policy that contains roles defined for this organization. Organization policies + * supplement the project-level RBAC policy with additional roles that are specific to the organization. + */ + @Json(name = "org_policy") + val orgPolicy: OrgPolicy, + ) + +/** +* Response type for `Organizations.setOrgPolicy`. +*/ +@JsonClass(generateAdapter = true) +public data class SetOrgPolicyResponse + @JvmOverloads + constructor( + /** + * Globally unique UUID that is returned with every API call. This value is important to log for debugging purposes; we + * may ask for this value to help identify a specific API call when helping you debug an issue. + */ + @Json(name = "request_id") + val requestId: String, + /** + * The organization-specific RBAC Policy that contains roles defined for this organization. Organization policies + * supplement the project-level RBAC policy with additional roles that are specific to the organization. + */ + @Json(name = "org_policy") + val orgPolicy: OrgPolicy, + /** + * The HTTP status code of the response. Stytch follows standard HTTP response status code patterns, e.g. 2XX values + * equate to success, 3XX values are redirects, 4XX are client errors, and 5XX are server errors. + */ + @Json(name = "status_code") + val statusCode: Int, + ) diff --git a/stytch/src/main/kotlin/com/stytch/java/common/PolicyCache.kt b/stytch/src/main/kotlin/com/stytch/java/common/PolicyCache.kt index b9bf595..83d2aea 100644 --- a/stytch/src/main/kotlin/com/stytch/java/common/PolicyCache.kt +++ b/stytch/src/main/kotlin/com/stytch/java/common/PolicyCache.kt @@ -1,8 +1,12 @@ package com.stytch.java.common import com.stytch.java.b2b.api.rbac.RBAC +import com.stytch.java.b2b.api.rbacorganizations.Organizations +import com.stytch.java.b2b.models.rbac.OrgPolicy import com.stytch.java.b2b.models.rbac.Policy import com.stytch.java.b2b.models.rbac.PolicyRequest +import com.stytch.java.b2b.models.rbac.PolicyRole +import com.stytch.java.b2b.models.rbacorganizations.GetOrgPolicyRequest import com.stytch.java.b2b.models.sessions.AuthorizationCheck import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -22,13 +26,20 @@ public class PermissionException( authorizationCheck: AuthorizationCheck, ) : RuntimeException("Permission denied for request $authorizationCheck") +private data class CachedOrgPolicy( + val orgPolicy: OrgPolicy, + val lastUpdate: Instant, +) + internal class PolicyCache( private val client: RBAC, coroutineScope: CoroutineScope, + private val organizations: Organizations? = null, ) { private val job = SupervisorJob(coroutineScope.coroutineContext[Job]) private val scope = CoroutineScope(coroutineScope.coroutineContext + job) private var cachedPolicy: Policy? = null + private val cachedOrgPolicies: MutableMap = mutableMapOf() private var policyLastUpdate: Instant? = null private var backgroundRefreshStarted = false @@ -53,6 +64,34 @@ internal class PolicyCache( return cachedPolicy ?: throw Exception("Error fetching the policy") } + private fun getOrgPolicy( + orgId: String, + invalidate: Boolean = false, + ): OrgPolicy? { + val cached = cachedOrgPolicies[orgId] + val isMissing = cached == null + val isStale = cached != null && Duration.between(cached.lastUpdate, Instant.now()).seconds > CACHE_TTL_SECONDS + + if (invalidate || isMissing || isStale) { + refreshOrgPolicy(orgId) + } + + return cachedOrgPolicies[orgId]?.orgPolicy + } + + private fun refreshOrgPolicy(orgId: String) { + val orgs = organizations ?: client.organizations + when (val result = orgs.getOrgPolicyCompletable(GetOrgPolicyRequest(orgId)).get()) { + is StytchResult.Success -> { + result.value.orgPolicy?.let { orgPolicy -> + cachedOrgPolicies[orgId] = CachedOrgPolicy(orgPolicy, Instant.now()) + } + } + + else -> {} + } + } + private fun refreshPolicy() { when (val result = client.policyCompletable(PolicyRequest()).get()) { is StytchResult.Success -> { @@ -69,6 +108,10 @@ internal class PolicyCache( while (isActive) { delay(REFRESH_INTERVAL_MS) refreshPolicy() + // Refresh all cached org policies + cachedOrgPolicies.keys.toList().forEach { orgId -> + refreshOrgPolicy(orgId) + } } } } @@ -90,8 +133,17 @@ internal class PolicyCache( throw TenancyException(subjectOrgId, authorizationCheck.organizationId) } val policy = getPolicy() + val orgPolicy = getOrgPolicy(subjectOrgId) + + // Combine roles from both global policy and org-specific policy + val allRoles: List = + buildList { + addAll(policy.roles) + orgPolicy?.roles?.let { addAll(it) } + } + val hasMatchingActionAndResource = - policy.roles + allRoles .filter { it.roleId in subjectRoles } .flatMap { it.permissions } .filter { diff --git a/stytch/src/main/kotlin/com/stytch/java/common/Version.kt b/stytch/src/main/kotlin/com/stytch/java/common/Version.kt index 5f131d3..228bef9 100644 --- a/stytch/src/main/kotlin/com/stytch/java/common/Version.kt +++ b/stytch/src/main/kotlin/com/stytch/java/common/Version.kt @@ -1,3 +1,3 @@ package com.stytch.java.common -internal const val VERSION = "9.0.0" +internal const val VERSION = "9.1.0" diff --git a/stytch/src/main/kotlin/com/stytch/java/consumer/api/sessions/Sessions.kt b/stytch/src/main/kotlin/com/stytch/java/consumer/api/sessions/Sessions.kt index cca7faf..d36b611 100644 --- a/stytch/src/main/kotlin/com/stytch/java/consumer/api/sessions/Sessions.kt +++ b/stytch/src/main/kotlin/com/stytch/java/consumer/api/sessions/Sessions.kt @@ -301,6 +301,7 @@ public interface Sessions { // ADDIMPORT: import com.stytch.java.common.StytchSessionClaim // ADDIMPORT: import com.stytch.java.common.parseJWTClaims // ADDIMPORT: import com.stytch.java.common.ParsedJWTClaims + // ADDIMPORT: import com.stytch.java.common.JWTErrorResponse // ADDIMPORT: import com.stytch.java.common.JWTResponse // ADDIMPORT: import com.stytch.java.common.JWTAuthResponse // ADDIMPORT: import com.stytch.java.common.JWTNullResponse diff --git a/stytch/src/main/kotlin/com/stytch/java/consumer/api/users/Users.kt b/stytch/src/main/kotlin/com/stytch/java/consumer/api/users/Users.kt index 1c632a9..4baaa03 100644 --- a/stytch/src/main/kotlin/com/stytch/java/consumer/api/users/Users.kt +++ b/stytch/src/main/kotlin/com/stytch/java/consumer/api/users/Users.kt @@ -21,6 +21,8 @@ import com.stytch.java.consumer.models.users.DeleteCryptoWalletRequest import com.stytch.java.consumer.models.users.DeleteCryptoWalletResponse import com.stytch.java.consumer.models.users.DeleteEmailRequest import com.stytch.java.consumer.models.users.DeleteEmailResponse +import com.stytch.java.consumer.models.users.DeleteExternalIdRequest +import com.stytch.java.consumer.models.users.DeleteExternalIdResponse import com.stytch.java.consumer.models.users.DeleteOAuthRegistrationRequest import com.stytch.java.consumer.models.users.DeleteOAuthRegistrationResponse import com.stytch.java.consumer.models.users.DeletePasswordRequest @@ -410,6 +412,15 @@ public interface Users { data: DeleteOAuthRegistrationRequest, ): CompletableFuture> + public suspend fun deleteExternalId(data: DeleteExternalIdRequest): StytchResult + + public fun deleteExternalId( + data: DeleteExternalIdRequest, + callback: (StytchResult) -> Unit, + ) + + public fun deleteExternalIdCompletable(data: DeleteExternalIdRequest): CompletableFuture> + /** * User Get Connected Apps retrieves a list of Connected Apps with which the User has successfully completed an * authorization flow. @@ -798,6 +809,28 @@ internal class UsersImpl( deleteOAuthRegistration(data) }.asCompletableFuture() + override suspend fun deleteExternalId(data: DeleteExternalIdRequest): StytchResult = + withContext(Dispatchers.IO) { + var headers = emptyMap() + + httpClient.delete("/v1/users/${data.userId}/external_id", headers) + } + + override fun deleteExternalId( + data: DeleteExternalIdRequest, + callback: (StytchResult) -> Unit, + ) { + coroutineScope.launch { + callback(deleteExternalId(data)) + } + } + + override fun deleteExternalIdCompletable(data: DeleteExternalIdRequest): CompletableFuture> = + coroutineScope + .async { + deleteExternalId(data) + }.asCompletableFuture() + override suspend fun connectedApps(data: ConnectedAppsRequest): StytchResult = withContext(Dispatchers.IO) { var headers = emptyMap() diff --git a/stytch/src/main/kotlin/com/stytch/java/consumer/models/connectedapps/ConnectedApps.kt b/stytch/src/main/kotlin/com/stytch/java/consumer/models/connectedapps/ConnectedApps.kt index bf43507..1178c00 100644 --- a/stytch/src/main/kotlin/com/stytch/java/consumer/models/connectedapps/ConnectedApps.kt +++ b/stytch/src/main/kotlin/com/stytch/java/consumer/models/connectedapps/ConnectedApps.kt @@ -62,6 +62,8 @@ public data class ConnectedApp */ @Json(name = "bypass_consent_for_offline_access") val bypassConsentForOfflineAccess: Boolean, + @Json(name = "creation_method") + val creationMethod: String, /** * The last four characters of the client secret. */ diff --git a/stytch/src/main/kotlin/com/stytch/java/consumer/models/users/Users.kt b/stytch/src/main/kotlin/com/stytch/java/consumer/models/users/Users.kt index abf9354..1b31409 100644 --- a/stytch/src/main/kotlin/com/stytch/java/consumer/models/users/Users.kt +++ b/stytch/src/main/kotlin/com/stytch/java/consumer/models/users/Users.kt @@ -687,6 +687,28 @@ public data class DeleteEmailResponse val statusCode: Int, ) +@JsonClass(generateAdapter = true) +public data class DeleteExternalIdRequest + @JvmOverloads + constructor( + @Json(name = "user_id") + val userId: String, + ) + +@JsonClass(generateAdapter = true) +public data class DeleteExternalIdResponse + @JvmOverloads + constructor( + @Json(name = "request_id") + val requestId: String, + @Json(name = "user_id") + val userId: String, + @Json(name = "user") + val user: User, + @Json(name = "status_code") + val statusCode: Int, + ) + /** * Request type for `Users.deleteOAuthRegistration`. */ diff --git a/stytch/src/main/kotlin/com/stytch/java/consumer/models/webauthn/WebAuthn.kt b/stytch/src/main/kotlin/com/stytch/java/consumer/models/webauthn/WebAuthn.kt index ec6f5ac..ac1bc6c 100644 --- a/stytch/src/main/kotlin/com/stytch/java/consumer/models/webauthn/WebAuthn.kt +++ b/stytch/src/main/kotlin/com/stytch/java/consumer/models/webauthn/WebAuthn.kt @@ -183,6 +183,8 @@ public data class AuthenticateStartRequest */ @Json(name = "return_passkey_credential_options") val returnPasskeyCredentialOptions: Boolean? = null, + @Json(name = "use_base64_url_encoding") + val useBase64URLEncoding: Boolean? = null, ) /** diff --git a/stytch/src/test/kotlin/com/stytch/java/common/PolicyCacheTest.kt b/stytch/src/test/kotlin/com/stytch/java/common/PolicyCacheTest.kt index 49975cf..980c9ff 100644 --- a/stytch/src/test/kotlin/com/stytch/java/common/PolicyCacheTest.kt +++ b/stytch/src/test/kotlin/com/stytch/java/common/PolicyCacheTest.kt @@ -1,6 +1,8 @@ package com.stytch.java.common import com.stytch.java.b2b.api.rbac.RBAC +import com.stytch.java.b2b.api.rbacorganizations.Organizations +import com.stytch.java.b2b.models.rbac.OrgPolicy import com.stytch.java.b2b.models.rbac.Policy import com.stytch.java.b2b.models.rbac.PolicyResource import com.stytch.java.b2b.models.rbac.PolicyResponse @@ -8,6 +10,7 @@ import com.stytch.java.b2b.models.rbac.PolicyRole import com.stytch.java.b2b.models.rbac.PolicyRolePermission import com.stytch.java.b2b.models.rbac.PolicyScope import com.stytch.java.b2b.models.rbac.PolicyScopePermission +import com.stytch.java.b2b.models.rbacorganizations.GetOrgPolicyResponse import com.stytch.java.b2b.models.sessions.AuthorizationCheck import io.mockk.every import io.mockk.mockk @@ -109,6 +112,35 @@ private val policy = ), ) +private val orgPolicy = + OrgPolicy( + roles = + listOf( + PolicyRole( + roleId = "org_admin", + description = "Organization-specific admin", + permissions = + listOf( + PolicyRolePermission( + resourceId = "baz", + actions = listOf("*"), + ), + ), + ), + PolicyRole( + roleId = "org_reader", + description = "Organization-specific reader", + permissions = + listOf( + PolicyRolePermission( + resourceId = "baz", + actions = listOf("read"), + ), + ), + ), + ), + ) + internal class PolicyCacheTest { private lateinit var rbac: RBAC private val testScope = CoroutineScope(Dispatchers.Unconfined) @@ -125,6 +157,14 @@ internal class PolicyCacheTest { policy = policy, ), ) + every { organizations.getOrgPolicyCompletable(any()).get() } returns + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = orgPolicy, + ), + ) } } @@ -215,6 +255,14 @@ internal class PolicyCacheTest { policy = policy, ), ) + every { organizations.getOrgPolicyCompletable(any()).get() } returns + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = OrgPolicy(roles = emptyList()), + ), + ) } val policyCache = PolicyCache(rbacMock, testScope) @@ -249,6 +297,14 @@ internal class PolicyCacheTest { ), ) } + every { organizations.getOrgPolicyCompletable(any()).get() } returns + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = OrgPolicy(roles = emptyList()), + ), + ) } val policyCache = PolicyCache(rbacMock, testScope) @@ -293,6 +349,14 @@ internal class PolicyCacheTest { policy = policy, ), ) + every { organizations.getOrgPolicyCompletable(any()).get() } returns + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = OrgPolicy(roles = emptyList()), + ), + ) } val policyCache = PolicyCache(rbacMock, testScope) @@ -312,4 +376,204 @@ internal class PolicyCacheTest { // Cancel the background refresh job policyCache.cancelBackgroundRefresh() } + + @Test + fun `succeeds when subject has matching org policy role`() { + val policyCache = PolicyCache(rbac, testScope) + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_admin"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "write", + ), + ) + } + + @Test + fun `succeeds when subject has org-specific role with read permission`() { + val policyCache = PolicyCache(rbac, testScope) + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_reader"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "read", + ), + ) + } + + @Test(expected = PermissionException::class) + fun `throws PermissionException when org role does not have matching action`() { + val policyCache = PolicyCache(rbac, testScope) + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_reader"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "write", + ), + ) + } + + @Test + fun `fetches org policy on first authorization check for an org`() { + val rbacOrgMock = + mockk(relaxed = true, relaxUnitFun = true) { + every { getOrgPolicyCompletable(any()).get() } returns + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = orgPolicy, + ), + ) + } + + val policyCache = PolicyCache(rbac, testScope, rbacOrgMock) + + // First call should fetch the org policy + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_admin"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "read", + ), + ) + + verify(exactly = 1) { rbacOrgMock.getOrgPolicyCompletable(any()) } + } + + @Test + fun `uses cached org policy on subsequent authorization checks for same org`() { + val callCount = AtomicInteger(0) + val rbacOrgMock = + mockk(relaxed = true, relaxUnitFun = true) { + every { getOrgPolicyCompletable(any()).get() } answers { + callCount.incrementAndGet() + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = orgPolicy, + ), + ) + } + } + + val policyCache = PolicyCache(rbac, testScope, rbacOrgMock) + + // First call fetches + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_admin"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "read", + ), + ) + + // Second call should use cache + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_admin"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "write", + ), + ) + + // Should only have called the API once (second call used cache) + assertEquals(1, callCount.get()) + } + + @Test + fun `fetches separate org policy for different organizations`() { + val callCount = AtomicInteger(0) + val rbacOrgMock = + mockk(relaxed = true, relaxUnitFun = true) { + every { getOrgPolicyCompletable(any()).get() } answers { + callCount.incrementAndGet() + StytchResult.Success( + GetOrgPolicyResponse( + statusCode = 200, + requestId = "", + orgPolicy = orgPolicy, + ), + ) + } + } + + val policyCache = PolicyCache(rbac, testScope, rbacOrgMock) + + // First call for org1 + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_admin"), + subjectOrgId = "org1", + authorizationCheck = + AuthorizationCheck( + organizationId = "org1", + resourceId = "baz", + action = "read", + ), + ) + + // Second call for org2 - should fetch again + policyCache.performAuthorizationCheck( + subjectRoles = listOf("org_admin"), + subjectOrgId = "org2", + authorizationCheck = + AuthorizationCheck( + organizationId = "org2", + resourceId = "baz", + action = "read", + ), + ) + + // Should have called the API twice (once per org) + assertEquals(2, callCount.get()) + } + + @Test + fun `succeeds when combining global and org policy permissions`() { + // Subject has global_reader (can read foo) and org_admin (can do anything on baz) + val policyCache = PolicyCache(rbac, testScope) + + // Should succeed using global policy + policyCache.performAuthorizationCheck( + subjectRoles = listOf("global_reader", "org_admin"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "foo", + action = "read", + ), + ) + + // Should succeed using org policy + policyCache.performAuthorizationCheck( + subjectRoles = listOf("global_reader", "org_admin"), + subjectOrgId = "my-org", + authorizationCheck = + AuthorizationCheck( + organizationId = "my-org", + resourceId = "baz", + action = "delete", + ), + ) + } } diff --git a/version.gradle.kts b/version.gradle.kts index d85606e..a046088 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -1 +1 @@ -version = "9.0.0" +version = "9.1.0"