Class: OpenFga::SdkClient

Inherits:
Object
  • Object
show all
Defined in:
lib/openfga/client/openfga_client.rb

Constant Summary collapse

PAGE_SIZE =
50
CREDENTIALS_METHODS =
%i[none api_token client_credentials].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ SdkClient

Returns a new instance of SdkClient.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/openfga/client/openfga_client.rb', line 12

def initialize(config = {})
  raise ConfigurationNilError.new(:api_url) unless config[:api_url]

  @config = config

  @config[:credentials] ||= {
    method: :none
  }

  @logger = new_logger(@config)
  @logger.debug('Using custom logger instance') if @config[:logger]

  # Later we can support custom token managers.
  @token_manager = case @config[:credentials][:method]
                   when :none
                     TokenManager::NoopTokenManager.new
                   when :api_token
                     TokenManager::StaticTokenManager.new(@config.dig(:credentials, :api_token))
                   when :client_credentials
                     oauth_config = TokenManager::Oauth2TokenManager::Config.new(
                       client_id: @config.dig(:credentials, :client_id),
                       client_secret: @config.dig(:credentials, :client_secret),
                       token_issuer: @config.dig(:credentials, :api_token_issuer),
                       audience: @config.dig(:credentials, :api_audience),
                       logger: @logger
                     )

                     TokenManager::Oauth2TokenManager.new(oauth_config)
                   else
                     valid_methods = CREDENTIALS_METHODS.join(', ')

                     raise ConfigurationError, "Unknown credentials method: #{@config[:credentials]}" \
                       "Supported methods: #{valid_methods}"
  end

  api_client_config = Configuration.new do |c|
    c.server_index = nil
    c.host = @config[:api_url]
    c.scheme = URI(@config[:api_url]).scheme
    c.logger = @logger
    c.debugging = true if @config[:logger]
  end

  @api_client = OpenFga::OpenFgaApi.new(ApiClient.new(api_client_config))
end

Instance Method Details

#batch_check(body = {}) ⇒ BatchCheckResponse

Performs multiple relationship checks in a single batch request.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing:

    • :checks [Array<Hash>] Each check item must include:

      - :tuple_key [Hash] The tuple key for the relationship to check, with:
          - :user [String] The user involved in the relationship.
          - :relation [String, Symbol] The relation to check (e.g. "reader" or :owner).
          - :object [String] The object involved in the relationship.
      - :correlation_id [String] Unique identifier for correlating the request with the response (max 36 chars, alphanumeric + hyphens).
      - :contextual_tuples [Hash] (optional) Additional contextual tuples to include in the check.
      - :context [Hash] (optional) Additional context for the check.
      
    • :opts [Hash] Optional parameters for the batch check:

      - :authorization_model_id [String] The ID of the authorization model.
      - :consistency [String] The consistency level for the checks.
      - :max_parallel_requests [Integer] Maximum concurrent requests \(default: 10\).
      - :max_batch_size [Integer] Maximum checks per batch \(default: 50\).
      

Returns:

  • (BatchCheckResponse)

    The result of the batch check operation with results keyed by correlation_id.

Raises:

  • (ArgumentError)

    If the checks array is empty, or if any check item is missing required parameters.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/openfga/client/openfga_client.rb', line 77

def batch_check(body = {})
  checks = body[:checks]
  fail ArgumentError, "Missing the required parameter 'checks'" if checks.nil? || checks.empty?

  opts = body[:opts] || {}
  # Configuration with defaults matching Python SDK
  max_parallel_requests = opts[:max_parallel_requests] || 10
  max_batch_size = opts[:max_batch_size] || 50

  # Validate all checks first and check for duplicates
  correlation_ids = Set.new

  checks.each_with_index do |check, index|
    fail ArgumentError, "Missing 'tuple_key' in check item at index #{index}" if check[:tuple_key].nil?
    fail ArgumentError, "Missing 'correlation_id' in check item at index #{index}" if check[:correlation_id].nil?

    # Validate correlation_id format
    correlation_id = check[:correlation_id].to_s
    unless correlation_id.match?(/^[\w\d-]{1,36}$/)
      fail ArgumentError, "correlation_id must be alphanumeric with hyphens only, max 36 characters: #{correlation_id}"
    end

    # Check for duplicates
    if correlation_ids.include?(correlation_id)
      fail ArgumentError, "Duplicate correlation_id found: #{correlation_id}"
    end
    correlation_ids.add(correlation_id)
  end

  # If we have fewer checks than the batch limit, use simple approach
  if checks.length <= max_batch_size
    return execute_single_batch_check(checks, opts)
  end

  # Split checks into batches and process concurrently
  check_batches = checks.each_slice(max_batch_size).to_a
  all_results = process_batches_concurrently(check_batches, max_parallel_requests, opts)

  # Merge all batch results
  merged_results = {}
  all_results.each do |batch_response|
    if batch_response&.result
      merged_results.merge!(batch_response.result)
    end
  end

  # Return a BatchCheckResponse with merged results
  BatchCheckResponse.new(result: merged_results)
