fix: enforce workspace isolation logging

This commit is contained in:
Leon Bösche
2026-01-11 05:01:52 +01:00
parent ac1bd2749c
commit 619b2fe23c
6 changed files with 76 additions and 22 deletions

View File

@@ -191,8 +191,13 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
ResetFileBrowser event,
Emitter<FileBrowserState> emit,
) {
final oldOrgId = _currentOrgId;
final clearedCount = _currentFiles.length;
print(
'[FILE-BROWSER] Switching workspace, oldOrgId=$oldOrgId, newOrgId=${event.nextOrgId}, clearing=$clearedCount files, reloading state',
);
emit(DirectoryInitial());
_currentOrgId = '';
_currentOrgId = event.nextOrgId;
_currentPath = '/';
_currentFiles = [];
_filteredFiles = [];

View File

@@ -62,7 +62,14 @@ class CreateFolder extends FileBrowserEvent {
List<Object> get props => [orgId, parentPath, folderName];
}
class ResetFileBrowser extends FileBrowserEvent {}
class ResetFileBrowser extends FileBrowserEvent {
final String nextOrgId;
const ResetFileBrowser(this.nextOrgId);
@override
List<Object> get props => [nextOrgId];
}
class LoadPage extends FileBrowserEvent {
final int page;

View File

@@ -94,7 +94,7 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
);
// Reset all dependent blocs
permissionBloc.add(PermissionsReset());
fileBrowserBloc.add(ResetFileBrowser());
fileBrowserBloc.add(ResetFileBrowser(event.orgId));
uploadBloc.add(ResetUploads());
// Load permissions for the selected org
permissionBloc.add(LoadPermissions(event.orgId));
@@ -168,7 +168,7 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
emit(OrganizationLoaded(organizations: updatedOrgs, selectedOrg: newOrg));
// Reset blocs and load permissions for new org
permissionBloc.add(PermissionsReset());
fileBrowserBloc.add(ResetFileBrowser());
fileBrowserBloc.add(ResetFileBrowser(newOrg.id));
uploadBloc.add(ResetUploads());
permissionBloc.add(LoadPermissions(newOrg.id));
} catch (e) {

View File

@@ -60,14 +60,17 @@ class _FileExplorerState extends State<FileExplorer> {
);
context.read<PermissionBloc>().add(LoadPermissions(widget.orgId));
}
@override
void didUpdateWidget(covariant FileExplorer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.orgId != widget.orgId) {
// Reset and reload when switching between Personal and Org workspaces
final bloc = context.read<FileBrowserBloc>();
bloc.add(ResetFileBrowser());
print(
'[FILE-BROWSER] UI detected workspace switch, oldOrgId=${oldWidget.orgId}, newOrgId=${widget.orgId}, clearing local view',
);
bloc.add(ResetFileBrowser(widget.orgId));
bloc.add(LoadDirectory(orgId: widget.orgId, path: '/'));
}
}
@@ -824,7 +827,9 @@ class _FileExplorerState extends State<FileExplorer> {
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () {
final parentPath = _getParentPath(state.currentPath);
final parentPath = _getParentPath(
state.currentPath,
);
context.read<FileBrowserBloc>().add(
LoadDirectory(
orgId: widget.orgId,

View File

@@ -3,6 +3,7 @@ package database
import (
"context"
"database/sql"
"log"
"time"
"github.com/google/uuid"
@@ -255,7 +256,7 @@ func (db *DB) GetOrgMembers(ctx context.Context, orgID uuid.UUID) ([]Membership,
}
// GetOrgFiles returns files for a given organization (top-level folder listing)
func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q string, page, pageSize int) ([]File, error) {
func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, userID uuid.UUID, path string, q string, page, pageSize int) ([]File, error) {
if page <= 0 {
page = 1
}
@@ -264,22 +265,31 @@ func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q s
}
offset := (page - 1) * pageSize
orgIDStr := orgID.String()
userIDStr := userID.String()
log.Printf("[DATA-ISOLATION] stage=before, action=list, orgId=%s, userId=%s, fileCount=0, path=%s", orgIDStr, userIDStr, path)
// Basic search and pagination. Returns only direct children of the given path.
// For root ("/"), we want files where path doesn't contain "/" after the first character.
// For subdirs, we want files where path starts with parent but has no additional "/" after parent.
rows, err := db.QueryContext(ctx, `
SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at
FROM files
WHERE org_id = $1
AND path != $2
AND (
($2 = '/' AND path LIKE '/%' AND path NOT LIKE '/%/%')
OR ($2 != '/' AND path LIKE $2 || '/%' AND path NOT LIKE $2 || '/%/%')
SELECT f.id, f.org_id::text, f.user_id::text, f.name, f.path, f.type, f.size, f.last_modified, f.created_at
FROM files f
WHERE f.org_id = $1
AND EXISTS (
SELECT 1
FROM memberships m
WHERE m.org_id = $1 AND m.user_id = $2
)
AND ($3 = '' OR name ILIKE '%' || $3 || '%')
ORDER BY CASE WHEN type = 'folder' THEN 0 ELSE 1 END, name
LIMIT $4 OFFSET $5
`, orgID, path, q, pageSize, offset)
AND f.path != $3
AND (
($3 = '/' AND f.path LIKE '/%' AND f.path NOT LIKE '/%/%')
OR ($3 != '/' AND f.path LIKE $3 || '/%' AND f.path NOT LIKE $3 || '/%/%')
)
AND ($4 = '' OR f.name ILIKE '%' || $4 || '%')
ORDER BY CASE WHEN f.type = 'folder' THEN 0 ELSE 1 END, f.name
LIMIT $5 OFFSET $6
`, orgID, userID, path, q, pageSize, offset)
if err != nil {
return nil, err
}
@@ -303,7 +313,11 @@ func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q s
}
files = append(files, f)
}
return files, rows.Err()
err = rows.Err()
if err == nil {
log.Printf("[DATA-ISOLATION] stage=after, action=list, orgId=%s, userId=%s, fileCount=%d, path=%s", orgIDStr, userIDStr, len(files), path)
}
return files, err
}
// GetUserFiles returns files for a user's personal workspace at a given path
@@ -317,10 +331,12 @@ func (db *DB) GetUserFiles(ctx context.Context, userID uuid.UUID, path string, q
offset := (page - 1) * pageSize
// Return only direct children of the given path
log.Printf("[DATA-ISOLATION] stage=before, action=list, orgId=, userId=%s, fileCount=0, path=%s", userID.String(), path)
rows, err := db.QueryContext(ctx, `
SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at
FROM files
WHERE user_id = $1
AND org_id IS NULL
AND path != $2
AND (
($2 = '/' AND path LIKE '/%' AND path NOT LIKE '/%/%')
@@ -353,7 +369,11 @@ func (db *DB) GetUserFiles(ctx context.Context, userID uuid.UUID, path string, q
}
files = append(files, f)
}
return files, rows.Err()
err = rows.Err()
if err == nil {
log.Printf("[DATA-ISOLATION] stage=after, action=list, orgId=, userId=%s, fileCount=%d, path=%s", userID.String(), len(files), path)
}
return files, err
}
// CreateFile inserts a file or folder record. orgID or userID may be nil.
@@ -361,16 +381,21 @@ func (db *DB) CreateFile(ctx context.Context, orgID *uuid.UUID, userID *uuid.UUI
var f File
var orgIDVal interface{}
var userIDVal interface{}
orgIDStr := ""
userIDStr := ""
if orgID != nil {
orgIDVal = *orgID
orgIDStr = orgID.String()
} else {
orgIDVal = nil
}
if userID != nil {
userIDVal = *userID
userIDStr = userID.String()
} else {
userIDVal = nil
}
log.Printf("[DATA-ISOLATION] stage=before, action=create, orgId=%s, userId=%s, fileCount=1, path=%s", orgIDStr, userIDStr, path)
err := db.QueryRowContext(ctx, `
INSERT INTO files (org_id, user_id, name, path, type, size)
@@ -380,6 +405,7 @@ func (db *DB) CreateFile(ctx context.Context, orgID *uuid.UUID, userID *uuid.UUI
if err != nil {
return nil, err
}
log.Printf("[DATA-ISOLATION] stage=after, action=create, orgId=%s, userId=%s, fileCount=1, path=%s", orgIDStr, userIDStr, f.Path)
return &f, nil
}

View File

@@ -357,6 +357,17 @@ func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, a
func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
// Org ID is provided by middleware.Org
orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
userIDStr, ok := middleware.GetUserID(r.Context())
if !ok || userIDStr == "" {
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
errors.LogError(r, err, "Invalid user id in context")
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
return
}
// Query params: path, q (search), page, pageSize
path := r.URL.Query().Get("path")
if path == "" {
@@ -372,7 +383,7 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
fmt.Sscanf(ps, "%d", &pageSize)
}
files, err := db.GetOrgFiles(r.Context(), orgID, path, q, page, pageSize)
files, err := db.GetOrgFiles(r.Context(), orgID, userID, path, q, page, pageSize)
if err != nil {
errors.LogError(r, err, "Failed to get org files")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)