diff --git a/contrib/endpoints/src/api_manager/BUILD b/contrib/endpoints/src/api_manager/BUILD index 39e85eacac5..83351e10a55 100644 --- a/contrib/endpoints/src/api_manager/BUILD +++ b/contrib/endpoints/src/api_manager/BUILD @@ -118,6 +118,7 @@ cc_library( "//contrib/endpoints/src/api_manager/context", "//contrib/endpoints/src/api_manager/service_control", "//contrib/endpoints/src/api_manager/utils", + "//contrib/endpoints/src/api_manager/firebase_rules", "//external:cc_wkt_protos", "//external:cloud_trace", "//external:googletest_prod", @@ -291,6 +292,8 @@ cc_test( deps = [ ":api_manager", ":mock_api_manager_environment", + ":security_rules_proto", + "//external:cc_wkt_protos", "//external:googletest_main", ], ) diff --git a/contrib/endpoints/src/api_manager/auth/service_account_token.h b/contrib/endpoints/src/api_manager/auth/service_account_token.h index 51e99c65b89..3a3453daf91 100644 --- a/contrib/endpoints/src/api_manager/auth/service_account_token.h +++ b/contrib/endpoints/src/api_manager/auth/service_account_token.h @@ -65,6 +65,9 @@ class ServiceAccountToken { JWT_TOKEN_FOR_SERVICE_CONTROL = 0, JWT_TOKEN_FOR_CLOUD_TRACING, JWT_TOKEN_FOR_FIREBASE, + + // JWT token for accessing the http endpoints defined in Firebase Rules. + JWT_TOKEN_FOR_AUTHORIZATION_SERVICE, JWT_TOKEN_TYPE_MAX, }; // Set audience. Only calcualtes JWT token with specified audience. diff --git a/contrib/endpoints/src/api_manager/check_security_rules.cc b/contrib/endpoints/src/api_manager/check_security_rules.cc index 845bc4cf6bb..2bb5180e981 100644 --- a/contrib/endpoints/src/api_manager/check_security_rules.cc +++ b/contrib/endpoints/src/api_manager/check_security_rules.cc @@ -17,87 +17,41 @@ #include #include #include "contrib/endpoints/src/api_manager/auth/lib/json_util.h" -#include "contrib/endpoints/src/api_manager/proto/security_rules.pb.h" +#include "contrib/endpoints/src/api_manager/firebase_rules/firebase_request.h" #include "contrib/endpoints/src/api_manager/utils/marshalling.h" -using ::google::api_manager::auth::GetProperty; using ::google::api_manager::auth::GetStringValue; +using ::google::api_manager::firebase_rules::FirebaseRequest; using ::google::api_manager::utils::Status; -using ::google::protobuf::Map; -using ::google::protobuf::util::error::Code; namespace google { namespace api_manager { namespace { -const char kFailedFirebaseReleaseFetch[] = "Failed to fetch Firebase Release"; -const char kFailedFirebaseTest[] = "Failed to execute Firebase Test"; -const char kInvalidResponse[] = "Invalid JSON response from Firebase Service"; -const char kTestSuccess[] = "SUCCESS"; -const char kHttpGetMethod[] = "GET"; -const char kHttpPostMethod[] = "POST"; -const char kHttpHeadMethod[] = "HEAD"; -const char kHttpOptionsMethod[] = "OPTIONS"; -const char kHttpDeleteMethod[] = "DELETE"; -const char kFirebaseCreateMethod[] = "create"; -const char kFirebaseGetMethod[] = "get"; -const char kFirebaseDeleteMethod[] = "delete"; -const char kFirebaseUpdateMethod[] = "update"; -const char kV1[] = "/v1"; -const char kTestQuery[] = ":test?alt=json"; -const char kProjects[] = "/projects"; -const char kReleases[] = "/releases"; -const char kRulesetName[] = "rulesetName"; -const char kTestResults[] = "testResults"; -const char kState[] = "state"; -const char kToken[] = "token"; -const char kAuth[] = "auth"; -const char kRequest[] = "request"; -const char kContentType[] = "Content-Type"; -const char kApplication[] = "application/json"; - -void SetProtoValue(const std::string &key, - const ::google::protobuf::Value &value, - ::google::protobuf::Value *head) { - ::google::protobuf::Struct *s = head->mutable_struct_value(); - Map *fields = s->mutable_fields(); - (*fields)[key] = value; -} +const std::string kFailedFirebaseReleaseFetch = + "Failed to fetch Firebase Release"; +const std::string kFailedFirebaseTest = "Failed to execute Firebase Test"; +const std::string kInvalidResponse = + "Invalid JSON response from Firebase Service"; +const std::string kV1 = "/v1"; +const std::string kHttpGetMethod = "GET"; +const std::string kProjects = "/projects"; +const std::string kReleases = "/releases"; +const std::string kRulesetName = "rulesetName"; +const std::string kContentType = "Content-Type"; +const std::string kApplication = "application/json"; std::string GetReleaseName(const context::RequestContext &context) { return context.service_context()->service_name() + ":" + context.service_context()->service().apis(0).version(); } -std::string GetRulesetTestUri(const context::RequestContext &context, - const std::string &ruleset_id) { - return context.service_context()->config()->GetFirebaseServer() + kV1 + "/" + - ruleset_id + kTestQuery; -} - std::string GetReleaseUrl(const context::RequestContext &context) { return context.service_context()->config()->GetFirebaseServer() + kV1 + kProjects + "/" + context.service_context()->project_id() + kReleases + "/" + GetReleaseName(context); } -std::string GetOperation(const std::string &httpMethod) { - if (httpMethod == kHttpPostMethod) { - return kFirebaseCreateMethod; - } - - if (httpMethod == kHttpGetMethod || httpMethod == kHttpHeadMethod || - httpMethod == kHttpOptionsMethod) { - return kFirebaseGetMethod; - } - - if (httpMethod == kHttpDeleteMethod) { - return kFirebaseDeleteMethod; - } - - return kFirebaseUpdateMethod; -} - // An AuthzChecker object is created for every incoming request. It does // authorizaiton by calling Firebase Rules service. class AuthzChecker : public std::enable_shared_from_this { @@ -111,38 +65,25 @@ class AuthzChecker : public std::enable_shared_from_this { std::function continuation); private: - // Helper method that invokes the test firebase service api. - void CallTest(const std::string &ruleset_id, - std::shared_ptr context, - std::function continuation); + // This method invokes the Firebase TestRuleset API endpoint as well as user + // defined endpoints provided by the TestRulesetResponse. + void CallNextRequest(std::function continuation); - // Parse the respose for GET RELEASE API call + // Parse the response for GET RELEASE API call Status ParseReleaseResponse(const std::string &json_str, std::string *ruleset_id); - // Parses the response for the TEST API call - Status ParseTestResponse(context::RequestContext &context, - const std::string &json_str); - - // Builds the request body for the TESP API call. - Status BuildTestRequestBody(context::RequestContext &context, - std::string *result_string); - // Invoke the HTTP call void HttpFetch(const std::string &url, const std::string &method, const std::string &request_body, + auth::ServiceAccountToken::JWT_TOKEN_TYPE token_type, std::function continuation); - // Get the auth token for Firebase service - const std::string &GetAuthToken() { - return sa_token_->GetAuthToken( - auth::ServiceAccountToken::JWT_TOKEN_FOR_FIREBASE); - } - std::shared_ptr GetPtr() { return shared_from_this(); } ApiManagerEnvInterface *env_; auth::ServiceAccountToken *sa_token_; + std::unique_ptr request_handler_; }; AuthzChecker::AuthzChecker(ApiManagerEnvInterface *env, @@ -162,9 +103,10 @@ void AuthzChecker::Check( return; } - // Fetch the Release attributes. + // Fetch the Release attributes and get ruleset name. auto checker = GetPtr(); HttpFetch(GetReleaseUrl(*context), kHttpGetMethod, "", + auth::ServiceAccountToken::JWT_TOKEN_FOR_FIREBASE, [context, final_continuation, checker](Status status, std::string &&body) { std::string ruleset_id; @@ -182,39 +124,38 @@ void AuthzChecker::Check( // If the parsing of the release body is successful, then call the // Test Api for firebase rules service. if (status.ok()) { - checker->CallTest(ruleset_id, context, final_continuation); + checker->request_handler_ = std::unique_ptr( + new FirebaseRequest(ruleset_id, checker->env_, context)); + checker->CallNextRequest(final_continuation); } else { final_continuation(status); } }); } -void AuthzChecker::CallTest(const std::string &ruleset_id, - std::shared_ptr context, - std::function continuation) { - std::string body; - Status status = BuildTestRequestBody(*context.get(), &body); - if (!status.ok()) { - continuation(status); +void AuthzChecker::CallNextRequest( + std::function continuation) { + if (request_handler_->is_done()) { + continuation(request_handler_->RequestStatus()); return; } auto checker = GetPtr(); - HttpFetch(GetRulesetTestUri(*context, ruleset_id), kHttpPostMethod, body, - [context, continuation, checker, ruleset_id](Status status, - std::string &&body) { + firebase_rules::HttpRequest http_request = request_handler_->GetHttpRequest(); + HttpFetch(http_request.url, http_request.method, http_request.body, + http_request.token_type, + [continuation, checker](Status status, std::string &&body) { + checker->env_->LogError(std::string("Response Body = ") + body); if (status.ok()) { - checker->env_->LogDebug( - std::string("Test API succeeded with ") + body); - status = checker->ParseTestResponse(*context.get(), body); + checker->request_handler_->UpdateResponse(body); + checker->CallNextRequest(continuation); } else { checker->env_->LogError(std::string("Test API failed with ") + status.ToString()); status = Status(Code::INTERNAL, kFailedFirebaseTest); + continuation(status); } - - continuation(status); }); } @@ -228,7 +169,7 @@ Status AuthzChecker::ParseReleaseResponse(const std::string &json_str, } Status status = Status::OK; - const char *id = GetStringValue(json, kRulesetName); + const char *id = GetStringValue(json, kRulesetName.c_str()); *ruleset_id = (id == nullptr) ? "" : id; if (ruleset_id->empty()) { @@ -242,85 +183,10 @@ Status AuthzChecker::ParseReleaseResponse(const std::string &json_str, return status; } -Status AuthzChecker::ParseTestResponse(context::RequestContext &context, - const std::string &json_str) { - grpc_json *json = grpc_json_parse_string_with_len( - const_cast(json_str.data()), json_str.length()); - - if (!json) { - return Status(Code::INVALID_ARGUMENT, - "Invalid JSON response from Firebase Service"); - } - - Status status = Status::OK; - Status invalid = Status(Code::INTERNAL, kInvalidResponse); - - const grpc_json *testResults = GetProperty(json, kTestResults); - if (testResults == nullptr) { - env_->LogError("TestResults are null"); - status = invalid; - } else { - const char *result = GetStringValue(testResults->child, kState); - if (result == nullptr) { - env_->LogInfo("Result state is empty"); - status = invalid; - } else if (std::string(result) != kTestSuccess) { - status = Status(Code::PERMISSION_DENIED, - std::string("Unauthorized ") + - context.request()->GetRequestHTTPMethod() + - " access to resource " + - context.request()->GetRequestPath(), - Status::AUTH); - } - } - - grpc_json_destroy(json); - return status; -} - -Status AuthzChecker::BuildTestRequestBody(context::RequestContext &context, - std::string *result_string) { - proto::TestRulesetRequest request; - auto *test_case = request.add_test_cases(); - auto httpMethod = context.request()->GetRequestHTTPMethod(); - - test_case->set_service_name(context.service_context()->service_name()); - test_case->set_resource_path(context.request()->GetRequestPath()); - test_case->set_operation(GetOperation(httpMethod)); - test_case->set_expectation(proto::TestRulesetRequest::TestCase::ALLOW); - - ::google::protobuf::Value auth; - ::google::protobuf::Value token; - ::google::protobuf::Value claims; - - Status status = utils::JsonToProto(context.auth_claims(), &claims); - if (!status.ok()) { - env_->LogError(std::string("Error creating Protobuf from claims") + - status.ToString()); - return status; - } - - SetProtoValue(kToken, claims, &token); - SetProtoValue(kAuth, token, &auth); - - auto *variables = test_case->mutable_variables(); - (*variables)[kRequest] = auth; - - status = - utils::ProtoToJson(request, result_string, utils::JsonOptions::DEFAULT); - if (status.ok()) { - env_->LogDebug(std::string("PRotobuf to JSON string = ") + *result_string); - } else { - env_->LogError(std::string("Error creating TestRulesetRequest") + - status.ToString()); - } - - return status; -} - void AuthzChecker::HttpFetch( const std::string &url, const std::string &method, const std::string &request_body, + auth::ServiceAccountToken::JWT_TOKEN_TYPE token_type, std::function continuation) { env_->LogDebug(std::string("Issue HTTP Request to url :") + url + " method : " + method + " body: " + request_body); @@ -334,9 +200,10 @@ void AuthzChecker::HttpFetch( return; } - request->set_method(method).set_url(url).set_auth_token(GetAuthToken()); + request->set_method(method).set_url(url).set_auth_token( + sa_token_->GetAuthToken(token_type)); - if (method != kHttpGetMethod) { + if (!request_body.empty()) { request->set_header(kContentType, kApplication).set_body(request_body); } diff --git a/contrib/endpoints/src/api_manager/check_security_rules_test.cc b/contrib/endpoints/src/api_manager/check_security_rules_test.cc index 97c555bc4df..bf60d21bd60 100644 --- a/contrib/endpoints/src/api_manager/check_security_rules_test.cc +++ b/contrib/endpoints/src/api_manager/check_security_rules_test.cc @@ -20,9 +20,13 @@ #include "contrib/endpoints/src/api_manager/context/service_context.h" #include "contrib/endpoints/src/api_manager/mock_api_manager_environment.h" #include "contrib/endpoints/src/api_manager/mock_request.h" +#include "contrib/endpoints/src/api_manager/proto/security_rules.pb.h" +#include "contrib/endpoints/src/api_manager/utils/marshalling.h" +#include "google/protobuf/util/message_differencer.h" using ::testing::_; using ::testing::AllOf; +using ::testing::InSequence; using ::testing::Invoke; using ::testing::Property; using ::testing::Return; @@ -30,8 +34,18 @@ using ::testing::StrCaseEq; using ::testing::StrEq; using ::testing::StrNe; +using ::google::protobuf::util::MessageDifferencer; using ::google::api_manager::utils::Status; +using ::google::protobuf::Map; using ::google::protobuf::util::error::Code; +using ::google::protobuf::RepeatedPtrField; + +// Tuple with arg<0> = function name +// arg<1> = url, arg<2> = method, arg<3> = body. +using FuncTuple = + std::tuple; +using ::google::api_manager::proto::TestRulesetResponse; +using FunctionCall = TestRulesetResponse::TestResult::FunctionCall; namespace google { namespace api_manager { @@ -138,27 +152,69 @@ static const char kReleaseError[] = R"( } })"; -// TestRuleset returns Failure which means unauthorized access. -static const char kTestResultFailure[] = R"( +static const char kDummyBody[] = R"( { - "testResults": [ - { - "state": "FAILURE" - } - ] + "key" : "value" +})"; + +const char kFirstRequest[] = + R"({"testSuite":{"testCases":[{"expectation":"ALLOW","request":{"method":"get","path":"/ListShelves","auth":{"token":{"email":"limin-429@appspot.gserviceaccount.com","email_verified":true,"azp":"limin-429@appspot.gserviceaccount.com","aud":"https://myfirebaseapp.appspot.com","sub":"113424383671131376652","iat":1486575396,"iss":"https://accounts.google.com","exp":1486578996}}}}]}})"; + +const char kSecondRequest[] = + R"({"testSuite":{"testCases":[{"expectation":"ALLOW","request":{"auth":{"token":{"email":"limin-429@appspot.gserviceaccount.com","azp":"limin-429@appspot.gserviceaccount.com","aud":"https://myfirebaseapp.appspot.com","sub":"113424383671131376652","iss":"https://accounts.google.com","email_verified":true,"iat":1486575396,"exp":1486578996}},"method":"get","path":"/ListShelves"},"functionMocks":[{"function":"f1","args":[{"exactValue":"http://url1"},{"exactValue":"POST"},{"exactValue":{"key":"value"}}],"result":{"value":{"key":"value"}}}]}]}})"; + +const char kThirdRequest[] = + R"({"testSuite":{"testCases":[{"expectation":"ALLOW","request":{"method":"get","path":"/ListShelves","auth":{"token":{"email":"limin-429@appspot.gserviceaccount.com","iat":1486575396,"azp":"limin-429@appspot.gserviceaccount.com","exp":1486578996,"email_verified":true,"sub":"113424383671131376652","aud":"https://myfirebaseapp.appspot.com","iss":"https://accounts.google.com"}}},"functionMocks":[{"function":"f2","args":[{"exactValue":"http://url2"},{"exactValue":"GET"},{"exactValue":{"key":"value"}}],"result":{"value":{"key":"value"}}},{"function":"f3","args":[{"exactValue":"https://url3"},{"exactValue":"GET"},{"exactValue":{"key":"value"}}],"result":{"value":{"key":"value"}}},{"function":"f1","args":[{"exactValue":"http://url1"},{"exactValue":"POST"},{"exactValue":{"key":"value"}}],"result":{"value":{"key":"value"}}}]}]}})"; + +::google::protobuf::Value ToValue(const std::string &arg) { + ::google::protobuf::Value value; + value.set_string_value(arg); + return value; } -)"; -// TestRuleset call to Firebase response on success. -static const char kTestResultSuccess[] = R"( -{ - "testResults": [ - { - "state": "SUCCESS" - } - ] +MATCHER_P3(HTTPRequestMatches, url, method, body, "") { + if (arg->url() != url) { + return false; + } + + if (strcasecmp(method.c_str(), arg->method().c_str()) != 0) { + return false; + } + + if (body.empty() || arg->body().empty()) { + return body.empty() && arg->body().empty(); + } + + google::protobuf::Value actual; + google::protobuf::Value expected; + + if (utils::JsonToProto(body, &expected) != Status::OK || + utils::JsonToProto(arg->body(), &actual) != Status::OK) { + return false; + } + + return MessageDifferencer::Equals(actual, expected); +} + +FunctionCall BuildCall(const std::string &name, const std::string &url, + const std::string &method, const std::string &body) { + FunctionCall func_call; + func_call.set_function(name); + + if (!url.empty()) { + *(func_call.add_args()) = ToValue(url); + } + + if (!method.empty()) { + *(func_call.add_args()) = ToValue(method); + } + + if (!body.empty()) { + *(func_call.add_args()) = ToValue(body); + } + + return func_call; } -)"; // Get a server configuration that has auth disabled. This should disable // security rules check by default. @@ -295,6 +351,56 @@ class CheckSecurityRulesTest : public ::testing::Test { ":test?alt=json"; } + void ExpectCall(std::string url, std::string method, std::string body, + std::string response, Status status = Status::OK) { + EXPECT_CALL(*raw_env_, + DoRunHTTPRequest(HTTPRequestMatches(url, method, body))) + .WillOnce(Invoke([response, status](HTTPRequest *req) { + std::map empty; + std::string body(response); + req->OnComplete(status, std::move(empty), std::move(body)); + })); + } + + std::string BuildTestRulesetResponse( + bool isSuccess, std::vector funcs = std::vector()) { + TestRulesetResponse response; + auto *result = response.add_test_results(); + result->set_state(isSuccess ? TestRulesetResponse::TestResult::SUCCESS + : TestRulesetResponse::TestResult::FAILURE); + + std::string url, method, body; + for (auto http : funcs) { + auto *func = result->add_function_calls(); + func->set_function(std::get<0>(http)); + if (!std::get<1>(http).empty()) { + func->add_args()->set_string_value(std::get<1>(http)); + } + + if (!std::get<2>(http).empty()) { + func->add_args()->set_string_value(std::get<2>(http)); + } + + if (!std::get<3>(http).empty()) { + ::google::protobuf::Value body; + Status status = utils::JsonToProto(std::get<3>(http), &body); + *(func->add_args()) = body; + } + } + + std::string json_str; + utils::ProtoToJson(response, &json_str, utils::JsonOptions::DEFAULT); + return json_str; + } + + void SetProtoValue(const std::string &key, + const ::google::protobuf::Value &value, + ::google::protobuf::Value *head) { + ::google::protobuf::Struct *s = head->mutable_struct_value(); + auto *fields = s->mutable_fields(); + (*fields)[key] = value; + } + MockApiManagerEnvironment *raw_env_; MockRequest *raw_request_; std::shared_ptr request_context_; @@ -353,27 +459,9 @@ TEST_F(CheckSecurityRulesTest, CheckAuthzFailTestRuleset) { SetUp(service_config, server_config); request_context_->set_auth_claims(kJwtEmailPayload); - EXPECT_CALL(*raw_env_, DoRunHTTPRequest(AllOf( - Property(&HTTPRequest::url, StrEq(release_url_)), - Property(&HTTPRequest::method, StrCaseEq("GET"))))) - .WillOnce(Invoke([](HTTPRequest *req) { - - std::map empty; - std::string body(kRelease); - req->OnComplete(Status::OK, std::move(empty), std::move(body)); - - })); - - EXPECT_CALL(*raw_env_, - DoRunHTTPRequest( - AllOf(Property(&HTTPRequest::url, StrEq(ruleset_test_url_)), - Property(&HTTPRequest::method, StrCaseEq("POST"))))) - .WillOnce(Invoke([](HTTPRequest *req) { - std::map empty; - std::string body; - req->OnComplete(Status(Code::INTERNAL, "Cannot talk to server"), - std::move(empty), std::move(body)); - })); + ExpectCall(release_url_, "GET", "", kRelease); + ExpectCall(ruleset_test_url_, "POST", kFirstRequest, "", + Status(Code::INTERNAL, "Cannot talk to server")); CheckSecurityRules(request_context_, [](Status status) { ASSERT_TRUE(status.CanonicalCode() == Code::INTERNAL); @@ -393,26 +481,10 @@ TEST_F(CheckSecurityRulesTest, CheckAuthzFailWithTestResultFailure) { SetUp(service_config, server_config); request_context_->set_auth_claims(kJwtEmailPayload); - EXPECT_CALL(*raw_env_, DoRunHTTPRequest(AllOf( - Property(&HTTPRequest::url, StrEq(release_url_)), - Property(&HTTPRequest::method, StrCaseEq("GET"))))) - .WillOnce(Invoke([](HTTPRequest *req) { + ExpectCall(release_url_, "GET", "", kRelease); - std::map empty; - std::string body(kRelease); - req->OnComplete(Status::OK, std::move(empty), std::move(body)); - - })); - - EXPECT_CALL(*raw_env_, - DoRunHTTPRequest( - AllOf(Property(&HTTPRequest::url, StrEq(ruleset_test_url_)), - Property(&HTTPRequest::method, StrCaseEq("POST"))))) - .WillOnce(Invoke([](HTTPRequest *req) { - std::map empty; - std::string body = kTestResultFailure; - req->OnComplete(Status::OK, std::move(empty), std::move(body)); - })); + ExpectCall(ruleset_test_url_, "POST", kFirstRequest, + BuildTestRulesetResponse(false)); CheckSecurityRules(request_context_, [](Status status) { ASSERT_TRUE(status.CanonicalCode() == Code::PERMISSION_DENIED); @@ -432,30 +504,125 @@ TEST_F(CheckSecurityRulesTest, CheckAuthzSuccess) { SetUp(service_config, server_config); request_context_->set_auth_claims(kJwtEmailPayload); - EXPECT_CALL(*raw_env_, DoRunHTTPRequest(AllOf( - Property(&HTTPRequest::url, StrEq(release_url_)), - Property(&HTTPRequest::method, StrCaseEq("GET"))))) - .WillOnce(Invoke([](HTTPRequest *req) { + ExpectCall(release_url_, "GET", "", kRelease); + ExpectCall(ruleset_test_url_, "POST", kFirstRequest, + BuildTestRulesetResponse(true)); - std::map empty; - std::string body(kRelease); - req->OnComplete(Status::OK, std::move(empty), std::move(body)); + CheckSecurityRules(request_context_, + [](Status status) { ASSERT_TRUE(status.ok()); }); +} - })); +class CheckSecurityRulesFunctions : public CheckSecurityRulesTest, + public ::testing::WithParamInterface { + public: + void SetUp() { + std::string service_config = std::string(kServiceName) + + kProducerProjectId + kApis + kAuthentication + + kHttp; + std::string server_config = kServerConfig; + CheckSecurityRulesTest::SetUp(service_config, server_config); + request_context_->set_auth_claims(kJwtEmailPayload); + + InSequence s; + + ExpectCall(release_url_, "GET", "", kRelease); + ExpectCall( + ruleset_test_url_, "POST", kFirstRequest, + BuildTestRulesetResponse( + false, {std::make_tuple("f1", "http://url1", "POST", kDummyBody)})); + + ExpectCall("http://url1", "POST", kDummyBody, kDummyBody); + ExpectCall( + ruleset_test_url_, "POST", kSecondRequest, + BuildTestRulesetResponse( + false, {std::make_tuple("f2", "http://url2", "GET", kDummyBody), + std::make_tuple("f3", "https://url3", "GET", kDummyBody), + std::make_tuple("f1", "http://url1", "POST", kDummyBody)})); + ExpectCall("http://url2", "GET", kDummyBody, kDummyBody); + ExpectCall("https://url3", "GET", kDummyBody, kDummyBody); + ExpectCall(ruleset_test_url_, "POST", kThirdRequest, + BuildTestRulesetResponse( + GetParam(), + {std::make_tuple("f2", "http://url2", "GET", kDummyBody), + std::make_tuple("f3", "https://url3", "GET", kDummyBody), + std::make_tuple("f1", "http://url1", "POST", kDummyBody)})); + } +}; - EXPECT_CALL(*raw_env_, - DoRunHTTPRequest( - AllOf(Property(&HTTPRequest::url, StrEq(ruleset_test_url_)), - Property(&HTTPRequest::method, StrCaseEq("POST"))))) - .WillOnce(Invoke([](HTTPRequest *req) { - std::map empty; - std::string body = kTestResultSuccess; - req->OnComplete(Status::OK, std::move(empty), std::move(body)); - })); +// Check the function call request response loop: +// 1. ESP Send TestRulesetRequest (No function calls) +// 2. ESP gets TestRulesetResponse with f1("http://url1, "POST", kDummyBody) +// 3. ESP Send HTTP request to f1.url +// 4. ESP Send TestRulesetRequest with (f1.url, "POST", kDummyBody, +// kDummyResponse); +// 5. EST gets TestRulesetResponse with f1, f2, f3 +// 6. ESP Send HTTP request to f2.url and f3.url. (checks f1 response is +// buffered). +// 7. ESP Send TestRulesetRequest with (f1, f2, f3) +// 8. ESP receives TestRulesetRequest +TEST_P(CheckSecurityRulesFunctions, CheckMultipleFunctions) { + auto ptr = this; + auto success = GetParam(); + CheckSecurityRules(request_context_, [ptr, success](Status status) { + if (success) { + ASSERT_TRUE(status.ok()) << status.ToString(); + } else { + ASSERT_TRUE(status.CanonicalCode() == Code::PERMISSION_DENIED) + << status.ToString(); + } + }); +} - CheckSecurityRules(request_context_, - [](Status status) { ASSERT_TRUE(status.ok()); }); +INSTANTIATE_TEST_CASE_P(CheckMultipleFunctionSuccessFailure, + CheckSecurityRulesFunctions, ::testing::Bool()); + +class CheckSecurityRulesBadFunctions + : public CheckSecurityRulesTest, + public ::testing::WithParamInterface { + public: + void SetUp() { + std::string service_config = std::string(kServiceName) + + kProducerProjectId + kApis + kAuthentication + + kHttp; + std::string server_config = kServerConfig; + CheckSecurityRulesTest::SetUp(service_config, server_config); + request_context_->set_auth_claims(kJwtEmailPayload); + + InSequence s; + + ExpectCall(release_url_, "GET", "", kRelease); + ExpectCall(ruleset_test_url_, "POST", kFirstRequest, + BuildTestRulesetResponse(false, {GetParam()})); + + EXPECT_CALL(*raw_env_, + DoRunHTTPRequest(AllOf( + Property(&HTTPRequest::url, StrEq(std::get<1>(GetParam()))), + Property(&HTTPRequest::method, + StrCaseEq(std::get<2>(GetParam())))))) + .Times(0); + } +}; + +TEST_P(CheckSecurityRulesBadFunctions, CheckBadFunctionArguments) { + auto ptr = this; + CheckSecurityRules(request_context_, [ptr](Status status) { + ASSERT_TRUE(status.CanonicalCode() == Code::INVALID_ARGUMENT) + << status.ToString(); + }); } + +INSTANTIATE_TEST_CASE_P(CheckSecurityRulesBadFunctionArguments, + CheckSecurityRulesBadFunctions, + ::testing::Values( + // Empty function name + std::make_tuple("", "http://url1", "POST", + kDummyBody), + // Argument count less than 2 + std::make_tuple("f1", "", "", ""), + // The url is not set + std::make_tuple("f1", "", "POST", kDummyBody), + // The url is not a http or https protocol + std::make_tuple("f1", "ftp://url1", "BODY", ""))); } } // namespace api_manager } // namespace google diff --git a/contrib/endpoints/src/api_manager/context/request_context.h b/contrib/endpoints/src/api_manager/context/request_context.h index c633952a811..57706c27a2e 100644 --- a/contrib/endpoints/src/api_manager/context/request_context.h +++ b/contrib/endpoints/src/api_manager/context/request_context.h @@ -42,7 +42,7 @@ class RequestContext { } // Get the request object. - Request *request() { return request_.get(); } + Request *request() const { return request_.get(); } // Get the method info. const MethodInfo *method() const { return method_call_.method_info; } @@ -116,7 +116,7 @@ class RequestContext { void set_auth_claims(const std::string &claims) { auth_claims_ = claims; } - const std::string &auth_claims() { return auth_claims_; } + const std::string &auth_claims() const { return auth_claims_; } private: // Fill OperationInfo diff --git a/contrib/endpoints/src/api_manager/context/service_context.cc b/contrib/endpoints/src/api_manager/context/service_context.cc index 28ce9271f80..8e1493d3cfa 100644 --- a/contrib/endpoints/src/api_manager/context/service_context.cc +++ b/contrib/endpoints/src/api_manager/context/service_context.cc @@ -76,6 +76,18 @@ ServiceContext::ServiceContext(std::unique_ptr env, service_account_token_.SetAudience( auth::ServiceAccountToken::JWT_TOKEN_FOR_FIREBASE, kFirebaseAudience); + + if (config_->server_config() && + !config_->server_config() + ->api_check_security_rules_config() + .authorization_service_audience() + .empty()) { + service_account_token_.SetAudience( + auth::ServiceAccountToken::JWT_TOKEN_FOR_AUTHORIZATION_SERVICE, + config_->server_config() + ->api_check_security_rules_config() + .authorization_service_audience()); + } } MethodCallInfo ServiceContext::GetMethodCallInfo( diff --git a/contrib/endpoints/src/api_manager/firebase_rules/BUILD b/contrib/endpoints/src/api_manager/firebase_rules/BUILD new file mode 100644 index 00000000000..8edaaba9411 --- /dev/null +++ b/contrib/endpoints/src/api_manager/firebase_rules/BUILD @@ -0,0 +1,41 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +package(default_visibility = ["//contrib/endpoints/src/api_manager:__subpackages__"]) + +cc_library( + name = "firebase_rules", + srcs = [ + "firebase_request.cc", + ], + hdrs = [ + "firebase_request.h", + ], + linkopts = select({ + "//:darwin": [], + "//conditions:default": [ + "-lm", + "-luuid", + ], + }), + deps = [ + "//contrib/endpoints/src/api_manager:security_rules_proto", + "//contrib/endpoints/src/api_manager/context", + "//contrib/endpoints/src/api_manager/utils", + "//external:cc_wkt_protos", + "//external:googletest_prod", + ], +) diff --git a/contrib/endpoints/src/api_manager/firebase_rules/firebase_request.cc b/contrib/endpoints/src/api_manager/firebase_rules/firebase_request.cc new file mode 100644 index 00000000000..300d521ed1f --- /dev/null +++ b/contrib/endpoints/src/api_manager/firebase_rules/firebase_request.cc @@ -0,0 +1,403 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// +#include "contrib/endpoints/src/api_manager/firebase_rules/firebase_request.h" +#include "contrib/endpoints/src/api_manager/utils/marshalling.h" +#include "contrib/endpoints/src/api_manager/utils/url_util.h" +#include "google/protobuf/util/message_differencer.h" + +#include +#include +using ::google::api_manager::utils::Status; +using ::google::api_manager::proto::TestRulesetResponse; +using ::google::protobuf::util::MessageDifferencer; +using ::google::protobuf::Map; +using TestRulesetResponse = ::google::api_manager::proto::TestRulesetResponse; +using FunctionCall = TestRulesetResponse::TestResult::FunctionCall; +using ::google::protobuf::RepeatedPtrField; + +namespace google { +namespace api_manager { +namespace firebase_rules { + +namespace { + +const std::string kToken = "token"; +const std::string kAuth = "auth"; +const std::string kPath = "path"; +const std::string kMethod = "method"; +const std::string kHttpGetMethod = "GET"; +const std::string kHttpPostMethod = "POST"; +const std::string kHttpHeadMethod = "HEAD"; +const std::string kHttpOptionsMethod = "OPTIONS"; +const std::string kHttpDeleteMethod = "DELETE"; +const std::string kFirebaseCreateMethod = "create"; +const std::string kFirebaseGetMethod = "get"; +const std::string kFirebaseDeleteMethod = "delete"; +const std::string kFirebaseUpdateMethod = "update"; +const std::string kV1 = "/v1"; +const std::string kTestQuery = ":test?alt=json"; + +void SetProtoValue(const std::string &key, + const ::google::protobuf::Value &value, + ::google::protobuf::Value *head) { + ::google::protobuf::Struct *s = head->mutable_struct_value(); + Map *fields = s->mutable_fields(); + (*fields)[key] = value; +} + +// Convert HTTP method to Firebase specific method. +const std::string &GetOperation(const std::string &httpMethod) { + if (httpMethod == kHttpPostMethod) { + return kFirebaseCreateMethod; + } + + if (httpMethod == kHttpGetMethod || httpMethod == kHttpHeadMethod || + httpMethod == kHttpOptionsMethod) { + return kFirebaseGetMethod; + } + + if (httpMethod == kHttpDeleteMethod) { + return kFirebaseDeleteMethod; + } + + return kFirebaseUpdateMethod; +} +} + +// Constructor +FirebaseRequest::FirebaseRequest( + const std::string &ruleset_name, ApiManagerEnvInterface *env, + std::shared_ptr context) + : env_(env), + context_(context), + ruleset_name_(ruleset_name), + firebase_server_( + context->service_context()->config()->GetFirebaseServer()), + current_status_(Status::OK), + is_done_(false), + next_request_(nullptr) { + firebase_http_request_.url = + firebase_server_ + kV1 + "/" + ruleset_name + kTestQuery; + firebase_http_request_.method = kHttpPostMethod; + firebase_http_request_.token_type = + auth::ServiceAccountToken::JWT_TOKEN_FOR_FIREBASE; + external_http_request_.token_type = + auth::ServiceAccountToken::JWT_TOKEN_FOR_AUTHORIZATION_SERVICE; + + // Update the first request to be sent which is the TestRulesetRequest + // request. + SetStatus(UpdateRulesetRequestBody(RepeatedPtrField())); + if (!current_status_.ok()) { + return; + } + + next_request_ = &firebase_http_request_; +} + +bool FirebaseRequest::is_done() { return is_done_; } + +HttpRequest FirebaseRequest::GetHttpRequest() { + if (is_done()) { + return HttpRequest(); + } + + if (next_request_ == nullptr) { + SetStatus(Status(Code::INTERNAL, "Internal state in error")); + return HttpRequest(); + } + + return *next_request_; +} + +Status FirebaseRequest::RequestStatus() { return current_status_; } + +void FirebaseRequest::UpdateResponse(const std::string &body) { + GOOGLE_DCHECK(!is_done()) + << "Receive a response body when no HTTP request is outstanding"; + + GOOGLE_DCHECK(next_request_) + << "Received a response when there is no request set" + "and when is_done is false." + " Looks like a code bug..."; + + if (is_done() || next_request_ == nullptr) { + SetStatus(Status(Code::INTERNAL, + "Internal state error while processing Http request")); + return; + } + + Status status = Status::OK; + + // If the previous request was firebase request, then process its response. + // Otherwise, it is the response for external HTTP request. + if (next_request_ == &firebase_http_request_) { + status = ProcessTestRulesetResponse(body); + } else { + status = ProcessFunctionCallResponse(body); + } + + if (status.ok()) { + status = SetNextRequest(); + } + + SetStatus(status); + return; +} + +void FirebaseRequest::SetStatus(const Status &status) { + if (!status.ok() && !is_done_) { + current_status_ = status; + is_done_ = true; + } +} + +// Create the TestRulesetRequest body. +Status FirebaseRequest::UpdateRulesetRequestBody( + const RepeatedPtrField &function_calls) { + proto::TestRulesetRequest request; + auto test_case = request.mutable_test_suite()->add_test_cases(); + test_case->set_expectation(proto::TestCase::ALLOW); + + ::google::protobuf::Value token; + ::google::protobuf::Value claims; + ::google::protobuf::Value path; + ::google::protobuf::Value method; + + Status status = utils::JsonToProto(context_->auth_claims(), &claims); + if (!status.ok()) { + return status; + } + + auto *variables = test_case->mutable_request()->mutable_struct_value(); + auto *fields = variables->mutable_fields(); + + path.set_string_value(context_->request()->GetRequestPath()); + (*fields)[kPath] = path; + + method.set_string_value( + GetOperation(context_->request()->GetRequestHTTPMethod())); + (*fields)[kMethod] = method; + + SetProtoValue(kToken, claims, &token); + (*fields)[kAuth] = token; + + for (auto func_call : function_calls) { + status = AddFunctionMock(&request, func_call); + if (!status.ok()) { + return status; + } + } + + std::string body; + status = utils::ProtoToJson(request, &body, utils::JsonOptions::DEFAULT); + if (status.ok()) { + env_->LogDebug(std::string("FIREBASE REQUEST BODY = ") + body); + firebase_http_request_.body = body; + } + + return status; +} + +Status FirebaseRequest::ProcessTestRulesetResponse(const std::string &body) { + Status status = utils::JsonToProto(body, &response_); + if (!status.ok()) { + return status; + } + + // If the state is SUCCESS, then we don't need to do any further processing. + if (response_.test_results(0).state() == + TestRulesetResponse::TestResult::SUCCESS) { + is_done_ = true; + next_request_ = nullptr; + return Status::OK; + } + + // Check that the test results size is 1 since we always send a single test + // case. + if (response_.test_results_size() != 1) { + std::ostringstream oss; + oss << "Received TestResultsetResponse with size = " + << response_.test_results_size() << " expecting only 1 test result"; + + env_->LogError(oss.str()); + return Status(Code::INTERNAL, "Unexpected TestResultsetResponse"); + } + + bool allFunctionsProcessed = true; + + // Iterate over all the function calls and make sure that the function calls + // are well formed. + for (auto func_call : response_.test_results(0).function_calls()) { + status = CheckFuncCallArgs(func_call); + if (!status.ok()) { + return status; + } + allFunctionsProcessed &= Find(func_call) != funcs_with_result_.end(); + } + + // Since all the functions have a response and the state is FAILURE, this + // means Unauthorized access to the resource. + if (allFunctionsProcessed) { + std::string message = "Unauthorized Access"; + if (response_.test_results(0).debug_messages_size() > 0) { + std::ostringstream oss; + for (std::string msg : response_.test_results(0).debug_messages()) { + oss << msg << " "; + } + message = oss.str(); + } + + return Status(Code::PERMISSION_DENIED, message); + } + + func_call_iter_ = response_.test_results(0).function_calls().begin(); + return Status::OK; +} + +std::vector>::const_iterator +FirebaseRequest::Find(const FunctionCall &func_call) { + return std::find_if(funcs_with_result_.begin(), funcs_with_result_.end(), + [func_call](std::tuple item) { + return MessageDifferencer::Equals(std::get<0>(item), + func_call); + }); +} + +Status FirebaseRequest::ProcessFunctionCallResponse(const std::string &body) { + if (is_done() || AllFunctionCallsProcessed()) { + return Status(Code::INTERNAL, + "No external function calls present." + " But received a response. Possible code bug"); + } + + funcs_with_result_.emplace_back(*func_call_iter_, body); + func_call_iter_++; + return Status::OK; +} + +// Sets the next HTTP request that should be issued. +Status FirebaseRequest::SetNextRequest() { + if (is_done()) { + next_request_ = nullptr; + return current_status_; + } + + Status status = Status::OK; + + // While there are more functions that should be processed, check if the HTTP + // response for the function is already buffered. Set the next HTTP request if + // we find a new function and break. + while (!AllFunctionCallsProcessed()) { + if (Find(*func_call_iter_) == funcs_with_result_.end()) { + auto call = *func_call_iter_; + external_http_request_.url = call.args(0).string_value(); + external_http_request_.method = call.args(1).string_value(); + std::string body; + status = + utils::ProtoToJson(call.args(2), &body, utils::JsonOptions::DEFAULT); + if (status.ok()) { + external_http_request_.body = body; + next_request_ = &external_http_request_; + } + break; + } + + func_call_iter_++; + } + + // If All functions are processed, then issue a TestRulesetRequest. + if (AllFunctionCallsProcessed()) { + next_request_ = &firebase_http_request_; + return UpdateRulesetRequestBody(response_.test_results(0).function_calls()); + } + + return status; +} + +Status FirebaseRequest::CheckFuncCallArgs(const FunctionCall &func) { + if (func.function().empty()) { + return Status(Code::INVALID_ARGUMENT, "No function name provided"); + } + + // We only support functions that call with three argument: HTTP URL, HTTP + // method and body. The body can be empty + if (func.args_size() < 2 || func.args_size() > 3) { + std::ostringstream os; + os << func.function() << " Require 2 or 3 arguments. But has " + << func.args_size(); + return Status(Code::INVALID_ARGUMENT, os.str()); + } + + if (func.args(0).kind_case() != google::protobuf::Value::kStringValue || + func.args(1).kind_case() != google::protobuf::Value::kStringValue) { + return Status( + Code::INVALID_ARGUMENT, + std::string(func.function() + " Arguments 1 and 2 should be strings")); + } + + if (!utils::IsHttpRequest(func.args(0).string_value())) { + return Status( + Code::INVALID_ARGUMENT, + func.function() + " The first argument should be a HTTP request"); + } + + if (std::string(func.args(1).string_value()).empty()) { + return Status( + Code::INVALID_ARGUMENT, + func.function() + " argument 2 [HTTP METHOD] cannot be emtpy"); + } + + return Status::OK; +} + +bool FirebaseRequest::AllFunctionCallsProcessed() { + return func_call_iter_ == response_.test_results(0).function_calls().end(); +} + +Status FirebaseRequest::AddFunctionMock(proto::TestRulesetRequest *request, + const FunctionCall &func_call) { + if (Find(func_call) == funcs_with_result_.end()) { + return Status(Code::INTERNAL, + std::string("Cannot find body for function call") + + func_call.function()); + } + + auto *func_mock = request->mutable_test_suite() + ->mutable_test_cases(0) + ->add_function_mocks(); + + func_mock->set_function(func_call.function()); + for (auto arg : func_call.args()) { + auto *toAdd = func_mock->add_args()->mutable_exact_value(); + *toAdd = arg; + } + + ::google::protobuf::Value result_json; + Status status = + utils::JsonToProto(std::get<1>(*Find(func_call)), &result_json); + if (!status.ok()) { + env_->LogError(std::string("Error creating protobuf from request body") + + status.ToString()); + return status; + } + + *(func_mock->mutable_result()->mutable_value()) = result_json; + return Status::OK; +} + +} // namespace firebase_rules +} // namespace api_manager +} // namespace google diff --git a/contrib/endpoints/src/api_manager/firebase_rules/firebase_request.h b/contrib/endpoints/src/api_manager/firebase_rules/firebase_request.h new file mode 100644 index 00000000000..ee26b77e646 --- /dev/null +++ b/contrib/endpoints/src/api_manager/firebase_rules/firebase_request.h @@ -0,0 +1,197 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef FIREBASE_RULES_FIREBASE_REQUEST_H_ +#define FIREBASE_RULES_FIREBASE_REQUEST_H_ + +#include +#include +#include +#include "contrib/endpoints/include/api_manager/utils/status.h" +#include "contrib/endpoints/src/api_manager/context/request_context.h" +#include "contrib/endpoints/src/api_manager/proto/security_rules.pb.h" + +// An object of this class should be created for each RequestContext object. +// Here is the flow of messages between ESP, Firebase rules and User provided +// HTTP endpoint: +// +// 1) ESP invokes GetRelease API call on Firebase Service to get the ruleset +// name associated with the Release. The ruleset a representation of the rules +// files that the user deployed to be enforced. The release name is built by +// concatenating the service name and the api version number. +// +// 2) ESP receives the response from Firebase Service which contains the ruleset +// name associated with the Release. From this point on, ESP invokes TestRuleset +// request against this ruleset name. +// +// 3) ESP issues the TestRuleset request that includes the following +// information: +// -- The payload of the JWT token which contains, uid, email or any additional +// claims that can be used to authorize the user. +// -- A test case that provides the Request's HTTP method and HTTP path. +// Note that the above information is provided for ALL TestRuleset requests. +// +// 4) ESP receives a response in TestRulesetResponse message which has a state +// variable that is either set to SUCCESS or FAILURE. +// -- If the state is SUCCESS, then ESP considers this as authorization +// approval and invokes the continution provided with Status::OK. +// -- If the state is FAILURE, then ESP looks more into the TestRulesetResponse +// message to see if there are any user defined HTTP requests that are to be +// invoked and ESP has not seen this request before. If there are no such +// functions, then ESP stops processing with Unauthorized Access error. +// Otherwise, ESP does Step 5. +// +// 5) ESP invokes rules defined HTTP requests that are not yet seen. +// ESP invokes these requests sequentially. Once all HTTP requests are invoked, +// then ESP builds a TestRulesetRequest which contains the following in addition +// to the JWT claims and HTTP method and HTTP path. +// -- For each HTTP request, ESP converts the JSON object into a protobuf::Value +// and sets the result of the HTTP call that be accessed in the Firebase rules +// like a Map. +// ESP send the TestRuleset message to Firebase Service and processing moved to +// step 4) above. +namespace google { +namespace api_manager { +namespace firebase_rules { + +// This structure models any HTTP request that is to be invoked. These include +// both the TestRuleset Request as well as the user defined requests. +struct HttpRequest { + std::string url; + std::string method; + std::string body; + auth::ServiceAccountToken::JWT_TOKEN_TYPE token_type; +}; + +// A FirebaseRequest object understands the various http requests that need +// to be generated as a part of the TestRuleset request and response cycle. +// Here is the intented use of this code: +// FirebaseRequest request(...); +// while(!request.is_done()) { +// std::string url, method, body; +// +// /* The following is not a valid C++ statement. But written so the reader can +// get a general idea ... */ +// +// (url, method, body, token_type) = request.GetHttpRequest(); +// std::string body = InvokeHttpRequest(url, method, body, +// GetToken(token_type)); +// updateResponse(body); +// } +// +// if (request.RequestStatus.ok()) { +// .... ALLOW ..... +// } else { +// .... DENY ..... +// } +class FirebaseRequest { + public: + // Constructor. + FirebaseRequest(const std::string &ruleset_name, ApiManagerEnvInterface *env, + std::shared_ptr context); + + // If the firebase Request calling can be terminated. + bool is_done(); + + // Get the request status. This request status is only valid if is_done is + // true. + utils::Status RequestStatus(); + + // This call should be invoked to get the next http request to execute. + HttpRequest GetHttpRequest(); + + // The response for previous HttpRequest. + void UpdateResponse(const std::string &body); + + private: + utils::Status UpdateRulesetRequestBody( + const ::google::protobuf::RepeatedPtrField< + proto::TestRulesetResponse::TestResult::FunctionCall> &func_calls); + utils::Status ProcessTestRulesetResponse(const std::string &body); + utils::Status ProcessFunctionCallResponse(const std::string &body); + utils::Status CheckFuncCallArgs( + const proto::TestRulesetResponse::TestResult::FunctionCall &func); + utils::Status AddFunctionMock( + proto::TestRulesetRequest *request, + const proto::TestRulesetResponse::TestResult::FunctionCall &func_call); + void SetStatus(const utils::Status &status); + utils::Status SetNextRequest(); + bool AllFunctionCallsProcessed(); + std::vector>::const_iterator + Find(const proto::TestRulesetResponse::TestResult::FunctionCall &func_call); + + // The API manager environment. Primarily used for logging. + ApiManagerEnvInterface *env_; + + // The request context for the current request in progress. + std::shared_ptr context_; + + // The test ruleset name which contains the firebase rules and is used to + // invoke TestRuleset API. + std::string ruleset_name_; + + // The Firebase server that supports the TestRuleset requests. + std::string firebase_server_; + + // This variable tracks the status of the state machine. + utils::Status current_status_; + + // Variable to track if the state machine is done processing. This is set to + // true either when the processing is successfully done or when an error is + // encountered and current_status_ is not Statu::OK anymore. + bool is_done_; + + // The map is used to buffer the response for the user defined function calls. + std::vector> + funcs_with_result_; + + // The iterator iterates over the FunctionCalls the user wishes to invoke. So + // long as this iterator is valid, the state machine issues HTTP requests to + // the user defined HTTP endpoints. Once the iterator is equl to + // func_call_iter.end(), then the TestRuleset is issued which includes the + // function calls along with their responses. + ::google::protobuf::RepeatedPtrField< + proto::TestRulesetResponse::TestResult::FunctionCall>::const_iterator + func_call_iter_; + + // The Test ruleset response currently being processed. + proto::TestRulesetResponse response_; + + // This variable points to either firebase_http_request_ or + // external_http_request_. This will allow the UpdateResponse method to + // understand if the response received is for TestRuleset or user + // defined HTTP endpoint. If next_request points to firebase_http_request_, + // upon receiving a response, UpdateResponse will convert the response to + // TestRulesetResponse and process the response. If next_request_ points + // to external_http_request_, then the reponse provided via UpdateResponse + // is converted into a protobuf::Value. This value is initialized to nullptr + // and will be nullptr once is_done_ is set to true. + HttpRequest *next_request_; + + // The HTTP request to be sent to firebase TestRuleset API + HttpRequest firebase_http_request_; + + // The HTTP request invoked for user provided HTTP endpoint. + HttpRequest external_http_request_; +}; + +} // namespace firebase_rules +} // namespace api_manager +} // namespace google + +#endif // FIREBASE_RULES_REQUEST_HELPER_H_ diff --git a/contrib/endpoints/src/api_manager/proto/security_rules.proto b/contrib/endpoints/src/api_manager/proto/security_rules.proto index ce8d2690fe0..2c78f75f3fd 100644 --- a/contrib/endpoints/src/api_manager/proto/security_rules.proto +++ b/contrib/endpoints/src/api_manager/proto/security_rules.proto @@ -18,40 +18,223 @@ syntax = "proto3"; package google.api_manager.proto; +import "google/protobuf/empty.proto"; import "google/protobuf/struct.proto"; +// The protobufs in this file model the messages that flow from ESP to Firebase +// rules service. The naming of the protobufs start with "Test" and should not +// be confused that the protobufs are used for testing. The protobuf names and +// message structure exactly match the protobufs defined in the firebase rules. +message TestCase { + // The set of supported test case expectations. + enum Expectation { + EXPECTATION_UNSPECIFIED = 0; // Unspecified expectation. + ALLOW = 1; // Expect an allowed result. + DENY = 2; // Expect a denied result. + } + + // Mock function definition. + // + // Mocks must refer to a function declared by the target service. The type of + // the function args and result will be inferred at test time. If either the + // arg or result values are not compatible with function type declaration, the + // request will be considered invalid. + // + // More than one `FunctionMock` may be provided for a given function name so + // long as the `Arg` matchers are distinct. In the event that multiple mocks + // match the expression, the request will be treated as an invalid argument. + message FunctionMock { + // Arg matchers for the mock function. + message Arg { + // Supported argument values. + oneof type { + // Argument exactly matches value provided. + google.protobuf.Value exact_value = 1; + // Argument matches any value provided. + google.protobuf.Empty any_value = 2; + } + } + + // Possible result values from the function mock invocation. + message Result { + // Supported result values. + oneof type { + // The result is an actual value. The type of the value must match that + // of the type declared by the service. + google.protobuf.Value value = 1; + // The result is undefined, meaning the result could not be computed. + google.protobuf.Empty undefined = 2; + } + } + + // The name of the function. + // + // The function name must match one provided by a service declaration. + string function = 1; + + // The list of `Arg` values to match. The order in which the arguments are + // provided is the order in which they must appear in the function + // invocation. + repeated Arg args = 2; + + // The mock result of the function call. + Result result = 3; + } + + // Test expectation. + Expectation expectation = 1; + + // Request context. + // + // The exact format of the request context is service-dependent. See the + // appropriate service documentation for information about the supported + // fields and types on the request. Minimally, all services support the + // following fields and types: + // + // Request field | Type + // ---------------|----------------- + // auth.uid | `string` + // auth.token | `map` + // headers | `map` + // method | `string` + // params | `map` + // path | `string` + // time | `google.protobuf.Timestamp` + // + // If the request value is not well-formed for the service, the request will + // be rejected as an invalid argument. + google.protobuf.Value request = 2; + + // Optional resource value as it appears in persistent storage before the + // request is fulfilled. + // + // The resource type depends on the `request.path` value. + google.protobuf.Value resource = 3; + + // Optional function mocks for service-defined functions. If not set, any + // service defined function is expected to return an error, which may or may + // not influence the test outcome. + repeated FunctionMock function_mocks = 4; +} + +message TestSuite { + // Test cases to be executed. + repeated TestCase test_cases = 1; +} message TestRulesetRequest { - message TestCase { - // The set of supported test case expectations. - enum Expectation { - EXPECTATION_UNSPECIFIED = 0; // Unspecified expectation. - ALLOW = 1; // Expect an allowed result. - DENY = 2; // Expect a denied result. + // Name of the ruleset resource. + // Format: 'projects/{project_id}/rulesets/{ruleset_id}' + string name = 1; + + // The test suite to run against the ruleset + oneof test { + // Inline 'TestSuite' to run. + TestSuite test_suite = 3; + } +} +// Position in the `Source` content including its line, column number, and an +// index of the `File` in the `Source` message. Used for debug purposes. +message SourcePosition { + // Name of the `File`. + string file_name = 1; + + // Index of the `File` in the `Source` message where the content appears. + // @OutputOnly + int32 file_index = 2; + + // Line number of the source fragment. 1-based. + int32 line = 3; + + // First column on the source line associated with the source fragment. + int32 column = 4; + + // Position relative to the beginning of the file. This is used by the IDEA + // plugin, while the line and column are used by the compiler. + int32 current_offset = 5; +} + +message TestRulesetResponse { + // Issues include warnings, errors, and deprecation notices. + message Issue { + // The set of issue severities. + enum Severity { + // An unspecified severity. + SEVERITY_UNSPECIFIED = 0; + // Deprecation issue for statements and method that may no longer be + // supported or maintained. + DEPRECATION = 1; + // Warnings such as: unused variables. + WARNING = 2; + // Errors such as: unmatched curly braces or variable redefinition. + ERROR = 3; } - // The name of the service that is the subject of the test case. - string service_name = 1; + // Position of the issue in the `Source`. + SourcePosition source_position = 1; - // The RESTful resource path of the mock `request`. - string resource_path = 2; + // Short error description. + string description = 2; - // The `request` `operation`. The operation will typically be one of `get`, - // `list`, `create`, `update`, or `delete`. Services also may provide custom - // operations. - string operation = 3; + // The severity of the issue. + Severity severity = 3; + } + + // Test result message containing the state of the test as well as a + // description and source position for test failures. + message TestResult { + // Valid states for the test result. + enum State { + STATE_UNSPECIFIED = 0; // Test state is not set. + SUCCESS = 1; // Test is a success. + FAILURE = 2; // Test is a failure. + } + + // Represents a service-defined function call that was invoked during test + // execution. + message FunctionCall { + // Name of the function invoked. + string function = 1; - // Test expectation. - Expectation expectation = 4; + // The arguments that were provided to the function. + repeated google.protobuf.Value args = 2; + } + + // State of the test. + State state = 1; - // (-- - // Variables and fake resources need to be updated to support multiple - // services and the standardized `request` definition. - // --) + // Debug messages related to test execution issues encountered during + // evaluation. + // + // Debug messages may be related to too many or too few invocations of + // function mocks or to runtime errors that occur during evaluation. + // + // For example: ```Unable to read variable [name: "resource"]``` + repeated string debug_messages = 2; - // Optional set of variable values to use during evaluation. - map variables = 5; + // Position in the `Source` or `Ruleset` where the principle runtime error + // occurs. + // + // Evaluation of an expression may result in an error. Rules are deny by + // default, so a `DENY` expectation when an error is generated is valid. + // When there is a `DENY` with an error, the `SourcePosition` is returned. + // + // E.g. `error_position { line: 19 column: 37 }` + SourcePosition error_position = 3; + + // The set of function calls made to service-defined methods. + // + // Function calls are included in the order in which they are encountered + // during evaluation, are provided for both mocked and unmocked functions, + // and included on the response regardless of the test `state`. + repeated FunctionCall function_calls = 4; } - // The set of test cases to run against the `Source` if it is well-formed. - repeated TestCase test_cases = 3; + // Syntactic and semantic `Source` issues of varying severity. Issues of + // `ERROR` severity will prevent tests from executing. + repeated Issue issues = 1; + + // The set of test results given the test cases in the `TestSuite`. + // The results will appear in the same order as the test cases appear in the + // `TestSuite`. + repeated TestResult test_results = 2; } diff --git a/contrib/endpoints/src/api_manager/proto/server_config.proto b/contrib/endpoints/src/api_manager/proto/server_config.proto index cbee58f8da8..d3f3189fcf3 100644 --- a/contrib/endpoints/src/api_manager/proto/server_config.proto +++ b/contrib/endpoints/src/api_manager/proto/server_config.proto @@ -146,6 +146,7 @@ message ApiAuthenticationConfig { message ApiCheckSecurityRulesConfig { // Firebase server to use. string firebase_server = 1; + string authorization_service_audience = 2; } message Experimental { diff --git a/contrib/endpoints/src/api_manager/utils/url_util.cc b/contrib/endpoints/src/api_manager/utils/url_util.cc index 3d66ffa2910..06ad2e225fc 100644 --- a/contrib/endpoints/src/api_manager/utils/url_util.cc +++ b/contrib/endpoints/src/api_manager/utils/url_util.cc @@ -19,15 +19,17 @@ namespace google { namespace api_manager { namespace utils { +namespace { +const std::string kHttpPrefix = "http://"; +const std::string kHttpsPrefix = "https://"; +} std::string GetUrlContent(const std::string &url) { - static const std::string https_prefix = "https://"; - static const std::string http_prefix = "http://"; std::string result; - if (url.compare(0, https_prefix.size(), https_prefix) == 0) { - result = url.substr(https_prefix.size()); - } else if (url.compare(0, http_prefix.size(), http_prefix) == 0) { - result = url.substr(http_prefix.size()); + if (url.compare(0, kHttpsPrefix.size(), kHttpsPrefix) == 0) { + result = url.substr(kHttpsPrefix.size()); + } else if (url.compare(0, kHttpPrefix.size(), kHttpPrefix) == 0) { + result = url.substr(kHttpPrefix.size()); } else { result = url; } @@ -37,6 +39,11 @@ std::string GetUrlContent(const std::string &url) { return result; } +bool IsHttpRequest(const std::string &url) { + return url.compare(0, kHttpPrefix.size(), kHttpPrefix) == 0 || + url.compare(0, kHttpsPrefix.size(), kHttpsPrefix) == 0; +} + } // namespace utils } // namespace api_manager } // namespace google diff --git a/contrib/endpoints/src/api_manager/utils/url_util.h b/contrib/endpoints/src/api_manager/utils/url_util.h index 2c002b37faf..cef8c2193d2 100644 --- a/contrib/endpoints/src/api_manager/utils/url_util.h +++ b/contrib/endpoints/src/api_manager/utils/url_util.h @@ -25,6 +25,8 @@ namespace utils { // processed string. std::string GetUrlContent(const std::string &url); +bool IsHttpRequest(const std::string &url); + } // namespace utils } // namespace api_manager } // namespace google