| // 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 impl |
| |
| import ( |
| "fmt" |
| "os" |
| "path" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/naming" |
| "v.io/v23/rpc" |
| "v.io/v23/security" |
| "v.io/v23/security/access" |
| "v.io/v23/services/device" |
| "v.io/v23/services/pprof" |
| "v.io/v23/services/stats" |
| "v.io/v23/vdl" |
| "v.io/v23/vdlroot/signature" |
| "v.io/v23/verror" |
| "v.io/x/lib/vlog" |
| "v.io/x/ref" |
| "v.io/x/ref/services/agent/keymgr" |
| s_device "v.io/x/ref/services/device" |
| "v.io/x/ref/services/device/internal/config" |
| "v.io/x/ref/services/internal/logreaderlib" |
| "v.io/x/ref/services/internal/pathperms" |
| ) |
| |
| // internalState wraps state shared between different device manager |
| // invocations. |
| type internalState struct { |
| callback *callbackState |
| updating *updatingState |
| securityAgent *securityAgentState |
| restartHandler func() |
| testMode bool |
| // reap is the app process monitoring subsystem. |
| reap *reaper |
| } |
| |
| // dispatcher holds the state of the device manager dispatcher. |
| type dispatcher struct { |
| // internal holds the state that persists across RPC method invocations. |
| internal *internalState |
| // config holds the device manager's (immutable) configuration state. |
| config *config.State |
| // dispatcherMutex is a lock for coordinating concurrent access to some |
| // dispatcher methods. |
| mu sync.RWMutex |
| // TODO(rjkroege): Consider moving this inside internal. |
| uat BlessingSystemAssociationStore |
| permsStore *pathperms.PathStore |
| // Namespace |
| mtAddress string // The address of the local mounttable. |
| } |
| |
| var _ rpc.Dispatcher = (*dispatcher)(nil) |
| |
| const ( |
| appsSuffix = "apps" |
| deviceSuffix = "device" |
| configSuffix = "cfg" |
| |
| pkgPath = "v.io/x/ref/services/device/internal/impl" |
| ) |
| |
| var ( |
| ErrInvalidSuffix = verror.Register(pkgPath+".InvalidSuffix", verror.NoRetry, "{1:}{2:} invalid suffix{:_}") |
| ErrOperationFailed = verror.Register(pkgPath+".OperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}") |
| ErrOperationInProgress = verror.Register(pkgPath+".OperationInProgress", verror.NoRetry, "{1:}{2:} operation in progress{:_}") |
| ErrAppTitleMismatch = verror.Register(pkgPath+".AppTitleMismatch", verror.NoRetry, "{1:}{2:} app title mismatch{:_}") |
| ErrUpdateNoOp = verror.Register(pkgPath+".UpdateNoOp", verror.NoRetry, "{1:}{2:} update is no op{:_}") |
| ErrInvalidOperation = verror.Register(pkgPath+".InvalidOperation", verror.NoRetry, "{1:}{2:} invalid operation{:_}") |
| ErrInvalidBlessing = verror.Register(pkgPath+".InvalidBlessing", verror.NoRetry, "{1:}{2:} invalid blessing{:_}") |
| ErrInvalidPairingToken = verror.Register(pkgPath+".InvalidPairingToken", verror.NoRetry, "{1:}{2:} pairing token mismatch{:_}") |
| ErrUnclaimedDevice = verror.Register(pkgPath+".UnclaimedDevice", verror.NoRetry, "{1:}{2:} device needs to be claimed first") |
| ErrDeviceAlreadyClaimed = verror.Register(pkgPath+".AlreadyClaimed", verror.NoRetry, "{1:}{2:} device has already been claimed") |
| |
| errInvalidConfig = verror.Register(pkgPath+".errInvalidConfig", verror.NoRetry, "{1:}{2:} invalid config {3}{:_}") |
| errCantCreateAccountStore = verror.Register(pkgPath+".errCantCreateAccountStore", verror.NoRetry, "{1:}{2:} cannot create persistent store for identity to system account associations{:_}") |
| errCantCreateAppWatcher = verror.Register(pkgPath+".errCantCreateAppWatcher", verror.NoRetry, "{1:}{2:} cannot create app status watcher{:_}") |
| errNewAgentFailed = verror.Register(pkgPath+".errNewAgentFailed", verror.NoRetry, "{1:}{2:} NewAgent() failed{:_}") |
| ) |
| |
| // NewClaimableDispatcher returns an rpc.Dispatcher that allows the device to |
| // be Claimed if it hasn't been already and a channel that will be closed once |
| // the device has been claimed. |
| // |
| // It returns (nil, nil) if the device is no longer claimable. |
| func NewClaimableDispatcher(ctx *context.T, config *config.State, pairingToken string) (rpc.Dispatcher, <-chan struct{}) { |
| var ( |
| permsDir = PermsDir(config) |
| permsStore = pathperms.NewPathStore(v23.GetPrincipal(ctx)) |
| ) |
| if _, _, err := permsStore.Get(permsDir); !os.IsNotExist(err) { |
| return nil, nil |
| } |
| // The device is claimable only if Claim hasn't been called before. The |
| // existence of the Permissions file is an indication of a successful prior |
| // call to Claim. |
| notify := make(chan struct{}) |
| return &claimable{token: pairingToken, permsStore: permsStore, permsDir: permsDir, notify: notify}, notify |
| } |
| |
| // NewDispatcher is the device manager dispatcher factory. |
| func NewDispatcher(ctx *context.T, config *config.State, mtAddress string, testMode bool, restartHandler func(), permStore *pathperms.PathStore) (rpc.Dispatcher, error) { |
| if err := config.Validate(); err != nil { |
| return nil, verror.New(errInvalidConfig, ctx, config, err) |
| } |
| uat, err := NewBlessingSystemAssociationStore(config.Root) |
| if err != nil { |
| return nil, verror.New(errCantCreateAccountStore, ctx, err) |
| } |
| initSuidHelper(config.Helper) |
| d := &dispatcher{ |
| internal: &internalState{ |
| callback: newCallbackState(config.Name), |
| updating: newUpdatingState(), |
| restartHandler: restartHandler, |
| testMode: testMode, |
| }, |
| config: config, |
| uat: uat, |
| permsStore: permStore, |
| mtAddress: mtAddress, |
| } |
| |
| // If we're in 'security agent mode', set up the key manager agent. |
| if len(os.Getenv(ref.EnvAgentEndpoint)) > 0 { |
| if keyMgrAgent, err := keymgr.NewAgent(); err != nil { |
| return nil, verror.New(errNewAgentFailed, ctx, err) |
| } else { |
| d.internal.securityAgent = &securityAgentState{ |
| keyMgrAgent: keyMgrAgent, |
| } |
| } |
| } |
| reap, err := newReaper(ctx, config.Root, &appRunner{ |
| callback: d.internal.callback, |
| securityAgent: d.internal.securityAgent, |
| }) |
| if err != nil { |
| return nil, verror.New(errCantCreateAppWatcher, ctx, err) |
| } |
| d.internal.reap = reap |
| |
| if testMode { |
| return &testModeDispatcher{d}, nil |
| } |
| return d, nil |
| } |
| |
| // Shutdown the dispatcher. |
| func Shutdown(rpcd rpc.Dispatcher) { |
| switch d := rpcd.(type) { |
| case *dispatcher: |
| d.internal.reap.shutdown() |
| case *testModeDispatcher: |
| Shutdown(d.realDispatcher) |
| default: |
| vlog.Panicf("%v not a supported dispatcher type.", rpcd) |
| } |
| } |
| |
| // Logging invoker that logs any error messages before returning. |
| func newLoggingInvoker(obj interface{}) (rpc.Invoker, error) { |
| if invoker, ok := obj.(rpc.Invoker); ok { |
| return &loggingInvoker{invoker}, nil |
| } |
| invoker, err := rpc.ReflectInvoker(obj) |
| if err != nil { |
| vlog.Errorf("rpc.ReflectInvoker returned error: %v", err) |
| return nil, err |
| } |
| return &loggingInvoker{invoker}, nil |
| } |
| |
| type loggingInvoker struct { |
| invoker rpc.Invoker |
| } |
| |
| func (l *loggingInvoker) Prepare(method string, numArgs int) (argptrs []interface{}, tags []*vdl.Value, err error) { |
| argptrs, tags, err = l.invoker.Prepare(method, numArgs) |
| if err != nil { |
| vlog.Errorf("Prepare(%s %d) returned error: %v", method, numArgs, err) |
| } |
| return |
| } |
| |
| func (l *loggingInvoker) Invoke(ctx *context.T, call rpc.StreamServerCall, method string, argptrs []interface{}) (results []interface{}, err error) { |
| results, err = l.invoker.Invoke(ctx, call, method, argptrs) |
| if err != nil { |
| vlog.Errorf("Invoke(method:%s argptrs:%v) returned error: %v", method, argptrs, err) |
| } |
| return |
| } |
| |
| func (l *loggingInvoker) Signature(ctx *context.T, call rpc.ServerCall) ([]signature.Interface, error) { |
| sig, err := l.invoker.Signature(ctx, call) |
| if err != nil { |
| vlog.Errorf("Signature returned error: %v", err) |
| } |
| return sig, err |
| } |
| |
| func (l *loggingInvoker) MethodSignature(ctx *context.T, call rpc.ServerCall, method string) (signature.Method, error) { |
| methodSig, err := l.invoker.MethodSignature(ctx, call, method) |
| if err != nil { |
| vlog.Errorf("MethodSignature(%s) returned error: %v", method, err) |
| } |
| return methodSig, err |
| } |
| |
| func (l *loggingInvoker) Globber() *rpc.GlobState { |
| return l.invoker.Globber() |
| } |
| |
| // DISPATCHER INTERFACE IMPLEMENTATION |
| func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) { |
| invoker, auth, err := d.internalLookup(suffix) |
| if err != nil { |
| return nil, nil, err |
| } |
| loggingInvoker, err := newLoggingInvoker(invoker) |
| if err != nil { |
| return nil, nil, err |
| } |
| return loggingInvoker, auth, nil |
| } |
| |
| func newTestableHierarchicalAuth(testMode bool, rootDir, childDir string, get pathperms.PermsGetter) (security.Authorizer, error) { |
| if testMode { |
| // In test mode, the device manager will not be able to read the |
| // Permissions, because they were signed with the key of the real device |
| // manager. It's not a problem because the testModeDispatcher overrides the |
| // authorizer anyway. |
| return nil, nil |
| } |
| return pathperms.NewHierarchicalAuthorizer(rootDir, childDir, get) |
| } |
| |
| func (d *dispatcher) internalLookup(suffix string) (interface{}, security.Authorizer, error) { |
| components := strings.Split(suffix, "/") |
| for i := 0; i < len(components); i++ { |
| if len(components[i]) == 0 { |
| components = append(components[:i], components[i+1:]...) |
| i-- |
| } |
| } |
| |
| // TODO(rjkroege): Permit the root Permissions to diverge for the device and |
| // app sub-namespaces of the device manager after claiming. |
| auth, err := newTestableHierarchicalAuth(d.internal.testMode, PermsDir(d.config), PermsDir(d.config), d.permsStore) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| if len(components) == 0 { |
| return rpc.ChildrenGlobberInvoker(deviceSuffix, appsSuffix), auth, nil |
| } |
| // The implementation of the device manager is split up into several |
| // invokers, which are instantiated depending on the receiver name |
| // prefix. |
| switch components[0] { |
| case deviceSuffix: |
| receiver := device.DeviceServer(&deviceService{ |
| callback: d.internal.callback, |
| updating: d.internal.updating, |
| restartHandler: d.internal.restartHandler, |
| config: d.config, |
| disp: d, |
| uat: d.uat, |
| securityAgent: d.internal.securityAgent, |
| }) |
| return receiver, auth, nil |
| case appsSuffix: |
| // Requests to apps/*/*/*/logs are handled locally by LogFileService. |
| // Requests to apps/*/*/*/pprof are proxied to the apps' __debug/pprof object. |
| // Requests to apps/*/*/*/stats are proxied to the apps' __debug/stats object. |
| // Everything else is handled by the Application server. |
| if len(components) >= 5 { |
| appInstanceDir, err := instanceDir(d.config.Root, components[1:4]) |
| if err != nil { |
| return nil, nil, err |
| } |
| switch kind := components[4]; kind { |
| case "logs": |
| logsDir := filepath.Join(appInstanceDir, "logs") |
| suffix := naming.Join(components[5:]...) |
| appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore) |
| if err != nil { |
| return nil, nil, err |
| } |
| return logreaderlib.NewLogFileService(logsDir, suffix), appSpecificAuthorizer, nil |
| case "pprof", "stats": |
| info, err := loadInstanceInfo(nil, appInstanceDir) |
| if err != nil { |
| return nil, nil, err |
| } |
| if !instanceStateIs(appInstanceDir, device.InstanceStateRunning) { |
| return nil, nil, verror.New(ErrInvalidSuffix, nil) |
| } |
| var desc []rpc.InterfaceDesc |
| switch kind { |
| case "pprof": |
| desc = pprof.PProfServer(nil).Describe__() |
| case "stats": |
| desc = stats.StatsServer(nil).Describe__() |
| } |
| suffix := naming.Join("__debug", naming.Join(components[4:]...)) |
| remote := naming.JoinAddressName(info.AppCycleMgrName, suffix) |
| |
| // Use hierarchical auth with debugacls under debug access. |
| appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore) |
| if err != nil { |
| return nil, nil, err |
| } |
| return newProxyInvoker(remote, access.Debug, desc), appSpecificAuthorizer, nil |
| } |
| } |
| receiver := device.ApplicationServer(&appService{ |
| config: d.config, |
| suffix: components[1:], |
| uat: d.uat, |
| permsStore: d.permsStore, |
| runner: &appRunner{ |
| reap: d.internal.reap, |
| callback: d.internal.callback, |
| securityAgent: d.internal.securityAgent, |
| mtAddress: d.mtAddress, |
| }, |
| }) |
| appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore) |
| if err != nil { |
| return nil, nil, err |
| } |
| return receiver, appSpecificAuthorizer, nil |
| case configSuffix: |
| if len(components) != 2 { |
| return nil, nil, verror.New(ErrInvalidSuffix, nil) |
| } |
| receiver := s_device.ConfigServer(&configService{ |
| callback: d.internal.callback, |
| suffix: components[1], |
| }) |
| // The nil authorizer ensures that only principals blessed by |
| // the device manager can talk back to it. All apps started by |
| // the device manager should fall in that category. |
| // |
| // TODO(caprita,rjkroege): We should further refine this, by |
| // only allowing the app to update state referring to itself |
| // (and not other apps). |
| return receiver, nil, nil |
| default: |
| return nil, nil, verror.New(ErrInvalidSuffix, nil) |
| } |
| } |
| |
| // testModeDispatcher is a wrapper around the real dispatcher. It returns the |
| // exact same object as the real dispatcher, but the authorizer only allows |
| // calls to "device".Delete(). |
| type testModeDispatcher struct { |
| realDispatcher rpc.Dispatcher |
| } |
| |
| func (d *testModeDispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) { |
| obj, _, err := d.realDispatcher.Lookup(suffix) |
| return obj, d, err |
| } |
| |
| func (testModeDispatcher) Authorize(ctx *context.T, call security.Call) error { |
| if call.Suffix() == deviceSuffix && call.Method() == "Delete" { |
| vlog.Infof("testModeDispatcher.Authorize: Allow %q.%s()", call.Suffix(), call.Method()) |
| return nil |
| } |
| vlog.Infof("testModeDispatcher.Authorize: Reject %q.%s()", call.Suffix(), call.Method()) |
| return verror.New(ErrInvalidSuffix, nil) |
| } |
| |
| func newAppSpecificAuthorizer(sec security.Authorizer, config *config.State, suffix []string, getter pathperms.PermsGetter) (security.Authorizer, error) { |
| // TODO(rjkroege): This does not support <appname>.Start() to start all |
| // instances. Correct this. |
| |
| // If we are attempting a method invocation against "apps/", we use the root |
| // Permissions. |
| if len(suffix) == 0 || len(suffix) == 1 { |
| return sec, nil |
| } |
| // Otherwise, we require a per-installation and per-instance Permissions file. |
| if len(suffix) == 2 { |
| p, err := installationDirCore(suffix, config.Root) |
| if err != nil { |
| return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err)) |
| } |
| return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "acls"), getter) |
| } |
| // Use the special debugacls for instance/logs, instance/pprof, instance/stats. |
| if len(suffix) > 3 && (suffix[3] == "logs" || suffix[3] == "pprof" || suffix[3] == "stats") { |
| p, err := instanceDir(config.Root, suffix[0:3]) |
| if err != nil { |
| return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err)) |
| } |
| return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "debugacls"), getter) |
| } |
| |
| p, err := instanceDir(config.Root, suffix[0:3]) |
| if err != nil { |
| return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err)) |
| } |
| return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "acls"), getter) |
| } |