veyron/services/identity, veyron/tools/identity: Ability to add user
chosen caveats to blessings from identity server.
* Form now has a nice caveat selector input for define caveats.
Screenshot: https://screenshot.googleplex.com/bzV5FiUAax.png
* The selector is populated from the map in services/identity/blesser/caveats.go.
TODO:
* Change oauth flow from identity tool to open form to allow specified caveats.
This way caveats will only be input into the identity server directly so we
don't have to "trust" the identity tool.
FUTURE CLEANUP:
* Eventually should move all JS/CSS to a new assets directory in identity server and serve from
there instead of embedding into template.
Change-Id: I4eb86e2c06c01b7a31659b7b29535e0d21972f7a
diff --git a/services/identity/handlers/bless.go b/services/identity/handlers/bless.go
index 70d43b7..468df38 100644
--- a/services/identity/handlers/bless.go
+++ b/services/identity/handlers/bless.go
@@ -2,11 +2,14 @@
import (
"fmt"
+ "html/template"
"net/http"
+ "strings"
"time"
"veyron.io/veyron/veyron/services/identity/util"
"veyron.io/veyron/veyron2/security"
+ "veyron.io/veyron/veyron2/vlog"
)
// Bless is an http.HandlerFunc that renders/processes a form that takes as
@@ -19,11 +22,11 @@
return
}
if r.Method != "POST" {
- util.HTTPBadRequest(w, r, fmt.Errorf("Only GET or POST requests accepted"))
+ util.HTTPBadRequest(w, r, fmt.Errorf("only GET or POST requests accepted"))
return
}
if err := r.ParseForm(); err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("Failed to parse form: %v", err))
+ util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse form: %v", err))
return
}
duration, err := time.ParseDuration(defaultIfEmpty(r.FormValue("duration"), "24h"))
@@ -42,7 +45,11 @@
return
}
name := r.FormValue("name")
- blessed, err := blessor.Bless(blessee, name, duration, nil)
+ caveats, err := caveats(r)
+ if err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("failed to created caveats: ", err))
+ }
+ blessed, err := blessor.Bless(blessee, name, duration, caveats)
if err != nil {
util.HTTPBadRequest(w, r, fmt.Errorf("%q.Bless(%q, %q) failed: %v", blessor, blessee, name, err))
return
@@ -50,6 +57,29 @@
util.HTTPSend(w, blessed)
}
+func caveats(r *http.Request) ([]security.Caveat, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+ var caveats []security.Caveat
+ for i, cavName := range r.Form["caveat"] {
+ if cavName == "none" {
+ continue
+ }
+ args := strings.Split(r.Form[cavName][i], ",")
+ cavInfo, ok := caveatMap[cavName]
+ if !ok {
+ return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
+ }
+ caveat, err := cavInfo.New(args...)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create caveat %s(%v): cavInfo.New failed: %v", cavName, args, err)
+ }
+ caveats = append(caveats, caveat)
+ }
+ return caveats, nil
+}
+
func decodeBlessor(r *http.Request) (security.PrivateID, error) {
var blessor security.PrivateID
b64 := r.FormValue("blessor")
@@ -74,14 +104,52 @@
}
func renderForm(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(`
-<!doctype html>
+ w.Header().Set("Context-Type", "text/html")
+ if err := tmpl.Execute(w, caveatMap); err != nil {
+ vlog.Errorf("Unable to execute bless page template: %v", err)
+ util.HTTPServerError(w, err)
+ }
+}
+
+var tmpl = template.Must(template.New("bless").Parse(`<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Veyron Identity Derivation</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script>
+ // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
+ $(document).ready(function() {
+ $('.caveatInput').hide(); // Hide all the inputs at start.
+
+ // When a caveat selector changes show the corresponding input box.
+ $('body').on('change', '.caveats', function (){
+ // Grab the div encapsulating the select and the corresponding inputs.
+ var caveatSelector = $(this).parents(".caveatRow");
+ // Hide the visible inputs and show the selected one.
+ caveatSelector.find('.caveatInput').hide();
+ caveatSelector.find('#'+$(this).val()).show();
+ });
+
+ // Upon clicking the '+' button a new caveat selector should appear.
+ $('body').on('click', '.addCaveat', function() {
+ var selector = $(this).parents(".caveatRow");
+ var newSelector = selector.clone();
+ // Hide all inputs since nothing is selected in this clone.
+ newSelector.find('.caveatInput').hide();
+ selector.after(newSelector);
+ // Change the '+' button to a '-' button.
+ $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
+ });
+
+ // Upon clicking the '-' button caveats should be removed.
+ $('body').on('click', '.removeCaveat', function() {
+ $(this).parents(".caveatRow").remove();
+ });
+ });
+</script>
</head>
<body class="container">
<form class="form-signin" method="POST" name="input" action="/bless/">
@@ -93,12 +161,31 @@
<input type="text" class="form-control" name="name" placeholder="Name">
<br/>
<input type="text" class="form-control" name="duration" placeholder="Duration. Defaults to 24h">
+<div class="caveatRow row">
+ <br/>
+ <div class="col-md-4">
+ <select name="caveat" class="form-control caveats">
+ <option value="none" selected="selected">Select a caveat.</option>
+ {{ $caveatMap := . }}
+ {{range $key, $value := $caveatMap}}
+ <option name="{{$key}}" value="{{$key}}">{{$key}}</option>
+ {{end}}
+ </select>
+ </div>
+ <div class="col-md-7">
+ {{range $key, $entry := $caveatMap}}
+ <input type="text" id="{{$key}}" class="form-control caveatInput" name="{{$key}}" placeholder="{{$entry.Placeholder}}">
+ {{end}}
+ </div>
+ <div class="col-md-1">
+ <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
+ </div>
+</div>
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
</form>
</body>
</html>`))
-}
func defaultIfEmpty(str, def string) string {
if len(str) == 0 {
@@ -106,3 +193,49 @@
}
return str
}
+
+// caveatMap is a map from Caveat name to caveat information.
+// To add to this map append
+// key = "CaveatName"
+// New = func that returns instantiation of specific caveat wrapped in security.Caveat.
+// Placeholder = the placeholder text for the html input element.
+var caveatMap = map[string]struct {
+ New func(args ...string) (security.Caveat, error)
+ Placeholder string
+}{
+ "ExpiryCaveat": {
+ New: func(args ...string) (security.Caveat, error) {
+ if len(args) != 1 {
+ return security.Caveat{}, fmt.Errorf("must pass exactly one duration string.")
+ }
+ dur, err := time.ParseDuration(args[0])
+ if err != nil {
+ return security.Caveat{}, fmt.Errorf("parse duration failed: %v", err)
+ }
+ return security.ExpiryCaveat(time.Now().Add(dur))
+ },
+ Placeholder: "i.e. 2h45m. Valid time units are ns, us (or µs), ms, s, m, h.",
+ },
+ "MethodCaveat": {
+ New: func(args ...string) (security.Caveat, error) {
+ if len(args) < 1 {
+ return security.Caveat{}, fmt.Errorf("must pass at least one method")
+ }
+ return security.MethodCaveat(args[0], args[1:]...)
+ },
+ Placeholder: "Comma-separated method names.",
+ },
+ "PeerBlessingsCaveat": {
+ New: func(args ...string) (security.Caveat, error) {
+ if len(args) < 1 {
+ return security.Caveat{}, fmt.Errorf("must pass at least one blessing pattern")
+ }
+ var patterns []security.BlessingPattern
+ for _, arg := range args {
+ patterns = append(patterns, security.BlessingPattern(arg))
+ }
+ return security.PeerBlessingsCaveat(patterns[0], patterns[1:]...)
+ },
+ Placeholder: "Comma-separated blessing patterns.",
+ },
+}
diff --git a/services/identity/revocation/bless.go b/services/identity/revocation/bless.go
index 57dcb71..473bcd4 100644
--- a/services/identity/revocation/bless.go
+++ b/services/identity/revocation/bless.go
@@ -12,16 +12,18 @@
// Bless creates a blessing on behalf of the identity server.
func Bless(server security.PrivateID, blessee security.PublicID, email string, duration time.Duration, revocationCaveat security.ThirdPartyCaveat) (security.PublicID, error) {
+ // TODO(suharshs): Pass caveats to here when macaroon new oauth flow is complete.
+ var caveats []security.Caveat
if revocationCaveat != nil {
caveat, err := security.NewCaveat(revocationCaveat)
if err != nil {
return nil, err
}
- // TODO(suharshs): Extend the duration for blessings with provided revocaionCaveats
- return server.Bless(blessee, email, duration, []security.Caveat{caveat})
+ // revocationCaveat must be prepended because it is assumed to be first by ReadBlessAuditEntry.
+ caveats = append([]security.Caveat{caveat}, caveats...)
}
- // return a blessing with a more limited duration, since there is no revocation caveat
- return server.Bless(blessee, email, duration, nil)
+ // TODO(suharshs): Extend the duration for blessings with provided revocaionCaveats.
+ return server.Bless(blessee, email, duration, caveats)
}
type BlessingAuditEntry struct {