Skip to content

Add password protection for documents and directories#11

Open
Copilot wants to merge 8 commits into
masterfrom
copilot/add-password-protection-feature
Open

Add password protection for documents and directories#11
Copilot wants to merge 8 commits into
masterfrom
copilot/add-password-protection-feature

Conversation

Copilot AI commented Dec 28, 2025

Copy link
Copy Markdown
Contributor

Implements password-based access control for documents and directories. Authentication flow: check standard permissions first, then password if protected. Returns 202 when password required but missing, 403 when incorrect.

Database Schema

  • New object_protections table (single unified table for all protection types)
  • Columns: target_type, target_id, protection_type, protection_data, protection_metadata
  • protection_type: Distinguishes protection types ('password', 'encryption', 'biometric', etc.)
  • protection_data: JSON format storing type-specific data (e.g., password_hash and salt for passwords)
  • protection_metadata: Reserved for future extensions

Architecture

  • Single unified table: All protection types share one table, distinguished by protection_type column
  • Clean BaseObject: Protection logic kept out of base class to avoid bloat
  • Handler-based checking: check_password_protection() helper function in handlers performs verification
  • Scalable design: Future protection types add methods to ObjectProtection model without modifying BaseObject

Security

  • PBKDF2-HMAC-SHA256, 600K iterations (OWASP 2023 minimum)
  • Constant-time comparison via secrets.compare_digest() (timing attack prevention)

API Endpoints

# Enable/update protection
{"action": "enable_password_protection", "data": {
    "target_type": "document",  # or "directory"
    "target_id": "...",
    "password": "..."
}}

# Remove protection
{"action": "remove_password_protection", "data": {
    "target_type": "document",
    "target_id": "..."
}}

# Verify password
{"action": "verify_password", "data": {
    "target_type": "document",
    "target_id": "...",
    "password": "..."
}}

Handler Integration

Updated handlers now accept optional password parameter:

  • get_document, get_document_info
  • list_directory, get_directory_info
# Example: accessing protected document
{"action": "get_document_info", "data": {
    "document_id": "...",
    "password": "..."  # optional, returns 202 if missing and required
}}

Implementation Notes

  • check_password_protection() helper function in handlers performs protection checks
  • ObjectProtection model contains type-specific methods (e.g., set_password(), verify_password())
  • TARGET_TYPE_MAPPING constant maps table names to API types
  • BaseObject remains clean with no protection-specific methods
Original prompt

Please implement a function to add password protection to directories or documents. The authentication process is as follows: First, authenticate as you would any other directory or document. Then, check if the accessed object is password protected. If it is password protected and the request did not include a password, return code 202. Obviously, you will need to implement APIs for enabling and removing password protection. If possible, implement this functionality as a separate table in the database structure and use a join query to link it to the main table. This will preserve scalability (allowing for different types of protection to be implemented later). In other words, you need to reserve appropriate columns for scalability.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 4 commits December 28, 2025 05:21
Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>
Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>
Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>
…rison

Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>
Copilot AI changed the title [WIP] Add password protection functionality for directories and documents Add password protection for documents and directories Dec 28, 2025
Copilot AI requested a review from Creeper19472 December 28, 2025 05:34
@Creeper19472

Copy link
Copy Markdown
Collaborator

@copilot Several issues exist: 1. You didn't understand what I meant. I meant that for current password protection and all future protection types, they will all share a single table, using a column of type such as "protection" to distinguish their types, instead of splitting each protection type into a separate table as you did before. 2. Don't arbitrarily add functions that are only valid for a specific protection type, such as check_password, to classes like BaseObject, because, as I said, various more protection types are likely to be added in the future. Move this logic to RequestHandler, or ensure that your implemented functions can handle multiple protection types (able to handle one or more protection types being enabled) to avoid making the BaseObject implementation too bloated. After all, it's just a base class.

- Renamed PasswordProtection to ObjectProtection for single table design
- Changed schema: protection_type column distinguishes types, protection_data stores type-specific JSON
- Removed password-specific methods from BaseObject to keep it clean
- Created check_password_protection helper function in protection handlers
- Updated all handlers to use helper function instead of BaseObject methods
- Table now supports multiple protection types (password, encryption, biometric, etc.)

Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>

