├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── requirements.txt ├── src ├── AuthenticationStrategies │ ├── AbstractAuthenticationStrategy.php │ ├── AbstractPathAuthenticationStrategy.php │ ├── AppRoleAuthenticationStrategy.php │ ├── AuthenticationStrategy.php │ ├── AwsIamAuthenticationStrategy.php │ ├── LdapAuthenticationStrategy.php │ ├── OktaAuthenticationStrategy.php │ ├── RadiusAuthenticationStrategy.php │ ├── TokenAuthenticationStrategy.php │ └── UserPassAuthenticationStrategy.php ├── BaseClient.php ├── BaseObject.php ├── Builders │ └── ResponseBuilder.php ├── CachedClient.php ├── Client.php ├── Exceptions │ ├── AuthenticationException.php │ ├── ClassNotFoundException.php │ ├── DependencyException.php │ ├── RequestException.php │ └── RuntimeException.php ├── Helpers │ ├── ArrayHelper.php │ └── ModelHelper.php ├── Models │ └── Token.php └── ResponseModels │ ├── Auth.php │ ├── Response.php │ └── Traits │ └── LeaseTrait.php └── tests ├── .gitignore ├── _bootstrap.php ├── _data └── vcr │ ├── .gitkeep │ ├── authentication-strategies │ ├── app-role │ ├── aws-iam │ └── token │ └── unit-client ├── _support ├── .gitignore ├── Helper │ └── Unit.php └── UnitTester.php ├── unit.suite.yml └── unit ├── AuthenticationStrategies ├── AppRoleAuthenticationStrategyTest.php ├── AwsIamAuthenticationStrategyTest.php └── TokenAuthenticationStrategyTest.php ├── CachedClientTest.php ├── ClientTest.php └── _bootstrap.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "composer" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | php: 14 | - '7.2' 15 | - '7.3' 16 | - '7.4' 17 | - '8.0' 18 | - '8.1' 19 | - '8.2' 20 | - '8.3' 21 | - '8.4' 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | - name: Setup PHP 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: ${{ matrix.php }} 30 | tools: composer:v2 31 | coverage: none 32 | 33 | - name: Install dependencies 34 | uses: nick-invision/retry@v1 35 | with: 36 | timeout_minutes: 5 37 | max_attempts: 5 38 | command: composer install --prefer-dist --no-interaction --no-progress 39 | 40 | - name: Run tests 41 | run: php vendor/bin/codecept run 42 | 43 | coverage: 44 | runs-on: ubuntu-latest 45 | needs: tests 46 | steps: 47 | - name: Checkout code 48 | uses: actions/checkout@v2 49 | 50 | - name: Setup PHP 51 | uses: shivammathur/setup-php@v2 52 | with: 53 | tools: composer:v2 54 | coverage: xdebug 55 | 56 | - name: Install dependencies 57 | uses: nick-invision/retry@v1 58 | with: 59 | timeout_minutes: 5 60 | max_attempts: 5 61 | command: composer install --prefer-dist --no-interaction --no-progress 62 | 63 | - name: Run tests 64 | run: XDEBUG_MODE=coverage php vendor/bin/codecept run --coverage-xml 65 | 66 | - name: Check coverage 67 | uses: codacy/codacy-coverage-reporter-action@v1 68 | with: 69 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 70 | coverage-reports: tests/_output/coverage.xml 71 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yaroslav Lukyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vault PHP client ![Build Status](https://github.com/CSharpRU/vault-php/actions/workflows/tests.yml/badge.svg?branch=master) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/0bf9f46a659844658d847c1b2ab01e8b)](https://www.codacy.com/app/c_sharp/vault-php?utm_source=github.com&utm_medium=referral&utm_content=CSharpRU/vault-php&utm_campaign=Badge_Coverage) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/0bf9f46a659844658d847c1b2ab01e8b)](https://www.codacy.com/app/c_sharp/vault-php?utm_source=github.com&utm_medium=referral&utm_content=CSharpRU/vault-php&utm_campaign=Badge_Grade) [![Read the Docs Badge](https://readthedocs.org/projects/vault-php/badge/?version=latest)](http://vault-php.readthedocs.io/en/latest/) 2 | 3 | ![example workflow](https://github.com/CSharpRU/vault-php/actions/workflows/tests.yml/badge.svg?branch=master) 4 | 5 | This is a PHP client for Vault - a tool for managing secrets. 6 | 7 | ## Features 8 | 9 | * Supports different authentication backends with token caching and re-authentication. 10 | * Different transports for different PHP versions. 11 | 12 | ## Installing / Getting started 13 | 14 | Simply run this command within your directory with composer.json. 15 | 16 | ```shell 17 | composer require csharpru/vault-php 18 | ``` 19 | 20 | ## Documentation 21 | 22 | Latest documentation is available here: http://vault-php.readthedocs.io/en/latest/ 23 | 24 | ## Developing 25 | 26 | If you want to contribute, execute following shell commands: 27 | 28 | ```shell 29 | git clone https://github.com/CSharpRU/vault-php.git 30 | cd vault-php/ 31 | composer install 32 | ``` 33 | 34 | Now you're ready to write tests and code. 35 | 36 | ## Contributing 37 | 38 | If you'd like to contribute, please fork the repository and use a feature 39 | branch. Pull requests are warmly welcome. 40 | 41 | Little hints for new contributors: 42 | * This repository follows gitflow and semver. 43 | * Please follow PSR and other good coding standards. 44 | 45 | ## Licensing 46 | 47 | The code in this project is licensed under MIT license. 48 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | support: tests/_support 7 | envs: tests/_envs 8 | bootstrap: _bootstrap.php 9 | settings: 10 | colors: true 11 | memory_limit: 1024M 12 | extensions: 13 | enabled: 14 | - Codeception\Extension\RunFailed 15 | coverage: 16 | enabled: true 17 | include: 18 | - src/* 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csharpru/vault-php", 3 | "description": "Best Vault client for PHP that you can find", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "vault", 8 | "hashicorp", 9 | "secrets" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Yaroslav Lukyanov", 14 | "email": "c_sharp@mail.ru" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": {"Vault\\": "src/"} 19 | }, 20 | "require": { 21 | "php": "^7.2 || ^8.0", 22 | "ext-json": "*", 23 | "psr/cache": "^1.0|^2.0|^3.0", 24 | "psr/log": "^1.0|^2.0|^3.0", 25 | "psr/http-client": "^1.0", 26 | "psr/http-factory": "^1.0", 27 | "aws/aws-sdk-php": "^3.0" 28 | }, 29 | "require-dev": { 30 | "codeception/codeception": "^4.1", 31 | "php-vcr/php-vcr": "^1.5", 32 | "alextartan/guzzle-psr18-adapter": "^1.2 || ^2.0", 33 | "laminas/laminas-diactoros": "^2.3 || ^3.0", 34 | "cache/array-adapter": "^1.0", 35 | "codeception/module-asserts": "^1.3", 36 | "symfony/event-dispatcher": "<5.0" 37 | }, 38 | "suggest": { 39 | "cache/array-adapter": "For usage with CachedClient class" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = VaultPHPClient 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Vault PHP Client documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Aug 10 14:35:27 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | import sphinx_rtd_theme 24 | 25 | from sphinx.highlighting import lexers 26 | from pygments.lexers.web import PhpLexer 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = ['sphinx.ext.githubpages'] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'Vault PHP Client' 53 | copyright = u'2017, Yaroslav Lukyanov' 54 | author = u'Yaroslav Lukyanov' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = u'3.3.1' 62 | # The full version, including alpha/beta/rc tags. 63 | release = u'3.3.1' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | # This patterns also effect to html_static_path and html_extra_path 75 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 76 | 77 | # The name of the Pygments (syntax highlighting) style to use. 78 | pygments_style = 'sphinx' 79 | 80 | # If true, `todo` and `todoList` produce output, else they produce nothing. 81 | todo_include_todos = False 82 | 83 | 84 | # -- Options for HTML output ---------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | html_theme = 'sphinx_rtd_theme' 90 | 91 | # Theme options are theme-specific and customize the look and feel of a theme 92 | # further. For a list of options available for each theme, see the 93 | # documentation. 94 | # 95 | # html_theme_options = {} 96 | 97 | # Add any paths that contain custom static files (such as style sheets) here, 98 | # relative to this directory. They are copied after the builtin static files, 99 | # so a file named "default.css" will overwrite the builtin "default.css". 100 | html_static_path = ['_static'] 101 | 102 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 103 | 104 | # Custom sidebar templates, must be a dictionary that maps document names 105 | # to template names. 106 | # 107 | # This is required for the alabaster theme 108 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 109 | html_sidebars = { 110 | '**': [ 111 | 'about.html', 112 | 'navigation.html', 113 | 'relations.html', # needs 'show_related': True theme option to display 114 | 'searchbox.html', 115 | 'donate.html', 116 | ] 117 | } 118 | 119 | 120 | # -- Options for HTMLHelp output ------------------------------------------ 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = 'VaultPHPClientdoc' 124 | 125 | 126 | # -- Options for LaTeX output --------------------------------------------- 127 | 128 | latex_elements = { 129 | # The paper size ('letterpaper' or 'a4paper'). 130 | # 131 | # 'papersize': 'letterpaper', 132 | 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | 137 | # Additional stuff for the LaTeX preamble. 138 | # 139 | # 'preamble': '', 140 | 141 | # Latex figure (float) alignment 142 | # 143 | # 'figure_align': 'htbp', 144 | } 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, 148 | # author, documentclass [howto, manual, or own class]). 149 | latex_documents = [ 150 | (master_doc, 'VaultPHPClient.tex', u'Vault PHP Client Documentation', 151 | u'Yaroslav Lukyanov', 'manual'), 152 | ] 153 | 154 | 155 | # -- Options for manual page output --------------------------------------- 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | (master_doc, 'vaultphpclient', u'Vault PHP Client Documentation', 161 | [author], 1) 162 | ] 163 | 164 | 165 | # -- Options for Texinfo output ------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | (master_doc, 'VaultPHPClient', u'Vault PHP Client Documentation', 172 | author, 'VaultPHPClient', 'One line description of project.', 173 | 'Miscellaneous'), 174 | ] 175 | 176 | 177 | # -- PHP options ---------------------------------------------------------- 178 | 179 | primary_domain = 'php' 180 | 181 | lexers['php'] = PhpLexer(startinline=True, linenos=1) 182 | lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1) 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Vault PHP Client documentation master file, created by 2 | sphinx-quickstart on Thu Aug 10 14:35:27 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Vault PHP Client's documentation! 7 | ============================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | This is a PHP client for Vault - a tool for managing secrets. 14 | 15 | Quick start 16 | ----------- 17 | 18 | .. code-block:: php 19 | 20 | setNamespace('my-namespace'); 41 | 42 | // Authenticating using userpass auth backend. 43 | 44 | $authenticated = $client 45 | ->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')) 46 | ->authenticate(); 47 | 48 | // Authenticating using approle auth backend. 49 | 50 | $authenticated = $client 51 | ->setAuthenticationStrategy(new AppRoleAuthenticationStrategy( 52 | 'd4131206-384f-75fa-11d6-55d1d63c07c0', 53 | 'cac86a12-c566-3932-09f3-5823ccdfa606' 54 | )) 55 | ->authenticate(); 56 | 57 | // Authenticating using token auth backend. 58 | $authenticated = $client 59 | ->setAuthenticationStrategy(new TokenAuthenticationStrategy('463763ae-0c3b-ff77-e137-af668941465c')) 60 | ->authenticate(); 61 | 62 | List secret keys 63 | ---------------- 64 | 65 | To retrieve a set of keys in a secret, after authentication, use the ``keys()`` method, passing in the database’s path, with the suffix ``/metadata``, as you can see in the highlighted section below. 66 | 67 | .. code-block:: php 68 | :linenos: 69 | :emphasize-lines: 29,30,31 70 | 71 | setAuthenticationStrategy(new TokenAuthenticationStrategy('463763ae-0c3b-ff77-e137-af668941465c')) 91 | ->authenticate(); 92 | 93 | if (!$authenticated) { 94 | // Throw an exception or handle authentication failure. 95 | } 96 | 97 | // Request exception could appear here. 98 | /** @var \Vault\ResponseModels\Response $response */ 99 | $response = $client->keys('/secret/metadata'); 100 | 101 | $data = $response->getData(); // Raw array with a list of secret keys. 102 | 103 | // ... 104 | 105 | On success, an associative array is returned, similar in structure to the example below. 106 | This array contains an element named ``keys``, whose value is an array of the secret's keys. 107 | 108 | .. code-block:: php 109 | 110 | [ 111 | "keys": [ 112 | "hello" 113 | ] 114 | ] 115 | Fetching a secret 116 | ----------------- 117 | 118 | .. code-block:: php 119 | 120 | setAuthenticationStrategy(new TokenAuthenticationStrategy('463763ae-0c3b-ff77-e137-af668941465c')) 140 | ->authenticate(); 141 | 142 | if (!$authenticated) { 143 | // Throw an exception or handle authentication failure. 144 | } 145 | 146 | // Request exception could appear here. 147 | /** @var \Vault\ResponseModels\Response $response */ 148 | $response = $client->read('/secret/database'); 149 | 150 | $data = $response->getData(); // Raw array with secret's content. 151 | 152 | // ... 153 | 154 | Indices and tables 155 | ================== 156 | 157 | * :ref:`search` 158 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=VaultPHPClient 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | -------------------------------------------------------------------------------- /src/AuthenticationStrategies/AbstractAuthenticationStrategy.php: -------------------------------------------------------------------------------- 1 | client; 25 | } 26 | 27 | /** 28 | * @param Client $client 29 | * 30 | * @return $this 31 | */ 32 | public function setClient(Client $client) 33 | { 34 | $this->client = $client; 35 | 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AuthenticationStrategies/AbstractPathAuthenticationStrategy.php: -------------------------------------------------------------------------------- 1 | username = $username; 40 | $this->password = $password; 41 | } 42 | 43 | /** 44 | * Returns auth for further interactions with Vault. 45 | * 46 | * @return Auth 47 | * @throws AuthenticationException 48 | * @throws ClientExceptionInterface 49 | */ 50 | public function authenticate(): Auth 51 | { 52 | if (!$this->methodPathSegment) { 53 | throw new AuthenticationException('methodPathSegment must be set before usage'); 54 | } 55 | 56 | $response = $this->client->write( 57 | sprintf('/auth/%s/login/%s', $this->methodPathSegment, $this->username), 58 | ['password' => $this->password] 59 | ); 60 | 61 | return $response->getAuth(); 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getMethodPathSegment(): string 68 | { 69 | return $this->methodPathSegment; 70 | } 71 | 72 | /** 73 | * @param string $methodPathSegment 74 | * 75 | * @return static 76 | */ 77 | public function setMethodPathSegment(string $methodPathSegment) 78 | { 79 | $this->methodPathSegment = $methodPathSegment; 80 | 81 | return $this; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/AuthenticationStrategies/AppRoleAuthenticationStrategy.php: -------------------------------------------------------------------------------- 1 | roleId = $roleId; 40 | $this->secretId = $secretId; 41 | $this->name = $name; 42 | } 43 | 44 | /** 45 | * Returns auth for further interactions with Vault. 46 | * 47 | * @return Auth 48 | * @throws ClientExceptionInterface 49 | */ 50 | public function authenticate(): ?Auth 51 | { 52 | $response = $this->client->write( 53 | '/auth/' . $this->name . '/login', 54 | [ 55 | 'role_id' => $this->roleId, 56 | 'secret_id' => $this->secretId, 57 | ] 58 | ); 59 | 60 | return $response->getAuth(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AuthenticationStrategies/AuthenticationStrategy.php: -------------------------------------------------------------------------------- 1 | role = $role; 46 | $this->region = $region; 47 | $this->serverId = $vaultServerId; 48 | $this->stsClient = $stsClient; 49 | } 50 | 51 | /** 52 | * Returns auth for further interactions with Vault. 53 | * 54 | * @return Auth 55 | * @throws AuthenticationException 56 | * @throws ClientExceptionInterface 57 | */ 58 | public function authenticate(): Auth 59 | { 60 | if (!$this->methodPathSegment) { 61 | throw new AuthenticationException('methodPathSegment must be set before usage'); 62 | } 63 | 64 | if (!$this->stsClient) { 65 | $this->stsClient = new StsClient([ 66 | 'region' => $this->region, 67 | 'version' => 'latest', 68 | 'sts_regional_endpoints' => 'regional', 69 | ]); 70 | } 71 | 72 | 73 | // Creating a signed command, to get the parameters for the actual login-request to vault 74 | $command = $this->stsClient->getCommand('GetCallerIdentity'); 75 | 76 | if ($this->serverId) { 77 | $command->getHandlerList()->appendBuild( 78 | Middleware::mapRequest(function (RequestInterface $request) { 79 | return $request->withHeader('X-Vault-AWS-IAM-Server-ID', $this->serverId); 80 | }), 81 | 'add-header' 82 | ); 83 | } 84 | 85 | $request = \Aws\serialize($command); 86 | 87 | $response = $this->client->write( 88 | sprintf('/auth/%s/login', $this->methodPathSegment), 89 | [ 90 | 'role' => $this->role, 91 | 'iam_http_request_method' => $request->getMethod(), 92 | 'iam_request_url' => base64_encode($request->getUri()), 93 | 'iam_request_body' => base64_encode($request->getBody()), 94 | 'iam_request_headers' => base64_encode(json_encode($request->getHeaders())), 95 | ] 96 | ); 97 | 98 | return $response->getAuth(); 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function getMethodPathSegment(): string 105 | { 106 | return $this->methodPathSegment; 107 | } 108 | 109 | /** 110 | * @param string $methodPathSegment 111 | * 112 | * @return static 113 | */ 114 | public function setMethodPathSegment(string $methodPathSegment) 115 | { 116 | $this->methodPathSegment = $methodPathSegment; 117 | 118 | return $this; 119 | } 120 | } -------------------------------------------------------------------------------- /src/AuthenticationStrategies/LdapAuthenticationStrategy.php: -------------------------------------------------------------------------------- 1 | token = $token; 27 | } 28 | 29 | /** 30 | * Returns auth for further interactions with Vault. 31 | * 32 | * @return Auth 33 | */ 34 | public function authenticate(): ?Auth 35 | { 36 | return new Auth(['clientToken' => $this->token]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AuthenticationStrategies/UserPassAuthenticationStrategy.php: -------------------------------------------------------------------------------- 1 | baseUri = $baseUri; 89 | $this->client = $client; 90 | $this->requestFactory = $requestFactory; 91 | $this->streamFactory = $streamFactory; 92 | $this->logger = $logger ?: new NullLogger(); 93 | $this->responseBuilder = new ResponseBuilder(); 94 | } 95 | 96 | /** 97 | * @param string $path 98 | * 99 | * @return Response 100 | * @throws InvalidArgumentException 101 | * @throws ClientExceptionInterface 102 | */ 103 | public function head(string $path): Response 104 | { 105 | return $this->responseBuilder->build($this->send('HEAD', $path)); 106 | } 107 | 108 | /** 109 | * @param string $method 110 | * @param string $path 111 | * @param string $body 112 | * 113 | * @return ResponseInterface 114 | * @throws RequestException 115 | * @throws InvalidArgumentException 116 | */ 117 | public function send(string $method, string $path, string $body = ''): ResponseInterface 118 | { 119 | $headers = [ 120 | 'User-Agent' => 'VaultPHP/1.0.0', 121 | 'Content-Type' => 'application/json', 122 | ]; 123 | 124 | if ($this->token) { 125 | $headers['X-Vault-Token'] = $this->token->getAuth()->getClientToken(); 126 | } 127 | 128 | if ($this->namespace) { 129 | $headers['X-Vault-Namespace'] = $this->getNamespace(); 130 | } 131 | 132 | if (strpos($path, '?') !== false) { 133 | [$path, $query] = explode('?', $path, 2); 134 | $this->baseUri = $this->baseUri->withQuery($query); 135 | } 136 | 137 | $request = $this->requestFactory->createRequest(strtoupper($method), $this->baseUri->withPath($path)); 138 | 139 | foreach ($headers as $name => $value) { 140 | $request = $request->withHeader($name, $value); 141 | } 142 | 143 | $request = $request->withBody($this->streamFactory->createStream($body)); 144 | 145 | $this->logger->debug('Request.', [ 146 | 'method' => $method, 147 | 'uri' => $request->getUri(), 148 | 'headers' => $headers, 149 | 'body' => $body, 150 | ]); 151 | 152 | try { 153 | $response = $this->client->sendRequest($request); 154 | 155 | if ($response->getStatusCode() > 399) { 156 | throw new RequestException( 157 | 'Bad status received from Vault', 158 | $response->getStatusCode(), 159 | null, 160 | $request 161 | ); 162 | } 163 | } catch (ClientExceptionInterface $e) { 164 | $this->logger->error('Something went wrong when calling Vault.', [ 165 | 'code' => $e->getCode(), 166 | 'message' => $e->getMessage(), 167 | ]); 168 | 169 | $this->logger->debug('Trace.', ['exception' => $e]); 170 | 171 | throw new RequestException($e->getMessage(), $e->getCode(), $e, $request); 172 | } 173 | 174 | $this->logger->debug('Response.', [ 175 | 'statusCode' => $response->getStatusCode(), 176 | 'reasonPhrase' => $response->getReasonPhrase(), 177 | 'headers ' => $response->getHeaders(), 178 | 'body' => $response->getBody()->getContents(), 179 | ]); 180 | 181 | return $response; 182 | } 183 | 184 | /** 185 | * @param string $path 186 | * 187 | * @return Response 188 | * @throws InvalidArgumentException 189 | * @throws ClientExceptionInterface 190 | */ 191 | public function list(string $path = ''): Response 192 | { 193 | return $this->responseBuilder->build($this->send('LIST', $path)); 194 | } 195 | 196 | /** 197 | * @param string $path 198 | * 199 | * @return Response 200 | * @throws InvalidArgumentException 201 | * @throws ClientExceptionInterface 202 | */ 203 | public function get(string $path = ''): Response 204 | { 205 | return $this->responseBuilder->build($this->send('GET', $path)); 206 | } 207 | 208 | /** 209 | * @param string $path 210 | * @param string $body 211 | * 212 | * @return Response 213 | * @throws InvalidArgumentException 214 | * @throws ClientExceptionInterface 215 | */ 216 | public function put(string $path, string $body = ''): Response 217 | { 218 | return $this->responseBuilder->build($this->send('PUT', $path, $body)); 219 | } 220 | 221 | /** 222 | * @param string $path 223 | * @param string $body 224 | * 225 | * @return Response 226 | * @throws InvalidArgumentException 227 | * @throws ClientExceptionInterface 228 | */ 229 | public function patch(string $path, string $body = ''): Response 230 | { 231 | return $this->responseBuilder->build($this->send('PATCH', $path, $body)); 232 | } 233 | 234 | /** 235 | * @param string $path 236 | * 237 | * @return Response 238 | * @throws InvalidArgumentException 239 | * @throws ClientExceptionInterface 240 | */ 241 | public function options(string $path): Response 242 | { 243 | return $this->responseBuilder->build($this->send('OPTIONS', $path)); 244 | } 245 | 246 | /** 247 | * @param string $path 248 | * @param string $body 249 | * 250 | * @return Response 251 | * @throws InvalidArgumentException 252 | * @throws ClientExceptionInterface 253 | */ 254 | public function post(string $path, string $body = ''): Response 255 | { 256 | return $this->responseBuilder->build($this->send('POST', $path, $body)); 257 | } 258 | 259 | /** 260 | * @param string $path 261 | * 262 | * @return Response 263 | * @throws InvalidArgumentException 264 | * @throws ClientExceptionInterface 265 | */ 266 | public function delete(string $path): Response 267 | { 268 | return $this->responseBuilder->build($this->send('DELETE', $path)); 269 | } 270 | 271 | /** 272 | * @return string 273 | */ 274 | public function getVersion(): string 275 | { 276 | return $this->version; 277 | } 278 | 279 | /** 280 | * @param string $version 281 | * 282 | * @return $this 283 | */ 284 | public function setVersion(string $version) 285 | { 286 | $this->version = $version; 287 | 288 | return $this; 289 | } 290 | 291 | /** 292 | * @return Token 293 | */ 294 | public function getToken(): Token 295 | { 296 | return $this->token; 297 | } 298 | 299 | /** 300 | * @param Token $token 301 | * 302 | * @return $this 303 | */ 304 | public function setToken(Token $token) 305 | { 306 | $this->token = $token; 307 | 308 | return $this; 309 | } 310 | 311 | /** 312 | * @return Namespace 313 | */ 314 | public function getNamespace(): string 315 | { 316 | return $this->namespace; 317 | } 318 | 319 | /** 320 | * @param String $namespace 321 | * 322 | * @return $this 323 | */ 324 | public function setNamespace(string $namespace) 325 | { 326 | $this->namespace = $namespace; 327 | 328 | return $this; 329 | } 330 | 331 | /** 332 | * @return UriInterface 333 | */ 334 | public function getBaseUri(): UriInterface 335 | { 336 | return $this->baseUri; 337 | } 338 | 339 | /** 340 | * @param UriInterface $baseUri 341 | * 342 | * @return $this 343 | */ 344 | public function setBaseUri(UriInterface $baseUri) 345 | { 346 | $this->baseUri = $baseUri; 347 | 348 | return $this; 349 | } 350 | 351 | /** 352 | * @return ClientInterface 353 | */ 354 | public function getClient(): ClientInterface 355 | { 356 | return $this->client; 357 | } 358 | 359 | /** 360 | * @param ClientInterface $client 361 | * 362 | * @return $this 363 | */ 364 | public function setClient(ClientInterface $client) 365 | { 366 | $this->client = $client; 367 | 368 | return $this; 369 | } 370 | 371 | /** 372 | * @return RequestFactoryInterface 373 | */ 374 | public function getRequestFactory(): RequestFactoryInterface 375 | { 376 | return $this->requestFactory; 377 | } 378 | 379 | /** 380 | * @param RequestFactoryInterface $requestFactory 381 | * 382 | * @return $this 383 | */ 384 | public function setRequestFactory(RequestFactoryInterface $requestFactory) 385 | { 386 | $this->requestFactory = $requestFactory; 387 | 388 | return $this; 389 | } 390 | 391 | /** 392 | * @return StreamFactoryInterface 393 | */ 394 | public function getStreamFactory(): StreamFactoryInterface 395 | { 396 | return $this->streamFactory; 397 | } 398 | 399 | /** 400 | * @param StreamFactoryInterface $streamFactory 401 | * 402 | * @return $this 403 | */ 404 | public function setStreamFactory($streamFactory) 405 | { 406 | $this->streamFactory = $streamFactory; 407 | 408 | return $this; 409 | } 410 | 411 | /** 412 | * @return ResponseBuilder 413 | */ 414 | public function getResponseBuilder(): ResponseBuilder 415 | { 416 | return $this->responseBuilder; 417 | } 418 | 419 | /** 420 | * @param ResponseBuilder $responseBuilder 421 | * 422 | * @return $this 423 | */ 424 | public function setResponseBuilder(ResponseBuilder $responseBuilder) 425 | { 426 | $this->responseBuilder = $responseBuilder; 427 | 428 | return $this; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/BaseObject.php: -------------------------------------------------------------------------------- 1 | $value) { 24 | if (!$this->canSetProperty($name)) { 25 | continue; 26 | } 27 | 28 | $this->$name = $value; 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * @param string $name 35 | * @param bool $checkVars 36 | * 37 | * @return bool 38 | */ 39 | public function canSetProperty($name, $checkVars = true): bool 40 | { 41 | return method_exists($this, 'set' . $name) || ($checkVars && property_exists($this, $name)); 42 | } 43 | 44 | /** 45 | * @param $name 46 | * 47 | * @return mixed 48 | * 49 | * @throws RuntimeException 50 | */ 51 | public function __get($name) 52 | { 53 | $getter = 'get' . $name; 54 | 55 | if (method_exists($this, $getter)) { 56 | return $this->$getter(); 57 | } 58 | 59 | if (method_exists($this, 'set' . $name)) { 60 | throw new RuntimeException('Getting write-only property: ' . get_class($this) . '::' . $name); 61 | } 62 | 63 | throw new RuntimeException('Getting unknown property: ' . get_class($this) . '::' . $name); 64 | } 65 | 66 | /** 67 | * @param string $name 68 | * @param mixed $value 69 | * 70 | * @throws RuntimeException 71 | */ 72 | public function __set($name, $value) 73 | { 74 | $setter = 'set' . $name; 75 | 76 | if (method_exists($this, $setter)) { 77 | $this->$setter($value); 78 | } elseif (method_exists($this, 'get' . $name)) { 79 | throw new RuntimeException('Setting read-only property: ' . get_class($this) . '::' . $name); 80 | } else { 81 | throw new RuntimeException('Setting unknown property: ' . get_class($this) . '::' . $name); 82 | } 83 | } 84 | 85 | /** 86 | * @param string $name 87 | * 88 | * @return bool 89 | */ 90 | public function __isset($name) 91 | { 92 | $getter = 'get' . $name; 93 | 94 | if (method_exists($this, $getter)) { 95 | return $this->$getter() !== null; 96 | } 97 | 98 | return false; 99 | } 100 | 101 | /** 102 | * @param string $name 103 | * 104 | * @throws RuntimeException 105 | */ 106 | public function __unset($name) 107 | { 108 | $setter = 'set' . $name; 109 | 110 | if (method_exists($this, $setter)) { 111 | $this->$setter(null); 112 | } elseif (method_exists($this, 'get' . $name)) { 113 | throw new RuntimeException('Unsetting read-only property: ' . get_class($this) . '::' . $name); 114 | } 115 | } 116 | 117 | /** 118 | * @param string $name 119 | * @param array $params 120 | * 121 | * @throws RuntimeException 122 | */ 123 | public function __call($name, $params) 124 | { 125 | throw new RuntimeException('Unknown method: ' . get_class($this) . "::$name()"); 126 | } 127 | 128 | /** 129 | * @param string $name 130 | * @param bool $checkVars 131 | * 132 | * @return bool 133 | */ 134 | public function hasProperty($name, $checkVars = true): bool 135 | { 136 | return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false); 137 | } 138 | 139 | /** 140 | * @param string $name 141 | * @param bool $checkVars 142 | * 143 | * @return bool 144 | */ 145 | public function canGetProperty($name, $checkVars = true): bool 146 | { 147 | return method_exists($this, 'get' . $name) || ($checkVars && property_exists($this, $name)); 148 | } 149 | 150 | /** 151 | * @param string $name 152 | * 153 | * @return bool 154 | */ 155 | public function hasMethod($name): bool 156 | { 157 | return method_exists($this, $name); 158 | } 159 | 160 | /** 161 | * @param bool $recursive 162 | * 163 | * @return array 164 | */ 165 | public function toArray($recursive = true): array 166 | { 167 | $data = []; 168 | 169 | foreach ($this->getFields() as $field => $definition) { 170 | $data[$field] = is_string($definition) ? $this->$definition : $definition($this, $field); 171 | } 172 | 173 | return $recursive ? ArrayHelper::toArray($data, $recursive) : $data; 174 | } 175 | 176 | /** 177 | * @return array 178 | */ 179 | public function getFields(): array 180 | { 181 | $result = []; 182 | 183 | $fields = array_keys(get_object_vars($this)); 184 | $fields = array_combine($fields, $fields); 185 | 186 | foreach ($fields as $field => $definition) { 187 | if (is_int($field)) { 188 | $field = $definition; 189 | } 190 | 191 | $result[$field] = $definition; 192 | } 193 | 194 | return $result; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Builders/ResponseBuilder.php: -------------------------------------------------------------------------------- 1 | getBody(), true) ?: []; 26 | $data = ModelHelper::camelize($rawData); 27 | $data['data'] = $rawData['data'] ?? []; 28 | 29 | if (array_key_exists('auth', $data) && $data['auth']) { 30 | $data['auth'] = new Auth($data['auth']); 31 | } 32 | 33 | return new Response($data); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CachedClient.php: -------------------------------------------------------------------------------- 1 | readCacheEnabled) { 36 | return parent::read($path); 37 | } 38 | 39 | if (!$this->cache) { 40 | $this->logger->warning('Cache is enabled, but cache pool is not set.'); 41 | 42 | return parent::read($path); 43 | } 44 | 45 | $key = self::READ_CACHE_KEY . str_replace(['{', '}', '(', ')', '/', '\\', '@', '-'], '_', $path); 46 | 47 | if ($this->cache->hasItem($key)) { 48 | $this->logger->debug('Has read response in cache.', ['path' => $path]); 49 | 50 | return $this->cache->getItem($key)->get(); 51 | } 52 | 53 | $response = parent::read($path); 54 | 55 | $item = $this->cache->getItem($key); 56 | 57 | $item->set($response)->expiresAfter($this->readCacheTtl); 58 | 59 | $this->logger->debug('Saving read response in cache.', ['path' => $path, 'item' => $item]); 60 | 61 | if (!$this->cache->save($item)) { 62 | $this->logger->warning('Cannot save read response into cache.', ['path' => $path]); 63 | } 64 | 65 | return $response; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function isReadCacheEnabled(): bool 72 | { 73 | return $this->readCacheEnabled; 74 | } 75 | 76 | /** 77 | * @return $this 78 | */ 79 | public function enableReadCache() 80 | { 81 | $this->readCacheEnabled = true; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @return $this 88 | */ 89 | public function disableReadCache() 90 | { 91 | $this->readCacheEnabled = false; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * @return int 98 | */ 99 | public function getReadCacheTtl(): int 100 | { 101 | return $this->readCacheTtl; 102 | } 103 | 104 | /** 105 | * @param int $readCacheTtl 106 | * 107 | * @return $this 108 | */ 109 | public function setReadCacheTtl(int $readCacheTtl) 110 | { 111 | $this->readCacheTtl = $readCacheTtl; 112 | 113 | return $this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | get($this->buildPath($path)); 51 | } 52 | 53 | /** 54 | * @param string $path 55 | * 56 | * @return string 57 | */ 58 | public function buildPath(string $path): string 59 | { 60 | if (!$this->version) { 61 | $this->logger->warning('API version is not set!'); 62 | 63 | return $path; 64 | } 65 | 66 | return sprintf('/%s%s', $this->version, $path); 67 | } 68 | 69 | /** 70 | * @param string $path 71 | * 72 | * @return Response 73 | * @throws \InvalidArgumentException 74 | * @throws ClientExceptionInterface 75 | */ 76 | public function keys(string $path): Response 77 | { 78 | return $this->list($this->buildPath($path)); 79 | } 80 | 81 | /** 82 | * @param string $path 83 | * @param array $data 84 | * 85 | * @return Response 86 | * @throws \InvalidArgumentException 87 | * @throws ClientExceptionInterface 88 | */ 89 | public function write(string $path, array $data = []): Response 90 | { 91 | return $this->post($this->buildPath($path), json_encode($data)); 92 | } 93 | 94 | /** 95 | * @param string $path 96 | * 97 | * @return Response 98 | * @throws \InvalidArgumentException 99 | * @throws ClientExceptionInterface 100 | */ 101 | public function revoke(string $path): Response 102 | { 103 | return $this->delete($this->buildPath($path)); 104 | } 105 | 106 | /** 107 | * @return CacheItemPoolInterface 108 | */ 109 | public function getCache(): CacheItemPoolInterface 110 | { 111 | return $this->cache; 112 | } 113 | 114 | /** 115 | * @param CacheItemPoolInterface $cache 116 | * 117 | * @return $this 118 | */ 119 | public function setCache(CacheItemPoolInterface $cache): self 120 | { 121 | $this->cache = $cache; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @return AuthenticationStrategy 128 | */ 129 | public function getAuthenticationStrategy(): AuthenticationStrategy 130 | { 131 | return $this->authenticationStrategy; 132 | } 133 | 134 | /** 135 | * @param AuthenticationStrategy $authenticationStrategy 136 | * 137 | * @return $this 138 | */ 139 | public function setAuthenticationStrategy(AuthenticationStrategy $authenticationStrategy): self 140 | { 141 | $authenticationStrategy->setClient($this); 142 | 143 | $this->authenticationStrategy = $authenticationStrategy; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * @inheritdoc 150 | * @throws DependencyException 151 | * @throws AuthenticationException 152 | * @throws Exception 153 | * @throws InvalidArgumentException 154 | * @throws ClientExceptionInterface 155 | */ 156 | public function send(string $method, string $path, string $body = ''): ResponseInterface 157 | { 158 | try { 159 | return parent::send($method, $path, $body); 160 | } /** @noinspection PhpRedundantCatchClauseInspection */ catch (RequestException $e) { 161 | // re-authenticate if 403 and token is expired 162 | if ( 163 | $this->token && 164 | $e->getCode() === 403 && 165 | $this->isTokenExpired($this->token) 166 | ) { 167 | try { 168 | if ($this->authenticate()) { 169 | return parent::send($method, $path, $body); 170 | } 171 | } catch (Exception $e) { 172 | $this->logger->error('Cannot re-authenticate.', [ 173 | 'code' => $e->getCode(), 174 | 'message' => $e->getMessage(), 175 | ]); 176 | 177 | $this->logger->debug('Trace.', ['exception' => $e]); 178 | } 179 | 180 | throw new AuthenticationException('Cannot re-authenticate'); 181 | } 182 | 183 | throw $e; 184 | } 185 | } 186 | 187 | /** 188 | * @param Token $token 189 | * 190 | * @return bool 191 | */ 192 | protected function isTokenExpired(Token $token): bool 193 | { 194 | return !$token || 195 | ( 196 | $token->getCreationTtl() > 0 && 197 | time() > $token->getCreationTime() + $token->getCreationTtl() 198 | ); 199 | } 200 | 201 | /** 202 | * @return bool 203 | * 204 | * @throws RuntimeException 205 | * @throws DependencyException 206 | * @throws Exception 207 | * @throws InvalidArgumentException 208 | * @throws ClientExceptionInterface 209 | */ 210 | public function authenticate(): bool 211 | { 212 | if ($this->token = $this->getTokenFromCache()) { 213 | $this->logger->debug('Using token from cache.'); 214 | 215 | $this->writeTokenInfoToDebugLog(); 216 | 217 | return (bool)$this->token; 218 | } 219 | 220 | if (!$this->authenticationStrategy) { 221 | $this->logger->critical('Trying to authenticate without strategy.'); 222 | 223 | throw new DependencyException(sprintf( 224 | 'Specify authentication strategy before calling this method (%s).', 225 | __METHOD__ 226 | )); 227 | } 228 | 229 | $this->logger->debug('Trying to authenticate.'); 230 | 231 | if ($auth = $this->authenticationStrategy->authenticate()) { 232 | $this->logger->debug('Authentication was successful.', ['clientToken' => $auth->getClientToken()]); 233 | 234 | // temporary 235 | $this->token = new Token(['auth' => $auth]); 236 | 237 | // get info about self 238 | $response = $this->get('/v1/auth/token/lookup-self'); 239 | 240 | $this->token = new Token(array_merge(ModelHelper::camelize($response->getData()), ['auth' => $auth])); 241 | 242 | $this->writeTokenInfoToDebugLog(); 243 | $this->putTokenIntoCache(); 244 | 245 | return true; 246 | } 247 | 248 | return false; 249 | } 250 | 251 | /** 252 | * @TODO: move to separated class 253 | * 254 | * @return Token|null 255 | * 256 | * @throws InvalidArgumentException 257 | */ 258 | protected function getTokenFromCache(): ?Token 259 | { 260 | if (!$this->cache || !$this->cache->hasItem(self::TOKEN_CACHE_KEY)) { 261 | return null; 262 | } 263 | 264 | /** @var Token $token */ 265 | $token = $this->cache->getItem(self::TOKEN_CACHE_KEY)->get(); 266 | 267 | if (!$token || !$token->getAuth()) { 268 | $this->logger->debug('No token in cache or auth is empty, returning null.'); 269 | 270 | return null; 271 | } 272 | 273 | // invalidate token 274 | if ($this->isTokenExpired($token)) { 275 | $this->logger->debug('Token is expired.'); 276 | 277 | $this->writeTokenInfoToDebugLog(); 278 | 279 | return null; 280 | } 281 | 282 | return $token; 283 | } 284 | 285 | private function writeTokenInfoToDebugLog(): void 286 | { 287 | if (!$this->token) { 288 | $this->logger->debug('Token is null, cannot write info to debug, potential error.'); 289 | 290 | return; 291 | } 292 | 293 | $this->logger->debug('Token info.', [ 294 | 'clientToken' => $this->token->getAuth() ? $this->token->getAuth()->getClientToken() : null, 295 | 'id' => $this->token->getId(), 296 | 'creationTime' => $this->token->getCreationTime(), 297 | 'ttl' => $this->token->getCreationTtl(), 298 | ]); 299 | } 300 | 301 | /** 302 | * @TODO: move to separated class 303 | * 304 | * @return bool 305 | * @throws Exception 306 | * @throws RuntimeException 307 | * @throws InvalidArgumentException 308 | */ 309 | protected function putTokenIntoCache(): bool 310 | { 311 | if (!$this->cache) { 312 | return true; // just ignore 313 | } 314 | 315 | if ($this->isTokenExpired($this->token)) { 316 | throw new RuntimeException('Cannot save expired token into cache!'); 317 | } 318 | 319 | $authItem = $this->cache->getItem(self::TOKEN_CACHE_KEY); 320 | 321 | $authItem->set($this->token)->expiresAfter($this->token->getAuth()->getLeaseDuration()); 322 | 323 | $this->logger->debug('Token is saved into cache.'); 324 | 325 | return $this->cache->save($authItem); 326 | } 327 | 328 | /** 329 | * @inheritdoc 330 | * @throws Exception 331 | * @throws RuntimeException 332 | * @throws InvalidArgumentException 333 | */ 334 | public function setToken(Token $token) 335 | { 336 | parent::setToken($token); 337 | 338 | $this->putTokenIntoCache(); 339 | 340 | return $this; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/Exceptions/AuthenticationException.php: -------------------------------------------------------------------------------- 1 | request = $request; 31 | } 32 | 33 | /** 34 | * Returns the request. 35 | * 36 | * The request object MAY be a different object from the one passed to ClientInterface::sendRequest() 37 | * 38 | * @return RequestInterface 39 | */ 40 | public function getRequest(): RequestInterface 41 | { 42 | return $this->request; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | toArray($recursive); 26 | } 27 | 28 | foreach ($object as $key => $value) { 29 | if ($value instanceof BaseObject) { 30 | $newValue = $value->toArray($recursive); 31 | } else { 32 | $newValue = (is_array($value) || is_object($value)) && $recursive ? self::toArray($value) : $value; 33 | } 34 | 35 | $array[$key] = $newValue; 36 | } 37 | 38 | return $array; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Helpers/ModelHelper.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | if (is_array($value) && $recursive) { 24 | $value = self::camelize($value, $recursive); 25 | } 26 | 27 | $return[self::camelizeString($key)] = $value; 28 | } 29 | 30 | return $return; 31 | } 32 | 33 | private static function camelizeString(string $data): string 34 | { 35 | $camelizedString = str_replace([' ', '_', '-'], '', ucwords($data, ' _-')); 36 | 37 | return lcfirst($camelizedString); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Models/Token.php: -------------------------------------------------------------------------------- 1 | auth; 86 | } 87 | 88 | /** 89 | * @return string|null 90 | */ 91 | public function getAccessor(): ?string 92 | { 93 | return $this->accessor; 94 | } 95 | 96 | /** 97 | * @return int|null 98 | */ 99 | public function getCreationTime(): ?int 100 | { 101 | return $this->creationTime; 102 | } 103 | 104 | /** 105 | * @return int|null 106 | */ 107 | public function getCreationTtl(): ?int 108 | { 109 | return $this->creationTtl; 110 | } 111 | 112 | /** 113 | * @return string|null 114 | */ 115 | public function getDisplayName(): ?string 116 | { 117 | return $this->displayName; 118 | } 119 | 120 | /** 121 | * @return int|null 122 | */ 123 | public function getExplicitMaxTtl(): ?int 124 | { 125 | return $this->explicitMaxTtl; 126 | } 127 | 128 | /** 129 | * @return string|null 130 | */ 131 | public function getId(): ?string 132 | { 133 | return $this->id; 134 | } 135 | 136 | /** 137 | * @return array 138 | */ 139 | public function getMeta(): array 140 | { 141 | return $this->meta; 142 | } 143 | 144 | /** 145 | * @return int 146 | */ 147 | public function getNumUses(): int 148 | { 149 | return $this->numUses; 150 | } 151 | 152 | /** 153 | * @return bool 154 | */ 155 | public function isOrphan(): bool 156 | { 157 | return $this->orphan; 158 | } 159 | 160 | /** 161 | * @return string|null 162 | */ 163 | public function getPath(): ?string 164 | { 165 | return $this->path; 166 | } 167 | 168 | /** 169 | * @return array 170 | */ 171 | public function getPolicies(): array 172 | { 173 | return $this->policies; 174 | } 175 | 176 | /** 177 | * @return int|null 178 | */ 179 | public function getTtl(): ?int 180 | { 181 | return $this->ttl; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/ResponseModels/Auth.php: -------------------------------------------------------------------------------- 1 | clientToken; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ResponseModels/Response.php: -------------------------------------------------------------------------------- 1 | requestId; 38 | } 39 | 40 | /** 41 | * @return Auth|null 42 | */ 43 | public function getAuth(): ?Auth 44 | { 45 | return $this->auth; 46 | } 47 | 48 | /** 49 | * @return array|null 50 | */ 51 | public function getData(): ?array 52 | { 53 | return $this->data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ResponseModels/Traits/LeaseTrait.php: -------------------------------------------------------------------------------- 1 | leaseId; 33 | } 34 | 35 | /** 36 | * @return int|null 37 | */ 38 | public function getLeaseDuration(): ?int 39 | { 40 | return $this->leaseDuration; 41 | } 42 | 43 | /** 44 | * @return bool|null 45 | */ 46 | public function isRenewable(): ?bool 47 | { 48 | return $this->renewable; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | _output -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | enableLibraryHooks(['curl'])->setCassettePath(__DIR__ . '/_data/vcr'); 9 | -------------------------------------------------------------------------------- /tests/_data/vcr/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSharpRU/vault-php/2064cd7a377b066c6d256eef1d7f4f4ecbb927f1/tests/_data/vcr/.gitkeep -------------------------------------------------------------------------------- /tests/_data/vcr/authentication-strategies/app-role: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: POST 5 | url: 'http://127.0.0.1:8200/v1/auth/approle/login' 6 | headers: 7 | Host: '127.0.0.1:8200' 8 | Expect: null 9 | Accept-Encoding: null 10 | User-Agent: VaultPHP/1.0.0 11 | Content-Type: application/json 12 | Accept: null 13 | body: '{"role_id":"db02de05-fa39-4855-059b-67221c5c2f63","secret_id":"6a174c20-f6de-a53c-74d2-6018fcceff64"}' 14 | response: 15 | status: 16 | http_version: '1.1' 17 | code: '200' 18 | message: OK 19 | headers: 20 | Cache-Control: no-store 21 | Content-Type: application/json 22 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 23 | Content-Length: '366' 24 | body: "{\"request_id\":\"08962062-3aab-fd89-3413-99c3521ecc75\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":null,\"wrap_info\":null,\"warnings\":null,\"auth\":{\"client_token\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"policies\":[\"default\",\"test\"],\"metadata\":{\"username\":\"test\"},\"lease_duration\":2764800,\"renewable\":true}}\n" 25 | - 26 | request: 27 | method: GET 28 | url: 'http://127.0.0.1:8200/v1/auth/token/lookup-self' 29 | headers: 30 | Host: '127.0.0.1:8200' 31 | Accept-Encoding: null 32 | User-Agent: VaultPHP/1.0.0 33 | Content-Type: application/json 34 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 35 | Accept: null 36 | response: 37 | status: 38 | http_version: '1.1' 39 | code: '200' 40 | message: OK 41 | headers: 42 | Cache-Control: no-store 43 | Content-Type: application/json 44 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 45 | Content-Length: '504' 46 | body: "{\"request_id\":\"ab3f3dc9-3633-0c18-44e2-f3f58cb56d0a\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"creation_time\":1501570473,\"creation_ttl\":2764800,\"display_name\":\"userpass-test\",\"explicit_max_ttl\":0,\"id\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"meta\":{\"username\":\"test\"},\"num_uses\":0,\"orphan\":true,\"path\":\"auth/userpass/login/test\",\"policies\":[\"default\",\"test\"],\"renewable\":true,\"ttl\":2764800},\"wrap_info\":null,\"warnings\":null,\"auth\":null}\n" 47 | -------------------------------------------------------------------------------- /tests/_data/vcr/authentication-strategies/aws-iam: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: POST 5 | url: 'http://127.0.0.1:8200/v1/auth/aws/login' 6 | headers: 7 | Host: '127.0.0.1:8200' 8 | Expect: null 9 | Accept-Encoding: null 10 | User-Agent: VaultPHP/1.0.0 11 | Content-Type: application/json 12 | Accept: null 13 | body: '{"role":"dev-role","iam_http_request_method":"POST","iam_request_url":"aHR0cHM6Ly9zdHMuZXUtY2VudHJhbC0xLmFtYXpvbmF3cy5jb20=","iam_request_body":"QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==","iam_request_headers":"eyJIb3N0IjpbInN0cy5ldS1jZW50cmFsLTEuYW1hem9uYXdzLmNvbSJdLCJDb250ZW50LUxlbmd0aCI6WyI0MyJdLCJDb250ZW50LVR5cGUiOlsiYXBwbGljYXRpb25cL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCJdLCJYLUFtei1Vc2VyLUFnZW50IjpbImF3cy1zZGstcGhwXC8zLjI2MC4zIE9TXC9EYXJ3aW5cLzIyLjEuMCBsYW5nXC9waHBcLzguMC4yNyJdLCJVc2VyLUFnZW50IjpbImF3cy1zZGstcGhwXC8zLjI2MC4zIE9TXC9EYXJ3aW5cLzIyLjEuMCBsYW5nXC9waHBcLzguMC4yNyJdLCJYLVZhdWx0LUFXUy1JQU0tU2VydmVyLUlEIjpbImxvY2FsaG9zdCJdLCJhd3Mtc2RrLXJldHJ5IjpbIjBcLzAiXX0="}' 14 | response: 15 | status: 16 | http_version: '1.1' 17 | code: '200' 18 | message: OK 19 | headers: 20 | Cache-Control: no-store 21 | Content-Type: application/json 22 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 23 | Content-Length: '366' 24 | body: "{\"request_id\":\"08962062-3aab-fd89-3413-99c3521ecc75\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":null,\"wrap_info\":null,\"warnings\":null,\"auth\":{\"client_token\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"policies\":[\"default\",\"test\"],\"metadata\":{\"username\":\"test\"},\"lease_duration\":2764800,\"renewable\":true}}\n" 25 | - 26 | request: 27 | method: GET 28 | url: 'http://127.0.0.1:8200/v1/auth/token/lookup-self' 29 | headers: 30 | Host: '127.0.0.1:8200' 31 | Accept-Encoding: null 32 | User-Agent: VaultPHP/1.0.0 33 | Content-Type: application/json 34 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 35 | Accept: null 36 | response: 37 | status: 38 | http_version: '1.1' 39 | code: '200' 40 | message: OK 41 | headers: 42 | Cache-Control: no-store 43 | Content-Type: application/json 44 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 45 | Content-Length: '504' 46 | body: "{\"request_id\":\"ab3f3dc9-3633-0c18-44e2-f3f58cb56d0a\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"creation_time\":1501570473,\"creation_ttl\":2764800,\"display_name\":\"userpass-test\",\"explicit_max_ttl\":0,\"id\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"meta\":{\"username\":\"test\"},\"num_uses\":0,\"orphan\":true,\"path\":\"auth/userpass/login/test\",\"policies\":[\"default\",\"test\"],\"renewable\":true,\"ttl\":2764800},\"wrap_info\":null,\"warnings\":null,\"auth\":null}\n" 47 | -------------------------------------------------------------------------------- /tests/_data/vcr/authentication-strategies/token: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: GET 5 | url: 'http://127.0.0.1:8200/v1/auth/token/lookup-self' 6 | headers: 7 | Host: '127.0.0.1:8200' 8 | Accept-Encoding: null 9 | User-Agent: VaultPHP/1.0.0 10 | Content-Type: application/json 11 | X-Vault-Token: db02de05-fa39-4855-059b-67221c5c2f63 12 | Accept: null 13 | response: 14 | status: 15 | http_version: '1.1' 16 | code: '200' 17 | message: OK 18 | headers: 19 | Cache-Control: no-store 20 | Content-Type: application/json 21 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 22 | Content-Length: '504' 23 | body: "{\"request_id\":\"ab3f3dc9-3633-0c18-44e2-f3f58cb56d0a\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"creation_time\":1501570473,\"creation_ttl\":2764800,\"display_name\":\"userpass-test\",\"explicit_max_ttl\":0,\"id\":\"db02de05-fa39-4855-059b-67221c5c2f63\",\"meta\":{\"username\":\"test\"},\"num_uses\":0,\"orphan\":true,\"path\":\"auth/userpass/login/test\",\"policies\":[\"default\",\"test\"],\"renewable\":true,\"ttl\":2764800},\"wrap_info\":null,\"warnings\":null,\"auth\":null}\n" 24 | -------------------------------------------------------------------------------- /tests/_data/vcr/unit-client: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: POST 5 | url: 'http://127.0.0.1:8200/v1/auth/userpass/login/test' 6 | headers: 7 | Host: '127.0.0.1:8200' 8 | Expect: null 9 | Accept-Encoding: null 10 | User-Agent: VaultPHP/1.0.0 11 | Content-Type: application/json 12 | Accept: null 13 | body: '{"password":"test"}' 14 | response: 15 | status: 16 | http_version: '1.1' 17 | code: '200' 18 | message: OK 19 | headers: 20 | Cache-Control: no-store 21 | Content-Type: application/json 22 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 23 | Content-Length: '366' 24 | body: "{\"request_id\":\"08962062-3aab-fd89-3413-99c3521ecc75\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":null,\"wrap_info\":null,\"warnings\":null,\"auth\":{\"client_token\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"policies\":[\"default\",\"test\"],\"metadata\":{\"username\":\"test\"},\"lease_duration\":2764800,\"renewable\":true}}\n" 25 | - 26 | request: 27 | method: GET 28 | url: 'http://127.0.0.1:8200/v1/auth/token/lookup-self' 29 | headers: 30 | Host: '127.0.0.1:8200' 31 | Accept-Encoding: null 32 | User-Agent: VaultPHP/1.0.0 33 | Content-Type: application/json 34 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 35 | Accept: null 36 | response: 37 | status: 38 | http_version: '1.1' 39 | code: '200' 40 | message: OK 41 | headers: 42 | Cache-Control: no-store 43 | Content-Type: application/json 44 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 45 | Content-Length: '504' 46 | body: "{\"request_id\":\"ab3f3dc9-3633-0c18-44e2-f3f58cb56d0a\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"creation_time\":1501570473,\"creation_ttl\":0,\"display_name\":\"userpass-test\",\"explicit_max_ttl\":0,\"id\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"meta\":{\"username\":\"test\"},\"num_uses\":0,\"orphan\":true,\"path\":\"auth/userpass/login/test\",\"policies\":[\"default\",\"test\"],\"renewable\":true,\"ttl\":2764800},\"wrap_info\":null,\"warnings\":null,\"auth\":null}\n" 47 | - 48 | request: 49 | method: POST 50 | url: 'http://127.0.0.1:8200/v1/secret/test' 51 | headers: 52 | Host: '127.0.0.1:8200' 53 | Expect: null 54 | Accept-Encoding: null 55 | User-Agent: VaultPHP/1.0.0 56 | Content-Type: application/json 57 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 58 | Accept: null 59 | body: '{"value":"test"}' 60 | response: 61 | status: 62 | http_version: '1.1' 63 | code: '204' 64 | message: 'No Content' 65 | headers: 66 | Cache-Control: no-store 67 | Content-Type: application/json 68 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 69 | - 70 | request: 71 | method: GET 72 | url: 'http://127.0.0.1:8200/v1/secret/test' 73 | headers: 74 | Host: '127.0.0.1:8200' 75 | Accept-Encoding: null 76 | User-Agent: VaultPHP/1.0.0 77 | Content-Type: application/json 78 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 79 | Accept: null 80 | response: 81 | status: 82 | http_version: '1.1' 83 | code: '200' 84 | message: OK 85 | headers: 86 | Cache-Control: no-store 87 | Content-Type: application/json 88 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 89 | Content-Length: '180' 90 | body: "{\"request_id\":\"72547a3a-cbc8-4afe-158e-fcbbe8274b6f\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"value\":\"test\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}\n" 91 | - 92 | request: 93 | method: DELETE 94 | url: 'http://127.0.0.1:8200/v1/secret/test' 95 | headers: 96 | Host: '127.0.0.1:8200' 97 | Accept-Encoding: null 98 | User-Agent: VaultPHP/1.0.0 99 | Content-Type: application/json 100 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 101 | Accept: null 102 | response: 103 | status: 104 | http_version: '1.1' 105 | code: '204' 106 | message: 'No Content' 107 | headers: 108 | Cache-Control: no-store 109 | Content-Type: application/json 110 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 111 | - 112 | request: 113 | method: POST 114 | url: 'http://127.0.0.1:8200/v1/secret/test_prohibited' 115 | headers: 116 | Host: '127.0.0.1:8200' 117 | Expect: null 118 | Accept-Encoding: null 119 | User-Agent: VaultPHP/1.0.0 120 | Content-Type: application/json 121 | X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 122 | Accept: null 123 | body: '{"value":"test"}' 124 | response: 125 | status: 126 | http_version: '1.1' 127 | code: '403' 128 | message: Forbidden 129 | headers: 130 | Cache-Control: no-store 131 | Content-Type: application/json 132 | Date: 'Tue, 01 Aug 2017 06:54:33 GMT' 133 | Content-Length: '33' 134 | body: "{\"errors\":[\"permission denied\"]}\n" 135 | -------------------------------------------------------------------------------- /tests/_support/.gitignore: -------------------------------------------------------------------------------- 1 | _generated -------------------------------------------------------------------------------- /tests/_support/Helper/Unit.php: -------------------------------------------------------------------------------- 1 | setAuthenticationStrategy( 34 | new AppRoleAuthenticationStrategy( 35 | 'db02de05-fa39-4855-059b-67221c5c2f63', 36 | '6a174c20-f6de-a53c-74d2-6018fcceff64' 37 | ) 38 | ); 39 | 40 | $this->assertEquals($client->getAuthenticationStrategy()->getClient(), $client); 41 | $this->assertTrue($client->authenticate()); 42 | $this->assertNotEmpty($client->getToken()); 43 | $this->assertNotEmpty($client->getToken()->getAuth()->getLeaseDuration()); 44 | $this->assertNotEmpty($client->getToken()->getAuth()->isRenewable()); 45 | } 46 | 47 | protected function setUp(): void 48 | { 49 | VCR::turnOn(); 50 | 51 | VCR::insertCassette('authentication-strategies/app-role'); 52 | 53 | parent::setUp(); 54 | } 55 | 56 | protected function tearDown(): void 57 | { 58 | // To stop recording requests, eject the cassette 59 | VCR::eject(); 60 | 61 | // Turn off VCR to stop intercepting requests 62 | VCR::turnOff(); 63 | 64 | parent::tearDown(); 65 | } 66 | 67 | protected function _before() 68 | { 69 | } 70 | 71 | protected function _after() 72 | { 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/unit/AuthenticationStrategies/AwsIamAuthenticationStrategyTest.php: -------------------------------------------------------------------------------- 1 | 'eu-central-1', 36 | 'version' => 'latest', 37 | 'sts_regional_endpoints' => 'regional' 38 | ]); 39 | // These middlewares would break the test, due to some dynamic headers 40 | $stsClient->getHandlerList()->remove('invocation-id'); 41 | $stsClient->getHandlerList()->remove('signer'); 42 | 43 | $client->setAuthenticationStrategy( 44 | new AwsIamAuthenticationStrategy( 45 | 'dev-role', 46 | 'eu-central-1', 47 | 'localhost', 48 | $stsClient 49 | ) 50 | ); 51 | 52 | $this->assertEquals($client->getAuthenticationStrategy()->getClient(), $client); 53 | $this->assertTrue($client->authenticate()); 54 | $this->assertNotEmpty($client->getToken()); 55 | $this->assertNotEmpty($client->getToken()->getAuth()->getLeaseDuration()); 56 | $this->assertNotEmpty($client->getToken()->getAuth()->isRenewable()); 57 | } 58 | 59 | protected function setUp(): void 60 | { 61 | $this->markTestSkipped('Does not work as expected'); 62 | 63 | VCR::turnOn(); 64 | VCR::configure()->setMode(VCR::MODE_ONCE); 65 | 66 | VCR::insertCassette('authentication-strategies/aws-iam'); 67 | 68 | parent::setUp(); 69 | } 70 | 71 | protected function tearDown(): void 72 | { 73 | // To stop recording requests, eject the cassette 74 | VCR::eject(); 75 | 76 | // Turn off VCR to stop intercepting requests 77 | VCR::turnOff(); 78 | 79 | parent::tearDown(); 80 | } 81 | 82 | protected function _before() 83 | { 84 | } 85 | 86 | protected function _after() 87 | { 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/unit/AuthenticationStrategies/TokenAuthenticationStrategyTest.php: -------------------------------------------------------------------------------- 1 | setAuthenticationStrategy(new TokenAuthenticationStrategy('db02de05-fa39-4855-059b-67221c5c2f63')) 35 | ->setLogger(new NullLogger()); 36 | 37 | $this->assertEquals($client->getAuthenticationStrategy()->getClient(), $client); 38 | $this->assertTrue($client->authenticate()); 39 | $this->assertNotEmpty($client->getToken()); 40 | } 41 | 42 | protected function setUp(): void 43 | { 44 | VCR::turnOn(); 45 | 46 | VCR::insertCassette('authentication-strategies/token'); 47 | 48 | parent::setUp(); 49 | } 50 | 51 | protected function tearDown(): void 52 | { 53 | // To stop recording requests, eject the cassette 54 | VCR::eject(); 55 | 56 | // Turn off VCR to stop intercepting requests 57 | VCR::turnOff(); 58 | 59 | parent::tearDown(); 60 | } 61 | 62 | protected function _before() 63 | { 64 | } 65 | 66 | protected function _after() 67 | { 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/unit/CachedClientTest.php: -------------------------------------------------------------------------------- 1 | getAuthenticatedClient()->enableReadCache()->setCache(new ArrayCachePool()); 30 | 31 | $this->assertNotEmpty($client->write('/secret/test', ['value' => 'test'])); 32 | 33 | $data = $client->read('/secret/test')->getData(); 34 | 35 | $this->assertArrayHasKey('value', $data); 36 | $this->assertEquals('test', $data['value']); 37 | $this->assertNotEmpty($client->revoke('/secret/test')); 38 | $this->assertTrue($client->getCache()->hasItem(CachedClient::READ_CACHE_KEY . '_secret_test')); 39 | } 40 | 41 | /** 42 | * @return CachedClient 43 | * @throws \Psr\Cache\InvalidArgumentException 44 | * @throws ClientExceptionInterface 45 | * @throws \Vault\Exceptions\RuntimeException 46 | */ 47 | private function getAuthenticatedClient(): CachedClient 48 | { 49 | $client = new CachedClient( 50 | new Uri('http://127.0.0.1:8200'), 51 | new Client(), 52 | new RequestFactory(), 53 | new StreamFactory() 54 | ); 55 | 56 | $client->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')); 57 | 58 | $this->assertEquals($client->getAuthenticationStrategy()->getClient(), $client); 59 | $this->assertTrue($client->authenticate()); 60 | $this->assertNotEmpty($client->getToken()); 61 | $this->assertNotEmpty($client->getToken()->getAuth()->getLeaseDuration()); 62 | $this->assertNotEmpty($client->getToken()->getAuth()->isRenewable()); 63 | 64 | return $client; 65 | } 66 | 67 | /** 68 | * @throws \Psr\Cache\InvalidArgumentException 69 | * @throws ClientExceptionInterface 70 | * @throws \Vault\Exceptions\RuntimeException 71 | */ 72 | public function testReadCacheKeyAlreadyInCache(): void 73 | { 74 | $client = $this->getAuthenticatedClient()->enableReadCache()->setCache(new ArrayCachePool()); 75 | $key = CachedClient::READ_CACHE_KEY . '_secret_test_2'; 76 | 77 | $cacheItem = $client->getCache()->getItem($key); 78 | 79 | $cacheItem->set(new Response(['data' => ['value' => 'test']]))->expiresAfter(10); 80 | 81 | $client->getCache()->save($cacheItem); 82 | 83 | $data = $client->read('/secret/test/2')->getData(); 84 | 85 | $this->assertArrayHasKey('value', $data); 86 | $this->assertEquals('test', $data['value']); 87 | $this->assertTrue($client->getCache()->hasItem($key)); 88 | } 89 | 90 | protected function setUp(): void 91 | { 92 | VCR::turnOn(); 93 | 94 | VCR::insertCassette('unit-client'); 95 | 96 | parent::setUp(); 97 | } 98 | 99 | protected function tearDown(): void 100 | { 101 | // To stop recording requests, eject the cassette 102 | VCR::eject(); 103 | 104 | // Turn off VCR to stop intercepting requests 105 | VCR::turnOff(); 106 | 107 | parent::tearDown(); 108 | } 109 | 110 | protected function _before() 111 | { 112 | } 113 | 114 | protected function _after() 115 | { 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/unit/ClientTest.php: -------------------------------------------------------------------------------- 1 | getAuthenticatedClient(); 37 | } 38 | 39 | /** 40 | * @return Client 41 | * @throws \Psr\Cache\InvalidArgumentException 42 | * @throws ClientExceptionInterface 43 | * @throws RuntimeException 44 | */ 45 | private function getAuthenticatedClient(): Client 46 | { 47 | $client = $this->getClient() 48 | ->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')); 49 | 50 | $this->assertEquals($client->getAuthenticationStrategy()->getClient(), $client); 51 | $this->assertTrue($client->authenticate()); 52 | $this->assertNotEmpty($client->getToken()); 53 | $this->assertNotEmpty($client->getToken()->getAuth()->getLeaseDuration()); 54 | $this->assertNotEmpty($client->getToken()->getAuth()->isRenewable()); 55 | 56 | return $client; 57 | } 58 | 59 | /** 60 | * @param ClientInterface|null $client 61 | * 62 | * @return Client 63 | */ 64 | private function getClient(?ClientInterface $client = null): Client 65 | { 66 | return new Client( 67 | new Uri('http://127.0.0.1:8200'), 68 | $client ?: new \AlexTartan\GuzzlePsr18Adapter\Client(), 69 | new RequestFactory(), 70 | new StreamFactory() 71 | ); 72 | } 73 | 74 | /** 75 | * @throws \Psr\Cache\InvalidArgumentException 76 | * @throws ClientExceptionInterface 77 | * @throws RuntimeException 78 | */ 79 | public function testWriteReadRevokeSecret(): void 80 | { 81 | $client = $this->getAuthenticatedClient(); 82 | 83 | $this->assertNotEmpty($client->write('/secret/test', ['value' => 'test'])); 84 | 85 | $data = $client->read('/secret/test')->getData(); 86 | 87 | $this->assertArrayHasKey('value', $data); 88 | $this->assertEquals('test', $data['value']); 89 | $this->assertNotEmpty($client->revoke('/secret/test')); 90 | } 91 | 92 | /** 93 | * @throws \Psr\Cache\InvalidArgumentException 94 | * @throws ClientExceptionInterface 95 | * @throws RuntimeException 96 | */ 97 | public function testWritePermissionDeniedSecret(): void 98 | { 99 | $this->expectException(RequestException::class); 100 | $this->expectExceptionCode(403); 101 | 102 | $client = $this->getAuthenticatedClient(); 103 | 104 | $client->write('/secret/test_prohibited', ['value' => 'test']); 105 | } 106 | 107 | /** 108 | * @throws \Psr\Cache\InvalidArgumentException 109 | * @throws ClientExceptionInterface 110 | * @throws RuntimeException 111 | */ 112 | public function testTokenCache(): void 113 | { 114 | $cache = new ArrayCachePool(); 115 | 116 | $client = $this->getClient() 117 | ->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')) 118 | ->setCache($cache); 119 | 120 | $this->assertTrue($client->authenticate()); 121 | 122 | $realToken = $client->getToken(); 123 | 124 | $this->assertNotEmpty($realToken); 125 | 126 | // create new client with cache 127 | $client = $this->getClient()->setCache($cache); 128 | 129 | $this->assertTrue($client->authenticate()); 130 | 131 | $tokenFromCache = $client->getToken(); 132 | 133 | $this->assertNotEmpty($tokenFromCache); 134 | $this->assertEquals($realToken, $tokenFromCache); 135 | } 136 | 137 | /** 138 | * @throws \Psr\Cache\InvalidArgumentException 139 | * @throws ClientExceptionInterface 140 | * @throws RuntimeException 141 | */ 142 | public function testTryToAuthenticateWithoutStrategy(): void 143 | { 144 | $this->expectException(DependencyException::class); 145 | 146 | $this->getClient()->authenticate(); 147 | } 148 | 149 | /** 150 | * @throws ClientExceptionInterface 151 | */ 152 | public function testServerProblems(): void 153 | { 154 | try { 155 | $client = Stub::makeEmpty(ClientInterface::class, [ 156 | 'sendRequest' => function () { 157 | throw new RequestException('', 500); 158 | }, 159 | ]); 160 | 161 | $this->getClient($client)->get(''); 162 | } catch (Exception $e) { 163 | $this->assertInstanceOf(RequestException::class, $e); 164 | } 165 | } 166 | 167 | /** 168 | * @throws ClientExceptionInterface 169 | * @throws RuntimeException 170 | * @throws Exception 171 | */ 172 | public function testReAuthentication(): void 173 | { 174 | $httpClient = Stub::makeEmpty(ClientInterface::class, [ 175 | 'sendRequest' => function (RequestInterface $request) { 176 | static $requestCounter = 0; 177 | 178 | if ($requestCounter === 0) { 179 | $requestCounter++; 180 | 181 | throw new RequestException('', 403); 182 | } 183 | 184 | return (new \AlexTartan\GuzzlePsr18Adapter\Client())->sendRequest($request); 185 | }, 186 | ]); 187 | 188 | $client = $this->getClient($httpClient) 189 | ->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')) 190 | ->setToken(new Token([ 191 | 'auth' => new Auth(['clientToken' => 123]), 192 | 'creationTtl' => (new DateTime())->getTimestamp() - 1, 193 | 'ttl' => 1, 194 | ])); 195 | 196 | $this->assertNotEmpty($client->write('/secret/test', ['value' => 'test'])); 197 | } 198 | 199 | /** 200 | * @throws ClientExceptionInterface 201 | */ 202 | public function testReAuthenticationFailure(): void 203 | { 204 | try { 205 | $httpClient = Stub::makeEmpty(ClientInterface::class, [ 206 | 'sendRequest' => function () { 207 | throw new RequestException('', 403); 208 | }, 209 | ]); 210 | 211 | $client = $this->getClient($httpClient) 212 | ->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')) 213 | ->setToken(new Token([ 214 | 'auth' => new Auth(['clientToken' => 123]), 215 | 'creationTtl' => (new DateTime())->getTimestamp() - 1, 216 | 'ttl' => 1, 217 | ])); 218 | 219 | $client->get(''); 220 | } catch (Exception $e) { 221 | $this->assertInstanceOf(AuthenticationException::class, $e); 222 | } 223 | } 224 | 225 | /** 226 | * @throws RuntimeException 227 | * @throws \Psr\Cache\InvalidArgumentException 228 | * @throws ClientExceptionInterface 229 | */ 230 | public function testTokenCacheInvalidate(): void 231 | { 232 | $cache = new ArrayCachePool(); 233 | 234 | $client = $this->getClient() 235 | ->setAuthenticationStrategy(new UserPassAuthenticationStrategy('test', 'test')) 236 | ->setCache($cache) 237 | ->setToken(new Token([ 238 | 'auth' => new Auth(['clientToken' => '123']), 239 | 'creationTime' => time(), 240 | 'creationTtl' => 300, 241 | ])); 242 | 243 | $realToken = $client->getToken(); 244 | 245 | $this->assertNotEmpty($realToken); 246 | 247 | // create new client with cache 248 | $client = $this->getClient()->setCache($cache); 249 | 250 | $tokenCacheItem = $cache->getItem(Client::TOKEN_CACHE_KEY); 251 | 252 | $tokenAsArray = $tokenCacheItem->get()->toArray(); 253 | 254 | $tokenAsArray['auth'] = new Auth($tokenAsArray['auth']); 255 | 256 | $tokenCacheItem->set(new Token(array_merge($tokenAsArray, ['creationTtl' => 0]))); 257 | 258 | $cache->save($tokenCacheItem); 259 | 260 | $this->assertTrue($client->authenticate()); 261 | 262 | $newToken = $client->getToken(); 263 | 264 | $this->assertNotEmpty($newToken); 265 | $this->assertNotEquals($realToken, $newToken); 266 | } 267 | 268 | protected function setUp(): void 269 | { 270 | VCR::turnOn(); 271 | 272 | VCR::insertCassette('unit-client'); 273 | 274 | parent::setUp(); 275 | } 276 | 277 | protected function tearDown(): void 278 | { 279 | // To stop recording requests, eject the cassette 280 | VCR::eject(); 281 | 282 | // Turn off VCR to stop intercepting requests 283 | VCR::turnOff(); 284 | 285 | parent::tearDown(); 286 | } 287 | 288 | protected function _before() 289 | { 290 | } 291 | 292 | protected function _after() 293 | { 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /tests/unit/_bootstrap.php: -------------------------------------------------------------------------------- 1 |