From 616af1df9bad39e54a3772ef833bbe4e2a1beb95 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Fri, 18 Oct 2024 21:16:34 +0800 Subject: [PATCH] [WEBHOOK]: Initial integration --- jac-cloud/jac_cloud/core/architype.py | 12 +- jac-cloud/jac_cloud/jaseci/__init__.py | 15 +- .../jac_cloud/jaseci/datasources/redis.py | 18 +- jac-cloud/jac_cloud/jaseci/dtos/__init__.py | 4 + jac-cloud/jac_cloud/jaseci/dtos/webhook.py | 31 ++ jac-cloud/jac_cloud/jaseci/models/__init__.py | 3 +- jac-cloud/jac_cloud/jaseci/models/webhook.py | 63 ++++ .../jac_cloud/jaseci/routers/__init__.py | 3 +- jac-cloud/jac_cloud/jaseci/routers/sso.py | 6 +- jac-cloud/jac_cloud/jaseci/routers/user.py | 6 +- jac-cloud/jac_cloud/jaseci/routers/webhook.py | 166 ++++++++++ .../jac_cloud/jaseci/security/__init__.py | 84 ++++- jac-cloud/jac_cloud/plugin/jaseci.py | 26 +- jac-cloud/jac_cloud/tests/openapi_specs.yaml | 313 ++++++++++++------ jac-cloud/jac_cloud/tests/simple.json | 3 + jac-cloud/jac_cloud/tests/simple_graph.jac | 10 + .../jac_cloud/tests/test_simple_graph.py | 26 +- .../jac-cloud/docs/jac_cloud_webhook.md | 115 +++++++ jac/support/jac-lang.org/mkdocs.yml | 1 + 19 files changed, 759 insertions(+), 146 deletions(-) create mode 100644 jac-cloud/jac_cloud/jaseci/dtos/webhook.py create mode 100644 jac-cloud/jac_cloud/jaseci/models/webhook.py create mode 100644 jac-cloud/jac_cloud/jaseci/routers/webhook.py create mode 100644 jac-cloud/jac_cloud/tests/simple.json create mode 100644 jac/support/jac-lang.org/docs/learn/coders/jac-cloud/docs/jac_cloud_webhook.md diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index c893c7cbb5..04def97285 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -243,17 +243,17 @@ def commit(session: ClientSession) -> None: if ex.has_error_label("UnknownTransactionCommitResult"): commit_retry += 1 logger.error( - "Error commiting bulk write! " + "Error commiting transaction! " f"Retrying [{commit_retry}/{commit_max_retry}] ..." ) continue logger.error( - f"Error commiting bulk write after max retry [{commit_max_retry}] !" + f"Error commiting transaction after max retry [{commit_max_retry}] !" ) raise except Exception: session.abort_transaction() - logger.error("Error commiting bulk write!") + logger.error("Error commiting transaction!") raise def execute(self, session: ClientSession) -> None: @@ -276,16 +276,16 @@ def execute(self, session: ClientSession) -> None: if ex.has_error_label("TransientTransactionError"): transaction_retry += 1 logger.error( - "Error executing bulk write! " + "Error executing transaction! " f"Retrying [{transaction_retry}/{transaction_max_retry}] ..." ) continue logger.error( - f"Error executing bulk write after max retry [{transaction_max_retry}] !" + f"Error executing transaction after max retry [{transaction_max_retry}] !" ) raise except Exception: - logger.error("Error executing bulk write!") + logger.error("Error executing transaction!") raise diff --git a/jac-cloud/jac_cloud/jaseci/__init__.py b/jac-cloud/jac_cloud/jaseci/__init__.py index e862465353..02e69e4964 100644 --- a/jac-cloud/jac_cloud/jaseci/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/__init__.py @@ -70,10 +70,17 @@ async def lifespan(app: _FaststAPI) -> AsyncGenerator[None, _FaststAPI]: populate_yaml_specs(cls.__app__) - from .routers import healthz_router, sso_router, user_router - from ..plugin.jaseci import walker_router - - for router in [healthz_router, sso_router, user_router, walker_router]: + from .routers import healthz_router, sso_router, user_router, webhook_router + from ..plugin.jaseci import walker_router, webhook_walker_router + + for router in [ + healthz_router, + sso_router, + user_router, + webhook_router, + walker_router, + webhook_walker_router, + ]: cls.__app__.include_router(router) @cls.__app__.exception_handler(Exception) diff --git a/jac-cloud/jac_cloud/jaseci/datasources/redis.py b/jac-cloud/jac_cloud/jaseci/datasources/redis.py index e8022cab27..76e50601bb 100644 --- a/jac-cloud/jac_cloud/jaseci/datasources/redis.py +++ b/jac-cloud/jac_cloud/jaseci/datasources/redis.py @@ -74,7 +74,7 @@ def keys(cls) -> list[bytes]: return [] @classmethod - def set(cls, key: str, data: dict | bool) -> bool: + def set(cls, key: str, data: dict | bool | float) -> bool: """Push key value pair.""" try: redis = cls.get_rd() @@ -115,7 +115,7 @@ def hkeys(cls) -> list[str]: return [] @classmethod - def hset(cls, key: str, data: dict | bool) -> bool: + def hset(cls, key: str, data: dict | bool | float) -> bool: """Push key value pair to group.""" try: redis = cls.get_rd() @@ -169,6 +169,16 @@ class TokenRedis(Redis): __table__ = "token" +class WebhookRedis(Redis): + """Token Memory Interface. + + This interface is for Token Management. + You may override this if you wish to implement different structure + """ + + __table__ = "webhook" + + class AsyncRedis: """ Base Memory interface. @@ -225,7 +235,7 @@ async def keys(cls) -> list[bytes]: return [] @classmethod - async def set(cls, key: str, data: dict | bool) -> bool: + async def set(cls, key: str, data: dict | bool | float) -> bool: """Push key value pair.""" try: redis = cls.get_rd() @@ -266,7 +276,7 @@ async def hkeys(cls) -> list[str]: return [] @classmethod - async def hset(cls, key: str, data: dict | bool) -> bool: + async def hset(cls, key: str, data: dict | bool | float) -> bool: """Push key value pair to group.""" try: redis = cls.get_rd() diff --git a/jac-cloud/jac_cloud/jaseci/dtos/__init__.py b/jac-cloud/jac_cloud/jaseci/dtos/__init__.py index 7c0c5efa07..6184abaad7 100644 --- a/jac-cloud/jac_cloud/jaseci/dtos/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/dtos/__init__.py @@ -8,6 +8,7 @@ UserResetPassword, UserVerification, ) +from .webhook import Expiration, GenerateKey, KeyIDs __all__ = [ @@ -18,4 +19,7 @@ "UserRequest", "UserResetPassword", "UserVerification", + "Expiration", + "GenerateKey", + "KeyIDs", ] diff --git a/jac-cloud/jac_cloud/jaseci/dtos/webhook.py b/jac-cloud/jac_cloud/jaseci/dtos/webhook.py new file mode 100644 index 0000000000..4677b14566 --- /dev/null +++ b/jac-cloud/jac_cloud/jaseci/dtos/webhook.py @@ -0,0 +1,31 @@ +"""Jaseci User DTOs.""" + +from typing import Literal + +from annotated_types import Len + +from pydantic import BaseModel, Field, StringConstraints + +from typing_extensions import Annotated + + +class Expiration(BaseModel): + """Key Expiration.""" + + count: Annotated[int, Field(strict=True, gt=0, default=60)] = 60 + interval: Literal["seconds", "minutes", "hours", "days"] = "days" + + +class GenerateKey(BaseModel): + """User Creation Request Model.""" + + name: Annotated[str, StringConstraints(min_length=1)] + walkers: list[str] = Field(default_factory=list) + nodes: list[str] = Field(default_factory=list) + expiration: Expiration = Field(default_factory=Expiration) + + +class KeyIDs(BaseModel): + """User Creation Request Model.""" + + ids: Annotated[list[str], Len(min_length=1)] diff --git a/jac-cloud/jac_cloud/jaseci/models/__init__.py b/jac-cloud/jac_cloud/jaseci/models/__init__.py index 6442cc1f1b..5177e9704d 100644 --- a/jac-cloud/jac_cloud/jaseci/models/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/models/__init__.py @@ -1,5 +1,6 @@ """Jaseci Models.""" from .user import NO_PASSWORD, User +from .webhook import Webhook -__all__ = ["NO_PASSWORD", "User"] +__all__ = ["NO_PASSWORD", "User", "Webhook"] diff --git a/jac-cloud/jac_cloud/jaseci/models/webhook.py b/jac-cloud/jac_cloud/jaseci/models/webhook.py new file mode 100644 index 0000000000..96a98a6b20 --- /dev/null +++ b/jac-cloud/jac_cloud/jaseci/models/webhook.py @@ -0,0 +1,63 @@ +"""Jaseci Models.""" + +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any, Generator, Mapping, cast + +from bson import ObjectId + +from ..datasources.collection import Collection as BaseCollection + + +@dataclass(kw_only=True) +class Webhook: + """User Base Model.""" + + id: ObjectId = field(default_factory=ObjectId) + name: str + root_id: ObjectId + walkers: list[str] + nodes: list[str] + expiration: datetime + key: str + + class Collection(BaseCollection["Webhook"]): + """ + User collection interface. + + This interface is for User's Management. + You may override this if you wish to implement different structure + """ + + __collection__ = "webhook" + __indexes__ = [ + {"keys": ["name"], "unique": True}, + {"keys": ["key"], "unique": True}, + ] + + @classmethod + def __document__(cls, doc: Mapping[str, Any]) -> "Webhook": + """ + Return parsed Webhook from document. + + This the default User parser after getting a single document. + You may override this to specify how/which class it will be casted/based. + """ + doc = cast(dict, doc) + return Webhook(id=doc.pop("_id"), **doc) + + @classmethod + def find_by_root_id(cls, root_id: ObjectId) -> Generator["Webhook", None, None]: + """Retrieve webhook via root_id.""" + return cls.find({"root_id": root_id}) + + @classmethod + def find_by_key(cls, key: str) -> "Webhook | None": + """Retrieve webhook via root_id.""" + return cls.find_one({"key": key}) + + def __serialize__(self) -> dict: + """Return serializable.""" + data = asdict(self) + data["_id"] = data.pop("id") + return data diff --git a/jac-cloud/jac_cloud/jaseci/routers/__init__.py b/jac-cloud/jac_cloud/jaseci/routers/__init__.py index 04dac026c5..8585b682d0 100644 --- a/jac-cloud/jac_cloud/jaseci/routers/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/routers/__init__.py @@ -3,5 +3,6 @@ from .healthz import router as healthz_router from .sso import router as sso_router from .user import router as user_router +from .webhook import router as webhook_router -__all__ = ["healthz_router", "sso_router", "user_router"] +__all__ = ["healthz_router", "sso_router", "user_router", "webhook_router"] diff --git a/jac-cloud/jac_cloud/jaseci/routers/sso.py b/jac-cloud/jac_cloud/jaseci/routers/sso.py index cb775652df..3f174d0208 100644 --- a/jac-cloud/jac_cloud/jaseci/routers/sso.py +++ b/jac-cloud/jac_cloud/jaseci/routers/sso.py @@ -245,15 +245,15 @@ def register(platform: str, open_id: OpenID) -> Response: if ex.has_error_label("TransientTransactionError"): retry += 1 logger.error( - "Error executing bulk write! " + "Error executing transaction! " f"Retrying [{retry}/{max_retry}] ..." ) continue - logger.exception("Error executing bulk write!") + logger.exception("Error executing transaction!") session.abort_transaction() break except Exception: - logger.exception("Error executing bulk write!") + logger.exception("Error executing transaction!") session.abort_transaction() break return ORJSONResponse({"message": "Registration Failed!"}, 409) diff --git a/jac-cloud/jac_cloud/jaseci/routers/user.py b/jac-cloud/jac_cloud/jaseci/routers/user.py index 2c69d4b360..3a2aedf70f 100644 --- a/jac-cloud/jac_cloud/jaseci/routers/user.py +++ b/jac-cloud/jac_cloud/jaseci/routers/user.py @@ -66,15 +66,15 @@ def register(req: User.register_type()) -> ORJSONResponse: # type: ignore if ex.has_error_label("TransientTransactionError"): retry += 1 logger.error( - "Error executing bulk write! " + "Error executing transaction! " f"Retrying [{retry}/{max_retry}] ..." ) continue - logger.exception("Error executing bulk write!") + logger.exception("Error executing transaction!") session.abort_transaction() break except Exception: - logger.exception("Error executing bulk write!") + logger.exception("Error executing transaction!") session.abort_transaction() break diff --git a/jac-cloud/jac_cloud/jaseci/routers/webhook.py b/jac-cloud/jac_cloud/jaseci/routers/webhook.py new file mode 100644 index 0000000000..21f383238b --- /dev/null +++ b/jac-cloud/jac_cloud/jaseci/routers/webhook.py @@ -0,0 +1,166 @@ +"""Webhook APIs.""" + +from datetime import timedelta + +from bson import ObjectId + +from fastapi import APIRouter, Request, status +from fastapi.responses import ORJSONResponse + +from pymongo.errors import ConnectionFailure, OperationFailure + +from ..datasources.redis import WebhookRedis +from ..dtos import Expiration, GenerateKey, KeyIDs +from ..models import Webhook +from ..security import authenticator +from ..utils import logger, random_string, utc_datetime, utc_timestamp +from ...core.architype import BulkWrite + +router = APIRouter(prefix="/webhook", tags=["webhook"]) + + +@router.get("", status_code=status.HTTP_200_OK, dependencies=authenticator) +def get(req: Request) -> ORJSONResponse: + """Get keys API.""" + root_id: ObjectId = req._user.root_id # type: ignore[attr-defined] + + return ORJSONResponse( + content={ + "keys": [ + { + "id": str(key.id), + "name": key.name, + "root_id": str(key.root_id), + "walkers": key.walkers, + "nodes": key.nodes, + "expiration": key.expiration, + "key": key.key, + } + for key in Webhook.Collection.find({"root_id": root_id}) + ] + } + ) + + +@router.post( + "/generate-key", status_code=status.HTTP_201_CREATED, dependencies=authenticator +) +def generate_key(req: Request, gen_key: GenerateKey) -> ORJSONResponse: + """Generate key API.""" + root_id: ObjectId = req._user.root_id # type: ignore[attr-defined] + + _exp: dict[str, int] = {gen_key.expiration.interval: gen_key.expiration.count} + exp = utc_datetime(**_exp) + + webhook = Webhook( + name=gen_key.name, + root_id=root_id, + walkers=gen_key.walkers, + nodes=gen_key.nodes, + expiration=exp, + key=f"{root_id}:{utc_timestamp()}:{random_string(32)}", + ) + + if ( + id := Webhook.Collection.insert_one(webhook.__serialize__()).inserted_id + ) and WebhookRedis.hset( + webhook.key, + { + "walkers": webhook.walkers, + "nodes": webhook.nodes, + "expiration": webhook.expiration.timestamp(), + }, + ): + return ORJSONResponse( + content={"id": str(id), "name": webhook.name, "key": webhook.key}, + status_code=201, + ) + + return ORJSONResponse( + content="Can't generate key at the moment. Please try again!", status_code=500 + ) + + +@router.patch( + "/extend/{id}", status_code=status.HTTP_201_CREATED, dependencies=authenticator +) +def extend(id: str, expiration: Expiration) -> ORJSONResponse: + """Generate key API.""" + with Webhook.Collection.get_session() as session, session.start_transaction(): + retry = 0 + max_retry = BulkWrite.SESSION_MAX_TRANSACTION_RETRY + while retry <= max_retry: + try: + _id = ObjectId(id) + if webhook := Webhook.Collection.find_by_id(_id, session=session): + _exp: dict[str, int] = {expiration.interval: expiration.count} + webhook.expiration += timedelta(**_exp) + + if Webhook.Collection.update_by_id( + _id, {"$set": {"expiration": webhook.expiration}}, session + ).modified_count: + WebhookRedis.hset( + webhook.key, + { + "walkers": webhook.walkers, + "nodes": webhook.nodes, + "expiration": webhook.expiration.timestamp(), + }, + ) + BulkWrite.commit(session) + return ORJSONResponse( + {"message": "Successfully Extended!"}, 200 + ) + break + except (ConnectionFailure, OperationFailure) as ex: + if ex.has_error_label("TransientTransactionError"): + retry += 1 + logger.error( + f"Error executing transaction! Retrying [{retry}/{max_retry}] ..." + ) + continue + logger.exception("Error executing transaction!") + session.abort_transaction() + break + except Exception: + logger.exception("Error executing transaction!") + session.abort_transaction() + break + + return ORJSONResponse( + content="Can't extend key at the moment. Please try again!", status_code=500 + ) + + +@router.delete("/delete", status_code=status.HTTP_200_OK, dependencies=authenticator) +def delete(key_ids: KeyIDs) -> ORJSONResponse: + """Delete keys API.""" + with Webhook.Collection.get_session() as session, session.start_transaction(): + retry = 0 + max_retry = BulkWrite.SESSION_MAX_TRANSACTION_RETRY + while retry <= max_retry: + try: + if Webhook.Collection.delete( + {"_id": {"$in": [ObjectId(id) for id in key_ids.ids]}}, session + ).deleted_count == len(key_ids.ids): + BulkWrite.commit(session) + return ORJSONResponse({"message": "Successfully Deleted!"}, 200) + break + except (ConnectionFailure, OperationFailure) as ex: + if ex.has_error_label("TransientTransactionError"): + retry += 1 + logger.error( + f"Error executing transaction! Retrying [{retry}/{max_retry}] ..." + ) + continue + logger.exception("Error executing transaction!") + session.abort_transaction() + break + except Exception: + logger.exception("Error executing transaction!") + session.abort_transaction() + break + + return ORJSONResponse( + content="Error occured during deletion. Please try again!", status_code=500 + ) diff --git a/jac-cloud/jac_cloud/jaseci/security/__init__.py b/jac-cloud/jac_cloud/jaseci/security/__init__.py index 64bfa642dd..b0b1f7c476 100644 --- a/jac-cloud/jac_cloud/jaseci/security/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/security/__init__.py @@ -3,16 +3,18 @@ from os import getenv from typing import Any +from asyncer import syncify + from bson import ObjectId from fastapi import Depends, Request from fastapi.exceptions import HTTPException -from fastapi.security import HTTPBearer +from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBearer from jwt import decode, encode -from ..datasources.redis import CodeRedis, TokenRedis -from ..models.user import User as BaseUser +from ..datasources.redis import CodeRedis, TokenRedis, WebhookRedis +from ..models import User as BaseUser, Webhook from ..utils import logger, random_string, utc_timestamp from ...core.architype import NodeAnchor @@ -84,14 +86,21 @@ def invalidate_token(user_id: ObjectId) -> None: TokenRedis.hdelete_rgx(f"{user_id}:*") +def validate_request(request: Request, walker: str, node: str) -> None: + """Trigger initial validation for request.""" + if ((walkers := getattr(request, "_walkers", None)) and walker not in walkers) or ( + (nodes := getattr(request, "_nodes", None)) and node not in nodes + ): + raise HTTPException(status_code=403) + + def authenticate(request: Request) -> None: """Authenticate current request and attach authenticated user and their root.""" authorization = request.headers.get("Authorization") if authorization and authorization.lower().startswith("bearer"): token = authorization[7:] - decrypted = decrypt(token) if ( - decrypted + (decrypted := decrypt(token)) and decrypted["expiration"] > utc_timestamp() and TokenRedis.hget(f"{decrypted['id']}:{token}") and (user := User.Collection.find_by_id(decrypted["id"])) @@ -104,4 +113,69 @@ def authenticate(request: Request) -> None: raise HTTPException(status_code=401) +def generate_webhook_auth(webhook: dict) -> list: + """Authenticate current request and attach authenticated user and their root.""" + authenticators = [] + name = webhook.get("name", "X-API-KEY") + match webhook.get("type", "header").lower(): + case "query": + authenticators.append(Depends(APIKeyQuery(name=name))) + + def getter(request: Request) -> str | None: + return request.query_params.get(name) + + case "path": + + def getter(request: Request) -> str | None: + return request.path_params.get(name) + + case "body": + + def getter(request: Request) -> str | None: + return syncify(request.json)().get(name) + + case _: + + authenticators.append(Depends(APIKeyHeader(name=name))) + + def getter(request: Request) -> str | None: + return request.headers.get(name) + + def authenticate_webhook(request: Request) -> None: + try: + if ( + (key := getter(request)) + and (decrypted := key.split(":")) + and (root := NodeAnchor.Collection.find_by_id(ObjectId(decrypted[0]))) + ): + request._root = root # type: ignore[attr-defined] + if (cache := WebhookRedis.hget(key)) and cache[ + "expiration" + ] > utc_timestamp(): + request._walkers = cache["walkers"] # type: ignore[attr-defined] + request._nodes = cache["nodes"] # type: ignore[attr-defined] + return + elif ( + webhook := Webhook.Collection.find_by_key(key) + ) and WebhookRedis.hset( + key, + { + "walkers": webhook.walkers, + "nodes": webhook.nodes, + "expiration": webhook.expiration.timestamp(), + }, + ): + request._walkers = webhook.walkers # type: ignore[attr-defined] + request._nodes = webhook.nodes # type: ignore[attr-defined] + return + except Exception as e: + logger.exception(e) + + raise HTTPException(status_code=401) + + authenticators.append(Depends(authenticate_webhook)) + + return authenticators + + authenticator = [Depends(HTTPBearer()), Depends(authenticate)] diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index 2b58283dce..802a7feda0 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -55,7 +55,7 @@ ) from ..core.context import ContextResponse, ExecutionContext, JaseciContext from ..jaseci import FastAPI -from ..jaseci.security import authenticator +from ..jaseci.security import authenticator, generate_webhook_auth, validate_request from ..jaseci.utils import log_entry, log_exit @@ -70,6 +70,7 @@ } walker_router = APIRouter(prefix="/walker", tags=["walker"]) +webhook_walker_router = APIRouter(prefix="/webhook/walker", tags=["webhook-walker"]) def get_specs(cls: type) -> Type["DefaultSpecs"] | None: @@ -106,6 +107,7 @@ def populate_apis(cls: Type[WalkerArchitype]) -> None: as_query: str | list[str] = specs.as_query or [] excluded: str | list[str] = specs.excluded or [] auth: bool = specs.auth or False + webhook: dict | None = specs.webhook query: dict[str, Any] = {} body: dict[str, Any] = {} @@ -197,8 +199,10 @@ def api_entry( jctx = JaseciContext.create(request, NodeAnchor.ref(node) if node else None) - wlk: WalkerAnchor = cls(**body, **pl["query"], **pl["files"]).__jac__ + validate_request(request, cls.__name__, jctx.entry_node.name or "root") + if Jac.check_read_access(jctx.entry_node): + wlk: WalkerAnchor = cls(**body, **pl["query"], **pl["files"]).__jac__ Jac.spawn_call(wlk.architype, jctx.entry_node.architype) jctx.close() @@ -211,7 +215,7 @@ def api_entry( return ORJSONResponse(resp, jctx.status) else: error = { - "error": f"You don't have access on target entry{cast(Anchor, jctx.entry_node).ref_id}!" + "error": f"You don't have access on target entry {cast(Anchor, jctx.entry_node).ref_id}!" } jctx.close() @@ -224,10 +228,17 @@ def api_root( ) -> Response: return api_entry(request, None, payload) + if webhook is None: + target_authenticator = authenticator + target_router = walker_router + else: + target_authenticator = generate_webhook_auth(webhook) + target_router = webhook_walker_router + for method in methods: method = method.lower() - walker_method = getattr(walker_router, method) + walker_method = getattr(target_router, method) raw_types: list[Type] = [ get_type_hints(jef.func).get("return", NoneType) @@ -243,11 +254,10 @@ def api_root( ret_types = NoneType # type: ignore[misc] settings: dict[str, Any] = { - "tags": ["walker"], "response_model": ContextResponse[ret_types] | Any, } if auth: - settings["dependencies"] = cast(list, authenticator) + settings["dependencies"] = cast(list, target_authenticator) walker_method(url := f"/{cls.__name__}{path}", summary=url, **settings)( api_root @@ -266,6 +276,7 @@ def specs( excluded: str | list[str] = [], # noqa: B006 auth: bool = True, private: bool = False, + webhook: dict | None = None, ) -> Callable: """Walker Decorator.""" @@ -277,6 +288,7 @@ def wrapper(cls: Type[WalkerArchitype]) -> Type[WalkerArchitype]: ex = excluded a = auth pv = private + w = webhook class __specs__(DefaultSpecs): # noqa: N801 path: str = p @@ -285,6 +297,7 @@ class __specs__(DefaultSpecs): # noqa: N801 excluded: str | list[str] = ex auth: bool = a private: bool = pv + webhook: dict | None = w cls.__specs__ = __specs__ # type: ignore[attr-defined] @@ -306,6 +319,7 @@ class DefaultSpecs: excluded: str | list[str] = [] auth: bool = True private: bool = False + webhook: dict | None = None class JacCallableImplementation: diff --git a/jac-cloud/jac_cloud/tests/openapi_specs.yaml b/jac-cloud/jac_cloud/tests/openapi_specs.yaml index a704c36dfa..e13b80d763 100644 --- a/jac-cloud/jac_cloud/tests/openapi_specs.yaml +++ b/jac-cloud/jac_cloud/tests/openapi_specs.yaml @@ -216,6 +216,48 @@ components: \ for details." enum: [] title: Enum + Expiration: + description: Key Expiration. + properties: + count: + default: 60 + exclusiveMinimum: 0.0 + title: Count + type: integer + interval: + default: days + enum: + - seconds + - minutes + - hours + - days + title: Interval + type: string + title: Expiration + type: object + GenerateKey: + description: User Creation Request Model. + properties: + expiration: + $ref: '#/components/schemas/Expiration' + name: + minLength: 1 + title: Name + type: string + nodes: + items: + type: string + title: Nodes + type: array + walkers: + items: + type: string + title: Walkers + type: array + required: + - name + title: GenerateKey + type: object HTTPValidationError: properties: detail: @@ -225,6 +267,19 @@ components: type: array title: HTTPValidationError type: object + KeyIDs: + description: User Creation Request Model. + properties: + ids: + items: + type: string + minItems: 1 + title: Ids + type: array + required: + - ids + title: KeyIDs + type: object Parent: properties: arr: @@ -476,6 +531,10 @@ components: title: update_custom_object_body_model type: object securitySchemes: + APIKeyHeader: + in: header + name: X-API-KEY + type: apiKey HTTPBearer: scheme: bearer type: http @@ -836,7 +895,6 @@ paths: summary: /allow_other_root_access tags: - walker - - walker /walker/allow_other_root_access/{node}: post: operationId: api_entry_walker_allow_other_root_access__node__post @@ -876,7 +934,6 @@ paths: summary: /allow_other_root_access/{node} tags: - walker - - walker /walker/check_memory_sync: post: operationId: api_root_walker_check_memory_sync_post @@ -907,7 +964,6 @@ paths: summary: /check_memory_sync tags: - walker - - walker /walker/check_memory_sync/{node}: post: operationId: api_entry_walker_check_memory_sync__node__post @@ -947,7 +1003,6 @@ paths: summary: /check_memory_sync/{node} tags: - walker - - walker /walker/check_populated_graph: post: operationId: api_root_walker_check_populated_graph_post @@ -966,7 +1021,6 @@ paths: summary: /check_populated_graph tags: - walker - - walker /walker/check_populated_graph/{node}: post: operationId: api_entry_walker_check_populated_graph__node__post @@ -1000,7 +1054,6 @@ paths: summary: /check_populated_graph/{node} tags: - walker - - walker /walker/combination1: get: operationId: api_root_walker_combination1_get @@ -1044,7 +1097,6 @@ paths: summary: /combination1 tags: - walker - - walker post: operationId: api_root_walker_combination1_post parameters: @@ -1087,7 +1139,6 @@ paths: summary: /combination1 tags: - walker - - walker /walker/combination1/{node}: get: operationId: api_entry_walker_combination1__node__get @@ -1139,7 +1190,6 @@ paths: summary: /combination1/{node} tags: - walker - - walker post: operationId: api_entry_walker_combination1__node__post parameters: @@ -1190,7 +1240,6 @@ paths: summary: /combination1/{node} tags: - walker - - walker /walker/combination2/{a}: delete: operationId: api_root_walker_combination2__a__delete @@ -1234,7 +1283,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker get: operationId: api_root_walker_combination2__a__get parameters: @@ -1277,7 +1325,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker head: operationId: api_root_walker_combination2__a__head parameters: @@ -1320,7 +1367,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker options: operationId: api_root_walker_combination2__a__options parameters: @@ -1357,7 +1403,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker patch: operationId: api_root_walker_combination2__a__patch parameters: @@ -1400,7 +1445,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker post: operationId: api_root_walker_combination2__a__post parameters: @@ -1443,7 +1487,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker put: operationId: api_root_walker_combination2__a__put parameters: @@ -1486,7 +1529,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker trace: operationId: api_root_walker_combination2__a__trace parameters: @@ -1523,7 +1565,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker /walker/combination2/{node}/{a}: delete: operationId: api_entry_walker_combination2__node___a__delete @@ -1575,7 +1616,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker get: operationId: api_entry_walker_combination2__node___a__get parameters: @@ -1626,7 +1666,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker head: operationId: api_entry_walker_combination2__node___a__head parameters: @@ -1677,7 +1716,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker options: operationId: api_entry_walker_combination2__node___a__options parameters: @@ -1722,7 +1760,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker patch: operationId: api_entry_walker_combination2__node___a__patch parameters: @@ -1773,7 +1810,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker post: operationId: api_entry_walker_combination2__node___a__post parameters: @@ -1824,7 +1860,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker put: operationId: api_entry_walker_combination2__node___a__put parameters: @@ -1875,7 +1910,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker trace: operationId: api_entry_walker_combination2__node___a__trace parameters: @@ -1920,7 +1954,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker /walker/create_custom_object: post: operationId: api_root_walker_create_custom_object_post @@ -1939,7 +1972,6 @@ paths: summary: /create_custom_object tags: - walker - - walker /walker/create_custom_object/{node}: post: operationId: api_entry_walker_create_custom_object__node__post @@ -1973,7 +2005,6 @@ paths: summary: /create_custom_object/{node} tags: - walker - - walker /walker/create_graph: post: operationId: api_root_walker_create_graph_post @@ -1992,7 +2023,6 @@ paths: summary: /create_graph tags: - walker - - walker /walker/create_graph/{node}: post: operationId: api_entry_walker_create_graph__node__post @@ -2026,7 +2056,6 @@ paths: summary: /create_graph/{node} tags: - walker - - walker /walker/create_nested_node: post: operationId: api_root_walker_create_nested_node_post @@ -2045,7 +2074,6 @@ paths: summary: /create_nested_node tags: - walker - - walker /walker/create_nested_node/{node}: post: operationId: api_entry_walker_create_nested_node__node__post @@ -2079,7 +2107,6 @@ paths: summary: /create_nested_node/{node} tags: - walker - - walker /walker/custom_report: post: operationId: api_root_walker_custom_report_post @@ -2096,7 +2123,6 @@ paths: summary: /custom_report tags: - walker - - walker /walker/custom_report/{node}: post: operationId: api_entry_walker_custom_report__node__post @@ -2128,7 +2154,6 @@ paths: summary: /custom_report/{node} tags: - walker - - walker /walker/custom_status_code: post: operationId: api_root_walker_custom_status_code_post @@ -2159,7 +2184,6 @@ paths: summary: /custom_status_code tags: - walker - - walker /walker/custom_status_code/{node}: post: operationId: api_entry_walker_custom_status_code__node__post @@ -2199,7 +2223,6 @@ paths: summary: /custom_status_code/{node} tags: - walker - - walker /walker/delete_custom_object: post: operationId: api_root_walker_delete_custom_object_post @@ -2230,7 +2253,6 @@ paths: summary: /delete_custom_object tags: - walker - - walker /walker/delete_custom_object/{node}: post: operationId: api_entry_walker_delete_custom_object__node__post @@ -2270,7 +2292,6 @@ paths: summary: /delete_custom_object/{node} tags: - walker - - walker /walker/delete_nested_edge: post: operationId: api_root_walker_delete_nested_edge_post @@ -2289,7 +2310,6 @@ paths: summary: /delete_nested_edge tags: - walker - - walker /walker/delete_nested_edge/{node}: post: operationId: api_entry_walker_delete_nested_edge__node__post @@ -2323,7 +2343,6 @@ paths: summary: /delete_nested_edge/{node} tags: - walker - - walker /walker/delete_nested_node: post: operationId: api_root_walker_delete_nested_node_post @@ -2342,7 +2361,6 @@ paths: summary: /delete_nested_node tags: - walker - - walker /walker/delete_nested_node/{node}: post: operationId: api_entry_walker_delete_nested_node__node__post @@ -2376,7 +2394,6 @@ paths: summary: /delete_nested_node/{node} tags: - walker - - walker /walker/detach_nested_node: post: operationId: api_root_walker_detach_nested_node_post @@ -2395,7 +2412,6 @@ paths: summary: /detach_nested_node tags: - walker - - walker /walker/detach_nested_node/{node}: post: operationId: api_entry_walker_detach_nested_node__node__post @@ -2429,7 +2445,6 @@ paths: summary: /detach_nested_node/{node} tags: - walker - - walker /walker/detach_node: post: operationId: api_root_walker_detach_node_post @@ -2448,7 +2463,6 @@ paths: summary: /detach_node tags: - walker - - walker /walker/detach_node/{node}: post: operationId: api_entry_walker_detach_node__node__post @@ -2482,7 +2496,6 @@ paths: summary: /detach_node/{node} tags: - walker - - walker /walker/different_return: post: operationId: api_root_walker_different_return_post @@ -2499,7 +2512,6 @@ paths: summary: /different_return tags: - walker - - walker /walker/different_return/{node}: post: operationId: api_entry_walker_different_return__node__post @@ -2531,7 +2543,6 @@ paths: summary: /different_return/{node} tags: - walker - - walker /walker/disallow_other_root_access: post: operationId: api_root_walker_disallow_other_root_access_post @@ -2562,7 +2573,6 @@ paths: summary: /disallow_other_root_access tags: - walker - - walker /walker/disallow_other_root_access/{node}: post: operationId: api_entry_walker_disallow_other_root_access__node__post @@ -2602,7 +2612,6 @@ paths: summary: /disallow_other_root_access/{node} tags: - walker - - walker /walker/get_all_query: get: operationId: api_root_walker_get_all_query_get @@ -2638,7 +2647,6 @@ paths: summary: /get_all_query tags: - walker - - walker /walker/get_all_query/{node}: get: operationId: api_entry_walker_get_all_query__node__get @@ -2682,7 +2690,6 @@ paths: summary: /get_all_query/{node} tags: - walker - - walker /walker/get_custom_object: post: operationId: api_root_walker_get_custom_object_post @@ -2713,7 +2720,6 @@ paths: summary: /get_custom_object tags: - walker - - walker /walker/get_custom_object/{node}: post: operationId: api_entry_walker_get_custom_object__node__post @@ -2753,7 +2759,6 @@ paths: summary: /get_custom_object/{node} tags: - walker - - walker /walker/get_no_body: get: operationId: api_root_walker_get_no_body_get @@ -2772,7 +2777,6 @@ paths: summary: /get_no_body tags: - walker - - walker /walker/get_no_body/{node}: get: operationId: api_entry_walker_get_no_body__node__get @@ -2806,7 +2810,6 @@ paths: summary: /get_no_body/{node} tags: - walker - - walker /walker/get_with_query: get: operationId: api_root_walker_get_with_query_get @@ -2838,7 +2841,6 @@ paths: summary: /get_with_query tags: - walker - - walker /walker/get_with_query/{node}: get: operationId: api_entry_walker_get_with_query__node__get @@ -2878,7 +2880,6 @@ paths: summary: /get_with_query/{node} tags: - walker - - walker /walker/manual_create_nested_node: post: operationId: api_root_walker_manual_create_nested_node_post @@ -2897,7 +2898,6 @@ paths: summary: /manual_create_nested_node tags: - walker - - walker /walker/manual_create_nested_node/{node}: post: operationId: api_entry_walker_manual_create_nested_node__node__post @@ -2931,7 +2931,6 @@ paths: summary: /manual_create_nested_node/{node} tags: - walker - - walker /walker/manual_delete_nested_edge: post: operationId: api_root_walker_manual_delete_nested_edge_post @@ -2950,7 +2949,6 @@ paths: summary: /manual_delete_nested_edge tags: - walker - - walker /walker/manual_delete_nested_edge/{node}: post: operationId: api_entry_walker_manual_delete_nested_edge__node__post @@ -2984,7 +2982,6 @@ paths: summary: /manual_delete_nested_edge/{node} tags: - walker - - walker /walker/manual_delete_nested_node: post: operationId: api_root_walker_manual_delete_nested_node_post @@ -3003,7 +3000,6 @@ paths: summary: /manual_delete_nested_node tags: - walker - - walker /walker/manual_delete_nested_node/{node}: post: operationId: api_entry_walker_manual_delete_nested_node__node__post @@ -3037,7 +3033,6 @@ paths: summary: /manual_delete_nested_node/{node} tags: - walker - - walker /walker/manual_detach_nested_node: post: operationId: api_root_walker_manual_detach_nested_node_post @@ -3056,7 +3051,6 @@ paths: summary: /manual_detach_nested_node tags: - walker - - walker /walker/manual_detach_nested_node/{node}: post: operationId: api_entry_walker_manual_detach_nested_node__node__post @@ -3090,7 +3084,6 @@ paths: summary: /manual_detach_nested_node/{node} tags: - walker - - walker /walker/manual_update_nested_node: post: operationId: api_root_walker_manual_update_nested_node_post @@ -3109,7 +3102,6 @@ paths: summary: /manual_update_nested_node tags: - walker - - walker /walker/manual_update_nested_node/{node}: post: operationId: api_entry_walker_manual_update_nested_node__node__post @@ -3143,7 +3135,6 @@ paths: summary: /manual_update_nested_node/{node} tags: - walker - - walker /walker/populate_graph: post: operationId: api_root_walker_populate_graph_post @@ -3162,7 +3153,6 @@ paths: summary: /populate_graph tags: - walker - - walker /walker/populate_graph/{node}: post: operationId: api_entry_walker_populate_graph__node__post @@ -3196,7 +3186,6 @@ paths: summary: /populate_graph/{node} tags: - walker - - walker /walker/post_all_excluded: post: operationId: api_root_walker_post_all_excluded_post @@ -3213,7 +3202,6 @@ paths: summary: /post_all_excluded tags: - walker - - walker /walker/post_all_excluded/{node}: post: operationId: api_entry_walker_post_all_excluded__node__post @@ -3245,7 +3233,6 @@ paths: summary: /post_all_excluded/{node} tags: - walker - - walker /walker/post_no_body: post: operationId: api_root_walker_post_no_body_post @@ -3264,7 +3251,6 @@ paths: summary: /post_no_body tags: - walker - - walker /walker/post_no_body/{node}: post: operationId: api_entry_walker_post_no_body__node__post @@ -3298,7 +3284,6 @@ paths: summary: /post_no_body/{node} tags: - walker - - walker /walker/post_partial_excluded: post: operationId: api_root_walker_post_partial_excluded_post @@ -3329,7 +3314,6 @@ paths: summary: /post_partial_excluded tags: - walker - - walker /walker/post_partial_excluded/{node}: post: operationId: api_entry_walker_post_partial_excluded__node__post @@ -3369,7 +3353,6 @@ paths: summary: /post_partial_excluded/{node} tags: - walker - - walker /walker/post_path_var/{a}: get: operationId: api_root_walker_post_path_var__a__get @@ -3401,7 +3384,6 @@ paths: summary: /post_path_var/{a} tags: - walker - - walker post: operationId: api_root_walker_post_path_var__a__post parameters: @@ -3432,7 +3414,6 @@ paths: summary: /post_path_var/{a} tags: - walker - - walker /walker/post_path_var/{node}/{a}: get: operationId: api_entry_walker_post_path_var__node___a__get @@ -3472,7 +3453,6 @@ paths: summary: /post_path_var/{node}/{a} tags: - walker - - walker post: operationId: api_entry_walker_post_path_var__node___a__post parameters: @@ -3511,7 +3491,6 @@ paths: summary: /post_path_var/{node}/{a} tags: - walker - - walker /walker/post_with_body: post: operationId: api_root_walker_post_with_body_post @@ -3542,7 +3521,6 @@ paths: summary: /post_with_body tags: - walker - - walker /walker/post_with_body/{node}: post: operationId: api_entry_walker_post_with_body__node__post @@ -3582,7 +3560,6 @@ paths: summary: /post_with_body/{node} tags: - walker - - walker /walker/post_with_body_and_file: post: operationId: api_root_walker_post_with_body_and_file_post @@ -3611,7 +3588,6 @@ paths: summary: /post_with_body_and_file tags: - walker - - walker /walker/post_with_body_and_file/{node}: post: operationId: api_entry_walker_post_with_body_and_file__node__post @@ -3649,7 +3625,6 @@ paths: summary: /post_with_body_and_file/{node} tags: - walker - - walker /walker/post_with_file: post: operationId: api_root_walker_post_with_file_post @@ -3680,7 +3655,6 @@ paths: summary: /post_with_file tags: - walker - - walker /walker/post_with_file/{node}: post: operationId: api_entry_walker_post_with_file__node__post @@ -3720,7 +3694,6 @@ paths: summary: /post_with_file/{node} tags: - walker - - walker /walker/purge_populated_graph: post: operationId: api_root_walker_purge_populated_graph_post @@ -3739,7 +3712,6 @@ paths: summary: /purge_populated_graph tags: - walker - - walker /walker/purge_populated_graph/{node}: post: operationId: api_entry_walker_purge_populated_graph__node__post @@ -3773,7 +3745,6 @@ paths: summary: /purge_populated_graph/{node} tags: - walker - - walker /walker/traverse_graph: post: operationId: api_root_walker_traverse_graph_post @@ -3792,7 +3763,6 @@ paths: summary: /traverse_graph tags: - walker - - walker /walker/traverse_graph/{node}: post: operationId: api_entry_walker_traverse_graph__node__post @@ -3826,7 +3796,6 @@ paths: summary: /traverse_graph/{node} tags: - walker - - walker /walker/traverse_populated_graph: post: operationId: api_root_walker_traverse_populated_graph_post @@ -3845,7 +3814,6 @@ paths: summary: /traverse_populated_graph tags: - walker - - walker /walker/traverse_populated_graph/{node}: post: operationId: api_entry_walker_traverse_populated_graph__node__post @@ -3879,7 +3847,6 @@ paths: summary: /traverse_populated_graph/{node} tags: - walker - - walker /walker/update_custom_object: post: operationId: api_root_walker_update_custom_object_post @@ -3910,7 +3877,6 @@ paths: summary: /update_custom_object tags: - walker - - walker /walker/update_custom_object/{node}: post: operationId: api_entry_walker_update_custom_object__node__post @@ -3950,7 +3916,6 @@ paths: summary: /update_custom_object/{node} tags: - walker - - walker /walker/update_graph: post: operationId: api_root_walker_update_graph_post @@ -3969,7 +3934,6 @@ paths: summary: /update_graph tags: - walker - - walker /walker/update_graph/{node}: post: operationId: api_entry_walker_update_graph__node__post @@ -4003,7 +3967,6 @@ paths: summary: /update_graph/{node} tags: - walker - - walker /walker/update_nested_node: post: operationId: api_root_walker_update_nested_node_post @@ -4022,7 +3985,6 @@ paths: summary: /update_nested_node tags: - walker - - walker /walker/update_nested_node/{node}: post: operationId: api_entry_walker_update_nested_node__node__post @@ -4056,7 +4018,6 @@ paths: summary: /update_nested_node/{node} tags: - walker - - walker /walker/visit_nested_node: post: operationId: api_root_walker_visit_nested_node_post @@ -4075,7 +4036,6 @@ paths: summary: /visit_nested_node tags: - walker - - walker /walker/visit_nested_node/{node}: post: operationId: api_entry_walker_visit_nested_node__node__post @@ -4109,4 +4069,157 @@ paths: summary: /visit_nested_node/{node} tags: - walker - - walker \ No newline at end of file + /webhook: + get: + description: Get keys API. + operationId: get_webhook_get + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + security: + - HTTPBearer: [] + summary: Get + tags: + - webhook + /webhook/delete: + delete: + description: Delete keys API. + operationId: delete_webhook_delete_delete + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/KeyIDs' + required: true + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: Delete + tags: + - webhook + /webhook/extend/{id}: + patch: + description: Generate key API. + operationId: extend_webhook_extend__id__patch + parameters: + - in: path + name: id + required: true + schema: + title: Id + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Expiration' + required: true + responses: + '201': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: Extend + tags: + - webhook + /webhook/generate-key: + post: + description: Generate key API. + operationId: generate_key_webhook_generate_key_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKey' + required: true + responses: + '201': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: Generate Key + tags: + - webhook + /webhook/walker/webhook: + post: + operationId: api_root_webhook_walker_webhook_post + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Root Webhook Walker Webhook Post + description: Successful Response + security: + - APIKeyHeader: [] + summary: /webhook + tags: + - webhook-walker + /webhook/walker/webhook/{node}: + post: + operationId: api_entry_webhook_walker_webhook__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Entry Webhook Walker Webhook Node Post + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - APIKeyHeader: [] + summary: /webhook/{node} + tags: + - webhook-walker \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/simple.json b/jac-cloud/jac_cloud/tests/simple.json new file mode 100644 index 0000000000..823aeae850 --- /dev/null +++ b/jac-cloud/jac_cloud/tests/simple.json @@ -0,0 +1,3 @@ +{ + "test": "success" +} \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index 62437e93ed..590d1a2685 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -803,4 +803,14 @@ walker delete_custom_object { # The only difference is BaseAnchor.ref doesn't # load the actual object and just use it as reference } +} + +walker webhook { + can enter1 with `root entry { + report here; + } + + class __specs__ { + has webhook: dict = {}; + } } \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph.py b/jac-cloud/jac_cloud/tests/test_simple_graph.py index 2ed04f080f..cdb8de616c 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph.py @@ -537,11 +537,11 @@ def trigger_custom_report(self) -> None: def trigger_upload_file(self) -> None: """Test upload file.""" - with open("jac_cloud/tests/simple_graph.jac", mode="br") as s: + with open("jac_cloud/tests/simple.json", mode="br") as s: files = [ - ("single", ("simple_graph.jac", s)), - ("multiple", ("simple_graph.jac", s)), - ("multiple", ("simple_graph.jac", s)), + ("single", ("simple.json", s)), + ("multiple", ("simple.json", s)), + ("multiple", ("simple.json", s)), ] res = post( f"{self.host}/walker/post_with_file", @@ -558,21 +558,21 @@ def trigger_upload_file(self) -> None: { "single": { "single": { - "name": "simple_graph.jac", - "content_type": "application/octet-stream", - "size": 17658, + "name": "simple.json", + "content_type": "application/json", + "size": 25, } }, "multiple": [ { - "name": "simple_graph.jac", - "content_type": "application/octet-stream", - "size": 17658, + "name": "simple.json", + "content_type": "application/json", + "size": 25, }, { - "name": "simple_graph.jac", - "content_type": "application/octet-stream", - "size": 17658, + "name": "simple.json", + "content_type": "application/json", + "size": 25, }, ], "singleOptional": None, diff --git a/jac/support/jac-lang.org/docs/learn/coders/jac-cloud/docs/jac_cloud_webhook.md b/jac/support/jac-lang.org/docs/learn/coders/jac-cloud/docs/jac_cloud_webhook.md new file mode 100644 index 0000000000..0d0ea56cce --- /dev/null +++ b/jac/support/jac-lang.org/docs/learn/coders/jac-cloud/docs/jac_cloud_webhook.md @@ -0,0 +1,115 @@ +# WEBHOOK +- webhook walker is similar to normal authenticated walker however, normal authenticated walker is associated to a user while webhook is directly to root. +- webhook api keys are manage by user +- supports different HTTP components as API key holder + - header (default): + - query + - path + - body +- name of the api-key can be change to any string (default: `X-API-KEY`) + +## CREATE WEBHOOK +```python +walker webhook { + can enter1 with `root entry { + report here; + } + + class __specs__ { + has webhook: dict = { + "type": "header | query | path | body", # optional: defaults to header + "name": "any string" # optional: defaults to X-API-KEY + }; + } +} +``` +![image](https://github.com/user-attachments/assets/75cceb2d-5618-4f68-97e2-31a4270e70b1) + +# WEBHOOK MANAGEMENT APIs +![image](https://github.com/user-attachments/assets/3a01ab35-06b0-4942-8f1f-0c4ae794ce21) + +## GENERATE API KEY +#### `REQUEST` +> **POST** /webhook/generate-key +```python +{ + # unique name of webhook + "name": "webhook1", + + # names of allowed webhook walkers. Not set or empty list means all webhook walkers is allowed. + "walkers": ["webhook"], + + # names of allowed nodes. Not set or empty list means all nodes is allowed. + "nodes": ["root"], + + # date now + timedelta( {{interval}}: {{count}} ) + "expiration": { + "count": 60, + + # seconds | minutes | hours | days + "interval": "days" + } +} +``` +#### `RESPONSE` +```python +{ + "id": "672203ee093fd3d208a4b6d4", + "name": "webhook1", + "key": "6721f000ee301e1d54c3de3d:1730282478:P4Nrs3DOLIkaw5aYsbIWNzWZZAwEyb20" +} +``` + +## GET API KEY +#### `REQUEST` +> **GET** /webhook +#### `RESPONSE` +```python +{ + "keys": [ + { + "id": "672203ee093fd3d208a4b6d4", + "name": "test", + "root_id": "6721f000ee301e1d54c3de3d", + "walkers": ["webhook"], + "nodes": ["root"], + "expiration": "2025-12-24T10:01:18.206000", + "key": "6721f000ee301e1d54c3de3d:1730282478:P4Nrs3DOLIkaw5aYsbIWNzWZZAwEyb20" + } + ] +} +``` + +## EXTEND API KEY +#### `REQUEST` +> **PATCH** /webhook/extend/`{id}` +```python +{ + "count": 60, + + # seconds | minutes | hours | days + "interval": "days" +} +``` +#### `RESPONSE` +```python +{ + "message": "Successfully Extended!" +} +``` + +## DELETE API KEY +#### `REQUEST` +> **DELETE** /webhook/delete +```python +{ + # list of id to be deleted + "ids": ["672203ee093fd3d208a4b6d4"] +} +``` +#### `RESPONSE` +```python +{ + "message": "Successfully Deleted!" +} +``` diff --git a/jac/support/jac-lang.org/mkdocs.yml b/jac/support/jac-lang.org/mkdocs.yml index f877c4ee66..36d0682fa2 100644 --- a/jac/support/jac-lang.org/mkdocs.yml +++ b/jac/support/jac-lang.org/mkdocs.yml @@ -20,6 +20,7 @@ nav: - "learn/coders/jac-cloud/docs/permission.md" - "learn/coders/jac-cloud/docs/jac_cloud_logging.md" - "learn/coders/jac-cloud/docs/jac_cloud_env_vars.md" + - "learn/coders/jac-cloud/docs/jac_cloud_webhook.md" - MTLLM: - "learn/with_llm.md" - Quick Start: