modified: app.py

modified:   openapi.yaml
	modified:   requirements.txt
	modified:   test_agent.sh
This commit is contained in:
Russell Ballestrini 2025-01-11 21:10:32 -05:00
parent 5af4d5a7e4
commit 638d19fd77
4 changed files with 242 additions and 252 deletions

70
app.py
View file

@ -1,8 +1,8 @@
###############################################################################
# app.py - PyraLogs Application with Enhanced Agent Management
# - Prevents duplicate agent names within a namespace
# - Handles agent revocation by updating agent status
# - Provides JWT regeneration for existing agents
# app.py - PyraLogs Application with Enhanced Secret Management
# - Secrets are generated once, saved to the filesystem, and loaded thereafter
# - Different secrets are used for session signing and JWT signing
# - Includes previous enhancements for agent management and OpenAPI integration
###############################################################################
import os
import base64
@ -58,26 +58,61 @@ from jwt import PyJWTError
# Set up logging
################################################################################
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
################################################################################
# Environment Variables and Defaults
################################################################################
# If PYRALOGS_SECRET is missing, generate a random 64-character secret.
pyralogs_secret = os.environ.get("PYRALOGS_SECRET", "")
if not pyralogs_secret:
pyralogs_secret = "".join(
random.choices(string.ascii_letters + string.digits, k=64)
)
log.info(f"Generated random PYRALOGS_SECRET: {pyralogs_secret}")
# Application directory
APP_DIR = os.path.dirname(os.path.abspath(__file__))
# JWT Secret Key
JWT_SECRET = os.environ.get("PYRALOGS_JWT_SECRET", pyralogs_secret)
# Paths to the secret files
PYRALOGS_SECRET_FILE = os.path.join(APP_DIR, "pyralogs_secret.txt")
JWT_SECRET_FILE = os.path.join(APP_DIR, "jwt_secret.txt")
def get_or_create_secret(env_var_name, secret_file_path):
"""
Retrieves the secret from an environment variable, or loads it from the
specified file. If neither is available, generates a new secret, saves
it to the file, and returns it.
"""
# Check environment variable
secret = os.environ.get(env_var_name, "")
if secret:
log.info(f"Using {env_var_name} from environment variable.")
return secret
# Check if the secret file exists
if os.path.exists(secret_file_path):
with open(secret_file_path, "r") as f:
secret = f.read().strip()
if secret:
log.info(f"Loaded {env_var_name} from {secret_file_path}")
return secret
else:
log.warning(f"{secret_file_path} is empty. Generating new secret.")
else:
log.info(f"{secret_file_path} does not exist. Generating new secret.")
# Generate a new secret
secret = "".join(random.choices(string.ascii_letters + string.digits, k=64))
# Save the secret to the file
with open(secret_file_path, "w") as f:
f.write(secret)
log.info(f"Generated and saved new {env_var_name} to {secret_file_path}")
return secret
# Retrieve or generate the secrets
pyralogs_secret = get_or_create_secret("PYRALOGS_SECRET", PYRALOGS_SECRET_FILE)
JWT_SECRET = get_or_create_secret("PYRALOGS_JWT_SECRET", JWT_SECRET_FILE)
JWT_ALGORITHM = "HS256"
# Database URL can be overridden by environment variable
APP_DIR = os.path.dirname(os.path.abspath(__file__))
default_main_db_url = f"sqlite:///{os.path.join(APP_DIR, 'main.db')}"
DB_URL = os.environ.get("PYRALOGS_DB_URL", default_main_db_url)
@ -1026,7 +1061,7 @@ def view_namespace_logs_view(request):
def main(global_config=None, **settings):
# Configure logging
logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.INFO)
# Set up the session factory
session_factory = SignedCookieSessionFactory(
@ -1048,6 +1083,10 @@ def main(global_config=None, **settings):
config.include("pyramid_jinja2")
config.include("pyramid_tm") # Include pyramid_tm for transaction management
config.include("pyramid_openapi3")
config.pyramid_openapi3_spec('openapi.yaml', route='/openapi.yaml')
config.pyramid_openapi3_add_explorer(route='/docs')
# Add .html.j2 extension for Jinja2 templates
config.add_jinja2_renderer(".j2")
config.add_jinja2_search_path("templates", name=".j2")
@ -1062,7 +1101,6 @@ def main(global_config=None, **settings):
)
session_factory_ = sessionmaker(bind=engine)
Base.metadata.bind = engine
Base.metadata.create_all(engine)
DBSession = scoped_session(session_factory_)
register(DBSession) # Register with zope.sqlalchemy

