Index: appengine/logdog/coordinator/config/projects.go |
diff --git a/appengine/logdog/coordinator/config/projects.go b/appengine/logdog/coordinator/config/projects.go |
index 689b073e5fbcf7e800a261481a05b5c09023ff30..2c9e02cb804a19643c14a9385d5b751a0f9a3319 100644 |
--- a/appengine/logdog/coordinator/config/projects.go |
+++ b/appengine/logdog/coordinator/config/projects.go |
@@ -11,21 +11,13 @@ import ( |
"github.com/golang/protobuf/proto" |
"github.com/luci/gae/service/info" |
"github.com/luci/luci-go/common/config" |
- "github.com/luci/luci-go/common/errors" |
log "github.com/luci/luci-go/common/logging" |
- "github.com/luci/luci-go/common/parallel" |
- configProto "github.com/luci/luci-go/common/proto/config" |
"github.com/luci/luci-go/common/proto/logdog/svcconfig" |
- "github.com/luci/luci-go/server/auth" |
- "github.com/luci/luci-go/server/auth/identity" |
"golang.org/x/net/context" |
) |
const maxProjectWorkers = 32 |
-// ErrNoAccess is returned if the user has no access to the requested project. |
-var ErrNoAccess = errors.New("no access") |
- |
// ProjectConfigPath returns the config set and path for project-specific |
// configuration. |
// |
@@ -66,208 +58,54 @@ func ProjectConfig(c context.Context, project config.ProjectName) (*svcconfig.Pr |
return &pcfg, nil |
} |
-// Projects lists the registered LogDog projects. |
-func Projects(c context.Context) ([]string, error) { |
- projects, err := config.Get(c).GetProjects() |
- if err != nil { |
- log.WithError(err).Errorf(c, "Failed to list 'luci-config' projects.") |
- return nil, err |
- } |
- |
- // TODO(dnj): Filter this list to projects with active LogDog configs, once we |
- // move to project-specific configurations. |
- |
- ids := make([]string, len(projects)) |
- for i, p := range projects { |
- ids[i] = p.ID |
- } |
- |
- // TODO(dnj): Restrict this by actual namespaces in datastore. |
- return ids, nil |
-} |
- |
-// AssertProjectAccess attempts to assert the current user's ability to access |
-// a given project. |
-// |
-// If the user cannot access the referenced project, ErrNoAccess will be |
-// returned. If an error occurs during checking, that error will be returned. |
-// The only time nil will be returned is if the check succeeded and the user was |
-// verified to have access to the requested project. |
-// |
-// NOTE: In the future, this will involve loading LogDog ACLs from the project's |
-// configuration. For now, though, since ACLs aren't implemented (yet), we will |
-// be content with asserting that the user has access to the base project. |
-func AssertProjectAccess(c context.Context, project config.ProjectName) error { |
- // Get our authenticator. |
- st := auth.GetState(c) |
- if st == nil { |
- log.Errorf(c, "No authentication state in Context.") |
- return errors.New("no authentication state in context") |
- } |
- |
- hasAccess, err := checkProjectAccess(c, config.Get(c), project, st) |
- if err != nil { |
- log.Fields{ |
- log.ErrorKey: err, |
- "project": project, |
- }.Errorf(c, "Failed to check for project access.") |
- return err |
- } |
- |
- if !hasAccess { |
- log.Fields{ |
- log.ErrorKey: err, |
- "project": project, |
- "identity": st.User().Identity, |
- }.Errorf(c, "User does not have access to this project.") |
- return ErrNoAccess |
- } |
- |
- return nil |
-} |
- |
-// UserProjects returns the list of luci-config projects that the current user |
-// has access to. |
-// |
-// It does this by listing the full set of luci-config projects, then loading |
-// the project configuration for each one. If the project configuration |
-// specifies an access restriction that the user satisfies, that project will |
-// be included in the list. |
+// AllProjectConfigs returns the project configurations for all projects that |
+// have a configuration. |
// |
-// In a production environment, most of the config accesses will be cached. |
-// |
-// If the current user is anonymous, this will still work, returning the set of |
-// projects that the anonymous user can access. |
-func UserProjects(c context.Context) ([]config.ProjectName, error) { |
- // NOTE: This client has a relatively short timeout, and using WithDeadline |
- // will apply to all serial calls. We may want to make getting a config client |
- // instance a coordinator.Service function if this proves to be a problem. |
- ci := config.Get(c) |
- |
- // Get our authenticator. |
- st := auth.GetState(c) |
- if st == nil { |
- log.Errorf(c, "No authentication state in Context.") |
- return nil, errors.New("no authentication state in context") |
- } |
- |
- allProjects, err := ci.GetProjects() |
+// If a project's configuration fails to load, an error will be logged and the |
+// project will be omitted from the output map. |
+func AllProjectConfigs(c context.Context) (map[config.ProjectName]*svcconfig.ProjectConfig, error) { |
+ // Get the project config path. The "project" field is only used for the |
+ // config set, which we ignore, so passing an empty string is acceptable. |
nodir
2016/05/19 17:17:21
the alternative is to split the func into two and
dnj (Google)
2016/05/19 20:10:46
Done.
|
+ _, path := ProjectConfigPath(c, "") |
+ configs, err := config.Get(c).GetProjectConfigs(path, false) |
nodir
2016/05/19 17:17:20
note that this endpoint is slow. Please mention in
dnj (Google)
2016/05/19 20:10:46
Done.
|
if err != nil { |
- log.WithError(err).Errorf(c, "Failed to list all projects.") |
+ log.WithError(err).Errorf(c, "Failed to list 'luci-config' projects.") |
nodir
2016/05/19 17:17:20
this err message is not accurate
dnj (Google)
2016/05/19 20:10:46
Done.
|
return nil, err |
} |
- // In parallel, pull each project's configuration and assert access. |
- access := make([]bool, len(allProjects)) |
- err = parallel.WorkPool(maxProjectWorkers, func(taskC chan<- func() error) { |
- for i, proj := range allProjects { |
- i := i |
- proj := config.ProjectName(proj.ID) |
- |
- taskC <- func() error { |
- hasAccess, err := checkProjectAccess(c, ci, proj, st) |
- switch err { |
- case nil: |
- access[i] = hasAccess |
- return nil |
- |
- case config.ErrNoConfig: |
- // No configuration for this project, so nothing to check. Assume no |
- // access. |
- return nil |
- |
- default: |
- log.Fields{ |
- log.ErrorKey: err, |
- "project": proj, |
- }.Errorf(c, "Failed to check project access.") |
- return err |
- } |
- } |
+ result := make(map[config.ProjectName]*svcconfig.ProjectConfig, len(configs)) |
+ for _, cfg := range configs { |
+ // Identify the project by removng the "projects/" prefix. |
+ project := config.ProjectName(strings.TrimPrefix(cfg.ConfigSet, "projects/")) |
+ if err := project.Validate(); err != nil { |
+ log.Fields{ |
+ log.ErrorKey: err, |
+ "configSet": cfg.ConfigSet, |
+ }.Errorf(c, "Invalid project name returned.") |
+ continue |
} |
- }) |
- if err != nil { |
- log.WithError(err).Errorf(c, "Error during project access check.") |
- return nil, errors.SingleError(err) |
- } |
- result := make([]config.ProjectName, 0, len(allProjects)) |
- for i, proj := range allProjects { |
- if access[i] { |
- result = append(result, config.ProjectName(proj.ID)) |
+ // Unmarshal the project's configuration. |
+ pcfg, err := loadProjectConfig(&cfg) |
+ if err != nil { |
+ log.Fields{ |
+ log.ErrorKey: err, |
+ "configSet": cfg.ConfigSet, |
+ "path": cfg.Path, |
+ "contentHash": cfg.ContentHash, |
+ }.Errorf(c, "Failed to unmarshal project configuration.") |
+ continue |
} |
+ |
+ result[project] = pcfg |
} |
return result, nil |
} |
-func checkProjectAccess(c context.Context, ci config.Interface, proj config.ProjectName, st auth.State) (bool, error) { |
- // Load the configuration for this project. |
- configSet, configPath := ProjectConfigPath(c, proj) |
- cfg, err := ci.GetConfig(configSet, configPath, false) |
- if err != nil { |
- if err == config.ErrNoConfig { |
- // If the configuration is missing, report no access. |
- return false, nil |
- } |
- return false, err |
- } |
- |
- var pcfg configProto.ProjectCfg |
+func loadProjectConfig(cfg *config.Config) (*svcconfig.ProjectConfig, error) { |
+ var pcfg svcconfig.ProjectConfig |
if err := proto.UnmarshalText(cfg.Content, &pcfg); err != nil { |
- log.Fields{ |
- log.ErrorKey: err, |
- "project": proj, |
- }.Errorf(c, "Failed to unmarshal project configuration.") |
- return false, err |
- } |
- |
- // Vet project access using the current Authenticator state. |
- id := st.User().Identity |
- |
- for _, v := range pcfg.Access { |
- // Is this a group access? |
- access, isGroup := trimPrefix(v, "group:") |
- if isGroup { |
- // Check group membership. |
- isMember, err := st.DB().IsMember(c, id, access) |
- if err != nil { |
- return false, fmt.Errorf("failed to check access %q: %v", v, err) |
- } |
- if isMember { |
- log.Fields{ |
- "project": proj, |
- "identity": id, |
- "group": access, |
- }.Debugf(c, "Identity has group membership.") |
- return true, nil |
- } |
- return false, nil |
- } |
- |
- // "access" is either an e-mail or an identity. If it is an e-mail, |
- // transform it into an identity. |
- if idx := strings.IndexRune(access, ':'); idx < 0 { |
- // Presumably an e-mail, convert e-mail to user identity. |
- access = "user:" + access |
- } |
- |
- if id == identity.Identity(access) { |
- // Check identity. |
- log.Fields{ |
- "project": proj, |
- "identity": id, |
- "accessIdentity": v, |
- }.Debugf(c, "Identity is included.") |
- return true, nil |
- } |
- } |
- |
- return false, nil |
-} |
- |
-func trimPrefix(s, p string) (string, bool) { |
- if strings.HasPrefix(s, p) { |
- return s[len(p):], true |
+ return nil, err |
} |
- return s, false |
+ return &pcfg, nil |
} |