├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── auth └── src │ ├── it │ └── scala │ │ └── janstenpickle │ │ └── vault │ │ └── auth │ │ └── UserPassIT.scala │ └── main │ └── scala │ └── janstenpickle │ └── vault │ └── auth │ └── UserPass.scala ├── build.sbt ├── core └── src │ ├── it │ └── scala │ │ └── janstenpickle │ │ └── vault │ │ └── core │ │ ├── SecretsIT.scala │ │ └── VaultSpec.scala │ └── main │ └── scala │ └── janstenpickle │ ├── scala │ └── syntax │ │ └── syntax.scala │ └── vault │ └── core │ ├── Secrets.scala │ └── VaultConfig.scala ├── manage └── src │ ├── it │ └── scala │ │ └── janstenpickle │ │ └── vault │ │ └── manage │ │ ├── AuthIT.scala │ │ ├── MountIT.scala │ │ ├── PolicyIT.scala │ │ └── UserPassIT.scala │ ├── main │ └── scala │ │ └── janstenpickle │ │ └── vault │ │ └── manage │ │ ├── UserPass.scala │ │ └── manage.scala │ └── test │ └── scala │ └── janstenpickle │ └── vault │ └── manage │ └── RuleSpec.scala ├── project ├── build.properties └── plugins.sbt ├── scalastyle-config.xml └── scripts └── start_vault /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = lf 8 | indent_style = space 9 | 10 | [{*.scala,*.sbt}] 11 | indent_size = 2 12 | max_line_length = 80 13 | 14 | [{*.hcl,*.json,*.conf}] 15 | indent_size = 2 16 | max_line_length = 100 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################### 2 | ## Linux.gitignore 3 | ################################################### 4 | *~ 5 | # KDE directory preferences 6 | .directory 7 | 8 | ################################################### 9 | ## OSX.gitignore 10 | ################################################### 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | # Icon must end with two \r 15 | Icon 16 | # Thumbnails 17 | ._* 18 | # Files that might appear on external disk 19 | .Spotlight-V100 20 | .Trashes 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | 27 | ################################################### 28 | ## Windows.gitignore 29 | ################################################### 30 | # Windows image file caches 31 | Thumbs.db 32 | ehthumbs.db 33 | # Folder config file 34 | Desktop.ini 35 | # Recycle Bin used on file shares 36 | $RECYCLE.BIN/ 37 | # Windows Installer files 38 | *.cab 39 | *.msi 40 | *.msm 41 | *.msp 42 | 43 | #*************************************************# 44 | 45 | ################################################### 46 | ## Vim.gitignore 47 | ################################################### 48 | [._]*.s[a-w][a-z] 49 | [._]s[a-w][a-z] 50 | *.un~ 51 | Session.vim 52 | .netrwhist 53 | *~ 54 | ################################################### 55 | ## SBT.gitignore 56 | ################################################### 57 | .cache/ 58 | .history/ 59 | .lib/ 60 | dist/* 61 | target/ 62 | lib_managed/ 63 | src_managed/ 64 | project/boot/ 65 | project/plugins/project/ 66 | .history 67 | 68 | #*************************************************# 69 | 70 | ################################################### 71 | ## Scala.gitignore 72 | ################################################### 73 | *.class 74 | *.log 75 | # Scala-IDE specific 76 | .scala_dependencies 77 | .worksheet 78 | 79 | #*************************************************# 80 | 81 | ################################################### 82 | ## Project.gitignore 83 | ################################################### 84 | .idea 85 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: scala 4 | 5 | services: 6 | - docker 7 | 8 | scala: 9 | - 2.11.8 10 | 11 | jdk: oraclejdk8 12 | 13 | cache: 14 | directories: 15 | - ~/.ivy2 16 | - ~/.sbt 17 | 18 | before_install: 19 | - bash scripts/start_vault 20 | 21 | script: 22 | - sbt scalastyle 23 | - sbt test 24 | - sbt it:test 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Chris Jansen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vault Scala Library 2 | 3 | [![Build Status](https://travis-ci.org/janstenpickle/scala-vault.svg?branch=master)](https://travis-ci.org/janstenpickle/scala-vault) 4 | 5 | Scala library for working with [Hashicorp Vault](https://www.vaultproject.io/). 6 | 7 | This library has three modules: 8 | 9 | |Name|Description|Download| 10 | |---|---|---| 11 | |**Core** | Basic client capable of obtaining a token using an App ID, supports getting and setting of secrets | [![Download](https://api.bintray.com/packages/janstenpickle/maven/vault-core/images/download.svg)](https://bintray.com/janstenpickle/maven/vault-core/_latestVersion) | 12 | | **Auth** | Functions to authenticate a user using userpass authentication and token verification | [![Download](https://api.bintray.com/packages/janstenpickle/maven/vault-auth/images/download.svg)](https://bintray.com/janstenpickle/maven/vault-auth/_latestVersion)| 13 | | **Manage** | Functions for managing auth modules, mounts and policies | [![Download](https://api.bintray.com/packages/janstenpickle/maven/vault-manage/images/download.svg)](https://bintray.com/janstenpickle/maven/vault-manage/_latestVersion) | 14 | 15 | ## Install with SBT 16 | Add the following to your sbt `project/plugins.sbt` file: 17 | ```scala 18 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") 19 | ``` 20 | Then add the following to your `build.sbt` 21 | ```scala 22 | resolvers += Resolver.bintrayRepo("janstenpickle", "maven") 23 | libraryDependencies += "janstenpickle.vault" %% "vault-core" % "0.4.0" 24 | libraryDependencies += "janstenpickle.vault" %% "vault-auth" % "0.4.0" 25 | libraryDependencies += "janstenpickle.vault" %% "vault-manage" % "0.4.0" 26 | ``` 27 | ## Usage 28 | Simple setup: 29 | ```scala 30 | import java.net.URL 31 | 32 | import janstenpickle.vault.core.AppRole 33 | import janstenpickle.vault.core.VaultConfig 34 | import janstenpickle.vault.core.WSClient 35 | 36 | val config = VaultConfig(WSClient(new URL("https://localhost:8200")), "token") 37 | 38 | val appRoleConfig = VaultConfig(WSClient(new URL("https://localhost:8200")), AppRole("roleId", "secretId")) 39 | ``` 40 | ### WSClient 41 | This library uses the [Dispatch](http://dispatch.databinder.net/Dispatch.html), a lightweight async HTTP client to communicate with Vault. 42 | 43 | ### Responses 44 | All responses from Vault are wrapped in an asynchronous [Result](http://github.com/albertpastrana/uscala). This allows any errors in the response are captured separately from the failure of the underlying future. 45 | 46 | ### Reading and writing secrets 47 | ```scala 48 | import java.net.URL 49 | 50 | import janstenpickle.vault.core.AppRole 51 | import janstenpickle.vault.core.VaultConfig 52 | import janstenpickle.vault.core.WSClient 53 | import janstenpickle.vault.core.Secrets 54 | 55 | 56 | val config = VaultConfig(WSClient(new URL("https://localhost:8200")), AppRole("roleId", "secretId")) 57 | 58 | val secrets = Secrets(config, "secret") 59 | ``` 60 | #### Getting a secret 61 | ```scala 62 | val response = secrets.get("some_secret") 63 | // Unsafely evaluate the Task 64 | println(response.unsafePerformSyncAttempt) 65 | ``` 66 | #### Setting a secret 67 | ```scala 68 | val response = secrets.set("some_secret", "some_value") 69 | ``` 70 | #### Setting a secret under a different sub key 71 | ```scala 72 | val response = secrets.set("some_secret", "some_key", "some_value") 73 | ``` 74 | #### Setting a secret as a map 75 | ```scala 76 | val response = secrets.set("some_secret", Map("k1" -> "v1", "k2" -> "v2")) 77 | ``` 78 | #### Getting a map of secrets 79 | ```scala 80 | val response = secrets.getAll("some_secret") 81 | ``` 82 | #### Listing all secrets 83 | ```scala 84 | val response = secrets.list 85 | ``` 86 | 87 | ### Authenticating a username/password 88 | ```scala 89 | import java.net.URL 90 | 91 | import janstenpickle.vault.core.WSClient 92 | import janstenpickle.vault.auth.UserPass 93 | 94 | 95 | val userPass = UserPass(WSClient(new URL("https://localhost:8200"))) 96 | 97 | val ttl = 10 * 60 98 | val response = userPass.authenticate("username", "password", ttl) 99 | ``` 100 | The [response](auth/src/main/scala/janstenpickle/vault/auth/UserPass.scala#L23:L27) will contain the fields as per the [Vault documentation](https://www.vaultproject.io/docs/auth/userpass.html). 101 | 102 | #### Multitenant username/password auth 103 | This requires that `userpass` authentication has been enabled on separate path to the default of `userpass`. Instructions of how to do this are documented below. By doing this credientials for different tenants may be stored separately within Vault. 104 | ```scala 105 | val response = userPass.authenticate("username", "password", ttl, "clientId") 106 | ``` 107 | 108 | ## Managing Vault 109 | This library also provides some limited management functionality for Vault around authenctiation, mounts and policy. 110 | ### Authentication Management 111 | ```scala 112 | import java.net.URL 113 | 114 | import janstenpickle.vault.core.AppRole 115 | import janstenpickle.vault.core.VaultConfig 116 | import janstenpickle.vault.core.WSClient 117 | import janstenpickle.vault.manage.Auth 118 | 119 | 120 | val config = VaultConfig(WSClient(new URL("https://localhost:8200")), AppRole("roleId", "secretId")) 121 | 122 | val auth = Auth(config) 123 | 124 | // enable an auth backend 125 | val enable = auth.enable("auth_type") 126 | 127 | // disable an auth backend 128 | val disable = auth.disable("auth_type") 129 | ``` 130 | The enable function can also take an optional mount point and description, the mount point is useful when setting up multitenant `userpass` backend as the mount point will correspond to the client ID. 131 | ```scala 132 | val response = auth.enable("auth_type", Some("client_id"), Some("description")) 133 | ``` 134 | 135 | 136 | # Example Usage - Multitenant Authentication Service 137 | Using this library it is very simple to set up a token authentication service for ReST API authentication made up of three components: 138 | 139 | * Vault 140 | * Thin authentication endpoint 141 | * API service 142 | 143 | The sequence diagram below shows how this may be constructed: 144 | 145 | ![Auth Sequence](https://i.imgur.com/nu6Gs77.png) 146 | 147 | ### Code Examples for Authentication Service 148 | 149 | The exmaples below show how clients can be set up, users authenticated and tokens validated: 150 | 151 | #### Client Administration 152 | ```scala 153 | import janstenpickle.vault.core.VaultConfig 154 | import janstenpickle.vault.manage.Auth 155 | 156 | class ClientAuth(config: VaultConfig) { 157 | val auth = Auth(config) 158 | def create(clientId: String, clientName: String): AsyncResult[WSResponse] = auth.enable("userpass", Some(clientId), Some(clientName)) 159 | def delete(clientId: String): AsyncResult[WSResponse] = auth.disable(clientId) 160 | } 161 | ``` 162 | #### User Administration 163 | ```scala 164 | import janstenpickle.vault.core.VaultConfig 165 | import janstenpickle.vault.manage.UserPass 166 | 167 | class UserAdmin(config: VaultConfig, ttl: Int) { 168 | val userPass = UserPass(config) 169 | def create(username: String, password: String, clientId: String, policies: Option[List[String]] = None): AsyncResult[WSResponse] = 170 | userPass.create(username, password, ttl, policies, clientId) 171 | def setPassword(username: String, password: String, clientId: String): AsyncResult[WSResponse] = 172 | userPass.setPassword(username, password, clientId) 173 | def setPolicies(username: String, policies: List[String], clientId: String): AsyncResult[WSResponse] = 174 | userPass.setPolicies(username, policies, clientId) 175 | def delete(username: String, clientId: String): AsyncResult[WSResponse] = userPass.delete(username, clientId) 176 | } 177 | ``` 178 | #### User Authentication 179 | ```scala 180 | import janstenpickle.vault.core.WSClient 181 | import janstenpickle.vault.manage.Auth 182 | 183 | class UserAuth(wsClient: WSClient, ttl: Int) { 184 | val userPass = UserPass(wsClient) 185 | 186 | // returns only the token 187 | def auth(username: String, password: String, clientId: String): AsyncResult[String] = 188 | userPass.authenticate(username, password, ttl, clientId).map(_.client_token) 189 | } 190 | ``` 191 | 192 | ## Develop `scala-vault` 193 | 194 | ### Testing 195 | 196 | `sbt clean startVaultTask coverage test it:test coverageReport` 197 | -------------------------------------------------------------------------------- /auth/src/it/scala/janstenpickle/vault/auth/UserPassIT.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.auth 2 | 3 | import janstenpickle.vault.core.VaultSpec 4 | import janstenpickle.vault.manage.Auth 5 | import org.scalacheck.{Gen, Prop} 6 | import org.specs2.ScalaCheck 7 | import org.specs2.matcher.MatchResult 8 | 9 | class UserPassIT extends VaultSpec with ScalaCheck { 10 | import VaultSpec._ 11 | 12 | override def is = 13 | s2""" 14 | Can authenticate a user against a specific "client" path $authPass 15 | Fails to authenticate a user $end 16 | against a bad "client" path $badClient 17 | with a non-existent username $badUser 18 | with a bad password $badPassword 19 | """ 20 | 21 | lazy val underTest = UserPass(config.wsClient) 22 | lazy val authAdmin = Auth(config) 23 | lazy val userAdmin = janstenpickle.vault.manage.UserPass(config) 24 | 25 | def setupClient(client: String) = authAdmin.enable("userpass", Some(client)) 26 | .attemptRun(_.getMessage()) must beOk 27 | 28 | def setupUser(username: String, password: String, client: String) = 29 | userAdmin.create(username, password, 30, None, client) 30 | .attemptRun(_.getMessage()) 31 | 32 | def removeClient(client: String) = 33 | authAdmin.disable(client).attemptRun(_.getMessage()) must beOk 34 | 35 | def authPass = test((username, password, client, ttl) => 36 | setupClient(client) and 37 | (setupUser(username, password, client) must beOk) and 38 | (underTest.authenticate(username, password, ttl, client) 39 | .attemptRun(_.getMessage()) must beOk) and 40 | removeClient(client) 41 | ) 42 | 43 | // TODO: test below may fail rarely (e.g. client is same as badClientName) 44 | 45 | def badClient = test{ (username, password, client, ttl) => 46 | val badClientName = "nic-kim-cage-client" 47 | setupClient(badClientName) and 48 | (setupUser(username, password, client) must beFail) and 49 | (underTest.authenticate(username, password, ttl, client) 50 | .attemptRun(_.getMessage()) must beFail) and 51 | removeClient(badClientName) 52 | } 53 | 54 | def badUser = test{ (username, password, client, ttl) => 55 | val badUserName = "nic-kim-cage-user" 56 | setupClient(client) and 57 | (setupUser(username, password, client) must beOk) and 58 | (underTest.authenticate(badUserName, password, ttl, client) 59 | .attemptRun(_.getMessage()) must beFail) and 60 | removeClient(client) 61 | } 62 | 63 | def badPassword = test{ (username, password, client, ttl) => 64 | val badPasswordValue = "nic-kim-cage-password" 65 | setupClient(client) and 66 | (setupUser(username, password, client) must beOk) and 67 | (underTest.authenticate(username, badPasswordValue, ttl, client) 68 | .attemptRun(_.getMessage()) must beFail) and 69 | removeClient(client) 70 | } 71 | 72 | def test(op: (String, String, String, Int) => MatchResult[Any]) = 73 | Prop.forAllNoShrink( 74 | longerStrGen, 75 | longerStrGen, 76 | Gen.numStr.suchThat(_.nonEmpty), Gen.posNum[Int] 77 | )(op) 78 | } 79 | -------------------------------------------------------------------------------- /auth/src/main/scala/janstenpickle/vault/auth/UserPass.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.auth 2 | 3 | import io.circe.generic.auto._ 4 | import janstenpickle.scala.syntax.AsyncResultSyntax._ 5 | import janstenpickle.scala.syntax.SyntaxRequest._ 6 | import janstenpickle.scala.syntax.ResponseSyntax._ 7 | import janstenpickle.vault.core.WSClient 8 | import uscala.concurrent.result.AsyncResult 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | case class UserPass(wsClient: WSClient) { 13 | 14 | def authenticate( 15 | username: String, 16 | password: String, 17 | ttl: Int, 18 | client: String = "userpass" 19 | )(implicit ec: ExecutionContext): AsyncResult[String, UserPassResponse] = 20 | wsClient.path(s"auth/$client/login/$username"). 21 | post(Map("password" -> password, "ttl" -> s"${ttl}s")). 22 | toAsyncResult. 23 | // scalastyle:off magic.number 24 | acceptStatusCodes(200). 25 | // scalastyle:on magic.number 26 | extractFromJson[UserPassResponse](_.downField("auth")) 27 | } 28 | 29 | case class UserPassResponse( 30 | client_token: String, 31 | policies: List[String], 32 | metadata: Option[Map[String, String]], 33 | lease_duration: Int, 34 | renewable: Boolean 35 | ) 36 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | 3 | name := "vault" 4 | 5 | lazy val uscalaVersion = "0.5.1" 6 | lazy val specs2Version = "3.8.8" 7 | lazy val circeVersion = "0.7.0" 8 | lazy val dispatchVersion = "0.11.3" 9 | lazy val startVaultTask = TaskKey[Unit]( 10 | "startVaultTask", 11 | "Start dev vault server for integration test" 12 | ) 13 | // start vault settings 14 | startVaultTask := { 15 | import sys.process._ 16 | "./scripts/start_vault" ! 17 | } 18 | lazy val checkStyleBeforeCompile = TaskKey[Unit]( 19 | "checkStyleBeforeCompile", 20 | "Check style before compile" 21 | ) 22 | 23 | val pomInfo = ( 24 | https://github.com/janstenpickle/scala-vault 25 | 26 | git@github.com:janstenpickle/scala-vault.git 27 | 28 | scm:git:git@github.com:janstenpickle/scala-vault.git 29 | 30 | 31 | 32 | 33 | janstepickle 34 | Chris Jansen 35 | 36 | 37 | ) 38 | 39 | lazy val commonSettings = Seq( 40 | version := "0.4.2-SNAPSHOT", 41 | scalaVersion := "2.11.11", 42 | organization := "janstenpickle.vault", 43 | pomExtra := pomInfo, 44 | autoAPIMappings := true, 45 | publishArtifact in Test := false, 46 | pomIncludeRepository := { _ => false }, 47 | bintrayReleaseOnPublish := false, 48 | licenses += ( 49 | "MIT", 50 | url("https://github.com/janstenpickle/scala-vault/blob/master/LICENSE") 51 | ), 52 | resolvers ++= Seq(Resolver.sonatypeRepo("releases"), Resolver.jcenterRepo), 53 | libraryDependencies ++= Seq( 54 | "net.databinder.dispatch" %% "dispatch-core" % dispatchVersion, 55 | "org.uscala" %% "uscala-result" % uscalaVersion, 56 | "org.uscala" %% "uscala-result-async" % uscalaVersion, 57 | "org.uscala" %% "uscala-result-specs2" % uscalaVersion % "it,test", 58 | "org.specs2" %% "specs2-core" % specs2Version % "it,test", 59 | "org.specs2" %% "specs2-scalacheck" % specs2Version % "it,test", 60 | "org.specs2" %% "specs2-junit" % specs2Version % "it,test" 61 | ), 62 | libraryDependencies ++= Seq( 63 | "io.circe" %% "circe-core", 64 | "io.circe" %% "circe-generic", 65 | "io.circe" %% "circe-parser" 66 | ).map(_ % circeVersion), 67 | scalacOptions in Test ++= Seq( 68 | "-Yrangepos", 69 | "-Xlint", 70 | "-deprecation", 71 | "-Xfatal-warnings" 72 | ), 73 | scalacOptions ++= Seq( 74 | "-Xlint", 75 | "-Xcheckinit", 76 | "-Xfatal-warnings", 77 | "-unchecked", 78 | "-deprecation", 79 | "-feature", 80 | "-language:implicitConversions"), 81 | javacOptions in Compile ++= Seq( 82 | "-source", "1.8", 83 | "-target", "1.8", 84 | "-Xlint:all" 85 | ), 86 | testOptions in IntegrationTest ++= Seq( 87 | Tests.Argument("junitxml"), 88 | Tests.Argument("console") 89 | ), 90 | unmanagedSourceDirectories in IntegrationTest += baseDirectory.value / 91 | "test" / "scala", 92 | // check style settings 93 | checkStyleBeforeCompile := 94 | org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Compile).toTask("").value, 95 | (compile in Compile) := ( 96 | (compile in Compile) dependsOn 97 | checkStyleBeforeCompile 98 | ).value 99 | ) ++ Defaults.itSettings 100 | 101 | lazy val core = (project in file("core")). 102 | settings(name := "vault-core"). 103 | settings(commonSettings: _*). 104 | configs(IntegrationTest) 105 | lazy val manage = (project in file("manage")). 106 | settings(name := "vault-manage"). 107 | settings(commonSettings: _*). 108 | configs(IntegrationTest). 109 | dependsOn(core % "compile->compile;it->it,it->test") 110 | lazy val auth = (project in file("auth")). 111 | settings(name := "vault-auth"). 112 | settings(commonSettings: _*). 113 | configs(IntegrationTest). 114 | dependsOn(core % "compile->compile;it->it", manage % "it->compile") 115 | -------------------------------------------------------------------------------- /core/src/it/scala/janstenpickle/vault/core/SecretsIT.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.core 2 | 3 | import org.scalacheck.Prop 4 | import org.specs2.ScalaCheck 5 | 6 | class GenericIT extends SecretsTests { 7 | override def backend: String = "secret" 8 | } 9 | 10 | class CubbyHoleIT extends SecretsTests { 11 | override def backend: String = "cubbyhole" 12 | } 13 | 14 | trait SecretsTests extends VaultSpec with ScalaCheck { 15 | import VaultSpec._ 16 | 17 | override def is = 18 | s2""" 19 | Can set a secret in vault $set 20 | Can set and get a secret in vault $get 21 | Can list keys $list 22 | Can set multiple subkeys $setMulti 23 | Can set and get multiple subKeys $getSetMulti 24 | Cannot get non-existent key $failGet 25 | Fails to perform actions on a non-vault server $failSetBadServer 26 | Fails to perform actions with a bad token $failSetBadToken 27 | """ 28 | 29 | def backend: String 30 | 31 | lazy val good = Secrets(config, backend) 32 | lazy val badToken = Secrets(badTokenConfig, backend) 33 | lazy val badServer = Secrets(badServerConfig, backend) 34 | 35 | def set = Prop.forAllNoShrink(strGen, strGen) { (key, value) => 36 | good.set(key, value).attemptRun(_.getMessage()) must beOk 37 | } 38 | 39 | def get = Prop.forAllNoShrink(strGen, strGen) { (key, value) => 40 | (good.set(key, value).attemptRun(_.getMessage()) must beOk) and 41 | (good.get(key).attemptRun(_.getMessage()) must beOk.like { 42 | case a => a === value 43 | }) 44 | } 45 | 46 | def list = Prop.forAllNoShrink(strGen, strGen, strGen) { (key1, key2, value) => 47 | (good.set(key1, value).attemptRun(_.getMessage()) must beOk) and 48 | (good.set(key2, value).attemptRun(_.getMessage()) must beOk) and 49 | (good.list.attemptRun(_.getMessage()) must beOk[List[String]].like { 50 | case a => a must containAllOf(Seq(key1, key2)) 51 | }) 52 | } 53 | 54 | def setMulti = Prop.forAllNoShrink(strGen, strGen, strGen, strGen) { 55 | (key1, key2, value1, value2) => 56 | good.set( 57 | "nicolas-cage", 58 | Map(key1 -> value1, key2 -> value2) 59 | ).attemptRun(_.getMessage()) must beOk 60 | } 61 | 62 | def getSetMulti = Prop.forAllNoShrink( 63 | strGen, strGen, strGen, strGen, strGen 64 | ) { (key1, key2, value1, value2, mainKey) => 65 | val testData = Map(key1 -> value1, key2 -> value2) 66 | (good.set(mainKey, testData).attemptRun(_.getMessage()) must beOk) and 67 | (good.getAll(mainKey).attemptRun(_.getMessage()) must beOk.like { 68 | case a => a === testData 69 | }) 70 | } 71 | 72 | def failGet = good.get("john").attemptRun(_.getMessage()) must beFail. 73 | like { case err => 74 | err must contain("Received failure response from server: 404") 75 | } 76 | 77 | def failSetBadServer = badServer.set( 78 | "nic", "cage" 79 | ).attemptRun(_.getMessage()) must beFail 80 | 81 | def failSetBadToken = badToken.set( 82 | "nic", "cage" 83 | ).attemptRun(_.getMessage()) must beFail 84 | } 85 | -------------------------------------------------------------------------------- /core/src/it/scala/janstenpickle/vault/core/VaultSpec.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.core 2 | 3 | import java.net.URL 4 | 5 | import janstenpickle.scala.syntax.SyntaxRequest._ 6 | import janstenpickle.scala.syntax.ResponseSyntax._ 7 | import janstenpickle.scala.syntax.VaultConfigSyntax._ 8 | import org.scalacheck.Gen 9 | import org.specs2.Specification 10 | import org.specs2.specification.core.Fragments 11 | import uscala.result.specs2.ResultMatchers 12 | 13 | import scala.concurrent.ExecutionContext 14 | import scala.io.Source 15 | 16 | 17 | trait VaultSpec extends Specification with ResultMatchers { 18 | implicit val errConverter: Throwable => String = _.getMessage 19 | implicit val ec: ExecutionContext = ExecutionContext.global 20 | 21 | lazy val rootToken = Source.fromFile("/tmp/.root-token").mkString.trim 22 | lazy val roleId = Source.fromFile("/tmp/.role-id").mkString.trim 23 | lazy val secretId = Source.fromFile("/tmp/.secret-id").mkString.trim 24 | 25 | lazy val rootConfig: VaultConfig = VaultConfig( 26 | WSClient(new URL("http://localhost:8200")), rootToken 27 | ) 28 | lazy val badTokenConfig = VaultConfig( 29 | rootConfig.wsClient, 30 | "face-off" 31 | ) 32 | lazy val config = VaultConfig( 33 | rootConfig.wsClient, 34 | AppRole(roleId, secretId) 35 | ) 36 | lazy val badServerConfig = VaultConfig( 37 | WSClient(new URL("http://nic-cage.xyz")), 38 | "con-air" 39 | ) 40 | 41 | def check = config.token.attemptRun(_.getMessage) must beOk 42 | 43 | override def map(fs: => Fragments) = 44 | s2""" 45 | Can receive a token for an AppRole $check 46 | """ ^ 47 | fs 48 | } 49 | 50 | object VaultSpec { 51 | val longerStrGen = Gen.alphaStr.suchThat(_.length >= 3) 52 | val strGen = Gen.alphaStr.suchThat(_.nonEmpty) 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/scala/janstenpickle/scala/syntax/syntax.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.scala.syntax 2 | 3 | import com.ning.http.client.Response 4 | import dispatch.{Http, Req} 5 | import io.circe._ 6 | import io.circe.parser._ 7 | import io.circe.syntax._ 8 | import janstenpickle.vault.core.VaultConfig 9 | import uscala.concurrent.result.AsyncResult 10 | import uscala.result.Result 11 | 12 | import scala.concurrent.{ExecutionContext, Future} 13 | 14 | object ConversionSyntax { 15 | implicit def toResult[A, B](xor: Either[A, B]): Result[A, B] = xor.fold( 16 | Result.fail, Result.ok 17 | ) 18 | } 19 | 20 | object OptionSyntax { 21 | implicit class ToTuple[T](opt: Option[T]) { 22 | def toMap(key: String): Map[String, T] = 23 | opt.fold[Map[String, T]](Map.empty)(v => Map(key -> v)) 24 | } 25 | } 26 | 27 | object AsyncResultSyntax { 28 | implicit class FutureToAsyncResult[T](future: Future[T]) 29 | (implicit ec: ExecutionContext) { 30 | def toAsyncResult: AsyncResult[String, T] = AsyncResult( 31 | future.map(Result.ok) 32 | ) 33 | } 34 | 35 | implicit class ReqToAsyncResult(req: Req) 36 | (implicit ec: ExecutionContext) { 37 | def toAsyncResult: AsyncResult[String, Response] = Http(req).toAsyncResult 38 | } 39 | 40 | implicit def toAsyncResult[T](future: scala.concurrent.Future[T]) 41 | (implicit ec: ExecutionContext): AsyncResult[String, T] = 42 | future.toAsyncResult 43 | } 44 | 45 | object VaultConfigSyntax { 46 | 47 | final val VaultTokenHeader = "X-Vault-Token" 48 | 49 | implicit class RequestHelper(config: VaultConfig) { 50 | def authenticatedRequest(path: String)(req: Req => Req) 51 | (implicit ec: ExecutionContext): AsyncResult[String, Req] = 52 | config.token.map[Req](token => 53 | req(config.wsClient.path(path).setHeader(VaultTokenHeader, token)) 54 | ) 55 | } 56 | } 57 | 58 | object JsonSyntax { 59 | import ConversionSyntax._ 60 | 61 | implicit class JsonHandler(json: AsyncResult[String, Json]) { 62 | def extractFromJson[T](jsonPath: HCursor => ACursor = _.downArray) 63 | ( 64 | implicit decode: Decoder[T], 65 | ec: ExecutionContext 66 | ): AsyncResult[String, T] = 67 | json.flatMapR(j => decode.tryDecode( 68 | jsonPath(j.hcursor) 69 | ).leftMap(_.message)) 70 | } 71 | } 72 | 73 | object ResponseSyntax { 74 | import ConversionSyntax._ 75 | import JsonSyntax._ 76 | 77 | implicit class ResponseHandler(resp: AsyncResult[String, Response]) { 78 | def acceptStatusCodes(codes: Int*) 79 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 80 | resp.flatMapR( 81 | response => 82 | if (codes.contains(response.getStatusCode)) { 83 | Result.ok(response) 84 | } 85 | else { 86 | Result.fail( 87 | s"Received failure response from server:" + 88 | s" ${response.getStatusCode}\n ${response.getResponseBody}" 89 | ) 90 | } 91 | ) 92 | 93 | def extractJson(implicit ec: ExecutionContext): AsyncResult[String, Json] = 94 | resp.flatMapR(response => 95 | parse(response.getResponseBody).leftMap(_.message) 96 | ) 97 | 98 | def extractFromJson[T](jsonPath: HCursor => ACursor = _.downArray) 99 | ( 100 | implicit decode: Decoder[T], 101 | ec: ExecutionContext 102 | ): AsyncResult[String, T] = 103 | resp.extractJson.extractFromJson[T](jsonPath) 104 | } 105 | } 106 | 107 | object SyntaxRequest { 108 | 109 | implicit class ExecuteRequest(req: AsyncResult[String, Req]) 110 | (implicit ec: ExecutionContext) { 111 | def execute: AsyncResult[String, Response] = 112 | req.flatMapF(Http(_)) 113 | } 114 | 115 | implicit class HttpOps(req: Req) { 116 | def get: Req = req.GET 117 | def post(body: String): Req = req.setBody(body).POST 118 | def post(json: Json): Req = post(json.noSpaces) 119 | def post(map: Map[String, String]): Req = post(map.asJson) 120 | def delete: Req = req.DELETE 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /core/src/main/scala/janstenpickle/vault/core/Secrets.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.core 2 | 3 | import com.ning.http.client.Response 4 | import janstenpickle.scala.syntax.SyntaxRequest._ 5 | import janstenpickle.scala.syntax.ResponseSyntax._ 6 | import janstenpickle.scala.syntax.VaultConfigSyntax._ 7 | import uscala.concurrent.result.AsyncResult 8 | import uscala.result.Result 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | // scalastyle:off magic.number 13 | case class Secrets(config: VaultConfig, backend: String) { 14 | def get(key: String, subKey: String = "value") 15 | (implicit ec: ExecutionContext): AsyncResult[String, String] = 16 | getAll(key).flatMapR(x => 17 | Result.fromOption(x.get(subKey), 18 | s"Cannot find sub-key $subKey in secret $key")) 19 | 20 | def getAll(key: String) 21 | (implicit ec: ExecutionContext): AsyncResult[String, Map[String, String]] = 22 | config.authenticatedRequest(path(key))(_.get). 23 | execute. 24 | acceptStatusCodes(200). 25 | extractFromJson[Map[String, String]](_.downField("data")) 26 | 27 | def set(key: String, value: String) 28 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 29 | set(key, "value", value) 30 | 31 | def set(key: String, subKey: String, value: String) 32 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 33 | set(key, Map(subKey -> value)) 34 | 35 | def set(key: String, values: Map[String, String]) 36 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 37 | config.authenticatedRequest(path(key))(_.post(values)). 38 | execute. 39 | acceptStatusCodes(204) 40 | 41 | def list(implicit ec: ExecutionContext): AsyncResult[String, List[String]] = 42 | config.authenticatedRequest(backend)( 43 | _.addQueryParameter("list", true.toString).get). 44 | execute. 45 | acceptStatusCodes(200). 46 | extractFromJson[List[String]](_.downField("data").downField("keys")) 47 | 48 | def path(key: String): String = s"$backend/$key" 49 | } 50 | // scalastyle:on magic.number 51 | -------------------------------------------------------------------------------- /core/src/main/scala/janstenpickle/vault/core/VaultConfig.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.core 2 | 3 | import java.net.URL 4 | 5 | import dispatch.{Req, url} 6 | import io.circe.generic.auto._ 7 | import io.circe.syntax._ 8 | import janstenpickle.scala.syntax.AsyncResultSyntax._ 9 | import janstenpickle.scala.syntax.SyntaxRequest._ 10 | import janstenpickle.scala.syntax.ResponseSyntax._ 11 | import uscala.concurrent.result.AsyncResult 12 | 13 | import scala.concurrent.ExecutionContext 14 | 15 | case class VaultConfig(wsClient: WSClient, token: AsyncResult[String, String]) 16 | @deprecated("Vault 0.6.5 deprecated AppId in favor of AppRole", "0.4.0") 17 | case class AppId(app_id: String, user_id: String) 18 | case class AppRole(role_id: String, secret_id: String) 19 | 20 | object VaultConfig { 21 | 22 | @deprecated("Vault 0.6.5 deprecated AppId in favor of AppRole", "0.4.0") 23 | def apply(client: WSClient, appId: AppId) 24 | (implicit ec: ExecutionContext): VaultConfig = 25 | VaultConfig(client, 26 | client.path("auth/app-id/login"). 27 | post(appId.asJson). 28 | toAsyncResult. 29 | // scalastyle:off magic.number 30 | acceptStatusCodes(200). 31 | // scalastyle:on magic.number 32 | extractFromJson[String]( 33 | _.downField("auth").downField("client_token") 34 | ) 35 | ) 36 | 37 | def apply(client: WSClient, appRole: AppRole) 38 | (implicit ec: ExecutionContext): VaultConfig = 39 | VaultConfig(client, 40 | client.path("auth/approle/login"). 41 | post(appRole.asJson). 42 | toAsyncResult. 43 | // scalastyle:off magic.number 44 | acceptStatusCodes(200). 45 | // scalastyle:on magic.number 46 | extractFromJson[String]( 47 | _.downField("auth").downField("client_token") 48 | ) 49 | ) 50 | 51 | def apply(wsClient: WSClient, token: String) 52 | (implicit ec: ExecutionContext): VaultConfig = 53 | VaultConfig(wsClient, AsyncResult.ok[String, String](token)) 54 | } 55 | 56 | 57 | 58 | case class WSClient(server: URL, 59 | version: String = "v1") { 60 | def path(p: String): Req = 61 | url(s"${server.toString}/$version/$p"). 62 | setContentType("application/json", "UTF-8") 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /manage/src/it/scala/janstenpickle/vault/manage/AuthIT.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import janstenpickle.vault.core.VaultSpec 4 | import org.scalacheck.{Prop, Gen} 5 | import org.specs2.ScalaCheck 6 | 7 | class AuthIT extends VaultSpec with ScalaCheck { 8 | import AuthIT._ 9 | import VaultSpec._ 10 | 11 | def is = 12 | s2""" 13 | Can enable and disable valid auth mount $happy 14 | Cannot enable an invalid auth type $enableFail 15 | """ 16 | 17 | lazy val underTest = new Auth(config) 18 | 19 | def happy = Prop.forAllNoShrink( 20 | backends, longerStrGen, Gen.option(longerStrGen))((backend, mount, desc) => 21 | (underTest.enable(backend, Some(mount), desc) 22 | .attemptRun(_.getMessage()) must beOk) and 23 | (underTest.disable(mount).attemptRun(_.getMessage()) must beOk) 24 | ) 25 | 26 | def enableFail = Prop.forAllNoShrink( 27 | longerStrGen.suchThat(!backendNames.contains(_)), 28 | longerStrGen, 29 | Gen.option(longerStrGen))((backend, mount, desc) => 30 | underTest.enable(mount).attemptRun(_.getMessage()) must beFail 31 | ) 32 | 33 | } 34 | 35 | object AuthIT { 36 | val backendNames = List("github", "app-id", "ldap", "userpass") 37 | val backends = Gen.oneOf(backendNames) 38 | } 39 | -------------------------------------------------------------------------------- /manage/src/it/scala/janstenpickle/vault/manage/MountIT.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import com.ning.http.client.Response 4 | import com.ning.http.client.providers.jdk.JDKResponse 5 | import janstenpickle.vault.core.VaultSpec 6 | import janstenpickle.vault.manage.Model.{Mount, MountConfig} 7 | import org.scalacheck.{Gen, Prop} 8 | import org.specs2.ScalaCheck 9 | import uscala.result.Result 10 | 11 | class MountIT extends VaultSpec with ScalaCheck { 12 | import MountIT._ 13 | import VaultSpec._ 14 | 15 | def is = 16 | s2""" 17 | Can enable, remount and disable a valid mount $happy 18 | Can enable, list and then disable valid mounts $listSuccess 19 | Cannot enable an invalid mount type $enableFail 20 | """ 21 | 22 | lazy val underTest = new Mounts(config) 23 | 24 | def happy = Prop.forAllNoShrink( 25 | mountGen, 26 | longerStrGen, 27 | longerStrGen, 28 | Gen.option(longerStrGen))((mount, mountPoint, remountPoint, desc) => { 29 | (underTest.mount(mount.`type`, Some(mountPoint), desc, Some(mount)) 30 | .attemptRun(_.getMessage()) must beOk) and 31 | (underTest.remount(mountPoint, remountPoint) 32 | .attemptRun(_.getMessage()) must beOk) and 33 | (underTest.delete(remountPoint) 34 | .attemptRun(_.getMessage()) must beOk) and 35 | (underTest.delete(mountPoint) 36 | .attemptRun(_.getMessage()) must beOk) 37 | }) 38 | 39 | def listSuccess = (processMountTypes((acc, mount) => 40 | acc.flatMap(_ => underTest.mount(mount) 41 | .attemptRun(_.getMessage()))) must beOk) and 42 | (underTest.list.attemptRun(_.getMessage()) must beOk.like { 43 | case a => a.map(_._2.`type`) must containAllOf(mountTypes) 44 | }) and 45 | (processMountTypes((acc, mount) => 46 | acc.flatMap(_ => 47 | underTest.delete(mount).attemptRun(_.getMessage())) 48 | ) must beOk) 49 | 50 | def enableFail = Prop.forAllNoShrink( 51 | longerStrGen.suchThat(!mountTypes.contains(_)), 52 | longerStrGen, 53 | Gen.option(longerStrGen))((`type`, mount, desc) => 54 | underTest.mount(`type`, Some(mount), desc) 55 | .attemptRun(_.getMessage()) must beFail 56 | ) 57 | 58 | } 59 | 60 | object MountIT { 61 | import VaultSpec._ 62 | 63 | val mountTypes = List( 64 | "aws", "cassandra", "consul", "generic", 65 | "mssql", "mysql", "pki", "postgresql", "ssh", "transit" 66 | ) 67 | val mount = Gen.oneOf(mountTypes) 68 | val mounts = Gen.listOf(mountTypes).suchThat(_.nonEmpty) 69 | 70 | val mountGen = for { 71 | mountType <- mount 72 | description <- Gen.option(longerStrGen) 73 | defaultTtl <- Gen.option(Gen.posNum[Int]) 74 | maxTtl <- Gen.option(Gen.posNum[Int]) 75 | forceNoCache <- Gen.option(Gen.oneOf(true, false)) 76 | } yield Mount(mountType, description, Some(MountConfig(defaultTtl, maxTtl, forceNoCache))) 77 | 78 | def processMountTypes(op: (Result[String, Response], String) => Result[String, 79 | Response]) = 80 | mountTypes.foldLeft[Result[String, Response]](Result.ok(new 81 | JDKResponse(null, null, null)))(op) 82 | 83 | } 84 | -------------------------------------------------------------------------------- /manage/src/it/scala/janstenpickle/vault/manage/PolicyIT.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import janstenpickle.vault.core.VaultSpec 4 | import janstenpickle.vault.manage.Model.Rule 5 | import org.scalacheck.{Gen, Prop} 6 | import org.specs2.ScalaCheck 7 | import uscala.result.Result 8 | 9 | class PolicyIT extends VaultSpec with ScalaCheck { 10 | import PolicyIT._ 11 | import VaultSpec._ 12 | 13 | override def is = 14 | s2""" 15 | Can successfully set and get policies $happy 16 | Cannot set an invalid policy $sad 17 | """ 18 | 19 | lazy val underTest = Policy(config) 20 | 21 | def happy = Prop.forAllNoShrink( 22 | longerStrGen, 23 | Gen.listOf(ruleGen(longerStrGen, policyGen, capabilitiesGen)). 24 | suchThat(_.nonEmpty)) { (name, rules) => 25 | (underTest.set(name.toLowerCase, rules) 26 | .attemptRun(_.getMessage()) must beOk) and 27 | (underTest.inspect(name.toLowerCase) 28 | .attemptRun(_.getMessage()) must beOk) and 29 | (underTest.delete(name.toLowerCase).attemptRun(_.getMessage()) must beOk) 30 | } 31 | 32 | // cannot use generated values here as 33 | // vault seems to have a failure rate limit 34 | def sad = underTest.set( 35 | "nic", List(Rule("cage", Some(List("kim", "copolla")))) 36 | ).attemptRun(_.getMessage()) must beFail 37 | } 38 | 39 | object PolicyIT { 40 | val policyGen = Gen.option(Gen.oneOf("read", "write", "sudo", "deny")) 41 | val capabilitiesGen = 42 | Gen.listOf(Gen.oneOf( 43 | "create", "read", "update", "delete", "list", "sudo", "deny")). 44 | suchThat(_.nonEmpty). 45 | map(_.distinct) 46 | 47 | def ruleGen( 48 | pathGen: Gen[String], 49 | polGen: Gen[Option[String]], 50 | capGen: Gen[List[String]] 51 | ) = for { 52 | path <- pathGen 53 | policy <- polGen 54 | capabilities <- capGen 55 | } yield Rule(path, Some(capabilities), policy) 56 | } 57 | 58 | -------------------------------------------------------------------------------- /manage/src/it/scala/janstenpickle/vault/manage/UserPassIT.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import janstenpickle.vault.core.VaultSpec 4 | import org.scalacheck.{Gen, Prop} 5 | import org.specs2.ScalaCheck 6 | 7 | class UserPassIT extends VaultSpec with ScalaCheck { 8 | import UserPassIT._ 9 | import VaultSpec._ 10 | 11 | def is = 12 | s2""" 13 | Can create, update and delete a user $good 14 | Cannot create a user for a non-existent client $badClient 15 | Cannot create user with a bad policy $badPolicy 16 | """ 17 | 18 | lazy val underTest = UserPass(config) 19 | lazy val authAdmin = Auth(config) 20 | 21 | def good = Prop.forAllNoShrink(longerStrGen, longerStrGen, longerStrGen, Gen.posNum[Int], longerStrGen, policyGen)( 22 | (username, password, newPassword, ttl, client, policy) => 23 | (authAdmin.enable("userpass", Some(client)).attemptRun(_.getMessage()) must beOk) and 24 | (underTest.create(username, password, ttl, None, client).attemptRun(_.getMessage()) must beOk) and 25 | (underTest.setPassword(username, newPassword, client).attemptRun(_.getMessage()) must beOk) and 26 | (underTest.setPolicies(username, policy, client).attemptRun(_.getMessage()) must beOk) and 27 | (underTest.delete(username, client).attemptRun(_.getMessage()) must beOk) and 28 | (authAdmin.disable(client).attemptRun(_.getMessage()) must beOk) 29 | ) 30 | 31 | def badClient = Prop.forAllNoShrink(longerStrGen, longerStrGen, Gen.posNum[Int], longerStrGen)( 32 | (username, password, ttl, client) => 33 | underTest.create(username, password, ttl, None, client).attemptRun(_.getMessage()) must beFail 34 | ) 35 | 36 | def badPolicy = Prop.forAllNoShrink(longerStrGen, 37 | longerStrGen, 38 | Gen.posNum[Int], 39 | longerStrGen, 40 | Gen.listOf(longerStrGen.suchThat(!policies.contains(_))))( 41 | (username, password, ttl, client, policy) => 42 | (authAdmin.enable("userpass", Some(client)).attemptRun(_.getMessage()) must beOk) and 43 | (underTest.create(username, password, ttl, Some(policy), client).attemptRun(_.getMessage()) must beOk) and 44 | (authAdmin.disable(client).attemptRun(_.getMessage()) must beOk) 45 | ) 46 | } 47 | 48 | object UserPassIT { 49 | val policies = List("default", "root") 50 | val policyGen = Gen.listOf(Gen.oneOf(policies)) 51 | } -------------------------------------------------------------------------------- /manage/src/main/scala/janstenpickle/vault/manage/UserPass.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import com.ning.http.client.Response 4 | import janstenpickle.scala.syntax.OptionSyntax._ 5 | import janstenpickle.scala.syntax.SyntaxRequest._ 6 | import janstenpickle.scala.syntax.ResponseSyntax._ 7 | import janstenpickle.scala.syntax.VaultConfigSyntax._ 8 | import janstenpickle.vault.core.VaultConfig 9 | import uscala.concurrent.result.AsyncResult 10 | 11 | import scala.concurrent.ExecutionContext 12 | 13 | // scalastyle:off magic.number 14 | case class UserPass(config: VaultConfig) { 15 | final val DefaultClient = "userpass" 16 | def create(username: String, 17 | password: String, 18 | ttl: Int, 19 | policies: Option[List[String]] = None, 20 | client: String = DefaultClient) 21 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 22 | config.authenticatedRequest(s"auth/$client/users/$username")( 23 | _.post(policies.map(_.mkString(",")).toMap("policies") ++ 24 | Map("username" -> username, 25 | "password" -> password, 26 | "ttl" -> s"${ttl}s")) 27 | ).execute.acceptStatusCodes(204) 28 | 29 | def delete(username: String, client: String = DefaultClient) 30 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 31 | config.authenticatedRequest(s"auth/$client/users/$username")(_.delete). 32 | execute. 33 | acceptStatusCodes(204) 34 | 35 | def setPassword( 36 | username: String, 37 | password: String, 38 | client: String = DefaultClient 39 | )(implicit ec: ExecutionContext): AsyncResult[String, Response] = 40 | config.authenticatedRequest(s"auth/$client/users/$username/password")( 41 | _.post(Map("username" -> username, "password" -> password)) 42 | ).execute.acceptStatusCodes(204) 43 | 44 | def setPolicies( 45 | username: String, 46 | policies: List[String], 47 | client: String = DefaultClient 48 | )(implicit ec: ExecutionContext): AsyncResult[String, Response] = 49 | config.authenticatedRequest(s"auth/$client/users/$username/policies")( 50 | _.post(Map("username" -> username, "policies" -> policies.mkString(","))) 51 | ).execute.acceptStatusCodes(204) 52 | } 53 | // scalastyle:on magic.number 54 | -------------------------------------------------------------------------------- /manage/src/main/scala/janstenpickle/vault/manage/manage.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import com.ning.http.client.Response 4 | import io.circe.generic.auto._ 5 | import io.circe.syntax._ 6 | import janstenpickle.scala.syntax.OptionSyntax._ 7 | import janstenpickle.scala.syntax.SyntaxRequest._ 8 | import janstenpickle.scala.syntax.ResponseSyntax._ 9 | import janstenpickle.scala.syntax.VaultConfigSyntax._ 10 | import janstenpickle.vault.core.VaultConfig 11 | import janstenpickle.vault.manage.Model._ 12 | import uscala.concurrent.result.AsyncResult 13 | import uscala.result.Result 14 | 15 | import scala.concurrent.ExecutionContext 16 | 17 | // scalastyle:off magic.number 18 | case class Auth(config: VaultConfig) { 19 | def enable(`type`: String, 20 | mountPoint: Option[String] = None, 21 | description: Option[String] = None) 22 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 23 | config.authenticatedRequest(s"sys/auth/${mountPoint.getOrElse(`type`)}")( 24 | _.post(description.toMap("description") + ("type" -> `type`)) 25 | ).execute.acceptStatusCodes(204) 26 | 27 | def disable(mountPoint: String) 28 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 29 | config.authenticatedRequest(s"sys/auth/$mountPoint")(_.delete). 30 | execute. 31 | acceptStatusCodes(204) 32 | } 33 | 34 | case class Mounts(config: VaultConfig) { 35 | def remount(from: String, to: String) 36 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 37 | config.authenticatedRequest("sys/remount")( 38 | _.post(Map("from" -> from, "to" -> to)) 39 | ).execute.acceptStatusCodes(204) 40 | 41 | def list(implicit ec: ExecutionContext): 42 | AsyncResult[String, Map[String, Mount]] = 43 | config.authenticatedRequest("sys/mounts")(_.get). 44 | execute. 45 | acceptStatusCodes(200). 46 | extractFromJson[Map[String, Mount]](_.downField("data")) 47 | 48 | def mount(`type`: String, 49 | mountPoint: Option[String] = None, 50 | description: Option[String] = None, 51 | conf: Option[Mount] = None) 52 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 53 | config.authenticatedRequest(s"sys/mounts/${mountPoint.getOrElse(`type`)}")( 54 | _.post(MountRequest(`type`, description, conf).asJson) 55 | ).execute.acceptStatusCodes(204) 56 | 57 | def delete(mountPoint: String) 58 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 59 | config.authenticatedRequest(s"sys/mounts/$mountPoint")(_.delete). 60 | execute. 61 | acceptStatusCodes(204) 62 | } 63 | 64 | case class Policy(config: VaultConfig) { 65 | 66 | def list(implicit ec: ExecutionContext): AsyncResult[String, List[String]] = 67 | config.authenticatedRequest("sys/policy")(_.get). 68 | execute. 69 | acceptStatusCodes(200). 70 | extractFromJson[List[String]](_.downField("policies")) 71 | 72 | // NOTE: `rules` is not valid Json 73 | def inspect(policy: String)(implicit ec: ExecutionContext): 74 | AsyncResult[String, String] = 75 | config.authenticatedRequest(s"sys/policy/$policy")(_.get). 76 | execute. 77 | acceptStatusCodes(200) 78 | .extractFromJson[String](_.downField("rules")) 79 | 80 | def set(policy: String, rules: List[Rule]) 81 | (implicit ec: ExecutionContext): AsyncResult[String, Response] = 82 | config.authenticatedRequest(s"sys/policy/$policy")( 83 | _.post(PolicySetting(policy, rules).asJson)). 84 | execute. 85 | acceptStatusCodes(204) 86 | 87 | def delete(policy: String)(implicit ec: ExecutionContext): 88 | AsyncResult[String, Response] = 89 | config.authenticatedRequest(s"sys/policy/$policy")(_.delete). 90 | execute. 91 | acceptStatusCodes(204) 92 | } 93 | 94 | object Model { 95 | case class MountRequest(`type`: String, 96 | description: Option[String], 97 | config: Option[Mount]) 98 | case class Mount(`type`: String, 99 | description: Option[String], 100 | config: Option[MountConfig]) 101 | case class MountConfig( 102 | default_lease_ttl: Option[Int], 103 | max_lease_ttl: Option[Int], 104 | force_no_cache: Option[Boolean] 105 | ) 106 | 107 | case class PolicySetting(name: String, rules: Option[String]) { 108 | lazy val decodeRules: Option[Result[String, List[Rule]]] = rules.filter( 109 | _.nonEmpty).map(Rule.decode) 110 | } 111 | object PolicySetting { 112 | def apply(name: String, rules: List[Rule]): PolicySetting = 113 | PolicySetting(name, Option(rules.map(_.encode).mkString("\n"))) 114 | } 115 | case class Rule( 116 | path: String, 117 | capabilities: Option[List[String]] = None, 118 | policy: Option[String] = None 119 | ) { 120 | lazy val encodeCapabilities = capabilities.filter(_.nonEmpty).map(caps => 121 | s"capabilities = [${caps.map(c => s""""$c"""").mkString(", ")}]" 122 | ).getOrElse("") 123 | 124 | lazy val encodePolicy = policy.map(pol => 125 | s"""policy = "$pol"""" 126 | ).getOrElse("") 127 | 128 | lazy val encode = 129 | s""" 130 | |path "$path" { 131 | | $encodePolicy 132 | | $encodeCapabilities 133 | |}""".stripMargin('|') 134 | } 135 | object Rule { 136 | val pathRegex = """\s*path\s+"(\S+)"\s+\{""".r 137 | val capabilitiesRegex = """\s+capabilities\s+=\s+\[(.+)\]""".r 138 | val policyRegex = """\s+policy\s+=\s+"(\S+)"""".r 139 | 140 | def decode(ruleString: String): Result[String, List[Rule]] = { 141 | val rules = ruleString.split("""\s*}\s+\n""").toList 142 | val decoded = rules.foldLeft(List.empty[Rule])( (acc, v) => 143 | acc ++ pathRegex.findFirstMatchIn(v).map(_.group(1)).map(path => 144 | Rule( 145 | path, 146 | capabilitiesRegex.findFirstMatchIn(v).map(_.group(1).split(',') 147 | .map(_.trim.replace("\"", "")).toList), 148 | policyRegex.findFirstMatchIn(v).map(_.group(1)) 149 | ) 150 | ) 151 | ) 152 | if (decoded.isEmpty) { 153 | Result.fail(s"Could not find any valid rules in string: $ruleString") 154 | } 155 | else { 156 | Result.ok(decoded) 157 | } 158 | } 159 | } 160 | } 161 | // scalastyle:on magic.number 162 | -------------------------------------------------------------------------------- /manage/src/test/scala/janstenpickle/vault/manage/RuleSpec.scala: -------------------------------------------------------------------------------- 1 | package janstenpickle.vault.manage 2 | 3 | import janstenpickle.vault.manage.Model.Rule 4 | import org.scalacheck.{Gen, Prop} 5 | import org.specs2.{ScalaCheck, Specification} 6 | import uscala.result.specs2.ResultMatchers 7 | 8 | class RuleSpec extends Specification with ScalaCheck with ResultMatchers { 9 | import RuleSpec._ 10 | 11 | override def is = 12 | s2""" 13 | Can encode and decode policy strings $passes 14 | Cannot decode bad policy strings $fails 15 | """ 16 | 17 | def passes = Prop.forAllNoShrink(Gen.listOf(ruleGen).suchThat(_.nonEmpty)) (rules => 18 | Rule.decode(rules.map(_.encode).mkString("\n")) must beOk.like { 19 | case a => a must containAllOf(rules) 20 | } 21 | ) 22 | 23 | def fails = Prop.forAllNoShrink(Gen.listOf(badRuleGen).suchThat(_.nonEmpty)) (rules => 24 | Rule.decode(rules.mkString("\n")) must beFail 25 | ) 26 | } 27 | 28 | object RuleSpec { 29 | val policyGen = Gen.option(Gen.oneOf("read", "write", "sudo", "deny")) 30 | val capabilitiesGen = Gen.option( 31 | Gen.listOf(Gen.oneOf("create", "read", "update", "delete", "list", "sudo", "deny")). 32 | suchThat(_.nonEmpty). 33 | map(_.distinct) 34 | ) 35 | 36 | val ruleGen = for { 37 | path <- Gen.alphaStr.suchThat(_.nonEmpty) 38 | policy <- policyGen 39 | capabilities <- capabilitiesGen 40 | } yield Rule(path, capabilities, policy) 41 | 42 | val badRuleGen = for { 43 | path <- Gen.alphaStr.suchThat(_.nonEmpty) 44 | policy <- policyGen 45 | capabilities <- capabilitiesGen 46 | } yield 47 | s""" 48 | |path "$path" 49 | | $policy cage 50 | | $capabilities }""".stripMargin('|') 51 | } 52 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | // publishing and resolving bintray packages 4 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") 5 | 6 | // measure code coverage 7 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") 8 | 9 | // measure code style 10 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") 11 | 12 | // check dependencies 13 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0") 14 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Scalastyle standard configuration 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 | -------------------------------------------------------------------------------- /scripts/start_vault: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | NAME='vault-dev' 6 | VERSION='0.6.5' 7 | ROOT_TOKEN='vault-root-token' 8 | 9 | type 'curl' > /dev/null || error "No 'curl' found on system." 10 | type 'docker' > /dev/null || error "No 'docker' found on system." 11 | type 'jq' > /dev/null || error "No 'jq' found on system." 12 | 13 | if [ "$(docker ps -qa -f name=$NAME)" ]; then 14 | docker rm -f $NAME 15 | fi 16 | 17 | docker run -d --name=$NAME \ 18 | --cap-add=IPC_LOCK \ 19 | -e "VAULT_DEV_ROOT_TOKEN_ID=$ROOT_TOKEN" \ 20 | -e "VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200" \ 21 | -p 8200:8200 vault:$VERSION 22 | 23 | echo $ROOT_TOKEN > /tmp/.root-token 24 | 25 | # wait for vault to start 26 | sleep 10 27 | 28 | curl -X POST -H "X-Vault-Token:$ROOT_TOKEN" \ 29 | -d '{"type":"approle"}' http://127.0.0.1:8200/v1/sys/auth/approle 30 | 31 | curl -X PUT -H "X-Vault-Token:$ROOT_TOKEN" \ 32 | -d '{"rules":"path \"*\" {\n capabilities = [\"create\",\"read\",\"update\",\"delete\",\"list\",\"sudo\"]\n}"}' \ 33 | http://127.0.0.1:8200/v1/sys/policy/test-policy 34 | 35 | curl -X POST -H "X-Vault-Token:$ROOT_TOKEN" \ 36 | -d '{"policies":"default,test-policy"}' http://127.0.0.1:8200/v1/auth/approle/role/test-role 37 | 38 | curl -X GET -H "X-Vault-Token:$ROOT_TOKEN" \ 39 | http://127.0.0.1:8200/v1/auth/approle/role/test-role/role-id \ 40 | | jq -r '.data.role_id' > /tmp/.role-id 41 | 42 | curl -X POST -H "X-Vault-Token:$ROOT_TOKEN" \ 43 | http://127.0.0.1:8200/v1/auth/approle/role/test-role/secret-id \ 44 | | jq -r '.data.secret_id' > /tmp/.secret-id 45 | 46 | 47 | --------------------------------------------------------------------------------