View file

@ -1,25 +1,25 @@
openapi: 3.0.3
info:
title: PyraFiles API
title: PyraLogs API
version: 1.0.0
description: |
API specification for the PyraFiles application.
PyraFiles allows users to register, authenticate, upload media files,
and manage their media content.
API specification for the PyraLogs application.
PyraLogs allows users to register, authenticate, manage namespaces,
generate JWT tokens for agents, and submit logs via a webhook.
servers:
- url: http://localhost:{port}
- url: http://127.0.0.1:{port}
description: Local development server
variables:
port:
default: '6544'
- url: https://upload.unturf.com
description: prod for humans & agents to mingle.
- url: https://logs.unturf.com
description: Production server
paths:
/:
get:
summary: Home Page
description: Displays the home page.
description: Displays the home page with public namespaces and user namespaces.
responses:
'200':
description: Successful response
@ -106,7 +106,7 @@ paths:
/auth/profile:
get:
summary: Display User Profile
description: Shows the user's profile, including upload stats.
description: Shows the user's profile, including owned namespaces.
security:
- sessionAuth: []
responses:
@ -143,212 +143,64 @@ paths:
'403':
description: Unauthorized (user not logged in or guest mode)
/auth/download_db:
/namespace/create:
get:
summary: Download User Database
description: Allows the user to download their personal database file.
summary: Display Namespace Creation Page
description: Renders the form to create a new namespace.
security:
- sessionAuth: []
responses:
'200':
description: Database file downloaded
content:
application/octet-stream:
schema:
type: string
format: binary
'403':
description: Unauthorized (user not logged in or unverified)
'404':
description: Database file not found
/auth/export_user_record:
get:
summary: Export User Record
description: Exports the user's record as a JSON file.
security:
- sessionAuth: []
responses:
'200':
description: User record JSON file downloaded
content:
application/json:
schema:
type: object
'403':
description: Unauthorized (user not logged in or unverified)
/admin/import_user_record:
get:
summary: Display Import User Record Page
description: Renders a page to import a user record (Admin only).
security:
- sessionAuth: []
responses:
'200':
description: Import user record page rendered
description: Namespace creation page rendered
content:
text/html:
schema:
type: string
'403':
description: Forbidden (user not admin)
description: Unauthorized (user not logged in)
post:
summary: Import User Record
description: Processes uploaded user record file and imports the user (Admin only).
summary: Create Namespace
description: Processes the namespace creation form.
security:
- sessionAuth: []
requestBody:
required: true
content:
multipart/form-data:
application/x-www-form-urlencoded:
schema:
type: object
properties:
user_record_file:
type: string
format: binary
required:
- user_record_file
responses:
'302':
description: Redirects to the home page after successful import
'400':
description: Bad Request (e.g., invalid file)
'403':
description: Forbidden (user not admin)
/media/upload:
get:
summary: Display Media Upload Page
description: Renders the media upload form.
security:
- sessionAuth: []
responses:
'200':
description: Upload media page rendered
content:
text/html:
schema:
type: string
'403':
description: Unauthorized (user not logged in or unverified)
post:
summary: Upload Media
description: Processes the uploaded media file.
security:
- sessionAuth: []
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
media_file:
type: string
format: binary
title:
name:
type: string
is_public:
type: string
enum: ['on']
required:
- media_file
- name
responses:
'302':
description: Redirects to the media details page
description: Redirects to the namespace management page
'400':
description: Bad Request (e.g., no file uploaded, unsupported media type)
description: Bad Request (e.g., namespace name already exists)
'403':
description: Unauthorized (user not logged in or unverified)
description: Unauthorized (user not logged in)
/media/list:
/namespace/{namespace_short_id}/manage:
get:
summary: List Public Media
description: Displays a list of public media from all users.
responses:
'200':
description: Media list page rendered
content:
text/html:
schema:
type: string
/media/user/{user_short_id}:
get:
summary: Display User's Media
description: Shows all media uploaded by a specific user.
parameters:
- in: path
name: user_short_id
required: true
schema:
type: string
description: The short ID of the user
responses:
'200':
description: User's media page rendered
content:
text/html:
schema:
type: string
'404':
description: User not found
'500':
description: Internal Server Error
/media/{user_short_id}/{media_short_id}/details:
get:
summary: Display Media Details
description: Shows details of a specific media item.
parameters:
- in: path
name: user_short_id
required: true
schema:
type: string
description: The short ID of the user
- in: path
name: media_short_id
required: true
schema:
type: string
description: The short ID of the media item
responses:
'200':
description: Media details page rendered
content:
text/html:
schema:
type: string
'404':
description: Media or user not found
'403':
description: Forbidden (media not public and not owner)
/media/{user_short_id}/{media_short_id}/edit:
get:
summary: Display Media Edit Page
description: Renders a form to edit media details (owner only).
summary: Manage Namespace
description: Displays the namespace management page (owners only).
security:
- sessionAuth: []
parameters:
- in: path
name: user_short_id
name: namespace_short_id
required: true
schema:
type: string
description: The short ID of the user
- in: path
name: media_short_id
required: true
schema:
type: string
description: The short ID of the media item
description: The short ID of the namespace
responses:
'200':
description: Media edit page rendered
description: Namespace management page rendered
content:
text/html:
schema:
@ -356,112 +208,205 @@ paths:
'403':
description: Forbidden (not owner or not logged in)
'404':
description: Media not found
description: Namespace not found
/namespace/{namespace_short_id}/update:
post:
summary: Edit Media
description: Updates the media item (owner only).
summary: Update Namespace
description: Updates namespace properties (owners only).
security:
- sessionAuth: []
parameters:
- in: path
name: user_short_id
name: namespace_short_id
required: true
schema:
type: string
description: The short ID of the user
- in: path
name: media_short_id
required: true
schema:
type: string
description: The short ID of the media item
description: The short ID of the namespace
requestBody:
required: true
content:
multipart/form-data:
application/x-www-form-urlencoded:
schema:
type: object
properties:
title:
type: string
is_public:
type: string
enum: ['on']
media_file:
type: string
format: binary
responses:
'302':
description: Redirects to the media details page
'400':
description: Bad Request (e.g., file too large)
description: Redirects to the namespace management page
'403':
description: Forbidden (not owner)
description: Forbidden (not owner or not logged in)
'404':
description: Media not found
description: Namespace not found
/media/{user_short_id}/{media_short_id}/delete:
/namespace/{namespace_short_id}/invite:
post:
summary: Delete Media
description: Deletes the media item (owner only).
summary: Invite User to Namespace
description: Invites a user to the namespace with a specified role (owners only).
security:
- sessionAuth: []
parameters:
- in: path
name: user_short_id
name: namespace_short_id
required: true
schema:
type: string
description: The short ID of the user
- in: path
name: media_short_id
required: true
schema:
type: string
description: The short ID of the media item
description: The short ID of the namespace
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
email:
type: string
format: email
role:
type: string
enum: ['owner', 'editor', 'reader']
required:
- email
- role
responses:
'302':
description: Redirects to the user's media list
description: Redirects to the namespace management page
'400':
description: Bad Request (e.g., invalid role)
'403':
description: Forbidden (not owner)
description: Forbidden (not owner or not logged in)
'404':
description: Media not found
description: Namespace not found
/media/{user_short_id}/{media_short_id}:
get:
summary: View Media
description: Retrieves the media file for viewing or download.
/namespace/{namespace_short_id}/generate_agent_jwt:
post:
summary: Generate Agent JWT
description: Generates a JWT token for an agent (owners only).
security:
- sessionAuth: []
parameters:
- in: path
name: user_short_id
name: namespace_short_id
required: true
schema:
type: string
description: The short ID of the user
- in: path
name: media_short_id
required: true
schema:
type: string
description: The short ID of the media item
- in: query
name: download
schema:
type: string
enum: ['true', 'false']
description: Set to 'true' to trigger download
description: The short ID of the namespace
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
agent_name:
type: string
required:
- agent_name
responses:
'200':
description: Media file retrieved
description: Agent JWT generated and displayed
content:
'*/*':
text/html:
schema:
type: string
format: binary
'400':
description: Bad Request (e.g., agent name missing)
'403':
description: Forbidden (media not public and not owner)
description: Forbidden (not owner or not logged in)
'404':
description: Media not found
description: Namespace not found
/namespace/{namespace_short_id}/revoke_agent:
post:
summary: Revoke Agent
description: Revokes an agent's access by invalidating their JWT (owners only).
security:
- sessionAuth: []
parameters:
- in: path
name: namespace_short_id
required: true
schema:
type: string
description: The short ID of the namespace
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
agent_id:
type: string
required:
- agent_id
responses:
'302':
description: Redirects to the namespace management page
'400':
description: Bad Request (e.g., agent ID missing)
'403':
description: Forbidden (not owner or not logged in)
'404':
description: Namespace or agent not found
/namespace/{namespace_short_id}/logs:
get:
summary: View Namespace Logs
description: Displays logs for the namespace (readers and above).
security:
- sessionAuth: []
parameters:
- in: path
name: namespace_short_id
required: true
schema:
type: string
description: The short ID of the namespace
responses:
'200':
description: Logs page rendered
content:
text/html:
schema:
type: string
'403':
description: Forbidden (user does not have access)
'404':
description: Namespace not found
/webhook:
post:
summary: Submit Log Entry
description: Allows agents to submit log entries via JWT authentication.
security:
- agentAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
message:
type: string
level:
type: string
metadata:
type: object
required:
- message
responses:
'201':
description: Log entry created
'400':
description: Bad Request (e.g., message missing)
'401':
description: Unauthorized (missing or invalid JWT)
'404':
description: Namespace or agent not found
components:
securitySchemes:
@ -470,3 +415,8 @@ components:
in: cookie
name: session
description: Session cookie for authenticated users
agentAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT authentication for agents

