From 5c06dc0e141f1a4ff439e7bd8c407324142a82ff Mon Sep 17 00:00:00 2001 From: Rohith Date: Mon, 21 Sep 2015 11:31:12 +0100 Subject: [PATCH] - added the csv output format - adding the auth backends - general cleanup of the code - fixed up the tests --- LICENSE | 202 -------------------------------------------- README.md | 4 + auth_userpass.go | 61 +++++++++++++ config.go | 45 ++++------ config_test.go | 5 +- main.go | 16 ++-- main_test.go | 17 ---- tests/auth_file.yml | 3 + utils.go | 68 ++++++++++++++- vault.go | 103 +++++++++++----------- vault_resource.go | 2 +- vault_test.go | 17 ---- watched_resource.go | 64 ++++++++++++++ 13 files changed, 283 insertions(+), 324 deletions(-) delete mode 100644 LICENSE create mode 100644 auth_userpass.go delete mode 100644 main_test.go create mode 100644 tests/auth_file.yml delete mode 100644 vault_test.go create mode 100644 watched_resource.go diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8f71f43..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/README.md b/README.md index 49b979d..6a9a049 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ The above say's - Apply the IAM policy, renew the policy when required and file the API tokens to .s3_creds in the /etc/secrets directory - Read the template at /etc/templates/db.tmpl, produce the content from Vault and write to /etc/credentials file +**Authentication** + +A authentication file can be specified + **Secret Renewals** The default behaviour of vault-sidekick is **not** to renew a lease, but to retrieve a new secret and allow the previous to diff --git a/auth_userpass.go b/auth_userpass.go new file mode 100644 index 0000000..2e84d86 --- /dev/null +++ b/auth_userpass.go @@ -0,0 +1,61 @@ +/* +Copyright 2015 Home Office All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + + "github.com/hashicorp/vault/api" + "github.com/golang/glog" +) + +// the userpass authentication plugin +type authUserPass struct { + // the vault client + client *api.Client +} + +// auth token +type UserPassLogin struct { + // the password for the account + Password string `json:"password,omitempty"` +} + +func newUserPass(client *api.Client) *authUserPass { + return &authUserPass{ + client: client, + } +} + +// create ... login with the username and password an +func (r authUserPass) create(username, password string) (*api.Secret, error) { + glog.V(10).Infof("using the userpass plugin, username: %s, password: %s", username, password) + + req := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", username)) + // step: create the token request + if err := req.SetJSONBody(UserPassLogin{Password: password}); err != nil { + return nil, err + } + // step: make the request + resp, err := r.client.RawRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + // step: parse and return auth + return api.ParseSecret(resp.Body) +} diff --git a/config.go b/config.go index bf2e8ff..16ff437 100644 --- a/config.go +++ b/config.go @@ -19,11 +19,8 @@ package main import ( "flag" "fmt" - "io/ioutil" "net/url" "time" - - "github.com/golang/glog" ) // config ... the command line configuration @@ -32,10 +29,12 @@ type config struct { vaultURL string // the token to connect to vault with vaultToken string - // a file / path containing a token for vault - vaultTokenFile string + // a file containing the authenticate options + vaultAuthFile string + // the authentication options + vaultAuthOptions map[string]string // the place to write the resources - secretsDirectory string + outputDir string // whether of not to remove the token post connection deleteToken bool // switch on dry run @@ -54,10 +53,10 @@ func init() { options.resources = new(vaultResources) flag.StringVar(&options.vaultURL, "vault", getEnv("VAULT_ADDR", "https://127.0.0.1:8200"), "the url the vault service is running behind (VAULT_ADDR if available)") flag.StringVar(&options.vaultToken, "token", "", "the token used to authenticate to teh vault service (VAULT_TOKEN if available)") - flag.StringVar(&options.vaultTokenFile, "tokenfile", getEnv("VAULT_TOKEN_FILE", ""), "the full path to file containing the vault token used for authentication (VAULT_TOKEN_FILE if available)") - flag.StringVar(&options.secretsDirectory, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write the protected resources (VAULT_OUTPUT if available)") + flag.StringVar(&options.vaultAuthFile, "auth", "", "a configuration file in a json or yaml containing authentication arguments") + flag.StringVar(&options.outputDir, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write the protected resources (VAULT_OUTPUT if available)") flag.BoolVar(&options.deleteToken, "delete-token", false, "once the we have connected to vault, delete the token file from disk") - flag.BoolVar(&options.dryRun, "dry-run", false, "perform a dry run, printing the content to screen") + flag.BoolVar(&options.dryRun, "dryrun", false, "perform a dry run, printing the content to screen") flag.DurationVar(&options.statsInterval, "stats", time.Duration(5)*time.Minute, "the interval to produce statistics on the accessed resources") flag.Var(options.resources, "cn", "a resource to retrieve and monitor from vault (e.g. pki:name:cert.name, secret:db_password, aws:s3_backup)") } @@ -72,14 +71,10 @@ func parseOptions() error { // validateOptions ... parses and validates the command line options func validateOptions(cfg *config) error { // step: validate the vault url - url, err := url.Parse(cfg.vaultURL) + _, err := url.Parse(cfg.vaultURL) if err != nil { return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL) } - // step: ensure the protocol scheme - if url.Scheme != "https" { - glog.Warningf("protocol scheme: %s is not secure and should be https", url.Scheme) - } // step: check if the token is in the VAULT_TOKEN var if cfg.vaultToken == "" { @@ -87,25 +82,19 @@ func validateOptions(cfg *config) error { } // step: ensure we have a token - if cfg.vaultToken == "" && cfg.vaultTokenFile == "" { - return fmt.Errorf("you have not either a token or a token file to authenticate with") + if cfg.vaultToken == "" && cfg.vaultAuthFile == "" { + return fmt.Errorf("you have not either a token or a authentication file to authenticate to vault with") } // step: read in the token if required - if cfg.vaultTokenFile != "" { - exists, err := fileExists(cfg.vaultTokenFile) - if !exists { - return fmt.Errorf("the token file: %s does not exists, please check", cfg.vaultTokenFile) + if cfg.vaultAuthFile != "" { + if exists, _ := fileExists(cfg.vaultAuthFile); !exists { + return fmt.Errorf("the token file: %s does not exists, please check", cfg.vaultAuthFile) } - if err != nil { - return fmt.Errorf("unable to check for token file: %s, error: %s", cfg.vaultTokenFile, err) + + if options.vaultAuthOptions, err = readConfigFile(options.vaultAuthFile); err != nil { + return fmt.Errorf("unable to read in authentication options from: %s, error: %s", cfg.vaultAuthFile, err) } - // step: read in the token - content, err := ioutil.ReadFile(cfg.vaultTokenFile) - if err != nil { - return fmt.Errorf("unable to read in token from file: %s, error: %s", cfg.vaultTokenFile, err) - } - cfg.vaultToken = string(content) } return nil diff --git a/config_test.go b/config_test.go index 12d6f77..e236224 100644 --- a/config_test.go +++ b/config_test.go @@ -24,11 +24,10 @@ import ( func TestValidateOptions(t *testing.T) { cfg := config{ - vaultToken: "", vaultURL: "https://127.0.0.1:8200", + vaultToken: "d0ds09dis09ids09ifsd", } - assert.NotNil(t, validateOptions(&cfg)) - cfg.vaultToken = "dkskdkskdsjdkjs" + assert.Nil(t, validateOptions(&cfg)) cfg.vaultURL = "https://127.0.0.1" assert.Nil(t, validateOptions(&cfg)) diff --git a/main.go b/main.go index 9fee736..f97d05c 100644 --- a/main.go +++ b/main.go @@ -37,9 +37,9 @@ func main() { } // step: create a client to vault - vault, err := newVaultService(options.vaultURL, options.vaultToken) + vault, err := newVaultService(options.vaultURL) if err != nil { - glog.Errorf("failed to create a vault client, error: %s", err) + showUsage("unable to create the vault client: %s", err) } // step: setup the termination signals @@ -47,7 +47,7 @@ func main() { signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) // step: create a channel to receive events upon and add our resources for renewal - ch := make(vaultEventsChannel, 10) + ch := make(chan vaultResourceEvent, 10) for _, rn := range options.resources.items { // step: valid the resource @@ -80,7 +80,7 @@ func processResource(rn *vaultResource, data map[string]interface{}) error { // step: determine the resource path resourcePath := rn.filename() if !strings.HasPrefix(resourcePath, "/") { - resourcePath = fmt.Sprintf("%s/%s", options.secretsDirectory, resourcePath) + resourcePath = fmt.Sprintf("%s/%s", options.outputDir, resourcePath) } // step: get the output format @@ -120,13 +120,19 @@ func processResource(rn *vaultResource, data map[string]interface{}) error { } return nil + case "csv": + var buf bytes.Buffer + for key, val := range data { + buf.WriteString(fmt.Sprintf("%s,%s\n", key, val)) + } + content = buf.Bytes() + case "txt": keys := getKeys(data) if len(keys) > 1 { // step: for plain formats we need to iterate the keys and produce a file per key for suffix, content := range data { filename := fmt.Sprintf("%s.%s", resourcePath, suffix) - // step: write the file if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil { glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", rn, suffix, filename, err) diff --git a/main_test.go b/main_test.go deleted file mode 100644 index a34bf67..0000000 --- a/main_test.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2015 Home Office All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main diff --git a/tests/auth_file.yml b/tests/auth_file.yml new file mode 100644 index 0000000..8eee4bc --- /dev/null +++ b/tests/auth_file.yml @@ -0,0 +1,3 @@ +method: userpass +username: admin +password: foo diff --git a/utils.go b/utils.go index a0bbb86..cd86c19 100644 --- a/utils.go +++ b/utils.go @@ -22,6 +22,10 @@ import ( "math/rand" "os" "time" +"io/ioutil" + "encoding/json" + "gopkg.in/yaml.v2" + "path" ) func init() { @@ -40,12 +44,23 @@ func showUsage(message string, args ...interface{}) { os.Exit(0) } -// randomWait ... wait for a random amout of time +// randomWait ... wait for a random amount of time +// min : the minimum amount of time willing to wait +// max : the maximum amount of time willing to wait func randomWait(min, max int) <-chan time.Time { return time.After(time.Duration(getRandomWithin(min, max)) * time.Second) } +// hasKey ... checks to see if a key is present +// key : the key we are looking for +// data : a map of strings to something we are looking at +func hasKey(key string, data map [string]interface{}) bool { + _, found := data[key] + return found +} + // getKeys ... retrieve a list of keys from the map +// data : the map which you wish to extract the keys from func getKeys(data map[string]interface{}) []string { var list []string for key := range data { @@ -54,6 +69,57 @@ func getKeys(data map[string]interface{}) []string { return list } +// readConfigFile ... read in a configuration file +// filename : the path to the file +func readConfigFile(filename string) (map[string]string, error) { + suffix := path.Ext(filename) + switch suffix { + case ".json": + return readJsonFile(filename) + case ".yaml": + return readYamlFile(filename) + case ".yml": + return readYamlFile(filename) + } + return nil, fmt.Errorf("unsupported config file format: %s", suffix) +} + +// readJsonFile ... read in and unmarshall the data into a map +// filename : the path to the file container the json data +func readJsonFile(filename string) (map[string]string, error) { + data := make(map[string]string, 0) + + content, err := ioutil.ReadFile(filename) + if err != nil { + return data, err + } + // unmarshall the data + err = json.Unmarshal(content, data) + if err != nil { + return data, err + } + + return data, nil +} + +// readYamlFile ... read in and unmarshall the data into a map +// filename : the path to the file container the yaml data +func readYamlFile(filename string) (map[string]string, error) { + data := make(map[string]string, 0) + + content, err := ioutil.ReadFile(filename) + if err != nil { + return data, err + } + // unmarshall the data + err = yaml.Unmarshal(content, data) + if err != nil { + return data, err + } + + return data, nil +} + // randomInt ... generate a random integer between min and max // min : the smallest number we can accept // max : the largest number we can accept diff --git a/vault.go b/vault.go index 05d9481..aa2b9ff 100644 --- a/vault.go +++ b/vault.go @@ -45,52 +45,9 @@ type vaultResourceEvent struct { secret map[string]interface{} } -// a channel of events -type vaultEventsChannel chan vaultResourceEvent - -// watchedResource ... is a resource which is being watched - i.e. when the item is coming up for renewal -// lets grab it and renew the lease -type watchedResource struct { - // the upstream listener to the event - listener vaultEventsChannel - // the resource itself - resource *vaultResource - // the last time the resource was retrieved - lastUpdated time.Time - // the time which the lease expires - leaseExpireTime time.Time - // the duration until we next time to renew lease - renewalTime time.Duration - // the secret - secret *api.Secret -} - -// notifyOnRenewal ... creates a trigger and notifies when a resource is up for renewal -func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) { - go func() { - // step: check if the resource has a pre-configured renewal time - r.renewalTime = r.resource.update - - // step: if the answer is no, we set the notification between 80-95% of the lease time of the secret - if r.renewalTime <= 0 { - glog.V(10).Infof("calculating the renewal between 80-95 pcent of lease time: %d seconds", r.secret.LeaseDuration) - r.renewalTime = time.Duration(getRandomWithin( - int(float64(r.secret.LeaseDuration)*0.8), - int(float64(r.secret.LeaseDuration)*0.95))) * time.Second - } - - glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime) - // step: wait for the duration - <-time.After(r.renewalTime) - // step: send the notification on the renewal channel - ch <- r - }() -} - // newVaultService ... creates a new implementation to speak to vault and retrieve the resources // url : the url of the vault service -// token : the token to use when speaking to vault -func newVaultService(url, token string) (*vaultService, error) { +func newVaultService(url string) (*vaultService, error) { var err error glog.Infof("creating a new vault client: %s", url) @@ -108,8 +65,16 @@ func newVaultService(url, token string) (*vaultService, error) { return nil, err } + // step: are we using a token? or do we need to authenticate and grab a token + if options.vaultToken == "" { + options.vaultToken, err = service.authenticate(options.vaultAuthOptions) + if err != nil { + return nil, err + } + } + // step: set the token for the client - service.client.SetToken(token) + service.client.SetToken(options.vaultToken) // step: start the service processor off service.vaultServiceProcessor() @@ -172,7 +137,7 @@ func (r vaultService) vaultServiceProcessor() { x.notifyOnRenewal(renewChannel) // step: update the upstream consumers - r.upstream(x, x.secret) + r.upstream(x) // A watched resource is coming up for renewal // - we attempt to renew the resource from vault @@ -221,7 +186,7 @@ func (r vaultService) vaultServiceProcessor() { x.notifyOnRenewal(renewChannel) // step: update any listener upstream - r.upstream(x, x.secret) + r.upstream(x) case lease := <-revokeChannel: @@ -242,6 +207,42 @@ func (r vaultService) vaultServiceProcessor() { }() } +// authenticate ... we need to authenticate to teh vault to grab a toke +// auth : a map containing the options required for authentication +func (r vaultService) authenticate(auth map[string]string) (string, error) { + var secret *api.Secret + var err error + + plugin, _ := auth["method"] + switch plugin { + case "userpass": + // step: get the options for this plugin + username, _ := auth["username"] + password, _ := auth["password"] + secret, err = newUserPass(r.client).create(username, password) + + default: + return "", fmt.Errorf("unsupported authentication plugin: %s", plugin) + } + // step: was there an error? + if err != nil { + return "", err + } + + // step: do we have auth information + if secret.Auth == nil { + return "", fmt.Errorf("invalid authentication response, no auth response") + } + + // step: return the client token + return secret.Auth.ClientToken, nil +} + +// reschedule ... reschedules an event back into a channel after n seconds +// rn : a pointer to the watched resource you wish to reschedule +// ch : the channel the resource should be placed into +// min : the minimum amount of time i'm willing to wait +// max : the maximum amount of time i'm willing to wait func (r vaultService) reschedule(rn *watchedResource, ch chan *watchedResource, min, max int) { go func(x *watchedResource) { glog.V(3).Infof("rescheduling the resource: %s, channel: %s", rn.resource, ch) @@ -250,13 +251,15 @@ func (r vaultService) reschedule(rn *watchedResource, ch chan *watchedResource, }(rn) } -func (r vaultService) upstream(item *watchedResource, s *api.Secret) { +// upstream ... the resource has changed thus we notify the upstream listener +// item : the item which has changed +func (r vaultService) upstream(item *watchedResource) { // step: chunk this into a go-routine not to block us go func() { glog.V(6).Infof("sending the event for resource: %s upstream to listener: %v", item.resource, item.listener) item.listener <- vaultResourceEvent{ resource: item.resource, - secret: s.Data, + secret: item.secret.Data, } }() } @@ -344,7 +347,7 @@ func (r vaultService) get(rn *watchedResource) (err error) { // watch ... add a watch on a resource and inform, renew which required and inform us when // the resource is ready -func (r *vaultService) watch(rn *vaultResource, ch vaultEventsChannel) { +func (r *vaultService) watch(rn *vaultResource, ch chan vaultResourceEvent) { glog.V(6).Infof("adding the resource: %s, listener: %v to service processor", rn, ch) r.resourceChannel <- &watchedResource{ diff --git a/vault_resource.go b/vault_resource.go index f8a5a7d..2a4d735 100644 --- a/vault_resource.go +++ b/vault_resource.go @@ -172,5 +172,5 @@ func (r vaultResource) filename() string { // String ... a string representation of the struct func (r vaultResource) String() string { - return fmt.Sprintf("%s/%s (%s|%t|%t)", r.resource, r.name, r.update, r.renewable, r.revoked) + return fmt.Sprintf("%s/%s", r.resource, r.name) } diff --git a/vault_test.go b/vault_test.go deleted file mode 100644 index a34bf67..0000000 --- a/vault_test.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2015 Home Office All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main diff --git a/watched_resource.go b/watched_resource.go new file mode 100644 index 0000000..7f568bb --- /dev/null +++ b/watched_resource.go @@ -0,0 +1,64 @@ +/* +Copyright 2015 Home Office All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "time" + + "github.com/golang/glog" + "github.com/hashicorp/vault/api" +) + + +// watchedResource ... is a resource which is being watched - i.e. when the item is coming up for renewal +// lets grab it and renew the lease +type watchedResource struct { + // the upstream listener to the event + listener chan vaultResourceEvent + // the resource itself + resource *vaultResource + // the last time the resource was retrieved + lastUpdated time.Time + // the time which the lease expires + leaseExpireTime time.Time + // the duration until we next time to renew lease + renewalTime time.Duration + // the secret + secret *api.Secret +} + +// notifyOnRenewal ... creates a trigger and notifies when a resource is up for renewal +func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) { + go func() { + // step: check if the resource has a pre-configured renewal time + r.renewalTime = r.resource.update + + // step: if the answer is no, we set the notification between 80-95% of the lease time of the secret + if r.renewalTime <= 0 { + glog.V(10).Infof("calculating the renewal between 80-95 pcent of lease time: %d seconds", r.secret.LeaseDuration) + r.renewalTime = time.Duration(getRandomWithin( + int(float64(r.secret.LeaseDuration)*0.8), + int(float64(r.secret.LeaseDuration)*0.95))) * time.Second + } + + glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime) + // step: wait for the duration + <-time.After(r.renewalTime) + // step: send the notification on the renewal channel + ch <- r + }() +}