Skip to content

Commit 60ebe63

Browse files
authored
Add support for FGA endpoints (#244)
* Add module with support for FGA endpoints * Add stub response method with scenario support * Add support for retries on FGA endpoints * Add FgaApi tests * Remove log * Update delete resource test * Fix linter errors * Add request options to Query to support warrant tokens * Add tests for request options
1 parent edbb508 commit 60ebe63

34 files changed

+2505
-0
lines changed

src/main/kotlin/com/workos/WorkOS.kt

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.workos.common.http.GenericErrorResponse
1616
import com.workos.common.http.RequestConfig
1717
import com.workos.common.http.UnprocessableEntityExceptionResponse
1818
import com.workos.directorysync.DirectorySyncApi
19+
import com.workos.fga.FgaApi
1920
import com.workos.mfa.MfaApi
2021
import com.workos.organizations.OrganizationsApi
2122
import com.workos.passwordless.PasswordlessApi
@@ -24,8 +25,18 @@ import com.workos.sso.SsoApi
2425
import com.workos.usermanagement.UserManagementApi
2526
import com.workos.webhooks.WebhooksApi
2627
import org.apache.http.client.utils.URIBuilder
28+
import java.io.IOException
2729
import java.lang.IllegalArgumentException
30+
import java.net.ConnectException
31+
import java.net.SocketTimeoutException
2832
import java.util.Properties
33+
import kotlin.random.Random
34+
import kotlin.time.Duration
35+
import kotlin.time.ExperimentalTime
36+
37+
@OptIn(ExperimentalTime::class)
38+
val MINIMUM_SLEEP_TIME = Duration.milliseconds(500)
39+
val BACKOFF_MULTIPLER = 1.5
2940

3041
/**
3142
* Global configuration class for interacting with the WorkOS API.
@@ -108,6 +119,12 @@ class WorkOS(
108119
@JvmField
109120
val webhooks = WebhooksApi()
110121

122+
/**
123+
* Module for interacting with the FGA API.
124+
*/
125+
@JvmField
126+
val fga = FgaApi(this)
127+
111128
/**
112129
* The base URL for making API requests to.
113130
*/
@@ -234,6 +251,10 @@ class WorkOS(
234251
}
235252

236253
private fun sendRequest(request: Request): String {
254+
if (request.url.path.contains("fga")) {
255+
return sendRequestWithRetry(request)
256+
}
257+
237258
val (_, response) = request.responseString()
238259

239260
var payload = String(response.data)
@@ -249,6 +270,82 @@ class WorkOS(
249270
return payload
250271
}
251272

273+
@OptIn(ExperimentalTime::class)
274+
private fun sendRequestWithRetry(request: Request): String {
275+
var requestException: Exception? = null
276+
var response: Response? = null
277+
var retryAttempts = 0
278+
279+
while (true) {
280+
requestException = null
281+
282+
try {
283+
val (_, res) = request.responseString()
284+
response = res
285+
} catch (e: IOException) {
286+
requestException = e
287+
} catch (e: InterruptedException) {
288+
requestException = e
289+
}
290+
291+
if (!shouldRetryRequest(response, retryAttempts, requestException)) {
292+
break
293+
}
294+
295+
retryAttempts += 1
296+
297+
try {
298+
Thread.sleep(getSleepTime(retryAttempts).inWholeMilliseconds)
299+
} catch (e: InterruptedException) {
300+
Thread.currentThread().interrupt()
301+
}
302+
}
303+
304+
if (requestException != null) {
305+
throw requestException
306+
}
307+
308+
var payload = if (response != null) String(response.data) else ""
309+
310+
if (payload.isEmpty()) {
311+
payload = "{}"
312+
}
313+
314+
if (response != null && response.statusCode >= 400) {
315+
handleResponseError(response, payload)
316+
}
317+
318+
return payload
319+
}
320+
321+
private fun shouldRetryRequest(response: Response?, retryAttempts: Int, requestException: Exception?): Boolean {
322+
if (retryAttempts >= 3) {
323+
return false
324+
}
325+
326+
if ((requestException != null) && (requestException.cause != null) && (requestException.cause is ConnectException || requestException.cause is SocketTimeoutException)) {
327+
return true
328+
}
329+
330+
if ((requestException != null) && (requestException.cause != null) && (requestException.cause is IOException)) {
331+
return true
332+
}
333+
334+
if (response != null && response.statusCode >= 500) {
335+
return true
336+
}
337+
338+
return false
339+
}
340+
341+
@OptIn(ExperimentalTime::class)
342+
private fun getSleepTime(retryAttempt: Int): Duration {
343+
var sleepTime: Duration = Duration.nanoseconds(MINIMUM_SLEEP_TIME.inWholeNanoseconds * Math.pow(BACKOFF_MULTIPLER, (retryAttempt + 1).toDouble()))
344+
val jitter = Random.nextDouble(0.5, 1.5)
345+
sleepTime = Duration.nanoseconds(sleepTime.inWholeNanoseconds * jitter)
346+
return sleepTime
347+
}
348+
252349
private fun <Res : Any> sendRequest(request: Request, responseType: Class<Res>): Res {
253350
val response = sendRequest(request)
254351
return mapper.readValue(response, responseType)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.workos.fga
2+
3+
import com.workos.WorkOS
4+
import com.workos.common.http.RequestConfig
5+
import com.workos.fga.models.CheckResponse
6+
import com.workos.fga.models.QueryResponse
7+
import com.workos.fga.models.Resource
8+
import com.workos.fga.models.Resources
9+
import com.workos.fga.models.Warrants
10+
import com.workos.fga.models.WriteWarrantResponse
11+
import com.workos.fga.types.CheckBatchOptions
12+
import com.workos.fga.types.CheckOptions
13+
import com.workos.fga.types.CheckRequestOptions
14+
import com.workos.fga.types.CreateResourceOptions
15+
import com.workos.fga.types.ListResourcesOptions
16+
import com.workos.fga.types.ListWarrantsOptions
17+
import com.workos.fga.types.QueryOptions
18+
import com.workos.fga.types.QueryRequestOptions
19+
import com.workos.fga.types.UpdateResourceOptions
20+
import com.workos.fga.types.WriteWarrantOptions
21+
22+
class FgaApi(private val workos: WorkOS) {
23+
/** Get an existing resource. */
24+
fun getResource(resourceType: String, resourceId: String): Resource {
25+
return workos.get("/fga/v1/resources/$resourceType/$resourceId", Resource::class.java)
26+
}
27+
28+
/** Get a list of all the existing resources matching the criteria specified */
29+
fun listResources(options: ListResourcesOptions? = null): Resources {
30+
val params: Map<String, String> =
31+
RequestConfig.toMap(options ?: ListResourcesOptions()) as Map<String, String>
32+
33+
return workos.get(
34+
"/fga/v1/resources",
35+
Resources::class.java,
36+
RequestConfig.builder().params(params).build()
37+
)
38+
}
39+
40+
/** Create a new resource in the current environment. */
41+
fun createResource(options: CreateResourceOptions): Resource {
42+
return workos.post(
43+
"/fga/v1/resources",
44+
Resource::class.java,
45+
RequestConfig.builder().data(options).build()
46+
)
47+
}
48+
49+
/** Update an existing resource. */
50+
fun updateResource(resourceType: String, resourceId: String, options: UpdateResourceOptions): Resource {
51+
return workos.put(
52+
"/fga/v1/resources/$resourceType/$resourceId",
53+
Resource::class.java,
54+
RequestConfig.builder().data(options).build()
55+
)
56+
}
57+
58+
/** Delete a resource in the current environment. */
59+
fun deleteResource(resourceType: String, resourceId: String) {
60+
workos.delete("/fga/v1/resources/$resourceType/$resourceId")
61+
}
62+
63+
/** Get a list of all existing warrants matching the criteria specified */
64+
fun listWarrants(options: ListWarrantsOptions? = null): Warrants {
65+
val params: Map<String, String> =
66+
RequestConfig.toMap(options ?: ListWarrantsOptions()) as Map<String, String>
67+
68+
return workos.get(
69+
"/fga/v1/warrants",
70+
Warrants::class.java,
71+
RequestConfig.builder().params(params).build()
72+
)
73+
}
74+
75+
/** Perform a warrant write (create/delete) in the current environment */
76+
fun writeWarrant(options: WriteWarrantOptions): WriteWarrantResponse {
77+
return workos.post(
78+
"/fga/v1/warrants",
79+
WriteWarrantResponse::class.java,
80+
RequestConfig.builder().data(options).build()
81+
)
82+
}
83+
84+
/** Performs multiple warrant writes in the current environment */
85+
fun batchWriteWarrants(options: List<WriteWarrantOptions>): WriteWarrantResponse {
86+
return workos.post(
87+
"/fga/v1/warrants",
88+
WriteWarrantResponse::class.java,
89+
RequestConfig.builder().data(options).build()
90+
)
91+
}
92+
93+
/** Perform a warrant check in the current environment */
94+
fun check(options: CheckOptions, requestOptions: CheckRequestOptions? = null): CheckResponse {
95+
val config = RequestConfig.builder()
96+
.data(options)
97+
98+
if (requestOptions != null) {
99+
config.headers(mapOf("Warrant-Token" to requestOptions.warrantToken))
100+
}
101+
102+
return workos.post("/fga/v1/check", CheckResponse::class.java, config.build())
103+
}
104+
105+
/** Perform a batch warrant check in the current environment */
106+
fun checkBatch(options: CheckBatchOptions, requestOptions: CheckRequestOptions? = null): Array<CheckResponse> {
107+
val config = RequestConfig.builder()
108+
.data(options)
109+
110+
if (requestOptions != null) {
111+
config.headers(mapOf("Warrant-Token" to requestOptions.warrantToken))
112+
}
113+
114+
return workos.post("/fga/v1/check", Array<CheckResponse>::class.java, config.build())
115+
}
116+
117+
/** Perform a query in the current environment */
118+
fun query(options: QueryOptions, requestOptions: QueryRequestOptions? = null): QueryResponse {
119+
val config = RequestConfig.builder()
120+
.data(options)
121+
122+
if (requestOptions != null) {
123+
config.headers(mapOf("Warrant-Token" to requestOptions.warrantToken))
124+
}
125+
126+
return workos.post("/fga/v1/query", QueryResponse::class.java, config.build())
127+
}
128+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.workos.fga.builders
2+
3+
import com.workos.fga.types.CheckBatchOptions
4+
import com.workos.fga.types.WarrantCheckOptions
5+
6+
class CheckBatchOptionsBuilder @JvmOverloads constructor(
7+
private var checks: List<WarrantCheckOptions>,
8+
private var debug: Boolean? = null,
9+
) {
10+
fun checks(value: List<WarrantCheckOptions>) = apply { checks = value }
11+
12+
fun debug(value: Boolean) = apply { debug = value }
13+
14+
fun build(): CheckBatchOptions {
15+
return CheckBatchOptions(
16+
checks = this.checks,
17+
debug = this.debug,
18+
)
19+
}
20+
21+
companion object {
22+
@JvmStatic
23+
fun create(checks: List<WarrantCheckOptions>, debug: Boolean? = null): CheckBatchOptionsBuilder {
24+
return CheckBatchOptionsBuilder(checks, debug)
25+
}
26+
}
27+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.workos.fga.builders
2+
3+
import com.workos.fga.types.CheckOptions
4+
import com.workos.fga.types.WarrantCheckOptions
5+
6+
class CheckOptionsBuilder @JvmOverloads constructor(
7+
private var op: String? = null,
8+
private var checks: List<WarrantCheckOptions>,
9+
private var debug: Boolean? = null,
10+
) {
11+
constructor(checks: List<WarrantCheckOptions>, debug: Boolean? = null) : this(null, checks, debug)
12+
13+
fun op(value: String) = apply { op = value }
14+
15+
fun checks(value: List<WarrantCheckOptions>) = apply { checks = value }
16+
17+
fun debug(value: Boolean) = apply { debug = value }
18+
19+
fun build(): CheckOptions {
20+
return CheckOptions(
21+
op = this.op,
22+
checks = this.checks,
23+
debug = this.debug,
24+
)
25+
}
26+
27+
companion object {
28+
@JvmStatic
29+
fun create(op: String, checks: List<WarrantCheckOptions>, debug: Boolean? = null): CheckOptionsBuilder {
30+
return CheckOptionsBuilder(op, checks, debug)
31+
}
32+
33+
@JvmStatic
34+
fun create(checks: List<WarrantCheckOptions>, debug: Boolean? = null): CheckOptionsBuilder {
35+
return CheckOptionsBuilder("", checks, debug)
36+
}
37+
}
38+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.workos.fga.builders
2+
3+
import com.workos.fga.types.CreateResourceOptions
4+
5+
class CreateResourceOptionsBuilder @JvmOverloads constructor(
6+
private var resourceType: String,
7+
private var resourceId: String? = null,
8+
private var meta: Map<String, String>? = null,
9+
) {
10+
fun resourceType(value: String) = apply { resourceType = value }
11+
12+
fun resourceId(value: String) = apply { resourceId = value }
13+
14+
fun meta(value: Map<String, String>) = apply { meta = value }
15+
16+
fun build(): CreateResourceOptions {
17+
return CreateResourceOptions(
18+
resourceType = this.resourceType,
19+
resourceId = this.resourceId,
20+
meta = this.meta,
21+
)
22+
}
23+
24+
companion object {
25+
@JvmStatic
26+
fun create(resourceType: String): CreateResourceOptionsBuilder {
27+
return CreateResourceOptionsBuilder(resourceType)
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)