Merge pull request #106 from ejcx/ordermsp

Add hipchat and ordering support to redoctober
This commit is contained in:
Nick Sullivan
2016-01-29 15:57:16 -08:00
14 changed files with 1489 additions and 66 deletions

View File

@@ -247,6 +247,87 @@ Example input JSON format:
-d '{"Name":"Alice","Password":"Lewis"}'
{"Status":"ok"}
### Order
Order creates a new order and lets other users know delegations are needed.
Example input JSON format:
$ curl --cacert server/server.crt https://localhost:8080/order \
-d '{"Name":"Alice","Password":"Lewis","Labels": ["Blue","Red"],\
"Duration":"1h","Uses":5,"EncryptedData":"ABCDE=="}'
{
"Admins": [
"Bob",
"Eve"
],
"AdminsDelegated": null,
"Delegated": 0,
"DurationRequested": 3.6e+12,
"Labels": [
"blue",
"red"
],
"Name": "Alice",
"Num": "77da1cfd8962fb9685c15c84",
"TimeRequested": "2016-01-25T15:58:41.961906679-08:00",
}
### Orders Outstanding
Orders Outstanding will return a list of current order numbers
Example input JSON format:
$ curl --cacert server/server.crt https://localhost:8080/orderout
-d '{"Name":"Alice","Password":"Lewis"}'
{
"77da1cfd8962fb9685c15c84":{
"Name":"Alice",
"Num":"77da1cfd8962fb9685c15c84",
"TimeRequested":"2016-01-25T15:58:41.961906679-08:00",
"DurationRequested":3600000000000,
"Delegated":0,"
AdminsDelegated":null,
"Admins":["Bob, Eve"],
"Labels":["Blue","Red"]
}
}
### Order Information
Example input JSON format:
$ curl --cacert server/server.crt https://localhost:8080/orderinfo
-d '{"Name":"Alice","Password":"Lewis", \
"OrderNum":"77da1cfd8962fb9685c15c84"}'
{
"Admins": [
"Bob",
"Eve"
],
"AdminsDelegated": null,
"Delegated": 0,
"DurationRequested": 3.6e+12,
"Labels": [
"blue",
"red"
],
"Name": "Alice",
"Num": "77da1cfd8962fb9685c15c84",
"TimeRequested": "2016-01-25T15:58:41.961906679-08:00"
}
### Order Cancel
Example input JSON format:
$ curl --cacert server/server.crt https://localhost:8080/orderinfo
-d '{"Name":"Alice","Password":"Lewis", \
"OrderNum":"77da1cfd8962fb9685c15c84"}'
{"Status":"ok"}
### Web interface
You can build a web interface to manage the Red October service using

View File

@@ -283,3 +283,63 @@ func (c *RemoteServer) Password(req []byte) (*core.ResponseData, error) {
return unmarshalResponseData(respBytes)
}
// Order issues an order request to the remote server
func (c *RemoteServer) Order(req core.OrderRequest) (*core.ResponseData, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.doAction("order", reqBytes)
if err != nil {
return nil, err
}
return unmarshalResponseData(respBytes)
}
// OrderOutstanding issues an order outstanding request to the remote server
func (c *RemoteServer) OrderOutstanding(req core.OrderOutstandingRequest) (*core.ResponseData, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.doAction("orderout", reqBytes)
if err != nil {
return nil, err
}
return unmarshalResponseData(respBytes)
}
// OrderInfo issues an order info request to the remote server
func (c *RemoteServer) OrderInfo(req core.OrderInfoRequest) (*core.ResponseData, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.doAction("orderinfo", reqBytes)
if err != nil {
return nil, err
}
return unmarshalResponseData(respBytes)
}
// OrderCancel issues an order cancel request to the remote server
func (c *RemoteServer) OrderCancel(req core.OrderInfoRequest) (*core.ResponseData, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.doAction("ordercancel", reqBytes)
if err != nil {
return nil, err
}
return unmarshalResponseData(respBytes)
}

View File

