Chromium Code Reviews| 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()) |
| 87 caller_ip = ipaddr.ip_from_string(self.request.remote_addr) | |
|
nodir
2016/07/20 18:04:22
ip is not provided by the user, so perhaps it shou
Vadim Sh.
2016/07/20 20:07:07
Done. Replaced with get_peer_ip, it is already val
| |
| 84 except (TypeError, ValueError) as exc: | 88 except (TypeError, ValueError) as exc: |
| 85 self.abort_with_error(400, text=str(exc)) | 89 self.abort_with_error(400, text=str(exc)) |
| 86 | 90 |
| 87 # Fill in defaults. | 91 # Fill in defaults. |
| 88 assert not subtoken.impersonator_id | 92 assert not subtoken.impersonator_id |
| 89 user_id = auth.get_current_identity().to_bytes() | 93 user_id = auth.get_current_identity().to_bytes() |
| 90 if not subtoken.issuer_id: | 94 if not subtoken.issuer_id: |
| 91 subtoken.issuer_id = user_id | 95 subtoken.issuer_id = user_id |
| 92 if subtoken.issuer_id != user_id: | 96 if subtoken.issuer_id != user_id: |
| 93 subtoken.impersonator_id = user_id | 97 subtoken.impersonator_id = user_id |
| 94 subtoken.creation_time = int(utils.time_time()) | 98 subtoken.creation_time = int(utils.time_time()) |
| 95 if not subtoken.validity_duration: | 99 if not subtoken.validity_duration: |
| 96 subtoken.validity_duration = DEF_VALIDITY_DURATION_SEC | 100 subtoken.validity_duration = DEF_VALIDITY_DURATION_SEC |
| 97 if not subtoken.services or '*' in subtoken.services: | 101 if not subtoken.services or '*' in subtoken.services: |
| 98 subtoken.services[:] = get_default_allowed_services(user_id) | 102 subtoken.services[:] = get_default_allowed_services(user_id) |
| 99 | 103 |
| 100 # Check ACL (raises auth.AuthorizationError on errors). | 104 # Check ACL (raises auth.AuthorizationError on errors). |
| 101 check_can_create_token(user_id, subtoken) | 105 rule = check_can_create_token(user_id, subtoken) |
| 106 | |
| 107 # Register the token in the datastore, generate its ID. | |
| 108 subtoken.subtoken_id = register_subtoken(subtoken, rule, caller_ip) | |
| 102 | 109 |
| 103 # Create and sign the token. | 110 # Create and sign the token. |
| 104 try: | 111 try: |
| 105 token = delegation.serialize_token( | 112 token = delegation.serialize_token( |
| 106 delegation.seal_token( | 113 delegation.seal_token( |
| 107 delegation_pb2.SubtokenList(subtokens=[subtoken]))) | 114 delegation_pb2.SubtokenList(subtokens=[subtoken]))) |
| 108 except delegation.BadTokenError as exc: | 115 except delegation.BadTokenError as exc: |
| 109 # This happens if resulting token is too large. | 116 # This happens if resulting token is too large. |
| 110 self.abort_with_error(400, text=str(exc)) | 117 self.abort_with_error(400, text=str(exc)) |
| 111 | 118 |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 176 try: | 183 try: |
| 177 auth.Identity.from_bytes(imp) | 184 auth.Identity.from_bytes(imp) |
| 178 except ValueError as exc: | 185 except ValueError as exc: |
| 179 raise ValueError( | 186 raise ValueError( |
| 180 'Invalid identity name "%s" in "impersonate": %s' % (imp, exc)) | 187 'Invalid identity name "%s" in "impersonate": %s' % (imp, exc)) |
| 181 msg.issuer_id = str(imp) | 188 msg.issuer_id = str(imp) |
| 182 | 189 |
| 183 return msg | 190 return msg |
| 184 | 191 |
| 185 | 192 |
| 193 ################################################################################ | |
| 194 | |
| 195 | |
| 186 # Fallback rule returned if nothing else matches. | 196 # Fallback rule returned if nothing else matches. |
| 187 DEFAULT_RULE = config_pb2.DelegationConfig.Rule( | 197 DEFAULT_RULE = config_pb2.DelegationConfig.Rule( |
| 188 user_id=['*'], | 198 user_id=['*'], |
| 189 target_service=['*'], | 199 target_service=['*'], |
| 190 max_validity_duration=MAX_VALIDITY_DURATION_SEC) | 200 max_validity_duration=MAX_VALIDITY_DURATION_SEC) |
| 191 | 201 |
| 192 | 202 |
| 193 def get_delegation_rule(user_id, services): | 203 def get_delegation_rule(user_id, services): |
| 194 """Returns first matching rule from delegation.cfg DelegationConfig rules. | 204 """Returns first matching rule from delegation.cfg DelegationConfig rules. |
| 195 | 205 |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 236 return False | 246 return False |
| 237 | 247 |
| 238 | 248 |
| 239 def check_can_create_token(user_id, subtoken): | 249 def check_can_create_token(user_id, subtoken): |
| 240 """Checks that caller is allowed to mint a given root token. | 250 """Checks that caller is allowed to mint a given root token. |
| 241 | 251 |
| 242 Args: | 252 Args: |
| 243 user_id: identity string of a current caller. | 253 user_id: identity string of a current caller. |
| 244 subtoken: instance of delegation_pb2.Subtoken describing root token. | 254 subtoken: instance of delegation_pb2.Subtoken describing root token. |
| 245 | 255 |
| 256 Returns: | |
| 257 config_pb2.DelegationConfig.Rule that allows the operation. | |
| 258 | |
| 246 Raises: | 259 Raises: |
| 247 auth.AuthorizationError if such token is not allowed for the caller. | 260 auth.AuthorizationError if such token is not allowed for the caller. |
| 248 """ | 261 """ |
| 249 rule = get_delegation_rule(user_id, subtoken.services) | 262 rule = get_delegation_rule(user_id, subtoken.services) |
| 250 | 263 |
| 251 if subtoken.validity_duration > rule.max_validity_duration: | 264 if subtoken.validity_duration > rule.max_validity_duration: |
| 252 raise auth.AuthorizationError( | 265 raise auth.AuthorizationError( |
| 253 'Maximum allowed validity_duration is %d sec, %d requested.' % | 266 'Maximum allowed validity_duration is %d sec, %d requested.' % |
| 254 (rule.max_validity_duration, subtoken.validity_duration)) | 267 (rule.max_validity_duration, subtoken.validity_duration)) |
| 255 | 268 |
| 256 # Just delegating one's own identity (not impersonating someone else)? Allow. | 269 # Just delegating one's own identity (not impersonating someone else)? Allow. |
| 257 if subtoken.issuer_id == user_id: | 270 if subtoken.issuer_id == user_id: |
| 258 return | 271 return rule |
| 259 | 272 |
| 260 # Verify it's OK to impersonate a given user. | 273 # Verify it's OK to impersonate a given user. |
| 261 impersonated = auth.Identity.from_bytes(subtoken.issuer_id) | 274 impersonated = auth.Identity.from_bytes(subtoken.issuer_id) |
| 262 for principal_set in rule.allowed_to_impersonate: | 275 for principal_set in rule.allowed_to_impersonate: |
| 263 if is_identity_in_principal_set(impersonated, principal_set): | 276 if is_identity_in_principal_set(impersonated, principal_set): |
| 264 return | 277 return rule |
| 265 | 278 |
| 266 raise auth.AuthorizationError( | 279 raise auth.AuthorizationError( |
| 267 '"%s" is not allowed to impersonate "%s" on %s' % | 280 '"%s" is not allowed to impersonate "%s" on %s' % |
| 268 (user_id, subtoken.issuer_id, subtoken.services or ['*'])) | 281 (user_id, subtoken.issuer_id, subtoken.services or ['*'])) |
| 269 | 282 |
| 270 | 283 |
| 271 def get_default_allowed_services(user_id): | 284 def get_default_allowed_services(user_id): |
| 272 """Returns the list of services defined by a first matching rule. | 285 """Returns the list of services defined by a first matching rule. |
| 273 | 286 |
| 274 Args: | 287 Args: |
| 275 user_id: identity string of a current caller. | 288 user_id: identity string of a current caller. |
| 276 """ | 289 """ |
| 277 rule = get_delegation_rule(user_id, ['*']) | 290 rule = get_delegation_rule(user_id, ['*']) |
| 278 return rule.target_service | 291 return rule.target_service |
| 279 | 292 |
| 293 | |
| 294 ################################################################################ | |
| 295 | |
| 296 | |
| 297 class AuthDelegationSubtoken(ndb.Model): | |
| 298 """Represents a delegation subtoken. | |
| 299 | |
| 300 Used to track what tokens are issued. Root entity. ID is autogenerated. | |
| 301 """ | |
| 302 # Serialized delegation_pb2.Subtoken proto. | |
| 303 subtoken = ndb.BlobProperty() | |
| 304 # Serialized config_pb2.DelegationConfig.Rule that allowed this token. | |
| 305 rule = ndb.BlobProperty() | |
| 306 # IP address the minting request came from. | |
| 307 caller_ip = ndb.StringProperty() | |
| 308 # Version of the auth_service that created the subtoken. | |
| 309 auth_service_version = ndb.StringProperty() | |
| 310 | |
| 311 # Fields below are extracted from 'subtoken', for indexing purposes. | |
| 312 | |
| 313 # Whose authority the token conveys. | |
| 314 issuer_id = ndb.StringProperty() | |
| 315 # When the token was created. | |
| 316 creation_time = ndb.DateTimeProperty() | |
| 317 # List of service that accept the token (or empty list if all). | |
|
nodir
2016/07/20 18:04:22
services
Vadim Sh.
2016/07/20 20:07:06
Done.
| |
| 318 services = ndb.StringProperty(repeated=True) | |
| 319 # Who initiated the minting request if it is an impersonation token. | |
| 320 impersonator_id = ndb.StringProperty() | |
| 321 | |
| 322 | |
| 323 def register_subtoken(subtoken, rule, caller_ip): | |
| 324 """Creates new AuthDelegationSubtoken entity in the datastore, returns its ID. | |
| 325 | |
| 326 Args: | |
| 327 subtoken: delegation_pb2.Subtoken describing the token. | |
| 328 rule: config_pb2.DelegationConfig.Rule that allows the operation | |
| 329 caller_ip: ipaddr.IP of the caller. | |
| 330 | |
| 331 Returns: | |
| 332 int64 with ID of the new entity. | |
| 333 """ | |
| 334 entity = AuthDelegationSubtoken( | |
| 335 subtoken=subtoken.SerializeToString(), | |
| 336 rule=rule.SerializeToString(), | |
| 337 caller_ip=ipaddr.ip_to_string(caller_ip), | |
| 338 auth_service_version=utils.get_app_version(), | |
| 339 issuer_id=subtoken.issuer_id, | |
| 340 creation_time=utils.timestamp_to_datetime(subtoken.creation_time*1e6), | |
|
nodir
2016/07/20 18:04:22
using datetime.fromtimestamp is probably more stra
Vadim Sh.
2016/07/20 20:07:06
It returns local time, not UTC.
| |
| 341 services=list(subtoken.services or []), | |
| 342 impersonator_id=subtoken.impersonator_id) | |
| 343 entity.put(use_cache=False, use_memcache=False) | |
| 344 subtoken_id = entity.key.integer_id() | |
| 345 | |
| 346 # Keep a logging entry (extractable via BigQuery) too. | |
| 347 logging.info( | |
| 348 'subtoken: subtoken_id=%d caller_ip=%s issuer_id=%s impersonator_id=%s', | |
| 349 subtoken_id, entity.caller_ip, entity.issuer_id, entity.impersonator_id) | |
| 350 | |
| 351 return subtoken_id | |
| OLD | NEW |