Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(886)

Side by Side Diff: appengine/logdog/coordinator/config/projects.go

Issue 1971493003: LogDog: Project READ access for user endpoints. (Closed) Base URL: https://github.com/luci/luci-go@logdog-project-service-config
Patch Set: Updated patchset dependency Created 4 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698