Skip to content

Commit 66debae

Browse files
committed
refactor: openfga sync client refactoring
1 parent 4d0e3b4 commit 66debae

6 files changed

Lines changed: 146 additions & 59 deletions

File tree

backend/app/api/routes/items.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from app.api.deps import CurrentUser, SessionDep
88
from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message
9-
from app.openfga.fgaMiddleware import create_fga_tuple, delete_fga_tuple
9+
from app.openfga.fgaMiddleware import check_user_has_relation, create_fga_tuple, delete_fga_tuple, initialize_fga_client
1010
from app.openfga.fgaMiddleware import check_user_has_permission
1111
from openfga_sdk.client.models.tuple import ClientTuple
1212

@@ -15,11 +15,13 @@
1515

1616
@router.get("/", response_model=ItemsPublic)
1717
def read_items(
18-
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
18+
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100, get_completed: bool = False
1919
) -> Any:
2020
"""
2121
Retrieve items.
2222
"""
23+
fga_client = initialize_fga_client()
24+
2325
if current_user.is_superuser:
2426
count_statement = select(func.count()).select_from(Item)
2527
count = session.exec(count_statement).one()
@@ -39,8 +41,16 @@ def read_items(
3941
.limit(limit)
4042
)
4143
items = session.exec(statement).all()
44+
45+
if get_completed:
46+
item_ids = check_user_has_relation(fga_client, relation="completed", user=f"{current_user.email}")
47+
if len(item_ids) == 0:
48+
return ItemsPublic(data=[], count=0)
49+
else:
50+
# get those items with those ids
51+
items = [session.get(Item, id) for id in item_ids]
4252

43-
return ItemsPublic(data=items, count=count) # type: ignore
53+
return ItemsPublic(data=[ItemPublic.model_validate(item) for item in items], count=count)
4454

4555

4656
@router.get("/{id}", response_model=ItemPublic)
@@ -57,17 +67,22 @@ def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) ->
5767

5868

