mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-11-01 07:48:03 -06:00
fix: drop pickled BigAnimal provider instance from session
The biganimal cloud-deployment module pickled the live BigAnimalProvider across 15 sites (verify, polling-for-token, projects, providers, regions, db_types, db_versions, instance_types, volume_types, volume_properties, deploy). Same pattern, same risk as the other cloud providers. Add BigAnimalProvider.to_state()/from_state() and _get_biganimal_from_session()/_save_biganimal_to_session() helpers. from_state uses cls.__new__(cls) to bypass __init__'s get_auth_provider() HTTP call -- the persisted `provider` dict already holds that data. Five new tests cover: round-trip preservation, helper construction, defaults for partial state, missing-session graceful return, and a regression assertion that pickle is gone.
This commit is contained in:
1 parent
fffc3cc242
commit
2b0d44a33f
3 files changed
+279
-15
No files matched your search
@@ -11,7 +11,6 @@
|
||||
|
||||
import requests
|
||||
import json
|
||||
import pickle
|
||||
from flask_babel import gettext
|
||||
from flask import session, current_app
|
||||
from pgadmin.user_login_check import pga_login_required
|
||||
@@ -49,15 +48,42 @@ blueprint = BigAnimalModule(MODULE_NAME, __name__,
|
||||
static_url_path='/misc/cloud/biganimal')
|
||||
|
||||
|
||||
def _get_biganimal_from_session():
|
||||
"""Build a BigAnimalProvider from `session['biganimal']['state']`.
|
||||
|
||||
Returns None when no state is in session — callers should treat that
|
||||
as "auth not yet started." Replaces an unsafe deserialization vector
|
||||
that previously persisted the live provider instance.
|
||||
"""
|
||||
biganimal = session.get('biganimal')
|
||||
if not biganimal:
|
||||
return None
|
||||
state = biganimal.get('state')
|
||||
if not state:
|
||||
return None
|
||||
return BigAnimalProvider.from_state(state)
|
||||
|
||||
|
||||
def _save_biganimal_to_session(biganimal_obj):
|
||||
"""Persist BigAnimalProvider state to session as a plain dict."""
|
||||
if 'biganimal' not in session:
|
||||
session['biganimal'] = {}
|
||||
session['biganimal']['state'] = biganimal_obj.to_state()
|
||||
|
||||
|
||||
@blueprint.route('/verification_ack/',
|
||||
methods=['GET'], endpoint='verification_ack')
|
||||
@pga_login_required
|
||||
def biganimal_verification_ack():
|
||||
"""Check the Verification is done or not."""
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(
|
||||
success=False,
|
||||
errormsg=gettext('Authentication session expired.'))
|
||||
status, error = biganimal_obj.polling_for_token()
|
||||
if status:
|
||||
session['biganimal']['provider_obj'] = pickle.dumps(biganimal_obj, -1)
|
||||
_save_biganimal_to_session(biganimal_obj)
|
||||
return make_json_response(success=status,
|
||||
errormsg=error)
|
||||
|
||||
@@ -70,7 +96,7 @@ def verification():
|
||||
biganimal = BigAnimalProvider()
|
||||
verification_uri = biganimal.get_device_code()
|
||||
session['biganimal'] = {}
|
||||
session['biganimal']['provider_obj'] = pickle.dumps(biganimal, -1)
|
||||
_save_biganimal_to_session(biganimal)
|
||||
|
||||
return make_json_response(data=verification_uri)
|
||||
|
||||
@@ -80,7 +106,10 @@ def verification():
|
||||
@pga_login_required
|
||||
def biganimal_projects():
|
||||
"""Get Providers."""
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[],
|
||||
errormsg='Authentication is failed.')
|
||||
projects, error = biganimal_obj.get_projects()
|
||||
return make_json_response(data=projects, errormsg=error)
|
||||
|
||||
@@ -90,9 +119,12 @@ def biganimal_projects():
|
||||
@pga_login_required
|
||||
def biganimal_providers(project_id):
|
||||
"""Get Providers."""
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[],
|
||||
errormsg='Authentication is failed.')
|
||||
providers, error = biganimal_obj.get_providers(project_id)
|
||||
session['biganimal']['provider_obj'] = pickle.dumps(biganimal_obj, -1)
|
||||
_save_biganimal_to_session(biganimal_obj)
|
||||
return make_json_response(data=providers, errormsg=error)
|
||||
|
||||
|
||||
@@ -101,9 +133,11 @@ def biganimal_providers(project_id):
|
||||
@pga_login_required
|
||||
def biganimal_regions():
|
||||
"""Get Regions."""
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[])
|
||||
_, regions = biganimal_obj.get_regions()
|
||||
session['biganimal']['provider_obj'] = pickle.dumps(biganimal_obj, -1)
|
||||
_save_biganimal_to_session(biganimal_obj)
|
||||
return make_json_response(data=regions)
|
||||
|
||||
|
||||
@@ -112,7 +146,9 @@ def biganimal_regions():
|
||||
@pga_login_required
|
||||
def biganimal_db_types():
|
||||
"""Get Database Types."""
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[])
|
||||
pg_types = biganimal_obj.get_postgres_types()
|
||||
return make_json_response(data=pg_types)
|
||||
|
||||
@@ -122,7 +158,9 @@ def biganimal_db_types():
|
||||
@pga_login_required
|
||||
def biganimal_db_versions(cluster_type, pg_type):
|
||||
"""Get Database Version."""
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[])
|
||||
pg_versions = biganimal_obj.get_postgres_versions(cluster_type, pg_type)
|
||||
return make_json_response(data=pg_versions)
|
||||
|
||||
@@ -134,7 +172,9 @@ def biganimal_instance_types(region_id, provider_id):
|
||||
"""Get Instance Types."""
|
||||
if not region_id or not provider_id:
|
||||
return make_json_response(data=[])
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[])
|
||||
biganimal_instances = biganimal_obj.get_instance_types(region_id,
|
||||
provider_id)
|
||||
return make_json_response(data=biganimal_instances)
|
||||
@@ -147,7 +187,9 @@ def biganimal_volume_types(region_id, provider_id):
|
||||
"""Get Volume Types."""
|
||||
if not region_id or not provider_id:
|
||||
return make_json_response(data=[])
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[])
|
||||
biganimal_volumes = biganimal_obj.get_volume_types(region_id, provider_id)
|
||||
return make_json_response(data=biganimal_volumes)
|
||||
|
||||
@@ -159,7 +201,9 @@ def biganimal_volume_properties(region_id, provider_id, volume_type):
|
||||
"""Get Volume Properties."""
|
||||
if not region_id or not provider_id:
|
||||
return make_json_response(data=[])
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None:
|
||||
return make_json_response(data=[])
|
||||
biganimal_volume_properties = biganimal_obj.get_volume_properties(
|
||||
region_id,
|
||||
provider_id,
|
||||
@@ -183,6 +227,46 @@ class BigAnimalProvider():
|
||||
self.get_auth_provider()
|
||||
self.project_id = None
|
||||
|
||||
def to_state(self):
|
||||
"""Serialize persistable state to a plain dict for `flask.session`.
|
||||
|
||||
Replaces persisting the live BigAnimalProvider via the unsafe
|
||||
serializer (which represented an insecure-deserialization vector).
|
||||
"""
|
||||
return {
|
||||
'provider': self.provider,
|
||||
'device_code': self.device_code,
|
||||
'token': self.token,
|
||||
'raw_access_token': self.raw_access_token,
|
||||
'access_token': self.access_token,
|
||||
'token_error': self.token_error,
|
||||
'token_status': self.token_status,
|
||||
'regions': self.regions,
|
||||
'project_id': self.project_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
"""Rebuild a BigAnimalProvider from a previously-serialized dict.
|
||||
|
||||
Bypasses `__init__` (which fetches the auth provider over HTTP) —
|
||||
the persisted `provider` field already has that data, so refetching
|
||||
would be a redundant network call.
|
||||
"""
|
||||
if not isinstance(state, dict):
|
||||
return None
|
||||
obj = cls.__new__(cls)
|
||||
obj.provider = state.get('provider', {}) or {}
|
||||
obj.device_code = state.get('device_code', {}) or {}
|
||||
obj.token = state.get('token', {}) or {}
|
||||
obj.raw_access_token = state.get('raw_access_token')
|
||||
obj.access_token = state.get('access_token')
|
||||
obj.token_error = state.get('token_error', {}) or {}
|
||||
obj.token_status = state.get('token_status', -1)
|
||||
obj.regions = state.get('regions', []) or []
|
||||
obj.project_id = state.get('project_id')
|
||||
return obj
|
||||
|
||||
def _get_headers(self):
|
||||
return {
|
||||
'content-type': MIMETYPE_APP_JSON,
|
||||
@@ -525,7 +609,9 @@ def deploy_on_biganimal(data):
|
||||
)
|
||||
|
||||
env = dict()
|
||||
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
|
||||
biganimal_obj = _get_biganimal_from_session()
|
||||
if biganimal_obj is None or biganimal_obj.access_token is None:
|
||||
return False, None, 'BigAnimal credentials missing from session.'
|
||||
env['BIGANIMAL_ACCESS_KEY'] = biganimal_obj.access_token
|
||||
|
||||
if 'password' in data['db_details']:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
@@ -0,0 +1,170 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
Unit tests for the BigAnimal cloud module's session-state refactor.
|
||||
|
||||
After the refactor, the live `BigAnimalProvider` instance is no longer
|
||||
persisted into the session via the unsafe deserializer. Instead a
|
||||
dict-shaped state is stored under `session['biganimal']['state']` and
|
||||
rebuilt via `BigAnimalProvider.from_state` per request.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
|
||||
|
||||
class _SkipServerSetUpMixin:
|
||||
"""Skip BaseTestGenerator's Postgres connection — pure logic tests."""
|
||||
|
||||
def setUp(self):
|
||||
unittest.TestCase.setUp(self)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Positive tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBigAnimalToStateRoundTrip(
|
||||
_SkipServerSetUpMixin, BaseTestGenerator):
|
||||
"""to_state/from_state preserves all persistable fields."""
|
||||
|
||||
scenarios = [('default', dict())]
|
||||
|
||||
def runTest(self):
|
||||
from pgadmin.misc.cloud.biganimal import BigAnimalProvider
|
||||
|
||||
# from_state path: bypasses __init__'s HTTP call, so safe to
|
||||
# construct in unit tests without network.
|
||||
original = BigAnimalProvider.from_state({
|
||||
'provider': {'clientId': 'X', 'issuerUri': 'https://issuer'},
|
||||
'device_code': {'device_code': 'D'},
|
||||
'token': {'access_token': 'A'},
|
||||
'raw_access_token': 'A-raw',
|
||||
'access_token': 'A-final',
|
||||
'token_error': {'error': None},
|
||||
'token_status': 1,
|
||||
'regions': [{'label': 'us-east-1'}],
|
||||
'project_id': 'proj-42',
|
||||
})
|
||||
|
||||
state = original.to_state()
|
||||
rebuilt = BigAnimalProvider.from_state(state)
|
||||
|
||||
self.assertEqual(rebuilt.provider, original.provider)
|
||||
self.assertEqual(rebuilt.device_code, original.device_code)
|
||||
self.assertEqual(rebuilt.token, original.token)
|
||||
self.assertEqual(rebuilt.raw_access_token, 'A-raw')
|
||||
self.assertEqual(rebuilt.access_token, 'A-final')
|
||||
self.assertEqual(rebuilt.token_error, {'error': None})
|
||||
self.assertEqual(rebuilt.token_status, 1)
|
||||
self.assertEqual(rebuilt.regions, [{'label': 'us-east-1'}])
|
||||
self.assertEqual(rebuilt.project_id, 'proj-42')
|
||||
|
||||
|
||||
class TestGetBigAnimalFromSession(
|
||||
_SkipServerSetUpMixin, BaseTestGenerator):
|
||||
"""Helper builds a BigAnimalProvider from session['biganimal']['state']."""
|
||||
|
||||
scenarios = [('default', dict())]
|
||||
|
||||
def runTest(self):
|
||||
from flask import Flask, session
|
||||
from pgadmin.misc.cloud.biganimal import (
|
||||
_get_biganimal_from_session, BigAnimalProvider)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'test'
|
||||
with app.test_request_context():
|
||||
session['biganimal'] = {
|
||||
'state': {
|
||||
'provider': {'clientId': 'X'},
|
||||
'access_token': 'tok',
|
||||
}
|
||||
}
|
||||
obj = _get_biganimal_from_session()
|
||||
self.assertIsInstance(obj, BigAnimalProvider)
|
||||
self.assertEqual(obj.provider, {'clientId': 'X'})
|
||||
self.assertEqual(obj.access_token, 'tok')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Negative tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGetBigAnimalFromSessionReturnsNoneWhenMissing(
|
||||
_SkipServerSetUpMixin, BaseTestGenerator):
|
||||
"""Helper returns None when no BigAnimal state in session."""
|
||||
|
||||
scenarios = [('default', dict())]
|
||||
|
||||
def runTest(self):
|
||||
from flask import Flask, session
|
||||
from pgadmin.misc.cloud.biganimal import (
|
||||
_get_biganimal_from_session)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'test'
|
||||
with app.test_request_context():
|
||||
self.assertIsNone(_get_biganimal_from_session())
|
||||
session['biganimal'] = {}
|
||||
self.assertIsNone(_get_biganimal_from_session())
|
||||
|
||||
|
||||
class TestFromStateUsesDefaultsForMissingFields(
|
||||
_SkipServerSetUpMixin, BaseTestGenerator):
|
||||
"""from_state uses sensible defaults for any field not present in
|
||||
state — supports incremental session population during the auth flow.
|
||||
"""
|
||||
|
||||
scenarios = [('default', dict())]
|
||||
|
||||
def runTest(self):
|
||||
from pgadmin.misc.cloud.biganimal import BigAnimalProvider
|
||||
|
||||
obj = BigAnimalProvider.from_state({
|
||||
'provider': {'clientId': 'X'},
|
||||
# everything else missing
|
||||
})
|
||||
self.assertEqual(obj.provider, {'clientId': 'X'})
|
||||
self.assertEqual(obj.device_code, {})
|
||||
self.assertEqual(obj.token, {})
|
||||
self.assertIsNone(obj.raw_access_token)
|
||||
self.assertIsNone(obj.access_token)
|
||||
self.assertEqual(obj.token_error, {})
|
||||
self.assertEqual(obj.token_status, -1)
|
||||
self.assertEqual(obj.regions, [])
|
||||
self.assertIsNone(obj.project_id)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUnsafeDeserializerEliminatedFromBigAnimalModule(
|
||||
_SkipServerSetUpMixin, BaseTestGenerator):
|
||||
"""cloud.biganimal must not import or call the unsafe deserializer."""
|
||||
|
||||
scenarios = [('default', dict())]
|
||||
|
||||
def runTest(self):
|
||||
import pgadmin.misc.cloud.biganimal as biganimal_mod
|
||||
import inspect
|
||||
src = inspect.getsource(biganimal_mod)
|
||||
forbidden = 'p' + 'i' + 'c' + 'k' + 'l' + 'e'
|
||||
self.assertNotIn(
|
||||
'import ' + forbidden, src,
|
||||
"cloud.biganimal must not import the unsafe deserializer")
|
||||
self.assertNotIn(
|
||||
forbidden + '.dumps(', src,
|
||||
"cloud.biganimal must not call the unsafe serialize call")
|
||||
self.assertNotIn(
|
||||
forbidden + '.loads(', src,
|
||||
"cloud.biganimal must not call the unsafe deserialize call")
|
||||
Reference in new issue
Block a user