@@ -299,99 +299,124 @@ async def create_token(
299299 tags : Optional [List [str ]] = None ,
300300 team_id : Optional [str ] = None ,
301301 ) -> tuple [EmailApiToken , str ]:
302- """Create a new API token for user or team.
302+ """
303+ Create a new API token with team-level scoping and additional configurations.
304+
305+ This method generates a JWT-based API token with team-level scoping and optional security configurations,
306+ such as expiration, permissions, IP restrictions, and usage limits. The token is associated with a user
307+ and a specific team, ensuring access control and multi-tenancy support.
308+
309+ The function will:
310+ - Validate the existence of the user.
311+ - Ensure the user is an active member of the specified team.
312+ - Verify that the token name is unique for the user+team combination.
313+ - Generate a JWT with the specified scoping parameters (e.g., permissions, IP, etc.).
314+ - Store the token in the database with the relevant details and return the token and raw JWT string.
303315
304316 Args:
305- user_email: Owner's email address
306- name: Human-readable token name
307- description: Optional token description
308- scope: Token scoping configuration
309- expires_in_days: Optional expiry in days
310- tags: Optional organizational tags
311- team_id: Optional team ID for team-scoped tokens
317+ user_email (str): The email address of the user requesting the token.
318+ name (str): A unique, human-readable name for the token (must be unique per user+team).
319+ description (Optional[str]): A description for the token (default is None).
320+ scope (Optional[TokenScope]): The scoping configuration for the token, including permissions,
321+ server ID, IP restrictions, etc. (default is None).
322+ expires_in_days (Optional[int]): The expiration time in days for the token (None means no expiration).
323+ tags (Optional[List[str]]): A list of organizational tags for the token (default is an empty list).
324+ team_id (Optional[str]): The team ID to which the token should be scoped. This is required for team-level scoping.
312325
313326 Returns:
314- Tuple of (EmailApiToken, raw_token_string)
327+ tuple[EmailApiToken, str]: A tuple where the first element is the `EmailApiToken` database record and
328+ the second element is the raw JWT token string. The `EmailApiToken` contains the database record with the
329+ token details.
315330
316331 Raises:
317- ValueError: If user not found, token name exists, or team access denied
332+ ValueError: If any of the following validation checks fail:
333+ - The `user_email` does not correspond to an existing user.
334+ - The `team_id` is missing or the user is not an active member of the specified team.
335+ - A token with the same name already exists for the given user and team.
336+ - Invalid token configuration (e.g., invalid expiration date).
318337
319338 Examples:
320339 >>> # This method requires database operations, shown for reference
321340 >>> service = TokenCatalogService(None) # Would use real DB session
322341 >>> # token, raw_token = await service.create_token(...)
323342 >>> # Returns (EmailApiToken, raw_token_string) tuple
324343 """
344+ # # Enforce team-level scoping requirement
345+ # if not team_id:
346+ # raise ValueError("team_id is required for token creation. " "Please select a specific team before creating a token. " "You cannot create tokens while viewing 'All Teams'.")
347+
325348 # Validate user exists
326349 user = self .db .execute (select (EmailUser ).where (EmailUser .email == user_email )).scalar_one_or_none ()
327350
328351 if not user :
329352 raise ValueError (f"User not found: { user_email } " )
330353
331- # Validate team access if team_id is provided
354+ # Validate team exists and user is active member
332355 if team_id :
333356 # First-Party
334357 from mcpgateway .db import EmailTeam , EmailTeamMember # pylint: disable=import-outside-toplevel
335358
336359 # Check if team exists
337360 team = self .db .execute (select (EmailTeam ).where (EmailTeam .id == team_id )).scalar_one_or_none ()
361+
338362 if not team :
339363 raise ValueError (f"Team not found: { team_id } " )
340364
341- # Check if user is a team OWNER
365+ # Verify user is an active member of the team
342366 membership = self .db .execute (
343- select (EmailTeamMember ).where (and_ (EmailTeamMember .team_id == team_id , EmailTeamMember .user_email == user_email , EmailTeamMember .role == "owner" , EmailTeamMember . is_active .is_ (True )))
367+ select (EmailTeamMember ).where (and_ (EmailTeamMember .team_id == team_id , EmailTeamMember .user_email == user_email , EmailTeamMember .is_active .is_ (True )))
344368 ).scalar_one_or_none ()
345369
346370 if not membership :
347- raise ValueError (f"Only team owners can create API keys for team { team_id } " )
348-
349- # Check for duplicate token name (scoped by user and team)
350- name_check_conditions = [EmailApiToken .user_email == user_email , EmailApiToken .name == name , EmailApiToken .is_active .is_ (True )]
371+ raise ValueError (f"User { user_email } is not an active member of team { team_id } . " f"Only team members can create tokens for the team." )
351372
352- if team_id :
353- name_check_conditions .append (EmailApiToken .team_id == team_id )
354- else :
355- name_check_conditions .append (EmailApiToken .team_id .is_ (None ))
356-
357- existing_token = self .db .execute (select (EmailApiToken ).where (and_ (* name_check_conditions ))).scalar_one_or_none ()
373+ # Check for duplicate active token name for this user+team
374+ existing_token = self .db .execute (
375+ select (EmailApiToken ).where (and_ (EmailApiToken .user_email == user_email , EmailApiToken .name == name , EmailApiToken .team_id == team_id , EmailApiToken .is_active .is_ (True )))
376+ ).scalar_one_or_none ()
358377
359378 if existing_token :
360- scope_desc = f" for team { team_id } " if team_id else ""
361- raise ValueError (f"Token name '{ name } ' already exists{ scope_desc } " )
379+ raise ValueError (f"Token with name '{ name } ' already exists for user { user_email } in team { team_id } . Please choose a different name." )
362380
363- # Calculate expiry
381+ # CALCULATE EXPIRATION DATE
364382 expires_at = None
365383 if expires_in_days :
366384 expires_at = utc_now () + timedelta (days = expires_in_days )
367385
368- # Generate JWT token with proper claims and user admin status
369- raw_token = await self ._generate_token (user_email = user_email , team_id = team_id , expires_at = expires_at , scope = scope , user = user )
386+ # Generate JWT token with all necessary claims
387+ raw_token = await self ._generate_token (user_email = user_email , team_id = team_id , expires_at = expires_at , scope = scope , user = user ) # Pass user object to include admin status
388+
389+ # Hash token for secure storage
370390 token_hash = self ._hash_token (raw_token )
371391
372- # Create token record
392+ # Create database record
373393 api_token = EmailApiToken (
394+ id = str (uuid .uuid4 ()),
374395 user_email = user_email ,
396+ team_id = team_id , # Store team association
375397 name = name ,
376- token_hash = token_hash ,
377398 description = description ,
399+ token_hash = token_hash , # Store hash, not raw token
378400 expires_at = expires_at ,
379401 tags = tags or [],
380- team_id = team_id ,
402+ # Store scoping information
381403 server_id = scope .server_id if scope else None ,
382404 resource_scopes = scope .permissions if scope else [],
383405 ip_restrictions = scope .ip_restrictions if scope else [],
384406 time_restrictions = scope .time_restrictions if scope else {},
385407 usage_limits = scope .usage_limits if scope else {},
408+ # Token status
409+ is_active = True ,
410+ created_at = utc_now (),
411+ last_used = None ,
386412 )
387413
388414 self .db .add (api_token )
389415 self .db .commit ()
390416 self .db .refresh (api_token )
391417
392- scope_desc = f" for team { team_id } " if team_id else ""
393- logger .info (f"Created API token '{ name } ' for user { user_email } { scope_desc } " )
394-
418+ token_type = f"team-scoped (team: { team_id } )" if team_id else "public-only"
419+ logger .info (f"Created { token_type } API token '{ name } ' for user { user_email } . " f"Token ID: { api_token .id } , Expires: { expires_at or 'Never' } " )
395420 return api_token , raw_token
396421
397422 async def list_user_tokens (self , user_email : str , include_inactive : bool = False , limit : int = 100 , offset : int = 0 ) -> List [EmailApiToken ]:
0 commit comments