@@ -9,10 +9,12 @@ import (
"log"
"os"
"strings"
"time"
"github.com/cloudflare/redoctober/client"
"github.com/cloudflare/redoctober/cmd/ro/gopass"
"github.com/cloudflare/redoctober/core"
"github.com/cloudflare/redoctober/order"
)
var action, user, pswd, userEnv, pswdEnv, server, caPath string
@@ -21,7 +23,9 @@ var owners, lefters, righters, inPath, labels, outPath, outEnv string
var uses int
var time, users string
var duration, users string
var pollInterval time.Duration
type command struct {
Run func()
@@ -37,6 +41,7 @@ var commandSet = map[string]command{
"encrypt": command{Run: runEncrypt, Desc: "encrypt a file"},
"decrypt": command{Run: runDecrypt, Desc: "decrypt a file"},
"re-encrypt": command{Run: runReEncrypt, Desc: "re-encrypt a file"},
"order": command{Run: runOrder, Desc: "place an order for delegations"},
}
func registerFlags() {
@@ -45,7 +50,7 @@ func registerFlags() {
flag.StringVar(&owners, "owners", "", "comma separated owner list")
flag.StringVar(&users, "users", "", "comma separated user list")
flag.IntVar(&uses, "uses", 0, "number of delegated key uses")
flag.StringVar(&time, "time", "0h", "duration of delegated key uses")
flag.StringVar(&duration, "time", "0h", "duration of delegated key uses")
flag.StringVar(&lefters, "left", "", "comma separated left owners")
flag.StringVar(&righters, "right", "", "comma separated right owners")
flag.StringVar(&labels, "labels", "", "comma separated labels")
@@ -56,6 +61,7 @@ func registerFlags() {
flag.StringVar(&pswd, "password", "", "password")
flag.StringVar(&userEnv, "userenv", "RO_USER", "env variable for user name")
flag.StringVar(&pswdEnv, "pswdenv", "RO_PASS", "env variable for user password")
flag.DurationVar(&pollInterval, "poll-interval", time.Second, "interval for polling an outstanding order (set 0 to disable polling)")
}
func getUserCredentials() {
@@ -99,7 +105,7 @@ func runDelegate() {
Name: user,
Password: pswd,
Uses: uses,
Time: time,
Time: duration,
Users: processCSL(users),
Labels: processCSL(labels),
}
@@ -207,6 +213,34 @@ func runDecrypt() {
ioutil.WriteFile(outPath, msg.Data, 0644)
}
func runOrder() {
req := core.OrderRequest{
Name: user,
Password: pswd,
Uses: uses,
Duration: duration,
Labels: processCSL(labels),
Users: processCSL(users),
}
resp, err := roServer.Order(req)
processError(err)
var o order.Order
err = json.Unmarshal(resp.Response, &o)
processError(err)
if pollInterval > 0 {
for o.Delegated < 2 {
time.Sleep(pollInterval)
resp, err = roServer.OrderInfo(core.OrderInfoRequest{Name: user, Password: pswd, OrderNum: o.Num})
processError(err)
err = json.Unmarshal(resp.Response, &o)
processError(err)
}
}
fmt.Println(resp.Status)
}
func main() {
flag.Usage = func() {
fmt.Println("Usage: ro [options] subcommand")

View File

@@ -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
@@ -53,9 +58,10 @@ type DelegateRequest struct {
}
type CreateUserRequest struct {
Name string
Password string
UserType string
Name string
Password string
UserType string
HipchatName string
}
type PasswordRequest struct {
@@ -63,6 +69,7 @@ type PasswordRequest struct {
Password string
NewPassword string
HipchatName string
}
type EncryptRequest struct {
@@ -106,6 +113,34 @@ type ExportRequest struct {
Password string
}
type OrderRequest struct {
Name string
Password string
Duration string
Uses int
Users []string
EncryptedData []byte
Labels []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 +204,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,7 +217,7 @@ func validateName(name, password string) error {
}
// Init reads the records from disk from a given path
func Init(path string) error {
func Init(path, hcKey, hcRoom, hcHost, roHost string) error {
var err error
defer func() {
@@ -198,6 +232,20 @@ func Init(path string) error {
err = fmt.Errorf("failed to load password vault %s: %s", path, 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 +400,32 @@ func Delegate(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
// Make sure we capture the number who have already delegated.
for _, delegatedUser := range s.Users {
if orderKey, found := orders.FindOrder(delegatedUser, s.Labels); found {
order := orders.Orders[orderKey]
// Don't re-add names to the list of people who have delegated. Instead
// just skip them but make sure we count their delegation
if len(order.OwnersDelegated) == 0 {
order.OwnersDelegated = append(order.OwnersDelegated, s.Name)
} else {
for _, ownerName := range order.OwnersDelegated {
if ownerName == s.Name {
continue
}
order.OwnersDelegated = append(order.OwnersDelegated, s.Name)
order.Delegated++
}
}
orders.Orders[orderKey] = order
// Notify the hipchat room that there was a new delegator
orders.NotifyDelegation(s.Name, delegatedUser, orderKey, s.Time, s.Labels)
}
}
return jsonStatusOk()
}
@@ -393,10 +467,13 @@ func CreateUser(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
if _, err = records.AddNewRecord(s.Name, s.Password, false, s.UserType); err != nil {
if _, err := records.AddNewRecord(s.Name, s.Password, false, s.UserType); err != nil {
return jsonStatusError(err)
}
if err = records.ChangePassword(s.Name, s.Password, "", s.HipchatName); err != nil {
return jsonStatusError(err)
}
return jsonStatusOk()
}
@@ -423,7 +500,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 +567,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 +612,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 +628,11 @@ func Decrypt(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
// Cleanup any orders that have been fulfilled and notify the room.
if orderKey, found := orders.FindOrder(s.Name, allLabels); found {
delete(orders.Orders, orderKey)
orders.NotifyOrderFulfilled(s.Name, orderKey)
}
return jsonResponse(out)
}
@@ -661,3 +743,162 @@ 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.EncryptedData)
if err != nil {
jsonStatusError(err)
}
if o.Duration == "" {
err = errors.New("Duration required when placing an order.")
jsonStatusError(err)
}
if o.Uses == 0 {
err = errors.New("Number of required uses necessary when placing an order.")
jsonStatusError(err)
}
cache.Refresh()
orderNum := order.GenerateNum()
if len(o.Users) == 0 {
err = errors.New("Must specify at least one user per order.")
jsonStatusError(err)
}
adminsDelegated, numDelegated := cache.DelegateStatus(o.Users[0], o.Labels, owners)
duration, err := time.ParseDuration(o.Duration)
if err != nil {
jsonStatusError(err)
}
currentTime := time.Now()
ord := order.CreateOrder(o.Name,
orderNum,
currentTime,
duration,
adminsDelegated,
owners,
o.Users,
o.Labels,
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.Duration, orderNum, o.Users, o.Labels, o.Uses, 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 jsonStatusError(errors.New("No order with that number"))
}
// 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.Creator {
delete(orders.Orders, o.OrderNum)
out = []byte("Successfully removed order")
return jsonResponse(out)
}
}
err = errors.New("Invalid Order Number")
return jsonStatusError(err)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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>
@@ -70,6 +71,8 @@
<label for="delegate-labels">Labels to allow <small>(comma separated)</small></label>
<input type="text" name="Labels" class="form-control" id="delegate-labels" placeholder="e.g. Blue, Red" />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="delegate-labels">Slot Name</label>
<input type="text" name="Slot" class="form-control" id="delegate-slot" placeholder="Afternoon" />
@@ -146,6 +149,12 @@
<input type="password" name="Password" class="form-control" id="create-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="create-user-hipchatname">Hipchat Name</label>
<input type="text" name="HipchatName" class="form-control" id="create-hipchatname" placeholder="HipchatName" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<label for="create-user-type">User Type</label>
@@ -164,7 +173,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,21 +181,25 @@
<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"/>
</div>
<div class="form-group">
<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>
<h3>Modify user</h3>
<h3>Admin Controls</h3>
<form id="user-modify" class="ro-user-modify" role="form" action="/modify" method="post">
<div class="feedback modify-feedback"></div>
@@ -309,6 +322,174 @@
</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 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>
<div class="form-group row">
<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="number" name="Uses" class="form-control" id="order-uses" placeholder="5" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="order-name-users">Users to allow <small>(comma separated)</small></label>
<input type="text" name="Users" class="form-control" id="order-name-users" placeholder="e.g. Alice, Bob" />
</div>
<div class="col-md-6">
<label for="order-label">Labels</label>
<input type="text" name="Labels" class="form-control" id="order-user-label" placeholder="Labels" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<label for="owners-data">Encrypted Data</label>
<textarea name="EncryptedData" class="form-control" id="owners-data" rows="5" required></textarea>
</div>
</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 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>
<div class="form-group row">
<div class="col-md-6">
<label for="orderinfo-order-num">Order Number</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>
</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>
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label for="ordercancel-order-num">Order Number</label>
<input type="text" name="OrderNum" class="form-control" id="ordercancel-user-label" placeholder="Order Number" required />
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Order Cancel</button>
</form>
</div>
</section>
<section class="row">
<div id="orderscancel" class="col-md-6">
<h3>Create Delegation Link</h3>
<form id="orderlink" class="ro-orderlink" role="form" action="#" method="post">
<div style="overflow-wrap: break-word;" class="feedback orderlink-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="orderlink-delegator">Delegator</label>
<input type="text" name="Name" class="form-control" id="orderlink-delegator" placeholder="User name"/>
</div>
<div class="col-md-6">
<label for="orderlink-labels">Labels</label>
<input type="text" name="labels" class="form-control" id="orderlink-labels" placeholder="Labels"/>
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="orderlink-duration">Duration</label>
<input type="text" name="duration" class="form-control" id="orderlink-duration" placeholder="1h 5m"/>
</div>
<div class="col-md-6">
<label for="orderlink-uses">Uses</label>
<input type="text" name="uses" class="form-control" id="orderlink-uses" placeholder="5"/>
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="orderlink-ordernum">Order Number</label>
<input type="text" name="ordernum" class="form-control" id="orderlink-ordernum" placeholder="d34db33f..."/>
</div>
<div class="col-md-6">
<label for="orderlink-delegatefor">Delegate For</label>
<input type="text" name="delegatefor" class="form-control" id="orderlink-delegatefor" placeholder="e.g. Alice, Bob"/>
</div>
</div>
<button type="submit" class="btn btn-primary">Create Link</button>
</div>
</form>
</div>
</section>
<hr />
</div>
<footer id="footer" class="footer">
@@ -327,7 +508,6 @@
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
@@ -364,7 +544,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 +605,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 +622,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 +636,13 @@
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+data.Name }) );
var msg = "Change password for ";
if (data.NewPassword != "" && data.HipchatName != "") {
msg = "Change Password and Hipchat Name for ";
} else if (data.NewPassword == "" && data.HipchatName != "") {
msg = "Change Hipchat Name for ";
}
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: msg+htmlspecialchars(data.Name) }) );
}
});
});
@@ -470,7 +656,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 +718,168 @@
}
});
});
// Create an order
$('body').on('submit', 'form#order', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
// Force uses to an integer
data.Uses = parseInt(data.Uses, 10);
data.Labels = data.Labels.split(',');
for(var i=0, l=data.Labels.length; i<l; i++){
data.Labels[i] = data.Labels[i].trim();
if (data.Labels[i] == "") { data.Labels.splice(i, 1); }
}
data.Users = data.Users.split(',');
for(var i=0, l=data.Users.length; i<l; i++){
data.Users[i] = data.Users[i].trim();
if (data.Users[i] == "") { data.Users.splice(i, 1); }
}
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 Number: '+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 = "";
for (var jj in respData) {
if (!jj)
continue;
if (!respData.hasOwnProperty(jj)) {
continue;
}
if (typeof(respData[jj]) == "object") {
msgText += "<p>"+htmlspecialchars(jj)+": "+htmlspecialchars(JSON.stringify(respData[jj]))+"</p>";
} else {
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 " + JSON.stringify(o.Labels) + " 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>' }) );
}
});
});
$('body').on('submit', 'form#orderlink', function(evt){
evt.preventDefault();
createLink();
});
// 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(decodeURIComponent(value));
}
}
function createLink() {
var delegator = decodeURIComponent(document.getElementById("orderlink-delegator").value);
var delegatee = decodeURIComponent(document.getElementById("orderlink-delegatefor").value);
var duration = decodeURIComponent(document.getElementById("orderlink-duration").value);
var orderNum = decodeURIComponent(document.getElementById("orderlink-ordernum").value);
var labels = decodeURIComponent(document.getElementById("orderlink-labels").value);
var uses = decodeURIComponent(document.getElementById("orderlink-uses").value);
var link = "https://" + document.location.host + "?delegator="+ delegator + "&delegatee="+ delegatee + "&label=" + labels + "&ordernum=" + orderNum + "&uses=" + uses + "&duration="+ duration;
$('.orderlink-feedback').empty().append(makeAlert({ type: 'success', message: '<p>'+htmlspecialchars(link)+'</p>' }) );
}
function htmlspecialchars(s) {
if (!isNaN(s)) {
return s;
}
s = s.replace('&', '&amp;');
s = s.replace('<', '&lt;');
s = s.replace('>', '&gt;');
s = s.replace('"', '&quot;');
s = s.replace("'", '&#x27;');
s = s.replace('/', '&#x2F;');
return s
}
});
</script>
</body>

