Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT]: street_network backend configuration by user #4235

Merged
merged 8 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion source/jormungandr/jormungandr/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ def get_user(token, abort_if_no_token=True):
return g.user
else:
if not token:
# a token is mandatory for non public jormungandr
# a token is mandatory for non-public jormungandr
if not current_app.config.get('PUBLIC', False):
if abort_if_no_token:
flask_restful.abort(
Expand Down
48 changes: 43 additions & 5 deletions source/jormungandr/jormungandr/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,26 @@ def _get_models(self):
self.instance_db = instance_db
return self.instance_db

@memory_cache.memoize(app.config[str('MEMORY_CACHE_CONFIGURATION')].get(str('TIMEOUT_PARAMS'), 30))
@cache.memoize(app.config[str('CACHE_CONFIGURATION')].get(str('TIMEOUT_PARAMS'), 5 * 60))
def _get_sn_backend_for_user(self, user_id, mode):
if app.config['DISABLE_DATABASE']:
return None
if not can_connect_to_database():
return None
try:
backend_record = models.SnBackendAuthorization.get_backend(user_id, mode)
except Exception as e:
logging.getLogger(__name__).exception(
'No access to table sn_backend_authorization (error: {})'.format(e)
)
return None

if backend_record:
return backend_record.sn_backend_id

return None

def get_instance_scenario_name_or_default(self, default='distributed'):
instance_db = self.get_models()
return instance_db.scenario if instance_db else default
Expand Down Expand Up @@ -1009,14 +1029,27 @@ def init(self):
return False

def _get_street_network(self, mode, request):
"""

:param mode: fallback mode among ['bike', 'bss', 'car', 'car_no_park', 'ridesharing', 'taxi', 'walking']
:param request: This parameter in required only for file configuration.
:return: street_network backend connector for the mode
"""
if app.config[str('DISABLE_DATABASE')]:
return self._streetnetwork_backend_manager.get_street_network_legacy(self, mode, request)
else:
# We get the name of the column in the database corresponding to the mode used in the request
# And we get the value of this column for this instance
column_in_db = "street_network_{}".format(mode)
streetnetwork_backend_conf = getattr(self, column_in_db)
return self._streetnetwork_backend_manager.get_street_network_db(self, streetnetwork_backend_conf)
streetnetwork_backend_id = None
if hasattr(g, 'user') and g.user.has_sn_backend:
# We can call a function to get streetnetwork_backend_id if present in the table
# sn_backend_authorization
streetnetwork_backend_id = self._get_sn_backend_for_user(g.user.id, mode)

if streetnetwork_backend_id is None:
# We get the name of the column in the database corresponding to the mode used in the request
# And we get the value of this column for this instance
column_in_db = "street_network_{}".format(mode)
streetnetwork_backend_id = getattr(self, column_in_db)
return self._streetnetwork_backend_manager.get_street_network_db(self, streetnetwork_backend_id)

def get_street_network(self, mode, request):
if mode != fallback_modes.FallbackModes.car.name:
Expand All @@ -1029,6 +1062,11 @@ def get_street_network(self, mode, request):
)

def get_all_street_networks(self):
"""
Used only to display street_network backends in status.street_networks[]
:return: A list of street_network backends configured in the attributes of the form
instance.street_network_{<mode>} and present in the table streetnetwork_backend
"""
if app.config[str('DISABLE_DATABASE')]:
return self._streetnetwork_backend_manager.get_all_street_networks_legacy(self)
else:
Expand Down
12 changes: 0 additions & 12 deletions source/jormungandr/jormungandr/instance_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,6 @@ def stop(self):
if not self.thread_event.is_set():
self.thread_event.set()

def _get_authorized_instances(self, user, api):
authorized_instances = [
i
for name, i in self.instances.items()
if authentication.has_access(name, abort=False, user=user, api=api)
]

if not authorized_instances:
context = 'User has no access to any instance'
authentication.abort_request(user, context)
return authorized_instances

def _find_coverage_by_object_id_in_instances(self, instances, object_id):
# Request without coverage and coord (from or to)
# Get list of instances if the coordinate point exist in instance.geom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,22 @@ def __init__(self, sn_backends_getter=None, update_interval=60):
self._streetnetwork_backends_by_instance_legacy = defaultdict(
list
) # type: Dict[Instance, List[AbstractStreetNetworkService]]
# sn_backends_getter contains all the street_network backends in the table streetnetwork_backend
# with discarded = false (models.StreetNetworkBackend.all)
self._sn_backends_getter = sn_backends_getter
self._streetnetwork_backends = {} # type: Dict[str, AbstractStreetNetworkService]
self._streetnetwork_backends_last_update = {} # type: Dict[str, datetime]
self._last_update = datetime.datetime(1970, 1, 1)
self._update_interval = update_interval

def init_streetnetwork_backends_legacy(self, instance, instance_configuration):
"""
Initialize street_network backends from configuration files
This function is no more used except for debugging
:param instance:
:param instance_configuration:
:return:
"""
instance_configuration = self._append_default_street_network_to_config(instance_configuration)
self._create_street_network_backends(instance, instance_configuration)

