| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. |
| 4 |
| 5 package delegation |
| 6 |
| 7 import ( |
| 8 "testing" |
| 9 "time" |
| 10 |
| 11 "github.com/golang/protobuf/proto" |
| 12 "golang.org/x/net/context" |
| 13 |
| 14 "github.com/luci/gae/service/datastore" |
| 15 "github.com/luci/luci-go/appengine/gaetesting" |
| 16 "github.com/luci/luci-go/common/clock/testclock" |
| 17 "github.com/luci/luci-go/server/auth" |
| 18 "github.com/luci/luci-go/server/auth/authtest" |
| 19 "github.com/luci/luci-go/server/auth/identity" |
| 20 admin "github.com/luci/luci-go/tokenserver/api/admin/v1" |
| 21 "github.com/luci/luci-go/tokenserver/appengine/utils/identityset" |
| 22 |
| 23 . "github.com/luci/luci-go/common/testing/assertions" |
| 24 . "github.com/smartystreets/goconvey/convey" |
| 25 ) |
| 26 |
| 27 func TestDelegationConfigLoader(t *testing.T) { |
| 28 Convey("DelegationConfigLoader works", t, func() { |
| 29 ctx := gaetesting.TestingContext() |
| 30 ctx, tc := testclock.UseTime(ctx, testclock.TestTimeUTC) |
| 31 |
| 32 loader := DelegationConfigLoader() |
| 33 |
| 34 // Put the initial copy into the datastore. |
| 35 cfg, err := loadConfig(` |
| 36 rules { |
| 37 name: "rule 1" |
| 38 requestor: "user:some-user@example.com" |
| 39 target_service: "service:some-service" |
| 40 allowed_to_impersonate: "group:some-group" |
| 41 allowed_audience: "REQUESTOR" |
| 42 max_validity_duration: 86400 |
| 43 }`) |
| 44 So(err, ShouldBeNil) |
| 45 cfg.Revision = "1" |
| 46 So(datastore.Put(ctx, cfg), ShouldBeNil) |
| 47 |
| 48 // Loader fetches it. |
| 49 fetched1, err := loader(ctx) |
| 50 So(err, ShouldBeNil) |
| 51 So(fetched1.ParsedConfig.Rules[0].Name, ShouldEqual, "rule 1") |
| 52 |
| 53 // Config is updated. |
| 54 cfg, err = loadConfig(` |
| 55 rules { |
| 56 name: "rule 2" |
| 57 requestor: "user:some-user@example.com" |
| 58 target_service: "service:some-service" |
| 59 allowed_to_impersonate: "group:some-group" |
| 60 allowed_audience: "REQUESTOR" |
| 61 max_validity_duration: 86400 |
| 62 }`) |
| 63 So(err, ShouldBeNil) |
| 64 cfg.Revision = "2" |
| 65 So(datastore.Put(ctx, cfg), ShouldBeNil) |
| 66 |
| 67 // Loader still returns old cached copy. |
| 68 fetched2, err := loader(ctx) |
| 69 So(err, ShouldBeNil) |
| 70 So(fetched2, ShouldEqual, fetched1) |
| 71 |
| 72 // Advance time to expire the cache. The new copy is fetched. |
| 73 tc.Add(procCacheExpiration + time.Second) |
| 74 fetched3, err := loader(ctx) |
| 75 So(err, ShouldBeNil) |
| 76 So(fetched3.ParsedConfig.Rules[0].Name, ShouldEqual, "rule 2") |
| 77 |
| 78 // Advance time again, but do not change the config. Loader reus
es existing |
| 79 // object. |
| 80 tc.Add(procCacheExpiration + time.Second) |
| 81 fetched4, err := loader(ctx) |
| 82 So(err, ShouldBeNil) |
| 83 So(fetched4, ShouldEqual, fetched3) |
| 84 }) |
| 85 } |
| 86 |
| 87 func TestIsAuthorizedRequestor(t *testing.T) { |
| 88 Convey("IsAuthorizedRequestor works", t, func() { |
| 89 cfg, err := loadConfig(` |
| 90 rules { |
| 91 name: "rule 1" |
| 92 requestor: "user:some-user@example.com" |
| 93 |
| 94 target_service: "service:some-service" |
| 95 allowed_to_impersonate: "group:some-group" |
| 96 allowed_audience: "REQUESTOR" |
| 97 max_validity_duration: 86400 |
| 98 } |
| 99 |
| 100 rules { |
| 101 name: "rule 2" |
| 102 requestor: "user:some-another-user@example.com" |
| 103 requestor: "group:some-group" |
| 104 |
| 105 target_service: "service:some-service" |
| 106 allowed_to_impersonate: "group:some-group" |
| 107 allowed_audience: "REQUESTOR" |
| 108 max_validity_duration: 86400 |
| 109 } |
| 110 `) |
| 111 So(err, ShouldBeNil) |
| 112 So(cfg, ShouldNotBeNil) |
| 113 |
| 114 ctx := auth.WithState(context.Background(), &authtest.FakeState{ |
| 115 Identity: "user:some-user@example.com", |
| 116 }) |
| 117 res, err := cfg.IsAuthorizedRequestor(ctx, identity.Identity("us
er:some-user@example.com")) |
| 118 So(err, ShouldBeNil) |
| 119 So(res, ShouldBeTrue) |
| 120 |
| 121 ctx = auth.WithState(context.Background(), &authtest.FakeState{ |
| 122 Identity: "user:some-another-user@example.com", |
| 123 }) |
| 124 res, err = cfg.IsAuthorizedRequestor(ctx, identity.Identity("use
r:some-another-user@example.com")) |
| 125 So(err, ShouldBeNil) |
| 126 So(res, ShouldBeTrue) |
| 127 |
| 128 ctx = auth.WithState(context.Background(), &authtest.FakeState{ |
| 129 Identity: "user:unknown-user@example.com", |
| 130 }) |
| 131 res, err = cfg.IsAuthorizedRequestor(ctx, identity.Identity("use
r:unknown-user@example.com")) |
| 132 So(err, ShouldBeNil) |
| 133 So(res, ShouldBeFalse) |
| 134 |
| 135 ctx = auth.WithState(context.Background(), &authtest.FakeState{ |
| 136 Identity: "user:via-group@example.com", |
| 137 IdentityGroups: []string{"some-group"}, |
| 138 }) |
| 139 res, err = cfg.IsAuthorizedRequestor(ctx, identity.Identity("use
r:via-group@example.com")) |
| 140 So(err, ShouldBeNil) |
| 141 So(res, ShouldBeTrue) |
| 142 }) |
| 143 } |
| 144 |
| 145 func TestFindMatchingRule(t *testing.T) { |
| 146 Convey("with example config", t, func() { |
| 147 cfg, err := loadConfig(` |
| 148 rules { |
| 149 name: "rule 1" |
| 150 requestor: "user:requestor@example.com" |
| 151 target_service: "service:some-service" |
| 152 allowed_to_impersonate: "user:allowed-to-imperso
nate@example.com" |
| 153 allowed_audience: "user:allowed-audience@example
.com" |
| 154 max_validity_duration: 86400 |
| 155 } |
| 156 |
| 157 rules { |
| 158 name: "rule 2" |
| 159 requestor: "group:requestor-group" |
| 160 target_service: "service:some-service" |
| 161 allowed_to_impersonate: "group:delegatees-group" |
| 162 allowed_audience: "group:audience-group" |
| 163 max_validity_duration: 86400 |
| 164 } |
| 165 |
| 166 rules { |
| 167 name: "rule 3" |
| 168 requestor: "group:requestor-group" |
| 169 target_service: "service:some-service" |
| 170 allowed_to_impersonate: "REQUESTOR" |
| 171 allowed_audience: "REQUESTOR" |
| 172 max_validity_duration: 86400 |
| 173 } |
| 174 |
| 175 rules { |
| 176 name: "rule 4" |
| 177 requestor: "user:some-requestor@example.com" |
| 178 requestor: "user:conflicts-with-rule-5@example.c
om" |
| 179 target_service: "*" |
| 180 allowed_to_impersonate: "REQUESTOR" |
| 181 allowed_audience: "*" |
| 182 max_validity_duration: 86400 |
| 183 } |
| 184 |
| 185 rules { |
| 186 name: "rule 5" |
| 187 requestor: "user:conflicts-with-rule-5@example.c
om" |
| 188 target_service: "*" |
| 189 allowed_to_impersonate: "REQUESTOR" |
| 190 allowed_audience: "*" |
| 191 max_validity_duration: 86400 |
| 192 } |
| 193 `) |
| 194 So(err, ShouldBeNil) |
| 195 So(cfg, ShouldNotBeNil) |
| 196 |
| 197 ctx := auth.WithState(context.Background(), &authtest.FakeState{ |
| 198 Identity: "user:requestor@example.com", |
| 199 FakeDB: authtest.FakeDB{ |
| 200 "user:requestor-group-member@example.com": []st
ring{"requestor-group"}, |
| 201 "user:delegatees-group-member@example.com": []st
ring{"delegatees-group"}, |
| 202 "user:audience-group-member@example.com": []st
ring{"audience-group"}, |
| 203 }, |
| 204 }) |
| 205 |
| 206 Convey("Direct matches and misses", func() { |
| 207 // Match. |
| 208 res, err := cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 209 Requestor: "user:requestor@example.com", |
| 210 Delegatee: "user:allowed-to-impersonate@example.
com", |
| 211 Audience: makeSet("user:allowed-audience@exampl
e.com"), |
| 212 Services: makeSet("service:some-service"), |
| 213 }) |
| 214 So(err, ShouldBeNil) |
| 215 So(res, ShouldNotBeNil) |
| 216 So(res.Name, ShouldEqual, "rule 1") |
| 217 |
| 218 // Unknown requestor. |
| 219 res, err = cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 220 Requestor: "user:unknown-requestor@example.com", |
| 221 Delegatee: "user:allowed-to-impersonate@example.
com", |
| 222 Audience: makeSet("user:allowed-audience@exampl
e.com"), |
| 223 Services: makeSet("service:some-service"), |
| 224 }) |
| 225 So(err, ShouldErrLike, "no matching delegation rules in
the config") |
| 226 So(res, ShouldBeNil) |
| 227 |
| 228 // Unknown delegatee. |
| 229 res, err = cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 230 Requestor: "user:requestor@example.com", |
| 231 Delegatee: "user:unknown-allowed-to-impersonate@
example.com", |
| 232 Audience: makeSet("user:allowed-audience@exampl
e.com"), |
| 233 Services: makeSet("service:some-service"), |
| 234 }) |
| 235 So(err, ShouldErrLike, "no matching delegation rules in
the config") |
| 236 So(res, ShouldBeNil) |
| 237 |
| 238 // Unknown audience. |
| 239 res, err = cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 240 Requestor: "user:requestor@example.com", |
| 241 Delegatee: "user:allowed-to-impersonate@example.
com", |
| 242 Audience: makeSet("user:unknown-allowed-audienc
e@example.com"), |
| 243 Services: makeSet("service:some-service"), |
| 244 }) |
| 245 So(err, ShouldErrLike, "no matching delegation rules in
the config") |
| 246 So(res, ShouldBeNil) |
| 247 |
| 248 // Unknown target service. |
| 249 res, err = cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 250 Requestor: "user:requestor@example.com", |
| 251 Delegatee: "user:allowed-to-impersonate@example.
com", |
| 252 Audience: makeSet("user:allowed-audience@exampl
e.com"), |
| 253 Services: makeSet("service:unknown-some-service
"), |
| 254 }) |
| 255 So(err, ShouldErrLike, "no matching delegation rules in
the config") |
| 256 So(res, ShouldBeNil) |
| 257 }) |
| 258 |
| 259 Convey("Matches via groups", func() { |
| 260 res, err := cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 261 Requestor: "user:requestor-group-member@example.
com", |
| 262 Delegatee: "user:delegatees-group-member@example
.com", |
| 263 Audience: makeSet("group:audience-group"), |
| 264 Services: makeSet("service:some-service"), |
| 265 }) |
| 266 So(err, ShouldBeNil) |
| 267 So(res, ShouldNotBeNil) |
| 268 So(res.Name, ShouldEqual, "rule 2") |
| 269 |
| 270 // Doesn't do group lookup when checking audience! |
| 271 res, err = cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 272 Requestor: "user:requestor-group-member@example.
com", |
| 273 Delegatee: "user:delegatees-group-member@example
.com", |
| 274 Audience: makeSet("user:audience-group-member@e
xample.com"), |
| 275 Services: makeSet("service:some-service"), |
| 276 }) |
| 277 So(err, ShouldErrLike, "no matching delegation rules in
the config") |
| 278 So(res, ShouldBeNil) |
| 279 }) |
| 280 |
| 281 Convey("REQUESTOR rules work", func() { |
| 282 res, err := cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 283 Requestor: "user:requestor-group-member@example.
com", |
| 284 Delegatee: "user:requestor-group-member@example.
com", |
| 285 Audience: makeSet("user:requestor-group-member@
example.com"), |
| 286 Services: makeSet("service:some-service"), |
| 287 }) |
| 288 So(err, ShouldBeNil) |
| 289 So(res, ShouldNotBeNil) |
| 290 So(res.Name, ShouldEqual, "rule 3") |
| 291 }) |
| 292 |
| 293 Convey("'*' rules work", func() { |
| 294 res, err := cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 295 Requestor: "user:some-requestor@example.com", |
| 296 Delegatee: "user:some-requestor@example.com", |
| 297 Audience: makeSet("group:abc", "user:def@exampl
e.com"), |
| 298 Services: makeSet("service:unknown"), |
| 299 }) |
| 300 So(err, ShouldBeNil) |
| 301 So(res, ShouldNotBeNil) |
| 302 So(res.Name, ShouldEqual, "rule 4") |
| 303 }) |
| 304 |
| 305 Convey("a conflict is handled", func() { |
| 306 res, err := cfg.FindMatchingRule(ctx, &RulesQuery{ |
| 307 Requestor: "user:conflicts-with-rule-5@example.c
om", |
| 308 Delegatee: "user:conflicts-with-rule-5@example.c
om", |
| 309 Audience: makeSet("group:abc", "user:def@exampl
e.com"), |
| 310 Services: makeSet("service:unknown"), |
| 311 }) |
| 312 So(err, ShouldErrLike, `ambiguous request, multiple dele
gation rules match ("rule 4", "rule 5")`) |
| 313 So(res, ShouldBeNil) |
| 314 }) |
| 315 }) |
| 316 } |
| 317 |
| 318 func loadConfig(text string) (*DelegationConfig, error) { |
| 319 cfg := &admin.DelegationPermissions{} |
| 320 err := proto.UnmarshalText(text, cfg) |
| 321 if err != nil { |
| 322 return nil, err |
| 323 } |
| 324 blob, err := proto.Marshal(cfg) |
| 325 if err != nil { |
| 326 return nil, err |
| 327 } |
| 328 c := &DelegationConfig{Config: blob} |
| 329 if err := c.Initialize(); err != nil { |
| 330 return nil, err |
| 331 } |
| 332 return c, nil |
| 333 } |
| 334 |
| 335 func makeSet(ident ...string) *identityset.Set { |
| 336 s, err := identityset.FromStrings(ident, nil) |
| 337 if err != nil { |
| 338 panic(err) |
| 339 } |
| 340 return s |
| 341 } |
| OLD | NEW |