feat: add orgid field to hermes_services for organization-scoped service isolation
- Add orgid field (str32, not nullable) to hermes_services table - Replace user_id with orgid in all service CRUD operations (SQL + functions) - Update function signatures: get_all_services, create_service, delete_service, get_service_by_id, test_service_connection, create_session, send_message_to_service, get_session_messages all use orgid - Add orgid indexes: idx_hermes_services_orgid, idx_hermes_services_orgid_status - Add logined_userorgid filtering to CRUD definition for automatic framework-level isolation - Update all .dspy files to use get_userorgid() for org-scoped service queries - Update init/data.json and db_tables.py to reflect orgid field
This commit is contained in:
parent
4df2f72758
commit
0e0ee695e6
@ -13,43 +13,43 @@ SERVICES_CRUD = {
|
||||
"operations": {
|
||||
"create": {
|
||||
"name": "create_service_record",
|
||||
"description": "Create a new service record for the current user",
|
||||
"parameters": ["user_id", "name", "service_url", "description", "apikey"],
|
||||
"description": "Create a new service record for the current organization",
|
||||
"parameters": ["orgid", "name", "service_url", "description", "apikey"],
|
||||
"sql_template": """
|
||||
INSERT INTO services (id, user_id, name, service_url, description, apikey, status, created_at, updated_at)
|
||||
VALUES (${id}$, ${user_id}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
INSERT INTO services (id, orgid, name, service_url, description, apikey, status, created_at, updated_at)
|
||||
VALUES (${id}$, ${orgid}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""",
|
||||
"return_fields": ["id"]
|
||||
},
|
||||
"read_all": {
|
||||
"name": "get_all_services_for_user",
|
||||
"description": "Get all services for the current user",
|
||||
"parameters": ["user_id"],
|
||||
"name": "get_all_services_for_org",
|
||||
"description": "Get all services for the current organization",
|
||||
"parameters": ["orgid"],
|
||||
"sql_template": """
|
||||
SELECT id, user_id, name, service_url, description, apikey, status,
|
||||
SELECT id, orgid, name, service_url, description, apikey, status,
|
||||
created_at, updated_at
|
||||
FROM services
|
||||
WHERE user_id = ${user_id}$
|
||||
WHERE orgid = ${orgid}$
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
"return_fields": ["id", "user_id", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"]
|
||||
"return_fields": ["id", "orgid", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"]
|
||||
},
|
||||
"read_by_id": {
|
||||
"name": "get_service_by_id_and_user",
|
||||
"description": "Get a specific service by ID for the current user",
|
||||
"parameters": ["service_id", "user_id"],
|
||||
"name": "get_service_by_id_and_org",
|
||||
"description": "Get a specific service by ID for the current organization",
|
||||
"parameters": ["service_id", "orgid"],
|
||||
"sql_template": """
|
||||
SELECT id, user_id, name, service_url, description, apikey, status,
|
||||
SELECT id, orgid, name, service_url, description, apikey, status,
|
||||
created_at, updated_at
|
||||
FROM services
|
||||
WHERE id = ${service_id}$ AND user_id = ${user_id}$
|
||||
WHERE id = ${service_id}$ AND orgid = ${orgid}$
|
||||
""",
|
||||
"return_fields": ["id", "user_id", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"]
|
||||
"return_fields": ["id", "orgid", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"]
|
||||
},
|
||||
"update": {
|
||||
"name": "update_service_record",
|
||||
"description": "Update an existing service record",
|
||||
"parameters": ["service_id", "user_id", "name", "service_url", "description", "apikey", "status"],
|
||||
"parameters": ["service_id", "orgid", "name", "service_url", "description", "apikey", "status"],
|
||||
"sql_template": """
|
||||
UPDATE services
|
||||
SET name = ${name}$,
|
||||
@ -58,17 +58,17 @@ SERVICES_CRUD = {
|
||||
apikey = ${apikey}$,
|
||||
status = ${status}$,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ${service_id}$ AND user_id = ${user_id}$
|
||||
WHERE id = ${service_id}$ AND orgid = ${orgid}$
|
||||
""",
|
||||
"return_fields": []
|
||||
},
|
||||
"delete": {
|
||||
"name": "delete_service_record",
|
||||
"description": "Delete a service record for the current user",
|
||||
"parameters": ["service_id", "user_id"],
|
||||
"description": "Delete a service record for the current organization",
|
||||
"parameters": ["service_id", "orgid"],
|
||||
"sql_template": """
|
||||
DELETE FROM services
|
||||
WHERE id = ${service_id}$ AND user_id = ${user_id}$
|
||||
WHERE id = ${service_id}$ AND orgid = ${orgid}$
|
||||
""",
|
||||
"return_fields": []
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ Follows database-table-definition-spec: summary/fields/indexes/codes four-sectio
|
||||
SERVICES_TABLE = {
|
||||
"summary": {
|
||||
"name": "services",
|
||||
"description": "Stores Hermes service configurations for each user",
|
||||
"description": "Stores Hermes service configurations for each organization",
|
||||
"module": "hermes-web-cli"
|
||||
},
|
||||
"fields": [
|
||||
@ -19,10 +19,10 @@ SERVICES_TABLE = {
|
||||
"description": "Unique service identifier (UUID)"
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"name": "orgid",
|
||||
"type": "varchar(64)",
|
||||
"nullable": False,
|
||||
"description": "Owner user ID for multi-user isolation"
|
||||
"description": "Organization ID that owns this service"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
@ -73,10 +73,10 @@ SERVICES_TABLE = {
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_services_user_id",
|
||||
"fields": ["user_id"],
|
||||
"name": "idx_services_orgid",
|
||||
"fields": ["orgid"],
|
||||
"unique": False,
|
||||
"description": "Index for user-based queries"
|
||||
"description": "Index for org-based queries"
|
||||
},
|
||||
{
|
||||
"name": "idx_services_status",
|
||||
@ -85,10 +85,10 @@ SERVICES_TABLE = {
|
||||
"description": "Index for status-based queries"
|
||||
},
|
||||
{
|
||||
"name": "idx_services_user_status",
|
||||
"fields": ["user_id", "status"],
|
||||
"name": "idx_services_orgid_status",
|
||||
"fields": ["orgid", "status"],
|
||||
"unique": False,
|
||||
"description": "Composite index for user and status queries"
|
||||
"description": "Composite index for org and status queries"
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
|
||||
@ -10,13 +10,14 @@ implement these endpoints by calling the functions provided in this module.
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
# Import sqlor database module
|
||||
from sqlor.dbpools import get_sor_context
|
||||
from sqlor.dbpools import get_sor_context, DBPools
|
||||
|
||||
# Import database table definitions and CRUD operations
|
||||
from .db_tables import TABLE_DEFINITIONS
|
||||
@ -54,23 +55,23 @@ def load_hermes_web_cli():
|
||||
return True
|
||||
|
||||
# Database operations using sqlor-database-module
|
||||
async def get_all_services(user_id: str) -> List[Dict]:
|
||||
"""Get all registered Hermes services for the specified user from database.
|
||||
async def get_all_services(orgid: str) -> List[Dict]:
|
||||
"""Get all registered Hermes services for the specified organization from database.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user whose services to retrieve
|
||||
orgid: The ID of the organization whose services to retrieve
|
||||
|
||||
Returns:
|
||||
List of service dictionaries belonging to the specified user
|
||||
List of service dictionaries belonging to the specified organization
|
||||
"""
|
||||
try:
|
||||
# Query services table with user_id filter using sqlor-database-module
|
||||
# Query services table with orgid filter using sqlor-database-module
|
||||
db = DBPools()
|
||||
env = ServerEnv()
|
||||
dbname = env.get_module_dbname()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
sql_template = SERVICES_CRUD['operations']['read_all']['sql_template']
|
||||
recs = await sor.sqlExe(sql_template, {'user_id': user_id})
|
||||
recs = await sor.sqlExe(sql_template, {'orgid': orgid})
|
||||
|
||||
# Convert datetime objects to ISO format strings for JSON serialization
|
||||
result = []
|
||||
@ -88,13 +89,13 @@ async def get_all_services(user_id: str) -> List[Dict]:
|
||||
print(f"Error getting services: {str(e)}")
|
||||
return []
|
||||
|
||||
async def create_service(name: str, url: str, user_id: str, description: str = "", apikey: str = "") -> str:
|
||||
"""Create a new Hermes service registration for the specified user.
|
||||
async def create_service(name: str, url: str, orgid: str, description: str = "", apikey: str = "") -> str:
|
||||
"""Create a new Hermes service registration for the specified organization.
|
||||
|
||||
Args:
|
||||
name: Service name
|
||||
url: Service URL
|
||||
user_id: The ID of the user creating the service
|
||||
orgid: The ID of the organization creating the service
|
||||
description: Service description (optional)
|
||||
apikey: API key for the service (optional)
|
||||
|
||||
@ -116,7 +117,7 @@ async def create_service(name: str, url: str, user_id: str, description: str = "
|
||||
sql_template = SERVICES_CRUD['operations']['create']['sql_template']
|
||||
await sor.sqlExe(sql_template, {
|
||||
'id': service_id,
|
||||
'user_id': user_id,
|
||||
'orgid': orgid,
|
||||
'name': name,
|
||||
'service_url': url,
|
||||
'description': description,
|
||||
@ -130,24 +131,24 @@ async def create_service(name: str, url: str, user_id: str, description: str = "
|
||||
print(f"Error creating service: {str(e)}")
|
||||
raise
|
||||
|
||||
async def delete_service(service_id: str, user_id: str) -> bool:
|
||||
"""Delete a Hermes service registration (only if owned by specified user).
|
||||
async def delete_service(service_id: str, orgid: str) -> bool:
|
||||
"""Delete a Hermes service registration (only if owned by specified organization).
|
||||
|
||||
Args:
|
||||
service_id: The ID of the service to delete
|
||||
user_id: The ID of the user attempting deletion
|
||||
orgid: The ID of the organization attempting deletion
|
||||
|
||||
Returns:
|
||||
True if deleted successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Verify service belongs to current user before deletion
|
||||
service = await get_service_by_id(service_id, user_id)
|
||||
# Verify service belongs to current org before deletion
|
||||
service = await get_service_by_id(service_id, orgid)
|
||||
if not service:
|
||||
return False
|
||||
|
||||
if service.get("user_id") != user_id:
|
||||
print(f"Permission denied: Service {service_id} does not belong to user {user_id}")
|
||||
if service.get("orgid") != orgid:
|
||||
print(f"Permission denied: Service {service_id} does not belong to org {orgid}")
|
||||
return False
|
||||
|
||||
# Delete from database using sqlor-database-module
|
||||
@ -158,7 +159,7 @@ async def delete_service(service_id: str, user_id: str) -> bool:
|
||||
sql_template = SERVICES_CRUD['operations']['delete']['sql_template']
|
||||
await sor.sqlExe(sql_template, {
|
||||
'service_id': service_id,
|
||||
'user_id': user_id
|
||||
'orgid': orgid
|
||||
})
|
||||
|
||||
# Also delete associated sessions
|
||||
@ -167,10 +168,9 @@ async def delete_service(service_id: str, user_id: str) -> bool:
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("""
|
||||
DELETE FROM sessions
|
||||
WHERE service_id = ${service_id}$ AND user_id = ${user_id}$
|
||||
WHERE service_id = ${service_id}$
|
||||
""", {
|
||||
'service_id': service_id,
|
||||
'user_id': user_id
|
||||
'service_id': service_id
|
||||
})
|
||||
|
||||
return True
|
||||
@ -178,18 +178,18 @@ async def delete_service(service_id: str, user_id: str) -> bool:
|
||||
print(f"Error deleting service: {str(e)}")
|
||||
return False
|
||||
|
||||
async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
|
||||
"""Get service configuration by ID (only if owned by specified user).
|
||||
async def get_service_by_id(service_id: str, orgid: str) -> Optional[Dict]:
|
||||
"""Get service configuration by ID (only if owned by specified organization).
|
||||
|
||||
Args:
|
||||
service_id: The ID of the service to retrieve
|
||||
user_id: The ID of the user requesting the service
|
||||
orgid: The ID of the organization requesting the service
|
||||
|
||||
Returns:
|
||||
Service dictionary if found and owned by user, None otherwise
|
||||
Service dictionary if found and owned by org, None otherwise
|
||||
"""
|
||||
try:
|
||||
# Query database directly with user_id filter for security
|
||||
# Query database directly with orgid filter for security
|
||||
db = DBPools()
|
||||
env = ServerEnv()
|
||||
dbname = env.get_module_dbname()
|
||||
@ -197,7 +197,7 @@ async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
|
||||
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
|
||||
recs = await sor.sqlExe(sql_template, {
|
||||
'service_id': service_id,
|
||||
'user_id': user_id
|
||||
'orgid': orgid
|
||||
})
|
||||
|
||||
if len(recs) > 0:
|
||||
@ -215,11 +215,12 @@ async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
|
||||
return None
|
||||
|
||||
# Service connection testing
|
||||
async def test_service_connection(service_id: str) -> Tuple[bool, str]:
|
||||
async def test_service_connection(service_id: str, orgid: str = "") -> Tuple[bool, str]:
|
||||
"""Test connection to a Hermes service endpoint.
|
||||
|
||||
Args:
|
||||
service_id: The ID of the service to test
|
||||
orgid: The ID of the organization (optional, for org-scoped lookup)
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (is_connected, status_message)
|
||||
@ -230,11 +231,21 @@ async def test_service_connection(service_id: str) -> Tuple[bool, str]:
|
||||
env = ServerEnv()
|
||||
dbname = env.get_module_dbname()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
|
||||
recs = await sor.sqlExe(sql_template, {
|
||||
'service_id': service_id,
|
||||
'user_id': ''
|
||||
})
|
||||
if orgid:
|
||||
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
|
||||
recs = await sor.sqlExe(sql_template, {
|
||||
'service_id': service_id,
|
||||
'orgid': orgid
|
||||
})
|
||||
else:
|
||||
recs = await sor.sqlExe("""
|
||||
SELECT id, orgid, name, service_url, description, apikey, status,
|
||||
created_at, updated_at
|
||||
FROM services
|
||||
WHERE id = ${service_id}$
|
||||
""", {
|
||||
'service_id': service_id
|
||||
})
|
||||
if not recs:
|
||||
return False, "Service not found"
|
||||
service = dict(recs[0])
|
||||
@ -263,11 +274,11 @@ async def test_service_connection(service_id: str) -> Tuple[bool, str]:
|
||||
return False, f"Error: {str(e)}"
|
||||
|
||||
# Session management
|
||||
async def create_session(service_id: str, user_id: str, user_message: str = "") -> str:
|
||||
async def create_session(service_id: str, user_id: str, orgid: str, user_message: str = "") -> str:
|
||||
"""Create a new session with a Hermes service."""
|
||||
try:
|
||||
# Get service configuration (verify it belongs to current user)
|
||||
service = await get_service_by_id(service_id, user_id)
|
||||
# Get service configuration (verify it belongs to current org)
|
||||
service = await get_service_by_id(service_id, orgid)
|
||||
if not service:
|
||||
raise ValueError(f"Service {service_id} not found or access denied")
|
||||
|
||||
@ -323,7 +334,7 @@ async def create_session(service_id: str, user_id: str, user_message: str = "")
|
||||
print(f"Error creating session: {str(e)}")
|
||||
raise
|
||||
|
||||
async def send_message_to_service(service_id: str, session_id: str, message: str, user_id: str) -> Dict:
|
||||
async def send_message_to_service(service_id: str, session_id: str, message: str, user_id: str, orgid: str) -> Dict:
|
||||
"""Send a message to a Hermes service and get response (only if session owned by specified user).
|
||||
|
||||
Args:
|
||||
@ -331,6 +342,7 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
|
||||
session_id: The session ID
|
||||
message: The message to send
|
||||
user_id: The ID of the user sending the message
|
||||
orgid: The ID of the organization
|
||||
|
||||
Returns:
|
||||
Response from the service
|
||||
@ -341,9 +353,9 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
|
||||
if not session:
|
||||
raise ValueError(f"Session {session_id} not found or access denied for user {user_id}")
|
||||
|
||||
service = await get_service_by_id(session['service_id'], user_id)
|
||||
service = await get_service_by_id(session['service_id'], orgid)
|
||||
if not service:
|
||||
raise ValueError(f"Service for session {session_id} not found or access denied for user {user_id}")
|
||||
raise ValueError(f"Service for session {session_id} not found or access denied for org {orgid}")
|
||||
|
||||
service_url = service["service_url"]
|
||||
apikey = service.get("apikey", "")
|
||||
@ -373,25 +385,26 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
|
||||
print(f"Error sending message: {e}")
|
||||
raise
|
||||
|
||||
async def get_session_messages(session_id: str, user_id: str) -> List[Dict]:
|
||||
async def get_session_messages(session_id: str, user_id: str, orgid: str) -> List[Dict]:
|
||||
"""Get all messages for a session (only if session owned by specified user).
|
||||
|
||||
Args:
|
||||
session_id: The session ID
|
||||
user_id: The ID of the user requesting messages
|
||||
orgid: The ID of the organization
|
||||
|
||||
Returns:
|
||||
List of message dictionaries
|
||||
"""
|
||||
try:
|
||||
# Verify session belongs to current user before getting messages
|
||||
session = await get_session_by_id(session_id)
|
||||
session = await get_session_by_id(session_id, user_id)
|
||||
if not session:
|
||||
print(f"Session {session_id} not found or access denied for user {user_id}")
|
||||
return []
|
||||
|
||||
# Get the associated service
|
||||
service = await get_service_by_id(session['service_id'], user_id)
|
||||
# Get the associated service (verify org access)
|
||||
service = await get_service_by_id(session['service_id'], orgid)
|
||||
if not service:
|
||||
print(f"Service for session {session_id} not found or access denied")
|
||||
return []
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
"hermes_services": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
"user_id": null,
|
||||
"orgid": "",
|
||||
"name": "Local Hermes Service",
|
||||
"service_url": "http://localhost:9120",
|
||||
"api_key": null,
|
||||
"apikey": "",
|
||||
"description": "Default local Hermes service instance",
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
"title": "Hermes Services",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"confidential_fields": [],
|
||||
"logined_userorgid": "orgid",
|
||||
"confidential_fields": ["apikey"],
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "service_url", "created_at", "updated_at"],
|
||||
"alters": {
|
||||
|
||||
@ -16,6 +16,14 @@
|
||||
"nullable": "no",
|
||||
"comments": "Primary key - UUID format"
|
||||
},
|
||||
{
|
||||
"name": "orgid",
|
||||
"title": "Organization ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"comments": "Organization ID that owns this service"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "Service Name",
|
||||
@ -72,6 +80,11 @@
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_hermes_services_orgid",
|
||||
"idxtype": "index",
|
||||
"idxfields": ["orgid"]
|
||||
},
|
||||
{
|
||||
"name": "idx_hermes_services_name",
|
||||
"idxtype": "index",
|
||||
@ -81,6 +94,11 @@
|
||||
"name": "idx_hermes_services_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": ["status"]
|
||||
},
|
||||
{
|
||||
"name": "idx_hermes_services_orgid_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": ["orgid", "status"]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
|
||||
191
test_orgid_refactor.py
Normal file
191
test_orgid_refactor.py
Normal file
@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify orgid refactoring of hermes_services table.
|
||||
Validates: function signatures, SQL templates, JSON definitions, .dspy files.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import inspect
|
||||
|
||||
MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def test_table_definition():
|
||||
"""Verify models/hermes_services.json has orgid field and correct indexes."""
|
||||
path = os.path.join(MODULE_DIR, 'models', 'hermes_services.json')
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
fields = {f['name']: f for f in data['fields']}
|
||||
|
||||
# orgid field exists with correct properties
|
||||
assert 'orgid' in fields, "orgid field missing from table definition"
|
||||
assert fields['orgid']['type'] == 'str', "orgid type should be str"
|
||||
assert fields['orgid']['length'] == 32, "orgid length should be 32"
|
||||
assert fields['orgid']['nullable'] == 'no', "orgid should be not nullable"
|
||||
|
||||
# user_id should NOT exist
|
||||
assert 'user_id' not in fields, "user_id field should be removed"
|
||||
|
||||
# orgid indexes exist
|
||||
index_names = [i['name'] for i in data['indexes']]
|
||||
assert 'idx_hermes_services_orgid' in index_names, "missing idx_hermes_services_orgid"
|
||||
assert 'idx_hermes_services_orgid_status' in index_names, "missing idx_hermes_services_orgid_status"
|
||||
|
||||
# old user indexes removed
|
||||
assert 'idx_hermes_services_user_id' not in index_names, "old user index should be removed"
|
||||
|
||||
print(" table definition OK")
|
||||
|
||||
def test_crud_definition():
|
||||
"""Verify json/hermes_services.json has logined_userorgid param."""
|
||||
path = os.path.join(MODULE_DIR, 'json', 'hermes_services.json')
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
params = data['params']
|
||||
assert params.get('logined_userorgid') == 'orgid', "logined_userorgid should be 'orgid'"
|
||||
assert 'apikey' in params.get('confidential_fields', []), "apikey should be confidential"
|
||||
print(" CRUD definition OK")
|
||||
|
||||
def test_db_tables():
|
||||
"""Verify db_tables.py SERVICES_TABLE uses orgid."""
|
||||
path = os.path.join(MODULE_DIR, 'hermes_web_cli', 'db_tables.py')
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
assert '"orgid"' in content, "db_tables.py should contain orgid"
|
||||
assert 'idx_services_orgid' in content, "should have idx_services_orgid index"
|
||||
assert 'idx_services_orgid_status' in content, "should have idx_services_orgid_status index"
|
||||
# old user_id references in SERVICES_TABLE should be gone
|
||||
svc_start = content.index('SERVICES_TABLE = {')
|
||||
svc_end = content.index('# Sessions table', svc_start)
|
||||
svc_section = content[svc_start:svc_end]
|
||||
assert '"user_id"' not in svc_section, "SERVICES_TABLE should not have user_id"
|
||||
print(" db_tables.py OK")
|
||||
|
||||
def test_crud_ops():
|
||||
"""Verify crud_ops.py SERVICES_CRUD SQL uses orgid."""
|
||||
path = os.path.join(MODULE_DIR, 'hermes_web_cli', 'crud_ops.py')
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
svc_start = content.index('SERVICES_CRUD = {')
|
||||
svc_end = content.index('# Sessions CRUD', svc_start)
|
||||
svc_section = content[svc_start:svc_end]
|
||||
|
||||
assert '${orgid}$' in svc_section, "SQL should use ${orgid}$ parameter"
|
||||
assert '${user_id}$' not in svc_section, "SQL should not use ${user_id}$ parameter"
|
||||
assert 'orgid = ${orgid}$' in svc_section or 'AND orgid = ${orgid}$' in svc_section, "WHERE should filter by orgid"
|
||||
print(" crud_ops.py OK")
|
||||
|
||||
def test_init_signatures():
|
||||
"""Verify init.py function signatures use orgid."""
|
||||
path = os.path.join(MODULE_DIR, 'hermes_web_cli', 'init.py')
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Check function signatures
|
||||
assert 'async def get_all_services(orgid: str)' in content, "get_all_services should take orgid"
|
||||
assert 'async def create_service(name: str, url: str, orgid: str' in content, "create_service should take orgid"
|
||||
assert 'async def delete_service(service_id: str, orgid: str)' in content, "delete_service should take orgid"
|
||||
assert 'async def get_service_by_id(service_id: str, orgid: str)' in content, "get_service_by_id should take orgid"
|
||||
assert 'test_service_connection(service_id: str, orgid: str = "")' in content, "test_service_connection should take optional orgid"
|
||||
assert 'async def create_session(service_id: str, user_id: str, orgid: str' in content, "create_session should take orgid"
|
||||
assert 'send_message_to_service(service_id: str, session_id: str, message: str, user_id: str, orgid: str)' in content, "send_message_to_service should take orgid"
|
||||
assert 'async def get_session_messages(session_id: str, user_id: str, orgid: str)' in content, "get_session_messages should take orgid"
|
||||
|
||||
# Verify SQL calls use orgid param
|
||||
assert "'orgid': orgid" in content, "SQL params should pass orgid"
|
||||
assert "'orgid': user_id" not in content, "should not pass user_id as orgid"
|
||||
print(" init.py signatures OK")
|
||||
|
||||
def test_dspy_files():
|
||||
"""Verify .dspy files use get_userorgid()."""
|
||||
files_to_check = [
|
||||
'wwwroot/services/list/index.dspy',
|
||||
'wwwroot/services/test/index.dspy',
|
||||
'wwwroot/services/remove/index.dspy',
|
||||
'wwwroot/sessions/create_session.dspy',
|
||||
]
|
||||
|
||||
for fpath in files_to_check:
|
||||
full_path = os.path.join(MODULE_DIR, fpath)
|
||||
with open(full_path) as f:
|
||||
content = f.read()
|
||||
assert 'get_userorgid()' in content, f"{fpath} should call get_userorgid()"
|
||||
|
||||
# services/list should pass orgid to get_all_services
|
||||
with open(os.path.join(MODULE_DIR, 'wwwroot/services/list/index.dspy')) as f:
|
||||
content = f.read()
|
||||
assert 'get_all_services(orgid)' in content, "services/list should pass orgid to get_all_services"
|
||||
|
||||
# services/remove should pass orgid to delete_service
|
||||
with open(os.path.join(MODULE_DIR, 'wwwroot/services/remove/index.dspy')) as f:
|
||||
content = f.read()
|
||||
assert 'delete_service(service_id, orgid)' in content, "services/remove should pass orgid to delete_service"
|
||||
|
||||
# create_session should pass both user_id and orgid
|
||||
with open(os.path.join(MODULE_DIR, 'wwwroot/sessions/create_session.dspy')) as f:
|
||||
content = f.read()
|
||||
assert 'create_session(service_id, user_id, orgid' in content, "create_session should pass both user_id and orgid"
|
||||
|
||||
print(" .dspy files OK")
|
||||
|
||||
def test_init_data():
|
||||
"""Verify init/data.json uses orgid field."""
|
||||
path = os.path.join(MODULE_DIR, 'init', 'data.json')
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
for svc in data.get('hermes_services', []):
|
||||
assert 'orgid' in svc, "init data should have orgid field"
|
||||
assert 'user_id' not in svc, "init data should not have user_id field"
|
||||
print(" init/data.json OK")
|
||||
|
||||
def test_syntax():
|
||||
"""Verify all Python files compile."""
|
||||
import py_compile
|
||||
for fpath in ['hermes_web_cli/init.py', 'hermes_web_cli/crud_ops.py', 'hermes_web_cli/db_tables.py']:
|
||||
full_path = os.path.join(MODULE_DIR, fpath)
|
||||
try:
|
||||
py_compile.compile(full_path, doraise=True)
|
||||
except py_compile.PyCompileError as e:
|
||||
assert False, f"Syntax error in {fpath}: {e}"
|
||||
print(" Python syntax OK")
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [
|
||||
("Syntax check", test_syntax),
|
||||
("Table definition", test_table_definition),
|
||||
("CRUD definition", test_crud_definition),
|
||||
("db_tables.py", test_db_tables),
|
||||
("crud_ops.py", test_crud_ops),
|
||||
("init.py signatures", test_init_signatures),
|
||||
(".dspy files", test_dspy_files),
|
||||
("init/data.json", test_init_data),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for name, test_fn in tests:
|
||||
try:
|
||||
test_fn()
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f" FAILED: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Results: {passed} passed, {failed} failed out of {len(tests)} tests")
|
||||
if failed > 0:
|
||||
print("FAILED - do not commit")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("ALL TESTS PASSED")
|
||||
sys.exit(0)
|
||||
@ -2,11 +2,11 @@
|
||||
# This .dspy file uses functions provided by load_hermes_web_cli()
|
||||
|
||||
try:
|
||||
# Get current user ID using ahserver's built-in get_user() function
|
||||
user_id = await get_user()
|
||||
# Get current user's org ID using ahserver's built-in get_userorgid() function
|
||||
orgid = await get_userorgid()
|
||||
|
||||
# Use the function provided by the hermes-web-cli module
|
||||
services = await get_all_services(user_id)
|
||||
services = await get_all_services(orgid)
|
||||
|
||||
# Format services for UI display
|
||||
result = []
|
||||
|
||||
@ -6,11 +6,11 @@ try:
|
||||
if not service_id:
|
||||
return {"error": "Service ID is required"}
|
||||
|
||||
# Get current user ID using ahserver's built-in get_user() function
|
||||
user_id = await get_user()
|
||||
# Get current user's org ID using ahserver's built-in get_userorgid() function
|
||||
orgid = await get_userorgid()
|
||||
|
||||
# Call the function provided by the hermes-web-cli module
|
||||
success = await delete_service(service_id, user_id)
|
||||
success = await delete_service(service_id, orgid)
|
||||
if success:
|
||||
return {"success": True, "message": "Service removed successfully"}
|
||||
else:
|
||||
|
||||
@ -6,8 +6,11 @@ try:
|
||||
if not service_id:
|
||||
return {"error": "Service ID is required"}
|
||||
|
||||
# Get current user's org ID
|
||||
orgid = await get_userorgid()
|
||||
|
||||
# Call the function provided by the hermes-web-cli module
|
||||
is_connected, status_msg = await test_service_connection(service_id)
|
||||
is_connected, status_msg = await test_service_connection(service_id, orgid)
|
||||
|
||||
return {"status": status_msg}
|
||||
|
||||
|
||||
@ -17,12 +17,13 @@ try:
|
||||
# Redirect back to form with error
|
||||
return {"redirect": "/hermes-web-cli/new_session.ui?error=Service+ID+is+required"}
|
||||
|
||||
# Get current user ID using ahserver's built-in get_user() function
|
||||
# Get current user ID and org ID
|
||||
user_id = await get_user()
|
||||
orgid = await get_userorgid()
|
||||
|
||||
# Use the function provided by the hermes-web-cli module
|
||||
# This will call the remote hermes-service API with user_id support
|
||||
session_result = await create_session(service_id, user_id, "")
|
||||
# Service lookup uses orgid, session ownership uses user_id
|
||||
session_result = await create_session(service_id, user_id, orgid, "")
|
||||
|
||||
if session_result:
|
||||
# Redirect to the new session's user interaction page
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user