зеркало из https://github.com/microsoft/CCF.git
518 строки
16 KiB
C++
518 строки
16 KiB
C++
// 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 <string>
|
|
#include <utility>
|
|
|
|
namespace ccf::endpoints
|
|
{
|
|
using URI = std::string;
|
|
|
|
struct EndpointKey
|
|
{
|
|
/// URI path to endpoint
|
|
URI uri_path;
|
|
/// HTTP Verb
|
|
RESTVerb verb = HTTP_POST;
|
|
|
|
std::string to_str() const
|
|
{
|
|
return fmt::format("{} {}", verb.c_str(), uri_path);
|
|
}
|
|
};
|
|
}
|
|
|
|
namespace ccf::kv::serialisers
|
|
{
|
|
template <>
|
|
struct BlitSerialiser<ccf::endpoints::EndpointKey>
|
|
{
|
|
static SerialisedEntry to_serialised(
|
|
const ccf::endpoints::EndpointKey& endpoint_key)
|
|
{
|
|
auto str =
|
|
fmt::format("{} {}", endpoint_key.verb.c_str(), endpoint_key.uri_path);
|
|
return SerialisedEntry(str.begin(), str.end());
|
|
}
|
|
|
|
static ccf::endpoints::EndpointKey from_serialised(
|
|
const SerialisedEntry& data)
|
|
{
|
|
std::string str{data.begin(), data.end()};
|
|
auto i = str.find(' ');
|
|
if (i == std::string::npos)
|
|
{
|
|
throw std::logic_error("invalid encoding of endpoint key");
|
|
}
|
|
auto verb = str.substr(0, i);
|
|
auto uri_path = str.substr(i + 1);
|
|
return {uri_path, verb};
|
|
}
|
|
};
|
|
}
|
|
|
|
namespace ccf::endpoints
|
|
{
|
|
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 RedirectionStrategy
|
|
{
|
|
/** This operation does not need to be redirected, and can be executed on
|
|
the receiving node. Most read-only operations can be executed on any
|
|
node, so should be marked as None. */
|
|
None,
|
|
|
|
/** This operation must be executed on a primary. If the current node is not
|
|
a primary, it should attempt to redirect to the primary, or else return
|
|
an error. Any write operations must be executed on a primary, so should
|
|
be marked as ToPrimary. */
|
|
ToPrimary,
|
|
|
|
/** This operation should be executed on a backup. If the current node is
|
|
not a backup, it should attempt to redirect to a backup, or else return
|
|
an error. Only read operations can be marked as ToBackup. */
|
|
ToBackup,
|
|
};
|
|
|
|
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(
|
|
RedirectionStrategy,
|
|
{{RedirectionStrategy::None, "none"},
|
|
{RedirectionStrategy::ToPrimary, "to_primary"},
|
|
{RedirectionStrategy::ToBackup, "to_backup"}});
|
|
|
|
DECLARE_JSON_ENUM(
|
|
Mode,
|
|
{{Mode::ReadWrite, "readwrite"},
|
|
{Mode::ReadOnly, "readonly"},
|
|
{Mode::Historical, "historical"}});
|
|
|
|
struct InterpreterReusePolicy
|
|
{
|
|
enum
|
|
{
|
|
KeyBased
|
|
} kind;
|
|
|
|
std::string key;
|
|
|
|
bool operator==(const InterpreterReusePolicy&) const = default;
|
|
};
|
|
|
|
void to_json(nlohmann::json& j, const InterpreterReusePolicy& grp);
|
|
void from_json(const nlohmann::json& j, InterpreterReusePolicy& grp);
|
|
std::string schema_name(const InterpreterReusePolicy*);
|
|
void fill_json_schema(nlohmann::json& schema, const InterpreterReusePolicy*);
|
|
|
|
struct EndpointProperties
|
|
{
|
|
/// Endpoint mode
|
|
Mode mode = Mode::ReadWrite;
|
|
/// Endpoint forwarding policy
|
|
ForwardingRequired forwarding_required = ForwardingRequired::Always;
|
|
/// Endpoint redirection policy
|
|
RedirectionStrategy redirection_strategy = RedirectionStrategy::ToPrimary;
|
|
/// Authentication policies
|
|
std::vector<nlohmann::json> 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;
|
|
/// Determines how JS interpreters may be reused between multiple calls,
|
|
/// sharing global state in potentially unsafe ways. The default empty value
|
|
/// means no reuse is permitted.
|
|
std::optional<InterpreterReusePolicy> interpreter_reuse = std::nullopt;
|
|
};
|
|
|
|
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,
|
|
interpreter_reuse,
|
|
redirection_strategy);
|
|
|
|
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<IdentType>(),
|
|
* @see ccf::UserCertAuthnIdentity
|
|
* @see ccf::MemberCertAuthnIdentity
|
|
* @see ccf::UserCOSESign1tAuthnIdentity
|
|
* @see ccf::MemberCOSESign1AuthnIdentity
|
|
* @see ccf::JwtAuthnIdentity
|
|
*
|
|
* @see ccf::empty_auth_policy
|
|
* @see ccf::user_cert_auth_policy
|
|
* @see ccf::any_cert_auth_policy
|
|
*/
|
|
AuthnPolicies authn_policies;
|
|
};
|
|
|
|
using EndpointDefinitionPtr = std::shared_ptr<const EndpointDefinition>;
|
|
|
|
using EndpointsMap = ccf::ServiceMap<EndpointKey, EndpointProperties>;
|
|
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<void(nlohmann::json&, const Endpoint&)>;
|
|
std::vector<SchemaBuilderFn> schema_builders = {};
|
|
|
|
bool openapi_hidden = false;
|
|
|
|
http_status success_status = HTTP_STATUS_OK;
|
|
nlohmann::json params_schema = nullptr;
|
|
nlohmann::json result_schema = nullptr;
|
|
|
|
std::optional<std::string> openapi_summary = std::nullopt;
|
|
std::optional<std::string> openapi_description = std::nullopt;
|
|
std::optional<bool> openapi_deprecated = std::nullopt;
|
|
|
|
/** Set the OpenAPI description for the endpoint.
|
|
*
|
|
* @return This Endpoint for further modification
|
|
*/
|
|
Endpoint& set_openapi_description(const std::string& description);
|
|
|
|
/** Set the OpenAPI summary for the endpoint.
|
|
*
|
|
* @return This Endpoint for further modification
|
|
*/
|
|
Endpoint& set_openapi_summary(const std::string& summary);
|
|
|
|
/** Set the endpoint as deprecated.
|
|
*
|
|
* @return This Endpoint for further modification
|
|
*/
|
|
Endpoint& set_openapi_deprecated(bool is_deprecated);
|
|
|
|
/** Set the endpoint as deprecated and overwrites the description to include
|
|
* deprecation version and point to a replacement endpoint.
|
|
*
|
|
* @return This Endpoint for further modification
|
|
*/
|
|
Endpoint& set_openapi_deprecated_replaced(
|
|
const std::string& deprecation_version, const std::string& replacement);
|
|
|
|
/** 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<http_status> 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 <typename In, typename Out>
|
|
Endpoint& set_auto_schema(std::optional<http_status> status = std::nullopt)
|
|
{
|
|
if constexpr (!std::is_same_v<In, void>)
|
|
{
|
|
params_schema = ds::json::build_schema<In>();
|
|
|
|
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<In>(
|
|
document, endpoint.full_uri_path, http_verb.value());
|
|
});
|
|
}
|
|
else
|
|
{
|
|
params_schema = nullptr;
|
|
}
|
|
|
|
if constexpr (!std::is_same_v<Out, void>)
|
|
{
|
|
success_status = status.value_or(HTTP_STATUS_OK);
|
|
|
|
result_schema = ds::json::build_schema<Out>();
|
|
|
|
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<Out>(
|
|
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 <typename T>
|
|
Endpoint& set_auto_schema(std::optional<http_status> status = std::nullopt)
|
|
{
|
|
return set_auto_schema<typename T::In, typename T::Out>(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 <typename T>
|
|
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<T>();
|
|
const auto query_schema = ds::json::build_schema<T>();
|
|
|
|
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);
|
|
|
|
Endpoint& set_redirection_strategy(RedirectionStrategy rs);
|
|
|
|
void install();
|
|
};
|
|
|
|
using EndpointPtr = std::shared_ptr<const Endpoint>;
|
|
}
|
|
|
|
FMT_BEGIN_NAMESPACE
|
|
template <>
|
|
struct formatter<ccf::endpoints::ForwardingRequired>
|
|
{
|
|
template <typename ParseContext>
|
|
constexpr auto parse(ParseContext& ctx)
|
|
{
|
|
return ctx.begin();
|
|
}
|
|
|
|
template <typename FormatContext>
|
|
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
|