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 |