├── .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 | [](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 | [](https://bintray.com/janstenpickle/maven/vault-core/_latestVersion) |
12 | | **Auth** | Functions to authenticate a user using userpass authentication and token verification | [](https://bintray.com/janstenpickle/maven/vault-auth/_latestVersion)|
13 | | **Manage** | Functions for managing auth modules, mounts and policies | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------