OLD | NEW |
1 # Copyright 2015 The LUCI Authors. All rights reserved. | 1 # Copyright 2015 The LUCI Authors. All rights reserved. |
2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
4 | 4 |
5 """API handler to mint delegation tokens.""" | 5 """API handler to mint delegation tokens.""" |
6 | 6 |
7 import logging | 7 import logging |
8 import webapp2 | 8 import webapp2 |
9 | 9 |
| 10 from google.appengine.ext import ndb |
| 11 |
10 from components import auth | 12 from components import auth |
11 from components import utils | 13 from components import utils |
12 | 14 |
13 from components.auth import delegation | 15 from components.auth import delegation |
| 16 from components.auth import ipaddr |
14 from components.auth.proto import delegation_pb2 | 17 from components.auth.proto import delegation_pb2 |
15 | 18 |
16 from proto import config_pb2 | 19 from proto import config_pb2 |
17 | 20 |
18 import config | 21 import config |
19 | 22 |
20 | 23 |
21 # Minimum accepted value for 'validity_duration'. | 24 # Minimum accepted value for 'validity_duration'. |
22 MIN_VALIDITY_DURATION_SEC = 30 | 25 MIN_VALIDITY_DURATION_SEC = 30 |
23 | 26 |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
71 @auth.require(lambda: not auth.get_current_identity().is_anonymous) | 74 @auth.require(lambda: not auth.get_current_identity().is_anonymous) |
72 def post(self): | 75 def post(self): |
73 # Forbid usage of delegation tokens for this particular call. Using | 76 # Forbid usage of delegation tokens for this particular call. Using |
74 # delegation when creating delegation tokens is too deep. Redelegation will | 77 # delegation when creating delegation tokens is too deep. Redelegation will |
75 # be done as separate explicit API call that accept existing delegation | 78 # be done as separate explicit API call that accept existing delegation |
76 # token via request body, not via headers. | 79 # token via request body, not via headers. |
77 if auth.get_current_identity() != auth.get_peer_identity(): | 80 if auth.get_current_identity() != auth.get_peer_identity(): |
78 raise auth.AuthorizationError( | 81 raise auth.AuthorizationError( |
79 'This API call must not be used with active delegation token') | 82 'This API call must not be used with active delegation token') |
80 | 83 |
81 # Convert request body to proto (with validation). | 84 # Convert request body to proto (with validation). Verify IP format. |
82 try: | 85 try: |
83 subtoken = subtoken_from_jsonish(self.parse_body()) | 86 subtoken = subtoken_from_jsonish(self.parse_body()) |
84 except (TypeError, ValueError) as exc: | 87 except (TypeError, ValueError) as exc: |
85 self.abort_with_error(400, text=str(exc)) | 88 self.abort_with_error(400, text=str(exc)) |
86 | 89 |
87 # Fill in defaults. | 90 # Fill in defaults. |
88 assert not subtoken.impersonator_id | 91 assert not subtoken.impersonator_id |
89 user_id = auth.get_current_identity().to_bytes() | 92 user_id = auth.get_current_identity().to_bytes() |
90 if not subtoken.issuer_id: | 93 if not subtoken.issuer_id: |
91 subtoken.issuer_id = user_id | 94 subtoken.issuer_id = user_id |
92 if subtoken.issuer_id != user_id: | 95 if subtoken.issuer_id != user_id: |
93 subtoken.impersonator_id = user_id | 96 subtoken.impersonator_id = user_id |
94 subtoken.creation_time = int(utils.time_time()) | 97 subtoken.creation_time = int(utils.time_time()) |
95 if not subtoken.validity_duration: | 98 if not subtoken.validity_duration: |
96 subtoken.validity_duration = DEF_VALIDITY_DURATION_SEC | 99 subtoken.validity_duration = DEF_VALIDITY_DURATION_SEC |
97 if not subtoken.services or '*' in subtoken.services: | 100 if not subtoken.services or '*' in subtoken.services: |
98 subtoken.services[:] = get_default_allowed_services(user_id) | 101 subtoken.services[:] = get_default_allowed_services(user_id) |
99 | 102 |
100 # Check ACL (raises auth.AuthorizationError on errors). | 103 # Check ACL (raises auth.AuthorizationError on errors). |
101 check_can_create_token(user_id, subtoken) | 104 rule = check_can_create_token(user_id, subtoken) |
| 105 |
| 106 # Register the token in the datastore, generate its ID. |
| 107 subtoken.subtoken_id = register_subtoken(subtoken, rule, auth.get_peer_ip()) |
102 | 108 |
103 # Create and sign the token. | 109 # Create and sign the token. |
104 try: | 110 try: |
105 token = delegation.serialize_token( | 111 token = delegation.serialize_token( |
106 delegation.seal_token( | 112 delegation.seal_token( |
107 delegation_pb2.SubtokenList(subtokens=[subtoken]))) | 113 delegation_pb2.SubtokenList(subtokens=[subtoken]))) |
108 except delegation.BadTokenError as exc: | 114 except delegation.BadTokenError as exc: |
109 # This happens if resulting token is too large. | 115 # This happens if resulting token is too large. |
110 self.abort_with_error(400, text=str(exc)) | 116 self.abort_with_error(400, text=str(exc)) |
111 | 117 |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
176 try: | 182 try: |
177 auth.Identity.from_bytes(imp) | 183 auth.Identity.from_bytes(imp) |
178 except ValueError as exc: | 184 except ValueError as exc: |
179 raise ValueError( | 185 raise ValueError( |
180 'Invalid identity name "%s" in "impersonate": %s' % (imp, exc)) | 186 'Invalid identity name "%s" in "impersonate": %s' % (imp, exc)) |
181 msg.issuer_id = str(imp) | 187 msg.issuer_id = str(imp) |
182 | 188 |
183 return msg | 189 return msg |
184 | 190 |
185 | 191 |
| 192 ################################################################################ |
| 193 |
| 194 |
186 # Fallback rule returned if nothing else matches. | 195 # Fallback rule returned if nothing else matches. |
187 DEFAULT_RULE = config_pb2.DelegationConfig.Rule( | 196 DEFAULT_RULE = config_pb2.DelegationConfig.Rule( |
188 user_id=['*'], | 197 user_id=['*'], |
189 target_service=['*'], | 198 target_service=['*'], |
190 max_validity_duration=MAX_VALIDITY_DURATION_SEC) | 199 max_validity_duration=MAX_VALIDITY_DURATION_SEC) |
191 | 200 |
192 | 201 |
193 def get_delegation_rule(user_id, services): | 202 def get_delegation_rule(user_id, services): |
194 """Returns first matching rule from delegation.cfg DelegationConfig rules. | 203 """Returns first matching rule from delegation.cfg DelegationConfig rules. |
195 | 204 |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
236 return False | 245 return False |
237 | 246 |
238 | 247 |
239 def check_can_create_token(user_id, subtoken): | 248 def check_can_create_token(user_id, subtoken): |
240 """Checks that caller is allowed to mint a given root token. | 249 """Checks that caller is allowed to mint a given root token. |
241 | 250 |
242 Args: | 251 Args: |
243 user_id: identity string of a current caller. | 252 user_id: identity string of a current caller. |
244 subtoken: instance of delegation_pb2.Subtoken describing root token. | 253 subtoken: instance of delegation_pb2.Subtoken describing root token. |
245 | 254 |
| 255 Returns: |
| 256 config_pb2.DelegationConfig.Rule that allows the operation. |
| 257 |
246 Raises: | 258 Raises: |
247 auth.AuthorizationError if such token is not allowed for the caller. | 259 auth.AuthorizationError if such token is not allowed for the caller. |
248 """ | 260 """ |
249 rule = get_delegation_rule(user_id, subtoken.services) | 261 rule = get_delegation_rule(user_id, subtoken.services) |
250 | 262 |
251 if subtoken.validity_duration > rule.max_validity_duration: | 263 if subtoken.validity_duration > rule.max_validity_duration: |
252 raise auth.AuthorizationError( | 264 raise auth.AuthorizationError( |
253 'Maximum allowed validity_duration is %d sec, %d requested.' % | 265 'Maximum allowed validity_duration is %d sec, %d requested.' % |
254 (rule.max_validity_duration, subtoken.validity_duration)) | 266 (rule.max_validity_duration, subtoken.validity_duration)) |
255 | 267 |
256 # Just delegating one's own identity (not impersonating someone else)? Allow. | 268 # Just delegating one's own identity (not impersonating someone else)? Allow. |
257 if subtoken.issuer_id == user_id: | 269 if subtoken.issuer_id == user_id: |
258 return | 270 return rule |
259 | 271 |
260 # Verify it's OK to impersonate a given user. | 272 # Verify it's OK to impersonate a given user. |
261 impersonated = auth.Identity.from_bytes(subtoken.issuer_id) | 273 impersonated = auth.Identity.from_bytes(subtoken.issuer_id) |
262 for principal_set in rule.allowed_to_impersonate: | 274 for principal_set in rule.allowed_to_impersonate: |
263 if is_identity_in_principal_set(impersonated, principal_set): | 275 if is_identity_in_principal_set(impersonated, principal_set): |
264 return | 276 return rule |
265 | 277 |
266 raise auth.AuthorizationError( | 278 raise auth.AuthorizationError( |
267 '"%s" is not allowed to impersonate "%s" on %s' % | 279 '"%s" is not allowed to impersonate "%s" on %s' % |
268 (user_id, subtoken.issuer_id, subtoken.services or ['*'])) | 280 (user_id, subtoken.issuer_id, subtoken.services or ['*'])) |
269 | 281 |
270 | 282 |
271 def get_default_allowed_services(user_id): | 283 def get_default_allowed_services(user_id): |
272 """Returns the list of services defined by a first matching rule. | 284 """Returns the list of services defined by a first matching rule. |
273 | 285 |
274 Args: | 286 Args: |
275 user_id: identity string of a current caller. | 287 user_id: identity string of a current caller. |
276 """ | 288 """ |
277 rule = get_delegation_rule(user_id, ['*']) | 289 rule = get_delegation_rule(user_id, ['*']) |
278 return rule.target_service | 290 return rule.target_service |
279 | 291 |
| 292 |
| 293 ################################################################################ |
| 294 |
| 295 |
| 296 class AuthDelegationSubtoken(ndb.Model): |
| 297 """Represents a delegation subtoken. |
| 298 |
| 299 Used to track what tokens are issued. Root entity. ID is autogenerated. |
| 300 """ |
| 301 # Serialized delegation_pb2.Subtoken proto. |
| 302 subtoken = ndb.BlobProperty() |
| 303 # Serialized config_pb2.DelegationConfig.Rule that allowed this token. |
| 304 rule = ndb.BlobProperty() |
| 305 # IP address the minting request came from. |
| 306 caller_ip = ndb.StringProperty() |
| 307 # Version of the auth_service that created the subtoken. |
| 308 auth_service_version = ndb.StringProperty() |
| 309 |
| 310 # Fields below are extracted from 'subtoken', for indexing purposes. |
| 311 |
| 312 # Whose authority the token conveys. |
| 313 issuer_id = ndb.StringProperty() |
| 314 # When the token was created. |
| 315 creation_time = ndb.DateTimeProperty() |
| 316 # List of services that accept the token (or ['*'] if all). |
| 317 services = ndb.StringProperty(repeated=True) |
| 318 # Who initiated the minting request if it is an impersonation token. |
| 319 impersonator_id = ndb.StringProperty() |
| 320 |
| 321 |
| 322 def register_subtoken(subtoken, rule, caller_ip): |
| 323 """Creates new AuthDelegationSubtoken entity in the datastore, returns its ID. |
| 324 |
| 325 Args: |
| 326 subtoken: delegation_pb2.Subtoken describing the token. |
| 327 rule: config_pb2.DelegationConfig.Rule that allows the operation |
| 328 caller_ip: ipaddr.IP of the caller. |
| 329 |
| 330 Returns: |
| 331 int64 with ID of the new entity. |
| 332 """ |
| 333 entity = AuthDelegationSubtoken( |
| 334 subtoken=subtoken.SerializeToString(), |
| 335 rule=rule.SerializeToString(), |
| 336 caller_ip=ipaddr.ip_to_string(caller_ip), |
| 337 auth_service_version=utils.get_app_version(), |
| 338 issuer_id=subtoken.issuer_id, |
| 339 creation_time=utils.timestamp_to_datetime(subtoken.creation_time*1e6), |
| 340 services=list(subtoken.services or ['*']), |
| 341 impersonator_id=subtoken.impersonator_id) |
| 342 entity.put(use_cache=False, use_memcache=False) |
| 343 subtoken_id = entity.key.integer_id() |
| 344 |
| 345 # Keep a logging entry (extractable via BigQuery) too. |
| 346 logging.info( |
| 347 'subtoken: subtoken_id=%d caller_ip=%s issuer_id=%s impersonator_id=%s', |
| 348 subtoken_id, entity.caller_ip, entity.issuer_id, entity.impersonator_id) |
| 349 |
| 350 return subtoken_id |
OLD | NEW |