Feature/gateway auth (#186)

* Added auth module, just a simple token at this stage
* Pass auth token GATEWAY_SECRET through
* Auth token not mandatory, can be provided in env var
This commit is contained in:
cybermaggedon 2024-12-02 19:57:21 +00:00 committed by GitHub
parent 6d200c79c5
commit 1b9c6be4fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 126 additions and 33 deletions

View file

@ -15,6 +15,9 @@ local prompt = import "prompt-template.jsonnet";
create:: function(engine) create:: function(engine)
local envSecrets = engine.envSecrets("gateway-secret")
.with_env_var("GATEWAY_SECRET", "gateway-secret");
local port = $["api-gateway-port"]; local port = $["api-gateway-port"];
local container = local container =
@ -29,6 +32,7 @@ local prompt = import "prompt-template.jsonnet";
"--port", "--port",
std.toString(port), std.toString(port),
]) ])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "256M") .with_limits("0.5", "256M")
.with_reservations("0.1", "256M") .with_reservations("0.1", "256M")
.with_port(8000, 8000, "metrics") .with_port(8000, 8000, "metrics")
@ -44,6 +48,7 @@ local prompt = import "prompt-template.jsonnet";
.with_port(port, port, "api"); .with_port(port, port, "api");
engine.resources([ engine.resources([
envSecrets,
containerSet, containerSet,
service, service,
]) ])

View file

