Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package config | 5 package config |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "fmt" | 8 "fmt" |
| 9 "strings" | 9 "strings" |
| 10 | 10 |
| 11 "github.com/golang/protobuf/proto" | 11 "github.com/golang/protobuf/proto" |
| 12 "github.com/luci/gae/service/info" | 12 "github.com/luci/gae/service/info" |
| 13 "github.com/luci/luci-go/common/config" | 13 "github.com/luci/luci-go/common/config" |
| 14 "github.com/luci/luci-go/common/errors" | |
| 15 log "github.com/luci/luci-go/common/logging" | 14 log "github.com/luci/luci-go/common/logging" |
| 16 "github.com/luci/luci-go/common/parallel" | |
| 17 configProto "github.com/luci/luci-go/common/proto/config" | |
| 18 "github.com/luci/luci-go/common/proto/logdog/svcconfig" | 15 "github.com/luci/luci-go/common/proto/logdog/svcconfig" |
| 19 "github.com/luci/luci-go/server/auth" | |
| 20 "github.com/luci/luci-go/server/auth/identity" | |
| 21 "golang.org/x/net/context" | 16 "golang.org/x/net/context" |
| 22 ) | 17 ) |
| 23 | 18 |
| 24 const maxProjectWorkers = 32 | 19 const maxProjectWorkers = 32 |
| 25 | 20 |
| 26 // ErrNoAccess is returned if the user has no access to the requested project. | |
| 27 var ErrNoAccess = errors.New("no access") | |
| 28 | |
| 29 // ProjectConfigPath returns the config set and path for project-specific | 21 // ProjectConfigPath returns the config set and path for project-specific |
| 30 // configuration. | 22 // configuration. |
| 31 // | 23 // |
| 32 // A given project's configuration is named after the current App ID. | 24 // A given project's configuration is named after the current App ID. |
| 33 func ProjectConfigPath(c context.Context, project config.ProjectName) (string, s tring) { | 25 func ProjectConfigPath(c context.Context, project config.ProjectName) (string, s tring) { |
| 34 return fmt.Sprintf("projects/%s", project), fmt.Sprintf("%s.cfg", info.G et(c).AppID()) | 26 return fmt.Sprintf("projects/%s", project), fmt.Sprintf("%s.cfg", info.G et(c).AppID()) |
| 35 } | 27 } |
| 36 | 28 |
| 37 // ProjectConfig loads the project config protobuf from the config service. | 29 // ProjectConfig loads the project config protobuf from the config service. |
| 38 // | 30 // |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 59 "configSet": cfg.ConfigSet, | 51 "configSet": cfg.ConfigSet, |
| 60 "path": cfg.Path, | 52 "path": cfg.Path, |
| 61 "contentHash": cfg.ContentHash, | 53 "contentHash": cfg.ContentHash, |
| 62 }.Errorf(c, "Failed to unmarshal project configuration.") | 54 }.Errorf(c, "Failed to unmarshal project configuration.") |
| 63 return nil, ErrInvalidConfig | 55 return nil, ErrInvalidConfig |
| 64 } | 56 } |
| 65 | 57 |
| 66 return &pcfg, nil | 58 return &pcfg, nil |
| 67 } | 59 } |
| 68 | 60 |
| 69 // Projects lists the registered LogDog projects. | 61 // AllProjectConfigs returns the project configurations for all projects that |
| 70 func Projects(c context.Context) ([]string, error) { | 62 // have a configuration. |
| 71 » projects, err := config.Get(c).GetProjects() | 63 // |
| 64 // If a project's configuration fails to load, an error will be logged and the | |
| 65 // project will be omitted from the output map. | |
| 66 func AllProjectConfigs(c context.Context) (map[config.ProjectName]*svcconfig.Pro jectConfig, error) { | |
| 67 » // Get the project config path. The "project" field is only used for the | |
| 68 » // 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.
| |
| 69 » _, path := ProjectConfigPath(c, "") | |
| 70 » 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.
| |
| 72 if err != nil { | 71 if err != nil { |
| 73 log.WithError(err).Errorf(c, "Failed to list 'luci-config' proje cts.") | 72 log.WithError(err).Errorf(c, "Failed to list 'luci-config' proje cts.") |
|
nodir
2016/05/19 17:17:20
this err message is not accurate
dnj (Google)
2016/05/19 20:10:46
Done.
| |
| 74 return nil, err | 73 return nil, err |
| 75 } | 74 } |
| 76 | 75 |
| 77 » // TODO(dnj): Filter this list to projects with active LogDog configs, o nce we | 76 » result := make(map[config.ProjectName]*svcconfig.ProjectConfig, len(conf igs)) |
| 78 » // move to project-specific configurations. | 77 » for _, cfg := range configs { |
| 78 » » // Identify the project by removng the "projects/" prefix. | |
| 79 » » project := config.ProjectName(strings.TrimPrefix(cfg.ConfigSet, "projects/")) | |
| 80 » » if err := project.Validate(); err != nil { | |
| 81 » » » log.Fields{ | |
| 82 » » » » log.ErrorKey: err, | |
| 83 » » » » "configSet": cfg.ConfigSet, | |
| 84 » » » }.Errorf(c, "Invalid project name returned.") | |
| 85 » » » continue | |
| 86 » » } | |
| 79 | 87 |
| 80 » ids := make([]string, len(projects)) | 88 » » // Unmarshal the project's configuration. |
| 81 » for i, p := range projects { | 89 » » pcfg, err := loadProjectConfig(&cfg) |
| 82 » » ids[i] = p.ID | 90 » » if err != nil { |
| 83 » } | 91 » » » log.Fields{ |
| 92 » » » » log.ErrorKey: err, | |
| 93 » » » » "configSet": cfg.ConfigSet, | |
| 94 » » » » "path": cfg.Path, | |
| 95 » » » » "contentHash": cfg.ContentHash, | |
| 96 » » » }.Errorf(c, "Failed to unmarshal project configuration." ) | |
| 97 » » » continue | |
| 98 » » } | |
| 84 | 99 |
| 85 » // TODO(dnj): Restrict this by actual namespaces in datastore. | 100 » » result[project] = pcfg |
| 86 » return ids, nil | |
| 87 } | |
| 88 | |
| 89 // AssertProjectAccess attempts to assert the current user's ability to access | |
| 90 // a given project. | |
| 91 // | |
| 92 // If the user cannot access the referenced project, ErrNoAccess will be | |
| 93 // returned. If an error occurs during checking, that error will be returned. | |
| 94 // The only time nil will be returned is if the check succeeded and the user was | |
| 95 // verified to have access to the requested project. | |
| 96 // | |
| 97 // NOTE: In the future, this will involve loading LogDog ACLs from the project's | |
| 98 // configuration. For now, though, since ACLs aren't implemented (yet), we will | |
| 99 // be content with asserting that the user has access to the base project. | |
| 100 func AssertProjectAccess(c context.Context, project config.ProjectName) error { | |
| 101 » // Get our authenticator. | |
| 102 » st := auth.GetState(c) | |
| 103 » if st == nil { | |
| 104 » » log.Errorf(c, "No authentication state in Context.") | |
| 105 » » return errors.New("no authentication state in context") | |
| 106 » } | |
| 107 | |
| 108 » hasAccess, err := checkProjectAccess(c, config.Get(c), project, st) | |
| 109 » if err != nil { | |
| 110 » » log.Fields{ | |
| 111 » » » log.ErrorKey: err, | |
| 112 » » » "project": project, | |
| 113 » » }.Errorf(c, "Failed to check for project access.") | |
| 114 » » return err | |
| 115 » } | |
| 116 | |
| 117 » if !hasAccess { | |
| 118 » » log.Fields{ | |
| 119 » » » log.ErrorKey: err, | |
| 120 » » » "project": project, | |
| 121 » » » "identity": st.User().Identity, | |
| 122 » » }.Errorf(c, "User does not have access to this project.") | |
| 123 » » return ErrNoAccess | |
| 124 » } | |
| 125 | |
| 126 » return nil | |
| 127 } | |
| 128 | |
| 129 // UserProjects returns the list of luci-config projects that the current user | |
| 130 // has access to. | |
| 131 // | |
| 132 // It does this by listing the full set of luci-config projects, then loading | |
| 133 // the project configuration for each one. If the project configuration | |
| 134 // specifies an access restriction that the user satisfies, that project will | |
| 135 // be included in the list. | |
| 136 // | |
| 137 // In a production environment, most of the config accesses will be cached. | |
| 138 // | |
| 139 // If the current user is anonymous, this will still work, returning the set of | |
| 140 // projects that the anonymous user can access. | |
| 141 func UserProjects(c context.Context) ([]config.ProjectName, error) { | |
| 142 » // NOTE: This client has a relatively short timeout, and using WithDeadl ine | |
| 143 » // will apply to all serial calls. We may want to make getting a config client | |
| 144 » // instance a coordinator.Service function if this proves to be a proble m. | |
| 145 » ci := config.Get(c) | |
| 146 | |
| 147 » // Get our authenticator. | |
| 148 » st := auth.GetState(c) | |
| 149 » if st == nil { | |
| 150 » » log.Errorf(c, "No authentication state in Context.") | |
| 151 » » return nil, errors.New("no authentication state in context") | |
| 152 » } | |
| 153 | |
| 154 » allProjects, err := ci.GetProjects() | |
| 155 » if err != nil { | |
| 156 » » log.WithError(err).Errorf(c, "Failed to list all projects.") | |
| 157 » » return nil, err | |
| 158 » } | |
| 159 | |
| 160 » // In parallel, pull each project's configuration and assert access. | |
| 161 » access := make([]bool, len(allProjects)) | |
| 162 » err = parallel.WorkPool(maxProjectWorkers, func(taskC chan<- func() erro r) { | |
| 163 » » for i, proj := range allProjects { | |
| 164 » » » i := i | |
| 165 » » » proj := config.ProjectName(proj.ID) | |
| 166 | |
| 167 » » » taskC <- func() error { | |
| 168 » » » » hasAccess, err := checkProjectAccess(c, ci, proj , st) | |
| 169 » » » » switch err { | |
| 170 » » » » case nil: | |
| 171 » » » » » access[i] = hasAccess | |
| 172 » » » » » return nil | |
| 173 | |
| 174 » » » » case config.ErrNoConfig: | |
| 175 » » » » » // No configuration for this project, so nothing to check. Assume no | |
| 176 » » » » » // access. | |
| 177 » » » » » return nil | |
| 178 | |
| 179 » » » » default: | |
| 180 » » » » » log.Fields{ | |
| 181 » » » » » » log.ErrorKey: err, | |
| 182 » » » » » » "project": proj, | |
| 183 » » » » » }.Errorf(c, "Failed to check project acc ess.") | |
| 184 » » » » » return err | |
| 185 » » » » } | |
| 186 » » » } | |
| 187 » » } | |
| 188 » }) | |
| 189 » if err != nil { | |
| 190 » » log.WithError(err).Errorf(c, "Error during project access check. ") | |
| 191 » » return nil, errors.SingleError(err) | |
| 192 » } | |
| 193 | |
| 194 » result := make([]config.ProjectName, 0, len(allProjects)) | |
| 195 » for i, proj := range allProjects { | |
| 196 » » if access[i] { | |
| 197 » » » result = append(result, config.ProjectName(proj.ID)) | |
| 198 » » } | |
| 199 } | 101 } |
| 200 return result, nil | 102 return result, nil |
| 201 } | 103 } |
| 202 | 104 |
| 203 func checkProjectAccess(c context.Context, ci config.Interface, proj config.Proj ectName, st auth.State) (bool, error) { | 105 func loadProjectConfig(cfg *config.Config) (*svcconfig.ProjectConfig, error) { |
| 204 » // Load the configuration for this project. | 106 » var pcfg svcconfig.ProjectConfig |
| 205 » configSet, configPath := ProjectConfigPath(c, proj) | 107 » if err := proto.UnmarshalText(cfg.Content, &pcfg); err != nil { |
| 206 » cfg, err := ci.GetConfig(configSet, configPath, false) | 108 » » return nil, err |
| 207 » if err != nil { | |
| 208 » » if err == config.ErrNoConfig { | |
| 209 » » » // If the configuration is missing, report no access. | |
| 210 » » » return false, nil | |
| 211 » » } | |
| 212 » » return false, err | |
| 213 } | 109 } |
| 214 | 110 » return &pcfg, nil |
| 215 » var pcfg configProto.ProjectCfg | |
| 216 » if err := proto.UnmarshalText(cfg.Content, &pcfg); err != nil { | |
| 217 » » log.Fields{ | |
| 218 » » » log.ErrorKey: err, | |
| 219 » » » "project": proj, | |
| 220 » » }.Errorf(c, "Failed to unmarshal project configuration.") | |
| 221 » » return false, err | |
| 222 » } | |
| 223 | |
| 224 » // Vet project access using the current Authenticator state. | |
| 225 » id := st.User().Identity | |
| 226 | |
| 227 » for _, v := range pcfg.Access { | |
| 228 » » // Is this a group access? | |
| 229 » » access, isGroup := trimPrefix(v, "group:") | |
| 230 » » if isGroup { | |
| 231 » » » // Check group membership. | |
| 232 » » » isMember, err := st.DB().IsMember(c, id, access) | |
| 233 » » » if err != nil { | |
| 234 » » » » return false, fmt.Errorf("failed to check access %q: %v", v, err) | |
| 235 » » » } | |
| 236 » » » if isMember { | |
| 237 » » » » log.Fields{ | |
| 238 » » » » » "project": proj, | |
| 239 » » » » » "identity": id, | |
| 240 » » » » » "group": access, | |
| 241 » » » » }.Debugf(c, "Identity has group membership.") | |
| 242 » » » » return true, nil | |
| 243 » » » } | |
| 244 » » » return false, nil | |
| 245 » » } | |
| 246 | |
| 247 » » // "access" is either an e-mail or an identity. If it is an e-ma il, | |
| 248 » » // transform it into an identity. | |
| 249 » » if idx := strings.IndexRune(access, ':'); idx < 0 { | |
| 250 » » » // Presumably an e-mail, convert e-mail to user identity . | |
| 251 » » » access = "user:" + access | |
| 252 » » } | |
| 253 | |
| 254 » » if id == identity.Identity(access) { | |
| 255 » » » // Check identity. | |
| 256 » » » log.Fields{ | |
| 257 » » » » "project": proj, | |
| 258 » » » » "identity": id, | |
| 259 » » » » "accessIdentity": v, | |
| 260 » » » }.Debugf(c, "Identity is included.") | |
| 261 » » » return true, nil | |
| 262 » » } | |
| 263 » } | |
| 264 | |
| 265 » return false, nil | |
| 266 } | 111 } |
| 267 | |
| 268 func trimPrefix(s, p string) (string, bool) { | |
| 269 if strings.HasPrefix(s, p) { | |
| 270 return s[len(p):], true | |
| 271 } | |
| 272 return s, false | |
| 273 } | |
| OLD | NEW |