Repository: mesos Updated Branches: refs/heads/master 6b8682b9c -> b6b617a01
Added the VIEW_FLAGS authorization action. Adds authorization for viewing flags. Instead of being part of `get_endpoints` it uses its own action `VIEW_FLAGS` which is used to restrict access to the `/flags` endpoint, as well as to filter the flag related results of the `/state` endpoint on both master and agents. Review: https://reviews.apache.org/r/49313/ Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/f35f606b Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/f35f606b Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/f35f606b Branch: refs/heads/master Commit: f35f606b08e0eaa4d452a81d439021433b242c14 Parents: 6b8682b Author: Alexander Rojas <alexan...@mesosphere.io> Authored: Thu Jun 30 13:19:57 2016 -0700 Committer: Vinod Kone <vinodk...@gmail.com> Committed: Thu Jun 30 13:21:57 2016 -0700 ---------------------------------------------------------------------- include/mesos/authorizer/acls.proto | 12 ++++ include/mesos/authorizer/authorizer.proto | 4 ++ src/authorizer/local/authorizer.cpp | 22 +++++++ src/common/http.cpp | 15 +++++ src/common/http.hpp | 3 + src/master/http.cpp | 84 +++++++++++++++++------- src/slave/http.cpp | 77 ++++++++++++++++------ src/tests/authorization_tests.cpp | 56 ++++++++++++++++ src/tests/master_authorization_tests.cpp | 85 +++++++++++++++++++++++++ src/tests/slave_authorization_tests.cpp | 88 ++++++++++++++++++++++++++ 10 files changed, 405 insertions(+), 41 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/include/mesos/authorizer/acls.proto ---------------------------------------------------------------------- diff --git a/include/mesos/authorizer/acls.proto b/include/mesos/authorizer/acls.proto index a6d93cd..31d5c14 100644 --- a/include/mesos/authorizer/acls.proto +++ b/include/mesos/authorizer/acls.proto @@ -244,6 +244,17 @@ message ACL { // access. required Entity logs = 2; } + + // Which principals are authorized to access the command-line flags used to + // launch the master/agent. + message ViewFlags { + // Subjects: HTTP Username. + required Entity principals = 1; + + // Objects: Given implicitly. Use Entity type ANY or NONE to allow or deny + // access. + required Entity flags = 2; + } } @@ -294,4 +305,5 @@ message ACLs { repeated ACL.AccessSandbox access_sandboxes = 19; repeated ACL.AccessMesosLog access_mesos_logs = 20; repeated ACL.GetWeight get_weights = 21; + repeated ACL.ViewFlags view_flags = 22; } http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/include/mesos/authorizer/authorizer.proto ---------------------------------------------------------------------- diff --git a/include/mesos/authorizer/authorizer.proto b/include/mesos/authorizer/authorizer.proto index fc76796..e22d3a4 100644 --- a/include/mesos/authorizer/authorizer.proto +++ b/include/mesos/authorizer/authorizer.proto @@ -94,6 +94,10 @@ enum Action { // This action will not fill in any object fields, since the object // is the master/agent log itself. ACCESS_MESOS_LOG = 17; + + // This action will not fill in any object fields, since the object + // is the entire set of flags. + VIEW_FLAGS = 18; } http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/authorizer/local/authorizer.cpp ---------------------------------------------------------------------- diff --git a/src/authorizer/local/authorizer.cpp b/src/authorizer/local/authorizer.cpp index 3fade41..aadb7f6 100644 --- a/src/authorizer/local/authorizer.cpp +++ b/src/authorizer/local/authorizer.cpp @@ -257,6 +257,11 @@ public: break; } + case authorization::VIEW_FLAGS: { + aclObject.set_type(mesos::ACL::Entity::ANY); + + break; + } case authorization::ACCESS_SANDBOX: { aclObject.set_type(mesos::ACL::Entity::ANY); @@ -654,6 +659,17 @@ private: return acls_; break; + case authorization::VIEW_FLAGS: + foreach (const ACL::ViewFlags& acl, acls.view_flags()) { + GenericACL acl_; + acl_.subjects = acl.principals(); + acl_.objects = acl.flags(); + + acls_.push_back(acl_); + } + + return acls_; + break; case authorization::ACCESS_SANDBOX: { foreach (const ACL::AccessSandbox& acl, acls.access_sandboxes()) { GenericACL acl_; @@ -762,6 +778,12 @@ Option<Error> LocalAuthorizer::validate(const ACLs& acls) } } + foreach (const ACL::ViewFlags& acl, acls.view_flags()) { + if (acl.flags().type() == ACL::Entity::SOME) { + return Error("acls.view_flags type must be either NONE or ANY"); + } + } + // TODO(alexr): Consider validating not only protobuf, but also the original // JSON in order to spot misspelled names. A misspelled action may affect // authorization result and hence lead to a security issue (e.g. when there http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/common/http.cpp ---------------------------------------------------------------------- diff --git a/src/common/http.cpp b/src/common/http.cpp index daf5672..2ef264f 100644 --- a/src/common/http.cpp +++ b/src/common/http.cpp @@ -698,4 +698,19 @@ bool approveViewTask( return approved.get(); } + +bool approveViewFlags( + const Owned<ObjectApprover>& flagsApprover) +{ + ObjectApprover::Object object; + + Try<bool> approved = flagsApprover->approved(object); + if (approved.isError()) { + LOG(WARNING) << "Error during Flags authorization: " << approved.error(); + // TODO(joerg84): Consider exposing these errors to the caller. + return false; + } + return approved.get(); +} + } // namespace mesos { http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/common/http.hpp ---------------------------------------------------------------------- diff --git a/src/common/http.hpp b/src/common/http.hpp index 55bd0ac..eb2d015 100644 --- a/src/common/http.hpp +++ b/src/common/http.hpp @@ -136,6 +136,9 @@ bool approveViewTask( const Task& task, const FrameworkInfo& frameworkInfo); + +bool approveViewFlags(const process::Owned<ObjectApprover>& flagsApprover); + } // namespace mesos { #endif // __COMMON_HTTP_HPP__ http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/master/http.cpp ---------------------------------------------------------------------- diff --git a/src/master/http.cpp b/src/master/http.cpp index 670b4e0..759e051 100644 --- a/src/master/http.cpp +++ b/src/master/http.cpp @@ -1455,7 +1455,11 @@ string Master::Http::FLAGS_HELP() return HELP( TLDR("Exposes the master's flag configuration."), None(), - AUTHENTICATION(true)); + AUTHENTICATION(true), + AUTHORIZATION( + "Querying this endpoint requires that the current principal", + "is authorized to view all flags.", + "See the authorization documentation for details.")); } @@ -1469,7 +1473,27 @@ Future<Response> Master::Http::flags( return MethodNotAllowed({"GET"}, request.method); } - return OK(_flags(), request.url.query.get("jsonp")); + if (master->authorizer.isNone()) { + return OK(_flags(), request.url.query.get("jsonp")); + } + + authorization::Request authRequest; + authRequest.set_action(authorization::VIEW_FLAGS); + + if (principal.isSome()) { + authRequest.mutable_subject()->set_value(principal.get()); + } + + return master->authorizer.get()->authorized(authRequest) + .then(defer( + master->self(), + [this, request](bool authorized) -> Future<Response> { + if (authorized) { + return OK(_flags(), request.url.query.get("jsonp")); + } else { + return Forbidden(); + } + })); } @@ -2194,6 +2218,7 @@ Future<Response> Master::Http::state( Future<Owned<ObjectApprover>> frameworksApprover; Future<Owned<ObjectApprover>> tasksApprover; Future<Owned<ObjectApprover>> executorsApprover; + Future<Owned<ObjectApprover>> flagsApprover; if (master->authorizer.isSome()) { authorization::Subject subject; @@ -2209,16 +2234,25 @@ Future<Response> Master::Http::state( executorsApprover = master->authorizer.get()->getObjectApprover( subject, authorization::VIEW_EXECUTOR); + + flagsApprover = master->authorizer.get()->getObjectApprover( + subject, authorization::VIEW_FLAGS); } else { frameworksApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); tasksApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); executorsApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); + flagsApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); } - return collect(frameworksApprover, tasksApprover, executorsApprover) + return collect( + frameworksApprover, + tasksApprover, + executorsApprover, + flagsApprover) .then(defer(master->self(), [this, request](const tuple<Owned<ObjectApprover>, Owned<ObjectApprover>, + Owned<ObjectApprover>, Owned<ObjectApprover>>& approvers) -> Response { // This lambda is consumed before the outer lambda @@ -2228,7 +2262,11 @@ Future<Response> Master::Http::state( Owned<ObjectApprover> frameworksApprover; Owned<ObjectApprover> tasksApprover; Owned<ObjectApprover> executorsApprover; - tie(frameworksApprover, tasksApprover, executorsApprover) = approvers; + Owned<ObjectApprover> flagsApprover; + tie(frameworksApprover, + tasksApprover, + executorsApprover, + flagsApprover) = approvers; writer->field("version", MESOS_VERSION); @@ -2259,31 +2297,33 @@ Future<Response> Master::Http::state( writer->field("activated_slaves", master->_slaves_active()); writer->field("deactivated_slaves", master->_slaves_inactive()); - if (master->flags.cluster.isSome()) { - writer->field("cluster", master->flags.cluster.get()); - } - if (master->leader.isSome()) { writer->field("leader", master->leader.get().pid()); } - if (master->flags.log_dir.isSome()) { - writer->field("log_dir", master->flags.log_dir.get()); - } + if (approveViewFlags(flagsApprover)) { + if (master->flags.cluster.isSome()) { + writer->field("cluster", master->flags.cluster.get()); + } - if (master->flags.external_log_file.isSome()) { - writer->field("external_log_file", - master->flags.external_log_file.get()); - } + if (master->flags.log_dir.isSome()) { + writer->field("log_dir", master->flags.log_dir.get()); + } - writer->field("flags", [this](JSON::ObjectWriter* writer) { - foreachvalue (const flags::Flag& flag, master->flags) { - Option<string> value = flag.stringify(master->flags); - if (value.isSome()) { - writer->field(flag.effective_name().value, value.get()); - } + if (master->flags.external_log_file.isSome()) { + writer->field("external_log_file", + master->flags.external_log_file.get()); } - }); + + writer->field("flags", [this](JSON::ObjectWriter* writer) { + foreachvalue (const flags::Flag& flag, master->flags) { + Option<string> value = flag.stringify(master->flags); + if (value.isSome()) { + writer->field(flag.effective_name().value, value.get()); + } + } + }); + } // Model all of the slaves. writer->field("slaves", [this](JSON::ArrayWriter* writer) { http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/slave/http.cpp ---------------------------------------------------------------------- diff --git a/src/slave/http.cpp b/src/slave/http.cpp index 335b773..60a780c 100644 --- a/src/slave/http.cpp +++ b/src/slave/http.cpp @@ -547,7 +547,10 @@ string Slave::Http::FLAGS_HELP() return HELP( TLDR("Exposes the agent's flag configuration."), None(), - AUTHENTICATION(true)); + AUTHENTICATION(true), + AUTHORIZATION( + "The request principal should be authorized to view all flags.", + "See the authorization documentation for details.")); } @@ -561,7 +564,27 @@ Future<Response> Slave::Http::flags( return MethodNotAllowed({"GET"}, request.method); } - return OK(_flags(), request.url.query.get("jsonp")); + if (slave->authorizer.isNone()) { + return OK(_flags(), request.url.query.get("jsonp")); + } + + authorization::Request authRequest; + authRequest.set_action(authorization::VIEW_FLAGS); + + if (principal.isSome()) { + authRequest.mutable_subject()->set_value(principal.get()); + } + + return slave->authorizer.get()->authorized(authRequest) + .then(defer( + slave->self(), + [this, request](bool authorized) -> Future<Response> { + if (authorized) { + return OK(_flags(), request.url.query.get("jsonp")); + } else { + return Forbidden(); + } + })); } @@ -825,6 +848,7 @@ Future<Response> Slave::Http::state( Future<Owned<ObjectApprover>> frameworksApprover; Future<Owned<ObjectApprover>> tasksApprover; Future<Owned<ObjectApprover>> executorsApprover; + Future<Owned<ObjectApprover>> flagsApprover; if (slave->authorizer.isSome()) { authorization::Subject subject; @@ -840,16 +864,25 @@ Future<Response> Slave::Http::state( executorsApprover = slave->authorizer.get()->getObjectApprover( subject, authorization::VIEW_EXECUTOR); + + flagsApprover = slave->authorizer.get()->getObjectApprover( + subject, authorization::VIEW_FLAGS); } else { frameworksApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); tasksApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); executorsApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); + flagsApprover = Owned<ObjectApprover>(new AcceptingObjectApprover()); } - return collect(frameworksApprover, tasksApprover, executorsApprover) + return collect( + frameworksApprover, + tasksApprover, + executorsApprover, + flagsApprover) .then(defer(slave->self(), [this, request](const tuple<Owned<ObjectApprover>, Owned<ObjectApprover>, + Owned<ObjectApprover>, Owned<ObjectApprover>>& approvers) -> Response { // This lambda is consumed before the outer lambda // returns, hence capture by reference is fine here. @@ -858,7 +891,11 @@ Future<Response> Slave::Http::state( Owned<ObjectApprover> frameworksApprover; Owned<ObjectApprover> tasksApprover; Owned<ObjectApprover> executorsApprover; - tie(frameworksApprover, tasksApprover, executorsApprover) = approvers; + Owned<ObjectApprover> flagsApprover; + tie(frameworksApprover, + tasksApprover, + executorsApprover, + flagsApprover) = approvers; writer->field("version", MESOS_VERSION); @@ -895,13 +932,24 @@ Future<Response> Slave::Http::state( } } - if (slave->flags.log_dir.isSome()) { - writer->field("log_dir", slave->flags.log_dir.get()); - } + if (approveViewFlags(flagsApprover)) { + if (slave->flags.log_dir.isSome()) { + writer->field("log_dir", slave->flags.log_dir.get()); + } - if (slave->flags.external_log_file.isSome()) { - writer->field( - "external_log_file", slave->flags.external_log_file.get()); + if (slave->flags.external_log_file.isSome()) { + writer->field( + "external_log_file", slave->flags.external_log_file.get()); + } + + writer->field("flags", [this](JSON::ObjectWriter* writer) { + foreachvalue (const flags::Flag& flag, slave->flags) { + Option<string> value = flag.stringify(slave->flags); + if (value.isSome()) { + writer->field(flag.effective_name().value, value.get()); + } + } + }); } // Model all of the frameworks. @@ -946,15 +994,6 @@ Future<Response> Slave::Http::state( writer->element(frameworkWriter); } }); - - writer->field("flags", [this](JSON::ObjectWriter* writer) { - foreachvalue (const flags::Flag& flag, slave->flags) { - Option<string> value = flag.stringify(slave->flags); - if (value.isSome()) { - writer->field(flag.effective_name().value, value.get()); - } - } - }); }; return OK(jsonify(state), request.url.query.get("jsonp")); http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/tests/authorization_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/authorization_tests.cpp b/src/tests/authorization_tests.cpp index 9b99da1..c1e8ea6 100644 --- a/src/tests/authorization_tests.cpp +++ b/src/tests/authorization_tests.cpp @@ -2288,6 +2288,62 @@ TYPED_TEST(AuthorizationTest, OptionalObject) } } + +TYPED_TEST(AuthorizationTest, ViewFlags) +{ + // Setup ACLs. + ACLs acls; + + { + // "foo" principal can see the flags. + mesos::ACL::ViewFlags* acl = acls.add_view_flags(); + acl->mutable_principals()->add_values("foo"); + acl->mutable_flags()->set_type(mesos::ACL::Entity::ANY); + } + + { + // Nobody else can see the flags. + mesos::ACL::ViewFlags* acl = acls.add_view_flags(); + acl->mutable_principals()->set_type(mesos::ACL::Entity::ANY); + acl->mutable_flags()->set_type(mesos::ACL::Entity::NONE); + } + + // Create an `Authorizer` with the ACLs. + Try<Authorizer*> create = TypeParam::create(parameterize(acls)); + ASSERT_SOME(create); + Owned<Authorizer> authorizer(create.get()); + + + { + authorization::Request request; + request.set_action(authorization::VIEW_FLAGS); + request.mutable_subject()->set_value("foo"); + + AWAIT_EXPECT_TRUE(authorizer.get()->authorized(request)); + } + + + { + authorization::Request request; + request.set_action(authorization::VIEW_FLAGS); + request.mutable_subject()->set_value("bar"); + + AWAIT_EXPECT_FALSE(authorizer.get()->authorized(request)); + } + + // Test that no authorzer is created with invalid flags. + { + ACLs invalid; + + mesos::ACL::ViewFlags* acl = invalid.add_view_flags(); + acl->mutable_principals()->add_values("foo"); + acl->mutable_flags()->add_values("yoda"); + + Try<Authorizer*> create = TypeParam::create(parameterize(invalid)); + EXPECT_ERROR(create); + } +} + } // namespace tests { } // namespace internal { } // namespace mesos { http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/tests/master_authorization_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/master_authorization_tests.cpp b/src/tests/master_authorization_tests.cpp index 207dfb2..0807469 100644 --- a/src/tests/master_authorization_tests.cpp +++ b/src/tests/master_authorization_tests.cpp @@ -1691,6 +1691,91 @@ TYPED_TEST(MasterAuthorizerTest, FilterTasksEndpoint) driver.join(); } + +TYPED_TEST(MasterAuthorizerTest, ViewFlags) +{ + ACLs acls; + + { + // Default principal can see the flags. + mesos::ACL::ViewFlags* acl = acls.add_view_flags(); + acl->mutable_principals()->add_values(DEFAULT_CREDENTIAL.principal()); + acl->mutable_flags()->set_type(ACL::Entity::ANY); + } + + { + // Second default principal can not see the flags. + mesos::ACL::ViewFlags* acl = acls.add_view_flags(); + acl->mutable_principals()->add_values(DEFAULT_CREDENTIAL_2.principal()); + acl->mutable_flags()->set_type(ACL::Entity::NONE); + } + + // Create an `Authorizer` with the ACLs. + Try<Authorizer*> create = TypeParam::create(parameterize(acls)); + ASSERT_SOME(create); + Owned<Authorizer> authorizer(create.get()); + + Try<Owned<cluster::Master>> master = this->StartMaster(authorizer.get()); + ASSERT_SOME(master); + + // The default principal should be able to access the flags. + { + Future<Response> response = http::get( + master.get()->pid, + "flags", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response.get().body; + + response = http::get( + master.get()->pid, + "state", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response.get().body; + + Try<JSON::Object> parse = JSON::parse<JSON::Object>(response.get().body); + ASSERT_SOME(parse); + JSON::Object state = parse.get(); + + ASSERT_TRUE(state.values["flags"].is<JSON::Object>()); + EXPECT_TRUE(1u <= state.values["flags"].as<JSON::Object>().values.size()); + } + + // The second default principal should not have access to the + // /flags endpoint and get a filtered view of the /state one. + { + Future<Response> response = http::get( + master.get()->pid, + "flags", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL_2)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(Forbidden().status, response) + << response.get().body; + + response = http::get( + master.get()->pid, + "state", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL_2)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response.get().body; + + Try<JSON::Object> parse = JSON::parse<JSON::Object>(response.get().body); + ASSERT_SOME(parse); + JSON::Object state = parse.get(); + + EXPECT_TRUE(state.values.find("flags") == state.values.end()); + } +} + + } // namespace tests { } // namespace internal { } // namespace mesos { http://git-wip-us.apache.org/repos/asf/mesos/blob/f35f606b/src/tests/slave_authorization_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/slave_authorization_tests.cpp b/src/tests/slave_authorization_tests.cpp index 78221e2..f76ed3a 100644 --- a/src/tests/slave_authorization_tests.cpp +++ b/src/tests/slave_authorization_tests.cpp @@ -266,6 +266,94 @@ TYPED_TEST(SlaveAuthorizerTest, FilterStateEndpoint) } +TYPED_TEST(SlaveAuthorizerTest, ViewFlags) +{ + ACLs acls; + + { + // Default principal can see the flags. + mesos::ACL::ViewFlags* acl = acls.add_view_flags(); + acl->mutable_principals()->add_values(DEFAULT_CREDENTIAL.principal()); + acl->mutable_flags()->set_type(ACL::Entity::ANY); + } + + { + // Second default principal can not see the flags. + mesos::ACL::ViewFlags* acl = acls.add_view_flags(); + acl->mutable_principals()->add_values(DEFAULT_CREDENTIAL_2.principal()); + acl->mutable_flags()->set_type(ACL::Entity::NONE); + } + + // Create an `Authorizer` with the ACLs. + Try<Authorizer*> create = TypeParam::create(parameterize(acls)); + ASSERT_SOME(create); + Owned<Authorizer> authorizer(create.get()); + + StandaloneMasterDetector detector; + + Try<Owned<cluster::Slave>> agent = + this->StartSlave(&detector, authorizer.get()); + + ASSERT_SOME(agent); + + // The default principal should be able to access the flags. + { + Future<Response> response = http::get( + agent.get()->pid, + "flags", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response.get().body; + + response = http::get( + agent.get()->pid, + "state", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response.get().body; + + Try<JSON::Object> parse = JSON::parse<JSON::Object>(response.get().body); + ASSERT_SOME(parse); + JSON::Object state = parse.get(); + + ASSERT_TRUE(state.values["flags"].is<JSON::Object>()); + EXPECT_TRUE(1u <= state.values["flags"].as<JSON::Object>().values.size()); + } + + // The second default principal should not have access to the + // /flags endpoint and get a filtered view of the /state one. + { + Future<Response> response = http::get( + agent.get()->pid, + "flags", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL_2)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(Forbidden().status, response) + << response.get().body; + + response = http::get( + agent.get()->pid, + "state", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL_2)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response.get().body; + + Try<JSON::Object> parse = JSON::parse<JSON::Object>(response.get().body); + ASSERT_SOME(parse); + JSON::Object state = parse.get(); + + EXPECT_TRUE(state.values.find("flags") == state.values.end()); + } +} + + // Parameterized fixture for agent-specific authorization tests. The // path of the tested endpoint is passed as the only parameter. class SlaveEndpointTest: