[resolve] Add option to restrict hostnames of projects
This change adds a new option '--allow-hosts' to jiri resolve to
allow Bots to prevent users using unapproved third_party repositories.
When jiri detects a project is not using a hostname in this allow
list, jiri resolve will fail.
Bug: IN-283
Test: Local and CQ
Change-Id: Ic235098e7255d0f448c5fe514beffddef2e1c81e
diff --git a/cmd/jiri/resolve.go b/cmd/jiri/resolve.go
index 70dca7a..702f357 100644
--- a/cmd/jiri/resolve.go
+++ b/cmd/jiri/resolve.go
@@ -5,6 +5,8 @@
package main
import (
+ "strings"
+
"fuchsia.googlesource.com/jiri"
"fuchsia.googlesource.com/jiri/cmdline"
"fuchsia.googlesource.com/jiri/project"
@@ -17,6 +19,7 @@
enableProjectLock bool
enablePackageVersion bool
allowFloatingRefs bool
+ hostnameAllowList string
}
func (r *resolveFlags) AllowFloatingRefs() bool {
@@ -39,8 +42,17 @@
return r.enableProjectLock
}
-func (r *resolveFlags) EnablePackageVersion() bool {
- return r.enablePackageVersion
+func (r *resolveFlags) HostnameAllowList() []string {
+ ret := make([]string, 0)
+ hosts := strings.Split(r.hostnameAllowList, ",")
+ for _, item := range hosts {
+ item = strings.TrimSpace(item)
+ if item == "" {
+ continue
+ }
+ ret = append(ret, item)
+ }
+ return ret
}
var resolveFlag resolveFlags
@@ -63,9 +75,8 @@
flags.BoolVar(&resolveFlag.localManifestFlag, "local-manifest", false, "Use local manifest")
flags.BoolVar(&resolveFlag.enablePackageLock, "enable-package-lock", true, "Enable resolving packages in lockfile")
flags.BoolVar(&resolveFlag.enableProjectLock, "enable-project-lock", false, "Enable resolving projects in lockfile")
- // TODO: Remove this placeholder flag after all references are deleted.
- flags.BoolVar(&resolveFlag.enablePackageVersion, "enable-package-version", true, "Enable version tag for packages in lockfile")
flags.BoolVar(&resolveFlag.allowFloatingRefs, "allow-floating-refs", false, "Allow packages to be pinned to floating refs such as \"latest\"")
+ flags.StringVar(&resolveFlag.hostnameAllowList, "allow-hosts", "", "List of hostnames that can be used in the url of a repository, seperated by comma. It will not be enforced if it is left empty.")
}
func runResolve(jirix *jiri.X, args []string) error {
diff --git a/project/project.go b/project/project.go
index f29bb3e..f7faf4c 100644
--- a/project/project.go
+++ b/project/project.go
@@ -359,7 +359,7 @@
LocalManifest() bool
EnablePackageLock() bool
EnableProjectLock() bool
- EnablePackageVersion() bool
+ HostnameAllowList() []string
}
// UnmarshalLockEntries unmarshals project locks and package locks from
@@ -1010,6 +1010,64 @@
return nil
}
+// HostnameAllowed determines if hostname is allowed under reference.
+// This function allows a single prefix '*' for wildcard matching E.g.
+// "*.google.com" will match "fuchsia.google.com" but does not match
+// "google.com".
+func HostnameAllowed(reference, hostname string) bool {
+ if strings.Count(reference, "*") > 1 || (strings.Count(reference, "*") == 1 && reference[0] != '*') {
+ return false
+ }
+ if !strings.HasPrefix(reference, "*") {
+ return reference == hostname
+ }
+ reference = reference[1:]
+ i := len(reference) - 1
+ j := len(hostname) - 1
+ for i >= 0 && j >= 0 {
+ if hostname[j] != reference[i] {
+ return false
+ }
+ i--
+ j--
+ }
+ if i >= 0 {
+ return false
+ }
+ return true
+}
+
+// CheckProjectsHostnames checks if the hostname of every project is allowed
+// under allowList. If allowList is empty, the check is skipped.
+func CheckProjectsHostnames(projects Projects, allowList []string) error {
+ if len(allowList) > 0 {
+ for _, item := range allowList {
+ if strings.Count(item, "*") > 1 || (strings.Count(item, "*") == 1 && item[0] != '*') {
+ return fmt.Errorf("failed to process %q. Only a single * at the beginning of a hostname is supported", item)
+ }
+ }
+ for _, proj := range projects {
+ projURL, err := url.Parse(proj.Remote)
+ if err != nil {
+ return fmt.Errorf("URL of project %q cannot be parsed due to error: %v", proj.Name, err)
+ }
+ remoteHost := projURL.Hostname()
+ allowed := false
+ for _, item := range allowList {
+ if HostnameAllowed(item, remoteHost) {
+ allowed = true
+ break
+ }
+ }
+ if !allowed {
+ err := fmt.Errorf("hostname: %s in project %s is not allowed", remoteHost, proj.Name)
+ return err
+ }
+ }
+ }
+ return nil
+}
+
// GenerateJiriLockFile generates jiri lockfile to lockFilePath using
// manifests in manifestFiles slice.
func GenerateJiriLockFile(jirix *jiri.X, manifestFiles []string, resolveConfig ResolveConfig) error {
@@ -1020,6 +1078,10 @@
if err != nil {
return nil, nil, err
}
+ // Check hostnames of projects.
+ if err := CheckProjectsHostnames(projects, resolveConfig.HostnameAllowList()); err != nil {
+ return nil, nil, err
+ }
if resolveConfig.EnableProjectLock() {
projectLocks, err = resolveProjectLocks(jirix, projects)
if err != nil {
diff --git a/project/project_test.go b/project/project_test.go
index 0e10e66..ed41f57 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -2567,3 +2567,91 @@
t.Errorf("expected git hash %q, got %q", testHash, currentHash)
}
}
+
+func TestHostnameAllowed(t *testing.T) {
+ tests := map[string]bool{
+ "*.google.com,fuchsia.google.com": true,
+ "*.google.com,fuchsia.dev.google.com": true,
+ "google.com,google.com": true,
+ "*google.com,fuchsiagoogle.com": true,
+ "google.com,fuchsiagoogle.com": false,
+ "google.com,oogle.com": false,
+ "fuchsia-internal,fuchsia-internal": true,
+ "fuchsia-internal,fuchsia": false,
+ ",": true,
+ "*google*.com,fuchsia.googlesource.com": false,
+ }
+ for k, v := range tests {
+ test := strings.Split(k, ",")
+ if len(test) != 2 {
+ t.Errorf("expecting a single ',' in %q", k)
+ }
+ ret := project.HostnameAllowed(test[0], test[1])
+ if v != ret {
+ t.Errorf("expecting %v, got %v from test %q", v, ret, k)
+ }
+ }
+}
+
+func TestCheckProjectsHostnames(t *testing.T) {
+ allowList := []string{
+ "*.googlesource.com",
+ "fuchsia-internal",
+ }
+ allowListIllegal := []string{
+ "*.google*.com",
+ "fuchsia-internal",
+ }
+ testProjectListsTrue := []project.Project{
+ {
+ Name: "project1",
+ Remote: "https://fuchsia.googlesource.com/project1",
+ },
+ {
+ Name: "project2",
+ Remote: "https://chromium.googlesource.com/project2",
+ },
+ {
+ Name: "project4",
+ Remote: "sso://fuchsia-internal/project4",
+ },
+ }
+ testProjectListsFalse := []project.Project{
+ {
+ Name: "project1",
+ Remote: "https://fuchsia.googlesource.com/project1",
+ },
+ {
+ Name: "project2",
+ Remote: "https://chromium.googlesource.com/project2",
+ },
+ {
+ Name: "project3",
+ Remote: "https://test.github.com/project3",
+ },
+ {
+ Name: "project4",
+ Remote: "sso://fuchsia-internal/project4",
+ },
+ }
+
+ mapTrue := make(project.Projects)
+ for _, item := range testProjectListsTrue {
+ mapTrue[item.Key()] = item
+ }
+ if err := project.CheckProjectsHostnames(mapTrue, allowList); err != nil {
+ t.Errorf("expecting nil from CheckProjectsHostnames, but got: %v", err)
+ }
+
+ mapFalse := make(project.Projects)
+ for _, item := range testProjectListsFalse {
+ mapFalse[item.Key()] = item
+ }
+ if err := project.CheckProjectsHostnames(mapFalse, allowList); err == nil {
+ t.Errorf("expecting error from CheckProjectsHostnames, but got nil")
+ }
+
+ if err := project.CheckProjectsHostnames(mapTrue, allowListIllegal); err == nil {
+ t.Errorf("expecting error from CheckProjectsHostnames, but got nil")
+ }
+}