Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contrib/endpoints/src/api_manager/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -291,6 +292,8 @@ cc_test(
deps = [
":api_manager",
":mock_api_manager_environment",
":security_rules_proto",
"//external:cc_wkt_protos",
"//external:googletest_main",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
217 changes: 42 additions & 175 deletions contrib/endpoints/src/api_manager/check_security_rules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,87 +17,41 @@
#include <iostream>
#include <sstream>
#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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does the new file have to be on its own folder? Can it be part of auth folder? Or should we create a new folder "authz" for all future authorization files?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially started off with authz but then decided to firebase_rules since this is very explicit as to what is being used.

  1. I don't think auth is the right place for this since authentication and authorization are two different things.
  2. Here are few ways things can go:
    a) If we continue using firebase rules for authorization, then this is directory is still good.
    b) If we decide to support another form of authz along with firebase rules, then I think we can create a directory called authz and move the firebase_rules under that directory and add another directory that appropriately names the authorization mechanism we will support.
    c) We decide to discontinue using firebase rules, then we delete this directory.

I think keeping this as firebase_rules makes a lot of sense since that is what we are doing.

#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<std::string, google::protobuf::Value> *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<AuthzChecker> {
Expand All @@ -111,38 +65,25 @@ class AuthzChecker : public std::enable_shared_from_this<AuthzChecker> {
std::function<void(Status status)> continuation);

private:
// Helper method that invokes the test firebase service api.
void CallTest(const std::string &ruleset_id,
std::shared_ptr<context::RequestContext> context,
std::function<void(Status status)> continuation);
// This method invokes the Firebase TestRuleset API endpoint as well as user
// defined endpoints provided by the TestRulesetResponse.
void CallNextRequest(std::function<void(Status status)> 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<void(Status, std::string &&)> 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<AuthzChecker> GetPtr() { return shared_from_this(); }

ApiManagerEnvInterface *env_;
auth::ServiceAccountToken *sa_token_;
std::unique_ptr<FirebaseRequest> request_handler_;
};

AuthzChecker::AuthzChecker(ApiManagerEnvInterface *env,
Expand All @@ -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;
Expand All @@ -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()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if-else clause can be combined with the if-else clause above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is another way but here is how that code will look.

if (status.ok()) {
checker->env_->LogDebug(
std::string("GetReleasName succeeded with ") + body);
status = checker->ParseReleaseResponse(body, &ruleset_id);
if (status.ok()) {
checker->request_handler_ = std::unique_ptr(
new FirebaseRequest(ruleset_id, checker->env_, context));
checker->CallNextRequest(final_continuation);
} else { // I cannot remove this else statement since this will not go into the else part below
final_continuation(status)
}
} else {
checker->env_->LogError(std::string("GetReleaseName for ") +
GetReleaseUrl(*context.get()) +
" with status " + status.ToString());
status = Status(Code::INTERNAL, kFailedFirebaseReleaseFetch);
}

The code does not look as good with status checks within status checks. Which is why I have written code in the way it is.

checker->CallTest(ruleset_id, context, final_continuation);
checker->request_handler_ = std::unique_ptr<FirebaseRequest>(
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::RequestContext> context,
std::function<void(Status status)> continuation) {
std::string body;
Status status = BuildTestRequestBody(*context.get(), &body);
if (!status.ok()) {
continuation(status);
void AuthzChecker::CallNextRequest(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment for what the function is doing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a comment that already exists in the class declaration. look at line 69. Let me know if you need me to add anything more to it.

std::function<void(Status status)> 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);
});
}

Expand All @@ -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()) {
Expand All @@ -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<char *>(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<void(Status, std::string &&)> continuation) {
env_->LogDebug(std::string("Issue HTTP Request to url :") + url +
" method : " + method + " body: " + request_body);
Expand All @@ -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);
}

Expand Down
Loading