Skip to content

feat: per-account device isolation with Firebase scoped paths#127

Merged
DaTiC0 merged 5 commits into
mainfrom
feat/device-management-firebase
May 4, 2026
Merged

feat: per-account device isolation with Firebase scoped paths#127
DaTiC0 merged 5 commits into
mainfrom
feat/device-management-firebase

Conversation

@DaTiC0

@DaTiC0 DaTiC0 commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Implements per-account device isolation backed by Firebase Realtime Database. Each user's devices are stored and served from a user-scoped path (/users/{user_id}/devices), with no cross-account leakage.

Changes

Device Management

  • action_devices.py: Refactored all device functions (rsync, rstate, rquery, rexecute, onSync, onQuery, onExecute, commands, actions, get_dashboard_devices) to accept and use user_id for scoped Firebase reads/writes
  • Added _device_namespace_mode(), _normalize_user_scope(), _get_scoped_snapshot(), and _get_scoped_device_states() helpers
  • Added get_dashboard_devices(user_id) to power the devices dashboard with real Firebase data

Routes

  • routes.py: /devices, /device/<device_id>, and /smarthome routes all pass current_user.id as scope
  • Added _resolve_smarthome_user_scope() to extract user scope from Google Home requests (agentUserId or authenticated session)

Config

  • config.py: Added DEVICE_NAMESPACE_MODE env var (default strict) — controls whether device reads fall back to the legacy global path (legacy | hybrid | strict)

Templates

  • templates/devices.html: Updated to render real device data (icon, display name, type label, online status) from Firebase

Firebase Path Schema

Path Contents
/users/{user_id}/devices/{device_id}/ Per-account device tree (canonical)
/devices/ Legacy global path — deleted

Isolation Behaviour

In strict mode, each account only reads from its own /users/{user_id}/devices path. No fallback to shared data.

User Devices
1 (dekanozishvili@gmail.com) Front Camera, Front Door Lock, My Lamp, My Sprinkler, My Switch
2 (ddekanozishvili@solvit.ge) Test Switch (added for testing)

Tests

38 tests passing. New tests added:

  • test_devices_route_scopes_to_logged_in_user
  • test_smarthome_uses_agent_user_scope
  • test_devices_route_passes_logged_in_user_scope
  • test_get_dashboard_devices_uses_live_state
  • test_devices_route_uses_dashboard_devices

Summary by Sourcery

Scope device management to per-user Firebase paths and wire this through web routes and smarthome intents, with configurable fallback behavior.

New Features:

  • Add per-user Firebase-backed device listing for the Device Management dashboard.
  • Introduce configurable device namespace modes to control legacy vs scoped paths.
  • Expose a helper to resolve user scope for smarthome requests based on agentUserId, session user, or fallback config.

Enhancements:

  • Refactor device action handlers and Firebase helpers to accept user scope and support hybrid/strict isolation modes.
  • Improve devices dashboard template to show real-time device metadata, icons, and online status derived from Firebase.

Tests:

  • Add tests to verify smarthome requests use the correct user scope and agentUserId.
  • Add tests to ensure devices routes pass logged-in user scope and use dashboard device data.
  • Add tests to validate dashboard devices rendering logic uses live state and status classes.

@sourcery-ai

sourcery-ai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements per-account Firebase device isolation by scoping all device access to user-specific paths, adding a namespace mode switch, wiring routes and smarthome handlers to pass user scope, and updating the devices dashboard to render live, scoped device state.

Sequence diagram for scoped smarthome request handling

