1313)
1414from src .adapters .crud_store .exceptions import ItemDoesNotExist
1515from src .api .middleware_utils import get_request_headers_to_forward , verify_auth_gateway
16+ from src .api .schemas .authorization_types import AgentexResource
1617from src .config .dependencies import (
1718 DHttpxClient ,
1819 resolve_environment_variable_dependency ,
2728 DAgentAPIKeyRepository ,
2829)
2930from src .domain .repositories .agent_repository import DAgentRepository
31+ from src .domain .services .authorization_service import DAuthorizationService
32+ from src .utils .feature_flags import DFeatureFlagProvider , FeatureFlagName
3033from src .utils .ids import orm_id
3134from src .utils .logging import make_logger
3235
@@ -39,10 +42,14 @@ def __init__(
3942 agent_api_key_repository : DAgentAPIKeyRepository ,
4043 agent_repository : DAgentRepository ,
4144 client : DHttpxClient ,
45+ authorization_service : DAuthorizationService ,
46+ feature_flags : DFeatureFlagProvider ,
4247 ):
4348 self .agent_api_key_repo = agent_api_key_repository
4449 self .agent_repo = agent_repository
4550 self .client = client
51+ self .authorization_service = authorization_service
52+ self .feature_flags = feature_flags
4653 self .auth_gateway_enabled = bool (
4754 resolve_environment_variable_dependency (EnvVarKeys .AGENTEX_AUTH_URL )
4855 )
@@ -76,24 +83,115 @@ async def create(
7683 agent_id : str ,
7784 api_key_type : AgentAPIKeyType ,
7885 api_key : str ,
86+ account_id : str | None = None ,
7987 ) -> AgentAPIKeyEntity :
8088 agent = await self .get_agent (agent_id = agent_id )
8189 if not agent :
8290 raise HTTPException (
8391 status_code = 404 ,
8492 detail = f"Agent ID { agent_id } not found." ,
8593 )
94+
95+ principal_context = self .authorization_service .principal_context
96+ creator_user_id = getattr (principal_context , "user_id" , None )
97+ creator_service_account_id = getattr (
98+ principal_context , "service_account_id" , None
99+ )
100+
101+ api_key_id = orm_id ()
102+ zedtoken : str | None = None
103+
104+ if self .feature_flags .is_enabled (
105+ FeatureFlagName .FGAC_AGENT_API_KEYS_DUAL_WRITE , account_id
106+ ):
107+ zedtoken = await self ._register_api_key_in_spark_authz (
108+ api_key_id = api_key_id ,
109+ agent_id = agent .id ,
110+ account_id = account_id ,
111+ creator_user_id = creator_user_id ,
112+ creator_service_account_id = creator_service_account_id ,
113+ )
114+
86115 # TODO: encrypt API key before storing it
87116 # Initialize a new agent api_key
88117 agent_api_key = AgentAPIKeyEntity (
89- id = orm_id () ,
118+ id = api_key_id ,
90119 name = name ,
91120 agent_id = agent .id ,
92121 api_key_type = api_key_type ,
93122 api_key = api_key ,
123+ creator_user_id = creator_user_id ,
124+ creator_service_account_id = creator_service_account_id ,
125+ spark_authz_zedtoken = zedtoken ,
94126 )
95127 return await self .agent_api_key_repo .create (item = agent_api_key )
96128
129+ async def _register_api_key_in_spark_authz (
130+ self ,
131+ * ,
132+ api_key_id : str ,
133+ agent_id : str ,
134+ account_id : str | None ,
135+ creator_user_id : str | None ,
136+ creator_service_account_id : str | None ,
137+ ) -> str | None :
138+ """Register a new agent_api_key in Spark AuthZ with creator as owner.
139+
140+ Called BEFORE the Postgres write — a failure raises and prevents the
141+ row from being persisted, so there is no compensating action to take.
142+ Mirrors the dual-write pattern used for tasks (AGX1-274).
143+
144+ The current ``Provider.spark`` adapter returns ``{}`` from ``grant``;
145+ no ZedToken is surfaced today, so we always return ``None`` for the
146+ new-write-isolation column. A follow-up will plumb the token through
147+ once the adapter exposes it.
148+
149+ Note: the ``agent_api_key`` SpiceDB schema has a ``parent_agent``
150+ relation that read/delete permissions cascade through. The current
151+ ``AuthorizationGateway.grant`` signature does not accept a parent
152+ relation — the agentex-auth adapter is expected to set
153+ ``parent_agent`` based on the resource shape. This is the same
154+ gap Asher's task PR has and is tracked as a follow-up.
155+ """
156+ if creator_user_id is None and creator_service_account_id is None :
157+ logger .warning (
158+ "Skipping Spark AuthZ api_key registration: no creator resolvable" ,
159+ extra = {
160+ "api_key_id" : api_key_id ,
161+ "agent_id" : agent_id ,
162+ "account_id" : account_id ,
163+ },
164+ )
165+ return None
166+ await self .authorization_service .grant (
167+ resource = AgentexResource .api_key (api_key_id ),
168+ )
169+ return None
170+
171+ async def _deregister_api_key_from_spark_authz (
172+ self , * , api_key_id : str , account_id : str | None
173+ ) -> None :
174+ """Best-effort revocation of an api_key's Spark AuthZ tuples on delete.
175+
176+ Only invoked when the FGAC_AGENT_API_KEYS_DUAL_WRITE flag is enabled
177+ for the caller's account. Failures are logged but do not block the
178+ delete.
179+ """
180+ if not self .feature_flags .is_enabled (
181+ FeatureFlagName .FGAC_AGENT_API_KEYS_DUAL_WRITE , account_id
182+ ):
183+ return
184+ try :
185+ await self .authorization_service .revoke (
186+ resource = AgentexResource .api_key (api_key_id ),
187+ )
188+ except Exception :
189+ logger .warning (
190+ "Spark AuthZ revoke failed for agent_api_key" ,
191+ extra = {"api_key_id" : api_key_id , "account_id" : account_id },
192+ exc_info = True ,
193+ )
194+
97195 async def get (self , id : str ) -> AgentAPIKeyEntity :
98196 return await self .agent_api_key_repo .get (id = id )
99197
@@ -123,22 +221,47 @@ async def get_external_by_agent_id_and_key(
123221 agent_id = agent_id , api_key = api_key
124222 )
125223
126- async def delete (self , id : str ) -> None :
127- return await self .agent_api_key_repo .delete (id = id )
224+ async def delete (self , id : str , account_id : str | None = None ) -> None :
225+ await self .agent_api_key_repo .delete (id = id )
226+ await self ._deregister_api_key_from_spark_authz (
227+ api_key_id = id , account_id = account_id
228+ )
128229
129230 async def delete_by_agent_id_and_key_name (
130- self , agent_id : str , key_name : str , api_key_type : AgentAPIKeyType
231+ self ,
232+ agent_id : str ,
233+ key_name : str ,
234+ api_key_type : AgentAPIKeyType ,
235+ account_id : str | None = None ,
131236 ) -> None :
132- return await self .agent_api_key_repo .delete_by_agent_id_and_key_name (
237+ existing = await self .agent_api_key_repo .get_by_agent_id_and_name (
238+ agent_id = agent_id , name = key_name , api_key_type = api_key_type
239+ )
240+ await self .agent_api_key_repo .delete_by_agent_id_and_key_name (
133241 agent_id = agent_id , key_name = key_name , api_key_type = api_key_type
134242 )
243+ if existing is not None :
244+ await self ._deregister_api_key_from_spark_authz (
245+ api_key_id = existing .id , account_id = account_id
246+ )
135247
136248 async def delete_by_agent_name_and_key_name (
137- self , agent_name : str , key_name : str , api_key_type : AgentAPIKeyType
249+ self ,
250+ agent_name : str ,
251+ key_name : str ,
252+ api_key_type : AgentAPIKeyType ,
253+ account_id : str | None = None ,
138254 ) -> None :
139- return await self .agent_api_key_repo .delete_by_agent_name_and_key_name (
255+ existing = await self .agent_api_key_repo .get_by_agent_name_and_key_name (
256+ agent_name , key_name , api_key_type
257+ )
258+ await self .agent_api_key_repo .delete_by_agent_name_and_key_name (
140259 agent_name = agent_name , key_name = key_name , api_key_type = api_key_type
141260 )
261+ if existing is not None :
262+ await self ._deregister_api_key_from_spark_authz (
263+ api_key_id = existing .id , account_id = account_id
264+ )
142265
143266 async def list (
144267 self , agent_id : str , limit : int , page_number : int
0 commit comments