comparison roundup/rest.py @ 7692:8fb42f41ef10 issue2550923_computed_property

merge in default branch to see if ti clears a travis-ci build error on 2.7 python; default branch builds fine
author John Rouillard <rouilj@ieee.org>
date Mon, 11 Sep 2023 00:10:29 -0400
parents 5b3ecdfd77f7
children b04e222501b8
comparison
equal deleted inserted replaced
7528:14a8e11f3a87 7692:8fb42f41ef10
565 if isinstance(prop, hyperdb.Multilink): 565 if isinstance(prop, hyperdb.Multilink):
566 raise UsageError( 566 raise UsageError(
567 'Multilink Traversal not allowed: %s' % p) 567 'Multilink Traversal not allowed: %s' % p)
568 # Now we have the classname in cn and the prop name in pn. 568 # Now we have the classname in cn and the prop name in pn.
569 if not self.db.security.hasPermission('View', uid, cn, pn): 569 if not self.db.security.hasPermission('View', uid, cn, pn):
570 raise(Unauthorised 570 raise (Unauthorised
571 ('User does not have permission on "%s.%s"' 571 ('User does not have permission on "%s.%s"'
572 % (cn, pn))) 572 % (cn, pn)))
573 try: 573 try:
574 cn = prop.classname 574 cn = prop.classname
575 except AttributeError: 575 except AttributeError:
2035 return RateLimit(calls, timedelta(seconds=interval)) 2035 return RateLimit(calls, timedelta(seconds=interval))
2036 else: 2036 else:
2037 # disable rate limiting if either parameter is 0 2037 # disable rate limiting if either parameter is 0
2038 return None 2038 return None
2039 2039
2040 def handle_apiRateLimitExceeded(self, apiRateLimit):
2041 """Determine if the rate limit is exceeded.
2042
2043 If not exceeded, return False and the rate limit header values.
2044 If exceeded, return error message and None
2045 """
2046 gcra = Gcra()
2047 # unique key is an "ApiLimit-" prefix and the uid)
2048 apiLimitKey = "ApiLimit-%s" % self.db.getuid()
2049 otk = self.db.Otk
2050 try:
2051 val = otk.getall(apiLimitKey)
2052 gcra.set_tat_as_string(apiLimitKey, val['tat'])
2053 except KeyError:
2054 # ignore if tat not set, it's 1970-1-1 by default.
2055 pass
2056 # see if rate limit exceeded and we need to reject the attempt
2057 reject = gcra.update(apiLimitKey, apiRateLimit)
2058
2059 # Calculate a timestamp that will make OTK expire the
2060 # unused entry 1 hour in the future
2061 ts = otk.lifetime(3600)
2062 otk.set(apiLimitKey,
2063 tat=gcra.get_tat_as_string(apiLimitKey),
2064 __timestamp=ts)
2065 otk.commit()
2066
2067 limitStatus = gcra.status(apiLimitKey, apiRateLimit)
2068 if not reject:
2069 return (False, limitStatus)
2070
2071 for header, value in limitStatus.items():
2072 self.client.setHeader(header, value)
2073
2074 # User exceeded limits: tell humans how long to wait
2075 # Headers above will do the right thing for api
2076 # aware clients.
2077 try:
2078 retry_after = limitStatus['Retry-After']
2079 except KeyError:
2080 # handle race condition. If the time between
2081 # the call to grca.update and grca.status
2082 # is sufficient to reload the bucket by 1
2083 # item, Retry-After will be missing from
2084 # limitStatus. So report a 1 second delay back
2085 # to the client. We treat update as sole
2086 # source of truth for exceeded rate limits.
2087 retry_after = '1'
2088 self.client.setHeader('Retry-After', retry_after)
2089
2090 msg = _("Api rate limits exceeded. Please wait: %s seconds.") % retry_after
2091 output = self.error_obj(429, msg, source="ApiRateLimiter")
2092
2093 # expose these headers to rest clients. Otherwise they can't
2094 # respond to:
2095 # rate limiting (*RateLimit*, Retry-After)
2096 # obsolete API endpoint (Sunset)
2097 # options request to discover supported methods (Allow)
2098 self.client.setHeader(
2099 "Access-Control-Expose-Headers",
2100 ", ".join([
2101 "X-RateLimit-Limit",
2102 "X-RateLimit-Remaining",
2103 "X-RateLimit-Reset",
2104 "X-RateLimit-Limit-Period",
2105 "Retry-After",
2106 "Sunset",
2107 "Allow",
2108 ])
2109 )
2110
2111 return (self.format_dispatch_output(
2112 self.__default_accept_type,
2113 output,
2114 True # pretty print for this error case as a
2115 # human may read it
2116 ), None)
2117
2040 def dispatch(self, method, uri, input): 2118 def dispatch(self, method, uri, input):
2041 """format and process the request""" 2119 """format and process the request"""
2042 output = None 2120 output = None
2043 2121
2044 # Before we do anything has the user hit the rate limit. 2122 # Before we do anything has the user hit the rate limit.
2045 # This should (but doesn't at the moment) bypass
2046 # all other processing to minimize load of badly
2047 # behaving client.
2048 2123
2049 # Get the limit here and not in the init() routine to allow 2124 # Get the limit here and not in the init() routine to allow
2050 # for a different rate limit per user. 2125 # for a different rate limit per user.
2051 apiRateLimit = self.getRateLimit() 2126 apiRateLimit = self.getRateLimit()
2052 2127
2053 if apiRateLimit: # if None, disable rate limiting 2128 if apiRateLimit: # if None, disable rate limiting
2054 gcra = Gcra() 2129 LimitExceeded, limitStatus = self.handle_apiRateLimitExceeded(
2055 # unique key is an "ApiLimit-" prefix and the uid) 2130 apiRateLimit)
2056 apiLimitKey = "ApiLimit-%s" % self.db.getuid() 2131 if LimitExceeded:
2057 otk = self.db.Otk 2132 return LimitExceeded # error message
2058 try: 2133
2059 val = otk.getall(apiLimitKey) 2134 for header, value in limitStatus.items():
2060 gcra.set_tat_as_string(apiLimitKey, val['tat']) 2135 # Retry-After will be 0 because
2061 except KeyError: 2136 # user still has quota available.
2062 # ignore if tat not set, it's 1970-1-1 by default. 2137 # Don't put out the header.
2063 pass 2138 if header in ('Retry-After',):
2064 # see if rate limit exceeded and we need to reject the attempt 2139 continue
2065 reject = gcra.update(apiLimitKey, apiRateLimit) 2140 self.client.setHeader(header, value)
2066
2067 # Calculate a timestamp that will make OTK expire the
2068 # unused entry 1 hour in the future
2069 ts = otk.lifetime(3600)
2070 otk.set(apiLimitKey,
2071 tat=gcra.get_tat_as_string(apiLimitKey),
2072 __timestamp=ts)
2073 otk.commit()
2074
2075 limitStatus = gcra.status(apiLimitKey, apiRateLimit)
2076 if reject:
2077 for header, value in limitStatus.items():
2078 self.client.setHeader(header, value)
2079 # User exceeded limits: tell humans how long to wait
2080 # Headers above will do the right thing for api
2081 # aware clients.
2082 try:
2083 retry_after = limitStatus['Retry-After']
2084 except KeyError:
2085 # handle race condition. If the time between
2086 # the call to grca.update and grca.status
2087 # is sufficient to reload the bucket by 1
2088 # item, Retry-After will be missing from
2089 # limitStatus. So report a 1 second delay back
2090 # to the client. We treat update as sole
2091 # source of truth for exceeded rate limits.
2092 retry_after = 1
2093
2094 msg = _("Api rate limits exceeded. Please wait: %s seconds.") % retry_after
2095 output = self.error_obj(429, msg, source="ApiRateLimiter")
2096 else:
2097 for header, value in limitStatus.items():
2098 # Retry-After will be 0 because
2099 # user still has quota available.
2100 # Don't put out the header.
2101 if header in ('Retry-After',):
2102 continue
2103 self.client.setHeader(header, value)
2104 2141
2105 # if X-HTTP-Method-Override is set, follow the override method 2142 # if X-HTTP-Method-Override is set, follow the override method
2106 headers = self.client.request.headers 2143 headers = self.client.request.headers
2107 # Never allow GET to be an unsafe operation (i.e. data changing). 2144 # Never allow GET to be an unsafe operation (i.e. data changing).
2108 # User must use POST to "tunnel" DELETE, PUT, OPTIONS etc. 2145 # User must use POST to "tunnel" DELETE, PUT, OPTIONS etc.
2187 # headers.get('Accept') is never empty if called here. 2224 # headers.get('Accept') is never empty if called here.
2188 # accept_type will be set to json if there is no Accept header 2225 # accept_type will be set to json if there is no Accept header
2189 # accept_type wil be empty only if there is an Accept header 2226 # accept_type wil be empty only if there is an Accept header
2190 # with invalid values. 2227 # with invalid values.
2191 data_type = ext_type or accept_type or headers.get('Accept') or "invalid" 2228 data_type = ext_type or accept_type or headers.get('Accept') or "invalid"
2192
2193 if method.upper() == 'OPTIONS': 2229 if method.upper() == 'OPTIONS':
2194 # add access-control-allow-* access-control-max-age to support 2230 # add access-control-allow-* access-control-max-age to support
2195 # CORS preflight 2231 # CORS preflight
2196 self.client.setHeader( 2232 self.client.setHeader(
2197 "Access-Control-Allow-Headers", 2233 "Access-Control-Allow-Headers",
2346 output = self.error_obj(404, msg) 2382 output = self.error_obj(404, msg)
2347 except Reject as msg: 2383 except Reject as msg:
2348 output = self.error_obj(405, msg.args[0]) 2384 output = self.error_obj(405, msg.args[0])
2349 self.client.setHeader("Allow", msg.args[1]) 2385 self.client.setHeader("Allow", msg.args[1])
2350 2386
2387 return self.format_dispatch_output(data_type, output, pretty_output)
2388
2389 def format_dispatch_output(self, accept_mime_type, output,
2390 pretty_print=True):
2351 # Format the content type 2391 # Format the content type
2352 if data_type.lower() == "json": 2392 if accept_mime_type.lower() == "json":
2353 self.client.setHeader("Content-Type", "application/json") 2393 self.client.setHeader("Content-Type", "application/json")
2354 if pretty_output: 2394 if pretty_print:
2355 indent = 4 2395 indent = 4
2356 else: 2396 else:
2357 indent = None 2397 indent = None
2358 output = RoundupJSONEncoder(indent=indent).encode(output) 2398 output = RoundupJSONEncoder(indent=indent).encode(output)
2359 elif data_type.lower() == "xml" and dicttoxml: 2399 elif accept_mime_type.lower() == "xml" and dicttoxml:
2360 self.client.setHeader("Content-Type", "application/xml") 2400 self.client.setHeader("Content-Type", "application/xml")
2361 if 'error' in output: 2401 if 'error' in output:
2362 # capture values in error with types unsupported 2402 # capture values in error with types unsupported
2363 # by dicttoxml e.g. an exception, into something it 2403 # by dicttoxml e.g. an exception, into something it
2364 # can handle 2404 # can handle
2387 # FIXME?? consider moving this earlier. We should 2427 # FIXME?? consider moving this earlier. We should
2388 # error out before doing any work if we can't 2428 # error out before doing any work if we can't
2389 # display acceptable output. 2429 # display acceptable output.
2390 self.client.response_code = 406 2430 self.client.response_code = 406
2391 output = ("Requested content type '%s' is not available.\n" 2431 output = ("Requested content type '%s' is not available.\n"
2392 "Acceptable types: %s" % (data_type, 2432 "Acceptable types: %s" % (accept_mime_type,
2393 ", ".join(sorted(self.__accepted_content_type.keys())))) 2433 ", ".join(sorted(self.__accepted_content_type.keys()))))
2394 2434
2395 # Make output json end in a newline to 2435 # Make output json end in a newline to
2396 # separate from following text in logs etc.. 2436 # separate from following text in logs etc..
2397 return bs2b(output + "\n") 2437 return bs2b(output + "\n")

Roundup Issue Tracker: http://roundup-tracker.org/