| 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" | |
| 10 | 9 |
| 11 "github.com/golang/protobuf/proto" | 10 "github.com/golang/protobuf/proto" |
| 12 "github.com/luci/gae/service/info" | 11 "github.com/luci/gae/service/info" |
| 13 "github.com/luci/luci-go/common/config" | 12 "github.com/luci/luci-go/common/config" |
| 14 "github.com/luci/luci-go/common/errors" | |
| 15 log "github.com/luci/luci-go/common/logging" | 13 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" | 14 "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" | 15 "golang.org/x/net/context" |
| 22 ) | 16 ) |
| 23 | 17 |
| 24 const maxProjectWorkers = 32 | 18 const maxProjectWorkers = 32 |
| 25 | 19 |
| 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 | 20 // ProjectConfigPath returns the config set and path for project-specific |
| 30 // configuration. | 21 // configuration. |
| 31 // | 22 // |
| 32 // A given project's configuration is named after the current App ID. | 23 // A given project's configuration is named after the current App ID. |
| 33 func ProjectConfigPath(c context.Context, project config.ProjectName) (string, s
tring) { | 24 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()) | 25 return fmt.Sprintf("projects/%s", project), fmt.Sprintf("%s.cfg", info.G
et(c).AppID()) |
| 35 } | 26 } |
| 36 | 27 |
| 37 // ProjectConfig loads the the project config protobuf from the config service. | 28 // ProjectConfig loads the the project config protobuf from the config service. |
| 38 // | 29 // |
| (...skipping 20 matching lines...) Expand all Loading... |
| 59 "contentHash": cfg.ContentHash, | 50 "contentHash": cfg.ContentHash, |
| 60 "path": cfg.Path, | 51 "path": cfg.Path, |
| 61 "configSet": cfg.ConfigSet, | 52 "configSet": cfg.ConfigSet, |
| 62 }.Errorf(c, "Failed to unmarshal project configuration.") | 53 }.Errorf(c, "Failed to unmarshal project configuration.") |
| 63 return nil, ErrInvalidConfig | 54 return nil, ErrInvalidConfig |
| 64 } | 55 } |
| 65 | 56 |
| 66 return &pcfg, nil | 57 return &pcfg, nil |
| 67 } | 58 } |
| 68 | 59 |
| 69 // Projects lists the registered LogDog projects. | 60 // Projects lists the set of projects registered with the config service. |
| 70 func Projects(c context.Context) ([]string, error) { | 61 func Projects(c context.Context) ([]config.ProjectName, error) { |
| 71 projects, err := config.Get(c).GetProjects() | 62 projects, err := config.Get(c).GetProjects() |
| 72 if err != nil { | 63 if err != nil { |
| 73 log.WithError(err).Errorf(c, "Failed to list 'luci-config' proje
cts.") | 64 log.WithError(err).Errorf(c, "Failed to list 'luci-config' proje
cts.") |
| 74 return nil, err | 65 return nil, err |
| 75 } | 66 } |
| 76 | 67 |
| 77 » // TODO(dnj): Filter this list to projects with active LogDog configs, o
nce we | 68 » ids := make([]config.ProjectName, len(projects)) |
| 78 » // move to project-specific configurations. | |
| 79 | |
| 80 » ids := make([]string, len(projects)) | |
| 81 for i, p := range projects { | 69 for i, p := range projects { |
| 82 » » ids[i] = p.ID | 70 » » ids[i] = config.ProjectName(p.ID) |
| 83 } | 71 } |
| 84 | 72 |
| 85 // TODO(dnj): Restrict this by actual namespaces in datastore. | |
| 86 return ids, nil | 73 return ids, nil |
| 87 } | 74 } |
| 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 } | |
| 200 return result, nil | |
| 201 } | |
| 202 | |
| 203 func checkProjectAccess(c context.Context, ci config.Interface, proj config.Proj
ectName, st auth.State) (bool, error) { | |
| 204 // Load the configuration for this project. | |
| 205 configSet, configPath := ProjectConfigPath(c, proj) | |
| 206 cfg, err := ci.GetConfig(configSet, configPath, false) | |
| 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 } | |
| 214 | |
| 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 } | |
| 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 |