end

#check(body = {}) ⇒ CheckResponse

Checks whether a specific relationship exists in the store.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing check details

    • :user [String] The user involved in the relationship

    • :relation [String, Symbol] The relation to check (e.g. “reader”, “owner”)

    • :object [String] The object involved in the relationship

    • :contextual_tuples [Hash] Additional contextual tuples to include in the check:

      - :tuple_keys [Array<Hash>] Array of tuple key hashes
      
    • :opts [Hash] Optional parameters for the check request:

      - :authorization_model_id [String] The ID of the authorization model to use for the check
      - :context [Hash] Additional context for the check
      - :store_id [String] The ID of the store (required if not set in client config)
      

Returns:

Raises:

  • (ArgumentError)

    If any of the required parameters (user, relation, or object) are missing



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/openfga/client/openfga_client.rb', line 140

def check(body = {})
  opts = body[:opts] || {}
  tuple_key = CheckRequestTupleKey.new({
                                         user: body[:user], relation: body[:relation].to_s, object: body[:object] })

  request_body = CheckRequest.new({ tuple_key: })

  unless body[:contextual_tuples].nil?
    tuple_keys = body[:contextual_tuples][:tuple_keys].map { |tuple_key| TupleKey.new(tuple_key) }
    request_body.contextual_tuples = ContextualTupleKeys.new(tuple_keys:)
  end

  request_body.context = opts[:context] unless opts[:context].nil?
  request_body.authorization_model_id = authorization_model_id(opts)

  @api_client.check(store_id(opts), request_body, wrap_options(opts))
end

#create_store(body = {}) ⇒ CreateStoreResponse

Creates a new OpenFGA store for storing authorization models and relationship tuples.

Parameters:

  • body (Hash) (defaults to: {})

    The request body with store details.

    • :name [String] The name of the store to create.

    • :opts [Hash] Optional parameters for the request.

Returns:



163
164
165
166
167
168
# File 'lib/openfga/client/openfga_client.rb', line 163

def create_store(body = {})
  request_body = OpenFga::CreateStoreRequest.new(body)
  opts = body[:opts] || {}

  @api_client.create_store(request_body, wrap_options(opts))
end

#delete_store(opts = {}) ⇒ nil

Delete a store Delete an OpenFGA store.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional parameters for the request

Options Hash (opts):

  • :store_id (String)

    The ID of the store to delete (required if not set in client config)

Returns:

  • (nil)


175
176
177
# File 'lib/openfga/client/openfga_client.rb', line 175

def delete_store(opts = {})
  @api_client.delete_store(store_id(opts), wrap_options(opts))
end

#execute_api_request(method:, path:, path_params: {}, query_params: {}, body: nil, headers: {}) ⇒ ApiExecutorResponse

Executes a raw API request against the FGA server with automatic auth injection.