@ -6,7 +6,7 @@ from ... schema import agent_response_queue
from . endpoint import MultiResponseServiceEndpoint from . endpoint import MultiResponseServiceEndpoint
class AgentEndpoint(MultiResponseServiceEndpoint): class AgentEndpoint(MultiResponseServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(AgentEndpoint, self).__init__( super(AgentEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class AgentEndpoint(MultiResponseServiceEndpoint):
response_schema=AgentResponse, response_schema=AgentResponse,
endpoint_path="/api/v1/agent", endpoint_path="/api/v1/agent",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -0,0 +1,22 @@
class Authenticator:
def __init__(self, token=None, allow_all=False):
if not allow_all and token is None:
raise RuntimeError("Need a token")
if not allow_all and token == "":
raise RuntimeError("Need a token")
self.token = token
self.allow_all = allow_all
def permitted(self, token, roles):
if self.allow_all: return True
if self.token != token: return False
return True

View file

@ -6,7 +6,7 @@ from ... schema import dbpedia_lookup_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class DbpediaEndpoint(ServiceEndpoint): class DbpediaEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(DbpediaEndpoint, self).__init__( super(DbpediaEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class DbpediaEndpoint(ServiceEndpoint):
response_schema=LookupResponse, response_schema=LookupResponse,
endpoint_path="/api/v1/dbpedia", endpoint_path="/api/v1/dbpedia",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -6,7 +6,7 @@ from ... schema import embeddings_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class EmbeddingsEndpoint(ServiceEndpoint): class EmbeddingsEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(EmbeddingsEndpoint, self).__init__( super(EmbeddingsEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class EmbeddingsEndpoint(ServiceEndpoint):
response_schema=EmbeddingsResponse, response_schema=EmbeddingsResponse,
endpoint_path="/api/v1/embeddings", endpoint_path="/api/v1/embeddings",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -6,7 +6,7 @@ from ... schema import encyclopedia_lookup_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class EncyclopediaEndpoint(ServiceEndpoint): class EncyclopediaEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(EncyclopediaEndpoint, self).__init__( super(EncyclopediaEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class EncyclopediaEndpoint(ServiceEndpoint):
response_schema=LookupResponse, response_schema=LookupResponse,
endpoint_path="/api/v1/encyclopedia", endpoint_path="/api/v1/encyclopedia",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -19,6 +19,7 @@ class ServiceEndpoint:
request_queue, request_schema, request_queue, request_schema,
response_queue, response_schema, response_queue, response_schema,
endpoint_path, endpoint_path,
auth,
subscription="api-gateway", consumer_name="api-gateway", subscription="api-gateway", consumer_name="api-gateway",
timeout=600, timeout=600,
): ):
@ -36,6 +37,9 @@ class ServiceEndpoint:
self.path = endpoint_path self.path = endpoint_path
self.timeout = timeout self.timeout = timeout
self.auth = auth
self.operation = "service"
async def start(self): async def start(self):
@ -58,14 +62,24 @@ class ServiceEndpoint:
id = str(uuid.uuid4()) id = str(uuid.uuid4())
try:
ht = request.headers["Authorization"]
tokens = ht.split(" ", 2)
if tokens[0] != "Bearer":
return web.HTTPUnauthorized()
token = tokens[1]
except:
token = ""
if not self.auth.permitted(token, self.operation):
return web.HTTPUnauthorized()
try: try:
data = await request.json() data = await request.json()
q = await self.sub.subscribe(id) q = await self.sub.subscribe(id)
print(data)
await self.pub.send( await self.pub.send(
id, id,
self.to_request(data), self.to_request(data),
@ -76,8 +90,6 @@ class ServiceEndpoint:
except: except:
raise RuntimeError("Timeout waiting for response") raise RuntimeError("Timeout waiting for response")
print(resp)
if resp.error: if resp.error:
return web.json_response( return web.json_response(
{ "error": resp.error.message } { "error": resp.error.message }
@ -110,8 +122,6 @@ class MultiResponseServiceEndpoint(ServiceEndpoint):
q = await self.sub.subscribe(id) q = await self.sub.subscribe(id)
print(data)
await self.pub.send( await self.pub.send(
id, id,
self.to_request(data), self.to_request(data),
@ -126,8 +136,6 @@ class MultiResponseServiceEndpoint(ServiceEndpoint):
except: except:
raise RuntimeError("Timeout waiting for response") raise RuntimeError("Timeout waiting for response")
print(resp)
if resp.error: if resp.error:
return web.json_response( return web.json_response(
{ "error": resp.error.message } { "error": resp.error.message }

View file

@ -14,10 +14,12 @@ from . serialize import to_subgraph, to_value
class GraphEmbeddingsLoadEndpoint(SocketEndpoint): class GraphEmbeddingsLoadEndpoint(SocketEndpoint):
def __init__(self, pulsar_host, path="/api/v1/load/graph-embeddings"): def __init__(
self, pulsar_host, auth, path="/api/v1/load/graph-embeddings",
):
super(GraphEmbeddingsLoadEndpoint, self).__init__( super(GraphEmbeddingsLoadEndpoint, self).__init__(
endpoint_path=path endpoint_path=path, auth=auth,
) )
self.pulsar_host=pulsar_host self.pulsar_host=pulsar_host

View file

@ -12,10 +12,12 @@ from . serialize import serialize_graph_embeddings
class GraphEmbeddingsStreamEndpoint(SocketEndpoint): class GraphEmbeddingsStreamEndpoint(SocketEndpoint):
def __init__(self, pulsar_host, path="/api/v1/stream/graph-embeddings"): def __init__(
self, pulsar_host, auth, path="/api/v1/stream/graph-embeddings"
):
super(GraphEmbeddingsStreamEndpoint, self).__init__( super(GraphEmbeddingsStreamEndpoint, self).__init__(
endpoint_path=path endpoint_path=path, auth=auth,
) )
self.pulsar_host=pulsar_host self.pulsar_host=pulsar_host

View file

@ -6,7 +6,7 @@ from ... schema import graph_rag_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class GraphRagEndpoint(ServiceEndpoint): class GraphRagEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(GraphRagEndpoint, self).__init__( super(GraphRagEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class GraphRagEndpoint(ServiceEndpoint):
response_schema=GraphRagResponse, response_schema=GraphRagResponse,
endpoint_path="/api/v1/graph-rag", endpoint_path="/api/v1/graph-rag",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -6,7 +6,7 @@ from ... schema import internet_search_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class InternetSearchEndpoint(ServiceEndpoint): class InternetSearchEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(InternetSearchEndpoint, self).__init__( super(InternetSearchEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class InternetSearchEndpoint(ServiceEndpoint):
response_schema=LookupResponse, response_schema=LookupResponse,
endpoint_path="/api/v1/internet-search", endpoint_path="/api/v1/internet-search",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -8,7 +8,7 @@ from ... schema import prompt_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class PromptEndpoint(ServiceEndpoint): class PromptEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(PromptEndpoint, self).__init__( super(PromptEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -18,6 +18,7 @@ class PromptEndpoint(ServiceEndpoint):
response_schema=PromptResponse, response_schema=PromptResponse,
endpoint_path="/api/v1/prompt", endpoint_path="/api/v1/prompt",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -45,6 +45,7 @@ from . triples_stream import TriplesStreamEndpoint
from . graph_embeddings_stream import GraphEmbeddingsStreamEndpoint from . graph_embeddings_stream import GraphEmbeddingsStreamEndpoint
from . triples_load import TriplesLoadEndpoint from . triples_load import TriplesLoadEndpoint
from . graph_embeddings_load import GraphEmbeddingsLoadEndpoint from . graph_embeddings_load import GraphEmbeddingsLoadEndpoint
from . auth import Authenticator
logger = logging.getLogger("api") logger = logging.getLogger("api")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
@ -52,6 +53,7 @@ logger.setLevel(logging.INFO)
default_pulsar_host = os.getenv("PULSAR_HOST", "pulsar://pulsar:6650") default_pulsar_host = os.getenv("PULSAR_HOST", "pulsar://pulsar:6650")
default_timeout = 600 default_timeout = 600
default_port = 8088 default_port = 8088
default_api_token = os.getenv("GATEWAY_SECRET", "")
class Api: class Api:
@ -66,45 +68,66 @@ class Api:
self.timeout = int(config.get("timeout", default_timeout)) self.timeout = int(config.get("timeout", default_timeout))
self.pulsar_host = config.get("pulsar_host", default_pulsar_host) self.pulsar_host = config.get("pulsar_host", default_pulsar_host)
api_token = config.get("api_token", default_api_token)
# Token not set, or token equal empty string means no auth
if api_token:
self.auth = Authenticator(token=api_token)
else:
self.auth = Authenticator(allow_all=True)
self.endpoints = [ self.endpoints = [
TextCompletionEndpoint( TextCompletionEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
PromptEndpoint( PromptEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
GraphRagEndpoint( GraphRagEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
TriplesQueryEndpoint( TriplesQueryEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
EmbeddingsEndpoint( EmbeddingsEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
AgentEndpoint( AgentEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
EncyclopediaEndpoint( EncyclopediaEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
DbpediaEndpoint( DbpediaEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
InternetSearchEndpoint( InternetSearchEndpoint(
pulsar_host=self.pulsar_host, timeout=self.timeout, pulsar_host=self.pulsar_host, timeout=self.timeout,
auth = self.auth,
), ),
TriplesStreamEndpoint( TriplesStreamEndpoint(
pulsar_host=self.pulsar_host pulsar_host=self.pulsar_host,
auth = self.auth,
), ),
GraphEmbeddingsStreamEndpoint( GraphEmbeddingsStreamEndpoint(
pulsar_host=self.pulsar_host pulsar_host=self.pulsar_host,
auth = self.auth,
), ),
TriplesLoadEndpoint( TriplesLoadEndpoint(
pulsar_host=self.pulsar_host pulsar_host=self.pulsar_host,
auth = self.auth,
), ),
GraphEmbeddingsLoadEndpoint( GraphEmbeddingsLoadEndpoint(
pulsar_host=self.pulsar_host pulsar_host=self.pulsar_host,
auth = self.auth,
), ),
] ]
@ -254,6 +277,12 @@ def run():
help=f'API request timeout in seconds (default: {default_timeout})', help=f'API request timeout in seconds (default: {default_timeout})',
) )
parser.add_argument(
'--api-token',
default=default_api_token,
help=f'Secret API token (default: no auth)',
)
parser.add_argument( parser.add_argument(
'-l', '--log-level', '-l', '--log-level',
type=LogLevel, type=LogLevel,

View file

@ -11,11 +11,12 @@ logger.setLevel(logging.INFO)
class SocketEndpoint: class SocketEndpoint:
def __init__( def __init__(
self, self, endpoint_path, auth,
endpoint_path="/api/v1/socket",
): ):
self.path = endpoint_path self.path = endpoint_path
self.auth = auth
self.operation = "socket"
async def listener(self, ws, running): async def listener(self, ws, running):
@ -43,18 +44,33 @@ class SocketEndpoint:
async def handle(self, request): async def handle(self, request):
try:
token = request.query['token']
except:
token = ""
if not self.auth.permitted(token, self.operation):
return web.HTTPUnauthorized()
running = Running() running = Running()
ws = web.WebSocketResponse() ws = web.WebSocketResponse()
await ws.prepare(request) await ws.prepare(request)
task = asyncio.create_task(self.async_thread(ws, running)) task = asyncio.create_task(self.async_thread(ws, running))
await self.listener(ws, running) try:
await task await self.listener(ws, running)
except Exception as e:
print(e, flush=True)
running.stop() running.stop()
await ws.close()
await task
return ws return ws
async def start(self): async def start(self):

View file

@ -6,7 +6,7 @@ from ... schema import text_completion_response_queue
from . endpoint import ServiceEndpoint from . endpoint import ServiceEndpoint
class TextCompletionEndpoint(ServiceEndpoint): class TextCompletionEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(TextCompletionEndpoint, self).__init__( super(TextCompletionEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -16,6 +16,7 @@ class TextCompletionEndpoint(ServiceEndpoint):
response_schema=TextCompletionResponse, response_schema=TextCompletionResponse,
endpoint_path="/api/v1/text-completion", endpoint_path="/api/v1/text-completion",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -14,10 +14,10 @@ from . serialize import to_subgraph
class TriplesLoadEndpoint(SocketEndpoint): class TriplesLoadEndpoint(SocketEndpoint):
def __init__(self, pulsar_host, path="/api/v1/load/triples"): def __init__(self, pulsar_host, auth, path="/api/v1/load/triples"):
super(TriplesLoadEndpoint, self).__init__( super(TriplesLoadEndpoint, self).__init__(
endpoint_path=path endpoint_path=path, auth=auth,
) )
self.pulsar_host=pulsar_host self.pulsar_host=pulsar_host

View file

@ -7,7 +7,7 @@ from . endpoint import ServiceEndpoint
from . serialize import to_value, serialize_subgraph from . serialize import to_value, serialize_subgraph
class TriplesQueryEndpoint(ServiceEndpoint): class TriplesQueryEndpoint(ServiceEndpoint):
def __init__(self, pulsar_host, timeout): def __init__(self, pulsar_host, timeout, auth):
super(TriplesQueryEndpoint, self).__init__( super(TriplesQueryEndpoint, self).__init__(
pulsar_host=pulsar_host, pulsar_host=pulsar_host,
@ -17,6 +17,7 @@ class TriplesQueryEndpoint(ServiceEndpoint):
response_schema=TriplesQueryResponse, response_schema=TriplesQueryResponse,
endpoint_path="/api/v1/triples-query", endpoint_path="/api/v1/triples-query",
timeout=timeout, timeout=timeout,
auth=auth,
) )
def to_request(self, body): def to_request(self, body):

View file

@ -12,10 +12,10 @@ from . serialize import serialize_triples
class TriplesStreamEndpoint(SocketEndpoint): class TriplesStreamEndpoint(SocketEndpoint):
def __init__(self, pulsar_host, path="/api/v1/stream/triples"): def __init__(self, pulsar_host, auth, path="/api/v1/stream/triples"):
super(TriplesStreamEndpoint, self).__init__( super(TriplesStreamEndpoint, self).__init__(
endpoint_path=path endpoint_path=path, auth=auth,
) )
self.pulsar_host=pulsar_host self.pulsar_host=pulsar_host