| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package handlers |
| |
| import ( |
| "bytes" |
| "encoding/base64" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "net/http" |
| "net/http/httptest" |
| "net/url" |
| "reflect" |
| "sort" |
| "testing" |
| "time" |
| |
| "v.io/v23" |
| "v.io/v23/security" |
| "v.io/v23/vom" |
| |
| _ "v.io/x/ref/runtime/factories/generic" |
| "v.io/x/ref/services/identity" |
| "v.io/x/ref/services/identity/internal/oauth" |
| "v.io/x/ref/services/identity/internal/revocation" |
| "v.io/x/ref/test" |
| "v.io/x/ref/test/testutil" |
| ) |
| |
| func TestBlessingRootJSON(t *testing.T) { |
| // TODO(ashankar,ataly): Handle multiple root names? |
| blessingNames := []string{"test-root"} |
| p := testutil.NewPrincipal(blessingNames...) |
| |
| ts := httptest.NewServer(BlessingRoot{p}) |
| defer ts.Close() |
| response, err := http.Get(ts.URL) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer response.Body.Close() |
| dec := json.NewDecoder(response.Body) |
| var res identity.BlessingRootResponse |
| if err := dec.Decode(&res); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Check that the names are correct. |
| sort.Strings(blessingNames) |
| sort.Strings(res.Names) |
| if !reflect.DeepEqual(res.Names, blessingNames) { |
| t.Errorf("Response has incorrect name. Got %v, want %v", res.Names, blessingNames) |
| } |
| |
| // Check that the public key is correct. |
| gotMarshalled, err := base64.URLEncoding.DecodeString(res.PublicKey) |
| if err != nil { |
| t.Fatal(err) |
| } |
| got, err := security.UnmarshalPublicKey(gotMarshalled) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if want := p.PublicKey(); !reflect.DeepEqual(got, want) { |
| t.Errorf("Response has incorrect public key. Got %v, want %v", got, want) |
| } |
| } |
| |
| func TestBlessingRootBase64VOM(t *testing.T) { |
| blessingNames := []string{"alpha"} |
| p := testutil.NewPrincipal(blessingNames...) |
| |
| ts := httptest.NewServer(BlessingRoot{p}) |
| defer ts.Close() |
| response, err := http.Get(ts.URL + "?output=base64vom") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer response.Body.Close() |
| var root security.Blessings |
| if body, err := ioutil.ReadAll(response.Body); err != nil { |
| t.Fatal(err) |
| } else if v, err := base64.URLEncoding.DecodeString(string(body)); err != nil { |
| t.Fatal(err) |
| } else if err = vom.Decode(v, &root); err != nil { |
| t.Fatal(err) |
| } |
| if want, _ := p.BlessingStore().Default(); !reflect.DeepEqual(root, want) { |
| t.Errorf("Got %v, want %v", root, want) |
| } |
| } |
| |
| func TestBless(t *testing.T) { |
| var ( |
| blesserPrin = testutil.NewPrincipal("blesser") |
| blesseePrin = testutil.NewPrincipal("blessee") |
| |
| methodCav, _ = security.NewMethodCaveat("foo") |
| expiryCav, _ = security.NewExpiryCaveat(time.Now().Add(time.Hour)) |
| |
| mkReqURL = func(baseURLStr string, caveats []security.Caveat, outputFormat string) string { |
| baseURL, err := url.Parse(baseURLStr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| params := url.Values{} |
| |
| if len(caveats) != 0 { |
| caveatsVom, err := vom.Encode(caveats) |
| if err != nil { |
| t.Fatal(err) |
| } |
| params.Add(caveatsFormKey, base64.URLEncoding.EncodeToString(caveatsVom)) |
| } |
| keyBytes, err := blesseePrin.PublicKey().MarshalBinary() |
| if err != nil { |
| t.Fatal(err) |
| } |
| params.Add(publicKeyFormKey, base64.URLEncoding.EncodeToString(keyBytes)) |
| params.Add(tokenFormKey, "mocktoken") |
| params.Add(outputFormatFormKey, outputFormat) |
| |
| baseURL.RawQuery = params.Encode() |
| return baseURL.String() |
| } |
| |
| vomRoundTrip = func(in, out interface{}) error { |
| data, err := vom.Encode(in) |
| if err != nil { |
| return err |
| } |
| return vom.Decode(data, out) |
| } |
| |
| decodeBlessings = func(b []byte, outputFormat string) security.Blessings { |
| if len(outputFormat) == 0 { |
| outputFormat = base64VomFormat |
| } |
| var res security.Blessings |
| switch outputFormat { |
| case base64VomFormat: |
| if raw, err := base64.URLEncoding.DecodeString(string(b)); err != nil { |
| t.Fatal(err) |
| } else if err = vom.Decode(raw, &res); err != nil { |
| t.Fatal(err) |
| } |
| case jsonFormat: |
| var wb security.WireBlessings |
| if err := json.Unmarshal(b, &wb); err != nil { |
| t.Fatal(err) |
| } else if err = vomRoundTrip(wb, &res); err != nil { |
| t.Fatal(err) |
| } |
| } |
| return res |
| } |
| ) |
| |
| trustedBlessing, err := blesserPrin.BlessSelf("trustedBlesser") |
| if err != nil { |
| t.Fatal(err) |
| } |
| blesserPrin.BlessingStore().Set(trustedBlessing, "trusted") |
| blesserPrin.BlessingStore().Set(security.Blessings{}, "...") |
| |
| ctx, shutdown := test.V23Init() |
| defer shutdown() |
| if ctx, err = v23.WithPrincipal(ctx, blesserPrin); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Make the blessee trust the blesser's roots |
| blesserB, _ := blesserPrin.BlessingStore().Default() |
| if err := security.AddToRoots(blesseePrin, blesserB); err != nil { |
| t.Fatal(err) |
| } |
| if err := security.AddToRoots(blesseePrin, trustedBlessing); err != nil { |
| t.Fatal(err) |
| } |
| |
| testEmail := "foo@bar.com" |
| testClientID := "test-client-id" |
| revocationManager := revocation.NewMockRevocationManager(ctx) |
| oauthProvider := oauth.NewMockOAuth(testEmail, testClientID) |
| |
| testcases := []struct { |
| params OAuthBlesserParams |
| caveats []security.Caveat |
| clients RegisteredAppMap |
| blessing string |
| }{ |
| { |
| OAuthBlesserParams{ |
| OAuthProvider: oauthProvider, |
| RevocationManager: revocationManager, |
| }, |
| []security.Caveat{expiryCav, methodCav}, |
| RegisteredAppMap{ |
| "test-client-id": { |
| Extension: "{email}:trusted-client", |
| }, |
| }, |
| "blesser:foo@bar.com:trusted-client", |
| }, |
| { |
| OAuthBlesserParams{ |
| OAuthProvider: oauthProvider, |
| BlessingDuration: 24 * time.Hour, |
| }, |
| nil, |
| nil, |
| "blesser:test-client-id:foo@bar.com", |
| }, |
| { |
| OAuthBlesserParams{ |
| OAuthProvider: oauthProvider, |
| RevocationManager: revocationManager, |
| }, |
| nil, |
| nil, |
| "blesser:test-client-id:foo@bar.com", |
| }, |
| { |
| OAuthBlesserParams{ |
| OAuthProvider: oauthProvider, |
| RevocationManager: revocationManager, |
| }, |
| []security.Caveat{expiryCav, methodCav}, |
| RegisteredAppMap{}, |
| "blesser:test-client-id:foo@bar.com", |
| }, |
| } |
| for _, testcase := range testcases { |
| for _, outputFormat := range []string{jsonFormat, base64VomFormat, ""} { |
| ts := httptest.NewServer(NewOAuthBlessingHandler(ctx, testcase.params, testcase.clients)) |
| defer ts.Close() |
| |
| response, err := http.Get(mkReqURL(ts.URL, testcase.caveats, outputFormat)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| b, err := ioutil.ReadAll(response.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| response.Body.Close() |
| blessings := decodeBlessings(b, outputFormat) |
| |
| // Blessing should be bound to the blessee. |
| if got, want := blessings.PublicKey(), blesseePrin.PublicKey(); !reflect.DeepEqual(got, want) { |
| t.Errorf("got blessings for public key %v, want blessings for public key %v", got, want) |
| } |
| |
| // Verify the name and caveats on the blessings. |
| if got, want := security.BlessingNames(blesseePrin, blessings), []string{testcase.blessing}; !reflect.DeepEqual(got, want) { |
| t.Errorf("Got %v, want %v", got, want) |
| } |
| caveats, err := extractCaveats(blessings) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| if len(testcase.caveats) > 0 { |
| // The blessing must have exactly those caveats that were provided in the request. |
| if !caveatsMatch(t, caveats, testcase.caveats) { |
| t.Errorf("got blessings with caveats %v, want blessings with caveats %v", caveats, testcase.caveats) |
| } |
| } else if len(caveats) != 1 { |
| t.Errorf("got blessings with %d caveats, want blessings with 1 caveats", len(caveats)) |
| } else if testcase.params.RevocationManager != nil && caveats[0].Id != security.PublicKeyThirdPartyCaveat.Id { |
| // The blessing must have a third-party revocation caveat. |
| t.Errorf("got blessings with caveat (%v), want blessings with a PublicKeyThirdPartyCaveat", caveats[0].Id) |
| } else if testcase.params.RevocationManager == nil && caveats[0].Id != security.ExpiryCaveat.Id { |
| // The blessing must have an expiry caveat. |
| t.Errorf("got blessings with caveat (%v), want blessings with an ExpiryCaveat", caveats[0].Id) |
| } |
| } |
| } |
| } |
| |
| func extractCaveats(b security.Blessings) ([]security.Caveat, error) { |
| // Extract the wire encoding of the blessings and fish them out. |
| bytes, err := vom.Encode(b) |
| if err != nil { |
| return nil, err |
| } |
| var wire security.WireBlessings |
| if err := vom.Decode(bytes, &wire); err != nil { |
| return nil, err |
| } |
| if got, want := len(wire.CertificateChains), 1; got != want { |
| return nil, fmt.Errorf("Got %d blessings, want %d", got, want) |
| } |
| var ret []security.Caveat |
| for _, chain := range wire.CertificateChains { |
| for _, cert := range chain { |
| ret = append(ret, cert.Caveats...) |
| } |
| } |
| return ret, nil |
| } |
| |
| type caveatsSorter struct { |
| caveats []security.Caveat |
| t *testing.T |
| } |
| |
| func (c caveatsSorter) Len() int { return len(c.caveats) } |
| func (c caveatsSorter) Swap(i, j int) { c.caveats[i], c.caveats[j] = c.caveats[j], c.caveats[i] } |
| func (c caveatsSorter) Less(i, j int) bool { |
| b_i, err := vom.Encode(c.caveats[i]) |
| if err != nil { |
| c.t.Fatal(err) |
| } |
| b_j, err := vom.Encode(c.caveats[j]) |
| if err != nil { |
| c.t.Fatal(err) |
| } |
| return bytes.Compare(b_i, b_j) == -1 |
| } |
| |
| func caveatsMatch(t *testing.T, got, want []security.Caveat) bool { |
| if len(got) != len(want) { |
| return false |
| } |
| g, w := caveatsSorter{got, t}, caveatsSorter{want, t} |
| sort.Sort(g) |
| sort.Sort(w) |
| return reflect.DeepEqual(g, w) |
| } |