mirror of
https://github.com/cloudflare/redoctober.git
synced 2026-01-06 21:47:37 +00:00
Add hipchat and ordering support to redoctober. Also fix XSS in RO
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.
This commit is contained in:
242
core/core.go
242
core/core.go
@@ -9,9 +9,13 @@ 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"
|
||||
)
|
||||
|
||||
@@ -19,6 +23,7 @@ var (
|
||||
crypt cryptor.Cryptor
|
||||
records passvault.Records
|
||||
cache keycache.Cache
|
||||
orders order.Orderer
|
||||
)
|
||||
|
||||
// Each of these structures corresponds to the JSON expected on the
|
||||
@@ -63,6 +68,7 @@ type PasswordRequest struct {
|
||||
Password string
|
||||
|
||||
NewPassword string
|
||||
HipchatName string
|
||||
}
|
||||
|
||||
type EncryptRequest struct {
|
||||
@@ -106,6 +112,33 @@ 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
|
||||
}
|
||||
|
||||
type OrderCancelRequest struct {
|
||||
Name string
|
||||
Password string
|
||||
|
||||
OrderNum string
|
||||
}
|
||||
|
||||
// These structures map the JSON responses that will be sent from the API
|
||||
|
||||
type ResponseData struct {
|
||||
@@ -169,8 +202,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")
|
||||
@@ -183,21 +215,35 @@ 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)
|
||||
}
|
||||
|
||||
var hipchatClient hipchat.HipchatClient
|
||||
if hcKey != "" && hcRoom != "" && hcHost != "" {
|
||||
roomId, err := strconv.Atoi(hcRoom)
|
||||
if err != nil {
|
||||
return errors.New("core.init unable to use hipchat roomId provided")
|
||||
}
|
||||
hipchatClient = hipchat.HipchatClient{
|
||||
ApiKey: hcKey,
|
||||
RoomId: roomId,
|
||||
HcHost: hcHost,
|
||||
RoHost: roHost,
|
||||
}
|
||||
}
|
||||
orders = order.NewOrderer(hipchatClient)
|
||||
cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)}
|
||||
crypt = cryptor.New(&records, &cache)
|
||||
|
||||
@@ -352,6 +398,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()
|
||||
}
|
||||
|
||||
@@ -423,7 +485,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)
|
||||
}
|
||||
@@ -490,7 +552,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)
|
||||
}
|
||||
@@ -535,7 +597,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)
|
||||
}
|
||||
@@ -551,6 +613,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)
|
||||
}
|
||||
|
||||
@@ -661,3 +730,158 @@ 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 := records.GetAltNamesFromName(orders.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
|
||||
}
|
||||
|
||||
// OrderCancel will cancel an order given an order num
|
||||
func OrderCancel(jsonIn []byte) (out []byte, err error) {
|
||||
var o OrderCancelRequest
|
||||
|
||||
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 o.Name == ord.Name {
|
||||
delete(orders.Orders, o.OrderNum)
|
||||
out = []byte("Successfully removed order")
|
||||
return jsonResponse(out)
|
||||
}
|
||||
}
|
||||
err = errors.New("Invalid Order Number")
|
||||
return jsonStatusError(err)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func TestCreate(t *testing.T) {
|
||||
createJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}")
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
respJson, err := Create(createJson)
|
||||
if err != nil {
|
||||
@@ -66,7 +66,7 @@ func TestSummary(t *testing.T) {
|
||||
t.Fatalf("Error in summary of account with no vault, %v", s.Status)
|
||||
}
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
// check for summary of initialized vault
|
||||
respJson, err = Create(createJson)
|
||||
@@ -220,7 +220,7 @@ func TestCreateUser(t *testing.T) {
|
||||
createUserECCJson := []byte("{\"Name\":\"Cat\",\"Password\":\"Cheshire\",\"UserType\":\"ECC\"}")
|
||||
createVaultJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}")
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
// Check that users cannot be created before a vault is
|
||||
respJson, err := CreateUser(createUserJson)
|
||||
@@ -292,7 +292,7 @@ func TestPassword(t *testing.T) {
|
||||
delegateJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Olleh\",\"Time\":\"2h\",\"Uses\":1}")
|
||||
passwordJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Olleh\",\"NewPassword\":\"Hello\"}")
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
// check for summary of initialized vault with new member
|
||||
var s ResponseData
|
||||
@@ -405,7 +405,7 @@ func TestEncryptDecrypt(t *testing.T) {
|
||||
encryptJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Minimum\":2,\"Owners\":[\"Alice\",\"Bob\",\"Carol\"],\"Data\":\"SGVsbG8gSmVsbG8=\",\"Labels\":[\"blue\",\"red\"]}")
|
||||
encryptJson3 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Minimum\":1,\"Owners\":[\"Alice\"],\"Data\":\"SGVsbG8gSmVsbG8=\"}")
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
// check for summary of initialized vault with new member
|
||||
var s ResponseData
|
||||
@@ -632,7 +632,7 @@ func TestReEncrypt(t *testing.T) {
|
||||
delegateJson7 := []byte(`{"Name":"Carol","Password":"Hello","Time":"10s","Uses":2,"Users":["Alice"],"Labels":["red"]}`)
|
||||
encryptJson := []byte(`{"Name":"Carol","Password":"Hello","Minimum":2,"Owners":["Alice","Bob","Carol"],"Data":"SGVsbG8gSmVsbG8=","Labels":["blue"]}`)
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
// check for summary of initialized vault with new member
|
||||
var s ResponseData
|
||||
@@ -812,7 +812,7 @@ func TestOwners(t *testing.T) {
|
||||
var s ResponseData
|
||||
var l OwnersData
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
Create(delegateJson)
|
||||
Delegate(delegateJson2)
|
||||
@@ -868,7 +868,7 @@ func TestModify(t *testing.T) {
|
||||
modifyJson4 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"ToModify\":\"Alice\",\"Command\":\"revoke\"}")
|
||||
modifyJson5 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"ToModify\":\"Alice\",\"Command\":\"delete\"}")
|
||||
|
||||
Init("memory")
|
||||
Init("memory", "", "", "", "")
|
||||
|
||||
// check for summary of initialized vault with new member
|
||||
var s ResponseData
|
||||
@@ -1059,7 +1059,7 @@ func TestStatic(t *testing.T) {
|
||||
t.Fatalf("Error closing file, %v", err)
|
||||
}
|
||||
|
||||
Init("/tmp/db1.json")
|
||||
Init("/tmp/db1.json", "", "", "", "")
|
||||
|
||||
// check for summary of initialized vault with new member
|
||||
var s ResponseData
|
||||
|
||||
@@ -524,14 +524,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
|
||||
@@ -551,7 +551,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
|
||||
@@ -579,6 +579,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
|
||||
}
|
||||
|
||||
@@ -637,7 +638,6 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er
|
||||
addedNames[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
predicate = encrypted.Predicate
|
||||
|
||||
return
|
||||
|
||||
@@ -83,7 +83,6 @@ func TestDuplicates(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
c := Cryptor{&records, &cache}
|
||||
|
||||
for _, name := range names {
|
||||
@@ -113,7 +112,7 @@ func TestDuplicates(t *testing.T) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
_, _, _, err := c.Decrypt(resp, name)
|
||||
_, _, _, _, err := c.Decrypt(resp, name)
|
||||
if err == nil {
|
||||
t.Fatalf("That shouldn't have worked!")
|
||||
}
|
||||
|
||||
67
hipchat/hipchat.go
Normal file
67
hipchat/hipchat.go
Normal file
@@ -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,
|
||||
"notify": "true",
|
||||
"color": color,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
272
index.html
272
index.html
@@ -27,6 +27,7 @@
|
||||
<li><a href="#encrypt">Encrypt</a></li>
|
||||
<li><a href="#decrypt">Decrypt</a></li>
|
||||
<li><a href="#owners">Owners</a></li>
|
||||
<li><a href="#orders">Order</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +165,7 @@
|
||||
|
||||
<section class="row">
|
||||
<div id="change-password" class="col-md-6">
|
||||
<h3>Change password</h3>
|
||||
<h3>Change account</h3>
|
||||
|
||||
<form id="user-change-password" class="ro-user-change-password" role="form" action="/password" method="post">
|
||||
<div class="feedback change-password-feedback"></div>
|
||||
@@ -172,16 +173,18 @@
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="user-name">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required />
|
||||
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="user-pass">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password" required />
|
||||
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password"/ required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="user-pass">New password</label>
|
||||
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New password" required />
|
||||
<label for="user-pass">New password. Blank for no change.</label>
|
||||
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New Password"/>
|
||||
<label for="user-email">Hipchat Name. Blank for no change.</label>
|
||||
<input type="text" name="HipchatName" class="form-control" id="user-hipchatname" placeholder="New Hipchat Name"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Change password</button>
|
||||
</form>
|
||||
@@ -309,6 +312,120 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="row">
|
||||
<div id="orders" class="col-md-6">
|
||||
<h3>Create Order</h3>
|
||||
|
||||
<form id="order" class="ro-user-order" role="form" action="/order" method="post">
|
||||
<div class="feedback order-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="order-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="order-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-user-pass">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="order-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-label">Label</label>
|
||||
<input type="text" name="Label" class="form-control" id="order-user-label" placeholder="Label" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-duration">Duration</label>
|
||||
<input type="text" name="Duration" class="form-control" id="order-duration" placeholder="Duration (e.g., 2h34m)" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-uses">Uses</label>
|
||||
<input type="text" name="Uses" class="form-control" id="order-uses" placeholder="Uses" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="owners-data">Data</label>
|
||||
<textarea name="Data" class="form-control" id="owners-data" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create Order</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="row">
|
||||
<div id="ordersinfo" class="col-md-6">
|
||||
<h3>Order Info</h3>
|
||||
|
||||
<form id="orderinfo" class="ro-user-order" role="form" action="/orderinfo" method="post">
|
||||
<div style="overflow-wrap: break-word;" class="feedback orderinfo-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="orderinfo-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="orderinfo-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="orderinfo-user-admin">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="orderinfo-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="orderinfo-order-num">Order Num</label>
|
||||
<input type="text" name="OrderNum" class="form-control" id="orderinfo-user-label" placeholder="Order Number" required />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Order Info</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="row">
|
||||
<div id="ordersout" class="col-md-6">
|
||||
<h3>Outstanding Orders</h3>
|
||||
|
||||
<form id="orderout" class="ro-user-order" role="form" action="/orderout" method="post">
|
||||
<div style="overflow-wrap: break-word;" class="feedback ordersout-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="ordersout-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="ordersout-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ordersout-user-admin">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="ordersout-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Outstanding Orders</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section class="row">
|
||||
<div id="orderscancel" class="col-md-6">
|
||||
<h3>Order Cancel</h3>
|
||||
|
||||
<form id="ordercancel" class="ro-user-order" role="form" action="/ordercancel" method="post">
|
||||
<div style="overflow-wrap: break-word;" class="feedback ordercancel-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="ordercancel-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="ordercancel-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ordercancel-user-admin">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="ordercancel-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ordercancel-order-num">Order Num</label>
|
||||
<input type="text" name="OrderNum" class="form-control" id="ordercancel-user-label" placeholder="Order Number" required />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Order Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<footer id="footer" class="footer">
|
||||
@@ -327,7 +444,6 @@
|
||||
|
||||
function submit( $form, options ){
|
||||
options || (options = {});
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
data: JSON.stringify( options.data ),
|
||||
@@ -364,7 +480,7 @@
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+data.Name }) );
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -425,7 +541,7 @@
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+data.Name }) );
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -442,7 +558,7 @@
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+data.Name }) );
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -456,7 +572,7 @@
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+data.Name }) );
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -470,7 +586,7 @@
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+data.ToModify }) );
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+htmlspecialchars(data.ToModify) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -532,6 +648,140 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
// Create an order
|
||||
$('body').on('submit', 'form#order', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = JSON.parse(window.atob(d.Response));
|
||||
$form.find('.feedback').empty().append(
|
||||
makeAlert({ type: 'success', message: '<p>Order Num: '+d.Num+'</p>' }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
// Get order info
|
||||
$('body').on('submit', 'form#orderinfo', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = window.atob(d.Response);
|
||||
try {
|
||||
var respData = JSON.parse(d);
|
||||
var msgText = ""
|
||||
alert(msgText);
|
||||
for (var jj in respData) {
|
||||
if (!jj || jj == "Admins")
|
||||
continue;
|
||||
if (!respData.hasOwnProperty(jj)) {
|
||||
continue;
|
||||
}
|
||||
alert(msgText);
|
||||
msgText += "<p>"+htmlspecialchars(jj)+": "+htmlspecialchars(respData[jj])+"</p>";
|
||||
}
|
||||
$form.find('.feedback').empty().append(makeAlert({ type: 'success', message: msgText }));
|
||||
} catch (e) {
|
||||
makeAlert({ type: 'failure', message: '<p>Invalid JSON returned</p>' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Get outstanding order info
|
||||
$('body').on('submit', 'form#orderout', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = JSON.parse(window.atob(d.Response));
|
||||
ordout = "";
|
||||
for (var jj in d){
|
||||
if (!d.hasOwnProperty(jj))
|
||||
continue;
|
||||
var o = d[jj];
|
||||
ordout += o.Name + " requesting " + o.Label + " has " + o.Delegated + "\n";
|
||||
|
||||
}
|
||||
$form.find('.feedback').empty().append(
|
||||
makeAlert({ type: 'success', message: '<p>'+ordout+'</p>' }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
$('body').on('submit', 'form#ordercancel', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = window.atob(d.Response);
|
||||
$form.find('.feedback').empty().append(
|
||||
makeAlert({ type: 'success', message: '<p>'+d+'</p>' }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Init from query string if possible.
|
||||
var queryParams = document.location.search;
|
||||
var queryParts = queryParams.split('&');
|
||||
for (var i=0; i<queryParts.length; i++) {
|
||||
var part = queryParts[i];
|
||||
part = part.replace("?", "");
|
||||
var partPieces = part.split("=");
|
||||
if (partPieces.length != 2) {
|
||||
continue;
|
||||
}
|
||||
var setValue = null;
|
||||
var key = partPieces[0];
|
||||
var value = partPieces[1];
|
||||
switch (key) {
|
||||
case "delegator":
|
||||
setValue = $("#delegate-user");
|
||||
break;
|
||||
case "delegatee":
|
||||
setValue = $("#delegate-users");
|
||||
break;
|
||||
case "uses":
|
||||
setValue = $("#delegate-uses");
|
||||
break;
|
||||
case "label":
|
||||
setValue = $("#delegate-labels");
|
||||
break;
|
||||
case "duration":
|
||||
setValue = $("#delegate-user-time");
|
||||
break;
|
||||
case "ordernum":
|
||||
setValue = $("#delegate-slot");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (setValue) {
|
||||
setValue.val(value);
|
||||
}
|
||||
}
|
||||
function htmlspecialchars(s) {
|
||||
if (!isNaN(s)) {
|
||||
return s;
|
||||
}
|
||||
s = s.replace('&', '&');
|
||||
s = s.replace('<', '<');
|
||||
s = s.replace('>', '>');
|
||||
s = s.replace('"', '"');
|
||||
s = s.replace("'", ''');
|
||||
s = s.replace('/', '/');
|
||||
return s
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -40,8 +40,9 @@ type Usage struct {
|
||||
// ActiveUser holds the information about an actively delegated key.
|
||||
type ActiveUser struct {
|
||||
Usage
|
||||
Admin bool
|
||||
Type string
|
||||
AltNames map[string]string
|
||||
Admin bool
|
||||
Type string
|
||||
|
||||
rsaKey rsa.PrivateKey
|
||||
eccKey *ecdsa.PrivateKey
|
||||
@@ -305,3 +306,35 @@ func (cache *Cache) DecryptShares(in [][]byte, name, user string, labels []strin
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DelegateStatus will return a list of admins who have delegated to a particular user, for a particular label.
|
||||
// This is useful information to have when determining the status of an order and conveying order progress.
|
||||
func (cache *Cache) DelegateStatus(name string, label string, admins []string) (adminsDelegated []string, hasDelegated int) {
|
||||
// Iterate over the admins of the ciphertext to look for users
|
||||
// who have already delegated the label to the delegatee.
|
||||
for _, admin := range admins {
|
||||
for di, use := range cache.UserKeys {
|
||||
if di.Name != admin {
|
||||
continue
|
||||
}
|
||||
labelFound := false
|
||||
nameFound := false
|
||||
for _, user := range use.Users {
|
||||
if user == name {
|
||||
nameFound = true
|
||||
}
|
||||
}
|
||||
for _, l := range use.Labels {
|
||||
if l == label {
|
||||
labelFound = true
|
||||
}
|
||||
}
|
||||
if labelFound && nameFound {
|
||||
adminsDelegated = append(adminsDelegated, admin)
|
||||
hasDelegated++
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
119
order/order.go
Normal file
119
order/order.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/redoctober/hipchat"
|
||||
)
|
||||
|
||||
const (
|
||||
NewOrder = "%s has created an order for the label %s. requesting %s delegations for %s"
|
||||
NewOrderLink = "@%s - https://%s?%s"
|
||||
OrderFulfilled = "%s has had order %s fulfilled."
|
||||
NewDelegation = "%s has delegated the label %s to %s (per order %s) for %s"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
Name string
|
||||
Num string
|
||||
|
||||
TimeRequested time.Time
|
||||
ExpiryTime time.Time
|
||||
DurationRequested time.Duration
|
||||
Delegated int
|
||||
AdminsDelegated []string
|
||||
Admins []string
|
||||
Label string
|
||||
}
|
||||
|
||||
type OrderIndex struct {
|
||||
OrderFor string
|
||||
|
||||
OrderId string
|
||||
OrderOwners []string
|
||||
}
|
||||
|
||||
// Orders represents a mapping of Order IDs to Orders. This structure
|
||||
// is useful for looking up information about individual Orders and
|
||||
// whether or not an order has been fulfilled. Orders that have been
|
||||
// fulfilled will be removed from the structure.
|
||||
type Orderer struct {
|
||||
Orders map[string]Order
|
||||
Hipchat hipchat.HipchatClient
|
||||
AlternateName string
|
||||
}
|
||||
|
||||
func CreateOrder(name string, labels string, orderNum string, time time.Time, expiryTime time.Time, duration time.Duration, adminsDelegated, contacts []string, numDelegated int) (ord Order) {
|
||||
ord.Name = name
|
||||
ord.Num = orderNum
|
||||
ord.Label = labels
|
||||
ord.TimeRequested = time
|
||||
ord.ExpiryTime = expiryTime
|
||||
ord.DurationRequested = duration
|
||||
ord.AdminsDelegated = adminsDelegated
|
||||
ord.Admins = contacts
|
||||
ord.Delegated = numDelegated
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateNum() (num string) {
|
||||
b := make([]byte, 12)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// NewOrder will create a new map of Orders
|
||||
func NewOrderer(hipchatClient hipchat.HipchatClient) (o Orderer) {
|
||||
o.Orders = make(map[string]Order)
|
||||
o.Hipchat = hipchatClient
|
||||
o.AlternateName = "HipchatName"
|
||||
return
|
||||
}
|
||||
|
||||
// notify is a generic function for using a notifier, but it checks to make
|
||||
// sure that there is a notifier available, since there won't always be.
|
||||
func notify(o *Orderer, msg, color string) {
|
||||
o.Hipchat.Notify(msg, color)
|
||||
}
|
||||
func (o *Orderer) NotifyNewOrder(name, duration, label, uses, orderNum string, owners map[string]string) {
|
||||
n := fmt.Sprintf(NewOrder, name, label, uses, duration)
|
||||
notify(o, n, hipchat.RedBackground)
|
||||
for owner, hipchatName := range owners {
|
||||
queryParams := url.Values{
|
||||
"delegator": {owner},
|
||||
"label": {label},
|
||||
"duration": {duration},
|
||||
"uses": {uses},
|
||||
"ordernum": {orderNum},
|
||||
"delegatee": {name},
|
||||
}.Encode()
|
||||
notify(o, fmt.Sprintf(NewOrderLink, hipchatName, o.Hipchat.RoHost, queryParams), hipchat.GreenBackground)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orderer) NotifyDelegation(delegator, label, delegatee, orderNum, duration string) {
|
||||
n := fmt.Sprintf(NewDelegation, delegator, label, delegatee, orderNum, duration)
|
||||
notify(o, n, hipchat.YellowBackground)
|
||||
}
|
||||
func (o *Orderer) NotifyOrderFulfilled(name, orderNum string) {
|
||||
n := fmt.Sprintf(OrderFulfilled, name, orderNum)
|
||||
notify(o, n, hipchat.PurpleBackground)
|
||||
}
|
||||
|
||||
func (o *Orderer) FindOrder(name, label string) (string, bool) {
|
||||
for key, order := range o.Orders {
|
||||
if name != order.Name {
|
||||
continue
|
||||
}
|
||||
if label != order.Label {
|
||||
continue
|
||||
}
|
||||
|
||||
return key, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -85,7 +85,8 @@ type PasswordRecord struct {
|
||||
ECPrivIV []byte
|
||||
ECPublic ECPublicKey
|
||||
}
|
||||
Admin bool
|
||||
AltNames map[string]string
|
||||
Admin bool
|
||||
}
|
||||
|
||||
// diskRecords is the structure used to read and write a JSON file
|
||||
@@ -187,6 +188,8 @@ func createPasswordRec(password string, admin bool, userType string) (newRec Pas
|
||||
return
|
||||
}
|
||||
|
||||
newRec.AltNames = make(map[string]string)
|
||||
|
||||
// generate a key pair
|
||||
switch userType {
|
||||
case RSARecord:
|
||||
@@ -273,7 +276,6 @@ func InitFrom(path string) (records Records, err error) {
|
||||
// from the file.
|
||||
|
||||
records.Version = 0
|
||||
|
||||
if len(jsonDiskRecord) != 0 {
|
||||
if err = json.Unmarshal(jsonDiskRecord, &records); err != nil {
|
||||
return
|
||||
@@ -281,7 +283,12 @@ func InitFrom(path string) (records Records, err error) {
|
||||
}
|
||||
|
||||
err = errors.New("Format error")
|
||||
for _, rec := range records.Passwords {
|
||||
for k, rec := range records.Passwords {
|
||||
|
||||
if rec.AltNames == nil {
|
||||
rec.AltNames = make(map[string]string)
|
||||
records.Passwords[k] = rec
|
||||
}
|
||||
if len(rec.PasswordSalt) != 16 {
|
||||
return
|
||||
}
|
||||
@@ -364,8 +371,16 @@ func (records *Records) AddNewRecord(name, password string, admin bool, userType
|
||||
}
|
||||
|
||||
// ChangePassword changes the password for a given user.
|
||||
func (records *Records) ChangePassword(name, password, newPassword string) (err error) {
|
||||
func (records *Records) ChangePassword(name, password, newPassword, hipchatName string) (err error) {
|
||||
pr, ok := records.GetRecord(name)
|
||||
|
||||
if len(newPassword) == 0 {
|
||||
if len(hipchatName) != 0 {
|
||||
pr.AltNames["HipchatName"] = hipchatName
|
||||
}
|
||||
records.SetRecord(pr, name)
|
||||
return records.WriteRecordsToDisk()
|
||||
}
|
||||
if !ok {
|
||||
err = errors.New("Record not present")
|
||||
return
|
||||
@@ -609,6 +624,26 @@ func (pr *PasswordRecord) GetKeyRSA(password string) (key rsa.PrivateKey, err er
|
||||
|
||||
return
|
||||
}
|
||||
func (r *Records) GetAltNameFromName(alt, name string) (altName string, found bool) {
|
||||
if passwordRecord, ok := r.Passwords[name]; ok {
|
||||
if altName, ok := passwordRecord.AltNames[alt]; ok {
|
||||
return altName, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
func (r *Records) GetAltNamesFromName(alt string, names []string) map[string]string {
|
||||
altNames := make(map[string]string)
|
||||
for _, name := range names {
|
||||
altName, found := r.GetAltNameFromName(alt, name)
|
||||
if !found {
|
||||
altName = name
|
||||
}
|
||||
altNames[name] = altName
|
||||
}
|
||||
return altNames
|
||||
|
||||
}
|
||||
|
||||
// ValidatePassword returns an error if the password is incorrect.
|
||||
func (pr *PasswordRecord) ValidatePassword(password string) error {
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestChangePassword(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check changing the password for a non-existent user
|
||||
err = records.ChangePassword("user", "weakpassword", "newpassword")
|
||||
err = records.ChangePassword("user", "weakpassword", "newpassword", "")
|
||||
if err == nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func TestChangePassword(t *testing.T) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
err = records.ChangePassword("user", "weakpassword", "newpassword")
|
||||
err = records.ChangePassword("user", "weakpassword", "newpassword", "")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func TestChangePassword(t *testing.T) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
err = records.ChangePassword("user2", "weakpassword", "newpassword")
|
||||
err = records.ChangePassword("user2", "weakpassword", "newpassword", "")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
284
redoctober.go
284
redoctober.go
@@ -29,10 +29,10 @@ import (
|
||||
|
||||
var functions = map[string]func([]byte) ([]byte, error){
|
||||
"/create": core.Create,
|
||||
"/create-user": core.CreateUser,
|
||||
"/summary": core.Summary,
|
||||
"/purge": core.Purge,
|
||||
"/delegate": core.Delegate,
|
||||
"/create-user": core.CreateUser,
|
||||
"/password": core.Password,
|
||||
"/encrypt": core.Encrypt,
|
||||
"/re-encrypt": core.ReEncrypt,
|
||||
@@ -40,6 +40,10 @@ var functions = map[string]func([]byte) ([]byte, error){
|
||||
"/owners": core.Owners,
|
||||
"/modify": core.Modify,
|
||||
"/export": core.Export,
|
||||
"/order": core.Order,
|
||||
"/orderout": core.OrdersOutstanding,
|
||||
"/orderinfo": core.OrderInfo,
|
||||
"/ordercancel": core.OrderCancel,
|
||||
}
|
||||
|
||||
type userRequest struct {
|
||||
@@ -220,6 +224,10 @@ func main() {
|
||||
var certsPathString = flag.String("certs", "", "Path(s) of TLS certificate in PEM format, comma-separated")
|
||||
var keysPathString = flag.String("keys", "", "Path(s) of TLS private key in PEM format, comma-separated, must me in the same order as the certs")
|
||||
var caPath = flag.String("ca", "", "Path of TLS CA for client authentication (optional)")
|
||||
var hcKey = flag.String("hckey", "", "Hipchat API Key")
|
||||
var hcRoom = flag.String("hcroom", "", "Hipchat Room Id")
|
||||
var hcHost = flag.String("hchost", "", "Hipchat Url Base (ex: hipchat.com)")
|
||||
var roHost = flag.String("rohost", "", "RedOctober Url Base (ex: localhost:8080)")
|
||||
flag.Parse()
|
||||
|
||||
if *vaultPath == "" || *certsPathString == "" || *keysPathString == "" || (*addr == "" && *useSystemdSocket == false) {
|
||||
@@ -231,7 +239,7 @@ func main() {
|
||||
certPaths := strings.Split(*certsPathString, ",")
|
||||
keyPaths := strings.Split(*keysPathString, ",")
|
||||
|
||||
if err := core.Init(*vaultPath); err != nil {
|
||||
if err := core.Init(*vaultPath, *hcKey, *hcRoom, *hcHost, *roHost); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
@@ -301,6 +309,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
<li><a href="#encrypt">Encrypt</a></li>
|
||||
<li><a href="#decrypt">Decrypt</a></li>
|
||||
<li><a href="#owners">Owners</a></li>
|
||||
<li><a href="#orders">Order</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -438,7 +447,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
|
||||
<section class="row">
|
||||
<div id="change-password" class="col-md-6">
|
||||
<h3>Change password</h3>
|
||||
<h3>Change account</h3>
|
||||
|
||||
<form id="user-change-password" class="ro-user-change-password" role="form" action="/password" method="post">
|
||||
<div class="feedback change-password-feedback"></div>
|
||||
@@ -446,16 +455,18 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="user-name">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required />
|
||||
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="user-pass">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password" required />
|
||||
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password"/ required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="user-pass">New password</label>
|
||||
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New password" required />
|
||||
<label for="user-pass">New password. Blank for no change.</label>
|
||||
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New Password"/>
|
||||
<label for="user-email">Hipchat Name. Blank for no change.</label>
|
||||
<input type="text" name="HipchatName" class="form-control" id="user-hipchatname" placeholder="New Hipchat Name"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Change password</button>
|
||||
</form>
|
||||
@@ -583,6 +594,120 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="row">
|
||||
<div id="orders" class="col-md-6">
|
||||
<h3>Create Order</h3>
|
||||
|
||||
<form id="order" class="ro-user-order" role="form" action="/order" method="post">
|
||||
<div class="feedback order-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="order-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="order-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-user-pass">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="order-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-label">Label</label>
|
||||
<input type="text" name="Label" class="form-control" id="order-user-label" placeholder="Label" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-duration">Duration</label>
|
||||
<input type="text" name="Duration" class="form-control" id="order-duration" placeholder="Duration (e.g., 2h34m)" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="order-uses">Uses</label>
|
||||
<input type="text" name="Uses" class="form-control" id="order-uses" placeholder="Uses" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="owners-data">Data</label>
|
||||
<textarea name="Data" class="form-control" id="owners-data" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create Order</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="row">
|
||||
<div id="ordersinfo" class="col-md-6">
|
||||
<h3>Order Info</h3>
|
||||
|
||||
<form id="orderinfo" class="ro-user-order" role="form" action="/orderinfo" method="post">
|
||||
<div style="overflow-wrap: break-word;" class="feedback orderinfo-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="orderinfo-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="orderinfo-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="orderinfo-user-admin">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="orderinfo-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="orderinfo-order-num">Order Num</label>
|
||||
<input type="text" name="OrderNum" class="form-control" id="orderinfo-user-label" placeholder="Order Number" required />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Order Info</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="row">
|
||||
<div id="ordersout" class="col-md-6">
|
||||
<h3>Outstanding Orders</h3>
|
||||
|
||||
<form id="orderout" class="ro-user-order" role="form" action="/orderout" method="post">
|
||||
<div style="overflow-wrap: break-word;" class="feedback ordersout-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="ordersout-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="ordersout-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ordersout-user-admin">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="ordersout-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Outstanding Orders</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section class="row">
|
||||
<div id="orderscancel" class="col-md-6">
|
||||
<h3>Order Cancel</h3>
|
||||
|
||||
<form id="ordercancel" class="ro-user-order" role="form" action="/ordercancel" method="post">
|
||||
<div style="overflow-wrap: break-word;" class="feedback ordercancel-feedback"></div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="ordercancel-user-admin">User name</label>
|
||||
<input type="text" name="Name" class="form-control" id="ordercancel-user-admin" placeholder="User name" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ordercancel-user-admin">Password</label>
|
||||
<input type="password" name="Password" class="form-control" id="ordercancel-user-pass" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="ordercancel-order-num">Order Num</label>
|
||||
<input type="text" name="OrderNum" class="form-control" id="ordercancel-user-label" placeholder="Order Number" required />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Order Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<footer id="footer" class="footer">
|
||||
@@ -601,7 +726,6 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
|
||||
function submit( $form, options ){
|
||||
options || (options = {});
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
data: JSON.stringify( options.data ),
|
||||
@@ -638,7 +762,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+data.Name }) );
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -699,7 +823,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+data.Name }) );
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -716,7 +840,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+data.Name }) );
|
||||
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -730,7 +854,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+data.Name }) );
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+htmlspecialchars(data.Name) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -744,7 +868,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+data.ToModify }) );
|
||||
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+htmlspecialchars(data.ToModify) }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -806,6 +930,140 @@ var indexHtml = []byte(`<!DOCTYPE html>
|
||||
}
|
||||
});
|
||||
});
|
||||
// Create an order
|
||||
$('body').on('submit', 'form#order', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = JSON.parse(window.atob(d.Response));
|
||||
$form.find('.feedback').empty().append(
|
||||
makeAlert({ type: 'success', message: '<p>Order Num: '+d.Num+'</p>' }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
// Get order info
|
||||
$('body').on('submit', 'form#orderinfo', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = window.atob(d.Response);
|
||||
try {
|
||||
var respData = JSON.parse(d);
|
||||
var msgText = ""
|
||||
alert(msgText);
|
||||
for (var jj in respData) {
|
||||
if (!jj || jj == "Admins")
|
||||
continue;
|
||||
if (!respData.hasOwnProperty(jj)) {
|
||||
continue;
|
||||
}
|
||||
alert(msgText);
|
||||
msgText += "<p>"+htmlspecialchars(jj)+": "+htmlspecialchars(respData[jj])+"</p>";
|
||||
}
|
||||
$form.find('.feedback').empty().append(makeAlert({ type: 'success', message: msgText }));
|
||||
} catch (e) {
|
||||
makeAlert({ type: 'failure', message: '<p>Invalid JSON returned</p>' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Get outstanding order info
|
||||
$('body').on('submit', 'form#orderout', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = JSON.parse(window.atob(d.Response));
|
||||
ordout = "";
|
||||
for (var jj in d){
|
||||
if (!d.hasOwnProperty(jj))
|
||||
continue;
|
||||
var o = d[jj];
|
||||
ordout += o.Name + " requesting " + o.Label + " has " + o.Delegated + "\n";
|
||||
|
||||
}
|
||||
$form.find('.feedback').empty().append(
|
||||
makeAlert({ type: 'success', message: '<p>'+ordout+'</p>' }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
$('body').on('submit', 'form#ordercancel', function(evt){
|
||||
evt.preventDefault();
|
||||
var $form = $(evt.currentTarget),
|
||||
data = serialize($form);
|
||||
|
||||
submit( $form, {
|
||||
data : data,
|
||||
success : function(d){
|
||||
d = window.atob(d.Response);
|
||||
$form.find('.feedback').empty().append(
|
||||
makeAlert({ type: 'success', message: '<p>'+d+'</p>' }) );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Init from query string if possible.
|
||||
var queryParams = document.location.search;
|
||||
var queryParts = queryParams.split('&');
|
||||
for (var i=0; i<queryParts.length; i++) {
|
||||
var part = queryParts[i];
|
||||
part = part.replace("?", "");
|
||||
var partPieces = part.split("=");
|
||||
if (partPieces.length != 2) {
|
||||
continue;
|
||||
}
|
||||
var setValue = null;
|
||||
var key = partPieces[0];
|
||||
var value = partPieces[1];
|
||||
switch (key) {
|
||||
case "delegator":
|
||||
setValue = $("#delegate-user");
|
||||
break;
|
||||
case "delegatee":
|
||||
setValue = $("#delegate-users");
|
||||
break;
|
||||
case "uses":
|
||||
setValue = $("#delegate-uses");
|
||||
break;
|
||||
case "label":
|
||||
setValue = $("#delegate-labels");
|
||||
break;
|
||||
case "duration":
|
||||
setValue = $("#delegate-user-time");
|
||||
break;
|
||||
case "ordernum":
|
||||
setValue = $("#delegate-slot");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (setValue) {
|
||||
setValue.val(value);
|
||||
}
|
||||
}
|
||||
function htmlspecialchars(s) {
|
||||
if (!isNaN(s)) {
|
||||
return s;
|
||||
}
|
||||
s = s.replace('&', '&');
|
||||
s = s.replace('<', '<');
|
||||
s = s.replace('>', '>');
|
||||
s = s.replace('"', '"');
|
||||
s = s.replace("'", ''');
|
||||
s = s.replace('/', '/');
|
||||
return s
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user