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