Parameters:

  • method (String, Symbol)

    HTTP method (e.g. :get, :post)

  • path (String)

    URL path template (e.g. ‘/stores/#store_id/check’)

  • path_params (Hash) (defaults to: {})

    Values for placeholder substitution

  • query_params (Hash) (defaults to: {})

    URL query parameters

  • body (Hash, nil) (defaults to: nil)

    Request body (JSON-serialized automatically)

  • headers (Hash) (defaults to: {})

    Additional request headers

Returns:



478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/openfga/client/openfga_client.rb', line 478

def execute_api_request(method:, path:, path_params: {}, query_params: {}, body: nil, headers: {})
  request = ApiExecutorRequest.new(
    method:,
    path:,
    path_params:,
    query_params:,
    body:,
    headers:
  )
  request.validate!

  resolved_path = substitute_path_params(request.path, request.path_params)
  merged_headers = build_auth_headers.merge(request.headers)

  opts = {
    header_params: merged_headers,
    query_params:  request.query_params,
    body:          request.body,
    return_type:   'Object'
  }

  data, status, response_headers = @api_client.api_client.call_api(request.method, resolved_path, opts)
  ApiExecutorResponse.new(data:, status:, headers: response_headers)
end

#expand(body = {}) ⇒ ExpandResponse

Expands a relationship tuple to retrieve all users and groups that have the specified relation with the given object.

Parameters:

  • body (Hash) (defaults to: {})

    Expansion request details:

    • :relation [String, Symbol] The relation to expand (e.g. “reader”, :writer)

    • :object [String] The object involved in the relationship

    • :contextual_tuples [Hash] Additional contextual tuples for the expansion

    • :opts [Hash] Optional parameters:

      - :store_id [String] Store ID (required if not set in client config)
      - :authorization_model_id [String] Authorization model ID for the expansion
      - :consistency [String] Consistency level ("UNSPECIFIED", "MINIMIZE_LATENCY", "HIGHER_CONSISTENCY")
      

Returns:

Raises:

  • (ArgumentError)

    If :relation or :object is missing



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/openfga/client/openfga_client.rb', line 190

def expand(body = {})
  relation, object = body[:relation], body[:object]

  fail ArgumentError, "Missing the required parameter 'relation'" if relation.nil?
  fail ArgumentError, "Missing the required parameter 'object'" if object.nil?

  opts = body[:opts] || {}

  request_body = ExpandRequest.new(
    tuple_key: ExpandRequestTupleKey.new(relation:, object:),
    authorization_model_id: authorization_model_id(opts),
    consistency: opts[:consistency] || 'UNSPECIFIED'
  )

  if body.include?(:contextual_tuples)
    contextual_tuples = body[:contextual_tuples]
    tuple_keys = contextual_tuples[:tuple_keys].map { |tuple_key| TupleKey.new(tuple_key) }
    request_body.contextual_tuples = ContextualTupleKeys.new(tuple_keys:)
  end

  # Call the API client to perform the expansion
  @api_client.expand(store_id(opts), request_body, wrap_options(opts))
end

#get_store(opts = {}) ⇒ GetStoreResponse

Get a store Returns an OpenFGA store by its identifier

Parameters:

  • opts (Hash) (defaults to: {})

    Optional parameters for the request

Options Hash (opts):

  • :store_id (String)

    The ID of the store to retrieve (required if not set in client config)

Returns:



219
220
221
# File 'lib/openfga/client/openfga_client.rb', line 219

def get_store(opts = {})
  @api_client.get_store(store_id(opts), wrap_options(opts))
end

#list_objects(body = {}) ⇒ ListObjectsResponse

List objects for a given user and relation. Returns objects of a specified type for a user and relation.

Parameters:

  • body (Hash) (defaults to: {})

    Request body:

    • :user [String] User to list objects for.

    • :relation [String, Symbol] Relation to list objects for (e.g., “reader”, :writer).

    • :type [String] Type of objects to list.

    • :contextual_tuples [Hash] (optional) Additional contextual tuples.

    • :context [Hash] (optional) Additional context.

    • :opts [Hash] (optional) Request options:

      - :store_id [String] Store ID (required if not set in client config).
      - :authorization_model_id [String] Authorization model ID.
      - :consistency [String] Consistency level ("UNSPECIFIED", "MINIMIZE_LATENCY", "HIGHER_CONSISTENCY").
      

Returns:

Raises:

  • (ArgumentError)

    If :user, :relation, or :type is missing.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/openfga/client/openfga_client.rb', line 246

def list_objects(body = {})
  user = body[:user]
  relation = body[:relation]
  type = body[:type]
  contextual_tuples = body[:contextual_tuples]
  context = body[:context]
  opts = body[:opts] || {}

  fail ArgumentError, "Missing the required parameter 'user'" if user.nil?
  fail ArgumentError, "Missing the required parameter 'relation'" if relation.nil?
  fail ArgumentError, "Missing the required parameter 'type'" if type.nil?

  request_body = ListObjectsRequest.new(
    type:,
    relation:,
    user:,
    authorization_model_id: authorization_model_id(opts),
    consistency: opts[:consistency] || 'UNSPECIFIED'
  )

  unless contextual_tuples.nil?
    tuple_keys = contextual_tuples[:tuple_keys].map { |tuple_key| TupleKey.new(tuple_key) }
    request_body.contextual_tuples = ContextualTupleKeys.new(tuple_keys:)
  end

  request_body.context = context unless context.nil?

  # Call the API client to perform the list objects request
  @api_client.list_objects(store_id(opts), request_body, wrap_options(opts))
end

#list_stores(opts = {}) ⇒ ListStoresResponse

List all stores Returns a paginated list of OpenFGA stores and a continuation token to get additional stores. The continuation token will be empty if there are no more stores.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional parameters for the request

Options Hash (opts):

  • :page_size (Integer)

    The number of stores to return per page

  • :continuation_token (String)

    The continuation token for pagination

Returns:



283
284
285
# File 'lib/openfga/client/openfga_client.rb', line 283

def list_stores(opts = {})
  @api_client.list_stores(wrap_options(opts))
end

#list_users(body = {}) ⇒ ListUsersResponse

List users that have a specific relation with an object Returns a list of all users that have the specified relation with the given object.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing list details

Options Hash (body):

  • :relation (String, Symbol)

    The relation to check for (e.g., “reader”, “writer”)

  • :object (String)

    The object to check relations against

  • :user_filters (Array<Hash>)

    Filter criteria for the users to return

  • :contextual_tuples (Array<Hash>)

    Additional contextual tuples to include in the query

  • :context (Hash)

    Additional context for the query

  • :opts (Hash)

    Optional parameters for the request, including:

    • :authorization_model_id [String] The ID of the authorization model to use for the query

    • :consistency [String] The consistency level for the query (defaults to ‘UNSPECIFIED’)

    • :store_id [String] The store ID to query users from (required if not set in client config)

Returns:

Raises:

  • (ArgumentError)

    If the required parameter ‘relation’ is missing

  • (ArgumentError)

    If the required parameter ‘object’ is missing



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/openfga/client/openfga_client.rb', line 302

def list_users(body = {})
  relation = body[:relation]
  object = body[:object]
  user_filters = body[:user_filters]
  contextual_tuples = body[:contextual_tuples]
  context = body[:context]
  opts = body[:opts] || {}

  fail ArgumentError, "Missing the required parameter 'relation'" if relation.nil?
  fail ArgumentError, "Missing the required parameter 'object'" if object.nil?

  request_body = ListUsersRequest.new(
    relation: relation.to_s,
    object:,
    context:,
    user_filters: (user_filters || []).map { |filter| UserTypeFilter.new(filter) },
    authorization_model_id: authorization_model_id(opts),
  )

  # Build the request body
  if contextual_tuples
    tuples = contextual_tuples.map { |tuple| TupleKey.new(tuple) }
    request_body.contextual_tuples = tuples
  end

  @api_client.list_users(store_id(opts), request_body, wrap_options(opts))
end

#read(body = {}) ⇒ ReadResponse

Read tuples from the store Reads tuples from the store.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing read query details

  • opts (Hash)

    a customizable set of options

Options Hash (body):

  • :user (String)

    The user to read tuples for

  • :relation (String, Symbol)

    The relation to read tuples for

  • :object (String)

    The object to read tuples for

Returns:



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/openfga/client/openfga_client.rb', line 341

def read(body = {})
  user = body[:user]
  relation = body[:relation]
  object = body[:object]
  opts = body[:opts] || {}

  request_body = ReadRequest.new(
    continuation_token: opts[:continuation_token],
    page_size: opts[:page_size] || PAGE_SIZE,
    consistency: opts[:consistency])

  if user || relation || object
    tuple_key = {}
    tuple_key[:user] = user if user
    tuple_key[:relation] = relation.to_s if relation
    tuple_key[:object] = object if object
    request_body.tuple_key = ReadRequestTupleKey.new(tuple_key)
  end

  @api_client.read(store_id(opts), request_body, wrap_options(opts))
end

#read_assertions(opts = {}) ⇒ ReadAssertionsResponse

Read assertions Retrieve assertions for a specific store and authorization model

Parameters:

  • opts (Hash) (defaults to: {})

    Optional parameters for the request

Options Hash (opts):

  • :store_id (String)

    The ID of the store (required if not set in client config)

  • :authorization_model_id (String)

    The ID of the authorization model (required if not set in client config)

Returns:

Raises:



371
372
373
374
# File 'lib/openfga/client/openfga_client.rb', line 371

def read_assertions(opts = {})
  fail MissingAuthorizationModelIdError unless authorization_model_id(opts)
  @api_client.read_assertions(store_id(opts), authorization_model_id(opts), wrap_options(opts))
end

#read_authorization_model(opts = {}) ⇒ ReadAuthorizationModelResponse

Read an authorization model Retrieves a specific authorization model by its ID from a given store.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional parameters for the request

Options Hash (opts):

  • :store_id (String)

    The ID of the store (required if not set in client config)

  • :authorization_model_id (String)

    The ID of the authorization model to read (required if not set in client config)

Returns:

Raises:



384
385
386
387
# File 'lib/openfga/client/openfga_client.rb', line 384

def read_authorization_model(opts = {})
  fail MissingAuthorizationModelIdError unless authorization_model_id(opts)
  @api_client.read_authorization_model(store_id(opts), authorization_model_id(opts), wrap_options(opts))
end

#read_authorization_models(opts = {}) ⇒ ReadAuthorizationModelsResponse

Read all authorization models Retrieves all authorization models for a specific store.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional parameters for the request

Options Hash (opts):

  • :store_id (String)

    The ID of the store (required if not set in client config)

Returns:

Raises:



395
396
397
# File 'lib/openfga/client/openfga_client.rb', line 395

def read_authorization_models(opts = {})
  @api_client.read_authorization_models(store_id(opts), wrap_options(opts))
end

#read_changes(opts = {}) ⇒ ReadChangesResponse

Read changes Reads the list of historical relationship tuple writes and deletes.

Parameters:

  • body (Hash)

    The request body containing change query details

  • opts (Hash) (defaults to: {})

    a customizable set of options

Options Hash (opts):

  • :page_size (Integer)

    The number of pages to return in the request

  • :continuation_token (String)

    The continuation token to use to get the next page of results. This will be empty if there are no more results

  • :store_id (String)

    The store ID to read changes from (required if not set in client config)

Returns:



408
409
410
# File 'lib/openfga/client/openfga_client.rb', line 408

def read_changes(opts = {})
  @api_client.read_changes(store_id(opts), wrap_options(opts))
end

#write(body = {}) ⇒ WriteResponse

Write tuples to the store. Atomically creates, updates, or deletes relationship tuples for a given store.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing write details:

    • :writes [Array<Hash>] Tuples to add or update in the store.

    • :deletes [Array<Hash>] Tuples to remove from the store.

    • :opts [Hash] Optional parameters for the request:

      - :store_id [String] Store ID to write to (required if not set in client config).
      - :authorization_model_id [String] Authorization model ID to use for writing.
      

Returns:

  • (WriteResponse)

    The response from the write operation.



421
422
423
424
425
426
427
# File 'lib/openfga/client/openfga_client.rb', line 421

def write(body = {})
  opts = body[:opts] || {}
  request_body = WriteRequest.new(writes: body[:writes], deletes: body[:deletes])

  request_body.authorization_model_id = opts[:authorization_model_id] if opts[:authorization_model_id]
  @api_client.write(store_id(opts), request_body, wrap_options(opts))
end

#write_assertions(body = {}) ⇒ nil

Writes assertions for a specific store and authorization model. Updates or creates assertions used for testing authorization logic.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing assertions:

    • :assertions [Array<Hash>] The assertions to write.

    • :opts [Hash] Optional parameters:

      - :store_id [String] Store ID (required if not set in client config).
      - :authorization_model_id [String] Authorization model ID (required if not set in client config).
      

Returns:

  • (nil)

Raises:



439
440
441
442
443
444
445
446
447
448
# File 'lib/openfga/client/openfga_client.rb', line 439

def write_assertions(body = {})
  opts = body[:opts] || {}
  store_id = store_id(opts)
  model_id = authorization_model_id(opts)
  fail MissingAuthorizationModelIdError unless model_id

  request_body = WriteAssertionsRequest.new(assertions: body[:assertions])

  @api_client.write_assertions(store_id, model_id, request_body, wrap_options(opts))
end

#write_authorization_model(body = {}) ⇒ WriteAuthorizationModelResponse

Writes an authorization model for a specific store. Creates or updates the authorization model with provided type definitions, schema version, and conditions.

Parameters:

  • body (Hash) (defaults to: {})

    The request body containing:

    • :type_definitions [Array<TypeDefinition>] Type definitions for the authorization model.

    • :schema_version [String] Schema version for the authorization model.

    • :conditions [Hash] Conditions for the authorization model.

    • :opts [Hash] Optional parameters:

      - :store_id [String] Store ID (required if not set in client config).
      

Returns:



459
460
461
462
463
464
465
466
467
468
# File 'lib/openfga/client/openfga_client.rb', line 459

def write_authorization_model(body = {})
  opts = body[:opts] || {}
  request_body = WriteAuthorizationModelRequest.new(
    type_definitions: body[:type_definitions],
    schema_version: body[:schema_version],
    conditions: body[:conditions]
  )

  @api_client.write_authorization_model(store_id(opts), request_body, wrap_options(opts))
end