Expand Down Expand Up @@ -121,6 +130,12 @@ def _create_backend_from_db(self, sn_backend, instance):

def _update_sn_backend(self, sn_backend, instance):
# type: (StreetNetworkBackend, Instance) -> None
"""
Updates street_network backend 'sn_backend' with last_update value for 'instance'.
:param sn_backend:
:param instance:
:return:
"""
self.logger.info('Updating / Adding {} streetnetwork backend'.format(sn_backend.id))
try:
self._streetnetwork_backends[sn_backend.id] = self._create_backend_from_db(sn_backend, instance)
Expand All @@ -134,7 +149,10 @@ def _can_connect_to_database(self):
def _update_config(self, instance):
# type: (Instance) -> None
"""
Update list of streetnetwork backends from db
Maintains and updates list of street_network backend connectors from the table streetnetwork_backend
with discarded=false after update interval or data update for instance.
:param instance: instance
:return: None
"""
if (
self._last_update + datetime.timedelta(seconds=self._update_interval) > datetime.datetime.utcnow()
Expand All @@ -152,6 +170,7 @@ def _update_config(self, instance):
self._last_update = datetime.datetime.utcnow()

try:
# Getting all the connectors from the table streetnetwork_backend with discarded=false
sn_backends = self._sn_backends_getter()
except Exception as e:
self.logger.exception('No access to table streetnetwork_backend (error: {})'.format(e))
Expand All @@ -173,16 +192,23 @@ def _update_config(self, instance):
):
self._update_sn_backend(sn_backend, instance)

def get_street_network_db(self, instance, streetnetwork_backend_conf):
def get_street_network_db(self, instance, streetnetwork_backend_id):
# type: (Instance, StreetNetworkBackend) -> Optional[AbstractStreetNetworkService]
# Make sure we update the streetnetwork_backends list from the database before returning them
"""
Gets the concerned street network service from the table streetnetwork_backend
with discarded=false and id = streetnetwork_backend_id
:param instance:
:param streetnetwork_backend_id:
:return:
"""
self._update_config(instance)

sn = self._streetnetwork_backends.get(streetnetwork_backend_conf, None)
sn = self._streetnetwork_backends.get(streetnetwork_backend_id, None)
if sn is None:
raise TechnicalError(
'impossible to find a streetnetwork module for instance {} with configuration {}'.format(
instance, streetnetwork_backend_conf
instance, streetnetwork_backend_id
)
)
return sn
Expand Down
26 changes: 26 additions & 0 deletions source/navitiacommon/navitiacommon/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ class User(db.Model, TimestampMixin): # type: ignore
billing_plan = db.relationship('BillingPlan', lazy='joined', cascade='save-update, merge')

keys = db.relationship('Key', backref='user', lazy='dynamic', cascade='save-update, merge, delete')
sn_backend_authorizations = db.relationship(
'SnBackendAuthorization', backref='user', lazy='dynamic', cascade='save-update, merge, delete'
)

authorizations = db.relationship(
'Authorization', backref='user', lazy='joined', cascade='save-update, merge, delete'
Expand All @@ -144,6 +147,10 @@ class User(db.Model, TimestampMixin): # type: ignore
server_default="{" + ", ".join(DEFAULT_SHAPE_SCOPE) + "}",
)

# Add an attributes to inform that this user contains streetnetwork_backend configuration
# False by default and will be updated on each action on the table sn_backend_authorization
has_sn_backend = db.Column(db.Boolean, nullable=False, default=False)

def __init__(self, login=None, email=None, block_until=None, keys=None, authorizations=None):
self.login = login
self.email = email
Expand Down Expand Up @@ -998,6 +1005,25 @@ def __repr__(self):
return '<Authorization %r-%r-%r>' % (self.user_id, self.instance_id, self.api_id)


class SnBackendAuthorization(db.Model, TimestampMixin): # type: ignore
# Unicity on user_id and mode: only one sn_backend for a user_id and mode
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True, nullable=False)
sn_backend_id = db.Column(db.Text, db.ForeignKey('streetnetwork_backend.id'), nullable=False)
mode = db.Column(db.Text, primary_key=True, nullable=False)

def __init__(self, user_id=None, sn_backend_id=None, mode=None):
self.user_id = user_id
self.sn_backend_id = sn_backend_id
self.mode = mode

def __repr__(self):
return '<SnBackendAuthorization %r-%r-%r>' % (self.user_id, self.sn_backend_id, self.mode)

@classmethod
def get_backend(cls, user_id, mode):
return cls.query.filter_by(user_id=user_id, mode=mode).first()


class Job(db.Model, TimestampMixin): # type: ignore
id = db.Column(db.Integer, primary_key=True)
task_uuid = db.Column(db.Text)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Add table sn_backend_authorization

Revision ID: d4b48ec451ed
Revises: e3e7d4fdee23
Create Date: 2024-03-05 18:02:08.524832

