Skip to content

Conversation

@rootflo-hardik
Copy link
Contributor

@rootflo-hardik rootflo-hardik commented Jan 6, 2026

  • role can be owner or app_admin
  • only owner can create, modify delete users
  • check if the user with role app_admin has app access in floware_proxy_service
  • removed super_admin env variable and also from readme

Summary by CodeRabbit

  • New Features

    • Role-based access (OWNER, APP_ADMIN) across UI and API; role added to users
    • App-user management endpoints, client API, service, and Manage App Access dialog in the user list
    • Flow proxy enforces per-user app access checks
  • Bug Fixes

    • Replaced SUPER_ADMIN/email checks with OWNER-role authorization; added self-deletion and last-owner safeguards
    • Edit user flow no longer allows changing email
  • Chores

    • DB migrations for role column and app-user association; removed SUPER_ADMIN_EMAIL from docs/config

✏️ Tip: You can customize this high-level summary in your review settings.

- role can be owner or app_admin
- only owner can create, modify delete users
- check if the user with role app_admin has app access in floware_proxy_service
- removed super_admin env variable and also from readme
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Replace email-based super-admin with role-based RBAC (OWNER, APP_ADMIN); add AppUser association, services, controllers, client APIs and UI for app-level access; update DB models, alembic migrations, DI wiring, and remove SUPER_ADMIN_EMAIL from docs/config.

Changes

