/command/operator_generate_root_test.go

https://github.com/hashicorp/vault · Go · 468 lines · 365 code · 87 blank · 16 comment · 82 complexity · 27fe3d5652d0cf8f36df280c8f974708 MD5 · raw file

  1. // +build !race
  2. package command
  3. import (
  4. "encoding/base64"
  5. "io"
  6. "os"
  7. "regexp"
  8. "strings"
  9. "testing"
  10. "github.com/hashicorp/vault/helper/xor"
  11. "github.com/hashicorp/vault/vault"
  12. "github.com/mitchellh/cli"
  13. )
  14. func testOperatorGenerateRootCommand(tb testing.TB) (*cli.MockUi, *OperatorGenerateRootCommand) {
  15. tb.Helper()
  16. ui := cli.NewMockUi()
  17. return ui, &OperatorGenerateRootCommand{
  18. BaseCommand: &BaseCommand{
  19. UI: ui,
  20. },
  21. }
  22. }
  23. func TestOperatorGenerateRootCommand_Run(t *testing.T) {
  24. t.Parallel()
  25. cases := []struct {
  26. name string
  27. args []string
  28. out string
  29. code int
  30. }{
  31. {
  32. "init_invalid_otp",
  33. []string{
  34. "-init",
  35. "-otp", "not-a-valid-otp",
  36. },
  37. "OTP string is wrong length",
  38. 2,
  39. },
  40. {
  41. "init_pgp_multi",
  42. []string{
  43. "-init",
  44. "-pgp-key", "keybase:hashicorp",
  45. "-pgp-key", "keybase:jefferai",
  46. },
  47. "can only be specified once",
  48. 1,
  49. },
  50. {
  51. "init_pgp_multi_inline",
  52. []string{
  53. "-init",
  54. "-pgp-key", "keybase:hashicorp,keybase:jefferai",
  55. },
  56. "can only specify one pgp key",
  57. 1,
  58. },
  59. {
  60. "init_pgp_otp",
  61. []string{
  62. "-init",
  63. "-pgp-key", "keybase:hashicorp",
  64. "-otp", "abcd1234",
  65. },
  66. "cannot specify both -otp and -pgp-key",
  67. 1,
  68. },
  69. }
  70. t.Run("validations", func(t *testing.T) {
  71. t.Parallel()
  72. for _, tc := range cases {
  73. tc := tc
  74. t.Run(tc.name, func(t *testing.T) {
  75. t.Parallel()
  76. client, closer := testVaultServer(t)
  77. defer closer()
  78. ui, cmd := testOperatorGenerateRootCommand(t)
  79. cmd.client = client
  80. code := cmd.Run(tc.args)
  81. if code != tc.code {
  82. t.Errorf("%s: expected %d to be %d", tc.name, code, tc.code)
  83. }
  84. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  85. if !strings.Contains(combined, tc.out) {
  86. t.Errorf("%s: expected %q to contain %q", tc.name, combined, tc.out)
  87. }
  88. })
  89. }
  90. })
  91. t.Run("generate_otp", func(t *testing.T) {
  92. t.Parallel()
  93. client, closer := testVaultServer(t)
  94. defer closer()
  95. _, cmd := testOperatorGenerateRootCommand(t)
  96. cmd.client = client
  97. code := cmd.Run([]string{
  98. "-generate-otp",
  99. })
  100. if exp := 0; code != exp {
  101. t.Errorf("expected %d to be %d", code, exp)
  102. }
  103. })
  104. t.Run("decode", func(t *testing.T) {
  105. t.Parallel()
  106. encoded := "Bxg9JQQqOCNKBRICNwMIRzo2J3cWCBRi"
  107. otp := "3JhHkONiyiaNYj14nnD9xZQS"
  108. client, closer := testVaultServer(t)
  109. defer closer()
  110. ui, cmd := testOperatorGenerateRootCommand(t)
  111. cmd.client = client
  112. // Simulate piped output to print raw output
  113. old := os.Stdout
  114. _, w, err := os.Pipe()
  115. if err != nil {
  116. t.Fatal(err)
  117. }
  118. os.Stdout = w
  119. code := cmd.Run([]string{
  120. "-decode", encoded,
  121. "-otp", otp,
  122. })
  123. if exp := 0; code != exp {
  124. t.Errorf("expected %d to be %d", code, exp)
  125. }
  126. w.Close()
  127. os.Stdout = old
  128. expected := "4RUmoevJ3lsLni9sTXcNnRE1"
  129. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  130. if combined != expected {
  131. t.Errorf("expected %q to be %q", combined, expected)
  132. }
  133. })
  134. t.Run("cancel", func(t *testing.T) {
  135. t.Parallel()
  136. client, closer := testVaultServer(t)
  137. defer closer()
  138. // Initialize a generation
  139. if _, err := client.Sys().GenerateRootInit("", ""); err != nil {
  140. t.Fatal(err)
  141. }
  142. ui, cmd := testOperatorGenerateRootCommand(t)
  143. cmd.client = client
  144. code := cmd.Run([]string{
  145. "-cancel",
  146. })
  147. if exp := 0; code != exp {
  148. t.Errorf("expected %d to be %d", code, exp)
  149. }
  150. expected := "Success! Root token generation canceled"
  151. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  152. if !strings.Contains(combined, expected) {
  153. t.Errorf("expected %q to contain %q", combined, expected)
  154. }
  155. status, err := client.Sys().GenerateRootStatus()
  156. if err != nil {
  157. t.Fatal(err)
  158. }
  159. if status.Started {
  160. t.Errorf("expected status to be canceled: %#v", status)
  161. }
  162. })
  163. t.Run("init_otp", func(t *testing.T) {
  164. t.Parallel()
  165. client, closer := testVaultServer(t)
  166. defer closer()
  167. ui, cmd := testOperatorGenerateRootCommand(t)
  168. cmd.client = client
  169. code := cmd.Run([]string{
  170. "-init",
  171. })
  172. if exp := 0; code != exp {
  173. t.Errorf("expected %d to be %d", code, exp)
  174. }
  175. expected := "Nonce"
  176. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  177. if !strings.Contains(combined, expected) {
  178. t.Errorf("expected %q to contain %q", combined, expected)
  179. }
  180. status, err := client.Sys().GenerateRootStatus()
  181. if err != nil {
  182. t.Fatal(err)
  183. }
  184. if !status.Started {
  185. t.Errorf("expected status to be started: %#v", status)
  186. }
  187. })
  188. t.Run("init_pgp", func(t *testing.T) {
  189. t.Parallel()
  190. pgpKey := "keybase:hashicorp"
  191. pgpFingerprint := "91a6e7f85d05c65630bef18951852d87348ffc4c"
  192. client, closer := testVaultServer(t)
  193. defer closer()
  194. ui, cmd := testOperatorGenerateRootCommand(t)
  195. cmd.client = client
  196. code := cmd.Run([]string{
  197. "-init",
  198. "-pgp-key", pgpKey,
  199. })
  200. if exp := 0; code != exp {
  201. t.Errorf("expected %d to be %d", code, exp)
  202. }
  203. expected := "Nonce"
  204. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  205. if !strings.Contains(combined, expected) {
  206. t.Errorf("expected %q to contain %q", combined, expected)
  207. }
  208. status, err := client.Sys().GenerateRootStatus()
  209. if err != nil {
  210. t.Fatal(err)
  211. }
  212. if !status.Started {
  213. t.Errorf("expected status to be started: %#v", status)
  214. }
  215. if status.PGPFingerprint != pgpFingerprint {
  216. t.Errorf("expected %q to be %q", status.PGPFingerprint, pgpFingerprint)
  217. }
  218. })
  219. t.Run("status", func(t *testing.T) {
  220. t.Parallel()
  221. client, closer := testVaultServer(t)
  222. defer closer()
  223. ui, cmd := testOperatorGenerateRootCommand(t)
  224. cmd.client = client
  225. code := cmd.Run([]string{
  226. "-status",
  227. })
  228. if exp := 0; code != exp {
  229. t.Errorf("expected %d to be %d", code, exp)
  230. }
  231. expected := "Nonce"
  232. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  233. if !strings.Contains(combined, expected) {
  234. t.Errorf("expected %q to contain %q", combined, expected)
  235. }
  236. })
  237. t.Run("provide_arg", func(t *testing.T) {
  238. t.Parallel()
  239. client, keys, closer := testVaultServerUnseal(t)
  240. defer closer()
  241. // Initialize a generation
  242. status, err := client.Sys().GenerateRootInit("", "")
  243. if err != nil {
  244. t.Fatal(err)
  245. }
  246. nonce := status.Nonce
  247. otp := status.OTP
  248. // Supply the first n-1 unseal keys
  249. for _, key := range keys[:len(keys)-1] {
  250. _, cmd := testOperatorGenerateRootCommand(t)
  251. cmd.client = client
  252. code := cmd.Run([]string{
  253. "-nonce", nonce,
  254. key,
  255. })
  256. if exp := 0; code != exp {
  257. t.Errorf("expected %d to be %d", code, exp)
  258. }
  259. }
  260. ui, cmd := testOperatorGenerateRootCommand(t)
  261. cmd.client = client
  262. code := cmd.Run([]string{
  263. "-nonce", nonce,
  264. keys[len(keys)-1], // the last unseal key
  265. })
  266. if exp := 0; code != exp {
  267. t.Fatalf("expected %d to be %d, out=%q, err=%q", code, exp, ui.OutputWriter, ui.ErrorWriter)
  268. }
  269. reToken := regexp.MustCompile(`Encoded Token\s+(.+)`)
  270. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  271. match := reToken.FindAllStringSubmatch(combined, -1)
  272. if len(match) < 1 || len(match[0]) < 2 {
  273. t.Fatalf("no match: %#v", match)
  274. }
  275. tokenBytes, err := base64.RawStdEncoding.DecodeString(match[0][1])
  276. if err != nil {
  277. t.Fatal(err)
  278. }
  279. token, err := xor.XORBytes(tokenBytes, []byte(otp))
  280. if err != nil {
  281. t.Fatal(err)
  282. }
  283. if l, exp := len(token), vault.TokenLength+2; l != exp {
  284. t.Errorf("expected %d to be %d: %s", l, exp, token)
  285. }
  286. })
  287. t.Run("provide_stdin", func(t *testing.T) {
  288. t.Parallel()
  289. client, keys, closer := testVaultServerUnseal(t)
  290. defer closer()
  291. // Initialize a generation
  292. status, err := client.Sys().GenerateRootInit("", "")
  293. if err != nil {
  294. t.Fatal(err)
  295. }
  296. nonce := status.Nonce
  297. otp := status.OTP
  298. // Supply the first n-1 unseal keys
  299. for _, key := range keys[:len(keys)-1] {
  300. stdinR, stdinW := io.Pipe()
  301. go func() {
  302. stdinW.Write([]byte(key))
  303. stdinW.Close()
  304. }()
  305. _, cmd := testOperatorGenerateRootCommand(t)
  306. cmd.client = client
  307. cmd.testStdin = stdinR
  308. code := cmd.Run([]string{
  309. "-nonce", nonce,
  310. "-",
  311. })
  312. if exp := 0; code != exp {
  313. t.Errorf("expected %d to be %d", code, exp)
  314. }
  315. }
  316. stdinR, stdinW := io.Pipe()
  317. go func() {
  318. stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key
  319. stdinW.Close()
  320. }()
  321. ui, cmd := testOperatorGenerateRootCommand(t)
  322. cmd.client = client
  323. cmd.testStdin = stdinR
  324. code := cmd.Run([]string{
  325. "-nonce", nonce,
  326. "-",
  327. })
  328. if exp := 0; code != exp {
  329. t.Errorf("expected %d to be %d", code, exp)
  330. }
  331. reToken := regexp.MustCompile(`Encoded Token\s+(.+)`)
  332. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  333. match := reToken.FindAllStringSubmatch(combined, -1)
  334. if len(match) < 1 || len(match[0]) < 2 {
  335. t.Fatalf("no match: %#v", match)
  336. }
  337. // encodedOTP := base64.RawStdEncoding.EncodeToString([]byte(otp))
  338. // tokenBytes, err := xor.XORBase64(match[0][1], encodedOTP)
  339. // if err != nil {
  340. // t.Fatal(err)
  341. // }
  342. // token, err := uuid.FormatUUID(tokenBytes)
  343. // if err != nil {
  344. // t.Fatal(err)
  345. // }
  346. tokenBytes, err := base64.RawStdEncoding.DecodeString(match[0][1])
  347. if err != nil {
  348. t.Fatal(err)
  349. }
  350. token, err := xor.XORBytes(tokenBytes, []byte(otp))
  351. if err != nil {
  352. t.Fatal(err)
  353. }
  354. if l, exp := len(token), vault.TokenLength+2; l != exp {
  355. t.Errorf("expected %d to be %d: %s", l, exp, token)
  356. }
  357. })
  358. t.Run("communication_failure", func(t *testing.T) {
  359. t.Parallel()
  360. client, closer := testVaultServerBad(t)
  361. defer closer()
  362. ui, cmd := testOperatorGenerateRootCommand(t)
  363. cmd.client = client
  364. code := cmd.Run([]string{
  365. "secret/foo",
  366. })
  367. if exp := 2; code != exp {
  368. t.Errorf("expected %d to be %d", code, exp)
  369. }
  370. expected := "Error getting root generation status: "
  371. combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
  372. if !strings.Contains(combined, expected) {
  373. t.Errorf("expected %q to contain %q", combined, expected)
  374. }
  375. })
  376. t.Run("no_tabs", func(t *testing.T) {
  377. t.Parallel()
  378. _, cmd := testOperatorGenerateRootCommand(t)
  379. assertNoTabs(t, cmd)
  380. })
  381. }