{"id":21354859,"url":"https://github.com/salrashid123/opa_external_groups","last_synced_at":"2025-07-12T22:32:20.140Z","repository":{"id":91310056,"uuid":"494200859","full_name":"salrashid123/opa_external_groups","owner":"salrashid123","description":"Authorization Control using OpenPolicy Agent and Google Groups","archived":false,"fork":false,"pushed_at":"2024-08-29T11:35:09.000Z","size":16617,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-08-29T12:53:42.641Z","etag":null,"topics":["authorization","google-cloud","google-cloud-platform","grpc","open-policy-agent"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/salrashid123.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-19T19:17:22.000Z","updated_at":"2024-08-29T11:35:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"558bd812-6478-474f-971c-c1661dde7fec","html_url":"https://github.com/salrashid123/opa_external_groups","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salrashid123%2Fopa_external_groups","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salrashid123%2Fopa_external_groups/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salrashid123%2Fopa_external_groups/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salrashid123%2Fopa_external_groups/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/salrashid123","download_url":"https://codeload.github.com/salrashid123/opa_external_groups/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225839601,"owners_count":17532308,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authorization","google-cloud","google-cloud-platform","grpc","open-policy-agent"],"created_at":"2024-11-22T04:14:46.557Z","updated_at":"2024-11-22T04:14:47.316Z","avatar_url":"https://github.com/salrashid123.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Authorization Control using OpenPolicy Agent and Google Groups\n\nTutorial to setup [OpenPolicy Agent](https://www.openpolicyagent.org/) where authorization decisions are based using the groups a Google CLoud Identity user is a direct or indirect member of.\n\nNormally, OPA decisions use signals sent to it by the requestor and combines that with data it loads either using its [push or pull model](https://www.openpolicyagent.org/docs/latest/philosophy/#the-opa-document-model).  Basically it requires a system to populate its information that it uses to make authroization decisions. \n\nIf you want to make a decision based on a user's google workspace/cloud identity groups, you could seed OPA with all the groups data...but you'll need to know about changes in membership and refresh OPA.   Thats the problem:  google cloud identity doesn't have any immediate, low latency way to 'get notified' of membership changes.  Sure you can use the [Group Audit Log Events](https://support.google.com/a/answer/6270454?hl=en) but that can take hours to update an event...you need these updates faster.\n\nThis sample uses OPA's [External Data: Pull Data during Evaluation](https://www.openpolicyagent.org/docs/latest/external-data/#option-5-pull-data-during-evaluation) mechanism to load the groups a user is a member of at runtime.\n\nWe're using Envoy as the frontend proxy that calls out to OPA for authorization decisions...however you can use the `OPA` and `Groups Lookup Server` for **any other service that integrates with OPA**...you just need a way to provide OPA's with the username to lookup...\n\nAnyway, we'll use envoy and the [OPA-Envoy Plugin](https://www.openpolicyagent.org/docs/latest/envoy-introduction/)\n\n![images/arch.png](images/arch.png)\n\n\nThe OPA plugin basically functions as a self contained `external_authorization server` for envoy and provides access decisions back to envoy.  OPA itself has authorization policies defined inside its own rego which means it is in exclusive control for the authorization decisions when it comes to group membership.\n\nFor more information about other options for external authorization Envoy, see\n\n* [Envoy External Processing Filter](https://blog.salrashid.dev/articles/2021/envoy_ext_proc/)\n* [Envoy External Authorization server (envoy.ext_authz) with OPA HelloWorld](https://blog.salrashid.dev/articles/2019/envoy_external_authz/)\n\n---\n\n\nLets get started...for this demo, we will run everything locally...i know, OPA, envoy and whatnot usually works with kubernetes ...but this is my demo to show you the component interactions and integration.\n\n\n\n### Setup\n\nFirst edit `/etc/hosts` and add on some hosts to make life a bit easier...\n\n```bash\n127.0.0.1\tgrpc.domain.com envoy.domain.com server.domain.com redis.domain.com\n```\n\nI used my own CA for these certs...if you want you can generate your own here ([CA_scratchpad](https://github.com/salrashid123/ca_scratchpad))\n\nTo run this demo, you'll also need the following:  `golang`, `docker`, `python3`\n\nOnce you have that, get a copy of the Envoy binary (you can also use envoy+dockerimage but i find this easier...)\n\n```bash\ndocker cp `docker create envoyproxy/envoy-dev:latest`:/usr/local/bin/envoy /tmp/envoy\n```\n\nThis demo will use a JWT sent over by a client that will get validated by either Envoy or OPA, then using the embedded claims, issue an API call to an external groups server for group memebership info.  OPA uses the group membership to make the final decision.\n\nThere are two modes you can run OPA here...pick either A or B..it just depends on what you what to test:\n\n\n#### A) Envoy JWT Validation\n\nIn ths mode, Envoy will first validate the JWT provided to it and hand off the decoded JWT to OPA as [Envoy Dynamic Metadata](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/data_sharing_between_filters).   Since envy will validate the JWT, all OPA has to do is parse and make the external call.\n\nTo use the mode, run\n\n```bash\ndocker run -p 8181:8181 -p 9191:9191 \\\n   --net=host \\\n   -v `pwd`/opa_policy:/policy \\\n   -v `pwd`/opa_config:/config openpolicyagent/opa:latest-envoy run \\\n   --server --addr=localhost:8181 \\\n      --set=plugins.envoy_ext_authz_grpc.addr=:9191 \\\n      --set=plugins.envoy_ext_authz_grpc.enable-reflection=true \\\n      --set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow \\\n      --set=status.console=true \\\n      --set=decision_logs.console=true --log-level=debug --log-format json-pretty \\\n      --ignore=.* /policy/policy_jwt.rego\n```\n\nThen run envoy with JWT validation filter enabled\n\n```bash\ncd envoy/\n/tmp//envoy -c envoy_jwt.yaml -l trace\n```\n\n#### B) OPA JWT Validation\n\nIn this mode, the JWT is not validated by envoy and is simply sent to OPA as-is. OPA loads the public certificate that issued the JWT and validates then extracts the claims.\n\nTo use the mode, run:\n\n```bash\ndocker run -p 8181:8181 -p 9191:9191 \\\n   --net=host \\\n   -v `pwd`/opa_policy:/policy \\\n   -v `pwd`/opa_config:/config openpolicyagent/opa:latest-envoy run \\\n   --server --addr=localhost:8181 \\\n      --set=plugins.envoy_ext_authz_grpc.addr=:9191 \\\n      --set=plugins.envoy_ext_authz_grpc.enable-reflection=true \\\n      --set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow \\\n      --set=status.console=true \\\n      --set=decision_logs.console=true --log-level=debug --log-format json-pretty \\\n      --ignore=.* /policy/policy_passthrough.rego\n```\n\nAnd its corresponding envoy filter\n\n```bash\ncd envoy/\n./envoy -c envoy_passthrough.yaml -l debug\n```\n\n#### Cache Options\n\nSince we're loading data from an external source, it'd help if we could cache the data.  \n\nThere are again two options here which you ca use independently from the section above.\n\nYou can either have a global shared cache or a per-opa local cache\n\n\n- `A) Global Redis server`\n\nYou can either run a common `Redis` Server which can hold (with a TTL) the various groups a user is a member of.\n\nThis cache is filled in at runtime so when a user's jwt is first presented, the groups server will lookup the user's groups and save it into Redis with an auto-expiring TTL.\n\nIf you want to suse this mode, run redis\n\n```bash\ndocker run -ti --net=host -p 6379:6379  redis:latest\n```\n\n- `B) Local OPA Cache`\n\nOPA usually loads data locally and uses that to make decisions and to the cache is determined by its pull or push scheme.\n\nExternal HTTP datasources, however, allows you to define how long to cache the response.  To use this,  edit `*.rego` and set the value for (`\"force_cache_duration_seconds\"`):\n\n```r\nr := http.send({\"method\": \"POST\", \"url\": \"https://server.domain.com:8443/authz\", \"body\":  jwt_payload.sub, \"timeout\": \"3s\", \"force_cache\": true, \"force_cache_duration_seconds\": 1, \"tls_ca_cert\": ca_cert})\n```\n\nfor more information, see [OPA External Pull Performance and Availability](https://www.openpolicyagent.org/docs/latest/external-data/#recommended-usage-highly-dynamic-or-large-sized-data)\n\n\n#### Groups Lookup External Datasource\n\nWe're finally rady to start our external groups lookups server. \n\nThe specific API that enables this mode is described here:\n\n* [Search Transitive Group Membership using Google Cloud Identity](https://blog.salrashid.dev/articles/2022/search_group_membership/)\n\n\nIts a lot to get setup and you as a reader probably dont' have the time or access to do this..\n\nto help with that, i've faked the groups lookup server for local testing by defining a set of static users and groups they are members of:\n\n\n\n\n```golang\n\tmocks               = map[string]groupsStruct{\n\t\t\"alice@domain.com\": {\n\t\t\tGroups: []string{\"securitygroup1@domain.com\", \"group_of_groups_1@domain.com\", \"group8_10@domain.com\", \"group4_7@domain.com\", \"deniedgcs@domain.com\", \"all_users_group@domain.com\"},\n\t\t},\n\t\t\"bob@domain.com\": {\n\t\t\tGroups: []string{\"all_users_group@domain.com\"},\n\t\t},\n\t}\n```\n\nso, lets start the server\n\n```bash\n## Start groups server\ncd groups_server\ngo run server.go \n```\n\n\nIf you want to use redis, specify the `--useRedis` flag when starting the groups_server, the default is to use the fakes and no redis\n\n```golang\n  -tlsCert string\n    \tPublic x509 (default \"../certs/server.crt\")\n  -tlsKey string\n    \tPrivate Key (default \"../certs/server.key\")\n  -useCloudIdentityAPI\n    \tUse mock Groups\n  -useRedis\n    \tUse redis cache\n```\n\nYou can test the local group server using the curl (this is what opa will be calling anyway)\n\n```bash\n$ curl -s --cacert certs/root-ca.crt  -X POST --resolve  server.domain.com:8081:127.0.0.1  --data \"alice@domain.com\" https://server.domain.com:8443/authz | jq '.'\n{\n  \"groups\": [\n    \"securitygroup1@domain.com\",\n    \"group_of_groups_1@domain.com\",\n    \"group8_10@domain.com\",\n    \"group4_7@domain.com\",\n    \"deniedgcs@domain.com\",\n    \"all_users_group@domain.com\"\n  ]\n}\n```\n\n#### Test Client\n\nWe're now ready to run the test client testing all this:\n\nFist we need a JWT...included in this repo is a handy script from istio i'm borrowing:\n\n```bash\ncd jwt_generator\nexport ADMIN_TOKEN=`python3 gen-jwt.py -iss foo.bar -aud bar.bar -sub alice@domain.com -claims role:admin -expire 100000 key.pem`\n```\n\nYou can see the specifics of this JWT at [jwt.io](jwt.io)\n\n```json\n{\n  \"alg\": \"RS256\",\n  \"kid\": \"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\",\n  \"typ\": \"JWT\"\n}\n{\n  \"aud\": \"bar.bar\",\n  \"exp\": 1653086822,\n  \"iat\": 1652986822,\n  \"iss\": \"foo.bar\",\n  \"role\": \"admin\",\n  \"sub\": \"alice@domain.com\"\n}\n```\n\nNotice the `aud`, `iss` and `sub` fields...we will use these in the OPA policies.\n\n\nNow use this token to call envoy\n\n```bash\n$ curl -s -H \"Authorization: Bearer $ADMIN_TOKEN\" --cacert certs/root-ca.crt  --resolve  envoy.domain.com:8081:127.0.0.1  -H \"xfoo: bar\" https://envoy.domain.com:8080/get\n\n\u003e GET /get HTTP/1.1\n\u003e Host: envoy.domain.com:8080\n\u003e User-Agent: curl/7.82.0\n\u003e Accept: */*\n\u003e Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJiYXIuYmFyIiwiZXhwIjoxNjUzMDg2OTQ2LCJpYXQiOjE2NTI5ODY5NDYsImlzcyI6ImZvby5iYXIiLCJyb2xlIjoiYWRtaW4iLCJzdWIiOiJhbGljZUBkb21haW4uY29tIn0.GPqG0kke_Lhc3Ma3Cl0H592w_v5Szl7qUyih26ftor6DK0FPCUaD8YueK4eOezYu4j8DMfTz1t-m-oSUcNKA_B9W1FkQy4S2AxtcI2LeG3Ffd6q0Yf0LPokxe1E3lkeumsn_NAw3gvzLaRDqMtZmK-1Qe_o6msPWaiPXpIoSeRSRrDusNRClt30mLKtBQgrnHxtN6Tfg3MnRRd9vycOCvNtNjk0_oE9VsrkKyIiPkS96fLgz_lQ-US51pPcKC6ChMQRJnm0k3eQJPD6XnDUTFjrXCQDyUSfgD7oRwr8JyNGSCrzCthN0Q_UPeZFmLzJ1ab9kHKStAHR0neEbmecuSQ\n\u003e xfoo: bar\n\n\u003c HTTP/1.1 200 OK\n\u003c date: Thu, 19 May 2022 19:02:57 GMT\n\u003c content-type: application/json\n\u003c content-length: 569\n\u003c server: envoy\n\u003c access-control-allow-origin: *\n\u003c access-control-allow-credentials: true\n\u003c x-envoy-upstream-service-time: 23\n\u003c response-header-key-1: resp_value_1\n\u003c \n{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Host\": \"envoy.domain.com\", \n    \"User-Agent\": \"curl/7.82.0\", \n    \"X-Amzn-Trace-Id\": \"Root=1-62869461-435c1d3b5cd1748d6fec7142\", \n    \"X-Envoy-Expected-Rq-Timeout-Ms\": \"15000\", \n    \"X-Ext-Auth-Allow\": \"yes\", \n    \"X-Google-Groups\": \"securitygroup1@domain.com group_of_groups_1@domain.com group8_10@domain.com group4_7@domain.com deniedgcs@domain.com all_users_group@domain.com\", \n    \"X-Validated-By\": \"security-checkpoint\"\n  }, \n  \"origin\": \"72.83.67.174\", \n  \"url\": \"https://envoy.domain.com/get\"\n}\n```\n\nWhat your'e seeing is three parts:\n\n1. request to envoy\n2. response from envoy\n3. the request from envoy that httpbin.org/get actually saw.\n\nOur OPA rules stipulated that we should add the groups information to the output request\n\n```\nheaders[\"x-ext-auth-allow\"] := \"yes\"\nheaders[\"x-validated-by\"] := \"security-checkpoint\"\nheaders[\"x-google-groups\"] := concat(\" \",external_data.upstream_body[\"groups\"])\nrequest_headers_to_remove := [\"xfoo\"]\nresponse_headers_to_add[\"response-header-key-1\"] :=  \"resp_value_1\"\n```\n\nand thats exactly what we see...the upstream service (httpbin in our case) will get to see the groups this user is a member of!\n\n\n#### gRPC\n\n\nEnvoy is very `grpc-first`...it has really good grpc handling capabilities.  \n\nOPA-Envoy plugin leverages all that to allow you to _inspect and make decisions_ based on the gRPC Probuf messages!\n\n (well, [in a limited way](https://www.openpolicyagent.org/docs/v0.27.1/envoy-introduction/#configuration): just unary requests w/o compression)\n\nAnyway, i think its nice so lets see this in action:\n\nFor this we will need a grpc client and server, then envoy that uses grpc and an OPA rego that will parse out and decode the message\n\nIn our case the Message proto is trivial:\n\n```proto\nsyntax = \"proto3\";\npackage echo;\n\nservice EchoServer {\n  rpc SayHelloUnary (EchoRequest) returns (EchoReply) {}\n}\n\nmessage EchoRequest {\n  string name = 1;\n}\n\nmessage EchoReply {\n  string message = 1;\n}\n```\n\nStart gRPC Server\n\n```bash\ncd grpc/\ngo run greeter_server/grpc_server.go --grpcport :50051 --tlsCert ../certs/grpc.crt --tlsKey ../certs/grpc.key\n```\n\nStart Envoy with grpc handlers:\n\n```bash\nenvoy -c envoy_grpc.yaml -l trace\n```\n\nStart OPA with grpc decoding enabled (`--set=plugins.envoy_ext_authz_grpc.proto-descriptor=/proto/echo.proto.pb`)\n\n```bash\ndocker run -p 8181:8181 -p 9191:9191 \\\n   --net=host \\\n   -v `pwd`/opa_policy:/policy \\\n   -v `pwd`/grpc/echo/echo.proto.pb:/proto/echo.proto.pb \\\n   -v `pwd`/opa_config:/config openpolicyagent/opa:latest-envoy run \\\n   --server --addr=localhost:8181 \\\n      --set=plugins.envoy_ext_authz_grpc.addr=:9191 \\\n      --set=plugins.envoy_ext_authz_grpc.enable-reflection=true \\\n      --set=plugins.envoy_ext_authz_grpc.proto-descriptor=/proto/echo.proto.pb \\\n      --set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow \\\n      --set=status.console=true \\\n      --set=decision_logs.console=true --log-level=debug --log-format json-pretty \\\n      --ignore=.* /policy/policy_jwt_grpc.rego\n```\n\nWhat this rego does is validates the JWT as in the examples above but it checks for some other stuff:\n\n```r\nallow_theeggman {\n  http_request.method == \"POST\"\n  glob.match(\"/echo.EchoServer/SayHelloUnary\", [], http_request.path)\n  input.parsed_body.name == \"iamtheeggman\"\n}\n```\n\nthis policy will only work if a specific endpoint is called **AND** if the client emits a gRPC _message_ of Type `EchoRequest` with the payload value of _iamtheeggman_ (yeah, see the command below)\n\n```golang\n\tctx = grpcMetadata.AppendToOutgoingContext(ctx, \"authorization\", \"Bearer \"+*authToken)\n\tctx = grpcMetadata.AppendToOutgoingContext(ctx, \"xfoo\", \"bar\")\n  r, err := c.SayHelloUnary(ctx, \u0026echo.EchoRequest{Name: *payloadData})\n```\n\n\nGet an `ADMIN_TOKEN` using the pythons script above\n\n\nTest gRPC client...remember to pass in `iamtheeggman` too!\n\n```bash\necho $ADMIN_TOKEN\n\ngo run greeter_client/grpc_client.go \\\n   --host localhost:8080 --cacert ../certs/root-ca.crt \\\n   --servername envoy.domain.com -skipHealthCheck  \\\n   --payloadData=iamtheeggman \\\n   --authToken=$ADMIN_TOKEN \n```\n\nnow notice the OPA request it got for evaluation...specifically the `parsed_body`!  thats the decoded protobuf\n\n\n```json\n{\n  \"decision_id\": \"c2da69a1-b678-4df4-9055-e8edec379ab2\",\n  \"input\": {\n    \"attributes\": {\n      \"destination\": {\n        \"address\": {\n          \"socketAddress\": {\n            \"address\": \"127.0.0.1\",\n            \"portValue\": 8080\n          }\n        },\n        \"principal\": \"envoy.domain.com\"\n      },\n      \"metadataContext\": {\n        \"filterMetadata\": {\n          \"envoy.filters.http.jwt_authn\": {\n            \"verified_jwt\": {\n              \"aud\": \"bar.bar\",\n              \"exp\": 1653086946,\n              \"iat\": 1652986946,\n              \"iss\": \"foo.bar\",\n              \"role\": \"admin\",\n              \"sub\": \"alice@domain.com\"\n            }\n          }\n        }\n      },\n      \"request\": {\n        \"http\": {\n          \"headers\": {\n            \":authority\": \"envoy.domain.com\",\n            \":method\": \"POST\",\n            \":path\": \"/echo.EchoServer/SayHelloUnary\",\n            \":scheme\": \"https\",\n            \"content-type\": \"application/grpc\",\n            \"grpc-timeout\": \"997788u\",\n            \"te\": \"trailers\",\n            \"user-agent\": \"grpc-go/1.33.2\",\n            \"x-envoy-auth-partial-body\": \"false\",\n            \"x-forwarded-proto\": \"https\",\n            \"x-request-id\": \"3a3ae89f-bc08-4216-a520-128c3cc0696d\",\n            \"xfoo\": \"bar\"\n          },\n          \"host\": \"envoy.domain.com\",\n          \"id\": \"13150880515659298070\",\n          \"method\": \"POST\",\n          \"path\": \"/echo.EchoServer/SayHelloUnary\",\n          \"protocol\": \"HTTP/2\",\n          \"rawBody\": \"AAAAAA4KDGlhbXRoZWVnZ21hbg==\",\n          \"scheme\": \"https\",\n          \"size\": \"19\"\n        },\n        \"time\": \"2022-05-19T19:14:04.177094Z\"\n      },\n      \"source\": {\n        \"address\": {\n          \"socketAddress\": {\n            \"address\": \"127.0.0.1\",\n            \"portValue\": 36564\n          }\n        }\n      }\n    },\n    \"parsed_body\": {\n      \"name\": \"iamtheeggman\"\n    },\n    \"parsed_path\": [\n      \"echo.EchoServer\",\n      \"SayHelloUnary\"\n    ],\n    \"parsed_query\": {},\n    \"truncated_body\": false,\n    \"version\": {\n      \"encoding\": \"protojson\",\n      \"ext_authz\": \"v3\"\n    }\n  },\n  \"labels\": {\n    \"id\": \"42c36cb1-8d64-43a4-b7cf-1c84faec0a45\",\n    \"version\": \"0.40.0-envoy-1\"\n  },\n  \"level\": \"info\",\n  \"metrics\": {\n    \"timer_rego_builtin_http_send_ns\": 3415355,\n    \"timer_rego_query_compile_ns\": 80794,\n    \"timer_rego_query_eval_ns\": 3847696,\n    \"timer_server_handler_ns\": 4795661\n  },\n  \"msg\": \"Decision Log\",\n  \"path\": \"envoy/authz/allow\",\n  \"result\": {\n    \"allowed\": true,\n    \"body\": \"ok\",\n    \"headers\": {\n      \"x-ext-auth-allow\": \"yes\",\n      \"x-google-groups\": \"securitygroup1@domain.com group_of_groups_1@domain.com group8_10@domain.com group4_7@domain.com deniedgcs@domain.com all_users_group@domain.com\",\n      \"x-validated-by\": \"security-checkpoint\"\n    },\n    \"http_status\": 200,\n    \"request_headers_to_remove\": [\n      \"xfoo\"\n    ],\n    \"response_headers_to_add\": {\n      \"response-header-key-1\": \"resp_value_1\"\n    }\n  },\n  \"time\": \"2022-05-19T19:14:04Z\",\n  \"timestamp\": \"2022-05-19T19:14:04.183258064Z\",\n  \"type\": \"openpolicyagent.org/decision_logs\"\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsalrashid123%2Fopa_external_groups","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsalrashid123%2Fopa_external_groups","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsalrashid123%2Fopa_external_groups/lists"}