Cohort / File(s) Summary
Configuration & Compose
\DOCKER_SETUP.md`, `docker-compose.sample.yml`, `.../floconsole/config.ini``
Remove SUPER_ADMIN_EMAIL documentation and environment/config entry.
RBAC Constants
\wavefront/server/apps/floconsole/floconsole/constants/user.py``
Add UserRole enum with OWNER and APP_ADMIN.
DB Models & Exports
\.../db/models/init.py`, `.../db/models/app.py`, `.../db/models/user.py`, `.../db/models/app_user.py``
Add AppUser join model and exports; add role column to User; add app_users relationships on User and App.
Migrations & Alembic
\.../db/alembic/env.py`, `.../versions/2026_01_06_1644-.py`, `.../versions/2026_01_06_1646-.py``
Register new models; migration adds non-nullable role (seeds existing users to owner) and migration creates app_user table with FKs, composite PK, and indexes.
Services
\.../services/user_service.py`, `.../services/app_user_service.py`, `.../services/floware_proxy_service.py``
Add owner-focused APIs (count_owners, is_owner, check_user_has_app_access); new AppUserService (grant/revoke/list); FlowareProxyService enforces user→app access checks and validates UUIDs.
Controllers & API
\.../controllers/user_controller.py`, `.../controllers/app_controller.py`, `.../controllers/app_user_controller.py``
Enforce OWNER authorization for create/update/delete flows; create app-user endpoints (grant/revoke/list users/list apps); request payloads adjusted (CreateUserRequest.role added, update removes email).
DI & Server Registration
\.../di/application_container.py`, `.../server.py``
Add app_user_repository and app_user_service providers; wire user_service into FlowareProxyService; register app_user_router.
Client API & UI
\wavefront/client/src/api/{app-user-service.ts,index.ts,user-service.ts}`, `wavefront/client/src/types/user.ts`, `wavefront/client/src/pages/apps/users/*``
New AppUserService and wiring; createUser accepts optional role; updateUser removes email; IUser adds role; UI: CreateUserDialog adds role select, EditUserDialog removes email, new ManageAppAccessDialog and users list integration.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant UserRepo as User Repository
    participant AppService as App Service
    participant DB as Database
    Note over Controller,UserRepo: App create/update/delete flows require OWNER

    Client->>Controller: POST /v1/apps (create_app) with session.user_id
    Controller->>UserRepo: get_user_by_id(session.user_id)
    UserRepo->>DB: SELECT user WHERE id=...
    DB-->>UserRepo: user row (role)
    UserRepo-->>Controller: User(role)
    alt role == OWNER
        Controller->>AppService: create_app(payload)
        AppService->>DB: INSERT app
        DB-->>AppService: created app
        AppService-->>Controller: app
        Controller-->>Client: 201 Created
    else
        Controller-->>Client: 403 Forbidden
    end
Loading
sequenceDiagram
    participant Client
    participant Proxy as FlowareProxyService
    participant UserService
    participant UserRepo as User Repository
    participant AppUserRepo as AppUser Repository
    participant DB as Database
    Note over Proxy,UserService: Proxy enforces app access via UserService

    Client->>Proxy: proxy_request(app_id) (session with user_id)
    Proxy->>UserService: check_user_has_app_access(user_id, app_id)
    UserService->>UserRepo: get_user_by_id(user_id)
    UserRepo->>DB: SELECT user WHERE id=...
    DB-->>UserRepo: user row
    UserRepo-->>UserService: User(role)
    alt role == OWNER
        UserService-->>Proxy: true
    else
        UserService->>AppUserRepo: query app_user WHERE user_id & app_id
        AppUserRepo->>DB: SELECT ...
        DB-->>AppUserRepo: rows
        AppUserRepo-->>UserService: association exists?
        alt association exists
            UserService-->>Proxy: true
        else
            UserService-->>Proxy: false
        end
    end
    alt access granted
        Proxy->>Client: forward/200
    else
        Proxy-->>Client: 403 Forbidden
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • vizsatiz

Poem

🐰 I hopped through configs, docs, and rows,

Owners now lead where app-access goes.
AppUser links make permissions sing,
Roles in place — watch the changes spring.
Carrots for reviewers, hop and ring! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.15% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly summarizes the main changes: user roles, migrations, and the app_user junction table are all core components of this changeset.

✏️ Tip: You can configure your own custom Pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI Agents
In
@wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py:
- Around line 79-126: The revoke_app_access handler lacks existence checks for
the app and target user; update revoke_app_access to mirror grant_app_access by
first verifying the target user and the app exist before calling
app_user_service.revoke_app_access: use user_repository.find_one(id=user_id) to
confirm the user, and likewise use the app_repository (or
app_service.find_one/find_by_id) to confirm the app exists, return a 404
JSONResponse via response_formatter.buildErrorResponse if either is missing,
then proceed to call app_user_service.revoke_app_access and keep the existing
logging and error handling.
- Around line 21-76: The grant_app_access handler currently skips validating
that app_id exists and returns raw exception text to clients; inject and use the
app repository or service (e.g., add app_repository: SQLAlchemyRepository[App]
or app_service: AppService as a dependency) in grant_app_access, call
app_repository.find_one(id=app_id, deleted=False) (or equivalent
app_service.get_app) and return a 404 with
response_formatter.buildErrorResponse('App not found') if absent before calling
app_user_service.grant_app_access; also stop exposing str(e) to clients — change
the exception handlers in grant_app_access (and apply the same pattern to the
other error handlers in this file) to logger.exception(...) or logger.error(...,
exc_info=True) to log full details, but return a generic
response_formatter.buildErrorResponse('Failed to grant app access') (no
exception text) with 500 to the client.
- Around line 157-184: The controller currently calls
user_repository.find(deleted=False) then filters in Python (variables
user_repository.find, user_ids, filtered_users, users_data); instead, call the
repository with an ID-IN filter so the DB does the work (e.g.
user_repository.find(deleted=False, id__in=user_ids) or the equivalent list/IN
filter supported by SQLAlchemyRepository.find) and then map the returned users
to users_data—replace the full-table fetch and post-filtering with a single find
call using the user_ids list.

In @wavefront/server/apps/floconsole/floconsole/db/alembic/env.py:
- Line 11: The Alembic env.py is missing the AppUser model import and
registration for autogeneration; add an import for AppUser (e.g., from
floconsole.db.models.app_user import AppUser or from floconsole.db.models.app
import AppUser if colocated) and ensure AppUser is included in the
models/target_metadata collection used by Alembic (the same list or MetaData
passed to autogenerate alongside App) so the app_user join table is tracked for
migrations.

In
@wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1644-0644a81ee4e1_add_role_to_user.py:
- Around line 27-29: The migration currently limits the role update to
non-deleted users; change the SQL executed by conn (obtained via op.get_bind())
so it updates every row in the "user" table (remove the WHERE deleted = false
clause) — i.e. execute sa.text('UPDATE "user" SET role = \'owner\'') so deleted
users get the same role applied for historical consistency.

In
@wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py:
- Around line 70-82: Convert and validate session.user_id and app_id with
explicit UUID conversion inside minimal try/except blocks and raise a clear,
specific exception (e.g., BadRequest for invalid UUIDs) instead of letting
UUID(...) raise uncaught ValueError; remove the redundant try/except that simply
re-raises around user_service.check_user_has_app_access, call
check_user_has_app_access(user_id=UUID(...), app_id=UUID(...)) directly, and if
it returns False raise a well-constructed exception with a clear message (do not
catch and rethrow ValueError). Ensure the error messages are created when
raising the exceptions (not inline in a no-op except) and reference
session.user_id, user_id, app_id, and user_service.check_user_has_app_access to
locate the changes.
🧹 Nitpick comments (6)
wavefront/server/apps/floconsole/floconsole/db/models/__init__.py (1)

8-8: Consider sorting __all__ alphabetically.

Static analysis suggests sorting the __all__ list for consistency. While not required, this improves maintainability.

🔎 Proposed fix for alphabetical sorting
-__all__ = ['User', 'Session', 'App', 'AppUser']
+__all__ = ['App', 'AppUser', 'Session', 'User']
wavefront/server/apps/floconsole/floconsole/services/user_service.py (2)

38-43: Optimize count query to avoid loading all records.

The current implementation fetches all owner users and counts them in memory. For better performance, use a database-level COUNT query.

🔎 Proposed optimization
 async def count_owners(self) -> int:
     """Count users with owner role that are not deleted"""
-    users = await self.user_repository.find(
-        role=UserRole.OWNER.value, deleted=False
-    )
-    return len(users)
+    # Use a COUNT query instead of loading all records
+    query = """
+        SELECT COUNT(*) as count
+        FROM "user"
+        WHERE role = :role AND deleted = false
+    """
+    result = await self.user_repository.execute_query(
+        query, params={'role': UserRole.OWNER.value}
+    )
+    return result[0]['count'] if result else 0

50-75: Consider using ORM instead of raw SQL for consistency.

The raw SQL query works correctly with parameterized values, but using the ORM would be more consistent with the rest of the codebase. Additionally, querying the app_user table through the user_repository feels conceptually misplaced.

🔎 Alternative ORM-based approach

If you have access to the AppUser model through relationships, you could refactor to:

async def check_user_has_app_access(self, user_id: UUID, app_id: UUID) -> bool:
    """
    Check if user has access to app.
    Owners have access to all apps.
    App admins need explicit app_user entry.
    """
    user = await self.user_repository.find_one(id=user_id, deleted=False)

    if not user:
        return False

    # Owners have access to all apps
    if user.role == UserRole.OWNER.value:
        return True

    # For app_admins, check app_user relationship via ORM
    # This assumes app_users relationship is properly loaded
    return any(au.app_id == app_id for au in user.app_users)

Note: This assumes the relationship is eager-loaded or you fetch it explicitly. The raw SQL approach may actually be more efficient if you don't need the full objects.

wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (1)

24-27: Consider returning deletion status.

The method always returns True regardless of whether any records were actually deleted. Consider returning the count of deleted records or a boolean indicating whether the access existed.

🔎 Potential improvement
 async def revoke_app_access(self, user_id: UUID, app_id: UUID) -> bool:
     """Revoke user access to app"""
-    await self.app_user_repository.delete_all(user_id=user_id, app_id=app_id)
-    return True
+    deleted_count = await self.app_user_repository.delete_all(user_id=user_id, app_id=app_id)
+    return deleted_count > 0
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (2)

187-233: Validate target user existence for consistency.

The endpoint retrieves apps for user_id without verifying the user exists. For consistency with the other endpoints and better error messages, consider adding user validation.

🔎 Recommended validation check
     if not current_user or current_user.role != UserRole.OWNER.value:
         return JSONResponse(
             status_code=status.HTTP_403_FORBIDDEN,
             content=response_formatter.buildErrorResponse(
                 'Only owners can view user apps'
             ),
         )
 
+    # Verify target user exists
+    target_user = await user_repository.find_one(id=user_id, deleted=False)
+    if not target_user:
+        return JSONResponse(
+            status_code=status.HTTP_404_NOT_FOUND,
+            content=response_formatter.buildErrorResponse('User not found'),
+        )
+
     # Get user apps
     try:

69-76: Consider catching more specific exceptions.

All endpoints catch broad Exception types. While this prevents unhandled errors, catching more specific exceptions (e.g., DatabaseError, IntegrityError) would enable better error handling and more informative responses to clients.

Example pattern:

try:
    await app_user_service.grant_app_access(user_id, app_id)
    # ...
except IntegrityError:
    return JSONResponse(
        status_code=status.HTTP_409_CONFLICT,
        content=response_formatter.buildErrorResponse('Access already granted')
    )
except DatabaseError as e:
    logger.error(f'Database error: {str(e)}')
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content=response_formatter.buildErrorResponse('Failed to grant app access')
    )

Also applies to: 119-126, 177-184, 226-233

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42c9543 and c37ed1d.

📒 Files selected for processing (19)
  • DOCKER_SETUP.md
  • docker-compose.sample.yml
  • wavefront/server/apps/floconsole/floconsole/config.ini
  • wavefront/server/apps/floconsole/floconsole/constants/user.py
  • wavefront/server/apps/floconsole/floconsole/controllers/app_controller.py
  • wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py
  • wavefront/server/apps/floconsole/floconsole/controllers/user_controller.py
  • wavefront/server/apps/floconsole/floconsole/db/alembic/env.py
  • wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1644-0644a81ee4e1_add_role_to_user.py
  • wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1646-8d947e3f8ec6_create_app_user_table.py
  • wavefront/server/apps/floconsole/floconsole/db/models/__init__.py
  • wavefront/server/apps/floconsole/floconsole/db/models/app.py
  • wavefront/server/apps/floconsole/floconsole/db/models/app_user.py
  • wavefront/server/apps/floconsole/floconsole/db/models/user.py
  • wavefront/server/apps/floconsole/floconsole/di/application_container.py
  • wavefront/server/apps/floconsole/floconsole/server.py
  • wavefront/server/apps/floconsole/floconsole/services/app_user_service.py
  • wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py
  • wavefront/server/apps/floconsole/floconsole/services/user_service.py
💤 Files with no reviewable changes (3)
  • DOCKER_SETUP.md
  • docker-compose.sample.yml
  • wavefront/server/apps/floconsole/floconsole/config.ini
🧰 Additional context used
🧬 Code graph analysis (11)
wavefront/server/apps/floconsole/floconsole/services/user_service.py (1)
wavefront/server/apps/floconsole/floconsole/constants/user.py (1)
  • UserRole (4-6)
wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (2)
wavefront/server/apps/floconsole/floconsole/db/models/app_user.py (1)
  • AppUser (11-23)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (2)
  • grant_app_access (23-76)
  • revoke_app_access (81-126)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (1)
wavefront/server/apps/floconsole/floconsole/services/user_service.py (2)
  • UserService (10-75)
  • check_user_has_app_access (50-75)
wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1644-0644a81ee4e1_add_role_to_user.py (1)
wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1646-8d947e3f8ec6_create_app_user_table.py (2)
  • upgrade (20-32)
  • downgrade (35-38)
wavefront/server/apps/floconsole/floconsole/db/models/__init__.py (1)
wavefront/server/apps/floconsole/floconsole/db/models/app_user.py (1)
  • AppUser (11-23)
wavefront/server/apps/floconsole/floconsole/db/models/user.py (2)
wavefront/server/apps/floconsole/floconsole/constants/user.py (1)
  • UserRole (4-6)
wavefront/server/apps/floconsole/floconsole/db/models/app.py (1)
  • to_dict (37-49)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (4)
wavefront/server/apps/floconsole/floconsole/constants/user.py (1)
  • UserRole (4-6)
wavefront/server/apps/floconsole/floconsole/db/models/user.py (2)
  • User (14-49)
  • to_dict (42-49)
wavefront/server/apps/floconsole/floconsole/di/application_container.py (1)
  • ApplicationContainer (21-100)
wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (5)
  • AppUserService (8-35)
  • grant_app_access (14-22)
  • revoke_app_access (24-27)
  • get_app_users (33-35)
  • get_user_apps (29-31)
wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1646-8d947e3f8ec6_create_app_user_table.py (1)
wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1644-0644a81ee4e1_add_role_to_user.py (2)
  • upgrade (20-35)
  • downgrade (38-40)
wavefront/server/apps/floconsole/floconsole/di/application_container.py (3)
wavefront/server/apps/floconsole/floconsole/db/models/app_user.py (1)
  • AppUser (11-23)
wavefront/server/apps/floconsole/floconsole/services/user_service.py (1)
  • UserService (10-75)
wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (1)
  • AppUserService (8-35)
wavefront/server/apps/floconsole/floconsole/controllers/user_controller.py (4)
wavefront/server/apps/floconsole/floconsole/constants/user.py (1)
  • UserRole (4-6)
wavefront/server/apps/floconsole/floconsole/db/models/user.py (1)
  • User (14-49)
wavefront/server/apps/floconsole/floconsole/di/application_container.py (1)
  • ApplicationContainer (21-100)
wavefront/server/apps/floconsole/floconsole/services/user_service.py (2)
  • is_owner (45-48)
  • count_owners (38-43)
wavefront/server/apps/floconsole/floconsole/db/alembic/env.py (1)
wavefront/server/apps/floconsole/floconsole/db/models/app.py (1)
  • App (13-49)
🪛 Ruff (0.14.10)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py

31-31: Possible hardcoded password assigned to function default: "token_prefix"

(S107)


80-80: Abstract raise to an inner function

(TRY301)


80-80: Avoid specifying long messages outside the exception class

(TRY003)


81-82: Remove exception handler; error is immediately re-raised

(TRY203)


82-82: Use raise without specifying exception name

Remove exception name

(TRY201)

wavefront/server/apps/floconsole/floconsole/db/models/__init__.py

8-8: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py

27-29: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


30-32: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


33-35: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


69-69: Do not catch blind exception: Exception

(BLE001)


70-70: Use explicit conversion flag

Replace with conversion flag

(RUF010)


74-74: Use explicit conversion flag

Replace with conversion flag

(RUF010)


85-87: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


88-90: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


91-93: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


119-119: Do not catch blind exception: Exception

(BLE001)


120-120: Use explicit conversion flag

Replace with conversion flag

(RUF010)


124-124: Use explicit conversion flag

Replace with conversion flag

(RUF010)


134-136: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


137-139: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


140-142: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


177-177: Do not catch blind exception: Exception

(BLE001)


178-178: Use explicit conversion flag

Replace with conversion flag

(RUF010)


182-182: Use explicit conversion flag

Replace with conversion flag

(RUF010)


192-194: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


195-197: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


198-200: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


226-226: Do not catch blind exception: Exception

(BLE001)


227-227: Use explicit conversion flag

Replace with conversion flag

(RUF010)


231-231: Use explicit conversion flag

Replace with conversion flag

(RUF010)

wavefront/server/apps/floconsole/floconsole/controllers/app_controller.py

96-98: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


99-99: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


100-100: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


101-103: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


237-239: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


240-240: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


241-243: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

wavefront/server/apps/floconsole/floconsole/controllers/user_controller.py

47-49: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


50-52: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (20)
wavefront/server/apps/floconsole/floconsole/db/models/app.py (1)

8-8: LGTM! Proper relationship setup for association table.

The app_users relationship is correctly configured with cascade='all, delete-orphan', ensuring that when an App is deleted, all associated AppUser records are automatically removed. The bidirectional setup with back_populates='app' maintains referential integrity.

Also applies to: 32-35

wavefront/server/apps/floconsole/floconsole/server.py (1)

20-20: LGTM! Router registration follows existing patterns.

The app_user_router is correctly imported and registered with the same /floconsole prefix as other routers, maintaining consistency across the API structure.

Also applies to: 124-124

wavefront/server/apps/floconsole/floconsole/db/models/__init__.py (1)

6-6: LGTM! AppUser correctly exported from models package.

The AppUser import and export are properly configured, making the model available through the package's public API.

Also applies to: 8-8

wavefront/server/apps/floconsole/floconsole/constants/user.py (1)

1-6: LGTM! UserRole enum correctly defines access control roles.

The UserRole enum properly defines the two roles (OWNER and APP_ADMIN) using the string enum pattern, which facilitates database storage and comparison operations. Role-based authorization is consistently enforced throughout the codebase:

  • OWNER-only operations are properly protected across user management (create, modify, delete), app management, and app-user associations
  • APP_ADMIN access is correctly validated in the floware_proxy_service via check_user_has_app_access, which grants unrestricted app access to OWNER roles while requiring explicit app-user entries for APP_ADMIN users
wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1646-8d947e3f8ec6_create_app_user_table.py (1)

20-38: LGTM!

The migration correctly creates the app_user junction table with:

  • Composite primary key for uniqueness
  • CASCADE deletes to maintain referential integrity
  • Individual indexes for efficient queries by user or app
wavefront/server/apps/floconsole/floconsole/services/user_service.py (1)

45-48: LGTM!

The method correctly checks user existence and role with proper null handling.

wavefront/server/apps/floconsole/floconsole/db/models/user.py (3)

9-9: LGTM!

The role field is properly configured with the correct default value and nullable constraint, matching the migration strategy.

Also applies to: 25-25


37-40: LGTM!

The app_users relationship is correctly configured with appropriate cascade behavior for the junction table pattern.


48-48: LGTM!

The role field is correctly included in the serialization output.

wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (1)

2-2: LGTM!

The UserService dependency is properly integrated using dependency injection.

Also applies to: 13-13, 28-28, 36-36

wavefront/server/apps/floconsole/floconsole/di/application_container.py (1)

13-13: LGTM! Clean dependency injection wiring.

The new AppUser components are properly integrated into the DI container following the established pattern. The repository and service are correctly typed and wired, and the user_service dependency is appropriately added to floware_proxy_service.

Also applies to: 18-18, 52-54, 64-67, 95-95

wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (2)

29-35: LGTM! Simple and clear accessor methods.

The get_user_apps and get_app_users methods provide straightforward access to app-user relationships.


14-22: No changes needed.

The upsert call is correct for the AppUser model. The model only contains two columns—user_id and app_id, both of which are primary keys—making them sufficient to create a record. When the record already exists, the upsert performs an empty update (no additional fields to set), which is the intended behavior. The call successfully ensures the user-app association record exists without requiring additional update values.

Likely an incorrect or invalid review comment.

wavefront/server/apps/floconsole/floconsole/db/models/app_user.py (1)

11-23: LGTM! Well-designed junction table model.

The AppUser model correctly implements a many-to-many association table with:

  • Composite primary key on (user_id, app_id)
  • Cascade delete for referential integrity
  • Proper bidirectional relationships
wavefront/server/apps/floconsole/floconsole/controllers/app_controller.py (3)

95-118: LGTM! Proper owner authorization for app creation.

The authorization check correctly restricts app creation to users with the OWNER role. The implementation properly retrieves the current user and validates their role before proceeding.


236-266: LGTM! Comprehensive authorization and validation for app updates.

The update endpoint properly:

  • Restricts updates to OWNER role users
  • Filters out None values from the request
  • Validates that at least one field is provided for update

This prevents both unauthorized access and empty update requests.


318-319: LGTM! Authorization updated for RBAC.

The comment correctly reflects the transition from super-admin to OWNER-based authorization.

wavefront/server/apps/floconsole/floconsole/controllers/user_controller.py (3)

33-33: LGTM! Proper role-based user creation.

The implementation correctly:

  • Defaults new users to APP_ADMIN role
  • Restricts user creation to OWNER role users
  • Passes the role through to user creation

This ensures proper access control for user management.

Also applies to: 54-70, 90-90


169-172: LGTM! Flexible yet secure update authorization.

The authorization logic correctly allows users to update their own profiles while giving owners the ability to update any user. The role-based check properly uses UserRole.OWNER.value.


247-279: LGTM! Excellent multi-layered deletion protection.

The delete logic implements critical security controls:

  1. Self-deletion prevention (Lines 247-255): Prevents users from accidentally locking themselves out
  2. Owner protection (Lines 257-267): Prevents non-owners from deleting owners, blocking privilege escalation
  3. Last owner constraint (Lines 270-279): Ensures the system always has at least one owner

These safeguards are essential for maintaining system integrity and access.

Comment on lines 21 to 76
@app_user_router.post('/apps/{app_id}/users/{user_id}')
@inject
async def grant_app_access(
app_id: UUID,
user_id: UUID,
request: Request,
response_formatter: ResponseFormatter = Depends(
Provide[CommonContainer.response_formatter]
),
app_user_service: AppUserService = Depends(
Provide[ApplicationContainer.app_user_service]
),
user_repository: SQLAlchemyRepository[User] = Depends(
Provide[ApplicationContainer.user_repository]
),
):
"""Grant user access to app (owners only)"""
# Check authorization
_, current_user_id, _ = get_current_user(request)
current_user = await user_repository.find_one(id=current_user_id)

if not current_user or current_user.role != UserRole.OWNER.value:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content=response_formatter.buildErrorResponse(
'Only owners can grant app access'
),
)

# Verify target user exists
target_user = await user_repository.find_one(id=user_id, deleted=False)
if not target_user:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content=response_formatter.buildErrorResponse('User not found'),
)

# Grant access
try:
await app_user_service.grant_app_access(user_id, app_id)
logger.info(f'Granted user {user_id} access to app {app_id}')

return JSONResponse(
status_code=status.HTTP_201_CREATED,
content=response_formatter.buildSuccessResponse(
{'message': 'App access granted successfully'}
),
)
except Exception as e:
logger.error(f'Failed to grant app access: {str(e)}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=response_formatter.buildErrorResponse(
f'Failed to grant app access: {str(e)}'
),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate app existence before granting access.

The endpoint grants access to app_id without verifying the app exists. This could allow references to non-existent apps in the junction table, leading to orphaned records or downstream failures.

🔎 Recommended fix to add app validation

Inject app_service or app_repository and verify the app exists:

     app_user_service: AppUserService = Depends(
         Provide[ApplicationContainer.app_user_service]
     ),
+    app_repository: SQLAlchemyRepository[App] = Depends(
+        Provide[ApplicationContainer.app_repository]
+    ),
     user_repository: SQLAlchemyRepository[User] = Depends(
         Provide[ApplicationContainer.user_repository]
     ),
 ):
     """Grant user access to app (owners only)"""
     # Check authorization
     _, current_user_id, _ = get_current_user(request)
     current_user = await user_repository.find_one(id=current_user_id)
 
     if not current_user or current_user.role != UserRole.OWNER.value:
         return JSONResponse(
             status_code=status.HTTP_403_FORBIDDEN,
             content=response_formatter.buildErrorResponse(
                 'Only owners can grant app access'
             ),
         )
 
+    # Verify app exists
+    app = await app_repository.find_one(id=app_id, deleted=False)
+    if not app:
+        return JSONResponse(
+            status_code=status.HTTP_404_NOT_FOUND,
+            content=response_formatter.buildErrorResponse('App not found'),
+        )
+
     # Verify target user exists
     target_user = await user_repository.find_one(id=user_id, deleted=False)

Avoid exposing internal error details to clients.

Lines 74 and other error responses include str(e), which may leak sensitive implementation details (stack traces, database errors, etc.) to API consumers.

🔎 Recommended fix to sanitize error responses

Log the full exception internally but return a generic message to the client:

     except Exception as e:
         logger.error(f'Failed to grant app access: {str(e)}')
         return JSONResponse(
             status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
             content=response_formatter.buildErrorResponse(
-                f'Failed to grant app access: {str(e)}'
+                'Failed to grant app access'
             ),
         )

Apply the same fix to all error handlers in this file (lines 124, 182, 231).

🧰 Tools
🪛 Ruff (0.14.10)

27-29: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


30-32: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


33-35: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


69-69: Do not catch blind exception: Exception

(BLE001)


70-70: Use explicit conversion flag

Replace with conversion flag

(RUF010)


74-74: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In
@wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py
around lines 21 - 76, The grant_app_access handler currently skips validating
that app_id exists and returns raw exception text to clients; inject and use the
app repository or service (e.g., add app_repository: SQLAlchemyRepository[App]
or app_service: AppService as a dependency) in grant_app_access, call
app_repository.find_one(id=app_id, deleted=False) (or equivalent
app_service.get_app) and return a 404 with
response_formatter.buildErrorResponse('App not found') if absent before calling
app_user_service.grant_app_access; also stop exposing str(e) to clients — change
the exception handlers in grant_app_access (and apply the same pattern to the
other error handlers in this file) to logger.exception(...) or logger.error(...,
exc_info=True) to log full details, but return a generic
response_formatter.buildErrorResponse('Failed to grant app access') (no
exception text) with 500 to the client.

Comment on lines +27 to +29
# Set all existing users to 'owner'
conn = op.get_bind()
conn.execute(sa.text('UPDATE "user" SET role = \'owner\' WHERE deleted = false'))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Deleted users are excluded from the role update.

The UPDATE query only modifies non-deleted users. Deleted users will retain the default 'app_admin' role from the server_default. If historical data consistency is important, consider updating all users regardless of deletion status.

🔎 Proposed fix to update all users
-    # Set all existing users to 'owner'
-    conn = op.get_bind()
-    conn.execute(sa.text('UPDATE "user" SET role = \'owner\' WHERE deleted = false'))
+    # Set all existing users to 'owner' (including deleted for data consistency)
+    conn = op.get_bind()
+    conn.execute(sa.text('UPDATE "user" SET role = \'owner\''))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Set all existing users to 'owner'
conn = op.get_bind()
conn.execute(sa.text('UPDATE "user" SET role = \'owner\' WHERE deleted = false'))
# Set all existing users to 'owner' (including deleted for data consistency)
conn = op.get_bind()
conn.execute(sa.text('UPDATE "user" SET role = \'owner\''))
🤖 Prompt for AI Agents
In
@wavefront/server/apps/floconsole/floconsole/db/alembic/versions/2026_01_06_1644-0644a81ee4e1_add_role_to_user.py
around lines 27 - 29, The migration currently limits the role update to
non-deleted users; change the SQL executed by conn (obtained via op.get_bind())
so it updates every row in the "user" table (remove the WHERE deleted = false
clause) — i.e. execute sa.text('UPDATE "user" SET role = \'owner\'') so deleted
users get the same role applied for historical consistency.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In @wavefront/client/src/pages/apps/users/CreateUserDialog.tsx:
- Around line 148-158: The Select in CreateUserDialog is using
defaultValue={field.value} which makes it uncontrolled; change it to a
controlled component by replacing defaultValue with value={field.value} on the
Select and keep onValueChange={field.onChange} so react-hook-form updates and
resets propagate correctly; remove defaultValue and ensure field.value is passed
as value to Select (and verify field.onChange signature matches Select's
onValueChange).
🧹 Nitpick comments (4)
wavefront/client/src/types/user.ts (1)

6-6: Consider using a union type or enum for role validation.

The role field accepts any string, but the system only supports 'owner' and 'app_admin'. Adding client-side validation would provide better UX by catching invalid values before submission.

🔎 Proposed refinement
+const validRoles = ['owner', 'app_admin'] as const;
+type UserRole = typeof validRoles[number];
+
 export interface IUser {
   id: string;
   email: string;
   first_name: string;
   last_name: string;
-  role: string;
+  role: UserRole;
 }

And for the schema:

 export const createUserSchema = z.object({
   // ... other fields
-  role: z.string().optional(),
+  role: z.enum(['owner', 'app_admin']).optional(),
 });

Also applies to: 20-20

wavefront/client/src/pages/apps/users/ManageAppAccessDialog.tsx (2)

77-85: Sequential API calls may be slow and partial failures leave inconsistent state.

The grant/revoke operations are executed sequentially. If multiple apps need updating, this can be slow. More critically, if an operation fails mid-way, some changes are applied while others are not, but the error handler doesn't indicate which operations succeeded.

🔎 Consider using Promise.allSettled for parallel execution with partial failure reporting
-      // Grant access to new apps
-      const appsToGrant = Array.from(currentAppIds).filter((id) => !previousAppIds.has(id));
-      for (const appId of appsToGrant) {
-        await floConsoleService.appUserService.grantAppAccess(appId, user.id);
-      }
-
-      // Revoke access from removed apps
-      const appsToRevoke = Array.from(previousAppIds).filter((id) => !currentAppIds.has(id));
-      for (const appId of appsToRevoke) {
-        await floConsoleService.appUserService.revokeAppAccess(appId, user.id);
-      }
+      const appsToGrant = Array.from(currentAppIds).filter((id) => !previousAppIds.has(id));
+      const appsToRevoke = Array.from(previousAppIds).filter((id) => !currentAppIds.has(id));
+
+      const results = await Promise.allSettled([
+        ...appsToGrant.map((appId) => floConsoleService.appUserService.grantAppAccess(appId, user.id)),
+        ...appsToRevoke.map((appId) => floConsoleService.appUserService.revokeAppAccess(appId, user.id)),
+      ]);
+
+      const failures = results.filter((r) => r.status === 'rejected');
+      if (failures.length > 0) {
+        notifyError(`${failures.length} operation(s) failed. Please verify app access.`);
+        return;
+      }

30-52: State not reset when dialog closes, potentially showing stale data on reopen.

When the dialog closes (via cancel or clicking outside), userAppIds retains its last value. If the user reopens the dialog for the same user, the stale local state may briefly display before the useEffect fetch completes.

🔎 Reset state when dialog closes
   useEffect(() => {
     const fetchUserApps = async () => {
-      if (!user || !isOpen) return;
+      if (!user || !isOpen) {
+        setUserAppIds([]);
+        return;
+      }

       setLoading(true);
       // ...
     };
     fetchUserApps();
   }, [user, isOpen, notifyError]);
wavefront/client/src/pages/apps/users/index.tsx (1)

147-154: Empty state messaging could be improved for non-owners.

When a non-owner sees no users (e.g., filtered results), the message says "No users match your search" even without an active search query. Also, passing an empty string and no-op function to EmptyStateCard is awkward.

🔎 Proposed improvement
           ) : (
             <EmptyStateCard
               title="No users found"
-              description="No users match your search."
-              actionText=""
-              onActionClick={() => {}}
+              description={searchQuery ? 'No users match your search.' : 'No users available.'}
             />
           )}

This assumes EmptyStateCard supports optional action props. If not, consider updating the component to make actionText and onActionClick optional.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c37ed1d and e32c0dc.

📒 Files selected for processing (8)
  • wavefront/client/src/api/app-user-service.ts
  • wavefront/client/src/api/index.ts
  • wavefront/client/src/api/user-service.ts
  • wavefront/client/src/pages/apps/users/CreateUserDialog.tsx
  • wavefront/client/src/pages/apps/users/EditUserDialog.tsx
  • wavefront/client/src/pages/apps/users/ManageAppAccessDialog.tsx
  • wavefront/client/src/pages/apps/users/index.tsx
  • wavefront/client/src/types/user.ts
💤 Files with no reviewable changes (1)
  • wavefront/client/src/pages/apps/users/EditUserDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
wavefront/client/src/pages/apps/users/CreateUserDialog.tsx (1)
wavefront/client/src/components/ui/select.tsx (5)
  • Select (133-133)
  • SelectTrigger (136-136)
  • SelectValue (135-135)
  • SelectContent (137-137)
  • SelectItem (139-139)
wavefront/client/src/api/index.ts (1)
wavefront/client/src/api/app-user-service.ts (1)
  • AppUserService (5-35)
wavefront/client/src/pages/apps/users/index.tsx (2)
wavefront/client/src/types/user.ts (1)
  • IUser (1-7)
wavefront/client/src/hooks/data/fetch-hooks.ts (2)
  • useGetUsers (414-416)
  • useGetCurrentUser (113-115)
wavefront/client/src/pages/apps/users/ManageAppAccessDialog.tsx (5)
wavefront/client/src/types/user.ts (1)
  • IUser (1-7)
wavefront/client/src/store/notification-store.ts (1)
  • useNotifyStore (32-32)
wavefront/client/src/hooks/data/fetch-hooks.ts (1)
  • useGetAllApps (109-111)
wavefront/client/src/lib/utils.ts (1)
  • extractErrorMessage (21-66)
wavefront/client/src/components/ui/dialog.tsx (6)
  • Dialog (111-111)
  • DialogContent (113-113)
  • DialogHeader (116-116)
  • DialogTitle (119-119)
  • DialogDescription (114-114)
  • DialogFooter (115-115)
wavefront/client/src/api/app-user-service.ts (2)
wavefront/client/src/lib/axios.ts (1)
  • IApiResponse (16-16)
wavefront/client/src/types/user.ts (1)
  • IUser (1-7)
🔇 Additional comments (5)
wavefront/client/src/api/index.ts (1)

6-6: LGTM!

The AppUserService import and getter follow the established pattern used by other services in FloConsoleService.

Also applies to: 43-45

wavefront/client/src/api/user-service.ts (1)

31-39: LGTM!

The createUser payload correctly includes the optional role field, aligning with the updated type definitions and supporting the new RBAC model.

wavefront/client/src/pages/apps/users/index.tsx (1)

34-50: LGTM on the RBAC implementation.

The owner-based permission checks are well-structured:

  • isOwner memoized for efficiency
  • canEditUser allows self-editing while giving owners full access
  • canDeleteUser prevents self-deletion
  • UI elements correctly gated behind isOwner checks
  • Manage App Access appropriately restricted to app_admin users (owners have implicit full access)

Also applies to: 126-130, 185-194

wavefront/client/src/api/app-user-service.ts (1)

5-35: LGTM!

The AppUserService follows the established patterns in the codebase:

  • Consistent constructor pattern with AxiosInstance injection
  • RESTful endpoint design
  • Proper TypeScript typing with IApiResponse generics
  • Clear JSDoc comments documenting owner-only access requirements
wavefront/client/src/pages/apps/users/CreateUserDialog.tsx (1)

40-46: LGTM on form default values.

The default role of 'app_admin' follows the principle of least privilege, ensuring new users don't inadvertently receive owner-level access.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
@wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py:
- Around line 81-88: The handler currently returns exception details to clients
by including str(e) in the JSONResponse; replace those occurrences so the client
gets a generic message (e.g., "Failed to grant app access") and log the full
traceback for debugging using logger.exception(...) instead of
logger.error(...). Specifically, in the except block around the grant logic
replace logger.error(f'Failed to grant app access: {str(e)}') with
logger.exception('Failed to grant app access') and remove str(e) from the
response_formatter.buildErrorResponse call so the JSONResponse contains only the
generic message; apply the same change pattern (use logger.exception and generic
messages) to the other except handlers in this file referenced in the review
(the blocks around lines 150-157, 206-213, 255-262).
- Around line 216-262: The list_user_apps handler lacks existence validation for
the target user_id; after the authorization check (in list_user_apps) call
user_repository.find_one(id=user_id) (similar to
grant_app_access/revoke_app_access) and if it returns None return a 404
JSONResponse using response_formatter.buildErrorResponse with a clear message
like "User not found" before calling app_user_service.get_user_apps; this
ensures consistent behavior and avoids returning an empty list for non-existent
users.
- Around line 160-213: The list_app_users endpoint (list_app_users) must
validate that the app exists before fetching users: add an injected
app_repository (matching ApplicationContainer.app_repository) to the function
signature, call its find_one/find method to check existence for app_id, and if
not found return a 404 JSONResponse using
response_formatter.buildErrorResponse('App not found') so the endpoint behavior
matches grant_app_access/revoke_app_access; keep the existing owner-only check
and only proceed to call app_user_service.get_app_users when the app exists.

In
@wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py:
- Around line 79-85: The UUID(app_id) conversion can raise an unhandled
ValueError; wrap the conversion in a try/except around UUID(app_id) in the
method containing self.user_service.check_user_has_app_access, catch ValueError
and raise a clear, consistent exception (or return the same error handling used
for session.user_id) before calling self.user_service.check_user_has_app_access
so that has_access is only evaluated with a valid UUID; reference the app_id
variable, the UUID(...) call, and the
self.user_service.check_user_has_app_access(...) invocation when making the
change.
🧹 Nitpick comments (2)
wavefront/server/apps/floconsole/floconsole/db/alembic/env.py (1)

32-38: Consider validating required environment variables.

If any of the database environment variables (CONSOLE_DB_USERNAME, CONSOLE_DB_PASSWORD, etc.) are missing, the URL construction will fail with a cryptic error. Adding validation with clear error messages would improve the developer experience during initial setup.

🔎 Suggested validation approach
 # Get database URL from environment variables
 db_user_name = os.getenv('CONSOLE_DB_USERNAME')
 db_password = os.getenv('CONSOLE_DB_PASSWORD')
 db_host = os.getenv('CONSOLE_DB_HOST')
 db_port = os.getenv('CONSOLE_DB_PORT')
 db_name = os.getenv('CONSOLE_DB_NAME')
 
+# Validate required environment variables
+required_vars = {
+    'CONSOLE_DB_USERNAME': db_user_name,
+    'CONSOLE_DB_PASSWORD': db_password,
+    'CONSOLE_DB_HOST': db_host,
+    'CONSOLE_DB_PORT': db_port,
+    'CONSOLE_DB_NAME': db_name,
+}
+missing = [k for k, v in required_vars.items() if v is None]
+if missing:
+    raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
+
 db_url = f'postgresql://{db_user_name}:{db_password}@{db_host}:{db_port}/{db_name}'
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (1)

55-68: Docstring step numbering is outdated.

The docstring mentions step 4 as "Generate T2 service token with app-specific secret," but this doesn't appear to be implemented in the current code. Additionally, the comment on line 106 says "Step 3: Prepare request to floware" but should be Step 4 based on the docstring flow (Step 3 is "Fetch app details").

Consider updating the docstring to reflect the actual implementation.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e32c0dc and e81b898.

📒 Files selected for processing (4)
  • wavefront/client/src/pages/apps/users/CreateUserDialog.tsx
  • wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py
  • wavefront/server/apps/floconsole/floconsole/db/alembic/env.py
  • wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • wavefront/client/src/pages/apps/users/CreateUserDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
wavefront/server/apps/floconsole/floconsole/db/alembic/env.py (2)
wavefront/server/apps/floconsole/floconsole/db/models/app.py (1)
  • App (13-49)
wavefront/server/apps/floconsole/floconsole/db/models/app_user.py (1)
  • AppUser (11-23)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (1)
wavefront/server/apps/floconsole/floconsole/services/user_service.py (1)
  • check_user_has_app_access (50-75)
🪛 Ruff (0.14.10)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py

31-31: Possible hardcoded password assigned to function default: "token_prefix"

(S107)


75-77: Avoid specifying long messages outside the exception class

(TRY003)


85-85: Avoid specifying long messages outside the exception class

(TRY003)

wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py

28-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


31-33: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


34-36: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


37-39: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


81-81: Do not catch blind exception: Exception

(BLE001)


82-82: Use explicit conversion flag

Replace with conversion flag

(RUF010)


86-86: Use explicit conversion flag

Replace with conversion flag

(RUF010)


97-99: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


100-102: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


103-105: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


106-108: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


150-150: Do not catch blind exception: Exception

(BLE001)


151-151: Use explicit conversion flag

Replace with conversion flag

(RUF010)


155-155: Use explicit conversion flag

Replace with conversion flag

(RUF010)


165-167: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


168-170: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


171-173: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


206-206: Do not catch blind exception: Exception

(BLE001)


207-207: Use explicit conversion flag

Replace with conversion flag

(RUF010)


211-211: Use explicit conversion flag

Replace with conversion flag

(RUF010)


221-223: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


224-226: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


227-229: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


255-255: Do not catch blind exception: Exception

(BLE001)


256-256: Use explicit conversion flag

Replace with conversion flag

(RUF010)


260-260: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🔇 Additional comments (4)
wavefront/server/apps/floconsole/floconsole/db/alembic/env.py (1)

11-12: Excellent - previous issue resolved!

The App and AppUser models are now properly imported and registered in the Alembic configuration. This ensures that Alembic's autogenerate will track schema changes for both the app and app_user tables, resolving the critical issue identified in the previous review.

Also applies to: 25-26

wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (3)

1-13: LGTM!

The imports for UUID and UserService are appropriate additions to support the new RBAC functionality.


23-42: LGTM!

The UserService dependency is correctly injected and stored, following the existing pattern for other service dependencies.


69-77: LGTM!

The explicit error handling for user_id UUID parsing addresses the concern from the previous review. The combined catch for ValueError and AttributeError appropriately handles both invalid format and missing attribute cases.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
@wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py:
- Around line 150-157: The exception details are being leaked to API consumers;
change the except block so it logs the full exception (use logger.exception(...)
or logger.error(..., exc_info=True)) but returns a generic error message to the
client (remove f'Failed to revoke app access: {str(e)}' from JSONResponse
content and replace with a non-sensitive message like 'Failed to revoke app
access'), keeping the HTTP_500 status; update the JSONResponse/content call that
uses response_formatter.buildErrorResponse to pass the generic message and keep
detailed error info only in the server log (references: logger.error /
logger.exception, JSONResponse, response_formatter.buildErrorResponse).
- Around line 81-88: The except block in app_user_controller.py currently logs
and returns the raw exception string (str(e)), which can leak sensitive details;
change the logging to use logger.exception(...) to capture full traceback in
logs and replace the client-facing message passed to
response_formatter.buildErrorResponse(...) with a generic message (e.g., "Failed
to grant app access") without including str(e); keep response status_code as
HTTP_500_INTERNAL_SERVER_ERROR and do not expose exception text in the JSON
response.
- Around line 273-280: The except block in app_user_controller.py currently
returns internal exception details to clients; change it to log the full
exception locally (e.g., use logger.exception(...) or logger.error(...,
exc_info=True)) and return a generic error message via
response_formatter.buildErrorResponse like "Failed to list user apps" without
including str(e); keep the status code/status.HTTP_500_INTERNAL_SERVER_ERROR and
remove the exception string from both the JSONResponse content and any
client-facing messages while preserving internal logging.
- Around line 217-224: The except block in app_user_controller.py currently logs
and returns the raw exception string (logger.error and
response_formatter.buildErrorResponse using str(e)), which leaks internal
details to clients; change the code so the logger records the full exception
(use logger.exception or include str(e) in the server log) but the JSONResponse
content returned by response_formatter.buildErrorResponse contains a generic
message like "Failed to list app users" without including exception details;
keep references to logger, JSONResponse, and
response_formatter.buildErrorResponse and update only the response payload to
remove str(e) while preserving detailed logging.
🧹 Nitpick comments (2)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (1)

92-100: Simplify redundant error handling.

Since app_id is already validated and converted to app_uuid at lines 80-83, the "Invalid app_id format" error on line 100 is now dead code—it can never be reached. The try-except block only catches the ValueError it raises itself (line 96), making the logic unnecessarily complex.

Additionally, line 99 uses raise e instead of bare raise (TRY201).

🔎 Proposed simplification
         # Step 3: Fetch app details from database
-        try:
-            app = await self.app_service.get_app_by_id(app_uuid)
-            if not app:
-                raise ValueError(f'App not found for ID: {app_id}')
-        except ValueError as e:
-            if 'App not found' in str(e):
-                raise e
-            raise ValueError(f'Invalid app_id format: {app_id}') from e
+        app = await self.app_service.get_app_by_id(app_uuid)
+        if not app:
+            raise ValueError(f'App not found for ID: {app_id}')
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (1)

263-271: Consider returning full app details for consistency.

This endpoint returns only app_ids as strings, while the corresponding list_app_users endpoint (line 207) returns full user objects via user.to_dict(). This inconsistency in response formats may surprise API consumers.

💡 Optional enhancement for API consistency

If full app details are desired, inject app_repository and fetch app objects:

     app_user_service: AppUserService = Depends(
         Provide[ApplicationContainer.app_user_service]
     ),
+    app_repository: SQLAlchemyRepository[App] = Depends(
+        Provide[ApplicationContainer.app_repository]
+    ),
     user_repository: SQLAlchemyRepository[User] = Depends(
         Provide[ApplicationContainer.user_repository]
     ),
 ):

Then fetch and return full app details:

     # Get user apps
     try:
         user_apps = await app_user_service.get_user_apps(user_id)
-        app_ids = [str(user_app.app_id) for user_app in user_apps]
+        
+        # Fetch full app details
+        if user_apps:
+            app_ids = [user_app.app_id for user_app in user_apps]
+            apps = await app_repository.find(id=app_ids, deleted=False)
+            apps_data = [app.to_dict() for app in apps]
+        else:
+            apps_data = []
 
-        logger.info(f'User {user_id} has access to {len(app_ids)} apps')
+        logger.info(f'User {user_id} has access to {len(apps_data)} apps')
 
         return JSONResponse(
             status_code=status.HTTP_200_OK,
-            content=response_formatter.buildSuccessResponse({'app_ids': app_ids}),
+            content=response_formatter.buildSuccessResponse({'apps': apps_data}),
         )

Alternatively, if returning only IDs is intentional, consider updating list_app_users to match this pattern for consistency.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e81b898 and c227039.

📒 Files selected for processing (2)
  • wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py
  • wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py
🧰 Additional context used
🧬 Code graph analysis (2)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (5)
wavefront/server/apps/floconsole/floconsole/constants/user.py (1)
  • UserRole (4-6)
wavefront/server/apps/floconsole/floconsole/db/models/app.py (2)
  • App (13-49)
  • to_dict (37-49)
wavefront/server/apps/floconsole/floconsole/db/models/user.py (2)
  • User (14-49)
  • to_dict (42-49)
wavefront/server/apps/floconsole/floconsole/di/application_container.py (1)
  • ApplicationContainer (21-100)
wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (5)
  • AppUserService (8-35)
  • grant_app_access (14-22)
  • revoke_app_access (24-27)
  • get_app_users (33-35)
  • get_user_apps (29-31)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (1)
wavefront/server/apps/floconsole/floconsole/services/user_service.py (2)
  • UserService (10-75)
  • check_user_has_app_access (50-75)
🪛 Ruff (0.14.10)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py

28-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


31-33: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


34-36: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


37-39: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


81-81: Do not catch blind exception: Exception

(BLE001)


82-82: Use explicit conversion flag

Replace with conversion flag

(RUF010)


86-86: Use explicit conversion flag

Replace with conversion flag

(RUF010)


97-99: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


100-102: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


103-105: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


106-108: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


150-150: Do not catch blind exception: Exception

(BLE001)


151-151: Use explicit conversion flag

Replace with conversion flag

(RUF010)


155-155: Use explicit conversion flag

Replace with conversion flag

(RUF010)


165-167: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


168-170: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


171-173: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


174-176: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


217-217: Do not catch blind exception: Exception

(BLE001)


218-218: Use explicit conversion flag

Replace with conversion flag

(RUF010)


222-222: Use explicit conversion flag

Replace with conversion flag

(RUF010)


232-234: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


235-237: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


238-240: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


273-273: Do not catch blind exception: Exception

(BLE001)


274-274: Use explicit conversion flag

Replace with conversion flag

(RUF010)


278-278: Use explicit conversion flag

Replace with conversion flag

(RUF010)

wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py

31-31: Possible hardcoded password assigned to function default: "token_prefix"

(S107)


75-77: Avoid specifying long messages outside the exception class

(TRY003)


83-83: Avoid specifying long messages outside the exception class

(TRY003)


90-90: Avoid specifying long messages outside the exception class

(TRY003)


96-96: Abstract raise to an inner function

(TRY301)


96-96: Avoid specifying long messages outside the exception class

(TRY003)


99-99: Use raise without specifying exception name

Remove exception name

(TRY201)


100-100: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (5)
wavefront/server/apps/floconsole/floconsole/services/floware_proxy_service.py (5)

24-42: LGTM!

The constructor properly injects the UserService dependency needed for RBAC checks. The static analysis warning S107 on token_prefix is a false positive—it's a token prefix string, not a credential.


69-77: LGTM!

The explicit error handling for session.user_id UUID parsing properly addresses the previously flagged concern, catching both ValueError (invalid UUID format) and AttributeError (missing attribute).


79-90: LGTM!

The explicit UUID validation for app_id and the RBAC access check are correctly implemented, addressing the previously flagged concern.


136-178: LGTM!

The streaming implementation correctly uses manual context management to keep the connection alive during iteration. The finally block in stream_generator() properly cleans up both the stream context and client when streaming completes or on error.


180-203: LGTM!

The non-streaming path correctly uses the async context manager for automatic cleanup. The 600-second timeout is notably higher than the streaming path (120s), which may be intentional for heavier operations.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (1)

223-274: Consider validating returned apps for consistency.

The endpoint correctly enforces OWNER role and validates the target user exists. However, unlike list_app_users (which validates the app and fetches full user details), this endpoint returns app_ids from the junction table without verifying those apps still exist or aren't deleted (line 261).

This could return IDs of soft-deleted apps, which may confuse API consumers. If the intent is to show only currently active apps, consider filtering:

🔎 Suggested enhancement
+    app_repository: SQLAlchemyRepository[App] = Depends(
+        Provide[ApplicationContainer.app_repository]
+    ),
):
    """List apps accessible to user (owners only)"""
    # ... existing authorization and user validation ...
    
    # Get user apps
    try:
        user_apps = await app_user_service.get_user_apps(user_id)
-       app_ids = [str(user_app.app_id) for user_app in user_apps]
+       
+       # Filter out deleted apps
+       if user_apps:
+           app_ids_list = [user_app.app_id for user_app in user_apps]
+           active_apps = await app_repository.find(id=app_ids_list, deleted=False)
+           app_ids = [str(app.id) for app in active_apps]
+       else:
+           app_ids = []

This would mirror the pattern used in list_app_users and ensure consistency.

Alternatively, if returning historical associations (including deleted apps) is intentional for audit purposes, the current implementation is acceptable.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c227039 and 00b9843.

📒 Files selected for processing (1)
  • wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py
🧰 Additional context used
🧬 Code graph analysis (1)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (4)
wavefront/server/apps/floconsole/floconsole/constants/user.py (1)
  • UserRole (4-6)
wavefront/server/apps/floconsole/floconsole/db/models/app.py (2)
  • App (13-49)
  • to_dict (37-49)
wavefront/server/apps/floconsole/floconsole/db/models/user.py (2)
  • User (14-49)
  • to_dict (42-49)
wavefront/server/apps/floconsole/floconsole/services/app_user_service.py (5)
  • AppUserService (8-35)
  • grant_app_access (14-22)
  • revoke_app_access (24-27)
  • get_app_users (33-35)
  • get_user_apps (29-31)
🪛 Ruff (0.14.10)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py

28-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


31-33: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


34-36: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


37-39: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


81-81: Do not catch blind exception: Exception

(BLE001)


95-97: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


98-100: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


101-103: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


104-106: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


148-148: Do not catch blind exception: Exception

(BLE001)


163-165: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


166-168: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


169-171: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


172-174: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


215-215: Do not catch blind exception: Exception

(BLE001)


228-230: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


231-233: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


234-236: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


269-269: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (3)
wavefront/server/apps/floconsole/floconsole/controllers/app_user_controller.py (3)

22-86: LGTM! Solid implementation with proper validations.

The grant_app_access endpoint correctly:

  • Enforces OWNER-only access
  • Validates both app and user existence before granting access
  • Uses logger.exception() for detailed server-side logging while returning generic error messages to clients
  • Returns appropriate HTTP status codes (201, 403, 404, 500)

The exception handling has been properly addressed from previous reviews.


89-155: LGTM! Consistent implementation with proper validations.

The revoke_app_access endpoint mirrors grant_app_access with:

  • OWNER role enforcement
  • App and user existence validation
  • Proper exception handling and logging
  • Appropriate HTTP status codes (200, 403, 404, 500)

The implementation is consistent and secure.


158-220: LGTM! Well-optimized query with proper validations.

The list_app_users endpoint correctly:

  • Enforces OWNER role
  • Validates app existence
  • Uses user_repository.find(id=user_ids, deleted=False) at line 204, which should generate an efficient SQL WHERE id IN (...) query instead of fetching all users
  • Returns full user details via to_dict()
  • Handles errors securely

The query optimization from previous reviews has been properly implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants