From 279a66fd52ff06e5c09c0e5bda562b443040d456 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Tue, 21 Jul 2020 18:00:48 -0400 Subject: [PATCH 001/136] improve usage docs and guides in API swagger --- README.rst | 4 ++-- docs/configuration.rst | 18 +++++++++--------- docs/usage.rst | 33 +++++++++++++++++++++++++++++---- magpie/api/schemas.py | 24 +++++++++++++++++++++++- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index b988408dc..b64159f72 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Magpie (the smart-bird) *a very smart bird who knows everything about you.* Magpie is service for AuthN/AuthZ accessible via a `REST API`_ implemented with the `Pyramid`_ web framework. -It allows you to manage User/Group/Service/Resource/Permission with a `Postgres`_ database. +It allows you to manage User/Group/Service/Resource/Permission with a `PostgreSQL`_ database. Behind the scene, it uses `Ziggurat-Foundations`_ and `Authomatic`_. @@ -128,7 +128,7 @@ Following most recent variants are available: .. REST API redoc reference is auto-generated by sphinx from magpie cornice-swagger definitions .. _REST API: https://pavics-magpie.readthedocs.io/en/latest/api.html .. _Authomatic: https://authomatic.github.io/authomatic/ -.. _Postgres: https://www.postgresql.org/ +.. _PostgreSQL: https://www.postgresql.org/ .. _Pyramid: https://docs.pylonsproject.org/projects/pyramid/ .. _Ziggurat-Foundations: https://github.com/ergo/ziggurat_foundations .. _Magpie Docker Images: https://hub.docker.com/r/pavics/magpie/tags diff --git a/docs/configuration.rst b/docs/configuration.rst index f9df6476b..001743820 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -4,7 +4,7 @@ Configuration ============= At startup, `Magpie` application will load multiple configuration files to define various behaviours or setup -operations. These are defined though the following configuration settings presented below. +operations. These are defined though the configuration settings presented below. All generic `Magpie` configuration settings can be defined through either the `magpie.ini`_ file or environment variables. Values defined in `magpie.ini`_ are expected to follow the @@ -388,7 +388,7 @@ Following settings define parameters required by `Twitcher`_ (OWS Security Proxy Postgres Settings ~~~~~~~~~~~~~~~~~~~~~ -Following settings define parameters required to define the `Postgres`_ database connection employed by `Magpie` as +Following settings define parameters required to define the `PostgreSQL`_ database connection employed by `Magpie` as well as some other database-related operation settings. Settings defined by ``magpie.[variable_name]`` definitions are available as described at the start of the `Configuration`_ section, as well as some special cases where additional configuration names are supported where mentioned. @@ -404,12 +404,12 @@ configuration names are supported where mentioned. - | ``MAGPIE_DB_URL`` | Full database connection URL formatted as ``://:@:/``. | Please refer to `SQLAlchemy Engine`_'s documentation for supported database implementations and their corresponding - configuration. Only `Postgres`_ has been extensively tested with `Magpie`, but other variants should be applicable. + configuration. Only `PostgreSQL`_ has been extensively tested with `Magpie`, but other variants should be applicable. | (Default: infer ``postgresql`` database connection URL formed using below ``MAGPIE_POSTGRES_<>`` parameters if the value was not explicitly provided) - | ``MAGPIE_POSTGRES_USERNAME`` - | Database connection username to retrieve `Magpie` data stored in `Postgres`_. + | Database connection username to retrieve `Magpie` data stored in `PostgreSQL`_. | On top of ``MAGPIE_POSTGRES_USERNAME``, environment variable ``POSTGRES_USERNAME`` and setting ``postgres.username`` are also supported. For backward compatibility, all above variants with ``user`` instead of ``username`` (with corresponding lower/upper case) are also verified for potential configuration if no prior parameter was @@ -418,30 +418,30 @@ configuration names are supported where mentioned. | (Default: ``"magpie"``) - | ``MAGPIE_POSTGRES_PASSWORD`` - | Database connection password to retrieve `Magpie` data stored in `Postgres`_. + | Database connection password to retrieve `Magpie` data stored in `PostgreSQL`_. | Environment variable ``POSTGRES_PASSWORD`` and setting ``postgres.password`` are also supported if not previously identified by their `Magpie`-prefixed variants. | (Default: ``"qwerty"``) - | ``MAGPIE_POSTGRES_HOST`` - | Database connection host location to retrieve `Magpie` data stored in `Postgres`_. + | Database connection host location to retrieve `Magpie` data stored in `PostgreSQL`_. | Environment variable ``POSTGRES_HOST`` and setting ``postgres.host`` are also supported if not previously identified by their `Magpie`-prefixed variants. | (Default: ``"postgres"``) - | ``MAGPIE_POSTGRES_PORT`` - | Database connection port to retrieve `Magpie` data stored in `Postgres`_. + | Database connection port to retrieve `Magpie` data stored in `PostgreSQL`_. | Environment variable ``POSTGRES_PORT`` and setting ``postgres.port`` are also supported if not previously identified by their `Magpie`-prefixed variants. | (Default: ``5432``) - | ``MAGPIE_POSTGRES_DB`` - | Name of the database located at the specified connection to retrieve `Magpie` data stored in `Postgres`_. + | Name of the database located at the specified connection to retrieve `Magpie` data stored in `PostgreSQL`_. | Environment variable ``POSTGRES_DB`` and setting ``postgres.db``, as well as the same variants with ``database`` instead of ``db``, are also supported if not previously identified by their `Magpie`-prefixed variants. | (Default: ``"magpie"``) -.. _Postgres: https://www.postgresql.org/ +.. _PostgreSQL: https://www.postgresql.org/ .. _Alembic: https://alembic.sqlalchemy.org/ .. _SQLAlchemy Engine: https://docs.sqlalchemy.org/en/13/core/engines.html diff --git a/docs/usage.rst b/docs/usage.rst index 3ba6af5c0..d4f45887b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,15 +7,40 @@ Usage Package ~~~~~~~ -To use Magpie in a project, fist you need to install it. To do so, you can do a basic ``pip install``. -For more details or other installation variants and preparation, see `installation`_ and +To use `Magpie` in a project, first you need to install it. To do so, you can do a basic ``pip install``. +For more details or other installation variants and environment preparation, see `installation`_ and `configuration`_ procedures. -Then simply import the Python package:: +After this, you should be able to import the Python package to validate it is installed properly using:: import magpie +Web Application +~~~~~~~~~~~~~~~~~~~~~ + +In most situation, you will want to run `Magpie` as a Web Application in combination with some Web Proxy +(e.g.: `Twitcher`_) that can interrogate `Magpie` about applicable user authentication and permission authorization +from the HTTP request session. To start the application, you can simply run the following command:: + + make start + +This will first install any missing dependencies in the current environment (see `installation`_), and will after start +a basic Web Application on ``localhost:2001`` with default configurations. Please note that you **MUST** have a +`PostgreSQL`_ database connection configured prior to running `Magpie` for it to operate (refer to `Configuration`_ +for details). + +For running the application, multiple +`WSGI HTTP Servers` can be employed (e.g.: `Gunicorn`_, `Waitress`_, etc.). They usually all support as input an INI +configuration file for specific settings. `Magpie` also employs such INI file to customize its behaviour. +See `Configuration`_ for further details, and please refer to the employed `WSGI` application documentation of your +liking for their respective setup requirements. + +.. _Gunicorn: https://gunicorn.org/ +.. _PostgreSQL: https://www.postgresql.org/ +.. _Twitcher: https://github.com/bird-house/twitcher +.. _Waitress: https://github.com/Pylons/waitress + API ~~~~~~~ @@ -34,7 +59,7 @@ administrator permissions. Additional Utilities ~~~~~~~~~~~~~~~~~~~~ -Multiple `utilities`_ are provided either directly within `Magpie` or through external resources. +Multiple `utilities`_ are provided either directly within `Magpie` as a package or through external resources. Please refer to this section for more details. .. _configuration: configuration.rst diff --git a/magpie/api/schemas.py b/magpie/api/schemas.py index 99588dc42..48e2b7b4b 100644 --- a/magpie/api/schemas.py +++ b/magpie/api/schemas.py @@ -79,7 +79,8 @@ def service_api_route_info(service_api): return {"name": service_api.name, "pattern": service_api.path} -LoggedUserBase = "/users/{}".format(get_constant("MAGPIE_LOGGED_USER")) +_LOGGED_USER_VALUE = get_constant("MAGPIE_LOGGED_USER") +LoggedUserBase = "/users/{}".format(_LOGGED_USER_VALUE) SwaggerGenerator = Service( @@ -283,6 +284,25 @@ def service_api_route_info(service_api): name="homepage") +TAG_DESCRIPTIONS = { + APITag: "General information about the API.", + LoginTag: "Session login management and available providers for authentification.", + UsersTag: + "Users information management and control of their applicable groups, services, resources and permissions.\n\n" + "Administrator-level permissions are required to access most paths. Depending on context, some paths are " + "permitted additional access if the logged session user corresponds to the path variable user.", + LoggedUserTag: + "Utility paths that correspond to their {} counterparts, but that automatically ".format(UserAPI.path) + + "determine the applicable user from the logged session. If there is no active session, the public anonymous " + "access is employed.\n\nNOTE: The value of '{}' depends on Magpie configuration.".format(_LOGGED_USER_VALUE), + GroupsTag: + "Groups management and control of their applicable users, services, resources and permissions.\n\n" + "Administrator-level permissions are required to access most paths. ", + ResourcesTag: "Management of resources that reside under a given service and their applicable permissions.", + ServicesTag: "Management of service definitions, children resources and their applicable permissions.", +} + + # Common path parameters GroupNameParameter = colander.SchemaNode( colander.String(), @@ -2975,6 +2995,8 @@ def generate_api_schema(swagger_base_spec): swagger_base_spec.update(SecurityDefinitionsAPI) generator.swagger = swagger_base_spec json_api_spec = generator.generate(title=TitleAPI, version=__meta__.__version__, info=InfoAPI) + for tag in json_api_spec["tags"]: + tag["description"] = TAG_DESCRIPTIONS[tag["name"]] return json_api_spec From e5229a9fa1e1ac31c4bd46cd47ecad42eb941d8c Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Tue, 21 Jul 2020 22:08:36 -0400 Subject: [PATCH 002/136] [wip] user info self-update --- magpie/api/requests.py | 67 ++++++++++++-- magpie/typedefs.py | 3 +- tests/interfaces.py | 188 ++++++++++++++++++++++++++++++++++++++- tests/runner.py | 1 + tests/test_magpie_api.py | 26 +++++- tests/utils.py | 24 +++-- 6 files changed, 287 insertions(+), 22 deletions(-) diff --git a/magpie/api/requests.py b/magpie/api/requests.py index 520cde36c..fb5a09b6c 100644 --- a/magpie/api/requests.py +++ b/magpie/api/requests.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: # pylint: disable=W0611,unused-import from pyramid.request import Request - from magpie.typedefs import Any, Str, Optional, ServiceOrResourceType # noqa: F401 + from magpie.typedefs import Any, AnySettingsContainer, Str, Optional, ServiceOrResourceType # noqa: F401 from magpie.permissions import Permission # noqa: F401 @@ -34,7 +34,7 @@ def get_request_method_content(request): def get_multiformat_any(request, key, default=None): # type: (Request, Str, Optional[Any]) -> Any """ - Obtains the ``key`` element from the request body using found `Content-Type` header. + Obtains the :paramref:`key` element from the request body using found `Content-Type` header. """ msg = "Key '{key}' could not be extracted from '{method}' of type '{type}'" \ .format(key=repr(key), method=request.method, type=request.content_type) @@ -80,14 +80,23 @@ def get_value_multiformat_post_checked(request, key, default=None): def get_user(request, user_name_or_token=None): # type: (Request, Optional[Str]) -> models.User - logged_user_name = get_constant("MAGPIE_LOGGED_USER") + """ + Obtains the user corresponding to the provided user-name, token or via lookup of the logged user request session. + + :param request: request from which to obtain application settings and session user as applicable. + :param user_name_or_token: reference value to employ for lookup of the user. + :returns: found user. + :raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request. + :raises HTTPNotFound: if the specified user name or token does not correspond to any existing user. + """ + logged_user_name = get_constant("MAGPIE_LOGGED_USER", settings_container=request) if user_name_or_token is None: user_name_or_token = logged_user_name if user_name_or_token == logged_user_name: curr_user = request.user if curr_user: return curr_user - anonymous_user = get_constant("MAGPIE_ANONYMOUS_USER") + anonymous_user = get_constant("MAGPIE_ANONYMOUS_USER", settings_container=request) anonymous = evaluate_call(lambda: UserService.by_user_name(anonymous_user, db_session=request.db), fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, msg_on_fail=s.User_CheckAnonymous_ForbiddenResponseSchema.description) @@ -97,7 +106,8 @@ def get_user(request, user_name_or_token=None): authn_policy = request.registry.queryUtility(IAuthenticationPolicy) principals = authn_policy.effective_principals(request) - admin_group = GroupService.by_group_name(get_constant("MAGPIE_ADMIN_GROUP"), db_session=request.db) + admin_group_name = get_constant("MAGPIE_ADMIN_GROUP", settings_container=request) + admin_group = GroupService.by_group_name(admin_group_name, db_session=request.db) admin_principal = "group:{}".format(admin_group.id) if admin_principal not in principals: raise HTTPForbidden() @@ -110,7 +120,8 @@ def get_user(request, user_name_or_token=None): def get_user_matchdict_checked_or_logged(request, user_name_key="user_name"): - logged_user_name = get_constant("MAGPIE_LOGGED_USER") + # type: (Request, Str) -> models.User + logged_user_name = get_constant("MAGPIE_LOGGED_USER", settings_container=request) logged_user_path = s.UserAPI.path.replace("{" + user_name_key + "}", logged_user_name) if user_name_key not in request.matchdict and request.path_info.startswith(logged_user_path): return get_user(request, logged_user_name) @@ -118,11 +129,25 @@ def get_user_matchdict_checked_or_logged(request, user_name_key="user_name"): def get_user_matchdict_checked(request, user_name_key="user_name"): + # type: (Request, Str) -> models.User + """Obtains the user matched against the specified request path variable. + + :returns: found user. + :raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request. + :raises HTTPNotFound: if the specified user name or token does not correspond to any existing user. + """ user_name = get_value_matchdict_checked(request, user_name_key) return get_user(request, user_name) def get_group_matchdict_checked(request, group_name_key="group_name"): + # type: (Request, Str) -> models.Group + """Obtains the group matched against the specified request path variable. + + :returns: found group. + :raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request. + :raises HTTPNotFound: if the specified group name does not correspond to any existing group. + """ group_name = get_value_matchdict_checked(request, group_name_key) group = evaluate_call(lambda: GroupService.by_group_name(group_name, db_session=request.db), fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, @@ -134,6 +159,13 @@ def get_group_matchdict_checked(request, group_name_key="group_name"): def get_resource_matchdict_checked(request, resource_name_key="resource_id"): # type: (Request, Str) -> models.Resource + """ + Obtains the resource matched against the specified request path variable. + + :returns: found resource. + :raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request. + :raises HTTPNotFound: if the specified resource ID does not correspond to any existing resource. + """ resource_id = get_value_matchdict_checked(request, resource_name_key) resource_id = evaluate_call(lambda: int(resource_id), http_error=HTTPBadRequest, msg_on_fail=s.Resource_MatchDictCheck_BadRequestResponseSchema.description) @@ -146,6 +178,14 @@ def get_resource_matchdict_checked(request, resource_name_key="resource_id"): def get_service_matchdict_checked(request, service_name_key="service_name"): + # type: (Request, Str) -> models.Service + """ + Obtains the service matched against the specified request path variable. + + :returns: found service. + :raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request. + :raises HTTPNotFound: if the specified service name does not correspond to any existing service. + """ service_name = get_value_matchdict_checked(request, service_name_key) service = evaluate_call(lambda: models.Service.by_service_name(service_name, db_session=request.db), fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, @@ -158,10 +198,10 @@ def get_service_matchdict_checked(request, service_name_key="service_name"): def get_permission_matchdict_checked(request, service_or_resource, permission_name_key="permission_name"): # type: (Request, models.Resource, Str) -> Permission """ - Obtains the `permission` specified in the ``request`` path and validates that it is allowed for the specified - ``service_or_resource`` which can be a `service` or a children `resource`. + Obtains the permission specified in the request path variable and validates that it is allowed for the specified + :paramref:`service_or_resource` which can be a `service` or a children `resource`. - Allowed permissions correspond to the direct `service` permissions or restrained permissions of the `resource` + Allowed permissions correspond to the *direct* `service` permissions or restrained permissions of the `resource` under its root `service`. :returns: found permission name if valid for the service/resource @@ -173,6 +213,15 @@ def get_permission_matchdict_checked(request, service_or_resource, permission_na def get_value_matchdict_checked(request, key): + # type: (Request, Str) -> Str + """ + Obtains the matched value located at the expected position of the specified path variable. + + :param request: request from which to retrieve the key. + :param key: path variable key. + :return: matched path variable value. + :raises HTTPUnprocessableEntity: if the key is not an applicable path variable for this request. + """ val = request.matchdict.get(key) verify_param(val, not_none=True, not_empty=True, http_error=HTTPUnprocessableEntity, param_name=key, msg_on_fail=s.UnprocessableEntityResponseSchema.description) diff --git a/magpie/typedefs.py b/magpie/typedefs.py index 3cc8fae0c..6adc7fb4b 100644 --- a/magpie/typedefs.py +++ b/magpie/typedefs.py @@ -41,10 +41,11 @@ ParamsType = Dict[Str, Any] CookiesType = Union[Dict[Str, Str], List[Tuple[Str, Str]]] HeadersType = Union[Dict[Str, Str], List[Tuple[Str, Str]]] - OptionalHeaderCookiesType = Union[Tuple[None, None], Tuple[HeadersType, CookiesType]] AnyHeadersType = Union[HeadersType, ResponseHeaders, EnvironHeaders, CaseInsensitiveDict] + AnyCookiesType = Union[CookiesType, RequestsCookieJar] AnyResponseType = Union[WebobResponse, PyramidResponse, TestResponse] CookiesOrSessionType = Union[RequestsCookieJar, Session] + OptionalHeaderCookiesType = Tuple[Optional[AnyHeadersType], Optional[AnyCookiesType]] AnyKey = Union[Str, int] AnyValue = Union[Str, Number, bool, None] diff --git a/tests/interfaces.py b/tests/interfaces.py index 8af0f626e..a04affb4d 100644 --- a/tests/interfaces.py +++ b/tests/interfaces.py @@ -90,6 +90,7 @@ def test_GetVersion(self): utils.check_val_equal(len(version_parts), 3) @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED def test_GetCurrentUser(self): logged_user = get_constant("MAGPIE_LOGGED_USER") resp = utils.test_request(self, "GET", "/users/{}".format(logged_user), headers=self.json_headers) @@ -107,8 +108,6 @@ def test_NotAcceptableRequest(self): utils.check_response_basic_info(resp, expected_code=406) -@unittest.skip("Not implemented.") -@pytest.mark.skip(reason="Not implemented.") @runner.MAGPIE_TEST_API class Interface_MagpieAPI_UsersAuth(six.with_metaclass(ABCMeta, Base_Magpie_TestCase)): # pylint: disable=C0103,invalid-name @@ -122,6 +121,186 @@ class Interface_MagpieAPI_UsersAuth(six.with_metaclass(ABCMeta, Base_Magpie_Test def setUpClass(cls): raise NotImplementedError + @classmethod + def login_test_user(cls): + """ + Obtain headers and cookies with session credentials of the test user (non administrator). + + .. warning:: + Must ensure that administrator user (for setup operations) is logged out completely to avoid invalid tests. + This is particularly important in the case of local TestApp that can store the session for one active user. + """ + raise NotImplementedError + + def tearDown(self): + self.check_requirements() # re-login as required in case test logged out the user with permissions + utils.TestSetup.delete_TestUser(self) + utils.TestSetup.delete_TestUser(self, override_user_name=self.other_user_name) + utils.TestSetup.delete_TestGroup(self) + + def run_PutUsers_username_update_itself(self, user_path_variable): + """ + Session user is allowed to update its own information via logged user path or corresponding user-name path. + + .. seealso:: + - :meth:`Interface_MagpieAPI_AdminAuth.test_PutUser_ReservedKeyword_Current` + """ + utils.TestSetup.create_TestUser(self) + new_name = self.other_user_name # should be deleted by previous tear downs + test_headers, test_cookies = self.login_test_user() + + # update existing user name + data = {"user_name": new_name} + path = "/users/{usr}".format(usr=user_path_variable) + resp = utils.test_request(self, "PUT", path, headers=test_headers, cookies=test_cookies, data=data) + utils.check_response_basic_info(resp, 200, expected_method="PUT") + + # validate change of user name (itself) + path = "/users/{usr}".format(usr=new_name) + resp = utils.test_request(self, "GET", path, headers=test_headers, cookies=test_cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["user"]["user_name"], new_name) + + # validate removed previous user name (need admin access because it's another users' information) + path = "/users/{usr}".format(usr=self.test_user_name) + resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies, + expect_errors=True) + utils.check_response_basic_info(resp, 404, expected_method="GET") + + # validate effective new user name + utils.check_or_try_logout_user(self) + headers, cookies = utils.check_or_try_login_user(self, username=new_name, password=self.test_user_name, + use_ui_form_submit=True, version=self.version) + resp = utils.test_request(self, "GET", "/session", headers=headers, cookies=cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["authenticated"], True) + utils.check_val_equal(body["user"]["user_name"], new_name) + + # validate ineffective previous user name + utils.check_or_try_logout_user(self) + headers, cookies = utils.check_or_try_login_user( + self, username=self.test_user_name, password=self.test_user_name, version=self.version, + use_ui_form_submit=True, expect_errors=True) + utils.check_val_equal(cookies, {}, msg="CookiesType should be empty from login failure.") + resp = utils.test_request(self, "GET", "/session", headers=headers, cookies=cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["authenticated"], False) + + @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED + def test_PutUsers_email_ReservedKeyword_Current(self): + """ + .. seealso:: + - :meth:`Interface_MagpieAPI_AdminAuth.test_PutUser_ReservedKeyword_Current` + """ + utils.warn_version(self, "user update own information ", "1.12.0", skip=True) + utils.TestSetup.create_TestUser(self) + test_headers, test_cookies = self.login_test_user() + new_email = "toto@new-email.lol" + data = {"email": new_email} + path = "/users/{usr}".format(usr=self.test_user_name) + resp = utils.test_request(self, "PUT", path, headers=self.json_headers, cookies=self.cookies, data=data) + utils.check_response_basic_info(resp, 200, expected_method="PUT") + + resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["user"]["email"], new_email) + + def run_PutUsers_password_update_itself(self, user_path_variable): + """ + Session user is allowed to update its own information via logged user path or corresponding user-name path. + + .. seealso:: + - :meth:`Interface_MagpieAPI_AdminAuth.test_PutUser_ReservedKeyword_Current` + """ + utils.TestSetup.create_TestUser(self) + old_password = self.test_user_name + new_password = "n0t-SO-ez-2-Cr4cK" # nosec + data = {"password": new_password} + path = "/users/{usr}".format(usr=user_path_variable) + resp = utils.test_request(self, "PUT", path, headers=self.json_headers, cookies=self.cookies, data=data) + utils.check_response_basic_info(resp, 200, expected_method="PUT") + utils.check_or_try_logout_user(self) + + # validate that the new password is effective + headers, cookies = utils.check_or_try_login_user( + self, username=self.test_user_name, password=new_password, + use_ui_form_submit=True, version=self.version) + resp = utils.test_request(self, "GET", "/session", headers=headers, cookies=cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["authenticated"], True) + utils.check_val_equal(body["user"]["user_name"], self.test_user_name) + utils.check_or_try_logout_user(self) + + # validate that previous password is ineffective + headers, cookies = utils.check_or_try_login_user( + self, username=self.test_user_name, password=old_password, version=self.version, + use_ui_form_submit=True, expect_errors=True) + resp = utils.test_request(self, "GET", "/session", headers=headers, cookies=cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["authenticated"], False) + + @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED + def test_PutUsers_password_ReservedKeyword_Current(self): + utils.warn_version(self, "user update own information ", "1.12.0", skip=True) + self.run_PutUsers_password_update_itself(get_constant("MAGPIE_LOGGED_USER")) + + @runner.MAGPIE_TEST_USERS + def test_PutUsers_password_MatchingUserName_Current(self): + utils.warn_version(self, "user update own information ", "1.12.0", skip=True) + self.run_PutUsers_password_update_itself(self.test_user_name) + + @runner.MAGPIE_TEST_USERS + def test_PutUsers_password_ForbiddenUpdateOthers(self): + """ + Although session user is allowed to update its own information, insufficient permissions (not admin) forbids + that user to update other user's information. + + .. seealso:: + - :meth:`Interface_MagpieAPI_AdminAuth.test_PutUser_ReservedKeyword_Current` + """ + utils.warn_version(self, "user update own information ", "1.12.0", skip=True) + utils.TestSetup.create_TestUser(self) + utils.TestSetup.create_TestUser(self, {"user_name": self.other_user_name, "password": self.other_user_name}) + test_headers, test_cookies = self.login_test_user() + new_password = "n0t-SO-ez-2-Cr4cK" # nosec + data = {"password": new_password} + path = "/users/{usr}".format(usr=self.other_user_name) + resp = utils.test_request(self, "PUT", path, headers=test_headers, cookies=test_cookies, data=data) + utils.check_response_basic_info(resp, 403, expected_method="PUT") + utils.check_or_try_logout_user(self) + + # validate that current user password was not randomly updated + # make sure we clear any potential leftover cookies by re-login + utils.check_or_try_logout_user(self) + headers, cookies = utils.check_or_try_login_user( + self, username=self.test_user_name, password=self.test_user_name, + use_ui_form_submit=True, version=self.version) + resp = utils.test_request(self, "GET", "/session", headers=headers, cookies=cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["authenticated"], True) + utils.check_val_equal(body["user"]["user_name"], self.test_user_name) + + # validate that the new password was not applied to other user + utils.check_or_try_logout_user(self) + headers, cookies = utils.check_or_try_login_user( + self, username=self.other_user_name, password=self.other_user_name, + use_ui_form_submit=True, version=self.version) + resp = utils.test_request(self, "GET", "/session", headers=headers, cookies=cookies) + body = utils.check_response_basic_info(resp, 200, expected_method="GET") + utils.check_val_equal(body["authenticated"], True) + utils.check_val_equal(body["user"]["user_name"], self.other_user_name) + utils.check_or_try_logout_user(self) + + @pytest.mark.skip(reason="Not implemented") + @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED + @runner.MAGPIE_TEST_RESOURCES + def test_PostUserResourcesPermissions_Forbidden(self): + """Logged user without administrator access is not allowed to add resource permissions for itself.""" + raise NotImplementedError # TODO + @runner.MAGPIE_TEST_API class Interface_MagpieAPI_AdminAuth(six.with_metaclass(ABCMeta, Base_Magpie_TestCase)): @@ -282,6 +461,7 @@ def check_GetUserResourcesPermissions(cls, user_name, resource_id, query=None): return body @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED def test_GetCurrentUser(self): logged_user = get_constant("MAGPIE_LOGGED_USER") path = "/users/{}".format(logged_user) @@ -293,6 +473,7 @@ def test_GetCurrentUser(self): utils.check_val_equal(body["user_name"], self.usr) @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED def test_GetCurrentUserResourcesPermissions(self): utils.TestSetup.create_TestService(self) body = utils.TestSetup.create_TestServiceResource(self) @@ -747,6 +928,7 @@ def test_PostUsers(self): utils.check_val_is_in(self.test_user_name, users) @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED def test_PostUsers_ReservedKeyword_Current(self): data = { "user_name": get_constant("MAGPIE_LOGGED_USER"), @@ -759,7 +941,9 @@ def test_PostUsers_ReservedKeyword_Current(self): utils.check_response_basic_info(resp, 400, expected_method="POST") @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_LOGGED def test_PutUser_ReservedKeyword_Current(self): + utils.warn_version(self, "user update own information ", "1.12.0", older=True, skip=True) utils.TestSetup.create_TestUser(self) path = "/users/{usr}".format(usr=get_constant("MAGPIE_LOGGED_USER")) data = {"user_name": self.test_user_name + "-new-put-over-current"} diff --git a/tests/runner.py b/tests/runner.py index 67df96edf..92269c4df 100644 --- a/tests/runner.py +++ b/tests/runner.py @@ -31,6 +31,7 @@ def filter_test_files(root, filename): MAGPIE_TEST_RESOURCES = RunOptionDecorator("MAGPIE_TEST_RESOURCES") MAGPIE_TEST_GROUPS = RunOptionDecorator("MAGPIE_TEST_GROUPS") MAGPIE_TEST_USERS = RunOptionDecorator("MAGPIE_TEST_USERS") +MAGPIE_TEST_LOGGED = RunOptionDecorator("MAGPIE_TEST_LOGGED") # validate 'Logged User' views (i.e.: current) MAGPIE_TEST_STATUS = RunOptionDecorator("MAGPIE_TEST_STATUS") # validate views found/displayed as per permissions MAGPIE_TEST_REMOTE = RunOptionDecorator("MAGPIE_TEST_REMOTE") MAGPIE_TEST_LOCAL = RunOptionDecorator("MAGPIE_TEST_LOCAL") diff --git a/tests/test_magpie_api.py b/tests/test_magpie_api.py index a60adf888..519bd3505 100644 --- a/tests/test_magpie_api.py +++ b/tests/test_magpie_api.py @@ -46,7 +46,7 @@ def setUpClass(cls): class TestCase_MagpieAPI_UsersAuth_Local(ti.Interface_MagpieAPI_UsersAuth, unittest.TestCase): # pylint: disable=C0103,invalid-name """ - Test any operation that require at least ``MAGPIE_USERS_GROUP`` AuthN/AuthZ. + Test any operation that require at least ``MAGPIE_USERS_GROUP`` AuthN/AuthZ, but lower than ``MAGPIE_ADMIN_GROUP``. Use a local Magpie test application. """ @@ -56,6 +56,28 @@ class TestCase_MagpieAPI_UsersAuth_Local(ti.Interface_MagpieAPI_UsersAuth, unitt @classmethod def setUpClass(cls): cls.app = utils.get_test_magpie_app() + # admin login credentials for setup operations, use 'test' parameters for testing actual feature + cls.grp = get_constant("MAGPIE_ADMIN_GROUP") + cls.usr = get_constant("MAGPIE_TEST_ADMIN_USERNAME") + cls.pwd = get_constant("MAGPIE_TEST_ADMIN_PASSWORD") + cls.json_headers = utils.get_headers(cls.app, {"Accept": CONTENT_TYPE_JSON, "Content-Type": CONTENT_TYPE_JSON}) + cls.cookies = None + cls.version = utils.TestSetup.get_Version(cls) + cls.headers, cls.cookies = utils.check_or_try_login_user(cls.app, cls.usr, cls.pwd, + use_ui_form_submit=True, version=cls.version) + cls.require = "cannot run tests without logged in user with '{}' permissions".format(cls.grp) + cls.check_requirements() + + cls.test_user_group = get_constant("MAGPIE_USERS_GROUP") + cls.test_user_name = "unittest-user_user-auth-username" + cls.other_user_name = "unittest-other_user-auth-username" + + @classmethod + def login_test_user(cls): + utils.check_or_try_logout_user(cls) + return utils.check_or_try_login_user( + cls, username=cls.test_user_name, password=cls.test_user_name, + use_ui_form_submit=True, version=cls.version) @runner.MAGPIE_TEST_API @@ -115,7 +137,7 @@ def setUpClass(cls): class TestCase_MagpieAPI_UsersAuth_Remote(ti.Interface_MagpieAPI_UsersAuth, unittest.TestCase): # pylint: disable=C0103,invalid-name """ - Test any operation that require at least ``MAGPIE_USERS_GROUP`` AuthN/AuthZ. + Test any operation that require at least ``MAGPIE_USERS_GROUP`` AuthN/AuthZ, but lower than ``MAGPIE_ADMIN_GROUP``. Use an already running remote bird server. """ diff --git a/tests/utils.py b/tests/utils.py index e7db60f1d..e894b81b0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -187,17 +187,25 @@ def get_service_types_for_version(version): return list(available_service_types) -def warn_version(test, functionality, version, skip=True): - # type: (Base_Magpie_TestCase, Str, Str, bool) -> None +def warn_version(test, functionality, version, skip=True, older=False): + # type: (Base_Magpie_TestCase, Str, Str, bool, bool) -> None """ - Verifies that ``test.version`` value meets the minimal ``version`` requirement to execute a test. - + Verifies that ``test.version`` value *minimally* has :paramref:`version` requirement to execute a test. (ie: ``test.version >= version``). + + If :paramref:`older` is ``True``, instead verifies that the instance is older then :paramref:`version`. + (ie: ``test.version < version``). + If version condition is not met, a warning is emitted and the test is skipped according to ``skip`` value. """ - if LooseVersion(test.version) < LooseVersion(version): - msg = "Functionality [{}] not yet implemented in version [{}], upgrade to [>={}]." \ - .format(functionality, test.version, version) + min_req = LooseVersion(test.version) < LooseVersion(version) + if min_req or (not min_req and older): + if min_req: + msg = "Functionality [{}] not yet implemented in version [{}], upgrade [>={}] required to test." \ + .format(functionality, test.version, version) + else: + msg = "Functionality [{}] was deprecated in version [{}], downgrade [<{}] required to test." \ + .format(functionality, test.version, version) warnings.warn(msg, FutureWarning) if skip: test.skipTest(reason=msg) # noqa: F401 @@ -618,8 +626,8 @@ def check_resource_children(resource_dict, parent_resource_id, root_service_id): check_resource_children(resource_info["children"], resource_int_id, root_service_id) -# Generic setup and validation methods across unittests class TestSetup(object): + """Generic setup and validation methods across unittests""" # pylint: disable=C0103,invalid-name @staticmethod From 3408b54eb68588f8a2276e8abe416808b3be2ecb Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 22 Jul 2020 23:14:56 -0400 Subject: [PATCH 003/136] [wip] add /register/groups routes + setup test for user-view functionalities --- HISTORY.rst | 14 +- magpie/api/generic.py | 13 +- magpie/api/login/login.py | 3 +- magpie/api/management/group/group_formats.py | 26 +- magpie/api/management/register/__init__.py | 11 + .../api/management/register/register_utils.py | 46 ++++ .../api/management/register/register_views.py | 88 ++++++ magpie/api/management/resource/__init__.py | 2 +- magpie/api/management/service/__init__.py | 3 +- magpie/api/management/user/__init__.py | 2 +- magpie/api/management/user/user_utils.py | 19 ++ magpie/api/management/user/user_views.py | 30 +- magpie/api/requests.py | 28 +- magpie/api/schemas.py | 258 +++++++++++++++--- magpie/api/swagger/__init__.py | 4 +- magpie/api/swagger/views.py | 19 ++ magpie/ui/__init__.py | 3 +- magpie/ui/home/__init__.py | 24 +- magpie/ui/home/static/style.css | 2 +- magpie/ui/home/templates/template.mako | 24 +- magpie/ui/home/views.py | 10 +- magpie/ui/login/__init__.py | 2 +- magpie/ui/login/views.py | 13 +- magpie/ui/management/__init__.py | 2 +- magpie/ui/management/templates/add_group.mako | 12 +- .../ui/management/templates/add_resource.mako | 36 ++- .../ui/management/templates/add_service.mako | 43 +-- magpie/ui/management/templates/add_user.mako | 51 ++-- .../ui/management/templates/edit_group.mako | 38 ++- .../ui/management/templates/edit_service.mako | 37 ++- magpie/ui/management/templates/edit_user.mako | 54 +++- .../ui/management/templates/view_groups.mako | 2 +- .../management/templates/view_services.mako | 13 +- .../ui/management/templates/view_users.mako | 2 +- magpie/ui/management/views.py | 56 ++-- magpie/ui/user/__init__.py | 9 + .../ui/user/templates/edit_current_user.mako | 105 +++++++ magpie/ui/user/views.py | 114 ++++++++ magpie/ui/utils.py | 26 +- tests/interfaces.py | 171 ++++++++---- tests/test_magpie_api.py | 4 +- tests/test_magpie_ui.py | 51 ++++ 42 files changed, 1154 insertions(+), 316 deletions(-) create mode 100644 magpie/api/management/register/__init__.py create mode 100644 magpie/api/management/register/register_utils.py create mode 100644 magpie/api/management/register/register_views.py create mode 100644 magpie/ui/user/__init__.py create mode 100644 magpie/ui/user/templates/edit_current_user.mako create mode 100644 magpie/ui/user/views.py diff --git a/HISTORY.rst b/HISTORY.rst index c1b30d63e..45a5c4f65 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,7 +6,19 @@ History `Unreleased `_ (latest) ------------------------------------------------------------------------------------ -* Nothing yet. +Features / Changes +~~~~~~~~~~~~~~~~~~~~~ +* Add ``/ui`` route redirect to frontpage when UI is enabled. +* Add ``/json`` route information into generated Swagger API documentation. +* Add tag description into generated Swagger API documentation. +* Add more usage details to start `Magpie` web application in documentation. +* Allow logged user to update its own information. +* Allow logged user to join *discoverable* groups. + +Bug Fixes +~~~~~~~~~~~~~~~~~~~~~ +* Fix invalid API documentation of request body for ``POST /users/{user_name}/groups``. +* Fix minor HTML issues in mako templates. `1.11.0 `_ (2020-06-19) ------------------------------------------------------------------------------------ diff --git a/magpie/api/generic.py b/magpie/api/generic.py index 2e126eff9..dbf27b6ea 100644 --- a/magpie/api/generic.py +++ b/magpie/api/generic.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from pyramid.authentication import Authenticated, IAuthenticationPolicy +from pyramid.authentication import Authenticated from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import ( HTTPForbidden, @@ -15,6 +15,7 @@ from magpie.api import schemas as s from magpie.api.exception import raise_http, verify_param +from magpie.api.requests import get_principals from magpie.utils import ( CONTENT_TYPE_ANY, CONTENT_TYPE_JSON, @@ -84,14 +85,12 @@ def unauthorized_or_forbidden(request): .. seealso:: http://www.restapitutorial.com/httpstatuscodes.html """ - authn_policy = request.registry.queryUtility(IAuthenticationPolicy) http_err = HTTPForbidden http_msg = s.HTTPForbiddenResponseSchema.description - if authn_policy: - principals = authn_policy.effective_principals(request) - if Authenticated not in principals: - http_err = HTTPUnauthorized - http_msg = s.UnauthorizedResponseSchema.description + principals = get_principals(request) + if Authenticated not in principals: + http_err = HTTPUnauthorized + http_msg = s.UnauthorizedResponseSchema.description content = get_request_info(request, default_message=http_msg) return raise_http(nothrow=True, http_error=http_err, detail=content[u"detail"], content=content, diff --git a/magpie/api/login/login.py b/magpie/api/login/login.py index 63225cd3a..fc5a86027 100644 --- a/magpie/api/login/login.py +++ b/magpie/api/login/login.py @@ -271,8 +271,7 @@ def get_session(request): Get information about current session. """ def _get_session(req): - authn_policy = req.registry.queryUtility(IAuthenticationPolicy) - principals = authn_policy.effective_principals(req) + principals = get_principals(req) if Authenticated in principals: user = request.user json_resp = {u"authenticated": True, u"user": format_user(user)} diff --git a/magpie/api/management/group/group_formats.py b/magpie/api/management/group/group_formats.py index ce43a47d3..6311f301b 100644 --- a/magpie/api/management/group/group_formats.py +++ b/magpie/api/management/group/group_formats.py @@ -10,23 +10,23 @@ from magpie.models import Group -def format_group(group, basic_info=False, db_session=None): - # type: (Group, bool, Optional[Session]) -> JSON - def fmt_grp(grp, info): - if info: - return { - u"group_name": str(grp.group_name), - u"group_id": grp.id, - } - return { +def format_group(group, basic_info=False, public_info=False, db_session=None): + # type: (Group, bool, bool, Optional[Session]) -> JSON + def fmt_grp(grp, is_basic, is_public): + info = { u"group_name": str(grp.group_name), - u"description": str(grp.description), - u"member_count": grp.get_member_count(db_session), u"group_id": grp.id, - u"user_names": [usr.user_name for usr in grp.users] } + if is_basic: + return info + info[u"description"] = str(grp.description) if grp.description else None + if is_public: + return info + info[u"member_count"] = grp.get_member_count(db_session) + info[u"user_names"] = [usr.user_name for usr in grp.users] + return info return evaluate_call( - lambda: fmt_grp(group, basic_info), http_error=HTTPInternalServerError, + lambda: fmt_grp(group, basic_info, public_info), http_error=HTTPInternalServerError, msg_on_fail="Failed to format group.", content={u"group": repr(group)} ) diff --git a/magpie/api/management/register/__init__.py b/magpie/api/management/register/__init__.py new file mode 100644 index 000000000..8f2e0d304 --- /dev/null +++ b/magpie/api/management/register/__init__.py @@ -0,0 +1,11 @@ +from magpie.api import schemas as s +from magpie.utils import get_logger + +LOGGER = get_logger(__name__) + + +def includeme(config): + LOGGER.info("Adding API register...") + config.add_route(**s.service_api_route_info(s.RegisterGroupsAPI)) + config.add_route(**s.service_api_route_info(s.RegisterGroupAPI)) + config.scan() diff --git a/magpie/api/management/register/register_utils.py b/magpie/api/management/register/register_utils.py new file mode 100644 index 000000000..1e9d10a49 --- /dev/null +++ b/magpie/api/management/register/register_utils.py @@ -0,0 +1,46 @@ +from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound +from typing import TYPE_CHECKING +from ziggurat_foundations.models.services.group import GroupService + +from magpie.api import exception as ax +from magpie.api import schemas as s +from magpie.models import Group +from magpie.utils import CONTENT_TYPE_JSON + +if TYPE_CHECKING: + from magpie.typedefs import List, Str + from sqlalchemy.orm.session import Session + + +def get_discoverable_groups(db_session): + # type: (Session) -> List[Group] + """ + Get all existing group that are marked as publicly discoverable from the database. + """ + groups = ax.evaluate_call( + lambda: [grp for grp in GroupService.all(Group, db_session=db_session) if grp.discoverable], + http_error=HTTPForbidden, msg_on_fail=s.RegisterGroups_GET_ForbiddenResponseSchema.description) + return groups + + +def get_discoverable_group_by_name(group_name, db_session): + # type: (Str, Session) -> Group + """ + Obtains the requested discoverable group by name. + + .. note:: + For security reason, an existing group that is **NOT** discoverable will return NotFound instead of Forbidden. + Otherwise we give an indication to a potentially non-admin user that *some group* of that name exists. + + :return: found group matched by name + :raises HTTPNotFound: if the group cannot be found or if matched group name is not discoverable. + """ + public_groups = get_discoverable_groups(db_session) + found_group = ax.evaluate_call(lambda: [grp for grp in public_groups if grp.group_name == group_name], + http_error=HTTPNotFound, + msg_on_fail=s.RegisterGroup_NotFoundResponseSchema.description, + content={u"group_name": group_name}) + ax.verify_param(found_group, param_name="group_name", not_empty=True, + http_error=HTTPNotFound, content_type=CONTENT_TYPE_JSON, + msg_on_fail=s.RegisterGroup_NotFoundResponseSchema.description) + return found_group[0] diff --git a/magpie/api/management/register/register_views.py b/magpie/api/management/register/register_views.py new file mode 100644 index 000000000..f136996b3 --- /dev/null +++ b/magpie/api/management/register/register_views.py @@ -0,0 +1,88 @@ +from pyramid.authentication import Authenticated +from pyramid.httpexceptions import ( + HTTPConflict, + HTTPCreated, + HTTPForbidden, + HTTPInternalServerError, + HTTPOk, +) +from pyramid.view import view_config +from typing import TYPE_CHECKING + +from magpie.api import exception as ax +from magpie.api import requests as ar +from magpie.api import schemas as s +from magpie.api.management.group import group_formats as gf +from magpie.api.management.register import register_utils as ru +from magpie.api.management.user import user_utils as uu + +if TYPE_CHECKING: + from pyramid.httpexceptions import HTTPException + from pyramid.request import Request + + +@s.RegisterGroupAPI.get(tags=[s.GroupsTag, s.LoggedUserTag, s.RegisterTag], + response_schemas=s.RegisterGroups_GET_responses) +@view_config(route_name=s.RegisterGroupsAPI.name, request_method="GET", permissions=Authenticated) +def get_discoverable_groups_view(request): + # type: (Request) -> HTTPException + """ + List all discoverable groups (publicly available to join). + """ + public_groups = ru.get_discoverable_groups(request.db) + public_group_names = ax.evaluate_call(lambda: [grp.group_name for grp in public_groups], + http_error=HTTPInternalServerError, + msg_on_fail=s.HTTPInternalServerError.description) + return ax.valid_http(http_success=HTTPOk, content={u"group_names": public_group_names}, + detail=s.RegisterGroups_GET_OkResponseSchema.description) + + +@s.RegisterGroupAPI.get(tags=[s.GroupsTag, s.LoggedUserTag, s.RegisterTag], + response_schemas=s.RegisterGroup_GET_responses) +@view_config(route_name=s.RegisterGroupAPI.name, request_method="GET", permissions=Authenticated) +def get_discoverable_group_info_view(request): + """ + Obtain the information of a discoverable group. + """ + group_name = ar.get_group_matchdict_checked(request) + public_group = ru.get_discoverable_group_by_name(group_name, db_session=request.db) + group_fmt = gf.format_group(public_group, public_info=True) + return ax.valid_http(http_success=HTTPOk, content={u"group": group_fmt}, + detail=s.RegisterGroup_GET_OkResponseSchema.description) + + +@s.RegisterGroupAPI.post(schema=s.RegisterGroup_POST_RequestSchema, tags=[s.GroupsTag, s.LoggedUserAPI, s.RegisterTag], + response_schemas=s.RegisterGroup_POST_responses) +@view_config(route_name=s.RegisterGroupAPI.name, request_method="POST", permissions=Authenticated) +def join_discoverable_group_view(request): + """ + Assigns membership of the logged user to a publicly discoverable group. + """ + group_name = ar.get_group_matchdict_checked(request) + user = ar.get_logged_user(request) + group = ru.get_discoverable_group_by_name(group_name, db_session=request.db) + + ax.verify_param(user.id, param_compare=[usr.id for usr in group.users], not_in=True, http_error=HTTPConflict, + content={u"user_name": user.user_name, u"group_name": group.group_name}, + msg_on_fail=s.RegisterGroup_POST_ConflictResponseSchema.description) + ax.evaluate_call(lambda: request.db.add(models.UserGroup(group_id=group.id, user_id=user.id)), # noqa + fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, + msg_on_fail=s.RegisterGroup_POST_ForbiddenResponseSchema.description, + content={u"user_name": user.user_name, u"group_name": group.group_name}) + return ax.valid_http(http_success=HTTPCreated, detail=s.RegisterGroup_POST_CreatedResponseSchema.description, + content={u"user_name": user.user_name, u"group_name": group.group_name}) + + +@s.RegisterGroupAPI.delete(schema=s.RegisterGroup_DELETE_RequestSchema, + tags=[s.GroupsTag, s.LoggedUserAPI, s.RegisterTag], + response_schemas=s.RegisterGroup_DELETE_responses) +@view_config(route_name=s.RegisterGroupAPI.name, request_method="DELETE", permissions=Authenticated) +def leave_discoverable_group_view(request): + """ + Removes membership of the logged user from a previously joined discoverable group. + """ + group_name = ar.get_group_matchdict_checked(request) + user = ar.get_logged_user(request) + group = ru.get_discoverable_group_by_name(group_name, db_session=request.db) + uu.delete_user_group(user, group, request.db) + return ax.valid_http(http_success=HTTPOk, detail=s.RegisterGroup_DELETE_OkResponseSchema.description) diff --git a/magpie/api/management/resource/__init__.py b/magpie/api/management/resource/__init__.py index 4c0d1c303..2c30e2d67 100644 --- a/magpie/api/management/resource/__init__.py +++ b/magpie/api/management/resource/__init__.py @@ -5,7 +5,7 @@ def includeme(config): - LOGGER.info("Adding api resource...") + LOGGER.info("Adding API resource...") # Add all the rest api routes config.add_route(**s.service_api_route_info(s.ResourcesAPI)) config.add_route(**s.service_api_route_info(s.ResourceAPI)) diff --git a/magpie/api/management/service/__init__.py b/magpie/api/management/service/__init__.py index 03b81368e..a16455ce1 100644 --- a/magpie/api/management/service/__init__.py +++ b/magpie/api/management/service/__init__.py @@ -5,11 +5,12 @@ def includeme(config): - LOGGER.info("Adding api service...") + LOGGER.info("Adding API service...") # NOTE: # routes 'by type' must be before 'by name' to be evaluated first # order is important to preserve expected behaviour, # otherwise service named 'types' is searched before + # --- service by type --- config.add_route(**s.service_api_route_info(s.ServiceTypesAPI)) config.add_route(**s.service_api_route_info(s.ServiceTypeAPI)) diff --git a/magpie/api/management/user/__init__.py b/magpie/api/management/user/__init__.py index 1c47f39e3..8bca7f84a 100644 --- a/magpie/api/management/user/__init__.py +++ b/magpie/api/management/user/__init__.py @@ -5,7 +5,7 @@ def includeme(config): - LOGGER.info("Adding api user...") + LOGGER.info("Adding API user...") # Add all the rest api routes config.add_route(**s.service_api_route_info(s.UsersAPI)) config.add_route(**s.service_api_route_info(s.UserAPI)) diff --git a/magpie/api/management/user/user_utils.py b/magpie/api/management/user/user_utils.py index e6a03a798..77c610591 100644 --- a/magpie/api/management/user/user_utils.py +++ b/magpie/api/management/user/user_utils.py @@ -127,6 +127,25 @@ def create_user_resource_permission_response(user, resource, permission, db_sess detail=s.UserResourcePermissions_POST_CreatedResponseSchema.description) +def delete_user_group(user, group, db_session): + # type: (models.User, models.Group, Session) -> None + """ + Deletes a user-group relationship (user membership to a group). + + :returns: nothing - user-group is deleted. + :raises HTTPNotFound: if the combination cannot be found. + """ + def del_usr_grp(usr, grp): + db_session.query(models.UserGroup) \ + .filter(models.UserGroup.user_id == usr.id) \ + .filter(models.UserGroup.group_id == grp.id) \ + .delete() + + ax.evaluate_call(lambda: del_usr_grp(user, group), fallback=lambda: db_session.rollback(), + http_error=HTTPNotFound, msg_on_fail=s.UserGroup_DELETE_NotFoundResponseSchema.description, + content={u"user_name": user.user_name, u"group_name": group.group_name}) + + def delete_user_resource_permission_response(user, resource, permission, db_session): # type: (models.User, ServiceOrResourceType, Permission, Session) -> HTTPException """ diff --git a/magpie/api/management/user/user_views.py b/magpie/api/management/user/user_views.py index b819f48b9..c04ebd73f 100644 --- a/magpie/api/management/user/user_views.py +++ b/magpie/api/management/user/user_views.py @@ -63,23 +63,23 @@ def update_user_view(request): """ Update user information by user name. """ - - user_name = ar.get_value_matchdict_checked(request, key="user_name") - ax.verify_param(user_name, param_compare=get_constant("MAGPIE_LOGGED_USER"), not_equal=True, - http_error=HTTPBadRequest, param_name="user_name", content={u"user_name": user_name}, - msg_on_fail=s.Service_PUT_BadRequestResponseSchema_ReservedKeyword.description) - - user = ar.get_user_matchdict_checked(request, user_name_key="user_name") + user_key = "user_name" + user = ar.get_user_matchdict_checked_or_logged(request, user_name_key=user_key) new_user_name = ar.get_multiformat_post(request, "user_name", default=user.user_name) new_email = ar.get_multiformat_post(request, "email", default=user.email) new_password = ar.get_multiformat_post(request, "password", default=user.user_password) uu.check_user_info(new_user_name, new_email, new_password, group_name=new_user_name) update_username = user.user_name != new_user_name + if update_username: + logged_user_name = get_constant("MAGPIE_LOGGED_USER", request) + ax.verify_param(new_user_name, param_compare=logged_user_name, not_equal=True, + http_error=HTTPBadRequest, param_name=user_key, content={user_key: logged_user_name}, + msg_on_fail=s.Service_PUT_BadRequestResponseSchema_ReservedKeyword.description) update_password = user.user_password != new_password update_email = user.email != new_email ax.verify_param(any([update_username, update_password, update_email]), is_true=True, http_error=HTTPBadRequest, - content={u"user_name": user.user_name}, + content={user_key: user.user_name}, msg_on_fail=s.User_PUT_BadRequestResponseSchema.description) if user.user_name != new_user_name: @@ -174,21 +174,11 @@ def assign_user_group_view(request): @view_config(route_name=s.UserGroupAPI.name, request_method="DELETE") def delete_user_group_view(request): """ - Remove a user from a group. + Removes a user from a group. """ - db = request.db user = ar.get_user_matchdict_checked_or_logged(request) group = ar.get_group_matchdict_checked(request) - - def del_usr_grp(usr, grp): - db.query(models.UserGroup) \ - .filter(models.UserGroup.user_id == usr.id) \ - .filter(models.UserGroup.group_id == grp.id) \ - .delete() - - ax.evaluate_call(lambda: del_usr_grp(user, group), fallback=lambda: db.rollback(), - http_error=HTTPNotFound, msg_on_fail=s.UserGroup_DELETE_NotFoundResponseSchema.description, - content={u"user_name": user.user_name, u"group_name": group.group_name}) + uu.delete_user_group(user, group, request.db) return ax.valid_http(http_success=HTTPOk, detail=s.UserGroup_DELETE_OkResponseSchema.description) diff --git a/magpie/api/requests.py b/magpie/api/requests.py index fb5a09b6c..61adbb5f1 100644 --- a/magpie/api/requests.py +++ b/magpie/api/requests.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from pyramid.authentication import IAuthenticationPolicy +from pyramid.authentication import Authenticated, IAuthenticationPolicy from pyramid.httpexceptions import ( HTTPBadRequest, HTTPForbidden, @@ -16,7 +16,7 @@ from magpie.api import schemas as s from magpie.api.exception import evaluate_call, verify_param from magpie.constants import get_constant -from magpie.utils import CONTENT_TYPE_JSON +from magpie.utils import CONTENT_TYPE_JSON, get_logger if TYPE_CHECKING: # pylint: disable=W0611,unused-import @@ -24,6 +24,8 @@ from magpie.typedefs import Any, AnySettingsContainer, Str, Optional, ServiceOrResourceType # noqa: F401 from magpie.permissions import Permission # noqa: F401 +LOGGER = get_logger(__name__) + def get_request_method_content(request): # 'request' object stores GET content into 'GET' property, while other methods are in 'POST' property @@ -78,6 +80,25 @@ def get_value_multiformat_post_checked(request, key, default=None): return val +def get_principals(request): + """Obtains the list of effective principals according to detected request session user.""" + authn_policy = request.registry.queryUtility(IAuthenticationPolicy) + principals = authn_policy.effective_principals(request) + return principals + + +def get_logged_user(request): + # type: (Request) -> Optional[models.User] + try: + principals = get_principals(request) + if Authenticated in principals: + LOGGER.info("User '%s' is authenticated", request.user.user_name) + return request.user + except AttributeError: + pass + return None + + def get_user(request, user_name_or_token=None): # type: (Request, Optional[Str]) -> models.User """ @@ -104,8 +125,7 @@ def get_user(request, user_name_or_token=None): msg_on_fail=s.User_CheckAnonymous_NotFoundResponseSchema.description) return anonymous - authn_policy = request.registry.queryUtility(IAuthenticationPolicy) - principals = authn_policy.effective_principals(request) + principals = get_principals(request) admin_group_name = get_constant("MAGPIE_ADMIN_GROUP", settings_container=request) admin_group = GroupService.by_group_name(admin_group_name, db_session=request.db) admin_principal = "group:{}".format(admin_group.id) diff --git a/magpie/api/schemas.py b/magpie/api/schemas.py index 48e2b7b4b..ff9c00d96 100644 --- a/magpie/api/schemas.py +++ b/magpie/api/schemas.py @@ -46,6 +46,7 @@ UsersTag = "User" LoggedUserTag = "Logged User" GroupsTag = "Group" +RegisterTag = "Register" ResourcesTag = "Resource" ServicesTag = "Service" @@ -225,6 +226,12 @@ def service_api_route_info(service_api): GroupResourceTypesAPI = Service( path="/groups/{group_name}/resources/types/{resource_type}", name="GroupResourceTypes") +RegisterGroupsAPI = Service( + path="/register/groups", + name="RegisterGroups") +RegisterGroupAPI = Service( + path="/register/groups/{group_name}", + name="RegisterGroup") ResourcesAPI = Service( path="/resources", name="Resources") @@ -298,6 +305,7 @@ def service_api_route_info(service_api): GroupsTag: "Groups management and control of their applicable users, services, resources and permissions.\n\n" "Administrator-level permissions are required to access most paths. ", + RegisterTag: "Registration paths for operations available to users (including non-administrators).", ResourcesTag: "Management of resources that reside under a given service and their applicable permissions.", ServicesTag: "Management of service definitions, children resources and their applicable permissions.", } @@ -416,6 +424,13 @@ class ErrorVerifyParamBodySchema(colander.MappingSchema): missing=colander.drop) +class ErrorResponseParamsSchema(colander.MappingSchema): + name = colander.SchemaNode(colander.String(), description="Name of the parameter that caused the error.") + value = colander.SchemaNode(colander.String(), description="Value that caused the error.", default=None) + compare = colander.SchemaNode(colander.String(), missing=colander.drop, + description="Comparison value(s) that caused the error due to invalid validation.") + + class ErrorResponseBodySchema(BaseResponseBodySchema): def __init__(self, code, description, **kw): super(ErrorResponseBodySchema, self).__init__(code, description, **kw) @@ -433,6 +448,8 @@ def __init__(self, code, description, **kw): colander.String(), description="Request method that generated the error.", example="GET") + param = ErrorResponseParamsSchema(missing=colander.drop, + description="Additional parameter details to explain the cause of error.") class InternalServerErrorResponseBodySchema(ErrorResponseBodySchema): @@ -451,14 +468,13 @@ def __init__(self, **kw): class UnauthorizedResponseSchema(colander.MappingSchema): - description = "Unauthorized access to this resource. " + \ - "Insufficient user privileges or missing authentication headers." + description = "Unauthorized access to this resource. Missing authentication headers or cookies." header = HeaderResponseSchema() body = UnauthorizedResponseBodySchema(code=HTTPUnauthorized.code, description=description) class HTTPForbiddenResponseSchema(colander.MappingSchema): - description = "Forbidden operation under this resource." + description = "Forbidden operation for this resource or insufficient user privileges." header = HeaderResponseSchema() body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description) @@ -476,7 +492,7 @@ class MethodNotAllowedResponseSchema(colander.MappingSchema): class NotAcceptableResponseSchema(colander.MappingSchema): - description = "Unsupported 'Accept Header' was specified." + description = "Unsupported Content-Type in 'Accept' header was specified." header = HeaderResponseSchema() body = BaseResponseBodySchema(code=HTTPNotAcceptable.code, description=description) @@ -562,6 +578,13 @@ class GroupDetailBodySchema(GroupBodySchema): example=["alice", "bob"], missing=colander.drop ) + discoverable = colander.SchemaNode( + colander.Boolean(), + description="Indicates if this group is publicly accessible. " + "Discoverable groups can be joined by any logged user.", + example=True, + default=False + ) class ServiceBodySchema(colander.MappingSchema): @@ -636,9 +659,12 @@ class ResourceBodySchema(colander.MappingSchema): permission_names.missing = colander.drop # if not returned (basic_info = True) -# TODO: improve by making recursive resources work (?) +# FIXME: improve by making recursive resources work (?) class Resource_ChildrenContainerWithoutChildResourceBodySchema(ResourceBodySchema): - children = colander.MappingSchema(default={}) + children = colander.MappingSchema( + default={}, + description="Recursive '{}' schema for each applicable children resources.".format(ResourceBodySchema.__name__) + ) class Resource_ChildResourceWithoutChildrenBodySchema(colander.MappingSchema): @@ -1521,11 +1547,6 @@ class UserGroups_GET_OkResponseSchema(colander.MappingSchema): class UserGroups_POST_RequestBodySchema(colander.MappingSchema): - user_name = colander.SchemaNode( - colander.String(), - description="Name of the user in the user-group relationship", - example="toto", - ) group_name = colander.SchemaNode( colander.String(), description="Name of the group in the user-group relationship", @@ -1553,13 +1574,13 @@ class UserGroups_POST_ResponseBodySchema(BaseResponseBodySchema): class UserGroups_POST_CreatedResponseSchema(colander.MappingSchema): - description = "Create user-group assignation successful." + description = "Create user-group assignation successful. User is a member of the group." header = HeaderResponseSchema() body = UserGroups_POST_ResponseBodySchema(code=HTTPCreated.code, description=description) class UserGroups_POST_GroupNotFoundResponseSchema(colander.MappingSchema): - description = "Can't find the group to assign to." + description = "Cannot find the group to assign to." header = HeaderResponseSchema() body = ErrorResponseBodySchema(code=HTTPNotFound.code, description=description) @@ -1602,13 +1623,13 @@ class UserGroup_DELETE_RequestSchema(colander.MappingSchema): class UserGroup_DELETE_OkResponseSchema(colander.MappingSchema): - description = "Delete user-group successful." + description = "Delete user-group successful. User is not a member of the group anymore." header = HeaderResponseSchema() body = BaseResponseBodySchema(code=HTTPOk.code, description=description) class UserGroup_DELETE_NotFoundResponseSchema(colander.MappingSchema): - description = "Invalid user-group combination for delete." + description = "Could not remove user from group. Could not find any matching group membership for user." header = HeaderResponseSchema() body = ErrorResponseBodySchema(code=HTTPNotFound.code, description=description) @@ -2253,16 +2274,101 @@ class GroupServicePermission_DELETE_ForbiddenResponseSchema(colander.MappingSche body = GroupServicePermission_DELETE_ResponseBodySchema(code=HTTPForbidden.code, description=description) -class Signout_GET_OkResponseSchema(colander.MappingSchema): - description = "Sign out successful." +class GroupServicePermission_DELETE_NotFoundResponseSchema(colander.MappingSchema): + description = "Permission not found for corresponding group and resource." + header = HeaderResponseSchema() + body = GroupServicePermission_DELETE_ResponseBodySchema(code=HTTPNotFound.code, description=description) + + +class RegisterGroup_NotFoundResponseSchema(colander.MappingSchema): + description = "Could not find any discoverable group matching provided name." + header = HeaderResponseSchema() + body = ErrorResponseBodySchema(code=HTTPNotFound.code, description=description) + + +class RegisterGroups_GET_ResponseBodySchema(BaseResponseBodySchema): + group_names = GroupNamesListSchema(description="List of discoverable group names.") + + +class RegisterGroups_GET_OkResponseSchema(colander.MappingSchema): + description = "Get discoverable groups successful." + header = HeaderResponseSchema() + body = RegisterGroups_GET_ResponseBodySchema(code=HTTPOk.code, description=description) + + +class RegisterGroups_GET_ForbiddenResponseSchema(colander.MappingSchema): + description = "Obtain discoverable groups refused by db." + header = HeaderResponseSchema() + body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description) + + +class RegisterGroup_GET_ResponseBodySchema(BaseResponseBodySchema): + group = GroupBodySchema() # not detailed because authenticated route has limited information + + +class RegisterGroup_GET_OkResponseSchema(colander.MappingSchema): + description = "Get discoverable group successful." + header = HeaderResponseSchema() + body = RegisterGroup_GET_ResponseBodySchema(code=HTTPOk.code, description=description) + + +class RegisterGroup_POST_RequestSchema(colander.MappingSchema): + header = HeaderRequestSchemaAPI() + body = colander.MappingSchema(description="Nothing required.") + group_name = GroupNameParameter + + +class RegisterGroup_POST_ResponseBodySchema(BaseResponseBodySchema): + user_name = colander.SchemaNode( + colander.String(), + description="Name of the user in the user-group relationship.", + example="logged-user", + ) + group_name = colander.SchemaNode( + colander.String(), + description="Name of the group in the user-group relationship.", + example="public-group", + ) + + +class RegisterGroup_POST_CreatedResponseSchema(colander.MappingSchema): + description = "Logged user successfully joined the discoverable group. User is now a member of the group." + header = HeaderResponseSchema() + body = RegisterGroup_POST_ResponseBodySchema(code=HTTPNotFound.code, description=description) + + +class RegisterGroup_POST_ForbiddenResponseSchema(colander.MappingSchema): + description = "Group membership was not permitted for the logged user." + header = HeaderResponseSchema() + body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description) + + +class RegisterGroup_POST_ConflictResponseSchema(colander.MappingSchema): + description = "Logged user is already a member of the group." + header = HeaderResponseSchema() + body = ErrorResponseBodySchema(code=HTTPConflict.code, description=description) + + +class RegisterGroup_DELETE_RequestSchema(colander.MappingSchema): + header = HeaderRequestSchemaAPI() + body = colander.MappingSchema(description="Nothing required.") + group_name = GroupNameParameter + + +class RegisterGroup_DELETE_OkResponseSchema(colander.MappingSchema): + description = "Logged user successfully removed from the group. User is not a member of the group anymore." header = HeaderResponseSchema() body = BaseResponseBodySchema(code=HTTPOk.code, description=description) -class GroupServicePermission_DELETE_NotFoundResponseSchema(colander.MappingSchema): - description = "Permission not found for corresponding group and resource." +class RegisterGroup_DELETE_ForbiddenResponseSchema(colander.MappingSchema): + description = "Remove logged used from discoverable group was refused by db." header = HeaderResponseSchema() - body = GroupServicePermission_DELETE_ResponseBodySchema(code=HTTPNotFound.code, description=description) + body = BaseResponseBodySchema(code=HTTPForbidden.code, description=description) + + +# check done using same util function +RegisterGroup_DELETE_NotFoundResponseSchema = UserGroup_DELETE_NotFoundResponseSchema class Session_GET_ResponseBodySchema(BaseResponseBodySchema): @@ -2427,6 +2533,12 @@ class Signin_POST_External_InternalServerErrorResponseSchema(colander.MappingSch body = Signin_POST_InternalServerErrorBodySchema(code=HTTPInternalServerError.code, description=description) +class Signout_GET_OkResponseSchema(colander.MappingSchema): + description = "Sign out successful." + header = HeaderResponseSchema() + body = BaseResponseBodySchema(code=HTTPOk.code, description=description) + + class Version_GET_ResponseBodySchema(BaseResponseBodySchema): version = colander.SchemaNode( colander.String(), @@ -2474,12 +2586,13 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Resource_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Resources_GET_responses = { "200": Resources_GET_OkResponseSchema(), "401": UnauthorizedResponseSchema(), "406": NotAcceptableResponseSchema(), - "500": Resource_GET_InternalServerErrorResponseSchema() + "500": Resource_GET_InternalServerErrorResponseSchema(), } Resources_POST_responses = { "201": Resources_POST_CreatedResponseSchema(), @@ -2490,6 +2603,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": Resources_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Resources_DELETE_responses = { "200": Resource_DELETE_OkResponseSchema(), @@ -2499,6 +2613,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Resource_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ResourcePermissions_GET_responses = { "200": ResourcePermissions_GET_OkResponseSchema(), @@ -2508,23 +2623,27 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Resource_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceTypes_GET_responses = { "200": ServiceTypes_GET_OkResponseSchema(), "401": UnauthorizedResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceType_GET_responses = { "200": Services_GET_OkResponseSchema(), "400": Services_GET_BadRequestResponseSchema(), "401": UnauthorizedResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Services_GET_responses = { "200": Services_GET_OkResponseSchema(), "400": Services_GET_BadRequestResponseSchema(), "401": UnauthorizedResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Services_POST_responses = { "201": Services_POST_CreatedResponseSchema(), @@ -2534,6 +2653,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": Services_POST_ConflictResponseSchema(), "422": Services_POST_UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Service_GET_responses = { "200": Service_GET_OkResponseSchema(), @@ -2541,6 +2661,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": Service_MatchDictCheck_ForbiddenResponseSchema(), "404": Service_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Service_PUT_responses = { "200": Service_PUT_OkResponseSchema(), @@ -2549,6 +2670,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": Service_PUT_ForbiddenResponseSchema(), "406": NotAcceptableResponseSchema(), "409": Service_PUT_ConflictResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Service_DELETE_responses = { "200": Service_DELETE_OkResponseSchema(), @@ -2556,6 +2678,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": Service_DELETE_ForbiddenResponseSchema(), "404": Service_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServicePermissions_GET_responses = { "200": ServicePermissions_GET_OkResponseSchema(), @@ -2565,6 +2688,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Service_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceResources_GET_responses = { "200": ServiceResources_GET_OkResponseSchema(), @@ -2573,6 +2697,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Service_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceResources_POST_responses = { "201": ServiceResources_POST_CreatedResponseSchema(), @@ -2583,6 +2708,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": ServiceResources_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceTypeResources_GET_responses = { "200": ServiceTypeResources_GET_OkResponseSchema(), @@ -2591,6 +2717,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": ServiceTypeResources_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceTypeResourceTypes_GET_responses = { "200": ServiceTypeResourceTypes_GET_OkResponseSchema(), @@ -2599,6 +2726,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": ServiceTypeResourceTypes_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ServiceResource_DELETE_responses = { "200": ServiceResource_DELETE_OkResponseSchema(), @@ -2608,12 +2736,14 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Resource_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Users_GET_responses = { "200": Users_GET_OkResponseSchema(), "401": UnauthorizedResponseSchema(), "403": Users_GET_ForbiddenResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Users_POST_responses = { "201": Users_POST_CreatedResponseSchema(), @@ -2622,6 +2752,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": Users_POST_ForbiddenResponseSchema(), "406": NotAcceptableResponseSchema(), "409": User_Check_ConflictResponseSchema(), + "500": InternalServerErrorResponseSchema(), } User_GET_responses = { "200": User_GET_OkResponseSchema(), @@ -2629,6 +2760,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } User_PUT_responses = { "200": Users_PUT_OkResponseSchema(), @@ -2637,6 +2769,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": UserGroup_GET_ForbiddenResponseSchema(), "406": NotAcceptableResponseSchema(), "409": User_Check_ConflictResponseSchema(), + "500": InternalServerErrorResponseSchema(), } User_DELETE_responses = { "200": User_DELETE_OkResponseSchema(), @@ -2645,6 +2778,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserResources_GET_responses = { "200": UserResources_GET_OkResponseSchema(), @@ -2652,6 +2786,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": UserResources_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserGroups_GET_responses = { "200": UserGroups_GET_OkResponseSchema(), @@ -2659,6 +2794,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserGroups_POST_responses = { "201": UserGroups_POST_CreatedResponseSchema(), @@ -2668,6 +2804,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": UserGroups_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserGroup_DELETE_responses = { "200": UserGroup_DELETE_OkResponseSchema(), @@ -2676,6 +2813,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserResourcePermissions_GET_responses = { "200": UserResourcePermissions_GET_OkResponseSchema(), @@ -2684,6 +2822,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Resource_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserResourcePermissions_POST_responses = { "201": UserResourcePermissions_POST_CreatedResponseSchema(), @@ -2693,6 +2832,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": UserResourcePermissions_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserResourcePermission_DELETE_responses = { "200": UserResourcePermissions_DELETE_OkResponseSchema(), @@ -2701,6 +2841,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": UserResourcePermissions_DELETE_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserServices_GET_responses = { "200": UserServices_GET_OkResponseSchema(), @@ -2708,6 +2849,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserServicePermissions_GET_responses = { "200": UserServicePermissions_GET_OkResponseSchema(), @@ -2715,6 +2857,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": UserServicePermissions_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserServiceResources_GET_responses = { "200": UserServiceResources_GET_OkResponseSchema(), @@ -2722,6 +2865,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Service_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } UserServicePermissions_POST_responses = UserResourcePermissions_POST_responses UserServicePermission_DELETE_responses = UserResourcePermission_DELETE_responses @@ -2731,6 +2875,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUser_PUT_responses = { "200": Users_PUT_OkResponseSchema(), @@ -2739,6 +2884,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": User_PUT_ForbiddenResponseSchema(), "406": NotAcceptableResponseSchema(), "409": User_PUT_ConflictResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUser_DELETE_responses = { "200": User_DELETE_OkResponseSchema(), @@ -2747,6 +2893,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserResources_GET_responses = { "200": UserResources_GET_OkResponseSchema(), @@ -2754,6 +2901,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": UserResources_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserGroups_GET_responses = { "200": UserGroups_GET_OkResponseSchema(), @@ -2761,6 +2909,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserGroups_POST_responses = { "201": UserGroups_POST_CreatedResponseSchema(), @@ -2770,6 +2919,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": UserGroups_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserGroup_DELETE_responses = { "200": UserGroup_DELETE_OkResponseSchema(), @@ -2778,6 +2928,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_CheckAnonymous_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserResourcePermissions_GET_responses = { "200": UserResourcePermissions_GET_OkResponseSchema(), @@ -2786,6 +2937,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Resource_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserResourcePermissions_POST_responses = { "201": UserResourcePermissions_POST_CreatedResponseSchema(), @@ -2794,6 +2946,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": UserResourcePermissions_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserResourcePermission_DELETE_responses = { "200": UserResourcePermissions_DELETE_OkResponseSchema(), @@ -2802,6 +2955,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": UserResourcePermissions_DELETE_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserServices_GET_responses = { "200": UserServices_GET_OkResponseSchema(), @@ -2809,6 +2963,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": User_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserServicePermissions_GET_responses = { "200": UserServicePermissions_GET_OkResponseSchema(), @@ -2816,6 +2971,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": UserServicePermissions_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserServiceResources_GET_responses = { "200": UserServiceResources_GET_OkResponseSchema(), @@ -2823,6 +2979,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Service_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } LoggedUserServicePermissions_POST_responses = LoggedUserResourcePermissions_POST_responses LoggedUserServicePermission_DELETE_responses = LoggedUserResourcePermission_DELETE_responses @@ -2831,6 +2988,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "401": UnauthorizedResponseSchema(), "403": Groups_GET_ForbiddenResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Groups_POST_responses = { "201": Groups_POST_CreatedResponseSchema(), @@ -2839,6 +2997,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": Groups_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Group_GET_responses = { "200": Group_GET_OkResponseSchema(), @@ -2847,6 +3006,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Group_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Group_PUT_responses = { "200": Group_PUT_OkResponseSchema(), @@ -2857,6 +3017,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": Group_PUT_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Group_DELETE_responses = { "200": Group_DELETE_OkResponseSchema(), @@ -2865,6 +3026,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Group_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } GroupUsers_GET_responses = { "200": GroupUsers_GET_OkResponseSchema(), @@ -2873,6 +3035,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Group_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } GroupServices_GET_responses = { "200": GroupServices_GET_OkResponseSchema(), @@ -2898,6 +3061,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Group_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } GroupResourcePermissions_POST_responses = { "201": GroupResourcePermissions_POST_CreatedResponseSchema(), @@ -2907,6 +3071,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "406": NotAcceptableResponseSchema(), "409": GroupResourcePermissions_POST_ConflictResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } GroupServicePermissions_POST_responses = GroupResourcePermissions_POST_responses GroupServicePermission_DELETE_responses = { @@ -2916,6 +3081,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": GroupServicePermission_DELETE_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } GroupResources_GET_responses = { "200": GroupResources_GET_OkResponseSchema(), @@ -2933,11 +3099,40 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "404": Group_MatchDictCheck_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "422": UnprocessableEntityResponseSchema(), + "500": InternalServerErrorResponseSchema(), } GroupResourcePermission_DELETE_responses = GroupServicePermission_DELETE_responses +RegisterGroups_GET_responses = { + "200": RegisterGroups_GET_OkResponseSchema(), + "401": UnauthorizedResponseSchema(), + "403": RegisterGroups_GET_ForbiddenResponseSchema(), + "500": InternalServerErrorResponseSchema(), +} +RegisterGroup_GET_responses = { + "200": RegisterGroup_GET_OkResponseSchema(), + "401": UnauthorizedResponseSchema(), + "404": RegisterGroup_NotFoundResponseSchema(), + "500": InternalServerErrorResponseSchema(), +} +RegisterGroup_POST_responses = { + "201": RegisterGroup_POST_CreatedResponseSchema(), + "401": UnauthorizedResponseSchema(), + "403": RegisterGroup_POST_ForbiddenResponseSchema(), + "404": RegisterGroup_NotFoundResponseSchema(), + "409": RegisterGroup_POST_ConflictResponseSchema(), + "500": InternalServerErrorResponseSchema(), +} +RegisterGroup_DELETE_responses = { + "200": RegisterGroup_DELETE_OkResponseSchema(), + "401": UnauthorizedResponseSchema(), + "403": RegisterGroup_DELETE_ForbiddenResponseSchema(), + "404": RegisterGroup_DELETE_NotFoundResponseSchema(), + "500": InternalServerErrorResponseSchema(), +} Providers_GET_responses = { "200": Providers_GET_OkResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } ProviderSignin_GET_responses = { "302": ProviderSignin_GET_FoundResponseSchema(), @@ -2946,7 +3141,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "403": ProviderSignin_GET_ForbiddenResponseSchema(), "404": ProviderSignin_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), - "500": InternalServerErrorResponseSchema() + "500": InternalServerErrorResponseSchema(), } Signin_POST_responses = { "200": Signin_POST_OkResponseSchema(), @@ -2962,6 +3157,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): Signout_GET_responses = { "200": Signout_GET_OkResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Session_GET_responses = { "200": Session_GET_OkResponseSchema(), @@ -2971,13 +3167,16 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): Version_GET_responses = { "200": Version_GET_OkResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } Homepage_GET_responses = { "200": Homepage_GET_OkResponseSchema(), "406": NotAcceptableResponseSchema(), + "500": InternalServerErrorResponseSchema(), } SwaggerAPI_GET_responses = { "200": SwaggerAPI_GET_OkResponseSchema(), + "500": InternalServerErrorResponseSchema(), } @@ -2986,6 +3185,8 @@ def generate_api_schema(swagger_base_spec): """ Return JSON Swagger specifications of Magpie REST API. + Uses Cornice Services and Schemas to return swagger specification. + :param swagger_base_spec: dictionary that specifies the 'host' and list of HTTP 'schemes' to employ. """ generator = CorniceSwagger(get_services()) @@ -2998,16 +3199,3 @@ def generate_api_schema(swagger_base_spec): for tag in json_api_spec["tags"]: tag["description"] = TAG_DESCRIPTIONS[tag["name"]] return json_api_spec - - -# use Cornice Services and Schemas to return swagger specifications -def api_schema(request): - # type: (Request) -> JSON - """ - Return JSON Swagger specifications of Magpie REST API. - """ - swagger_base_spec = { - "host": get_magpie_url(request.registry), - "schemes": [request.scheme] - } - return generate_api_schema(swagger_base_spec) diff --git a/magpie/api/swagger/__init__.py b/magpie/api/swagger/__init__.py index 372e1e97c..9f94f3ce6 100644 --- a/magpie/api/swagger/__init__.py +++ b/magpie/api/swagger/__init__.py @@ -1,7 +1,7 @@ from pyramid.security import NO_PERMISSION_REQUIRED from magpie.api import schemas as s -from magpie.api.swagger.views import api_swagger +from magpie.api.swagger.views import api_schema, api_swagger from magpie.utils import get_logger LOGGER = get_logger(__name__) @@ -11,7 +11,7 @@ def includeme(config): LOGGER.info("Adding swagger...") config.add_route(**s.service_api_route_info(s.SwaggerAPI)) config.add_route(**s.service_api_route_info(s.SwaggerGenerator)) - config.add_view(s.api_schema, route_name=s.SwaggerGenerator.name, request_method="GET", + config.add_view(api_schema, route_name=s.SwaggerGenerator.name, request_method="GET", renderer="json", permission=NO_PERMISSION_REQUIRED) config.add_view(api_swagger, route_name=s.SwaggerAPI.name, renderer="templates/swagger_ui.mako", permission=NO_PERMISSION_REQUIRED) diff --git a/magpie/api/swagger/views.py b/magpie/api/swagger/views.py index b806095cb..ccb7f3b2e 100644 --- a/magpie/api/swagger/views.py +++ b/magpie/api/swagger/views.py @@ -1,7 +1,13 @@ import os +from typing import TYPE_CHECKING from magpie.api import schemas as s from magpie.constants import MAGPIE_MODULE_DIR +from magpie.utils import get_magpie_url + +if TYPE_CHECKING: + from magpie.typedefs import JSON + from pyramid.request import Request @s.SwaggerAPI.get(tags=[s.APITag], response_schemas=s.SwaggerAPI_GET_responses) @@ -15,3 +21,16 @@ def api_swagger(request): # noqa: F811 "api_schema_path": swagger_ui_path, "api_schema_versions_dir": swagger_versions_dir} return return_data + + +@s.SwaggerGenerator.get(tags=[s.APITag], response_schemas=s.SwaggerAPI_GET_responses) +def api_schema(request): + # type: (Request) -> JSON + """ + Return JSON Swagger specifications of Magpie REST API. + """ + swagger_base_spec = { + "host": get_magpie_url(request.registry), + "schemes": [request.scheme] + } + return s.generate_api_schema(swagger_base_spec) diff --git a/magpie/ui/__init__.py b/magpie/ui/__init__.py index b6e5258a4..58c39b439 100644 --- a/magpie/ui/__init__.py +++ b/magpie/ui/__init__.py @@ -7,6 +7,7 @@ def includeme(config): LOGGER.info("Adding UI routes...") # Add all the admin ui routes - config.include("magpie.ui.login") config.include("magpie.ui.home") + config.include("magpie.ui.login") config.include("magpie.ui.management") + config.include("magpie.ui.user") diff --git a/magpie/ui/home/__init__.py b/magpie/ui/home/__init__.py index 3b81e3d7f..d70b81a50 100644 --- a/magpie/ui/home/__init__.py +++ b/magpie/ui/home/__init__.py @@ -1,31 +1,11 @@ -from pyramid.authentication import Authenticated, IAuthenticationPolicy - from magpie.utils import get_logger LOGGER = get_logger(__name__) -def add_template_data(request, data=None): - all_data = data or {} - magpie_logged_user = None - - try: - authn_policy = request.registry.queryUtility(IAuthenticationPolicy) - principals = authn_policy.effective_principals(request) - - if Authenticated in principals: - LOGGER.info("User '%s' is authenticated", request.user.user_name) - magpie_logged_user = request.user.user_name - except AttributeError: - pass - - if magpie_logged_user: - all_data.update({u"MAGPIE_LOGGED_USER": magpie_logged_user}) - return all_data - - def includeme(config): - LOGGER.info("Adding home...") + LOGGER.info("Adding UI home...") config.add_route("home", "/") + config.add_route("home_ui", "/ui") config.add_static_view("static", "static", cache_max_age=3600) config.scan() diff --git a/magpie/ui/home/static/style.css b/magpie/ui/home/static/style.css index 77768b34d..eec0a4cf8 100644 --- a/magpie/ui/home/static/style.css +++ b/magpie/ui/home/static/style.css @@ -2,7 +2,7 @@ html,body { margin:0; padding:0; border:0; - font-family: "Open Sans"; + font-family: "Open Sans", sans-serif; } .content { diff --git a/magpie/ui/home/templates/template.mako b/magpie/ui/home/templates/template.mako index f74e867cc..d295998ef 100644 --- a/magpie/ui/home/templates/template.mako +++ b/magpie/ui/home/templates/template.mako @@ -1,7 +1,7 @@ - + - Magpie Administration + Magpie @@ -17,16 +17,24 @@
-
-
+
+ +
+
+ +
-
Magpie Administration
+
Magpie ${MAGPIE_SUB_TITLE}
%if MAGPIE_LOGGED_USER: - + + %else: - + %endif
@@ -36,7 +44,7 @@ <%block name="breadcrumb"/> %if MAGPIE_LOGGED_USER: -
Logged in: ${MAGPIE_LOGGED_USER}
+ %endif diff --git a/magpie/ui/home/views.py b/magpie/ui/home/views.py index 7066cf373..a71bbc75c 100644 --- a/magpie/ui/home/views.py +++ b/magpie/ui/home/views.py @@ -1,9 +1,11 @@ from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.view import view_config -from magpie.ui.home import add_template_data +from magpie.ui.utils import BaseViews -@view_config(route_name="home", renderer="templates/home.mako", permission=NO_PERMISSION_REQUIRED) -def home_view(request): - return add_template_data(request) +class HomeViews(BaseViews): + @view_config(route_name="home", renderer="templates/home.mako", permission=NO_PERMISSION_REQUIRED) + @view_config(route_name="home_ui", renderer="templates/home.mako", permission=NO_PERMISSION_REQUIRED) + def home_view(self): + return self.add_template_data() diff --git a/magpie/ui/login/__init__.py b/magpie/ui/login/__init__.py index 23964437f..c2dfa9a49 100755 --- a/magpie/ui/login/__init__.py +++ b/magpie/ui/login/__init__.py @@ -4,7 +4,7 @@ def includeme(config): - LOGGER.info("Adding login...") + LOGGER.info("Adding UI login...") config.add_route("login", "/ui/login") config.add_route("logout", "/ui/logout") config.add_route("register", "/ui/register") diff --git a/magpie/ui/login/views.py b/magpie/ui/login/views.py index d55176e3a..d235a35f7 100755 --- a/magpie/ui/login/views.py +++ b/magpie/ui/login/views.py @@ -5,16 +5,11 @@ from pyramid.view import view_config from magpie.api import schemas -from magpie.ui.home import add_template_data -from magpie.ui.utils import check_response, request_api -from magpie.utils import get_json, get_magpie_url +from magpie.ui.utils import BaseViews, check_response, request_api +from magpie.utils import get_json -class LoginViews(object): - def __init__(self, request): - self.request = request - self.magpie_url = get_magpie_url(request.registry) - +class LoginViews(BaseViews): def request_providers_json(self): resp = request_api(self.request, schemas.ProvidersAPI.path, "GET") check_response(resp) @@ -72,7 +67,7 @@ def login(self): except Exception as exc: return HTTPInternalServerError(detail=repr(exc)) - return add_template_data(self.request, data=return_data) + return self.add_template_data(data=return_data) @view_config(route_name="logout", renderer="templates/login.mako", permission=NO_PERMISSION_REQUIRED) def logout(self): diff --git a/magpie/ui/management/__init__.py b/magpie/ui/management/__init__.py index 70670289d..f008f86b4 100644 --- a/magpie/ui/management/__init__.py +++ b/magpie/ui/management/__init__.py @@ -5,7 +5,7 @@ def includeme(config): from magpie.ui.management.views import ManagementViews - LOGGER.info("Adding management...") + LOGGER.info("Adding UI management...") config.add_route(ManagementViews.view_groups.__name__, "/ui/groups") config.add_route(ManagementViews.add_group.__name__, diff --git a/magpie/ui/management/templates/add_group.mako b/magpie/ui/management/templates/add_group.mako index 1d48c63f3..072708ffc 100644 --- a/magpie/ui/management/templates/add_group.mako +++ b/magpie/ui/management/templates/add_group.mako @@ -13,14 +13,20 @@ - + %if conflict_group_name: %elif invalid_group_name: %else: diff --git a/magpie/ui/management/templates/add_resource.mako b/magpie/ui/management/templates/add_resource.mako index 408e0d06e..fdcde3b07 100644 --- a/magpie/ui/management/templates/add_resource.mako +++ b/magpie/ui/management/templates/add_resource.mako @@ -1,10 +1,14 @@ <%inherit file="ui.home:templates/template.mako"/> <%block name="breadcrumb"> -
  • Home
  • -
  • Services
  • -
  • Service ${service_name}
  • -
  • Add Resource
  • +
  • + Home
  • +
  • + Services
  • +
  • + Service [${service_name}]
  • +
  • + Add Resource
  • New Resource

    @@ -13,19 +17,23 @@
    Group name: +

    - Conflict

    + WARNING Conflict +

    - Invalid

    + WARNING Invalid

     

    -
    -
    - + -
    -
    - +
    Resource name: +
    +
    Resource type:
    diff --git a/magpie/ui/management/templates/add_service.mako b/magpie/ui/management/templates/add_service.mako index aa23db8e9..d095ab72f 100644 --- a/magpie/ui/management/templates/add_service.mako +++ b/magpie/ui/management/templates/add_service.mako @@ -10,9 +10,9 @@
    -
    +
    Service: ${service_name} diff --git a/magpie/ui/management/templates/edit_user.mako b/magpie/ui/management/templates/edit_user.mako index 14e2f8c3d..974795d9b 100644 --- a/magpie/ui/management/templates/edit_user.mako +++ b/magpie/ui/management/templates/edit_user.mako @@ -62,15 +62,15 @@
    -
    -
    +
    + User: ${user_name} -
    - + +
    @@ -141,7 +141,7 @@
    - +
    %for group in groups:
    diff --git a/magpie/ui/management/templates/view_groups.mako b/magpie/ui/management/templates/view_groups.mako index 78bf45029..9bc55ad93 100644 --- a/magpie/ui/management/templates/view_groups.mako +++ b/magpie/ui/management/templates/view_groups.mako @@ -8,13 +8,13 @@

    Groups

    - - + diff --git a/magpie/ui/management/templates/view_services.mako b/magpie/ui/management/templates/view_services.mako index 4f7372370..8112b51b8 100644 --- a/magpie/ui/management/templates/view_services.mako +++ b/magpie/ui/management/templates/view_services.mako @@ -100,7 +100,7 @@ --> %endif -
    Group Members count
    - + diff --git a/magpie/ui/user/templates/edit_current_user.mako b/magpie/ui/user/templates/edit_current_user.mako index e801c1731..e71b62bfc 100644 --- a/magpie/ui/user/templates/edit_current_user.mako +++ b/magpie/ui/user/templates/edit_current_user.mako @@ -16,7 +16,7 @@ -->
    -
    +
    Details
    @@ -82,7 +82,7 @@
    -
    User Action
    +
    %for group in groups:
    From 67106ed5197321d4c0de04950b8ed7fe2840e427 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Tue, 28 Jul 2020 01:54:27 -0400 Subject: [PATCH 010/136] ui style changes --- magpie/ui/home/static/exclamation-circle.png | Bin 0 -> 7585 bytes .../ui/home/static/exclamation-triangle.png | Bin 0 -> 5458 bytes magpie/ui/home/static/style.css | 265 +++++++++--------- magpie/ui/home/static/warning_exclamation.png | Bin 9773 -> 0 bytes .../static/warning_exclamation_orange.png | Bin 2612 -> 0 bytes magpie/ui/home/templates/error.mako | 10 +- magpie/ui/home/templates/home.mako | 8 +- magpie/ui/home/templates/template.mako | 14 +- magpie/ui/login/templates/login.mako | 40 +-- magpie/ui/management/templates/add_group.mako | 22 +- .../ui/management/templates/add_resource.mako | 12 +- .../ui/management/templates/add_service.mako | 24 +- magpie/ui/management/templates/add_user.mako | 62 ++-- .../ui/management/templates/edit_group.mako | 74 ++--- .../ui/management/templates/edit_service.mako | 90 +++--- magpie/ui/management/templates/edit_user.mako | 86 +++--- .../ui/management/templates/tree_scripts.mako | 2 +- .../ui/management/templates/view_groups.mako | 10 +- .../management/templates/view_services.mako | 62 ++-- .../ui/management/templates/view_users.mako | 10 +- .../ui/user/templates/edit_current_user.mako | 36 +-- tests/interfaces.py | 2 +- tests/test_magpie_ui.py | 2 +- 23 files changed, 433 insertions(+), 398 deletions(-) create mode 100755 magpie/ui/home/static/exclamation-circle.png create mode 100755 magpie/ui/home/static/exclamation-triangle.png delete mode 100644 magpie/ui/home/static/warning_exclamation.png delete mode 100644 magpie/ui/home/static/warning_exclamation_orange.png diff --git a/magpie/ui/home/static/exclamation-circle.png b/magpie/ui/home/static/exclamation-circle.png new file mode 100755 index 0000000000000000000000000000000000000000..1c9a9937a7f0fe0030469693641541ea80c168f5 GIT binary patch literal 7585 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 G*M9-|O~PdW literal 0 HcmV?d00001 diff --git a/magpie/ui/home/static/style.css b/magpie/ui/home/static/style.css index cf95f9cb4..77e0b2bf3 100644 --- a/magpie/ui/home/static/style.css +++ b/magpie/ui/home/static/style.css @@ -9,13 +9,14 @@ html,body { .theme { border: 1px solid #2C8F30; background-color: #4CAF50; + color: white; } .content { margin: 5em; } -.img_button { +.img-button { padding: 0.7em; margin: 1em 0 1em; } @@ -31,12 +32,12 @@ a:hover { text-decoration: underline; } -input[type="submit"].img_button.disabled, -input[type="submit"].img_button.disabled:hover, -input[type="submit"].img_button.disabled:focus, -input[type="button"].img_button.disabled, -input[type="button"].img_button.disabled:hover, -input[type="button"].img_button.disabled:focus, +input[type="submit"].img-button.disabled, +input[type="submit"].img-button.disabled:hover, +input[type="submit"].img-button.disabled:focus, +input[type="button"].img-button.disabled, +input[type="button"].img-button.disabled:hover, +input[type="button"].img-button.disabled:focus, input[type="submit"].button.disabled, input[type="submit"].button.disabled:hover, input[type="submit"].button.disabled:focus, @@ -50,8 +51,8 @@ input[type="button"].button.disabled:focus { cursor:not-allowed; } -input[type="button"].img_button.delete, -input[type="submit"].img_button.delete, +input[type="button"].img-button.delete, +input[type="submit"].img-button.delete, input[type="button"].button.delete, input[type="submit"].button.delete { border: 1px solid #8B3A3A; @@ -61,10 +62,10 @@ input[type="submit"].button.delete { cursor:pointer; } -input[type="button"].img_button.delete:hover, -input[type="button"].img_button.delete:focus, -input[type="submit"].img_button.delete:hover, -input[type="submit"].img_button.delete:focus, +input[type="button"].img-button.delete:hover, +input[type="button"].img-button.delete:focus, +input[type="submit"].img-button.delete:hover, +input[type="submit"].img-button.delete:focus, input[type="button"].button.delete:hover, input[type="button"].button.delete:focus, input[type="submit"].button.delete:hover, @@ -72,10 +73,10 @@ input[type="submit"].button.delete:focus { background-color: #EE6363; } -input[type="button"].img_button.warning, -input[type="submit"].img_button.warning, -input[type="button"].button.warning, -input[type="submit"].button.warning { +input[type="button"].img-button-warning, +input[type="submit"].img-button-warning, +input[type="button"].button-warning, +input[type="submit"].button-warning { border: 1px solid #8B3A3A; background-color: #CD6600; color: white; @@ -83,10 +84,10 @@ input[type="submit"].button.warning { cursor:pointer; } -input[type="button"].img_button.warning:hover, -input[type="button"].img_button.warning:focus, -input[type="submit"].img_button.warning:hover, -input[type="submit"].img_button.warning:focus, +input[type="button"].img-button.warning:hover, +input[type="button"].img-button.warning:focus, +input[type="submit"].img-button.warning:hover, +input[type="submit"].img-button.warning:focus, input[type="button"].button.warning:hover, input[type="button"].button.warning:focus, input[type="submit"].button.warning:hover, @@ -94,8 +95,8 @@ input[type="submit"].button.warning:focus { background-color: #FFAA33; } -input[type="button"].img_button.cancel, -input[type="submit"].img_button.cancel, +input[type="button"].img-button.cancel, +input[type="submit"].img-button.cancel, input[type="button"].button.cancel, input[type="submit"].button.cancel { border: 1px solid #808080; @@ -105,8 +106,8 @@ input[type="submit"].button.cancel { a.tab:link, a.tab:visited, -.img_button, -.admin_button, +.img-button, +.admin-button, input[type="submit"], input[type="submit"].button.normal { vertical-align: center; @@ -115,15 +116,19 @@ input[type="submit"].button.normal { color: #FFFFFF; } -a.tab:hover, a.tab:active, -.img_button:hover, .img_button:focus, -.admin_button:hover, .admin_button:active, -input[type="submit"]:hover, input[type="submit"]:focus { +a.tab:hover, +a.tab:active, +.img-button:hover, +.img-button:focus, +.admin-button:hover, +.admin-button:active, +input[type="submit"]:hover, +input[type="submit"]:focus { background-color: #5CBF60; text-decoration: none; } -.img_button, +.img-button, input[type="button"], input[type="submit"] { border-radius: 3px; @@ -138,7 +143,7 @@ input[type="submit"] { height: 1.5em; } -.img_button>img { +.img-button>img { height: 1em; } @@ -155,9 +160,10 @@ td, th { table thead { color: white; + font-weight: bold; } -.checkbox_align label { +.checkbox-align label { display: block; /*float: left;*/ padding-left: 0.2em; @@ -165,58 +171,61 @@ table thead { padding-bottom: 1em; white-space: nowrap; } -.checkbox_align input { +.checkbox-align input { vertical-align: middle; } -.checkbox_align label span { +.checkbox-align label span { vertical-align: middle; } /*---Panel Information Box---*/ -.panel_box { - background-color: #fff; +.panel-box { + background-color: #FFFFFF; border: 1px solid; border-radius: 4px; margin-top: 10px; padding: 1px; } -.panel_heading { - background-color: #F5F5F5; - border-bottom: 1px solid transparent; +.panel-heading { + border-bottom: 1px solid #333333; border-top-left-radius: 3px; border-top-right-radius: 3px; - color: #333; padding: 10px 15px; + font-weight: bold; +} + +.subsection { + background-color: #CCCCCC; } -.panel_heading_button { +.panel-heading-button { float: right!important; margin-top: -3px; } -.panel_body { +.panel-body { padding: 15px; } -.panel_line { +.panel-line { margin: 0.5em; } -.panel_line input[type="submit"] { +.panel-line input[type="submit"] { height: auto; } -.panel_title { +.panel-title { font-weight: bold; } -.panel_entry { +.panel-entry { font-weight: bold; } -.panel_value { +.panel-value { } /*---Labels---*/ @@ -235,19 +244,19 @@ table thead { background-color: #5BC0DE; /*info default*/ } -.label.danger { +.label-danger { background-color: #CD0000; } -.label.warning { +.label-warning { background-color: #F0AD4E; } -.label.info { +.label-info { background-color: #5BC0DE; } -.label.success { +.label-success { background-color: #4CAF50; } @@ -263,59 +272,47 @@ table thead { background-color: #2196F3; /*info default*/ } -.alert.danger { +.alert-danger { background-color: #F44336; } -.alert.warning { +.alert-warning { background-color: #FF9900; } -.alert.info { +.alert-info { background-color: #2196F3; } -.alert.success { +.alert-success { background-color: #4CAF50; } -.alert.danger.visible { - display: block; /*override hidden default*/ -} - -.alert.warning.visible { - display: block; /*override hidden default*/ -} - -.alert.info.visible { +.alert-visible { display: block; /*override hidden default*/ } -.alert.success.visible { - display: block; /*override hidden default*/ -} - -.alert_title { +.alert-title { margin: 0; } -.alert_title.danger { +.alert-title-danger { color: #660000; } -.alert_title.warning { +.alert-title-warning { color: #994400; } -.alert_title.info { +.alert-title-info { color: #FFFFFF; } -.alert_title.success { +.alert-title-success { color: #FFFFFF; } -.alert_button { +.alert-button { margin-left: 15px; color: white; font-weight: bold; @@ -326,48 +323,58 @@ table thead { transition: 0.3s; } -.alert_button:hover { +.alert-button:hover { color: black; } -.alert_form_error>img { +.alert-form-error>img { width: 1.5em; height: 1.5em; margin: -0.25em 0 0 0; + mix-blend-mode: multiply; } -.alert_form_error { +.alert-form-error { width: 100px; - color: red; display: inline-flex; float: left; text-align: center; margin: 0.25em 0 0 0; } +.icon-error { + background-color: #CD0000; + mix-blend-mode: multiply; +} + +.icon-warning { + background-color: #FF9900; + mix-blend-mode: multiply; +} + /*---View users & groups pages ---*/ /* note: use explicit row classes instead of 'tr:nth-child(even)' because generated code doesn't handle it well */ -table.simple_list tr.list_row_odd { +table.simple-list tr.list-row-odd { background-color: white; } -table.simple_list tr.list_row_even { +table.simple-list tr.list-row-even { background-color: #F2F2F2; } -table.simple_list tr, -table.simple_list td:first-child, -table.simple_list th:first-child { +table.simple-list tr, +table.simple-list td:first-child, +table.simple-list th:first-child { width: 75%; text-align: left; } -table.simple_list td:not(:first-child), -table.simple_list th:not(:first-child) { +table.simple-list td:not(:first-child), +table.simple-list th:not(:first-child) { text-align: center; } -table.simple_list input[type="submit"] { +table.simple-list input[type="submit"] { margin: 0 0.5em; } @@ -377,7 +384,7 @@ table.simple_list input[type="submit"] { background: white; } -.tree_header { +.tree-header { padding: 5em 0 2em; font-weight: bold; } @@ -396,12 +403,12 @@ table.simple_list input[type="submit"] { cursor:pointer; } -div.tree_item { +div.tree-item { display:block; float:left; } -div.perm_title { +div.perm-title { width:3em; float:right; text-align: left; @@ -412,11 +419,11 @@ div.perm_title { -o-transform: rotate(-70deg); /* Opera */ } -.tree_button.goto_service { +.tree-button.goto-service { margin: 0.1em 0 0 -4em; } -.tree_item_message { +.tree-item-message { text-align: left; color: gray; display: inline-flex; @@ -424,20 +431,20 @@ div.perm_title { margin: 0.25em 0 0 0; } -.tree_item_message>img { +.tree-item-message>img { width: 1.5em; height: 1.5em; margin: -0.15em 0 0 0; } -div.perm_checkbox { +div.perm-checkbox { width:3em; float:right; text-align: left; margin: 0.1em 0 0 0; } -div.tree_button { +div.tree-button { width:5em; float:right; text-align: center; @@ -503,30 +510,30 @@ ul.breadcrumb li a:hover { float: right; } -#title_header { +#title-header { display: inline-block; position: relative; left: 0.25em; } -#image_container { +#image-container { float: left; position: relative; width: 64px; height: 64px; } -#image_background { +#image-background { width: inherit; height: inherit; } -#image_background>img { +#image-background>img { width: inherit; height: inherit; } -#image_overlay { +#image-overlay { position: absolute; width: inherit; height: inherit; @@ -534,7 +541,7 @@ ul.breadcrumb li a:hover { top: 10px; } -#image_overlay>img { +#image-overlay>img { width: inherit; height: inherit; } @@ -547,29 +554,29 @@ ul.breadcrumb li a:hover { 60em number from margins and button sizes */ @media only screen and (max-width: 60em) { - .admin_content { + .admin-content { display: inline; } } -.admin_content { +.admin-content { width: 100%; text-align: center; } -.admin_button { +.admin-button { margin: auto; width: 100%; } -.admin_button, -.admin_button:focus, -.admin_button:hover, -.admin_button:active { +.admin-button, +.admin-button:focus, +.admin-button:hover, +.admin-button:active { color: #000000; } -.admin_button { +.admin-button { display:inline-block; box-sizing: border-box; width: 10em; @@ -584,12 +591,12 @@ ul.breadcrumb li a:hover { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } -.admin_button>img { +.admin-button>img { height: 60%; padding: 5%; } -.button_link { +.button-link { border: none; outline: none; background: none; @@ -601,7 +608,7 @@ ul.breadcrumb li a:hover { font-size: inherit; } -.img_forbidden { +.img-forbidden { width: 24px; height: 24px; vertical-align: middle; @@ -611,29 +618,29 @@ ul.breadcrumb li a:hover { text-align: center; } -table.request_details { +table.request-details { text-align: left; width: 60%; margin-left:auto; margin-right:auto; } -table.request_details td:first-child { +table.request-details td:first-child { white-space: nowrap; } -table.request_details thead tbody td th { +table.request-details thead tbody td th { text-align: left; } /*---Service tabs---*/ -.tabs_panel { +.tabs-panel { background: #DDDDDD; padding: 1px; } -.current_tab_panel { +.current-tab-panel { background: white; padding: 1em; margin: 0; @@ -641,8 +648,8 @@ table.request_details thead tbody td th { min-height: 10em; } -a.current_tab:link, -a.current_tab:visited { +a.current-tab:link, +a.current-tab:visited { background-color: white; color: black; padding: 1em 1em 1.4em 1em; @@ -662,28 +669,28 @@ a.tab:visited { /*---service details---*/ -table.service_details { +table.service-details { table-layout: fixed; } -/*---field_form---*/ +/*---field form---*/ -.new_item_form { +.new-item-form { margin: auto; width: 30%; min-width: 500px; } -table.fields_table th, -table.fields_table td { +table.fields-table th, +table.fields-table td { border: 0; } -table.fields_table td.centered { +table.fields-table td.centered { text-align: center; } -table.fields_table input[type="radio"] { +table.fields-table input[type="radio"] { margin-left: 2em; } @@ -691,9 +698,9 @@ table.fields_table input[type="radio"] { support different browsers using an higher level container ---*/ -select.equal_width, -option.equal_width, -input.equal_width { +select.equal-width, +option.equal-width, +input.equal-width { width: 250px; /*-webkit-box-sizing: border-box;*/ -moz-box-sizing: border-box; @@ -701,7 +708,7 @@ input.equal_width { float: left; } -div.input_container { +div.input-container { width: 100%; height:100%; -webkit-box-sizing: border-box; diff --git a/magpie/ui/home/static/warning_exclamation.png b/magpie/ui/home/static/warning_exclamation.png deleted file mode 100644 index 08accf26efbf010b64026d21b765c2d443c6b0f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9773 zcmeHt=UY=**Y`<65m2xIDhP~)!4U)-ARQ|rQk0McLy0g_Bs7@-CWM4x6d6RUAku6I zBmokUZb-xtR1_3E^rk2{hpHG#LXvlLzkk8=;rZ}f*PUEkC+D2C*IN6x%3gbIQa#+9 zR;lW!LJ+jd`Pku85QGLl(a;JdaQPYkQ4B6h0WMC5A^Gz69>3@&c%l+@%sU2x)YdKk zp`g3D8^A;5Kb&2UD1TDZ)!M5%jgR0%&@Yhl;RD$C-pPRr9}DpZ#w638ht?ikyLQFq zYkysHymLT^IwL!aXi?o!aN+mm#-ePa}yR`j~ z<~8OotI-N8ekZ*;cJLC*dOm~xWW}93*dI7}qkTc;pqDd&9X~W=HQgf!c98Vc1AYF_ zen=VFhoP2W9FgS;cPxX9c?r z8|+n(&+sMCAJ#px7Sw&=)GcK`^mzODyU{&Jcv@qQ_D&GtGZ2tinRtvci`+pv;Scam zwlwowvrU|zsvEoYPS#(>^1f^v#5m{Zftm!J3Kb?AGPe6>@V@46hu`gm2EGHe)_){w zF+wO&fp6%r&tOenl$!`fTwo(s8?NXb@~osa@91tw6Cxle#xzm|U!O(E{b1j*!KZ|ARDLjO<8ypGpzdpv0|UeR9XZ9!L?&Jo{eY9JqNfuC07(dY>6YGuM6}p4o2R zYcH>R#LFLE2McgU`04DX2ZtO{OgsDTNEtPJAo9eJoFoz=rs?p!vUA9bTwi*G{h^sS zxPGicsqYrr10q%ToJ?`0u$yklGH^q?4QA%Ax0=q2kT96W60q$3@~qM+ieQ$yiIoge z;5oVJtg&>OZ2KViIHRq{k6sv^Hax96t~?m-zRYZ^lFSD0&bi8-@)&pu`xFW$s>~y_ z5*|=#BBPG@iPav^u(gJm$ZE#7g^=qi;zk6#O{S6Lqd#TH%Z}{|Kn~vK+jK`abL=;` zLZ?H*xwMz%Um^>j(djMQR{$&B)mW-#HL$6KKvn@v2^H1DM7D-|pi3YS?winHF+lBa zle@!=;s*a6VR!{u6)$RLRY&*j)d2BNY$;zSV?ydPvZmqT`A9~dZ|DB+GkLHvyJNx_ zh-Oo2tJ?-C0wfg1=*-Xa<+bf|O=pfUaMJHsgVidkkV#1#{D+`7tYgEB0nv`tyJKV7 z%L}jp-pJ3xz&gs-Zv`NEttEd>%iu2*c-YnO92_RDnBgMJWKXRrEVaXbq9L_&xayFJ z&iUe|#)jm=##MhD6?N)~9=nPfLUT#Wh1=eC>&SFlrDk`;f5kPKFq zt0*)Yz}>WJ$+ApF6n$wr&LA8v@-oXqv>pKe(zRbOz~>PL_Vmp?Suib^h0^RUv}{-4 zUb9GLD(d~ToGd23Lo8WwUQQd9yw}pahN^4b@?p=iVvM}~rFIZB+#Al;4cAXwN-T?R zGsx^2XRl+;TZgPrgvu7*$X_8=@Q7~P&$ego-l$-#5xE>6xlva&33$iGCB`H1OT@mN zr09#2x{4ffEiR*>z~P}Hs-I|GPkjL#M-@FN@iSD_ggP(GI6S6a5IKf&kCqkt5$(cs znl)ZTjQs}9U3Lj8_qKb?YaAMGtLr&!$D{Ufrp5wR?u9rP!zm;0M1LYhY`S6NU7HG? z>9E4ddl2_Y5G5qgvp_Ohn^@ES5+<^YPE`7C+75Aghviau{K;)IpBLrF3`+FP;i7+@ zmj$hY>c?+R8nNPZmOc&ff2nt(v~v<2!2pv=<8yLv>G8lq!A7H+*lLc9jLXQbb!{?@ z$mO!j#2O`jSm~X4or$L|qXGxB{H9+I0`Z#3a$jAi2ZMN>D8BDWK94$w4Nk)-GLIN6 z3CgKwDZ!z_RaK=_qUddj{Z@60zs(`?osqD+u!fhsd00a?$VGGw$q1si9Y(8KP8=5}a8-Haov-$2@Y0#2R1SQC}k6MQ$1FLt;u{>@p~ z_)cM|lQWl%#Jx<~2w6~P7!ji1orMQ`1> _6h-eAx^h^|q^wZW}tsAox@lCkE37 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 rj*+^cuVjHsNyD2xf5smsH?CHEoBPh;Pk6!i{~
    - FORBIDDEN + FORBIDDEN Forbidden Access.
    @@ -15,13 +15,13 @@ %if error_request:
    - ()
    - + @@ -34,7 +34,7 @@
    diff --git a/magpie/ui/home/templates/home.mako b/magpie/ui/home/templates/home.mako index af9afd115..d9032ecf3 100644 --- a/magpie/ui/home/templates/home.mako +++ b/magpie/ui/home/templates/home.mako @@ -4,16 +4,16 @@
  • Home
  • -
    - +
    +
    Edit Users
    - +
    Edit Groups
    - +
    Edit Services
    diff --git a/magpie/ui/home/templates/template.mako b/magpie/ui/home/templates/template.mako index 6f8fb7d35..33c5f9bb2 100644 --- a/magpie/ui/home/templates/template.mako +++ b/magpie/ui/home/templates/template.mako @@ -16,24 +16,24 @@
    -
    -
    +
    +
    -
    +
    -
    Magpie ${MAGPIE_SUB_TITLE}
    +
    Magpie ${MAGPIE_SUB_TITLE}
    %if MAGPIE_LOGGED_USER: - - %else: - %endif
    diff --git a/magpie/ui/login/templates/login.mako b/magpie/ui/login/templates/login.mako index cbbecd5db..e6b397872 100755 --- a/magpie/ui/login/templates/login.mako +++ b/magpie/ui/login/templates/login.mako @@ -6,8 +6,8 @@ %if invalid_credentials: -
    -

    Invalid Credentials!

    +
    +

    Invalid Credentials!

    Incorrect username or password.

    @@ -17,8 +17,8 @@
    %elif error: -
    -

    Login Error!

    +
    +

    Login Error!

    Login procedure generated an error.

    @@ -31,32 +31,32 @@

    Log in

    -
    +

    Magpie

    - +
    - + - +
    Username: -
    +

     

     

    Password: -
    +

     

     

    @@ -67,27 +67,27 @@ -
    +

    External SignIn

    - +
    - + - +
    Username:

     

     

    Provider: -
    +

     

     

    diff --git a/magpie/ui/management/templates/add_group.mako b/magpie/ui/management/templates/add_group.mako index 072708ffc..85818377c 100644 --- a/magpie/ui/management/templates/add_group.mako +++ b/magpie/ui/management/templates/add_group.mako @@ -8,9 +8,9 @@

    Add Group

    - + - +
    %if conflict_group_name: - %elif invalid_group_name: - %else: - + %endif - + + +
    Group name:

    - WARNING

    + ERROR Conflict

    - WARNING

    + ERROR Invalid

     

     

    + +
    diff --git a/magpie/ui/management/templates/add_resource.mako b/magpie/ui/management/templates/add_resource.mako index fdcde3b07..fe9c29fda 100644 --- a/magpie/ui/management/templates/add_resource.mako +++ b/magpie/ui/management/templates/add_resource.mako @@ -13,21 +13,21 @@

    New Resource

    -
    - + +
    Resource name: -
    Resource type: