Skip to content

Commit

Permalink
Add the ability to limit changeset size
Browse files Browse the repository at this point in the history
  • Loading branch information
mmd-osm committed Jun 19, 2024
1 parent 467a61d commit b2a8e7c
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 0 deletions.
2 changes: 2 additions & 0 deletions include/cgimap/backend/apidb/pgsql_update.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class pgsql_update : public data_update {

uint32_t get_rate_limit(osm_user_id_t uid) override;

uint64_t get_bbox_size_limit(osm_user_id_t uid) override;

/**
* abstracts the creation of transactions for the
* data updates.
Expand Down
3 changes: 3 additions & 0 deletions include/cgimap/data_update.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class data_update {
// get the current rate limit for changeset uploads for a given user id
virtual uint32_t get_rate_limit(osm_user_id_t) = 0;

// get the current maximum bounding box size for a given user id
virtual uint64_t get_bbox_size_limit(osm_user_id_t uid) = 0;

/**
* factory for the creation of data updates. this abstracts away
* the creation process of transactions, and allows some up-front
Expand Down
14 changes: 14 additions & 0 deletions include/cgimap/options.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class global_settings_base {
virtual uint32_t get_ratelimiter_ratelimit(bool) const = 0;
virtual uint32_t get_ratelimiter_maxdebt(bool) const = 0;
virtual bool get_ratelimiter_upload() const = 0;
virtual bool get_bbox_size_limiter_upload() const = 0;
};

class global_settings_default : public global_settings_base {
Expand Down Expand Up @@ -97,6 +98,10 @@ class global_settings_default : public global_settings_base {
bool get_ratelimiter_upload() const override {
return false;
}

bool get_bbox_size_limiter_upload() const override {
return false;
}
};

class global_settings_via_options : public global_settings_base {
Expand Down Expand Up @@ -175,6 +180,10 @@ class global_settings_via_options : public global_settings_base {
return m_ratelimiter_upload;
}

bool get_bbox_size_limiter_upload() const override {
return m_bbox_size_limiter_upload;
}

private:
void init_fallback_values(const global_settings_base &def);
void set_new_options(const po::variables_map &options);
Expand All @@ -191,6 +200,7 @@ class global_settings_via_options : public global_settings_base {
void set_ratelimiter_ratelimit(const po::variables_map &options);
void set_ratelimiter_maxdebt(const po::variables_map &options);
void set_ratelimiter_upload(const po::variables_map &options);
void set_bbox_size_limiter_upload(const po::variables_map &options);
bool validate_timeout(const std::string &timeout) const;

uint32_t m_payload_max_size;
Expand All @@ -208,6 +218,7 @@ class global_settings_via_options : public global_settings_base {
uint32_t m_ratelimiter_maxdebt;
uint32_t m_moderator_ratelimiter_maxdebt;
bool m_ratelimiter_upload;
bool m_bbox_size_limiter_upload;
};

class global_settings final {
Expand Down Expand Up @@ -256,6 +267,9 @@ class global_settings final {
// Use ratelimiter for changeset uploads
static bool get_ratelimiter_upload() { return settings->get_ratelimiter_upload(); }

// Use bbox size limiter for changeset uploads
static bool get_bbox_size_limiter_upload() { return settings->get_bbox_size_limiter_upload(); }

private:
static std::unique_ptr<global_settings_base> settings; // gets initialized with global_settings_default instance
};
Expand Down
4 changes: 4 additions & 0 deletions include/cgimap/util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ class bbox_t {
os << "[" << bbox.minlat << "," << bbox.minlon << "," << bbox.maxlat << "," << bbox.maxlon << "]";
return os;
}

long linear_size() const {
return ((maxlon - minlon) + (maxlat - minlat));
}
};

#endif
Expand Down
20 changes: 20 additions & 0 deletions src/api06/changeset_upload_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ changeset_upload_responder::changeset_upload_responder(mime::type mt,
}
}

if (global_settings::get_bbox_size_limiter_upload()) {

auto const cs_bbox = handler.get_bbox();

if (!(cs_bbox == bbox_t())) // valid bbox?
{
auto const max_bbox_size = upd.get_bbox_size_limit(*user_id);

if (cs_bbox.linear_size() > max_bbox_size) {

logger::message(
fmt::format(
"Upload of {} changes by user {} in changeset {} blocked due to bbox size limit exceeded, max bbox size {}",
new_changes, *user_id, changeset, max_bbox_size));

throw http::payload_too_large("Upload has been blocked because the affected area is too large.");
}
}
}

changeset_updater->update_changeset(new_changes, handler.get_bbox());

upd.commit();
Expand Down
19 changes: 19 additions & 0 deletions src/backend/apidb/pgsql_update.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ uint32_t pgsql_update::get_rate_limit(osm_user_id_t uid)
return std::max(0, rate_limit);
}

uint64_t pgsql_update::get_bbox_size_limit(osm_user_id_t uid)
{
{
m.prepare("api_size_limit",
R"(SELECT * FROM api_size_limit($1) LIMIT 1 )");

auto res = m.exec_prepared("api_size_limit", uid);

if (res.size() != 1) {
throw http::server_error("api_size_limit db function did not return any data");
}

auto row = res[0];
auto bbox_size_limit = row[0].as<int64_t>();

return std::max(bbox_size_limit, 0l);
}
}


pgsql_update::factory::factory(const po::variables_map &opts)
: m_connection(connect_db_str(opts)),
Expand Down
1 change: 1 addition & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ void get_options(int argc, char **argv, po::variables_map &options) {
("max-relation-members", po::value<int>(), "max number of relation members per relation")
("max-element-tags", po::value<int>(), "max number of tags per OSM element")
("ratelimit-upload", po::value<bool>(), "enable rate limiting for changeset upload")
("bbox-size-limit-upload", po::value<bool>(), "enable bbox size limit for changeset upload")
;
// clang-format on

Expand Down
7 changes: 7 additions & 0 deletions src/options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ void global_settings_via_options::init_fallback_values(const global_settings_bas
m_ratelimiter_maxdebt = def.get_ratelimiter_maxdebt(false);
m_moderator_ratelimiter_maxdebt = def.get_ratelimiter_maxdebt(true);
m_ratelimiter_upload = def.get_ratelimiter_upload();
m_bbox_size_limiter_upload = def.get_bbox_size_limiter_upload();
}

void global_settings_via_options::set_new_options(const po::variables_map &options) {
Expand All @@ -50,6 +51,7 @@ void global_settings_via_options::set_new_options(const po::variables_map &optio
set_ratelimiter_ratelimit(options);
set_ratelimiter_maxdebt(options);
set_ratelimiter_upload(options);
set_bbox_size_limiter_upload(options);
}

void global_settings_via_options::set_payload_max_size(const po::variables_map &options) {
Expand Down Expand Up @@ -185,6 +187,11 @@ void global_settings_via_options::set_ratelimiter_upload(const po::variables_map
}
}

void global_settings_via_options::set_bbox_size_limiter_upload(const po::variables_map &options) {
if (options.count("bbox-size-limit-upload")) {
m_bbox_size_limiter_upload = options["bbox-size-limit-upload"].as<bool>();
}
}

bool global_settings_via_options::validate_timeout(const std::string &timeout) const {
std::smatch sm;
Expand Down
143 changes: 143 additions & 0 deletions test/test_apidb_backend_changeset_uploads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ class global_settings_enable_upload_rate_limiter_test_class : public global_sett
};


class global_setting_enable_bbox_size_limiter_test_class : public global_settings_default {

public:
// enable bbox size limiter
bool get_bbox_size_limiter_upload() const override { return true; }
};

std::unique_ptr<xmlDoc, void (*)(xmlDoc *)> getDocument(const std::string &document)
{
return {xmlReadDoc((xmlChar *)(document.c_str()), NULL, NULL, XML_PARSE_PEDANTIC | XML_PARSE_NONET), xmlFreeDoc};
Expand Down Expand Up @@ -2400,6 +2407,7 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_end_to_end", "[changeset

}


TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_rate_limiter", "[changeset][upload][db]" ) {

// Upload rate limiter enabling
Expand Down Expand Up @@ -2557,6 +2565,141 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_rate_limiter", "[changes
}
}


TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_bbox_size_limiter", "[changeset][upload][db]" ) {

// Upload bbox size limiter enabling
auto test_settings = std::unique_ptr<
global_setting_enable_bbox_size_limiter_test_class >(
new global_setting_enable_bbox_size_limiter_test_class());
global_settings::set_configuration(std::move(test_settings));

const std::string bearertoken = "Bearer 4f41f2328befed5a33bcabdf14483081c8df996cbafc41e313417776e8fafae8";
const std::string generator = "Test";

auto sel_factory = tdb.get_data_selection_factory();
auto upd_factory = tdb.get_data_update_factory();

null_rate_limiter limiter;
routes route;

SECTION("Initialize test data") {

tdb.run_sql(R"(
INSERT INTO users (id, email, pass_crypt, pass_salt, creation_time, display_name, data_public, status)
VALUES
(1, 'demo@example.com', 'xx', '', '2013-11-14T02:10:00Z', 'demo', true, 'confirmed'),
(2, 'user_2@example.com', '', '', '2013-11-14T02:10:00Z', 'user_2', false, 'active');
INSERT INTO changesets (id, user_id, created_at, closed_at, num_changes)
VALUES
(1, 1, now() at time zone 'utc', now() at time zone 'utc' + '1 hour' ::interval, 0),
(3, 1, now() at time zone 'utc' - '12 hour' ::interval,
now() at time zone 'utc' - '11 hour' ::interval, 10000),
(4, 2, now() at time zone 'utc', now() at time zone 'utc' + '1 hour' ::interval, 10000),
(5, 2, '2013-11-14T02:10:00Z', '2013-11-14T03:10:00Z', 10000);
INSERT INTO user_blocks (user_id, creator_id, reason, ends_at, needs_view)
VALUES (1, 2, '', now() at time zone 'utc' - ('1 hour' ::interval), false);
SELECT setval('current_nodes_id_seq', 14000000000, false);
INSERT INTO oauth_applications (id, owner_type, owner_id, name, uid, secret, redirect_uri, scopes, confidential, created_at, updated_at)
VALUES (3, 'User', 1, 'App 1', 'dHKmvGkmuoMjqhCNmTJkf-EcnA61Up34O1vOHwTSvU8', '965136b8fb8d00e2faa2faaaed99c0ec10225518d0c8d9fb1d2af701e87eb68c',
'http://demo.localhost:3000', 'write_api read_gpx', false, '2021-04-12 17:53:30', '2021-04-12 17:53:30');
INSERT INTO public.oauth_access_tokens (id, resource_owner_id, application_id, token, refresh_token, expires_in, revoked_at, created_at, scopes, previous_refresh_token)
VALUES (67, 1, 3, '4f41f2328befed5a33bcabdf14483081c8df996cbafc41e313417776e8fafae8', NULL, NULL, NULL, '2021-04-14 19:38:21', 'write_api', '');
)"
);

// Test api_size_limit database function.
// User ids != 1 may not upload any changes,
// Real database function is managed outside of CGImap

tdb.run_sql(R"(
CREATE OR REPLACE FUNCTION api_size_limit(user_id bigint)
RETURNS bigint
AS $$
BEGIN
IF user_id <> 1 THEN
RETURN 0;
ELSE
RETURN 5000000;
END IF;
END;
$$ LANGUAGE plpgsql STABLE;
)");
}

SECTION("Try to upload one way with two nodes, with very large bbox")
{
// set up request headers from test case
test_request req;
req.set_header("REQUEST_METHOD", "POST");
req.set_header("REQUEST_URI", "/api/0.6/changeset/1/upload");
req.set_header("HTTP_AUTHORIZATION", bearertoken);
req.set_header("REMOTE_ADDR", "127.0.0.1");

req.set_payload(R"(<?xml version="1.0" encoding="UTF-8"?>
<osmChange version="0.6" generator="iD">
<create>
<node id='-25355' lat='68.13898255618' lon='-105.8206640625' changeset="1" />
<node id='-25357' lat='-34.30685345531' lon='80.8590234375' changeset="1" />
<way id='-579' changeset="1">
<nd ref='-25355' />
<nd ref='-25357' />
</way>
</create>
</osmChange>)" );

// execute the request
process_request(req, limiter, generator, route, *sel_factory, upd_factory.get());

CAPTURE(req.body().str());
REQUIRE(req.response_status() == 413);
}

SECTION("Try to upload one way with two nodes, with very small bbox")
{
// set up request headers from test case
test_request req;
req.set_header("REQUEST_METHOD", "POST");
req.set_header("REQUEST_URI", "/api/0.6/changeset/1/upload");
req.set_header("HTTP_AUTHORIZATION", bearertoken);
req.set_header("REMOTE_ADDR", "127.0.0.1");

req.set_payload(R"(<?xml version="1.0" encoding="UTF-8"?>
<osmChange version="0.6" generator="iD">
<create>
<node id='-25360' lat='51.50723246769' lon='-0.12171328202' changeset="1" />
<node id='-25361' lat='51.50719824397' lon='-0.12160197034' changeset="1" />
<way id='-582' changeset="1">
<nd ref='-25360' />
<nd ref='-25361' />
</way>
</create>
</osmChange>)" );

// execute the request
process_request(req, limiter, generator, route, *sel_factory, upd_factory.get());

CAPTURE(req.body().str());
REQUIRE(req.response_status() == 200);

auto doc = getDocument(req.body().str());
REQUIRE(getXPath(doc.get(), "/diffResult/node[1]/@old_id") == "-25360");
REQUIRE(getXPath(doc.get(), "/diffResult/node[2]/@old_id") == "-25361");
REQUIRE(getXPath(doc.get(), "/diffResult/way[1]/@old_id") == "-582");
REQUIRE(getXPath(doc.get(), "/diffResult/node[1]/@new_version") == "1");
REQUIRE(getXPath(doc.get(), "/diffResult/node[2]/@new_version") == "1");
REQUIRE(getXPath(doc.get(), "/diffResult/way[1]/@new_version") == "1");
}
}

int main(int argc, char *argv[]) {
Catch::Session session;

Expand Down

0 comments on commit b2a8e7c

Please sign in to comment.