| // 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 principal implements a principal manager that maps origins to |
| // vanadium principals. |
| // |
| // Each instance of wspr is expected to have a single (human) user at a time |
| // that will be signed in and using various apps. A user may have different |
| // Blessings, which in practice will be done by having multiple accounts |
| // across many Blessing providers (e.g., google, facebook, etc). This is similar |
| // to having a master identity that is linked to multiple identities in today's |
| // technology. In our case, each user account is represented as a Blessing |
| // obtained from the corresponding Blessing provider. |
| // |
| // Every app/origin is a different principal and has its own public/private key |
| // pair, represented by a Principal object that is created by this manager on |
| // the app's behalf. For each app/origin, the user may choose which account to |
| // use for the app, which results in a blessing generated for the app principal |
| // using the selected account's blessing. |
| // |
| // For example, a user Alice may have an account at Google and Facebook, |
| // resulting in the blessings "google/alice@gmail.com" and |
| // "facebook/alice@facebook.com". She may then choose to use her Google account |
| // for the "googleplay" app, which would result in the creation of a new |
| // principal for that app and a blessing "google/alice@gmail.com/googleplay" for |
| // that principal. |
| // |
| // The principal manager only serializes the mapping from apps to (chosen) |
| // accounts and the account information, but not the private keys for each app. |
| package principal |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "net/url" |
| "sync" |
| "time" |
| |
| "v.io/v23/security" |
| "v.io/v23/verror" |
| "v.io/v23/vom" |
| vsecurity "v.io/x/ref/lib/security" |
| "v.io/x/ref/lib/security/serialization" |
| ) |
| |
| // permissions is a set of a permissions given to an app, containing the account |
| // the app has access to and the caveats associated with it. |
| type permissions struct { |
| // Account is the name of the account given to the app. |
| Account string |
| // Caveats that must be added to the blessing generated for the app from |
| // the account's blessing. |
| Caveats []security.Caveat |
| // Expirations is the expiration times of any expiration caveats. Must |
| // be unix-time so it can be persisted. |
| Expirations []int64 |
| } |
| |
| // persistentState is the state of the manager that will be persisted to disk. |
| type persistentState struct { |
| // A mapping of origins to the permissions provided for the origin (such as |
| // caveats and the account given to the origin) |
| Origins map[string]permissions |
| |
| // A set of accounts that maps from an account name to the Blessings associated |
| // with the account. |
| Accounts map[string]security.Blessings |
| } |
| |
| const pkgPath = "v.io/x/ref/services/wspr/internal/principal" |
| |
| // Errors. |
| var ( |
| errUnknownAccount = verror.Register(pkgPath+".errUnknownAccount", verror.NoRetry, "{1:}{2:} unknown account{:_}") |
| errFailedToCreatePrincipal = verror.Register(pkgPath+".errFailedToCreatePrincipal", verror.NoRetry, "{1:}{2:} failed to create new principal{:_}") |
| errFailedToConstructBlessings = verror.Register(pkgPath+".errFailedToConstructBlessings", verror.NoRetry, "{1:}{2:} failed to construct Blessings to bless with{:_}") |
| errFailedToBlessPrincipal = verror.Register(pkgPath+".errFailedToBlessPrincipal", verror.NoRetry, "{1:}{2:} failed to bless new principal with the provided account{:_}") |
| errFailedToSetDefaultBlessings = verror.Register(pkgPath+".errFailedToSetDefaultBlessings", verror.NoRetry, "{1:}{2:} failed to set account blessings as default{:_}") |
| errFailedToSetAllPrincipalBlessings = verror.Register(pkgPath+".errFailedToSetAllPrincipalBlessings", verror.NoRetry, "{1:}{2:} failed to set account blessings for all principals{:_}") |
| errFailedToAddRoots = verror.Register(pkgPath+".errFailedToAddRoots", verror.NoRetry, "{1:}{2:} failed to add roots of account blessing{:_}") |
| ) |
| |
| // bufferCloser implements io.ReadWriteCloser. |
| type bufferCloser struct { |
| bytes.Buffer |
| } |
| |
| func (*bufferCloser) Close() error { |
| return nil |
| } |
| |
| // InMemorySerializer implements SerializerReaderWriter. This Serializer should only |
| // be used in tests. |
| type InMemorySerializer struct { |
| data bufferCloser |
| signature bufferCloser |
| hasData bool |
| } |
| |
| func (s *InMemorySerializer) Readers() (io.ReadCloser, io.ReadCloser, error) { |
| if !s.hasData { |
| return nil, nil, nil |
| } |
| return &s.data, &s.signature, nil |
| } |
| |
| func (s *InMemorySerializer) Writers() (io.WriteCloser, io.WriteCloser, error) { |
| s.hasData = true |
| s.data.Reset() |
| s.signature.Reset() |
| return &s.data, &s.signature, nil |
| } |
| |
| // PrincipalManager manages app principals. We only serialize the accounts |
| // associated with this principal manager and the mapping of apps to permissions |
| // that they were given. |
| type PrincipalManager struct { |
| mu sync.Mutex |
| state persistentState |
| |
| // root is the Principal that hosts this PrincipalManager. |
| root security.Principal |
| |
| serializer vsecurity.SerializerReaderWriter |
| |
| // Dummy account name |
| // TODO(bjornick, nlacasse): Remove this once the tests no longer need |
| // it. |
| dummyAccount string |
| } |
| |
| // NewPrincipalManager returns a new PrincipalManager that creates new principals |
| // for various app/origins and blessed them with blessings for the provided 'root' |
| // principal from the specified blessing provider. |
| // . |
| // |
| // It is initialized by reading data from the 'serializer' passed in which must |
| // be non-nil. |
| func NewPrincipalManager(root security.Principal, serializer vsecurity.SerializerReaderWriter) (*PrincipalManager, error) { |
| result := &PrincipalManager{ |
| state: persistentState{ |
| Origins: map[string]permissions{}, |
| Accounts: map[string]security.Blessings{}, |
| }, |
| root: root, |
| serializer: serializer, |
| } |
| data, signature, err := serializer.Readers() |
| if err != nil { |
| return nil, err |
| } |
| if (data == nil) || (signature == nil) { |
| // No serialized data exists, returning an empty PrincipalManager. |
| return result, nil |
| } |
| vr, err := serialization.NewVerifyingReader(data, signature, root.PublicKey()) |
| if err != nil { |
| return nil, err |
| } |
| |
| decoder, err := vom.NewDecoder(vr) |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := decoder.Decode(&result.state); err != nil { |
| return nil, err |
| } |
| return result, nil |
| } |
| |
| func (i *PrincipalManager) save() error { |
| data, signature, err := i.serializer.Writers() |
| if err != nil { |
| return err |
| } |
| swc, err := serialization.NewSigningWriteCloser(data, signature, i.root, nil) |
| if err != nil { |
| return err |
| } |
| |
| encoder, err := vom.NewEncoder(swc) |
| |
| if err != nil { |
| return err |
| } |
| if err := encoder.Encode(i.state); err != nil { |
| return err |
| } |
| return swc.Close() |
| } |
| |
| // Principal returns the Principal for an origin or an error if there is |
| // no linked account. |
| func (i *PrincipalManager) Principal(origin string) (security.Principal, error) { |
| i.mu.Lock() |
| defer i.mu.Unlock() |
| perm, found := i.state.Origins[origin] |
| if !found { |
| return nil, verror.New(verror.ErrNoExist, nil, origin) |
| } |
| blessings, found := i.state.Accounts[perm.Account] |
| if !found { |
| return nil, verror.New(errUnknownAccount, nil, perm.Account) |
| } |
| return i.createPrincipal(origin, blessings, perm.Caveats) |
| } |
| |
| // OriginHasAccount returns true iff the origin has been associated with |
| // permissions and an account for which blessings have been obtained from a |
| // blessing provider, and if the blessings have no expiration caveats or an |
| // expiration in the future. |
| func (i *PrincipalManager) OriginHasAccount(origin string) bool { |
| i.mu.Lock() |
| defer i.mu.Unlock() |
| perm, found := i.state.Origins[origin] |
| if !found { |
| return false |
| } |
| |
| // Check if all expiration caveats are satisfied. |
| now := time.Now() |
| for _, unixExp := range perm.Expirations { |
| exp := time.Unix(unixExp, 0) |
| if exp.Before(now) { |
| return false |
| } |
| } |
| |
| _, found = i.state.Accounts[perm.Account] |
| return found |
| } |
| |
| // BlessingsForAccount returns the Blessing associated with the provided |
| // account. It returns an error if account does not exist. |
| // |
| // TODO(ataly, ashankar, bjornick): Modify this method to allow searching |
| // for accounts from a specific root blessing provider. One option is |
| // that the method could take a set of root blessing providers as argument |
| // and then return accounts whose blessings are from one of these providers. |
| func (i *PrincipalManager) BlessingsForAccount(account string) (security.Blessings, error) { |
| i.mu.Lock() |
| defer i.mu.Unlock() |
| |
| blessings, found := i.state.Accounts[account] |
| if !found { |
| return security.Blessings{}, verror.New(errUnknownAccount, nil, account) |
| } |
| return blessings, nil |
| } |
| |
| // AddAccount associates the provided Blessing with the provided account. |
| func (i *PrincipalManager) AddAccount(account string, blessings security.Blessings) error { |
| i.mu.Lock() |
| defer i.mu.Unlock() |
| |
| old, existed := i.state.Accounts[account] |
| i.state.Accounts[account] = blessings |
| |
| if err := i.save(); err != nil { |
| delete(i.state.Accounts, account) |
| if existed { |
| i.state.Accounts[account] = old |
| } |
| return err |
| } |
| return nil |
| } |
| |
| // AddOrigin adds an origin to the manager linked to the given account. |
| func (i *PrincipalManager) AddOrigin(origin string, account string, caveats []security.Caveat, expirations []time.Time) error { |
| i.mu.Lock() |
| defer i.mu.Unlock() |
| if _, found := i.state.Accounts[account]; !found { |
| return verror.New(errUnknownAccount, nil, account) |
| } |
| |
| unixExpirations := []int64{} |
| for _, exp := range expirations { |
| unixExpirations = append(unixExpirations, exp.Unix()) |
| } |
| |
| old, existed := i.state.Origins[origin] |
| i.state.Origins[origin] = permissions{account, caveats, unixExpirations} |
| |
| if err := i.save(); err != nil { |
| delete(i.state.Origins, origin) |
| if existed { |
| i.state.Origins[origin] = old |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func (i *PrincipalManager) createPrincipal(origin string, withBlessings security.Blessings, caveats []security.Caveat) (security.Principal, error) { |
| ret, err := vsecurity.NewPrincipal() |
| if err != nil { |
| return nil, verror.New(errFailedToCreatePrincipal, nil, err) |
| } |
| |
| if len(caveats) == 0 { |
| caveats = append(caveats, security.UnconstrainedUse()) |
| } |
| // Origins have the form protocol://hostname:port, which is not a valid |
| // blessing extension. Hence we must url-encode. |
| blessings, err := i.root.Bless(ret.PublicKey(), withBlessings, url.QueryEscape(origin), caveats[0], caveats[1:]...) |
| if err != nil { |
| return nil, verror.New(errFailedToBlessPrincipal, nil, err) |
| } |
| |
| if err := ret.BlessingStore().SetDefault(blessings); err != nil { |
| return nil, verror.New(errFailedToSetDefaultBlessings, nil, err) |
| } |
| if _, err := ret.BlessingStore().Set(blessings, security.AllPrincipals); err != nil { |
| return nil, verror.New(errFailedToSetAllPrincipalBlessings, nil, err) |
| } |
| if err := ret.AddToRoots(blessings); err != nil { |
| return nil, verror.New(errFailedToAddRoots, nil, err) |
| } |
| return ret, nil |
| } |
| |
| // Add dummy account with default blessings, for use by unauthenticated |
| // clients. |
| // TODO(nlacasse, bjornick): This should go away once unauthenticate clients |
| // are no longer allowed. |
| func (i *PrincipalManager) DummyAccount() (string, error) { |
| if i.dummyAccount == "" { |
| // Note: We only set i.dummyAccount once the account has been |
| // successfully created. Otherwise, if an error occurs, the |
| // next time this function is called it the account won't exist |
| // but this function will return the name of the account |
| // without trying to create it. |
| dummyAccount := "unauthenticated-dummy-account" |
| blessings, err := i.root.BlessSelf(dummyAccount) |
| if err != nil { |
| return "", fmt.Errorf("i.root.BlessSelf(%v) failed: %v", dummyAccount, err) |
| } |
| |
| if err := i.AddAccount(dummyAccount, blessings); err != nil { |
| return "", fmt.Errorf("browspr.principalManager.AddAccount(%v, %v) failed: %v", dummyAccount, blessings, err) |
| } |
| i.dummyAccount = dummyAccount |
| } |
| return i.dummyAccount, nil |
| } |