Add hipchat and ordering support to redoctober

Supports MSP and requires several arguments to add hipchat integration to
red october. RedOctober will then alert on creation of an order, any new
delegation, or several other states.
This commit is contained in:
e
2015-08-04 14:01:04 -07:00
committed by Evan Johnson
parent f06cd674ba
commit fd3fdc6355
8 changed files with 851 additions and 40 deletions

View File

@@ -9,16 +9,22 @@ import (
"errors"
"fmt"
"log"
"strconv"
"time"
"github.com/cloudflare/redoctober/cryptor"
"github.com/cloudflare/redoctober/hipchat"
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/order"
"github.com/cloudflare/redoctober/passvault"
)
var (
crypt cryptor.Cryptor
records passvault.Records
cache keycache.Cache
crypt cryptor.Cryptor
records passvault.Records
cache keycache.Cache
orders order.Orderer
alternateName string
)
// Each of these structures corresponds to the JSON expected on the
@@ -63,6 +69,7 @@ type PasswordRequest struct {
Password string
NewPassword string
HipchatName string
}
type EncryptRequest struct {
@@ -105,6 +112,26 @@ type ExportRequest struct {
Password string
}
type OrderRequest struct {
Name string
Password string
Duration string
Uses string
Data []byte
Label string
}
type OrderInfoRequest struct {
Name string
Password string
OrderNum string
}
type OrderOutstandingRequest struct {
Name string
Password string
}
// These structures map the JSON responses that will be sent from the API
type ResponseData struct {
@@ -145,6 +172,26 @@ func jsonResponse(resp []byte) ([]byte, error) {
return json.Marshal(ResponseData{Status: "ok", Response: resp})
}
func getAltNameFromName(alt, name string) (altName string, found bool) {
if passwordRecord, ok := records.Passwords[name]; ok {
if altName, ok := passwordRecord.AltNames[alt]; ok {
return altName, true
}
}
return "", false
}
func getAltNamesFromNames(alt string, names []string) map[string]string {
altNames := make(map[string]string)
for _, name := range names {
altName, found := getAltNameFromName(alt, name)
if !found {
altName = name
}
altNames[name] = altName
}
return altNames
}
// validateUser checks that the username and password passed in are
// correct. If admin is true, the user must be an admin as well.
func validateUser(name, password string, admin bool) error {
@@ -168,8 +215,7 @@ func validateUser(name, password string, admin bool) error {
return nil
}
// validateName checks that the username and password pass the minimal
// validation check
// validateName checks that the username and password pass a validation test.
func validateName(name, password string) error {
if name == "" {
return errors.New("User name must not be blank")
@@ -182,23 +228,37 @@ func validateName(name, password string) error {
}
// Init reads the records from disk from a given path
func Init(path string) error {
func Init(ca, hcKey, hcRoom, hcHost, roHost string) error {
var err error
defer func() {
if err != nil {
log.Printf("core.init failed: %v", err)
} else {
log.Printf("core.init success: path=%s", path)
log.Printf("core.init success: ca=%s", ca)
}
}()
if records, err = passvault.InitFrom(path); err != nil {
err = fmt.Errorf("failed to load password vault %s: %s", path, err)
if records, err = passvault.InitFrom(ca); err != nil {
err = fmt.Errorf("failed to load password vault %s: %s", ca, err)
}
if hcKey != "" && hcRoom != "" && hcHost != "" {
alternateName = "HipchatName"
roomId, err := strconv.Atoi(hcRoom)
if err != nil {
return errors.New("core.init unable to use hipchat roomId provided")
}
orders.Hipchat = hipchat.HipchatClient{
ApiKey: hcKey,
RoomId: roomId,
HcHost: hcHost,
RoHost: roHost,
}
}
cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)}
crypt = cryptor.New(&records, &cache)
orders.PrepareOrders()
crypt = cryptor.New(&records, &cache, &orders)
return err
}
@@ -351,6 +411,22 @@ func Delegate(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
// Make sure we capture the number who have already delegated.
for _, delegatedUser := range s.Users {
for _, delegatedLabel := range s.Labels {
if orderKey, found := orders.FindOrder(delegatedUser, delegatedLabel); found {
order := orders.Orders[orderKey]
order.AdminsDelegated = append(order.AdminsDelegated, s.Name)
order.Delegated++
orders.Orders[orderKey] = order
// Notify the hipchat room that there was a new delegator
orders.NotifyDelegation(s.Name, delegatedLabel, delegatedUser, orderKey, s.Time)
}
}
}
return jsonStatusOk()
}
@@ -422,7 +498,7 @@ func Password(jsonIn []byte) ([]byte, error) {
}
// add signed-in record to active set
err = records.ChangePassword(s.Name, s.Password, s.NewPassword)
err = records.ChangePassword(s.Name, s.Password, s.NewPassword, s.HipchatName)
if err != nil {
return jsonStatusError(err)
}
@@ -488,7 +564,7 @@ func ReEncrypt(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
data, _, secure, err := crypt.Decrypt(s.Data, s.Name)
data, _, _, secure, err := crypt.Decrypt(s.Data, s.Name)
if err != nil {
return jsonStatusError(err)
}
@@ -532,7 +608,7 @@ func Decrypt(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
data, names, secure, err := crypt.Decrypt(s.Data, s.Name)
data, allLabels, names, secure, err := crypt.Decrypt(s.Data, s.Name)
if err != nil {
return jsonStatusError(err)
}
@@ -548,6 +624,13 @@ func Decrypt(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
// Cleanup any orders that have been fulfilled and notify the room.
for _, label := range allLabels {
if orderKey, found := orders.FindOrder(s.Name, label); found {
delete(orders.Orders, orderKey)
orders.NotifyOrderFulfilled(s.Name, orderKey)
}
}
return jsonResponse(out)
}
@@ -658,3 +741,128 @@ func Export(jsonIn []byte) ([]byte, error) {
return jsonResponse(out)
}
// Order will request delegations from other users.
func Order(jsonIn []byte) (out []byte, err error) {
var o OrderRequest
defer func() {
if err != nil {
log.Printf("core.order failed: user=%s %v", o.Name, err)
} else {
log.Printf("core.order success: user=%s", o.Name)
}
}()
if err = json.Unmarshal(jsonIn, &o); err != nil {
return jsonStatusError(err)
}
if err := validateUser(o.Name, o.Password, false); err != nil {
return jsonStatusError(err)
}
// Get the owners of the ciphertext.
owners, _, err := crypt.GetOwners(o.Data)
if err != nil {
jsonStatusError(err)
}
if o.Duration == "" {
err = errors.New("Duration required when placing an order.")
jsonStatusError(err)
}
if o.Uses == "" || o.Uses == "0" {
err = errors.New("Number of required uses necessary when placing an order.")
jsonStatusError(err)
}
cache.Refresh()
orderNum := order.GenerateNum()
adminsDelegated, numDelegated := cache.DelegateStatus(o.Name, o.Label, owners)
duration, err := time.ParseDuration(o.Duration)
if err != nil {
jsonStatusError(err)
}
currentTime := time.Now()
expiryTime := currentTime.Add(duration)
ord := order.CreateOrder(o.Name,
o.Label,
orderNum,
currentTime,
expiryTime,
duration,
adminsDelegated,
owners,
numDelegated)
orders.Orders[orderNum] = ord
out, err = json.Marshal(ord)
// Get a map to any alternative name we want to notify
altOwners := getAltNamesFromNames(alternateName, owners)
// Let everyone on hipchat know there is a new order.
orders.NotifyNewOrder(o.Name, o.Duration, o.Label, o.Uses, orderNum, altOwners)
if err != nil {
return jsonStatusError(err)
}
return jsonResponse(out)
}
// OrdersOutstanding will return a list of currently outstanding orders.
func OrdersOutstanding(jsonIn []byte) (out []byte, err error) {
var o OrderOutstandingRequest
defer func() {
if err != nil {
log.Printf("core.ordersout failed: user=%s %v", o.Name, err)
} else {
log.Printf("core.ordersout success: user=%s", o.Name)
}
}()
if err = json.Unmarshal(jsonIn, &o); err != nil {
return jsonStatusError(err)
}
if err := validateUser(o.Name, o.Password, false); err != nil {
return jsonStatusError(err)
}
out, err = json.Marshal(orders.Orders)
if err != nil {
return jsonStatusError(err)
}
return jsonResponse(out)
}
// OrderInfo will return a list of currently outstanding order numbers.
func OrderInfo(jsonIn []byte) (out []byte, err error) {
var o OrderInfoRequest
defer func() {
if err != nil {
log.Printf("core.order failed: user=%s %v", o.Name, err)
} else {
log.Printf("core.order success: user=%s", o.Name)
}
}()
if err = json.Unmarshal(jsonIn, &o); err != nil {
return jsonStatusError(err)
}
if err := validateUser(o.Name, o.Password, false); err != nil {
return jsonStatusError(err)
}
if ord, ok := orders.Orders[o.OrderNum]; ok {
if out, err = json.Marshal(ord); err != nil {
return jsonStatusError(err)
} else if len(out) == 0 {
return jsonStatusError(errors.New("No order with that number"))
}
return jsonResponse(out)
}
return
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/msp"
"github.com/cloudflare/redoctober/order"
"github.com/cloudflare/redoctober/padding"
"github.com/cloudflare/redoctober/passvault"
"github.com/cloudflare/redoctober/symcrypt"
@@ -29,10 +30,11 @@ const (
type Cryptor struct {
records *passvault.Records
cache *keycache.Cache
orders *order.Orderer
}
func New(records *passvault.Records, cache *keycache.Cache) Cryptor {
return Cryptor{records, cache}
func New(records *passvault.Records, cache *keycache.Cache, orders *order.Orderer) Cryptor {
return Cryptor{records, cache, orders}
}
// AccessStructure represents different possible access structures for
@@ -508,14 +510,14 @@ func (c *Cryptor) Encrypt(in []byte, labels []string, access AccessStructure) (r
}
// Decrypt decrypts a file using the keys in the key cache.
func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, secure bool, err error) {
func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, labels, names []string, secure bool, err error) {
// unwrap encrypted file
var encrypted EncryptedData
if err = json.Unmarshal(in, &encrypted); err != nil {
return
}
if encrypted.Version != DEFAULT_VERSION && encrypted.Version != -1 {
return nil, nil, secure, errors.New("Unknown version")
return nil, nil, nil, secure, errors.New("Unknown version")
}
secure = encrypted.Version == -1
@@ -535,7 +537,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string,
return
}
if encrypted.VaultId != vaultId {
return nil, nil, secure, errors.New("Wrong vault")
return nil, nil, nil, secure, errors.New("Wrong vault")
}
// compute HMAC
@@ -563,6 +565,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string,
aesCBC.CryptBlocks(clearData, encrypted.Data)
resp, err = padding.RemovePadding(clearData)
labels = encrypted.Labels
return
}
@@ -621,7 +624,6 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er
addedNames[name] = true
}
}
predicate = encrypted.Predicate
return

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,
"color": color,
"notify": true,
}
body, err := json.Marshal(msgBody)
if err != nil {
return err
}
roomId := url.QueryEscape(strconv.Itoa(h.RoomId))
hipchatUrl := fmt.Sprintf("https://%s/v2/room/%s/notification?auth_token=%s", h.HcHost, roomId, h.ApiKey)
req, err := http.NewRequest("POST", hipchatUrl, bytes.NewReader(body))
req.Header.Add("Content-Type", "application/json")
Client := http.Client{}
resp, err := Client.Do(req)
if err != nil {
log.Printf("Could not post to hipchat for the reason %s", err.Error())
return err
}
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Could not post to hipchat for the reason %s", err.Error())
return err
}
return nil
}

View File

@@ -164,7 +164,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 +172,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 +311,93 @@
</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>
</div>
<footer id="footer" class="footer">
@@ -327,7 +416,6 @@
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
@@ -532,7 +620,100 @@
}
});
});
// 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);
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>'+d+'</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>' }) );
}
});
});
// 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);
}
}
});
</script>
</body>
</html>
</html>/html>

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
@@ -301,3 +302,33 @@ func (cache *Cache) DecryptShares(in [][]byte, name, user string, labels []strin
return
}
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
View File

