| package impl |
| |
| // The app invoker is responsible for managing the state of applications on the |
| // device manager. The device manager manages the applications it installs and |
| // runs using the following directory structure: |
| // |
| // TODO(caprita): Not all is yet implemented. |
| // |
| // <config.Root>/ |
| // app-<hash 1>/ - the application dir is named using a hash of the application title |
| // installation-<id 1>/ - installations are labelled with ids |
| // acls/ |
| // data - the ACL data for this |
| // installation. Controls acces to |
| // Start, Uinstall, Update, UpdateTo |
| // and Revert. |
| // signature - the signature for the ACLs in data |
| // <status> - one of the values for installationState enum |
| // origin - object name for application envelope |
| // <version 1 timestamp>/ - timestamp of when the version was downloaded |
| // bin - application binary |
| // previous - symbolic link to previous version directory |
| // envelope - application envelope (JSON-encoded) |
| // pkg/ - the application packages |
| // <pkg name> |
| // <pkg name>.__info |
| // ... |
| // <version 2 timestamp> |
| // ... |
| // current - symbolic link to the current version |
| // instances/ |
| // instance-<id a>/ - instances are labelled with ids |
| // credentials/ - holds veyron credentials (unless running |
| // through security agent) |
| // root/ - workspace that the instance is run from |
| // packages/ - the installed packages |
| // <pkg name>/ |
| // ... |
| // logs/ - stderr/stdout and log files generated by instance |
| // info - metadata for the instance (such as app |
| // cycle manager name and process id) |
| // version - symbolic link to installation version for the instance |
| // acls/ |
| // data - the ACLs for this instance. These |
| // ACLs control access to Refresh, |
| // Restart, Resume, Stop and |
| // Suspend. |
| // signature - the signature for these ACLs. |
| // <status> - one of the values for instanceState enum |
| // systemname - the system name used to execute this instance |
| // instance-<id b> |
| // ... |
| // installation-<id 2> |
| // ... |
| // app-<hash 2> |
| // ... |
| // |
| // The device manager uses the suid helper binary to invoke an application as a |
| // specified user. The path to the helper is specified as config.Helper. |
| |
| // When device manager starts up, it goes through all instances and resumes the |
| // ones that are not suspended. If the application was still running, it |
| // suspends it first. If an application fails to resume, it stays suspended. |
| // |
| // When device manager shuts down, it suspends all running instances. |
| // |
| // Start starts an instance. Suspend kills the process but leaves the workspace |
| // untouched. Resume restarts the process. Stop kills the process and prevents |
| // future resumes (it also eventually gc's the workspace). |
| // |
| // If the process dies on its own, it stays dead and is assumed suspended. |
| // TODO(caprita): Later, we'll add auto-restart option. |
| // |
| // Concurrency model: installations can be created independently of one another; |
| // installations can be removed at any time (any running instances will be |
| // stopped). The first call to Uninstall will rename the installation dir as a |
| // first step; subsequent Uninstall's will fail. Instances can be created |
| // independently of one another, as long as the installation exists (if it gets |
| // Uninstall'ed during an instance Start, the Start may fail). |
| // |
| // The status file present in each instance is used to flag the state of the |
| // instance and prevent concurrent operations against the instance: |
| // |
| // - when an instance is created with Start, it is placed in state 'suspended'. |
| // To run the instance, Start transitions 'suspended' to 'starting' and then |
| // 'started' (upon success) or the instance is deleted (upon failure). |
| // |
| // - Suspend attempts to transition from 'started' to 'suspending' (if the |
| // instance was not in 'started' state, Suspend fails). From 'suspending', the |
| // instance transitions to 'suspended' upon success or back to 'started' upon |
| // failure. |
| // |
| // - Resume attempts to transition from 'suspended' to 'starting' (if the |
| // instance was not in 'suspended' state, Resume fails). From 'starting', the |
| // instance transitions to 'started' upon success or back to 'suspended' upon |
| // failure. |
| // |
| // - Stop attempts to transition from 'started' to 'stopping' and then to |
| // 'stopped' (upon success) or back to 'started' (upon failure); or from |
| // 'suspended' to 'stopped'. If the initial state is neither 'started' or |
| // 'suspended', Stop fails. |
| // |
| // TODO(caprita): There is room for synergy between how device manager organizes |
| // its own workspace and that for the applications it runs. In particular, |
| // previous, origin, and envelope could be part of a single config. We'll |
| // refine that later. |
| |
| import ( |
| "crypto/md5" |
| "crypto/rand" |
| "encoding/base64" |
| "encoding/binary" |
| "encoding/hex" |
| "encoding/json" |
| "fmt" |
| "hash/crc64" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "os/user" |
| "path" |
| "path/filepath" |
| "reflect" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "v.io/core/veyron2" |
| "v.io/core/veyron2/context" |
| "v.io/core/veyron2/ipc" |
| "v.io/core/veyron2/mgmt" |
| "v.io/core/veyron2/naming" |
| "v.io/core/veyron2/options" |
| "v.io/core/veyron2/security" |
| "v.io/core/veyron2/services/mgmt/appcycle" |
| "v.io/core/veyron2/services/mgmt/application" |
| "v.io/core/veyron2/services/security/access" |
| "v.io/core/veyron2/verror2" |
| "v.io/core/veyron2/vlog" |
| |
| vexec "v.io/core/veyron/lib/exec" |
| "v.io/core/veyron/lib/flags/consts" |
| vsecurity "v.io/core/veyron/security" |
| "v.io/core/veyron/security/agent" |
| "v.io/core/veyron/security/agent/keymgr" |
| iconfig "v.io/core/veyron/services/mgmt/device/config" |
| libbinary "v.io/core/veyron/services/mgmt/lib/binary" |
| libpackages "v.io/core/veyron/services/mgmt/lib/packages" |
| ) |
| |
| // instanceInfo holds state about a running instance. |
| type instanceInfo struct { |
| AppCycleMgrName string |
| Pid int |
| DeviceManagerPeerPattern string |
| SecurityAgentHandle []byte |
| } |
| |
| func saveInstanceInfo(dir string, info *instanceInfo) error { |
| jsonInfo, err := json.Marshal(info) |
| if err != nil { |
| vlog.Errorf("Marshal(%v) failed: %v", info, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| infoPath := filepath.Join(dir, "info") |
| if err := ioutil.WriteFile(infoPath, jsonInfo, 0600); err != nil { |
| vlog.Errorf("WriteFile(%v) failed: %v", infoPath, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| return nil |
| } |
| |
| func loadInstanceInfo(dir string) (*instanceInfo, error) { |
| infoPath := filepath.Join(dir, "info") |
| info := new(instanceInfo) |
| if infoBytes, err := ioutil.ReadFile(infoPath); err != nil { |
| vlog.Errorf("ReadFile(%v) failed: %v", infoPath, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } else if err := json.Unmarshal(infoBytes, info); err != nil { |
| vlog.Errorf("Unmarshal(%v) failed: %v", infoBytes, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } |
| return info, nil |
| } |
| |
| type securityAgentState struct { |
| // Security agent key manager client. |
| keyMgrAgent *keymgr.Agent |
| // Ensures only one security agent connection socket is created |
| // at any time, preventing fork/exec from potentially passing |
| // down sockets meant for other children (as per ribrdb@, Go's |
| // exec implementation does not prune the set of files passed |
| // down to only include those specified in cmd.ExtraFiles). |
| startLock sync.Mutex |
| } |
| |
| // appService implements the Device manager's Application interface. |
| type appService struct { |
| callback *callbackState |
| config *iconfig.State |
| // suffix contains the name components of the current invocation name |
| // suffix. It is used to identify an application, installation, or |
| // instance. |
| suffix []string |
| uat BlessingSystemAssociationStore |
| locks aclLocks |
| // Reference to the devicemanager top-level ACL list. |
| deviceACL access.TaggedACLMap |
| // securityAgent holds state related to the security agent (nil if not |
| // using the agent). |
| securityAgent *securityAgentState |
| } |
| |
| func saveEnvelope(dir string, envelope *application.Envelope) error { |
| jsonEnvelope, err := json.Marshal(envelope) |
| if err != nil { |
| vlog.Errorf("Marshal(%v) failed: %v", envelope, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| path := filepath.Join(dir, "envelope") |
| if err := ioutil.WriteFile(path, jsonEnvelope, 0600); err != nil { |
| vlog.Errorf("WriteFile(%v) failed: %v", path, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| return nil |
| } |
| |
| func loadEnvelope(dir string) (*application.Envelope, error) { |
| path := filepath.Join(dir, "envelope") |
| envelope := new(application.Envelope) |
| if envelopeBytes, err := ioutil.ReadFile(path); err != nil { |
| vlog.Errorf("ReadFile(%v) failed: %v", path, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } else if err := json.Unmarshal(envelopeBytes, envelope); err != nil { |
| vlog.Errorf("Unmarshal(%v) failed: %v", envelopeBytes, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } |
| return envelope, nil |
| } |
| |
| func saveOrigin(dir, originVON string) error { |
| path := filepath.Join(dir, "origin") |
| if err := ioutil.WriteFile(path, []byte(originVON), 0600); err != nil { |
| vlog.Errorf("WriteFile(%v) failed: %v", path, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| return nil |
| } |
| |
| func loadOrigin(dir string) (string, error) { |
| path := filepath.Join(dir, "origin") |
| if originBytes, err := ioutil.ReadFile(path); err != nil { |
| vlog.Errorf("ReadFile(%v) failed: %v", path, err) |
| return "", verror2.Make(ErrOperationFailed, nil) |
| } else { |
| return string(originBytes), nil |
| } |
| } |
| |
| // generateID returns a new unique id string. The uniqueness is based on the |
| // current timestamp. Not cryptographically secure. |
| func generateID() string { |
| timestamp := fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)) |
| h := crc64.New(crc64.MakeTable(crc64.ISO)) |
| h.Write([]byte(timestamp)) |
| b := make([]byte, 8) |
| binary.LittleEndian.PutUint64(b, uint64(h.Sum64())) |
| return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") |
| } |
| |
| // generateRandomString returns a cryptographically-strong random string. |
| func generateRandomString() (string, error) { |
| b := make([]byte, 16) |
| _, err := rand.Read(b) |
| if err != nil { |
| return "", err |
| } |
| return hex.EncodeToString(b), nil |
| } |
| |
| // TODO(caprita): Nothing prevents different applications from sharing the same |
| // title, and thereby being installed in the same app dir. Do we want to |
| // prevent that for the same user or across users? |
| |
| // applicationDirName generates a cryptographic hash of the application title, |
| // to be used as a directory name for installations of the application with the |
| // given title. |
| func applicationDirName(title string) string { |
| h := md5.New() |
| h.Write([]byte(title)) |
| hash := strings.TrimRight(base64.URLEncoding.EncodeToString(h.Sum(nil)), "=") |
| return "app-" + hash |
| } |
| |
| func installationDirName(installationID string) string { |
| return "installation-" + installationID |
| } |
| |
| func instanceDirName(instanceID string) string { |
| return "instance-" + instanceID |
| } |
| |
| func mkdir(dir string) error { |
| perm := os.FileMode(0700) |
| if err := os.MkdirAll(dir, perm); err != nil { |
| vlog.Errorf("MkdirAll(%v, %v) failed: %v", dir, perm, err) |
| return err |
| } |
| return nil |
| } |
| |
| func fetchAppEnvelope(ctx *context.T, origin string) (*application.Envelope, error) { |
| envelope, err := fetchEnvelope(ctx, origin) |
| if err != nil { |
| return nil, err |
| } |
| if envelope.Title == application.DeviceManagerTitle { |
| // Disallow device manager apps from being installed like a |
| // regular app. |
| return nil, verror2.Make(ErrInvalidOperation, ctx) |
| } |
| return envelope, nil |
| } |
| |
| // newVersion sets up the directory for a new application version. |
| func newVersion(ctx *context.T, installationDir string, envelope *application.Envelope, oldVersionDir string) (string, error) { |
| versionDir := filepath.Join(installationDir, generateVersionDirName()) |
| if err := mkdir(versionDir); err != nil { |
| return "", verror2.Make(ErrOperationFailed, nil) |
| } |
| pkgDir := filepath.Join(versionDir, "pkg") |
| if err := mkdir(pkgDir); err != nil { |
| return "", verror2.Make(ErrOperationFailed, nil) |
| } |
| // TODO(caprita): Share binaries if already existing locally. |
| if err := downloadBinary(ctx, versionDir, "bin", envelope.Binary); err != nil { |
| return versionDir, err |
| } |
| for localPkg, pkgName := range envelope.Packages { |
| if localPkg == "" || localPkg[0] == '.' || strings.Contains(localPkg, string(filepath.Separator)) { |
| vlog.Infof("invalid local package name: %q", localPkg) |
| return versionDir, verror2.Make(ErrOperationFailed, nil) |
| } |
| path := filepath.Join(pkgDir, localPkg) |
| if err := libbinary.DownloadToFile(ctx, pkgName, path); err != nil { |
| vlog.Infof("DownloadToFile(%q, %q) failed: %v", pkgName, path, err) |
| return versionDir, verror2.Make(ErrOperationFailed, nil) |
| } |
| } |
| if err := saveEnvelope(versionDir, envelope); err != nil { |
| return versionDir, err |
| } |
| if oldVersionDir != "" { |
| previousLink := filepath.Join(versionDir, "previous") |
| if err := os.Symlink(oldVersionDir, previousLink); err != nil { |
| vlog.Errorf("Symlink(%v, %v) failed: %v", oldVersionDir, previousLink, err) |
| return versionDir, verror2.Make(ErrOperationFailed, nil) |
| } |
| } |
| // updateLink should be the last thing we do, after we've ensured the |
| // new version is viable (currently, that just means it installs |
| // properly). |
| return versionDir, updateLink(versionDir, filepath.Join(installationDir, "current")) |
| } |
| |
| // TODO(rjkroege): Refactor this code with the instance creation code. |
| func initializeInstallationACLs(principal security.Principal, dir string, blessings []string, acl access.TaggedACLMap) error { |
| // Add the invoker's blessings. |
| for _, b := range blessings { |
| for _, tag := range access.AllTypicalTags() { |
| acl.Add(security.BlessingPattern(b), string(tag)) |
| } |
| } |
| aclDir := path.Join(dir, "acls") |
| aclData := path.Join(aclDir, "data") |
| aclSig := path.Join(aclDir, "signature") |
| return writeACLs(principal, aclData, aclSig, aclDir, acl) |
| } |
| |
| func (i *appService) Install(call ipc.ServerContext, applicationVON string) (string, error) { |
| if len(i.suffix) > 0 { |
| return "", verror2.Make(ErrInvalidSuffix, call.Context()) |
| } |
| ctx, cancel := context.WithTimeout(call.Context(), ipcContextTimeout) |
| defer cancel() |
| envelope, err := fetchAppEnvelope(ctx, applicationVON) |
| if err != nil { |
| return "", err |
| } |
| installationID := generateID() |
| installationDir := filepath.Join(i.config.Root, applicationDirName(envelope.Title), installationDirName(installationID)) |
| deferrer := func() { |
| cleanupDir(installationDir, "") |
| } |
| defer func() { |
| if deferrer != nil { |
| deferrer() |
| } |
| }() |
| if _, err := newVersion(call.Context(), installationDir, envelope, ""); err != nil { |
| return "", err |
| } |
| if err := saveOrigin(installationDir, applicationVON); err != nil { |
| return "", err |
| } |
| if err := initializeInstallation(installationDir, active); err != nil { |
| return "", err |
| } |
| |
| // TODO(caprita,rjkroege): Should the installation ACLs really be |
| // seeded with the device ACL? Instead, might want to hide the deviceACL |
| // from the app? |
| if err := initializeInstallationACLs(call.LocalPrincipal(), installationDir, call.RemoteBlessings().ForContext(call), i.deviceACL.Copy()); err != nil { |
| return "", err |
| } |
| deferrer = nil |
| return naming.Join(envelope.Title, installationID), nil |
| } |
| |
| func (*appService) Refresh(ipc.ServerContext) error { |
| // TODO(jsimsa): Implement. |
| return nil |
| } |
| |
| func (*appService) Restart(ipc.ServerContext) error { |
| // TODO(jsimsa): Implement. |
| return nil |
| } |
| |
| func openWriteFile(path string) (*os.File, error) { |
| perm := os.FileMode(0600) |
| file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, perm) |
| if err != nil { |
| vlog.Errorf("OpenFile(%v) failed: %v", path, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } |
| return file, nil |
| } |
| |
| func installationDirCore(components []string, root string) (string, error) { |
| if nComponents := len(components); nComponents != 2 { |
| return "", verror2.Make(ErrInvalidSuffix, nil) |
| } |
| app, installation := components[0], components[1] |
| installationDir := filepath.Join(root, applicationDirName(app), installationDirName(installation)) |
| if _, err := os.Stat(installationDir); err != nil { |
| if os.IsNotExist(err) { |
| return "", verror2.Make(verror2.NoExist, nil, naming.Join(components...)) |
| } |
| vlog.Errorf("Stat(%v) failed: %v", installationDir, err) |
| return "", verror2.Make(ErrOperationFailed, nil) |
| } |
| return installationDir, nil |
| } |
| |
| // setupPrincipal sets up the instance's principal, with the right blessings. |
| func setupPrincipal(ctx *context.T, instanceDir, versionDir string, call ipc.ServerContext, securityAgent *securityAgentState, info *instanceInfo) error { |
| var p security.Principal |
| if securityAgent != nil { |
| // TODO(caprita): Part of the cleanup upon destroying an |
| // instance, we should tell the agent to drop the principal. |
| handle, conn, err := securityAgent.keyMgrAgent.NewPrincipal(ctx, false) |
| defer conn.Close() |
| |
| runtime := veyron2.RuntimeFromContext(ctx) |
| client, err := runtime.NewClient(options.VCSecurityNone) |
| if err != nil { |
| vlog.Errorf("NewClient() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| defer client.Close() |
| // TODO(caprita): release the socket created by NewAgentPrincipal. |
| if p, err = agent.NewAgentPrincipal(client, int(conn.Fd()), ctx); err != nil { |
| vlog.Errorf("NewAgentPrincipal() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| info.SecurityAgentHandle = handle |
| } else { |
| credentialsDir := filepath.Join(instanceDir, "credentials") |
| // TODO(caprita): The app's system user id needs access to this dir. |
| // Use the suidhelper to chown it. |
| var err error |
| if p, err = vsecurity.CreatePersistentPrincipal(credentialsDir, nil); err != nil { |
| vlog.Errorf("CreatePersistentPrincipal(%v, nil) failed: %v", credentialsDir, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| } |
| // Read the app installation version's envelope to obtain the app title. |
| // |
| // NOTE: we could have gotten this from the suffix as well, but the |
| // format of the object name suffix may change in the future: there's no |
| // guarantee it will always include the title. |
| envelope, err := loadEnvelope(versionDir) |
| if err != nil { |
| return err |
| } |
| dmPrincipal := call.LocalPrincipal() |
| // Take the blessings conferred upon us by the Start-er, extend them |
| // with the app title. |
| grantedBlessings := call.Blessings() |
| if grantedBlessings == nil { |
| return verror2.Make(ErrInvalidBlessing, nil) |
| } |
| // TODO(caprita): Revisit UnconstrainedUse. |
| appBlessings, err := dmPrincipal.Bless(p.PublicKey(), grantedBlessings, envelope.Title, security.UnconstrainedUse()) |
| if err != nil { |
| vlog.Errorf("Bless() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| // The blessings we extended from the blessings that the Start-er |
| // granted are the default blessings for the app. |
| if err := p.BlessingStore().SetDefault(appBlessings); err != nil { |
| vlog.Errorf("BlessingStore.SetDefault() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| if _, err := p.BlessingStore().Set(appBlessings, security.AllPrincipals); err != nil { |
| vlog.Errorf("BlessingStore.Set() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| if err := p.AddToRoots(appBlessings); err != nil { |
| vlog.Errorf("AddToRoots() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| // In addition, we give the app separate blessings for the purpose of |
| // communicating with the device manager. |
| // |
| // NOTE(caprita/ataly): Giving the app an unconstrained blessing from |
| // the device manager's default presents the app with a blessing that's |
| // potentially more powerful than what is strictly needed to allow |
| // communication between device manager and app (which could be more |
| // narrowly accomplished by using a custom-purpose self-signed blessing |
| // that the device manger produces solely to talk to the app). |
| // |
| // TODO(caprita): Figure out if there is any feature value in providing |
| // the app with a device manager-derived blessing (e.g., may the app |
| // need to prove it's running on the device?). |
| dmBlessings, err := dmPrincipal.Bless(p.PublicKey(), dmPrincipal.BlessingStore().Default(), "callback", security.UnconstrainedUse()) |
| // Put the names of the device manager's default blessings as patterns |
| // for the child, so that the child uses the right blessing when talking |
| // back to the device manager. |
| names := dmPrincipal.BlessingStore().Default().ForContext(call) |
| for _, n := range names { |
| if _, err := p.BlessingStore().Set(dmBlessings, security.BlessingPattern(n)); err != nil { |
| vlog.Errorf("BlessingStore.Set() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| } |
| // We also want to override the app cycle manager's server blessing in |
| // the child (so that the device manager can send RPCs to it). We |
| // signal to the child's app manager to use a randomly generated pattern |
| // to extract the right blessing to use from its store for this purpose. |
| randomPattern, err := generateRandomString() |
| if err != nil { |
| vlog.Errorf("generateRandomString() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| if _, err := p.BlessingStore().Set(dmBlessings, security.BlessingPattern(randomPattern)); err != nil { |
| vlog.Errorf("BlessingStore.Set() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| info.DeviceManagerPeerPattern = randomPattern |
| if err := p.AddToRoots(dmBlessings); err != nil { |
| vlog.Errorf("AddToRoots() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| return nil |
| } |
| |
| // installationDir returns the path to the directory containing the app |
| // installation referred to by the invoker's suffix. Returns an error if the |
| // suffix does not name an installation or if the named installation does not |
| // exist. |
| func (i *appService) installationDir() (string, error) { |
| return installationDirCore(i.suffix, i.config.Root) |
| } |
| |
| // installPackages installs all the packages for a new instance. |
| func installPackages(versionDir, instanceDir string) error { |
| envelope, err := loadEnvelope(versionDir) |
| if err != nil { |
| return err |
| } |
| packagesDir := filepath.Join(instanceDir, "root", "packages") |
| if err := os.MkdirAll(packagesDir, os.FileMode(0700)); err != nil { |
| return err |
| } |
| // TODO(rthellend): Consider making the packages read-only and sharing |
| // them between apps or instances. |
| for pkg, _ := range envelope.Packages { |
| pkgFile := filepath.Join(versionDir, "pkg", pkg) |
| dstDir := filepath.Join(packagesDir, pkg) |
| if err := os.MkdirAll(dstDir, os.FileMode(0700)); err != nil { |
| return err |
| } |
| if err := libpackages.Install(pkgFile, dstDir); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func initializeInstanceACLs(principal security.Principal, instanceDir string, blessings []string, acl access.TaggedACLMap) error { |
| for _, b := range blessings { |
| for _, tag := range access.AllTypicalTags() { |
| acl.Add(security.BlessingPattern(b), string(tag)) |
| } |
| } |
| aclDir := path.Join(instanceDir, "acls") |
| aclData := path.Join(aclDir, "data") |
| aclSig := path.Join(aclDir, "signature") |
| return writeACLs(principal, aclData, aclSig, aclDir, acl) |
| } |
| |
| // newInstance sets up the directory for a new application instance. |
| func (i *appService) newInstance(call ipc.ServerContext) (string, string, error) { |
| installationDir, err := i.installationDir() |
| if err != nil { |
| return "", "", err |
| } |
| if !installationStateIs(installationDir, active) { |
| return "", "", verror2.Make(ErrInvalidOperation, call.Context()) |
| } |
| instanceID := generateID() |
| instanceDir := filepath.Join(installationDir, "instances", instanceDirName(instanceID)) |
| if mkdir(instanceDir) != nil { |
| return "", instanceID, verror2.Make(ErrOperationFailed, call.Context()) |
| } |
| currLink := filepath.Join(installationDir, "current") |
| versionDir, err := filepath.EvalSymlinks(currLink) |
| if err != nil { |
| vlog.Errorf("EvalSymlinks(%v) failed: %v", currLink, err) |
| return instanceDir, instanceID, verror2.Make(ErrOperationFailed, call.Context()) |
| } |
| versionLink := filepath.Join(instanceDir, "version") |
| if err := os.Symlink(versionDir, versionLink); err != nil { |
| vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err) |
| return instanceDir, instanceID, verror2.Make(ErrOperationFailed, call.Context()) |
| } |
| if err := installPackages(versionDir, instanceDir); err != nil { |
| vlog.Errorf("installPackages(%v, %v) failed: %v", versionDir, instanceDir, err) |
| return instanceDir, instanceID, verror2.Make(ErrOperationFailed, call.Context()) |
| } |
| instanceInfo := new(instanceInfo) |
| if err := setupPrincipal(call.Context(), instanceDir, versionDir, call, i.securityAgent, instanceInfo); err != nil { |
| return instanceDir, instanceID, err |
| } |
| if err := saveInstanceInfo(instanceDir, instanceInfo); err != nil { |
| return instanceDir, instanceID, err |
| } |
| if err := initializeInstance(instanceDir, suspended); err != nil { |
| return instanceDir, instanceID, err |
| } |
| |
| if err := initializeInstanceACLs(call.LocalPrincipal(), instanceDir, call.RemoteBlessings().ForContext(call), i.deviceACL.Copy()); err != nil { |
| return instanceDir, instanceID, err |
| } |
| return instanceDir, instanceID, nil |
| } |
| |
| // isSetuid is defined like this so we can override its |
| // implementation for tests. |
| var isSetuid = func(fileStat os.FileInfo) bool { |
| vlog.VI(2).Infof("running the original isSetuid") |
| return fileStat.Mode()&os.ModeSetuid == os.ModeSetuid |
| } |
| |
| // systemAccountForHelper returns the uname that the helper uses to invoke the |
| // application. If the helper exists and is setuid, the device manager |
| // requires that there is a uname associated with the Veyron |
| // identity that requested starting an application. |
| // TODO(rjkroege): This function assumes a desktop installation target |
| // and is probably not a good fit in other contexts. Revisit the design |
| // as appropriate. This function also internalizes a decision as to when |
| // it is possible to start an application that needs to be made explicit. |
| func systemAccountForHelper(ctx ipc.ServerContext, helperPath string, uat BlessingSystemAssociationStore) (systemName string, err error) { |
| identityNames := ctx.RemoteBlessings().ForContext(ctx) |
| helperStat, err := os.Stat(helperPath) |
| if err != nil { |
| vlog.Errorf("Stat(%v) failed: %v. helper is required.", helperPath, err) |
| return "", verror2.Make(ErrOperationFailed, ctx.Context()) |
| } |
| haveHelper := isSetuid(helperStat) |
| systemName, present := uat.SystemAccountForBlessings(identityNames) |
| |
| switch { |
| case haveHelper && present: |
| return systemName, nil |
| case haveHelper && !present: |
| // The helper is owned by the device manager and installed as |
| // setuid root. Therefore, the device manager must never run an |
| // app as itself to prevent an app trivially granting itself |
| // root permissions. There must be an associated uname for the |
| // account in this case. |
| return "", verror2.Make(verror2.NoAccess, ctx.Context(), "use of setuid helper requires an associated uname.") |
| case !haveHelper: |
| // When the helper is not setuid, the helper can't change the |
| // app's uid so just run the app as the device manager's uname |
| // whether or not there is an association. |
| vlog.VI(1).Infof("helper not setuid. Device manager will invoke app with its own userid") |
| user, err := user.Current() |
| if err != nil { |
| vlog.Errorf("user.Current() failed: %v", err) |
| return "", verror2.Make(ErrOperationFailed, ctx.Context()) |
| } |
| return user.Username, nil |
| } |
| return "", verror2.Make(ErrOperationFailed, ctx.Context()) |
| } |
| |
| func genCmd(instanceDir, helperPath, systemName string, nsRoots []string) (*exec.Cmd, error) { |
| versionLink := filepath.Join(instanceDir, "version") |
| versionDir, err := filepath.EvalSymlinks(versionLink) |
| if err != nil { |
| vlog.Errorf("EvalSymlinks(%v) failed: %v", versionLink, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } |
| envelope, err := loadEnvelope(versionDir) |
| if err != nil { |
| return nil, err |
| } |
| binPath := filepath.Join(versionDir, "bin") |
| if _, err := os.Stat(binPath); err != nil { |
| vlog.Errorf("Stat(%v) failed: %v", binPath, err) |
| return nil, verror2.Make(ErrOperationFailed, nil) |
| } |
| |
| cmd := exec.Command(helperPath) |
| cmd.Args = append(cmd.Args, "--username", systemName) |
| |
| var nsRootEnvs []string |
| for i, r := range nsRoots { |
| nsRootEnvs = append(nsRootEnvs, fmt.Sprintf("%s%d=%s", consts.NamespaceRootPrefix, i, r)) |
| } |
| cmd.Env = append(nsRootEnvs, envelope.Env...) |
| rootDir := filepath.Join(instanceDir, "root") |
| if err := mkdir(rootDir); err != nil { |
| return nil, err |
| } |
| cmd.Dir = rootDir |
| cmd.Args = append(cmd.Args, "--workspace", rootDir) |
| |
| logDir := filepath.Join(instanceDir, "logs") |
| if err := mkdir(logDir); err != nil { |
| return nil, err |
| } |
| cmd.Args = append(cmd.Args, "--logdir", logDir) |
| timestamp := time.Now().UnixNano() |
| |
| stdoutLog := filepath.Join(logDir, fmt.Sprintf("STDOUT-%d", timestamp)) |
| if cmd.Stdout, err = openWriteFile(stdoutLog); err != nil { |
| return nil, err |
| } |
| stderrLog := filepath.Join(logDir, fmt.Sprintf("STDERR-%d", timestamp)) |
| if cmd.Stderr, err = openWriteFile(stderrLog); err != nil { |
| return nil, err |
| } |
| cmd.Args = append(cmd.Args, "--run", binPath) |
| cmd.Args = append(cmd.Args, "--") |
| |
| // Args to be passed by helper to the app. |
| cmd.Args = append(cmd.Args, "--log_dir=../logs") |
| cmd.Args = append(cmd.Args, envelope.Args...) |
| return cmd, nil |
| } |
| |
| func (i *appService) startCmd(instanceDir string, cmd *exec.Cmd) error { |
| info, err := loadInstanceInfo(instanceDir) |
| if err != nil { |
| return err |
| } |
| // Setup up the child process callback. |
| callbackState := i.callback |
| listener := callbackState.listenFor(mgmt.AppCycleManagerConfigKey) |
| defer listener.cleanup() |
| cfg := vexec.NewConfig() |
| cfg.Set(mgmt.ParentNameConfigKey, listener.name()) |
| cfg.Set(mgmt.ProtocolConfigKey, "tcp") |
| cfg.Set(mgmt.AddressConfigKey, "127.0.0.1:0") |
| cfg.Set(mgmt.ParentBlessingConfigKey, info.DeviceManagerPeerPattern) |
| |
| // Set up any agent-specific state. |
| // NOTE(caprita): This ought to belong in genCmd, but we do it here |
| // to avoid holding on to the lock for too long. |
| // |
| // TODO(caprita): We need to take care to grab/release the lock |
| // excluding concurrent start operations. See if we can make this more |
| // robust. |
| var agentCleaner func() |
| if sa := i.securityAgent; sa != nil { |
| sa.startLock.Lock() |
| file, err := sa.keyMgrAgent.NewConnection(info.SecurityAgentHandle) |
| if err != nil { |
| sa.startLock.Unlock() |
| vlog.Errorf("NewConnection(%v) failed: %v", info.SecurityAgentHandle, err) |
| return err |
| } |
| agentCleaner = func() { |
| file.Close() |
| sa.startLock.Unlock() |
| } |
| // We need to account for the file descriptors corresponding to |
| // std{err|out|in} as well as the implementation-specific pipes |
| // that the vexec library adds to ExtraFiles during |
| // handle.Start. vexec.FileOffset properly offsets fd |
| // accordingly. |
| fd := len(cmd.ExtraFiles) + vexec.FileOffset |
| cmd.ExtraFiles = append(cmd.ExtraFiles, file) |
| cfg.Set(mgmt.SecurityAgentFDConfigKey, strconv.Itoa(fd)) |
| } else { |
| cmd.Env = append(cmd.Env, consts.VeyronCredentials+"="+filepath.Join(instanceDir, "credentials")) |
| } |
| handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg}) |
| defer func() { |
| if handle != nil { |
| if err := handle.Clean(); err != nil { |
| vlog.Errorf("Clean() failed: %v", err) |
| } |
| } |
| }() |
| // Start the child process. |
| if err := handle.Start(); err != nil { |
| if agentCleaner != nil { |
| agentCleaner() |
| } |
| vlog.Errorf("Start() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| if agentCleaner != nil { |
| agentCleaner() |
| } |
| |
| // Wait for the child process to start. |
| if err := handle.WaitForReady(childReadyTimeout); err != nil { |
| vlog.Errorf("WaitForReady(%v) failed: %v", childReadyTimeout, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| childName, err := listener.waitForValue(childReadyTimeout) |
| if err != nil { |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| info.AppCycleMgrName, info.Pid = childName, handle.Pid() |
| if err := saveInstanceInfo(instanceDir, info); err != nil { |
| return err |
| } |
| // TODO(caprita): Spin up a goroutine to reap child status upon exit and |
| // transition it to suspended state if it exits on its own. |
| handle = nil |
| return nil |
| } |
| |
| func (i *appService) run(nsRoots []string, instanceDir, systemName string) error { |
| if err := transitionInstance(instanceDir, suspended, starting); err != nil { |
| return err |
| } |
| |
| cmd, err := genCmd(instanceDir, i.config.Helper, systemName, nsRoots) |
| if err == nil { |
| err = i.startCmd(instanceDir, cmd) |
| } |
| if err != nil { |
| transitionInstance(instanceDir, starting, suspended) |
| return err |
| } |
| return transitionInstance(instanceDir, starting, started) |
| } |
| |
| func (i *appService) Start(call ipc.ServerContext) ([]string, error) { |
| helper := i.config.Helper |
| instanceDir, instanceID, err := i.newInstance(call) |
| |
| if err != nil { |
| cleanupDir(instanceDir, helper) |
| return nil, err |
| } |
| |
| systemName, err := systemAccountForHelper(call, helper, i.uat) |
| if err != nil { |
| cleanupDir(instanceDir, helper) |
| return nil, err |
| } |
| |
| if err := saveSystemNameForInstance(instanceDir, systemName); err != nil { |
| cleanupDir(instanceDir, helper) |
| return nil, err |
| } |
| |
| // For now, use the namespace roots of the device manager runtime to |
| // pass to the app. |
| if err = i.run(veyron2.RuntimeFromContext(call.Context()).Namespace().Roots(), instanceDir, systemName); err != nil { |
| // TODO(caprita): We should call cleanupDir here, but we don't |
| // in order to not lose the logs for the instance (so we can |
| // debug why run failed). Clean this up. |
| // cleanupDir(instanceDir, helper) |
| return nil, err |
| } |
| return []string{instanceID}, nil |
| } |
| |
| // instanceDir returns the path to the directory containing the app instance |
| // referred to by the given suffix relative to the given root directory. |
| func instanceDir(root string, suffix []string) (string, error) { |
| if nComponents := len(suffix); nComponents != 3 { |
| return "", verror2.Make(ErrInvalidSuffix, nil) |
| } |
| app, installation, instance := suffix[0], suffix[1], suffix[2] |
| instancesDir := filepath.Join(root, applicationDirName(app), installationDirName(installation), "instances") |
| instanceDir := filepath.Join(instancesDir, instanceDirName(instance)) |
| return instanceDir, nil |
| } |
| |
| // instanceDir returns the path to the directory containing the app instance |
| // referred to by the invoker's suffix, as well as the corresponding stopped |
| // instance dir. Returns an error if the suffix does not name an instance. |
| func (i *appService) instanceDir() (string, error) { |
| return instanceDir(i.config.Root, i.suffix) |
| } |
| |
| func (i *appService) Resume(call ipc.ServerContext) error { |
| instanceDir, err := i.instanceDir() |
| if err != nil { |
| return err |
| } |
| |
| systemName, err := systemAccountForHelper(call, i.config.Helper, i.uat) |
| if err != nil { |
| return err |
| } |
| |
| startSystemName, err := readSystemNameForInstance(instanceDir) |
| if err != nil { |
| return err |
| } |
| |
| if startSystemName != systemName { |
| return verror2.Make(verror2.NoAccess, call.Context(), "Not allowed to resume an application under a different system name.") |
| } |
| return i.run(veyron2.RuntimeFromContext(call.Context()).Namespace().Roots(), instanceDir, systemName) |
| } |
| |
| func stopAppRemotely(ctx *context.T, appVON string) error { |
| appStub := appcycle.AppCycleClient(appVON) |
| ctx, cancel := context.WithTimeout(ctx, ipcContextTimeout) |
| defer cancel() |
| stream, err := appStub.Stop(ctx) |
| if err != nil { |
| vlog.Errorf("%v.Stop() failed: %v", appVON, err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| rstream := stream.RecvStream() |
| for rstream.Advance() { |
| vlog.VI(2).Infof("%v.Stop() task update: %v", appVON, rstream.Value()) |
| } |
| if err := rstream.Err(); err != nil { |
| vlog.Errorf("Advance() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| if err := stream.Finish(); err != nil { |
| vlog.Errorf("Finish() failed: %v", err) |
| return verror2.Make(ErrOperationFailed, nil) |
| } |
| return nil |
| } |
| |
| func stop(ctx *context.T, instanceDir string) error { |
| info, err := loadInstanceInfo(instanceDir) |
| if err != nil { |
| return err |
| } |
| return stopAppRemotely(ctx, info.AppCycleMgrName) |
| } |
| |
| // TODO(caprita): implement deadline for Stop. |
| |
| func (i *appService) Stop(ctx ipc.ServerContext, deadline uint32) error { |
| instanceDir, err := i.instanceDir() |
| if err != nil { |
| return err |
| } |
| if err := transitionInstance(instanceDir, suspended, stopped); verror2.Is(err, ErrOperationFailed.ID) || err == nil { |
| return err |
| } |
| if err := transitionInstance(instanceDir, started, stopping); err != nil { |
| return err |
| } |
| if err := stop(ctx.Context(), instanceDir); err != nil { |
| transitionInstance(instanceDir, stopping, started) |
| return err |
| } |
| return transitionInstance(instanceDir, stopping, stopped) |
| } |
| |
| func (i *appService) Suspend(ctx ipc.ServerContext) error { |
| instanceDir, err := i.instanceDir() |
| if err != nil { |
| return err |
| } |
| if err := transitionInstance(instanceDir, started, suspending); err != nil { |
| return err |
| } |
| if err := stop(ctx.Context(), instanceDir); err != nil { |
| transitionInstance(instanceDir, suspending, started) |
| return err |
| } |
| return transitionInstance(instanceDir, suspending, suspended) |
| } |
| |
| func (i *appService) Uninstall(ipc.ServerContext) error { |
| installationDir, err := i.installationDir() |
| if err != nil { |
| return err |
| } |
| return transitionInstallation(installationDir, active, uninstalled) |
| } |
| |
| func (i *appService) Update(call ipc.ServerContext) error { |
| installationDir, err := i.installationDir() |
| if err != nil { |
| return err |
| } |
| if !installationStateIs(installationDir, active) { |
| return verror2.Make(ErrInvalidOperation, call.Context()) |
| } |
| originVON, err := loadOrigin(installationDir) |
| if err != nil { |
| return err |
| } |
| ctx, cancel := context.WithTimeout(call.Context(), ipcContextTimeout) |
| defer cancel() |
| newEnvelope, err := fetchAppEnvelope(ctx, originVON) |
| if err != nil { |
| return err |
| } |
| currLink := filepath.Join(installationDir, "current") |
| oldVersionDir, err := filepath.EvalSymlinks(currLink) |
| if err != nil { |
| vlog.Errorf("EvalSymlinks(%v) failed: %v", currLink, err) |
| return verror2.Make(ErrOperationFailed, call.Context()) |
| } |
| // NOTE(caprita): A race can occur between two competing updates, where |
| // both use the old version as their baseline. This can result in both |
| // updates succeeding even if they are updating the app installation to |
| // the same new envelope. This will result in one of the updates |
| // becoming the new 'current'. Both versions will point their |
| // 'previous' link to the old version. This doesn't appear to be of |
| // practical concern, so we avoid the complexity of synchronizing |
| // updates. |
| oldEnvelope, err := loadEnvelope(oldVersionDir) |
| if err != nil { |
| return err |
| } |
| if oldEnvelope.Title != newEnvelope.Title { |
| return verror2.Make(ErrAppTitleMismatch, call.Context()) |
| } |
| if reflect.DeepEqual(oldEnvelope, newEnvelope) { |
| return verror2.Make(ErrUpdateNoOp, call.Context()) |
| } |
| versionDir, err := newVersion(call.Context(), installationDir, newEnvelope, oldVersionDir) |
| if err != nil { |
| cleanupDir(versionDir, "") |
| return err |
| } |
| return nil |
| } |
| |
| func (*appService) UpdateTo(_ ipc.ServerContext, von string) error { |
| // TODO(jsimsa): Implement. |
| return nil |
| } |
| |
| func (i *appService) Revert(ctx ipc.ServerContext) error { |
| installationDir, err := i.installationDir() |
| if err != nil { |
| return err |
| } |
| if !installationStateIs(installationDir, active) { |
| return verror2.Make(ErrInvalidOperation, ctx.Context()) |
| } |
| // NOTE(caprita): A race can occur between an update and a revert, where |
| // both use the same current version as their starting point. This will |
| // render the update inconsequential. This doesn't appear to be of |
| // practical concern, so we avoid the complexity of synchronizing |
| // updates and revert operations. |
| currLink := filepath.Join(installationDir, "current") |
| currVersionDir, err := filepath.EvalSymlinks(currLink) |
| if err != nil { |
| vlog.Errorf("EvalSymlinks(%v) failed: %v", currLink, err) |
| return verror2.Make(ErrOperationFailed, ctx.Context()) |
| } |
| previousLink := filepath.Join(currVersionDir, "previous") |
| if _, err := os.Lstat(previousLink); err != nil { |
| if os.IsNotExist(err) { |
| // No 'previous' link -- must be the first version. |
| return verror2.Make(ErrUpdateNoOp, ctx.Context()) |
| } |
| vlog.Errorf("Lstat(%v) failed: %v", previousLink, err) |
| return verror2.Make(ErrOperationFailed, ctx.Context()) |
| } |
| prevVersionDir, err := filepath.EvalSymlinks(previousLink) |
| if err != nil { |
| vlog.Errorf("EvalSymlinks(%v) failed: %v", previousLink, err) |
| return verror2.Make(ErrOperationFailed, ctx.Context()) |
| } |
| return updateLink(prevVersionDir, currLink) |
| } |
| |
| type treeNode struct { |
| children map[string]*treeNode |
| } |
| |
| func newTreeNode() *treeNode { |
| return &treeNode{children: make(map[string]*treeNode)} |
| } |
| |
| func (n *treeNode) find(names []string, create bool) *treeNode { |
| for { |
| if len(names) == 0 { |
| return n |
| } |
| if next, ok := n.children[names[0]]; ok { |
| n = next |
| names = names[1:] |
| continue |
| } |
| if create { |
| nn := newTreeNode() |
| n.children[names[0]] = nn |
| n = nn |
| names = names[1:] |
| continue |
| } |
| return nil |
| } |
| } |
| |
| func (i *appService) scanEnvelopes(tree *treeNode, appDir string) { |
| // Find all envelopes, extract installID. |
| envGlob := []string{i.config.Root, appDir, "installation-*", "*", "envelope"} |
| envelopes, err := filepath.Glob(filepath.Join(envGlob...)) |
| if err != nil { |
| vlog.Errorf("unexpected error: %v", err) |
| return |
| } |
| for _, path := range envelopes { |
| env, err := loadEnvelope(filepath.Dir(path)) |
| if err != nil { |
| continue |
| } |
| relpath, _ := filepath.Rel(i.config.Root, path) |
| elems := strings.Split(relpath, string(filepath.Separator)) |
| if len(elems) != len(envGlob)-1 { |
| vlog.Errorf("unexpected number of path components: %q (%q)", elems, path) |
| continue |
| } |
| installID := strings.TrimPrefix(elems[1], "installation-") |
| tree.find([]string{env.Title, installID}, true) |
| } |
| return |
| } |
| |
| func (i *appService) scanInstances(tree *treeNode) { |
| if len(i.suffix) < 2 { |
| return |
| } |
| title := i.suffix[0] |
| installDir, err := installationDirCore(i.suffix[:2], i.config.Root) |
| if err != nil { |
| return |
| } |
| // Add the node corresponding to the installation itself. |
| tree.find(i.suffix[:2], true) |
| // Find all instances. |
| infoGlob := []string{installDir, "instances", "instance-*", "info"} |
| instances, err := filepath.Glob(filepath.Join(infoGlob...)) |
| if err != nil { |
| vlog.Errorf("unexpected error: %v", err) |
| return |
| } |
| for _, path := range instances { |
| instanceDir := filepath.Dir(path) |
| i.scanInstance(tree, title, instanceDir) |
| } |
| return |
| } |
| |
| func (i *appService) scanInstance(tree *treeNode, title, instanceDir string) { |
| if _, err := loadInstanceInfo(instanceDir); err != nil { |
| return |
| } |
| relpath, _ := filepath.Rel(i.config.Root, instanceDir) |
| elems := strings.Split(relpath, string(filepath.Separator)) |
| if len(elems) < 4 { |
| vlog.Errorf("unexpected number of path components: %q (%q)", elems, instanceDir) |
| return |
| } |
| installID := strings.TrimPrefix(elems[1], "installation-") |
| instanceID := strings.TrimPrefix(elems[3], "instance-") |
| tree.find([]string{title, installID, instanceID, "logs"}, true) |
| if instanceStateIs(instanceDir, started) { |
| for _, obj := range []string{"pprof", "stats"} { |
| tree.find([]string{title, installID, instanceID, obj}, true) |
| } |
| } |
| } |
| |
| func (i *appService) GlobChildren__(ipc.ServerContext) (<-chan string, error) { |
| tree := newTreeNode() |
| switch len(i.suffix) { |
| case 0: |
| i.scanEnvelopes(tree, "app-*") |
| case 1: |
| appDir := applicationDirName(i.suffix[0]) |
| i.scanEnvelopes(tree, appDir) |
| case 2: |
| i.scanInstances(tree) |
| case 3: |
| dir, err := i.instanceDir() |
| if err != nil { |
| break |
| } |
| i.scanInstance(tree, i.suffix[0], dir) |
| default: |
| return nil, verror2.Make(verror2.NoExist, nil, i.suffix) |
| } |
| n := tree.find(i.suffix, false) |
| if n == nil { |
| return nil, verror2.Make(ErrInvalidSuffix, nil) |
| } |
| ch := make(chan string) |
| go func() { |
| for child, _ := range n.children { |
| ch <- child |
| } |
| close(ch) |
| }() |
| return ch, nil |
| } |
| |
| // TODO(rjkroege): Refactor to eliminate redundancy with newAppSpecificAuthorizer. |
| func dirFromSuffix(suffix []string, root string) (string, error) { |
| if len(suffix) == 2 { |
| p, err := installationDirCore(suffix, root) |
| if err != nil { |
| vlog.Errorf("dirFromSuffix failed: %v", err) |
| return "", err |
| } |
| return p, nil |
| } else if len(suffix) > 2 { |
| p, err := instanceDir(root, suffix[0:3]) |
| if err != nil { |
| vlog.Errorf("dirFromSuffix failed: %v", err) |
| return "", err |
| } |
| return p, nil |
| } |
| return "", verror2.Make(ErrInvalidSuffix, nil) |
| } |
| |
| // TODO(rjkroege): Consider maintaining an in-memory ACL cache. |
| func (i *appService) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error { |
| dir, err := dirFromSuffix(i.suffix, i.config.Root) |
| if err != nil { |
| return err |
| } |
| return setAppACL(ctx.LocalPrincipal(), i.locks, dir, acl, etag) |
| } |
| |
| func (i *appService) GetACL(_ ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) { |
| dir, err := dirFromSuffix(i.suffix, i.config.Root) |
| if err != nil { |
| return nil, "", err |
| } |
| return getAppACL(i.locks, dir) |
| } |