"""

# revision identifiers, used by Alembic.
revision = 'd4b48ec451ed'
down_revision = 'e3e7d4fdee23'

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'sn_backend_authorization',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('sn_backend_id', sa.Text(), nullable=False),
sa.Column('mode', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['sn_backend_id'], ['streetnetwork_backend.id']),
sa.ForeignKeyConstraint(['user_id'], ['user.id']),
sa.PrimaryKeyConstraint('user_id', 'mode'),
)

op.add_column('user', sa.Column('has_sn_backend', sa.Boolean(), server_default='False', nullable=False))


def downgrade():
op.drop_table('sn_backend_authorization')

op.drop_column('user', 'has_sn_backend')
104 changes: 104 additions & 0 deletions source/tyr/tests/integration/users_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from tyr.rabbit_mq_handler import RabbitMqHandler
from tyr import app
from six.moves.urllib.parse import quote
import ujson


@pytest.fixture
Expand Down Expand Up @@ -908,3 +909,106 @@ def test_filter_users_by_key(create_user, create_multiple_users):
token = resp_user['keys'][0]['token']
resp_token = api_get('/v0/users?key={}'.format(token))
assert resp_token['id'] == user['id']


def test_streetnetwork_backend_authorization_for_a_user(create_user, create_multiple_users):
resp_users = api_get('/v1/users')
assert len(resp_users['users']) == 3
user_id = resp_users['users'][0]['id']
assert user_id == 47

# No street network backend is added to the user yet.
assert resp_users['users'][0]['has_sn_backend'] is False

# Add three streetnetwork backends for the test
new_backend = {'klass': 'valhalla.klass'}
resp, status = api_post(
'v0/streetnetwork_backends/valhallaDev',
data=ujson.dumps(new_backend),
content_type='application/json',
check=False,
)
assert status == 201
assert resp['streetnetwork_backend']['id'] == "valhallaDev"

new_backend = {'klass': 'asgard.klass'}
resp, status = api_post(
'v0/streetnetwork_backends/asgardDev',
data=ujson.dumps(new_backend),
content_type='application/json',
check=False,
)
assert status == 201
assert resp['streetnetwork_backend']['id'] == "asgardDev"

# Add sn_backend_authorization for the user
new_obj = {'sn_backend_id': 'valhallaDev', 'mode': 'walking'}
resp, status = api_post(
'v1/users/{}/sn_backend_authorizations'.format(user_id),
data=ujson.dumps(new_obj),
content_type='application/json',
check=False,
)
assert status == 201
assert resp['sn_backend_id'] == 'valhallaDev'
assert resp['mode'] == 'walking'
assert resp['user_id'] == user_id

# We cannot add another sn_backend for the same user + mode
new_obj = {'sn_backend_id': 'asgardDev', 'mode': 'walking'}
resp, status = api_post(
'v1/users/{}/sn_backend_authorizations'.format(user_id),
data=ujson.dumps(new_obj),
content_type='application/json',
check=False,
)
assert status == 409
assert 'duplicate key value' in resp['error']

new_obj = {'sn_backend_id': 'asgardDev', 'mode': 'car'}
resp, status = api_post(
'v1/users/{}/sn_backend_authorizations'.format(user_id),
data=ujson.dumps(new_obj),
content_type='application/json',
check=False,
)
assert status == 201
assert resp['sn_backend_id'] == 'asgardDev'
assert resp['mode'] == 'car'
assert resp['user_id'] == user_id

# # Calling api /users/user_id/sn_backend_authorizations to verify street_network backends
resp, status = api_get('/v1/users/{}/sn_backend_authorizations'.format(user_id), check=False)
assert status == 200
assert len(resp['sn_backend_authorizations']) == 2

# Calling /user/user_id to verify that has_sn_backend is True and street_network backends
resp = api_get('/v1/users/{}'.format(user_id))
assert resp['users']['id'] == user_id
assert resp['users']['has_sn_backend'] is True
assert len(resp['users']['sn_backend_authorizations']) == 2

# Testing api delete()
resp, status = api_delete(
"/v1/users/{}/sn_backend_authorizations?mode={}".format(user_id, "car"),
check=False,
no_json=True,
)
assert status == 204
resp, status = api_delete(
"/v1/users/{}/sn_backend_authorizations?mode={}".format(user_id, "walking"),
check=False,
no_json=True,
)
assert status == 204

# Calling api /users/user_id/sn_backend_authorizations to verify that sn_backend is absent
resp, status = api_get('/v1/users/{}/sn_backend_authorizations'.format(user_id), check=False)
assert status == 200
assert len(resp['sn_backend_authorizations']) == 0

# Calling /user/user_id to verify that has_sn_backend is False and sn_backend is absent
resp = api_get('/v1/users/{}'.format(user_id))
assert resp['users']['id'] == user_id
assert resp['users']['has_sn_backend'] is False
assert len(resp['users']['sn_backend_authorizations']) == 0
2 changes: 2 additions & 0 deletions source/tyr/tyr/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@

api.add_resource(resources.Authorization, '/v<int:version>/users/<int:user_id>/authorizations/')

api.add_resource(resources.SnBackendAuthorization, '/v1/users/<int:user_id>/sn_backend_authorizations/')

api.add_resource(resources.Index, '/')

api.add_resource(resources.Status, '/v0/status')
Expand Down
Loading
Loading