// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. #pragma once #include "ccf/ds/json.h" #include "ccf/ds/openapi.h" #include "ccf/endpoint_context.h" #include "ccf/http_consts.h" #include "ccf/rest_verb.h" #include "ccf/service/map.h" #include #include namespace ccf::endpoints { using URI = std::string; struct EndpointKey { /// URI path to endpoint URI uri_path; /// HTTP Verb RESTVerb verb = HTTP_POST; }; DECLARE_JSON_TYPE(EndpointKey); DECLARE_JSON_REQUIRED_FIELDS(EndpointKey, uri_path, verb); enum class ForwardingRequired { /** ForwardingRequired::Sometimes is the default value, and should be used * for most read-only operations. If this request is made to a backup node, * it may be forwarded to the primary node for execution to maintain session * consistency. Specifically, if this request is sent as part of a session * which was already forwarded, then it will also be forwarded. */ Sometimes, /** ForwardingRequired::Always should be used for operations which may * produce writes. If this request is made to a backup node, it will be * forwarded to the primary node for execution. */ Always, /** ForwardingRequired::Never should be used for operations which want to * read node-local state rather than the latest replicated state, such as * historical queries or local consensus information. This call will never * be forwarded, and is always executed on the receiving node, potentiall * breaking session consistency. If this attempts to write on a backup, this * will fail. */ Never }; enum class Mode { ReadWrite, ReadOnly, Historical }; enum QueryParamPresence { RequiredParameter, OptionalParameter, }; DECLARE_JSON_ENUM( ForwardingRequired, {{ForwardingRequired::Sometimes, "sometimes"}, {ForwardingRequired::Always, "always"}, {ForwardingRequired::Never, "never"}}); DECLARE_JSON_ENUM( Mode, {{Mode::ReadWrite, "readwrite"}, {Mode::ReadOnly, "readonly"}, {Mode::Historical, "historical"}}); struct EndpointProperties { /// Endpoint mode Mode mode = Mode::ReadWrite; /// Endpoint forwarding policy ForwardingRequired forwarding_required = ForwardingRequired::Always; /// Authentication policies std::vector authn_policies = {}; /// OpenAPI schema for endpoint nlohmann::json openapi; //// Whether to include endpoint schema in frontend schema bool openapi_hidden = false; /// JavaScript module std::string js_module; /// JavaScript function name std::string js_function; }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(EndpointProperties); DECLARE_JSON_REQUIRED_FIELDS( EndpointProperties, forwarding_required, authn_policies); DECLARE_JSON_OPTIONAL_FIELDS( EndpointProperties, openapi, openapi_hidden, mode, js_module, js_function); struct EndpointDefinition { virtual ~EndpointDefinition() = default; EndpointKey dispatch; /// Full URI path to endpoint, including method prefix URI full_uri_path; EndpointProperties properties; /** List of authentication policies which will be checked before executing * this endpoint. * * When multiple policies are specified, any single successful check is * sufficient to grant access, even if others fail. If all policies fail, * the last will set an error status on the response, and the endpoint * will not be invoked. If no policies are specified then the default * behaviour is that the endpoint accepts all requests, without any * authentication checks. * * If an auth policy passes, it may construct an object describing the * Identity of the caller to be used by the endpoint. This can be * retrieved inside the endpoint with ctx.get_caller(), * @see ccf::UserCertAuthnIdentity * @see ccf::JwtAuthnIdentity * @see ccf::UserSignatureAuthnIdentity * * @see ccf::empty_auth_policy * @see ccf::user_cert_auth_policy * @see ccf::user_signature_auth_policy */ AuthnPolicies authn_policies; }; using EndpointDefinitionPtr = std::shared_ptr; using EndpointsMap = ccf::ServiceMap; namespace Tables { static constexpr auto ENDPOINTS = "public:ccf.gov.endpoints"; } /** An Endpoint represents a user-defined resource that can be invoked by * authorised users via HTTP requests, over TLS. An Endpoint is accessible * at a specific verb and URI, e.g. POST /app/accounts or GET /app/records. * * Endpoints can read from and mutate the state of the replicated key-value * store. * * A CCF application is a collection of Endpoints recorded in the * application's EndpointRegistry. */ struct Endpoint : public EndpointDefinition { // Functor which is invoked to process requests for this Endpoint EndpointFunction func = {}; // Functor which is invoked to modify the response post commit. LocallyCommittedEndpointFunction locally_committed_func = {}; struct Installer { virtual void install(Endpoint&) = 0; }; Installer* installer; using SchemaBuilderFn = std::function; std::vector schema_builders = {}; bool openapi_hidden = false; http_status success_status = HTTP_STATUS_OK; nlohmann::json params_schema = nullptr; nlohmann::json result_schema = nullptr; /** Whether the endpoint should be omitted from the OpenAPI document. * * @return This Endpoint for further modification */ Endpoint& set_openapi_hidden(bool hidden); /** Sets the JSON schema that the request parameters must comply with. * * @param j Request parameters JSON schema * @return This Endpoint for further modification */ Endpoint& set_params_schema(const nlohmann::json& j); /** Sets the JSON schema that the request response must comply with. * * @param j Request response JSON schema * @param status Request response status code * @return This Endpoint for further modification */ Endpoint& set_result_schema( const nlohmann::json& j, std::optional status = std::nullopt); /** Sets the schema that the request and response bodies should comply * with. These are used to populate the generated OpenAPI document, but do * not introduce any constraints on the actual types that are parsed or * produced by the handling functor. * * \verbatim embed:rst:leading-asterisk * .. note:: * See ``DECLARE_JSON_`` serialisation macros for serialising * user-defined data structures. * \endverbatim * * @tparam In Request body JSON-serialisable data structure * @tparam Out Response body JSON-serialisable data structure * @param status Response status code * @return This Endpoint for further modification */ template Endpoint& set_auto_schema(std::optional status = std::nullopt) { if constexpr (!std::is_same_v) { params_schema = ds::json::build_schema(); schema_builders.push_back( [](nlohmann::json& document, const Endpoint& endpoint) { const auto http_verb = endpoint.dispatch.verb.get_http_method(); if (!http_verb.has_value()) { // Non-HTTP endpoints are not documented return; } ds::openapi::add_request_body_schema( document, endpoint.full_uri_path, http_verb.value()); }); } else { params_schema = nullptr; } if constexpr (!std::is_same_v) { success_status = status.value_or(HTTP_STATUS_OK); result_schema = ds::json::build_schema(); schema_builders.push_back( [](nlohmann::json& document, const Endpoint& endpoint) { const auto http_verb = endpoint.dispatch.verb.get_http_method(); if (!http_verb.has_value()) { return; } ds::openapi::add_response_schema( document, endpoint.full_uri_path, http_verb.value(), endpoint.success_status); }); } else { success_status = status.value_or(HTTP_STATUS_NO_CONTENT); result_schema = nullptr; } return *this; } /** Sets schemas for request and response bodies using typedefs within T. * @see set_auto_schema * * \verbatim embed:rst:leading-asterisk * .. note:: * ``T`` data structure should contain two nested ``In`` and ``Out`` * structures for request parameters and response format, respectively. * \endverbatim * * @tparam T Type containing ``In`` and ``Out`` typedefs with JSON-schema * description specialisations * @param status Request response status code * @return This Endpoint for further modification */ template Endpoint& set_auto_schema(std::optional status = std::nullopt) { return set_auto_schema(status); } /** Add OpenAPI documentation for a query parameter which can be passed to * this endpoint. * * @tparam T Type with appropriate ``ds::json`` specialisations to * generate a JSON schema description * @param param_name Name to be used for the query parameter to this * Endpoint * @param presence Enum value indicating whether this parameter is * required or optional * @return This Endpoint for further modification */ template Endpoint& add_query_parameter( const std::string& param_name, QueryParamPresence presence = RequiredParameter) { schema_builders.push_back( [param_name, presence](nlohmann::json& document, const Endpoint& endpoint) { const auto http_verb = endpoint.dispatch.verb.get_http_method(); if (!http_verb.has_value()) { // Non-HTTP endpoints are not documented return; } const auto schema_name = ds::json::schema_name(); const auto query_schema = ds::json::build_schema(); auto parameter = nlohmann::json::object(); parameter["name"] = param_name; parameter["in"] = "query"; parameter["required"] = presence == RequiredParameter; parameter["schema"] = ds::openapi::add_schema_to_components( document, schema_name, query_schema); ds::openapi::add_request_parameter_schema( document, endpoint.full_uri_path, http_verb.value(), parameter); }); return *this; } /** Overrides whether a Endpoint is always forwarded, or whether it is * safe to sometimes execute on followers. * * @param fr Enum value with desired status * @return This Endpoint for further modification */ Endpoint& set_forwarding_required(ForwardingRequired fr); void install(); }; using EndpointPtr = std::shared_ptr; } FMT_BEGIN_NAMESPACE template <> struct formatter { template constexpr auto parse(ParseContext& ctx) { return ctx.begin(); } template auto format( const ccf::endpoints::ForwardingRequired& v, FormatContext& ctx) const { char const* s; switch (v) { case ccf::endpoints::ForwardingRequired::Sometimes: { s = "sometimes"; break; } case ccf::endpoints::ForwardingRequired::Always: { s = "always"; break; } case ccf::endpoints::ForwardingRequired::Never: { s = "never"; break; } default: { throw std::logic_error("Unhandled value for ForwardingRequired"); } } return format_to(ctx.out(), "{}", s); } }; FMT_END_NAMESPACE