| 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 |