blob: d673b8651d1a032e1f302fefe5b25dc19d8fb97e [file] [log] [blame]
// Binary pbankd is a simple implementation of the bank service.
// Binary bank clients can connect to this bank service to manage virtual bank
// accounts. Unlike bankd, pbankd uses the Veyron Store. It can recover user
// data after a crash, but it must always connect to the stored service.
// Unlike bankd, pbankd prevents race conditions with transactions.
package main
import (
"errors"
"flag"
"fmt"
"math/rand"
"os"
"os/user"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"veyron/examples/bank"
"veyron/examples/bank/schema"
"veyron/lib/signals"
vsecurity "veyron/security"
"veyron/security/caveat"
idutil "veyron/services/identity/util"
"veyron2"
"veyron2/ipc"
"veyron2/naming"
"veyron2/rt"
"veyron2/security"
"veyron2/storage"
"veyron2/storage/vstore"
"veyron2/vlog"
)
// Duration of a bank account blessing, intended to be very long.
const BLESS_DURATION = 24 * 10000 * time.Hour
// Ensure that account numbers are all 6 digits long.
const MIN_ACCOUNT_NUMBER = 100000
const MAX_ACCOUNT_NUMBER = 999999
const SUFFIX_REGEXP = "/[0-9]{6}"
// Where we will store the bank in the store database.
const BANK_ROOT string = "/Bank"
var (
storeName string
ACCOUNTS string
runtime veyron2.Runtime
)
func init() {
username := "unknown"
if u, err := user.Current(); err == nil {
username = u.Username
}
hostname := "unknown"
if h, err := os.Hostname(); err == nil {
hostname = h
}
dir := "global/vstore/" + hostname + "/" + username
// Parse the flags (variableName, flagName, defaultValue, description)
flag.StringVar(&storeName, "store", dir, "Name of the Veyron store")
// Set the random seed to the current time to increase psuedorandomness.
rand.Seed(time.Now().Unix())
// Set the name for the ACCOUNTS 'constant'. TODO(alexfandrianto): Is there a better way to know the field is named "Accounts"?
ACCOUNTS = reflect.TypeOf(schema.Bank{}).Field(0).Name
}
// The following struct and functions handle construction of the persistent bank.
type pbankd struct {
// Pointer to the store
store storage.Store
// Current Transaction name; empty if there's no transaction
tname string
// The bank's private ID (used for blessing)
ID security.PrivateID
}
// newPbankd creates a new persistent bank structure.
func newPbankd(store storage.Store, identity security.PrivateID) *pbankd {
b := &pbankd{
store: store,
ID: identity,
}
return b
}
// InitializeBank bank details in the store; currently only initializes the root.
func (b *pbankd) initializeBank() {
if err := b.newTransaction(); err != nil {
vlog.Fatal(err)
}
// NOTE(sadovsky): initializeBankRoot ought to return an error. Currently,
// some errors (e.g. failed puts) could slip through unnoticed.
b.initializeBankRoot()
if err := b.commit(); err != nil {
vlog.Fatal(err)
}
}
// initializeBankRoot prepares the bank root as BANK_ROOT if it isn't yet in the Veyron store.
func (b *pbankd) initializeBankRoot() {
// Create parent directories for the bank root, if necessary
l := strings.Split(BANK_ROOT, "/")
fmt.Println(l)
for i, _ := range l {
fmt.Println(i)
prefix := filepath.Join(l[:i]...)
o := b.store.BindObject(naming.Join(b.tname, prefix))
if exist, err := o.Exists(runtime.TODOContext()); err != nil {
vlog.Infof("Error checking existence at %q: %s", prefix, err)
} else if !exist {
if _, err := o.Put(runtime.TODOContext(), &schema.Dir{}); err != nil {
vlog.Infof("Error creating parent %q: %s", prefix, err)
}
fmt.Printf("%q was created!\n", prefix)
} else {
fmt.Printf("%q was already present in the store.\n", prefix)
}
}
// Add the bank schema to the store at BANK_ROOT, if necessary
o := b.store.BindObject(naming.Join(b.tname, BANK_ROOT))
if exist, err := o.Exists(runtime.TODOContext()); err != nil {
vlog.Infof("Error checking existence at %q: %s", BANK_ROOT, err)
} else if !exist {
_, err := o.Put(runtime.TODOContext(), &schema.Bank{})
if err != nil {
vlog.Infof("Error creating bank at %q: %s", BANK_ROOT, err)
}
}
}
// Register creates an account for a new user with the given bankName.
func (b *pbankd) Connect(context ipc.ServerContext) (string, int64, error) {
// Check if the RemoteID() has been blessed by the bank
if num := getBankAccountNumber(context); num != 0 {
// Look up the user and return their bank account number
fmt.Println("This client is blessed!")
fmt.Printf("ID: %d\n", num)
return "", num, nil
} else {
fmt.Println("This client isn't blessed. Let's bless them!")
// Use the store
if err := b.newTransaction(); err != nil {
vlog.Fatal(err)
}
// Keep rolling until we get an unseen number
randID := rand.Int63n(MAX_ACCOUNT_NUMBER-MIN_ACCOUNT_NUMBER) + MIN_ACCOUNT_NUMBER
for b.isUser(randID) {
randID = rand.Int63n(MAX_ACCOUNT_NUMBER-MIN_ACCOUNT_NUMBER) + MIN_ACCOUNT_NUMBER
}
fmt.Printf("ID: %d\n", randID)
// Bless the user
pp := security.PrincipalPattern(context.LocalID().Names()[0])
pID, err := b.ID.Bless(
context.RemoteID(),
fmt.Sprintf("%d", randID),
BLESS_DURATION,
[]security.ServiceCaveat{security.UniversalCaveat(caveat.PeerIdentity{pp})},
)
if err != nil {
vlog.Fatal(err)
}
// Encode the public ID
enc, err := idutil.Base64VomEncode(pID)
if err != nil {
vlog.Fatal(err)
}
// Store the user into the database
b.registerNewUser(randID)
err = b.commit()
if err != nil {
vlog.Fatal(err)
}
// Ensure the new user is a user before returning the blessing and ID
if b.isUser(randID) {
return enc, randID, nil
}
return "", 0, fmt.Errorf("failed to register user")
}
}
// Deposit adds the amount given to this account.
func (b *pbankd) Deposit(context ipc.ServerContext, amount int64) error {
user := getBankAccountNumber(context)
if user == 0 {
return fmt.Errorf("couldn't retrieve account number")
}
if err := b.newTransaction(); err != nil {
return err
}
if !b.isUser(user) {
return fmt.Errorf("user isn't registered")
} else if amount < 0 {
return fmt.Errorf("deposit amount %d is negative", amount)
}
b.changeBalance(user, amount)
return b.commit()
}
// Withdraw reduces the amount given from this account.
func (b *pbankd) Withdraw(context ipc.ServerContext, amount int64) error {
user := getBankAccountNumber(context)
if user == 0 {
return fmt.Errorf("couldn't retrieve account number")
}
if err := b.newTransaction(); err != nil {
return err
}
if !b.isUser(user) {
return fmt.Errorf("user isn't registered")
} else if amount < 0 {
return fmt.Errorf("withdraw amount %d is negative", amount)
} else if balance := b.checkBalance(user); amount > balance {
return fmt.Errorf("withdraw amount %d exceeds balance %d", amount, balance)
}
b.changeBalance(user, -amount)
return b.commit()
}
// Transfer moves the amount given to the receiver.
func (b *pbankd) Transfer(context ipc.ServerContext, accountNumber int64, amount int64) error {
user := getBankAccountNumber(context)
if user == 0 {
return fmt.Errorf("couldn't retrieve account number")
}
if err := b.newTransaction(); err != nil {
return err
}
if !b.isUser(user) {
return fmt.Errorf("user isn't registered")
} else if !b.isUser(accountNumber) {
return fmt.Errorf("%d isn't registered", accountNumber)
} else if amount < 0 {
return fmt.Errorf("transfer amount %d is negative", amount)
} else if balance := b.checkBalance(user); amount > balance {
return fmt.Errorf("transfer amount %d exceeds balance %d", amount, balance)
}
b.changeBalance(user, -amount)
b.changeBalance(accountNumber, amount)
return b.commit()
}
// Balance returns the amount stored by the given user.
// Throws an error if the user is invalid.
func (b *pbankd) Balance(context ipc.ServerContext) (int64, error) {
user := getBankAccountNumber(context)
if user == 0 {
return 0, fmt.Errorf("couldn't retrieve account number")
}
if !b.isUser(user) {
return 0, fmt.Errorf("user isn't registered")
}
return b.checkBalance(user), nil
}
/*
Helper functions for the persistent bank service that deal with store access.
*/
// newTransaction starts a new transaction.
func (b *pbankd) newTransaction() error {
tid, err := b.store.BindTransactionRoot("").CreateTransaction(runtime.TODOContext())
if err != nil {
b.tname = ""
return err
}
b.tname = tid // Transaction is rooted at "", so tname == tid.
return nil
}
// commit commits the current transaction.
func (b *pbankd) commit() error {
if b.tname == "" {
return errors.New("No transaction to commit")
}
err := b.store.BindTransaction(b.tname).Commit(runtime.TODOContext())
b.tname = ""
if err != nil {
return fmt.Errorf("Failed to commit transaction: %s", err)
}
return nil
}
// isUser helps determine if the given user is part of the system or not.
func (b *pbankd) isUser(accountNumber int64) bool {
// If this is a user, their location in the store should exist.
prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", accountNumber))
o := b.store.BindObject(naming.Join(b.tname, prefix))
exist, err := o.Exists(runtime.TODOContext())
if err != nil {
vlog.Infof("Error checking existence at %s: %s", prefix, err)
return false
}
return exist
}
// Obtains the bank account number of the user. Returns 0 if it could not be found.
func getBankAccountNumber(context ipc.ServerContext) int64 {
bankName := context.LocalID().Names()[0]
// Untrusted clients have no names.
if len(context.RemoteID().Names()) == 0 {
return 0
}
// Otherwise, extract the account number from the name.
name := context.RemoteID().Names()[0]
if match, err := regexp.MatchString(bankName+SUFFIX_REGEXP, name); err != nil {
vlog.Infof("MatchString error: %s", err)
return 0
} else if !match {
vlog.Infof("No matching name found")
return 0
}
var v int64
_, err := fmt.Sscanf(name, bankName+"/%d", &v)
if err != nil {
vlog.Infof("Failure to parse ID from %s: %s", name, err)
return 0
}
return v
}
// registerNewUser adds the user to the system under the specified name and returns success.
func (b *pbankd) registerNewUser(user int64) {
// Create the user's account
prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", user))
o := b.store.BindObject(naming.Join(b.tname, prefix))
if _, err := o.Put(runtime.TODOContext(), int64(0)); err != nil {
vlog.Infof("Error creating %s: %s", prefix, err)
}
}
// checkBalance gets the user's balance from the store
func (b *pbankd) checkBalance(user int64) int64 {
prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", user))
o := b.store.BindObject(naming.Join(b.tname, prefix))
e, err := o.Get(runtime.TODOContext())
if err != nil {
vlog.Infof("Error getting %s: %s", prefix, err)
}
value, _ := e.Value.(int64)
return value
}
// changeBalance modifies the user's balance in the store
func (b *pbankd) changeBalance(user int64, amount int64) {
prefix := filepath.Join(BANK_ROOT, ACCOUNTS, fmt.Sprintf("%d", user))
o := b.store.BindObject(naming.Join(b.tname, prefix))
e, err := o.Get(runtime.TODOContext())
if err != nil {
vlog.Infof("Error getting %s: %s", prefix, err)
}
if _, err := o.Put(runtime.TODOContext(), e.Value.(int64)+amount); err != nil {
vlog.Infof("Error changing %s: %s", prefix, err)
}
}
// This custom bank dispatcher has two interfaces with distinct authorizers.
func newBankDispatcher(bankServer interface{}, bankAccountServer interface{}, authBank security.Authorizer, authBankAccount security.Authorizer) ipc.Dispatcher {
return BankDispatcher{ipc.ReflectInvoker(bankServer), ipc.ReflectInvoker(bankAccountServer), authBank, authBankAccount}
}
type BankDispatcher struct {
invokerBank ipc.Invoker
invokerBankAccount ipc.Invoker
authBank security.Authorizer
authBankAccount security.Authorizer
}
func (d BankDispatcher) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
fmt.Println("Dispatcher Lookup Suffix:", suffix)
if suffix != "" {
return d.invokerBankAccount, d.authBankAccount, nil
}
return d.invokerBank, d.authBank, nil
}
// The custom account authorizer checks if the RemoteID matches a regexp pattern and allows only Reads and Writes.
type AccountAuthorizer string
func (aa AccountAuthorizer) Authorize(ctx security.Context) error {
name := ctx.RemoteID().Names()[0]
match, err := regexp.MatchString(string(aa), name)
fmt.Printf("Authorizing for Account %s %t\n", name, match)
if err != nil {
return err
}
if match {
if ctx.Label() != security.ReadLabel && ctx.Label() != security.WriteLabel {
return errors.New("unauthorized; may only read or write")
}
return nil
}
return errors.New("unauthorized to access account")
}
func main() {
// Create a new server instance.
runtime = rt.Init()
s, err := runtime.NewServer()
if err != nil {
vlog.Fatal("failure creating server: ", err)
}
// Connect to the Veyron Store
vlog.Infof("Binding to store on %s", storeName)
st, err := vstore.New(storeName)
if err != nil {
vlog.Fatalf("Can't connect to store: %s: %s", storeName, err)
}
// Create the bank server and bank account server stubs, using the store connection
pbankd := newPbankd(st, runtime.Identity())
pbankd.initializeBank()
bankServer := bank.NewServerBank(pbankd)
bankAccountServer := bank.NewServerBankAccount(pbankd)
// Setup bank and account authorizers.
bankAuth := vsecurity.NewACLAuthorizer(security.ACL{security.AllPrincipals: security.LabelSet(security.ReadLabel | security.WriteLabel)})
bankAccountAuth := AccountAuthorizer(runtime.Identity().PublicID().Names()[0] + SUFFIX_REGEXP)
dispatcher := newBankDispatcher(bankServer, bankAccountServer, bankAuth, bankAccountAuth)
// Create an endpoint and begin listening.
endpoint, err := s.Listen("tcp", "127.0.0.1:0")
if err == nil {
fmt.Printf("Listening at: %v\n", endpoint)
} else {
vlog.Fatal("error listening to service: ", err)
}
// Publish the service in the mount table.
mountName := "veyron/bank"
fmt.Printf("Mounting bank on %s, endpoint /%s\n", mountName, endpoint)
if err := s.Serve(mountName, dispatcher); err != nil {
vlog.Fatal("s.Serve() failed: ", err)
}
// Wait forever.
<-signals.ShutdownOnSignals()
}