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 |