[cipd] Using cipd resolve to check access for internal packages

This patch add cipd access check for internal packages. If a
package declared in manifest has attribute 'internal="true"', jiri
will use "cipd resolve" to determine if current cipd session
has correct access. Unaccessible internal packages
will not be fetched to avoid errors.

BLD-201

Change-Id: I75e3485b4f103474892ae58ba004329892730041
diff --git a/cipd/cipd.go b/cipd/cipd.go
index f34fa95..c28f465 100644
--- a/cipd/cipd.go
+++ b/cipd/cipd.go
@@ -51,6 +51,7 @@
 windows-386     sha256  b8102c9a1b93915c128e7577c89fd77991ab83d52c356913e56ea505ab338735
 windows-amd64   sha256  a117e3984c111c68698faf91815c4b7d374404fa82dff318aadb9f2f0582ca8d
 `
+	cipdNotLoggedInStr = "Not logged in"
 )
 
 var (
@@ -244,6 +245,91 @@
 	return ioutil.ReadAll(resp.Body)
 }
 
+type packageACL struct {
+	path   string
+	access bool
+}
+
+func checkPackageACL(jirix *jiri.X, path, version string, c chan<- packageACL) {
+	// cipd should be already bootstrapped before this go routine.
+	// Silently return a false just in case if cipd is not found.
+	if cipdBinary == "" {
+		c <- packageACL{path: path, access: false}
+		return
+	}
+
+	args := []string{"resolve", path, "-version", version}
+	if jirix != nil {
+		jirix.Logger.Debugf("Invoke cipd with %v", args)
+	}
+	command := exec.Command(cipdBinary, args...)
+	var stdoutBuf, stderrBuf bytes.Buffer
+	command.Stdout = &stdoutBuf
+	command.Stderr = &stderrBuf
+	// Return false if cipd cannot be executed or cipd returned a non-zero
+	// return code, which usually means the package cannot be found due to
+	// access control.
+	if err := command.Run(); err != nil {
+		if jirix != nil {
+			jirix.Logger.Debugf("Error happend while executing cipd, err: %q, stderr: %q", err, stderrBuf.String())
+		}
+		c <- packageACL{path: path, access: false}
+		return
+	}
+	// cipd returned zero. Package can be accessed.
+	c <- packageACL{path: path, access: true}
+	return
+}
+
+// CheckPackageACL checks cipd's access to packages in map "pkgs". The package
+// names in "pkgs" should have trailing '/' removed before calling this
+// function.
+func CheckPackageACL(jirix *jiri.X, pkgs map[string]bool, versions map[string]string) error {
+	// Not declared as CheckPackageACL(jirix *jiri.X, pkgs map[*package.Package]bool)
+	// due to import cycles. Package jiri/package imports jiri/cipd so here we cannot
+	// import jiri/package.
+	if _, err := Bootstrap(); err != nil {
+		return err
+	}
+
+	c := make(chan packageACL)
+	for key := range pkgs {
+		go checkPackageACL(jirix, key, versions[key], c)
+	}
+
+	for i := 0; i < len(pkgs); i++ {
+		acl := <-c
+		pkgs[acl.path] = acl.access
+	}
+	return nil
+}
+
+// CheckLoggedIn checks cipd's user login information. It will return true
+// if login information is found or return false if login information is not
+// found.
+func CheckLoggedIn(jirix *jiri.X) (bool, error) {
+	cipdPath, err := Bootstrap()
+	if err != nil {
+		return false, err
+	}
+	args := []string{"auth-info"}
+	command := exec.Command(cipdPath, args...)
+	var stdoutBuf, stderrBuf bytes.Buffer
+	command.Stdout = &stdoutBuf
+	command.Stderr = &stderrBuf
+	if err := command.Run(); err != nil {
+		stdErrMsg := strings.TrimSpace(stderrBuf.String())
+		if jirix != nil {
+			jirix.Logger.Debugf("Error happend while executing cipd, err: %q, stderr: %q", err, stdErrMsg)
+		}
+		if _, ok := err.(*exec.ExitError); ok && stdErrMsg == cipdNotLoggedInStr {
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
 // Ensure runs cipd binary's ensure funcationality over file. Fetched packages will be
 // saved to projectRoot directory. Parameter timeout is in minutes.
 func Ensure(jirix *jiri.X, file, projectRoot string, timeout uint) error {
diff --git a/cipd/cipd_test.go b/cipd/cipd_test.go
index fdff7ed..41cce90 100644
--- a/cipd/cipd_test.go
+++ b/cipd/cipd_test.go
@@ -17,6 +17,11 @@
 	// Some random valid cipd version tags from infra/tools/cipd
 	cipdVersionForTestA = "git_revision:00e2d8b49a4e7505d1c71f19d15c9e7c5b9245a5"
 	cipdVersionForTestB = "git_revision:8fac632847b1ce0de3b57d16d0f2193625f4a4f0"
+	// package path and versions for ACL tests
+	cipdPkgPathA    = "gn/gn/${platform}"
+	cipdPkgVersionA = "git_revision:bdb0fd02324b120cacde634a9235405061c8ea06"
+	cipdPkgPathB    = "notexist/notexist"
+	cipdPkgVersionB = "git_revision:bdb0fd02324b120cacde634a9235405061c8ea06"
 )
 
 var (
@@ -164,3 +169,30 @@
 		t.Errorf("failed to execute os.Stat() on fetched cipd package due to error: %v", err)
 	}
 }
+
+func TestCheckACL(t *testing.T) {
+	cipdPath, err := Bootstrap()
+	if err != nil {
+		t.Errorf("bootstrap failed due to error: %v", err)
+	}
+	defer os.Remove(cipdPath)
+
+	pkgMap := make(map[string]bool)
+	pkgMap[cipdPkgPathA] = false
+	pkgMap[cipdPkgPathB] = false
+	versionMap := make(map[string]string)
+	versionMap[cipdPkgPathA] = cipdPkgVersionA
+	versionMap[cipdPkgPathB] = cipdPkgVersionB
+	if err := CheckPackageACL(nil, pkgMap, versionMap); err != nil {
+		t.Errorf("CheckPackageACL failed due to error: %v", err)
+	}
+
+	if !pkgMap[cipdPkgPathA] {
+		t.Errorf("pkg %q should be accessible, but it is not accessible by cipd", cipdPkgPathA)
+	}
+
+	if pkgMap[cipdPkgPathB] {
+		t.Errorf("pkg %q should not be accessible, but it is accessible by cipd", cipdPkgPathB)
+	}
+
+}
diff --git a/project/manifest.go b/project/manifest.go
index 0e5162a..232fc9c 100644
--- a/project/manifest.go
+++ b/project/manifest.go
@@ -503,8 +503,28 @@
 	ensureFileBuf.WriteString("$ParanoidMode CheckPresence\n")
 	ensureFileBuf.WriteString("\n")
 
-	// TODO: perform ACL checks on internal projects
+	// Perform ACL checks on internal projects
+	pkgACLMap := make(map[string]bool)
+	pkgVersionMap := make(map[string]string)
 	for _, pkg := range pkgs {
+		pkg.Name = strings.TrimRight(pkg.Name, "/")
+		if pkg.Internal {
+			pkgACLMap[pkg.Name] = false
+			pkgVersionMap[pkg.Name] = pkg.Version
+		}
+	}
+	if len(pkgACLMap) != 0 {
+		if err := cipd.CheckPackageACL(jirix, pkgACLMap, pkgVersionMap); err != nil {
+			return err
+		}
+	}
+
+	hasSkippedPkgs := false
+	for _, pkg := range pkgs {
+		if val, ok := pkgACLMap[pkg.Name]; ok && !val {
+			hasSkippedPkgs = true
+			continue
+		}
 		cipdDecl, err := pkg.cipdDecl()
 		if err != nil {
 			return err
@@ -513,16 +533,25 @@
 		ensureFileBuf.WriteString("\n")
 	}
 
+	jirix.Logger.Debugf("Generated ensure file content:\n%v", ensureFileBuf.String())
 	if _, err := ensureFileBuf.WriteTo(ensureFile); err != nil {
 		return err
 	}
 	if err := ensureFile.Sync(); err != nil {
 		return err
 	}
-	jirix.Logger.Debugf("Generated cipd ensure file at %s ", ensureFilePath)
 	if err := cipd.Ensure(jirix, ensureFilePath, jirix.Root, fetchTimeout); err != nil {
 		return err
 	}
+	if hasSkippedPkgs {
+		cipdLoggedIn, err := cipd.CheckLoggedIn(jirix)
+		if err != nil {
+			return err
+		}
+		if !cipdLoggedIn {
+			jirix.Logger.Warningf("Some packages are skipped by cipd due to lack of access, you might want to run \"cipd auth-login\" and try again")
+		}
+	}
 
 	return nil
 }