Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate with Neon Users Service #33

Merged
merged 28 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ad1e134
Update `client_manager` to use `mq_connector` for authentication via …
NeonDaniel Oct 30, 2024
6ec7e5c
Add `register` endpoint with default `USER` permissions
NeonDaniel Oct 30, 2024
13b8a22
Add `user` route with authenticated `get` and `update` endpoints.
NeonDaniel Oct 30, 2024
61c9fea
Update data model imports
NeonDaniel Nov 4, 2024
7f0fdf1
Update missed import refactor
NeonDaniel Nov 4, 2024
4cf6f2a
Update missed import refactor
NeonDaniel Nov 4, 2024
9f7c1d1
Update missed import refactor
NeonDaniel Nov 4, 2024
2c4c269
Update Client Manager refactor to enable backwards-compat. without a …
NeonDaniel Nov 4, 2024
01ce5d9
Add `jwt_issuer` config
NeonDaniel Nov 5, 2024
eb4e8c7
Update auth tests to address code changes
NeonDaniel Nov 5, 2024
28fc8db
Update `neon-data-models` dependency to validate changes
NeonDaniel Nov 5, 2024
701b264
Resolve test failures
NeonDaniel Nov 5, 2024
fd172a5
Update `authorized_clients` handling to partially restore backwards-c…
NeonDaniel Nov 5, 2024
b98e2f2
Restore original test cases
NeonDaniel Nov 5, 2024
b72a4b2
Deprecate configured `node_username`/`node_password` and annotate `di…
NeonDaniel Nov 5, 2024
993ad09
Mark refactored import paths for deprecation
NeonDaniel Nov 6, 2024
23cef46
Fix bug that reset tokens' `creation_timestamp` upon refresh
NeonDaniel Nov 6, 2024
27e1caa
Refactor refresh endpoint handler to use only refresh token instead o…
NeonDaniel Nov 6, 2024
299ff22
Add rate limiting to `/register` endpoint and update documented examp…
NeonDaniel Nov 8, 2024
5c2c6ac
Refactor `get_user_profile` to `read_user` for consistency in method …
NeonDaniel Nov 9, 2024
e1694ba
Refactor token handling to use same HanaToken model that is JWT-encoded
NeonDaniel Nov 12, 2024
757ff15
Refactor token refresh handling to account for data model changes
NeonDaniel Nov 13, 2024
eb76f47
Implement unit tests for auth endpoints
NeonDaniel Nov 22, 2024
b1d3b03
Mock token return for unit tests
NeonDaniel Nov 22, 2024
e843bbf
Fix invalid references in unit tests
NeonDaniel Nov 22, 2024
6f862c8
Catch and log errors when an old/invalid JWT token is parsed
NeonDaniel Dec 21, 2024
a31ad5f
Update neon_hana/app/routers/user.py
NeonDaniel Dec 23, 2024
bb3e2fb
Update logging and mark `authorized_clients` as deprecated to address…
NeonDaniel Dec 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ FROM python:3.9-slim
LABEL vendor=neon.ai \
ai.neon.name="neon-hana"

ENV OVOS_CONFIG_BASE_FOLDER neon
ENV OVOS_CONFIG_FILENAME diana.yaml
ENV XDG_CONFIG_HOME /config
ENV OVOS_CONFIG_BASE_FOLDER=neon
ENV OVOS_CONFIG_FILENAME=diana.yaml
ENV XDG_CONFIG_HOME=/config

RUN apt update && apt install -y swig gcc libpulse-dev portaudio19-dev

Expand Down
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ hana:
mq_default_timeout: 10
access_token_ttl: 86400 # 1 day
refresh_token_ttl: 604800 # 1 week
requests_per_minute: 60
requests_per_minute: 60 # All other requests (auth, registration, etc) also count towards this limit
auth_requests_per_minute: 6 # This counts valid and invalid requests from an IP address
registration_requests_per_hour: 4 # This is low to prevent malicious user creation that will pollute the database
access_token_secret: a800445648142061fc238d1f84e96200da87f4f9fa7835cac90db8b4391b117b
refresh_token_secret: 833d369ac73d883123743a44b4a7fe21203cffc956f4c8fec712e71aafa8e1aa
jwt_issuer: neon.ai # Used in the `iss` field of generated JWT tokens.
fastapi_title: "My HANA API Host"
fastapi_summary: "Personal HTTP API to access my DIANA backend."
disable_auth: True
disable_auth: True # If true, no authentication will be attempted; all connections will be allowed
stt_max_length_encoded: 500000 # Arbitrary limit that is larger than any expected voice command
tts_max_words: 128 # Arbitrary limit that is longer than any default LLM token limit
enable_email: True # Disabled by default; anyone with access to the API will be able to send emails from the configured address
node_username: node_user # Username to authenticate Node API access; leave empty to disable Node API access
node_password: node_password # Password associated with node_username
max_streaming_clients: -1 # Maximum audio streaming clients allowed (including 0). Default unset value allows infinite clients
```
It is recommended to generate unique values for configured tokens, these are 32
Expand All @@ -45,7 +45,27 @@ docker run -p 8080:8080 -v ~/.config/neon:/config/neon ghcr.io/neongeckocom/neon
are using the default port 8080

## Usage
Full API documentation is available at `/docs`. The `/auth/login` endpoint should
be used to generate a `client_id`, `access_token`, and `refresh_token`. The
`access_token` should be included in every request and upon expiration of the
`access_token`, a new token can be obtained from the `auth/refresh` endpoint.
Full API documentation is available at `/docs`.

