From 5a8e70047a8451491d89bc2992388bef012b9c1e Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 12 Aug 2016 09:09:56 -0700 Subject: [PATCH] Add a restore endpoint to Red October core. (#167) This takes the work done in 7c95007cda74d28318987a53bfda196bbc7111a1 and provides an interface via the server's API. --- core/core.go | 48 +++++-- core/core_test.go | 251 +++++++++++++++++++++++++++++++++++-- cryptor/cryptor.go | 5 + cryptor/cryptor_test.go | 6 +- persist/file.go | 11 +- persist/file_test.go | 30 +++++ redoctober.go | 1 + redoctober_test.go | 271 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 594 insertions(+), 29 deletions(-) diff --git a/core/core.go b/core/core.go index 3125cdf..596cbb2 100644 --- a/core/core.go +++ b/core/core.go @@ -18,7 +18,6 @@ import ( "github.com/cloudflare/redoctober/keycache" "github.com/cloudflare/redoctober/order" "github.com/cloudflare/redoctober/passvault" - "github.com/cloudflare/redoctober/persist" ) var ( @@ -177,11 +176,6 @@ type StatusData struct { Status string } -var restore struct { - Config *config.Delegations - State string -} - // Helper functions that create JSON responses sent by core func jsonStatusOk() ([]byte, error) { @@ -264,9 +258,6 @@ func Init(path string, config *config.Config) error { } } - restore.Config = config.Delegations - restore.State = persist.Disabled - orders = order.NewOrderer(hipchatClient) crypt, err = cryptor.New(&records, nil, config) @@ -959,10 +950,45 @@ func Status(jsonIn []byte) (out []byte, err error) { return jsonStatusError(err) } - resp := StatusData{Status: restore.State} + st := crypt.Status() + resp := &StatusData{Status: st.State} if out, err = json.Marshal(resp); err != nil { return jsonStatusError(err) } - return + return jsonResponse(out) +} + +// Restore attempts a restoration of the persistence store. +func Restore(jsonIn []byte) (out []byte, err error) { + var req DelegateRequest + + defer func() { + if err != nil { + log.Printf("core.restore failed: user=%s %v", req.Name, err) + } else { + log.Printf("core.restore success: user=%s", req.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &req); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(req.Name, req.Password, false); err != nil { + return jsonStatusError(err) + } + + err = crypt.Restore(req.Name, req.Password, req.Uses, req.Slot, req.Time) + if err != nil && err != cryptor.ErrRestoreDelegations { + return jsonStatusError(err) + } + + st := crypt.Status() + resp := &StatusData{Status: st.State} + if out, err = json.Marshal(resp); err != nil { + return jsonStatusError(err) + } + + return jsonResponse(out) } diff --git a/core/core_test.go b/core/core_test.go index ed6fb21..f861f81 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -7,6 +7,7 @@ package core import ( "bytes" "encoding/json" + "io/ioutil" "os" "reflect" "sort" @@ -17,6 +18,17 @@ import ( "github.com/cloudflare/redoctober/persist" ) +func tempName(t *testing.T) string { + tmpf, err := ioutil.TempFile("", "ro_core") + if err != nil { + t.Fatalf("failed to get a temporary file: %s", err) + } + + name := tmpf.Name() + tmpf.Close() + return name +} + func TestCreate(t *testing.T) { createJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") cfg := config.New() @@ -93,8 +105,13 @@ func TestSummary(t *testing.T) { t.Fatalf("Error in creating account, %v", err) } + var rd ResponseData var st StatusData - err = json.Unmarshal(respJson, &st) + err = json.Unmarshal(respJson, &rd) + if err != nil { + t.Fatalf("Error getting status, %v", err) + } + err = json.Unmarshal(rd.Response, &st) if err != nil { t.Fatalf("Error getting status, %v", err) } @@ -1086,23 +1103,15 @@ func TestStatic(t *testing.T) { diskVault := []byte("{\"Version\":1,\"VaultId\":5298538957754540810,\"HmacKey\":\"Qugc5ZQ0vC7KQSgmDHTVgQ==\",\"Passwords\":{\"Alice\":{\"Type\":\"RSA\",\"PasswordSalt\":\"HrVbQ4JvEdORxWN6FrSy4w==\",\"HashedPassword\":\"nanadB0t/EmVuHyRUNtfyQ==\",\"KeySalt\":\"u0cwMmHikadpxm9wClrf3g==\",\"AESKey\":\"pOcxCYMk0l+kaEM5IHolfA==\",\"RSAKey\":{\"RSAExp\":\"yDBotK0XkaDk6rt35ciBnlyPGSXjp9ypdTH2j89CybXe2ReF6xLVZ10CCoz91UUFpbiQi2tVWFS8V7lxUehx7C6HI30Zr0iwJMXk8sgRKs2Ee3rAGCrM9vvQMO8ApKwe4kvB+PrFgnhIEgZI7zyPJPcdZnxbcVFyUC3uIivFS4Bv3jVBnrkAx71keQqrkKzu3jTquCIS3rTEm2hrgsZ3n1t/4BADvUpcMpII2Phv2Senonp1EMz9sRFsR4w9HVep8AgfUGuPTcQ/uv9R16xiHvlN80rOtWzVLpEruzGw/JTvlsshFBU5SY/zthGl9TwxWz1yZpVHYhIWhbxw34CXYczB6q19bdEPkJJldi/1coI=\",\"RSAExpIV\":\"RI9B8nzwW/CXiIU4lSYRWg==\",\"RSAPrimeP\":\"yB3TMdRhWLiZ/ayPlu/iLDHxWsuMi9pA8Ctps7WZFxVsxYEzy/s0Otzsbtgay8of72xVkO64MaudXXRjj26kVSQhS8WhPmv7xDO5ba3SsTffCA99aSr3MH+JmUoL+EDjYviUf5F1DSZniv+Ae+6x9AMbbRQqRvmdH/INW76rFbLX4VtsMpgVkAhADwCUSfNS\",\"RSAPrimePIV\":\"ZE8xe7zVqz4fTN1XWvD1Tg==\",\"RSAPrimeQ\":\"pq7rEAmXoiMWhuEpTy2pQiheLHuzcGeGm1IsgtP7TRIpHaumBAjasxhMY95ODspzehfHp8RPb3pz550g+EXpRP5bfmZkiPMGWKsFUWY7h51hm2Yg6t7yOSXxQvzseWDUjJqBXIG56su0ItD/7NT9YPnhOWAcLaV+L/D3dLdUOP3sgrfp2fcz5IWgcAHUKwLj\",\"RSAPrimeQIV\":\"po4GB9LNDJEFwVo7pd7M+w==\",\"RSAPublic\":{\"N\":23149224936745228096494487629641309516714939646787578848987096719230079441991920927625328021835418605829165905957281054739040782220661415211495551591590967025905842090248342119030961900558364831442589592519977574953153506098793781379522492850670706181134690851176026007414311463584686376540335844073069224992659463459793269384975134553539299820716318523937519020382533972574258510063896159690996712751336541794097684994088922002126390397554509324964165816682788604802958437629743951677011277308533675371215221012180522157452368867794857042597831942805118364469257052754518983480732149823871291876054393236400292711237,\"E\":65537}},\"Admin\":true},\"Bob\":{\"Type\":\"RSA\",\"PasswordSalt\":\"yqWZFNuuCRMw+snL83FcWQ==\",\"HashedPassword\":\"jYLSUIdvw8UVXhxVSS5uKQ==\",\"KeySalt\":\"ZJDOGSv9yUlJ5+83+FV6Nw==\",\"AESKey\":\"9bj4qQDJKglD9eIm7MNeag==\",\"RSAKey\":{\"RSAExp\":\"rnmHX1FgAN0KFm89Uj+O4L7/njDlQPwGHSYjGKB7qMyNPGF4jKn9ez1LI9e9jI8uE475KBhoXRmIuHdap6HtD+sH+nHugDzD3np6Dbq1MM+19PW41n9xblwx6tsNwvufCYgb2qtZd0bLXemeKBszJg2UXlTeSHkXGmjY/VAbZYbUFUNKIykiJWnQ+3HlAo7UHjjKSDI6HtiMnODwYucKH9uYdmroM3DqRUP+j+AhMctyeyOt68q6RuVyubzG0PM1/T9QqPgTIzFvg9dxk+LmPYmlv4b7Euea7KQHww4kTUlpNYondRisuA436G7EfWIJFeRShFuSVk0GvmrgN5vK+/FkdqMuikPaPV1dITFzaCU=\",\"RSAExpIV\":\"eoIov2XYwK19tpFkZyTL+Q==\",\"RSAPrimeP\":\"x9VqeHfHZBMksWNz3Bq90KyLYO0+y9tbfH17uAxQHIZbhn6RUHMXJFs4/H+TnL9s7D05HdxKNCD3ilISkhLZ/DQVD+VMvSFQ/2DL/OoKNg4UyxeGpKefJxCU9LOeXGTfN+UpGHN4Myal3PL2yi0yWCVZvX+zPks05Nqmzkmtx1Yz0IqeUaR+I1jw2y0xy+nc\",\"RSAPrimePIV\":\"h+J0h35kSaNFjBFyejmqSQ==\",\"RSAPrimeQ\":\"CUGaukdC8slcy1Qm6kBw8QYhVenJWFylPqJTvf03ATkkaxQJ8ZSK/RodXFiKCztRSx/HVw0LoGGx9RiavSxl1I+NPIiGEBXiYnLCwWjIbohEIviX0XrKEegECKZtEPxlkDGI8C5ScmUiUOoCUFODqPi1ymEPPIpqj6NZu0Q3lOXseZ7vHbCwiO7Nxznoed2V\",\"RSAPrimeQIV\":\"fglAccK/JKbbP6FKd3MoPA==\",\"RSAPublic\":{\"N\":28335031342838743289078885439639803673108967200362163631216970159249785951860249148476503896024171065194110189927394386737974335124568755441420594193064949823336810514078294805084876384362160530316962043860952904024334641317905429962254720146672817625880231384279651687169091903559644110839987019710966818515746956556387614880164417442090115347521394174077031531398826388780347541493782387177778507223791525305993050791342196579264299166530031123364885677134064847664024722422373550942639559346558112737229502294633354781474882714474174085566901518771722057743396746938093859383451665388899451851738694979184054459471,\"E\":65537}},\"Admin\":false},\"Carol\":{\"Type\":\"RSA\",\"PasswordSalt\":\"kPCA68PNIi3qODVfBHonMA==\",\"HashedPassword\":\"L05ueGkNhqRI7xo7OaG5fQ==\",\"KeySalt\":\"bUXHAjmdtpcYMNN/YzHvhA==\",\"AESKey\":\"34sh2HYt11JMd63CTC2g3Q==\",\"RSAKey\":{\"RSAExp\":\"blJ6FvmAC6ZegcR6ITdg8WDSMljcikrUp3dRGbRDgKIfK0sx7b9i/kDtp4uD7/3IuTpD/qq09k07PO10T5R9NI2OdaEUGsoKJ4wqyzP6XzC9l/KeQmU7cMTh5OHO5UcqXWBn+g1INWaWI6DNAPFKK+4jcTyy6gTQQZQKSYRvRmgN6GkjOhbsdH0eM29eZScShJy2cGemZwbx8g2x7+cebALJmnJxycq29uBZaNBq4gV2/oNXxUhAogJY8SVhgPzCgig2MAJBLK/PzzxJ2LgnBLosZkUXo4vLPKSELX0qXSkSOoC/qPXvcQGawsknqUkSaaDwB1T1MYHmcJS/wpiIRM89Qru0Oy8sy8NRApk3ef0=\",\"RSAExpIV\":\"pQnJ6tvdJkPmMkqFjwJg5g==\",\"RSAPrimeP\":\"xTwgxhDvEvMCn8W6Mqv4ogRnBXFTLbuLP8YC2exQ+RwKmUbNsjB1eBlHyYRtKwc7qoDLf/zqpMZk3yPPcQDP+szAS5mmhCgpd+ePee8vvwVR5DImDuCquMGrPNEgpg3LL1aBHMF66pfnYGob+P6GZYzJP3mhlewUlh86NDzP8YkoVlrNRZrarB1/ZmA+w2Y2\",\"RSAPrimePIV\":\"BYynxyFw7WxrpSKJ3ogwOw==\",\"RSAPrimeQ\":\"wrSXyN/gpDTvK/BMeH+04uhzGUFVbBrfo47L3i+E1QsQndJDy2yyyE1D26mFcvyiefObaD097M3ruR9Wz7WfjxmyawG8N2W/BgtHZz/Ds2ThN/T9t1XqskxnIV8l2eq9LL7SqjAyp8jUGMNVS4WODDtDngLIR6OeehfyHpgwVZRuqQMxIT3wA78SwDcWo41s\",\"RSAPrimeQIV\":\"nh42rsJHRpRyrT7DUHCSGQ==\",\"RSAPublic\":{\"N\":21157349824302424474586534982259249211803834319040688464355493234801787797103302507532796077498450217821871970018694577875997137564403612290101696297388762661598985028358214691463545987525906601624475015641369758738594698734277968689933610815708041386873182005594893971935327515126641632178583839753604241275307504709356922001828087700047657269629468786192584360390570302228520684623917906322926285597325969441240603594319235541820769758760147548185811818605055779607442920451051925838835969679126714958513824527149123555098280031335447316080110515987423541389176918537439220114191601986031091845506241054078769049741,\"E\":65537}},\"Admin\":false}}}") cfg := config.New() - // TODO: create a proper temp file for this. - file, err := os.Create("/tmp/db1.json") - if err != nil { - t.Fatalf("Error opening file, %v", err) - } - defer os.Remove("/tmp/db1.json") + file := tempName(t) + defer os.Remove(file) - _, err = file.Write(diskVault) + err := ioutil.WriteFile(file, diskVault, 0644) if err != nil { t.Fatalf("Error writing file, %v", err) } - err = file.Close() - if err != nil { - t.Fatalf("Error closing file, %v", err) - } - Init("/tmp/db1.json", cfg) + Init(file, cfg) // check for summary of initialized vault with new member var s ResponseData @@ -1175,3 +1184,219 @@ func TestValidateName(t *testing.T) { t.Fatalf("No error expected when username and password provided, %v", err) } } + +// Create a vault for the restore tests. +func restoreCreateVault(t *testing.T, pstore, file string) { + cfg := config.New() + cfg.Delegations = &config.Delegations{ + Persist: true, + Mechanism: persist.FileMechanism, + Policy: "Alice & Carl", + Users: []string{"Alice", "Carl"}, + Location: pstore, + } + + if err := Init(file, cfg); err != nil { + t.Fatalf("Unable to initialise core: %s", err) + } +} + +func restoreState(t *testing.T, expected string) { + statusJson := []byte("{\"Name\":\"Admin\",\"Password\":\"Admin\"}") + + var rd ResponseData + var st StatusData + + if respJson, err := Status(statusJson); err != nil { + t.Fatalf("Failed to look up vault state: %s", err) + } else if err = json.Unmarshal(respJson, &rd); err != nil { + t.Fatalf("Error getting status, %v", err) + } else if err = json.Unmarshal(rd.Response, &st); err != nil { + t.Fatalf("Error getting status, %v", err) + } else if st.Status != expected { + t.Fatalf("Persistent delegations should be '%s' but are '%s'", + expected, st.Status) + } +} + +// beforeRestartRestore contains the test sequence to run before +// simulating a restart of the vault. +func beforeRestartRestore(t *testing.T) { + adminUser := []byte("{\"Name\":\"Admin\",\"Password\":\"Admin\"}") + createUser := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\"}") + + _, err := Create(adminUser) + if err != nil { + t.Fatalf("Unable to create new diskrecord: %s", err) + } + + var s ResponseData + respJson, err := CreateUser(createUser) + if err != nil { + t.Fatalf("Error creating user: %s", err) + } else if err = json.Unmarshal(respJson, &s); err != nil { + t.Fatalf("Error creating user: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Failed to create user: %v", s) + } + + restoreState(t, persist.Active) + restoreDelegateNormal(t) +} + +// restoreDelegateNormal performs standard delegations against the +// vault; the test will make sure these exist. +func restoreDelegateNormal(t *testing.T) { + delegateJson1 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":2}") + delegateJson2 := []byte("{\"Name\":\"Bob\",\"Password\":\"Hello\",\"Time\":\"15m\",\"Uses\":3}") + + var s ResponseData + respJson, err := Delegate(delegateJson1) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } + + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Error delegating: %s", s.Status) + } + + respJson, err = Delegate(delegateJson2) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } + + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Error delegating: %s", s.Status) + } +} + +func restoreGetSummary(t *testing.T) SummaryData { + adminUser := []byte("{\"Name\":\"Admin\",\"Password\":\"Admin\"}") + respJson, err := Summary(adminUser) + if err != nil { + t.Fatalf("Error retrieving vault summary: %s", err) + } + + var s SummaryData + if err = json.Unmarshal(respJson, &s); err != nil { + t.Fatalf("Error unmarshalling summary data: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Vault returned error retrieving summary: %s", err) + } + + return s +} + +func afterRestartRestore(t *testing.T) { + restoreState(t, persist.Inactive) + + // Ensure there are no active delegations after the restart. + summary := restoreGetSummary(t) + if len(summary.Live) != 0 { + t.Fatalf("There should be no live users after restart, but there are %d", len(summary.Live)) + } + + restoreDelegateRestore(t) +} + +func restoreDelegateRestore(t *testing.T) { + delegateJsonBad := []byte("{\"Name\":\"Alice\",\"Password\":\"Hi\",\"Time\":\"5m\",\"Uses\":1}") + delegateJson1 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":1}") + delegateJson2 := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":1}") + delegateJson3 := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":1}") + + var s ResponseData + respJson, err := Restore(delegateJsonBad) + if err != nil { + t.Fatalf("Error making restore request: %s", err) + } + + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } else if s.Status != "Wrong Password" { + t.Fatal("Restore should fail with bad password.") + } + + restoreState(t, persist.Inactive) + + respJson, err = Restore(delegateJson1) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } + + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Error delegating: %s", s.Status) + } + restoreState(t, persist.Inactive) + + // The following delegation should be lost in the restoration. + respJson, err = Delegate(delegateJson2) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } + + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Error delegating: %s", s.Status) + } + + summary := restoreGetSummary(t) + if len(summary.Live) != 1 { + t.Fatalf("There should be a single live delegation, but there are %d", len(summary.Live)) + } + + respJson, err = Restore(delegateJson3) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } + + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error delegating: %s", err) + } else if s.Status != "ok" { + t.Fatalf("Error delegating: %s", s.Status) + } + restoreState(t, persist.Active) + + summary = restoreGetSummary(t) + if len(summary.Live) != 2 { + t.Fatalf("There should be two active delegations, but there are %d", len(summary.Live)) + } + + if _, ok := summary.Live["Carl"]; ok { + t.Fatalf("Bob shouldn't be present in the active delegations.") + } else if _, ok = summary.Live["Alice"]; !ok { + t.Fatalf("Alice should be present in the active delegations.") + } else if _, ok = summary.Live["Bob"]; !ok { + t.Fatalf("Bob should be present in the active delegations.") + } +} + +func TestRestore(t *testing.T) { + pstore := tempName(t) + defer os.Remove(pstore) + + file := tempName(t) + defer os.Remove(file) + + // Create the vault, an admin user, and perform some delegations. + restoreCreateVault(t, pstore, file) + beforeRestartRestore(t) + + // Restart the vault; this simulates restarting the Red + // October server. + restoreCreateVault(t, pstore, file) + afterRestartRestore(t) +} diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index c509764..9cd35aa 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -776,3 +776,8 @@ func (c *Cryptor) Restore(name, password string, uses int, slot, durationString c.persist.Persist() return nil } + +// Status returns the status of the underlying persistence store. +func (c *Cryptor) Status() *persist.Status { + return c.persist.Status() +} diff --git a/cryptor/cryptor_test.go b/cryptor/cryptor_test.go index 183d1b8..27cbf08 100644 --- a/cryptor/cryptor_test.go +++ b/cryptor/cryptor_test.go @@ -190,7 +190,7 @@ func TestEncryptDecrypt(t *testing.T) { } func tempName() (string, error) { - tmpf, err := ioutil.TempFile("", "transport_cachedkp_") + tmpf, err := ioutil.TempFile("", "ro_cryptor") if err != nil { return "", err } @@ -287,7 +287,7 @@ func TestRestore(t *testing.T) { t.Fatal(err) } - status := c.persist.Status() + status := c.Status() if status.State != persist.Inactive { t.Fatalf("The persistent delegations should be %s, not %s", persist.Inactive, status.State) @@ -303,7 +303,7 @@ func TestRestore(t *testing.T) { t.Fatal(err) } - status = c.persist.Status() + status = c.Status() if status.State != persist.Active { t.Fatalf("The persistent delegations should be %s, not %s", persist.Active, status.State) diff --git a/persist/file.go b/persist/file.go index b2f1cf2..b03a018 100644 --- a/persist/file.go +++ b/persist/file.go @@ -85,8 +85,7 @@ func (f *File) Store(blob []byte) error { } func (f *File) Load() error { - in, err := ioutil.ReadFile(f.config.Location) - if err != nil { + if fi, err := os.Stat(f.config.Location); err != nil { // If the file doesn't exist, it can be persisted // immediately. if os.IsNotExist(err) { @@ -94,6 +93,14 @@ func (f *File) Load() error { return nil } + return err + } else if fi.Size() == 0 { + f.state = Active + return nil + } + + in, err := ioutil.ReadFile(f.config.Location) + if err != nil { return err } diff --git a/persist/file_test.go b/persist/file_test.go index 1bea127..44f6963 100644 --- a/persist/file_test.go +++ b/persist/file_test.go @@ -135,3 +135,33 @@ func TestFileSanity(t *testing.T) { } } + +func TestNewFilePersists(t *testing.T) { + sf, err := tempName() + if err != nil { + t.Fatal(err) + } + defer os.Remove(sf) + + cfg := &config.Delegations{ + Persist: true, + Mechanism: FileMechanism, + Policy: "alice & bob", + Users: []string{"alice", "bob"}, + Location: sf, + } + + f, err := New(cfg) + if err != nil { + t.Fatal(err) + } + + file, ok := f.(*File) + if !ok { + t.Fatalf("persist: expected to get a *File but have %T", f) + } + + if file.state != Active { + t.Fatalf("fresh store should be persisting") + } +} diff --git a/redoctober.go b/redoctober.go index d643585..cad520b 100644 --- a/redoctober.go +++ b/redoctober.go @@ -45,6 +45,7 @@ var functions = map[string]func([]byte) ([]byte, error){ "/orderout": core.OrdersOutstanding, "/orderinfo": core.OrderInfo, "/ordercancel": core.OrderCancel, + "/restore": core.Restore, "/status": core.Status, } diff --git a/redoctober_test.go b/redoctober_test.go index b0fc741..05e8aa6 100644 --- a/redoctober_test.go +++ b/redoctober_test.go @@ -19,7 +19,9 @@ import ( "testing" "time" + "github.com/cloudflare/redoctober/config" "github.com/cloudflare/redoctober/core" + "github.com/cloudflare/redoctober/persist" ) const baseURL = "https://localhost:8080/" @@ -44,6 +46,18 @@ var ( Time: "2h34m", Uses: 1, } + delegateInput3 = &core.DelegateRequest{ + Name: createUserInput3.Name, + Password: createUserInput3.Password, + Time: "2h34m", + Uses: 1, + } + delegateInput4 = &core.DelegateRequest{ + Name: createVaultInput.Name, + Password: createVaultInput.Password, + Time: "2h34m", + Uses: 1, + } encryptInput = &core.EncryptRequest{ Minimum: 2, @@ -615,3 +629,260 @@ func TestPurge(t *testing.T) { t.Fatalf("Error purging with admin user, %v", err) } } + +//////////////////////////////////////////////////////////////////////////////// +// Restore tests // +// // +// These need to write files to disk in order to test recovering delegations. // +//////////////////////////////////////////////////////////////////////////////// + +func restoreSetup(t *testing.T, configPath, vaultPath string) (cmd *exec.Cmd) { + const maxAttempts = 5 + + // Look for the redoctober binary in current directory and then in $GOPATH/bin + binaryPath, err := exec.LookPath("./redoctober") + if err != nil { + goPathBinary := fmt.Sprintf("%s/bin/redoctober", os.Getenv("GOPATH")) + binaryPath, err = exec.LookPath(goPathBinary) + if err != nil { + t.Fatalf(`Could not find redoctober binary at "./redoctober" or "%s"`, goPathBinary) + } + } + + cmd = exec.Command(binaryPath, "-vaultpath", vaultPath, "-f", configPath) + + if err := cmd.Start(); err != nil { + t.Fatalf("Error running redoctober command, %v", err) + } + + attempts := 0 + + for { + resp, err := http.Get("http://localhost:8081") + if err == nil { + resp.Body.Close() + break + } + + attempts++ + if attempts > maxAttempts { + t.Fatalf("failed to start redoctober (max connection attempts exceeded)") + } + time.Sleep(500 * time.Millisecond) + } + + return +} + +func tempName(t *testing.T) string { + tmpf, err := ioutil.TempFile("", "redoctober_integration") + if err != nil { + t.Fatalf("failed to get a temporary file: %s", err) + } + + name := tmpf.Name() + tmpf.Close() + return name +} + +func TestRestore(t *testing.T) { + // Set up the vault. + pstore := tempName(t) + defer os.Remove(pstore) + + cfgPath := tempName(t) + defer os.Remove(cfgPath) + + prepareSetup(t, pstore, cfgPath) + + vaultPath := tempName(t) + defer os.Remove(vaultPath) + + // Run the server, perform some delegations, then kill the + // server. + beforeRestartRestore(t, cfgPath, vaultPath) + + // The server has restarted --- verify that the persisted + // delegations are available. + afterRestartRestore(t, cfgPath, vaultPath) +} + +func prepareSetup(t *testing.T, pstore, cfgPath string) { + // Write the config file. + cfg := config.New() + cfg.Delegations = &config.Delegations{ + Persist: true, + Mechanism: persist.FileMechanism, + Policy: "(Alice & Bill)", + Users: []string{"Alice", "Bill"}, + Location: pstore, + } + cfg.Server = &config.Server{ + Addr: "localhost:8080", + CertPaths: "testdata/server.crt", + KeyPaths: "testdata/server.pem", + } + cfg.Metrics = &config.Metrics{ + Host: "localhost", + Port: "8081", + } + + out, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("failed to marshal config file: %s", err) + } + + err = ioutil.WriteFile(cfgPath, out, 0644) + if err != nil { + t.Fatalf("failed to write config file: %s", err) + } +} + +func restoreCheckStatus(t *testing.T, expected string) { + // Verify the vault is persisting. + statusRequest := core.StatusRequest{ + Name: createUserInput1.Name, + Password: createUserInput1.Password, + } + + var status core.StatusData + var response core.ResponseData + + respBytes, _, err := post("status", statusRequest) + if err != nil { + t.Fatalf("status request failed: %s", err) + } else if err = json.Unmarshal(respBytes, &response); err != nil { + t.Fatalf("failed to unmarshal status request: %s", err) + } else if response.Status != "ok" { + t.Fatalf("status request failed: %s", response.Status) + } else if err = json.Unmarshal(response.Response, &status); err != nil { + t.Fatalf("failed to unmarshal status response data: %s", err) + } else if status.Status != expected { + t.Fatalf("server delegation persistence should be %s but is %s", expected, status.Status) + } +} + +func restoreCheckLiveCount(t *testing.T, expected int) { + // Verify the vault is persisting. + summaryRequest := core.SummaryRequest{ + Name: createUserInput1.Name, + Password: createUserInput1.Password, + } + + var summary core.SummaryData + respBytes, _, err := post("summary", summaryRequest) + if err != nil { + t.Fatalf("summary request failed: %s", err) + } else if err = json.Unmarshal(respBytes, &summary); err != nil { + t.Fatalf("failed to unmarshal summary response data: %s", err) + } else if summary.Status != "ok" { + t.Fatalf("summary request failed: %s", summary.Status) + } else if len(summary.Live) != expected { + t.Fatalf("expected %d delegations to be live but have %d", expected, len(summary.Live)) + } +} + +func restoreCheckLiveUsers(t *testing.T, present, absent []string) { + // Verify the vault is persisting. + summaryRequest := core.SummaryRequest{ + Name: createUserInput1.Name, + Password: createUserInput1.Password, + } + + var summary core.SummaryData + respBytes, _, err := post("summary", summaryRequest) + if err != nil { + t.Fatalf("summary request failed: %s", err) + } else if err = json.Unmarshal(respBytes, &summary); err != nil { + t.Fatalf("failed to unmarshal summary response data: %s", err) + } else if summary.Status != "ok" { + t.Fatalf("summary request failed: %s", summary.Status) + } + + for _, user := range present { + if _, ok := summary.Live[user]; !ok { + t.Fatalf("%s should be in the active delegations, but isn't", user) + } + } + + for _, user := range absent { + if _, ok := summary.Live[user]; ok { + t.Fatalf("%s shouldn't be in the active delegations, but is", user) + } + } +} + +func beforeRestartRestore(t *testing.T, cfgPath, vaultPath string) { + cmd := restoreSetup(t, cfgPath, vaultPath) + defer teardown(t, cmd) + + // Create a vault/admin user and 2 normal users so there is data to work with. + if _, _, err := post("create", createVaultInput); err != nil { + t.Fatalf("failed to create the vault: %s", err) + } + if _, _, err := post("create-user", createUserInput1); err != nil { + t.Fatalf("couldn't create user %s: %s", createUserInput1.Name, err) + } + if _, _, err := post("create-user", createUserInput2); err != nil { + t.Fatalf("couldn't create user %s: %s", createUserInput2.Name, err) + } + if _, _, err := post("create-user", createUserInput3); err != nil { + t.Fatalf("couldn't create user %s: %s", createUserInput3.Name, err) + } + + // A newly-created vault with a valid persistence config + // should be persisting. + restoreCheckStatus(t, persist.Active) + + // Delegate two users. + if _, _, err := post("delegate", delegateInput2); err != nil { + t.Fatalf("failed to delegate for %s: %s", delegateInput2.Name, err) + } + if _, _, err := post("delegate", delegateInput3); err != nil { + t.Fatalf("failed to delegate for %s: %s", delegateInput3.Name, err) + } + + restoreCheckLiveCount(t, 2) + restoreCheckLiveUsers(t, []string{delegateInput2.Name, delegateInput3.Name}, + []string{createUserInput1.Name, createVaultInput.Name}) +} + +func afterRestartRestore(t *testing.T, cfgPath, vaultPath string) { + cmd := restoreSetup(t, cfgPath, vaultPath) + defer teardown(t, cmd) + + // An existing vault with a persisted delegation store should + // be inactive. + restoreCheckStatus(t, persist.Inactive) + + // Delegate a user who wasn't in the persisted delegation set. + if _, _, err := post("delegate", delegateInput1); err != nil { + t.Fatalf("Error delegating with user 1, %v", err) + } + + restoreCheckLiveCount(t, 1) + restoreCheckLiveUsers(t, []string{delegateInput1.Name}, + []string{createUserInput2.Name, delegateInput3.Name, createVaultInput.Name}) + + // Begin the restoration by delegating for a single user. + if _, _, err := post("restore", delegateInput1); err != nil { + t.Fatalf("restoration by user %s failed: %s", delegateInput1.Name, err) + } + + // The vault should not have been restored yet. + restoreCheckStatus(t, persist.Inactive) + restoreCheckLiveCount(t, 1) + restoreCheckLiveUsers(t, []string{delegateInput1.Name}, + []string{createUserInput2.Name, delegateInput3.Name, createVaultInput.Name}) + + // Delegate the second user, which should lead to restoring + // the delegations. + if _, _, err := post("restore", delegateInput4); err != nil { + t.Fatalf("restoration by user %s failed: %s", delegateInput4.Name, err) + } + + restoreCheckStatus(t, persist.Active) + restoreCheckLiveCount(t, 2) + restoreCheckLiveUsers(t, []string{delegateInput2.Name, delegateInput3.Name}, + []string{createUserInput1.Name, createVaultInput.Name}) +}