sequenceDiagram
    actor User
    participant GoogleHome as GoogleHome
    participant Routes as FlaskRoutes_smarthome
    participant Actions as actions
    participant Devices as action_devices
    participant Firebase as FirebaseRealtimeDB

    User->>GoogleHome: Voice command
    GoogleHome->>Routes: POST /smarthome
    Routes->>Routes: _resolve_smarthome_user_scope(req)
    Routes->>Actions: actions(req, user_id, agent_user_id)

    loop For each intent
        Actions->>Actions: Inspect intent
        alt SYNC intent
            Actions->>Devices: onSync(user_id, agent_user_id)
            Devices->>Devices: _normalize_user_scope(user_id)
            Devices->>Devices: rsync(user_scope)
            Devices->>Devices: _get_scoped_snapshot(user_scope)
            alt namespace_mode legacy or user_scope None
                Devices->>Firebase: GET /devices
            else namespace_mode hybrid or strict
                Devices->>Firebase: GET /users/{user_scope}/devices
                opt hybrid fallback when empty
                    Devices->>Firebase: GET /devices
                end
            end
            Devices->>Actions: devices list
        else QUERY intent
            Actions->>Devices: onQuery(body, user_id)
            loop For each deviceId
                Devices->>Devices: rquery(deviceId, user_id)
                Devices->>Devices: _get_scoped_device_states(deviceId, user_id)
                alt namespace_mode legacy or user_scope None
                    Devices->>Firebase: GET /devices/{deviceId}/states
                else namespace_mode hybrid or strict
                    Devices->>Firebase: GET /users/{user_scope}/devices/{deviceId}/states
                    opt hybrid fallback when None
                        Devices->>Firebase: GET /devices/{deviceId}/states
                    end
                end
            end
            Devices->>Actions: states per device
        else EXECUTE intent
            Actions->>Devices: onExecute(body, user_id)
            loop For each device command
                Devices->>Devices: commands(payload, deviceId, execCommand, params, user_id)
                Devices->>Devices: rexecute(deviceId, params, user_id)
                alt namespace_mode legacy or user_scope None
                    Devices->>Firebase: UPDATE /devices/{deviceId}/states
                    Devices->>Firebase: GET /devices/{deviceId}/states
                else namespace_mode hybrid or strict
                    Devices->>Firebase: UPDATE /users/{user_scope}/devices/{deviceId}/states
                    Devices->>Firebase: GET /users/{user_scope}/devices/{deviceId}/states
                end
            end
            Devices->>Actions: updated states
        end
    end

    Actions->>Routes: payload (agentUserId, devices, states)
    Routes->>GoogleHome: JSON response
    GoogleHome->>User: Confirmation
Loading

File-Level Changes

Change Details Files
Scope all Firebase device reads/writes to per-user paths with configurable namespace modes.
  • Introduced helpers (_device_namespace_mode, _normalize_user_scope, _get_scoped_snapshot, _get_scoped_device_states) to centralize user scoping and fallback behavior.
  • Updated reference() and rstate/rsync/rquery/rexecute to accept a user_id and use /users/{user_id}/devices with legacy/hybrid/strict fallback semantics.
  • Ensured EXECUTE writes target the appropriate namespace based on DEVICE_NAMESPACE_MODE while still reading updated state safely.
action_devices.py
config.py
Propagate user scope through SYNC/QUERY/EXECUTE and smarthome routing for correct per-account isolation.
  • Extended onSync, onQuery, onExecute, commands, and actions to accept user_id and agent_user_id, using them when building responses and querying Firebase.
  • Added _resolve_smarthome_user_scope() in routes.py to derive scope from agentUserId, current_user, or a configured fallback.
  • Updated /smarthome, /devices, and /device/ routes to pass current_user.id into action_devices APIs.
action_devices.py
routes.py
Drive the devices dashboard from live, scoped Firebase data with richer presentation.
  • Added get_dashboard_devices() and _normalize_device_type() to build UI-friendly device objects (display name, type_label, icon, online status) from Firebase snapshots.
  • Changed the /devices route to use get_dashboard_devices(current_user.id) and updated devices.html bindings to use the new fields and status styling.
action_devices.py
routes.py
templates/devices.html
Add configuration and tests to support and verify the new scoping behavior.
  • Introduced DEVICE_NAMESPACE_MODE config/env var with default strict and validation logic in _device_namespace_mode().
  • Added tests to verify smarthome agent user scoping, route scoping for logged-in users, dashboard device rendering from live state, and use of get_dashboard_devices in the devices route.