@@ -0,0 +1,119 @@
package order
import (
// "fmt"
"crypto/rand"
"fmt"
"net/url"
"time"
"github.com/cloudflare/redoctober/hipchat"
// "errors"
"encoding/hex"
)
const (
NewOrder = "%s has created an order requesting %s worth of 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
ToDelegate 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 removed from the structure.
type Orderer struct {
Orders map[string]Order
Hipchat hipchat.HipchatClient
}
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)
}
// PrepareOrders Create a new map of Orders
func (o *Orderer) PrepareOrders() {
o.Orders = make(map[string]Order)
}
// 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, duration, label)
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
}

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

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,9 @@ 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,
}
type userRequest struct {
@@ -219,6 +222,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", "", "RedOctber Url Base (ex: localhost:8080)")
flag.Parse()
if *vaultPath == "" || *certsPathString == "" || *keysPathString == "" || (*addr == "" && *useSystemdSocket == false) {
@@ -230,7 +237,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())
}
@@ -437,7 +444,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>
@@ -445,16 +452,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>
@@ -582,6 +591,93 @@ 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>
</div>
<footer id="footer" class="footer">
@@ -600,7 +696,6 @@ var indexHtml = []byte(`<!DOCTYPE html>
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
@@ -805,6 +900,99 @@ 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);
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>'+d+'</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>' }) );
}
});
});
// 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);
}
}
});
</script>
</body>