def getAlias(self, aliasid, token, path): """Returns a LibraryFileAlias, or raises LookupError. A LookupError is raised if no record with the given ID exists or if not related LibraryFileContent exists. :param token: The token for the file. If None no token is present. When a token is supplied, it is looked up with path. :param path: The path the request is for, unused unless a token is supplied; when supplied it must match the token. The value of path is expected to be that from a twisted request.args e.g. /foo/bar. """ restricted = self.restricted if token and path: # with a token and a path we may be able to serve restricted files # on the public port. store = session_store() token_found = store.find(TimeLimitedToken, SQL("age(created) < interval '1 day'"), TimeLimitedToken.token == token, TimeLimitedToken.path==path).is_empty() store.reset() if token_found: raise LookupError("Token stale/pruned/path mismatch") else: restricted = True alias = LibraryFileAlias.selectOne(And( LibraryFileAlias.id == aliasid, LibraryFileAlias.contentID == LibraryFileContent.q.id, LibraryFileAlias.restricted == restricted)) if alias is None: raise LookupError("No file alias with LibraryFileContent") return alias
def allocate(url): """Allocate a token for url path in the librarian. :param url: A url bytestring. e.g. https://i123.restricted.launchpad-librarian.net/123/foo.txt Note that the token is generated for 123/foo.txt :return: A url fragment token ready to be attached to the url. e.g. 'a%20token' """ # We use random.random to get a string which varies reasonably, and we # hash it to distribute it widely and get a easily copy and pastable # single string (nice for debugging). The randomness is not a key # factor here: as long as tokens are not guessable, they are hidden by # https, not exposed directly in the API (tokens will be allocated by # the appropriate objects), not by direct access to the # TimeLimitedToken class. baseline = str(random.random()) hashed = md5(baseline).hexdigest() token = hashed store = session_store() path = TimeLimitedToken.url_to_token_path(url) store.add(TimeLimitedToken(path, token)) # The session isn't part of the main transaction model, and in fact it # has autocommit on. The commit here is belts and bracers: after # allocation the external librarian must be able to serve the file # immediately. store.commit() return token
def test_TimeLimitedTokenPruner(self): # Ensure there are no tokens store = sqlbase.session_store() map(store.remove, store.find(TimeLimitedToken)) store.flush() self.assertEqual(0, len(list(store.find(TimeLimitedToken, path="sample path")))) # One to clean and one to keep store.add(TimeLimitedToken(path="sample path", token="foo", created=datetime(2008, 01, 01, tzinfo=UTC)))
def test_allocate(self): store = session_store() store.find(TimeLimitedToken).remove() token1 = TimeLimitedToken.allocate('foo://') token2 = TimeLimitedToken.allocate('foo://') # We must get unique tokens self.assertNotEqual(token1, token2) # They must be bytestrings (as a surrogate for valid url fragment') self.assertIsInstance(token1, str) self.assertIsInstance(token2, str)
def _get_secret(self): # Because our CookieClientIdManager is not persistent, we need to # pull the secret from some other data store - failing to do this # would mean a new secret is generated every time the server is # restarted, invalidating all old session information. # Secret is looked up here rather than in __init__, because # we can't be sure the database connections are setup at that point. if self._secret is None: store = session_store() result = store.execute("SELECT secret FROM secret") self._secret = result.get_one()[0] return self._secret
def getAlias(self, aliasid, token, path): """Returns a LibraryFileAlias, or raises LookupError. A LookupError is raised if no record with the given ID exists or if not related LibraryFileContent exists. :param aliasid: A `LibraryFileAlias` ID. :param token: The token for the file. If None no token is present. When a token is supplied, it is looked up with path. :param path: The path the request is for, unused unless a token is supplied; when supplied it must match the token. The value of path is expected to be that from a twisted request.args e.g. /foo/bar. """ restricted = self.restricted if token and path: # With a token and a path we may be able to serve restricted files # on the public port. if isinstance(token, Macaroon): # Macaroons have enough other constraints that they don't # need to be path-specific; it's simpler and faster to just # check the alias ID. token_ok = threads.blockingCallFromThread( default_reactor, self._verifyMacaroon, token, aliasid) else: # The URL-encoding of the path may have changed somewhere # along the line, so reencode it canonically. LFA.filename # can't contain slashes, so they're safe to leave unencoded. # And urllib.quote erroneously excludes ~ from its safe set, # while RFC 3986 says it should be unescaped and Chromium # forcibly decodes it in any URL that it sees. # # This needs to match url_path_quote. normalised_path = urllib.quote(urllib.unquote(path), safe='/~+') store = session_store() token_ok = not store.find( TimeLimitedToken, SQL("age(created) < interval '1 day'"), TimeLimitedToken.token == hashlib.sha256(token).hexdigest(), TimeLimitedToken.path == normalised_path).is_empty() store.reset() if token_ok: restricted = True else: raise LookupError("Token stale/pruned/path mismatch") alias = LibraryFileAlias.selectOne( And(LibraryFileAlias.id == aliasid, LibraryFileAlias.contentID == LibraryFileContent.q.id, LibraryFileAlias.restricted == restricted)) if alias is None: raise LookupError("No file alias with LibraryFileContent") return alias
def test_restricted_with_expired_token(self): fileAlias, url = self.get_restricted_file_and_public_url() # We have the base url for a restricted file; grant access to it # for a short time. token = TimeLimitedToken.allocate(url) # But time has passed store = session_store() tokens = store.find(TimeLimitedToken, TimeLimitedToken.token==token) tokens.set( TimeLimitedToken.created==SQL("created - interval '1 week'")) url = url + "?token=%s" % token # Now, as per test_restricted_no_token we should get a 404. self.require404(url)
def test_restricted_with_expired_token(self): fileAlias, url = self.get_restricted_file_and_public_url() # We have the base url for a restricted file; grant access to it # for a short time. token = TimeLimitedToken.allocate(url) # But time has passed store = session_store() tokens = store.find( TimeLimitedToken, TimeLimitedToken.token == hashlib.sha256(token).hexdigest()) tokens.set( TimeLimitedToken.created == SQL("created - interval '1 week'")) # Now, as per test_restricted_no_token we should get a 404. self.require404(url, params={"token": token})
def allocate(url): """Allocate a token for url path in the librarian. :param url: A url bytestring. e.g. https://i123.restricted.launchpad-librarian.net/123/foo.txt Note that the token is generated for 123/foo.txt :return: A url fragment token ready to be attached to the url. e.g. 'a%20token' """ store = session_store() path = TimeLimitedToken.url_to_token_path(url) token = create_token(32).encode('ascii') store.add(TimeLimitedToken(path, token)) # The session isn't part of the main transaction model, and in fact it # has autocommit on. The commit here is belts and bracers: after # allocation the external librarian must be able to serve the file # immediately. store.commit() return token
def session_default_store(cls): """Adapt an Session database object to an `IStore`.""" return session_store()
def session_slave_store(cls): """Adapt a Session database object to an `ISlaveStore`.""" return session_store()
def session_master_store(cls): """Adapt a Session database object to an `IMasterStore`.""" return session_store()