From 8aa187a92d1c8e1008c4fbe12812fe52f4a9859d Mon Sep 17 00:00:00 2001 From: "M. David Bennett" Date: Thu, 23 Jan 2020 16:57:41 -0600 Subject: [PATCH] Uri prefix support (#247) Added support for specifying a URI prefix for cases when Opsy is behind a reverse proxy. Added _links references to auth schema, seems this was forgotten about when the auth API was created. Updated tests to reflect this. Signed-off-by: M. David Bennett --- opsy.toml.example | 6 ++++++ opsy/app.py | 17 ++++++++++++++++ opsy/auth/schema.py | 25 +++++++++++++++++++---- opsy/config.py | 1 + opsy/flask_extensions.py | 3 ++- scripts/entrypoint.sh | 1 + tests/schema/test_auth.py | 43 +++++++++++++++++++++++++++++++-------- 7 files changed, 82 insertions(+), 14 deletions(-) diff --git a/opsy.toml.example b/opsy.toml.example index 0eb31bc..8dca720 100644 --- a/opsy.toml.example +++ b/opsy.toml.example @@ -13,6 +13,12 @@ database_uri = 'sqlite:///../opsy.db' # Required: true secret_key = 'this is a secret' +# This is used if Opsy is behind a reverse proxy. It should be set to where +# you have it mounted, like '/opsy' for example. +# Required: false +# Default value: / +uri_prefix = '/' + [server] # This section contains configuration for the embedded WSGI server. diff --git a/opsy/app.py b/opsy/app.py index 5049926..aaf1fbe 100644 --- a/opsy/app.py +++ b/opsy/app.py @@ -7,9 +7,26 @@ from opsy.inventory.views import create_inventory_views +class PrefixMiddleware: + + def __init__(self, app, prefix=''): + self.app = app + self.prefix = prefix + + def __call__(self, environ, start_response): + if environ['PATH_INFO'].startswith(self.prefix): + environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):] + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + start_response('404', [('Content-Type', 'text/plain')]) + return ["This url does not belong to the app.".encode()] + + def create_app(config): configure_logging(config) app = Flask('opsy') + app.wsgi_app = PrefixMiddleware( + app.wsgi_app, prefix=config['app']['uri_prefix']) app.before_request(log_before_request) app.after_request(log_after_request) configure_app(app, config) diff --git a/opsy/auth/schema.py b/opsy/auth/schema.py index 443c7cf..1fa8e99 100644 --- a/opsy/auth/schema.py +++ b/opsy/auth/schema.py @@ -3,7 +3,7 @@ from marshmallow_sqlalchemy import field_for from opsy.auth.models import User, Role, Permission from opsy.flask_extensions import ma -from opsy.schema import BaseSchema +from opsy.schema import BaseSchema, Hyperlinks ############################################################################### # Non-sqlalchemy schemas @@ -38,7 +38,7 @@ class UserSchema(BaseSchema): class Meta: model = User fields = ('id', 'name', 'full_name', 'email', 'enabled', 'ldap_user', - 'created_at', 'updated_at', 'roles', 'permissions') + 'created_at', 'updated_at', 'roles', 'permissions', '_links') ordered = True unknown = RAISE @@ -53,6 +53,11 @@ class Meta: 'RolePermissionRefSchema', many=True, dump_only=True) roles = ma.Nested( # pylint: disable=no-member 'RoleRefSchema', many=True, dump_only=True) + _links = Hyperlinks( + {"self": ma.URLFor("auth_users.users_get", id_or_name=""), + "collection": ma.URLFor("auth_users.users_list")}, + dump_only=True + ) class UserCreateSchema(UserSchema): @@ -147,7 +152,7 @@ class RoleSchema(BaseSchema): class Meta: model = Role fields = ('id', 'name', 'ldap_group', 'description', 'created_at', - 'updated_at', 'permissions', 'users') + 'updated_at', 'permissions', 'users', '_links') ordered = True unknown = RAISE @@ -161,6 +166,11 @@ class Meta: users = ma.Nested( # pylint: disable=no-member 'UserRefSchema', many=True, dump_only=True) + _links = Hyperlinks( + {"self": ma.URLFor("auth_roles.roles_get", id_or_name=""), + "collection": ma.URLFor("auth_roles.roles_list")}, + dump_only=True) + class RoleUpdateSchema(RoleSchema): @@ -203,7 +213,7 @@ class RolePermissionSchema(BaseSchema): class Meta: model = Permission - fields = ('id', 'role', 'name', 'created_at', 'updated_at') + fields = ('id', 'role', 'name', 'created_at', 'updated_at', '_links') ordered = True id = field_for(Permission, 'id', dump_only=True) @@ -214,6 +224,13 @@ class Meta: role = ma.Nested( # pylint: disable=no-member 'RoleRefSchema', dump_only=True) + _links = Hyperlinks({ + "self": ma.URLFor("auth_roles.role_permissions_get", + id_or_name="", + permission_id_or_name=""), + "collection": ma.URLFor("auth_roles.role_permissions_list", + id_or_name="")}, dump_only=True) + class RolePermissionUpdateSchema(BaseSchema): diff --git a/opsy/config.py b/opsy/config.py index 10a011d..3be4d51 100644 --- a/opsy/config.py +++ b/opsy/config.py @@ -8,6 +8,7 @@ class ConfigAppSchema(Schema): database_uri = fields.Str(required=True) secret_key = fields.Str(required=True) + uri_prefix = fields.Str(missing='/') class ConfigAuthSchema(Schema): diff --git a/opsy/flask_extensions.py b/opsy/flask_extensions.py index 85a0404..5aa0db4 100644 --- a/opsy/flask_extensions.py +++ b/opsy/flask_extensions.py @@ -42,7 +42,8 @@ def configure_extensions(app): version='v1', openapi_version='2.0', info={'description': "It's Opsy!"}, - plugins=[MarshmallowPlugin()] + plugins=[MarshmallowPlugin()], + basePath=app.config.opsy['app']['uri_prefix'] ), 'APISPEC_SWAGGER_URL': '/docs/swagger.json', 'APISPEC_SWAGGER_UI_URL': '/docs/', diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 84f27c0..833ff82 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -11,6 +11,7 @@ cat > ${OPSY_CONFIG} <<__EOF__ [app] database_uri = '${OPSY_DATABASE_URI}' secret_key = '${OPSY_SECRET_KEY}' +uri_prefix = '${OPSY_URI_PREFIX:-/}' [server] host = '0.0.0.0' diff --git a/tests/schema/test_auth.py b/tests/schema/test_auth.py index 96b423b..f3b5678 100644 --- a/tests/schema/test_auth.py +++ b/tests/schema/test_auth.py @@ -70,11 +70,23 @@ def test_user_schema(test_user): ('created_at', test_user.created_at.isoformat()), ('updated_at', test_user.updated_at.isoformat()), ('roles', [ - OrderedDict([('id', x.id), ('name', x.name)]) + OrderedDict([ + ('id', x.id), + ('name', x.name), + ('_links', { + 'self': f'/api/v1/roles/{x.id}', + 'collection': '/api/v1/roles/'})]) for x in test_user.roles]), - ('permissions', [ - OrderedDict([('id', x.id), ('name', x.name)]) - for x in test_user.permissions])]) + ('permissions', [OrderedDict([ + ('id', x.id), + ('name', x.name), + ('_links', { + 'self': f'/api/v1/roles/{x.role.id}/permissions/{x.id}', + 'collection': f'/api/v1/roles/{x.role.id}/permissions/'})]) + for x in test_user.permissions]), + ('_links', { + 'self': f'/api/v1/users/{test_user.id}', + 'collection': '/api/v1/users/'})]) assert UserSchema().dump(test_user) == expected_user_schema_output @@ -114,10 +126,17 @@ def test_role_schema(test_role): ('description', test_role.description), ('created_at', test_role.created_at.isoformat()), ('updated_at', test_role.updated_at.isoformat()), - ('permissions', [ - OrderedDict([('id', x.id), ('name', x.name)]) + ('permissions', [OrderedDict([ + ('id', x.id), + ('name', x.name), + ('_links', { + 'self': f'/api/v1/roles/{x.role.id}/permissions/{x.id}', + 'collection': f'/api/v1/roles/{x.role.id}/permissions/'})]) for x in test_role.permissions]), - ('users', test_role.users)]) + ('users', test_role.users), + ('_links', { + 'self': f'/api/v1/roles/{test_role.id}', + 'collection': '/api/v1/roles/'})]) assert RoleSchema().dump(test_role) == expected_role_schema_output @@ -135,10 +154,16 @@ def test_role_permission_schema(admin_user): ('id', test_perm.id), ('role', OrderedDict([ ('id', test_perm.role.id), - ('name', test_perm.role.name)])), + ('name', test_perm.role.name), + ('_links', { + 'self': f'/api/v1/roles/{test_perm.role.id}', + 'collection': '/api/v1/roles/'})])), ('name', test_perm.name), ('created_at', test_perm.created_at.isoformat()), - ('updated_at', test_perm.updated_at.isoformat())]) + ('updated_at', test_perm.updated_at.isoformat()), + ('_links', { + 'self': f'/api/v1/roles/{test_perm.role.id}/permissions/{test_perm.id}', + 'collection': f'/api/v1/roles/{test_perm.role.id}/permissions/'})]) assert RolePermissionSchema().dump(test_perm) \ == expected_role_permission_schema_output