├── .copywrite.hcl ├── .envrc ├── .github ├── dependabot.yml └── workflows │ ├── get-go-version.yml │ └── test.yml ├── .gitignore ├── .go-version ├── CODEOWNERS ├── LICENSE ├── META.d └── _summary.yaml ├── Makefile ├── README.md ├── encoding ├── encoding_test.go ├── go_to_value.go └── value_to_go.go ├── flake.lock ├── flake.nix ├── framework ├── framework.go ├── interface.go ├── map.go ├── mock_NamespaceCreator_test.go ├── mock_Namespace_test.go ├── mock_Root_test.go ├── plugin.go ├── plugin_test.go └── reflect.go ├── go.mod ├── go.sum ├── mock_Plugin.go ├── mock_Plugin_Closer.go ├── mock_Plugin_Closer.go.src ├── nix ├── overlay.nix └── sentinel_sdk.nix ├── plugin.go ├── plugin_test.go ├── proto ├── README.md ├── go │ ├── plugin.pb.go │ └── proto.go └── plugin.proto ├── rpc ├── plugin.go ├── plugin_grpc_client.go ├── plugin_grpc_client_test.go ├── plugin_grpc_server.go ├── plugin_grpc_test.go ├── plugin_test.go ├── rpc.go └── serve.go ├── shell.nix └── testing ├── README.md ├── data └── main.go.tpl ├── plugin.go ├── plugin_test.go ├── testdata └── plugin-test-dir │ ├── error-pragma-regex.sentinel │ ├── nope.txt │ ├── subdir │ └── bad.sentinel │ └── test.sentinel ├── testing.go └── testplugin └── plugin.go /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | project { 5 | header_ignore = [ 6 | "testing/testdata/**/*.sentinel", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export GOPRIVATE=github.com/hashicorp/* 2 | 3 | if has nix-shell; then 4 | use nix 5 | fi 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/get-go-version.yml: -------------------------------------------------------------------------------- 1 | name: Reusable Go Version Workflow 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | go-version: 7 | description: "The Go version to use" 8 | value: ${{ jobs.get-go-version.outputs.go-version }} 9 | 10 | jobs: 11 | get-go-version: 12 | name: "Determine Go toolchain version" 13 | runs-on: [ ubuntu-latest ] 14 | outputs: 15 | go-version: ${{ steps.get-go-version.outputs.go-version }} 16 | steps: 17 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 18 | - name: Determine Go version 19 | id: get-go-version 20 | # We use .go-version as our source of truth for current Go 21 | # version, because "goenv" can react to it automatically. 22 | run: | 23 | echo "Building with Go $(cat .go-version)" 24 | echo "go-version=$(cat .go-version)" >> $GITHUB_OUTPUT 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Sentinel SDK CI Tests 2 | 3 | on: [workflow_dispatch, push] 4 | 5 | env: 6 | # Increment this to clear GHA cache 7 | GHA_CACHE_VERSION: v2 8 | # The sentinel version to test against 9 | SENTINEL_VERSION: 0.19.0 10 | 11 | jobs: 12 | get-go-version: 13 | uses: ./.github/workflows/get-go-version.yml 14 | 15 | test: 16 | needs: 17 | - get-go-version 18 | runs-on: [ ubuntu-latest ] 19 | name: Tests 20 | 21 | steps: 22 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 23 | 24 | - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 25 | with: 26 | path: | 27 | ~/.cache/go-build 28 | ~/go/pkg/mod 29 | key: ${{ env.GHA_CACHE_VERSION }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ env.GHA_CACHE_VERSION }}-${{ runner.os }}-go- 32 | 33 | - name: Setup go 34 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 35 | with: 36 | go-version: ${{ needs.get-go-version.outputs.go-version }} 37 | 38 | - name: Install sentinel 39 | uses: hashicorp/setup-sentinel@a25ee454cc706381e2bcab87fd2cb354c2736953 # v0.0.1 40 | with: 41 | version: ${{ env.SENTINEL_VERSION }} 42 | 43 | - name: Test 44 | run: | 45 | make modules 46 | make tools 47 | make test-ci 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test-results/ 2 | .direnv 3 | .vscode 4 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.23 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/tf-compliance 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 HashiCorp, Inc. 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. "Contributor" 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. "Contributor Version" 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. "Covered Software" 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. "Incompatible With Secondary Licenses" 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of 35 | version 1.1 or earlier of the License, but not also under the terms of 36 | a Secondary License. 37 | 38 | 1.6. "Executable Form" 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. "Larger Work" 43 | 44 | means a work that combines Covered Software with other material, in a 45 | separate file or files, that is not Covered Software. 46 | 47 | 1.8. "License" 48 | 49 | means this document. 50 | 51 | 1.9. "Licensable" 52 | 53 | means having the right to grant, to the maximum extent possible, whether 54 | at the time of the initial grant or subsequently, any and all of the 55 | rights conveyed by this License. 56 | 57 | 1.10. "Modifications" 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, 62 | deletion from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. "Patent Claims" of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, 69 | process, and apparatus claims, in any patent Licensable by such 70 | Contributor that would be infringed, but for the grant of the License, 71 | by the making, using, selling, offering for sale, having made, import, 72 | or transfer of either its Contributions or its Contributor Version. 73 | 74 | 1.12. "Secondary License" 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. "Source Code Form" 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. "You" (or "Your") 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, "You" includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, "control" means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or 106 | as part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its 110 | Contributions or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution 115 | become effective for each Contribution on the date the Contributor first 116 | distributes such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under 121 | this License. No additional rights or licenses will be implied from the 122 | distribution or licensing of Covered Software under this License. 123 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 124 | Contributor: 125 | 126 | a. for any code that a Contributor has removed from Covered Software; or 127 | 128 | b. for infringements caused by: (i) Your and any other third party's 129 | modifications of Covered Software, or (ii) the combination of its 130 | Contributions with other software (except as part of its Contributor 131 | Version); or 132 | 133 | c. under Patent Claims infringed by Covered Software in the absence of 134 | its Contributions. 135 | 136 | This License does not grant any rights in the trademarks, service marks, 137 | or logos of any Contributor (except as may be necessary to comply with 138 | the notice requirements in Section 3.4). 139 | 140 | 2.4. Subsequent Licenses 141 | 142 | No Contributor makes additional grants as a result of Your choice to 143 | distribute the Covered Software under a subsequent version of this 144 | License (see Section 10.2) or under the terms of a Secondary License (if 145 | permitted under the terms of Section 3.3). 146 | 147 | 2.5. Representation 148 | 149 | Each Contributor represents that the Contributor believes its 150 | Contributions are its original creation(s) or it has sufficient rights to 151 | grant the rights to its Contributions conveyed by this License. 152 | 153 | 2.6. Fair Use 154 | 155 | This License is not intended to limit any rights You have under 156 | applicable copyright doctrines of fair use, fair dealing, or other 157 | equivalents. 158 | 159 | 2.7. Conditions 160 | 161 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 162 | Section 2.1. 163 | 164 | 165 | 3. Responsibilities 166 | 167 | 3.1. Distribution of Source Form 168 | 169 | All distribution of Covered Software in Source Code Form, including any 170 | Modifications that You create or to which You contribute, must be under 171 | the terms of this License. You must inform recipients that the Source 172 | Code Form of the Covered Software is governed by the terms of this 173 | License, and how they can obtain a copy of this License. You may not 174 | attempt to alter or restrict the recipients' rights in the Source Code 175 | Form. 176 | 177 | 3.2. Distribution of Executable Form 178 | 179 | If You distribute Covered Software in Executable Form then: 180 | 181 | a. such Covered Software must also be made available in Source Code Form, 182 | as described in Section 3.1, and You must inform recipients of the 183 | Executable Form how they can obtain a copy of such Source Code Form by 184 | reasonable means in a timely manner, at a charge no more than the cost 185 | of distribution to the recipient; and 186 | 187 | b. You may distribute such Executable Form under the terms of this 188 | License, or sublicense it under different terms, provided that the 189 | license for the Executable Form does not attempt to limit or alter the 190 | recipients' rights in the Source Code Form under this License. 191 | 192 | 3.3. Distribution of a Larger Work 193 | 194 | You may create and distribute a Larger Work under terms of Your choice, 195 | provided that You also comply with the requirements of this License for 196 | the Covered Software. If the Larger Work is a combination of Covered 197 | Software with a work governed by one or more Secondary Licenses, and the 198 | Covered Software is not Incompatible With Secondary Licenses, this 199 | License permits You to additionally distribute such Covered Software 200 | under the terms of such Secondary License(s), so that the recipient of 201 | the Larger Work may, at their option, further distribute the Covered 202 | Software under the terms of either this License or such Secondary 203 | License(s). 204 | 205 | 3.4. Notices 206 | 207 | You may not remove or alter the substance of any license notices 208 | (including copyright notices, patent notices, disclaimers of warranty, or 209 | limitations of liability) contained within the Source Code Form of the 210 | Covered Software, except that You may alter any license notices to the 211 | extent required to remedy known factual inaccuracies. 212 | 213 | 3.5. Application of Additional Terms 214 | 215 | You may choose to offer, and to charge a fee for, warranty, support, 216 | indemnity or liability obligations to one or more recipients of Covered 217 | Software. However, You may do so only on Your own behalf, and not on 218 | behalf of any Contributor. You must make it absolutely clear that any 219 | such warranty, support, indemnity, or liability obligation is offered by 220 | You alone, and You hereby agree to indemnify every Contributor for any 221 | liability incurred by such Contributor as a result of warranty, support, 222 | indemnity or liability terms You offer. You may include additional 223 | disclaimers of warranty and limitations of liability specific to any 224 | jurisdiction. 225 | 226 | 4. Inability to Comply Due to Statute or Regulation 227 | 228 | If it is impossible for You to comply with any of the terms of this License 229 | with respect to some or all of the Covered Software due to statute, 230 | judicial order, or regulation then You must: (a) comply with the terms of 231 | this License to the maximum extent possible; and (b) describe the 232 | limitations and the code they affect. Such description must be placed in a 233 | text file included with all distributions of the Covered Software under 234 | this License. Except to the extent prohibited by statute or regulation, 235 | such description must be sufficiently detailed for a recipient of ordinary 236 | skill to be able to understand it. 237 | 238 | 5. Termination 239 | 240 | 5.1. The rights granted under this License will terminate automatically if You 241 | fail to comply with any of its terms. However, if You become compliant, 242 | then the rights granted under this License from a particular Contributor 243 | are reinstated (a) provisionally, unless and until such Contributor 244 | explicitly and finally terminates Your grants, and (b) on an ongoing 245 | basis, if such Contributor fails to notify You of the non-compliance by 246 | some reasonable means prior to 60 days after You have come back into 247 | compliance. Moreover, Your grants from a particular Contributor are 248 | reinstated on an ongoing basis if such Contributor notifies You of the 249 | non-compliance by some reasonable means, this is the first time You have 250 | received notice of non-compliance with this License from such 251 | Contributor, and You become compliant prior to 30 days after Your receipt 252 | of the notice. 253 | 254 | 5.2. If You initiate litigation against any entity by asserting a patent 255 | infringement claim (excluding declaratory judgment actions, 256 | counter-claims, and cross-claims) alleging that a Contributor Version 257 | directly or indirectly infringes any patent, then the rights granted to 258 | You by any and all Contributors for the Covered Software under Section 259 | 2.1 of this License shall terminate. 260 | 261 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 262 | license agreements (excluding distributors and resellers) which have been 263 | validly granted by You or Your distributors under this License prior to 264 | termination shall survive termination. 265 | 266 | 6. Disclaimer of Warranty 267 | 268 | Covered Software is provided under this License on an "as is" basis, 269 | without warranty of any kind, either expressed, implied, or statutory, 270 | including, without limitation, warranties that the Covered Software is free 271 | of defects, merchantable, fit for a particular purpose or non-infringing. 272 | The entire risk as to the quality and performance of the Covered Software 273 | is with You. Should any Covered Software prove defective in any respect, 274 | You (not any Contributor) assume the cost of any necessary servicing, 275 | repair, or correction. This disclaimer of warranty constitutes an essential 276 | part of this License. No use of any Covered Software is authorized under 277 | this License except under this disclaimer. 278 | 279 | 7. Limitation of Liability 280 | 281 | Under no circumstances and under no legal theory, whether tort (including 282 | negligence), contract, or otherwise, shall any Contributor, or anyone who 283 | distributes Covered Software as permitted above, be liable to You for any 284 | direct, indirect, special, incidental, or consequential damages of any 285 | character including, without limitation, damages for lost profits, loss of 286 | goodwill, work stoppage, computer failure or malfunction, or any and all 287 | other commercial damages or losses, even if such party shall have been 288 | informed of the possibility of such damages. This limitation of liability 289 | shall not apply to liability for death or personal injury resulting from 290 | such party's negligence to the extent applicable law prohibits such 291 | limitation. Some jurisdictions do not allow the exclusion or limitation of 292 | incidental or consequential damages, so this exclusion and limitation may 293 | not apply to You. 294 | 295 | 8. Litigation 296 | 297 | Any litigation relating to this License may be brought only in the courts 298 | of a jurisdiction where the defendant maintains its principal place of 299 | business and such litigation shall be governed by laws of that 300 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 301 | in this Section shall prevent a party's ability to bring cross-claims or 302 | counter-claims. 303 | 304 | 9. Miscellaneous 305 | 306 | This License represents the complete agreement concerning the subject 307 | matter hereof. If any provision of this License is held to be 308 | unenforceable, such provision shall be reformed only to the extent 309 | necessary to make it enforceable. Any law or regulation which provides that 310 | the language of a contract shall be construed against the drafter shall not 311 | be used to construe this License against a Contributor. 312 | 313 | 314 | 10. Versions of the License 315 | 316 | 10.1. New Versions 317 | 318 | Mozilla Foundation is the license steward. Except as provided in Section 319 | 10.3, no one other than the license steward has the right to modify or 320 | publish new versions of this License. Each version will be given a 321 | distinguishing version number. 322 | 323 | 10.2. Effect of New Versions 324 | 325 | You may distribute the Covered Software under the terms of the version 326 | of the License under which You originally received the Covered Software, 327 | or under the terms of any subsequent version published by the license 328 | steward. 329 | 330 | 10.3. Modified Versions 331 | 332 | If you create software not governed by this License, and you want to 333 | create a new license for such software, you may create and use a 334 | modified version of this License if you rename the license and remove 335 | any references to the name of the license steward (except to note that 336 | such modified license differs from this License). 337 | 338 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 339 | Licenses If You choose to distribute Source Code Form that is 340 | Incompatible With Secondary Licenses under the terms of this version of 341 | the License, the notice described in Exhibit B of this License must be 342 | attached. 343 | 344 | Exhibit A - Source Code Form License Notice 345 | 346 | This Source Code Form is subject to the 347 | terms of the Mozilla Public License, v. 348 | 2.0. If a copy of the MPL was not 349 | distributed with this file, You can 350 | obtain one at 351 | http://mozilla.org/MPL/2.0/. 352 | 353 | If it is not possible or desirable to put the notice in a particular file, 354 | then You may include the notice in a location (such as a LICENSE file in a 355 | relevant directory) where a recipient would be likely to look for such a 356 | notice. 357 | 358 | You may add additional accurate notices of copyright ownership. 359 | 360 | Exhibit B - "Incompatible With Secondary Licenses" Notice 361 | 362 | This Source Code Form is "Incompatible 363 | With Secondary Licenses", as defined by 364 | the Mozilla Public License, v. 2.0. 365 | 366 | -------------------------------------------------------------------------------- /META.d/_summary.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema: 1.1 5 | 6 | partition: tfc 7 | category: library 8 | 9 | summary: 10 | owner: team-tf-compliance 11 | description: 12 | This repository contains the Sentinel plugin SDK. This SDK allows developers 13 | to extend Sentinel to source external information for use in their policies. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOTOOLS = \ 2 | github.com/golang/protobuf/protoc-gen-go@latest \ 3 | github.com/vektra/mockery/v2@latest \ 4 | gotest.tools/gotestsum@latest 5 | 6 | SENTINEL_VERSION = 0.19.0 7 | SENTINEL_BIN_PATH := $(shell go env GOPATH)/bin 8 | 9 | test: tools 10 | gotestsum --format=short-verbose $(TEST) $(TESTARGS) 11 | 12 | generate: tools 13 | go generate ./... 14 | 15 | modules: 16 | go mod download && go mod verify 17 | 18 | test-ci: 19 | mkdir -p test-results/sentinel-sdk 20 | gotestsum --format=short-verbose --junitfile test-results/sentinel-sdk/results.xml 21 | 22 | tools: 23 | @echo $(GOTOOLS) | xargs -t -n1 go install 24 | go mod tidy 25 | 26 | .PHONY: test generate modules test-ci tools 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentinel Plugin SDK 2 | 3 | ![Tests](https://github.com/hashicorp/sentinel-sdk/actions/workflows/test.yml/badge.svg) 4 | [![GoDoc](https://godoc.org/github.com/hashicorp/sentinel-sdk?status.svg)](https://godoc.org/github.com/hashicorp/sentinel-sdk) 5 | [![Heimdall](https://heimdall.hashicorp.services/api/v1/assets/sentinel-sdk/badge.svg?key=8a99f5a22605231081b7fb8be0453015916fb79441a73af371dc625373e4a919)](https://heimdall.hashicorp.services/site/assets/sentinel-sdk) 6 | 7 | This repository contains the [Sentinel](https://www.hashicorp.com/sentinel) 8 | plugin SDK. This SDK allows developers to extend Sentinel to source external 9 | information for use in their policies. 10 | 11 | Sentinel plugins can be written in any language, but the recommended language is 12 | [Go](https://golang.org/). We provide a high-level framework to make writing 13 | plugins in Go extremely easy. For other languages, plugins can be written by 14 | implementing the 15 | [protocol](https://github.com/hashicorp/sentinel-sdk/blob/main/proto/plugin.proto) 16 | over gRPC. 17 | 18 | To get started writing a Sentinel plugin, we recommend reading the [extending 19 | Sentinel](https://docs.hashicorp.com/sentinel/extending/) guide. 20 | 21 | You can also view the plugin API via 22 | [GoDoc](https://godoc.org/github.com/hashicorp/sentinel-sdk). 23 | 24 | ## SDK Compatibility Matrix 25 | 26 | Sentinel's plugin protocol is, at this time, _not_ backwards compatible. This 27 | means that a specific version of the Sentinel runtime is always coupled to a 28 | specific version of the plugin protocol, and SDK. The following table can help 29 | you determine which version of the SDK is necessary to work with which versions 30 | of Sentinel. 31 | 32 | Sentinel Version|Plugin Protocol Version|SDK Version 33 | -|-|- 34 | **Up to v0.10.4**|**1**|**Up to v0.1.1** 35 | Up to v0.18.13|2|Up to v0.3.13 36 | From v0.19.0|3|Since v0.4.0 37 | 38 | ## Development Info 39 | 40 | The following tools are required to work with the Sentinel SDK: 41 | 42 | * [The Sentinel runtime](https://docs.hashicorp.com/sentinel/downloads), usually 43 | at the most recent version. There are rare exceptions to this, such as when 44 | the protocol is in active development. Refer to the [SDK Compatibility 45 | Matrix](#sdk-compatibility-matrix) to locate the correct version of the SDK to 46 | work with the most current version of the runtime. 47 | * [Google's Protocol 48 | Buffers](https://developers.google.com/protocol-buffers/docs/downloads). 49 | 50 | After both of these are installed, you can use the following `make` commands: 51 | 52 | * `make test` will run tests on the SDK. You can use the `TEST` and `TESTARGS` 53 | variables to control the packages and test arguments, respectively. 54 | * `make tools` will install any necessary Go tools. 55 | * `make generate` will generate any auto-generated code. Currently this includes 56 | the protocol, mockery files, and the code for the plugin testing toolkit. 57 | 58 | The `modules`, `test-circle`, and `/usr/bin/sentinel` targets are only used in 59 | Circle and are not necessary for interactive development. 60 | 61 | ## Help and Discussion 62 | 63 | For issues specific to the SDK, please use the GitHub issue tracker (the 64 | [Issues](https://github.com/hashicorp/sentinel-sdk/issues) tab). 65 | 66 | For general Sentinel support and discussion, please use the [Sentinel Community 67 | Forum](https://discuss.hashicorp.com/c/sentinel). 68 | -------------------------------------------------------------------------------- /encoding/encoding_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package encoding 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | sdk "github.com/hashicorp/sentinel-sdk" 11 | ) 12 | 13 | func TestEncoding(t *testing.T) { 14 | for _, tc := range encodingTests { 15 | t.Run(tc.Name, func(t *testing.T) { 16 | // Go => Value 17 | value, err := GoToValue(tc.Source) 18 | if err != nil { 19 | t.Fatalf("GoToValue err: %s", err) 20 | } 21 | 22 | // Determine the expected type and value. This is just the 23 | // value set to tc.Expected unless we wrap it in targetType. We 24 | // use that wrapper to signal that we want to set the typ to nil 25 | // to force automatic type inference and test that. 26 | typ := reflect.ValueOf(tc.Expected).Type() 27 | expected := tc.Expected 28 | if n, ok := expected.(targetType); ok { 29 | expected = n.Expected 30 | typ = n.Type 31 | } 32 | 33 | // Value => Go 34 | actual, err := ValueToGo(value, typ) 35 | if err != nil { 36 | if tc.Err { 37 | return 38 | } 39 | 40 | t.Fatalf("ValueToGo: %s", err) 41 | } 42 | 43 | // It should be what we expect 44 | if !reflect.DeepEqual(actual, expected) { 45 | t.Fatalf("bad: %#v", actual) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | // targetType is a wrapper struct that can be put around Expected values 52 | // in the table below to signal that the test should run with the "typ" 53 | // parameter set to nil. This will force the Go conversion to enforce the type. 54 | type targetType struct { 55 | Type reflect.Type // can be nil to automatically infer 56 | Expected interface{} 57 | } 58 | 59 | // encodingTests are the test cases for all encodings 60 | var encodingTests = []struct { 61 | Name string 62 | Source interface{} 63 | Expected interface{} 64 | Err bool 65 | }{ 66 | //----------------------------------------------------------- 67 | // Map 68 | 69 | { 70 | "map to matching map type", 71 | map[string]interface{}{ 72 | "foo": 42, 73 | "bar": 21, 74 | }, 75 | map[string]int8{ 76 | "foo": 42, 77 | "bar": 21, 78 | }, 79 | false, 80 | }, 81 | 82 | { 83 | "map to interface type", 84 | map[string]interface{}{ 85 | "foo": 42, 86 | "bar": 21, 87 | }, 88 | map[string]interface{}{ 89 | "foo": int64(42), 90 | "bar": int64(21), 91 | }, 92 | false, 93 | }, 94 | 95 | { 96 | "map with nil type", 97 | map[string]interface{}{ 98 | "foo": 42, 99 | "bar": 21, 100 | }, 101 | targetType{Expected: map[string]int64{ 102 | "foo": 42, 103 | "bar": 21, 104 | }}, 105 | false, 106 | }, 107 | 108 | { 109 | "map with interface type", 110 | map[string]interface{}{ 111 | "foo": 42, 112 | "bar": 21, 113 | }, 114 | targetType{ 115 | Type: interfaceTyp, 116 | Expected: map[string]int64{ 117 | "foo": 42, 118 | "bar": 21, 119 | }, 120 | }, 121 | false, 122 | }, 123 | 124 | { 125 | "map with null value", 126 | map[string]interface{}{ 127 | "foo": nil, 128 | }, 129 | map[string]interface{}{ 130 | "foo": sdk.Null, 131 | }, 132 | false, 133 | }, 134 | 135 | { 136 | "map with empty map", 137 | map[string]interface{}{ 138 | "foo": map[interface{}]interface{}{}, 139 | }, 140 | map[string]interface{}{ 141 | "foo": map[string]interface{}{}, 142 | }, 143 | false, 144 | }, 145 | 146 | //----------------------------------------------------------- 147 | // Slice 148 | 149 | { 150 | "slice to matching slice type", 151 | []int32{1, 2, 3, 4}, 152 | []int{1, 2, 3, 4}, 153 | false, 154 | }, 155 | 156 | { 157 | "slice to interface{} slice type", 158 | []interface{}{1, "foo"}, 159 | []interface{}{int64(1), "foo"}, 160 | false, 161 | }, 162 | 163 | { 164 | "slice to interface{} type", 165 | []interface{}{1, "foo"}, 166 | []interface{}{int64(1), "foo"}, 167 | false, 168 | }, 169 | 170 | { 171 | "slice to nil type", 172 | []int32{1, 2, 3, 4}, 173 | targetType{Expected: []int64{1, 2, 3, 4}}, 174 | false, 175 | }, 176 | 177 | { 178 | "slice with interface type", 179 | []int32{1, 2, 3, 4}, 180 | targetType{ 181 | Type: interfaceTyp, 182 | Expected: []int64{1, 2, 3, 4}, 183 | }, 184 | false, 185 | }, 186 | 187 | { 188 | "slice to incompatible slice type", 189 | []int32{1, 2, 3, 4}, 190 | []bool{}, 191 | true, 192 | }, 193 | 194 | { 195 | "slice with null value", 196 | []interface{}{nil}, 197 | []interface{}{sdk.Null}, 198 | false, 199 | }, 200 | 201 | //----------------------------------------------------------- 202 | // Bool 203 | 204 | { 205 | "bool to int", 206 | true, 207 | int(0), 208 | true, 209 | }, 210 | 211 | { 212 | "bool to uint", 213 | true, 214 | uint(0), 215 | true, 216 | }, 217 | 218 | { 219 | "bool to string", 220 | true, 221 | `42`, 222 | true, 223 | }, 224 | 225 | { 226 | "bool to bool", 227 | true, 228 | true, 229 | false, 230 | }, 231 | 232 | //----------------------------------------------------------- 233 | // Int 234 | 235 | { 236 | "int to int", 237 | 42, 238 | int(42), 239 | false, 240 | }, 241 | 242 | { 243 | "int to int8", 244 | 42, 245 | int8(42), 246 | false, 247 | }, 248 | 249 | { 250 | "int to int16", 251 | 42, 252 | int16(42), 253 | false, 254 | }, 255 | 256 | { 257 | "int to int32", 258 | 42, 259 | int32(42), 260 | false, 261 | }, 262 | 263 | { 264 | "int to int64", 265 | 42, 266 | int64(42), 267 | false, 268 | }, 269 | 270 | { 271 | "int to uint", 272 | 42, 273 | uint(42), 274 | false, 275 | }, 276 | 277 | { 278 | "int to uint8", 279 | 42, 280 | uint8(42), 281 | false, 282 | }, 283 | 284 | { 285 | "int to uint16", 286 | 42, 287 | uint16(42), 288 | false, 289 | }, 290 | 291 | { 292 | "int to uint32", 293 | 42, 294 | uint32(42), 295 | false, 296 | }, 297 | 298 | { 299 | "int to uint64", 300 | 42, 301 | uint64(42), 302 | false, 303 | }, 304 | 305 | { 306 | "int to float32", 307 | 42, 308 | float32(42), 309 | false, 310 | }, 311 | 312 | { 313 | "int to float64", 314 | 42, 315 | float64(42), 316 | false, 317 | }, 318 | 319 | { 320 | "int to uint negative", 321 | -42, 322 | uint(0), 323 | true, 324 | }, 325 | 326 | { 327 | "int to string", 328 | 42, 329 | `42`, 330 | false, 331 | }, 332 | 333 | //----------------------------------------------------------- 334 | // String 335 | 336 | { 337 | "string to int", 338 | `42`, 339 | int(42), 340 | false, 341 | }, 342 | 343 | { 344 | "string to int8", 345 | `42`, 346 | int8(42), 347 | false, 348 | }, 349 | 350 | { 351 | "string to int16", 352 | `42`, 353 | int16(42), 354 | false, 355 | }, 356 | 357 | { 358 | "string to int32", 359 | `42`, 360 | int32(42), 361 | false, 362 | }, 363 | 364 | { 365 | "string to int64", 366 | `42`, 367 | int64(42), 368 | false, 369 | }, 370 | 371 | { 372 | "string to uint", 373 | `42`, 374 | uint(42), 375 | false, 376 | }, 377 | 378 | { 379 | "string to uint8", 380 | `42`, 381 | uint8(42), 382 | false, 383 | }, 384 | 385 | { 386 | "string to uint16", 387 | `42`, 388 | uint16(42), 389 | false, 390 | }, 391 | 392 | { 393 | "string to uint32", 394 | `42`, 395 | uint32(42), 396 | false, 397 | }, 398 | 399 | { 400 | "string to uint64", 401 | `42`, 402 | uint64(42), 403 | false, 404 | }, 405 | 406 | { 407 | "string to float32", 408 | `42`, 409 | float32(42), 410 | false, 411 | }, 412 | 413 | { 414 | "string to float64", 415 | `42`, 416 | float64(42), 417 | false, 418 | }, 419 | 420 | { 421 | "string to string", 422 | `42`, 423 | `42`, 424 | false, 425 | }, 426 | 427 | //----------------------------------------------------------- 428 | // Struct 429 | 430 | { 431 | "struct field exported, no tag", 432 | struct{ Foo int }{Foo: 42}, 433 | map[string]int8{ 434 | "foo": 42, 435 | }, 436 | false, 437 | }, 438 | 439 | { 440 | "struct field exported, tagged", 441 | struct { 442 | Foo int `sentinel:"foo_bar"` 443 | }{Foo: 42}, 444 | map[string]int8{ 445 | "foo_bar": 42, 446 | }, 447 | false, 448 | }, 449 | 450 | { 451 | "struct field exported, camel case", 452 | struct{ FooBarBaz int }{FooBarBaz: 42}, 453 | map[string]int8{ 454 | "foo_bar_baz": 42, 455 | }, 456 | false, 457 | }, 458 | 459 | { 460 | "struct field unexported", 461 | struct { 462 | foo int `sentinel:"foo"` 463 | }{foo: 42}, 464 | map[string]interface{}{}, 465 | false, 466 | }, 467 | 468 | //----------------------------------------------------------- 469 | // Null 470 | 471 | { 472 | "null to null", 473 | sdk.Null, 474 | sdk.Null, 475 | false, 476 | }, 477 | 478 | { 479 | "null to nil type", 480 | sdk.Null, 481 | targetType{Expected: sdk.Null}, 482 | false, 483 | }, 484 | 485 | //----------------------------------------------------------- 486 | // Undefined 487 | 488 | { 489 | "undefined to undefined", 490 | sdk.Undefined, 491 | sdk.Undefined, 492 | false, 493 | }, 494 | 495 | { 496 | "undefined to nil type", 497 | sdk.Undefined, 498 | targetType{Expected: sdk.Undefined}, 499 | false, 500 | }, 501 | } 502 | -------------------------------------------------------------------------------- /encoding/go_to_value.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package encoding 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | "unicode" 12 | 13 | sdk "github.com/hashicorp/sentinel-sdk" 14 | proto "github.com/hashicorp/sentinel-sdk/proto/go" 15 | ) 16 | 17 | // GoToValue converts the Go value to a protobuf Object. 18 | // 19 | // The Go value must contain only primitives, collections of primitives, 20 | // and structures. It must not contain any other type of value or an error 21 | // will be returned. 22 | // 23 | // The primitive types byte and rune are aliases to integer types (as 24 | // defined by the Go spec) and are treated as integers in conversion. 25 | func GoToValue(raw interface{}) (*proto.Value, error) { 26 | return toValue_reflect(reflect.ValueOf(raw)) 27 | } 28 | 29 | func toValue_reflect(v reflect.Value) (*proto.Value, error) { 30 | // Null pointer 31 | if !v.IsValid() { 32 | return &proto.Value{Type: proto.Value_NULL}, nil 33 | } 34 | 35 | // Decode depending on the type. We need to redo all of the primitives 36 | // above unfortunately since they may fall to this point if they're 37 | // wrapped in an interface type. 38 | switch v.Kind() { 39 | case reflect.Interface: 40 | return toValue_reflect(v.Elem()) 41 | 42 | case reflect.Ptr: 43 | switch v.Interface() { 44 | case sdk.Null: 45 | return &proto.Value{Type: proto.Value_NULL}, nil 46 | 47 | case sdk.Undefined: 48 | return &proto.Value{Type: proto.Value_UNDEFINED}, nil 49 | } 50 | 51 | return toValue_reflect(v.Elem()) 52 | 53 | case reflect.Bool: 54 | return &proto.Value{ 55 | Type: proto.Value_BOOL, 56 | Value: &proto.Value_ValueBool{ValueBool: v.Bool()}, 57 | }, nil 58 | 59 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 60 | return &proto.Value{ 61 | Type: proto.Value_INT, 62 | Value: &proto.Value_ValueInt{ValueInt: v.Int()}, 63 | }, nil 64 | 65 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 66 | return &proto.Value{ 67 | Type: proto.Value_INT, 68 | Value: &proto.Value_ValueInt{ValueInt: int64(v.Uint())}, 69 | }, nil 70 | 71 | case reflect.Float32, reflect.Float64: 72 | return &proto.Value{ 73 | Type: proto.Value_FLOAT, 74 | Value: &proto.Value_ValueFloat{ValueFloat: v.Float()}, 75 | }, nil 76 | 77 | case reflect.Complex64, reflect.Complex128: 78 | return nil, errors.New("cannot convert complex number to Sentinel value") 79 | 80 | case reflect.String: 81 | return &proto.Value{ 82 | Type: proto.Value_STRING, 83 | Value: &proto.Value_ValueString{ValueString: v.String()}, 84 | }, nil 85 | 86 | case reflect.Array, reflect.Slice: 87 | return toValue_array(v) 88 | 89 | case reflect.Map: 90 | return toValue_map(v) 91 | 92 | case reflect.Struct: 93 | return toValue_struct(v) 94 | 95 | case reflect.Chan: 96 | return nil, errors.New("cannot convert channel to Sentinel value") 97 | 98 | case reflect.Func: 99 | return nil, errors.New("cannot convert func to Sentinel value") 100 | } 101 | 102 | return nil, fmt.Errorf("cannot convert type %s to Sentinel value", v.Kind()) 103 | } 104 | 105 | func toValue_array(v reflect.Value) (*proto.Value, error) { 106 | vs := make([]*proto.Value, v.Len()) 107 | for i := range vs { 108 | elem, err := toValue_reflect(v.Index(i)) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | vs[i] = elem 114 | } 115 | 116 | return &proto.Value{ 117 | Type: proto.Value_LIST, 118 | Value: &proto.Value_ValueList{ 119 | ValueList: &proto.Value_List{ 120 | Elems: vs, 121 | }, 122 | }, 123 | }, nil 124 | } 125 | 126 | func toValue_map(v reflect.Value) (*proto.Value, error) { 127 | vs := make([]*proto.Value_KV, v.Len()) 128 | for i, keyV := range v.MapKeys() { 129 | key, err := toValue_reflect(keyV) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | value, err := toValue_reflect(v.MapIndex(keyV)) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | vs[i] = &proto.Value_KV{ 140 | Key: key, 141 | Value: value, 142 | } 143 | } 144 | 145 | return &proto.Value{ 146 | Type: proto.Value_MAP, 147 | Value: &proto.Value_ValueMap{ 148 | ValueMap: &proto.Value_Map{ 149 | Elems: vs, 150 | }, 151 | }, 152 | }, nil 153 | } 154 | 155 | func toValue_struct(v reflect.Value) (*proto.Value, error) { 156 | // Get the type since we need this to determine what is exported, 157 | // field tags, etc. 158 | t := v.Type() 159 | 160 | vs := make([]*proto.Value_KV, 0, v.NumField()) 161 | for i := 0; i < v.NumField(); i++ { 162 | field := t.Field(i) 163 | 164 | // If PkgPath is non-empty, this is unexported and can be ignored 165 | if field.PkgPath != "" { 166 | continue 167 | } 168 | 169 | // Determine the map key 170 | key := toValue_struct_fieldName([]rune(field.Name)) 171 | if v, ok := field.Tag.Lookup("sentinel"); ok { 172 | // A blank value means to not export this value 173 | if v == "" { 174 | continue 175 | } 176 | 177 | key = v 178 | } 179 | 180 | // Convert the value 181 | value, err := toValue_reflect(v.Field(i)) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | vs = append(vs, &proto.Value_KV{ 187 | Value: value, 188 | Key: &proto.Value{ 189 | Type: proto.Value_STRING, 190 | Value: &proto.Value_ValueString{ValueString: key}, 191 | }, 192 | }) 193 | } 194 | 195 | return &proto.Value{ 196 | Type: proto.Value_MAP, 197 | Value: &proto.Value_ValueMap{ 198 | ValueMap: &proto.Value_Map{ 199 | Elems: vs, 200 | }, 201 | }, 202 | }, nil 203 | } 204 | 205 | func toValue_struct_fieldName(s []rune) string { 206 | var result []string 207 | var last int 208 | 209 | // Always convert the zero-index rune to a lowercase letter. Since we always 210 | // operate on exported struct fields, this is fine and is actually less 211 | // costly than doing an IsUpper first. 212 | s[0] = unicode.ToLower(s[0]) 213 | 214 | for idx := 1; idx < len(s); idx++ { 215 | if unicode.IsUpper(s[idx]) { 216 | result = append(result, string(s[last:idx])) 217 | last = idx 218 | s[idx] = unicode.ToLower(s[idx]) 219 | } 220 | } 221 | 222 | // Append anything remaining 223 | result = append(result, string(s[last:])) 224 | 225 | return strings.Join(result, "_") 226 | } 227 | -------------------------------------------------------------------------------- /encoding/value_to_go.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package encoding 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "strconv" 10 | 11 | sdk "github.com/hashicorp/sentinel-sdk" 12 | proto "github.com/hashicorp/sentinel-sdk/proto/go" 13 | ) 14 | 15 | var ( 16 | interfaceTyp = reflect.TypeOf((*interface{})(nil)).Elem() 17 | boolTyp = reflect.TypeOf(true) 18 | intTyp = reflect.TypeOf(int64(0)) 19 | floatTyp = reflect.TypeOf(float64(0)) 20 | stringTyp = reflect.TypeOf("") 21 | ) 22 | 23 | // ValueToGo converts a protobuf Value structure to a native Go value. 24 | func ValueToGo(v *proto.Value, t reflect.Type) (interface{}, error) { 25 | return valueToGo(v, t) 26 | } 27 | 28 | func valueToGo(v *proto.Value, t reflect.Type) (interface{}, error) { 29 | // t == nil if you call reflect.TypeOf(interface{}{}) or 30 | // if the user explicitly send in nil which we make to mean 31 | // the same thing. 32 | kind := reflect.Interface 33 | if t != nil { 34 | kind = t.Kind() 35 | } 36 | if kind == reflect.Interface { 37 | switch v.Type { 38 | case proto.Value_BOOL: 39 | kind = reflect.Bool 40 | 41 | case proto.Value_INT: 42 | kind = reflect.Int64 43 | 44 | case proto.Value_FLOAT: 45 | kind = reflect.Float64 46 | 47 | case proto.Value_STRING: 48 | kind = reflect.String 49 | 50 | case proto.Value_MAP: 51 | kind = reflect.Map 52 | 53 | case proto.Value_LIST: 54 | kind = reflect.Slice 55 | 56 | case proto.Value_NULL: 57 | return sdk.Null, nil 58 | 59 | case proto.Value_UNDEFINED: 60 | return sdk.Undefined, nil 61 | 62 | default: 63 | return nil, convertErr(v, "interface{}") 64 | } 65 | } 66 | 67 | // If the type is nil, we set a default based on the kind 68 | if t == nil || t.Kind() == reflect.Interface { 69 | switch kind { 70 | case reflect.Bool: 71 | t = boolTyp 72 | 73 | case reflect.Int64: 74 | t = intTyp 75 | 76 | case reflect.Float64: 77 | t = floatTyp 78 | 79 | case reflect.String: 80 | t = stringTyp 81 | 82 | case reflect.Map: 83 | t = valueMapType(v) 84 | 85 | case reflect.Slice: 86 | t = valueSliceType(v) 87 | 88 | default: 89 | return nil, convertErr(v, "nil type") 90 | } 91 | } 92 | 93 | switch kind { 94 | case reflect.Bool: 95 | return convertValueBool(v) 96 | 97 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 98 | v, err := convertValueInt64(v) 99 | if err != nil { 100 | return v, err 101 | } 102 | 103 | // This is pretty expensive but makes the implementation easy. 104 | // The performance is likely to be overshadowed by the RPC cost 105 | // and function cost itself. 106 | return reflect.ValueOf(v).Convert(t).Interface(), nil 107 | 108 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 109 | v, err := convertValueUint64(v) 110 | if err != nil { 111 | return v, err 112 | } 113 | 114 | return reflect.ValueOf(v).Convert(t).Interface(), nil 115 | 116 | case reflect.Float32: 117 | v, err := convertValueFloat(v, 32) 118 | if err != nil { 119 | return v, err 120 | } 121 | 122 | return float32(v.(float64)), nil 123 | 124 | case reflect.Float64: 125 | return convertValueFloat(v, 64) 126 | 127 | case reflect.String: 128 | return convertValueString(v) 129 | 130 | case reflect.Slice: 131 | return convertValueSlice(v, t) 132 | 133 | case reflect.Map: 134 | return convertValueMap(v, t) 135 | 136 | case reflect.Ptr: 137 | switch v.Type { 138 | case proto.Value_NULL: 139 | return sdk.Null, nil 140 | 141 | case proto.Value_UNDEFINED: 142 | return sdk.Undefined, nil 143 | } 144 | 145 | fallthrough 146 | 147 | default: 148 | return nil, convertErr(v, t.Kind().String()) 149 | } 150 | } 151 | 152 | func convertValueBool(raw *proto.Value) (interface{}, error) { 153 | if raw.Type == proto.Value_BOOL { 154 | return raw.Value.(*proto.Value_ValueBool).ValueBool, nil 155 | } 156 | 157 | return nil, convertErr(raw, "bool") 158 | } 159 | 160 | func convertValueInt64(raw *proto.Value) (interface{}, error) { 161 | switch raw.Type { 162 | case proto.Value_INT: 163 | return raw.Value.(*proto.Value_ValueInt).ValueInt, nil 164 | 165 | case proto.Value_STRING: 166 | return strconv.ParseInt(raw.Value.(*proto.Value_ValueString).ValueString, 0, 64) 167 | 168 | default: 169 | return nil, convertErr(raw, "int") 170 | } 171 | } 172 | 173 | func convertValueUint64(raw *proto.Value) (interface{}, error) { 174 | switch raw.Type { 175 | case proto.Value_INT: 176 | value := raw.Value.(*proto.Value_ValueInt).ValueInt 177 | if value < 0 { 178 | return nil, fmt.Errorf( 179 | "expected unsigned value, got negative integer") 180 | } 181 | 182 | return uint64(value), nil 183 | 184 | case proto.Value_STRING: 185 | return strconv.ParseUint(raw.Value.(*proto.Value_ValueString).ValueString, 0, 64) 186 | 187 | default: 188 | return nil, convertErr(raw, "uint") 189 | } 190 | } 191 | 192 | func convertValueFloat(raw *proto.Value, bitSize int) (interface{}, error) { 193 | switch raw.Type { 194 | case proto.Value_INT: 195 | return float64(raw.Value.(*proto.Value_ValueInt).ValueInt), nil 196 | 197 | case proto.Value_FLOAT: 198 | return raw.Value.(*proto.Value_ValueFloat).ValueFloat, nil 199 | 200 | case proto.Value_STRING: 201 | return strconv.ParseFloat(raw.Value.(*proto.Value_ValueString).ValueString, bitSize) 202 | 203 | default: 204 | return nil, convertErr(raw, "float") 205 | } 206 | } 207 | 208 | func convertValueString(raw *proto.Value) (interface{}, error) { 209 | switch raw.Type { 210 | case proto.Value_INT: 211 | return strconv.FormatInt(raw.Value.(*proto.Value_ValueInt).ValueInt, 10), nil 212 | 213 | case proto.Value_STRING: 214 | return raw.Value.(*proto.Value_ValueString).ValueString, nil 215 | 216 | default: 217 | return nil, convertErr(raw, "string") 218 | } 219 | } 220 | 221 | func convertValueSlice(raw *proto.Value, t reflect.Type) (interface{}, error) { 222 | if raw.Type != proto.Value_LIST { 223 | return nil, convertErr(raw, "list") 224 | } 225 | 226 | list := raw.Value.(*proto.Value_ValueList).ValueList 227 | elemTyp := t.Elem() 228 | sliceVal := reflect.MakeSlice(t, len(list.Elems), len(list.Elems)) 229 | for i, elt := range list.Elems { 230 | v, err := valueToGo(elt, elemTyp) 231 | if err != nil { 232 | return nil, fmt.Errorf("element %d: %s", i, err) 233 | } 234 | 235 | sliceVal.Index(i).Set(reflect.ValueOf(v)) 236 | } 237 | 238 | return sliceVal.Interface(), nil 239 | } 240 | 241 | func convertValueMap(raw *proto.Value, t reflect.Type) (interface{}, error) { 242 | if raw.Type != proto.Value_MAP { 243 | return nil, convertErr(raw, "map") 244 | } 245 | 246 | if t.Kind() != reflect.Map { 247 | return nil, fmt.Errorf("target type is not map, is: %s", t.Kind()) 248 | } 249 | 250 | m := raw.Value.(*proto.Value_ValueMap).ValueMap 251 | keyTyp := t.Key() 252 | elemTyp := t.Elem() 253 | if len(m.Elems) == 0 { 254 | // as we have no elements, it is much safer to presume a key type of 255 | // string to ensure safety when attempting to perform actions such 256 | // as json marshalling 257 | mapType := reflect.MapOf(reflect.TypeOf(""), elemTyp) 258 | return reflect.MakeMap(mapType).Interface(), nil 259 | } 260 | 261 | mapVal := reflect.MakeMap(t) 262 | for _, elt := range m.Elems { 263 | // Convert the key 264 | key, err := valueToGo(elt.Key, keyTyp) 265 | if err != nil { 266 | return nil, fmt.Errorf("key %s: %s", elt.Key.String(), err) 267 | } 268 | 269 | // Convert the value 270 | elem, err := valueToGo(elt.Value, elemTyp) 271 | if err != nil { 272 | return nil, fmt.Errorf("element for key %s: %s", elt.Key.String(), err) 273 | } 274 | 275 | // Set it 276 | mapVal.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(elem)) 277 | } 278 | 279 | return mapVal.Interface(), nil 280 | } 281 | 282 | // valueMapType creates a map type to match the keys/values in the value. 283 | func valueMapType(raw *proto.Value) reflect.Type { 284 | m := raw.Value.(*proto.Value_ValueMap).ValueMap 285 | var keys []*proto.Value 286 | var values []*proto.Value 287 | for _, elt := range m.Elems { 288 | keys = append(keys, elt.Key) 289 | values = append(values, elt.Value) 290 | } 291 | 292 | return reflect.MapOf(elemType(keys), elemType(values)) 293 | } 294 | 295 | // valueSliceType creates a slice type to match the keys/values in the value. 296 | func valueSliceType(raw *proto.Value) reflect.Type { 297 | list := raw.Value.(*proto.Value_ValueList).ValueList 298 | return reflect.SliceOf(elemType(list.Elems)) 299 | } 300 | 301 | // elemTyp determines the least common type for a set of values, defaulting 302 | // to interface{} as the most generic type. 303 | func elemType(vs []*proto.Value) reflect.Type { 304 | current := proto.Value_INVALID 305 | for _, v := range vs { 306 | // If we haven't set a type yet, set it to this one 307 | if current == proto.Value_INVALID { 308 | current = v.Type 309 | } 310 | 311 | // If the types don't match, we have an interface type 312 | if current != v.Type { 313 | return interfaceTyp 314 | } 315 | } 316 | 317 | // We found a matching type, return the type based on the proto type 318 | switch current { 319 | case proto.Value_BOOL: 320 | return boolTyp 321 | 322 | case proto.Value_INT: 323 | return intTyp 324 | 325 | case proto.Value_FLOAT: 326 | return floatTyp 327 | 328 | case proto.Value_STRING: 329 | return stringTyp 330 | 331 | default: 332 | return interfaceTyp 333 | } 334 | } 335 | 336 | func convertErr(raw *proto.Value, t string) error { 337 | return fmt.Errorf("cannot convert to %s: %s", t, raw.Type) 338 | } 339 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1726560853, 9 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 0, 24 | "narHash": "sha256-WLxED18lodtQiayIPDE5zwAfkPJSjHJ35UhZ8h3cJUg=", 25 | "path": "/nix/store/wdk3xa0vwx7swjdl1samf1bccvyyzfc1-source", 26 | "type": "path" 27 | }, 28 | "original": { 29 | "id": "nixpkgs", 30 | "type": "indirect" 31 | } 32 | }, 33 | "nixpkgs-unstable": { 34 | "locked": { 35 | "lastModified": 1729081001, 36 | "narHash": "sha256-8SOUratytZwZoH4pbXg560jOwqjykp8gB5w44YS16TU=", 37 | "owner": "nixos", 38 | "repo": "nixpkgs", 39 | "rev": "66a52d212ed8790db788d0c47c9d77f8143d55ed", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "nixos", 44 | "ref": "master", 45 | "repo": "nixpkgs", 46 | "type": "github" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-utils": "flake-utils", 52 | "nixpkgs": "nixpkgs", 53 | "nixpkgs-unstable": "nixpkgs-unstable" 54 | } 55 | }, 56 | "systems": { 57 | "locked": { 58 | "lastModified": 1681028828, 59 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 60 | "owner": "nix-systems", 61 | "repo": "default", 62 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "owner": "nix-systems", 67 | "repo": "default", 68 | "type": "github" 69 | } 70 | } 71 | }, 72 | "root": "root", 73 | "version": 7 74 | } 75 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "nixpkgs"; 4 | nixpkgs-unstable.url = "github:nixos/nixpkgs/master"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: 9 | let 10 | localOverlay = import ./nix/overlay.nix; 11 | overlayUnstable = final: prev: { 12 | # allow the ability to add an unstable package as required. 13 | unstable = nixpkgs-unstable.legacyPackages.${prev.system}; 14 | }; 15 | overlays = [ overlayUnstable localOverlay ]; 16 | in flake-utils.lib.eachDefaultSystem (system: 17 | let 18 | pkgs = import nixpkgs { 19 | inherit system overlays; 20 | }; 21 | in { 22 | legacyPackages = pkgs; 23 | inherit (pkgs) devShell; 24 | }) // { 25 | # platform independent attrs 26 | overlay = final: prev: (nixpkgs.lib.composeManyExtensions overlays) final prev; 27 | inherit overlays; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /framework/framework.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package framework contains a high-level framework for implementing 5 | // Sentinel plugins with Go. 6 | // 7 | // The direct sdk.Plugin interface is a low-level interface that is 8 | // tediuos, clunky, and difficult to implement correctly. The 9 | // interface is this way to assist in the performance of plugins 10 | // while executing Sentinel policies. This package provides a 11 | // high-level API that eases plugin implementation while still 12 | // supporting the performance-sensitive interface underneath. 13 | // 14 | // Plugins are generally activated in this framework by serving the 15 | // plugin with the root namespace embedded in Plugin: 16 | // 17 | // package main 18 | // 19 | // import ( 20 | // "github.com/hashicorp/sentinel-sdk" 21 | // "github.com/hashicorp/sentinel-sdk/rpc" 22 | // ) 23 | // 24 | // func main() { 25 | // rpc.Serve(&rpc.ServeOpts{ 26 | // PluginFunc: func() sdk.Plugin { 27 | // return &framework.Plugin{Root: &root{}} 28 | // }, 29 | // }) 30 | // } 31 | // 32 | // The plugin framework is based around the concept of namespaces. 33 | // Root is the entrypoint namespace and must be implemented as a 34 | // minimum. From there, nested access may be delegated to other 35 | // Namespace implementations. 36 | // 37 | // Namespaces outside of the root must at least implement the 38 | // Namespace interface. All namespaces, including the root, may 39 | // implement the optional Call or Map interfaces, to support function 40 | // calls or selective memoization calls, respectively. 41 | // 42 | // Root namespaces are generally global, that is, for the lifetime of 43 | // the execution of Sentinel, one single plugin Root namespace state 44 | // will be shared by all policies that need to be executed. Take care 45 | // when storing state in the Root namespace. If you require state 46 | // in the Root namespace that must be unique across policy 47 | // executions, implement the NamespaceCreator interface. 48 | // 49 | // The Root namespace (or the NamespaceCreator interface, which 50 | // embeds Root) may optionally implement the New interface, which 51 | // allows for the construction of namespaces via the handling of 52 | // arbitrary object data. New is ignored for namespaces past the 53 | // root. 54 | // 55 | // Non-primitive plugin return data is normally memoized, including 56 | // for namespaces. This prevents expensive calls over the plugin RPC. 57 | // Memoization can be controlled by a couple of methods: 58 | // 59 | // * Implementing the Map interface allows for the explicit return of 60 | // a map of values, sidestepping struct memoization. Normally, this 61 | // is combined with the MapFromKeys function which will call Get for 62 | // each defined key and add the return values to the map. Note that 63 | // multi-key plugin calls always bypass memoization - so if foo.bar 64 | // is a namespace that implements Map but foo.bar.baz is looked up in 65 | // a single expression, it does not matter if baz is excluded from 66 | // Map. 67 | // 68 | // * Struct memoization is implicit otherwise. Only exported fields 69 | // are acted on - fields are lower and snake cased where applicable. 70 | // To control this behavior, you can use the "sentinel" struct tag. 71 | // sentinel:"NAME" will alter the field to have the name indicated by 72 | // NAME, while an empty string will exclude the field. 73 | // 74 | // Additionally, there are a couple of nuances that the plugin author 75 | // should be cognizant of: 76 | // 77 | // * nil values within slices, maps, and structs are converted to 78 | // nulls in the return object. 79 | // 80 | // * Returning a nil from a Get call is undefined, not null. 81 | // 82 | // The author can alter this behavior explicitly by assigning or 83 | // returning the sdk.Null and sdk.Undefined values. 84 | package framework 85 | -------------------------------------------------------------------------------- /framework/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package framework 5 | 6 | //go:generate rm -f mock_*.go 7 | //go:generate mockery --inpackage --note "Generated code. DO NOT MODIFY." --name=Root --testonly 8 | //go:generate mockery --inpackage --note "Generated code. DO NOT MODIFY." --name=Namespace --testonly 9 | //go:generate mockery --inpackage --note "Generated code. DO NOT MODIFY." --name=NamespaceCreator --testonly 10 | 11 | // Root is the plugin root. For any plugin, there is only a single root. 12 | // For example, if you're implementing a plugin named "time", then the "time" 13 | // identifier itself represents the plugin root. 14 | // 15 | // The root of a plugin is configurable and is able to return the actual 16 | // interfaces uses for value retrieval. The root itself can never contain 17 | // a value, be callable, return all mappings, etc. 18 | // 19 | // A single root implementation and instance may be shared by many policy 20 | // executions if their configurations match. 21 | type Root interface { 22 | // Configure is called to configure this plugin with the operator 23 | // supplied configuration for this plugin. 24 | Configure(map[string]interface{}) error 25 | 26 | // Root must further implement one of two interfaces: NamespaceCreator 27 | // or Namespace itself. See the documentation for each for when you'd 28 | // want to implement one or the other. If neither is implemented, 29 | // an error will be returned immediately upon configuration. 30 | } 31 | 32 | // NamespaceCreator is an interface only used in conjunction with the 33 | // Root interface. It allows the Root implementation to create a unique 34 | // Namespace implementation for each policy execution. 35 | // 36 | // This is useful for plugins that maintain state per policy execution. 37 | // For example for the "time" package, it may be useful for the state to 38 | // be the current time so that all access returns a singular view of time 39 | // for a policy execution. 40 | // 41 | // If your plugin doesn't require per-execution state, Root should 42 | // implement Namespace directly instead. 43 | type NamespaceCreator interface { 44 | Root 45 | 46 | // Namespace is called to return the root namespace for accessing keys. 47 | // 48 | // This will be called once for each policy execution. If data and access 49 | // is shared by all policy executions (such as static data), then you 50 | // can return a singleton value. 51 | // 52 | // If each policy execution should maintain its own state, then this 53 | // should return a new value. 54 | Namespace() Namespace 55 | } 56 | 57 | // New is an interface indicating that the namespace supports object 58 | // construction via the handling of arbitrary object data. New is 59 | // only supported on root namespaces, so either created through 60 | // Root or NamespaceCreator. 61 | // 62 | // The format of the object and the kinds of namespaces returned by 63 | // the constructor are up to the plugin author. 64 | type New interface { 65 | Namespace 66 | 67 | // New is called to construct new namespaces based on arbitrary 68 | // receiver data. 69 | // 70 | // The format of the object and the kinds of namespaces returned by 71 | // the constructor are up to the plugin author. 72 | // 73 | // Namespaces returned by this function must implement 74 | // framework.Map, or else errors will be returned on 75 | // post-processing of the receiver. 76 | // 77 | // New should return an error if there are issues instantiating the 78 | // namespace. This includes if the namespace cannot be determined 79 | // from the receiver data. Returning nil from this function will 80 | // return undefined to the caller. 81 | New(map[string]interface{}) (Namespace, error) 82 | } 83 | 84 | // Namespace represents a namespace of attributes that can be requested 85 | // by key. For example in "time.pst.hour, time.pst.minute", "time.pst" would 86 | // be a namespace. 87 | // 88 | // Namespaces are either represented or returned by the Root implementation. 89 | // Root is the top-level implementation for a plugin. See Plugin and Root 90 | // for more details. 91 | // 92 | // A Namespace on its own doesn't allow accessing the full mapping of 93 | // keys and values. Map may be optionally implemented to support this. 94 | // Following the example in the first paragraph of this documentation, 95 | // "time.pst" itself wouldn't be allowed for a Namespace on its own. If 96 | // the implementation also implements Map, then "time.pst" would return 97 | // a complete mapping. 98 | type Namespace interface { 99 | // Get requests the value for a specific key. This must return a value 100 | // convertable by lang/object.ToObject or another Interface value. 101 | // 102 | // If the value doesn't exist, nil should be returned. This will turn 103 | // into "undefined" eventually in the Sentinel policy. If you want to 104 | // return an explicit "null" value, please return object.Null directly. 105 | // 106 | // If an Interface implementation is returned, this is treated like 107 | // a namespace. For example, "time.pst" may return an Interface since 108 | // the value itself expects further keys such as ".hour". 109 | Get(string) (interface{}, error) 110 | } 111 | 112 | // Map is a Namespace that supports returning the entire map of data. 113 | // For example, if "time.pst" implemented this, then the writer of a policy 114 | // may request "time.pst" and get the entire value back as a map. 115 | type Map interface { 116 | Namespace 117 | 118 | // Map returns the entire map for this value. The return value 119 | // must only contain values convertable by lang/object.ToObject. It 120 | // cannot contain functions or other framework interface implementations. 121 | Map() (map[string]interface{}, error) 122 | } 123 | 124 | // List is a Namespace that supports returning a list of data. 125 | type List interface { 126 | Namespace 127 | 128 | List() ([]interface{}, error) 129 | } 130 | 131 | // Call is a Namespace that supports call expressions. For example, "time.now()" 132 | // would invoke the Func function for "now". 133 | type Call interface { 134 | Namespace 135 | 136 | // Func returns a function to call for the given string. The function 137 | // must take some number of arguments and return (interface{}, error). 138 | // The argument types may be Go types and the framework will handle 139 | // conversion and validation automatically. 140 | // 141 | // The returned function may also return only interface{}. In this case, 142 | // it is assumed an error scenario is impossible. Any other number of 143 | // return values will result in an error. 144 | // 145 | // This should return nil if the key doesn't support being called. 146 | Func(string) interface{} 147 | } 148 | -------------------------------------------------------------------------------- /framework/map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package framework 5 | 6 | // MapFromKeys creates a map[string]interface{} for a Namespace from the 7 | // given set of keys. This is a useful helper for implementing the Map 8 | // interface. 9 | func MapFromKeys(ns Namespace, keys []string) (map[string]interface{}, error) { 10 | result := make(map[string]interface{}) 11 | for _, k := range keys { 12 | var err error 13 | result[k], err = ns.Get(k) 14 | if err != nil { 15 | return nil, err 16 | } 17 | } 18 | 19 | return result, nil 20 | } 21 | -------------------------------------------------------------------------------- /framework/mock_NamespaceCreator_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.14.0. DO NOT EDIT. 2 | 3 | // Generated code. DO NOT MODIFY. 4 | 5 | package framework 6 | 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // MockNamespaceCreator is an autogenerated mock type for the NamespaceCreator type 10 | type MockNamespaceCreator struct { 11 | mock.Mock 12 | } 13 | 14 | // Configure provides a mock function with given fields: _a0 15 | func (_m *MockNamespaceCreator) Configure(_a0 map[string]interface{}) error { 16 | ret := _m.Called(_a0) 17 | 18 | var r0 error 19 | if rf, ok := ret.Get(0).(func(map[string]interface{}) error); ok { 20 | r0 = rf(_a0) 21 | } else { 22 | r0 = ret.Error(0) 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // Namespace provides a mock function with given fields: 29 | func (_m *MockNamespaceCreator) Namespace() Namespace { 30 | ret := _m.Called() 31 | 32 | var r0 Namespace 33 | if rf, ok := ret.Get(0).(func() Namespace); ok { 34 | r0 = rf() 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).(Namespace) 38 | } 39 | } 40 | 41 | return r0 42 | } 43 | 44 | type mockConstructorTestingTNewMockNamespaceCreator interface { 45 | mock.TestingT 46 | Cleanup(func()) 47 | } 48 | 49 | // NewMockNamespaceCreator creates a new instance of MockNamespaceCreator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 50 | func NewMockNamespaceCreator(t mockConstructorTestingTNewMockNamespaceCreator) *MockNamespaceCreator { 51 | mock := &MockNamespaceCreator{} 52 | mock.Mock.Test(t) 53 | 54 | t.Cleanup(func() { mock.AssertExpectations(t) }) 55 | 56 | return mock 57 | } 58 | -------------------------------------------------------------------------------- /framework/mock_Namespace_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.14.0. DO NOT EDIT. 2 | 3 | // Generated code. DO NOT MODIFY. 4 | 5 | package framework 6 | 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // MockNamespace is an autogenerated mock type for the Namespace type 10 | type MockNamespace struct { 11 | mock.Mock 12 | } 13 | 14 | // Get provides a mock function with given fields: _a0 15 | func (_m *MockNamespace) Get(_a0 string) (interface{}, error) { 16 | ret := _m.Called(_a0) 17 | 18 | var r0 interface{} 19 | if rf, ok := ret.Get(0).(func(string) interface{}); ok { 20 | r0 = rf(_a0) 21 | } else { 22 | if ret.Get(0) != nil { 23 | r0 = ret.Get(0).(interface{}) 24 | } 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func(string) error); ok { 29 | r1 = rf(_a0) 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | 37 | type mockConstructorTestingTNewMockNamespace interface { 38 | mock.TestingT 39 | Cleanup(func()) 40 | } 41 | 42 | // NewMockNamespace creates a new instance of MockNamespace. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 43 | func NewMockNamespace(t mockConstructorTestingTNewMockNamespace) *MockNamespace { 44 | mock := &MockNamespace{} 45 | mock.Mock.Test(t) 46 | 47 | t.Cleanup(func() { mock.AssertExpectations(t) }) 48 | 49 | return mock 50 | } 51 | -------------------------------------------------------------------------------- /framework/mock_Root_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.14.0. DO NOT EDIT. 2 | 3 | // Generated code. DO NOT MODIFY. 4 | 5 | package framework 6 | 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // MockRoot is an autogenerated mock type for the Root type 10 | type MockRoot struct { 11 | mock.Mock 12 | } 13 | 14 | // Configure provides a mock function with given fields: _a0 15 | func (_m *MockRoot) Configure(_a0 map[string]interface{}) error { 16 | ret := _m.Called(_a0) 17 | 18 | var r0 error 19 | if rf, ok := ret.Get(0).(func(map[string]interface{}) error); ok { 20 | r0 = rf(_a0) 21 | } else { 22 | r0 = ret.Error(0) 23 | } 24 | 25 | return r0 26 | } 27 | 28 | type mockConstructorTestingTNewMockRoot interface { 29 | mock.TestingT 30 | Cleanup(func()) 31 | } 32 | 33 | // NewMockRoot creates a new instance of MockRoot. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 34 | func NewMockRoot(t mockConstructorTestingTNewMockRoot) *MockRoot { 35 | mock := &MockRoot{} 36 | mock.Mock.Test(t) 37 | 38 | t.Cleanup(func() { mock.AssertExpectations(t) }) 39 | 40 | return mock 41 | } 42 | -------------------------------------------------------------------------------- /framework/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package framework 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | sdk "github.com/hashicorp/sentinel-sdk" 15 | "github.com/hashicorp/sentinel-sdk/encoding" 16 | ) 17 | 18 | var stringTyp = reflect.TypeOf("") 19 | 20 | // Plugin implements sdk.Plugin. Configure and return this structure 21 | // to simplify implementation of sdk.Plugin. 22 | type Plugin struct { 23 | // Root is the implementation of the plugin that the user of the 24 | // framework should implement. It represents the minimum necessary 25 | // implementation for a plugin. See the docs for Root for more details. 26 | Root Root 27 | 28 | // namespaceMap keeps track of all the Namespaces for the various 29 | // executions. These are cleaned up based on the ExecDeadline. 30 | namespaceMap map[uint64]Namespace 31 | namespaceLock sync.RWMutex 32 | } 33 | 34 | // plugin.Plugin impl. 35 | func (m *Plugin) Configure(raw map[string]interface{}) error { 36 | // Verify the root implementation is a Namespace or NamespaceCreator. 37 | switch m.Root.(type) { 38 | case Namespace: 39 | case NamespaceCreator: 40 | default: 41 | return fmt.Errorf("invalid plugin implementation, please report a " + 42 | "bug to the developer of this plugin") 43 | } 44 | 45 | // Configure the object itself 46 | return m.Root.Configure(raw) 47 | } 48 | 49 | // plugin.Plugin impl. 50 | func (m *Plugin) Get(reqs []*sdk.GetReq) ([]*sdk.GetResult, error) { 51 | resp := make([]*sdk.GetResult, len(reqs)) 52 | for i, req := range reqs { 53 | // Get the namespace 54 | ns := m.namespace(req) 55 | 56 | // If Context is supplied and the root supports New, handle it. 57 | // We use the value of constructorOk later on to determine if the 58 | // return value will be callable as well. 59 | constructor, constructorOk := ns.(New) 60 | if req.Context != nil { 61 | if constructorOk { 62 | var err error 63 | ns, err = constructor.New(req.Context) 64 | if err != nil { 65 | return nil, fmt.Errorf("error instantiating namespace: %s", err) 66 | } 67 | 68 | if ns == nil { 69 | // No namespace was returned. This is technically 70 | // undefined, but we need to check to make sure there were 71 | // no function calls first, or else this is an error, not 72 | // undefined. 73 | for i, k := range req.Keys { 74 | if k.Call() { 75 | return nil, fmt.Errorf( 76 | "attempting to call function %q on undefined receiver", 77 | strings.Join(req.GetKeys()[:i+1], ".")) 78 | } 79 | } 80 | 81 | // If this was just a get call, we can short-circuit the 82 | // result here, with undefined. 83 | resp[i] = &sdk.GetResult{ 84 | KeyId: req.KeyId, 85 | Keys: req.GetKeys(), 86 | Value: sdk.Undefined, 87 | } 88 | continue 89 | } 90 | } else { 91 | // Invalid implementation. This should not happen and is 92 | // indicative of something more than likely wrong with the 93 | // runtime. Nonetheless, this is not the plugin's problem 94 | // as the malformed data did not come from it. 95 | return nil, errors.New( 96 | "sdk.GetReq.Context present but plugin does not support framework.New") 97 | } 98 | } 99 | 100 | // For each key, perform a get 101 | var result interface{} = ns 102 | for i, k := range req.Keys { 103 | // If we have arguments at this level, perform a function call. 104 | if k.Call() { 105 | x, ok := result.(Call) 106 | if !ok { 107 | return nil, fmt.Errorf( 108 | "key %q doesn't support function calls", 109 | strings.Join(req.GetKeys()[:i+1], ".")) 110 | } 111 | 112 | v, err := m.call(x.Func(k.Key), k.Args) 113 | if err != nil { 114 | return nil, fmt.Errorf( 115 | "error calling function %q: %s", k.Key, err) 116 | } 117 | 118 | result = v 119 | continue 120 | } 121 | 122 | switch x := result.(type) { 123 | // For namespaces, we get the next value in the chain 124 | case Namespace: 125 | v, err := x.Get(k.Key) 126 | if err != nil { 127 | return nil, fmt.Errorf( 128 | "error retrieving key %q: %s", 129 | strings.Join(req.GetKeys()[:i+1], "."), err) 130 | } 131 | 132 | result = v 133 | 134 | // For maps with string keys, get the value. If the value is 135 | // nil, return sdk.Null to ensure that we don't mess with how 136 | // reflection deals with "invalid" zero values in maps. See 137 | // Plugin.reflectMap for more details. 138 | case map[string]interface{}: 139 | var ok bool 140 | if result, ok = x[k.Key]; ok { 141 | if result == nil { 142 | result = sdk.Null 143 | } 144 | } else { 145 | result = sdk.Undefined 146 | } 147 | 148 | // Else... 149 | default: 150 | // If it is a map with reflection with a string key, 151 | // then access it. 152 | v := reflect.ValueOf(x) 153 | if v.Kind() == reflect.Map && v.Type().Key() == stringTyp { 154 | // If the value exists within the map, set it to the value 155 | if v = v.MapIndex(reflect.ValueOf(k.Key)); v.IsValid() { 156 | result = v.Interface() 157 | break 158 | } 159 | } 160 | 161 | // Finally, its undefined 162 | result = nil 163 | } 164 | 165 | if result == nil { 166 | break 167 | } 168 | } 169 | 170 | var err error 171 | result, err = m.resultReflect(result) 172 | if err != nil { 173 | return nil, fmt.Errorf( 174 | "error retrieving key %q: %s", 175 | strings.Join(req.GetKeys(), "."), err) 176 | } 177 | 178 | // Convert the result based on types 179 | if result == nil { 180 | result = sdk.Undefined 181 | } 182 | 183 | // Start building the actual result 184 | resp[i] = &sdk.GetResult{ 185 | KeyId: req.KeyId, 186 | Keys: req.GetKeys(), 187 | Value: result, 188 | } 189 | 190 | // If the root supported framework.New and we have a map, flag 191 | // the ability to call methods on the result. 192 | if _, ok := result.(map[string]interface{}); ok && constructorOk { 193 | resp[i].Callable = true 194 | } 195 | 196 | // If Context was supplied, get the receiver to be returned 197 | if req.Context != nil { 198 | respCtxRaw, err := m.resultReflect(ns) 199 | if err != nil { 200 | return nil, fmt.Errorf( 201 | "error marshaling receiver after retrieving key %q: %s", 202 | strings.Join(req.GetKeys(), "."), err) 203 | } 204 | 205 | respCtx, ok := respCtxRaw.(map[string]interface{}) 206 | if !ok { 207 | return nil, fmt.Errorf( 208 | "error marshaling receiver after retrieving key %q: receiver is no longer an object", 209 | strings.Join(req.GetKeys(), ".")) 210 | } 211 | 212 | if respCtx == nil { 213 | return nil, fmt.Errorf( 214 | "error marshaling receiver after retrieving key %q: receiver is now nil", 215 | strings.Join(req.GetKeys(), ".")) 216 | } 217 | 218 | resp[i].Context = respCtx 219 | } 220 | 221 | // End of request loop. Result should be fully built here and 222 | // request will proceed to the next request (if it exists in the 223 | // payload). 224 | } 225 | 226 | // Done processing all requests, return response. 227 | return resp, nil 228 | } 229 | 230 | func (m *Plugin) resultReflect(result interface{}) (interface{}, error) { 231 | // If we have a Map implementation, we return the whole thing. 232 | if m, ok := result.(Map); ok { 233 | var err error 234 | result, err = m.Map() 235 | if err != nil { 236 | return nil, err 237 | } 238 | } 239 | 240 | if l, ok := result.(List); ok { 241 | var err error 242 | result, err = l.List() 243 | if err != nil { 244 | return nil, err 245 | } 246 | } 247 | 248 | // We now need to do a bit of reflection to convert any dangling 249 | // namespace values into values that can be returned across the 250 | // plugin interface. 251 | result, err := m.reflect(result) 252 | if err != nil { 253 | return nil, err 254 | } 255 | 256 | return result, nil 257 | } 258 | 259 | // namespace returns the namespace for the request. 260 | func (m *Plugin) namespace(req *sdk.GetReq) Namespace { 261 | if global, ok := m.Root.(Namespace); ok { 262 | return global 263 | } 264 | 265 | // Look for it in the cache of executions 266 | m.namespaceLock.RLock() 267 | ns, ok := m.namespaceMap[req.ExecId] 268 | m.namespaceLock.RUnlock() 269 | if ok { 270 | return ns 271 | } 272 | 273 | nsFunc, ok := m.Root.(NamespaceCreator) 274 | if !ok { 275 | panic("Root must be NamespaceCreator if not Namespace") 276 | } 277 | 278 | // Not found, we have to create it 279 | m.namespaceLock.Lock() 280 | defer m.namespaceLock.Unlock() 281 | 282 | // If it was created while we didn't have the lock, return it 283 | ns, ok = m.namespaceMap[req.ExecId] 284 | if ok { 285 | return ns 286 | } 287 | 288 | // Init if we have to 289 | if m.namespaceMap == nil { 290 | m.namespaceMap = make(map[uint64]Namespace) 291 | } 292 | 293 | // Create it 294 | ns = nsFunc.Namespace() 295 | m.namespaceMap[req.ExecId] = ns 296 | 297 | // Create the expiration function 298 | time.AfterFunc(time.Until(req.ExecDeadline), func() { 299 | m.invalidateNamespace(req.ExecId) 300 | }) 301 | 302 | return ns 303 | } 304 | 305 | func (m *Plugin) invalidateNamespace(id uint64) { 306 | m.namespaceLock.Lock() 307 | defer m.namespaceLock.Unlock() 308 | delete(m.namespaceMap, id) 309 | } 310 | 311 | // call performs the typed function call via reflection for f. 312 | func (m *Plugin) call(f interface{}, args []interface{}) (interface{}, error) { 313 | // If a function call isn't supported for this key, then it is an error 314 | if f == nil { 315 | return nil, fmt.Errorf("function call unsupported") 316 | } 317 | 318 | // Reflect on the function and verify it is a function 319 | funcVal := reflect.ValueOf(f) 320 | if funcVal.Kind() != reflect.Func { 321 | return nil, fmt.Errorf( 322 | "internal error: plugin didn't return function for key") 323 | } 324 | funcType := funcVal.Type() 325 | 326 | // Verify argument count 327 | if len(args) != funcType.NumIn() { 328 | return nil, fmt.Errorf( 329 | "expected %d arguments, got %d", 330 | funcType.NumIn(), len(args)) 331 | } 332 | 333 | // Go through the arguments and convert them to the proper type 334 | funcArgs := make([]reflect.Value, funcType.NumIn()) 335 | for i := 0; i < funcType.NumIn(); i++ { 336 | arg := args[i] 337 | argValue := reflect.ValueOf(arg) 338 | 339 | // If the raw argument cannot be assign to the expected arg 340 | // types then we attempt a conversion. This is slow because we 341 | // expect this to be rare. 342 | t := funcType.In(i) 343 | if !argValue.Type().AssignableTo(t) { 344 | v, err := encoding.GoToValue(arg) 345 | if err != nil { 346 | return nil, fmt.Errorf( 347 | "error converting argument to %s: %s", 348 | t, err) 349 | } 350 | 351 | arg, err = encoding.ValueToGo(v, t) 352 | if err != nil { 353 | return nil, fmt.Errorf( 354 | "error converting argument to %s: %s", 355 | t, err) 356 | } 357 | 358 | argValue = reflect.ValueOf(arg) 359 | } 360 | 361 | funcArgs[i] = argValue 362 | } 363 | 364 | // Call the function 365 | funcRets := funcVal.Call(funcArgs) 366 | 367 | // Build the return values 368 | var err error 369 | if len(funcRets) > 1 { 370 | if v := funcRets[1].Interface(); v != nil { 371 | err = v.(error) 372 | } 373 | } 374 | 375 | return funcRets[0].Interface(), err 376 | } 377 | -------------------------------------------------------------------------------- /framework/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package framework 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "sync" 11 | "sync/atomic" 12 | "testing" 13 | "time" 14 | 15 | "github.com/kr/pretty" 16 | 17 | sdk "github.com/hashicorp/sentinel-sdk" 18 | ) 19 | 20 | func TestPlugin_impl(t *testing.T) { 21 | var _ sdk.Plugin = new(Plugin) 22 | } 23 | 24 | //------------------------------------------------------------------- 25 | // Configure 26 | 27 | func TestPluginConfigure(t *testing.T) { 28 | mockRoot := new(MockNamespaceCreator) 29 | mockRoot.On("Configure", 30 | map[string]interface{}{"key": 42}).Return(nil) 31 | 32 | impt := &Plugin{Root: mockRoot} 33 | err := impt.Configure(map[string]interface{}{"key": 42}) 34 | if err != nil { 35 | t.Fatalf("err: %s", err) 36 | } 37 | 38 | mockRoot.AssertExpectations(t) 39 | } 40 | 41 | func TestPluginConfigure_noNamespace(t *testing.T) { 42 | cases := []struct { 43 | Name string 44 | Root Root 45 | Err bool 46 | }{ 47 | { 48 | "root with no other implementations", 49 | &rootNoImpl{}, 50 | true, 51 | }, 52 | 53 | { 54 | "root with Namespace", 55 | &rootNamespace{}, 56 | false, 57 | }, 58 | 59 | { 60 | "root with NamespaceCreator", 61 | &rootNamespaceCreator{}, 62 | false, 63 | }, 64 | } 65 | 66 | for _, tc := range cases { 67 | t.Run(tc.Name, func(t *testing.T) { 68 | impt := &Plugin{Root: tc.Root} 69 | err := impt.Configure(map[string]interface{}{}) 70 | if (err != nil) != tc.Err { 71 | t.Fatalf("err: %s", err) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | type rootNoImpl struct{} 78 | 79 | func (r *rootNoImpl) Configure(map[string]interface{}) error { return nil } 80 | 81 | type rootNamespace struct{} 82 | 83 | func (r *rootNamespace) Configure(map[string]interface{}) error { return nil } 84 | func (r *rootNamespace) Get(string) (interface{}, error) { return nil, nil } 85 | 86 | type rootNamespaceCreator struct{} 87 | 88 | func (r *rootNamespaceCreator) Configure(map[string]interface{}) error { return nil } 89 | func (r *rootNamespaceCreator) Namespace() Namespace { return nil } 90 | 91 | //------------------------------------------------------------------- 92 | // Get 93 | 94 | var getCases = []struct { 95 | Name string 96 | Root Root 97 | Req []*sdk.GetReq 98 | Resp []*sdk.GetResult 99 | ExpectedErr string 100 | }{ 101 | { 102 | "key get", 103 | &rootEmbedNamespace{&nsKeyValue{Key: "foo", Value: "bar"}}, 104 | []*sdk.GetReq{ 105 | { 106 | Keys: []sdk.GetKey{ 107 | {Key: "foo"}, 108 | }, 109 | KeyId: 42, 110 | }, 111 | }, 112 | []*sdk.GetResult{ 113 | { 114 | Keys: []string{"foo"}, 115 | KeyId: 42, 116 | Value: "bar", 117 | }, 118 | }, 119 | "", 120 | }, 121 | 122 | { 123 | "key get nil", 124 | &rootEmbedNamespace{&nsKeyValue{Key: "foo", Value: nil}}, 125 | []*sdk.GetReq{ 126 | { 127 | Keys: []sdk.GetKey{ 128 | {Key: "foo"}, 129 | }, 130 | KeyId: 42, 131 | }, 132 | }, 133 | []*sdk.GetResult{ 134 | { 135 | Keys: []string{"foo"}, 136 | KeyId: 42, 137 | Value: sdk.Undefined, 138 | }, 139 | }, 140 | "", 141 | }, 142 | 143 | { 144 | "key get map", 145 | &rootEmbedNamespace{&nsKeyValue{ 146 | Key: "foo", 147 | Value: map[string]interface{}{ 148 | "bar": 42, 149 | }, 150 | }}, 151 | []*sdk.GetReq{ 152 | { 153 | Keys: []sdk.GetKey{ 154 | {Key: "foo"}, 155 | }, 156 | KeyId: 42, 157 | }, 158 | }, 159 | []*sdk.GetResult{ 160 | { 161 | Keys: []string{"foo"}, 162 | KeyId: 42, 163 | Value: map[string]interface{}{ 164 | "bar": 42, 165 | }, 166 | }, 167 | }, 168 | "", 169 | }, 170 | 171 | { 172 | "key get map with int key", 173 | &rootEmbedNamespace{&nsKeyValue{ 174 | Key: "foo", 175 | Value: map[int]interface{}{ 176 | 42: "bar", 177 | }, 178 | }}, 179 | []*sdk.GetReq{ 180 | { 181 | Keys: []sdk.GetKey{ 182 | {Key: "foo"}, 183 | }, 184 | KeyId: 42, 185 | }, 186 | }, 187 | []*sdk.GetResult{ 188 | { 189 | Keys: []string{"foo"}, 190 | KeyId: 42, 191 | Value: map[int]interface{}{ 192 | 42: "bar", 193 | }, 194 | }, 195 | }, 196 | "", 197 | }, 198 | 199 | { 200 | "key get map with nil value", 201 | &rootEmbedNamespace{&nsKeyValue{ 202 | Key: "foo", 203 | Value: map[string]interface{}{ 204 | "bar": nil, 205 | }, 206 | }}, 207 | []*sdk.GetReq{ 208 | { 209 | Keys: []sdk.GetKey{ 210 | {Key: "foo"}, 211 | }, 212 | KeyId: 42, 213 | }, 214 | }, 215 | []*sdk.GetResult{ 216 | { 217 | Keys: []string{"foo"}, 218 | KeyId: 42, 219 | Value: map[string]interface{}{ 220 | "bar": nil, 221 | }, 222 | }, 223 | }, 224 | "", 225 | }, 226 | 227 | { 228 | "key get slice with nil value", 229 | &rootEmbedNamespace{&nsKeyValue{ 230 | Key: "foo", 231 | Value: []interface{}{nil}, 232 | }}, 233 | []*sdk.GetReq{ 234 | { 235 | Keys: []sdk.GetKey{ 236 | {Key: "foo"}, 237 | }, 238 | KeyId: 42, 239 | }, 240 | }, 241 | []*sdk.GetResult{ 242 | { 243 | Keys: []string{"foo"}, 244 | KeyId: 42, 245 | Value: []interface{}{nil}, 246 | }, 247 | }, 248 | "", 249 | }, 250 | 251 | { 252 | "key get map with nil value, full key in get request", 253 | &rootEmbedNamespace{&nsKeyValue{ 254 | Key: "foo", 255 | Value: map[string]interface{}{ 256 | "bar": nil, 257 | }, 258 | }}, 259 | []*sdk.GetReq{ 260 | { 261 | Keys: []sdk.GetKey{ 262 | {Key: "foo"}, 263 | {Key: "bar"}, 264 | }, 265 | KeyId: 42, 266 | }, 267 | }, 268 | []*sdk.GetResult{ 269 | { 270 | Keys: []string{"foo", "bar"}, 271 | KeyId: 42, 272 | Value: sdk.Null, 273 | }, 274 | }, 275 | "", 276 | }, 277 | 278 | { 279 | "key get map with unknown key, full key in get request", 280 | &rootEmbedNamespace{&nsKeyValue{ 281 | Key: "foo", 282 | Value: map[string]interface{}{}, 283 | }}, 284 | []*sdk.GetReq{ 285 | { 286 | Keys: []sdk.GetKey{ 287 | {Key: "foo"}, 288 | {Key: "bar"}, 289 | }, 290 | KeyId: 42, 291 | }, 292 | }, 293 | []*sdk.GetResult{ 294 | { 295 | Keys: []string{"foo", "bar"}, 296 | KeyId: 42, 297 | Value: sdk.Undefined, 298 | }, 299 | }, 300 | "", 301 | }, 302 | 303 | { 304 | "key get invalid", 305 | &rootEmbedNamespace{&nsKeyValue{Key: "foo", Value: "bar"}}, 306 | []*sdk.GetReq{ 307 | { 308 | Keys: []sdk.GetKey{ 309 | {Key: "unknown"}, 310 | }, 311 | KeyId: 42, 312 | }, 313 | }, 314 | []*sdk.GetResult{ 315 | { 316 | Keys: []string{"unknown"}, 317 | KeyId: 42, 318 | Value: sdk.Undefined, 319 | }, 320 | }, 321 | "", 322 | }, 323 | 324 | { 325 | "key get nested", 326 | &rootEmbedNamespace{&nsKeyValue{ 327 | Key: "foo", 328 | Value: &nsKeyValue{ 329 | Key: "child", 330 | Value: "bar", 331 | }, 332 | }}, 333 | []*sdk.GetReq{ 334 | { 335 | Keys: []sdk.GetKey{ 336 | {Key: "foo"}, 337 | {Key: "child"}, 338 | }, 339 | KeyId: 42, 340 | }, 341 | }, 342 | []*sdk.GetResult{ 343 | { 344 | Keys: []string{"foo", "child"}, 345 | KeyId: 42, 346 | Value: "bar", 347 | }, 348 | }, 349 | "", 350 | }, 351 | 352 | { 353 | "key get nested list", 354 | &rootEmbedNamespace{&nsKeyValue{ 355 | Key: "foo", 356 | Value: &nsList{ 357 | Value: []interface{}{"bar", "baz"}, 358 | }, 359 | }}, 360 | []*sdk.GetReq{ 361 | { 362 | Keys: []sdk.GetKey{ 363 | {Key: "foo"}, 364 | }, 365 | KeyId: 42, 366 | }, 367 | }, 368 | []*sdk.GetResult{ 369 | { 370 | Keys: []string{"foo"}, 371 | KeyId: 42, 372 | Value: []interface{}{"bar", "baz"}, 373 | }, 374 | }, 375 | "", 376 | }, 377 | 378 | { 379 | "key get map value", 380 | &rootEmbedNamespace{&nsKeyValue{ 381 | Key: "foo", 382 | Value: map[string]interface{}{ 383 | "child": "bar", 384 | }, 385 | }}, 386 | []*sdk.GetReq{ 387 | { 388 | Keys: []sdk.GetKey{ 389 | {Key: "foo"}, 390 | {Key: "child"}, 391 | }, 392 | KeyId: 42, 393 | }, 394 | }, 395 | []*sdk.GetResult{ 396 | { 397 | Keys: []string{"foo", "child"}, 398 | KeyId: 42, 399 | Value: "bar", 400 | }, 401 | }, 402 | "", 403 | }, 404 | 405 | { 406 | "key get map value with specific type", 407 | &rootEmbedNamespace{&nsKeyValue{ 408 | Key: "foo", 409 | Value: map[string]int64{ 410 | "child": 84, 411 | }, 412 | }}, 413 | []*sdk.GetReq{ 414 | { 415 | Keys: []sdk.GetKey{ 416 | {Key: "foo"}, 417 | {Key: "child"}, 418 | }, 419 | KeyId: 42, 420 | }, 421 | }, 422 | []*sdk.GetResult{ 423 | { 424 | Keys: []string{"foo", "child"}, 425 | KeyId: 42, 426 | Value: int64(84), 427 | }, 428 | }, 429 | "", 430 | }, 431 | 432 | { 433 | "key get missing map value with specific type", 434 | &rootEmbedNamespace{&nsKeyValue{ 435 | Key: "foo", 436 | Value: map[string]int64{ 437 | "child": 84, 438 | }, 439 | }}, 440 | []*sdk.GetReq{ 441 | { 442 | Keys: []sdk.GetKey{ 443 | {Key: "foo"}, 444 | {Key: "nope"}, 445 | }, 446 | KeyId: 42, 447 | }, 448 | }, 449 | []*sdk.GetResult{ 450 | { 451 | Keys: []string{"foo", "nope"}, 452 | KeyId: 42, 453 | Value: sdk.Undefined, 454 | }, 455 | }, 456 | "", 457 | }, 458 | 459 | { 460 | "key get map value that is a namespace", 461 | &rootEmbedNamespace{&nsKeyValue{ 462 | Key: "foo", 463 | Value: map[string]interface{}{ 464 | "child": &nsKeyValueMap{ 465 | Value: map[string]interface{}{ 466 | "foo": "bar", 467 | }, 468 | }, 469 | }, 470 | }}, 471 | []*sdk.GetReq{ 472 | { 473 | Keys: []sdk.GetKey{ 474 | {Key: "foo"}, 475 | }, 476 | KeyId: 42, 477 | }, 478 | }, 479 | []*sdk.GetResult{ 480 | { 481 | Keys: []string{"foo"}, 482 | KeyId: 42, 483 | Value: map[string]interface{}{ 484 | "child": map[string]interface{}{ 485 | "foo": "bar", 486 | }, 487 | }, 488 | }, 489 | }, 490 | "", 491 | }, 492 | 493 | { 494 | "key get map value that is a namespace (two levels)", 495 | &rootEmbedNamespace{&nsKeyValue{ 496 | Key: "foo", 497 | Value: map[string]interface{}{ 498 | "child": &nsKeyValueMap{ 499 | Value: map[string]interface{}{ 500 | "foo": &nsKeyValueMap{ 501 | Value: map[string]interface{}{ 502 | "bar": "baz", 503 | }, 504 | }, 505 | }, 506 | }, 507 | }, 508 | }}, 509 | []*sdk.GetReq{ 510 | { 511 | Keys: []sdk.GetKey{ 512 | {Key: "foo"}, 513 | }, 514 | KeyId: 42, 515 | }, 516 | }, 517 | []*sdk.GetResult{ 518 | { 519 | Keys: []string{"foo"}, 520 | KeyId: 42, 521 | Value: map[string]interface{}{ 522 | "child": map[string]interface{}{ 523 | "foo": map[string]interface{}{ 524 | "bar": "baz", 525 | }, 526 | }, 527 | }, 528 | }, 529 | }, 530 | "", 531 | }, 532 | 533 | { 534 | "key get slice value that is a namespace", 535 | &rootEmbedNamespace{&nsKeyValue{ 536 | Key: "foo", 537 | Value: []interface{}{ 538 | &nsKeyValueMap{ 539 | Value: map[string]interface{}{ 540 | "foo": "bar", 541 | }, 542 | }, 543 | }, 544 | }}, 545 | []*sdk.GetReq{ 546 | { 547 | Keys: []sdk.GetKey{ 548 | {Key: "foo"}, 549 | }, 550 | KeyId: 42, 551 | }, 552 | }, 553 | []*sdk.GetResult{ 554 | { 555 | Keys: []string{"foo"}, 556 | KeyId: 42, 557 | Value: []interface{}{ 558 | map[string]interface{}{ 559 | "foo": "bar", 560 | }, 561 | }, 562 | }, 563 | }, 564 | "", 565 | }, 566 | 567 | { 568 | "key get nested invalid", 569 | &rootEmbedNamespace{&nsKeyValue{ 570 | Key: "foo", 571 | Value: &nsKeyValue{ 572 | Key: "child", 573 | Value: "bar", 574 | }, 575 | }}, 576 | []*sdk.GetReq{ 577 | { 578 | Keys: []sdk.GetKey{ 579 | {Key: "foo"}, 580 | {Key: "unknown"}, 581 | }, 582 | KeyId: 42, 583 | }, 584 | }, 585 | []*sdk.GetResult{ 586 | { 587 | Keys: []string{"foo", "unknown"}, 588 | KeyId: 42, 589 | Value: sdk.Undefined, 590 | }, 591 | }, 592 | "", 593 | }, 594 | 595 | { 596 | "key get multiple", 597 | &rootEmbedNamespace{&nsKeyValueMap{ 598 | Value: map[string]interface{}{ 599 | "foo": "foovalue", 600 | "bar": "barvalue", 601 | }, 602 | }}, 603 | []*sdk.GetReq{ 604 | { 605 | Keys: []sdk.GetKey{ 606 | {Key: "foo"}, 607 | }, 608 | KeyId: 1, 609 | }, 610 | { 611 | Keys: []sdk.GetKey{ 612 | {Key: "bar"}, 613 | }, 614 | KeyId: 3, 615 | }, 616 | }, 617 | []*sdk.GetResult{ 618 | { 619 | Keys: []string{"foo"}, 620 | KeyId: 1, 621 | Value: "foovalue", 622 | }, 623 | { 624 | Keys: []string{"bar"}, 625 | KeyId: 3, 626 | Value: "barvalue", 627 | }, 628 | }, 629 | "", 630 | }, 631 | 632 | { 633 | "key get map", 634 | &rootEmbedNamespace{&nsKeyValue{ 635 | Key: "foo", 636 | Value: &nsKeyValueMap{ 637 | Value: map[string]interface{}{ 638 | "key": "value", 639 | "another": "value", 640 | }, 641 | }, 642 | }}, 643 | []*sdk.GetReq{ 644 | { 645 | Keys: []sdk.GetKey{ 646 | {Key: "foo"}, 647 | }, 648 | KeyId: 42, 649 | }, 650 | }, 651 | []*sdk.GetResult{ 652 | { 653 | Keys: []string{"foo"}, 654 | KeyId: 42, 655 | Value: map[string]interface{}{ 656 | "key": "value", 657 | "another": "value", 658 | }, 659 | }, 660 | }, 661 | "", 662 | }, 663 | 664 | { 665 | "key get result is a namespace that does not implement map", 666 | &rootEmbedNamespace{ 667 | &nsKeyValue{ 668 | Key: "foo", 669 | Value: &nsKeyValue{ 670 | Key: "one", 671 | Value: "two", 672 | }, 673 | }, 674 | }, 675 | []*sdk.GetReq{ 676 | { 677 | Keys: []sdk.GetKey{ 678 | {Key: "foo"}, 679 | }, 680 | KeyId: 42, 681 | }, 682 | }, 683 | []*sdk.GetResult{ 684 | { 685 | Keys: []string{"foo"}, 686 | KeyId: 42, 687 | Value: &nsKeyValue{ 688 | Key: "one", 689 | Value: "two", 690 | }, 691 | }, 692 | }, 693 | "", 694 | }, 695 | 696 | { 697 | "key call", 698 | &rootEmbedCall{&nsCall{ 699 | F: func(v string) (interface{}, error) { 700 | return v, nil 701 | }, 702 | }}, 703 | []*sdk.GetReq{ 704 | { 705 | Keys: []sdk.GetKey{ 706 | {Key: "foo", Args: []interface{}{"asdf"}}, 707 | }, 708 | KeyId: 42, 709 | }, 710 | }, 711 | []*sdk.GetResult{ 712 | { 713 | Keys: []string{"foo"}, 714 | KeyId: 42, 715 | Value: "asdf", 716 | }, 717 | }, 718 | "", 719 | }, 720 | 721 | { 722 | "key call with invalid but convertable type", 723 | &rootEmbedCall{&nsCall{ 724 | F: func(v string) (interface{}, error) { 725 | return v, nil 726 | }, 727 | }}, 728 | []*sdk.GetReq{ 729 | { 730 | Keys: []sdk.GetKey{ 731 | {Key: "foo", Args: []interface{}{42}}, 732 | }, 733 | KeyId: 42, 734 | }, 735 | }, 736 | []*sdk.GetResult{ 737 | { 738 | Keys: []string{"foo"}, 739 | KeyId: 42, 740 | Value: "42", 741 | }, 742 | }, 743 | "", 744 | }, 745 | 746 | { 747 | "key call with namespace return", 748 | &rootEmbedCall{&nsCall{ 749 | F: func(v string) (interface{}, error) { 750 | return &nsKeyValueMap{Value: map[string]interface{}{v: "bar"}}, nil 751 | }, 752 | }}, 753 | []*sdk.GetReq{ 754 | { 755 | Keys: []sdk.GetKey{ 756 | {Key: "foo", Args: []interface{}{"asdf"}}, 757 | }, 758 | KeyId: 42, 759 | }, 760 | }, 761 | []*sdk.GetResult{ 762 | { 763 | Keys: []string{"foo"}, 764 | KeyId: 42, 765 | Value: map[string]interface{}{ 766 | "asdf": "bar", 767 | }, 768 | }, 769 | }, 770 | "", 771 | }, 772 | 773 | { 774 | "key call with no error result", 775 | &rootEmbedCall{&nsCall{ 776 | F: func(v string) interface{} { 777 | return v 778 | }, 779 | }}, 780 | []*sdk.GetReq{ 781 | { 782 | Keys: []sdk.GetKey{ 783 | {Key: "foo", Args: []interface{}{"asdf"}}, 784 | }, 785 | KeyId: 42, 786 | }, 787 | }, 788 | []*sdk.GetResult{ 789 | { 790 | Keys: []string{"foo"}, 791 | KeyId: 42, 792 | Value: "asdf", 793 | }, 794 | }, 795 | "", 796 | }, 797 | 798 | { 799 | "multiple levels, multiple calls", 800 | &rootEmbedCall{&nsCall{ 801 | F: func(v string) (interface{}, error) { 802 | if v != "one" { 803 | return nil, fmt.Errorf("expected \"one\", got %q", v) 804 | } 805 | 806 | return &nsCall{ 807 | F: func(a, b int) (interface{}, error) { 808 | if a != 2 && b != 3 { 809 | return nil, fmt.Errorf("expected: 2, 3; got: %d, %d", a, b) 810 | } 811 | 812 | return "baz", nil 813 | }, 814 | }, nil 815 | }, 816 | }}, 817 | []*sdk.GetReq{ 818 | { 819 | Keys: []sdk.GetKey{ 820 | {Key: "foo", Args: []interface{}{"one"}}, 821 | {Key: "bar", Args: []interface{}{2, 3}}, 822 | }, 823 | KeyId: 42, 824 | }, 825 | }, 826 | []*sdk.GetResult{ 827 | { 828 | Keys: []string{"foo", "bar"}, 829 | KeyId: 42, 830 | Value: "baz", 831 | }, 832 | }, 833 | "", 834 | }, 835 | 836 | { 837 | "call, get, call", 838 | &rootEmbedCall{&nsCall{ 839 | F: func(v string) (interface{}, error) { 840 | if v != "one" { 841 | return nil, fmt.Errorf("expected \"one\", got %q", v) 842 | } 843 | 844 | return &nsKeyValue{ 845 | Key: "bar", 846 | Value: &nsCall{ 847 | F: func(a, b int) (interface{}, error) { 848 | if a != 2 && b != 3 { 849 | return nil, fmt.Errorf("expected: 2, 3; got: %d, %d", a, b) 850 | } 851 | 852 | return "qux", nil 853 | }, 854 | }, 855 | }, nil 856 | }, 857 | }}, 858 | []*sdk.GetReq{ 859 | { 860 | Keys: []sdk.GetKey{ 861 | {Key: "foo", Args: []interface{}{"one"}}, 862 | {Key: "bar"}, 863 | {Key: "baz", Args: []interface{}{2, 3}}, 864 | }, 865 | KeyId: 42, 866 | }, 867 | }, 868 | []*sdk.GetResult{ 869 | { 870 | Keys: []string{"foo", "bar", "baz"}, 871 | KeyId: 42, 872 | Value: "qux", 873 | }, 874 | }, 875 | "", 876 | }, 877 | 878 | { 879 | "get call with receiver", 880 | &rootNew{}, 881 | []*sdk.GetReq{ 882 | { 883 | Keys: []sdk.GetKey{ 884 | {Key: "foo"}, 885 | }, 886 | KeyId: 42, 887 | Context: map[string]interface{}{"a": "b"}, 888 | }, 889 | }, 890 | []*sdk.GetResult{ 891 | { 892 | Keys: []string{"foo"}, 893 | KeyId: 42, 894 | Value: map[string]interface{}{"result": "New called"}, 895 | Context: map[string]interface{}{"foo": map[string]interface{}{"result": "New called"}}, 896 | Callable: true, 897 | }, 898 | }, 899 | "", 900 | }, 901 | 902 | { 903 | "get call with receiver (assert input)", 904 | &rootNew{ 905 | F: func(data map[string]interface{}) (Namespace, error) { 906 | if data["a"] == "b" { 907 | return &nsKeyValueMap{map[string]interface{}{ 908 | "foo": map[string]interface{}{ 909 | "result": "OK", 910 | }, 911 | }}, nil 912 | } 913 | 914 | return nil, nil 915 | }, 916 | }, 917 | []*sdk.GetReq{ 918 | { 919 | Keys: []sdk.GetKey{ 920 | {Key: "foo"}, 921 | }, 922 | KeyId: 42, 923 | Context: map[string]interface{}{"a": "b"}, 924 | }, 925 | }, 926 | []*sdk.GetResult{ 927 | { 928 | Keys: []string{"foo"}, 929 | KeyId: 42, 930 | Value: map[string]interface{}{"result": "OK"}, 931 | Context: map[string]interface{}{"foo": map[string]interface{}{"result": "OK"}}, 932 | Callable: true, 933 | }, 934 | }, 935 | "", 936 | }, 937 | 938 | { 939 | "func call with receiver (non-callable result, mutate receiver)", 940 | &rootNew{ 941 | F: func(data map[string]interface{}) (Namespace, error) { 942 | return &nsMutable{Value: data["value"].(string)}, nil 943 | }, 944 | }, 945 | []*sdk.GetReq{ 946 | { 947 | Keys: []sdk.GetKey{ 948 | {Key: "foo", Args: []interface{}{"two"}}, 949 | }, 950 | KeyId: 42, 951 | Context: map[string]interface{}{"value": "one"}, 952 | }, 953 | }, 954 | []*sdk.GetResult{ 955 | { 956 | Keys: []string{"foo"}, 957 | KeyId: 42, 958 | Value: "OK", 959 | Context: map[string]interface{}{"value": "two"}, 960 | }, 961 | }, 962 | "", 963 | }, 964 | 965 | { 966 | "get without receiver on New implementation", 967 | &rootNew{}, 968 | []*sdk.GetReq{ 969 | { 970 | Keys: []sdk.GetKey{ 971 | {Key: "foo"}, 972 | }, 973 | KeyId: 42, 974 | }, 975 | }, 976 | []*sdk.GetResult{ 977 | { 978 | Keys: []string{"foo"}, 979 | KeyId: 42, 980 | Value: map[string]interface{}{"result": "New not called (Get)"}, 981 | Callable: true, 982 | }, 983 | }, 984 | "", 985 | }, 986 | 987 | { 988 | "func call without receiver on New implementation", 989 | &rootNew{}, 990 | []*sdk.GetReq{ 991 | { 992 | Keys: []sdk.GetKey{ 993 | {Key: "foo", Args: []interface{}{}}, 994 | }, 995 | KeyId: 42, 996 | }, 997 | }, 998 | []*sdk.GetResult{ 999 | { 1000 | Keys: []string{"foo"}, 1001 | KeyId: 42, 1002 | Value: map[string]interface{}{"result": "New not called (Func)"}, 1003 | Callable: true, 1004 | }, 1005 | }, 1006 | "", 1007 | }, 1008 | 1009 | { 1010 | "unknown receiver data from instantiation", 1011 | &rootNew{ 1012 | F: func(map[string]interface{}) (Namespace, error) { 1013 | return nil, nil 1014 | }, 1015 | }, 1016 | []*sdk.GetReq{ 1017 | { 1018 | Keys: []sdk.GetKey{ 1019 | {Key: "foo"}, 1020 | }, 1021 | KeyId: 42, 1022 | Context: map[string]interface{}{"a": "b"}, 1023 | }, 1024 | }, 1025 | []*sdk.GetResult{ 1026 | { 1027 | Keys: []string{"foo"}, 1028 | KeyId: 42, 1029 | Value: sdk.Undefined, 1030 | }, 1031 | }, 1032 | "", 1033 | }, 1034 | 1035 | { 1036 | "key call unsupported", 1037 | &rootEmbedNamespace{&nsKeyValue{Key: "foo", Value: "bar"}}, 1038 | []*sdk.GetReq{ 1039 | { 1040 | Keys: []sdk.GetKey{ 1041 | {Key: "foo", Args: []interface{}{"asdf"}}, 1042 | }, 1043 | KeyId: 42, 1044 | }, 1045 | }, 1046 | nil, 1047 | `key "foo" doesn't support function calls`, 1048 | }, 1049 | 1050 | { 1051 | "key call with too few arguments", 1052 | &rootEmbedCall{&nsCall{ 1053 | F: func(v string) (interface{}, error) { 1054 | return v, nil 1055 | }, 1056 | }}, 1057 | []*sdk.GetReq{ 1058 | { 1059 | Keys: []sdk.GetKey{ 1060 | {Key: "foo", Args: []interface{}{}}, 1061 | }, 1062 | KeyId: 42, 1063 | }, 1064 | }, 1065 | nil, 1066 | `error calling function "foo": expected 1 arguments, got 0`, 1067 | }, 1068 | 1069 | { 1070 | "key call with too many arguments", 1071 | &rootEmbedCall{&nsCall{ 1072 | F: func(v string) (interface{}, error) { 1073 | return v, nil 1074 | }, 1075 | }}, 1076 | []*sdk.GetReq{ 1077 | { 1078 | Keys: []sdk.GetKey{ 1079 | {Key: "foo", Args: []interface{}{1, 2}}, 1080 | }, 1081 | KeyId: 42, 1082 | }, 1083 | }, 1084 | nil, 1085 | `error calling function "foo": expected 1 arguments, got 2`, 1086 | }, 1087 | 1088 | { 1089 | "multi-level key call error message", 1090 | &rootEmbedNamespace{&nsKeyValue{ 1091 | Key: "foo", 1092 | Value: &nsCall{ 1093 | F: func() (interface{}, error) { 1094 | return "", fmt.Errorf("foo") 1095 | }, 1096 | }, 1097 | }}, 1098 | []*sdk.GetReq{ 1099 | { 1100 | Keys: []sdk.GetKey{ 1101 | {Key: "foo"}, 1102 | {Key: "bar", Args: []interface{}{}}, 1103 | }, 1104 | }, 1105 | }, 1106 | nil, 1107 | `error calling function "bar": foo`, 1108 | }, 1109 | 1110 | { 1111 | "bad get", 1112 | &rootEmbedNamespace{&nsGetErr{}}, 1113 | []*sdk.GetReq{ 1114 | { 1115 | Keys: []sdk.GetKey{ 1116 | {Key: "foo"}, 1117 | }, 1118 | KeyId: 42, 1119 | }, 1120 | }, 1121 | nil, 1122 | `error retrieving key "foo": get error`, 1123 | }, 1124 | 1125 | { 1126 | "bad map", 1127 | &rootEmbedNamespace{&nsKeyValue{Key: "foo", Value: &nsMapErr{}}}, 1128 | []*sdk.GetReq{ 1129 | { 1130 | Keys: []sdk.GetKey{ 1131 | {Key: "foo"}, 1132 | }, 1133 | KeyId: 42, 1134 | }, 1135 | }, 1136 | nil, 1137 | `error retrieving key "foo": map error`, 1138 | }, 1139 | 1140 | { 1141 | "multi-call, error in outer", 1142 | &rootEmbedCall{&nsCall{ 1143 | F: func(v string) (interface{}, error) { 1144 | if v != "one" { 1145 | return nil, fmt.Errorf("expected \"one\", got %q", v) 1146 | } 1147 | 1148 | return &nsCall{ 1149 | F: func(a, b int) (interface{}, error) { 1150 | if a != 2 && b != 3 { 1151 | return nil, fmt.Errorf("expected: 2, 3; got: %d, %d", a, b) 1152 | } 1153 | 1154 | return "baz", nil 1155 | }, 1156 | }, nil 1157 | }, 1158 | }}, 1159 | []*sdk.GetReq{ 1160 | { 1161 | Keys: []sdk.GetKey{ 1162 | {Key: "foo", Args: []interface{}{"bad"}}, 1163 | {Key: "bar", Args: []interface{}{2, 3}}, 1164 | }, 1165 | KeyId: 42, 1166 | }, 1167 | }, 1168 | nil, 1169 | `error calling function "foo": expected "one", got "bad"`, 1170 | }, 1171 | 1172 | { 1173 | "multi-call, error in inner", 1174 | &rootEmbedCall{&nsCall{ 1175 | F: func(v string) (interface{}, error) { 1176 | if v != "one" { 1177 | return nil, fmt.Errorf("expected \"one\", got %q", v) 1178 | } 1179 | 1180 | return &nsCall{ 1181 | F: func(a, b int) (interface{}, error) { 1182 | if a != 2 && b != 3 { 1183 | return nil, fmt.Errorf("expected: 2, 3; got: %d, %d", a, b) 1184 | } 1185 | 1186 | return "baz", nil 1187 | }, 1188 | }, nil 1189 | }, 1190 | }}, 1191 | []*sdk.GetReq{ 1192 | { 1193 | Keys: []sdk.GetKey{ 1194 | {Key: "foo", Args: []interface{}{"one"}}, 1195 | {Key: "bar", Args: []interface{}{42, 43}}, 1196 | }, 1197 | KeyId: 42, 1198 | }, 1199 | }, 1200 | nil, 1201 | `error calling function "bar": expected: 2, 3; got: 42, 43`, 1202 | }, 1203 | 1204 | { 1205 | "error from receiver constructor", 1206 | &rootNew{ 1207 | F: func(map[string]interface{}) (Namespace, error) { 1208 | return nil, fmt.Errorf("OK") 1209 | }, 1210 | }, 1211 | []*sdk.GetReq{ 1212 | { 1213 | Keys: []sdk.GetKey{ 1214 | {Key: "foo"}, 1215 | }, 1216 | KeyId: 42, 1217 | Context: map[string]interface{}{"a": "b"}, 1218 | }, 1219 | }, 1220 | nil, 1221 | "error instantiating namespace: OK", 1222 | }, 1223 | 1224 | { 1225 | "error from receiver constructor, function call", 1226 | &rootNew{ 1227 | F: func(map[string]interface{}) (Namespace, error) { 1228 | return nil, nil 1229 | }, 1230 | }, 1231 | []*sdk.GetReq{ 1232 | { 1233 | Keys: []sdk.GetKey{ 1234 | {Key: "foo"}, 1235 | {Key: "bar", Args: []interface{}{"one"}}, 1236 | }, 1237 | KeyId: 42, 1238 | Context: map[string]interface{}{"a": "b"}, 1239 | }, 1240 | }, 1241 | nil, 1242 | `attempting to call function "foo.bar" on undefined receiver`, 1243 | }, 1244 | 1245 | { 1246 | "Context supplied but New not implemented", 1247 | &rootEmbedNamespace{&nsKeyValue{ 1248 | Key: "foo", 1249 | Value: "bar", 1250 | }}, 1251 | []*sdk.GetReq{ 1252 | { 1253 | Keys: []sdk.GetKey{ 1254 | {Key: "foo"}, 1255 | }, 1256 | KeyId: 42, 1257 | Context: map[string]interface{}{"a": "b"}, 1258 | }, 1259 | }, 1260 | nil, 1261 | "sdk.GetReq.Context present but plugin does not support framework.New", 1262 | }, 1263 | 1264 | { 1265 | "receiver marshal error", 1266 | &rootNew{ 1267 | F: func(data map[string]interface{}) (Namespace, error) { 1268 | return &nsKeyValueMap{map[string]interface{}{ 1269 | "foo": map[string]interface{}{ 1270 | "result": "Not OK", 1271 | }, 1272 | "bar": &nsMapErr{}, 1273 | }}, nil 1274 | }, 1275 | }, 1276 | []*sdk.GetReq{ 1277 | { 1278 | Keys: []sdk.GetKey{ 1279 | {Key: "foo"}, 1280 | }, 1281 | KeyId: 42, 1282 | Context: map[string]interface{}{"a": "b"}, 1283 | }, 1284 | }, 1285 | nil, 1286 | `error marshaling receiver after retrieving key "foo": map error`, 1287 | }, 1288 | 1289 | { 1290 | "receiver non-object", 1291 | &rootNew{ 1292 | F: func(data map[string]interface{}) (Namespace, error) { 1293 | return &nsKeyValue{ 1294 | Key: "foo", 1295 | Value: "Not OK", 1296 | }, nil 1297 | }, 1298 | }, 1299 | []*sdk.GetReq{ 1300 | { 1301 | Keys: []sdk.GetKey{ 1302 | {Key: "foo"}, 1303 | }, 1304 | KeyId: 42, 1305 | Context: map[string]interface{}{"a": "b"}, 1306 | }, 1307 | }, 1308 | nil, 1309 | `error marshaling receiver after retrieving key "foo": receiver is no longer an object`, 1310 | }, 1311 | 1312 | { 1313 | "receiver nil object", 1314 | &rootNew{ 1315 | F: func(data map[string]interface{}) (Namespace, error) { 1316 | return &nsNilable{}, nil 1317 | }, 1318 | }, 1319 | []*sdk.GetReq{ 1320 | { 1321 | Keys: []sdk.GetKey{ 1322 | {Key: "foo"}, 1323 | }, 1324 | KeyId: 42, 1325 | Context: map[string]interface{}{"a": "b"}, 1326 | }, 1327 | }, 1328 | nil, 1329 | `error marshaling receiver after retrieving key "foo": receiver is now nil`, 1330 | }, 1331 | } 1332 | 1333 | func TestPluginGet(t *testing.T) { 1334 | for _, tc := range getCases { 1335 | t.Run(tc.Name, func(t *testing.T) { 1336 | impt := &Plugin{ 1337 | Root: tc.Root, 1338 | } 1339 | 1340 | // Configure 1341 | err := impt.Configure(map[string]interface{}{}) 1342 | if err != nil { 1343 | t.Fatalf("err: %s", err) 1344 | } 1345 | 1346 | // Perform the req 1347 | actual, err := impt.Get(tc.Req) 1348 | if err != nil { 1349 | if tc.ExpectedErr != "" { 1350 | if err.Error() != tc.ExpectedErr { 1351 | t.Fatalf("expected error to be %q, got %q", tc.ExpectedErr, err.Error()) 1352 | } 1353 | } else { 1354 | t.Fatalf("err: %s", err) 1355 | } 1356 | } 1357 | 1358 | // Compare the response 1359 | if !reflect.DeepEqual(actual, tc.Resp) { 1360 | t.Fatalf("bad: %s", pretty.Sprint(actual)) 1361 | } 1362 | }) 1363 | } 1364 | } 1365 | 1366 | func TestPluginGetConcurrent(t *testing.T) { 1367 | for _, tc := range getCases { 1368 | t.Run(tc.Name, func(t *testing.T) { 1369 | impt := &Plugin{ 1370 | Root: tc.Root, 1371 | } 1372 | 1373 | // Configure 1374 | err := impt.Configure(map[string]interface{}{}) 1375 | if err != nil { 1376 | t.Fatalf("err: %s", err) 1377 | } 1378 | 1379 | // Perform the reqs, in parallel, 1000x 1380 | var wg sync.WaitGroup 1381 | runErr := make(chan error, 1000) 1382 | actuals := make(chan []*sdk.GetResult, 1000) 1383 | for i := 0; i < 1000; i++ { 1384 | wg.Add(1) 1385 | go func() { 1386 | actual, err := impt.Get(tc.Req) 1387 | if err != nil { 1388 | if tc.ExpectedErr != "" { 1389 | if err.Error() != tc.ExpectedErr { 1390 | runErr <- fmt.Errorf("expected error to be %q, got %q", tc.ExpectedErr, err.Error()) 1391 | } 1392 | } else { 1393 | runErr <- fmt.Errorf("err: %s", err) 1394 | } 1395 | 1396 | return 1397 | } 1398 | 1399 | actuals <- actual 1400 | }() 1401 | 1402 | wg.Done() 1403 | } 1404 | 1405 | wg.Wait() 1406 | 1407 | nErrs := len(runErr) 1408 | if nErrs > 0 { 1409 | t.Fatalf("%d errors, first error: %s", nErrs, <-runErr) 1410 | } 1411 | 1412 | for len(actuals) > 0 { 1413 | actual := <-actuals 1414 | if !reflect.DeepEqual(actual, tc.Resp) { 1415 | t.Fatalf("bad response encountered: %s", pretty.Sprint(actual)) 1416 | } 1417 | } 1418 | }) 1419 | } 1420 | } 1421 | 1422 | // TestPluginGetImmutable checks to ensure that any deep response 1423 | // reflection we do does not alter the structure of the original 1424 | // underlying plugin data. 1425 | func TestPluginGetImmutable(t *testing.T) { 1426 | imptF := func() *Plugin { 1427 | return &Plugin{ 1428 | Root: &rootEmbedNamespace{&nsKeyValue{ 1429 | Key: "foo", 1430 | Value: &nsKeyValueMap{ 1431 | Value: map[string]interface{}{ 1432 | "key": "value", 1433 | "another": "value", 1434 | "embedded_map": &nsKeyValueMap{ 1435 | Value: map[string]interface{}{ 1436 | "key": "value", 1437 | }, 1438 | }, 1439 | "embedded_slice": []*nsKeyValueMap{ 1440 | { 1441 | Value: map[string]interface{}{ 1442 | "key": "value", 1443 | }, 1444 | }, 1445 | }, 1446 | }, 1447 | }, 1448 | }}, 1449 | } 1450 | } 1451 | 1452 | actual := imptF() 1453 | expected := imptF() 1454 | 1455 | err := actual.Configure(map[string]interface{}{}) 1456 | if err != nil { 1457 | t.Fatal(err) 1458 | } 1459 | 1460 | _, err = actual.Get([]*sdk.GetReq{ 1461 | { 1462 | Keys: []sdk.GetKey{ 1463 | {Key: "foo"}, 1464 | }, 1465 | KeyId: 42, 1466 | }, 1467 | }) 1468 | if err != nil { 1469 | t.Fatal(err) 1470 | } 1471 | 1472 | if !reflect.DeepEqual(expected, actual) { 1473 | t.Fatal("plugin data should not have been altered") 1474 | } 1475 | } 1476 | 1477 | // rootEmbedNamespace embeds a Namespace for easy testing. 1478 | type rootEmbedNamespace struct{ Namespace } 1479 | 1480 | func (r *rootEmbedNamespace) Configure(map[string]interface{}) error { return nil } 1481 | 1482 | // rootEmbedCall embeds a Call for easy testing. 1483 | type rootEmbedCall struct{ C Call } 1484 | 1485 | func (r *rootEmbedCall) Configure(map[string]interface{}) error { return nil } 1486 | 1487 | func (r *rootEmbedCall) Get(k string) (interface{}, error) { 1488 | return r.C.Get(k) 1489 | } 1490 | 1491 | func (r *rootEmbedCall) Func(k string) interface{} { 1492 | return r.C.Func(k) 1493 | } 1494 | 1495 | // nsKeyValue implements Namespace and returns a value for a specific key. 1496 | type nsKeyValue struct { 1497 | Key string 1498 | Value interface{} 1499 | } 1500 | 1501 | func (v *nsKeyValue) Get(key string) (interface{}, error) { 1502 | if v.Key != key { 1503 | return nil, nil 1504 | } 1505 | 1506 | return v.Value, nil 1507 | } 1508 | 1509 | // nsKeyValueMap implements Namespace and returns a value by looking up 1510 | // the key in a static map. 1511 | type nsKeyValueMap struct{ Value map[string]interface{} } 1512 | 1513 | func (v *nsKeyValueMap) Get(key string) (interface{}, error) { 1514 | result, ok := v.Value[key] 1515 | if !ok { 1516 | return nil, nil 1517 | } 1518 | 1519 | return result, nil 1520 | } 1521 | 1522 | func (v *nsKeyValueMap) Map() (map[string]interface{}, error) { 1523 | return v.Value, nil 1524 | } 1525 | 1526 | // nsList implements Namespace and returns a value by looking up 1527 | // the index in a slice 1528 | type nsList struct{ Value []interface{} } 1529 | 1530 | func (v *nsList) Get(key string) (interface{}, error) { 1531 | return nil, nil 1532 | } 1533 | 1534 | func (v *nsList) List() ([]interface{}, error) { 1535 | return v.Value, nil 1536 | } 1537 | 1538 | // nsCall implements Call that you can implement with a function. 1539 | type nsCall struct { 1540 | F interface{} 1541 | } 1542 | 1543 | func (v *nsCall) Func(key string) interface{} { 1544 | return v.F 1545 | } 1546 | 1547 | func (v *nsCall) Get(key string) (interface{}, error) { 1548 | return nil, fmt.Errorf("can't get") 1549 | } 1550 | 1551 | // nsGetErr implements Namespace and just stubs an error response. 1552 | type nsGetErr struct{} 1553 | 1554 | func (v *nsGetErr) Get(string) (interface{}, error) { return nil, errors.New("get error") } 1555 | 1556 | // nsvMapErr implements a Map Namespace and just stubs an error response. 1557 | type nsMapErr struct{} 1558 | 1559 | func (v *nsMapErr) Get(string) (interface{}, error) { return map[string]interface{}{}, nil } 1560 | func (v *nsMapErr) Map() (map[string]interface{}, error) { return nil, errors.New("map error") } 1561 | 1562 | // rootNew implements a mock namespace for testing framework.New. 1563 | type rootNew struct { 1564 | F func(map[string]interface{}) (Namespace, error) 1565 | } 1566 | 1567 | func (r *rootNew) Configure(map[string]interface{}) error { return nil } 1568 | 1569 | func (r *rootNew) Get(k string) (interface{}, error) { 1570 | return map[string]interface{}{"result": "New not called (Get)"}, nil 1571 | } 1572 | 1573 | func (r *rootNew) Func(k string) interface{} { 1574 | return func() (interface{}, error) { 1575 | return map[string]interface{}{"result": "New not called (Func)"}, nil 1576 | } 1577 | } 1578 | 1579 | func (r *rootNew) New(data map[string]interface{}) (Namespace, error) { 1580 | if r.F != nil { 1581 | return r.F(data) 1582 | } 1583 | 1584 | return &nsKeyValueMap{map[string]interface{}{ 1585 | "foo": map[string]interface{}{ 1586 | "result": "New called", 1587 | }, 1588 | }}, nil 1589 | } 1590 | 1591 | // nsMutable represents a mutable namespace. 1592 | type nsMutable struct { 1593 | Value string 1594 | } 1595 | 1596 | func (v *nsMutable) Get(key string) (interface{}, error) { 1597 | return v.Value, nil 1598 | } 1599 | 1600 | func (v *nsMutable) Map() (map[string]interface{}, error) { 1601 | return map[string]interface{}{"value": v.Value}, nil 1602 | } 1603 | 1604 | func (v *nsMutable) Func(key string) interface{} { 1605 | return func(s string) (interface{}, error) { 1606 | v.Value = s 1607 | return "OK", nil 1608 | } 1609 | } 1610 | 1611 | // nsNilable is a fake namespace that returns nil for everything. 1612 | type nsNilable struct{} 1613 | 1614 | func (v *nsNilable) Get(key string) (interface{}, error) { 1615 | return nil, nil 1616 | } 1617 | 1618 | func (v *nsNilable) Map() (map[string]interface{}, error) { 1619 | return nil, nil 1620 | } 1621 | 1622 | // Test Get with a Root that implements NamespaceCreator. 1623 | func TestPluginGet_namespaceCreator(t *testing.T) { 1624 | impt := &Plugin{ 1625 | Root: &rootCounter{}, 1626 | } 1627 | 1628 | // Configure 1629 | err := impt.Configure(map[string]interface{}{}) 1630 | if err != nil { 1631 | t.Fatalf("err: %s", err) 1632 | } 1633 | 1634 | { 1635 | // Make a Get request, assert the response 1636 | actual, err := impt.Get([]*sdk.GetReq{ 1637 | { 1638 | ExecId: 1, 1639 | Keys: []sdk.GetKey{{Key: "foo"}}, 1640 | KeyId: 1, 1641 | }, 1642 | { 1643 | ExecId: 2, 1644 | Keys: []sdk.GetKey{{Key: "bar"}}, 1645 | KeyId: 3, 1646 | }, 1647 | { 1648 | ExecId: 1, 1649 | Keys: []sdk.GetKey{{Key: "baz"}}, 1650 | KeyId: 5, 1651 | }, 1652 | }) 1653 | if err != nil { 1654 | t.Fatalf("err: %s", err) 1655 | } 1656 | 1657 | expected := []*sdk.GetResult{ 1658 | { 1659 | Keys: []string{"foo"}, 1660 | KeyId: 1, 1661 | Value: uint64(1), 1662 | }, 1663 | { 1664 | Keys: []string{"bar"}, 1665 | KeyId: 3, 1666 | Value: uint64(1), 1667 | }, 1668 | { 1669 | Keys: []string{"baz"}, 1670 | KeyId: 5, 1671 | Value: uint64(2), 1672 | }, 1673 | } 1674 | if !reflect.DeepEqual(actual, expected) { 1675 | t.Fatalf("expected %#v, got %#v", expected, actual) 1676 | } 1677 | } 1678 | } 1679 | 1680 | // Test Get with a Root that implements NamespaceCreator expires the 1681 | // created namespaces properly. 1682 | func TestPluginGet_namespaceCreatorExpire(t *testing.T) { 1683 | impt := &Plugin{ 1684 | Root: &rootCounter{}, 1685 | } 1686 | 1687 | // Configure 1688 | err := impt.Configure(map[string]interface{}{}) 1689 | if err != nil { 1690 | t.Fatalf("err: %s", err) 1691 | } 1692 | 1693 | // Create the deadlines 1694 | deadline1 := time.Now().Add(10 * time.Millisecond) 1695 | deadline2 := time.Now().Add(100 * time.Millisecond) 1696 | 1697 | { 1698 | // Make a Get request, assert the response 1699 | _, err := impt.Get([]*sdk.GetReq{ 1700 | { 1701 | ExecId: 1, 1702 | ExecDeadline: deadline1, 1703 | Keys: []sdk.GetKey{{Key: "foo"}}, 1704 | KeyId: 1, 1705 | }, 1706 | { 1707 | ExecId: 2, 1708 | ExecDeadline: deadline2, 1709 | Keys: []sdk.GetKey{{Key: "bar"}}, 1710 | KeyId: 3, 1711 | }, 1712 | }) 1713 | if err != nil { 1714 | t.Fatalf("err: %s", err) 1715 | } 1716 | } 1717 | 1718 | // Sleep for one 1719 | time.Sleep(time.Until(deadline1) + 5*time.Millisecond) 1720 | 1721 | // Verify we have only one 1722 | impt.namespaceLock.RLock() 1723 | if len(impt.namespaceMap) != 1 { 1724 | t.Fatal("should be one") 1725 | } 1726 | impt.namespaceLock.RUnlock() 1727 | 1728 | // Sleep for two 1729 | time.Sleep(time.Until(deadline2) + 5*time.Millisecond) 1730 | 1731 | // Verify we have only one 1732 | impt.namespaceLock.RLock() 1733 | if len(impt.namespaceMap) != 0 { 1734 | t.Fatal("should be empty") 1735 | } 1736 | impt.namespaceLock.RUnlock() 1737 | } 1738 | 1739 | type rootCounter struct{} 1740 | 1741 | func (r *rootCounter) Configure(map[string]interface{}) error { return nil } 1742 | func (r *rootCounter) Namespace() Namespace { return &nsCounter{} } 1743 | 1744 | // nsCounter is a stateful Namespace that increments a counter every Get. 1745 | type nsCounter struct { 1746 | Count uint64 1747 | } 1748 | 1749 | func (v *nsCounter) Get(string) (interface{}, error) { 1750 | return atomic.AddUint64(&v.Count, 1), nil 1751 | } 1752 | -------------------------------------------------------------------------------- /framework/reflect.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package framework 5 | 6 | import ( 7 | "reflect" 8 | ) 9 | 10 | // various convenience types for reflect calls 11 | var ( 12 | mapTyp = reflect.TypeOf((*Map)(nil)).Elem() 13 | listTyp = reflect.TypeOf((*List)(nil)).Elem() 14 | interfaceTyp = reflect.TypeOf((*interface{})(nil)).Elem() 15 | sliceInterfaceTyp = reflect.TypeOf([]interface{}{}) 16 | ) 17 | 18 | // Reflect takes a value and uses reflection to traverse the value, finding 19 | // any further namespaces that need to be converted to types that can be 20 | // sent across the plugin barrier. 21 | // 22 | // Currently, this means flattening them all to maps. In the future, we intend 23 | // to support "thunks" to allow efficiently transferring this data without 24 | // having to flatten it all. 25 | func (m *Plugin) reflect(value interface{}) (interface{}, error) { 26 | v, err := m.reflectValue(reflect.ValueOf(value)) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if !v.IsValid() { 32 | return nil, nil 33 | } 34 | 35 | return v.Interface(), nil 36 | } 37 | 38 | func (m *Plugin) reflectValue(v reflect.Value) (reflect.Value, error) { 39 | // If the value isn't valid, return right away 40 | if !v.IsValid() { 41 | return v, nil 42 | } 43 | 44 | // Unwrap the interface wrappers 45 | for v.Kind() == reflect.Interface { 46 | v = v.Elem() 47 | } 48 | 49 | // Determine if we have a nil pointer. This will turn a typed nil 50 | // into a plain nil so that we can turn it into an undefined value 51 | // properly. 52 | ptr := v 53 | for ptr.Kind() == reflect.Ptr { 54 | ptr = ptr.Elem() 55 | } 56 | if !ptr.IsValid() { 57 | return ptr, nil 58 | } 59 | 60 | // If the value implements Map, then we call that and use the map 61 | // value as the actual thing to look at. 62 | if v.Type().Implements(mapTyp) { 63 | m, err := v.Interface().(Map).Map() 64 | if err != nil { 65 | return v, err 66 | } 67 | 68 | v = reflect.ValueOf(m) 69 | } 70 | 71 | if v.Type().Implements(listTyp) { 72 | l, err := v.Interface().(List).List() 73 | if err != nil { 74 | return v, err 75 | } 76 | 77 | v = reflect.ValueOf(l) 78 | } 79 | 80 | switch v.Kind() { 81 | case reflect.Map: 82 | return m.reflectMap(v) 83 | 84 | case reflect.Slice: 85 | return m.reflectSlice(v) 86 | 87 | default: 88 | return v, nil 89 | } 90 | } 91 | 92 | func (m *Plugin) reflectMap(mv reflect.Value) (reflect.Value, error) { 93 | // Create a new map for this. This avoids conflicts and panics on shared 94 | // data, and ensures we aren't altering data in the original namespace. 95 | // map[string]interface{} is always used, regardless of the actual type of 96 | // the map sent along. This prevents type panics. 97 | // 98 | // Do a quick check to see if we have a zero-value map (nil map). If we do, 99 | // return that. 100 | if mv.IsZero() { 101 | return mv, nil 102 | } 103 | 104 | // Otherwise make a map and proceed with copy. 105 | // 106 | // Preserve key type from the original map. 107 | result := reflect.MakeMapWithSize(reflect.MapOf(mv.Type().Key(), interfaceTyp), mv.Len()) 108 | for _, k := range mv.MapKeys() { 109 | v, err := m.reflectValue(mv.MapIndex(k)) 110 | if err != nil { 111 | return mv, err 112 | } 113 | 114 | // If the value isn't valid, we set the value of the map to 115 | // the zero value for the proper type. 116 | if !v.IsValid() { 117 | v = reflect.Zero(mv.Type().Elem()) 118 | } 119 | 120 | result.SetMapIndex(k, v) 121 | } 122 | 123 | return result, nil 124 | } 125 | 126 | func (m *Plugin) reflectSlice(v reflect.Value) (reflect.Value, error) { 127 | // Create a new slice for this. This avoids conflicts and panics on 128 | // shared data, and ensures that we aren't altering data in the 129 | // original namespace. []interface{} is always used, regardless of 130 | // the actual type of the value sent along. This prevents type 131 | // panics. 132 | // 133 | // Do a quick check to see if we have a zero-value map (nil map). If we do, 134 | // return that. 135 | if v.IsZero() { 136 | return v, nil 137 | } 138 | 139 | // Otherwise make a slice and proceed with copy. 140 | result := reflect.MakeSlice(sliceInterfaceTyp, v.Len(), v.Cap()) 141 | for i := 0; i < v.Len(); i++ { 142 | elem := v.Index(i) 143 | 144 | newElem, err := m.reflectValue(elem) 145 | if err != nil { 146 | return v, err 147 | } 148 | 149 | // If the value isn't valid, we set the value of the element to 150 | // the zero value for the proper type. 151 | if !newElem.IsValid() { 152 | newElem = reflect.Zero(v.Type().Elem()) 153 | } 154 | 155 | result.Index(i).Set(newElem) 156 | } 157 | 158 | return result, nil 159 | } 160 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/sentinel-sdk 2 | 3 | require ( 4 | github.com/hashicorp/go-plugin v1.5.2 5 | github.com/kr/pretty v0.1.0 6 | github.com/mitchellh/go-testing-interface v1.14.1 7 | github.com/stretchr/testify v1.8.4 8 | golang.org/x/net v0.38.0 9 | google.golang.org/grpc v1.59.0 10 | google.golang.org/protobuf v1.36.5 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/fatih/color v1.15.0 // indirect 16 | github.com/golang/protobuf v1.5.3 // indirect 17 | github.com/hashicorp/go-hclog v1.5.0 // indirect 18 | github.com/hashicorp/yamux v0.1.1 // indirect 19 | github.com/kr/text v0.1.0 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/oklog/run v1.1.0 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/stretchr/objx v0.5.1 // indirect 25 | golang.org/x/sys v0.31.0 // indirect 26 | golang.org/x/text v0.23.0 // indirect 27 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | 31 | go 1.23.2 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 2 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 7 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 8 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 9 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 10 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 11 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 12 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 13 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 16 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 17 | github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= 18 | github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= 19 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 20 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 21 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 22 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 23 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 24 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 25 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 26 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 28 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 29 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 30 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 31 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 32 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 33 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 35 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 36 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 37 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 38 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 39 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 40 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 45 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 46 | github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= 47 | github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= 48 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 50 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 51 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 52 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 53 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 54 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 55 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 56 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 64 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 65 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 66 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 67 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 68 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= 69 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= 70 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 71 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 72 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 73 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 74 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 75 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | -------------------------------------------------------------------------------- /mock_Plugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.14.0. DO NOT EDIT. 2 | 3 | // Generated code. DO NOT MODIFY. 4 | 5 | package sdk 6 | 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // MockPlugin is an autogenerated mock type for the Plugin type 10 | type MockPlugin struct { 11 | mock.Mock 12 | } 13 | 14 | // Configure provides a mock function with given fields: _a0 15 | func (_m *MockPlugin) Configure(_a0 map[string]interface{}) error { 16 | ret := _m.Called(_a0) 17 | 18 | var r0 error 19 | if rf, ok := ret.Get(0).(func(map[string]interface{}) error); ok { 20 | r0 = rf(_a0) 21 | } else { 22 | r0 = ret.Error(0) 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // Get provides a mock function with given fields: reqs 29 | func (_m *MockPlugin) Get(reqs []*GetReq) ([]*GetResult, error) { 30 | ret := _m.Called(reqs) 31 | 32 | var r0 []*GetResult 33 | if rf, ok := ret.Get(0).(func([]*GetReq) []*GetResult); ok { 34 | r0 = rf(reqs) 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).([]*GetResult) 38 | } 39 | } 40 | 41 | var r1 error 42 | if rf, ok := ret.Get(1).(func([]*GetReq) error); ok { 43 | r1 = rf(reqs) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | type mockConstructorTestingTNewMockPlugin interface { 52 | mock.TestingT 53 | Cleanup(func()) 54 | } 55 | 56 | // NewMockPlugin creates a new instance of MockPlugin. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 57 | func NewMockPlugin(t mockConstructorTestingTNewMockPlugin) *MockPlugin { 58 | mock := &MockPlugin{} 59 | mock.Mock.Test(t) 60 | 61 | t.Cleanup(func() { mock.AssertExpectations(t) }) 62 | 63 | return mock 64 | } 65 | -------------------------------------------------------------------------------- /mock_Plugin_Closer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Do not edit mock_Plugin_Closer.go directly as your changes will be 5 | // overwritten. Instead, edit mock_Plugin_Closer.go.src and re-run 6 | // "go generate ./" in the root SDK package. 7 | package sdk 8 | 9 | import "io" 10 | 11 | // MockPluginCloser augments MockPlugin to also implement io.Closer 12 | type MockPluginCloser struct { 13 | MockPlugin 14 | } 15 | 16 | // Close mocks Close for MockPluginCloser 17 | func (_m *MockPluginCloser) Close() error { 18 | ret := _m.Called() 19 | return ret.Error(0) 20 | } 21 | 22 | var _ io.Closer = (*MockPluginCloser)(nil) 23 | -------------------------------------------------------------------------------- /mock_Plugin_Closer.go.src: -------------------------------------------------------------------------------- 1 | // Do not edit mock_Plugin_Closer.go directly as your changes will be 2 | // overwritten. Instead, edit mock_Plugin_Closer.go.src and re-run 3 | // "go generate ./" in the root SDK package. 4 | package sdk 5 | 6 | import "io" 7 | 8 | // MockPluginCloser augments MockPlugin to also implement io.Closer 9 | type MockPluginCloser struct { 10 | MockPlugin 11 | } 12 | 13 | // Close mocks Close for MockPluginCloser 14 | func (_m *MockPluginCloser) Close() error { 15 | ret := _m.Called() 16 | return ret.Error(0) 17 | } 18 | 19 | var _ io.Closer = (*MockPluginCloser)(nil) 20 | -------------------------------------------------------------------------------- /nix/overlay.nix: -------------------------------------------------------------------------------- 1 | final: prev: { 2 | devShell = final.callPackage ./sentinel_sdk.nix { }; 3 | 4 | go = final.unstable.go; 5 | } 6 | -------------------------------------------------------------------------------- /nix/sentinel_sdk.nix: -------------------------------------------------------------------------------- 1 | { go 2 | , delve 3 | , nodejs 4 | , zlib 5 | , mozjpeg 6 | , libtool 7 | , golangci-lint 8 | , kgt 9 | , mkShell 10 | }: 11 | 12 | mkShell rec { 13 | name = "sentinel-sdk"; 14 | 15 | hardeningDisable = [ "fortify" ]; 16 | 17 | packages = [ 18 | go 19 | 20 | # dev tools 21 | delve 22 | golangci-lint 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package sdk contains the low-level interfaces and API for creating Sentinel 5 | // plugins. A Sentinel plugin can provide data dynamically to Sentinel policies. 6 | // 7 | // For plugin authors, the subfolder "framework" contains a high-level 8 | // framework for easily implementing plugins in Go. 9 | package sdk 10 | 11 | import ( 12 | "encoding/json" 13 | "time" 14 | ) 15 | 16 | type undefined struct{} 17 | type null struct{} 18 | 19 | var ( 20 | // Undefined is a constant value that represents the undefined object in 21 | // Sentinel. By making this the return value, it'll be converted to 22 | // Undefined. 23 | Undefined = &undefined{} 24 | 25 | // Null is a constant value that represents the null object in Sentinel. 26 | // By making this a return value, it will convert to null. 27 | Null = &null{} 28 | ) 29 | 30 | // MarshalJSON provides custom marshalling logic to the encoding/json 31 | // package, allowing for the null type to be marshalled as nil 32 | func (*null) MarshalJSON() ([]byte, error) { return json.Marshal(nil) } 33 | 34 | //go:generate rm -f mock_Plugin.go mock_Plugin_Closer.go 35 | //go:generate mockery --inpackage --note "Generated code. DO NOT MODIFY." --name=Plugin 36 | //go:generate cp mock_Plugin_Closer.go.src mock_Plugin_Closer.go 37 | 38 | // Plugin is an importable package. 39 | // 40 | // Plugins are a namespace that may contain objects and functions. The 41 | // root level has no value nor can it be called. For example `import "a'` 42 | // allows access to fields within "a" such as `a.b` but doesn't allow 43 | // referencing or calling it directly with `a` alone. This is an important 44 | // difference between plugins and external objects (which themselves express 45 | // a value). 46 | type Plugin interface { 47 | // Configure is called to configure the plugin before it is accessed. 48 | // This must be called before any call to Get(). 49 | Configure(map[string]interface{}) error 50 | 51 | // Get is called when an plugin field is accessed or called as a function. 52 | // 53 | // Get may request more than one value at a time, represented by multiple 54 | // GetReq values. The result GetResult should contain the matching 55 | // KeyId for the requests. 56 | // 57 | // The result value is not a map keyed on KeyId to allow flexibility 58 | // in the future of potentially allowing pre-fetched data. This has no 59 | // effect currently. 60 | Get(reqs []*GetReq) ([]*GetResult, error) 61 | } 62 | 63 | // GetReq are the arguments given to Get for an Plugin. 64 | type GetReq struct { 65 | // ExecId is a unique ID representing the particular execution for this 66 | // request. This can be used to maintain state locally. 67 | // 68 | // ExecDeadline is a hint of when the execution will be over and the 69 | // state can be thrown away. The time given here will always be in UTC 70 | // time. Note that this is susceptible to clock shifts, but Go is planning 71 | // to make the time APIs monotonic by default (see proposal 12914). After 72 | // that this will be resolved. 73 | ExecId uint64 74 | ExecDeadline time.Time 75 | 76 | // Keys is the list of keys being requested. For example for "a.b.c" 77 | // where "a" is the plugin, Keys would be ["b", "c"]. 78 | Keys []GetKey 79 | 80 | // KeyId is a unique ID for this key. This should match exactly the 81 | // GetResult KeyId so that the result for this can be found quickly. 82 | KeyId uint64 83 | 84 | // Context, if supplied, is an arbitrary object intended to 85 | // represent the data from an existing namespace. If the plugin 86 | // supports the framework.New interface, the contents are passed to 87 | // it, with any resulting namespace being what Get operates on. 88 | // 89 | // The Get call operates on the root of the plugin if this is set 90 | // to nil. If this is set and the plugin does not implement 91 | // framework.New, an error is returned. 92 | Context map[string]interface{} 93 | } 94 | 95 | // GetKey is an individual key in the larger possible selector of the 96 | // specific plugin call, along with any supplied arguments for the 97 | // specific key. 98 | type GetKey struct { 99 | // The key for this part of the request. 100 | Key string 101 | 102 | // The list of arguments for a call expression. This is "nil" if 103 | // this key is not a call. This may be length zero (but non-nil) if 104 | // this is a call with no arguments. 105 | Args []interface{} 106 | } 107 | 108 | // Call returns true if this request is a call expression. 109 | func (g *GetKey) Call() bool { 110 | return g.Args != nil 111 | } 112 | 113 | // GetKeys returns a list of the string keys in the GetReq, without 114 | // the arguments. 115 | func (g *GetReq) GetKeys() []string { 116 | s := make([]string, len(g.Keys)) 117 | for i, k := range g.Keys { 118 | s[i] = k.Key 119 | } 120 | 121 | return s 122 | } 123 | 124 | // GetResult is the result structure for a Get request. 125 | type GetResult struct { 126 | KeyId uint64 // KeyId matching GetReq.KeyId, or zero. 127 | Keys []string // Keys structure from GetReq.Keys, or new key set. 128 | Value interface{} // Value compatible with lang/object.ToObject 129 | Context map[string]interface{} // Updated Context if it was sent 130 | Callable bool // true if returned Value is callable 131 | } 132 | 133 | // GetResultList is a wrapper around a slice of GetResult structures 134 | // to provide helpers. 135 | type GetResultList []*GetResult 136 | 137 | // KeyId gets the result with the given key ID, or nil if its not found. 138 | func (r GetResultList) KeyId(id uint64) *GetResult { 139 | for _, v := range r { 140 | if v.KeyId == id { 141 | return v 142 | } 143 | } 144 | 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sdk 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestGetResultListKeyId(t *testing.T) { 14 | cases := []struct { 15 | Name string 16 | KeyId uint64 17 | Expected *GetResult 18 | }{ 19 | { 20 | Name: "found", 21 | KeyId: 42, 22 | Expected: &GetResult{KeyId: 42}, 23 | }, 24 | { 25 | Name: "not found", 26 | KeyId: 43, 27 | Expected: nil, 28 | }, 29 | } 30 | results := GetResultList([]*GetResult{ 31 | { 32 | KeyId: 42, 33 | }, 34 | }) 35 | 36 | for _, tc := range cases { 37 | tc := tc 38 | t.Run(tc.Name, func(t *testing.T) { 39 | actual := results.KeyId(tc.KeyId) 40 | if !reflect.DeepEqual(tc.Expected, actual) { 41 | t.Fatalf("expected %#v, got %#v", tc.Expected, actual) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func Test_Null_MarshalJSON(t *testing.T) { 48 | res, err := json.Marshal(Null) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if !bytes.Equal(res, []byte("null")) { 54 | t.Fatalf("unexpected response, marshal of Null should be \"null\", got %q", string(res)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | # Protocol Buffers for Sentinel 2 | 3 | This folder contains the Protocol Buffers files for Sentinel. Sentinel 4 | uses [gRPC](http://www.grpc.io) for supporting plugins. This allows plugins 5 | to be written in any language. 6 | -------------------------------------------------------------------------------- /proto/go/proto.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package proto contains the Go generated files for the protocol buffer files. 5 | package proto 6 | 7 | //go:generate protoc -I ../ ../plugin.proto --go_out=plugins=grpc:. 8 | -------------------------------------------------------------------------------- /proto/plugin.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | package hashicorp.sentinel.proto; 6 | option go_package = "./;proto"; 7 | 8 | //------------------------------------------------------------------- 9 | // Plugin Service 10 | 11 | // Plugin is the RPC service that must be implemented by a valid plugin 12 | // for Sentinel. Sentinel can then consume this plugin. 13 | service Plugin { 14 | rpc Configure(Configure.Request) returns (Configure.Response); 15 | rpc Get(Get.MultiRequest) returns (Get.MultiResponse); 16 | rpc Close(Close.Request) returns (Empty); 17 | } 18 | 19 | // Empty is just an empty message. 20 | message Empty {} 21 | 22 | // Configure are the structures for Plugin.Configure 23 | message Configure { 24 | message Request { 25 | Value config = 3; 26 | } 27 | 28 | message Response { 29 | uint64 instance_id = 1; 30 | } 31 | } 32 | 33 | // Get are the structures for a Plugin.Get. 34 | message Get { 35 | // Request is a single request for a Get. 36 | message Request { 37 | message Key { 38 | string key = 1; 39 | repeated Value args = 2; 40 | bool call = 3; 41 | } 42 | 43 | uint64 instance_id = 1; 44 | uint64 exec_id = 2; 45 | uint64 exec_deadline = 3; 46 | repeated Key keys = 4; 47 | uint64 key_id = 5; 48 | map context = 6; 49 | } 50 | 51 | // Response is a single response for a Get. 52 | message Response { 53 | uint64 instance_id = 1; 54 | uint64 key_id = 2; 55 | repeated string keys = 3; 56 | Value value = 4; 57 | map context = 5; 58 | bool callable = 6; 59 | } 60 | 61 | // MultiRequest allows multiple requests in a single Get. 62 | message MultiRequest { 63 | repeated Request requests = 1; 64 | } 65 | 66 | // MultiResponse allows multiple responses in a single Get. 67 | message MultiResponse { 68 | repeated Response responses = 1; 69 | } 70 | } 71 | 72 | // Close contains the structures for Close RPC calls. 73 | message Close { 74 | message Request { 75 | uint64 instance_id = 1; 76 | } 77 | } 78 | 79 | //------------------------------------------------------------------- 80 | // Sentinel Values 81 | 82 | // Value represents a Sentinel value. 83 | message Value { 84 | // Type is an enum representing the type of the value. This isn't the 85 | // full set of Sentinel types since some types cannot be sent via 86 | // Protobufs such as rules or functions. 87 | enum Type { 88 | INVALID = 0; 89 | UNDEFINED = 1; 90 | NULL = 2; 91 | BOOL = 3; 92 | INT = 4; 93 | FLOAT = 5; 94 | STRING = 6; 95 | LIST = 7; 96 | MAP = 8; 97 | } 98 | 99 | message KV { 100 | Value key = 1; 101 | Value value = 2; 102 | } 103 | 104 | message Map { 105 | repeated KV elems = 1; 106 | } 107 | 108 | message List { 109 | repeated Value elems = 1; 110 | } 111 | 112 | // type is the type of this value 113 | Type type = 1; 114 | 115 | // value is the value only if the type is not UNDEFINED or NULL. 116 | // If the value is UNDEFINED or NULL, then the value is known. 117 | oneof value { 118 | bool value_bool = 2; 119 | int64 value_int = 3; 120 | double value_float = 4; 121 | string value_string = 5; 122 | List value_list = 6; 123 | Map value_map = 7; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /rpc/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "context" 8 | 9 | "google.golang.org/grpc" 10 | 11 | goplugin "github.com/hashicorp/go-plugin" 12 | 13 | sdk "github.com/hashicorp/sentinel-sdk" 14 | proto "github.com/hashicorp/sentinel-sdk/proto/go" 15 | ) 16 | 17 | // Plugin is the goplugin.Plugin implementation to serve sdk.Plugin. 18 | type Plugin struct { 19 | goplugin.NetRPCUnsupportedPlugin 20 | 21 | F func() sdk.Plugin 22 | } 23 | 24 | func (p *Plugin) GRPCServer(_ *goplugin.GRPCBroker, s *grpc.Server) error { 25 | proto.RegisterPluginServer(s, &PluginGRPCServer{F: p.F}) 26 | return nil 27 | } 28 | 29 | func (p *Plugin) GRPCClient(_ context.Context, _ *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 30 | return &PluginGRPCClient{Client: proto.NewPluginClient(c)}, nil 31 | } 32 | -------------------------------------------------------------------------------- /rpc/plugin_grpc_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "fmt" 8 | "math" 9 | 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | 13 | sdk "github.com/hashicorp/sentinel-sdk" 14 | "github.com/hashicorp/sentinel-sdk/encoding" 15 | proto "github.com/hashicorp/sentinel-sdk/proto/go" 16 | ) 17 | 18 | // PluginGRPCClient is a gRPC server for Plugins. 19 | type PluginGRPCClient struct { 20 | Client proto.PluginClient 21 | 22 | instanceId uint64 23 | } 24 | 25 | func (m *PluginGRPCClient) Close() error { 26 | if m.instanceId > 0 { 27 | _, err := m.Client.Close(context.Background(), &proto.Close_Request{ 28 | InstanceId: m.instanceId, 29 | }) 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (m *PluginGRPCClient) Configure(config map[string]interface{}) error { 37 | v, err := encoding.GoToValue(config) 38 | if err != nil { 39 | return fmt.Errorf("config couldn't be encoded to plugin: %s", err) 40 | } 41 | 42 | resp, err := m.Client.Configure(context.Background(), &proto.Configure_Request{ 43 | Config: v, 44 | }) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | m.instanceId = resp.InstanceId 50 | return nil 51 | } 52 | 53 | func (m *PluginGRPCClient) Get(rawReqs []*sdk.GetReq) ([]*sdk.GetResult, error) { 54 | reqs := make([]*proto.Get_Request, 0, len(rawReqs)) 55 | for _, req := range rawReqs { 56 | // Request keys 57 | keys := make([]*proto.Get_Request_Key, len(req.Keys)) 58 | for i, reqKey := range req.Keys { 59 | keys[i] = &proto.Get_Request_Key{Key: reqKey.Key} 60 | if reqKey.Args != nil { 61 | keys[i].Call = true 62 | keys[i].Args = make([]*proto.Value, len(reqKey.Args)) 63 | for j, raw := range reqKey.Args { 64 | v, err := encoding.GoToValue(raw) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | keys[i].Args[j] = v 70 | } 71 | } 72 | } 73 | 74 | // Request context 75 | var reqCtx map[string]*proto.Value 76 | if req.Context != nil { 77 | reqCtx = make(map[string]*proto.Value) 78 | for k, raw := range req.Context { 79 | v, err := encoding.GoToValue(raw) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | reqCtx[k] = v 85 | } 86 | } 87 | 88 | reqs = append(reqs, &proto.Get_Request{ 89 | InstanceId: m.instanceId, 90 | ExecId: req.ExecId, 91 | ExecDeadline: uint64(req.ExecDeadline.Unix()), 92 | Keys: keys, 93 | KeyId: req.KeyId, 94 | Context: reqCtx, 95 | }) 96 | } 97 | 98 | resp, err := m.Client.Get( 99 | context.Background(), 100 | &proto.Get_MultiRequest{ 101 | Requests: reqs, 102 | }, 103 | grpc.MaxRecvMsgSizeCallOption{MaxRecvMsgSize: math.MaxInt32}, 104 | grpc.MaxSendMsgSizeCallOption{MaxSendMsgSize: math.MaxInt32}, 105 | ) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | results := make([]*sdk.GetResult, 0, len(resp.Responses)) 111 | for _, resp := range resp.Responses { 112 | v, err := encoding.ValueToGo(resp.Value, nil) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | // Response context 118 | var resCtx map[string]interface{} 119 | if resp.Context != nil { 120 | resCtx = make(map[string]interface{}) 121 | for k, raw := range resp.Context { 122 | v, err := encoding.ValueToGo(raw, nil) 123 | if err != nil { 124 | return nil, fmt.Errorf("error converting context value for key %q: %s", k, err) 125 | } 126 | 127 | resCtx[k] = v 128 | } 129 | } 130 | 131 | results = append(results, &sdk.GetResult{ 132 | KeyId: resp.KeyId, 133 | Keys: resp.Keys, 134 | Value: v, 135 | Context: resCtx, 136 | Callable: resp.Callable, 137 | }) 138 | } 139 | 140 | return results, nil 141 | } 142 | -------------------------------------------------------------------------------- /rpc/plugin_grpc_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "io" 8 | "testing" 9 | 10 | sdk "github.com/hashicorp/sentinel-sdk" 11 | ) 12 | 13 | func TestPluginGRPCClient_impl(t *testing.T) { 14 | var _ sdk.Plugin = new(PluginGRPCClient) 15 | var _ io.Closer = new(PluginGRPCClient) 16 | } 17 | -------------------------------------------------------------------------------- /rpc/plugin_grpc_server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "reflect" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "golang.org/x/net/context" 15 | 16 | sdk "github.com/hashicorp/sentinel-sdk" 17 | "github.com/hashicorp/sentinel-sdk/encoding" 18 | proto "github.com/hashicorp/sentinel-sdk/proto/go" 19 | ) 20 | 21 | // PluginGRPCServer is a gRPC server for Plugins. 22 | type PluginGRPCServer struct { 23 | F func() sdk.Plugin 24 | 25 | // instanceId is the current instance ID. This should be modified 26 | // with sync/atomic. 27 | instanceId uint64 28 | instances map[uint64]sdk.Plugin 29 | instancesLock sync.RWMutex 30 | } 31 | 32 | func (m *PluginGRPCServer) Close( 33 | ctx context.Context, v *proto.Close_Request) (*proto.Empty, error) { 34 | // Get the plugin and remove it immediately 35 | m.instancesLock.Lock() 36 | impt, ok := m.instances[v.InstanceId] 37 | delete(m.instances, v.InstanceId) 38 | m.instancesLock.Unlock() 39 | 40 | // If we have it, attempt to call Close on the plugin if it is 41 | // a closer. 42 | if ok { 43 | if c, ok := impt.(io.Closer); ok { 44 | c.Close() 45 | } 46 | } 47 | 48 | return &proto.Empty{}, nil 49 | } 50 | 51 | func (m *PluginGRPCServer) Configure( 52 | ctx context.Context, v *proto.Configure_Request) (*proto.Configure_Response, error) { 53 | // Build the configuration 54 | var config map[string]interface{} 55 | configRaw, err := encoding.ValueToGo(v.Config, reflect.TypeOf(config)) 56 | if err != nil { 57 | return nil, fmt.Errorf("error converting config: %s", err) 58 | } 59 | config = configRaw.(map[string]interface{}) 60 | 61 | // Configure is called once to configure a new plugin. Allocate the plugin. 62 | impt := m.F() 63 | 64 | // Call configure 65 | if err := impt.Configure(config); err != nil { 66 | return nil, err 67 | } 68 | 69 | // We have to allocate a new instance ID. 70 | id := atomic.AddUint64(&m.instanceId, 1) 71 | 72 | // Put the plugin into the store 73 | m.instancesLock.Lock() 74 | if m.instances == nil { 75 | m.instances = make(map[uint64]sdk.Plugin) 76 | } 77 | m.instances[id] = impt 78 | m.instancesLock.Unlock() 79 | 80 | // Configure the plugin 81 | return &proto.Configure_Response{ 82 | InstanceId: id, 83 | }, nil 84 | } 85 | 86 | func (m *PluginGRPCServer) Get( 87 | ctx context.Context, v *proto.Get_MultiRequest) (*proto.Get_MultiResponse, error) { 88 | // Build the mapping of requests by instance ID. Then we can make the 89 | // calls for each proper instance easily. 90 | requestsById := make(map[uint64][]*sdk.GetReq) 91 | for _, req := range v.Requests { 92 | // Request keys 93 | keys := make([]sdk.GetKey, len(req.Keys)) 94 | for i, reqKey := range req.Keys { 95 | keys[i] = sdk.GetKey{Key: reqKey.Key} 96 | if reqKey.Call { 97 | keys[i].Args = make([]interface{}, len(reqKey.Args)) 98 | for j, arg := range reqKey.Args { 99 | obj, err := encoding.ValueToGo(arg, nil) 100 | if err != nil { 101 | return nil, fmt.Errorf("error converting arg %d: %s", i, err) 102 | } 103 | 104 | keys[i].Args[j] = obj 105 | } 106 | } 107 | } 108 | 109 | // Object context 110 | var reqCtx map[string]interface{} 111 | if req.Context != nil { 112 | reqCtx = make(map[string]interface{}) 113 | for k, raw := range req.Context { 114 | v, err := encoding.ValueToGo(raw, nil) 115 | if err != nil { 116 | return nil, fmt.Errorf("error converting context value for key %q: %s", k, err) 117 | } 118 | 119 | reqCtx[k] = v 120 | } 121 | } 122 | 123 | getReq := &sdk.GetReq{ 124 | ExecId: req.ExecId, 125 | ExecDeadline: time.Unix(int64(req.ExecDeadline), 0), 126 | Keys: keys, 127 | KeyId: req.KeyId, 128 | Context: reqCtx, 129 | } 130 | 131 | requestsById[req.InstanceId] = append(requestsById[req.InstanceId], getReq) 132 | } 133 | 134 | responses := make([]*proto.Get_Response, 0, len(v.Requests)) 135 | for id, reqs := range requestsById { 136 | m.instancesLock.RLock() 137 | impt, ok := m.instances[id] 138 | m.instancesLock.RUnlock() 139 | if !ok { 140 | return nil, fmt.Errorf("unknown instance ID given: %d", id) 141 | } 142 | 143 | results, err := impt.Get(reqs) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | for _, result := range results { 149 | // Return value 150 | v, err := encoding.GoToValue(result.Value) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | // Return context 156 | var resCtx map[string]*proto.Value 157 | if result.Context != nil { 158 | resCtx = make(map[string]*proto.Value) 159 | for k, raw := range result.Context { 160 | v, err := encoding.GoToValue(raw) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | resCtx[k] = v 166 | } 167 | } 168 | 169 | responses = append(responses, &proto.Get_Response{ 170 | InstanceId: id, 171 | KeyId: result.KeyId, 172 | Keys: result.Keys, 173 | Value: v, 174 | Context: resCtx, 175 | Callable: result.Callable, 176 | }) 177 | } 178 | } 179 | 180 | return &proto.Get_MultiResponse{Responses: responses}, nil 181 | } 182 | -------------------------------------------------------------------------------- /rpc/plugin_grpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/mock" 11 | 12 | sdk "github.com/hashicorp/sentinel-sdk" 13 | ) 14 | 15 | func TestPlugin_gRPC_configure(t *testing.T) { 16 | // Create a mock object 17 | pluginMock := new(sdk.MockPlugin) 18 | pluginMock.On("Configure", 19 | map[string]interface{}{"key": int64(42)}). 20 | Return(nil) 21 | 22 | obj, closer := testPluginServeGRPC(t, pluginMock) 23 | defer closer() 24 | 25 | // Get 26 | err := obj.Configure(map[string]interface{}{"key": 42}) 27 | pluginMock.AssertExpectations(t) 28 | if err != nil { 29 | t.Fatalf("err: %s", err) 30 | } 31 | } 32 | 33 | func TestPlugin_gRPC_get(t *testing.T) { 34 | cases := []struct { 35 | Name string 36 | Requests []*sdk.GetReq 37 | Results []*sdk.GetResult 38 | }{ 39 | { 40 | Name: "basic", 41 | Requests: []*sdk.GetReq{ 42 | { 43 | KeyId: 42, 44 | Keys: []sdk.GetKey{ 45 | { 46 | Key: "foo", 47 | Args: []interface{}{"foo", int64(42)}, 48 | }, 49 | }, 50 | Context: map[string]interface{}{ 51 | "_type": "SomeNamespace", 52 | "data": map[string]interface{}{ 53 | "string": "foo", 54 | "number": int64(0), 55 | }, 56 | }, 57 | }, 58 | }, 59 | Results: []*sdk.GetResult{ 60 | { 61 | KeyId: 42, 62 | Keys: []string{"key"}, 63 | Value: "bar", 64 | Context: map[string]interface{}{ 65 | "_type": "SomeNamespace", 66 | "data": map[string]interface{}{ 67 | "string": "bar", 68 | "number": int64(1), 69 | }, 70 | }, 71 | Callable: true, 72 | }, 73 | }, 74 | }, 75 | { 76 | Name: "niladic", 77 | Requests: []*sdk.GetReq{ 78 | { 79 | KeyId: 42, 80 | Keys: []sdk.GetKey{ 81 | { 82 | Key: "foo", 83 | Args: []interface{}{}, 84 | }, 85 | }, 86 | }, 87 | }, 88 | Results: []*sdk.GetResult{ 89 | { 90 | KeyId: 42, 91 | Keys: []string{"key"}, 92 | Value: "bar", 93 | }, 94 | }, 95 | }, 96 | { 97 | Name: "not a function", 98 | Requests: []*sdk.GetReq{ 99 | { 100 | KeyId: 42, 101 | Keys: []sdk.GetKey{ 102 | { 103 | Key: "foo", 104 | }, 105 | }, 106 | }, 107 | }, 108 | Results: []*sdk.GetResult{ 109 | { 110 | KeyId: 42, 111 | Keys: []string{"key"}, 112 | Value: "bar", 113 | }, 114 | }, 115 | }, 116 | } 117 | 118 | for _, tc := range cases { 119 | tc := tc 120 | t.Run(tc.Name, func(t *testing.T) { 121 | pluginMock := new(sdk.MockPlugin) 122 | pluginMock.On("Configure", map[string]interface{}{}).Return(nil) 123 | pluginMock.On("Get", 124 | mock.MatchedBy(func(reqs []*sdk.GetReq) bool { 125 | if len(tc.Requests) != len(reqs) { 126 | return false 127 | } 128 | 129 | for i := range tc.Requests { 130 | tc.Requests[i].ExecDeadline = reqs[i].ExecDeadline 131 | } 132 | 133 | return reflect.DeepEqual(tc.Requests, reqs) 134 | })).Return(tc.Results, nil) 135 | 136 | obj, closer := testPluginServeGRPC(t, pluginMock) 137 | defer closer() 138 | 139 | // We need to configure first 140 | if err := obj.Configure(nil); err != nil { 141 | t.Fatalf("err: %s", err) 142 | } 143 | 144 | // Get 145 | results, err := obj.Get(tc.Requests) 146 | pluginMock.AssertExpectations(t) 147 | if err != nil { 148 | t.Fatalf("err: %s", err) 149 | } 150 | 151 | if !reflect.DeepEqual(results, tc.Results) { 152 | t.Fatalf("expected %#v, got %#v", tc.Results, results) 153 | } 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /rpc/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "testing" 8 | 9 | goplugin "github.com/hashicorp/go-plugin" 10 | 11 | sdk "github.com/hashicorp/sentinel-sdk" 12 | ) 13 | 14 | func TestPlugin_impl(t *testing.T) { 15 | var _ goplugin.Plugin = new(Plugin) 16 | var _ goplugin.GRPCPlugin = new(Plugin) 17 | } 18 | 19 | func testPluginServeGRPC(t *testing.T, o sdk.Plugin) (sdk.Plugin, func()) { 20 | client, _ := goplugin.TestPluginGRPCConn(t, pluginMap(&ServeOpts{ 21 | PluginFunc: testPluginFixed(o), 22 | })) 23 | 24 | // Request the Plugin 25 | raw, err := client.Dispense(PluginName) 26 | if err != nil { 27 | client.Close() 28 | t.Fatalf("err: %s", err) 29 | } 30 | 31 | return raw.(sdk.Plugin), func() { 32 | client.Close() 33 | } 34 | } 35 | 36 | func testPluginFixed(p sdk.Plugin) PluginFunc { 37 | return func() sdk.Plugin { 38 | return p 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rpc/rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package rpc contains the API that can be used to serve Sentinel 5 | // plugins over an RPC interface. Sentinel supports consuming plugins 6 | // across RPC with the requirement that the RPC must happen over a completely 7 | // reliable network (effectively a local network). 8 | // 9 | // ## Object Plugins 10 | // 11 | // Object plugins allow Sentinel values to be served over a plugin interface. 12 | // This implements the object.External interface exported by lang/object. 13 | // 14 | // There are limitations to the types of values that can be returned when 15 | // this is served over a plugin: 16 | // 17 | // * All Go primitives and collections that the External interface 18 | // allows may be returned, including structs. 19 | // 20 | // * All primitive and collection Object implementations may be returned. 21 | // 22 | // * ExternalObj, External may not yet be returned. We plan to allow this. 23 | // 24 | package rpc 25 | -------------------------------------------------------------------------------- /rpc/serve.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package rpc 5 | 6 | import ( 7 | "math" 8 | 9 | "google.golang.org/grpc" 10 | 11 | goplugin "github.com/hashicorp/go-plugin" 12 | 13 | sdk "github.com/hashicorp/sentinel-sdk" 14 | ) 15 | 16 | // The constants below are the names of the plugins that can be dispensed 17 | // from the plugin server. 18 | const ( 19 | PluginName = "import" 20 | ) 21 | 22 | // Handshake is the HandshakeConfig used to configure clients and servers. 23 | var Handshake = goplugin.HandshakeConfig{ 24 | // The ProtocolVersion is the version that must match between core 25 | // and plugins. This should be bumped whenever a change happens in 26 | // one or the other that makes it so that they can't safely communicate. 27 | // This could be adding a new interface value, it could be how 28 | // helper/schema computes diffs, etc. 29 | ProtocolVersion: 3, 30 | 31 | // The magic cookie values should NEVER be changed. 32 | MagicCookieKey: "SENTINEL_PLUGIN_MAGIC_COOKIE", 33 | MagicCookieValue: "2b7847b7b705781d7cf21a05e9c1bb37cbf078aea103bc3edcc6aca52ab65453", 34 | } 35 | 36 | // PluginMap should be used by clients for the map of plugins. 37 | var PluginMap = map[string]goplugin.Plugin{ 38 | PluginName: &Plugin{}, 39 | } 40 | 41 | type PluginFunc func() sdk.Plugin 42 | 43 | // ServeOpts are the configurations to serve a plugin. 44 | type ServeOpts struct { 45 | PluginFunc PluginFunc 46 | } 47 | 48 | // Serve serves a plugin. This function never returns and should be the final 49 | // function called in the main function of the plugin. 50 | func Serve(opts *ServeOpts) { 51 | goplugin.Serve(&goplugin.ServeConfig{ 52 | HandshakeConfig: Handshake, 53 | Plugins: pluginMap(opts), 54 | GRPCServer: func(opts []grpc.ServerOption) *grpc.Server { 55 | opts = append(opts, grpc.MaxRecvMsgSize(math.MaxInt32)) 56 | opts = append(opts, grpc.MaxSendMsgSize(math.MaxInt32)) 57 | return goplugin.DefaultGRPCServer(opts) 58 | }, 59 | }) 60 | } 61 | 62 | // pluginMap returns the map[string]goplugin.Plugin to use for configuring a plugin 63 | // server or client. 64 | func pluginMap(opts *ServeOpts) map[string]goplugin.Plugin { 65 | return map[string]goplugin.Plugin{ 66 | PluginName: &Plugin{F: opts.PluginFunc}, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | ( 2 | import (fetchTarball https://github.com/edolstra/flake-compat/archive/master.tar.gz) { 3 | src = builtins.fetchGit ./.; 4 | } 5 | ).shellNix 6 | -------------------------------------------------------------------------------- /testing/README.md: -------------------------------------------------------------------------------- 1 | # Sentinel Plugin Test Framework 2 | 3 | This folder contains a library for testing plugins that are written in Go. 4 | 5 | This works by building the plugin binary dynamically during `go test` and 6 | executing your test policy. The policy must pass. If the policy fails, the 7 | failure trace is logged and shown. Execution is done via the publicly available 8 | `sentinel` binary. 9 | 10 | ## Example 11 | 12 | You can see an example in the `plugin_test.go` file in this folder. This 13 | test actually runs as part of the unit tests to verify the behavior. 14 | -------------------------------------------------------------------------------- /testing/data/main.go.tpl: -------------------------------------------------------------------------------- 1 | // This is a template used to generate the main file for the test binaries 2 | // built by the "testing" package for Sentinel plugins. This isn't expected 3 | // to be modified manually. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/hashicorp/sentinel-sdk/rpc" 9 | 10 | impl "PATH" 11 | ) 12 | 13 | func main() { 14 | rpc.Serve(&rpc.ServeOpts{ 15 | PluginFunc: impl.New, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /testing/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package testing 5 | 6 | import ( 7 | "bytes" 8 | "embed" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "regexp" 18 | "runtime" 19 | "strings" 20 | "text/scanner" 21 | 22 | "github.com/mitchellh/go-testing-interface" 23 | ) 24 | 25 | //go:embed data 26 | var content embed.FS 27 | 28 | // pluginMap is the list of built plugin binaries keyed by plugin path. 29 | // This plugin path should be canonicalized via PluginPath. 30 | var pluginBuildDir string 31 | var pluginMap = map[string]string{} 32 | var pluginErr = map[string]error{} 33 | 34 | // TestPluginCase is a single test case for configuring TestPlugin. 35 | type TestPluginCase struct { 36 | // Source is a policy to execute. This should be a full program ending 37 | // in `main = ` and an assignment. For example `main = subject.foo`. 38 | Source string 39 | 40 | // This is the configuration that will be sent to the plugin. This 41 | // must serialize to JSON since the JSON will be used to pass the 42 | // configuration. 43 | Config map[string]interface{} 44 | 45 | // This is extra data to inject into the global scope of the policy 46 | // execution 47 | Global map[string]interface{} 48 | 49 | // Mock is mocked plugin data 50 | Mock map[string]map[string]interface{} 51 | 52 | // PluginPath is the path to a Go package on your GOPATH containing 53 | // the plugin to test. If this is blank, the test case uses heuristics 54 | // to extract the GOPATH and use the current package for testing. 55 | // This package is expected to expose a "New" function which adheres to 56 | // the sdk/rpc.PluginFunc signature. 57 | // 58 | // This should usually be blank. This maximizes portability of the 59 | // plugin if it were to be forked or moved. 60 | // 61 | // For a given plugin path, the test binary will be built exactly once 62 | // per test run. 63 | PluginPath string 64 | 65 | // PluginName allows passing a custom name for the plugin to be used in 66 | // test cases. By default, the plugin is simply named "subject". The 67 | // plugin name is what is used within this policy's source to access 68 | // functionality provided by the plugin. 69 | PluginName string 70 | 71 | // A string containing any expected runtime error during evaluation. If 72 | // this field is non-empty, a runtime error is expected to occur, and 73 | // the Sentinel output is searched for the string given here. If the 74 | // output contains the string, the test passes. If it does not contain 75 | // the string, the test will fail. 76 | // 77 | // More advanced matches can be done with regular expression patterns. 78 | // If the Error string is delimited by slashes (/), the string is 79 | // compiled as a regular expression and the Sentinel output is matched 80 | // against the resulting pattern. If a match is found, the test passes. 81 | // If it does not match, the tests will fail. 82 | Error string 83 | } 84 | 85 | // LoadTestPluginCase is used to load a TestPluginCase from a Sentinel policy 86 | // file. Certain test case pragmas are supported in the top-most comment body. 87 | // The following is a completely valid example: 88 | // 89 | // //config: {"option1": "value1"} 90 | // //error: failed to do the thing 91 | // main = rule { true } 92 | // 93 | // The above would load a TestPlugin case using the specified options. The 94 | // config is loaded as a JSON string and unmarshaled into the Config field. 95 | // The error field is loaded as a string into the Error field. Pragmas *must* 96 | // be at the very top of the file, starting at line one. When a non-pragma 97 | // line is encountered, parsing will end and any further pragmas are discarded. 98 | // 99 | // This makes boilerplate very simple for a large number of Sentinel tests, 100 | // and allows an entire test to be captured neatly into a single file which 101 | // also happens to be the policy being tested. 102 | func LoadTestPluginCase(t testing.T, path string) TestPluginCase { 103 | fh, err := os.Open(path) 104 | if err != nil { 105 | t.Fatalf("error opening policy: %v", err) 106 | } 107 | defer fh.Close() 108 | 109 | var s scanner.Scanner 110 | s.Init(fh) 111 | s.Mode ^= scanner.SkipComments 112 | 113 | var errMatch string 114 | var configStr string 115 | 116 | for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() { 117 | raw := s.TokenText() 118 | content := strings.TrimPrefix(raw, "//") 119 | 120 | // Make sure we are still in the top comments. 121 | if raw == content { 122 | break 123 | } 124 | 125 | parts := strings.SplitN(content, ":", 2) 126 | if len(parts) < 2 { 127 | continue 128 | } 129 | 130 | switch parts[0] { 131 | case "error": 132 | errMatch = strings.TrimSpace(parts[1]) 133 | case "config": 134 | configStr = strings.TrimSpace(parts[1]) 135 | default: 136 | break // Require magic comments to be at the top. 137 | } 138 | } 139 | 140 | if _, err := fh.Seek(0, 0); err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | policyBytes, err := ioutil.ReadAll(fh) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | 149 | tc := TestPluginCase{ 150 | Source: string(policyBytes), 151 | Error: errMatch, 152 | } 153 | 154 | if configStr != "" { 155 | tc.Config = make(map[string]interface{}) 156 | if err := json.Unmarshal([]byte(configStr), &tc.Config); err != nil { 157 | t.Fatalf("error decoding configuration: %v", err) 158 | } 159 | } 160 | 161 | return tc 162 | } 163 | 164 | // TestPluginDir iterates over files in a directory, calls 165 | // LoadTestPluginCase on each file suffixed with ".sentinel", and executes all 166 | // of the plugin tests. 167 | func TestPluginDir(t testing.T, path string, customize func(*TestPluginCase)) { 168 | files, err := ioutil.ReadDir(path) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | cases := make(map[string]TestPluginCase) 174 | for _, fi := range files { 175 | // Allow the directory to be structured. 176 | if fi.IsDir() { 177 | continue 178 | } 179 | 180 | // Only use files ending with '.sentinel' 181 | if !strings.HasSuffix(fi.Name(), ".sentinel") { 182 | continue 183 | } 184 | 185 | // Load the sentinel file and parse it. 186 | fp := filepath.Join(path, fi.Name()) 187 | tc := LoadTestPluginCase(t, fp) 188 | 189 | // If a customization function was provided, execute it. 190 | if customize != nil { 191 | customize(&tc) 192 | } 193 | 194 | // Add the test to the set. 195 | cases[fi.Name()] = tc 196 | } 197 | 198 | // Run all of the tests. 199 | for file, tc := range cases { 200 | // The testing interface (mitchellh/go-testing-interface) doesn't 201 | // support a t.Run(), and adding context about which policy is failing 202 | // to the error is obtuse otherwise, so we'll just log the policy file 203 | // name here to give that context to the developer. 204 | t.Logf("Checking %s ...", file) 205 | TestPlugin(t, tc) 206 | } 207 | } 208 | 209 | // Clean cleans any temporary files created. This should always be called 210 | // at the end of any set of plugin tests. 211 | func Clean() { 212 | // Delete our build directory 213 | if pluginBuildDir != "" { 214 | os.RemoveAll(pluginBuildDir) 215 | } 216 | 217 | // Reset all globals 218 | pluginMap = map[string]string{} 219 | pluginErr = map[string]error{} 220 | } 221 | 222 | // TestPlugin tests that a sdk.Plugin implementation works as expected. 223 | func TestPlugin(t testing.T, c TestPluginCase) { 224 | // Infer the path 225 | path, err := PluginPath(c.PluginPath) 226 | if err != nil { 227 | t.Fatalf("error inferring GOPATH: %s", err) 228 | } 229 | 230 | // If we already errored building this, report it 231 | if err, ok := pluginErr[path]; ok { 232 | t.Fatalf("error building plugin: %s", err) 233 | } 234 | 235 | // Get the path to the built plugin, or build it 236 | binaryPath, ok := pluginMap[path] 237 | if !ok { 238 | binaryPath = buildPlugin(t, path) 239 | } 240 | 241 | // Build the full source which requires importing the subject 242 | src := `import "subject"` 243 | if c.PluginName != "" { 244 | src += " as " + c.PluginName 245 | } 246 | src += "\n\n" + c.Source 247 | 248 | // Make the test directory where we'll run the test. 249 | td, err := ioutil.TempDir("", "sentinel-sdk") 250 | if err != nil { 251 | t.Fatalf("err: %s", err) 252 | } 253 | defer os.RemoveAll(td) 254 | 255 | // Write the policy 256 | policyPath := filepath.Join(td, "policy.sentinel") 257 | if err := ioutil.WriteFile(policyPath, []byte(src), 0644); err != nil { 258 | t.Fatalf("error writing policy: %s", err) 259 | } 260 | 261 | // Write the configuration to execute 262 | configPath := filepath.Join(td, "config.json") 263 | config, err := json.MarshalIndent(map[string]interface{}{ 264 | "imports": map[string]interface{}{ 265 | "subject": map[string]interface{}{ 266 | "path": binaryPath, 267 | "config": c.Config, 268 | }, 269 | }, 270 | "global": c.Global, 271 | "mock": c.Mock, 272 | }, "", "\t") 273 | if err != nil { 274 | t.Fatalf("err: %s", err) 275 | } 276 | if err := ioutil.WriteFile(configPath, config, 0644); err != nil { 277 | t.Fatalf("error writing config: %s", err) 278 | } 279 | 280 | // Execute Sentinel 281 | cmd := exec.Command("sentinel", "apply", "-config", configPath, policyPath) 282 | cmd.Dir = td 283 | output, err := cmd.CombinedOutput() 284 | if err != nil { 285 | if c.Error != "" { 286 | if c.Error[:1]+c.Error[len(c.Error)-1:] == "//" { 287 | pattern := c.Error[1 : len(c.Error)-1] 288 | exp, err := regexp.Compile(pattern) 289 | if err != nil { 290 | t.Fatalf("error compiling expected error pattern: %s", err) 291 | } 292 | if !exp.Match(output) { 293 | t.Fatalf("the resulting error does not match the expected pattern: %s\n\nError output:\n\n%s", 294 | c.Error, string(output)) 295 | } 296 | } else { 297 | if !strings.Contains(string(output), c.Error) { 298 | t.Fatalf("resulting error does not contain %q\n\nError output:\n\n%s", 299 | c.Error, string(output)) 300 | } 301 | } 302 | } else { 303 | t.Fatalf("error executing test. output:\n\n%s", string(output)) 304 | } 305 | } else if c.Error != "" { 306 | t.Fatalf("expected error %q but policy passed", c.Error) 307 | } 308 | } 309 | 310 | // pluginPathModule determines the plugin path when modules are 311 | // enabled, through the use of "go list". 312 | // 313 | // The working directory is set to dir, if supplied. 314 | func pluginPathModule(dir string) (string, error) { 315 | cmd := exec.Command("go", "list") 316 | if dir != "" { 317 | wd, err := filepath.Abs(dir) 318 | if err != nil { 319 | return "", err 320 | } 321 | 322 | cmd.Dir = wd 323 | } 324 | 325 | out, err := cmd.Output() 326 | if err != nil { 327 | if e, ok := err.(*exec.ExitError); ok { 328 | log.Println(string(e.Stderr)) 329 | } 330 | 331 | return "", err 332 | } 333 | 334 | return strings.TrimSpace(string(out)), nil 335 | } 336 | 337 | // isUsingModules checks to see if modules are enabled on the working 338 | // repository. 339 | func isUsingModules() bool { 340 | if err := exec.Command("go", "list", "-m").Run(); err != nil { 341 | if e, ok := err.(*exec.ExitError); ok { 342 | // Log stderr if we have it 343 | log.Println(strings.TrimSpace(string(e.Stderr))) 344 | } 345 | 346 | return false 347 | } 348 | 349 | return true 350 | } 351 | 352 | // PluginPath attempts to infer the plugin path based on the GOPATH 353 | // environment variable and the directory. 354 | func PluginPath(dir string) (string, error) { 355 | if isUsingModules() { 356 | return pluginPathModule(dir) 357 | } 358 | 359 | gopath := os.Getenv("GOPATH") 360 | if gopath == "" { 361 | return "", errors.New("no GOPATH set") 362 | } 363 | 364 | // Append src to the GOPATH since we're looking for a source path 365 | gopath = filepath.Join(gopath, "src") 366 | 367 | // Create the absolute path for the directory 368 | dir, err := filepath.Abs(dir) 369 | if err != nil { 370 | return "", fmt.Errorf("error expanding %q: %s", dir, err) 371 | } 372 | 373 | // The directory should have the gopath as a prefix if its within the GOPATH 374 | if !strings.HasPrefix(dir, gopath) { 375 | return "", fmt.Errorf("Directory %q doesn't appear in GOPATH %q", dir, gopath) 376 | } 377 | 378 | // Trim the gopath from the front. If we have a slash remaining, trim that 379 | path := strings.TrimPrefix(dir, gopath) 380 | if path[0] == '/' { 381 | path = path[1:] 382 | } 383 | 384 | return path, nil 385 | } 386 | 387 | // buildPlugin compiles the plugin binary with the given Go import path. 388 | // The path to the completed binary is inserted into the global pluginMap. 389 | func buildPlugin(t testing.T, path string) string { 390 | log.Printf("Building binary: %s", path) 391 | 392 | tpl, err := content.ReadFile("data/main.go.tpl") 393 | if err != nil { 394 | t.Fatalf("err: %s", err) 395 | } 396 | // Create the main.go 397 | main := bytes.Replace( 398 | tpl, 399 | []byte("PATH"), []byte(path), -1) 400 | 401 | // If we don't have a build dir, make one 402 | if pluginBuildDir == "" { 403 | // Create the directory to compile this 404 | wd, err := os.Getwd() 405 | if err != nil { 406 | t.Fatalf("err: %s", err) 407 | } 408 | td, err := ioutil.TempDir(wd, "sentinel-sdk") 409 | if err != nil { 410 | t.Fatalf("err: %s", err) 411 | } 412 | 413 | pluginBuildDir = td 414 | } 415 | 416 | // Create the build dir for this plugin 417 | td, err := ioutil.TempDir(pluginBuildDir, "sentinel-sdk") 418 | if err != nil { 419 | t.Fatalf("err: %s", err) 420 | } 421 | 422 | // Write the file 423 | if err := ioutil.WriteFile(filepath.Join(td, "main.go"), main, 0644); err != nil { 424 | t.Fatalf("err: %s", err) 425 | } 426 | 427 | // Build. Note that when running on Windows systems the 428 | // plugin will need an .EXE extension 429 | buildOutput := "plugin-test" 430 | if isWindows() { 431 | buildOutput += ".exe" 432 | } 433 | 434 | cmd := exec.Command("go", "build", "-o", buildOutput) 435 | cmd.Dir = td 436 | output, err := cmd.CombinedOutput() 437 | if err != nil { 438 | pluginErr[path] = err 439 | t.Fatalf("err building the test binary. output:\n\n%s", string(output)) 440 | } 441 | 442 | // Record it 443 | pluginMap[path] = filepath.Join(td, buildOutput) 444 | log.Printf("Plugin binary built at: %s", pluginMap[path]) 445 | return pluginMap[path] 446 | } 447 | 448 | func isWindows() bool { 449 | return runtime.GOOS == "windows" 450 | } 451 | -------------------------------------------------------------------------------- /testing/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package testing 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | testingiface "github.com/mitchellh/go-testing-interface" 12 | ) 13 | 14 | func TestMain(m *testing.M) { 15 | exitCode := m.Run() 16 | Clean() 17 | os.Exit(exitCode) 18 | } 19 | 20 | func TestTestPlugin(t *testing.T) { 21 | path, err := filepath.Abs("testplugin") 22 | if err != nil { 23 | t.Fatalf("err: %s", err) 24 | } 25 | 26 | // Test good 27 | t.Run("success", func(t *testing.T) { 28 | TestPlugin(t, TestPluginCase{ 29 | PluginPath: path, 30 | Source: `main = subject.foo == "foo!!"`, 31 | }) 32 | }) 33 | 34 | // TestBad 35 | t.Run("failure", func(t *testing.T) { 36 | // Use a defer to catch a panic that RuntimeT will throw. We can 37 | // detect the failure this way. 38 | defer func() { 39 | if e := recover(); e == nil { 40 | t.Fatal("should fail") 41 | } 42 | }() 43 | 44 | TestPlugin(&testingiface.RuntimeT{}, TestPluginCase{ 45 | PluginPath: path, 46 | Source: `main = subject.foo == "foo!"`, 47 | }) 48 | }) 49 | 50 | // Test runtime error 51 | t.Run("error", func(t *testing.T) { 52 | TestPlugin(&testingiface.RuntimeT{}, TestPluginCase{ 53 | PluginPath: path, 54 | Source: `main = rule { error("nope") }`, 55 | Error: "nope", 56 | }) 57 | }) 58 | 59 | // Test runtime error w/ regular expression 60 | t.Run("error with regex", func(t *testing.T) { 61 | TestPlugin(&testingiface.RuntimeT{}, TestPluginCase{ 62 | PluginPath: path, 63 | Source: `main = rule { error("super 1337 error") }`, 64 | Error: `/super \d+ error/`, 65 | }) 66 | }) 67 | 68 | // Test runtime error w/ errored regular expression 69 | t.Run("error with errored regex", func(t *testing.T) { 70 | // Use a defer to catch a panic that RuntimeT will throw. We can 71 | // detect the failure this way. 72 | defer func() { 73 | if e := recover(); e == nil { 74 | t.Fatal("should fail") 75 | } 76 | }() 77 | 78 | TestPlugin(&testingiface.RuntimeT{}, TestPluginCase{ 79 | PluginPath: path, 80 | Source: `main = rule { error("super 1337 error") }`, 81 | Error: `/(super \d+ error/`, 82 | }) 83 | }) 84 | 85 | // Test configuration 86 | t.Run("config", func(t *testing.T) { 87 | TestPlugin(t, TestPluginCase{ 88 | PluginPath: path, 89 | Config: map[string]interface{}{"suffix": "??"}, 90 | Source: `main = subject.foo == "foo??"`, 91 | }) 92 | }) 93 | 94 | // Test globals 95 | t.Run("global", func(t *testing.T) { 96 | TestPlugin(t, TestPluginCase{ 97 | PluginPath: path, 98 | Global: map[string]interface{}{"value": "foo??"}, 99 | Source: `main = value == "foo??"`, 100 | }) 101 | }) 102 | 103 | // Test mocks 104 | t.Run("mock", func(t *testing.T) { 105 | TestPlugin(t, TestPluginCase{ 106 | PluginPath: path, 107 | Mock: map[string]map[string]interface{}{ 108 | "data": map[string]interface{}{ 109 | "value": "foo??", 110 | }, 111 | }, 112 | Source: `import "data"; main = data.value == "foo??"`, 113 | }) 114 | }) 115 | 116 | t.Run("custom plugin name", func(t *testing.T) { 117 | TestPlugin(t, TestPluginCase{ 118 | PluginPath: path, 119 | PluginName: "foo", 120 | Source: `main = foo.bar is "bar!!"`, 121 | }) 122 | }) 123 | 124 | // TestDirectory helper 125 | t.Run("directory", func(t *testing.T) { 126 | TestPluginDir(t, "testdata/plugin-test-dir", func(tc *TestPluginCase) { 127 | tc.PluginPath = path 128 | tc.Global = map[string]interface{}{"exclamation": "!"} 129 | }) 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /testing/testdata/plugin-test-dir/error-pragma-regex.sentinel: -------------------------------------------------------------------------------- 1 | //error: /hello \w+!/ 2 | 3 | main = rule { 4 | error("hello world" + exclamation) 5 | } 6 | -------------------------------------------------------------------------------- /testing/testdata/plugin-test-dir/nope.txt: -------------------------------------------------------------------------------- 1 | This file should be ignored. 2 | -------------------------------------------------------------------------------- /testing/testdata/plugin-test-dir/subdir/bad.sentinel: -------------------------------------------------------------------------------- 1 | // This policy should not be executed as it is in a subdirectory. 2 | main = rule { false } 3 | -------------------------------------------------------------------------------- /testing/testdata/plugin-test-dir/test.sentinel: -------------------------------------------------------------------------------- 1 | //config: {"suffix": " world"} 2 | //error: hello world! 3 | 4 | main = rule { 5 | error(subject.hello + exclamation) 6 | } 7 | -------------------------------------------------------------------------------- /testing/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package testing provides support for automated testing for plugins. 5 | package testing 6 | -------------------------------------------------------------------------------- /testing/testplugin/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package testplugin contains a test plugin that the testing package uses 5 | // for unit tests. This plugin should not be actually used for anything. 6 | package testplugin 7 | 8 | import ( 9 | sdk "github.com/hashicorp/sentinel-sdk" 10 | "github.com/hashicorp/sentinel-sdk/framework" 11 | ) 12 | 13 | // New creates a new Plugin. 14 | func New() sdk.Plugin { 15 | return &framework.Plugin{ 16 | Root: &root{}, 17 | } 18 | } 19 | 20 | type root struct { 21 | suffix string 22 | } 23 | 24 | // framework.Root impl. 25 | func (m *root) Configure(raw map[string]interface{}) error { 26 | if v, ok := raw["suffix"]; ok { 27 | m.suffix = v.(string) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // framework.Namespace impl. 34 | func (m *root) Get(key string) (interface{}, error) { 35 | suffix := m.suffix 36 | if suffix == "" { 37 | suffix = "!!" 38 | } 39 | 40 | return key + suffix, nil 41 | } 42 | --------------------------------------------------------------------------------