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": {
|
"operations": {
|
||||||
"create": {
|
"create": {
|
||||||
"name": "create_service_record",
|
"name": "create_service_record",
|
||||||
"description": "Create a new service record for the current user",
|
"description": "Create a new service record for the current organization",
|
||||||
"parameters": ["user_id", "name", "service_url", "description", "apikey"],
|
"parameters": ["orgid", "name", "service_url", "description", "apikey"],
|
||||||
"sql_template": """
|
"sql_template": """
|
||||||
INSERT INTO services (id, user_id, name, service_url, description, apikey, status, created_at, updated_at)
|
INSERT INTO services (id, orgid, name, service_url, description, apikey, status, created_at, updated_at)
|
||||||
VALUES (${id}$, ${user_id}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (${id}$, ${orgid}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
""",
|
""",
|
||||||
"return_fields": ["id"]
|
"return_fields": ["id"]
|
||||||
},
|
},
|
||||||
"read_all": {
|
"read_all": {
|
||||||
"name": "get_all_services_for_user",
|
"name": "get_all_services_for_org",
|
||||||
"description": "Get all services for the current user",
|
"description": "Get all services for the current organization",
|
||||||
"parameters": ["user_id"],
|
"parameters": ["orgid"],
|
||||||
"sql_template": """
|
"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
|
created_at, updated_at
|
||||||
FROM services
|
FROM services
|
||||||
WHERE user_id = ${user_id}$
|
WHERE orgid = ${orgid}$
|
||||||
ORDER BY created_at DESC
|
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": {
|
"read_by_id": {
|
||||||
"name": "get_service_by_id_and_user",
|
"name": "get_service_by_id_and_org",
|
||||||
"description": "Get a specific service by ID for the current user",
|
"description": "Get a specific service by ID for the current organization",
|
||||||
"parameters": ["service_id", "user_id"],
|
"parameters": ["service_id", "orgid"],
|
||||||
"sql_template": """
|
"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
|
created_at, updated_at
|
||||||
FROM services
|
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": {
|
"update": {
|
||||||
"name": "update_service_record",
|
"name": "update_service_record",
|
||||||
"description": "Update an existing 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": """
|
"sql_template": """
|
||||||
UPDATE services
|
UPDATE services
|
||||||
SET name = ${name}$,
|
SET name = ${name}$,
|
||||||
@ -58,17 +58,17 @@ SERVICES_CRUD = {
|
|||||||
apikey = ${apikey}$,
|
apikey = ${apikey}$,
|
||||||
status = ${status}$,
|
status = ${status}$,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ${service_id}$ AND user_id = ${user_id}$
|
WHERE id = ${service_id}$ AND orgid = ${orgid}$
|
||||||
""",
|
""",
|
||||||
"return_fields": []
|
"return_fields": []
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"name": "delete_service_record",
|
"name": "delete_service_record",
|
||||||
"description": "Delete a service record for the current user",
|
"description": "Delete a service record for the current organization",
|
||||||
"parameters": ["service_id", "user_id"],
|
"parameters": ["service_id", "orgid"],
|
||||||
"sql_template": """
|
"sql_template": """
|
||||||
DELETE FROM services
|
DELETE FROM services
|
||||||
WHERE id = ${service_id}$ AND user_id = ${user_id}$
|
WHERE id = ${service_id}$ AND orgid = ${orgid}$
|
||||||
""",
|
""",
|
||||||
"return_fields": []
|
"return_fields": []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ Follows database-table-definition-spec: summary/fields/indexes/codes four-sectio
|
|||||||
SERVICES_TABLE = {
|
SERVICES_TABLE = {
|
||||||
"summary": {
|
"summary": {
|
||||||
"name": "services",
|
"name": "services",
|
||||||
"description": "Stores Hermes service configurations for each user",
|
"description": "Stores Hermes service configurations for each organization",
|
||||||
"module": "hermes-web-cli"
|
"module": "hermes-web-cli"
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -19,10 +19,10 @@ SERVICES_TABLE = {
|
|||||||
"description": "Unique service identifier (UUID)"
|
"description": "Unique service identifier (UUID)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "user_id",
|
"name": "orgid",
|
||||||
"type": "varchar(64)",
|
"type": "varchar(64)",
|
||||||
"nullable": False,
|
"nullable": False,
|
||||||
"description": "Owner user ID for multi-user isolation"
|
"description": "Organization ID that owns this service"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
@ -73,10 +73,10 @@ SERVICES_TABLE = {
|
|||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"name": "idx_services_user_id",
|
"name": "idx_services_orgid",
|
||||||
"fields": ["user_id"],
|
"fields": ["orgid"],
|
||||||
"unique": False,
|
"unique": False,
|
||||||
"description": "Index for user-based queries"
|
"description": "Index for org-based queries"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_services_status",
|
"name": "idx_services_status",
|
||||||
@ -85,10 +85,10 @@ SERVICES_TABLE = {
|
|||||||
"description": "Index for status-based queries"
|
"description": "Index for status-based queries"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_services_user_status",
|
"name": "idx_services_orgid_status",
|
||||||
"fields": ["user_id", "status"],
|
"fields": ["orgid", "status"],
|
||||||
"unique": False,
|
"unique": False,
|
||||||
"description": "Composite index for user and status queries"
|
"description": "Composite index for org and status queries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codes": []
|
"codes": []
|
||||||
|
|||||||
@ -10,13 +10,14 @@ implement these endpoints by calling the functions provided in this module.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Import sqlor database module
|
# 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
|
# Import database table definitions and CRUD operations
|
||||||
from .db_tables import TABLE_DEFINITIONS
|
from .db_tables import TABLE_DEFINITIONS
|
||||||
@ -54,23 +55,23 @@ def load_hermes_web_cli():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Database operations using sqlor-database-module
|
# Database operations using sqlor-database-module
|
||||||
async def get_all_services(user_id: str) -> List[Dict]:
|
async def get_all_services(orgid: str) -> List[Dict]:
|
||||||
"""Get all registered Hermes services for the specified user from database.
|
"""Get all registered Hermes services for the specified organization from database.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The ID of the user whose services to retrieve
|
orgid: The ID of the organization whose services to retrieve
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of service dictionaries belonging to the specified user
|
List of service dictionaries belonging to the specified organization
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Query services table with user_id filter using sqlor-database-module
|
# Query services table with orgid filter using sqlor-database-module
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
dbname = env.get_module_dbname()
|
dbname = env.get_module_dbname()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
sql_template = SERVICES_CRUD['operations']['read_all']['sql_template']
|
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
|
# Convert datetime objects to ISO format strings for JSON serialization
|
||||||
result = []
|
result = []
|
||||||
@ -88,13 +89,13 @@ async def get_all_services(user_id: str) -> List[Dict]:
|
|||||||
print(f"Error getting services: {str(e)}")
|
print(f"Error getting services: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def create_service(name: str, url: str, user_id: str, description: str = "", apikey: str = "") -> str:
|
async def create_service(name: str, url: str, orgid: str, description: str = "", apikey: str = "") -> str:
|
||||||
"""Create a new Hermes service registration for the specified user.
|
"""Create a new Hermes service registration for the specified organization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Service name
|
name: Service name
|
||||||
url: Service URL
|
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)
|
description: Service description (optional)
|
||||||
apikey: API key for the service (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']
|
sql_template = SERVICES_CRUD['operations']['create']['sql_template']
|
||||||
await sor.sqlExe(sql_template, {
|
await sor.sqlExe(sql_template, {
|
||||||
'id': service_id,
|
'id': service_id,
|
||||||
'user_id': user_id,
|
'orgid': orgid,
|
||||||
'name': name,
|
'name': name,
|
||||||
'service_url': url,
|
'service_url': url,
|
||||||
'description': description,
|
'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)}")
|
print(f"Error creating service: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def delete_service(service_id: str, user_id: str) -> bool:
|
async def delete_service(service_id: str, orgid: str) -> bool:
|
||||||
"""Delete a Hermes service registration (only if owned by specified user).
|
"""Delete a Hermes service registration (only if owned by specified organization).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_id: The ID of the service to delete
|
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:
|
Returns:
|
||||||
True if deleted successfully, False otherwise
|
True if deleted successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Verify service belongs to current user before deletion
|
# Verify service belongs to current org before deletion
|
||||||
service = await get_service_by_id(service_id, user_id)
|
service = await get_service_by_id(service_id, orgid)
|
||||||
if not service:
|
if not service:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if service.get("user_id") != user_id:
|
if service.get("orgid") != orgid:
|
||||||
print(f"Permission denied: Service {service_id} does not belong to user {user_id}")
|
print(f"Permission denied: Service {service_id} does not belong to org {orgid}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Delete from database using sqlor-database-module
|
# 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']
|
sql_template = SERVICES_CRUD['operations']['delete']['sql_template']
|
||||||
await sor.sqlExe(sql_template, {
|
await sor.sqlExe(sql_template, {
|
||||||
'service_id': service_id,
|
'service_id': service_id,
|
||||||
'user_id': user_id
|
'orgid': orgid
|
||||||
})
|
})
|
||||||
|
|
||||||
# Also delete associated sessions
|
# 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:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
await sor.sqlExe("""
|
await sor.sqlExe("""
|
||||||
DELETE FROM sessions
|
DELETE FROM sessions
|
||||||
WHERE service_id = ${service_id}$ AND user_id = ${user_id}$
|
WHERE service_id = ${service_id}$
|
||||||
""", {
|
""", {
|
||||||
'service_id': service_id,
|
'service_id': service_id
|
||||||
'user_id': user_id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -178,18 +178,18 @@ async def delete_service(service_id: str, user_id: str) -> bool:
|
|||||||
print(f"Error deleting service: {str(e)}")
|
print(f"Error deleting service: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
|
async def get_service_by_id(service_id: str, orgid: str) -> Optional[Dict]:
|
||||||
"""Get service configuration by ID (only if owned by specified user).
|
"""Get service configuration by ID (only if owned by specified organization).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_id: The ID of the service to retrieve
|
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:
|
Returns:
|
||||||
Service dictionary if found and owned by user, None otherwise
|
Service dictionary if found and owned by org, None otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Query database directly with user_id filter for security
|
# Query database directly with orgid filter for security
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
dbname = env.get_module_dbname()
|
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']
|
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
|
||||||
recs = await sor.sqlExe(sql_template, {
|
recs = await sor.sqlExe(sql_template, {
|
||||||
'service_id': service_id,
|
'service_id': service_id,
|
||||||
'user_id': user_id
|
'orgid': orgid
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(recs) > 0:
|
if len(recs) > 0:
|
||||||
@ -215,11 +215,12 @@ async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Service connection testing
|
# 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.
|
"""Test connection to a Hermes service endpoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_id: The ID of the service to test
|
service_id: The ID of the service to test
|
||||||
|
orgid: The ID of the organization (optional, for org-scoped lookup)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[bool, str]: (is_connected, status_message)
|
Tuple[bool, str]: (is_connected, status_message)
|
||||||
@ -230,11 +231,21 @@ async def test_service_connection(service_id: str) -> Tuple[bool, str]:
|
|||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
dbname = env.get_module_dbname()
|
dbname = env.get_module_dbname()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
|
if orgid:
|
||||||
recs = await sor.sqlExe(sql_template, {
|
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
|
||||||
'service_id': service_id,
|
recs = await sor.sqlExe(sql_template, {
|
||||||
'user_id': ''
|
'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:
|
if not recs:
|
||||||
return False, "Service not found"
|
return False, "Service not found"
|
||||||
service = dict(recs[0])
|
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)}"
|
return False, f"Error: {str(e)}"
|
||||||
|
|
||||||
# Session management
|
# 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."""
|
"""Create a new session with a Hermes service."""
|
||||||
try:
|
try:
|
||||||
# Get service configuration (verify it belongs to current user)
|
# Get service configuration (verify it belongs to current org)
|
||||||
service = await get_service_by_id(service_id, user_id)
|
service = await get_service_by_id(service_id, orgid)
|
||||||
if not service:
|
if not service:
|
||||||
raise ValueError(f"Service {service_id} not found or access denied")
|
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)}")
|
print(f"Error creating session: {str(e)}")
|
||||||
raise
|
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).
|
"""Send a message to a Hermes service and get response (only if session owned by specified user).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -331,6 +342,7 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
|
|||||||
session_id: The session ID
|
session_id: The session ID
|
||||||
message: The message to send
|
message: The message to send
|
||||||
user_id: The ID of the user sending the message
|
user_id: The ID of the user sending the message
|
||||||
|
orgid: The ID of the organization
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response from the service
|
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:
|
if not session:
|
||||||
raise ValueError(f"Session {session_id} not found or access denied for user {user_id}")
|
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:
|
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"]
|
service_url = service["service_url"]
|
||||||
apikey = service.get("apikey", "")
|
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}")
|
print(f"Error sending message: {e}")
|
||||||
raise
|
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).
|
"""Get all messages for a session (only if session owned by specified user).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: The session ID
|
session_id: The session ID
|
||||||
user_id: The ID of the user requesting messages
|
user_id: The ID of the user requesting messages
|
||||||
|
orgid: The ID of the organization
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of message dictionaries
|
List of message dictionaries
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Verify session belongs to current user before getting messages
|
# 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:
|
if not session:
|
||||||
print(f"Session {session_id} not found or access denied for user {user_id}")
|
print(f"Session {session_id} not found or access denied for user {user_id}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get the associated service
|
# Get the associated service (verify org access)
|
||||||
service = await get_service_by_id(session['service_id'], user_id)
|
service = await get_service_by_id(session['service_id'], orgid)
|
||||||
if not service:
|
if not service:
|
||||||
print(f"Service for session {session_id} not found or access denied")
|
print(f"Service for session {session_id} not found or access denied")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
"hermes_services": [
|
"hermes_services": [
|
||||||
{
|
{
|
||||||
"id": "00000000-0000-0000-0000-000000000001",
|
"id": "00000000-0000-0000-0000-000000000001",
|
||||||
"user_id": null,
|
"orgid": "",
|
||||||
"name": "Local Hermes Service",
|
"name": "Local Hermes Service",
|
||||||
"service_url": "http://localhost:9120",
|
"service_url": "http://localhost:9120",
|
||||||
"api_key": null,
|
"apikey": "",
|
||||||
"description": "Default local Hermes service instance",
|
"description": "Default local Hermes service instance",
|
||||||
"status": "active"
|
"status": "active"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
"title": "Hermes Services",
|
"title": "Hermes Services",
|
||||||
"params": {
|
"params": {
|
||||||
"sortby": ["created_at desc"],
|
"sortby": ["created_at desc"],
|
||||||
"confidential_fields": [],
|
"logined_userorgid": "orgid",
|
||||||
|
"confidential_fields": ["apikey"],
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": ["id", "service_url", "created_at", "updated_at"],
|
"exclouded": ["id", "service_url", "created_at", "updated_at"],
|
||||||
"alters": {
|
"alters": {
|
||||||
|
|||||||
@ -16,6 +16,14 @@
|
|||||||
"nullable": "no",
|
"nullable": "no",
|
||||||
"comments": "Primary key - UUID format"
|
"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",
|
"name": "name",
|
||||||
"title": "Service Name",
|
"title": "Service Name",
|
||||||
@ -72,6 +80,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_hermes_services_orgid",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["orgid"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_hermes_services_name",
|
"name": "idx_hermes_services_name",
|
||||||
"idxtype": "index",
|
"idxtype": "index",
|
||||||
@ -81,6 +94,11 @@
|
|||||||
"name": "idx_hermes_services_status",
|
"name": "idx_hermes_services_status",
|
||||||
"idxtype": "index",
|
"idxtype": "index",
|
||||||
"idxfields": ["status"]
|
"idxfields": ["status"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_hermes_services_orgid_status",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["orgid", "status"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codes": []
|
"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()
|
# This .dspy file uses functions provided by load_hermes_web_cli()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get current user ID using ahserver's built-in get_user() function
|
# Get current user's org ID using ahserver's built-in get_userorgid() function
|
||||||
user_id = await get_user()
|
orgid = await get_userorgid()
|
||||||
|
|
||||||
# Use the function provided by the hermes-web-cli module
|
# 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
|
# Format services for UI display
|
||||||
result = []
|
result = []
|
||||||
|
|||||||
@ -6,15 +6,15 @@ try:
|
|||||||
if not service_id:
|
if not service_id:
|
||||||
return {"error": "Service ID is required"}
|
return {"error": "Service ID is required"}
|
||||||
|
|
||||||
# Get current user ID using ahserver's built-in get_user() function
|
# Get current user's org ID using ahserver's built-in get_userorgid() function
|
||||||
user_id = await get_user()
|
orgid = await get_userorgid()
|
||||||
|
|
||||||
# Call the function provided by the hermes-web-cli module
|
# 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:
|
if success:
|
||||||
return {"success": True, "message": "Service removed successfully"}
|
return {"success": True, "message": "Service removed successfully"}
|
||||||
else:
|
else:
|
||||||
return {"error": "Failed to remove service"}
|
return {"error": "Failed to remove service"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|||||||
@ -6,10 +6,13 @@ try:
|
|||||||
if not service_id:
|
if not service_id:
|
||||||
return {"error": "Service ID is required"}
|
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
|
# 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}
|
return {"status": status_msg}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|||||||
@ -17,12 +17,13 @@ try:
|
|||||||
# Redirect back to form with error
|
# Redirect back to form with error
|
||||||
return {"redirect": "/hermes-web-cli/new_session.ui?error=Service+ID+is+required"}
|
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()
|
user_id = await get_user()
|
||||||
|
orgid = await get_userorgid()
|
||||||
|
|
||||||
# Use the function provided by the hermes-web-cli module
|
# Use the function provided by the hermes-web-cli module
|
||||||
# This will call the remote hermes-service API with user_id support
|
# Service lookup uses orgid, session ownership uses user_id
|
||||||
session_result = await create_session(service_id, user_id, "")
|
session_result = await create_session(service_id, user_id, orgid, "")
|
||||||
|
|
||||||
if session_result:
|
if session_result:
|
||||||
# Redirect to the new session's user interaction page
|
# Redirect to the new session's user interaction page
|
||||||
@ -34,4 +35,4 @@ try:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log the error and redirect back with error message
|
# Log the error and redirect back with error message
|
||||||
print(f"Error creating session: {str(e)}")
|
print(f"Error creating session: {str(e)}")
|
||||||
return {"redirect": f"/hermes-web-cli/new_session.ui?error=Internal+error:+{str(e)}"}
|
return {"redirect": f"/hermes-web-cli/new_session.ui?error=Internal+error:+{str(e)}"}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user