View File

@@ -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,34 @@ 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, labels, 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
}
nameFound := false
for _, user := range use.Users {
if user == name {
nameFound = true
}
}
for _, ol := range use.Labels {
for _, il := range labels {
if ol == il {
if nameFound {
adminsDelegated = append(adminsDelegated, admin)
hasDelegated++
}
}
}
}
}
}
return
}

168
order/order.go Normal file
View File

@@ -0,0 +1,168 @@
// Package order manages the bookkeeping and utilies required
// for users to create an 'order' meaning they have requested
// delegations for a certian resource.
//
// Copyright (c) 2016 CloudFlare, Inc.
package order
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"time"
"github.com/cloudflare/redoctober/hipchat"
)
const (
NewOrder = "%s has created an order for the label %s. requesting %d 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 {
Creator string
Users []string
Num string
TimeRequested time.Time
DurationRequested time.Duration
Delegated int
OwnersDelegated []string
Owners []string
Labels []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, orderNum string, time time.Time, duration time.Duration, adminsDelegated, contacts, users, labels []string, numDelegated int) (ord Order) {
ord.Creator = name
ord.Num = orderNum
ord.Labels = labels
ord.TimeRequested = time
ord.DurationRequested = duration
ord.OwnersDelegated = adminsDelegated
ord.Owners = contacts
ord.Delegated = numDelegated
ord.Users = users
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(duration, orderNum string, names, labels []string, uses int, owners map[string]string) {
labelList := ""
for i, label := range labels {
if i == 0 {
labelList += label
} else {
// Never include spaces in something go URI encodes. Go will
// add a + to the string, instead of a %20
labelList += "," + label
}
}
nameList := ""
for i, name := range names {
if i == 0 {
nameList += name
} else {
// Never include spaces in something go URI encodes. Go will
// add a + to the string, instead of a %20
nameList += "," + name
}
}
n := fmt.Sprintf(NewOrder, nameList, labelList, uses, duration)
notify(o, n, hipchat.RedBackground)
for owner, hipchatName := range owners {
queryParams := url.Values{
"delegator": {owner},
"label": {labelList},
"duration": {duration},
"uses": {strconv.Itoa(uses)},
"ordernum": {orderNum},
"delegatee": {nameList},
}.Encode()
notify(o, fmt.Sprintf(NewOrderLink, hipchatName, o.Hipchat.RoHost, queryParams), hipchat.GreenBackground)
}
}
func (o *Orderer) NotifyDelegation(delegator, delegatee, orderNum, duration string, labels []string) {
labelList := ""
for i, label := range labels {
if i == 0 {
labelList += label
} else {
labelList += ", " + label
}
}
n := fmt.Sprintf(NewDelegation, delegator, labelList, 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(user string, labels []string) (string, bool) {
for key, order := range o.Orders {
foundLabel := false
foundUser := false
for _, orderUser := range order.Users {
if orderUser == user {
foundUser = true
}
}
if !foundUser {
continue
}
for _, ol := range order.Labels {
foundLabel = false
for _, il := range labels {
if il == ol {
foundLabel = true
}
}
}
if !foundLabel {
continue
}
return key, true
}
return "", false
}

View File

@@ -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,13 +371,23 @@ 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 !ok {
err = errors.New("Record not present")
return
}
if len(hipchatName) != 0 {
pr.AltNames["HipchatName"] = hipchatName
}
if len(newPassword) == 0 {
records.SetRecord(pr, name)
return records.WriteRecordsToDisk()
}
var keySalt []byte
if keySalt, err = symcrypt.MakeRandom(16); err != nil {
return
@@ -609,6 +626,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 {

View File

@@ -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)
}

View File

@@ -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 {
@@ -200,9 +204,9 @@ const usage = `Usage:
redoctober -static <path> -vaultpath <path> -addr <addr> -certs <path1>[,<path2>,...] -keys <path1>[,<path2>,...] [-ca <path>]
single-cert example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert.pem -keys cert.key
redoctober -vaultpath diskrecord.json -addr localhost:8081 -certs cert.pem -keys cert.key
multi-cert example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert1.pem,cert2.pem -keys cert1.key,cert2.key
redoctober -vaultpath diskrecord.json -addr localhost:8081 -certs cert1.pem,cert2.pem -keys cert1.key,cert2.key
`
func main() {
@@ -215,11 +219,15 @@ func main() {
var staticPath = flag.String("static", "", "Path to override built-in index.html")
var vaultPath = flag.String("vaultpath", "diskrecord.json", "Path to the the disk vault")
var addr = flag.String("addr", "localhost:8080", "Server and port separated by :")
var addr = flag.String("addr", "localhost:8081", "Server and port separated by :")
var useSystemdSocket = flag.Bool("systemdfds", false, "Use systemd socket activation to listen on a file. Useful for binding privileged sockets.")
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:8081)")
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>
@@ -344,6 +353,8 @@ var indexHtml = []byte(`<!DOCTYPE html>
<label for="delegate-labels">Labels to allow <small>(comma separated)</small></label>
<input type="text" name="Labels" class="form-control" id="delegate-labels" placeholder="e.g. Blue, Red" />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="delegate-labels">Slot Name</label>
<input type="text" name="Slot" class="form-control" id="delegate-slot" placeholder="Afternoon" />
@@ -420,6 +431,12 @@ var indexHtml = []byte(`<!DOCTYPE html>
<input type="password" name="Password" class="form-control" id="create-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="create-user-hipchatname">Hipchat Name</label>
<input type="text" name="HipchatName" class="form-control" id="create-hipchatname" placeholder="HipchatName" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<label for="create-user-type">User Type</label>
@@ -438,7 +455,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,21 +463,25 @@ 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"/>
</div>
<div class="form-group">
<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>
<h3>Modify user</h3>
<h3>Admin Controls</h3>
<form id="user-modify" class="ro-user-modify" role="form" action="/modify" method="post">
<div class="feedback modify-feedback"></div>
@@ -583,6 +604,174 @@ 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 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>
<div class="form-group row">
<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="number" name="Uses" class="form-control" id="order-uses" placeholder="5" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="order-name-users">Users to allow <small>(comma separated)</small></label>
<input type="text" name="Users" class="form-control" id="order-name-users" placeholder="e.g. Alice, Bob" />
</div>
<div class="col-md-6">
<label for="order-label">Labels</label>
<input type="text" name="Labels" class="form-control" id="order-user-label" placeholder="Labels" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<label for="owners-data">Encrypted Data</label>
<textarea name="EncryptedData" class="form-control" id="owners-data" rows="5" required></textarea>
</div>
</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 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>
<div class="form-group row">
<div class="col-md-6">
<label for="orderinfo-order-num">Order Number</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>
</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>
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label for="ordercancel-order-num">Order Number</label>
<input type="text" name="OrderNum" class="form-control" id="ordercancel-user-label" placeholder="Order Number" required />
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Order Cancel</button>
</form>
</div>
</section>
<section class="row">
<div id="orderscancel" class="col-md-6">
<h3>Create Delegation Link</h3>
<form id="orderlink" class="ro-orderlink" role="form" action="#" method="post">
<div style="overflow-wrap: break-word;" class="feedback orderlink-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="orderlink-delegator">Delegator</label>
<input type="text" name="Name" class="form-control" id="orderlink-delegator" placeholder="User name"/>
</div>
<div class="col-md-6">
<label for="orderlink-labels">Labels</label>
<input type="text" name="labels" class="form-control" id="orderlink-labels" placeholder="Labels"/>
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="orderlink-duration">Duration</label>
<input type="text" name="duration" class="form-control" id="orderlink-duration" placeholder="1h 5m"/>
</div>
<div class="col-md-6">
<label for="orderlink-uses">Uses</label>
<input type="text" name="uses" class="form-control" id="orderlink-uses" placeholder="5"/>
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="orderlink-ordernum">Order Number</label>
<input type="text" name="ordernum" class="form-control" id="orderlink-ordernum" placeholder="d34db33f..."/>
</div>
<div class="col-md-6">
<label for="orderlink-delegatefor">Delegate For</label>
<input type="text" name="delegatefor" class="form-control" id="orderlink-delegatefor" placeholder="e.g. Alice, Bob"/>
</div>
</div>
<button type="submit" class="btn btn-primary">Create Link</button>
</div>
</form>
</div>
</section>
<hr />
</div>
<footer id="footer" class="footer">
@@ -601,7 +790,6 @@ var indexHtml = []byte(`<!DOCTYPE html>
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
@@ -638,7 +826,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 +887,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 +904,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 +918,13 @@ 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 }) );
var msg = "Change password for ";
if (data.NewPassword != "" && data.HipchatName != "") {
msg = "Change Password and Hipchat Name for ";
} else if (data.NewPassword == "" && data.HipchatName != "") {
msg = "Change Hipchat Name for ";
}
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: msg+htmlspecialchars(data.Name) }) );
}
});
});
@@ -744,7 +938,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 +1000,168 @@ var indexHtml = []byte(`<!DOCTYPE html>
}
});
});
// Create an order
$('body').on('submit', 'form#order', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
// Force uses to an integer
data.Uses = parseInt(data.Uses, 10);
data.Labels = data.Labels.split(',');
for(var i=0, l=data.Labels.length; i<l; i++){
data.Labels[i] = data.Labels[i].trim();
if (data.Labels[i] == "") { data.Labels.splice(i, 1); }
}
data.Users = data.Users.split(',');
for(var i=0, l=data.Users.length; i<l; i++){
data.Users[i] = data.Users[i].trim();
if (data.Users[i] == "") { data.Users.splice(i, 1); }
}
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 Number: '+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 = "";
for (var jj in respData) {
if (!jj)
continue;
if (!respData.hasOwnProperty(jj)) {
continue;
}
if (typeof(respData[jj]) == "object") {
msgText += "<p>"+htmlspecialchars(jj)+": "+htmlspecialchars(JSON.stringify(respData[jj]))+"</p>";
} else {
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 " + JSON.stringify(o.Labels) + " 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>' }) );
}
});
});
$('body').on('submit', 'form#orderlink', function(evt){
evt.preventDefault();
createLink();
});
// 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(decodeURIComponent(value));
}
}
function createLink() {
var delegator = decodeURIComponent(document.getElementById("orderlink-delegator").value);
var delegatee = decodeURIComponent(document.getElementById("orderlink-delegatefor").value);
var duration = decodeURIComponent(document.getElementById("orderlink-duration").value);
var orderNum = decodeURIComponent(document.getElementById("orderlink-ordernum").value);
var labels = decodeURIComponent(document.getElementById("orderlink-labels").value);
var uses = decodeURIComponent(document.getElementById("orderlink-uses").value);
var link = "https://" + document.location.host + "?delegator="+ delegator + "&delegatee="+ delegatee + "&label=" + labels + "&ordernum=" + orderNum + "&uses=" + uses + "&duration="+ duration;
$('.orderlink-feedback').empty().append(makeAlert({ type: 'success', message: '<p>'+htmlspecialchars(link)+'</p>' }) );
}
function htmlspecialchars(s) {
if (!isNaN(s)) {
return s;
}
s = s.replace('&', '&amp;');
s = s.replace('<', '&lt;');
s = s.replace('>', '&gt;');
s = s.replace('"', '&quot;');
s = s.replace("'", '&#x27;');
s = s.replace('/', '&#x2F;');
return s
}
});
</script>
</body>