config.py
tests/test_app.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces user-level scoping for devices stored in Firebase, enabling multi-tenancy through a configurable namespace mode (legacy, hybrid, or strict). The changes include refactoring core device interaction functions to propagate user IDs, implementing scoped snapshot retrieval with fallback logic, and updating the web dashboard to display user-specific device data. Review feedback highlights an inconsistency in default configuration values, a potential logic issue in rexecute that could lead to partial records in hybrid mode, and the need to use configured fallbacks instead of hardcoded strings in exception handlers.

Comment thread action_devices.py Outdated
Comment thread action_devices.py Outdated
Comment thread action_devices.py Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="action_devices.py" line_range="90-93" />
<code_context>
         return current[keys[-1]]


+def _device_namespace_mode():
+    """Return configured device namespace mode."""
+    mode = str(current_app.config.get('DEVICE_NAMESPACE_MODE', 'hybrid')).strip().lower()
+    return mode if mode in {'legacy', 'hybrid', 'strict'} else 'hybrid'
+
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Align DEVICE_NAMESPACE_MODE default between config and helper to avoid hidden mode changes.

`Config.DEVICE_NAMESPACE_MODE` defaults to `strict`, but `_device_namespace_mode` falls back to `hybrid` when the config value is missing or invalid. This means code that reads `current_app.config` directly (e.g. tests or ad‑hoc apps) may run with a different namespace mode than code using `Config`. Please either make `Config.DEVICE_NAMESPACE_MODE` the single source of truth (and drop the `'hybrid'` fallback here) or change the config default to `hybrid` so both paths share the same default.

Suggested implementation:

```python
def _device_namespace_mode():
    """Return configured device namespace mode.

    Uses the same default as Config.DEVICE_NAMESPACE_MODE to keep
    current_app.config and Config in sync.
    """
    # Fall back to Config.DEVICE_NAMESPACE_MODE so both code paths share
    # the same default and we don't silently switch to a different mode.
    mode = str(
        current_app.config.get('DEVICE_NAMESPACE_MODE', Config.DEVICE_NAMESPACE_MODE)
    ).strip().lower()
    return mode if mode in {'legacy', 'hybrid', 'strict'} else Config.DEVICE_NAMESPACE_MODE

```

1. Ensure `Config` is imported or otherwise available in this module. For example, near the top of `action_devices.py`, add an appropriate import such as `from .config import Config` or `from yourpackage.config import Config`, matching how the rest of the codebase accesses `Config`.
2. Confirm that `Config.DEVICE_NAMESPACE_MODE` retains the intended default (currently `strict` per your comment) so that both direct `Config` users and `_device_namespace_mode()` share the same default and fallback behavior.
</issue_to_address>

### Comment 2
<location path="action_devices.py" line_range="160-166" />
<code_context>
+    return reference(None).get() or {}
+
+
+def _get_scoped_device_states(device_id, user_id):
+    """Get device states under user scope with hybrid fallback for reads."""
+    mode = _device_namespace_mode()
+    user_scope = _normalize_user_scope(user_id)
+
+    if mode == 'legacy' or not user_scope:
+        return reference(None).child(device_id).child('states').get()
+
+    scoped_states = reference(user_scope).child(device_id).child('states').get()
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider aligning hybrid write behavior with read fallback to avoid stale state reads.

In `_get_scoped_device_states`, hybrid mode reads from the user scope but falls back to `/devices` when no scoped states exist, while `rexecute` in hybrid mode writes only to `reference(user_scope)`:

```python
mode = _device_namespace_mode()
user_scope = _normalize_user_scope(user_id)
target_reference = reference(None) if mode == 'legacy' or not user_scope else reference(user_scope)
```

This can produce stale reads: initial reads hit legacy, writes go only to the scoped path, and subsequent reads may still hit legacy if `scoped_states` is `None`, missing the new state.

To keep hybrid behavior consistent and avoid this mismatch, you could either:
- Write to both scoped and legacy locations in hybrid mode, or
- Adjust `_get_scoped_device_states` so a `None` scoped value triggers promotion from legacy and/or prevents further fallback once scoped data exists.

Aligning these will reduce confusing state discrepancies between reads and writes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread action_devices.py Outdated
Comment thread action_devices.py Outdated
@DaTiC0
DaTiC0 merged commit 8be89e1 into main May 4, 2026
7 checks passed
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.

1 participant