### Registration
The `/auth/register` endpoint may be used to create a new user if auth is enabled.
If auth is disabled, any login requests will return a successful response.

### Token Generation
The `/auth/login` endpoint should be used to generate a `client_id`,
`access_token`, and `refresh_token`. The `access_token` should be included in
every request and upon expiration of the `access_token`, a new token can be
obtained from the `auth/refresh` endpoint. Tokens are client-specific and clients
are expected to include the same `client_id` and valid tokens for that client
with every request.

### Token Management
`access_token`s should not be saved to persistent storage; they are only valid
for a short period of time and a new `access_token` should be generated for
every new session.

`refresh_token`s should be saved to persistent storage and used to generate a new
`access_token` and `refresh_token` at the beginning of a session, or when the
current `access_token` expires. A `refresh_token` may only be used once; a new
`refresh_token` returned from the `/auth/refresh` endpoint will replace the one
included in the request.
2 changes: 2 additions & 0 deletions neon_hana/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from neon_hana.app.routers.llm import llm_route
from neon_hana.app.routers.mq_backend import mq_route
from neon_hana.app.routers.auth import auth_route
from neon_hana.app.routers.user import user_route
from neon_hana.app.routers.util import util_route
from neon_hana.app.routers.node_server import node_route
from neon_hana.version import __version__
Expand All @@ -49,5 +50,6 @@ def create_app(config: dict):
app.include_router(llm_route)
app.include_router(util_route)
app.include_router(node_route)
app.include_router(user_route)

return app
2 changes: 1 addition & 1 deletion neon_hana/app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@

config = Configuration().get("hana") or dict()
mq_connector = MQServiceManager(config)
client_manager = ClientManager(config)
client_manager = ClientManager(config, mq_connector)
jwt_bearer = UserTokenAuth(client_manager)
8 changes: 8 additions & 0 deletions neon_hana/app/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from neon_hana.app.dependencies import client_manager
from neon_hana.schema.auth_requests import *
from neon_data_models.models.user import User

auth_route = APIRouter(prefix="/auth", tags=["authentication"])

Expand All @@ -43,3 +44,10 @@ async def check_login(auth_request: AuthenticationRequest,
@auth_route.post("/refresh")
async def check_refresh(request: RefreshRequest) -> AuthenticationResponse:
return client_manager.check_refresh_request(**dict(request))


@auth_route.post("/register")
async def register_user(register_request: RegistrationRequest,
request: Request) -> User:
return client_manager.check_registration_request(**dict(register_request),
origin_ip=request.client.host)
19 changes: 11 additions & 8 deletions neon_hana/app/routers/node_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

from asyncio import Event
from signal import signal, SIGINT
from time import sleep
from typing import Optional, Union

from fastapi import APIRouter, WebSocket, HTTPException
Expand All @@ -36,13 +35,17 @@
from neon_hana.app.dependencies import config, client_manager
from neon_hana.mq_websocket_api import MQWebsocketAPI, ClientNotKnown

from neon_hana.schema.node_v1 import (NodeAudioInput, NodeGetStt,
NodeGetTts, NodeKlatResponse,
NodeAudioInputResponse,
NodeGetSttResponse,
NodeGetTtsResponse, CoreWWDetected,
CoreIntentFailure, CoreErrorResponse,
CoreClearData, CoreAlertExpired)
from neon_data_models.models.api.node_v1 import (NodeAudioInput, NodeGetStt,
NodeGetTts, NodeKlatResponse,
NodeAudioInputResponse,
NodeGetSttResponse,
NodeGetTtsResponse,
CoreWWDetected,
CoreIntentFailure,
CoreErrorResponse,
CoreClearData,
CoreAlertExpired)

node_route = APIRouter(prefix="/node", tags=["node"])

socket_api = MQWebsocketAPI(config)
Expand Down
48 changes: 48 additions & 0 deletions neon_hana/app/routers/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2021 Neongecko.com Inc.
# BSD-3
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from fastapi import APIRouter, Depends
from neon_hana.app.dependencies import jwt_bearer, mq_connector
from neon_hana.schema.user_requests import GetUserRequest, UpdateUserRequest
from neon_data_models.models.user import User

user_route = APIRouter(tags=["user"], dependencies=[Depends(jwt_bearer)])


@user_route.post("/get")
async def get_user(request: GetUserRequest,
token: str = Depends(jwt_bearer)) -> User:
hana_token = jwt_bearer.client_manager.get_token_data(token)
return mq_connector.read_user(access_token=hana_token,
auth_user=hana_token.sub,
**dict(request))


@user_route.post("/update")
async def update_user(request: UpdateUserRequest,
token: str = Depends(jwt_bearer)) -> User:
return mq_connector.update_user(access_token=token,
**dict(request))
2 changes: 2 additions & 0 deletions neon_hana/app/routers/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ async def api_client_ip(request: Request) -> str:
# Reported host is a hostname, not an IP address. Return a generic
# loopback value
ip_addr = "127.0.0.1"
# Validation will fail, but this increments the rate-limiting
client_manager.validate_auth("", ip_addr)
return ip_addr


@util_route.get("/headers")
async def api_headers(request: Request):
ip_addr = request.client.host if request.client else "127.0.0.1"
# Validation will fail, but this increments the rate-limiting
client_manager.validate_auth("", ip_addr)
return request.headers
Loading
Loading