View file

@ -7,6 +7,8 @@ pyramid_retry
pyramid_jwt
pyramid_openapi3
pyramid_tm
zope.sqlalchemy

View file

@ -9,7 +9,7 @@
# Variables
BASE_URL="http://127.0.0.1:6544"
JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2VudF9pZCI6IjcxZmQ0ZjA0LTQ2NzgtNGEyNi05ZmU4LTkxNjU2OTNmOTBmOSIsImFnZW50X25hbWUiOiJ0ZXN0LTkwMDAiLCJuYW1lc3BhY2VfaWQiOiJhZGQwYWQ1YS0zNjE0LTRhODgtOTRmZC0xNGQ4ZDBkYjIxYjUiLCJ0b2tlbl92ZXJzaW9uIjoyLCJpYXQiOjE3MzY2NDUxMDF9.WPsitdEYcJ6gpvExO8BmddJdmqBlczqQAD1SzHB0Y1M"
JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2VudF9pZCI6IjcxZmQ0ZjA0LTQ2NzgtNGEyNi05ZmU4LTkxNjU2OTNmOTBmOSIsImFnZW50X25hbWUiOiJ0ZXN0LTkwMDAiLCJuYW1lc3BhY2VfaWQiOiJhZGQwYWQ1YS0zNjE0LTRhODgtOTRmZC0xNGQ4ZDBkYjIxYjUiLCJ0b2tlbl92ZXJzaW9uIjozLCJpYXQiOjE3MzY2NDYxNzV9.BzTXP6W6dcKQEwB970zTT5JPaJJsb0rybQv774wQy-s"
# Log message from the first argument
LOG_MESSAGE="$1"