5969
@router.post("/", response_model=ItemPublic)
60-
async def create_item(
70+
def create_item(
6171
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
6272
) -> Any:
6373
"""
6474
Create new item.
6575
"""
76+
6677
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
6778
session.add(item)
6879
session.commit()
6980
session.refresh(item)
70-
await create_fga_tuple([ClientTuple(user=f"user:{current_user.id}", relation="owner", object=f"item:{item.id}")])
81+
82+
fga_client = initialize_fga_client()
83+
create_fga_tuple(fga_client, [
84+
ClientTuple(user=f"user:{current_user.email}", relation="owner", object=f"item:{item.id}")
85+
])
7186
return item
7287

7388

@@ -79,11 +94,19 @@ async def update_item(
7994
id: uuid.UUID,
8095
item_in: ItemUpdate,
8196
) -> Any:
82-
if not await check_user_has_permission(ClientTuple(user=f"user:{current_user.id}", relation="update", object=f"item:{id}")):
83-
raise HTTPException(status_code=400, detail="Not enough permissions")
97+
8498
"""
8599
Update an item.
86100
"""
101+
# check if the user has the permission to update the item
102+
# if not, raise an error
103+
# if the user has the permission, update the item
104+
# return the updated item
105+
106+
fga_client = initialize_fga_client()
107+
if not check_user_has_permission(fga_client, ClientTuple(user=f"user:{current_user.id}", relation="update", object=f"item:{id}")):
108+
raise HTTPException(status_code=400, detail="Not enough permissions")
109+
87110
item = session.get(Item, id)
88111
if not item:
89112
raise HTTPException(status_code=404, detail="Item not found")
@@ -103,7 +126,9 @@ async def can_update_item(
103126
current_user: CurrentUser,
104127
id: uuid.UUID,
105128
):
106-
has_permission = await check_user_has_permission(ClientTuple(user=f"user:{current_user.id}", relation="update", object=f"item:{id}"))
129+
fga_client = initialize_fga_client()
130+
has_permission = check_user_has_permission(fga_client, ClientTuple(user=f"user:{current_user.id}", relation="update", object=f"item:{id}"))
131+
107132
return has_permission
108133

109134
@router.delete("/{id}")
@@ -120,5 +145,8 @@ async def delete_item(
120145
raise HTTPException(status_code=400, detail="Not enough permissions")
121146
session.delete(item)
122147
session.commit()
123-
await delete_fga_tuple([ClientTuple(user=f"user:{current_user.id}", relation="owner", object=f"item:{id}")])
148+
149+
fga_client = initialize_fga_client()
150+
delete_fga_tuple(fga_client, [ClientTuple(user=f"user:{current_user.id}", relation="owner", object=f"item:{id}")])
151+
124152
return Message(message="Item deleted successfully")

backend/app/core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ def _enforce_non_default_secrets(self) -> Self:
117117
self._check_default_secret(
118118
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
119119
)
120+
self._check_default_secret("OPENFGA_API_URL", self.OPENFGA_API_URL)
121+
self._check_default_secret("OPENFGA_STORE_ID", self.OPENFGA_STORE_ID)
122+
self._check_default_secret("OPENFGA_AUTHORIZATION_MODEL_ID", self.OPENFGA_AUTHORIZATION_MODEL_ID)
120123

121124
return self
122125

backend/app/main.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,10 @@ def custom_generate_unique_id(route: APIRoute) -> str:
1717
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
1818
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)
1919

20-
@asynccontextmanager
21-
async def lifespan(app: FastAPI):
22-
await initialize_fga_client()
23-
yield
24-
2520
app = FastAPI(
2621
title=settings.PROJECT_NAME,
2722
openapi_url=f"{settings.API_V1_STR}/openapi.json",
2823
generate_unique_id_function=custom_generate_unique_id,
29-
lifespan=lifespan,
3024
)
3125

3226
# Set all CORS enabled origins
Lines changed: 102 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,72 @@
1-
import asyncio
1+
import logging
2+
from typing import Any
23
from app.core.config import settings
3-
from fastapi import logger
4-
from openfga_sdk.client import ClientConfiguration, OpenFgaClient, ClientCheckRequest
5-
from openfga_sdk.client.models import ClientBatchCheckRequest, ClientWriteRequest, ClientBatchCheckItem
4+
from openfga_sdk import (
5+
Condition,
6+
ConditionParamTypeRef,
7+
CreateStoreRequest,
8+
Metadata,
9+
ObjectRelation,
10+
OpenFgaClient,
11+
ReadRequestTupleKey,
12+
RelationMetadata,
13+
RelationReference,
14+
RelationshipCondition,
15+
TypeDefinition,
16+
Userset,
17+
Usersets,
18+
UserTypeFilter,
19+
WriteAuthorizationModelRequest,
20+
)
21+
from openfga_sdk.client.models import (
22+
ClientAssertion,
23+
ClientBatchCheckItem,
24+
ClientBatchCheckRequest,
25+
ClientCheckRequest,
26+
ClientListObjectsRequest,
27+
ClientListRelationsRequest,
28+
ClientReadChangesRequest,
29+
ClientTuple,
30+
ClientWriteRequest,
31+
WriteTransactionOpts,
32+
)
33+
from openfga_sdk.client.models.list_users_request import ClientListUsersRequest
34+
from openfga_sdk.credentials import CredentialConfiguration, Credentials
35+
from openfga_sdk.models.fga_object import FgaObject
636
from openfga_sdk.client.models.tuple import ClientTuple
37+
from openfga_sdk.client import ClientConfiguration
38+
from openfga_sdk.sync import OpenFgaClient
739

8-
# We default fga_client to None until it is initialized
9-
fga_client = None
1040

11-
async def initialize_fga_client():
12-
# This function is called to initialize our FGA Client instance if it is not already available
13-
print("Initializing OpenFGA Client SDK")
14-
configuration = ClientConfiguration(
15-
api_url = settings.OPENFGA_API_URL,
16-
store_id = settings.OPENFGA_STORE_ID,
17-
authorization_model_id = settings.OPENFGA_AUTHORIZATION_MODEL_ID,
18-
)
41+
import traceback
1942

20-
global fga_client
43+
logging.basicConfig(level=logging.INFO)
44+
logger = logging.getLogger(__name__)
2145

46+
def initialize_fga_client():
47+
configuration = ClientConfiguration(
48+
api_url=settings.OPENFGA_API_URL, # required
49+
store_id=settings.OPENFGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores`
50+
authorization_model_id=settings.OPENFGA_AUTHORIZATION_MODEL_ID, # optional, can be overridden per request
51+
)
2252
# Enter a context with an instance of the OpenFgaClient
23-
async with OpenFgaClient(configuration) as fga_client:
24-
fga_client = OpenFgaClient(configuration)
25-
await fga_client.read_authorization_models() # type: ignore
26-
print("FGA Client initialized.")
27-
return True
53+
with OpenFgaClient(configuration) as fga_client:
54+
return fga_client
2855

29-
async def close_fga_client(fga_client: OpenFgaClient):
30-
await fga_client.close()
56+
def close_fga_client(fga_client: OpenFgaClient):
57+
fga_client.close()
3158

3259
# Check if a user has a permission on an object
33-
async def check_user_has_permission(tuple: ClientTuple) -> bool:
34-
if fga_client is None:
35-
await initialize_fga_client()
60+
def check_user_has_permission(fga_client: OpenFgaClient, tuple: ClientTuple) -> bool:
61+
if fga_client is None: # type: ignore
62+
fga_client = initialize_fga_client()
3663

3764
if fga_client is None:
3865
logger.info("FGA client not initialized") # type: ignore
3966
return False
4067

4168
try:
42-
response = await fga_client.check(
69+
response = fga_client.check(
4370
ClientCheckRequest(
4471
user=tuple.user,
4572
relation=tuple.relation,
@@ -65,16 +92,19 @@ async def check_user_has_permission(tuple: ClientTuple) -> bool:
6592
# "object": "document:123"
6693
# }
6794
# ]
68-
async def check_user_has_permission_batch(tuples: list[ClientBatchCheckItem]) -> bool:
69-
if fga_client is None:
70-
await initialize_fga_client()
95+
def check_user_has_permission_batch(fga_client: OpenFgaClient, tuples: list[ClientBatchCheckItem]) -> bool:
96+
if fga_client is None: # type: ignore
97+
fga_client = initialize_fga_client()
7198

7299
if fga_client is None:
73100
logger.info("FGA client not initialized") # type: ignore
74101
return False
75102

103+
logger.info(f"Check fga_client: {fga_client}")
104+
logger.info(f"Check fga status: {fga_client.get_store_id()}")
105+
76106
try:
77-
response = await fga_client.batch_check(
107+
response = fga_client.batch_check(
78108
ClientBatchCheckRequest(
79109
checks=tuples
80110
)
@@ -93,37 +123,37 @@ async def check_user_has_permission_batch(tuples: list[ClientBatchCheckItem]) ->
93123
# "object": "document:123"
94124
# }
95125
# ]
96-
async def create_fga_tuple(tuples: list[ClientTuple]) -> bool:
97-
if fga_client is None:
98-
await initialize_fga_client()
126+
def create_fga_tuple(fga_client: OpenFgaClient, tuples: list[ClientTuple]) -> bool:
127+
if fga_client is None: # type: ignore
128+
fga_client = initialize_fga_client()
99129

100130
if fga_client is None:
101131
logger.info("FGA client not initialized") # type: ignore
102132
return False
103133

134+
logger.info(f"Creating FGA tuple: {tuples}")
135+
logger.info(f"So is this even working lol: {fga_client.get_store_id()}")
136+
104137
try:
105-
response = await fga_client.write(
106-
ClientWriteRequest(
107-
writes=tuples
108-
)
109-
)
110-
return response.allowed # type: ignore
138+
response = fga_client.write_tuples(tuples)
139+
logger.info(f"Response vars: {vars(response)}")
140+
return True
111141
except Exception as e:
112142
logger.info(f"Error creating FGA tuple: {e}") # type: ignore
113143
return False
114144

115145
# Delete a tuple
116-
async def delete_fga_tuple(tuples: list[ClientTuple]) -> bool:
146+
def delete_fga_tuple(fga_client: OpenFgaClient, tuples: list[ClientTuple]) -> bool:
117147
# This is the opposite of the create_fga_tuple function
118-
if fga_client is None:
119-
await initialize_fga_client()
148+
if fga_client is None: # type: ignore
149+
fga_client = initialize_fga_client()
120150

121151
if fga_client is None:
122152
logger.info("FGA client not initialized") # type: ignore
123153
return False
124154

125155
try:
126-
response = await fga_client.write(
156+
response = fga_client.write(
127157
ClientWriteRequest(
128158
deletes=tuples
129159
)
@@ -132,3 +162,32 @@ async def delete_fga_tuple(tuples: list[ClientTuple]) -> bool:
132162
except Exception as e:
133163
logger.info(f"Error deleting FGA tuple: {e}") # type: ignore
134164
return False
165+
166+
167+
def check_user_has_relation(fga_client: OpenFgaClient, relation: str, user: str) -> list[str]:
168+
169+
if fga_client is None: # type: ignore
170+
fga_client = initialize_fga_client()
171+
172+
if fga_client is None:
173+
logger.info("FGA client not initialized") # type: ignore
174+
return []
175+
176+
body = ClientListObjectsRequest(
177+
user=f"user:{user}",
178+
relation=f"{relation}",
179+
type="item"
180+
)
181+
182+
try:
183+
response = fga_client.list_objects(body)
184+
# response.objects = ["document:otherdoc", "document:planning"]
185+
# we need to return a list of objects
186+
if len(response.objects) > 0:
187+
return [item.split(":")[1] for item in response.objects] # type: ignore
188+
else:
189+
return []
190+
except Exception as e:
191+
logger.info(f"Error checking user relation: {e}")
192+
logger.info(traceback.format_exc())
193+
return []

docker-compose.fga.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
openfga:
3-
image: openfga/openfga:v1.8.0
3+
image: openfga/openfga:latest
44
container_name: openfga
55
restart: always
66
tty: true

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ services:
7474
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
7575
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
7676
- SENTRY_DSN=${SENTRY_DSN}
77+
- OPENFGA_API_URL=${OPENFGA_API_URL}
78+
- OPENFGA_STORE_ID=${OPENFGA_STORE_ID}
79+
- OPENFGA_AUTHORIZATION_MODEL_ID=${OPENFGA_AUTHORIZATION_MODEL_ID}
7780

7881
backend:
7982
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'

0 commit comments

Comments
 (0)