diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index 349b32d47f..bfbbba1015 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -219,17 +219,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: @@ -250,16 +250,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 52be816451..e65889a23e 100644 --- a/jac-cloud/jac_cloud/jaseci/datasources/redis.py +++ b/jac-cloud/jac_cloud/jaseci/datasources/redis.py @@ -73,7 +73,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() @@ -114,7 +114,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() @@ -168,6 +168,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. @@ -224,7 +234,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() @@ -265,7 +275,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..2a2acd6428 100644 --- a/jac-cloud/jac_cloud/jaseci/security/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/security/__init__.py @@ -7,12 +7,12 @@ from fastapi import Depends, Request from fastapi.exceptions import HTTPException -from fastapi.security import HTTPBearer +from fastapi.security import APIKeyHeader, 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 +84,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 +111,35 @@ def authenticate(request: Request) -> None: raise HTTPException(status_code=401) +def authenticate_webhook(request: Request) -> None: + """Authenticate current request and attach authenticated user and their root.""" + if ( + (key := request.headers.get("X-API-Key")) + 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 + + raise HTTPException(status_code=401) + + authenticator = [Depends(HTTPBearer()), Depends(authenticate)] +authenticator_webhook = [ + Depends(APIKeyHeader(name="X-API-Key")), + Depends(authenticate_webhook), +] diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index 25bc4f8f6c..dd20e00d3d 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -52,7 +52,7 @@ ) from ..core.context import ContextResponse, ExecutionContext, JaseciContext from ..jaseci import FastAPI -from ..jaseci.security import authenticator +from ..jaseci.security import authenticator, authenticator_webhook, validate_request from ..jaseci.utils import log_entry, log_exit @@ -67,6 +67,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: @@ -103,6 +104,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 + is_webhook: bool = specs.is_webhook query: dict[str, Any] = {} body: dict[str, Any] = {} @@ -194,8 +196,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() @@ -208,7 +212,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() @@ -221,10 +225,17 @@ def api_root( ) -> Response: return api_entry(request, None, payload) + if is_webhook: + target_authenticator = authenticator_webhook + target_router = webhook_walker_router + else: + target_authenticator = authenticator + target_router = 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) @@ -240,11 +251,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 @@ -263,6 +273,7 @@ def specs( excluded: str | list[str] = [], # noqa: B006 auth: bool = True, private: bool = False, + is_webhook: bool = False, ) -> Callable: """Walker Decorator.""" @@ -274,6 +285,7 @@ def wrapper(cls: Type[WalkerArchitype]) -> Type[WalkerArchitype]: ex = excluded a = auth pv = private + iw = is_webhook class __specs__(DefaultSpecs): # noqa: N801 path: str = p @@ -282,6 +294,7 @@ class __specs__(DefaultSpecs): # noqa: N801 excluded: str | list[str] = ex auth: bool = a private: bool = pv + is_webhook: bool = iw cls.__specs__ = __specs__ # type: ignore[attr-defined] @@ -303,6 +316,7 @@ class DefaultSpecs: excluded: str | list[str] = [] auth: bool = True private: bool = False + is_webhook: bool = False class JacAccessValidationPlugin: diff --git a/jac-cloud/jac_cloud/tests/openapi_specs.yaml b/jac-cloud/jac_cloud/tests/openapi_specs.yaml index a7fa6a14a1..7943eef8cc 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: @@ -440,6 +495,10 @@ components: title: post_with_body_body_model type: object securitySchemes: + APIKeyHeader: + in: header + name: X-API-Key + type: apiKey HTTPBearer: scheme: bearer type: http @@ -800,7 +859,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 @@ -840,7 +898,6 @@ paths: summary: /allow_other_root_access/{node} tags: - walker - - walker /walker/combination1: get: operationId: api_root_walker_combination1_get @@ -884,7 +941,6 @@ paths: summary: /combination1 tags: - walker - - walker post: operationId: api_root_walker_combination1_post parameters: @@ -927,7 +983,6 @@ paths: summary: /combination1 tags: - walker - - walker /walker/combination1/{node}: get: operationId: api_entry_walker_combination1__node__get @@ -979,7 +1034,6 @@ paths: summary: /combination1/{node} tags: - walker - - walker post: operationId: api_entry_walker_combination1__node__post parameters: @@ -1030,7 +1084,6 @@ paths: summary: /combination1/{node} tags: - walker - - walker /walker/combination2/{a}: delete: operationId: api_root_walker_combination2__a__delete @@ -1074,7 +1127,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker get: operationId: api_root_walker_combination2__a__get parameters: @@ -1117,7 +1169,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker head: operationId: api_root_walker_combination2__a__head parameters: @@ -1160,7 +1211,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker options: operationId: api_root_walker_combination2__a__options parameters: @@ -1197,7 +1247,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker patch: operationId: api_root_walker_combination2__a__patch parameters: @@ -1240,7 +1289,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker post: operationId: api_root_walker_combination2__a__post parameters: @@ -1283,7 +1331,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker put: operationId: api_root_walker_combination2__a__put parameters: @@ -1326,7 +1373,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker trace: operationId: api_root_walker_combination2__a__trace parameters: @@ -1363,7 +1409,6 @@ paths: summary: /combination2/{a} tags: - walker - - walker /walker/combination2/{node}/{a}: delete: operationId: api_entry_walker_combination2__node___a__delete @@ -1415,7 +1460,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker get: operationId: api_entry_walker_combination2__node___a__get parameters: @@ -1466,7 +1510,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker head: operationId: api_entry_walker_combination2__node___a__head parameters: @@ -1517,7 +1560,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker options: operationId: api_entry_walker_combination2__node___a__options parameters: @@ -1562,7 +1604,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker patch: operationId: api_entry_walker_combination2__node___a__patch parameters: @@ -1613,7 +1654,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker post: operationId: api_entry_walker_combination2__node___a__post parameters: @@ -1664,7 +1704,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker put: operationId: api_entry_walker_combination2__node___a__put parameters: @@ -1715,7 +1754,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker trace: operationId: api_entry_walker_combination2__node___a__trace parameters: @@ -1760,7 +1798,6 @@ paths: summary: /combination2/{node}/{a} tags: - walker - - walker /walker/create_graph: post: operationId: api_root_walker_create_graph_post @@ -1779,7 +1816,6 @@ paths: summary: /create_graph tags: - walker - - walker /walker/create_graph/{node}: post: operationId: api_entry_walker_create_graph__node__post @@ -1813,7 +1849,6 @@ paths: summary: /create_graph/{node} tags: - walker - - walker /walker/create_nested_node: post: operationId: api_root_walker_create_nested_node_post @@ -1832,7 +1867,6 @@ paths: summary: /create_nested_node tags: - walker - - walker /walker/create_nested_node/{node}: post: operationId: api_entry_walker_create_nested_node__node__post @@ -1866,7 +1900,6 @@ paths: summary: /create_nested_node/{node} tags: - walker - - walker /walker/custom_report: post: operationId: api_root_walker_custom_report_post @@ -1883,7 +1916,6 @@ paths: summary: /custom_report tags: - walker - - walker /walker/custom_report/{node}: post: operationId: api_entry_walker_custom_report__node__post @@ -1915,7 +1947,6 @@ paths: summary: /custom_report/{node} tags: - walker - - walker /walker/custom_status_code: post: operationId: api_root_walker_custom_status_code_post @@ -1946,7 +1977,6 @@ paths: summary: /custom_status_code tags: - walker - - walker /walker/custom_status_code/{node}: post: operationId: api_entry_walker_custom_status_code__node__post @@ -1986,7 +2016,6 @@ paths: summary: /custom_status_code/{node} tags: - walker - - walker /walker/delete_nested_edge: post: operationId: api_root_walker_delete_nested_edge_post @@ -2005,7 +2034,6 @@ paths: summary: /delete_nested_edge tags: - walker - - walker /walker/delete_nested_edge/{node}: post: operationId: api_entry_walker_delete_nested_edge__node__post @@ -2039,7 +2067,6 @@ paths: summary: /delete_nested_edge/{node} tags: - walker - - walker /walker/delete_nested_node: post: operationId: api_root_walker_delete_nested_node_post @@ -2058,7 +2085,6 @@ paths: summary: /delete_nested_node tags: - walker - - walker /walker/delete_nested_node/{node}: post: operationId: api_entry_walker_delete_nested_node__node__post @@ -2092,7 +2118,6 @@ paths: summary: /delete_nested_node/{node} tags: - walker - - walker /walker/detach_nested_node: post: operationId: api_root_walker_detach_nested_node_post @@ -2111,7 +2136,6 @@ paths: summary: /detach_nested_node tags: - walker - - walker /walker/detach_nested_node/{node}: post: operationId: api_entry_walker_detach_nested_node__node__post @@ -2145,7 +2169,6 @@ paths: summary: /detach_nested_node/{node} tags: - walker - - walker /walker/detach_node: post: operationId: api_root_walker_detach_node_post @@ -2164,7 +2187,6 @@ paths: summary: /detach_node tags: - walker - - walker /walker/detach_node/{node}: post: operationId: api_entry_walker_detach_node__node__post @@ -2198,7 +2220,6 @@ paths: summary: /detach_node/{node} tags: - walker - - walker /walker/different_return: post: operationId: api_root_walker_different_return_post @@ -2215,7 +2236,6 @@ paths: summary: /different_return tags: - walker - - walker /walker/different_return/{node}: post: operationId: api_entry_walker_different_return__node__post @@ -2247,7 +2267,6 @@ paths: summary: /different_return/{node} tags: - walker - - walker /walker/disallow_other_root_access: post: operationId: api_root_walker_disallow_other_root_access_post @@ -2278,7 +2297,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 @@ -2318,7 +2336,6 @@ paths: summary: /disallow_other_root_access/{node} tags: - walker - - walker /walker/get_all_query: get: operationId: api_root_walker_get_all_query_get @@ -2354,7 +2371,6 @@ paths: summary: /get_all_query tags: - walker - - walker /walker/get_all_query/{node}: get: operationId: api_entry_walker_get_all_query__node__get @@ -2398,7 +2414,6 @@ paths: summary: /get_all_query/{node} tags: - walker - - walker /walker/get_no_body: get: operationId: api_root_walker_get_no_body_get @@ -2417,7 +2432,6 @@ paths: summary: /get_no_body tags: - walker - - walker /walker/get_no_body/{node}: get: operationId: api_entry_walker_get_no_body__node__get @@ -2451,7 +2465,6 @@ paths: summary: /get_no_body/{node} tags: - walker - - walker /walker/get_with_query: get: operationId: api_root_walker_get_with_query_get @@ -2483,7 +2496,6 @@ paths: summary: /get_with_query tags: - walker - - walker /walker/get_with_query/{node}: get: operationId: api_entry_walker_get_with_query__node__get @@ -2523,7 +2535,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 @@ -2542,7 +2553,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 @@ -2576,7 +2586,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 @@ -2595,7 +2604,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 @@ -2629,7 +2637,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 @@ -2648,7 +2655,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 @@ -2682,7 +2688,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 @@ -2701,7 +2706,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 @@ -2735,7 +2739,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 @@ -2754,7 +2757,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 @@ -2788,7 +2790,6 @@ paths: summary: /manual_update_nested_node/{node} tags: - walker - - walker /walker/post_all_excluded: post: operationId: api_root_walker_post_all_excluded_post @@ -2805,7 +2806,6 @@ paths: summary: /post_all_excluded tags: - walker - - walker /walker/post_all_excluded/{node}: post: operationId: api_entry_walker_post_all_excluded__node__post @@ -2837,7 +2837,6 @@ paths: summary: /post_all_excluded/{node} tags: - walker - - walker /walker/post_no_body: post: operationId: api_root_walker_post_no_body_post @@ -2856,7 +2855,6 @@ paths: summary: /post_no_body tags: - walker - - walker /walker/post_no_body/{node}: post: operationId: api_entry_walker_post_no_body__node__post @@ -2890,7 +2888,6 @@ paths: summary: /post_no_body/{node} tags: - walker - - walker /walker/post_partial_excluded: post: operationId: api_root_walker_post_partial_excluded_post @@ -2921,7 +2918,6 @@ paths: summary: /post_partial_excluded tags: - walker - - walker /walker/post_partial_excluded/{node}: post: operationId: api_entry_walker_post_partial_excluded__node__post @@ -2961,7 +2957,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 @@ -2993,7 +2988,6 @@ paths: summary: /post_path_var/{a} tags: - walker - - walker post: operationId: api_root_walker_post_path_var__a__post parameters: @@ -3024,7 +3018,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 @@ -3064,7 +3057,6 @@ paths: summary: /post_path_var/{node}/{a} tags: - walker - - walker post: operationId: api_entry_walker_post_path_var__node___a__post parameters: @@ -3103,7 +3095,6 @@ paths: summary: /post_path_var/{node}/{a} tags: - walker - - walker /walker/post_with_body: post: operationId: api_root_walker_post_with_body_post @@ -3134,7 +3125,6 @@ paths: summary: /post_with_body tags: - walker - - walker /walker/post_with_body/{node}: post: operationId: api_entry_walker_post_with_body__node__post @@ -3174,7 +3164,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 @@ -3203,7 +3192,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 @@ -3241,7 +3229,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 @@ -3272,7 +3259,6 @@ paths: summary: /post_with_file tags: - walker - - walker /walker/post_with_file/{node}: post: operationId: api_entry_walker_post_with_file__node__post @@ -3312,7 +3298,6 @@ paths: summary: /post_with_file/{node} tags: - walker - - walker /walker/traverse_graph: post: operationId: api_root_walker_traverse_graph_post @@ -3331,7 +3316,6 @@ paths: summary: /traverse_graph tags: - walker - - walker /walker/traverse_graph/{node}: post: operationId: api_entry_walker_traverse_graph__node__post @@ -3365,7 +3349,6 @@ paths: summary: /traverse_graph/{node} tags: - walker - - walker /walker/update_graph: post: operationId: api_root_walker_update_graph_post @@ -3384,7 +3367,6 @@ paths: summary: /update_graph tags: - walker - - walker /walker/update_graph/{node}: post: operationId: api_entry_walker_update_graph__node__post @@ -3418,7 +3400,6 @@ paths: summary: /update_graph/{node} tags: - walker - - walker /walker/update_nested_node: post: operationId: api_root_walker_update_nested_node_post @@ -3437,7 +3418,6 @@ paths: summary: /update_nested_node tags: - walker - - walker /walker/update_nested_node/{node}: post: operationId: api_entry_walker_update_nested_node__node__post @@ -3471,7 +3451,6 @@ paths: summary: /update_nested_node/{node} tags: - walker - - walker /walker/visit_nested_node: post: operationId: api_root_walker_visit_nested_node_post @@ -3490,7 +3469,6 @@ paths: summary: /visit_nested_node tags: - walker - - walker /walker/visit_nested_node/{node}: post: operationId: api_entry_walker_visit_nested_node__node__post @@ -3524,4 +3502,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_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index 3fcc4753b9..4eaea5f23d 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -277,4 +277,14 @@ walker custom_report { class __specs__ { has auth: bool = False; } +} + +walker webhook { + can enter1 with `root entry { + report here; + } + + class __specs__ { + has is_webhook: bool = True; + } } \ 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 5cc1b82395..affb068261 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph.py @@ -612,19 +612,19 @@ def trigger_upload_file(self) -> None: "single": { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7139, } }, "multiple": [ { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7139, }, { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7139, }, ], "singleOptional": None, diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py index 9c24a23416..0f8af11e9a 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py @@ -390,19 +390,19 @@ def trigger_upload_file(self) -> None: "single": { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7139, } }, "multiple": [ { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7139, }, { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7139, }, ], "singleOptional": None,