Copilot AI commented Dec 28, 2025

Copy link
Copy Markdown
Contributor Author

@copilot Several issues exist: 1. You didn't understand what I meant. I meant that for current password protection and all future protection types, they will all share a single table, using a column o...

Refactored in commit 09e892b:

  1. Single unified table: Renamed to ObjectProtection with protection_type column to distinguish types ('password', 'encryption', 'biometric', etc.) and protection_data as JSON for type-specific data

  2. Removed BaseObject methods: Deleted get_password_protection(), is_password_protected(), and verify_password() from BaseObject to keep it clean

  3. Logic moved to handlers: Created check_password_protection() helper function in protection handlers. All document/directory handlers now import and use this helper instead of calling BaseObject methods

The design now supports multiple protection types in a single table and keeps BaseObject focused solely on access control.

Copilot AI and others added 2 commits December 28, 2025 06:05
Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>
Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com>
@Creeper19472 Creeper19472 marked this pull request as ready for review December 28, 2025 06:10
Copilot AI review requested due to automatic review settings December 28, 2025 06:10

Copilot AI 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.

Pull request overview

This PR implements password-based access control for documents and directories using a unified protection system. The implementation adds a new object_protections database table that can support multiple protection types (password, encryption, biometric, etc.) distinguished by a protection_type column.

Key Changes

  • New password protection database model with PBKDF2-HMAC-SHA256 hashing (600K iterations) and constant-time comparison
  • Three new API endpoints for enabling, removing, and verifying password protection
  • Integration of password checks into existing document and directory access handlers

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/test_password_protection.py Comprehensive test suite covering password protection on documents and directories, including enable/remove/verify operations and access control scenarios
src/main.py Imports the new ObjectProtection model for database initialization
src/include/handlers/protection.py New handlers for password protection management and helper function for password verification
src/include/handlers/document.py Adds optional password parameter to document access endpoints and integrates password protection checks
src/include/handlers/directory.py Adds optional password parameter to directory access endpoints and integrates password protection checks
src/include/database/models/protection.py New database model for unified protection system with password hashing and verification methods
src/include/database/models/entity.py Refactors TARGET_TYPE_MAPPING from local to global constant
src/include/constants.py Adds TARGET_TYPE_MAPPING constant for mapping database table names to API types
src/include/connection_handler.py Registers new password protection handlers in the available functions dictionary

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"enum": ["document", "directory"]
},
"target_id": {"type": "string", "minLength": 1},
"password": {"type": "string"}

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

The password validation in the data schema allows empty strings. Similar to enable_password_protection, the password field should have minLength validation to prevent empty passwords from being accepted during verification.

Suggested change
"password": {"type": "string"}
"password": {"type": "string", "minLength": 1}

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +62
if password is None:
# Password required but not provided

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

The check_password_protection function only checks for None password, but doesn't handle empty strings. If a user provides an empty password string in the request, it will be passed to verify_password and processed, which could lead to unexpected behavior. Consider treating empty strings the same as None (password required), or add validation to reject empty password strings.

Suggested change
if password is None:
# Password required but not provided
if password is None or password == "":
# Password required but not provided or empty

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +57
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

# Target object identification
target_type: Mapped[str] = mapped_column(
VARCHAR(64), nullable=False, comment="Type: 'document' or 'directory'"
)
target_id: Mapped[str] = mapped_column(
VARCHAR(255), nullable=False, comment="ID of the protected object"
)

# Protection type identifier
protection_type: Mapped[str] = mapped_column(
VARCHAR(64), nullable=False, default="password",
comment="Protection type: 'password', 'encryption', 'biometric', etc."
)

# Protection data (type-specific, stored as JSON or text)
# For password: contains password_hash and salt
# For other types: contains type-specific data
protection_data: Mapped[str] = mapped_column(
Text, nullable=False,
comment="Protection-specific data (JSON format for flexibility)"
)

# Additional metadata (reserved for future use)
protection_metadata: Mapped[Optional[str]] = mapped_column(
Text, nullable=True,
comment="JSON metadata for future extensions"
)

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

The database model is missing a unique constraint to prevent duplicate protection entries for the same target. Without a unique constraint on (target_type, target_id, protection_type), multiple password protection entries could be created for the same document or directory, leading to undefined behavior during verification. Consider adding a unique constraint or composite index on these three columns.

Copilot uses AI. Check for mistakes.
else:
handler.conclude_request(400, {}, "Invalid target type")
return 400, target_id, handler.username

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

The verify_password handler does not check if the user has read permissions for the target object before allowing password verification. This could allow unauthorized users to perform brute-force password attacks on documents/directories they don't have access to. Standard access permissions should be checked first (similar to how enable_password_protection and remove_password_protection check for manage permissions).

Suggested change
# Check that the user has read permissions for the target object
has_read_permission = False
if hasattr(user, "can_read"):
# Prefer an explicit can_read(target) capability method if available.
has_read_permission = bool(user.can_read(target)) # type: ignore[call-arg]
elif hasattr(user, "has_permission"):
# Fallback to a generic permission check API, if provided by the User model.
try:
has_read_permission = bool(user.has_permission("read", target)) # type: ignore[call-arg]
except TypeError:
# In case has_permission has a different signature, fail closed.
has_read_permission = False
if not has_read_permission:
handler.conclude_request(
403,
{},
"You do not have permission to access this object",
)
return 403, target_id, handler.username

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +86
data_schema = {
"type": "object",
"properties": {
"target_type": {
"type": "string",
"enum": ["document", "directory"]
},
"target_id": {"type": "string", "minLength": 1},
"password": {"type": "string", "minLength": 1}
},

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

The password validation in the data schema allows empty strings (minLength is 1 for target_id but password only has type "string" without minLength). An empty password would be hashed and stored, but this is a poor security practice. Consider adding minLength validation for the password field, or adding explicit validation logic to reject empty passwords.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +69
def check_password_protection(target, password: Optional[str], session) -> tuple[int, str]:
"""
Check password protection for a document or directory.

This is a helper function to be used by request handlers to check
if an object has password protection and verify the password if provided.

Args:
target: The Document or Folder object to check
password: The password provided by the user (None if not provided)
session: The database session

Returns:
Tuple of (code, message):
- (0, "success") if access granted (no protection or correct password)
- (202, "Password required") if protected but no password provided
- (403, "Incorrect password") if protected and wrong password provided
"""
target_type = TARGET_TYPE_MAPPING[target.__tablename__]

# Check if object has password protection
protection = (
session.query(ObjectProtection)
.filter(
ObjectProtection.target_type == target_type,
ObjectProtection.target_id == target.id,
ObjectProtection.protection_type == "password"
)
.first()
)

if not protection:
# No password protection, access granted
return (0, "success")

if password is None:
# Password required but not provided
return (202, "Password required")

# Verify the password
if protection.verify_password(password):
return (0, "success")
else:
return (403, "Incorrect password")

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

The return value (0, "success") for successful password verification is inconsistent with HTTP status code conventions. The function returns tuple[int, str] where 0 represents success, but this is mixed with HTTP status codes (202, 403) for other cases. This could be confusing. Consider using a more consistent approach, such as returning None for success or using a dedicated status code like 200.

Copilot uses AI. Check for mistakes.
"additionalProperties": False,
}

def handle(self, handler: ConnectionHandler):

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.
RequestGetDirectoryInfoHandler.handle returns tuple of size 2 and tuple of size 3.

Copilot uses AI. Check for mistakes.
Comment thread src/main.py
from include.database.models.classic import User, UserGroup
from include.database.models.entity import Document, DocumentRevision
from include.database.models.file import File
from include.database.models.protection import ObjectProtection

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

Import of 'ObjectProtection' is not used.

Suggested change
from include.database.models.protection import ObjectProtection

Copilot uses AI. Check for mistakes.
# Cleanup
try:
await authenticated_client.delete_directory(directory_id)
except Exception:

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
except Exception:
# Best-effort cleanup: ignore errors if the directory was already deleted
# or cannot be removed, so that they do not mask earlier test failures.

Copilot uses AI. Check for mistakes.
# Cleanup
try:
await authenticated_client.delete_directory(directory_id)
except Exception:

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
except Exception:
# Best-effort cleanup: ignore errors if the directory is already deleted

Copilot uses AI. Check for mistakes.
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.

3 participants