├── .buildpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.php.core.prefs ├── org.eclipse.wst.common.project.facet.core.xml └── org.eclipse.wst.validation.prefs ├── LICENSE.md ├── README.md ├── composer.json ├── src └── TgVault │ ├── BaseVault.php │ ├── CredentialsProvider.php │ ├── File │ └── FileVault.php │ ├── Hashicorp │ ├── Cache.php │ ├── Config.php │ ├── HashicorpVault.php │ └── Token.php │ ├── Logger.php │ ├── Memory │ └── MemoryVault.php │ ├── Secret.php │ ├── SecretProvider.php │ ├── Vault.php │ ├── VaultException.php │ ├── VaultFactory.php │ └── commons.php └── tests └── TgVault ├── CredentialsProviderTest.php ├── File ├── FileVaultTest.php └── test-config.json ├── Hashicorp └── CacheTest.php ├── Memory └── MemoryVaultTest.php ├── SecretProviderTest.php ├── SecretTest.php └── VaultFactoryTest.php /.buildpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | php-vault 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.php.composer.core.builder.buildPathManagementBuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.wst.common.project.facet.core.builder 15 | 16 | 17 | 18 | 19 | org.eclipse.wst.validation.validationbuilder 20 | 21 | 22 | 23 | 24 | org.eclipse.dltk.core.scriptbuilder 25 | 26 | 27 | 28 | 29 | 30 | org.eclipse.php.core.PHPNature 31 | org.eclipse.wst.common.project.facet.core.nature 32 | 33 | 34 | -------------------------------------------------------------------------------- /.settings/org.eclipse.php.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | include_path=0;/php-vault/src 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.common.project.facet.core.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.validation.prefs: -------------------------------------------------------------------------------- 1 | disabled=06vendor 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # technicalguru/php-vault 2 | A flexible PHP-based vault to provide secrets dynamically 3 | 4 | # License 5 | This project is licensed under [GNU LGPL 3.0](LICENSE.md). 6 | 7 | # Installation 8 | 9 | ## By Composer 10 | 11 | ```sh 12 | composer install technicalguru/vault 13 | ``` 14 | 15 | ## By Package Download 16 | You can download the source code packages from [GitHub Release Page](https://github.com/technicalguru/php-vault/releases) 17 | 18 | # Hashicorp Setup 19 | The procedure is best described at [Hashicorp Blog](https://www.hashicorp.com/blog/authenticating-applications-with-vault-approle). It describes 20 | how to create an `approle`. Here is the essence of it: 21 | 22 | ```sh 23 | # Enable the auth method for approle 24 | vault auth enable approle 25 | 26 | # Create a renewal policy 27 | echo 'path "auth/token/*" { capabilities = [ "create", "read", "update", "delete", "list", "sudo" ] }' >renewal-policy.hcl 28 | vault policy write renewal-policy renewal-policy.hcl 29 | 30 | # Create a file with your policy on the respective secret path: 31 | cat 'path "secret/my-secret" { capabilities = ["read", "list"] }' >app-policy.hcl 32 | 33 | # Create the policy 34 | vault policy write my-app-policy app-policy.hcl 35 | 36 | # Create the approle with renewal-policy and your application policy 37 | vault write auth/approle/role/my-approle token_policies=renewal-policy,my-app-policy token_period=30m token_ttl=30m token_max_ttl=1h token_explicit_max_ttl=2h 38 | 39 | # Get the role ID printed 40 | vault read auth/approle/role/my-approle/role-id 41 | 42 | # Create the secret ID and print it 43 | vault write -f auth/approle/role/my-approle/secret-id 44 | ``` 45 | 46 | Please notice that you need to recreate the secret ID whenever you change the application role or a policy. 47 | 48 | # Examples 49 | ## Create a HashicorpVault 50 | Please note that this vault is actually a client to an existing Hashicorp Vault. 51 | 52 | ```php 53 | // Create configuration 54 | $config = array( 55 | 'type' => 'hashicorp', 56 | 'config' => array( 57 | 'uri' => 'https://127.0.0.1:8200/v1', 58 | 'roleId' => '123456-12345-12345-123456', 59 | 'secretId' => 'abcdef-abcde-abcde-abcdef' 60 | ) 61 | ); 62 | 63 | // Create the vault instance 64 | try { 65 | $vault = \TgVault\VaultFactory::create($config); 66 | } catch (\TgVault\VaultException $e) { 67 | // Vault could not be created 68 | } 69 | 70 | ``` 71 | 72 | ## Create a MemoryVault 73 | 74 | ```php 75 | // Create configuration 76 | $config = array( 77 | 'type' => 'memory', 78 | 'config' => array( 79 | 'secrets' => array( 80 | 'my/secret/number/1' => array( 81 | 'username' => 'my-username1', 82 | 'password' => 'my-password1', 83 | ), 84 | 'my/secret/number/2' => array( 85 | 'username' => 'my-username2', 86 | 'password' => 'my-password2', 87 | ), 88 | ) 89 | ) 90 | ); 91 | 92 | // Create the vault instance 93 | try { 94 | $vault = \TgVault\VaultFactory::create($config); 95 | } catch (\TgVault\VaultException $e) { 96 | // Vault could not be created 97 | } 98 | ``` 99 | 100 | ## Create a FileVault 101 | 102 | ```php 103 | // Create configuration 104 | $config = array( 105 | 'type' => 'file', 106 | 'config' => array( 107 | 'filename' => 'path-to-json-secret-file' 108 | ) 109 | ); 110 | 111 | // Create the vault instance 112 | try { 113 | $vault = \TgVault\VaultFactory::create($config); 114 | } catch (\TgVault\VaultException $e) { 115 | // Vault could not be created 116 | } 117 | ``` 118 | 119 | The secrets file (JSON) shall look like this: 120 | 121 | ```json 122 | { 123 | "secrets": { 124 | "my/secret/number/1" : { 125 | "username" : "my-username1", 126 | "password" : "my-password1" 127 | }, 128 | "my/secret/number/2" : { 129 | "username" : "my-username2", 130 | "password" : "my-password2" 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | ## Retrieving a secret 137 | 138 | ```php 139 | try { 140 | $mySecret1 = $vault->getSecret('my/secret/number/1'); 141 | $mySecret2 = $vault->getSecret('my/secret/number/2'); 142 | } catch (\TgVault\VaultException $e) { 143 | // secret was not found 144 | } 145 | 146 | $username1 = $mySecret1->get('username'); 147 | $password1 = $mySecret1->get('password'); 148 | $username2 = $mySecret2->get('username'); 149 | $password2 = $mySecret2->get('password'); 150 | ``` 151 | 152 | A value in a secret is `NULL` when the key does not exists whereas an exception will be thrown when the secret itself cannot be found 153 | or an error occurred while retrieval. 154 | 155 | ## Using lazy callback credentials 156 | You can use the `SecretProvider` or `CredentialsProvider` helper classes to pass them credentials without knowing where they come from 157 | or how to use a vault. 158 | 159 | ```php 160 | $callback1 = new \TgVault\SecretProvider($vault, 'my/secret/number/1'); 161 | $callback2 = new \TgVault\CredentialsProvider($vault, 'my/secret/number/2'); 162 | 163 | try { 164 | $username1 = $callback1->get('username'); 165 | $password1 = $callback1->get('password'); 166 | 167 | $username2 = $callback2->getUsername(); 168 | $password2 = $callback2->getPassword(); 169 | } catch (\TgVault\VaultException $e) { 170 | // Secret cannot be retrieved or does not exist 171 | } 172 | ``` 173 | 174 | The `CredentialsProvider` takes additional constructor arguments that define, which keys in the secret provide username and password. The 175 | defaults are as given above for the `SecretProvider`. 176 | 177 | 178 | # Contribution 179 | Report a bug, request an enhancement or pull request at the [GitHub Issue Tracker](https://github.com/technicalguru/php-vault/issues). 180 | 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "technicalguru/vault", 3 | "description": "A flexible, lightweight PHP-based vault to provide secrets dynamically", 4 | "type": "library", 5 | "keywords": [ 6 | "framework", 7 | "vault", 8 | "vault-client", 9 | "hashicorp", 10 | "hashicorp-vault" 11 | ], 12 | "license": "LGPL-3.0-or-later", 13 | "authors": [ 14 | { 15 | "name": "TechnicalGuru", 16 | "homepage": "https://github.com/technicalguru/php-vault" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.0.0", 21 | "technicalguru/utils": "^1" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "TgVault\\": "src/TgVault/" 26 | } 27 | }, 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "1.0-dev" 31 | } 32 | }, 33 | "require-dev": { 34 | "phpunit/phpunit": "^9" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/TgVault/BaseVault.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 22 | } 23 | 24 | /** 25 | * Returns the secret at the given path. 26 | * Must be overridden by subclasses. 27 | * @param string $path - an arbitrary path that uniquely identifies a secret in the vault. 28 | * @return Secret 29 | * @throws VaultException when the secret cannot be found or retrieved. 30 | */ 31 | public function getSecret($path) { 32 | throw new VaultException(get_class().'::getSecret() must be implemented.', VAULT_ERR_INTERNAL); 33 | } 34 | 35 | /** 36 | * Set the logger and log all information via this object. 37 | * This can be a Psr\Log\LoggerInterface or a \TgVault\Logger. 38 | * @param Logger - the logging object. 39 | */ 40 | public function setLogger($logger) { 41 | $this->logger = $logger; 42 | } 43 | 44 | /** 45 | * Log in debug level. 46 | * @see Logger interface 47 | * @param $s - the string to be logged 48 | * @param $object - the object to be logged 49 | */ 50 | protected function debug($s, $object = NULL) { 51 | if ($this->logger != NULL) { 52 | $object = self::cleanObject($object); 53 | $psrInterface = '\\Psr\\Log\\LoggerInterface'; 54 | if ($this->logger instanceof $psrInterface) { 55 | } else if ($this->logger instanceof Logger) { 56 | $this->logger->debug($this->getLoggerPrefix().$s, $object); 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Log in warn level. 63 | * @see Logger interface 64 | * @param $s - the string to be logged 65 | * @param $object - the object to be logged 66 | */ 67 | protected function warn($s, $object = NULL) { 68 | if ($this->logger != NULL) { 69 | $object = self::cleanObject($object); 70 | $psrInterface = '\\Psr\\Log\\LoggerInterface'; 71 | if ($this->logger instanceof $psrInterface) { 72 | } else if ($this->logger instanceof Logger) { 73 | $this->logger->warn($this->getLoggerPrefix().$s, $object); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Log in info level. 80 | * @see Logger interface 81 | * @param $s - the string to be logged 82 | * @param $object - the object to be logged 83 | */ 84 | protected function info($s, $object = NULL) { 85 | if ($this->logger != NULL) { 86 | $object = self::cleanObject($object); 87 | $psrInterface = '\\Psr\\Log\\LoggerInterface'; 88 | if ($this->logger instanceof $psrInterface) { 89 | } else if ($this->logger instanceof Logger) { 90 | $this->logger->info($this->getLoggerPrefix().$s, $object); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Log in error level. 97 | * @see Logger interface 98 | * @param $s - the string to be logged 99 | * @param $object - the object to be logged 100 | */ 101 | protected function error($s, $object = NULL) { 102 | if ($this->logger != NULL) { 103 | $object = self::cleanObject($object); 104 | $psrInterface = '\\Psr\\Log\\LoggerInterface'; 105 | if ($this->logger instanceof $psrInterface) { 106 | } else if ($this->logger instanceof Logger) { 107 | $this->logger->error($this->getLoggerPrefix().$s, $object); 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Returns a prefix for the logging string. 114 | * Default implementation uses the short class name. 115 | * @return string for prefixing logging strings. 116 | */ 117 | protected function getLoggerPrefix() { 118 | if ($this->prefix == NULL) { 119 | $helper = new \ReflectionClass($this); 120 | $this->prefix = '['.$helper->getShortName().'] '; 121 | } 122 | return $this->prefix; 123 | } 124 | 125 | /** 126 | * Copies the given object and redacts any sensitive strings, such as 127 | * passwords, usernames and tokens. The decision is based on object 128 | * attribute names. Arrays are not redacted (!). 129 | * @param object $o - the object to clean 130 | * @return \stdClass a cleaned object 131 | */ 132 | public static function cleanObject($o) { 133 | if ($o == NULL) return NULL; 134 | if (!is_object($o)) return $o; 135 | $copy = new \stdClass; 136 | foreach (get_object_vars($o) AS $key => $value) { 137 | if (is_object($value)) { 138 | $copy->$key = self::cleanObject($value); 139 | } else if (is_string($key)) { 140 | switch ($key) { 141 | case 'username': 142 | case 'password': 143 | case 'passwd': 144 | case 'client_token': 145 | case 'accessor': 146 | $copy->$key = '***REDACTED***'; 147 | break; 148 | default: 149 | if (strpos($key, 'pass') !== FALSE) { 150 | $copy->$key = '***REDACTED***'; 151 | } else { 152 | $copy->$key = $value; 153 | } 154 | } 155 | } else { 156 | $copy->$key = $value; 157 | } 158 | } 159 | return $copy; 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /src/TgVault/CredentialsProvider.php: -------------------------------------------------------------------------------- 1 | usernameKey = $usernameKey; 32 | $this->passwordKey = $passwordKey; 33 | } 34 | 35 | /** 36 | * Returns the username in the secret. 37 | * @return string the username as given in the secret 38 | * @throws VaultException when the secret cannot be retrieved or does not exist 39 | */ 40 | public function getUsername() { 41 | return $this->get($this->usernameKey); 42 | } 43 | 44 | /** 45 | * Returns the password in the secret. 46 | * @return string the password as given in the secret 47 | * @throws VaultException when the secret cannot be retrieved or does not exist 48 | */ 49 | public function getPassword() { 50 | return $this->get($this->passwordKey); 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/TgVault/File/FileVault.php: -------------------------------------------------------------------------------- 1 | secrets = NULL; 34 | if (is_object($config)) $this->filename = $config->filename; 35 | else if (is_array($config)) $this->filename = $config['filename']; 36 | else throw new VaultException('Configuration must contain filename', VAULT_ERR_CONFIG); 37 | } 38 | 39 | /** 40 | * Loads the secrets file from disk if required. 41 | */ 42 | protected function load() { 43 | if ($this->secrets == NULL) { 44 | $this->secrets = array(); 45 | $json = file_get_contents($this->filename); 46 | if ($json === FALSE) { 47 | throw new VaultException('Cannot find secrets file.', VAULT_ERR_FILE_NOT_FOUND); 48 | } else { 49 | $config = json_decode($json); 50 | $secrets = array(); 51 | if (is_object($config) && isset($config->secrets)) $secrets = $config->secrets; 52 | foreach ($secrets AS $path => $data) { 53 | $this->secrets[$path] = new Secret(array('data' => $data)); 54 | } 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Returns the secret at the given path. 61 | * @param string $path - an arbitrary path that uniquely identifies a secret in the vault. 62 | * @return Secret 63 | * @throws VaultException when the secret cannot be found or retrieved. 64 | */ 65 | public function getSecret($path) { 66 | $this->load(); 67 | if (!isset($this->secrets[$path])) { 68 | throw new VaultException('Secret not available', VAULT_ERR_NOT_FOUND); 69 | } 70 | return $this->secrets[$path]; 71 | } 72 | 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/TgVault/Hashicorp/Cache.php: -------------------------------------------------------------------------------- 1 | cacheFile = $cacheFile; 26 | $this->logger = $logger; 27 | } 28 | 29 | /** 30 | * Loads the cache if required. 31 | */ 32 | protected function load() { 33 | if ($this->data == null) { 34 | if (($this->cacheFile) && file_exists($this->cacheFile)) { 35 | $contents = file_get_contents($this->cacheFile); 36 | if ($contents === FALSE) { 37 | if ($this->logger != null) $this->logger->error('[Cache] Cannot read cache'); 38 | } else if (!empty($contents)) $this->data = json_decode($contents); 39 | else $this->data = new \stdClass; 40 | } else { 41 | $this->data = new \stdClass; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Saves the cache. 48 | * @return TRUE when cache was written successfully, FALSE otherwise. 49 | */ 50 | protected function save() { 51 | if (($this->data != null) && $this->cacheFile) { 52 | $rc = file_put_contents($this->cacheFile, json_encode($this->data), LOCK_EX); 53 | if ($rc === FALSE) { 54 | if ($this->logger != NULL) $this->logger->error('[Cache] Cannot write cache'); 55 | return false; 56 | } 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | /** 63 | * Return data from cache. 64 | * @param string $key - the key in the cache. 65 | * @return mixed - the data from the cache or NULL if not available. 66 | */ 67 | public function get($key) { 68 | $this->load(); 69 | if (isset($this->data->$key)) { 70 | return $this->data->$key; 71 | } 72 | return null; 73 | } 74 | 75 | /** 76 | * Sets a value in the cache. 77 | * @param string $key - the key in the cache. 78 | * @param mixed $value - the value to be stored. 79 | */ 80 | public function set($key, $value) { 81 | $this->load(); 82 | $this->data->$key = $value; 83 | $this->save(); 84 | } 85 | 86 | /** 87 | * Deletes a value in the cache. 88 | * @param string $key - the key in the cache. 89 | */ 90 | public function delete($key) { 91 | $this->load(); 92 | if (isset($this->data->$key)) { 93 | unset($this->data->$key); 94 | $this->save(); 95 | } 96 | } 97 | 98 | /** 99 | * Set the logger and log all information via this object. 100 | * @param object $logger - a logger, either TgVault\Logger or Psr\Log\LoggerInterface 101 | */ 102 | public function setLogger($logger) { 103 | $this->logger = $logger; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/TgVault/Hashicorp/Config.php: -------------------------------------------------------------------------------- 1 | timeout = 5; 43 | $this->verifyCertificate = false; 44 | $this->debug = false; 45 | $this->cacheFile = '/tmp/vault_client.cache'; 46 | $this->renewTokens = true; 47 | $this->renewalPeriod = 300; 48 | $this->maxTtl = 0; 49 | 50 | if (is_object($data)) $data = get_object_vars($data); 51 | if (is_array($data)) { 52 | foreach ($data AS $key => $value) { 53 | $this->$key = $value; 54 | } 55 | } 56 | 57 | // Check for any errors 58 | $this->check('uri', 'Vault URI not set'); 59 | $this->check('roleId', 'Vault AppRole ID not set'); 60 | $this->check('secretId', 'Vault Secret ID not set'); 61 | } 62 | 63 | /** 64 | * check that the given value exists and is not empty. 65 | * @param string $valueKey - the config key to be verified 66 | * @param string $errorMessage - exception message to be thrown in case or error. 67 | * @throws VaultException when the given key was not set. 68 | */ 69 | private function check($valueKey, $errorMessage) { 70 | if (!isset($this->$valueKey) || ($this->$valueKey == NULL) || 71 | !is_string($this->$valueKey) || (trim($this->$valueKey) == '')) { 72 | throw new VaultException($errorMessage. ' ('.$valueKey.')', VAULT_ERR_CONFIG); 73 | } 74 | } 75 | 76 | /** 77 | * Set the vault login credentials. 78 | * @param string $roleId - the role ID in vault 79 | * @param string $secretId - the secret ID of the client 80 | */ 81 | public function setVaultCredentials($roleId, $secretId) { 82 | $this->roleId = $roleId; 83 | $this->secretId = $secretId; 84 | $this->check('roleId', 'Vault AppRole ID not set'); 85 | $this->check('secretId', 'Vault Secret ID not set'); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/TgVault/Hashicorp/HashicorpVault.php: -------------------------------------------------------------------------------- 1 | config = new Config($config); 38 | $this->isTls = substr($this->config->uri, 0, 5) == 'https'; 39 | $this->cache = new Cache($this->config->cacheFile, $logger); 40 | $this->loggedToken = FALSE; 41 | } 42 | 43 | /** 44 | * Get rid of the current token. This can be necessary when the policies changed. 45 | */ 46 | public function removeToken() { 47 | $this->cache->delete('token'); 48 | $this->token = NULL; 49 | } 50 | 51 | /** 52 | * Returns the secret at the given path. 53 | * @param string $path - an arbitrary path that uniquely identifies a secret in the vault. 54 | * @return Secret 55 | * @throws VaultException when the secret cannot be found or retrieved. 56 | */ 57 | public function getSecret($path) { 58 | if (!isset($this->secrets[$path])) { 59 | $this->getToken(); 60 | $rc = $this->GET($path); 61 | if (($rc->error == 0) && ($rc->http_code == 200) && is_object($rc->data->data)) { 62 | // It's unclear why some vaults do answer with one level less (without metadata) 63 | if (isset($rc->data->data->data)) { 64 | $this->secrets[$path] = new Secret($rc->data->data); 65 | } else { 66 | $this->secrets[$path] = new Secret($rc->data); 67 | } 68 | } else { 69 | $this->secrets[$path] = $rc; 70 | } 71 | } 72 | 73 | if (get_class($this->secrets[$path]) != 'TgVault\\Secret') { 74 | $ex = new VaultException('Secret not available', VAULT_ERR_SECRET); 75 | $ex->setDetails($this->secrets[$path]); 76 | throw $ex; 77 | } 78 | return $this->secrets[$path]; 79 | } 80 | 81 | /** 82 | * Set the logger and log all information via this object. 83 | * @param object $logger - the logging object, either TgVault\Logger or Psr\Log\LoggerInterface 84 | */ 85 | public function setLogger($logger) { 86 | parent::setLogger($logger); 87 | $this->cache->setLogger($logger); 88 | } 89 | 90 | public function getTokenStatus($asText = true) { 91 | if ($this->token == NULL) { 92 | return $asText ? 'no token available' : VAULT_ERR_NO_TOKEN; 93 | } 94 | if ($this->token->isExpired()) { 95 | return $asText ? 'token expired at '.$this->token->expiryString.' (current time is '.date(DATE_ATOM).')' : VAULT_ERR_TOKEN_EXPIRED; 96 | } 97 | if ($this->shallRenewToken()) { 98 | return $asText ? 'token valid but needs renewal (expires at '.$this->token->expiryString.', current time is '.date(DATE_ATOM).')' : VAULT_ERR_RENEWAL; 99 | } 100 | return $asText ? 'token valid (expires at '.$this->token->expiryString.', current time is '.date(DATE_ATOM).')' : VAULT_OK; 101 | } 102 | 103 | public function getSecretError($path, $asText = true) { 104 | $response = $this->secrets[$path]; 105 | if ($response instanceof Secret) { 106 | return $asText ? 'Secret successfully retrieved' : VAULT_SECRET_OK; 107 | } 108 | if ($response->error > 0) { 109 | return $asText ? $response->errorMessage : VAULT_ERR_CURL_BASE+$response->error; 110 | } 111 | if (is_object($response->data) && is_array($response->data->errors)) { 112 | return $asText ? implode(' / ', $response->data->errors) : VAULT_ERR_HTTP_BASE+$response->http_code; 113 | } 114 | return $asText ? 'Unknown internal error' : VAULT_ERR_SECRET_INTERNAL; 115 | } 116 | 117 | public function dieOn($path) { 118 | // Error log message 119 | $status1 = $this->getTokenStatus(); 120 | $this->error('Vault Token Status: '.$status1); 121 | $status2 = $this->getSecretError($path); 122 | $this->error('Database Secret Status: '.$status2); 123 | 124 | // User message 125 | $status1 = $this->getTokenStatus(false); 126 | $status2 = $this->getSecretError($path, false); 127 | $renewal = false; 128 | switch ($status1) { 129 | case VAULT_OK: 130 | // It's a secret problem 131 | break; 132 | case VAULT_ERR_RENEWAL: 133 | // Token actually ok but renewal required / So it might be a secret issue 134 | $renewal = true; 135 | break; 136 | case VAULT_ERR_TOKEN_EXPIRED: 137 | exit('No valid access token'); 138 | break; 139 | case VAULT_ERR_NO_TOKEN: 140 | exit('No access token'); 141 | break; 142 | } 143 | 144 | switch ($status2) { 145 | case VAULT_SECRET_OK: 146 | exit($renewal ? 'Token renewal warning' : 'OK'); 147 | break; 148 | case VAULT_ERR_SECRET_INTERNAL: 149 | exit($renewal ? 'Internal error on renewable token' : 'Internal error on secret'); 150 | break; 151 | default: 152 | if ($status2 >= VAULT_ERR_CURL_BASE) { 153 | exit($renewal ? 'Cannot reach vault with renewable token: '.$status2 : 'Cannot reach vault: '.$status2); 154 | } 155 | exit($renewal ? 'Vault declines renewable token: '.$status2 : 'Vault declines: '.$status2); 156 | } 157 | exit($renewal ? 'Token renewal warning' : 'OK'); 158 | } 159 | 160 | /** 161 | * Returns the last result from a vault call. 162 | */ 163 | public function getLastResult() { 164 | return $this->lastResult; 165 | } 166 | 167 | /** 168 | * Returns a token either from object cache, file cache or 169 | * triggers a token request. 170 | */ 171 | protected function getToken() { 172 | if ($this->token == NULL) { 173 | $this->setTokenFromCache(); 174 | } 175 | if ($this->token != NULL) { 176 | if ($this->isTokenExpired()) { 177 | $this->removeToken(); 178 | } else if ($this->shallRenewToken()) { 179 | $this->renewToken(); 180 | } 181 | } 182 | 183 | if ($this->token == NULL) { 184 | $this->requestNewToken(); 185 | } 186 | 187 | if (($this->token != NULL) && !$this->loggedToken) { 188 | $this->info('Using token: '.$this->token->getInfo()); 189 | $this->loggedToken = TRUE; 190 | } 191 | 192 | return $this->token; 193 | } 194 | 195 | protected function isTokenExpired() { 196 | // Token itself must exist and not be expired 197 | if (($this->token != NULL) && !$this->token->isExpired()) { 198 | // Get expiry time 199 | $expiry = $this->getTokenExpiryTime(); 200 | if (time() > $expiry) return true; 201 | return false; 202 | } 203 | return true; 204 | } 205 | 206 | protected function shallRenewToken() { 207 | // Token must exist and not be expired 208 | if (($this->token != NULL) && !$this->token->isExpired()) { 209 | // Token renewable and renewal permitted 210 | if ($this->token->renewable && $this->config->renewTokens) { 211 | // Get expiry time 212 | $expiry = $this->getTokenExpiryTime(); 213 | if ($expiry > 0) { 214 | $renewalTime = $expiry - 300; // Default renewal 215 | $renewalPeriod = $this->config->renewalPeriod; 216 | if ($renewalPeriod > 10) $renewalTime = $expiry - $renewalPeriod; 217 | // So, is it time? 218 | if (time() > $renewalTime) return true; 219 | } 220 | 221 | } 222 | } 223 | return false; 224 | } 225 | 226 | /** 227 | * Compute the current token's expiry time using the token and our config. 228 | * @return int - the Unix epoch expiry time, -1 if token is NULL 229 | */ 230 | protected function getTokenExpiryTime() { 231 | $rc = -1; 232 | if ($this->token != NULL) { 233 | $rc = $this->token->expiryTime; 234 | if ($this->config->maxTtl > 0) { 235 | $expiry = $this->token->creationTime + $this->config->maxTtl; 236 | if ($expiry < $rc) $rc = $expiry; 237 | } 238 | } 239 | return $rc; 240 | } 241 | 242 | /** 243 | * Load the token from the file cache. 244 | */ 245 | protected function setTokenFromCache() { 246 | $token = $this->cache->get('token'); 247 | if ($token != NULL) { 248 | $this->token = new Token($token); 249 | } 250 | } 251 | 252 | /** 253 | * Request a token from Vault 254 | */ 255 | protected function requestNewToken() { 256 | $data = new \stdClass; 257 | $data->role_id = $this->config->roleId; 258 | $data->secret_id = $this->config->secretId; 259 | $data->renewable = true; 260 | $rc = $this->POST('/auth/approle/login', $data); 261 | if (($rc->error == 0) && is_object($rc->data)) { 262 | if (isset($rc->data->auth)) { 263 | $this->token = new Token($rc->data->auth); 264 | $this->cache->set('token', $this->token); 265 | $this->info('Token replaced'); 266 | } 267 | } 268 | } 269 | 270 | /** 271 | * Renew the token 272 | */ 273 | protected function renewToken() { 274 | if ($this->token != NULL) { 275 | if (!$this->token->isExpired()) { 276 | $data = new \stdClass; 277 | $data->token = $this->token->client_token; 278 | $rc = $this->POST('auth/token/renew', $data); 279 | if (($rc->error == 0) && is_object($rc->data)) { 280 | if (isset($rc->data->auth)) { 281 | $this->token = new Token($rc->data->auth); 282 | $this->cache->set('token', $this->token); 283 | $this->info('Token renewed'); 284 | } 285 | } 286 | } else { 287 | $this->removeToken(); 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * Performs a GET request on the given path 294 | * @param string $path - the relative path at Vault URI to request 295 | * @return \stdClass see #request($curl, $path) method. 296 | */ 297 | protected function GET($path) { 298 | $curl = curl_init(); 299 | return $this->request($curl, $path); 300 | } 301 | 302 | /** 303 | * Performs a POST request on the given path 304 | * @param string $path - the relative path at Vault URI to request 305 | * @param object $data - the data to be posted (will be json-encoded) 306 | * @return mixed see #request($curl, $path) method. 307 | */ 308 | protected function POST($path, $data) { 309 | $body = json_encode($data); 310 | $headers = array( 311 | 'Content-Length: '.strlen($body), 312 | 'Content-Type: application/json', 313 | ); 314 | 315 | $curl = curl_init(); 316 | curl_setopt($curl, CURLOPT_POST, true); 317 | curl_setopt($curl, CURLOPT_POSTFIELDS, $body); 318 | return $this->request($curl, $path, $headers); 319 | } 320 | 321 | /** 322 | * Performs the actual curl request. 323 | * @param resource $curl - cURL handle as returned from curl_init() 324 | * @param string $path - the relative path at Vault URI to request 325 | * @return \stdClass see documentation below 326 | */ 327 | protected function request($curl, $path, $additionalHeaders = array()) { 328 | /********************************** 329 | Return is an object of: 330 | { 331 | "request_id": string, // request ID given / not needed 332 | "lease_id": string, // lease created for this request / not needed 333 | "renewable": bool, // lease for current request is renewable / not needed 334 | "lease_duration": int, // duration of least for current request / not needed 335 | "data": object, // specific to request 336 | "wrap_info": object, // unknown / not needed 337 | "warnings": array, // unknown / string messages 338 | "errors": array, // unknown / string messages 339 | "auth": { object // authentication information 340 | "client_token": string, // token that was assigned 341 | "accessor": string, // accessor token / not needed 342 | "policies": array, // policies / info only 343 | "token_policies": array, // token policies / info only 344 | "metadata": object, // authorization meta data, such as "role_name" 345 | "lease_duration": int, // lease duration in seconds (remaining) 346 | "renewable": bool, // is token renewable 347 | "entity_id": string, // entity ID / not needed 348 | "token_type": string, // type of token / not needed 349 | "orphan": bool, // is orphaned token / not needed 350 | } 351 | } 352 | ***********************************/ 353 | $additionalHeaders[] = 'X-Vault-Request: true'; 354 | if (($this->token != NULL) && isset($this->token->client_token)) { 355 | $additionalHeaders[] = 'X-Vault-Token: '.$this->token->client_token; 356 | } 357 | 358 | // Fix path issues here 359 | if ((substr($this->config->uri, -1) != '/') && (substr($path, 0, 1) != '/')) { 360 | $path = '/'.$path; 361 | } else if ((substr($this->config->uri, -1) == '/') && (substr($path, 0, 1) == '/')) { 362 | $path = substr($path, 1); 363 | } 364 | curl_setopt($curl, CURLOPT_URL, $this->config->uri.$path); 365 | curl_setopt($curl, CURLOPT_TIMEOUT, $this->config->timeout); 366 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 367 | curl_setopt($curl, CURLOPT_USERAGENT, 'HashicorpVaultClient/1.0'); 368 | if (count($additionalHeaders) > 0) { 369 | curl_setopt($curl, CURLOPT_HTTPHEADER, $additionalHeaders); 370 | } 371 | 372 | // We might need this: CURLOPT_PORT 373 | if ($this->isTls) { 374 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->config->verifyCertificate); 375 | curl_setopt($curl, CURLOPT_SSL_VERIFYSTATUS, $this->config->verifyCertificate); 376 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->config->verifyCertificate); 377 | } 378 | if ($this->config->debug) { 379 | curl_setopt($curl, CURLINFO_HEADER_OUT, true); 380 | } 381 | 382 | $rc = $this->createCurlResult($curl); 383 | curl_close($curl); 384 | return $rc; 385 | } 386 | 387 | protected function createCurlResult($curl) { 388 | $data = curl_exec($curl); 389 | 390 | $rc = new \stdClass; 391 | $rc->url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); 392 | $rc->error = curl_errno($curl); 393 | $rc->errorMessage = curl_error($curl); 394 | if (!$rc->error) { 395 | $rc->http_code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); 396 | $rc->contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE); 397 | if ($rc->contentType == 'application/json') { 398 | $rc->data = json_decode($data); 399 | if (isset($rc->data->errors)) { 400 | $this->error('Error while calling '.$rc->url.': ', $rc->data->errors); 401 | } 402 | } else { 403 | $rc->data = $data; 404 | } 405 | } else { 406 | $this->error('Error while calling '.$rc->url.': ', $rc->errorMessage); 407 | } 408 | if ($this->config->debug) { 409 | $rc->debug = new \stdClass; 410 | $rc->debug->headers = curl_getinfo($curl, CURLINFO_HEADER_OUT); 411 | } 412 | $this->lastResult = $rc; 413 | return $rc; 414 | } 415 | 416 | } 417 | 418 | -------------------------------------------------------------------------------- /src/TgVault/Hashicorp/Token.php: -------------------------------------------------------------------------------- 1 | $value) { 33 | $this->$key = $value; 34 | } 35 | if (!isset($this->creationTime)) { 36 | $this->creationTime = time(); 37 | } 38 | if (!isset($this->expiryTime) && isset($this->lease_duration)) { 39 | $this->expiryTime = time() + $this->lease_duration; 40 | } 41 | if (!isset($this->expiryString) && isset($this->expiryTime)) { 42 | $this->expiryString = date(DATE_ATOM, $this->expiryTime); 43 | } 44 | $this->now = time(); 45 | } 46 | 47 | /** 48 | * Is the token expired? 49 | * @return TRUE when the token has expired, FALSE otherwise. 50 | */ 51 | public function isExpired() { 52 | return time() >= $this->expiryTime-5; 53 | } 54 | 55 | /** 56 | * Returns a basic info string for logging purposes. 57 | * @return string info string about the token. 58 | */ 59 | public function getInfo() { 60 | return substr($this->client_token, 0, 3).'**********'.substr($this->client_token, -3).' ['. 61 | 'created='.date(DATE_ATOM, $this->creationTime). 62 | ',expires='.$this->expiryString. 63 | ',renewable='.($this->renewable ? 'true' : 'false'). 64 | ',policies='.json_encode($this->policies). 65 | ']'; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/TgVault/Logger.php: -------------------------------------------------------------------------------- 1 | data). 25 | * @param object $logger - the logger to be used (not used in this implementation) 26 | */ 27 | public function __construct($config, $logger = NULL) { 28 | parent::__construct($logger); 29 | if ($config == NULL) throw new VaultException('Configuration must be set', VAULT_ERR_CONFIG_EMPTY); 30 | $this->secrets = array(); 31 | $secrets = array(); 32 | if (is_object($config) && isset($config->secrets)) $secrets = $config->secrets; 33 | else if (is_array($config) && isset($config['secrets'])) $secrets = $config['secrets']; 34 | foreach ($secrets AS $path => $data) { 35 | $this->secrets[$path] = new Secret(array('data' => $data)); 36 | } 37 | } 38 | 39 | /** 40 | * Returns the secret at the given path. 41 | * @param string $path - an arbitrary path that uniquely identifies a secret in the vault. 42 | * @return Secret 43 | * @throws VaultException when the secret cannot be found or retrieved. 44 | */ 45 | public function getSecret($path) { 46 | if (!isset($this->secrets[$path])) { 47 | throw new VaultException('Secret not available', VAULT_ERR_NOT_FOUND); 48 | } 49 | return $this->secrets[$path]; 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/TgVault/Secret.php: -------------------------------------------------------------------------------- 1 | $value) { 24 | if (is_array($value)) { 25 | $this->$key = json_decode(json_encode($value)); 26 | } else { 27 | $this->$key = $value; 28 | } 29 | } 30 | } else { 31 | $this->data = $data; 32 | } 33 | } 34 | 35 | /** 36 | * Returns a value from the secret. 37 | * @param string $key - the key of the value to be retrieved. 38 | * @return string the value or NULL if not set. 39 | */ 40 | public function get($key) { 41 | if (isset($this->data->$key)) return $this->data->$key; 42 | return NULL; 43 | } 44 | 45 | /** 46 | * Returns the keys that are available in this secret. 47 | * @return list of keys. 48 | */ 49 | public function keys() { 50 | return array_keys(get_object_vars($this->data)); 51 | } 52 | 53 | /** 54 | * Returns any metadata - if set - from the vault for this secret 55 | * @return mixed the metadata or NULL if not set 56 | */ 57 | public function getMeta() { 58 | return $this->metadata; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/TgVault/SecretProvider.php: -------------------------------------------------------------------------------- 1 | vault = $vault; 33 | $this->path = $path; 34 | $this->secret = NULL; 35 | } 36 | 37 | /** 38 | * Returns a value from the secret. 39 | * The provider will load the secret if not done yet. 40 | * @param string $key - the key of the value to be retrieved. 41 | * @return string the value or NULL if not set. 42 | * @throws VaultException when the secret does not exist. 43 | */ 44 | public function get($key) { 45 | if ($this->secret == NULL) { 46 | $this->loadSecret(); 47 | } 48 | if (($this->secret != NULL) && ($this->secret instanceof Secret)) { 49 | return $this->secret->get($key); 50 | } 51 | throw new VaultException('No such secret: '.$this->path, VAULT_ERR_NOT_FOUND); 52 | } 53 | 54 | /** 55 | * Loads the secret. 56 | * @throws VaultException when loading fails. 57 | */ 58 | protected function loadSecret() { 59 | if ($this->secret == NULL) { 60 | try { 61 | $this->secret = $this->vault->getSecret($this->path); 62 | } catch (VaultException $e) { 63 | $this->secret = $e; 64 | throw $e; 65 | } 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/TgVault/Vault.php: -------------------------------------------------------------------------------- 1 | details = $details; 27 | } 28 | 29 | /** 30 | * Returns some debug information if available. 31 | * @return mixed $details - some debug info 32 | */ 33 | public function getDetails() { 34 | return $this->details; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/TgVault/VaultFactory.php: -------------------------------------------------------------------------------- 1 | type, $config->config, $logger); 26 | if (is_array($config)) return self::createVault($config['type'], $config['config'], $logger); 27 | throw new VaultException('Vault configuration must be object or array', VAULT_ERR_CONFIG_TYPE); 28 | } 29 | 30 | /** 31 | * Creates the vault according to type and passes the config object. 32 | * The vault class must be defined as "TgVault\Type\TypeVault". 33 | * @param string $type - the type of the vault 34 | * @param mixed $config - the configuration to pass on. 35 | * @param Logger $logger - the logger (optional) 36 | * @return Vault created and configured 37 | * @throws VaultException when the vault could not be created. 38 | */ 39 | public static function createVault($type, $config = NULL, $logger = NULL) { 40 | if (($type == NULL) || (trim($type) == '')) throw new VaultException('Vault type cannot be empty', VAULT_ERR_TYPE_EMPTY); 41 | $type = ucfirst(trim($type)); 42 | $className = 'TgVault\\'.$type.'\\'.$type.'Vault'; 43 | if (class_exists($className)) { 44 | return new $className($config, $logger); 45 | } 46 | throw new VaultException('Cannot find Vault instance: '.$className, VAULT_ERR_TYPE_NOT_FOUND); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/TgVault/commons.php: -------------------------------------------------------------------------------- 1 | getTestVault(); 16 | $provider = new CredentialsProvider($vault, 'path/to/secret1'); 17 | $this->assertEquals('username1', $provider->getUsername()); 18 | $this->assertEquals('password1', $provider->getPassword()); 19 | } 20 | 21 | public function testWithCustomKeys(): void { 22 | $vault = $this->getTestVault(); 23 | $provider = new CredentialsProvider($vault, 'path/to/secret2', 'username-2', 'password-2'); 24 | $this->assertEquals('username2', $provider->getUsername()); 25 | $this->assertEquals('password2', $provider->getPassword()); 26 | } 27 | 28 | protected function getTestVault(): Vault { 29 | return new Memory\MemoryVault(array( 30 | 'secrets' => array( 31 | 'path/to/secret1' => array( 32 | 'username' => 'username1', 33 | 'password' => 'password1', 34 | ), 35 | 'path/to/secret2' => array( 36 | 'username-2' => 'username2', 37 | 'password-2' => 'password2', 38 | ), 39 | ), 40 | )); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/TgVault/File/FileVaultTest.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/test-config.json')); 16 | for ($i = 1; $i<3; $i++) { 17 | $this->assertNotNull($vault->getSecret('my/secret/number/'.$i)); 18 | $this->assertEquals('my-username'.$i, $vault->getSecret('my/secret/number/'.$i)->get('username')); 19 | $this->assertEquals('my-password'.$i, $vault->getSecret('my/secret/number/'.$i)->get('password')); 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /tests/TgVault/File/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "secrets": { 3 | "my/secret/number/1" : { 4 | "username" : "my-username1", 5 | "password" : "my-password1" 6 | }, 7 | "my/secret/number/2" : { 8 | "username" : "my-username2", 9 | "password" : "my-password2" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/TgVault/Hashicorp/CacheTest.php: -------------------------------------------------------------------------------- 1 | getConfig()); 16 | for ($i = 1; $i<3; $i++) { 17 | $this->assertNotNull($vault->getSecret('path/to/secret'.$i)); 18 | $this->assertEquals('value-'.$i.'-1', $vault->getSecret('path/to/secret'.$i)->get('key-1')); 19 | $this->assertEquals('value-'.$i.'-2', $vault->getSecret('path/to/secret'.$i)->get('key-2')); 20 | } 21 | } 22 | 23 | protected function getConfig(): array { 24 | return $config = array( 25 | 'secrets' => array( 26 | 'path/to/secret1' => $this->getSecret(1), 27 | 'path/to/secret2' => $this->getSecret(2), 28 | ), 29 | ); 30 | } 31 | 32 | protected function getSecret(int $init): array { 33 | return array( 34 | 'key-1' => 'value-'.$init.'-1', 35 | 'key-2' => 'value-'.$init.'-2', 36 | ); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/TgVault/SecretProviderTest.php: -------------------------------------------------------------------------------- 1 | getTestVault(); 16 | $provider = new SecretProvider($vault, 'my/secret/number/2'); 17 | $this->assertEquals('my-username2', $provider->get('username')); 18 | $this->assertEquals('my-password2', $provider->get('password')); 19 | } 20 | 21 | protected function getTestVault(): Vault { 22 | return new File\FileVault(array('filename' => __DIR__.'/File/test-config.json')); 23 | } 24 | } -------------------------------------------------------------------------------- /tests/TgVault/SecretTest.php: -------------------------------------------------------------------------------- 1 | metadata = new \stdClass; 17 | $data->metadata->time = time(); 18 | $data->data = new \stdClass; 19 | $data->data->key1 = "value1"; 20 | $data->data->key2 = "value2"; 21 | $secret = new Secret($data); 22 | $this->assertInstanceOf(Secret::class, $secret); 23 | $this->assertSame('value1', $secret->get('key1')); 24 | $this->assertSame('value2', $secret->get('key2')); 25 | } 26 | 27 | public function testConstructFromArray(): void { 28 | $data = array( 29 | 'metadata' => array( 30 | 'time' => time(), 31 | ), 32 | 'data' => array( 33 | 'key1' => "value1", 34 | 'key2' => "value2", 35 | ), 36 | ); 37 | $secret = new Secret($data); 38 | $this->assertInstanceOf(Secret::class, $secret); 39 | $this->assertSame('value1', $secret->get('key1')); 40 | $this->assertSame('value2', $secret->get('key2')); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /tests/TgVault/VaultFactoryTest.php: -------------------------------------------------------------------------------- 1 | getConfig(); 16 | $vault = VaultFactory::create($config['file']); 17 | $this->assertInstanceOf(File\FileVault::class, $vault); 18 | for ($i = 1; $i<3; $i++) { 19 | $this->assertNotNull($vault->getSecret('my/secret/number/'.$i)); 20 | $this->assertEquals('my-username'.$i, $vault->getSecret('my/secret/number/'.$i)->get('username')); 21 | $this->assertEquals('my-password'.$i, $vault->getSecret('my/secret/number/'.$i)->get('password')); 22 | } 23 | } 24 | 25 | public function testConstructMemoryVault(): void { 26 | $config = $this->getConfig(); 27 | $vault = VaultFactory::create($config['memory']); 28 | $this->assertInstanceOf(Memory\MemoryVault::class, $vault); 29 | for ($i = 1; $i<3; $i++) { 30 | $this->assertNotNull($vault->getSecret('path/to/secret'.$i)); 31 | $this->assertEquals('value-'.$i.'-1', $vault->getSecret('path/to/secret'.$i)->get('key-1')); 32 | $this->assertEquals('value-'.$i.'-2', $vault->getSecret('path/to/secret'.$i)->get('key-2')); 33 | } 34 | } 35 | 36 | protected function getConfig(): array { 37 | return array( 38 | 'file' => array( 39 | 'type' => 'file', 40 | 'config' => array( 41 | 'filename' => __DIR__.'/File/test-config.json' 42 | ), 43 | ), 44 | 'memory' => array( 45 | 'type' => 'memory', 46 | 'config' => array( 47 | 'secrets' => array( 48 | 'path/to/secret1' => array( 49 | 'key-1' => 'value-1-1', 50 | 'key-2' => 'value-1-2', 51 | ), 52 | 'path/to/secret2' => array( 53 | 'key-1' => 'value-2-1', 54 | 'key-2' => 'value-2-2', 55 | ), 56 | ), 57 | ), 58 | ), 59 | ); 60 | } 61 | } --------------------------------------------------------------------------------