| #include "resource_authz.h" |
| |
| #include "net/proto2/contrib/parse_proto/parse_text_proto.h" |
| #include "gmock.h" |
| #include "gunit.h" |
| #include "testing/fuzzing/fuzztest.h" |
| #include "absl/status/status.h" |
| #include "absl/strings/string_view.h" |
| #include "fake_auth_context.h" |
| #include "proxy_config.pb.h" |
| #include "redfish_plugin.h" |
| |
| namespace milotic { |
| namespace { |
| |
| using ::testing::AtMost; |
| using ::testing::HasSubstr; |
| using ::testing::Ref; |
| using ::testing::Return; |
| using ::testing::status::StatusIs; |
| |
| using ::milotic_grpc_proxy::AuthorizationPolicy; |
| using ::google::protobuf::contrib::parse_proto::ParseTextProtoOrDie; |
| using RequestVerb = RedfishPlugin::RequestVerb; |
| |
| TEST(ResourceAuthzTest, EmptyRulePassesMatchingResource) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/resource" |
| allow: { key: "empty_rule" } |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(false); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_OK( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some/resource", context)); |
| } |
| |
| TEST(ResourceAuthzTest, EmptyRuleRejectsNonMatchingResource) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/resource" |
| allow: { key: "empty_rule" } |
| })pb"); |
| |
| milotic::FakeAuthContext auth_context(false); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_THAT(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some/other/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(ResourceAuthzTest, NoAllowBlocksAll) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/resource" |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(ResourceAuthzTest, WithSubtreeWorks) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some" |
| with_subtree: true |
| allow: { key: "all" } |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_OK( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some/resource", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, "/some/other/resource", |
| context)); |
| |
| EXPECT_THAT(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some_blocked/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(ResourceAuthzTest, ExpandBlockedIfWithoutSubtree) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some" |
| allow: { key: "all" } |
| } |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/allowed" |
| with_subtree: true |
| allow: { key: "all" } |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_THAT(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some?$expand=*&$select=a", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some/allowed/resource?$expand=*", context)); |
| } |
| |
| TEST(ResourceAuthzTest, NonExpandQueryParametersIgnored) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some" |
| allow: { key: "all" } |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_OK( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some?$select=a", context)); |
| } |
| |
| TEST(ResourceAuthzTest, MappingsAppliedInSequence) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping1" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/resource" |
| } |
| mappings: { |
| name: "test_mapping2" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some" |
| with_subtree: true |
| allow: { key: "all" } |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, "/some/other/resource", |
| context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some/resource/allowed_subresource", context)); |
| } |
| |
| TEST(ResourceAuthzTest, NoVerbMatchesAll) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "always_reject_some_resource" |
| resource_path: "/some/resource" |
| } |
| mappings: { |
| name: "accept_others" |
| with_subtree: true |
| allow: { key: "all" } |
| resource_path: "/" |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(false); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some/allowed/resource", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kPost, |
| "/some/allowed/resource", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kPatch, |
| "/some/allowed/resource", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kPut, |
| "/some/allowed/resource", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kDelete, |
| "/some/allowed/resource", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kSubscribe, |
| "/some/allowed/resource", context)); |
| |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied, |
| HasSubstr("always_reject_some_resource"))); |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kPost, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied, |
| HasSubstr("always_reject_some_resource"))); |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kPatch, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied, |
| HasSubstr("always_reject_some_resource"))); |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kPut, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied, |
| HasSubstr("always_reject_some_resource"))); |
| EXPECT_THAT( |
| AuthorizeRequest(policy, RequestVerb::kDelete, "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied, |
| HasSubstr("always_reject_some_resource"))); |
| EXPECT_THAT(AuthorizeRequest(policy, RequestVerb::kSubscribe, |
| "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied, |
| HasSubstr("always_reject_some_resource"))); |
| } |
| |
| TEST(ResourceAuthzTest, RootResourcePathMatchesAll) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| request_verb: REQUEST_VERB_GET |
| allow: { key: "all" } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, "/some", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, "/other", context)); |
| EXPECT_THAT(AuthorizeRequest(policy, RequestVerb::kPost, "/other", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(ResourceAuthzTest, TrailingSlashMatches) { |
| AuthorizationPolicy policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "some_rule" |
| resource_path: "/some/resource" |
| allow: { key: "all" } |
| } |
| )pb"); |
| milotic::FakeAuthContext auth_context(true); |
| AuthorizationContext context{.grpc_context = auth_context}; |
| |
| EXPECT_OK( |
| AuthorizeRequest(policy, RequestVerb::kGet, "/some/resource/", context)); |
| EXPECT_OK(AuthorizeRequest(policy, RequestVerb::kGet, |
| "/some/resource/?$select=a", context)); |
| } |
| |
| TEST(ResourceAuthzTest, NoPermissionCheckerFailsPermissionCheck) { |
| FakeAuthContext fake_auth_context(true); |
| AuthorizationContext context{.grpc_context = fake_auth_context}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| allow: { |
| key: "test_rule", |
| value: { permission_id: "test-permission" } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| |
| EXPECT_THAT(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| class MockPermissionChecker : public PermissionChecker { |
| public: |
| MOCK_METHOD(bool, Check, |
| (const AuthorizationContext& context, |
| absl::string_view permission_id), |
| (override)); |
| }; |
| |
| TEST(ResourceAuthzTest, PermissionCheckerCanApprove) { |
| FakeAuthContext fake_auth_context(true); |
| MockPermissionChecker permission_checker; |
| AuthorizationContext context{.grpc_context = fake_auth_context, |
| .permission_checker = &permission_checker}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| allow: { |
| key: "test_rule", |
| value: { permission_id: "test-permission" } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| EXPECT_CALL(permission_checker, Check(Ref(context), "test-permission")) |
| .WillOnce(Return(true)); |
| EXPECT_OK(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context)); |
| } |
| |
| TEST(ResourceAuthzTest, PermissionCheckerCanReject) { |
| FakeAuthContext fake_auth_context(true); |
| MockPermissionChecker permission_checker; |
| AuthorizationContext context{.grpc_context = fake_auth_context, |
| .permission_checker = &permission_checker}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| allow: { |
| key: "test_rule", |
| value: { permission_id: "test-permission" } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| EXPECT_CALL(permission_checker, Check(Ref(context), "test-permission")) |
| .WillOnce(Return(false)); |
| EXPECT_THAT(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(ResourceAuthzTest, LocalPermissionCanAllow) { |
| FakeAuthContext fake_auth_context(true); |
| AuthorizationContext context{.grpc_context = fake_auth_context}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| allow: { |
| key: "test_rule", |
| value: { local: true } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| fake_auth_context.AddProperty("transport_security_type", "local"); |
| EXPECT_OK(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context)); |
| } |
| |
| TEST(ResourceAuthzTest, LocalPermissionCanReject) { |
| FakeAuthContext fake_auth_context(true); |
| AuthorizationContext context{.grpc_context = fake_auth_context}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| allow: { |
| key: "test_rule", |
| value: { local: true } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| EXPECT_THAT(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| |
| fake_auth_context.AddProperty("transport_security_type", "none"); |
| EXPECT_THAT(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(ResourceAuthzTest, MultipleResourcePathsMatch) { |
| FakeAuthContext fake_auth_context(true); |
| MockPermissionChecker permission_checker; |
| AuthorizationContext context{.grpc_context = fake_auth_context, |
| .permission_checker = &permission_checker}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping1" |
| allow: { |
| key: "test_rule1", |
| value: { permission_id: "matching-resources" } |
| } |
| resource_path: "/some/resource" |
| resource_path: "/some/other/resource" |
| } |
| mappings: { |
| name: "test_mapping2" |
| allow: { |
| key: "test_rule2" |
| value: { permission_id: "non-matching-resources" } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| EXPECT_CALL(permission_checker, Check(Ref(context), "matching-resources")) |
| .Times(2) |
| .WillRepeatedly(Return(true)); |
| EXPECT_CALL(permission_checker, Check(Ref(context), "non-matching-resources")) |
| .WillOnce(Return(false)); |
| EXPECT_OK(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/resource", context)); |
| EXPECT_OK(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/other/resource", context)); |
| EXPECT_THAT(AuthorizeRequest(authorization_policy, RequestVerb::kGet, |
| "/some/non-matching/resource", context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| |
| void RejectedPermissionIsDenied(int verb, absl::string_view resource_id) { |
| FakeAuthContext fake_auth_context(true); |
| testing::StrictMock<MockPermissionChecker> permission_checker; |
| AuthorizationContext context{.grpc_context = fake_auth_context, |
| .permission_checker = &permission_checker}; |
| |
| AuthorizationPolicy authorization_policy = ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping" |
| allow: { |
| key: "test_permission" |
| value: { permission_id: "reject" } |
| } |
| allow: { |
| key: "test_local" |
| value: { local: true } |
| } |
| resource_path: "/" |
| with_subtree: true |
| } |
| )pb"); |
| EXPECT_CALL(permission_checker, Check(Ref(context), "reject")) |
| .Times(AtMost(1)) |
| .WillOnce(Return(false)); |
| EXPECT_THAT( |
| AuthorizeRequest(authorization_policy, static_cast<RequestVerb>(verb), |
| resource_id, context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| FUZZ_TEST(ResourceAuthzTest, RejectedPermissionIsDenied); |
| |
| } // namespace |
| } // namespace milotic |