From dc277ee59ed4fc35f26b5bb0f0ce2ae9868b1c65 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 19 Nov 2025 22:40:31 +0530 Subject: [PATCH 1/5] fix: lint fixes, sync with generator --- api_client.go | 804 +++++++++++------------ api_open_fga.go | 1338 +++++++++++++++++++------------------- client/streaming_test.go | 10 +- streaming.go | 7 +- streaming_test.go | 16 +- 5 files changed, 1090 insertions(+), 1085 deletions(-) diff --git a/api_client.go b/api_client.go index 51b891f..53251d6 100644 --- a/api_client.go +++ b/api_client.go @@ -1,499 +1,499 @@ package openfga import ( - "bytes" - "context" - "encoding/json" - "encoding/xml" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/http/httputil" - "net/url" - "os" - "reflect" - "regexp" - "strings" - "time" - "unicode/utf8" - - "github.com/openfga/go-sdk/internal/utils/retryutils" - "github.com/openfga/go-sdk/telemetry" + "bytes" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "reflect" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/openfga/go-sdk/internal/utils/retryutils" + "github.com/openfga/go-sdk/telemetry" ) var ( - jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) - xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`) + jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) + xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`) ) // ErrorResponse defines the error that will be asserted by FGA API. // This will only be used for error that is not defined type ErrorResponse struct { - Code string `json:"code"` - Message string `json:"message"` + Code string `json:"code"` + Message string `json:"message"` } // APIClient manages communication with the OpenFGA API v1.x // In most cases there should be only one, shared, APIClient. type APIClient struct { - cfg *Configuration - common service // Reuse a single struct instead of allocating one for each service on the heap. + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. - // API Services + // API Services - OpenFgaApi OpenFgaApi + OpenFgaApi OpenFgaApi } type service struct { - client *APIClient - RetryParams *RetryParams + client *APIClient + RetryParams *RetryParams } // NewAPIClient creates a new API client. Requires a userAgent string describing your application. // optionally a custom http.Client to allow for advanced features such as caching. func NewAPIClient(cfg *Configuration) *APIClient { - if cfg.Telemetry == nil { - cfg.Telemetry = telemetry.DefaultTelemetryConfiguration() - } - if cfg.HTTPClient == nil { - if cfg.Credentials == nil { - cfg.HTTPClient = http.DefaultClient - } else { - cfg.Credentials.Context = context.Background() - telemetry.Bind(cfg.Credentials.Context, telemetry.Get(telemetry.TelemetryFactoryParameters{Configuration: cfg.Telemetry})) - var httpClient, headers = cfg.Credentials.GetHttpClientAndHeaderOverrides(retryutils.GetRetryParamsOrDefault(cfg.RetryParams), cfg.Debug) - if len(headers) > 0 { - for idx := range headers { - cfg.AddDefaultHeader(headers[idx].Key, headers[idx].Value) - } - } - if httpClient != nil { - cfg.HTTPClient = httpClient - } - } - } - - c := &APIClient{} - c.cfg = cfg - c.common.client = c - c.common.RetryParams = cfg.RetryParams - - // API Services - c.OpenFgaApi = (*OpenFgaApiService)(&c.common) - - return c + if cfg.Telemetry == nil { + cfg.Telemetry = telemetry.DefaultTelemetryConfiguration() + } + if cfg.HTTPClient == nil { + if cfg.Credentials == nil { + cfg.HTTPClient = http.DefaultClient + } else { + cfg.Credentials.Context = context.Background() + telemetry.Bind(cfg.Credentials.Context, telemetry.Get(telemetry.TelemetryFactoryParameters{Configuration: cfg.Telemetry})) + var httpClient, headers = cfg.Credentials.GetHttpClientAndHeaderOverrides(retryutils.GetRetryParamsOrDefault(cfg.RetryParams), cfg.Debug) + if len(headers) > 0 { + for idx := range headers { + cfg.AddDefaultHeader(headers[idx].Key, headers[idx].Value) + } + } + if httpClient != nil { + cfg.HTTPClient = httpClient + } + } + } + + c := &APIClient{} + c.cfg = cfg + c.common.client = c + c.common.RetryParams = cfg.RetryParams + + // API Services + c.OpenFgaApi = (*OpenFgaApiService)(&c.common) + + return c } // selectHeaderContentType select a content type from the available list. func selectHeaderContentType(contentTypes []string) string { - if len(contentTypes) == 0 { - return "" - } - if contains(contentTypes, "application/json") { - return "application/json" - } - return contentTypes[0] // use the first content type specified in 'consumes' + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' } // selectHeaderAccept join all accept types and return func selectHeaderAccept(accepts []string) string { - if len(accepts) == 0 { - return "" - } + if len(accepts) == 0 { + return "" + } - if contains(accepts, "application/json") { - return "application/json" - } + if contains(accepts, "application/json") { + return "application/json" + } - return strings.Join(accepts, ",") + return strings.Join(accepts, ",") } // contains is a case insensitive match, finding needle in a haystack func contains(haystack []string, needle string) bool { - loweredNeedle := strings.ToLower(needle) - for _, a := range haystack { - if strings.ToLower(a) == loweredNeedle { - return true - } - } - return false + loweredNeedle := strings.ToLower(needle) + for _, a := range haystack { + if strings.ToLower(a) == loweredNeedle { + return true + } + } + return false } // Verify optional parameters are of the correct type. func typeCheckParameter(obj interface{}, expected string, name string) error { - // Make sure there is an object. - if obj == nil { - return nil - } - - // Check the type is as expected. - if reflect.TypeOf(obj).String() != expected { - return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) - } - return nil + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) + } + return nil } // parameterToString convert interface{} parameters to string, using a delimiter if format is provided. func parameterToString(obj interface{}, collectionFormat string) string { - var delimiter string - - switch collectionFormat { - case "pipes": - delimiter = "|" - case "ssv": - delimiter = " " - case "tsv": - delimiter = "\t" - case "csv": - delimiter = "," - } - - if reflect.TypeOf(obj).Kind() == reflect.Slice { - return strings.Trim(strings.ReplaceAll(fmt.Sprint(obj), " ", delimiter), "[]") - } else if t, ok := obj.(time.Time); ok { - return t.Format(time.RFC3339) - } - - return fmt.Sprintf("%v", obj) + var delimiter string + + switch collectionFormat { + case "pipes": + delimiter = "|" + case "ssv": + delimiter = " " + case "tsv": + delimiter = "\t" + case "csv": + delimiter = "," + } + + if reflect.TypeOf(obj).Kind() == reflect.Slice { + return strings.Trim(strings.ReplaceAll(fmt.Sprint(obj), " ", delimiter), "[]") + } else if t, ok := obj.(time.Time); ok { + return t.Format(time.RFC3339) + } + + return fmt.Sprintf("%v", obj) } // helper for converting interface{} parameters to json strings func parameterToJson(obj interface{}) (string, error) { - jsonBuf, err := json.Marshal(obj) - if err != nil { - return "", err - } - return string(jsonBuf), err + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err } // callAPI do the request. func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { - if c.cfg.Debug { - dump, err := httputil.DumpRequestOut(request, true) - if err != nil { - return nil, err - } - log.Printf("\n%s\n", string(dump)) - } - - resp, err := c.cfg.HTTPClient.Do(request) - if err != nil { - if resp != nil && resp.Request == nil { - resp.Request = request - } - - return resp, err - } - - if c.cfg.Debug { - // for debugging, don't dump the body for streaming resp. as it would buffer the entire response - // only dump headers - isStreamingResponse := resp.Header.Get("Content-Type") == "application/x-ndjson" - dump, err := httputil.DumpResponse(resp, !isStreamingResponse) - if err != nil { - return resp, err - } - log.Printf("\n%s\n", string(dump)) - if isStreamingResponse { - log.Printf("Streaming response body - not dumped to preserve streaming\n") - } - } - - if resp.Request == nil { - resp.Request = request - } - - return resp, err + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request) + if err != nil { + if resp != nil && resp.Request == nil { + resp.Request = request + } + + return resp, err + } + + if c.cfg.Debug { + // for debugging, don't dump the body for streaming resp. as it would buffer the entire response + // only dump headers + isStreamingResponse := resp.Header.Get("Content-Type") == "application/x-ndjson" + dump, err := httputil.DumpResponse(resp, !isStreamingResponse) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + if isStreamingResponse { + log.Printf("Streaming response body - not dumped to preserve streaming\n") + } + } + + if resp.Request == nil { + resp.Request = request + } + + return resp, err } // Allow modification of underlying config for alternate implementations and testing // Caution: modifying the configuration while live can cause data races and potentially unwanted behavior func (c *APIClient) GetConfig() *Configuration { - return c.cfg + return c.cfg } // prepareRequest build the request func (c *APIClient) prepareRequest( - ctx context.Context, - path string, method string, - postBody interface{}, - headerParams map[string]string, - queryParams url.Values) (localVarRequest *http.Request, err error) { - - var body *bytes.Buffer - - // Detect postBody type and post. - if postBody != nil { - contentType := headerParams["Content-Type"] - if contentType == "" { - contentType = detectContentType(postBody) - headerParams["Content-Type"] = contentType - } - - body, err = setBody(postBody, contentType) - if err != nil { - return nil, err - } - } - - // Setup path and query parameters - uri, err := url.Parse(c.cfg.ApiUrl + path) - if err != nil { - return nil, err - } - - // Adding Query Param - query := uri.Query() - for k, v := range queryParams { - for _, iv := range v { - query.Add(k, iv) - } - } - - // Encode the parameters. - uri.RawQuery = query.Encode() - - // Generate a new request - if body != nil { - localVarRequest, err = http.NewRequest(method, uri.String(), body) - } else { - localVarRequest, err = http.NewRequest(method, uri.String(), nil) - } - if err != nil { - return nil, err - } - - // add header parameters, if any - if len(headerParams) > 0 { - headers := http.Header{} - for h, v := range headerParams { - headers.Set(h, v) - } - localVarRequest.Header = headers - } - - // Add the user agent to the request. - localVarRequest.Header.Set("User-Agent", c.cfg.UserAgent) - - for header, value := range c.cfg.DefaultHeaders { - if localVarRequest.Header.Get(header) == "" { - localVarRequest.Header.Set(header, value) - } - } - - if ctx != nil { - // add context to the request - localVarRequest = localVarRequest.WithContext(ctx) - } - - return localVarRequest, nil + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // Setup path and query parameters + uri, err := url.Parse(c.cfg.ApiUrl + path) + if err != nil { + return nil, err + } + + // Adding Query Param + query := uri.Query() + for k, v := range queryParams { + for _, iv := range v { + query.Add(k, iv) + } + } + + // Encode the parameters. + uri.RawQuery = query.Encode() + + // Generate a new request + if body != nil { + localVarRequest, err = http.NewRequest(method, uri.String(), body) + } else { + localVarRequest, err = http.NewRequest(method, uri.String(), nil) + } + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers.Set(h, v) + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Set("User-Agent", c.cfg.UserAgent) + + for header, value := range c.cfg.DefaultHeaders { + if localVarRequest.Header.Get(header) == "" { + localVarRequest.Header.Set(header, value) + } + } + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + } + + return localVarRequest, nil } func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { - if len(b) == 0 { - return nil - } - if s, ok := v.(*string); ok { - *s = string(b) - return nil - } - if xmlCheck.MatchString(contentType) { - if err = xml.Unmarshal(b, v); err != nil { - return err - } - return nil - } - if jsonCheck.MatchString(contentType) { - if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas - if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined - if err = unmarshalObj.UnmarshalJSON(b); err != nil { - return err - } - } else { - return errors.New("unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") - } - } else if err = json.Unmarshal(b, v); err != nil { // simple model - return err - } - return nil - } - return errors.New("undefined response type") + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if xmlCheck.MatchString(contentType) { + if err = xml.Unmarshal(b, v); err != nil { + return err + } + return nil + } + if jsonCheck.MatchString(contentType) { + if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas + if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined + if err = unmarshalObj.UnmarshalJSON(b); err != nil { + return err + } + } else { + return errors.New("unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") + } + } else if err = json.Unmarshal(b, v); err != nil { // simple model + return err + } + return nil + } + return errors.New("undefined response type") } func (c *APIClient) handleAPIError(httpResponse *http.Response, responseBody []byte, requestBody interface{}, operationName string, storeId string) error { - switch httpResponse.StatusCode { - case http.StatusBadRequest, http.StatusUnprocessableEntity: - err := NewFgaApiValidationError(operationName, requestBody, httpResponse, responseBody, storeId) - var v ValidationErrorMessageResponse - errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) - if errBody != nil { - err.modelDecodeError = err - return err - } - err.model = v - err.responseCode = v.GetCode() - err.error += " with error code " + string(v.GetCode()) + " error message: " + v.GetMessage() - return err - - case http.StatusUnauthorized, http.StatusForbidden: - return NewFgaApiAuthenticationError(operationName, requestBody, httpResponse, responseBody, storeId) - - case http.StatusNotFound: - err := NewFgaApiNotFoundError(operationName, requestBody, httpResponse, responseBody, storeId) - var v PathUnknownErrorMessageResponse - errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) - if errBody != nil { - err.modelDecodeError = err - return err - } - err.model = v - err.responseCode = v.GetCode() - err.error += " with error code " + string(v.GetCode()) + " error message: " + v.GetMessage() - return err - - case http.StatusTooManyRequests: - return NewFgaApiRateLimitExceededError(operationName, requestBody, httpResponse, responseBody, storeId) - - default: - if httpResponse.StatusCode >= http.StatusInternalServerError { - err := NewFgaApiInternalError(operationName, requestBody, httpResponse, responseBody, storeId) - var v InternalErrorMessageResponse - errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) - if errBody != nil { - err.modelDecodeError = err - return err - } - err.model = v - err.responseCode = v.GetCode() - err.error += " with error code " + string(v.GetCode()) + " error message: " + v.GetMessage() - return err - } - - err := NewFgaApiError(operationName, requestBody, httpResponse, responseBody, storeId) - var v ErrorResponse - errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) - if errBody != nil { - err.modelDecodeError = err - return err - } - err.model = v - err.responseCode = v.Code - err.error += " with error code " + string(v.Code) + " error message: " + v.Message - return err - } + switch httpResponse.StatusCode { + case http.StatusBadRequest, http.StatusUnprocessableEntity: + err := NewFgaApiValidationError(operationName, requestBody, httpResponse, responseBody, storeId) + var v ValidationErrorMessageResponse + errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) + if errBody != nil { + err.modelDecodeError = err + return err + } + err.model = v + err.responseCode = v.GetCode() + err.error += " with error code " + string(v.GetCode()) + " error message: " + v.GetMessage() + return err + + case http.StatusUnauthorized, http.StatusForbidden: + return NewFgaApiAuthenticationError(operationName, requestBody, httpResponse, responseBody, storeId) + + case http.StatusNotFound: + err := NewFgaApiNotFoundError(operationName, requestBody, httpResponse, responseBody, storeId) + var v PathUnknownErrorMessageResponse + errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) + if errBody != nil { + err.modelDecodeError = err + return err + } + err.model = v + err.responseCode = v.GetCode() + err.error += " with error code " + string(v.GetCode()) + " error message: " + v.GetMessage() + return err + + case http.StatusTooManyRequests: + return NewFgaApiRateLimitExceededError(operationName, requestBody, httpResponse, responseBody, storeId) + + default: + if httpResponse.StatusCode >= http.StatusInternalServerError { + err := NewFgaApiInternalError(operationName, requestBody, httpResponse, responseBody, storeId) + var v InternalErrorMessageResponse + errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) + if errBody != nil { + err.modelDecodeError = err + return err + } + err.model = v + err.responseCode = v.GetCode() + err.error += " with error code " + string(v.GetCode()) + " error message: " + v.GetMessage() + return err + } + + err := NewFgaApiError(operationName, requestBody, httpResponse, responseBody, storeId) + var v ErrorResponse + errBody := c.decode(&v, responseBody, httpResponse.Header.Get("Content-Type")) + if errBody != nil { + err.modelDecodeError = err + return err + } + err.model = v + err.responseCode = v.Code + err.error += " with error code " + string(v.Code) + " error message: " + v.Message + return err + } } // Prevent trying to import "fmt" func reportError(format string, a ...interface{}) error { - return fmt.Errorf(format, a...) + return fmt.Errorf(format, a...) } // Set request body from an interface{} func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { - if bodyBuf == nil { - bodyBuf = &bytes.Buffer{} - } - - if reader, ok := body.(io.Reader); ok { - _, err = bodyBuf.ReadFrom(reader) - } else if fp, ok := body.(**os.File); ok { - _, err = bodyBuf.ReadFrom(*fp) - } else if b, ok := body.([]byte); ok { - _, err = bodyBuf.Write(b) - } else if s, ok := body.(string); ok { - _, err = bodyBuf.WriteString(s) - } else if s, ok := body.(*string); ok { - _, err = bodyBuf.WriteString(*s) - } else if jsonCheck.MatchString(contentType) { - err = json.NewEncoder(bodyBuf).Encode(body) - } else if xmlCheck.MatchString(contentType) { - err = xml.NewEncoder(bodyBuf).Encode(body) - } - - if err != nil { - return nil, err - } - - if bodyBuf.Len() == 0 { - err = fmt.Errorf("invalid body type %s", contentType) - return nil, err - } - return bodyBuf, nil + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if fp, ok := body.(**os.File); ok { + _, err = bodyBuf.ReadFrom(*fp) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if jsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } else if xmlCheck.MatchString(contentType) { + err = xml.NewEncoder(bodyBuf).Encode(body) + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("invalid body type %s", contentType) + return nil, err + } + return bodyBuf, nil } // detectContentType method is used to figure out `Request.Body` content type for request header func detectContentType(body interface{}) string { - contentType := "text/plain; charset=utf-8" - kind := reflect.TypeOf(body).Kind() - - switch kind { - case reflect.Struct, reflect.Map, reflect.Ptr: - contentType = "application/json; charset=utf-8" - case reflect.String: - contentType = "text/plain; charset=utf-8" - default: - if b, ok := body.([]byte); ok { - contentType = http.DetectContentType(b) - } else if kind == reflect.Slice { - contentType = "application/json; charset=utf-8" - } - } - - return contentType + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + return contentType } // Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go type cacheControl map[string]string func parseCacheControl(headers http.Header) cacheControl { - cc := cacheControl{} - ccHeader := headers.Get("Cache-Control") - for _, part := range strings.Split(ccHeader, ",") { - part = strings.Trim(part, " ") - if part == "" { - continue - } - if strings.ContainsRune(part, '=') { - keyval := strings.Split(part, "=") - cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") - } else { - cc[part] = "" - } - } - return cc + cc := cacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") + } else { + cc[part] = "" + } + } + return cc } // CacheExpires helper function to determine remaining time before repeating a request. func CacheExpires(r *http.Response) time.Time { - // Figure out when the cache expires. - var expires time.Time - now, err := time.Parse(time.RFC1123, r.Header.Get("date")) - if err != nil { - return time.Now() - } - respCacheControl := parseCacheControl(r.Header) - - if maxAge, ok := respCacheControl["max-age"]; ok { - lifetime, err := time.ParseDuration(maxAge + "s") - if err != nil { - expires = now - } else { - expires = now.Add(lifetime) - } - } else { - expiresHeader := r.Header.Get("Expires") - if expiresHeader != "" { - expires, err = time.Parse(time.RFC1123, expiresHeader) - if err != nil { - expires = now - } - } - } - return expires + // Figure out when the cache expires. + var expires time.Time + now, err := time.Parse(time.RFC1123, r.Header.Get("date")) + if err != nil { + return time.Now() + } + respCacheControl := parseCacheControl(r.Header) + + if maxAge, ok := respCacheControl["max-age"]; ok { + lifetime, err := time.ParseDuration(maxAge + "s") + if err != nil { + expires = now + } else { + expires = now.Add(lifetime) + } + } else { + expiresHeader := r.Header.Get("Expires") + if expiresHeader != "" { + expires, err = time.Parse(time.RFC1123, expiresHeader) + if err != nil { + expires = now + } + } + } + return expires } func strlen(s string) int { - return utf8.RuneCountInString(s) + return utf8.RuneCountInString(s) } diff --git a/api_open_fga.go b/api_open_fga.go index d2f17f2..045765f 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -39,62 +39,62 @@ type RequestOptions struct { type OpenFgaApi interface { /* - * BatchCheck Send a list of `check` operations in a single request - * The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. - - An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\w\d-]{1,36}$` - - NOTE: The maximum number of checks that can be passed in the `BatchCheck` API is configurable via the [OPENFGA_MAX_CHECKS_PER_BATCH_CHECK](https://openfga.dev/docs/getting-started/setup-openfga/configuration#OPENFGA_MAX_CHECKS_PER_BATCH_CHECK) environment variable. If `BatchCheck` is called using the SDK, the SDK can split the batch check requests for you. - - For more details on how `Check` functions, see the docs for `/check`. - - ### Examples - #### A BatchCheckRequest - ```json - { - "checks": [ - { - "tuple_key": { - "object": "document:2021-budget" - "relation": "reader", - "user": "user:anne", - }, - "contextual_tuples": {...} - "context": {} - "correlation_id": "01JA8PM3QM7VBPGB8KMPK8SBD5" - }, - { - "tuple_key": { - "object": "document:2021-budget" - "relation": "reader", - "user": "user:bob", - }, - "contextual_tuples": {...} - "context": {} - "correlation_id": "01JA8PMM6A90NV5ET0F28CYSZQ" - } - ] - } - ``` - - Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: - ```json - { - "result": { - "01JA8PMM6A90NV5ET0F28CYSZQ": { - "allowed": false, - "error": {"message": ""} - }, - "01JA8PM3QM7VBPGB8KMPK8SBD5": { - "allowed": true, - "error": {"message": ""} - } - } - ``` + * BatchCheck Send a list of `check` operations in a single request + * The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. + + An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\w\d-]{1,36}$` + + NOTE: The maximum number of checks that can be passed in the `BatchCheck` API is configurable via the [OPENFGA_MAX_CHECKS_PER_BATCH_CHECK](https://openfga.dev/docs/getting-started/setup-openfga/configuration#OPENFGA_MAX_CHECKS_PER_BATCH_CHECK) environment variable. If `BatchCheck` is called using the SDK, the SDK can split the batch check requests for you. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiBatchCheckRequest + For more details on how `Check` functions, see the docs for `/check`. + + ### Examples + #### A BatchCheckRequest + ```json + { + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget" + "relation": "reader", + "user": "user:anne", + }, + "contextual_tuples": {...} + "context": {} + "correlation_id": "01JA8PM3QM7VBPGB8KMPK8SBD5" + }, + { + "tuple_key": { + "object": "document:2021-budget" + "relation": "reader", + "user": "user:bob", + }, + "contextual_tuples": {...} + "context": {} + "correlation_id": "01JA8PMM6A90NV5ET0F28CYSZQ" + } + ] + } + ``` + + Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: + ```json + { + "result": { + "01JA8PMM6A90NV5ET0F28CYSZQ": { + "allowed": false, + "error": {"message": ""} + }, + "01JA8PM3QM7VBPGB8KMPK8SBD5": { + "allowed": true, + "error": {"message": ""} + } + } + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiBatchCheckRequest */ BatchCheck(ctx context.Context, storeId string) ApiBatchCheckRequest @@ -105,129 +105,129 @@ type OpenFgaApi interface { BatchCheckExecute(r ApiBatchCheckRequest) (BatchCheckResponse, *http.Response, error) /* - * Check Check whether a user is authorized to access an object - * The Check API returns whether a given user has a relationship with a given object in a given store. - The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. - You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. - The response will return whether the relationship exists in the field `allowed`. - - Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. - For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. - ## Examples - ### Querying with contextual tuples - In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple - ```json - { - "user": "user:anne", - "relation": "member", - "object": "time_slot:office_hours" - } - ``` - the Check API can be used with the following request body: - ```json - { - "tuple_key": { - "user": "user:anne", - "relation": "reader", - "object": "document:2021-budget" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "member", - "object": "time_slot:office_hours" - } - ] - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - ### Querying usersets - Some Checks will always return `true`, even without any tuples. For example, for the following authorization model - ```python - model - schema 1.1 - type user - type document - relations - define reader: [user] - ``` - the following query - ```json - { - "tuple_key": { - "user": "document:2021-budget#reader", - "relation": "reader", - "object": "document:2021-budget" - } - } - ``` - will always return `{ "allowed": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. - ### Querying usersets with difference in the model - A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model - ```python - model - schema 1.1 - type user - type group - relations - define member: [user] - type document - relations - define blocked: [user] - define reader: [group#member] but not blocked - ``` - the following query - ```json - { - "tuple_key": { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "member", - "object": "group:finance" - }, - { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - { - "user": "user:anne", - "relation": "blocked", - "object": "document:2021-budget" - } - ] - }, - } - ``` - will return `{ "allowed": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. - ### Requesting higher consistency - By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. - ```json - { - "tuple_key": { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - "consistency": "HIGHER_CONSISTENCY" - } - ``` + * Check Check whether a user is authorized to access an object + * The Check API returns whether a given user has a relationship with a given object in a given store. + The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. + You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. + The response will return whether the relationship exists in the field `allowed`. + + Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. + For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. + ## Examples + ### Querying with contextual tuples + In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple + ```json + { + "user": "user:anne", + "relation": "member", + "object": "time_slot:office_hours" + } + ``` + the Check API can be used with the following request body: + ```json + { + "tuple_key": { + "user": "user:anne", + "relation": "reader", + "object": "document:2021-budget" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "member", + "object": "time_slot:office_hours" + } + ] + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + ### Querying usersets + Some Checks will always return `true`, even without any tuples. For example, for the following authorization model + ```python + model + schema 1.1 + type user + type document + relations + define reader: [user] + ``` + the following query + ```json + { + "tuple_key": { + "user": "document:2021-budget#reader", + "relation": "reader", + "object": "document:2021-budget" + } + } + ``` + will always return `{ "allowed": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. + ### Querying usersets with difference in the model + A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model + ```python + model + schema 1.1 + type user + type group + relations + define member: [user] + type document + relations + define blocked: [user] + define reader: [group#member] but not blocked + ``` + the following query + ```json + { + "tuple_key": { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "member", + "object": "group:finance" + }, + { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + { + "user": "user:anne", + "relation": "blocked", + "object": "document:2021-budget" + } + ] + }, + } + ``` + will return `{ "allowed": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. + ### Requesting higher consistency + By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. + ```json + { + "tuple_key": { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + "consistency": "HIGHER_CONSISTENCY" + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiCheckRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiCheckRequest */ Check(ctx context.Context, storeId string) ApiCheckRequest @@ -266,169 +266,169 @@ type OpenFgaApi interface { DeleteStoreExecute(r ApiDeleteStoreRequest) (*http.Response, error) /* - * Expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship - * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. - This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. - Body parameters `tuple_key.object` and `tuple_key.relation` are all required. - A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. - The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. - - ## Example - To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body - ```json - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader" - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - OpenFGA's response will be a userset tree of the users and usersets that have read access to the document. - ```json - { - "tree":{ - "root":{ - "type":"document:2021-budget#reader", - "union":{ - "nodes":[ - { - "type":"document:2021-budget#reader", - "leaf":{ - "users":{ - "users":[ - "user:bob" - ] - } - } - }, - { - "type":"document:2021-budget#reader", - "leaf":{ - "computed":{ - "userset":"document:2021-budget#writer" - } - } - } - ] - } - } - } - } - ``` - The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. - ### Expand Request with Contextual Tuples - - Given the model - ```python - model - schema 1.1 - - type user - - type folder - relations - define owner: [user] - - type document - relations - define parent: [folder] - define viewer: [user] or writer - define writer: [user] or owner from parent - ``` - and the initial tuples - ```json - [{ - "user": "user:bob", - "relation": "owner", - "object": "folder:1" - }] - ``` - - To expand all `writers` of `document:1` when `document:1` is put in `folder:1`, the first call could be - - ```json - { - "tuple_key": { - "object": "document:1", - "relation": "writer" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "folder:1", - "relation": "parent", - "object": "document:1" - } - ] - } - } - ``` - this returns: - ```json - { - "tree": { - "root": { - "name": "document:1#writer", - "union": { - "nodes": [ - { - "name": "document:1#writer", - "leaf": { - "users": { - "users": [] - } - } - }, - { - "name": "document:1#writer", - "leaf": { - "tupleToUserset": { - "tupleset": "document:1#parent", - "computed": [ - { - "userset": "folder:1#owner" - } - ] - } - } - } - ] - } - } - } - } - ``` - This tells us that the `owner` of `folder:1` may also be a writer. So our next call could be to find the `owners` of `folder:1` - ```json - { - "tuple_key": { - "object": "folder:1", - "relation": "owner" - } - } - ``` - which gives - ```json - { - "tree": { - "root": { - "name": "folder:1#owner", - "leaf": { - "users": { - "users": [ - "user:bob" - ] - } - } - } - } - } - ``` + * Expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship + * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. + This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. + Body parameters `tuple_key.object` and `tuple_key.relation` are all required. + A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. + The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. + + ## Example + To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body + ```json + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader" + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + OpenFGA's response will be a userset tree of the users and usersets that have read access to the document. + ```json + { + "tree":{ + "root":{ + "type":"document:2021-budget#reader", + "union":{ + "nodes":[ + { + "type":"document:2021-budget#reader", + "leaf":{ + "users":{ + "users":[ + "user:bob" + ] + } + } + }, + { + "type":"document:2021-budget#reader", + "leaf":{ + "computed":{ + "userset":"document:2021-budget#writer" + } + } + } + ] + } + } + } + } + ``` + The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. + ### Expand Request with Contextual Tuples + + Given the model + ```python + model + schema 1.1 + + type user + + type folder + relations + define owner: [user] + + type document + relations + define parent: [folder] + define viewer: [user] or writer + define writer: [user] or owner from parent + ``` + and the initial tuples + ```json + [{ + "user": "user:bob", + "relation": "owner", + "object": "folder:1" + }] + ``` + + To expand all `writers` of `document:1` when `document:1` is put in `folder:1`, the first call could be + + ```json + { + "tuple_key": { + "object": "document:1", + "relation": "writer" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "folder:1", + "relation": "parent", + "object": "document:1" + } + ] + } + } + ``` + this returns: + ```json + { + "tree": { + "root": { + "name": "document:1#writer", + "union": { + "nodes": [ + { + "name": "document:1#writer", + "leaf": { + "users": { + "users": [] + } + } + }, + { + "name": "document:1#writer", + "leaf": { + "tupleToUserset": { + "tupleset": "document:1#parent", + "computed": [ + { + "userset": "folder:1#owner" + } + ] + } + } + } + ] + } + } + } + } + ``` + This tells us that the `owner` of `folder:1` may also be a writer. So our next call could be to find the `owners` of `folder:1` + ```json + { + "tuple_key": { + "object": "folder:1", + "relation": "owner" + } + } + ``` + which gives + ```json + { + "tree": { + "root": { + "name": "folder:1#owner", + "leaf": { + "users": { + "users": [ + "user:bob" + ] + } + } + } + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiExpandRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiExpandRequest */ Expand(ctx context.Context, storeId string) ApiExpandRequest @@ -454,19 +454,19 @@ type OpenFgaApi interface { GetStoreExecute(r ApiGetStoreRequest) (GetStoreResponse, *http.Response, error) /* - * ListObjects List all objects of the given type that the user has a relation with - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. - You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. - The response will contain the related objects in an array in the "objects" field of the response and they will be strings in the object format `:` (e.g. "document:roadmap"). - The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. - The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiListObjectsRequest + * ListObjects List all objects of the given type that the user has a relation with + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. + You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. + The response will contain the related objects in an array in the "objects" field of the response and they will be strings in the object format `:` (e.g. "document:roadmap"). + The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. + The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiListObjectsRequest */ ListObjects(ctx context.Context, storeId string) ApiListObjectsRequest @@ -477,12 +477,12 @@ type OpenFgaApi interface { ListObjectsExecute(r ApiListObjectsRequest) (ListObjectsResponse, *http.Response, error) /* - * ListStores 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. + * ListStores 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. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return ApiListStoresRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return ApiListStoresRequest */ ListStores(ctx context.Context) ApiListStoresRequest @@ -493,20 +493,20 @@ type OpenFgaApi interface { ListStoresExecute(r ApiListStoresRequest) (ListStoresResponse, *http.Response, error) /* - * ListUsers List the users matching the provided filter who have a certain relation to a particular type. - * The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. - You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - The response will contain the related users in an array in the "users" field of the response. These results may include specific objects, usersets - or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects - of that type have a relation to the object; it is possible that negations exist and checks should still be queried - on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. - The returned users will not be sorted, and therefore two identical calls may yield different sets of users. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiListUsersRequest + * ListUsers List the users matching the provided filter who have a certain relation to a particular type. + * The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. + You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + The response will contain the related users in an array in the "users" field of the response. These results may include specific objects, usersets + or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects + of that type have a relation to the object; it is possible that negations exist and checks should still be queried + on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. + The returned users will not be sorted, and therefore two identical calls may yield different sets of users. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiListUsersRequest */ ListUsers(ctx context.Context, storeId string) ApiListUsersRequest @@ -517,109 +517,109 @@ type OpenFgaApi interface { ListUsersExecute(r ApiListUsersRequest) (ListUsersResponse, *http.Response, error) /* - * Read Get tuples from the store that matches a query, without following userset rewrite rules - * The Read API will return the tuples for a certain store that match a query filter specified in the body of the request. - The API doesn't guarantee order by any field. - It is different from the `/stores/{store_id}/expand` API in that it only returns relationship tuples that are stored in the system and satisfy the query. - In the body: - 1. `tuple_key` is optional. If not specified, it will return all tuples in the store. - 2. `tuple_key.object` is mandatory if `tuple_key` is specified. It can be a full object (e.g., `type:object_id`) or type only (e.g., `type:`). - 3. `tuple_key.user` is mandatory if tuple_key is specified in the case the `tuple_key.object` is a type only. If tuple_key.user is specified, it needs to be a full object (e.g., `type:user_id`). - ## Examples - ### Query for all objects in a type definition - To query for all objects that `user:bob` has `reader` relationship in the `document` type definition, call read API with body of - ```json - { - "tuple_key": { - "user": "user:bob", - "relation": "reader", - "object": "document:" - } - } - ``` - The API will return tuples and a continuation token, something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `user:bob` has a `reader` relationship with 1 document `document:2021-budget`. Note that this API, unlike the List Objects API, does not evaluate the tuples in the store. - The continuation token will be empty if there are no more tuples to query. - ### Query for all stored relationship tuples that have a particular relation and object - To query for all users that have `reader` relationship with `document:2021-budget`, call read API with body of - ```json - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader" - } - } - ``` - The API will return something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `document:2021-budget` has 1 `reader` (`user:bob`). Note that, even if the model said that all `writers` are also `readers`, the API will not return writers such as `user:anne` because it only returns tuples and does not evaluate them. - ### Query for all users with all relationships for a particular document - To query for all users that have any relationship with `document:2021-budget`, call read API with body of - ```json - { - "tuple_key": { - "object": "document:2021-budget" - } - } - ``` - The API will return something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:anne", - "relation": "writer", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-05T13:42:12.356Z" - }, - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `document:2021-budget` has 1 `reader` (`user:bob`) and 1 `writer` (`user:anne`). + * Read Get tuples from the store that matches a query, without following userset rewrite rules + * The Read API will return the tuples for a certain store that match a query filter specified in the body of the request. + The API doesn't guarantee order by any field. + It is different from the `/stores/{store_id}/expand` API in that it only returns relationship tuples that are stored in the system and satisfy the query. + In the body: + 1. `tuple_key` is optional. If not specified, it will return all tuples in the store. + 2. `tuple_key.object` is mandatory if `tuple_key` is specified. It can be a full object (e.g., `type:object_id`) or type only (e.g., `type:`). + 3. `tuple_key.user` is mandatory if tuple_key is specified in the case the `tuple_key.object` is a type only. If tuple_key.user is specified, it needs to be a full object (e.g., `type:user_id`). + ## Examples + ### Query for all objects in a type definition + To query for all objects that `user:bob` has `reader` relationship in the `document` type definition, call read API with body of + ```json + { + "tuple_key": { + "user": "user:bob", + "relation": "reader", + "object": "document:" + } + } + ``` + The API will return tuples and a continuation token, something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `user:bob` has a `reader` relationship with 1 document `document:2021-budget`. Note that this API, unlike the List Objects API, does not evaluate the tuples in the store. + The continuation token will be empty if there are no more tuples to query. + ### Query for all stored relationship tuples that have a particular relation and object + To query for all users that have `reader` relationship with `document:2021-budget`, call read API with body of + ```json + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader" + } + } + ``` + The API will return something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `document:2021-budget` has 1 `reader` (`user:bob`). Note that, even if the model said that all `writers` are also `readers`, the API will not return writers such as `user:anne` because it only returns tuples and does not evaluate them. + ### Query for all users with all relationships for a particular document + To query for all users that have any relationship with `document:2021-budget`, call read API with body of + ```json + { + "tuple_key": { + "object": "document:2021-budget" + } + } + ``` + The API will return something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:anne", + "relation": "writer", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-05T13:42:12.356Z" + }, + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `document:2021-budget` has 1 `reader` (`user:bob`) and 1 `writer` (`user:anne`). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadRequest */ Read(ctx context.Context, storeId string) ApiReadRequest @@ -646,52 +646,52 @@ type OpenFgaApi interface { ReadAssertionsExecute(r ApiReadAssertionsRequest) (ReadAssertionsResponse, *http.Response, error) /* - * ReadAuthorizationModel Return a particular version of an authorization model - * The ReadAuthorizationModel API returns an authorization model by its identifier. - The response will return the authorization model for the particular version. - - ## Example - To retrieve the authorization model with ID `01G5JAVJ41T49E9TT3SKVS7X1J` for the store, call the GET authorization-models by ID API with `01G5JAVJ41T49E9TT3SKVS7X1J` as the `id` path parameter. The API will return: - ```json - { - "authorization_model":{ - "id":"01G5JAVJ41T49E9TT3SKVS7X1J", - "type_definitions":[ - { - "type":"user" - }, - { - "type":"document", - "relations":{ - "reader":{ - "union":{ - "child":[ - { - "this":{} - }, - { - "computedUserset":{ - "object":"", - "relation":"writer" - } - } - ] - } - }, - "writer":{ - "this":{} - } - } - } - ] - } - } - ``` - In the above example, there are 2 types (`user` and `document`). The `document` type has 2 relations (`writer` and `reader`). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @param id - * @return ApiReadAuthorizationModelRequest + * ReadAuthorizationModel Return a particular version of an authorization model + * The ReadAuthorizationModel API returns an authorization model by its identifier. + The response will return the authorization model for the particular version. + + ## Example + To retrieve the authorization model with ID `01G5JAVJ41T49E9TT3SKVS7X1J` for the store, call the GET authorization-models by ID API with `01G5JAVJ41T49E9TT3SKVS7X1J` as the `id` path parameter. The API will return: + ```json + { + "authorization_model":{ + "id":"01G5JAVJ41T49E9TT3SKVS7X1J", + "type_definitions":[ + { + "type":"user" + }, + { + "type":"document", + "relations":{ + "reader":{ + "union":{ + "child":[ + { + "this":{} + }, + { + "computedUserset":{ + "object":"", + "relation":"writer" + } + } + ] + } + }, + "writer":{ + "this":{} + } + } + } + ] + } + } + ``` + In the above example, there are 2 types (`user` and `document`). The `document` type has 2 relations (`writer` and `reader`). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @param id + * @return ApiReadAuthorizationModelRequest */ ReadAuthorizationModel(ctx context.Context, storeId string, id string) ApiReadAuthorizationModelRequest @@ -702,47 +702,47 @@ type OpenFgaApi interface { ReadAuthorizationModelExecute(r ApiReadAuthorizationModelRequest) (ReadAuthorizationModelResponse, *http.Response, error) /* - * ReadAuthorizationModels Return all the authorization models for a particular store - * The ReadAuthorizationModels API will return all the authorization models for a certain store. - OpenFGA's response will contain an array of all authorization models, sorted in descending order of creation. - - ## Example - Assume that a store's authorization model has been configured twice. To get all the authorization models that have been created in this store, call GET authorization-models. The API will return a response that looks like: - ```json - { - "authorization_models": [ - { - "id": "01G50QVV17PECNVAHX1GG4Y5NC", - "type_definitions": [...] - }, - { - "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", - "type_definitions": [...] - }, - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - If there are no more authorization models available, the `continuation_token` field will be empty - ```json - { - "authorization_models": [ - { - "id": "01G50QVV17PECNVAHX1GG4Y5NC", - "type_definitions": [...] - }, - { - "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", - "type_definitions": [...] - }, - ], - "continuation_token": "" - } - ``` + * ReadAuthorizationModels Return all the authorization models for a particular store + * The ReadAuthorizationModels API will return all the authorization models for a certain store. + OpenFGA's response will contain an array of all authorization models, sorted in descending order of creation. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadAuthorizationModelsRequest + ## Example + Assume that a store's authorization model has been configured twice. To get all the authorization models that have been created in this store, call GET authorization-models. The API will return a response that looks like: + ```json + { + "authorization_models": [ + { + "id": "01G50QVV17PECNVAHX1GG4Y5NC", + "type_definitions": [...] + }, + { + "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", + "type_definitions": [...] + }, + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + If there are no more authorization models available, the `continuation_token` field will be empty + ```json + { + "authorization_models": [ + { + "id": "01G50QVV17PECNVAHX1GG4Y5NC", + "type_definitions": [...] + }, + { + "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", + "type_definitions": [...] + }, + ], + "continuation_token": "" + } + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadAuthorizationModelsRequest */ ReadAuthorizationModels(ctx context.Context, storeId string) ApiReadAuthorizationModelsRequest @@ -753,15 +753,15 @@ type OpenFgaApi interface { ReadAuthorizationModelsExecute(r ApiReadAuthorizationModelsRequest) (ReadAuthorizationModelsResponse, *http.Response, error) /* - * ReadChanges Return a list of all the tuple changes - * The ReadChanges API will return a paginated list of tuple changes (additions and deletions) that occurred in a given store, sorted by ascending time. The response will include a continuation token that is used to get the next set of changes. If there are no changes after the provided continuation token, the same token will be returned in order for it to be used when new changes are recorded. If the store never had any tuples added or removed, this token will be empty. - You can use the `type` parameter to only get the list of tuple changes that affect objects of that type. - When reading a write tuple change, if it was conditioned, the condition will be returned. - When reading a delete tuple change, the condition will NOT be returned regardless of whether it was originally conditioned or not. - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadChangesRequest + * ReadChanges Return a list of all the tuple changes + * The ReadChanges API will return a paginated list of tuple changes (additions and deletions) that occurred in a given store, sorted by ascending time. The response will include a continuation token that is used to get the next set of changes. If there are no changes after the provided continuation token, the same token will be returned in order for it to be used when new changes are recorded. If the store never had any tuples added or removed, this token will be empty. + You can use the `type` parameter to only get the list of tuple changes that affect objects of that type. + When reading a write tuple change, if it was conditioned, the condition will be returned. + When reading a delete tuple change, the condition will NOT be returned regardless of whether it was originally conditioned or not. + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadChangesRequest */ ReadChanges(ctx context.Context, storeId string) ApiReadChangesRequest @@ -772,14 +772,14 @@ type OpenFgaApi interface { ReadChangesExecute(r ApiReadChangesRequest) (ReadChangesResponse, *http.Response, error) /* - * StreamedListObjects Stream all objects of the given type that the user has a relation with - * The Streamed ListObjects API is very similar to the the ListObjects API, with two differences: - 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected. - 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE. - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiStreamedListObjectsRequest + * StreamedListObjects Stream all objects of the given type that the user has a relation with + * The Streamed ListObjects API is very similar to the the ListObjects API, with two differences: + 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected. + 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE. + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiStreamedListObjectsRequest */ StreamedListObjects(ctx context.Context, storeId string) ApiStreamedListObjectsRequest @@ -790,53 +790,53 @@ type OpenFgaApi interface { StreamedListObjectsExecute(r ApiStreamedListObjectsRequest) (StreamResultOfStreamedListObjectsResponse, *http.Response, error) /* - * Write Add or delete tuples from the store - * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. - In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. - The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. - To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. - To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. - If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. - The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. - An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. - ## Example - ### Adding relationships - To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following - ```json - { - "writes": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "writer", - "object": "document:2021-budget" - } - ], - "on_duplicate": "ignore" - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - ### Removing relationships - To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following - ```json - { - "deletes": { - "tuple_keys": [ - { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - } - ], - "on_missing": "ignore" - } - } - ``` + * Write Add or delete tuples from the store + * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. + In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. + The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. + To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. + If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. + The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. + An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. + ## Example + ### Adding relationships + To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following + ```json + { + "writes": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "writer", + "object": "document:2021-budget" + } + ], + "on_duplicate": "ignore" + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + ### Removing relationships + To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following + ```json + { + "deletes": { + "tuple_keys": [ + { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + } + ], + "on_missing": "ignore" + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiWriteRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiWriteRequest */ Write(ctx context.Context, storeId string) ApiWriteRequest @@ -862,53 +862,53 @@ type OpenFgaApi interface { WriteAssertionsExecute(r ApiWriteAssertionsRequest) (*http.Response, error) /* - * WriteAuthorizationModel Create a new authorization model - * The WriteAuthorizationModel API will add a new authorization model to a store. - Each item in the `type_definitions` array is a type definition as specified in the field `type_definition`. - The response will return the authorization model's ID in the `id` field. - - ## Example - To add an authorization model with `user` and `document` type definitions, call POST authorization-models API with the body: - ```json - { - "type_definitions":[ - { - "type":"user" - }, - { - "type":"document", - "relations":{ - "reader":{ - "union":{ - "child":[ - { - "this":{} - }, - { - "computedUserset":{ - "object":"", - "relation":"writer" - } - } - ] - } - }, - "writer":{ - "this":{} - } - } - } - ] - } - ``` - OpenFGA's response will include the version id for this authorization model, which will look like - ``` - {"authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC"} - ``` - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiWriteAuthorizationModelRequest + * WriteAuthorizationModel Create a new authorization model + * The WriteAuthorizationModel API will add a new authorization model to a store. + Each item in the `type_definitions` array is a type definition as specified in the field `type_definition`. + The response will return the authorization model's ID in the `id` field. + + ## Example + To add an authorization model with `user` and `document` type definitions, call POST authorization-models API with the body: + ```json + { + "type_definitions":[ + { + "type":"user" + }, + { + "type":"document", + "relations":{ + "reader":{ + "union":{ + "child":[ + { + "this":{} + }, + { + "computedUserset":{ + "object":"", + "relation":"writer" + } + } + ] + } + }, + "writer":{ + "this":{} + } + } + } + ] + } + ``` + OpenFGA's response will include the version id for this authorization model, which will look like + ``` + {"authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC"} + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiWriteAuthorizationModelRequest */ WriteAuthorizationModel(ctx context.Context, storeId string) ApiWriteAuthorizationModelRequest diff --git a/client/streaming_test.go b/client/streaming_test.go index 272037f..d687742 100644 --- a/client/streaming_test.go +++ b/client/streaming_test.go @@ -40,7 +40,7 @@ func TestClientStreamedListObjects_Success(t *testing.T) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -98,7 +98,7 @@ func TestClientStreamedListObjects_WithOptions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -146,7 +146,7 @@ func TestClientStreamedListObjects_WithOptions(t *testing.T) { func TestClientStreamedListObjects_ErrorHandling(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(`{"code":"internal_error","message":"Internal server error"}`)) + _, _ = w.Write([]byte(`{"code":"internal_error","message":"Internal server error"}`)) })) defer server.Close() @@ -213,7 +213,7 @@ func TestClientStreamedListObjects_CustomBufferSize(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -280,7 +280,7 @@ func TestClientStreamedListObjects_DefaultBufferSize(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() diff --git a/streaming.go b/streaming.go index acc5b30..829f010 100644 --- a/streaming.go +++ b/streaming.go @@ -58,7 +58,12 @@ func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http. defer close(channel.Objects) defer close(channel.Errors) defer cancel() - defer httpResponse.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + channel.Errors <- err + } + }(httpResponse.Body) scanner := bufio.NewScanner(httpResponse.Body) // Allow large NDJSON entries (up to 10MB). Tune as needed. diff --git a/streaming_test.go b/streaming_test.go index 66ad213..3282494 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -57,7 +57,7 @@ func TestStreamedListObjectsWithChannel_Success(t *testing.T) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -115,7 +115,7 @@ func TestStreamedListObjectsWithChannel_EmptyLines(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -160,7 +160,7 @@ func TestStreamedListObjectsWithChannel_ErrorInStream(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -214,7 +214,7 @@ invalid json` server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -262,7 +262,7 @@ func TestStreamedListObjectsWithChannel_ContextCancellation(t *testing.T) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) for i := 0; i < 100; i++ { - w.Write([]byte(`{"result":{"object":"document:` + strconv.Itoa(i) + `"}}` + "\n")) + _, _ = w.Write([]byte(`{"result":{"object":"document:` + strconv.Itoa(i) + `"}}` + "\n")) if f, ok := w.(http.Flusher); ok { f.Flush() } @@ -326,7 +326,7 @@ func TestStreamedListObjectsWithChannel_CustomBufferSize(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -386,7 +386,7 @@ func TestStreamedListObjectsWithChannel_DefaultBufferSize(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - w.Write([]byte(responseBody)) + _, _ = w.Write([]byte(responseBody)) })) defer server.Close() @@ -437,7 +437,7 @@ func TestStreamedListObjectsWithChannel_ProperNumericStrings(t *testing.T) { w.WriteHeader(http.StatusOK) // Generate 15 objects to ensure we test values >= 10 for i := 0; i < 15; i++ { - w.Write([]byte(`{"result":{"object":"document:` + strconv.Itoa(i) + `"}}` + "\n")) + _, _ = w.Write([]byte(`{"result":{"object":"document:` + strconv.Itoa(i) + `"}}` + "\n")) } })) defer server.Close() From 7bbbb6b8789d1006654a081ddc9b66ddcaed4e10 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Wed, 19 Nov 2025 10:47:15 -0500 Subject: [PATCH 2/5] chore: run fmt and lint on api_client.go --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b0396cf..89e5c93 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: ## Run all tests go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... fmt: ## Run code formatting - go fmt ./... + gofmt -w . vet: ## Run static code analysis go vet ./... From 5c7378949a6c5a7ee1b9d3ab19bceb548c4b6abe Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 20 Nov 2025 14:56:02 +0530 Subject: [PATCH 3/5] feat: add common streaming utils that can be reused --- .openapi-generator/FILES | 2 + api_open_fga.go | 1338 +++++++++++++------------- example/example1/go.mod | 2 +- example/opentelemetry/go.mod | 2 +- example/streamed_list_objects/go.mod | 2 +- streaming.go | 118 ++- streaming_test.go | 173 ++-- 7 files changed, 831 insertions(+), 806 deletions(-) diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index a3c05a2..7a2ded6 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -189,3 +189,5 @@ model_write_authorization_model_response.go model_write_request.go model_write_request_deletes.go model_write_request_writes.go +streaming.go +streaming_test.go diff --git a/api_open_fga.go b/api_open_fga.go index 045765f..d2f17f2 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -39,62 +39,62 @@ type RequestOptions struct { type OpenFgaApi interface { /* - * BatchCheck Send a list of `check` operations in a single request - * The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. - - An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\w\d-]{1,36}$` - - NOTE: The maximum number of checks that can be passed in the `BatchCheck` API is configurable via the [OPENFGA_MAX_CHECKS_PER_BATCH_CHECK](https://openfga.dev/docs/getting-started/setup-openfga/configuration#OPENFGA_MAX_CHECKS_PER_BATCH_CHECK) environment variable. If `BatchCheck` is called using the SDK, the SDK can split the batch check requests for you. - - For more details on how `Check` functions, see the docs for `/check`. - - ### Examples - #### A BatchCheckRequest - ```json - { - "checks": [ - { - "tuple_key": { - "object": "document:2021-budget" - "relation": "reader", - "user": "user:anne", - }, - "contextual_tuples": {...} - "context": {} - "correlation_id": "01JA8PM3QM7VBPGB8KMPK8SBD5" - }, - { - "tuple_key": { - "object": "document:2021-budget" - "relation": "reader", - "user": "user:bob", - }, - "contextual_tuples": {...} - "context": {} - "correlation_id": "01JA8PMM6A90NV5ET0F28CYSZQ" - } - ] - } - ``` - - Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: - ```json - { - "result": { - "01JA8PMM6A90NV5ET0F28CYSZQ": { - "allowed": false, - "error": {"message": ""} - }, - "01JA8PM3QM7VBPGB8KMPK8SBD5": { - "allowed": true, - "error": {"message": ""} - } - } - ``` + * BatchCheck Send a list of `check` operations in a single request + * The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. + + An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\w\d-]{1,36}$` + + NOTE: The maximum number of checks that can be passed in the `BatchCheck` API is configurable via the [OPENFGA_MAX_CHECKS_PER_BATCH_CHECK](https://openfga.dev/docs/getting-started/setup-openfga/configuration#OPENFGA_MAX_CHECKS_PER_BATCH_CHECK) environment variable. If `BatchCheck` is called using the SDK, the SDK can split the batch check requests for you. + + For more details on how `Check` functions, see the docs for `/check`. + + ### Examples + #### A BatchCheckRequest + ```json + { + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget" + "relation": "reader", + "user": "user:anne", + }, + "contextual_tuples": {...} + "context": {} + "correlation_id": "01JA8PM3QM7VBPGB8KMPK8SBD5" + }, + { + "tuple_key": { + "object": "document:2021-budget" + "relation": "reader", + "user": "user:bob", + }, + "contextual_tuples": {...} + "context": {} + "correlation_id": "01JA8PMM6A90NV5ET0F28CYSZQ" + } + ] + } + ``` + + Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: + ```json + { + "result": { + "01JA8PMM6A90NV5ET0F28CYSZQ": { + "allowed": false, + "error": {"message": ""} + }, + "01JA8PM3QM7VBPGB8KMPK8SBD5": { + "allowed": true, + "error": {"message": ""} + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiBatchCheckRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiBatchCheckRequest */ BatchCheck(ctx context.Context, storeId string) ApiBatchCheckRequest @@ -105,129 +105,129 @@ type OpenFgaApi interface { BatchCheckExecute(r ApiBatchCheckRequest) (BatchCheckResponse, *http.Response, error) /* - * Check Check whether a user is authorized to access an object - * The Check API returns whether a given user has a relationship with a given object in a given store. - The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. - You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. - The response will return whether the relationship exists in the field `allowed`. - - Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. - For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. - ## Examples - ### Querying with contextual tuples - In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple - ```json - { - "user": "user:anne", - "relation": "member", - "object": "time_slot:office_hours" - } - ``` - the Check API can be used with the following request body: - ```json - { - "tuple_key": { - "user": "user:anne", - "relation": "reader", - "object": "document:2021-budget" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "member", - "object": "time_slot:office_hours" - } - ] - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - ### Querying usersets - Some Checks will always return `true`, even without any tuples. For example, for the following authorization model - ```python - model - schema 1.1 - type user - type document - relations - define reader: [user] - ``` - the following query - ```json - { - "tuple_key": { - "user": "document:2021-budget#reader", - "relation": "reader", - "object": "document:2021-budget" - } - } - ``` - will always return `{ "allowed": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. - ### Querying usersets with difference in the model - A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model - ```python - model - schema 1.1 - type user - type group - relations - define member: [user] - type document - relations - define blocked: [user] - define reader: [group#member] but not blocked - ``` - the following query - ```json - { - "tuple_key": { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "member", - "object": "group:finance" - }, - { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - { - "user": "user:anne", - "relation": "blocked", - "object": "document:2021-budget" - } - ] - }, - } - ``` - will return `{ "allowed": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. - ### Requesting higher consistency - By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. - ```json - { - "tuple_key": { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - "consistency": "HIGHER_CONSISTENCY" - } - ``` + * Check Check whether a user is authorized to access an object + * The Check API returns whether a given user has a relationship with a given object in a given store. + The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. + You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. + The response will return whether the relationship exists in the field `allowed`. + + Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. + For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. + ## Examples + ### Querying with contextual tuples + In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple + ```json + { + "user": "user:anne", + "relation": "member", + "object": "time_slot:office_hours" + } + ``` + the Check API can be used with the following request body: + ```json + { + "tuple_key": { + "user": "user:anne", + "relation": "reader", + "object": "document:2021-budget" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "member", + "object": "time_slot:office_hours" + } + ] + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + ### Querying usersets + Some Checks will always return `true`, even without any tuples. For example, for the following authorization model + ```python + model + schema 1.1 + type user + type document + relations + define reader: [user] + ``` + the following query + ```json + { + "tuple_key": { + "user": "document:2021-budget#reader", + "relation": "reader", + "object": "document:2021-budget" + } + } + ``` + will always return `{ "allowed": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. + ### Querying usersets with difference in the model + A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model + ```python + model + schema 1.1 + type user + type group + relations + define member: [user] + type document + relations + define blocked: [user] + define reader: [group#member] but not blocked + ``` + the following query + ```json + { + "tuple_key": { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "member", + "object": "group:finance" + }, + { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + { + "user": "user:anne", + "relation": "blocked", + "object": "document:2021-budget" + } + ] + }, + } + ``` + will return `{ "allowed": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. + ### Requesting higher consistency + By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. + ```json + { + "tuple_key": { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + "consistency": "HIGHER_CONSISTENCY" + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiCheckRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiCheckRequest */ Check(ctx context.Context, storeId string) ApiCheckRequest @@ -266,169 +266,169 @@ type OpenFgaApi interface { DeleteStoreExecute(r ApiDeleteStoreRequest) (*http.Response, error) /* - * Expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship - * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. - This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. - Body parameters `tuple_key.object` and `tuple_key.relation` are all required. - A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. - The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. - - ## Example - To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body - ```json - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader" - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - OpenFGA's response will be a userset tree of the users and usersets that have read access to the document. - ```json - { - "tree":{ - "root":{ - "type":"document:2021-budget#reader", - "union":{ - "nodes":[ - { - "type":"document:2021-budget#reader", - "leaf":{ - "users":{ - "users":[ - "user:bob" - ] - } - } - }, - { - "type":"document:2021-budget#reader", - "leaf":{ - "computed":{ - "userset":"document:2021-budget#writer" - } - } - } - ] - } - } - } - } - ``` - The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. - ### Expand Request with Contextual Tuples - - Given the model - ```python - model - schema 1.1 - - type user - - type folder - relations - define owner: [user] - - type document - relations - define parent: [folder] - define viewer: [user] or writer - define writer: [user] or owner from parent - ``` - and the initial tuples - ```json - [{ - "user": "user:bob", - "relation": "owner", - "object": "folder:1" - }] - ``` - - To expand all `writers` of `document:1` when `document:1` is put in `folder:1`, the first call could be - - ```json - { - "tuple_key": { - "object": "document:1", - "relation": "writer" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "folder:1", - "relation": "parent", - "object": "document:1" - } - ] - } - } - ``` - this returns: - ```json - { - "tree": { - "root": { - "name": "document:1#writer", - "union": { - "nodes": [ - { - "name": "document:1#writer", - "leaf": { - "users": { - "users": [] - } - } - }, - { - "name": "document:1#writer", - "leaf": { - "tupleToUserset": { - "tupleset": "document:1#parent", - "computed": [ - { - "userset": "folder:1#owner" - } - ] - } - } - } - ] - } - } - } - } - ``` - This tells us that the `owner` of `folder:1` may also be a writer. So our next call could be to find the `owners` of `folder:1` - ```json - { - "tuple_key": { - "object": "folder:1", - "relation": "owner" - } - } - ``` - which gives - ```json - { - "tree": { - "root": { - "name": "folder:1#owner", - "leaf": { - "users": { - "users": [ - "user:bob" - ] - } - } - } - } - } - ``` + * Expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship + * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. + This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. + Body parameters `tuple_key.object` and `tuple_key.relation` are all required. + A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. + The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. + + ## Example + To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body + ```json + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader" + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + OpenFGA's response will be a userset tree of the users and usersets that have read access to the document. + ```json + { + "tree":{ + "root":{ + "type":"document:2021-budget#reader", + "union":{ + "nodes":[ + { + "type":"document:2021-budget#reader", + "leaf":{ + "users":{ + "users":[ + "user:bob" + ] + } + } + }, + { + "type":"document:2021-budget#reader", + "leaf":{ + "computed":{ + "userset":"document:2021-budget#writer" + } + } + } + ] + } + } + } + } + ``` + The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. + ### Expand Request with Contextual Tuples + + Given the model + ```python + model + schema 1.1 + + type user + + type folder + relations + define owner: [user] + + type document + relations + define parent: [folder] + define viewer: [user] or writer + define writer: [user] or owner from parent + ``` + and the initial tuples + ```json + [{ + "user": "user:bob", + "relation": "owner", + "object": "folder:1" + }] + ``` + + To expand all `writers` of `document:1` when `document:1` is put in `folder:1`, the first call could be + + ```json + { + "tuple_key": { + "object": "document:1", + "relation": "writer" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "folder:1", + "relation": "parent", + "object": "document:1" + } + ] + } + } + ``` + this returns: + ```json + { + "tree": { + "root": { + "name": "document:1#writer", + "union": { + "nodes": [ + { + "name": "document:1#writer", + "leaf": { + "users": { + "users": [] + } + } + }, + { + "name": "document:1#writer", + "leaf": { + "tupleToUserset": { + "tupleset": "document:1#parent", + "computed": [ + { + "userset": "folder:1#owner" + } + ] + } + } + } + ] + } + } + } + } + ``` + This tells us that the `owner` of `folder:1` may also be a writer. So our next call could be to find the `owners` of `folder:1` + ```json + { + "tuple_key": { + "object": "folder:1", + "relation": "owner" + } + } + ``` + which gives + ```json + { + "tree": { + "root": { + "name": "folder:1#owner", + "leaf": { + "users": { + "users": [ + "user:bob" + ] + } + } + } + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiExpandRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiExpandRequest */ Expand(ctx context.Context, storeId string) ApiExpandRequest @@ -454,19 +454,19 @@ type OpenFgaApi interface { GetStoreExecute(r ApiGetStoreRequest) (GetStoreResponse, *http.Response, error) /* - * ListObjects List all objects of the given type that the user has a relation with - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. - You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. - The response will contain the related objects in an array in the "objects" field of the response and they will be strings in the object format `:` (e.g. "document:roadmap"). - The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. - The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiListObjectsRequest + * ListObjects List all objects of the given type that the user has a relation with + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. + You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. + The response will contain the related objects in an array in the "objects" field of the response and they will be strings in the object format `:` (e.g. "document:roadmap"). + The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. + The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiListObjectsRequest */ ListObjects(ctx context.Context, storeId string) ApiListObjectsRequest @@ -477,12 +477,12 @@ type OpenFgaApi interface { ListObjectsExecute(r ApiListObjectsRequest) (ListObjectsResponse, *http.Response, error) /* - * ListStores 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. + * ListStores 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. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return ApiListStoresRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return ApiListStoresRequest */ ListStores(ctx context.Context) ApiListStoresRequest @@ -493,20 +493,20 @@ type OpenFgaApi interface { ListStoresExecute(r ApiListStoresRequest) (ListStoresResponse, *http.Response, error) /* - * ListUsers List the users matching the provided filter who have a certain relation to a particular type. - * The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. - You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - The response will contain the related users in an array in the "users" field of the response. These results may include specific objects, usersets - or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects - of that type have a relation to the object; it is possible that negations exist and checks should still be queried - on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. - The returned users will not be sorted, and therefore two identical calls may yield different sets of users. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiListUsersRequest + * ListUsers List the users matching the provided filter who have a certain relation to a particular type. + * The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. + You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + The response will contain the related users in an array in the "users" field of the response. These results may include specific objects, usersets + or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects + of that type have a relation to the object; it is possible that negations exist and checks should still be queried + on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. + The returned users will not be sorted, and therefore two identical calls may yield different sets of users. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiListUsersRequest */ ListUsers(ctx context.Context, storeId string) ApiListUsersRequest @@ -517,109 +517,109 @@ type OpenFgaApi interface { ListUsersExecute(r ApiListUsersRequest) (ListUsersResponse, *http.Response, error) /* - * Read Get tuples from the store that matches a query, without following userset rewrite rules - * The Read API will return the tuples for a certain store that match a query filter specified in the body of the request. - The API doesn't guarantee order by any field. - It is different from the `/stores/{store_id}/expand` API in that it only returns relationship tuples that are stored in the system and satisfy the query. - In the body: - 1. `tuple_key` is optional. If not specified, it will return all tuples in the store. - 2. `tuple_key.object` is mandatory if `tuple_key` is specified. It can be a full object (e.g., `type:object_id`) or type only (e.g., `type:`). - 3. `tuple_key.user` is mandatory if tuple_key is specified in the case the `tuple_key.object` is a type only. If tuple_key.user is specified, it needs to be a full object (e.g., `type:user_id`). - ## Examples - ### Query for all objects in a type definition - To query for all objects that `user:bob` has `reader` relationship in the `document` type definition, call read API with body of - ```json - { - "tuple_key": { - "user": "user:bob", - "relation": "reader", - "object": "document:" - } - } - ``` - The API will return tuples and a continuation token, something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `user:bob` has a `reader` relationship with 1 document `document:2021-budget`. Note that this API, unlike the List Objects API, does not evaluate the tuples in the store. - The continuation token will be empty if there are no more tuples to query. - ### Query for all stored relationship tuples that have a particular relation and object - To query for all users that have `reader` relationship with `document:2021-budget`, call read API with body of - ```json - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader" - } - } - ``` - The API will return something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `document:2021-budget` has 1 `reader` (`user:bob`). Note that, even if the model said that all `writers` are also `readers`, the API will not return writers such as `user:anne` because it only returns tuples and does not evaluate them. - ### Query for all users with all relationships for a particular document - To query for all users that have any relationship with `document:2021-budget`, call read API with body of - ```json - { - "tuple_key": { - "object": "document:2021-budget" - } - } - ``` - The API will return something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:anne", - "relation": "writer", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-05T13:42:12.356Z" - }, - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `document:2021-budget` has 1 `reader` (`user:bob`) and 1 `writer` (`user:anne`). + * Read Get tuples from the store that matches a query, without following userset rewrite rules + * The Read API will return the tuples for a certain store that match a query filter specified in the body of the request. + The API doesn't guarantee order by any field. + It is different from the `/stores/{store_id}/expand` API in that it only returns relationship tuples that are stored in the system and satisfy the query. + In the body: + 1. `tuple_key` is optional. If not specified, it will return all tuples in the store. + 2. `tuple_key.object` is mandatory if `tuple_key` is specified. It can be a full object (e.g., `type:object_id`) or type only (e.g., `type:`). + 3. `tuple_key.user` is mandatory if tuple_key is specified in the case the `tuple_key.object` is a type only. If tuple_key.user is specified, it needs to be a full object (e.g., `type:user_id`). + ## Examples + ### Query for all objects in a type definition + To query for all objects that `user:bob` has `reader` relationship in the `document` type definition, call read API with body of + ```json + { + "tuple_key": { + "user": "user:bob", + "relation": "reader", + "object": "document:" + } + } + ``` + The API will return tuples and a continuation token, something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `user:bob` has a `reader` relationship with 1 document `document:2021-budget`. Note that this API, unlike the List Objects API, does not evaluate the tuples in the store. + The continuation token will be empty if there are no more tuples to query. + ### Query for all stored relationship tuples that have a particular relation and object + To query for all users that have `reader` relationship with `document:2021-budget`, call read API with body of + ```json + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader" + } + } + ``` + The API will return something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `document:2021-budget` has 1 `reader` (`user:bob`). Note that, even if the model said that all `writers` are also `readers`, the API will not return writers such as `user:anne` because it only returns tuples and does not evaluate them. + ### Query for all users with all relationships for a particular document + To query for all users that have any relationship with `document:2021-budget`, call read API with body of + ```json + { + "tuple_key": { + "object": "document:2021-budget" + } + } + ``` + The API will return something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:anne", + "relation": "writer", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-05T13:42:12.356Z" + }, + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `document:2021-budget` has 1 `reader` (`user:bob`) and 1 `writer` (`user:anne`). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadRequest */ Read(ctx context.Context, storeId string) ApiReadRequest @@ -646,52 +646,52 @@ type OpenFgaApi interface { ReadAssertionsExecute(r ApiReadAssertionsRequest) (ReadAssertionsResponse, *http.Response, error) /* - * ReadAuthorizationModel Return a particular version of an authorization model - * The ReadAuthorizationModel API returns an authorization model by its identifier. - The response will return the authorization model for the particular version. - - ## Example - To retrieve the authorization model with ID `01G5JAVJ41T49E9TT3SKVS7X1J` for the store, call the GET authorization-models by ID API with `01G5JAVJ41T49E9TT3SKVS7X1J` as the `id` path parameter. The API will return: - ```json - { - "authorization_model":{ - "id":"01G5JAVJ41T49E9TT3SKVS7X1J", - "type_definitions":[ - { - "type":"user" - }, - { - "type":"document", - "relations":{ - "reader":{ - "union":{ - "child":[ - { - "this":{} - }, - { - "computedUserset":{ - "object":"", - "relation":"writer" - } - } - ] - } - }, - "writer":{ - "this":{} - } - } - } - ] - } - } - ``` - In the above example, there are 2 types (`user` and `document`). The `document` type has 2 relations (`writer` and `reader`). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @param id - * @return ApiReadAuthorizationModelRequest + * ReadAuthorizationModel Return a particular version of an authorization model + * The ReadAuthorizationModel API returns an authorization model by its identifier. + The response will return the authorization model for the particular version. + + ## Example + To retrieve the authorization model with ID `01G5JAVJ41T49E9TT3SKVS7X1J` for the store, call the GET authorization-models by ID API with `01G5JAVJ41T49E9TT3SKVS7X1J` as the `id` path parameter. The API will return: + ```json + { + "authorization_model":{ + "id":"01G5JAVJ41T49E9TT3SKVS7X1J", + "type_definitions":[ + { + "type":"user" + }, + { + "type":"document", + "relations":{ + "reader":{ + "union":{ + "child":[ + { + "this":{} + }, + { + "computedUserset":{ + "object":"", + "relation":"writer" + } + } + ] + } + }, + "writer":{ + "this":{} + } + } + } + ] + } + } + ``` + In the above example, there are 2 types (`user` and `document`). The `document` type has 2 relations (`writer` and `reader`). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @param id + * @return ApiReadAuthorizationModelRequest */ ReadAuthorizationModel(ctx context.Context, storeId string, id string) ApiReadAuthorizationModelRequest @@ -702,47 +702,47 @@ type OpenFgaApi interface { ReadAuthorizationModelExecute(r ApiReadAuthorizationModelRequest) (ReadAuthorizationModelResponse, *http.Response, error) /* - * ReadAuthorizationModels Return all the authorization models for a particular store - * The ReadAuthorizationModels API will return all the authorization models for a certain store. - OpenFGA's response will contain an array of all authorization models, sorted in descending order of creation. - - ## Example - Assume that a store's authorization model has been configured twice. To get all the authorization models that have been created in this store, call GET authorization-models. The API will return a response that looks like: - ```json - { - "authorization_models": [ - { - "id": "01G50QVV17PECNVAHX1GG4Y5NC", - "type_definitions": [...] - }, - { - "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", - "type_definitions": [...] - }, - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - If there are no more authorization models available, the `continuation_token` field will be empty - ```json - { - "authorization_models": [ - { - "id": "01G50QVV17PECNVAHX1GG4Y5NC", - "type_definitions": [...] - }, - { - "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", - "type_definitions": [...] - }, - ], - "continuation_token": "" - } - ``` + * ReadAuthorizationModels Return all the authorization models for a particular store + * The ReadAuthorizationModels API will return all the authorization models for a certain store. + OpenFGA's response will contain an array of all authorization models, sorted in descending order of creation. + + ## Example + Assume that a store's authorization model has been configured twice. To get all the authorization models that have been created in this store, call GET authorization-models. The API will return a response that looks like: + ```json + { + "authorization_models": [ + { + "id": "01G50QVV17PECNVAHX1GG4Y5NC", + "type_definitions": [...] + }, + { + "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", + "type_definitions": [...] + }, + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + If there are no more authorization models available, the `continuation_token` field will be empty + ```json + { + "authorization_models": [ + { + "id": "01G50QVV17PECNVAHX1GG4Y5NC", + "type_definitions": [...] + }, + { + "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", + "type_definitions": [...] + }, + ], + "continuation_token": "" + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadAuthorizationModelsRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadAuthorizationModelsRequest */ ReadAuthorizationModels(ctx context.Context, storeId string) ApiReadAuthorizationModelsRequest @@ -753,15 +753,15 @@ type OpenFgaApi interface { ReadAuthorizationModelsExecute(r ApiReadAuthorizationModelsRequest) (ReadAuthorizationModelsResponse, *http.Response, error) /* - * ReadChanges Return a list of all the tuple changes - * The ReadChanges API will return a paginated list of tuple changes (additions and deletions) that occurred in a given store, sorted by ascending time. The response will include a continuation token that is used to get the next set of changes. If there are no changes after the provided continuation token, the same token will be returned in order for it to be used when new changes are recorded. If the store never had any tuples added or removed, this token will be empty. - You can use the `type` parameter to only get the list of tuple changes that affect objects of that type. - When reading a write tuple change, if it was conditioned, the condition will be returned. - When reading a delete tuple change, the condition will NOT be returned regardless of whether it was originally conditioned or not. - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadChangesRequest + * ReadChanges Return a list of all the tuple changes + * The ReadChanges API will return a paginated list of tuple changes (additions and deletions) that occurred in a given store, sorted by ascending time. The response will include a continuation token that is used to get the next set of changes. If there are no changes after the provided continuation token, the same token will be returned in order for it to be used when new changes are recorded. If the store never had any tuples added or removed, this token will be empty. + You can use the `type` parameter to only get the list of tuple changes that affect objects of that type. + When reading a write tuple change, if it was conditioned, the condition will be returned. + When reading a delete tuple change, the condition will NOT be returned regardless of whether it was originally conditioned or not. + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadChangesRequest */ ReadChanges(ctx context.Context, storeId string) ApiReadChangesRequest @@ -772,14 +772,14 @@ type OpenFgaApi interface { ReadChangesExecute(r ApiReadChangesRequest) (ReadChangesResponse, *http.Response, error) /* - * StreamedListObjects Stream all objects of the given type that the user has a relation with - * The Streamed ListObjects API is very similar to the the ListObjects API, with two differences: - 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected. - 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE. - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiStreamedListObjectsRequest + * StreamedListObjects Stream all objects of the given type that the user has a relation with + * The Streamed ListObjects API is very similar to the the ListObjects API, with two differences: + 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected. + 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE. + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiStreamedListObjectsRequest */ StreamedListObjects(ctx context.Context, storeId string) ApiStreamedListObjectsRequest @@ -790,53 +790,53 @@ type OpenFgaApi interface { StreamedListObjectsExecute(r ApiStreamedListObjectsRequest) (StreamResultOfStreamedListObjectsResponse, *http.Response, error) /* - * Write Add or delete tuples from the store - * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. - In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. - The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. - To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. - To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. - If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. - The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. - An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. - ## Example - ### Adding relationships - To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following - ```json - { - "writes": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "writer", - "object": "document:2021-budget" - } - ], - "on_duplicate": "ignore" - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - ### Removing relationships - To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following - ```json - { - "deletes": { - "tuple_keys": [ - { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - } - ], - "on_missing": "ignore" - } - } - ``` + * Write Add or delete tuples from the store + * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. + In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. + The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. + To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. + If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. + The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. + An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. + ## Example + ### Adding relationships + To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following + ```json + { + "writes": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "writer", + "object": "document:2021-budget" + } + ], + "on_duplicate": "ignore" + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + ### Removing relationships + To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following + ```json + { + "deletes": { + "tuple_keys": [ + { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + } + ], + "on_missing": "ignore" + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiWriteRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiWriteRequest */ Write(ctx context.Context, storeId string) ApiWriteRequest @@ -862,53 +862,53 @@ type OpenFgaApi interface { WriteAssertionsExecute(r ApiWriteAssertionsRequest) (*http.Response, error) /* - * WriteAuthorizationModel Create a new authorization model - * The WriteAuthorizationModel API will add a new authorization model to a store. - Each item in the `type_definitions` array is a type definition as specified in the field `type_definition`. - The response will return the authorization model's ID in the `id` field. - - ## Example - To add an authorization model with `user` and `document` type definitions, call POST authorization-models API with the body: - ```json - { - "type_definitions":[ - { - "type":"user" - }, - { - "type":"document", - "relations":{ - "reader":{ - "union":{ - "child":[ - { - "this":{} - }, - { - "computedUserset":{ - "object":"", - "relation":"writer" - } - } - ] - } - }, - "writer":{ - "this":{} - } - } - } - ] - } - ``` - OpenFGA's response will include the version id for this authorization model, which will look like - ``` - {"authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC"} - ``` - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiWriteAuthorizationModelRequest + * WriteAuthorizationModel Create a new authorization model + * The WriteAuthorizationModel API will add a new authorization model to a store. + Each item in the `type_definitions` array is a type definition as specified in the field `type_definition`. + The response will return the authorization model's ID in the `id` field. + + ## Example + To add an authorization model with `user` and `document` type definitions, call POST authorization-models API with the body: + ```json + { + "type_definitions":[ + { + "type":"user" + }, + { + "type":"document", + "relations":{ + "reader":{ + "union":{ + "child":[ + { + "this":{} + }, + { + "computedUserset":{ + "object":"", + "relation":"writer" + } + } + ] + } + }, + "writer":{ + "this":{} + } + } + } + ] + } + ``` + OpenFGA's response will include the version id for this authorization model, which will look like + ``` + {"authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC"} + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiWriteAuthorizationModelRequest */ WriteAuthorizationModel(ctx context.Context, storeId string) ApiWriteAuthorizationModelRequest diff --git a/example/example1/go.mod b/example/example1/go.mod index 70aaac1..11da0ac 100644 --- a/example/example1/go.mod +++ b/example/example1/go.mod @@ -19,5 +19,5 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/sync v0.18.0 // indirect ) diff --git a/example/opentelemetry/go.mod b/example/opentelemetry/go.mod index d9d2f5b..63ef998 100644 --- a/example/opentelemetry/go.mod +++ b/example/opentelemetry/go.mod @@ -31,7 +31,7 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/example/streamed_list_objects/go.mod b/example/streamed_list_objects/go.mod index e88786a..f7eebf9 100644 --- a/example/streamed_list_objects/go.mod +++ b/example/streamed_list_objects/go.mod @@ -25,7 +25,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect diff --git a/streaming.go b/streaming.go index 829f010..478cc73 100644 --- a/streaming.go +++ b/streaming.go @@ -23,19 +23,38 @@ import ( "strings" ) -type StreamedListObjectsChannel struct { - Objects chan StreamedListObjectsResponse +// StreamResult represents a generic streaming result wrapper with either a result or an error +type StreamResult[T any] struct { + Result *T `json:"result,omitempty" yaml:"result,omitempty"` + Error *Status `json:"error,omitempty" yaml:"error,omitempty"` +} + +// StreamingChannel represents a generic channel for streaming responses +type StreamingChannel[T any] struct { + Results chan T Errors chan error cancel context.CancelFunc } -func (s *StreamedListObjectsChannel) Close() { +// Close cancels the streaming context and cleans up resources +func (s *StreamingChannel[T]) Close() { if s.cancel != nil { s.cancel() } } -func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http.Response, bufferSize int) (*StreamedListObjectsChannel, error) { +// ProcessStreamingResponse processes an HTTP response as a streaming NDJSON response +// and returns a StreamingChannel with results and errors +// +// Parameters: +// - ctx: The context for cancellation +// - httpResponse: The HTTP response to process +// - bufferSize: The buffer size for the channels (default 10 if <= 0) +// +// Returns: +// - *StreamingChannel[T]: A channel containing streaming results and errors +// - error: An error if the response is invalid +func ProcessStreamingResponse[T any](ctx context.Context, httpResponse *http.Response, bufferSize int) (*StreamingChannel[T], error) { streamCtx, cancel := context.WithCancel(ctx) // Use default buffer size of 10 if not specified or invalid @@ -43,8 +62,8 @@ func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http. bufferSize = 10 } - channel := &StreamedListObjectsChannel{ - Objects: make(chan StreamedListObjectsResponse, bufferSize), + channel := &StreamingChannel[T]{ + Results: make(chan T, bufferSize), Errors: make(chan error, 1), cancel: cancel, } @@ -55,20 +74,16 @@ func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http. } go func() { - defer close(channel.Objects) + defer close(channel.Results) defer close(channel.Errors) defer cancel() - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - channel.Errors <- err - } - }(httpResponse.Body) + defer func() { _ = httpResponse.Body.Close() }() scanner := bufio.NewScanner(httpResponse.Body) // Allow large NDJSON entries (up to 10MB). Tune as needed. buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 10*1024*1024) + for scanner.Scan() { select { case <-streamCtx.Done(): @@ -80,7 +95,7 @@ func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http. continue } - var streamResult StreamResultOfStreamedListObjectsResponse + var streamResult StreamResult[T] if err := json.Unmarshal(line, &streamResult); err != nil { channel.Errors <- err return @@ -100,7 +115,7 @@ func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http. case <-streamCtx.Done(): channel.Errors <- streamCtx.Err() return - case channel.Objects <- *streamResult.Result: + case channel.Results <- *streamResult.Result: } } } @@ -119,16 +134,83 @@ func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http. return channel, nil } +// StreamedListObjectsChannel maintains backward compatibility with the old channel structure +type StreamedListObjectsChannel struct { + Objects chan StreamedListObjectsResponse + Errors chan error + cancel context.CancelFunc +} + +// Close cancels the streaming context and cleans up resources +func (s *StreamedListObjectsChannel) Close() { + if s.cancel != nil { + s.cancel() + } +} + +// ProcessStreamedListObjectsResponse processes a StreamedListObjects response +// This is a backward compatibility wrapper around ProcessStreamingResponse +func ProcessStreamedListObjectsResponse(ctx context.Context, httpResponse *http.Response, bufferSize int) (*StreamedListObjectsChannel, error) { + channel, err := ProcessStreamingResponse[StreamedListObjectsResponse](ctx, httpResponse, bufferSize) + if err != nil { + return nil, err + } + + // Create a new channel with the old field name for backward compatibility + compatChannel := &StreamedListObjectsChannel{ + Objects: channel.Results, + Errors: channel.Errors, + cancel: channel.cancel, + } + + return compatChannel, nil +} + +// ExecuteStreamedListObjects executes a StreamedListObjects request func ExecuteStreamedListObjects(client *APIClient, ctx context.Context, storeId string, body ListObjectsRequest, options RequestOptions) (*StreamedListObjectsChannel, error) { return ExecuteStreamedListObjectsWithBufferSize(client, ctx, storeId, body, options, 0) } +// ExecuteStreamedListObjectsWithBufferSize executes a StreamedListObjects request with a custom buffer size func ExecuteStreamedListObjectsWithBufferSize(client *APIClient, ctx context.Context, storeId string, body ListObjectsRequest, options RequestOptions, bufferSize int) (*StreamedListObjectsChannel, error) { - path := "/stores/{store_id}/streamed-list-objects" + channel, err := executeStreamingRequest[ListObjectsRequest, StreamedListObjectsResponse]( + client, + ctx, + "/stores/{store_id}/streamed-list-objects", + storeId, + body, + options, + bufferSize, + "StreamedListObjects", + ) + if err != nil { + return nil, err + } + + // Convert to backward-compatible channel structure + return &StreamedListObjectsChannel{ + Objects: channel.Results, + Errors: channel.Errors, + cancel: channel.cancel, + }, nil +} + +// executeStreamingRequest is a generic function to execute streaming requests +func executeStreamingRequest[TReq any, TRes any]( + client *APIClient, + ctx context.Context, + pathTemplate string, + storeId string, + body TReq, + options RequestOptions, + bufferSize int, + operationName string, +) (*StreamingChannel[TRes], error) { if storeId == "" { return nil, reportError("storeId is required and must be specified") } + path := pathTemplate path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(storeId, ""))) localVarHeaderParams := make(map[string]string) @@ -158,9 +240,9 @@ func ExecuteStreamedListObjectsWithBufferSize(client *APIClient, ctx context.Con if readErr != nil { return nil, readErr } - err = client.handleAPIError(httpResponse, responseBody, body, "StreamedListObjects", storeId) + err = client.handleAPIError(httpResponse, responseBody, body, operationName, storeId) return nil, err } - return ProcessStreamedListObjectsResponse(ctx, httpResponse, bufferSize) + return ProcessStreamingResponse[TRes](ctx, httpResponse, bufferSize) } diff --git a/streaming_test.go b/streaming_test.go index 3282494..70b0a3b 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -22,6 +22,23 @@ import ( "time" ) +func TestStreamingChannel_Close(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + channel := &StreamingChannel[StreamedListObjectsResponse]{ + Results: make(chan StreamedListObjectsResponse), + Errors: make(chan error), + cancel: cancel, + } + + channel.Close() + + select { + case <-ctx.Done(): + case <-time.After(100 * time.Millisecond): + t.Error("Context was not cancelled") + } +} + func TestStreamedListObjectsChannel_Close(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) channel := &StreamedListObjectsChannel{ @@ -249,7 +266,7 @@ invalid json` err = <-channel.Errors if err == nil { - t.Fatal("Expected error from channel for invalid JSON, got nil") + t.Fatal("Expected error from channel, got nil") } if len(receivedObjects) != 1 { @@ -261,13 +278,13 @@ func TestStreamedListObjectsWithChannel_ContextCancellation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") w.WriteHeader(http.StatusOK) - for i := 0; i < 100; i++ { - _, _ = w.Write([]byte(`{"result":{"object":"document:` + strconv.Itoa(i) + `"}}` + "\n")) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - time.Sleep(10 * time.Millisecond) + // Send first result + _, _ = w.Write([]byte(`{"result":{"object":"document:1"}}` + "\n")) + if f, ok := w.(http.Flusher); ok { + f.Flush() } + // Wait indefinitely to simulate a long stream + <-r.Context().Done() })) defer server.Close() @@ -295,31 +312,31 @@ func TestStreamedListObjectsWithChannel_ContextCancellation(t *testing.T) { defer channel.Close() - receivedObjects := []string{} - for i := 0; i < 5; i++ { - obj := <-channel.Objects - receivedObjects = append(receivedObjects, obj.Object) + // Read first result + obj := <-channel.Objects + if obj.Object != "document:1" { + t.Errorf("Expected document:1, got %s", obj.Object) } + // Cancel context cancel() - time.Sleep(100 * time.Millisecond) - - remaining := 0 - for range channel.Objects { - remaining++ + // Check that we get a cancellation error + err = <-channel.Errors + if err == nil { + t.Fatal("Expected cancellation error, got nil") } - if len(receivedObjects) < 5 { - t.Fatalf("Expected at least 5 objects, got %d", len(receivedObjects)) + if !strings.Contains(err.Error(), "context canceled") && !strings.Contains(err.Error(), "operation was canceled") { + t.Errorf("Expected context cancellation error, got: %v", err) } } func TestStreamedListObjectsWithChannel_CustomBufferSize(t *testing.T) { - objects := []string{"document:1", "document:2", "document:3", "document:4", "document:5"} + numObjects := 100 expectedResults := []string{} - for _, obj := range objects { - expectedResults = append(expectedResults, `{"result":{"object":"`+obj+`"}}`) + for i := 0; i < numObjects; i++ { + expectedResults = append(expectedResults, `{"result":{"object":"document:`+strconv.Itoa(i)+`"}}`) } responseBody := strings.Join(expectedResults, "\n") @@ -346,11 +363,11 @@ func TestStreamedListObjectsWithChannel_CustomBufferSize(t *testing.T) { User: "user:anne", } - // Test with custom buffer size using the new function + // Use custom buffer size of 50 channel, err := ExecuteStreamedListObjectsWithBufferSize(client, ctx, "test-store", request, RequestOptions{}, 50) if err != nil { - t.Fatalf("ExecuteStreamedListObjects failed: %v", err) + t.Fatalf("ExecuteStreamedListObjectsWithBufferSize failed: %v", err) } defer channel.Close() @@ -364,24 +381,15 @@ func TestStreamedListObjectsWithChannel_CustomBufferSize(t *testing.T) { t.Fatalf("Received error from channel: %v", err) } - if len(receivedObjects) != len(objects) { - t.Fatalf("Expected %d objects, got %d", len(objects), len(receivedObjects)) - } - - for i, expected := range objects { - if receivedObjects[i] != expected { - t.Errorf("Expected object %s at index %d, got %s", expected, i, receivedObjects[i]) - } + if len(receivedObjects) != numObjects { + t.Fatalf("Expected %d objects, got %d", numObjects, len(receivedObjects)) } } -func TestStreamedListObjectsWithChannel_DefaultBufferSize(t *testing.T) { - objects := []string{"document:1", "document:2"} - expectedResults := []string{} - for _, obj := range objects { - expectedResults = append(expectedResults, `{"result":{"object":"`+obj+`"}}`) - } - responseBody := strings.Join(expectedResults, "\n") +func TestProcessStreamingResponse_Generic(t *testing.T) { + // Test the generic ProcessStreamingResponse function + responseBody := `{"result":{"object":"document:1"}} +{"result":{"object":"document:2"}}` server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-ndjson") @@ -390,33 +398,22 @@ func TestStreamedListObjectsWithChannel_DefaultBufferSize(t *testing.T) { })) defer server.Close() - config, err := NewConfiguration(Configuration{ - ApiUrl: server.URL, - }) + resp, err := http.Get(server.URL) if err != nil { - t.Fatalf("Failed to create configuration: %v", err) + t.Fatalf("Failed to make request: %v", err) } - client := NewAPIClient(config) ctx := context.Background() - - request := ListObjectsRequest{ - Type: "document", - Relation: "viewer", - User: "user:anne", - } - - // Test with default buffer size (0 uses default of 10) - channel, err := ExecuteStreamedListObjectsWithBufferSize(client, ctx, "test-store", request, RequestOptions{}, 0) + channel, err := ProcessStreamingResponse[StreamedListObjectsResponse](ctx, resp, 10) if err != nil { - t.Fatalf("ExecuteStreamedListObjects failed: %v", err) + t.Fatalf("ProcessStreamingResponse failed: %v", err) } defer channel.Close() receivedObjects := []string{} - for obj := range channel.Objects { + for obj := range channel.Results { receivedObjects = append(receivedObjects, obj.Object) } @@ -424,70 +421,14 @@ func TestStreamedListObjectsWithChannel_DefaultBufferSize(t *testing.T) { t.Fatalf("Received error from channel: %v", err) } - if len(receivedObjects) != len(objects) { - t.Fatalf("Expected %d objects, got %d", len(objects), len(receivedObjects)) + if len(receivedObjects) != 2 { + t.Fatalf("Expected 2 objects, got %d", len(receivedObjects)) } -} -func TestStreamedListObjectsWithChannel_ProperNumericStrings(t *testing.T) { - // Test that document IDs are generated correctly for values >= 10 - // This verifies the fix: using strconv.Itoa(i) instead of string(rune('0'+i%10)) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/x-ndjson") - w.WriteHeader(http.StatusOK) - // Generate 15 objects to ensure we test values >= 10 - for i := 0; i < 15; i++ { - _, _ = w.Write([]byte(`{"result":{"object":"document:` + strconv.Itoa(i) + `"}}` + "\n")) - } - })) - defer server.Close() - - config, err := NewConfiguration(Configuration{ - ApiUrl: server.URL, - }) - if err != nil { - t.Fatalf("Failed to create configuration: %v", err) - } - - client := NewAPIClient(config) - ctx := context.Background() - - request := ListObjectsRequest{ - Type: "document", - Relation: "viewer", - User: "user:anne", + if receivedObjects[0] != "document:1" { + t.Errorf("Expected document:1, got %s", receivedObjects[0]) } - - channel, err := ExecuteStreamedListObjects(client, ctx, "test-store", request, RequestOptions{}) - - if err != nil { - t.Fatalf("ExecuteStreamedListObjects failed: %v", err) - } - - defer channel.Close() - - receivedObjects := []string{} - for obj := range channel.Objects { - receivedObjects = append(receivedObjects, obj.Object) - } - - if err := <-channel.Errors; err != nil { - t.Fatalf("Received error from channel: %v", err) - } - - expectedObjects := []string{ - "document:0", "document:1", "document:2", "document:3", "document:4", - "document:5", "document:6", "document:7", "document:8", "document:9", - "document:10", "document:11", "document:12", "document:13", "document:14", - } - - if len(receivedObjects) != len(expectedObjects) { - t.Fatalf("Expected %d objects, got %d", len(expectedObjects), len(receivedObjects)) - } - - for i, expected := range expectedObjects { - if receivedObjects[i] != expected { - t.Errorf("At index %d: expected %s, got %s", i, expected, receivedObjects[i]) - } + if receivedObjects[1] != "document:2" { + t.Errorf("Expected document:2, got %s", receivedObjects[1]) } } From e4212bd42abd30fbdf2db2e596a134d46b20d009 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 20 Nov 2025 15:04:42 +0530 Subject: [PATCH 4/5] fix: go mod tidy --- api_open_fga.go | 1338 +++++++++++++------------- example/example1/go.mod | 1 - example/opentelemetry/go.mod | 10 - example/streamed_list_objects/go.mod | 11 - 4 files changed, 669 insertions(+), 691 deletions(-) diff --git a/api_open_fga.go b/api_open_fga.go index d2f17f2..045765f 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -39,62 +39,62 @@ type RequestOptions struct { type OpenFgaApi interface { /* - * BatchCheck Send a list of `check` operations in a single request - * The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. - - An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\w\d-]{1,36}$` - - NOTE: The maximum number of checks that can be passed in the `BatchCheck` API is configurable via the [OPENFGA_MAX_CHECKS_PER_BATCH_CHECK](https://openfga.dev/docs/getting-started/setup-openfga/configuration#OPENFGA_MAX_CHECKS_PER_BATCH_CHECK) environment variable. If `BatchCheck` is called using the SDK, the SDK can split the batch check requests for you. - - For more details on how `Check` functions, see the docs for `/check`. - - ### Examples - #### A BatchCheckRequest - ```json - { - "checks": [ - { - "tuple_key": { - "object": "document:2021-budget" - "relation": "reader", - "user": "user:anne", - }, - "contextual_tuples": {...} - "context": {} - "correlation_id": "01JA8PM3QM7VBPGB8KMPK8SBD5" - }, - { - "tuple_key": { - "object": "document:2021-budget" - "relation": "reader", - "user": "user:bob", - }, - "contextual_tuples": {...} - "context": {} - "correlation_id": "01JA8PMM6A90NV5ET0F28CYSZQ" - } - ] - } - ``` - - Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: - ```json - { - "result": { - "01JA8PMM6A90NV5ET0F28CYSZQ": { - "allowed": false, - "error": {"message": ""} - }, - "01JA8PM3QM7VBPGB8KMPK8SBD5": { - "allowed": true, - "error": {"message": ""} - } - } - ``` + * BatchCheck Send a list of `check` operations in a single request + * The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. + + An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\w\d-]{1,36}$` + + NOTE: The maximum number of checks that can be passed in the `BatchCheck` API is configurable via the [OPENFGA_MAX_CHECKS_PER_BATCH_CHECK](https://openfga.dev/docs/getting-started/setup-openfga/configuration#OPENFGA_MAX_CHECKS_PER_BATCH_CHECK) environment variable. If `BatchCheck` is called using the SDK, the SDK can split the batch check requests for you. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiBatchCheckRequest + For more details on how `Check` functions, see the docs for `/check`. + + ### Examples + #### A BatchCheckRequest + ```json + { + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget" + "relation": "reader", + "user": "user:anne", + }, + "contextual_tuples": {...} + "context": {} + "correlation_id": "01JA8PM3QM7VBPGB8KMPK8SBD5" + }, + { + "tuple_key": { + "object": "document:2021-budget" + "relation": "reader", + "user": "user:bob", + }, + "contextual_tuples": {...} + "context": {} + "correlation_id": "01JA8PMM6A90NV5ET0F28CYSZQ" + } + ] + } + ``` + + Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: + ```json + { + "result": { + "01JA8PMM6A90NV5ET0F28CYSZQ": { + "allowed": false, + "error": {"message": ""} + }, + "01JA8PM3QM7VBPGB8KMPK8SBD5": { + "allowed": true, + "error": {"message": ""} + } + } + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiBatchCheckRequest */ BatchCheck(ctx context.Context, storeId string) ApiBatchCheckRequest @@ -105,129 +105,129 @@ type OpenFgaApi interface { BatchCheckExecute(r ApiBatchCheckRequest) (BatchCheckResponse, *http.Response, error) /* - * Check Check whether a user is authorized to access an object - * The Check API returns whether a given user has a relationship with a given object in a given store. - The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. - You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. - The response will return whether the relationship exists in the field `allowed`. - - Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. - For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. - ## Examples - ### Querying with contextual tuples - In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple - ```json - { - "user": "user:anne", - "relation": "member", - "object": "time_slot:office_hours" - } - ``` - the Check API can be used with the following request body: - ```json - { - "tuple_key": { - "user": "user:anne", - "relation": "reader", - "object": "document:2021-budget" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "member", - "object": "time_slot:office_hours" - } - ] - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - ### Querying usersets - Some Checks will always return `true`, even without any tuples. For example, for the following authorization model - ```python - model - schema 1.1 - type user - type document - relations - define reader: [user] - ``` - the following query - ```json - { - "tuple_key": { - "user": "document:2021-budget#reader", - "relation": "reader", - "object": "document:2021-budget" - } - } - ``` - will always return `{ "allowed": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. - ### Querying usersets with difference in the model - A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model - ```python - model - schema 1.1 - type user - type group - relations - define member: [user] - type document - relations - define blocked: [user] - define reader: [group#member] but not blocked - ``` - the following query - ```json - { - "tuple_key": { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "member", - "object": "group:finance" - }, - { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - { - "user": "user:anne", - "relation": "blocked", - "object": "document:2021-budget" - } - ] - }, - } - ``` - will return `{ "allowed": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. - ### Requesting higher consistency - By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. - ```json - { - "tuple_key": { - "user": "group:finance#member", - "relation": "reader", - "object": "document:2021-budget" - }, - "consistency": "HIGHER_CONSISTENCY" - } - ``` + * Check Check whether a user is authorized to access an object + * The Check API returns whether a given user has a relationship with a given object in a given store. + The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. + You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. + The response will return whether the relationship exists in the field `allowed`. + + Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. + For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. + ## Examples + ### Querying with contextual tuples + In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple + ```json + { + "user": "user:anne", + "relation": "member", + "object": "time_slot:office_hours" + } + ``` + the Check API can be used with the following request body: + ```json + { + "tuple_key": { + "user": "user:anne", + "relation": "reader", + "object": "document:2021-budget" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "member", + "object": "time_slot:office_hours" + } + ] + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + ### Querying usersets + Some Checks will always return `true`, even without any tuples. For example, for the following authorization model + ```python + model + schema 1.1 + type user + type document + relations + define reader: [user] + ``` + the following query + ```json + { + "tuple_key": { + "user": "document:2021-budget#reader", + "relation": "reader", + "object": "document:2021-budget" + } + } + ``` + will always return `{ "allowed": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. + ### Querying usersets with difference in the model + A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model + ```python + model + schema 1.1 + type user + type group + relations + define member: [user] + type document + relations + define blocked: [user] + define reader: [group#member] but not blocked + ``` + the following query + ```json + { + "tuple_key": { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "member", + "object": "group:finance" + }, + { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + { + "user": "user:anne", + "relation": "blocked", + "object": "document:2021-budget" + } + ] + }, + } + ``` + will return `{ "allowed": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. + ### Requesting higher consistency + By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. + ```json + { + "tuple_key": { + "user": "group:finance#member", + "relation": "reader", + "object": "document:2021-budget" + }, + "consistency": "HIGHER_CONSISTENCY" + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiCheckRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiCheckRequest */ Check(ctx context.Context, storeId string) ApiCheckRequest @@ -266,169 +266,169 @@ type OpenFgaApi interface { DeleteStoreExecute(r ApiDeleteStoreRequest) (*http.Response, error) /* - * Expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship - * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. - This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. - Body parameters `tuple_key.object` and `tuple_key.relation` are all required. - A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. - The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. - - ## Example - To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body - ```json - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader" - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - OpenFGA's response will be a userset tree of the users and usersets that have read access to the document. - ```json - { - "tree":{ - "root":{ - "type":"document:2021-budget#reader", - "union":{ - "nodes":[ - { - "type":"document:2021-budget#reader", - "leaf":{ - "users":{ - "users":[ - "user:bob" - ] - } - } - }, - { - "type":"document:2021-budget#reader", - "leaf":{ - "computed":{ - "userset":"document:2021-budget#writer" - } - } - } - ] - } - } - } - } - ``` - The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. - ### Expand Request with Contextual Tuples - - Given the model - ```python - model - schema 1.1 - - type user - - type folder - relations - define owner: [user] - - type document - relations - define parent: [folder] - define viewer: [user] or writer - define writer: [user] or owner from parent - ``` - and the initial tuples - ```json - [{ - "user": "user:bob", - "relation": "owner", - "object": "folder:1" - }] - ``` - - To expand all `writers` of `document:1` when `document:1` is put in `folder:1`, the first call could be - - ```json - { - "tuple_key": { - "object": "document:1", - "relation": "writer" - }, - "contextual_tuples": { - "tuple_keys": [ - { - "user": "folder:1", - "relation": "parent", - "object": "document:1" - } - ] - } - } - ``` - this returns: - ```json - { - "tree": { - "root": { - "name": "document:1#writer", - "union": { - "nodes": [ - { - "name": "document:1#writer", - "leaf": { - "users": { - "users": [] - } - } - }, - { - "name": "document:1#writer", - "leaf": { - "tupleToUserset": { - "tupleset": "document:1#parent", - "computed": [ - { - "userset": "folder:1#owner" - } - ] - } - } - } - ] - } - } - } - } - ``` - This tells us that the `owner` of `folder:1` may also be a writer. So our next call could be to find the `owners` of `folder:1` - ```json - { - "tuple_key": { - "object": "folder:1", - "relation": "owner" - } - } - ``` - which gives - ```json - { - "tree": { - "root": { - "name": "folder:1#owner", - "leaf": { - "users": { - "users": [ - "user:bob" - ] - } - } - } - } - } - ``` + * Expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship + * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. + This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. + Body parameters `tuple_key.object` and `tuple_key.relation` are all required. + A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. + The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. + + ## Example + To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body + ```json + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader" + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + OpenFGA's response will be a userset tree of the users and usersets that have read access to the document. + ```json + { + "tree":{ + "root":{ + "type":"document:2021-budget#reader", + "union":{ + "nodes":[ + { + "type":"document:2021-budget#reader", + "leaf":{ + "users":{ + "users":[ + "user:bob" + ] + } + } + }, + { + "type":"document:2021-budget#reader", + "leaf":{ + "computed":{ + "userset":"document:2021-budget#writer" + } + } + } + ] + } + } + } + } + ``` + The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. + ### Expand Request with Contextual Tuples + + Given the model + ```python + model + schema 1.1 + + type user + + type folder + relations + define owner: [user] + + type document + relations + define parent: [folder] + define viewer: [user] or writer + define writer: [user] or owner from parent + ``` + and the initial tuples + ```json + [{ + "user": "user:bob", + "relation": "owner", + "object": "folder:1" + }] + ``` + + To expand all `writers` of `document:1` when `document:1` is put in `folder:1`, the first call could be + + ```json + { + "tuple_key": { + "object": "document:1", + "relation": "writer" + }, + "contextual_tuples": { + "tuple_keys": [ + { + "user": "folder:1", + "relation": "parent", + "object": "document:1" + } + ] + } + } + ``` + this returns: + ```json + { + "tree": { + "root": { + "name": "document:1#writer", + "union": { + "nodes": [ + { + "name": "document:1#writer", + "leaf": { + "users": { + "users": [] + } + } + }, + { + "name": "document:1#writer", + "leaf": { + "tupleToUserset": { + "tupleset": "document:1#parent", + "computed": [ + { + "userset": "folder:1#owner" + } + ] + } + } + } + ] + } + } + } + } + ``` + This tells us that the `owner` of `folder:1` may also be a writer. So our next call could be to find the `owners` of `folder:1` + ```json + { + "tuple_key": { + "object": "folder:1", + "relation": "owner" + } + } + ``` + which gives + ```json + { + "tree": { + "root": { + "name": "folder:1#owner", + "leaf": { + "users": { + "users": [ + "user:bob" + ] + } + } + } + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiExpandRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiExpandRequest */ Expand(ctx context.Context, storeId string) ApiExpandRequest @@ -454,19 +454,19 @@ type OpenFgaApi interface { GetStoreExecute(r ApiGetStoreRequest) (GetStoreResponse, *http.Response, error) /* - * ListObjects List all objects of the given type that the user has a relation with - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. - You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. - The response will contain the related objects in an array in the "objects" field of the response and they will be strings in the object format `:` (e.g. "document:roadmap"). - The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. - The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiListObjectsRequest + * ListObjects List all objects of the given type that the user has a relation with + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. + You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. + The response will contain the related objects in an array in the "objects" field of the response and they will be strings in the object format `:` (e.g. "document:roadmap"). + The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. + The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiListObjectsRequest */ ListObjects(ctx context.Context, storeId string) ApiListObjectsRequest @@ -477,12 +477,12 @@ type OpenFgaApi interface { ListObjectsExecute(r ApiListObjectsRequest) (ListObjectsResponse, *http.Response, error) /* - * ListStores 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. + * ListStores 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. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return ApiListStoresRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return ApiListStoresRequest */ ListStores(ctx context.Context) ApiListStoresRequest @@ -493,20 +493,20 @@ type OpenFgaApi interface { ListStoresExecute(r ApiListStoresRequest) (ListStoresResponse, *http.Response, error) /* - * ListUsers List the users matching the provided filter who have a certain relation to a particular type. - * The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. - To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). - An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. - You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. - You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. - The response will contain the related users in an array in the "users" field of the response. These results may include specific objects, usersets - or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects - of that type have a relation to the object; it is possible that negations exist and checks should still be queried - on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. - The returned users will not be sorted, and therefore two identical calls may yield different sets of users. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiListUsersRequest + * ListUsers List the users matching the provided filter who have a certain relation to a particular type. + * The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. + To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). + An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. + You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. + You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. + The response will contain the related users in an array in the "users" field of the response. These results may include specific objects, usersets + or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects + of that type have a relation to the object; it is possible that negations exist and checks should still be queried + on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. + The returned users will not be sorted, and therefore two identical calls may yield different sets of users. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiListUsersRequest */ ListUsers(ctx context.Context, storeId string) ApiListUsersRequest @@ -517,109 +517,109 @@ type OpenFgaApi interface { ListUsersExecute(r ApiListUsersRequest) (ListUsersResponse, *http.Response, error) /* - * Read Get tuples from the store that matches a query, without following userset rewrite rules - * The Read API will return the tuples for a certain store that match a query filter specified in the body of the request. - The API doesn't guarantee order by any field. - It is different from the `/stores/{store_id}/expand` API in that it only returns relationship tuples that are stored in the system and satisfy the query. - In the body: - 1. `tuple_key` is optional. If not specified, it will return all tuples in the store. - 2. `tuple_key.object` is mandatory if `tuple_key` is specified. It can be a full object (e.g., `type:object_id`) or type only (e.g., `type:`). - 3. `tuple_key.user` is mandatory if tuple_key is specified in the case the `tuple_key.object` is a type only. If tuple_key.user is specified, it needs to be a full object (e.g., `type:user_id`). - ## Examples - ### Query for all objects in a type definition - To query for all objects that `user:bob` has `reader` relationship in the `document` type definition, call read API with body of - ```json - { - "tuple_key": { - "user": "user:bob", - "relation": "reader", - "object": "document:" - } - } - ``` - The API will return tuples and a continuation token, something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `user:bob` has a `reader` relationship with 1 document `document:2021-budget`. Note that this API, unlike the List Objects API, does not evaluate the tuples in the store. - The continuation token will be empty if there are no more tuples to query. - ### Query for all stored relationship tuples that have a particular relation and object - To query for all users that have `reader` relationship with `document:2021-budget`, call read API with body of - ```json - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader" - } - } - ``` - The API will return something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `document:2021-budget` has 1 `reader` (`user:bob`). Note that, even if the model said that all `writers` are also `readers`, the API will not return writers such as `user:anne` because it only returns tuples and does not evaluate them. - ### Query for all users with all relationships for a particular document - To query for all users that have any relationship with `document:2021-budget`, call read API with body of - ```json - { - "tuple_key": { - "object": "document:2021-budget" - } - } - ``` - The API will return something like - ```json - { - "tuples": [ - { - "key": { - "user": "user:anne", - "relation": "writer", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-05T13:42:12.356Z" - }, - { - "key": { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - This means that `document:2021-budget` has 1 `reader` (`user:bob`) and 1 `writer` (`user:anne`). + * Read Get tuples from the store that matches a query, without following userset rewrite rules + * The Read API will return the tuples for a certain store that match a query filter specified in the body of the request. + The API doesn't guarantee order by any field. + It is different from the `/stores/{store_id}/expand` API in that it only returns relationship tuples that are stored in the system and satisfy the query. + In the body: + 1. `tuple_key` is optional. If not specified, it will return all tuples in the store. + 2. `tuple_key.object` is mandatory if `tuple_key` is specified. It can be a full object (e.g., `type:object_id`) or type only (e.g., `type:`). + 3. `tuple_key.user` is mandatory if tuple_key is specified in the case the `tuple_key.object` is a type only. If tuple_key.user is specified, it needs to be a full object (e.g., `type:user_id`). + ## Examples + ### Query for all objects in a type definition + To query for all objects that `user:bob` has `reader` relationship in the `document` type definition, call read API with body of + ```json + { + "tuple_key": { + "user": "user:bob", + "relation": "reader", + "object": "document:" + } + } + ``` + The API will return tuples and a continuation token, something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `user:bob` has a `reader` relationship with 1 document `document:2021-budget`. Note that this API, unlike the List Objects API, does not evaluate the tuples in the store. + The continuation token will be empty if there are no more tuples to query. + ### Query for all stored relationship tuples that have a particular relation and object + To query for all users that have `reader` relationship with `document:2021-budget`, call read API with body of + ```json + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader" + } + } + ``` + The API will return something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `document:2021-budget` has 1 `reader` (`user:bob`). Note that, even if the model said that all `writers` are also `readers`, the API will not return writers such as `user:anne` because it only returns tuples and does not evaluate them. + ### Query for all users with all relationships for a particular document + To query for all users that have any relationship with `document:2021-budget`, call read API with body of + ```json + { + "tuple_key": { + "object": "document:2021-budget" + } + } + ``` + The API will return something like + ```json + { + "tuples": [ + { + "key": { + "user": "user:anne", + "relation": "writer", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-05T13:42:12.356Z" + }, + { + "key": { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + This means that `document:2021-budget` has 1 `reader` (`user:bob`) and 1 `writer` (`user:anne`). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadRequest */ Read(ctx context.Context, storeId string) ApiReadRequest @@ -646,52 +646,52 @@ type OpenFgaApi interface { ReadAssertionsExecute(r ApiReadAssertionsRequest) (ReadAssertionsResponse, *http.Response, error) /* - * ReadAuthorizationModel Return a particular version of an authorization model - * The ReadAuthorizationModel API returns an authorization model by its identifier. - The response will return the authorization model for the particular version. - - ## Example - To retrieve the authorization model with ID `01G5JAVJ41T49E9TT3SKVS7X1J` for the store, call the GET authorization-models by ID API with `01G5JAVJ41T49E9TT3SKVS7X1J` as the `id` path parameter. The API will return: - ```json - { - "authorization_model":{ - "id":"01G5JAVJ41T49E9TT3SKVS7X1J", - "type_definitions":[ - { - "type":"user" - }, - { - "type":"document", - "relations":{ - "reader":{ - "union":{ - "child":[ - { - "this":{} - }, - { - "computedUserset":{ - "object":"", - "relation":"writer" - } - } - ] - } - }, - "writer":{ - "this":{} - } - } - } - ] - } - } - ``` - In the above example, there are 2 types (`user` and `document`). The `document` type has 2 relations (`writer` and `reader`). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @param id - * @return ApiReadAuthorizationModelRequest + * ReadAuthorizationModel Return a particular version of an authorization model + * The ReadAuthorizationModel API returns an authorization model by its identifier. + The response will return the authorization model for the particular version. + + ## Example + To retrieve the authorization model with ID `01G5JAVJ41T49E9TT3SKVS7X1J` for the store, call the GET authorization-models by ID API with `01G5JAVJ41T49E9TT3SKVS7X1J` as the `id` path parameter. The API will return: + ```json + { + "authorization_model":{ + "id":"01G5JAVJ41T49E9TT3SKVS7X1J", + "type_definitions":[ + { + "type":"user" + }, + { + "type":"document", + "relations":{ + "reader":{ + "union":{ + "child":[ + { + "this":{} + }, + { + "computedUserset":{ + "object":"", + "relation":"writer" + } + } + ] + } + }, + "writer":{ + "this":{} + } + } + } + ] + } + } + ``` + In the above example, there are 2 types (`user` and `document`). The `document` type has 2 relations (`writer` and `reader`). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @param id + * @return ApiReadAuthorizationModelRequest */ ReadAuthorizationModel(ctx context.Context, storeId string, id string) ApiReadAuthorizationModelRequest @@ -702,47 +702,47 @@ type OpenFgaApi interface { ReadAuthorizationModelExecute(r ApiReadAuthorizationModelRequest) (ReadAuthorizationModelResponse, *http.Response, error) /* - * ReadAuthorizationModels Return all the authorization models for a particular store - * The ReadAuthorizationModels API will return all the authorization models for a certain store. - OpenFGA's response will contain an array of all authorization models, sorted in descending order of creation. - - ## Example - Assume that a store's authorization model has been configured twice. To get all the authorization models that have been created in this store, call GET authorization-models. The API will return a response that looks like: - ```json - { - "authorization_models": [ - { - "id": "01G50QVV17PECNVAHX1GG4Y5NC", - "type_definitions": [...] - }, - { - "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", - "type_definitions": [...] - }, - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" - } - ``` - If there are no more authorization models available, the `continuation_token` field will be empty - ```json - { - "authorization_models": [ - { - "id": "01G50QVV17PECNVAHX1GG4Y5NC", - "type_definitions": [...] - }, - { - "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", - "type_definitions": [...] - }, - ], - "continuation_token": "" - } - ``` + * ReadAuthorizationModels Return all the authorization models for a particular store + * The ReadAuthorizationModels API will return all the authorization models for a certain store. + OpenFGA's response will contain an array of all authorization models, sorted in descending order of creation. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadAuthorizationModelsRequest + ## Example + Assume that a store's authorization model has been configured twice. To get all the authorization models that have been created in this store, call GET authorization-models. The API will return a response that looks like: + ```json + { + "authorization_models": [ + { + "id": "01G50QVV17PECNVAHX1GG4Y5NC", + "type_definitions": [...] + }, + { + "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", + "type_definitions": [...] + }, + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" + } + ``` + If there are no more authorization models available, the `continuation_token` field will be empty + ```json + { + "authorization_models": [ + { + "id": "01G50QVV17PECNVAHX1GG4Y5NC", + "type_definitions": [...] + }, + { + "id": "01G4ZW8F4A07AKQ8RHSVG9RW04", + "type_definitions": [...] + }, + ], + "continuation_token": "" + } + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadAuthorizationModelsRequest */ ReadAuthorizationModels(ctx context.Context, storeId string) ApiReadAuthorizationModelsRequest @@ -753,15 +753,15 @@ type OpenFgaApi interface { ReadAuthorizationModelsExecute(r ApiReadAuthorizationModelsRequest) (ReadAuthorizationModelsResponse, *http.Response, error) /* - * ReadChanges Return a list of all the tuple changes - * The ReadChanges API will return a paginated list of tuple changes (additions and deletions) that occurred in a given store, sorted by ascending time. The response will include a continuation token that is used to get the next set of changes. If there are no changes after the provided continuation token, the same token will be returned in order for it to be used when new changes are recorded. If the store never had any tuples added or removed, this token will be empty. - You can use the `type` parameter to only get the list of tuple changes that affect objects of that type. - When reading a write tuple change, if it was conditioned, the condition will be returned. - When reading a delete tuple change, the condition will NOT be returned regardless of whether it was originally conditioned or not. - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiReadChangesRequest + * ReadChanges Return a list of all the tuple changes + * The ReadChanges API will return a paginated list of tuple changes (additions and deletions) that occurred in a given store, sorted by ascending time. The response will include a continuation token that is used to get the next set of changes. If there are no changes after the provided continuation token, the same token will be returned in order for it to be used when new changes are recorded. If the store never had any tuples added or removed, this token will be empty. + You can use the `type` parameter to only get the list of tuple changes that affect objects of that type. + When reading a write tuple change, if it was conditioned, the condition will be returned. + When reading a delete tuple change, the condition will NOT be returned regardless of whether it was originally conditioned or not. + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiReadChangesRequest */ ReadChanges(ctx context.Context, storeId string) ApiReadChangesRequest @@ -772,14 +772,14 @@ type OpenFgaApi interface { ReadChangesExecute(r ApiReadChangesRequest) (ReadChangesResponse, *http.Response, error) /* - * StreamedListObjects Stream all objects of the given type that the user has a relation with - * The Streamed ListObjects API is very similar to the the ListObjects API, with two differences: - 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected. - 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE. - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiStreamedListObjectsRequest + * StreamedListObjects Stream all objects of the given type that the user has a relation with + * The Streamed ListObjects API is very similar to the the ListObjects API, with two differences: + 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected. + 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE. + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiStreamedListObjectsRequest */ StreamedListObjects(ctx context.Context, storeId string) ApiStreamedListObjectsRequest @@ -790,53 +790,53 @@ type OpenFgaApi interface { StreamedListObjectsExecute(r ApiStreamedListObjectsRequest) (StreamResultOfStreamedListObjectsResponse, *http.Response, error) /* - * Write Add or delete tuples from the store - * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. - In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. - The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. - To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. - To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. - If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. - The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. - An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. - ## Example - ### Adding relationships - To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following - ```json - { - "writes": { - "tuple_keys": [ - { - "user": "user:anne", - "relation": "writer", - "object": "document:2021-budget" - } - ], - "on_duplicate": "ignore" - }, - "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" - } - ``` - ### Removing relationships - To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following - ```json - { - "deletes": { - "tuple_keys": [ - { - "user": "user:bob", - "relation": "reader", - "object": "document:2021-budget" - } - ], - "on_missing": "ignore" - } - } - ``` + * Write Add or delete tuples from the store + * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. + In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. + The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. + To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. + If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. + The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. + An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. + ## Example + ### Adding relationships + To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following + ```json + { + "writes": { + "tuple_keys": [ + { + "user": "user:anne", + "relation": "writer", + "object": "document:2021-budget" + } + ], + "on_duplicate": "ignore" + }, + "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" + } + ``` + ### Removing relationships + To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following + ```json + { + "deletes": { + "tuple_keys": [ + { + "user": "user:bob", + "relation": "reader", + "object": "document:2021-budget" + } + ], + "on_missing": "ignore" + } + } + ``` - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiWriteRequest + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiWriteRequest */ Write(ctx context.Context, storeId string) ApiWriteRequest @@ -862,53 +862,53 @@ type OpenFgaApi interface { WriteAssertionsExecute(r ApiWriteAssertionsRequest) (*http.Response, error) /* - * WriteAuthorizationModel Create a new authorization model - * The WriteAuthorizationModel API will add a new authorization model to a store. - Each item in the `type_definitions` array is a type definition as specified in the field `type_definition`. - The response will return the authorization model's ID in the `id` field. - - ## Example - To add an authorization model with `user` and `document` type definitions, call POST authorization-models API with the body: - ```json - { - "type_definitions":[ - { - "type":"user" - }, - { - "type":"document", - "relations":{ - "reader":{ - "union":{ - "child":[ - { - "this":{} - }, - { - "computedUserset":{ - "object":"", - "relation":"writer" - } - } - ] - } - }, - "writer":{ - "this":{} - } - } - } - ] - } - ``` - OpenFGA's response will include the version id for this authorization model, which will look like - ``` - {"authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC"} - ``` - - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param storeId - * @return ApiWriteAuthorizationModelRequest + * WriteAuthorizationModel Create a new authorization model + * The WriteAuthorizationModel API will add a new authorization model to a store. + Each item in the `type_definitions` array is a type definition as specified in the field `type_definition`. + The response will return the authorization model's ID in the `id` field. + + ## Example + To add an authorization model with `user` and `document` type definitions, call POST authorization-models API with the body: + ```json + { + "type_definitions":[ + { + "type":"user" + }, + { + "type":"document", + "relations":{ + "reader":{ + "union":{ + "child":[ + { + "this":{} + }, + { + "computedUserset":{ + "object":"", + "relation":"writer" + } + } + ] + } + }, + "writer":{ + "this":{} + } + } + } + ] + } + ``` + OpenFGA's response will include the version id for this authorization model, which will look like + ``` + {"authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC"} + ``` + + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param storeId + * @return ApiWriteAuthorizationModelRequest */ WriteAuthorizationModel(ctx context.Context, storeId string) ApiWriteAuthorizationModelRequest diff --git a/example/example1/go.mod b/example/example1/go.mod index b3b3460..5fd14ac 100644 --- a/example/example1/go.mod +++ b/example/example1/go.mod @@ -17,7 +17,6 @@ require ( go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.18.0 // indirect ) diff --git a/example/opentelemetry/go.mod b/example/opentelemetry/go.mod index a63e60a..0e6f144 100644 --- a/example/opentelemetry/go.mod +++ b/example/opentelemetry/go.mod @@ -27,16 +27,6 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/protobuf v1.36.8 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/example/streamed_list_objects/go.mod b/example/streamed_list_objects/go.mod index fd762bf..b5e7e84 100644 --- a/example/streamed_list_objects/go.mod +++ b/example/streamed_list_objects/go.mod @@ -26,17 +26,6 @@ require ( go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect golang.org/x/net v0.47.0 // indirect From 9b7706fc88e3ed772ab21eece599de606f481d52 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 20 Nov 2025 15:44:59 +0530 Subject: [PATCH 5/5] feat: address copilot/coderabbit comments --- streaming.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/streaming.go b/streaming.go index 478cc73..6736e7f 100644 --- a/streaming.go +++ b/streaming.go @@ -230,9 +230,12 @@ func executeStreamingRequest[TReq any, TRes any]( } httpResponse, err := client.callAPI(req) - if err != nil || httpResponse == nil { + if err != nil { return nil, err } + if httpResponse == nil { + return nil, errors.New("nil HTTP response from API client") + } if httpResponse.StatusCode >= http.StatusMultipleChoices { responseBody, readErr := io.ReadAll(httpResponse.Body)