Index: appengine/logdog/coordinator/config/projects.go |
diff --git a/appengine/logdog/coordinator/config/projects.go b/appengine/logdog/coordinator/config/projects.go |
index 3914c84396e6e41259adeb8d6b58ba2f8c29f355..768460ed4bfe59ecc77c19296ecd951a7259dcf8 100644 |
--- a/appengine/logdog/coordinator/config/projects.go |
+++ b/appengine/logdog/coordinator/config/projects.go |
@@ -6,26 +6,17 @@ package config |
import ( |
"fmt" |
- "strings" |
"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 +57,18 @@ 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 lists the set of projects registered with the config service. |
+func Projects(c context.Context) ([]config.ProjectName, 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)) |
+ ids := make([]config.ProjectName, len(projects)) |
for i, p := range projects { |
- ids[i] = p.ID |
+ ids[i] = config.ProjectName(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. |
-// |
-// 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 err != nil { |
- log.WithError(err).Errorf(c, "Failed to list all projects.") |
- 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 |
- } |
- } |
- } |
- }) |
- 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)) |
- } |
- } |
- 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 |
- 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 s, false |
-} |