From fd3fdc63552a086dd4f74180a4fc2d067531a627 Mon Sep 17 00:00:00 2001 From: e Date: Tue, 4 Aug 2015 14:01:04 -0700 Subject: [PATCH] Add hipchat and ordering support to redoctober Supports MSP and requires several arguments to add hipchat integration to red october. RedOctober will then alert on creation of an order, any new delegation, or several other states. --- core/core.go | 234 ++++++++++++++++++++++++++++++++++++++--- cryptor/cryptor.go | 14 +-- hipchat/hipchat.go | 67 ++++++++++++ index.html | 195 ++++++++++++++++++++++++++++++++-- keycache/keycache.go | 35 +++++- order/order.go | 119 +++++++++++++++++++++ passvault/passvault.go | 23 +++- redoctober.go | 204 +++++++++++++++++++++++++++++++++-- 8 files changed, 851 insertions(+), 40 deletions(-) create mode 100644 hipchat/hipchat.go create mode 100644 order/order.go diff --git a/core/core.go b/core/core.go index 0362703..cad4034 100644 --- a/core/core.go +++ b/core/core.go @@ -9,16 +9,22 @@ import ( "errors" "fmt" "log" + "strconv" + "time" "github.com/cloudflare/redoctober/cryptor" + "github.com/cloudflare/redoctober/hipchat" "github.com/cloudflare/redoctober/keycache" + "github.com/cloudflare/redoctober/order" "github.com/cloudflare/redoctober/passvault" ) var ( - crypt cryptor.Cryptor - records passvault.Records - cache keycache.Cache + crypt cryptor.Cryptor + records passvault.Records + cache keycache.Cache + orders order.Orderer + alternateName string ) // Each of these structures corresponds to the JSON expected on the @@ -63,6 +69,7 @@ type PasswordRequest struct { Password string NewPassword string + HipchatName string } type EncryptRequest struct { @@ -105,6 +112,26 @@ type ExportRequest struct { Password string } +type OrderRequest struct { + Name string + Password string + Duration string + Uses string + Data []byte + Label string +} + +type OrderInfoRequest struct { + Name string + Password string + + OrderNum string +} +type OrderOutstandingRequest struct { + Name string + Password string +} + // These structures map the JSON responses that will be sent from the API type ResponseData struct { @@ -145,6 +172,26 @@ func jsonResponse(resp []byte) ([]byte, error) { return json.Marshal(ResponseData{Status: "ok", Response: resp}) } +func getAltNameFromName(alt, name string) (altName string, found bool) { + if passwordRecord, ok := records.Passwords[name]; ok { + if altName, ok := passwordRecord.AltNames[alt]; ok { + return altName, true + } + } + return "", false +} +func getAltNamesFromNames(alt string, names []string) map[string]string { + altNames := make(map[string]string) + for _, name := range names { + altName, found := getAltNameFromName(alt, name) + if !found { + altName = name + } + altNames[name] = altName + } + return altNames +} + // validateUser checks that the username and password passed in are // correct. If admin is true, the user must be an admin as well. func validateUser(name, password string, admin bool) error { @@ -168,8 +215,7 @@ func validateUser(name, password string, admin bool) error { return nil } -// validateName checks that the username and password pass the minimal -// validation check +// validateName checks that the username and password pass a validation test. func validateName(name, password string) error { if name == "" { return errors.New("User name must not be blank") @@ -182,23 +228,37 @@ func validateName(name, password string) error { } // Init reads the records from disk from a given path -func Init(path string) error { +func Init(ca, hcKey, hcRoom, hcHost, roHost string) error { var err error defer func() { if err != nil { log.Printf("core.init failed: %v", err) } else { - log.Printf("core.init success: path=%s", path) + log.Printf("core.init success: ca=%s", ca) } }() - if records, err = passvault.InitFrom(path); err != nil { - err = fmt.Errorf("failed to load password vault %s: %s", path, err) + if records, err = passvault.InitFrom(ca); err != nil { + err = fmt.Errorf("failed to load password vault %s: %s", ca, err) } + if hcKey != "" && hcRoom != "" && hcHost != "" { + alternateName = "HipchatName" + roomId, err := strconv.Atoi(hcRoom) + if err != nil { + return errors.New("core.init unable to use hipchat roomId provided") + } + orders.Hipchat = hipchat.HipchatClient{ + ApiKey: hcKey, + RoomId: roomId, + HcHost: hcHost, + RoHost: roHost, + } + } cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)} - crypt = cryptor.New(&records, &cache) + orders.PrepareOrders() + crypt = cryptor.New(&records, &cache, &orders) return err } @@ -351,6 +411,22 @@ func Delegate(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } + // Make sure we capture the number who have already delegated. + for _, delegatedUser := range s.Users { + for _, delegatedLabel := range s.Labels { + if orderKey, found := orders.FindOrder(delegatedUser, delegatedLabel); found { + order := orders.Orders[orderKey] + order.AdminsDelegated = append(order.AdminsDelegated, s.Name) + order.Delegated++ + orders.Orders[orderKey] = order + + // Notify the hipchat room that there was a new delegator + orders.NotifyDelegation(s.Name, delegatedLabel, delegatedUser, orderKey, s.Time) + + } + } + } + return jsonStatusOk() } @@ -422,7 +498,7 @@ func Password(jsonIn []byte) ([]byte, error) { } // add signed-in record to active set - err = records.ChangePassword(s.Name, s.Password, s.NewPassword) + err = records.ChangePassword(s.Name, s.Password, s.NewPassword, s.HipchatName) if err != nil { return jsonStatusError(err) } @@ -488,7 +564,7 @@ func ReEncrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - data, _, secure, err := crypt.Decrypt(s.Data, s.Name) + data, _, _, secure, err := crypt.Decrypt(s.Data, s.Name) if err != nil { return jsonStatusError(err) } @@ -532,7 +608,7 @@ func Decrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - data, names, secure, err := crypt.Decrypt(s.Data, s.Name) + data, allLabels, names, secure, err := crypt.Decrypt(s.Data, s.Name) if err != nil { return jsonStatusError(err) } @@ -548,6 +624,13 @@ func Decrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } + // Cleanup any orders that have been fulfilled and notify the room. + for _, label := range allLabels { + if orderKey, found := orders.FindOrder(s.Name, label); found { + delete(orders.Orders, orderKey) + orders.NotifyOrderFulfilled(s.Name, orderKey) + } + } return jsonResponse(out) } @@ -658,3 +741,128 @@ func Export(jsonIn []byte) ([]byte, error) { return jsonResponse(out) } + +// Order will request delegations from other users. +func Order(jsonIn []byte) (out []byte, err error) { + var o OrderRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + // Get the owners of the ciphertext. + owners, _, err := crypt.GetOwners(o.Data) + if err != nil { + jsonStatusError(err) + } + if o.Duration == "" { + err = errors.New("Duration required when placing an order.") + jsonStatusError(err) + } + if o.Uses == "" || o.Uses == "0" { + err = errors.New("Number of required uses necessary when placing an order.") + jsonStatusError(err) + } + cache.Refresh() + orderNum := order.GenerateNum() + + adminsDelegated, numDelegated := cache.DelegateStatus(o.Name, o.Label, owners) + duration, err := time.ParseDuration(o.Duration) + if err != nil { + jsonStatusError(err) + } + currentTime := time.Now() + expiryTime := currentTime.Add(duration) + ord := order.CreateOrder(o.Name, + o.Label, + orderNum, + currentTime, + expiryTime, + duration, + adminsDelegated, + owners, + numDelegated) + orders.Orders[orderNum] = ord + out, err = json.Marshal(ord) + + // Get a map to any alternative name we want to notify + altOwners := getAltNamesFromNames(alternateName, owners) + + // Let everyone on hipchat know there is a new order. + orders.NotifyNewOrder(o.Name, o.Duration, o.Label, o.Uses, orderNum, altOwners) + if err != nil { + return jsonStatusError(err) + } + return jsonResponse(out) +} + +// OrdersOutstanding will return a list of currently outstanding orders. +func OrdersOutstanding(jsonIn []byte) (out []byte, err error) { + var o OrderOutstandingRequest + + defer func() { + if err != nil { + log.Printf("core.ordersout failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.ordersout success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + out, err = json.Marshal(orders.Orders) + if err != nil { + return jsonStatusError(err) + } + return jsonResponse(out) +} + +// OrderInfo will return a list of currently outstanding order numbers. +func OrderInfo(jsonIn []byte) (out []byte, err error) { + var o OrderInfoRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + if ord, ok := orders.Orders[o.OrderNum]; ok { + if out, err = json.Marshal(ord); err != nil { + return jsonStatusError(err) + } else if len(out) == 0 { + return jsonStatusError(errors.New("No order with that number")) + } + + return jsonResponse(out) + } + return +} diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index fbdf556..26ed154 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -17,6 +17,7 @@ import ( "github.com/cloudflare/redoctober/keycache" "github.com/cloudflare/redoctober/msp" + "github.com/cloudflare/redoctober/order" "github.com/cloudflare/redoctober/padding" "github.com/cloudflare/redoctober/passvault" "github.com/cloudflare/redoctober/symcrypt" @@ -29,10 +30,11 @@ const ( type Cryptor struct { records *passvault.Records cache *keycache.Cache + orders *order.Orderer } -func New(records *passvault.Records, cache *keycache.Cache) Cryptor { - return Cryptor{records, cache} +func New(records *passvault.Records, cache *keycache.Cache, orders *order.Orderer) Cryptor { + return Cryptor{records, cache, orders} } // AccessStructure represents different possible access structures for @@ -508,14 +510,14 @@ func (c *Cryptor) Encrypt(in []byte, labels []string, access AccessStructure) (r } // Decrypt decrypts a file using the keys in the key cache. -func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, secure bool, err error) { +func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, labels, names []string, secure bool, err error) { // unwrap encrypted file var encrypted EncryptedData if err = json.Unmarshal(in, &encrypted); err != nil { return } if encrypted.Version != DEFAULT_VERSION && encrypted.Version != -1 { - return nil, nil, secure, errors.New("Unknown version") + return nil, nil, nil, secure, errors.New("Unknown version") } secure = encrypted.Version == -1 @@ -535,7 +537,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, return } if encrypted.VaultId != vaultId { - return nil, nil, secure, errors.New("Wrong vault") + return nil, nil, nil, secure, errors.New("Wrong vault") } // compute HMAC @@ -563,6 +565,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, aesCBC.CryptBlocks(clearData, encrypted.Data) resp, err = padding.RemovePadding(clearData) + labels = encrypted.Labels return } @@ -621,7 +624,6 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er addedNames[name] = true } } - predicate = encrypted.Predicate return diff --git a/hipchat/hipchat.go b/hipchat/hipchat.go new file mode 100644 index 0000000..0d1ddc2 --- /dev/null +++ b/hipchat/hipchat.go @@ -0,0 +1,67 @@ +package hipchat + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strconv" +) + +const ( + RedBackground = "red" + YellowBackground = "yellow" + GreenBackground = "green" + GrayBackground = "gray" + RandomBackground = "random" + PurpleBackground = "purple" +) + +type HipchatClient struct { + RoomId int + ApiKey string + HcHost string + RoHost string +} + +func NewClient() *HipchatClient { + return &HipchatClient{} +} +func (h *HipchatClient) Notify(msg, color string) error { + if h.ApiKey == "" { + return errors.New("ApiKey unset") + } + msgBody := map[string]interface{}{ + "message": msg, + "color": color, + "notify": true, + } + + body, err := json.Marshal(msgBody) + if err != nil { + return err + } + roomId := url.QueryEscape(strconv.Itoa(h.RoomId)) + hipchatUrl := fmt.Sprintf("https://%s/v2/room/%s/notification?auth_token=%s", h.HcHost, roomId, h.ApiKey) + req, err := http.NewRequest("POST", hipchatUrl, bytes.NewReader(body)) + + req.Header.Add("Content-Type", "application/json") + + Client := http.Client{} + resp, err := Client.Do(req) + if err != nil { + log.Printf("Could not post to hipchat for the reason %s", err.Error()) + return err + } + + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("Could not post to hipchat for the reason %s", err.Error()) + return err + } + return nil +} diff --git a/index.html b/index.html index ea9ef7e..a0c3f46 100644 --- a/index.html +++ b/index.html @@ -164,7 +164,7 @@
-

Change password

+

Change account

@@ -172,16 +172,18 @@
- +
- +
- - + + + +
@@ -309,6 +311,93 @@
+
+
+
+

Create Order

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ +
+
+
+
+
+
+

Order Info

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+

Outstanding Orders

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+