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. | 21 // ProjectConfigPath returns the path of the project-specific configuration. |
27 var ErrNoAccess = errors.New("no access") | 22 // This path should be used with a project config set. |
28 | |
29 // ProjectConfigPath returns the config set and path for project-specific | |
30 // 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) string { |
34 » return fmt.Sprintf("projects/%s", project), fmt.Sprintf("%s.cfg", info.G
et(c).AppID()) | 26 » return fmt.Sprintf("%s.cfg", info.Get(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 // |
39 // If the configuration was not present, config.ErrNoConfig will be returned. | 31 // If the configuration was not present, config.ErrNoConfig will be returned. |
40 func ProjectConfig(c context.Context, project config.ProjectName) (*svcconfig.Pr
ojectConfig, error) { | 32 func ProjectConfig(c context.Context, project config.ProjectName) (*svcconfig.Pr
ojectConfig, error) { |
41 // TODO(dnj): When empty project is disabled, make this return | 33 // TODO(dnj): When empty project is disabled, make this return |
42 // config.ErrNoConfig. | 34 // config.ErrNoConfig. |
43 if project == "" { | 35 if project == "" { |
44 return &svcconfig.ProjectConfig{ | 36 return &svcconfig.ProjectConfig{ |
45 ReaderAuthGroups: []string{"all"}, | 37 ReaderAuthGroups: []string{"all"}, |
46 }, nil | 38 }, nil |
47 } | 39 } |
48 | 40 |
49 » configSet, configPath := ProjectConfigPath(c, project) | 41 » configSet, configPath := config.ProjectConfigSet(project), ProjectConfig
Path(c) |
50 cfg, err := config.Get(c).GetConfig(configSet, configPath, false) | 42 cfg, err := config.Get(c).GetConfig(configSet, configPath, false) |
51 if err != nil { | 43 if err != nil { |
52 return nil, err | 44 return nil, err |
53 } | 45 } |
54 | 46 |
55 var pcfg svcconfig.ProjectConfig | 47 var pcfg svcconfig.ProjectConfig |
56 if err := proto.UnmarshalText(cfg.Content, &pcfg); err != nil { | 48 if err := proto.UnmarshalText(cfg.Content, &pcfg); err != nil { |
57 log.Fields{ | 49 log.Fields{ |
58 log.ErrorKey: err, | 50 log.ErrorKey: err, |
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 » // TODO: This endpoint is generally slow. Even though there is memcache-
based |
| 68 » // config cache, this really should be loaded from a more failsafe cache
like |
| 69 » // datastore to protect against config service outages. |
| 70 » configs, err := config.Get(c).GetProjectConfigs(ProjectConfigPath(c), fa
lse) |
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 load project configs.") |
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 |