├── .circleci └── config.yml ├── .gitignore ├── .goreleaser.yml ├── .vscode └── launch.json ├── LICENSE ├── Makefile ├── README.md ├── backend.go ├── backend_test.go ├── clients └── splunk │ ├── access.go │ ├── auth.go │ ├── auth_test.go │ ├── client.go │ ├── client_test.go │ ├── deployment.go │ ├── error.go │ ├── introspection.go │ ├── introspection_test.go │ ├── properties.go │ ├── properties_test.go │ ├── splunk.go │ ├── splunk_test.go │ ├── testing.go │ ├── testmain_test.go │ ├── timestamp.go │ ├── user.go │ └── user_test.go ├── cmd └── vault-plugin-splunk │ └── main.go ├── conn.go ├── go.mod ├── go.sum ├── password.go ├── path_config_connection.go ├── path_creds_create.go ├── path_creds_create_test.go ├── path_reset.go ├── path_roles.go ├── path_rotate_root.go ├── role.go ├── rollback.go ├── scripts ├── golangci-lint.sh └── goreleaser ├── secret_creds.go ├── testdata └── Test_findNode.json ├── testmain_test.go ├── time_test.go ├── util.go ├── uuid.go ├── uuid_test.go └── vault.hcl.in /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaultenv: &defaultenv 4 | environment: 5 | SPLUNK_ADDR: https://localhost:8089 6 | SPLUNK_PASSWORD: test1234 7 | SPLUNK_START_ARGS: --accept-license 8 | 9 | jobs: 10 | build: 11 | docker: 12 | - image: cimg/go:1.18 13 | <<: *defaultenv 14 | - image: splunk/splunk:latest 15 | user: root 16 | <<: *defaultenv 17 | environment: 18 | - GOCACHE: /tmp/go/cache 19 | steps: 20 | - checkout 21 | - run: 22 | name: Code Quality 23 | command: make lint 24 | - run: 25 | name: Wait for Splunk Container 26 | command: | 27 | curl -4sSk --retry 40 --retry-connrefused --retry-delay 3 -o /dev/null ${SPLUNK_ADDR} 28 | sleep 5 29 | curl -4sSk --retry 40 --retry-connrefused --retry-delay 3 -o /dev/null ${SPLUNK_ADDR} 30 | - run: 31 | name: Test 32 | command: make test TESTREPORT=test-results/go/results.xml 33 | - run: 34 | name: Release 35 | command: | 36 | export GOVERSION=$(go version | awk '{sub("^go","",$3);print $3;}') 37 | [ -n "$CIRCLE_TAG" ] || tagargs="--snapshot" 38 | scripts/goreleaser --rm-dist $tagargs 39 | - store_test_results: 40 | path: test-results/ 41 | - store_artifacts: 42 | path: test-results/ 43 | - store_artifacts: 44 | path: dist/ 45 | 46 | workflows: 47 | version: 2 48 | workflow: 49 | jobs: 50 | - build: 51 | filters: 52 | tags: 53 | only: /^v\d+\.\d+\.\d+$/ 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .DS_Store 15 | /dist/ 16 | /tmp/ 17 | /vendor/ 18 | 19 | /vault.hcl 20 | /test-results.xml 21 | /test-results/ 22 | _vendor-* 23 | *.idea 24 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | # hooks: 3 | # # you may remove this if you don't use vgo 4 | # - go mod download 5 | builds: 6 | - main: cmd/vault-plugin-splunk/main.go 7 | ldflags: 8 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.goVersion={{.Env.GOVERSION}} 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | goarch: 15 | - amd64 16 | archives: 17 | - replacements: 18 | 386: i386 19 | format: zip 20 | format_overrides: 21 | - goos: linux 22 | format: tar.bz2 23 | name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}" 24 | checksum: 25 | name_template: 'checksums.txt' 26 | snapshot: 27 | name_template: "{{.Version}}-next" 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Splunk Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOLANGCI_LINT_ARGS := -v --enable=gosec --enable=dupl --timeout 5m 2 | TESTREPORT := test-results.xml 3 | 4 | # XXX BUG(mweber) "go env GOBIN" is empty? 5 | GOBIN := $(shell go env GOPATH)/bin 6 | 7 | .PHONY: all 8 | all: build lint test 9 | 10 | .PHONY: build 11 | build: vault.hcl 12 | go install ./... 13 | 14 | vault.hcl: vault.hcl.in 15 | sed -e 's;@@GOBIN@@;$(GOBIN);g' < $< > $@ 16 | 17 | .PHONY: dev 18 | dev: build 19 | @test -n "$$VAULT_ADDR" || { echo 'error: environment variable VAULT_ADDR not set'; exit 1; } 20 | @test -f ~/.vault-token || { echo 'error: ~/.vault-token does not exist. Use "vault auth ..." to login.'; exit 1; } 21 | SHASUM=$$(shasum -a 256 "$(GOBIN)/vault-plugin-splunk" | cut -d " " -f1); \ 22 | vault write sys/plugins/catalog/secret/vault-plugin-splunk sha_256="$$SHASUM" command="vault-plugin-splunk" 23 | vault secrets enable -path=splunk -plugin-name=vault-plugin-splunk plugin || true 24 | curl -vk -H "X-Vault-Token: $$(cat ~/.vault-token)" $$VAULT_ADDR/v1/sys/plugins/reload/backend -XPUT -d '{"plugin":"vault-plugin-splunk"}' 25 | 26 | .PHONY: test 27 | test: build 28 | @test -n "$$SPLUNK_ADDR" || { echo 'warning: SPLUNK_ADDR not set, creating new Splunk instances. This will be slow.'; } 29 | mkdir -p $(dir $(TESTREPORT)) 30 | go clean -testcache || true 31 | gotestsum --junitfile $(TESTREPORT) --format standard-verbose -- -cover -v ./... 32 | 33 | .PHONY: lint 34 | lint: dep 35 | $(GOBIN)/golangci-lint run $(GOLANGCI_LINT_ARGS) 36 | 37 | .PHONY: dep 38 | dep: 39 | ./scripts/golangci-lint.sh -b $(GOBIN) v1.45.0 40 | 41 | .PHONY: clean 42 | clean: 43 | # XXX clean 44 | rm -rf vault.hcl $(TESTREPORT) vendor/ dist/ 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vault-plugin-splunk 2 | =================== 3 | 4 | A Hashicorp Vault[1] plugin that aims to securely manage Splunk admin 5 | accounts, including secrets rotation for compliance purposes. 6 | 7 | [1] https://www.vaultproject.io/ 8 | 9 | ## Project status 10 | 11 | [![Build Status](https://circleci.com/gh/splunk/vault-plugin-splunk.svg?style=shield)](https://circleci.com/gh/splunk/vault-plugin-splunk) 12 | [![GoReport](https://goreportcard.com/badge/github.com/splunk/vault-plugin-splunk)](https://goreportcard.com/report/github.com/splunk/vault-plugin-splunk) 13 | 14 | 15 | # Building from Source 16 | 17 | ```shell 18 | git clone git@github.com:splunk/vault-plugin-splunk 19 | cd vault-plugin-splunk 20 | make 21 | ``` 22 | 23 | 24 | # Testing 25 | 26 | ## Splunk Setup 27 | 28 | The `go test` command creates new Splunk instances for running 29 | integration tests, which requires Docker. Since this can be slow, 30 | alternatively, if a `SPLUNK_ADDR` environment variable is set, this 31 | instance will be reused. An example for starting a new instance: 32 | 33 | ```shell 34 | export SPLUNK_ADDR='https://localhost:8089' 35 | export SPLUNK_PASSWORD='test1234' 36 | docker run -d -p 8000:8000 -p 8089:8089 -e 'SPLUNK_START_ARGS=--accept-license' -e SPLUNK_PASSWORD splunk/splunk:latest 37 | ``` 38 | 39 | Integration tests can be turned off entirely by using `go test 40 | -short`. However, note that this disables the majority of tests, 41 | which is not recommended. 42 | 43 | ## Vault Setup 44 | 45 | ```shell 46 | # server 47 | export VAULT_ADDR='http://localhost:8200' 48 | vault server -log-level debug -dev -dev-root-token-id="root" -config=vault.hcl # does not detach 49 | # client use 50 | export VAULT_ADDR='http://localhost:8200' 51 | vault login root 52 | ``` 53 | 54 | ## Rebuilding and Loading Plugin 55 | 56 | ```shell 57 | export SPLUNK_ADDR='https://localhost:8089' 58 | export SPLUNK_PASSWORD='test1234' 59 | export VAULT_ADDR='http://localhost:8200' 60 | make dev 61 | ``` 62 | 63 | ## Plugin Setup 64 | 65 | ```shell 66 | vault secrets enable -path=splunk -plugin-name=vault-plugin-splunk plugin || true 67 | vault write splunk/config/local url="${SPLUNK_ADDR}" insecure_tls=true username=admin password="${SPLUNK_PASSWORD}" allowed_roles='*' 68 | vault write splunk/roles/local-admin roles=admin email='test@example.com' connection=local default_ttl=30s max_ttl=5m 69 | vault read splunk/roles/local-admin 70 | Key Value 71 | --- ----- 72 | connection local 73 | default_app n/a 74 | default_ttl 30s 75 | email test@example.com 76 | max_ttl 5m 77 | roles [admin] 78 | tz n/a 79 | user_prefix vault 80 | ``` 81 | 82 | ## Plugin Usage 83 | 84 | Create temporary admin account: 85 | 86 | $ vault read splunk/creds/local-admin 87 | Key Value 88 | --- ----- 89 | lease_id splunk/creds/local-admin/5htFZ7QytJKbvslG5gukSPNd 90 | lease_duration 5m 91 | lease_renewable true 92 | connection local 93 | password 439e831b-e395-9999-2cd7-856381db3394 94 | roles [admin] 95 | url https://localhost:8089 96 | username vault_70c6c140-238d-e12b-3289-8e38f8c4d9f5 97 | 98 | This creates a new user account `vault_70c6c140-238d-e12b-3289-8e38f8c4d9f5` 99 | with a new random password. The account was configured to have the 100 | admin role. It will automatically be queued for deletion by vault 101 | after the configured lease ends, in 5 minutes. We can use `vault 102 | lease [renew|revoke]` to manually alter the length of the lease, up to 103 | the configured maximum time. 104 | 105 | For clustered stacks, we create ephemeral credentials for specific nodes: 106 | 107 | $ vault read splunk/creds/local-admin/idx.example.com 108 | Key Value 109 | --- ----- 110 | lease_id splunk/creds/local-admin/idx.example.com/u2N97uUVVDw3YVaETB1yRK74 111 | lease_duration 30s 112 | lease_renewable true 113 | connection local 114 | password &R1iX5W%$41QGcf^yN2i9%%#tUNf58h! 115 | roles [admin] 116 | url https://idx.example.com:8089 117 | username vault_29079642-4aa1-1979-f402-b3775f2713a7 118 | 119 | 120 | Rotate the Splunk admin password: 121 | 122 | vault write -f splunk/rotate-root/local 123 | 124 | NOTE: this alters the password of the configured admin account. It 125 | does not print out the new password. In order not to lock yourself 126 | out of the Splunk instance during testing, it is recommended to create 127 | another admin account. 128 | 129 | ## Test driver 130 | 131 | GoConvey automatically tests on saving a file: 132 | 133 | go get github.com/smartystreets/goconvey 134 | 135 | Usage: 136 | 137 | ```shell 138 | export SPLUNK_ADDR=https://localhost:8089 139 | goconvey -excludedDirs vendor 140 | ``` 141 | 142 | 143 | # TODO 144 | 145 | ## Vault Plugin 146 | * benchmark with thousands of simultaneous connections 147 | * vault client cert & auto-renewal 148 | * support "DIY" Splunk cluster without CM 149 | * better HTTP error codes 150 | * support for license rotation? 151 | * add (default) secrets mount description (currently "n/a") 152 | * DisplayName for config parameters (where is it shown?) 153 | 154 | ### Tests 155 | * TTLs roundtrip 156 | * externally deleted user 157 | * externally revoked admin access 158 | * not in allowed_roles 159 | * updating roles, connections with partial params 160 | * creating conns first, then roles, and vice versa 161 | 162 | ## Splunk API 163 | * use ctx in every operation 164 | * metrics 165 | * error handling 166 | * move to separate package 167 | * generate API from OpenAPI spec 168 | * expand doc strings 169 | * comment strings: caps, punctuation 170 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 11 | ) 12 | 13 | type backend struct { 14 | *framework.Backend 15 | conn *sync.Map 16 | } 17 | 18 | // Factory is the factory function to create a Splunk backend. 19 | func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 20 | b := newBackend() 21 | if err := b.Setup(ctx, conf); err != nil { 22 | return nil, err 23 | } 24 | return b, nil 25 | } 26 | 27 | func newBackend() logical.Backend { 28 | b := backend{} 29 | b.Backend = &framework.Backend{ 30 | Help: strings.TrimSpace(backendHelp), 31 | PathsSpecial: &logical.Paths{ 32 | SealWrapStorage: []string{ 33 | "config/", 34 | }, 35 | }, 36 | Paths: []*framework.Path{ 37 | b.pathConfigConnection(), 38 | b.pathConnectionsList(), 39 | b.pathResetConnection(), 40 | b.pathRotateRoot(), 41 | b.pathRolesList(), 42 | b.pathRoles(), 43 | b.pathCredsCreate(), 44 | b.pathCredsCreateMulti(), 45 | }, 46 | Secrets: []*framework.Secret{ 47 | b.pathSecretCreds(), 48 | }, 49 | WALRollback: b.walRollback, 50 | WALRollbackMinAge: walRollbackMinAge, 51 | BackendType: logical.TypeLogical, 52 | } 53 | b.conn = new(sync.Map) 54 | return &b 55 | } 56 | 57 | func (b *backend) ensureConnection(ctx context.Context, config *splunkConfig) (*splunk.API, error) { 58 | if conn, ok := b.conn.Load(config.ID); ok { 59 | return conn.(*splunk.API), nil 60 | } 61 | 62 | // create and cache connection 63 | conn, err := config.newConnection(ctx) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if conn, loaded := b.conn.LoadOrStore(config.ID, conn); loaded { 68 | // somebody else won the race 69 | return conn.(*splunk.API), nil 70 | } 71 | return conn, nil 72 | } 73 | 74 | // clearConnection closes the connection and removes it from the cache. 75 | func (b *backend) clearConnection(id string) error { 76 | b.conn.Delete(id) 77 | return nil 78 | } 79 | 80 | const backendHelp = ` 81 | The Splunk backend rotates admin credentials and dynamically generates new 82 | users with limited life-time. 83 | 84 | After mounting this backend, credentials for a Splunk admin role must 85 | be configured and connections and roles must be written using 86 | the "config/" and "roles/" endpoints before any logins can be generated. 87 | ` 88 | -------------------------------------------------------------------------------- /backend_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "github.com/mitchellh/mapstructure" 12 | "gotest.tools/v3/assert" 13 | 14 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 15 | ) 16 | 17 | func TestBackend_basic(t *testing.T) { 18 | b, err := testNewSplunkBackend(t) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | schemes := []string{ 24 | userIDSchemeUUID4_v0_5_0, 25 | userIDSchemeUUID4, 26 | userIDSchemeBase58_64, 27 | userIDSchemeBase58_128, 28 | } 29 | for _, scheme := range schemes { 30 | roleConfig := roleConfig{ 31 | Connection: "testconn", 32 | Roles: []string{"admin"}, 33 | UserPrefix: defaultUserPrefix, 34 | UserIDScheme: scheme, 35 | } 36 | 37 | logicaltest.Test(t, logicaltest.TestCase{ 38 | LogicalBackend: b, 39 | Steps: []logicaltest.TestStep{ 40 | testAccStepConfig(t), 41 | testAccStepRole(t, "test", roleConfig), 42 | testAccStepCredsRead(t, "test"), 43 | testAccStepCredsReadMultiBadConfig(t, "test"), 44 | }, 45 | }) 46 | } 47 | } 48 | 49 | func TestBackend_RotateRoot(t *testing.T) { 50 | b, err := testNewSplunkBackend(t) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | logicaltest.Test(t, logicaltest.TestCase{ 56 | LogicalBackend: b, 57 | Steps: []logicaltest.TestStep{ 58 | testAccStepConfig(t), 59 | testAccRotateRoot(t, "testconn"), 60 | // and again, to check if we can still login 61 | testAccRotateRoot(t, "testconn"), 62 | }, 63 | }) 64 | } 65 | 66 | func TestBackend_ConnectionCRUD(t *testing.T) { 67 | b, err := testNewSplunkBackend(t) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | connConfig := splunkConfig{ 73 | Username: splunk.TestGlobalSplunkClient(t).Params().Config.ClientID, 74 | URL: splunk.TestGlobalSplunkClient(t).Params().BaseURL, 75 | AllowedRoles: []string{"*"}, 76 | Verify: true, 77 | InsecureTLS: true, 78 | CAChain: []string{}, 79 | RootCA: []string{}, 80 | ConnectTimeout: time.Duration(30) * time.Second, 81 | } 82 | 83 | logicaltest.Test(t, logicaltest.TestCase{ 84 | LogicalBackend: b, 85 | Steps: []logicaltest.TestStep{ 86 | testAccStepConfig(t), 87 | testAccStepConnectionRead(t, "testconn", connConfig), 88 | testAccStepConnectionDelete(t, "testconn"), 89 | }, 90 | }) 91 | } 92 | 93 | func TestBackend_RoleCRUD(t *testing.T) { 94 | b, err := testNewSplunkBackend(t) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | testRoleConfig := roleConfig{ 100 | Connection: "testconn", 101 | Roles: []string{"admin"}, 102 | AllowedServerRoles: []string{"*"}, 103 | PasswordSpec: DefaultPasswordSpec(), 104 | UserPrefix: "my-custom-prefix", 105 | UserIDScheme: userIDSchemeUUID4, 106 | } 107 | 108 | logicaltest.Test(t, logicaltest.TestCase{ 109 | LogicalBackend: b, 110 | Steps: []logicaltest.TestStep{ 111 | testAccStepConfig(t), 112 | testAccStepRole(t, "test", testRoleConfig), 113 | testAccStepRoleMissingRoleName(t), 114 | testAccStepRoleMissingRoles(t, "MISSING"), 115 | testAccStepRoleRead(t, "test", testRoleConfig), 116 | testAccStepRoleDelete(t, "test"), 117 | }, 118 | }) 119 | emptyUserPrefixConfig := testRoleConfig 120 | emptyUserPrefixConfig.UserPrefix = "" 121 | logicaltest.Test(t, logicaltest.TestCase{ 122 | LogicalBackend: b, 123 | Steps: []logicaltest.TestStep{ 124 | testEmptyUserPrefix(t, "test", emptyUserPrefixConfig), 125 | }, 126 | }) 127 | 128 | userIDSchemeConfig := testRoleConfig 129 | userIDSchemeConfig.UserIDScheme = "-invalid-" 130 | logicaltest.Test(t, logicaltest.TestCase{ 131 | LogicalBackend: b, 132 | Steps: []logicaltest.TestStep{ 133 | testUserIDScheme(t, "test", "-invalid-", userIDSchemeConfig), 134 | }, 135 | }) 136 | } 137 | 138 | // Test steps 139 | 140 | // Connection 141 | func testAccStepConfig(t *testing.T) logicaltest.TestStep { 142 | return logicaltest.TestStep{ 143 | Operation: logical.UpdateOperation, 144 | Path: "config/testconn", 145 | Data: map[string]interface{}{ 146 | "url": splunk.TestGlobalSplunkClient(t).Params().BaseURL, 147 | "username": splunk.TestGlobalSplunkClient(t).Params().Config.ClientID, 148 | "password": splunk.TestGlobalSplunkClient(t).Params().Config.ClientSecret, 149 | "allowed_roles": "*", 150 | "insecure_tls": true, 151 | }, 152 | } 153 | } 154 | 155 | func testAccStepConnectionRead(t *testing.T, conn string, config splunkConfig) logicaltest.TestStep { 156 | return logicaltest.TestStep{ 157 | Operation: logical.ReadOperation, 158 | Path: "config/" + conn, 159 | Check: func(resp *logical.Response) error { 160 | if resp == nil { 161 | return fmt.Errorf("response is nil") 162 | } 163 | expected := config.toResponseData() 164 | expected["id"] = resp.Data["id"].(string) 165 | assert.DeepEqual(t, expected, resp.Data) 166 | return nil 167 | }, 168 | } 169 | } 170 | 171 | func testAccStepConnectionDelete(t *testing.T, conn string) logicaltest.TestStep { 172 | return logicaltest.TestStep{ 173 | Operation: logical.DeleteOperation, 174 | Path: "config/" + conn, 175 | } 176 | } 177 | 178 | // Role 179 | func testAccStepRole(t *testing.T, role string, config roleConfig) logicaltest.TestStep { 180 | return logicaltest.TestStep{ 181 | Operation: logical.UpdateOperation, 182 | Path: rolesPrefix + role, 183 | Data: config.toResponseData(), 184 | } 185 | } 186 | 187 | func testAccStepRoleMissingRoles(t *testing.T, role string) logicaltest.TestStep { 188 | return logicaltest.TestStep{ 189 | Operation: logical.UpdateOperation, 190 | Path: rolesPrefix + role, 191 | Data: map[string]interface{}{ 192 | "connection": "testconn", 193 | }, 194 | ErrorOk: true, 195 | Check: func(resp *logical.Response) error { 196 | if resp == nil { 197 | return fmt.Errorf("response is nil") 198 | } 199 | assert.Error(t, resp.Error(), "roles cannot be empty") 200 | return nil 201 | }, 202 | } 203 | } 204 | 205 | func testAccStepRoleMissingRoleName(t *testing.T) logicaltest.TestStep { 206 | return logicaltest.TestStep{ 207 | Operation: logical.CreateOperation, 208 | Path: rolesPrefix, 209 | Data: map[string]interface{}{ 210 | "connection": "testconn", 211 | }, 212 | ErrorOk: true, 213 | Check: func(resp *logical.Response) error { 214 | if resp == nil { 215 | return fmt.Errorf("response is nil") 216 | } 217 | assert.Error(t, resp.Error(), "cannot write to a path ending in '/'") 218 | return nil 219 | }, 220 | } 221 | } 222 | 223 | func testEmptyUserPrefix(t *testing.T, role string, config roleConfig) logicaltest.TestStep { 224 | return logicaltest.TestStep{ 225 | Operation: logical.CreateOperation, 226 | Path: rolesPrefix + role, 227 | Data: config.toResponseData(), 228 | ErrorOk: true, 229 | Check: func(resp *logical.Response) error { 230 | if resp == nil { 231 | return fmt.Errorf("response is nil") 232 | } 233 | assert.Error(t, resp.Error(), "user_prefix can't be set to empty string") 234 | return nil 235 | }, 236 | } 237 | } 238 | 239 | func testUserIDScheme(t *testing.T, role, idScheme string, config roleConfig) logicaltest.TestStep { 240 | return logicaltest.TestStep{ 241 | Operation: logical.CreateOperation, 242 | Path: rolesPrefix + role, 243 | Data: config.toResponseData(), 244 | ErrorOk: true, 245 | Check: func(resp *logical.Response) error { 246 | if resp == nil { 247 | return fmt.Errorf("response is nil") 248 | } 249 | assert.Error(t, resp.Error(), fmt.Sprintf("invalid user_id_scheme: %q", idScheme)) 250 | return nil 251 | }, 252 | } 253 | } 254 | 255 | func testAccStepCredsRead(t *testing.T, role string) logicaltest.TestStep { 256 | return logicaltest.TestStep{ 257 | Operation: logical.ReadOperation, 258 | Path: "creds/" + role, 259 | Check: func(resp *logical.Response) error { 260 | if resp == nil { 261 | return fmt.Errorf("response is nil") 262 | } 263 | var d struct { 264 | Username string `mapstructure:"username"` 265 | Password string `mapstructure:"password"` 266 | URL string `mapstructure:"url"` 267 | } 268 | if err := mapstructure.Decode(resp.Data, &d); err != nil { 269 | return err 270 | } 271 | // check that generated user can login 272 | conn := splunk.NewTestSplunkClient(d.URL, d.Username, d.Password) 273 | _, _, err := conn.Introspection.ServerInfo() 274 | assert.NilError(t, err) 275 | 276 | // XXXX check that generated user is deleted if lease expires 277 | return nil 278 | }, 279 | } 280 | } 281 | 282 | func testAccStepCredsReadMultiBadConfig(t *testing.T, role string) logicaltest.TestStep { 283 | return logicaltest.TestStep{ 284 | Operation: logical.ReadOperation, 285 | Path: "creds/" + role + "/someNonExistentNodeID", 286 | ErrorOk: true, 287 | Check: func(resp *logical.Response) error { 288 | if resp == nil { 289 | return fmt.Errorf("response is nil") 290 | } 291 | assert.Error(t, resp.Error(), `host "someNonExistentNodeID" not found`) 292 | return nil 293 | }, 294 | } 295 | } 296 | 297 | func testAccRotateRoot(t *testing.T, conn string) logicaltest.TestStep { 298 | return logicaltest.TestStep{ 299 | Operation: logical.UpdateOperation, 300 | Path: "rotate-root/" + conn, 301 | Check: func(resp *logical.Response) error { 302 | if resp == nil { 303 | return fmt.Errorf("response is nil") 304 | } 305 | var d struct { 306 | Username string `mapstructure:"username"` 307 | } 308 | if err := mapstructure.Decode(resp.Data, &d); err != nil { 309 | return err 310 | } 311 | assert.Assert(t, d.Username != "") 312 | return nil 313 | }, 314 | } 315 | } 316 | 317 | func testAccStepRoleRead(t *testing.T, role string, config roleConfig) logicaltest.TestStep { 318 | return logicaltest.TestStep{ 319 | Operation: logical.ReadOperation, 320 | Path: rolesPrefix + role, 321 | Check: func(resp *logical.Response) error { 322 | if resp == nil { 323 | return fmt.Errorf("response is nil") 324 | } 325 | 326 | expected := config.toResponseData() 327 | assert.DeepEqual(t, expected, resp.Data) 328 | return nil 329 | }, 330 | } 331 | } 332 | 333 | func testAccStepRoleDelete(t *testing.T, role string) logicaltest.TestStep { 334 | return logicaltest.TestStep{ 335 | Operation: logical.DeleteOperation, 336 | Path: rolesPrefix + role, 337 | } 338 | } 339 | 340 | // Helpers 341 | func testNewSplunkBackend(t *testing.T) (logical.Backend, error) { 342 | t.Helper() 343 | if splunk.TestGlobalSplunkClient(t) == nil { 344 | t.SkipNow() 345 | } 346 | config := logical.TestBackendConfig() 347 | config.StorageView = &logical.InmemStorage{} 348 | return Factory(context.Background(), config) 349 | } 350 | -------------------------------------------------------------------------------- /clients/splunk/access.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | // AccessControlService encapsulates the Access Control portion of the Splunk API. 4 | type AccessControlService struct { 5 | client *Client 6 | Authentication *AuthenticationService 7 | } 8 | 9 | func newAccessControlService(client *Client) *AccessControlService { 10 | return &AccessControlService{ 11 | client: client, 12 | Authentication: newAuthenticationService(client.New()), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /clients/splunk/auth.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | // AuthenticationService encapsulates the Authentication portion of the Splunk API. 4 | type AuthenticationService struct { 5 | client *Client 6 | authClient *Client 7 | Users *UserService 8 | } 9 | 10 | func newAuthenticationService(client *Client) *AuthenticationService { 11 | base := client.New().Path("authentication/") 12 | return &AuthenticationService{ 13 | client: base, 14 | authClient: client.New().Path("auth/"), 15 | Users: newUserService(base.New()), 16 | } 17 | } 18 | 19 | type userCredentials struct { 20 | Username string `url:"username"` 21 | Password string `url:"password"` 22 | } 23 | 24 | // LoginResponse is returned from Login() calls. 25 | type LoginResponse struct { 26 | APIError 27 | SessionKey string `json:"sessionKey"` 28 | } 29 | 30 | // Login returns a valid session key, or an error. 31 | func (s *AuthenticationService) Login(username, password string) (*LoginResponse, error) { 32 | creds := userCredentials{username, password} 33 | apiResp := &LoginResponse{} 34 | apiErr := &APIError{} 35 | 36 | _, err := s.authClient.New().BodyForm(&creds).Post("login").Receive(apiResp, apiErr) 37 | if err != nil || !apiErr.Empty() { // XXX check fatal 38 | return nil, relevantError(err, apiErr) 39 | } 40 | return apiResp, err 41 | } 42 | -------------------------------------------------------------------------------- /clients/splunk/auth_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestAuthenticationService(t *testing.T) { 10 | svc := TestGlobalSplunkClient(t).AccessControl.Authentication 11 | assert.Assert(t, svc != nil) 12 | } 13 | 14 | func TestAuthenticationService_Login(t *testing.T) { 15 | svc := TestGlobalSplunkClient(t).AccessControl.Authentication 16 | username := testGlobalSplunkConn.Params().ClientID 17 | password := testGlobalSplunkConn.Params().ClientSecret 18 | resp, err := svc.Login(username, password) 19 | assert.NilError(t, err) 20 | assert.Assert(t, len(resp.SessionKey) > 0) 21 | t.Logf("session key for %q: %v", username, resp.SessionKey) 22 | } 23 | 24 | func TestAuthenticationService_Login_Failed(t *testing.T) { 25 | svc := TestGlobalSplunkClient(t).AccessControl.Authentication 26 | _, err := svc.Login("", "") 27 | assert.Error(t, err, "WARN splunk: Login failed") 28 | } 29 | -------------------------------------------------------------------------------- /clients/splunk/client.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/dghubble/sling" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | // The Client type wraps the underlying API transport. 14 | type Client struct { 15 | *sling.Sling 16 | } 17 | 18 | // APIParams provides the configuration for setting up a new API client with the NewClient() function. 19 | type APIParams struct { 20 | BaseURL string 21 | UserAgent string 22 | TokenTTL time.Duration 23 | 24 | // pass in an actual OAuth2 client, if supported by Splunk; if nil, use Splunk's basic auth/sessionkey token flow 25 | AuthClient *http.Client 26 | oauth2.Config 27 | } 28 | 29 | // defaultAPIParams fills in default values for APIParams. It is called automatically when instantiating a new Client. 30 | func (p *APIParams) defaultAPIParams(ctx context.Context) { 31 | if p.BaseURL == "" { 32 | p.BaseURL = "https://localhost:8089" 33 | } 34 | if p.UserAgent == "" { 35 | p.UserAgent = "go-splunk" 36 | } 37 | if p.AuthClient == nil { 38 | p.AuthClient = oauth2.NewClient(ctx, p.TokenSource(ctx)) 39 | } 40 | if p.TokenTTL.Nanoseconds() == 0 { 41 | // default for Splunk is 60 min, we keep a default 15 min buffer 42 | p.TokenTTL = time.Duration(45) * time.Minute 43 | } 44 | } 45 | 46 | // TokenSource returns a TokenSource using the configuration 47 | // in params and the HTTP client from the provided context. 48 | func (p *APIParams) TokenSource(ctx context.Context) oauth2.TokenSource { 49 | return oauth2.ReuseTokenSource(nil, splunkSource{ctx, p}) 50 | } 51 | 52 | // NewClient creates a new transport for the Splunk API. 53 | func (p *APIParams) NewClient(ctx context.Context) *Client { 54 | p.defaultAPIParams(ctx) 55 | 56 | sling := sling.New().Client(p.AuthClient).Base(p.BaseURL) 57 | // changing output mode requires changing response unmarshalling as well 58 | sling.QueryStruct(jsonOutputMode).Set("Accept", "application/json") 59 | sling.Set("User-Agent", p.UserAgent) 60 | 61 | return &Client{sling} 62 | } 63 | 64 | type splunkSource struct { 65 | ctx context.Context 66 | params *APIParams 67 | } 68 | 69 | // Token returns a valid session token. Cached tokens are reused until they expire, then a new token is requested. 70 | func (ss splunkSource) Token() (*oauth2.Token, error) { 71 | // Obtaining a session token uses the same API conventions as the rest of the API, 72 | // hence we use the same API client, but without authentication (otherwise, we'd create a loop) 73 | p := &APIParams{ 74 | BaseURL: ss.params.BaseURL, 75 | UserAgent: ss.params.UserAgent + "/no-auth", 76 | // nil TokenSource => no auth 77 | // XXX Q: why use oauth2.NewClient in the first place? 78 | // A: to get at the underlying context client 79 | AuthClient: oauth2.NewClient(ss.ctx, nil), 80 | } 81 | // one-time use, full API instantiation; however, the token gets cached, and this method is called infrequently 82 | resp, err := p.NewAPI(ss.ctx).AccessControl.Authentication.Login(ss.params.ClientID, ss.params.ClientSecret) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | token := &oauth2.Token{ 88 | AccessToken: resp.SessionKey, 89 | TokenType: "Splunk", 90 | Expiry: time.Now().Add(ss.params.TokenTTL), 91 | } 92 | return token, nil 93 | } 94 | 95 | // The following functions cover the majority of modifications of the underlying transport. 96 | // We wrap them to make their use easier. Other modifications have to be performed directly 97 | // on the underlying transport. 98 | 99 | // New returns a copy of a Client for creating a new client with properties 100 | // from a parent client. 101 | // 102 | // Note that query and body values are copied so if pointer values are used, 103 | // mutating the original value will mutate the value within the child client. 104 | func (c *Client) New() *Client { 105 | return &Client{c.Sling.New()} 106 | } 107 | 108 | // Path extends the current API client with the given path by resolving the reference to 109 | // an absolute URL. If parsing errors occur, the client is left unmodified. 110 | func (c *Client) Path(pathURL string) *Client { 111 | c.Sling.Path(pathURL) 112 | return c 113 | } 114 | 115 | // Receive kicks off an API call to the underlying transport. 116 | // It attempts to deserialize a value into v, if there is neither a transport error nor an API error. 117 | // Otherwise, the error is returned. 118 | // This function also returns the full API response. 119 | func Receive(sling *sling.Sling, v interface{}) (*Response, error) { 120 | apiResp := &Response{} 121 | apiErr := &APIError{} 122 | resp, err := sling.Receive(apiResp, apiErr) 123 | apiResp.HTTPResponse = resp 124 | if err != nil || !apiErr.Empty() { 125 | return apiResp, relevantError(err, apiErr) 126 | } 127 | 128 | if err = json.Unmarshal(apiResp.Entry, v); err != nil { 129 | return apiResp, relevantError(err, apiErr) 130 | } 131 | 132 | return apiResp, relevantError(nil, apiErr) 133 | } 134 | -------------------------------------------------------------------------------- /clients/splunk/client_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestTokenSource_Token(t *testing.T) { 10 | ctx := TestDefaultContext() 11 | conn := TestGlobalSplunkClient(t) 12 | params := conn.Params() 13 | tok, err := params.TokenSource(ctx).Token() 14 | assert.NilError(t, err) 15 | assert.Assert(t, len(tok.AccessToken) > 0) 16 | } 17 | -------------------------------------------------------------------------------- /clients/splunk/deployment.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | // DeploymentService encapsulates the Deployment portion of the Splunk API 4 | type DeploymentService struct { 5 | client *Client 6 | } 7 | 8 | func newDeploymentService(client *Client) *DeploymentService { 9 | return &DeploymentService{ 10 | client: client, 11 | } 12 | } 13 | 14 | var ( 15 | ServerInfoEntryFilterDefault *PaginationFilter 16 | 17 | ServerInfoEntryFilterMinimal *PaginationFilter = &PaginationFilter{ 18 | Filter: []string{"host", "host_fqdn", "server_roles"}, 19 | } 20 | ) 21 | 22 | // SearchPeers returns information about all search peers 23 | func (d *DeploymentService) SearchPeers(filter *PaginationFilter) ([]ServerInfoEntry, *Response, error) { 24 | var info []ServerInfoEntry 25 | sling := d.client.New().Get("search/distributed/peers") 26 | if filter != ServerInfoEntryFilterDefault { 27 | sling = sling.QueryStruct(filter) 28 | } 29 | resp, err := Receive(sling, &info) 30 | return info, resp, err 31 | } 32 | -------------------------------------------------------------------------------- /clients/splunk/error.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import "fmt" 4 | 5 | // The APIError type encapsulates API errors and status responses. 6 | type APIError struct { 7 | Messages []APIErrorMessage `json:"messages"` 8 | } 9 | 10 | // The APIErrorMessage type encapsulates a single API error or status message. 11 | type APIErrorMessage struct { 12 | Type string `json:"type"` 13 | Text string `json:"text"` 14 | Code string `json:"code"` 15 | } 16 | 17 | // Error is an implementation of the error interface 18 | func (e APIError) Error() string { 19 | if len(e.Messages) > 0 { 20 | err := e.Messages[0] // XXX concat Messages 21 | s := fmt.Sprintf("%s splunk: %s", err.Type, err.Text) 22 | return s 23 | } 24 | return "" 25 | } 26 | 27 | // Empty returns true if empty. Otherwise, at least 1 error message is 28 | // present and false is returned. 29 | func (e APIError) Empty() bool { 30 | return len(e.Messages) == 0 31 | } 32 | 33 | // relevantError returns any non-nil http-related error (creating the request, 34 | // getting the response, decoding) if any. If the decoded apiError is non-zero 35 | // the apiError is returned. Otherwise, no errors occurred, returns nil. 36 | func relevantError(httpError error, apiError *APIError) error { 37 | if httpError != nil { 38 | return httpError 39 | } 40 | if apiError.Empty() { 41 | return nil 42 | } 43 | return apiError 44 | } 45 | -------------------------------------------------------------------------------- /clients/splunk/introspection.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | // IntrospectionService encapsulates the Introspection portion of the Splunk API. 4 | type IntrospectionService struct { 5 | client *Client 6 | } 7 | 8 | func newIntrospectionService(client *Client) *IntrospectionService { 9 | return &IntrospectionService{ 10 | client: client, 11 | } 12 | } 13 | 14 | // ServerInfoEntry is returned from ServerInfo() calls. 15 | // 16 | // BUG(mweber): this type is incomplete. 17 | type ServerInfoEntry struct { 18 | EntryMetadata 19 | Content struct { 20 | ActiveLicenseGroup string `json:"activeLicenseGroup"` 21 | // XXX ... 22 | Build string `json:"build"` 23 | CPUArch string `json:"cpu_arch"` 24 | GUID string `json:"guid"` 25 | Host string `json:"host"` 26 | HostFQDN string `json:"host_fqdn"` 27 | IsFree bool `json:"isFree"` 28 | IsTrial bool `json:"isTrial"` 29 | // XXX ... 30 | Roles []string `json:"server_roles"` 31 | ServerName string `json:"serverName"` 32 | StartupTime Timestamp `json:"startup_time"` 33 | Version string `json:"version"` 34 | } `json:"content"` 35 | } 36 | 37 | // ServerInfo returns information about the Splunk instance. 38 | func (s *IntrospectionService) ServerInfo() ([]ServerInfoEntry, *Response, error) { 39 | info := make([]ServerInfoEntry, 0) 40 | resp, err := Receive(s.client.New().Get("server/info"), &info) 41 | return info, resp, err 42 | } 43 | -------------------------------------------------------------------------------- /clients/splunk/introspection_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func testIntrospectionService(t *testing.T) *IntrospectionService { 10 | return TestGlobalSplunkClient(t).Introspection 11 | } 12 | 13 | func TestIntrospectionService_ServerInfo(t *testing.T) { 14 | s := testIntrospectionService(t) 15 | 16 | info, resp, err := s.ServerInfo() 17 | assert.NilError(t, err) 18 | assert.Equal(t, len(info), 1) 19 | assert.Assert(t, info[0].ID != "") 20 | assert.Assert(t, len(info[0].Links) > 0) 21 | assert.Equal(t, resp.Paging.Offset, 0) 22 | _, build := resp.Generator["build"] 23 | assert.Assert(t, build) 24 | _, version := resp.Generator["version"] 25 | assert.Assert(t, version) 26 | 27 | t.Logf("%+v", info) 28 | } 29 | -------------------------------------------------------------------------------- /clients/splunk/properties.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | // PropertiesService encapsulates Splunk Properties API 14 | 15 | type PropertiesService struct { 16 | client *Client 17 | } 18 | 19 | func newPropertiesService(client *Client) *PropertiesService { 20 | return &PropertiesService{ 21 | client: client, 22 | } 23 | } 24 | 25 | type Entry struct { 26 | Value string 27 | } 28 | 29 | // stringResponseDecoder decodes http response string 30 | // Properties API operates on particular key in the configuration file. 31 | // CRUD for properties API returns JSON/XML encoded response for error cases and returns a string response for success 32 | type stringResponseDecoder struct { 33 | } 34 | 35 | func getPropertiesUri(file string, stanza string, key string) string { 36 | return fmt.Sprintf("properties/%s/%s/%s", url.PathEscape(file), url.PathEscape(stanza), url.PathEscape(key)) 37 | } 38 | 39 | func (d stringResponseDecoder) Decode(resp *http.Response, v interface{}) error { 40 | body, err := ioutil.ReadAll(resp.Body) 41 | if err != nil { 42 | return err 43 | } 44 | if 200 <= resp.StatusCode && resp.StatusCode <= 299 { 45 | tempEntry := &Entry{ 46 | Value: string(body), 47 | } 48 | vVal, tempVal := reflect.ValueOf(v), reflect.ValueOf(tempEntry) 49 | vVal.Elem().Set(tempVal.Elem()) 50 | return nil 51 | } 52 | return json.Unmarshal(body, v) 53 | } 54 | 55 | // UpdateKey updates value for specified key from the specified stanza in the configuration file 56 | func (p *PropertiesService) UpdateKey(file string, stanza string, key string, value string) (*string, *http.Response, error) { 57 | apiError := &APIError{} 58 | body := strings.NewReader(fmt.Sprintf("value=%s", value)) 59 | resp, err := p.client.New().Post( 60 | getPropertiesUri(file, stanza, key)).Body(body).ResponseDecoder(stringResponseDecoder{}).Receive(nil, apiError) 61 | if err != nil || !apiError.Empty() { 62 | return nil, resp, relevantError(err, apiError) 63 | } 64 | return nil, resp, relevantError(err, apiError) 65 | } 66 | 67 | // GetKey returns value for the given key from the specified stanza in the configuration file 68 | func (p *PropertiesService) GetKey(file string, stanza string, key string) (*string, *http.Response, error) { 69 | apiError := &APIError{} 70 | output := &Entry{} 71 | resp, err := p.client.New().Get( 72 | getPropertiesUri(file, stanza, key)).ResponseDecoder(stringResponseDecoder{}).Receive(output, apiError) 73 | if err != nil || !apiError.Empty() { 74 | return nil, resp, relevantError(err, apiError) 75 | } 76 | return &output.Value, resp, relevantError(err, apiError) 77 | } 78 | -------------------------------------------------------------------------------- /clients/splunk/properties_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestPropertiesService_GetKey(t *testing.T) { 10 | propertiesSvc := TestGlobalSplunkClient(t).Properties 11 | 12 | // Negative cases 13 | _, response, err := propertiesSvc.GetKey("foo", "bar", "key") 14 | assert.ErrorContains(t, err, "splunk: foo does not exist") 15 | assert.Equal(t, response.StatusCode, 404) 16 | _, response, err = propertiesSvc.GetKey("b/a/z", "b-ar", "k-ey") 17 | assert.ErrorContains(t, err, "ERROR splunk: Directory traversal risk in /nobody/system/b/a/z at segment \"b/a/z\"") 18 | assert.Equal(t, response.StatusCode, 403) 19 | _, response, err = propertiesSvc.GetKey("foo-bar", "b/a/z", "k-ey") 20 | assert.ErrorContains(t, err, "splunk: foo-bar does not exist") 21 | assert.Equal(t, response.StatusCode, 404) 22 | _, response, err = propertiesSvc.UpdateKey("foo", "bar", "pass4SymmKey", "bar") 23 | assert.ErrorContains(t, err, "splunk: bar does not exist") 24 | assert.Equal(t, response.StatusCode, 404) 25 | 26 | _, response, _ = propertiesSvc.GetKey("server", "general", "pass4SymmKey") 27 | assert.Equal(t, response.StatusCode, 200) 28 | 29 | // Update value for pass4SymmKey and check if the new value is reflected 30 | _, response, _ = propertiesSvc.UpdateKey("server", "general", "pass4SymmKey", "bar") 31 | assert.Equal(t, response.StatusCode, 200) 32 | currentValue, response, _ := propertiesSvc.GetKey("server", "general", "pass4SymmKey") 33 | assert.Equal(t, response.StatusCode, 200) 34 | assert.Equal(t, *currentValue, "bar") 35 | } 36 | -------------------------------------------------------------------------------- /clients/splunk/splunk.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // API https://docs.splunk.com/Documentation/Splunk/8.0.2/RESTREF/RESTprolog#Request_and_response_details 11 | type PaginationFilter struct { 12 | Count int `url:"count,omitempty"` // NOTE: we omit zero value, since this is already set in outputMode 13 | Filter []string `url:"f,omitempty"` 14 | Offset int `url:"offset,omitempty"` 15 | Search string `url:"search,omitempty"` 16 | SortDir string `url:"sort_dir,omitempty"` 17 | SortKey string `url:"sort_key,omitempty"` 18 | SortMode string `url:"sort_mode,omitempty"` 19 | Summarize bool `url:"summarize,omitempty"` 20 | } 21 | 22 | type outputMode struct { 23 | Mode string `url:"output_mode,omitempty"` 24 | // by default, we do not want any pagination, 25 | // override with PaginationFilter 26 | Count int `url:"count"` 27 | } 28 | 29 | var jsonOutputMode = outputMode{"json", 0} 30 | 31 | // API https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTprolog 32 | type API struct { 33 | params *APIParams 34 | client *Client 35 | Introspection *IntrospectionService 36 | AccessControl *AccessControlService 37 | Properties *PropertiesService 38 | Deployment *DeploymentService 39 | // XXX ... 40 | } 41 | 42 | // Params returns the configuration for this API instance. 43 | func (api *API) Params() *APIParams { 44 | return api.params 45 | } 46 | 47 | // NewAPI creates a new instance to access the API of the configured Splunk instance. 48 | func (params *APIParams) NewAPI(ctx context.Context) *API { 49 | client := params.NewClient(ctx) 50 | paramsCopy := *params 51 | 52 | return &API{ 53 | params: ¶msCopy, 54 | client: client.Path("services/"), 55 | Introspection: newIntrospectionService(client.New()), 56 | AccessControl: newAccessControlService(client.New()), 57 | Properties: newPropertiesService(client.New()), 58 | Deployment: newDeploymentService(client.New()), 59 | } 60 | } 61 | 62 | // Response https://docs.splunk.com/Documentation/Splunk/latest/RESTUM/RESTusing#Atom_Feed_response 63 | type Response struct { 64 | Title string `json:"title"` 65 | ID string `json:"id"` 66 | Updated time.Time `json:"updated"` 67 | Generator map[string]string `json:"generator"` 68 | Author string `json:"author"` 69 | Links map[string]string `json:"links"` // XXX doc mismatch 70 | Messages []APIErrorMessage `json:"messages"` 71 | Paging Paging `json:"paging"` // XXX doc mismatch 72 | Entry json.RawMessage `json:"entry"` 73 | 74 | HTTPResponse *http.Response 75 | } 76 | 77 | // The Paging type encapsulates paging information provided by the API. 78 | // 79 | // See also: https://docs.splunk.com/Documentation/Splunk/latest/RESTUM/RESTusing#Atom_Feed_response 80 | type Paging struct { 81 | Offset int `json:"offset"` 82 | PerPage int `json:"perPage"` 83 | Total int `json:"total"` 84 | } 85 | 86 | // EntryMetadata https://docs.splunk.com/Documentation/Splunk/latest/RESTUM/RESTusing#Response_elements 87 | type EntryMetadata struct { 88 | ACL // XXX doc mismatch 89 | Messages []APIErrorMessage `json:"messages"` 90 | Title string `json:"title"` 91 | ID string `json:"id"` 92 | Updated time.Time `json:"updated"` 93 | Links map[string]string `json:"links"` // XXX doc mismatch 94 | Author string `json:"author"` 95 | } 96 | 97 | // ACL https://docs.splunk.com/Documentation/Splunk/7.2.4/RESTUM/RESTusing#Access_Control_List 98 | type ACL struct { 99 | App string `json:"app"` 100 | CanChangePerms bool `json:"can_change_perms"` 101 | CanShareApp bool `json:"can_share_app"` 102 | CanShareGlobal bool `json:"can_share_global"` 103 | CanShareUser bool `json:"can_share_user"` 104 | CanWrite bool `json:"can_write"` 105 | Modifiable bool `json:"modifiable"` // XXX doc mismatch 106 | Owner string `json:"owner"` 107 | Perms struct { 108 | Read []string `json:"read"` 109 | Write []string `json:"write"` 110 | } `json:"perms"` 111 | Removable bool `json:"removable"` 112 | Sharing string `json:"sharing"` 113 | } 114 | 115 | // Bool is a helper routine that allocates a new bool value 116 | // to store v and returns a pointer to it. 117 | func Bool(v bool) *bool { return &v } 118 | 119 | // Int is a helper routine that allocates a new int value 120 | // to store v and returns a pointer to it. 121 | func Int(v int) *int { return &v } 122 | 123 | // Int64 is a helper routine that allocates a new int64 value 124 | // to store v and returns a pointer to it. 125 | func Int64(v int64) *int64 { return &v } 126 | 127 | // Float64 is a helper routine that allocates a new float64 value 128 | // to store v and returns a pointer to it. 129 | func Float64(v float64) *float64 { return &v } 130 | 131 | // String is a helper routine that allocates a new string value 132 | // to store v and returns a pointer to it. 133 | func String(v string) *string { return &v } 134 | -------------------------------------------------------------------------------- /clients/splunk/splunk_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | 8 | "github.com/google/go-querystring/query" 9 | ) 10 | 11 | func TestOutputMode(t *testing.T) { 12 | val, err := query.Values(jsonOutputMode) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | t.Log(val) 17 | } 18 | 19 | func TestAPIService(t *testing.T) { 20 | svc := TestGlobalSplunkClient(t) 21 | assert.Assert(t, svc != nil) 22 | } 23 | -------------------------------------------------------------------------------- /clients/splunk/testing.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "os" 11 | "testing" 12 | "time" 13 | 14 | "github.com/hashicorp/go-uuid" 15 | "github.com/ory/dockertest/v3" 16 | "golang.org/x/oauth2" 17 | ) 18 | 19 | const ( 20 | testDefaultSplunkContainer = "splunk/splunk" 21 | testDefaultSplunkVersion = "latest" /// XXX configurable 22 | testDefaultAdmin = "admin" 23 | testDefaultPassword = "test1234" // minimally satisfies Splunk password requirements 24 | testDefaultPort = "8089/tcp" 25 | ) 26 | 27 | var testGlobalSplunkConn *API 28 | 29 | // TestMainRunner is an interface for running Tests. 30 | // 31 | // This interface is implemented by testing.M. 32 | type TestMainRunner interface { 33 | Run() (status int) 34 | } 35 | 36 | func shouldRunIntegrationTests() bool { 37 | return !testing.Short() 38 | } 39 | 40 | // WithTestMainSetup provides global test setup for integration tests with Splunk. 41 | // 42 | // During the life-time of a test run, a Splunk container is provided, and a client, 43 | // which is configured to access the Splunk instance. Configuration details 44 | // (e.g., admin user and password) can be obtained from the client. 45 | // 46 | // See also: TestGlobalSplunkClient 47 | func WithTestMainSetup(runner TestMainRunner) { 48 | // os.Exit() prevents deferred functions from running, hence these shenanigans 49 | var ( 50 | err error 51 | status int 52 | ) 53 | defer func() { 54 | if err != nil { 55 | log.Fatalln(err) 56 | } 57 | os.Exit(status) 58 | }() 59 | 60 | flag.Parse() 61 | if shouldRunIntegrationTests() { 62 | var ( 63 | cleanup func() 64 | conn *API 65 | ) 66 | log.Print("starting new service...") 67 | cleanup, conn, err = NewTestSplunkServiceWithTempAdmin() 68 | defer cleanup() 69 | if err != nil { 70 | return 71 | } 72 | log.Printf("using Splunk service at %s", conn.Params().BaseURL) 73 | testGlobalSplunkConn = conn 74 | } 75 | status = runner.Run() 76 | } 77 | 78 | // TestGlobalSplunkClient returns a Splunk API client that is configured to access a test Splunk instance. 79 | // 80 | // If no Splunk instance has been set up via WithTestMainSetup, the calling test is skipped. 81 | func TestGlobalSplunkClient(t *testing.T) *API { 82 | t.Helper() 83 | if testGlobalSplunkConn == nil { 84 | t.SkipNow() 85 | } 86 | return testGlobalSplunkConn 87 | } 88 | 89 | // TestDefaultContext returns a context set up for use in a Splunk client. 90 | // 91 | // See also: APIParams.NewAPI 92 | func TestDefaultContext() context.Context { 93 | tr := &http.Transport{ 94 | // #nosec G402 95 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 96 | } 97 | // client is the underlying transport for API calls, including Login (for obtaining session token) 98 | client := &http.Client{ 99 | Transport: tr, 100 | Timeout: time.Duration(1) * time.Minute, 101 | } 102 | return context.WithValue(context.Background(), oauth2.HTTPClient, client) 103 | } 104 | 105 | // NewTestSplunkClient returns a new Splunk API client using the context provided by TestDefaultContext. 106 | func NewTestSplunkClient(url, username, password string) *API { 107 | p := &APIParams{ 108 | BaseURL: url, 109 | Config: oauth2.Config{ 110 | ClientID: username, 111 | ClientSecret: password, 112 | }, 113 | } 114 | return p.NewAPI(TestDefaultContext()) 115 | } 116 | 117 | // NewTestSplunkService spins up a new Splunk service, and returns a Splunk API configured to access it. 118 | // 119 | // If the SPLUNK_ADDR environment variable is set, the tests will run against the specified Splunk. 120 | // If also the SPLUNK_PASSWORD environment variable is set, this password will be used for admin access, 121 | // instead of a default password ("test1234"). 122 | // 123 | // Example: 124 | // export SPLUNK_ADDR='https://localhost:8089' 125 | // export SPLUNK_PASSWORD='SECRET' 126 | func NewTestSplunkService() (cleanup func(), conn *API, err error) { 127 | cleanup = func() {} 128 | url := os.Getenv("SPLUNK_ADDR") 129 | if url != "" { 130 | password := os.Getenv("SPLUNK_PASSWORD") 131 | if password == "" { 132 | password = testDefaultPassword 133 | } 134 | conn = NewTestSplunkClient(url, testDefaultAdmin, password) 135 | return 136 | } 137 | password, err := uuid.GenerateUUID() 138 | if err != nil { 139 | err = fmt.Errorf("error generating password: %w", err) 140 | return 141 | } 142 | 143 | pool, err := dockertest.NewPool("") 144 | if err != nil { 145 | err = fmt.Errorf("Failed to connect to docker: %w", err) 146 | return 147 | } 148 | 149 | env := []string{ 150 | "SPLUNK_START_ARGS=--accept-license", 151 | fmt.Sprintf("SPLUNK_PASSWORD=%s", password), 152 | } 153 | resource, err := pool.Run(testDefaultSplunkContainer, testDefaultSplunkVersion, env) 154 | if err != nil { 155 | err = fmt.Errorf("failed to start local container: %w", err) 156 | return 157 | } 158 | 159 | cleanup = func() { 160 | if err := pool.Purge(resource); err != nil { 161 | log.Printf("failed to cleanup local container: %s", err) 162 | } 163 | } 164 | 165 | url = fmt.Sprintf("https://localhost:%s", resource.GetPort(testDefaultPort)) 166 | conn = NewTestSplunkClient(url, testDefaultAdmin, password) 167 | 168 | // the container seems to take at least one minute to start 169 | pool.MaxWait = time.Duration(2) * time.Minute 170 | err = pool.Retry(func() error { 171 | _, _, err := conn.Introspection.ServerInfo() 172 | return err 173 | }) 174 | if err != nil { 175 | err = fmt.Errorf("Could not connect to Splunk container: %w", err) 176 | return 177 | } 178 | return 179 | } 180 | 181 | // NewTestSplunkServiceWithTempAdmin spins up a new Splunk instance, and also creates a new test admin user. 182 | // 183 | // See also: NewTestSplunkService 184 | func NewTestSplunkServiceWithTempAdmin() (cleanup func(), conn *API, err error) { 185 | cleanup, conn, err = NewTestSplunkService() 186 | if err != nil { 187 | return 188 | } 189 | testUserID, _ := uuid.GenerateUUID() 190 | testUser := fmt.Sprintf("test-admin-%s", testUserID) 191 | testPass, _ := uuid.GenerateUUID() 192 | _, _, err = conn.AccessControl.Authentication.Users.Create(&CreateUserOptions{ 193 | Name: testUser, 194 | Password: testPass, 195 | Roles: []string{"admin"}, 196 | }) 197 | if err != nil { 198 | err = fmt.Errorf("unable to create test user %q: %w", testUser, err) 199 | return 200 | } 201 | 202 | clConn := conn 203 | clCleanup := cleanup 204 | cleanup = func() { 205 | // nolint:errcheck 206 | // #nosec G104 207 | clConn.AccessControl.Authentication.Users.Delete(testUser) 208 | clCleanup() 209 | } 210 | // switch to test (admin) user 211 | conn = NewTestSplunkClient(conn.Params().BaseURL, testUser, testPass) 212 | return 213 | } 214 | -------------------------------------------------------------------------------- /clients/splunk/testmain_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import "testing" 4 | 5 | func TestMain(m *testing.M) { 6 | WithTestMainSetup(m) 7 | } 8 | -------------------------------------------------------------------------------- /clients/splunk/timestamp.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // The Timestamp type is used for (un)marshalling Unix timestamps from JSON. 10 | type Timestamp time.Time 11 | 12 | // MarshalJSON marshals Timestamp structs to JSON. 13 | func (t *Timestamp) MarshalJSON() ([]byte, error) { 14 | ts := time.Time(*t).Unix() 15 | stamp := fmt.Sprint(ts) 16 | 17 | return []byte(stamp), nil 18 | } 19 | 20 | // UnmarshalJSON unmarshals Timestamp structs from JSON. 21 | func (t *Timestamp) UnmarshalJSON(b []byte) error { 22 | ts, err := strconv.Atoi(string(b)) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | *t = Timestamp(time.Unix(int64(ts), 0)) 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /clients/splunk/user.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // UserService encapsulates the User portion of the Splunk API. 8 | type UserService struct { 9 | client *Client 10 | } 11 | 12 | func newUserService(client *Client) *UserService { 13 | return &UserService{ 14 | client: client, 15 | } 16 | } 17 | 18 | // UserEntry is returned from Users() calls. 19 | type UserEntry struct { 20 | EntryMetadata 21 | Name string `json:"name"` 22 | Content struct { 23 | Capabilities []string `json:"capabilities"` 24 | DefaultApp string `json:"defaultApp"` 25 | DefaultAppIsUserOverride *bool `json:"DefaultAppIsUserOverride"` 26 | Email string `json:"email"` 27 | Password string `json:"password"` 28 | RealName string `json:"realname"` 29 | RestartBackgroundJobs *bool `json:"restart_background_jobs"` 30 | Roles []string `json:"roles"` 31 | Type string `json:"type"` 32 | TZ string `json:"tz"` 33 | } `json:"content"` 34 | } 35 | 36 | // Users returns information about all users. 37 | func (s *UserService) Users() ([]UserEntry, *Response, error) { 38 | users := make([]UserEntry, 0) 39 | resp, err := Receive(s.client.New().Get("users"), &users) 40 | return users, resp, err 41 | } 42 | 43 | // The CreateUserOptions type provides options for creating a new user. 44 | type CreateUserOptions struct { 45 | CreateRole *bool `url:"createrole,omitempty"` 46 | DefaultApp string `url:"defaultApp,omitempty"` 47 | Email string `url:"email,omitempty"` 48 | ForceChangePass *bool `url:"force-change-pass,omitempty"` 49 | Name string `url:"name"` 50 | Password string `url:"password,omitempty"` 51 | Realname string `url:"realname,omitempty"` 52 | RestartBackgroundJobs *bool `url:"restart_background_jobs,omitempty"` 53 | Roles []string `url:"roles,omitempty"` 54 | TZ string `url:"tz,omitempty"` 55 | } 56 | 57 | // Create creates a new user, and returns additional meta data. 58 | func (s *UserService) Create(opts *CreateUserOptions) (*UserEntry, *Response, error) { 59 | users := make([]UserEntry, 0) 60 | resp, err := Receive(s.client.New().BodyForm(opts).Post("users"), &users) 61 | if err != nil || len(users) == 0 { 62 | return nil, resp, err 63 | } 64 | return &users[0], resp, err 65 | } 66 | 67 | // The UpdateUserOptions type provides options for updating a user. 68 | type UpdateUserOptions struct { 69 | DefaultApp string `url:"defaultApp,omitempty"` 70 | Email string `url:"email,omitempty"` 71 | ForceChangePass *bool `url:"force-change-pass,omitempty"` 72 | OldPassword string `url:"oldpassword,omitempty"` 73 | Password string `url:"password,omitempty"` 74 | Realname string `url:"realname,omitempty"` 75 | RestartBackgroundJobs *bool `url:"restart_background_jobs,omitempty"` 76 | Roles []string `url:"roles,omitempty"` 77 | TZ string `url:"tz,omitempty"` 78 | } 79 | 80 | // Update updates a user, and returns additional meta data. 81 | func (s *UserService) Update(user string, opts *UpdateUserOptions) (*UserEntry, *Response, error) { 82 | users := make([]UserEntry, 0) 83 | resp, err := Receive(s.client.New().BodyForm(opts).Path("users/").Post(url.PathEscape(user)), &users) 84 | if err != nil || len(users) == 0 { 85 | return nil, resp, err 86 | } 87 | return &users[0], resp, err 88 | } 89 | 90 | // Delete deletes a user, and returns additional meta data. 91 | func (s *UserService) Delete(user string) (*UserEntry, *Response, error) { 92 | users := make([]UserEntry, 0) 93 | resp, err := Receive(s.client.New().Path("users/").Delete(url.PathEscape(user)), &users) 94 | if err != nil || len(users) == 0 { 95 | return nil, resp, err 96 | } 97 | return &users[0], resp, err 98 | } 99 | -------------------------------------------------------------------------------- /clients/splunk/user_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/hashicorp/go-uuid" 9 | 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | const defaultAdminUser = "admin" 14 | 15 | func TestUserService_Users(t *testing.T) { 16 | us := testUserService(t) 17 | 18 | users, _, err := us.Users() 19 | assert.NilError(t, err) 20 | for ii := range users { 21 | if users[ii].Name == defaultAdminUser { 22 | return 23 | } 24 | } 25 | t.Fail() 26 | } 27 | 28 | func TestUserService_Create(t *testing.T) { 29 | userSvc := testUserService(t) 30 | params := testUserParams("") 31 | 32 | user, _, err := userSvc.Create(params) 33 | assert.NilError(t, err) 34 | // nolint:errcheck 35 | defer userSvc.Delete(user.Name) 36 | assert.Equal(t, user.Name, params.Name) 37 | assert.Equal(t, user.Content.Email, params.Email) 38 | } 39 | 40 | func TestUserService_Delete(t *testing.T) { 41 | userSvc := testUserService(t) 42 | params := testUserParams("") 43 | 44 | user, _, err := userSvc.Create(params) 45 | assert.NilError(t, err) 46 | 47 | _, _, err = userSvc.Delete(user.Name) 48 | assert.NilError(t, err) 49 | } 50 | 51 | func TestUserService_Update_Email(t *testing.T) { 52 | userSvc := testUserService(t) 53 | params := testUserParams("") 54 | 55 | user, _, err := userSvc.Create(params) 56 | assert.NilError(t, err) 57 | // nolint:errcheck 58 | defer userSvc.Delete(user.Name) 59 | assert.Equal(t, user.Name, params.Name) 60 | 61 | user, _, err = userSvc.Update(user.Name, &UpdateUserOptions{ 62 | Email: "changed@example.com", 63 | }) 64 | assert.NilError(t, err) 65 | assert.Equal(t, user.Content.Email, "changed@example.com") 66 | } 67 | 68 | func TestUserService_Update_Password(t *testing.T) { 69 | userSvc := testUserService(t) 70 | params := testUserParams("") 71 | 72 | user, _, err := userSvc.Create(params) 73 | assert.NilError(t, err) 74 | // nolint:errcheck 75 | defer userSvc.Delete(user.Name) 76 | assert.NilError(t, err) 77 | assert.Equal(t, user.Name, params.Name) 78 | 79 | _, _, err = userSvc.Update(user.Name, &UpdateUserOptions{ 80 | Password: "changed1234", 81 | }) 82 | assert.NilError(t, err) 83 | } 84 | 85 | func TestUserService_Update_MissingOldPassword(t *testing.T) { 86 | userSvc := testUserService(t) 87 | self := testGlobalSplunkConn.Params().ClientID 88 | 89 | _, _, err := userSvc.Update(self, &UpdateUserOptions{ 90 | Password: "changed1234", 91 | }) 92 | assert.Error(t, err, "ERROR splunk: Missing old password.") 93 | } 94 | 95 | func TestUserService_Update_OwnPassword(t *testing.T) { 96 | userSvc := testUserService(t) 97 | 98 | params := testUserParams("") 99 | user, _, err := userSvc.Create(params) 100 | assert.NilError(t, err) 101 | // nolint:errcheck 102 | defer userSvc.Delete(user.Name) 103 | 104 | _, _, err = userSvc.Update(user.Name, &UpdateUserOptions{ 105 | OldPassword: params.Password, 106 | Password: "password", 107 | }) 108 | assert.NilError(t, err) 109 | } 110 | 111 | // Helpers 112 | func testUserService(t *testing.T) *UserService { 113 | return TestGlobalSplunkClient(t).AccessControl.Authentication.Users 114 | } 115 | 116 | func testNewUsername(prefix string) string { 117 | id, err := uuid.GenerateUUID() 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | name := fmt.Sprintf("%s%s", prefix, id) 122 | return name 123 | } 124 | 125 | func testUserParams(name string) *CreateUserOptions { 126 | if name == "" { 127 | name = testNewUsername("testuser-") 128 | } 129 | return &CreateUserOptions{ 130 | Name: name, 131 | Email: fmt.Sprintf("%s@example.com", name), 132 | Password: "test1234", 133 | CreateRole: Bool(true), 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /cmd/vault-plugin-splunk/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/vault/api" 9 | "github.com/hashicorp/vault/sdk/plugin" 10 | 11 | splunk "github.com/splunk/vault-plugin-splunk" 12 | ) 13 | 14 | // nolint: gochecknoglobals 15 | var ( 16 | version = "dev" 17 | commit = "" 18 | date = "" 19 | goVersion = "" 20 | ) 21 | 22 | func main() { 23 | apiClientMeta := &api.PluginAPIClientMeta{} 24 | flags := apiClientMeta.FlagSet() 25 | printVersion := flags.Bool("version", false, "Prints version") 26 | 27 | // all plugins ignore Parse errors 28 | // #nosec G104 29 | // nolint:errcheck 30 | flags.Parse(os.Args[1:]) 31 | 32 | printField := func(field, value string) { 33 | if field != "" && value != "" { 34 | fmt.Printf("%s: %s\n", field, value) 35 | } 36 | } 37 | switch { 38 | case *printVersion: 39 | printField("version", version) 40 | printField("commit", commit) 41 | printField("date", date) 42 | printField("go", goVersion) 43 | os.Exit(0) 44 | } 45 | 46 | tlsConfig := apiClientMeta.GetTLSConfig() 47 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 48 | 49 | err := plugin.Serve(&plugin.ServeOpts{ 50 | BackendFactoryFunc: splunk.Factory, 51 | TLSProviderFunc: tlsProviderFunc, 52 | }) 53 | if err != nil { 54 | logger := hclog.New(&hclog.LoggerOptions{}) 55 | 56 | logger.Error("plugin shutting down", "error", err) 57 | os.Exit(1) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/fatih/structs" 12 | uuid "github.com/hashicorp/go-uuid" 13 | "github.com/hashicorp/vault/sdk/framework" 14 | "github.com/hashicorp/vault/sdk/helper/certutil" 15 | "github.com/hashicorp/vault/sdk/helper/tlsutil" 16 | "github.com/hashicorp/vault/sdk/helper/useragent" 17 | "github.com/hashicorp/vault/sdk/logical" 18 | "golang.org/x/oauth2" 19 | 20 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 21 | ) 22 | 23 | const ( 24 | respErrEmptyName = `missing or empty "name" parameter` 25 | ) 26 | 27 | type splunkConfig struct { 28 | ID string `json:"id" structs:"id"` 29 | Username string `json:"username" structs:"username"` 30 | Password string `json:"password" structs:"password"` 31 | URL string `json:"url" structs:"url"` 32 | IsStandalone bool `json:"is_standalone" structs:"is_standalone"` 33 | AllowedRoles []string `json:"allowed_roles" structs:"allowed_roles"` 34 | Verify bool `json:"verify" structs:"verify"` 35 | InsecureTLS bool `json:"insecure_tls" structs:"insecure_tls"` 36 | Certificate string `json:"certificate" structs:"certificate"` 37 | PrivateKey string `json:"private_key" structs:"private_key"` 38 | CAChain []string `json:"ca_chain" structs:"ca_chain"` 39 | RootCA []string `json:"root_ca" structs:"root_ca"` 40 | TLSMinVersion string `json:"tls_min_version" structs:"tls_min_version"` 41 | ConnectTimeout time.Duration `json:"connect_timeout" structs:"connect_timeout"` 42 | } 43 | 44 | func (config *splunkConfig) toResponseData() map[string]interface{} { 45 | data := structs.New(config).Map() 46 | data["connect_timeout"] = int64(config.ConnectTimeout.Seconds()) 47 | data["password"] = "n/a" 48 | data["private_key"] = "n/a" 49 | return data 50 | } 51 | 52 | func (config *splunkConfig) toMinimalResponseData() map[string]interface{} { 53 | data := map[string]interface{}{ 54 | "id": config.ID, 55 | "username": config.Username, 56 | // "X-DEBUG-password": config.Password, 57 | "url": config.URL, 58 | } 59 | return data 60 | } 61 | 62 | func (config *splunkConfig) store(ctx context.Context, s logical.Storage, name string) (err error) { 63 | oldConfigID := config.ID 64 | if oldConfigID != "" { 65 | // we cannot reliably clean up the old cached connection, since some in-progress operation 66 | // might just call ensureConnection and reinstate it. The window for this is the max life-time 67 | // of any request that was in flight during this store operation. 68 | // 69 | // Therefore, we'll have the WAL clean up after some time that's longer than the longest 70 | // expected response time. 71 | var walID string 72 | walID, err = framework.PutWAL(ctx, s, walTypeConn, &walConnection{oldConfigID}) 73 | if err != nil { 74 | return fmt.Errorf("unable to create WAL for deleting cached connection: %w", err) 75 | } 76 | 77 | defer func() { 78 | if err != nil { 79 | // config was not stored => cancel cleanup 80 | // #nosec G104 81 | // nolint:errcheck 82 | framework.DeleteWAL(ctx, s, walID) 83 | } 84 | }() 85 | } 86 | 87 | config.ID, err = uuid.GenerateUUID() 88 | if err != nil { 89 | return fmt.Errorf("error generating new configuration ID: %w", err) 90 | } 91 | 92 | var newEntry *logical.StorageEntry 93 | newEntry, err = logical.StorageEntryJSON(fmt.Sprintf("config/%s", name), config) 94 | if err != nil { 95 | return fmt.Errorf("error writing config/%s JSON: %w", name, err) 96 | } 97 | if err = s.Put(ctx, newEntry); err != nil { 98 | return fmt.Errorf("error saving new config/%s: %w", name, err) 99 | } 100 | 101 | // if config.Verify { 102 | // config.verifyConnection(ctx, s, name) 103 | // } 104 | 105 | return err 106 | } 107 | 108 | func connectionConfigExists(ctx context.Context, s logical.Storage, name string) (bool, error) { 109 | if name == "" { 110 | return false, fmt.Errorf(respErrEmptyName) 111 | } 112 | 113 | entry, err := s.Get(ctx, fmt.Sprintf("config/%s", name)) 114 | if err != nil { 115 | return false, fmt.Errorf("error reading connection configuration: %w", err) 116 | } 117 | return entry != nil, nil 118 | } 119 | 120 | func connectionConfigLoad(ctx context.Context, s logical.Storage, name string) (*splunkConfig, error) { 121 | if name == "" { 122 | return nil, fmt.Errorf(respErrEmptyName) 123 | } 124 | entry, err := s.Get(ctx, fmt.Sprintf("config/%s", name)) 125 | if err != nil { 126 | return nil, fmt.Errorf("error reading connection configuration: %w", err) 127 | } 128 | if entry == nil { 129 | return nil, fmt.Errorf("connection configuration not found: %q", name) 130 | } 131 | 132 | config := splunkConfig{} 133 | if err := entry.DecodeJSON(&config); err != nil { 134 | return nil, err 135 | } 136 | return &config, nil 137 | } 138 | 139 | func (config *splunkConfig) newConnection(ctx context.Context) (*splunk.API, error) { 140 | p := &splunk.APIParams{ 141 | BaseURL: config.URL, 142 | UserAgent: useragent.String(), 143 | Config: oauth2.Config{ 144 | ClientID: config.Username, 145 | ClientSecret: config.Password, 146 | }, 147 | } 148 | 149 | tlsConfig, err := config.tlsConfig() 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | tr := &http.Transport{ 155 | TLSClientConfig: tlsConfig, 156 | Proxy: http.ProxyFromEnvironment, 157 | } 158 | 159 | // client is the underlying transport for API calls, including Login (for obtaining session token) 160 | client := &http.Client{ 161 | Transport: tr, 162 | Timeout: config.ConnectTimeout, 163 | } 164 | ctx = context.WithValue(ctx, oauth2.HTTPClient, client) 165 | 166 | return p.NewAPI(ctx), nil 167 | } 168 | 169 | func (config *splunkConfig) tlsConfig() (tlsConfig *tls.Config, err error) { 170 | if len(config.Certificate) > 0 || (config.CAChain != nil && len(config.CAChain) > 0) { 171 | if len(config.Certificate) > 0 && len(config.PrivateKey) == 0 { 172 | return nil, fmt.Errorf("found certificate for TLS authentication but no private key") 173 | } 174 | 175 | certBundle := &certutil.CertBundle{ 176 | Certificate: config.Certificate, 177 | PrivateKey: config.PrivateKey, 178 | CAChain: config.CAChain, 179 | } 180 | parsedCertBundle, err := certBundle.ToParsedCertBundle() 181 | if err != nil { 182 | return nil, fmt.Errorf("failed to parse certificate bundle: %w", err) 183 | } 184 | 185 | tlsConfig, err = parsedCertBundle.GetTLSConfig(certutil.TLSClient) 186 | if err != nil || tlsConfig == nil { 187 | return nil, fmt.Errorf("failed to get TLS configuration: tlsConfig: %#v; %w", tlsConfig, err) 188 | } 189 | } else { 190 | tlsConfig = &tls.Config{ 191 | MinVersion: tls.VersionTLS12, // gosec G402 192 | } 193 | } 194 | 195 | tlsConfig.InsecureSkipVerify = config.InsecureTLS 196 | if config.TLSMinVersion != "" { 197 | var ok bool 198 | if tlsConfig.MinVersion, ok = tlsutil.TLSLookup[config.TLSMinVersion]; !ok { 199 | return nil, fmt.Errorf(`invalid "tls_min_version" in config`) 200 | } 201 | } else { 202 | // MinVersion was not being set earlier. Reset it to 203 | // zero to gracefully handle upgrades. 204 | tlsConfig.MinVersion = 0 205 | } 206 | 207 | if config.RootCA != nil && len(config.RootCA) > 0 { 208 | if tlsConfig.RootCAs == nil { 209 | tlsConfig.RootCAs = x509.NewCertPool() 210 | } 211 | for _, cert := range config.RootCA { 212 | tlsConfig.RootCAs.AppendCertsFromPEM([]byte(cert)) 213 | } 214 | } 215 | 216 | return tlsConfig, nil 217 | } 218 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/splunk/vault-plugin-splunk 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/dghubble/sling v1.4.0 7 | github.com/fatih/structs v1.1.0 8 | github.com/google/go-querystring v1.1.0 9 | github.com/hashicorp/go-hclog v1.2.0 10 | github.com/hashicorp/go-uuid v1.0.2 11 | github.com/hashicorp/vault v1.10.0-rc1 12 | github.com/hashicorp/vault/api v1.4.1 13 | github.com/hashicorp/vault/sdk v0.4.1 14 | github.com/mitchellh/mapstructure v1.4.3 15 | github.com/mr-tron/base58 v1.2.0 16 | github.com/ory/dockertest/v3 v3.8.1 17 | github.com/sethvargo/go-password v0.2.0 18 | golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a 19 | gotest.tools/v3 v3.1.0 20 | ) 21 | 22 | require ( 23 | cloud.google.com/go v0.65.0 // indirect 24 | github.com/Azure/azure-sdk-for-go v61.4.0+incompatible // indirect 25 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 26 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 27 | github.com/Azure/go-autorest/autorest v0.11.24 // indirect 28 | github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect 29 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect 30 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect 31 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 32 | github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect 33 | github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect 34 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 35 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 36 | github.com/DataDog/datadog-go v3.2.0+incompatible // indirect 37 | github.com/Jeffail/gabs v1.1.1 // indirect 38 | github.com/Masterminds/goutils v1.1.0 // indirect 39 | github.com/Masterminds/semver v1.5.0 // indirect 40 | github.com/Masterminds/sprig v2.22.0+incompatible // indirect 41 | github.com/Microsoft/go-winio v0.5.1 // indirect 42 | github.com/NYTimes/gziphandler v1.1.1 // indirect 43 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 44 | github.com/StackExchange/wmi v1.2.1 // indirect 45 | github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f // indirect 46 | github.com/armon/go-metrics v0.3.10 // indirect 47 | github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a // indirect 48 | github.com/armon/go-radix v1.0.0 // indirect 49 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 50 | github.com/aws/aws-sdk-go v1.37.19 // indirect 51 | github.com/beorn7/perks v1.0.1 // indirect 52 | github.com/bgentry/speakeasy v0.1.0 // indirect 53 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 54 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 55 | github.com/cenkalti/backoff/v4 v4.1.2 // indirect 56 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 57 | github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0 // indirect 58 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect 59 | github.com/circonus-labs/circonusllhist v0.1.3 // indirect 60 | github.com/containerd/continuity v0.2.1 // indirect 61 | github.com/davecgh/go-spew v1.1.1 // indirect 62 | github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba // indirect 63 | github.com/digitalocean/godo v1.7.5 // indirect 64 | github.com/dimchansky/utfbom v1.1.1 // indirect 65 | github.com/dnaeon/go-vcr v1.2.0 // indirect 66 | github.com/docker/cli v20.10.11+incompatible // indirect 67 | github.com/docker/docker v20.10.10+incompatible // indirect 68 | github.com/docker/go-connections v0.4.0 // indirect 69 | github.com/docker/go-units v0.4.0 // indirect 70 | github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 // indirect 71 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 72 | github.com/fatih/color v1.13.0 // indirect 73 | github.com/go-logr/logr v0.4.0 // indirect 74 | github.com/go-ole/go-ole v1.2.5 // indirect 75 | github.com/go-sql-driver/mysql v1.5.0 // indirect 76 | github.com/go-test/deep v1.0.8 // indirect 77 | github.com/gogo/protobuf v1.3.2 // indirect 78 | github.com/golang-jwt/jwt/v4 v4.3.0 // indirect 79 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 80 | github.com/golang/protobuf v1.5.2 // indirect 81 | github.com/golang/snappy v0.0.4 // indirect 82 | github.com/google/go-cmp v0.5.6 // indirect 83 | github.com/google/go-metrics-stackdriver v0.2.0 // indirect 84 | github.com/google/gofuzz v1.1.0 // indirect 85 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 86 | github.com/google/uuid v1.3.0 // indirect 87 | github.com/googleapis/gax-go/v2 v2.0.5 // indirect 88 | github.com/googleapis/gnostic v0.5.5 // indirect 89 | github.com/gophercloud/gophercloud v0.1.0 // indirect 90 | github.com/hashicorp/consul/sdk v0.8.0 // indirect 91 | github.com/hashicorp/errwrap v1.1.0 // indirect 92 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 93 | github.com/hashicorp/go-discover v0.0.0-20210818145131-c573d69da192 // indirect 94 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 95 | github.com/hashicorp/go-kms-wrapping v0.7.0 // indirect 96 | github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 // indirect 97 | github.com/hashicorp/go-memdb v1.3.2 // indirect 98 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 99 | github.com/hashicorp/go-multierror v1.1.1 // indirect 100 | github.com/hashicorp/go-plugin v1.4.3 // indirect 101 | github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a // indirect 102 | github.com/hashicorp/go-retryablehttp v0.7.0 // indirect 103 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 104 | github.com/hashicorp/go-secure-stdlib/awsutil v0.1.5 // indirect 105 | github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 // indirect 106 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect 107 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 // indirect 108 | github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 // indirect 109 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 110 | github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 // indirect 111 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 112 | github.com/hashicorp/go-version v1.4.0 // indirect 113 | github.com/hashicorp/golang-lru v0.5.4 // indirect 114 | github.com/hashicorp/hcl v1.0.1-vault-3 // indirect 115 | github.com/hashicorp/mdns v1.0.4 // indirect 116 | github.com/hashicorp/raft v1.3.3 // indirect 117 | github.com/hashicorp/raft-autopilot v0.1.3 // indirect 118 | github.com/hashicorp/raft-boltdb/v2 v2.0.0-20210421194847-a7e34179d62c // indirect 119 | github.com/hashicorp/raft-snapshot v1.0.3 // indirect 120 | github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 // indirect 121 | github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect 122 | github.com/huandu/xstrings v1.3.2 // indirect 123 | github.com/imdario/mergo v0.3.12 // indirect 124 | github.com/jarcoal/httpmock v1.0.7 // indirect 125 | github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f // indirect 126 | github.com/jefferai/jsonx v1.0.0 // indirect 127 | github.com/jmespath/go-jmespath v0.4.0 // indirect 128 | github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect 129 | github.com/json-iterator/go v1.1.12 // indirect 130 | github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f // indirect 131 | github.com/lib/pq v1.10.3 // indirect 132 | github.com/linode/linodego v0.7.1 // indirect 133 | github.com/mattn/go-colorable v0.1.12 // indirect 134 | github.com/mattn/go-isatty v0.0.14 // indirect 135 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 136 | github.com/miekg/dns v1.1.41 // indirect 137 | github.com/mitchellh/cli v1.1.2 // indirect 138 | github.com/mitchellh/copystructure v1.2.0 // indirect 139 | github.com/mitchellh/go-homedir v1.1.0 // indirect 140 | github.com/mitchellh/go-testing-interface v1.14.0 // indirect 141 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 142 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 143 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 144 | github.com/modern-go/reflect2 v1.0.2 // indirect 145 | github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect 146 | github.com/oklog/run v1.1.0 // indirect 147 | github.com/opencontainers/go-digest v1.0.0 // indirect 148 | github.com/opencontainers/image-spec v1.0.2 // indirect 149 | github.com/opencontainers/runc v1.0.2 // indirect 150 | github.com/oracle/oci-go-sdk v13.1.0+incompatible // indirect 151 | github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect 152 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 153 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect 154 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 155 | github.com/pkg/errors v0.9.1 // indirect 156 | github.com/pmezard/go-difflib v1.0.0 // indirect 157 | github.com/posener/complete v1.2.3 // indirect 158 | github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d // indirect 159 | github.com/prometheus/client_golang v1.11.1 // indirect 160 | github.com/prometheus/client_model v0.2.0 // indirect 161 | github.com/prometheus/common v0.26.0 // indirect 162 | github.com/prometheus/procfs v0.6.0 // indirect 163 | github.com/rboyer/safeio v0.2.1 // indirect 164 | github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect 165 | github.com/ryanuber/go-glob v1.0.0 // indirect 166 | github.com/sasha-s/go-deadlock v0.2.0 // indirect 167 | github.com/sethvargo/go-limiter v0.7.1 // indirect 168 | github.com/shirou/gopsutil v3.21.5+incompatible // indirect 169 | github.com/sirupsen/logrus v1.8.1 // indirect 170 | github.com/smartystreets/goconvey v1.6.4 // indirect 171 | github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d // indirect 172 | github.com/spf13/pflag v1.0.5 // indirect 173 | github.com/stretchr/testify v1.7.0 // indirect 174 | github.com/tencentcloud/tencentcloud-sdk-go v3.0.171+incompatible // indirect 175 | github.com/tklauser/go-sysconf v0.3.9 // indirect 176 | github.com/tklauser/numcpus v0.3.0 // indirect 177 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c // indirect 178 | github.com/vmware/govmomi v0.18.0 // indirect 179 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 180 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 181 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 182 | go.etcd.io/bbolt v1.3.6 // indirect 183 | go.opencensus.io v0.23.0 // indirect 184 | go.uber.org/atomic v1.9.0 // indirect 185 | golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab // indirect 186 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 187 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 188 | golang.org/x/sys v0.0.0-20220207234003-57398862261d // indirect 189 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 190 | golang.org/x/text v0.3.7 // indirect 191 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect 192 | google.golang.org/api v0.30.0 // indirect 193 | google.golang.org/appengine v1.6.7 // indirect 194 | google.golang.org/genproto v0.0.0-20220207185906-7721543eae58 // indirect 195 | google.golang.org/grpc v1.44.0 // indirect 196 | google.golang.org/protobuf v1.27.1 // indirect 197 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 198 | gopkg.in/inf.v0 v0.9.1 // indirect 199 | gopkg.in/ini.v1 v1.62.0 // indirect 200 | gopkg.in/resty.v1 v1.12.0 // indirect 201 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 202 | gopkg.in/yaml.v2 v2.4.0 // indirect 203 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 204 | k8s.io/api v0.22.2 // indirect 205 | k8s.io/apimachinery v0.22.2 // indirect 206 | k8s.io/client-go v0.22.2 // indirect 207 | k8s.io/klog/v2 v2.9.0 // indirect 208 | k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect 209 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect 210 | sigs.k8s.io/yaml v1.2.0 // indirect 211 | ) 212 | -------------------------------------------------------------------------------- /password.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "github.com/sethvargo/go-password/password" 5 | ) 6 | 7 | type PasswordSpec struct { 8 | Length int `json:"length" structs:"length"` 9 | NumDigits int `json:"num_digits" structs:"num_digits"` 10 | NumSymbols int `json:"num_symbols" structs:"num_symbols"` 11 | AllowUpper bool `json:"allow_upper" structs:"allow_upper"` 12 | AllowRepeat bool `json:"allow_repeat" structs:"allow_repeat"` 13 | } 14 | 15 | func DefaultPasswordSpec() *PasswordSpec { 16 | return &PasswordSpec{ 17 | Length: 32, 18 | NumDigits: 4, 19 | NumSymbols: 4, 20 | AllowUpper: true, 21 | AllowRepeat: true, 22 | } 23 | } 24 | 25 | func GeneratePassword(spec *PasswordSpec) (string, error) { 26 | passwdgen, err := password.NewGenerator(&password.GeneratorInput{ 27 | LowerLetters: password.LowerLetters, 28 | UpperLetters: password.UpperLetters, 29 | Digits: password.Digits, 30 | Symbols: "_&^%$#@!", // mostly shell-safe set, TE-101 31 | }) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | if spec == nil { 37 | spec = DefaultPasswordSpec() 38 | } 39 | return passwdgen.Generate(spec.Length, spec.NumDigits, spec.NumSymbols, !spec.AllowUpper, spec.AllowRepeat) 40 | } 41 | -------------------------------------------------------------------------------- /path_config_connection.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/helper/certutil" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | // pathConfigConnection returns a configured framework.Path setup to 14 | // operate on plugins. 15 | func (b *backend) pathConfigConnection() *framework.Path { 16 | return &framework.Path{ 17 | Pattern: fmt.Sprintf("config/%s", framework.GenericNameRegex("name")), 18 | Fields: map[string]*framework.FieldSchema{ 19 | "name": { 20 | Type: framework.TypeString, 21 | Description: "Name of the Splunk connection.", 22 | }, 23 | "username": { 24 | Type: framework.TypeString, 25 | Description: "Admin user with permission to create new accounts.", 26 | }, 27 | "password": { 28 | Type: framework.TypeString, 29 | Description: "Admin password.", 30 | }, 31 | "url": { 32 | Type: framework.TypeString, 33 | Description: "Splunk server URL.", 34 | }, 35 | "is_standalone": { 36 | Type: framework.TypeBool, 37 | Description: `Whether this is a standalone or multi-node deployment. Default: false`, 38 | Default: false, 39 | }, 40 | "allowed_roles": { 41 | Type: framework.TypeCommaStringSlice, 42 | Description: trimIndent(` 43 | Comma separated string or array of the role names allowed to get creds 44 | from this Splunk connection. If empty, no roles are allowed. If "*", all 45 | roles are allowed.`), 46 | }, 47 | "verify": { 48 | Type: framework.TypeBool, 49 | Default: true, 50 | Description: trimIndent(` 51 | If true, the connection details are verified by actually connecting to 52 | Splunk. Default: true`), 53 | }, 54 | "insecure_tls": { 55 | Type: framework.TypeBool, 56 | Default: false, 57 | Description: trimIndent(` 58 | Whether to use TLS but skip verification; has no effect if a CA 59 | certificate is provided. Default: false`), 60 | }, 61 | "tls_min_version": { 62 | Type: framework.TypeString, 63 | Default: "tls12", 64 | Description: trimIndent(` 65 | Minimum TLS version to use. Accepted values are "tls10", "tls11" or 66 | "tls12". Default: "tls12".`), 67 | }, 68 | "pem_bundle": { 69 | Type: framework.TypeString, 70 | Description: trimIndent(` 71 | PEM-format, concatenated unencrypted secret key and certificate, with 72 | optional CA certificate.`), 73 | }, 74 | "pem_json": { 75 | Type: framework.TypeString, 76 | Description: trimIndent(` 77 | JSON containing a PEM-format, unencrypted secret key and certificate, with 78 | optional CA certificate. The JSON output of a certificate issued with the 79 | PKI backend can be directly passed into this parameter. If both this and 80 | "pem_bundle" are specified, this will take precedence.`), 81 | }, 82 | "root_ca": { 83 | Type: framework.TypeString, 84 | Description: `PEM-format, concatenated CA certificates.`, 85 | }, 86 | "connect_timeout": { 87 | Type: framework.TypeDurationSecond, 88 | Default: "30s", 89 | Description: `The connection timeout to use. Default: 30s.`, 90 | }, 91 | }, 92 | 93 | ExistenceCheck: b.connectionExistenceCheck, 94 | Callbacks: map[logical.Operation]framework.OperationFunc{ 95 | logical.CreateOperation: b.connectionWriteHandler, 96 | logical.UpdateOperation: b.connectionWriteHandler, 97 | logical.ReadOperation: b.connectionReadHandler, 98 | logical.DeleteOperation: b.connectionDeleteHandler, 99 | }, 100 | 101 | HelpSynopsis: pathConfigConnectionHelpSyn, 102 | HelpDescription: pathConfigConnectionHelpDesc, 103 | } 104 | } 105 | 106 | func (b *backend) connectionExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 107 | name := data.Get("name").(string) 108 | return connectionConfigExists(ctx, req.Storage, name) 109 | } 110 | 111 | func (b *backend) connectionReadHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 112 | name := data.Get("name").(string) 113 | config, err := connectionConfigLoad(ctx, req.Storage, name) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | resp := &logical.Response{ 119 | Data: config.toResponseData(), 120 | } 121 | return resp, nil 122 | } 123 | 124 | func (b *backend) connectionDeleteHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 125 | name := data.Get("name").(string) 126 | config, err := connectionConfigLoad(ctx, req.Storage, name) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | if err := req.Storage.Delete(ctx, fmt.Sprintf("config/%s", name)); err != nil { 132 | return nil, fmt.Errorf("error reading connection configuration: %w", err) 133 | } 134 | 135 | // XXXX WAL 136 | if err := b.clearConnection(config.ID); err != nil { 137 | return nil, err 138 | } 139 | return nil, nil 140 | } 141 | 142 | func (b *backend) connectionWriteHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 143 | name := data.Get("name").(string) 144 | if name == "" { 145 | return logical.ErrorResponse(respErrEmptyName), nil 146 | } 147 | 148 | config := &splunkConfig{} 149 | if req.Operation != logical.CreateOperation { 150 | var err error 151 | config, err = connectionConfigLoad(ctx, req.Storage, name) 152 | if err != nil { 153 | return nil, fmt.Errorf("error reading connection configuration: %w", err) 154 | } 155 | } 156 | 157 | if usernameRaw, ok := getValue(data, req.Operation, "username"); ok { 158 | config.Username = usernameRaw.(string) 159 | } 160 | if config.Username == "" { 161 | return logical.ErrorResponse("empty username"), nil 162 | } 163 | if passwordRaw, ok := getValue(data, req.Operation, "password"); ok { 164 | config.Password = passwordRaw.(string) 165 | } 166 | if urlRaw, ok := getValue(data, req.Operation, "url"); ok { 167 | config.URL = urlRaw.(string) 168 | } 169 | if config.URL == "" { 170 | return logical.ErrorResponse("empty URL"), nil 171 | } 172 | if isStandalone, ok := getValue(data, req.Operation, "is_standalone"); ok { 173 | config.IsStandalone = isStandalone.(bool) 174 | } 175 | 176 | if verifyRaw, ok := getValue(data, req.Operation, "verify"); ok { 177 | config.Verify = verifyRaw.(bool) 178 | } 179 | if allowedRolesRaw, ok := getValue(data, req.Operation, "allowed_roles"); ok { 180 | config.AllowedRoles = allowedRolesRaw.([]string) 181 | } 182 | if len(config.AllowedRoles) == 0 { 183 | return logical.ErrorResponse("allowed_roles cannot be empty"), nil 184 | } 185 | // XXX go through all established leases if allowed_roles change? 186 | 187 | if insecureTLSRaw, ok := getValue(data, req.Operation, "insecure_tls"); ok { 188 | config.InsecureTLS = insecureTLSRaw.(bool) 189 | } 190 | 191 | pemBundle := data.Get("pem_bundle").(string) 192 | pemJSON := data.Get("pem_json").(string) 193 | rootCA := data.Get("root_ca").(string) 194 | 195 | var certBundle *certutil.CertBundle 196 | var parsedCertBundle *certutil.ParsedCertBundle 197 | var err error 198 | 199 | switch { 200 | case len(pemJSON) != 0: 201 | parsedCertBundle, err = certutil.ParsePKIJSON([]byte(pemJSON)) 202 | if err != nil { 203 | return logical.ErrorResponse(fmt.Sprintf("Could not parse given JSON; it must be in the format of the output of the PKI backend certificate issuing command: %s", err)), nil 204 | } 205 | certBundle, err = parsedCertBundle.ToCertBundle() 206 | if err != nil { 207 | return logical.ErrorResponse(fmt.Sprintf("Error marshaling PEM information: %s", err)), nil 208 | } 209 | config.Certificate = certBundle.Certificate 210 | config.PrivateKey = certBundle.PrivateKey 211 | config.CAChain = certBundle.CAChain 212 | 213 | case len(pemBundle) != 0: 214 | parsedCertBundle, err = certutil.ParsePEMBundle(pemBundle) 215 | if err != nil { 216 | return logical.ErrorResponse(fmt.Sprintf("Error parsing the given PEM information: %s", err)), nil 217 | } 218 | certBundle, err = parsedCertBundle.ToCertBundle() 219 | if err != nil { 220 | return logical.ErrorResponse(fmt.Sprintf("Error marshaling PEM information: %s", err)), nil 221 | } 222 | config.Certificate = certBundle.Certificate 223 | config.PrivateKey = certBundle.PrivateKey 224 | config.CAChain = certBundle.CAChain 225 | } 226 | if config.CAChain == nil { 227 | config.CAChain = []string{} 228 | } 229 | 230 | if len(rootCA) > 0 { 231 | config.RootCA = []string{rootCA} // XXXX parse PEM 232 | } 233 | if config.RootCA == nil { 234 | config.RootCA = []string{} 235 | } 236 | 237 | if connectTimeoutRaw, ok := getValue(data, req.Operation, "connect_timeout"); ok { 238 | config.ConnectTimeout = time.Duration(connectTimeoutRaw.(int)) * time.Second 239 | } 240 | 241 | if err := config.store(ctx, req.Storage, name); err != nil { 242 | return nil, fmt.Errorf("error writing connection configuration: %w", err) 243 | } 244 | 245 | // if config.Verify { 246 | // config.verifyConnection(ctx, req.Storage, name) 247 | // } 248 | 249 | return nil, nil 250 | } 251 | 252 | func (b *backend) pathConnectionsList() *framework.Path { 253 | return &framework.Path{ 254 | Pattern: "config/?$", 255 | 256 | Callbacks: map[logical.Operation]framework.OperationFunc{ 257 | logical.ListOperation: b.connectionListHandler, 258 | }, 259 | 260 | HelpSynopsis: pathConfigConnectionHelpSyn, 261 | HelpDescription: pathConfigConnectionHelpDesc, 262 | } 263 | } 264 | 265 | func (b *backend) connectionListHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 266 | entries, err := req.Storage.List(ctx, "config/") 267 | if err != nil { 268 | return nil, err 269 | } 270 | return logical.ListResponse(entries), nil 271 | } 272 | 273 | const pathConfigConnectionHelpSyn = ` 274 | Configure connection details to a Splunk instance. 275 | ` 276 | 277 | const pathConfigConnectionHelpDesc = ` 278 | See the documentation for config/name for a full list of accepted 279 | connection details. 280 | 281 | "username", "password" and "url" are self-explanatory, although the 282 | given user must have admin access within Splunk. Note that since 283 | this backend issues username/password credentials, Splunk must be 284 | configured to allow local users for authentication. 285 | 286 | TLS works as follows: 287 | 288 | * If "insecure_tls" is set to true, the connection will not perform 289 | verification of the server certificate 290 | 291 | * If only "issuing_ca" is set in "pem_json", or the only certificate 292 | in "pem_bundle" is a CA certificate, the given CA certificate will 293 | be used for server certificate verification; otherwise the system CA 294 | certificates will be used. 295 | 296 | * If "certificate" and "private_key" are set in "pem_bundle" or 297 | "pem_json", client auth will be turned on for the connection. 298 | 299 | * If "root_ca" is set, the PEM-concatenated set of CA certificates 300 | will be added, and used instead of the system CA certificates. 301 | 302 | "pem_bundle" should be a PEM-concatenated bundle of a private key + 303 | client certificate, an issuing CA certificate, or both. "pem_json" 304 | should contain the same information; for convenience, the JSON format 305 | is the same as that output by the issue command from the PKI backend. 306 | 307 | When configuring the connection information, the backend will verify 308 | its validity. 309 | ` 310 | -------------------------------------------------------------------------------- /path_creds_create.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | uuid "github.com/hashicorp/go-uuid" 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/helper/strutil" 11 | "github.com/hashicorp/vault/sdk/logical" 12 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 13 | ) 14 | 15 | func (b *backend) pathCredsCreate() *framework.Path { 16 | return &framework.Path{ 17 | Pattern: "creds/" + framework.GenericNameRegex("name"), 18 | Fields: map[string]*framework.FieldSchema{ 19 | "name": { 20 | Type: framework.TypeString, 21 | Description: "Name of the role", 22 | }, 23 | }, 24 | 25 | Callbacks: map[logical.Operation]framework.OperationFunc{ 26 | logical.ReadOperation: b.credsReadHandler, 27 | }, 28 | 29 | HelpSynopsis: pathCredsCreateHelpSyn, 30 | HelpDescription: pathCredsCreateHelpDesc, 31 | } 32 | } 33 | 34 | func (b *backend) pathCredsCreateMulti() *framework.Path { 35 | return &framework.Path{ 36 | Pattern: "creds/" + framework.GenericNameRegex("name") + "/" + framework.GenericNameRegex("node_fqdn"), 37 | Fields: map[string]*framework.FieldSchema{ 38 | "name": { 39 | Type: framework.TypeString, 40 | Description: "Name of the role", 41 | }, 42 | "node_fqdn": { 43 | Type: framework.TypeString, 44 | Description: "FQDN for the Splunk Stack node", 45 | }, 46 | }, 47 | 48 | Callbacks: map[logical.Operation]framework.OperationFunc{ 49 | logical.ReadOperation: b.credsReadHandler, 50 | }, 51 | 52 | HelpSynopsis: pathCredsCreateHelpSyn, 53 | HelpDescription: pathCredsCreateHelpDesc, 54 | } 55 | } 56 | 57 | func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 58 | name := d.Get("name").(string) 59 | role, err := roleConfigLoad(ctx, req.Storage, name) 60 | if err != nil { 61 | return nil, err 62 | } 63 | if role == nil { 64 | return logical.ErrorResponse(fmt.Sprintf("role not found: %q", name)), nil 65 | } 66 | 67 | config, err := connectionConfigLoad(ctx, req.Storage, role.Connection) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | // If role name isn't in allowed roles, send back a permission denied. 73 | if !strutil.StrListContains(config.AllowedRoles, "*") && !strutil.StrListContainsGlob(config.AllowedRoles, name) { 74 | return nil, fmt.Errorf("%q is not an allowed role for connection %q", name, role.Connection) 75 | } 76 | 77 | conn, err := b.ensureConnection(ctx, config) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | // Generate credentials 83 | userUUID, err := generateUserID(role) 84 | if err != nil { 85 | return nil, err 86 | } 87 | userPrefix := role.UserPrefix 88 | if role.UserPrefix == defaultUserPrefix { 89 | userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName) 90 | } 91 | username := fmt.Sprintf("%s_%s", userPrefix, userUUID) 92 | passwd, err := generateUserPassword(role) 93 | if err != nil { 94 | return nil, fmt.Errorf("error generating new password %w", err) 95 | } 96 | opts := splunk.CreateUserOptions{ 97 | Name: username, 98 | Password: passwd, 99 | Roles: role.Roles, 100 | DefaultApp: role.DefaultApp, 101 | Email: role.Email, 102 | TZ: role.TZ, 103 | } 104 | if _, _, err := conn.AccessControl.Authentication.Users.Create(&opts); err != nil { 105 | return nil, err 106 | } 107 | 108 | resp := b.Secret(secretCredsType).Response(map[string]interface{}{ 109 | // return to user 110 | "username": username, 111 | "password": passwd, 112 | "roles": role.Roles, 113 | "connection": role.Connection, 114 | "url": conn.Params().BaseURL, 115 | }, map[string]interface{}{ 116 | // store (with lease) 117 | "username": username, 118 | "role": name, 119 | "connection": role.Connection, 120 | "url": conn.Params().BaseURL, // new in v0.7.0 121 | }) 122 | resp.Secret.TTL = role.DefaultTTL 123 | resp.Secret.MaxTTL = role.MaxTTL 124 | 125 | return resp, nil 126 | } 127 | 128 | func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry, roleConfig *roleConfig) (*splunk.ServerInfoEntry, error) { 129 | for _, host := range hosts { 130 | // check if node_fqdn is in either of HostFQDN or Host. User might not always the FQDN on the cli input 131 | if strings.EqualFold(host.Content.HostFQDN, nodeFQDN) || strings.EqualFold(host.Content.Host, nodeFQDN) { 132 | // Return host if the requested node type is allowed 133 | if strutil.StrListContains(roleConfig.AllowedServerRoles, "*") { 134 | return &host, nil 135 | } 136 | for _, role := range host.Content.Roles { 137 | if strutil.StrListContainsGlob(roleConfig.AllowedServerRoles, role) { 138 | return &host, nil 139 | } 140 | } 141 | return nil, fmt.Errorf("host %q does not have any of the allowed server roles: %q", nodeFQDN, roleConfig.AllowedServerRoles) 142 | } 143 | } 144 | return nil, fmt.Errorf("host %q not found", nodeFQDN) 145 | } 146 | 147 | func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 148 | name := d.Get("name").(string) 149 | node, _ := d.GetOk("node_fqdn") 150 | nodeFQDN := node.(string) 151 | role, err := roleConfigLoad(ctx, req.Storage, name) 152 | if err != nil { 153 | return nil, err 154 | } 155 | if role == nil { 156 | return logical.ErrorResponse(fmt.Sprintf("role not found: %q", name)), nil 157 | } 158 | 159 | config, err := connectionConfigLoad(ctx, req.Storage, role.Connection) 160 | if err != nil { 161 | return nil, err 162 | } 163 | // Check if isStandalone is set 164 | if config.IsStandalone { 165 | return logical.ErrorResponse("expected is_standalone to be unset for connection: %q", role.Connection), nil 166 | } 167 | 168 | // If role name isn't in allowed roles, send back a permission denied. 169 | if !strutil.StrListContains(config.AllowedRoles, "*") && !strutil.StrListContainsGlob(config.AllowedRoles, name) { 170 | return logical.ErrorResponse("%q is not an allowed role for connection %q", name, role.Connection), nil 171 | } 172 | 173 | conn, err := b.ensureConnection(ctx, config) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | nodes, _, err := conn.Deployment.SearchPeers(splunk.ServerInfoEntryFilterMinimal) 179 | if err != nil { 180 | b.Logger().Error("Error while reading SearchPeers from cluster master", "err", err) 181 | return nil, fmt.Errorf("unable to read searchpeers from cluster master: %w", err) 182 | } 183 | 184 | foundNode, err := findNode(nodeFQDN, nodes, role) 185 | if err != nil { 186 | return logical.ErrorResponse(err.Error()), nil 187 | } 188 | if foundNode.Content.Host == "" { 189 | return nil, fmt.Errorf("host field unexpectedly empty for %q", nodeFQDN) 190 | } 191 | nodeFQDN = foundNode.Content.Host // the actual FQDN as returned by the cluster master, confusingly 192 | 193 | // Re-create connection for node 194 | conn, err = b.ensureNodeConnection(ctx, config, nodeFQDN) 195 | if err != nil { 196 | return nil, err 197 | } 198 | // Generate credentials 199 | userUUID, err := generateUserID(role) 200 | if err != nil { 201 | return nil, err 202 | } 203 | userPrefix := role.UserPrefix 204 | if role.UserPrefix == defaultUserPrefix { 205 | userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName) 206 | } 207 | username := fmt.Sprintf("%s_%s", userPrefix, userUUID) 208 | passwd, err := generateUserPassword(role) 209 | if err != nil { 210 | return nil, fmt.Errorf("error generating new password: %w", err) 211 | } 212 | opts := splunk.CreateUserOptions{ 213 | Name: username, 214 | Password: passwd, 215 | Roles: role.Roles, 216 | DefaultApp: role.DefaultApp, 217 | Email: role.Email, 218 | TZ: role.TZ, 219 | } 220 | if _, _, err := conn.AccessControl.Authentication.Users.Create(&opts); err != nil { 221 | return nil, err 222 | } 223 | 224 | resp := b.Secret(secretCredsType).Response(map[string]interface{}{ 225 | // return to user 226 | "username": username, 227 | "password": passwd, 228 | "roles": role.Roles, 229 | "connection": role.Connection, 230 | "url": conn.Params().BaseURL, 231 | }, map[string]interface{}{ 232 | // store (with lease) 233 | "username": username, 234 | "role": name, 235 | "connection": role.Connection, 236 | "node_fqdn": nodeFQDN, 237 | "url": conn.Params().BaseURL, // new in v0.7.0 238 | }) 239 | resp.Secret.TTL = role.DefaultTTL 240 | resp.Secret.MaxTTL = role.MaxTTL 241 | 242 | return resp, nil 243 | } 244 | 245 | func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 246 | name := d.Get("name").(string) 247 | node_fqdn, present := d.GetOk("node_fqdn") 248 | // if node_fqdn is specified then the treat the request for a multi-node deployment 249 | if present { 250 | b.Logger().Debug("node_fqdn specified for role. using clustered mode for getting temporary creds", "nodeFQDN", node_fqdn.(string), "role", name) 251 | return b.credsReadHandlerMulti(ctx, req, d) 252 | } 253 | b.Logger().Debug("node_fqdn not specified for role. using standalone mode for getting temporary creds", "role", name) 254 | return b.credsReadHandlerStandalone(ctx, req, d) 255 | } 256 | 257 | func generateUserID(roleConfig *roleConfig) (string, error) { 258 | switch roleConfig.UserIDScheme { 259 | case userIDSchemeUUID4_v0_5_0: 260 | fallthrough 261 | case userIDSchemeUUID4: 262 | return uuid.GenerateUUID() 263 | case userIDSchemeBase58_64: 264 | return GenerateShortUUID(8) 265 | case userIDSchemeBase58_128: 266 | return GenerateShortUUID(16) 267 | default: 268 | return "", fmt.Errorf("invalid user_id_scheme: %q", roleConfig.UserIDScheme) 269 | } 270 | } 271 | 272 | func generateUserPassword(roleConfig *roleConfig) (string, error) { 273 | passwd, err := GeneratePassword(roleConfig.PasswordSpec) 274 | if err == nil { 275 | return passwd, nil 276 | } 277 | // fallback 278 | return uuid.GenerateUUID() 279 | } 280 | 281 | // #nosec G101 282 | const pathCredsCreateHelpSyn = ` 283 | Request Splunk credentials for a certain role. 284 | ` 285 | 286 | const pathCredsCreateHelpDesc = ` 287 | This path reads Splunk credentials for a certain role. The credentials 288 | will be generated on demand and will be automatically revoked when 289 | their lease expires. Leases can be extended until a configured 290 | maximum life-time. 291 | ` 292 | -------------------------------------------------------------------------------- /path_creds_create_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "path/filepath" 7 | "testing" 8 | 9 | "gotest.tools/v3/assert" 10 | 11 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 12 | ) 13 | 14 | func Test_findNode(t *testing.T) { 15 | nodes := make([]splunk.ServerInfoEntry, 0) 16 | 17 | gp := filepath.Join("testdata", t.Name()+".json") 18 | jsonResponseSearchDistributedPeers, err := ioutil.ReadFile(gp) 19 | assert.NilError(t, err) 20 | 21 | err = json.Unmarshal(jsonResponseSearchDistributedPeers, &nodes) 22 | assert.NilError(t, err) 23 | 24 | type args struct { 25 | nodeFQDN string 26 | hosts []splunk.ServerInfoEntry 27 | roleConfig *roleConfig 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want bool 33 | wantErr bool 34 | }{ 35 | { 36 | name: "server entry first", 37 | args: args{ 38 | nodeFQDN: "idm-i-074b0895939212e99.foo.example.com", 39 | hosts: nodes, 40 | roleConfig: &roleConfig{ 41 | AllowedServerRoles: []string{"*"}, 42 | }, 43 | }, 44 | want: true, 45 | wantErr: false, 46 | }, 47 | { 48 | name: "server entry last", 49 | args: args{ 50 | nodeFQDN: "sh-i-0a12fdd509c2a2954.foo.example.com", 51 | hosts: nodes, 52 | roleConfig: &roleConfig{ 53 | AllowedServerRoles: []string{"*"}, 54 | }, 55 | }, 56 | want: true, 57 | wantErr: false, 58 | }, 59 | { 60 | name: "server entry case insensitive", 61 | args: args{ 62 | nodeFQDN: "SH-I-0A12FDD509C2A2954.FOO.EXAMPLE.COM", 63 | hosts: nodes, 64 | roleConfig: &roleConfig{ 65 | AllowedServerRoles: []string{"*"}, 66 | }, 67 | }, 68 | want: true, 69 | wantErr: false, 70 | }, 71 | { 72 | name: "server entry short name", 73 | args: args{ 74 | nodeFQDN: "SH-I-0A12FDD509C2A2954", 75 | hosts: nodes, 76 | roleConfig: &roleConfig{ 77 | AllowedServerRoles: []string{"*"}, 78 | }, 79 | }, 80 | want: true, 81 | wantErr: false, 82 | }, 83 | { 84 | name: "server entry not found", 85 | args: args{ 86 | nodeFQDN: "unknown-host", 87 | hosts: nodes, 88 | roleConfig: &roleConfig{ 89 | AllowedServerRoles: []string{"*"}, 90 | }, 91 | }, 92 | want: false, 93 | wantErr: true, 94 | }, 95 | { 96 | name: "role match mismatch", 97 | args: args{ 98 | nodeFQDN: "sh-i-0a12fdd509c2a2954.foo.example.com", 99 | hosts: nodes, 100 | roleConfig: &roleConfig{ 101 | AllowedServerRoles: []string{"unknown-role"}, 102 | }, 103 | }, 104 | want: false, 105 | wantErr: true, 106 | }, 107 | { 108 | name: "role match first", 109 | args: args{ 110 | nodeFQDN: "sh-i-0a12fdd509c2a2954.foo.example.com", 111 | hosts: nodes, 112 | roleConfig: &roleConfig{ 113 | AllowedServerRoles: []string{"cluster_search_head"}, 114 | }, 115 | }, 116 | want: true, 117 | wantErr: false, 118 | }, 119 | { 120 | name: "role match last", 121 | args: args{ 122 | nodeFQDN: "sh-i-0a12fdd509c2a2954.foo.example.com", 123 | hosts: nodes, 124 | roleConfig: &roleConfig{ 125 | AllowedServerRoles: []string{"unknown_role", "kv_store"}, 126 | }, 127 | }, 128 | want: true, 129 | wantErr: false, 130 | }, 131 | } 132 | for _, tt := range tests { 133 | t.Run(tt.name, func(t *testing.T) { 134 | got, err := findNode(tt.args.nodeFQDN, tt.args.hosts, tt.args.roleConfig) 135 | if (err != nil) != tt.wantErr { 136 | t.Errorf("findNode() error = %v, wantErr %v", err, tt.wantErr) 137 | return 138 | } 139 | if (got != nil) != tt.want { 140 | t.Errorf("findNode() = %v, want %v", got, tt.want) 141 | } 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /path_reset.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/vault/sdk/framework" 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | // pathResetConnection configures a path to reset a plugin. 12 | func (b *backend) pathResetConnection() *framework.Path { 13 | return &framework.Path{ 14 | Pattern: fmt.Sprintf("reset/%s", framework.GenericNameRegex("name")), 15 | Fields: map[string]*framework.FieldSchema{ 16 | "name": { 17 | Type: framework.TypeString, 18 | Description: "Name of this Splunk connection", 19 | }, 20 | }, 21 | 22 | Callbacks: map[logical.Operation]framework.OperationFunc{ 23 | logical.UpdateOperation: b.connectionResetHandler, 24 | }, 25 | 26 | HelpSynopsis: pathResetConnectionHelpSyn, 27 | HelpDescription: pathResetConnectionHelpDesc, 28 | } 29 | } 30 | 31 | // connectionResetHandler resets a connection by clearing the existing instance 32 | func (b *backend) connectionResetHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 33 | name := data.Get("name").(string) 34 | config, err := connectionConfigLoad(ctx, req.Storage, name) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if err := b.clearConnection(config.ID); err != nil { 39 | return nil, err 40 | } 41 | 42 | return nil, nil 43 | } 44 | 45 | const pathResetConnectionHelpSyn = ` 46 | Resets a Splunk connection. 47 | ` 48 | 49 | const pathResetConnectionHelpDesc = ` 50 | This path resets the Splunk connection by closing the existing 51 | connection. Upon further access, new connections are established. 52 | ` 53 | -------------------------------------------------------------------------------- /path_roles.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | const ( 13 | rolesPrefix = "roles/" 14 | defaultUserPrefix = "vault" 15 | 16 | userIDSchemeUUID4_v0_5_0 = "" 17 | userIDSchemeUUID4 = "uuid4" 18 | userIDSchemeBase58_64 = "base58-64" 19 | userIDSchemeBase58_128 = "base58-128" 20 | ) 21 | 22 | func (b *backend) pathRoles() *framework.Path { 23 | return &framework.Path{ 24 | Pattern: rolesPrefix + framework.GenericNameRegex("name"), 25 | Fields: map[string]*framework.FieldSchema{ 26 | "name": { 27 | Type: framework.TypeString, 28 | Description: "Name of the role", 29 | }, 30 | "connection": { 31 | Type: framework.TypeString, 32 | Description: "Name of the Splunk connection this role acts on", 33 | }, 34 | "default_ttl": { 35 | Type: framework.TypeDurationSecond, 36 | Description: "Default TTL for role", 37 | }, 38 | "max_ttl": { 39 | Type: framework.TypeDurationSecond, 40 | Description: "Maximum time a credential is valid for", 41 | }, 42 | "roles": { 43 | Type: framework.TypeCommaStringSlice, 44 | Description: "Comma-separated string or list of Splunk roles.", 45 | }, 46 | "allowed_server_roles": { 47 | Type: framework.TypeCommaStringSlice, 48 | Description: trimIndent(` 49 | Comma-separated string or array of node type (glob) patterns that are allowed 50 | to fetch credentials for. If empty, no nodes are allowed. If "*", all 51 | node types are allowed.`), 52 | Default: []string{"*"}, 53 | }, 54 | "default_app": { 55 | Type: framework.TypeString, 56 | Description: trimIndent(` 57 | User default app. Overrides the default app inherited from the user roles.`), 58 | }, 59 | "email": { 60 | Type: framework.TypeString, 61 | Description: "User email address.", 62 | }, 63 | "tz": { 64 | Type: framework.TypeString, 65 | Description: "User time zone.", 66 | }, 67 | "user_prefix": { 68 | Type: framework.TypeString, 69 | Description: "Prefix for creating new users.", 70 | Default: defaultUserPrefix, 71 | }, 72 | "user_id_scheme": { 73 | Type: framework.TypeLowerCaseString, 74 | Description: fmt.Sprintf("ID generation scheme (%s, %s, %s). Default: %s", 75 | userIDSchemeUUID4, userIDSchemeBase58_64, userIDSchemeBase58_128, userIDSchemeBase58_64), 76 | Default: userIDSchemeBase58_64, 77 | }, 78 | }, 79 | Callbacks: map[logical.Operation]framework.OperationFunc{ 80 | logical.ReadOperation: b.rolesReadHandler, 81 | logical.CreateOperation: b.rolesWriteHandler, 82 | logical.UpdateOperation: b.rolesWriteHandler, 83 | logical.DeleteOperation: b.rolesDeleteHandler, 84 | }, 85 | ExistenceCheck: b.rolesExistenceCheckHandler, 86 | HelpSynopsis: pathRoleHelpSyn, 87 | HelpDescription: pathRoleHelpDesc, 88 | } 89 | } 90 | 91 | // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. 92 | func (b *backend) rolesExistenceCheckHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { 93 | name := d.Get("name").(string) 94 | role, err := roleConfigLoad(ctx, req.Storage, name) 95 | if err != nil { 96 | return false, err 97 | } 98 | return role != nil, nil 99 | } 100 | 101 | func (b *backend) rolesReadHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 102 | name := d.Get("name").(string) 103 | role, err := roleConfigLoad(ctx, req.Storage, name) 104 | if err != nil { 105 | return nil, err 106 | } 107 | if role == nil { 108 | return nil, nil 109 | } 110 | 111 | resp := &logical.Response{ 112 | Data: role.toResponseData(), 113 | } 114 | return resp, nil 115 | } 116 | 117 | func (b *backend) rolesWriteHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 118 | name := data.Get("name").(string) 119 | role, err := roleConfigLoad(ctx, req.Storage, name) 120 | if err != nil { 121 | return nil, err 122 | } 123 | if role == nil { 124 | role = &roleConfig{} 125 | } 126 | 127 | if connRaw, ok := getValue(data, req.Operation, "connection"); ok { 128 | role.Connection = connRaw.(string) 129 | } 130 | if role.Connection == "" { 131 | return logical.ErrorResponse("empty Splunk connection name"), nil 132 | } 133 | if defaultTTLRaw, ok := getValue(data, req.Operation, "default_ttl"); ok { 134 | role.DefaultTTL = time.Duration(defaultTTLRaw.(int)) * time.Second 135 | } 136 | if maxTTLRaw, ok := getValue(data, req.Operation, "max_ttl"); ok { 137 | role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second 138 | } 139 | if allowedServerRoles, ok := getValue(data, req.Operation, "allowed_server_roles"); ok { 140 | role.AllowedServerRoles = allowedServerRoles.([]string) 141 | } 142 | role.PasswordSpec = DefaultPasswordSpec() // XXX make configurable 143 | 144 | if roles, ok := getValue(data, req.Operation, "roles"); ok { 145 | role.Roles = roles.([]string) 146 | } 147 | if len(role.Roles) == 0 { 148 | return logical.ErrorResponse("roles cannot be empty"), nil 149 | } 150 | if defaultAppRaw, ok := getValue(data, req.Operation, "default_app"); ok { 151 | role.DefaultApp = defaultAppRaw.(string) 152 | } 153 | if emailRaw, ok := getValue(data, req.Operation, "email"); ok { 154 | role.Email = emailRaw.(string) 155 | } 156 | if tzRaw, ok := getValue(data, req.Operation, "tz"); ok { 157 | role.TZ = tzRaw.(string) 158 | } 159 | if userPrefixRaw, ok := getValue(data, req.Operation, "user_prefix"); ok { 160 | role.UserPrefix = userPrefixRaw.(string) 161 | } 162 | if role.UserPrefix == "" { 163 | return logical.ErrorResponse("user_prefix can't be set to empty string"), nil 164 | } 165 | 166 | if userIDSchemeRaw, ok := getValue(data, req.Operation, "user_id_scheme"); ok { 167 | role.UserIDScheme = userIDSchemeRaw.(string) 168 | } 169 | switch role.UserIDScheme { 170 | case userIDSchemeUUID4_v0_5_0: 171 | case userIDSchemeUUID4: 172 | case userIDSchemeBase58_64: 173 | case userIDSchemeBase58_128: 174 | default: 175 | return logical.ErrorResponse("invalid user_id_scheme: %q", role.UserIDScheme), nil 176 | } 177 | 178 | if err := role.store(ctx, req.Storage, name); err != nil { 179 | return nil, err 180 | } 181 | return nil, nil 182 | } 183 | 184 | func (b *backend) rolesDeleteHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 185 | name := d.Get("name").(string) 186 | if err := req.Storage.Delete(ctx, rolesPrefix+name); err != nil { 187 | return nil, err 188 | } 189 | return nil, nil 190 | } 191 | 192 | func (b *backend) pathRolesList() *framework.Path { 193 | return &framework.Path{ 194 | Pattern: rolesPrefix + "?$", 195 | Callbacks: map[logical.Operation]framework.OperationFunc{ 196 | logical.ListOperation: b.rolesListHandler, 197 | }, 198 | HelpSynopsis: pathRoleHelpSyn, 199 | HelpDescription: pathRoleHelpDesc, 200 | } 201 | } 202 | 203 | func (b *backend) rolesListHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 204 | entries, err := req.Storage.List(ctx, rolesPrefix) 205 | if err != nil { 206 | return nil, err 207 | } 208 | 209 | return logical.ListResponse(entries), nil 210 | } 211 | 212 | const pathRoleHelpSyn = ` 213 | Manage the roles that can be created with this backend. 214 | ` 215 | 216 | const pathRoleHelpDesc = ` 217 | This path lets you manage the roles that can be created with this backend. 218 | 219 | See the documentation for roles/name for a full list of accepted 220 | connection details. 221 | ` 222 | -------------------------------------------------------------------------------- /path_rotate_root.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/go-uuid" 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 11 | ) 12 | 13 | func (b *backend) pathRotateRoot() *framework.Path { 14 | return &framework.Path{ 15 | Pattern: "rotate-root/" + framework.GenericNameRegex("name"), 16 | Fields: map[string]*framework.FieldSchema{ 17 | "name": { 18 | Type: framework.TypeString, 19 | Description: "Name of this Splunk connection", 20 | }, 21 | }, 22 | Callbacks: map[logical.Operation]framework.OperationFunc{ 23 | logical.UpdateOperation: b.rotateRootUpdateHandler, 24 | }, 25 | 26 | HelpSynopsis: pathRotateRootHelpSyn, 27 | HelpDescription: pathRotateRootHelpDesc, 28 | } 29 | } 30 | 31 | func (b *backend) rotateRootUpdateHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 32 | name := data.Get("name").(string) 33 | oldConfig, err := connectionConfigLoad(ctx, req.Storage, name) 34 | if err != nil { 35 | return nil, err 36 | } 37 | conn, err := b.ensureConnection(ctx, oldConfig) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | config := *oldConfig 43 | passwd, err := uuid.GenerateUUID() 44 | if err != nil { 45 | return nil, fmt.Errorf("error generating new password %w", err) 46 | } 47 | config.Password = passwd 48 | 49 | opts := splunk.UpdateUserOptions{ 50 | OldPassword: oldConfig.Password, 51 | Password: config.Password, 52 | } 53 | 54 | // XXX write WAL in case we restart between successful update and store 55 | if _, _, err := conn.AccessControl.Authentication.Users.Update(config.Username, &opts); err != nil { 56 | return nil, fmt.Errorf("error updating password: %w", err) 57 | } 58 | 59 | if err := config.store(ctx, req.Storage, name); err != nil { 60 | return nil, err 61 | } 62 | 63 | resp := &logical.Response{ 64 | Data: config.toMinimalResponseData(), 65 | } 66 | return resp, nil 67 | } 68 | 69 | const pathRotateRootHelpSyn = ` 70 | Request to rotate the Splunk credentials for a Splunk connection. 71 | ` 72 | 73 | const pathRotateRootHelpDesc = ` 74 | This path attempts to rotate the root credentials for the given Splunk connection. 75 | ` 76 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/fatih/structs" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | type roleConfig struct { 13 | Connection string `json:"connection" structs:"connection"` 14 | DefaultTTL time.Duration `json:"default_ttl" structs:"default_ttl"` 15 | MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl"` 16 | AllowedServerRoles []string `json:"allowed_server_roles" structs:"allowed_server_roles"` 17 | PasswordSpec *PasswordSpec `json:"password_spec" structs:"password_spec"` 18 | 19 | // Splunk user attributes 20 | Roles []string `json:"roles" structs:"roles"` 21 | DefaultApp string `json:"default_app,omitempty" structs:"default_app"` 22 | Email string `json:"email,omitempty" structs:"email"` 23 | TZ string `json:"tz,omitempty" structs:"tz"` 24 | UserPrefix string `json:"user_prefix,omitempty" structs:"user_prefix"` 25 | UserIDScheme string `json:"user_id_scheme,omitempty" structs:"user_id_scheme"` 26 | } 27 | 28 | // Role returns nil if role named `name` does not exist in `storage`, otherwise 29 | // returns the role. The second return value is non-nil on error. 30 | func roleConfigLoad(ctx context.Context, s logical.Storage, name string) (*roleConfig, error) { 31 | if name == "" { 32 | return nil, fmt.Errorf("invalid role name") 33 | } 34 | 35 | entry, err := s.Get(ctx, rolesPrefix+name) 36 | if err != nil { 37 | return nil, fmt.Errorf("error retrieving role: %w", err) 38 | } 39 | if entry == nil { 40 | return nil, nil 41 | } 42 | 43 | role := roleConfig{} 44 | if err := entry.DecodeJSON(&role); err != nil { 45 | return nil, fmt.Errorf("error decoding role: %w", err) 46 | } 47 | return &role, nil 48 | } 49 | 50 | func (role *roleConfig) store(ctx context.Context, s logical.Storage, name string) error { 51 | entry, err := logical.StorageEntryJSON(rolesPrefix+name, role) 52 | if err != nil { 53 | return err 54 | } 55 | if err := s.Put(ctx, entry); err != nil { 56 | return fmt.Errorf("error writing %q JSON: %w", rolesPrefix+name, err) 57 | } 58 | return nil 59 | } 60 | 61 | func (role *roleConfig) toResponseData() map[string]interface{} { 62 | data := structs.New(role).Map() 63 | // need to patch up TTLs because time.Duration gets garbled 64 | data["default_ttl"] = int64(role.DefaultTTL.Seconds()) 65 | data["max_ttl"] = int64(role.MaxTTL.Seconds()) 66 | return data 67 | } 68 | -------------------------------------------------------------------------------- /rollback.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/logical" 9 | "github.com/mitchellh/mapstructure" 10 | ) 11 | 12 | const ( 13 | walTypeConn = "connection" 14 | walRollbackMinAge = 5 * time.Minute 15 | ) 16 | 17 | type walConnection struct { 18 | ID string 19 | } 20 | 21 | func (b *backend) walRollback(ctx context.Context, req *logical.Request, kind string, data interface{}) error { 22 | switch kind { 23 | case walTypeConn: 24 | return b.connectionRollback(ctx, req, data) 25 | default: 26 | return fmt.Errorf("unknown type to rollback") 27 | } 28 | } 29 | 30 | func (b *backend) connectionRollback(ctx context.Context, req *logical.Request, data interface{}) error { 31 | var entry walConnection 32 | if err := mapstructure.Decode(data, &entry); err != nil { 33 | return err 34 | } 35 | 36 | // remove old connection from cache 37 | if err := b.clearConnection(entry.ID); err != nil { 38 | // log and ignore errors 39 | b.Logger().Warn("error while clearing connection", "err", err) 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /scripts/golangci-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2019-06-13T12:03:03Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 119 | } 120 | echoerr() { 121 | echo "$@" 1>&2 122 | } 123 | log_prefix() { 124 | echo "$0" 125 | } 126 | _logp=6 127 | log_set_priority() { 128 | _logp="$1" 129 | } 130 | log_priority() { 131 | if test -z "$1"; then 132 | echo "$_logp" 133 | return 134 | fi 135 | [ "$1" -le "$_logp" ] 136 | } 137 | log_tag() { 138 | case $1 in 139 | 0) echo "emerg" ;; 140 | 1) echo "alert" ;; 141 | 2) echo "crit" ;; 142 | 3) echo "err" ;; 143 | 4) echo "warning" ;; 144 | 5) echo "notice" ;; 145 | 6) echo "info" ;; 146 | 7) echo "debug" ;; 147 | *) echo "$1" ;; 148 | esac 149 | } 150 | log_debug() { 151 | log_priority 7 || return 0 152 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 153 | } 154 | log_info() { 155 | log_priority 6 || return 0 156 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 157 | } 158 | log_err() { 159 | log_priority 3 || return 0 160 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 161 | } 162 | log_crit() { 163 | log_priority 2 || return 0 164 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 165 | } 166 | uname_os() { 167 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 168 | case "$os" in 169 | msys_nt) os="windows" ;; 170 | esac 171 | echo "$os" 172 | } 173 | uname_arch() { 174 | arch=$(uname -m) 175 | case $arch in 176 | x86_64) arch="amd64" ;; 177 | x86) arch="386" ;; 178 | i686) arch="386" ;; 179 | i386) arch="386" ;; 180 | aarch64) arch="arm64" ;; 181 | armv5*) arch="armv5" ;; 182 | armv6*) arch="armv6" ;; 183 | armv7*) arch="armv7" ;; 184 | esac 185 | echo ${arch} 186 | } 187 | uname_os_check() { 188 | os=$(uname_os) 189 | case "$os" in 190 | darwin) return 0 ;; 191 | dragonfly) return 0 ;; 192 | freebsd) return 0 ;; 193 | linux) return 0 ;; 194 | android) return 0 ;; 195 | nacl) return 0 ;; 196 | netbsd) return 0 ;; 197 | openbsd) return 0 ;; 198 | plan9) return 0 ;; 199 | solaris) return 0 ;; 200 | windows) return 0 ;; 201 | esac 202 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 203 | return 1 204 | } 205 | uname_arch_check() { 206 | arch=$(uname_arch) 207 | case "$arch" in 208 | 386) return 0 ;; 209 | amd64) return 0 ;; 210 | arm64) return 0 ;; 211 | armv5) return 0 ;; 212 | armv6) return 0 ;; 213 | armv7) return 0 ;; 214 | ppc64) return 0 ;; 215 | ppc64le) return 0 ;; 216 | mips) return 0 ;; 217 | mipsle) return 0 ;; 218 | mips64) return 0 ;; 219 | mips64le) return 0 ;; 220 | s390x) return 0 ;; 221 | amd64p32) return 0 ;; 222 | esac 223 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 224 | return 1 225 | } 226 | untar() { 227 | tarball=$1 228 | case "${tarball}" in 229 | *.tar.gz | *.tgz) tar -xzf "${tarball}" ;; 230 | *.tar) tar -xf "${tarball}" ;; 231 | *.zip) unzip "${tarball}" ;; 232 | *) 233 | log_err "untar unknown archive format for ${tarball}" 234 | return 1 235 | ;; 236 | esac 237 | } 238 | http_download_curl() { 239 | local_file=$1 240 | source_url=$2 241 | header=$3 242 | if [ -z "$header" ]; then 243 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 244 | else 245 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 246 | fi 247 | if [ "$code" != "200" ]; then 248 | log_debug "http_download_curl received HTTP status $code" 249 | return 1 250 | fi 251 | return 0 252 | } 253 | http_download_wget() { 254 | local_file=$1 255 | source_url=$2 256 | header=$3 257 | if [ -z "$header" ]; then 258 | wget -q -O "$local_file" "$source_url" 259 | else 260 | wget -q --header "$header" -O "$local_file" "$source_url" 261 | fi 262 | } 263 | http_download() { 264 | log_debug "http_download $2" 265 | if is_command curl; then 266 | http_download_curl "$@" 267 | return 268 | elif is_command wget; then 269 | http_download_wget "$@" 270 | return 271 | fi 272 | log_crit "http_download unable to find wget or curl" 273 | return 1 274 | } 275 | http_copy() { 276 | tmp=$(mktemp) 277 | http_download "${tmp}" "$1" "$2" || return 1 278 | body=$(cat "$tmp") 279 | rm -f "${tmp}" 280 | echo "$body" 281 | } 282 | github_release() { 283 | owner_repo=$1 284 | version=$2 285 | test -z "$version" && version="latest" 286 | giturl="https://github.com/${owner_repo}/releases/${version}" 287 | json=$(http_copy "$giturl" "Accept:application/json") 288 | test -z "$json" && return 1 289 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 290 | test -z "$version" && return 1 291 | echo "$version" 292 | } 293 | hash_sha256() { 294 | TARGET=${1:-/dev/stdin} 295 | if is_command gsha256sum; then 296 | hash=$(gsha256sum "$TARGET") || return 1 297 | echo "$hash" | cut -d ' ' -f 1 298 | elif is_command sha256sum; then 299 | hash=$(sha256sum "$TARGET") || return 1 300 | echo "$hash" | cut -d ' ' -f 1 301 | elif is_command shasum; then 302 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 303 | echo "$hash" | cut -d ' ' -f 1 304 | elif is_command openssl; then 305 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 306 | echo "$hash" | cut -d ' ' -f a 307 | else 308 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 309 | return 1 310 | fi 311 | } 312 | hash_sha256_verify() { 313 | TARGET=$1 314 | checksums=$2 315 | if [ -z "$checksums" ]; then 316 | log_err "hash_sha256_verify checksum file not specified in arg2" 317 | return 1 318 | fi 319 | BASENAME=${TARGET##*/} 320 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 321 | if [ -z "$want" ]; then 322 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 323 | return 1 324 | fi 325 | got=$(hash_sha256 "$TARGET") 326 | if [ "$want" != "$got" ]; then 327 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 328 | return 1 329 | fi 330 | } 331 | cat /dev/null <&2 19 | exit 1 20 | } 21 | rm -f "$TAR_FILE" 22 | curl -s -L -o "$TAR_FILE" \ 23 | "$RELEASES_URL/download/$VERSION/goreleaser_$(uname -s)_$(uname -m).tar.gz" 24 | } 25 | 26 | download 27 | tar -xf "$TAR_FILE" -C "$TMPDIR" 28 | "${TMPDIR}/goreleaser" "$@" 29 | -------------------------------------------------------------------------------- /secret_creds.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 11 | ) 12 | 13 | const secretCredsType = "creds" 14 | 15 | func (b *backend) pathSecretCreds() *framework.Secret { 16 | return &framework.Secret{ 17 | Type: secretCredsType, 18 | Fields: map[string]*framework.FieldSchema{}, 19 | 20 | Renew: b.secretCredsRenewHandler, 21 | Revoke: b.secretCredsRevokeHandler, 22 | } 23 | } 24 | 25 | func (b *backend) secretCredsRenewHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 26 | roleNameRaw, ok := req.Secret.InternalData["role"] 27 | if !ok { 28 | return nil, fmt.Errorf("missing role name") 29 | } 30 | roleName := roleNameRaw.(string) 31 | role, err := roleConfigLoad(ctx, req.Storage, roleName) 32 | if err != nil { 33 | return nil, err 34 | } 35 | if role == nil { 36 | return nil, fmt.Errorf("error during renew: could not find role with name %q", roleName) 37 | } 38 | 39 | nodeFQDN := "" 40 | nodeFQDNRaw, ok := req.Secret.InternalData["node_fqdn"] 41 | if ok { 42 | nodeFQDN = nodeFQDNRaw.(string) 43 | } 44 | 45 | // Make sure we increase the VALID UNTIL endpoint for this user. 46 | ttl, _, err := framework.CalculateTTL(b.System(), req.Secret.Increment, role.DefaultTTL, 0, role.MaxTTL, 0, req.Secret.IssueTime) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | resp := &logical.Response{Secret: req.Secret} 52 | resp.Secret.TTL = role.DefaultTTL 53 | resp.Secret.MaxTTL = role.MaxTTL 54 | if ttl > 0 { 55 | expireTime := time.Now().Add(ttl) 56 | _ = expireTime 57 | config, err := connectionConfigLoad(ctx, req.Storage, role.Connection) 58 | if err != nil { 59 | return nil, err 60 | } 61 | conn, err := b.ensureNodeConnection(ctx, config, nodeFQDN) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if conn == nil { 66 | return nil, fmt.Errorf("error getting Splunk connection") 67 | } 68 | if _, _, err = conn.Introspection.ServerInfo(); err != nil { 69 | resp.AddWarning(fmt.Sprintf("failed to renew lease: %s", err)) 70 | } 71 | } 72 | return resp, nil 73 | } 74 | 75 | func (b *backend) secretCredsRevokeHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 76 | connNameRaw, ok := req.Secret.InternalData["connection"] 77 | if !ok { 78 | return nil, fmt.Errorf("no connection name was provided") 79 | } 80 | connName, ok := connNameRaw.(string) 81 | if !ok { 82 | return nil, fmt.Errorf("unable to convert connection name") 83 | } 84 | nodeFQDN := "" 85 | nodeFQDNRaw, ok := req.Secret.InternalData["node_fqdn"] 86 | if ok { 87 | nodeFQDN = nodeFQDNRaw.(string) 88 | } 89 | usernameRaw, ok := req.Secret.InternalData["username"] 90 | if !ok { 91 | return nil, fmt.Errorf("username is missing on the lease") 92 | } 93 | username := usernameRaw.(string) 94 | 95 | config, err := connectionConfigLoad(ctx, req.Storage, connName) 96 | if err != nil { 97 | return nil, err 98 | } 99 | conn, err := b.ensureNodeConnection(ctx, config, nodeFQDN) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | _, _, err = conn.AccessControl.Authentication.Users.Delete(username) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return nil, nil 109 | } 110 | 111 | func (b *backend) ensureNodeConnection(ctx context.Context, config *splunkConfig, nodeFQDN string) (*splunk.API, error) { 112 | b.Logger().Debug("node connection", "nodeFQDN", nodeFQDN) 113 | if nodeFQDN == "" { 114 | return b.ensureConnection(ctx, config) 115 | } 116 | 117 | // we connect to a node, not the cluster master 118 | nodeConfig := *config 119 | nodeConfig.URL = "https://" + nodeFQDN + ":8089" 120 | return nodeConfig.newConnection(ctx) // XXX cache 121 | } 122 | -------------------------------------------------------------------------------- /testdata/Test_findNode.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idm-i-074b0895939212e99.foo.example.com%3A8089", 4 | "name": "idm-i-074b0895939212e99.foo.example.com:8089", 5 | "content": { 6 | "host": "idm-i-074b0895939212e99.foo.example.com", 7 | "host_fqdn": "idm-i-074b0895939212e99", 8 | "peerName": "idm-i-074b0895939212e99.foo.example.com", 9 | "server_roles": [ 10 | "cluster_search_head", 11 | "search_head", 12 | "kv_store" 13 | ], 14 | "version": "7.0.0" 15 | } 16 | }, 17 | { 18 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-00a4c6813929dcccd.foo.example.com%3A8089", 19 | "name": "idx-i-00a4c6813929dcccd.foo.example.com:8089", 20 | "content": { 21 | "host": "idx-i-00a4c6813929dcccd.foo.example.com", 22 | "host_fqdn": "idx-i-00a4c6813929dcccd", 23 | "peerName": "idx-i-00a4c6813929dcccd.foo.example.com", 24 | "server_roles": [ 25 | "indexer", 26 | "cluster_slave", 27 | "search_peer" 28 | ], 29 | "version": "7.0.0" 30 | } 31 | }, 32 | { 33 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-01248bc6b6f6dcf29.foo.example.com%3A8089", 34 | "name": "idx-i-01248bc6b6f6dcf29.foo.example.com:8089", 35 | "content": { 36 | "host": "idx-i-01248bc6b6f6dcf29.foo.example.com", 37 | "host_fqdn": "idx-i-01248bc6b6f6dcf29", 38 | "peerName": "idx-i-01248bc6b6f6dcf29.foo.example.com", 39 | "server_roles": [ 40 | "indexer", 41 | "cluster_slave", 42 | "search_peer" 43 | ], 44 | "version": "7.0.0" 45 | } 46 | }, 47 | { 48 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-017690c5a08a30279.foo.example.com%3A8089", 49 | "name": "idx-i-017690c5a08a30279.foo.example.com:8089", 50 | "content": { 51 | "host": "idx-i-017690c5a08a30279.foo.example.com", 52 | "host_fqdn": "idx-i-017690c5a08a30279", 53 | "peerName": "idx-i-017690c5a08a30279.foo.example.com", 54 | "server_roles": [ 55 | "indexer", 56 | "cluster_slave", 57 | "search_peer" 58 | ], 59 | "version": "7.0.0" 60 | } 61 | }, 62 | { 63 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-01825076fa96ad270.foo.example.com%3A8089", 64 | "name": "idx-i-01825076fa96ad270.foo.example.com:8089", 65 | "content": { 66 | "host": "idx-i-01825076fa96ad270.foo.example.com", 67 | "host_fqdn": "idx-i-01825076fa96ad270", 68 | "peerName": "idx-i-01825076fa96ad270.foo.example.com", 69 | "server_roles": [ 70 | "indexer", 71 | "cluster_slave", 72 | "search_peer" 73 | ], 74 | "version": "7.0.0" 75 | } 76 | }, 77 | { 78 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-01fccfb9b81e76f03.foo.example.com%3A8089", 79 | "name": "idx-i-01fccfb9b81e76f03.foo.example.com:8089", 80 | "content": { 81 | "host": "idx-i-01fccfb9b81e76f03.foo.example.com", 82 | "host_fqdn": "idx-i-01fccfb9b81e76f03", 83 | "peerName": "idx-i-01fccfb9b81e76f03.foo.example.com", 84 | "server_roles": [ 85 | "indexer", 86 | "cluster_slave", 87 | "search_peer" 88 | ], 89 | "version": "7.0.0" 90 | } 91 | }, 92 | { 93 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-02336e0fe07b5e211.foo.example.com%3A8089", 94 | "name": "idx-i-02336e0fe07b5e211.foo.example.com:8089", 95 | "content": { 96 | "host": "idx-i-02336e0fe07b5e211.foo.example.com", 97 | "host_fqdn": "idx-i-02336e0fe07b5e211", 98 | "peerName": "idx-i-02336e0fe07b5e211.foo.example.com", 99 | "server_roles": [ 100 | "indexer", 101 | "cluster_slave", 102 | "search_peer" 103 | ], 104 | "version": "7.0.0" 105 | } 106 | }, 107 | { 108 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-030038baf9fcddf0b.foo.example.com%3A8089", 109 | "name": "idx-i-030038baf9fcddf0b.foo.example.com:8089", 110 | "content": { 111 | "host": "idx-i-030038baf9fcddf0b.foo.example.com", 112 | "host_fqdn": "idx-i-030038baf9fcddf0b", 113 | "peerName": "idx-i-030038baf9fcddf0b.foo.example.com", 114 | "server_roles": [ 115 | "indexer", 116 | "cluster_slave", 117 | "search_peer" 118 | ], 119 | "version": "7.0.0" 120 | } 121 | }, 122 | { 123 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0357077de4a68b84f.foo.example.com%3A8089", 124 | "name": "idx-i-0357077de4a68b84f.foo.example.com:8089", 125 | "content": { 126 | "host": "idx-i-0357077de4a68b84f.foo.example.com", 127 | "host_fqdn": "idx-i-0357077de4a68b84f", 128 | "peerName": "idx-i-0357077de4a68b84f.foo.example.com", 129 | "server_roles": [ 130 | "indexer", 131 | "cluster_slave", 132 | "search_peer" 133 | ], 134 | "version": "7.0.0" 135 | } 136 | }, 137 | { 138 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-03be181bf5618364e.foo.example.com%3A8089", 139 | "name": "idx-i-03be181bf5618364e.foo.example.com:8089", 140 | "content": { 141 | "host": "idx-i-03be181bf5618364e.foo.example.com", 142 | "host_fqdn": "idx-i-03be181bf5618364e", 143 | "peerName": "idx-i-03be181bf5618364e.foo.example.com", 144 | "server_roles": [ 145 | "indexer", 146 | "cluster_slave", 147 | "search_peer" 148 | ], 149 | "version": "7.0.0" 150 | } 151 | }, 152 | { 153 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-04f45a3b9fa99bd36.foo.example.com%3A8089", 154 | "name": "idx-i-04f45a3b9fa99bd36.foo.example.com:8089", 155 | "content": { 156 | "host": "idx-i-04f45a3b9fa99bd36.foo.example.com", 157 | "host_fqdn": "idx-i-04f45a3b9fa99bd36", 158 | "peerName": "idx-i-04f45a3b9fa99bd36.foo.example.com", 159 | "server_roles": [ 160 | "indexer", 161 | "cluster_slave", 162 | "search_peer" 163 | ], 164 | "version": "7.0.0" 165 | } 166 | }, 167 | { 168 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-057b4fe7cf4d83907.foo.example.com%3A8089", 169 | "name": "idx-i-057b4fe7cf4d83907.foo.example.com:8089", 170 | "content": { 171 | "host": "idx-i-057b4fe7cf4d83907.foo.example.com", 172 | "host_fqdn": "idx-i-057b4fe7cf4d83907", 173 | "peerName": "idx-i-057b4fe7cf4d83907.foo.example.com", 174 | "server_roles": [ 175 | "indexer", 176 | "cluster_slave", 177 | "search_peer" 178 | ], 179 | "version": "7.0.0" 180 | } 181 | }, 182 | { 183 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-06693856a15906dae.foo.example.com%3A8089", 184 | "name": "idx-i-06693856a15906dae.foo.example.com:8089", 185 | "content": { 186 | "host": "idx-i-06693856a15906dae.foo.example.com", 187 | "host_fqdn": "idx-i-06693856a15906dae", 188 | "peerName": "idx-i-06693856a15906dae.foo.example.com", 189 | "server_roles": [ 190 | "indexer", 191 | "cluster_slave", 192 | "search_peer" 193 | ], 194 | "version": "7.0.0" 195 | } 196 | }, 197 | { 198 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-07a9549878f0426c5.foo.example.com%3A8089", 199 | "name": "idx-i-07a9549878f0426c5.foo.example.com:8089", 200 | "content": { 201 | "host": "idx-i-07a9549878f0426c5.foo.example.com", 202 | "host_fqdn": "idx-i-07a9549878f0426c5", 203 | "peerName": "idx-i-07a9549878f0426c5.foo.example.com", 204 | "server_roles": [ 205 | "indexer", 206 | "cluster_slave", 207 | "search_peer" 208 | ], 209 | "version": "7.0.0" 210 | } 211 | }, 212 | { 213 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-086d414437292a0af.foo.example.com%3A8089", 214 | "name": "idx-i-086d414437292a0af.foo.example.com:8089", 215 | "content": { 216 | "host": "idx-i-086d414437292a0af.foo.example.com", 217 | "host_fqdn": "idx-i-086d414437292a0af", 218 | "peerName": "idx-i-086d414437292a0af.foo.example.com", 219 | "server_roles": [ 220 | "indexer", 221 | "cluster_slave", 222 | "search_peer" 223 | ], 224 | "version": "7.0.0" 225 | } 226 | }, 227 | { 228 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0882cd969e664ce99.foo.example.com%3A8089", 229 | "name": "idx-i-0882cd969e664ce99.foo.example.com:8089", 230 | "content": { 231 | "host": "idx-i-0882cd969e664ce99.foo.example.com", 232 | "host_fqdn": "idx-i-0882cd969e664ce99", 233 | "peerName": "idx-i-0882cd969e664ce99.foo.example.com", 234 | "server_roles": [ 235 | "indexer", 236 | "cluster_slave", 237 | "search_peer" 238 | ], 239 | "version": "7.0.0" 240 | } 241 | }, 242 | { 243 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-08b44057c9fc0c522.foo.example.com%3A8089", 244 | "name": "idx-i-08b44057c9fc0c522.foo.example.com:8089", 245 | "content": { 246 | "host": "idx-i-08b44057c9fc0c522.foo.example.com", 247 | "host_fqdn": "idx-i-08b44057c9fc0c522", 248 | "peerName": "idx-i-08b44057c9fc0c522.foo.example.com", 249 | "server_roles": [ 250 | "indexer", 251 | "cluster_slave", 252 | "search_peer" 253 | ], 254 | "version": "7.0.0" 255 | } 256 | }, 257 | { 258 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-08c29ddec22a834db.foo.example.com%3A8089", 259 | "name": "idx-i-08c29ddec22a834db.foo.example.com:8089", 260 | "content": { 261 | "host": "idx-i-08c29ddec22a834db.foo.example.com", 262 | "host_fqdn": "idx-i-08c29ddec22a834db", 263 | "peerName": "idx-i-08c29ddec22a834db.foo.example.com", 264 | "server_roles": [ 265 | "indexer", 266 | "cluster_slave", 267 | "search_peer" 268 | ], 269 | "version": "7.0.0" 270 | } 271 | }, 272 | { 273 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-091e002380e20205d.foo.example.com%3A8089", 274 | "name": "idx-i-091e002380e20205d.foo.example.com:8089", 275 | "content": { 276 | "host": "idx-i-091e002380e20205d.foo.example.com", 277 | "host_fqdn": "idx-i-091e002380e20205d", 278 | "peerName": "idx-i-091e002380e20205d.foo.example.com", 279 | "server_roles": [ 280 | "indexer", 281 | "cluster_slave", 282 | "search_peer" 283 | ], 284 | "version": "7.0.0" 285 | } 286 | }, 287 | { 288 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0958db44eea01a796.foo.example.com%3A8089", 289 | "name": "idx-i-0958db44eea01a796.foo.example.com:8089", 290 | "content": { 291 | "host": "idx-i-0958db44eea01a796.foo.example.com", 292 | "host_fqdn": "idx-i-0958db44eea01a796", 293 | "peerName": "idx-i-0958db44eea01a796.foo.example.com", 294 | "server_roles": [ 295 | "indexer", 296 | "cluster_slave", 297 | "search_peer" 298 | ], 299 | "version": "7.0.0" 300 | } 301 | }, 302 | { 303 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-09aea64a82ba34b70.foo.example.com%3A8089", 304 | "name": "idx-i-09aea64a82ba34b70.foo.example.com:8089", 305 | "content": { 306 | "host": "idx-i-09aea64a82ba34b70.foo.example.com", 307 | "host_fqdn": "idx-i-09aea64a82ba34b70", 308 | "peerName": "idx-i-09aea64a82ba34b70.foo.example.com", 309 | "server_roles": [ 310 | "indexer", 311 | "cluster_slave", 312 | "search_peer" 313 | ], 314 | "version": "7.0.0" 315 | } 316 | }, 317 | { 318 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-09cea942b1459e1dc.foo.example.com%3A8089", 319 | "name": "idx-i-09cea942b1459e1dc.foo.example.com:8089", 320 | "content": { 321 | "host": "idx-i-09cea942b1459e1dc.foo.example.com", 322 | "host_fqdn": "idx-i-09cea942b1459e1dc", 323 | "peerName": "idx-i-09cea942b1459e1dc.foo.example.com", 324 | "server_roles": [ 325 | "indexer", 326 | "cluster_slave", 327 | "search_peer" 328 | ], 329 | "version": "7.0.0" 330 | } 331 | }, 332 | { 333 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0a65be6b5d80da019.foo.example.com%3A8089", 334 | "name": "idx-i-0a65be6b5d80da019.foo.example.com:8089", 335 | "content": { 336 | "host": "idx-i-0a65be6b5d80da019.foo.example.com", 337 | "host_fqdn": "idx-i-0a65be6b5d80da019", 338 | "peerName": "idx-i-0a65be6b5d80da019.foo.example.com", 339 | "server_roles": [ 340 | "indexer", 341 | "cluster_slave", 342 | "search_peer" 343 | ], 344 | "version": "7.0.0" 345 | } 346 | }, 347 | { 348 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0bac5952b7f99e592.foo.example.com%3A8089", 349 | "name": "idx-i-0bac5952b7f99e592.foo.example.com:8089", 350 | "content": { 351 | "host": "idx-i-0bac5952b7f99e592.foo.example.com", 352 | "host_fqdn": "idx-i-0bac5952b7f99e592", 353 | "peerName": "idx-i-0bac5952b7f99e592.foo.example.com", 354 | "server_roles": [ 355 | "indexer", 356 | "cluster_slave", 357 | "search_peer" 358 | ], 359 | "version": "7.0.0" 360 | } 361 | }, 362 | { 363 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0d0aa6c139f795124.foo.example.com%3A8089", 364 | "name": "idx-i-0d0aa6c139f795124.foo.example.com:8089", 365 | "content": { 366 | "host": "idx-i-0d0aa6c139f795124.foo.example.com", 367 | "host_fqdn": "idx-i-0d0aa6c139f795124", 368 | "peerName": "idx-i-0d0aa6c139f795124.foo.example.com", 369 | "server_roles": [ 370 | "indexer", 371 | "cluster_slave", 372 | "search_peer" 373 | ], 374 | "version": "7.0.0" 375 | } 376 | }, 377 | { 378 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0d8a68643fab7af61.foo.example.com%3A8089", 379 | "name": "idx-i-0d8a68643fab7af61.foo.example.com:8089", 380 | "content": { 381 | "host": "idx-i-0d8a68643fab7af61.foo.example.com", 382 | "host_fqdn": "idx-i-0d8a68643fab7af61", 383 | "peerName": "idx-i-0d8a68643fab7af61.foo.example.com", 384 | "server_roles": [ 385 | "indexer", 386 | "cluster_slave", 387 | "search_peer" 388 | ], 389 | "version": "7.0.0" 390 | } 391 | }, 392 | { 393 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0e792bc3f73908a3c.foo.example.com%3A8089", 394 | "name": "idx-i-0e792bc3f73908a3c.foo.example.com:8089", 395 | "content": { 396 | "host": "idx-i-0e792bc3f73908a3c.foo.example.com", 397 | "host_fqdn": "idx-i-0e792bc3f73908a3c", 398 | "peerName": "idx-i-0e792bc3f73908a3c.foo.example.com", 399 | "server_roles": [ 400 | "indexer", 401 | "cluster_slave", 402 | "search_peer" 403 | ], 404 | "version": "7.0.0" 405 | } 406 | }, 407 | { 408 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0ead7cbf544769726.foo.example.com%3A8089", 409 | "name": "idx-i-0ead7cbf544769726.foo.example.com:8089", 410 | "content": { 411 | "host": "idx-i-0ead7cbf544769726.foo.example.com", 412 | "host_fqdn": "idx-i-0ead7cbf544769726", 413 | "peerName": "idx-i-0ead7cbf544769726.foo.example.com", 414 | "server_roles": [ 415 | "indexer", 416 | "cluster_slave", 417 | "search_peer" 418 | ], 419 | "version": "7.0.0" 420 | } 421 | }, 422 | { 423 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0f393f537bcd30a79.foo.example.com%3A8089", 424 | "name": "idx-i-0f393f537bcd30a79.foo.example.com:8089", 425 | "content": { 426 | "host": "idx-i-0f393f537bcd30a79.foo.example.com", 427 | "host_fqdn": "idx-i-0f393f537bcd30a79", 428 | "peerName": "idx-i-0f393f537bcd30a79.foo.example.com", 429 | "server_roles": [ 430 | "indexer", 431 | "cluster_slave", 432 | "search_peer" 433 | ], 434 | "version": "7.0.0" 435 | } 436 | }, 437 | { 438 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0f615781c4db70d0e.foo.example.com%3A8089", 439 | "name": "idx-i-0f615781c4db70d0e.foo.example.com:8089", 440 | "content": { 441 | "host": "idx-i-0f615781c4db70d0e.foo.example.com", 442 | "host_fqdn": "idx-i-0f615781c4db70d0e", 443 | "peerName": "idx-i-0f615781c4db70d0e.foo.example.com", 444 | "server_roles": [ 445 | "indexer", 446 | "cluster_slave", 447 | "search_peer" 448 | ], 449 | "version": "7.0.0" 450 | } 451 | }, 452 | { 453 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/idx-i-0f7caa712a143b5fc.foo.example.com%3A8089", 454 | "name": "idx-i-0f7caa712a143b5fc.foo.example.com:8089", 455 | "content": { 456 | "host": "idx-i-0f7caa712a143b5fc.foo.example.com", 457 | "host_fqdn": "idx-i-0f7caa712a143b5fc", 458 | "peerName": "idx-i-0f7caa712a143b5fc.foo.example.com", 459 | "server_roles": [ 460 | "indexer", 461 | "cluster_slave", 462 | "search_peer" 463 | ], 464 | "version": "7.0.0" 465 | } 466 | }, 467 | { 468 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/sh-i-0363b83af8c70d04a.foo.example.com%3A8089", 469 | "name": "sh-i-0363b83af8c70d04a.foo.example.com:8089", 470 | "content": { 471 | "host": "sh-i-0363b83af8c70d04a.foo.example.com", 472 | "host_fqdn": "sh-i-0363b83af8c70d04a", 473 | "peerName": "sh-i-0363b83af8c70d04a.foo.example.com", 474 | "server_roles": [ 475 | "cluster_search_head", 476 | "search_head", 477 | "kv_store" 478 | ], 479 | "version": "7.0.0" 480 | } 481 | }, 482 | { 483 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/sh-i-038b5e4d4542efdbd.foo.example.com%3A8089", 484 | "name": "sh-i-038b5e4d4542efdbd.foo.example.com:8089", 485 | "content": { 486 | "host": "sh-i-038b5e4d4542efdbd.foo.example.com", 487 | "host_fqdn": "sh-i-038b5e4d4542efdbd", 488 | "peerName": "sh-i-038b5e4d4542efdbd.foo.example.com", 489 | "server_roles": [ 490 | "cluster_search_head", 491 | "deployment_client", 492 | "search_head", 493 | "kv_store" 494 | ], 495 | "version": "7.0.0" 496 | } 497 | }, 498 | { 499 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/sh-i-0781d2de3c9f81249.foo.example.com%3A8089", 500 | "name": "sh-i-0781d2de3c9f81249.foo.example.com:8089", 501 | "content": { 502 | "host": "sh-i-0781d2de3c9f81249.foo.example.com", 503 | "host_fqdn": "sh-i-0781d2de3c9f81249", 504 | "peerName": "sh-i-0781d2de3c9f81249.foo.example.com", 505 | "server_roles": [ 506 | "cluster_search_head", 507 | "search_head", 508 | "kv_store" 509 | ], 510 | "version": "7.0.0" 511 | } 512 | }, 513 | { 514 | "id": "https://cm.foo.example.com:8089/services/search/distributed/peers/sh-i-0a12fdd509c2a2954.foo.example.com%3A8089", 515 | "name": "sh-i-0a12fdd509c2a2954.foo.example.com:8089", 516 | "content": { 517 | "host": "sh-i-0a12fdd509c2a2954.foo.example.com", 518 | "host_fqdn": "sh-i-0a12fdd509c2a2954", 519 | "peerName": "sh-i-0a12fdd509c2a2954.foo.example.com", 520 | "server_roles": [ 521 | "cluster_search_head", 522 | "search_head", 523 | "search_peer", 524 | "kv_store" 525 | ], 526 | "version": "7.0.0" 527 | } 528 | } 529 | ] 530 | -------------------------------------------------------------------------------- /testmain_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splunk/vault-plugin-splunk/clients/splunk" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | splunk.WithTestMainSetup(m) 11 | } 12 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/fatih/structs" 8 | 9 | "gotest.tools/v3/assert" 10 | ) 11 | 12 | func TestTimeMarshalling(t *testing.T) { 13 | type test struct { 14 | TTL time.Duration `json:"ttl" structs:"ttl"` 15 | } 16 | 17 | i := 10 18 | s := &test{ 19 | TTL: time.Duration(i) * time.Second, 20 | } 21 | assert.Assert(t, s.TTL.Seconds() == 10) 22 | 23 | m := structs.New(s).Map() 24 | 25 | ttl, ok := m["ttl"] 26 | assert.Assert(t, ok) 27 | assert.Assert(t, ttl.(time.Duration).Seconds() == 10) 28 | } 29 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | func getValue(data *framework.FieldData, op logical.Operation, key string) (interface{}, bool) { 13 | if raw, ok := data.GetOk(key); ok { 14 | return raw, true 15 | } 16 | if op == logical.CreateOperation { 17 | return data.Get(key), true 18 | } 19 | return nil, false 20 | } 21 | 22 | // nolint:deadcode,unused 23 | func decodeValue(data *framework.FieldData, op logical.Operation, key string, v interface{}) error { 24 | raw, ok := getValue(data, op, key) 25 | if ok { 26 | rraw := reflect.ValueOf(raw) 27 | rv := reflect.ValueOf(v) 28 | rv.Elem().Set(rraw) 29 | } 30 | return nil 31 | } 32 | 33 | var indentWhitespace = regexp.MustCompile("(?m:^[ \t]*)") 34 | 35 | func trimIndent(s string) string { 36 | return strings.TrimRight(indentWhitespace.ReplaceAllString(s, ""), "") 37 | } 38 | -------------------------------------------------------------------------------- /uuid.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "github.com/hashicorp/go-uuid" 5 | "github.com/mr-tron/base58" 6 | ) 7 | 8 | func GenerateShortUUID(size int) (string, error) { 9 | bytes, err := uuid.GenerateRandomBytes(size) 10 | if err != nil { 11 | return "", err 12 | } 13 | return FormatShortUUID(bytes), nil 14 | } 15 | 16 | func FormatShortUUID(bytes []byte) string { 17 | return base58.Encode(bytes) 18 | } 19 | -------------------------------------------------------------------------------- /uuid_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/mr-tron/base58" 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestGenerateShortUUID(t *testing.T) { 12 | for _, size := range []int{8, 16} { 13 | uuid, err := GenerateShortUUID(size) 14 | assert.NilError(t, err) 15 | fmt.Println(uuid) 16 | bytes, err := base58.Decode(uuid) 17 | assert.NilError(t, err) 18 | assert.Equal(t, size, len(bytes)) 19 | } 20 | } 21 | 22 | func TestFormatShortUUID(t *testing.T) { 23 | type args struct { 24 | bytes []byte 25 | } 26 | tests := []struct { 27 | name string 28 | args args 29 | want string 30 | }{ 31 | { 32 | name: "0_x_8", 33 | args: args{[]byte{0, 0, 0, 0, 0, 0, 0, 0}}, 34 | want: "11111111", 35 | }, 36 | { 37 | name: "1_x_8", 38 | args: args{[]byte{0, 0, 0, 0, 0, 0, 0, 1}}, 39 | want: "11111112", 40 | }, 41 | { 42 | name: "255_x_8", 43 | args: args{[]byte{255, 255, 255, 255, 255, 255, 255, 255}}, 44 | want: "jpXCZedGfVQ", 45 | }, 46 | { 47 | name: "uuid4", 48 | args: args{[]byte{0xb3, 0x3b, 0x6b, 0x76, 0xcb, 0x1f, 0xbe, 0x28, 0xbd, 0x5b, 0x86, 0xca, 0x76, 0x23, 0x72, 0x72}}, 49 | want: "P8gD5AcMf2n6FkGz9nydEZ", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | if got := FormatShortUUID(tt.args.bytes); got != tt.want { 56 | t.Errorf("FormatShortUUID() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /vault.hcl.in: -------------------------------------------------------------------------------- 1 | plugin_directory = "@@GOBIN@@" 2 | --------------------------------------------------------------------------------