Skip to content

Commit 1538215

Browse files
committedJul 28, 2020
force non-default secret/admin-credentials (relates to #229) + update & fix doc (#337) + add checks for udpate special users (#164)
1 parent e760638 commit 1538215

File tree

14 files changed

+88
-37
lines changed

14 files changed

+88
-37
lines changed
 

‎.pylintrc

+4-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ disable=C0111,missing-docstring,
9393
# either give multiple identifier separated by comma (,) or put this option
9494
# multiple time (only on the command line, not in the configuration file where
9595
# it should appear only once). See also the "--disable" option for examples.
96-
enable=c-extension-no-member
96+
enable=
97+
c-extension-no-member
98+
W0122
99+
W0123
97100

98101

99102
[REPORTS]

‎HISTORY.rst

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Features / Changes
2323
specified with ``MAGPIE_CONFIG_PATH`` environment variable or ``magpie.config_path`` setting (example in ``configs``).
2424
* Add disabled checkboxes for UI rendering of non-editable items
2525
(relates to `#164 <https://github.com/Ouranosinc/Magpie/issues/164>`_).
26+
* Configuration parameters ``MAGPIE_SECRET``, ``MAGPIE_ADMIN_USER`` and ``MAGPIE_ADMIN_PASSWORD`` now require explicit
27+
definitions (either by environment variable or INI settings) to avoid using defaults for security purposes.
2628

2729
Bug Fixes
2830
~~~~~~~~~~~~~~~~~~~~~

‎config/magpie.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ magpie.port = 2001
3030
magpie.url = http://localhost:2001
3131
magpie.max_restart = 5
3232
# This secret should be the same in Twitcher !
33-
magpie.secret = seekrit
33+
magpie.secret =
3434
magpie.push_phoenix = true
3535
magpie.config_path =
3636

‎docs/configuration.rst

+20-10
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ remain available as described at the start of the `Configuration`_ section.
288288
| Secret value employed to encrypt user authentication tokens.
289289
| **Important Note:**
290290
| Changing this value at a later time will cause previously created user tokens to be invalidated.
291-
It is **strongly** recommended to change this value before proceeding to user accounts and permissions creation
292-
in your `Magpie` instance.
293-
| (Default: ``"seekrit"``)
291+
This value **MUST** be defined before starting the application in order to move on to user accounts and permissions
292+
creation in your `Magpie` instance. The application will quit with an error if this value cannot be found.
293+
| (Default: None)
294294
295295
- | ``MAGPIE_COOKIE_NAME``
296296
| Identifier of the cookie that will be used for reading and writing in the requests from login and for
@@ -303,15 +303,25 @@ remain available as described at the start of the `Configuration`_ section.
303303
304304
- | ``MAGPIE_ADMIN_USER``
305305
| Name of the default 'administrator' generated by the application.
306-
| **Note:**
307-
| This user is required for initial launch of the application to avoid being 'looked out' as routes for creating new
306+
| **Important Notes:**
307+
| This user is required for initial launch of the application to avoid being 'locked out' as routes for creating new
308308
users require administrative permissions and access rights. It should be used as a first login method to setup other
309-
accounts. It will also be used by other `Magpie` internal operations such as service synchronization and setup
310-
during the application startup. If this user is missing, it is automatically re-created on following start.
309+
accounts. It is afterwards recommended to employ other user accounts with ``MAGPIE_ADMIN_GROUP`` membership to
310+
accomplish administrative management operations.
311+
| This value **MUST** be defined before starting the application in order to move on any other operation in your
312+
`Magpie` instance. The application will quit with an error if this value cannot be found. Also, no defaults are
313+
applied to motivate the developer to configured new instances with server-specific and strong credentials.
314+
| If this user is missing, it is automatically recreated on following start. The best way to invalidate this user's
315+
credentials is therefore to completely remove its entry it from the database so it gets regenerated from updated
316+
configuration values. Note also that modifying this value without actually updating the user entry in the database
317+
could cause other operations to fail drastically since this special user will be employed by other `Magpie` internal
318+
operations such as service synchronization and setup during the application startup.
311319
| (Default: ``"admin"``)
312320
313321
- | ``MAGPIE_ADMIN_PASSWORD``
314322
| Password of the default 'administrator' generated by the application.
323+
| **Important Notes:**
324+
| This parameter is required in order for the `Magpie` instance to start. See details in above ``MAGPIE_ADMIN_USER``.
315325
| (Default: ``"qwerty"``)
316326
317327
- | ``MAGPIE_ADMIN_EMAIL``
@@ -326,7 +336,7 @@ remain available as described at the start of the `Configuration`_ section.
326336
higher level permissions on this group to ease the management process of granted access to all their members.
327337
| (Default: ``"administrators"``)
328338
329-
- | ``MAGPIE_ADMIN_PERMISSION``
339+
- | ``MAGPIE_ADMIN_PERMISSION`` [constant]
330340
| Name of the permission used to represent highest administration privilege in the application.
331341
| Except for some public routes, most API and UI paths will require the user to have this permission (either with
332342
direct permission or by inherited group permission) to be granted access to view and edit content.
@@ -372,7 +382,7 @@ remain available as described at the start of the `Configuration`_ section.
372382
| This value should not be greater then the token length used to identify a user to preserve some utility behaviour.
373383
| (Default: ``64``)
374384
375-
- | ``MAGPIE_LOGGED_USER``
385+
- | ``MAGPIE_LOGGED_USER`` [constant]
376386
| Keyword used to define route resolution using the currently logged in user. This value allows, for example,
377387
retrieving the user details of the logged user with ``GET /users/${MAGPIE_LOGGED_USER}`` instead of having to
378388
find explicitly the ``GET /users/<my-user-id>`` variant. User resolution is done using the authentication cookie
@@ -384,7 +394,7 @@ remain available as described at the start of the `Configuration`_ section.
384394
unauthorized response if using is ID in the path if he doesn't have administrator privilege.
385395
| (Default: ``"current"``)
386396
387-
- | ``MAGPIE_DEFAULT_PROVIDER``
397+
- | ``MAGPIE_DEFAULT_PROVIDER`` [constant]
388398
| Name of the provider used for local login. This represents the identifier that will be set to define who to
389399
differentiate between a local sign-in procedure and a dispatched one to one of the known `External Providers`_.
390400
| *The default is the value of the internal package used to manage user permissions.*

‎magpie/adapter/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def owsproxy_config(self, container):
160160
config = self.configurator_factory(container)
161161
owsproxy_defaultconfig(config) # let Twitcher configure the rest normally
162162

163-
def configurator_factory(self, container): # noqa: N805
163+
def configurator_factory(self, container): # noqa: N805, R0201
164164
# type: (AnySettingsContainer) -> Configurator
165165
settings = get_settings(container)
166166
set_cache_regions_from_settings(settings)

‎magpie/api/management/user/user_views.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,18 @@ def update_user_view(request):
7373
update_username = user.user_name != new_user_name and new_user_name is not None
7474
if update_username:
7575
logged_user_name = get_constant("MAGPIE_LOGGED_USER", request)
76-
ax.verify_param(new_user_name, param_compare=logged_user_name, not_equal=True,
77-
http_error=HTTPBadRequest, param_name=user_key, content={user_key: logged_user_name},
78-
msg_on_fail=s.Service_PUT_BadRequestResponseSchema_ReservedKeyword.description)
76+
always_forbidden_user_names = [
77+
get_constant("MAGPIE_ADMIN_USER", request),
78+
get_constant("MAGPIE_ANONYMOUS_USER", request)
79+
]
80+
ax.verify_param(user.user_name, not_in=True, with_param=False, # avoid leaking username details
81+
param_compare=always_forbidden_user_names, param_name=user_key,
82+
http_error=HTTPBadRequest, content={user_key: logged_user_name},
83+
msg_on_fail=s.User_PUT_ForbiddenResponseSchema.description)
84+
ax.verify_param(new_user_name, not_in=True, with_param=False, # avoid leaking username details
85+
param_compare=[logged_user_name] + always_forbidden_user_names, param_name=user_key,
86+
http_error=HTTPBadRequest, content={user_key: logged_user_name},
87+
msg_on_fail=s.User_PUT_ForbiddenResponseSchema.description)
7988
update_password = user.user_password != new_password and new_password is not None
8089
update_email = user.email != new_email and new_email is not None
8190
ax.verify_param(any([update_username, update_password, update_email]), is_true=True, http_error=HTTPBadRequest,
@@ -120,6 +129,10 @@ def delete_user_view(request):
120129
Delete a user by name.
121130
"""
122131
user = ar.get_user_matchdict_checked_or_logged(request)
132+
ax.verify_param(user.user_name, not_in=True, with_param=False, # avoid leaking username details
133+
param_compare=[get_constant("MAGPIE_ADMIN_USER", request),
134+
get_constant("MAGPIE_ANONYMOUS_USER", request)],
135+
http_error=HTTPForbidden, msg_on_fail=s.User_DELETE_ForbiddenResponseSchema.description)
123136
ax.evaluate_call(lambda: request.db.delete(user), fallback=lambda: request.db.rollback(),
124137
http_error=HTTPForbidden, msg_on_fail=s.User_DELETE_ForbiddenResponseSchema.description)
125138
return ax.valid_http(http_success=HTTPOk, detail=s.User_DELETE_OkResponseSchema.description)

‎magpie/api/requests.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ def get_user(request, user_name_or_token=None):
141141

142142
def get_user_matchdict_checked_or_logged(request, user_name_key="user_name"):
143143
# type: (Request, Str) -> models.User
144+
"""
145+
Obtains either the explicit or logged user user specified in the request path variable.
146+
147+
:returns found user.
148+
:raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request.
149+
:raises HTTPNotFound: if the specified user name or logged user keyword does not correspond to any existing user.
150+
"""
144151
logged_user_name = get_constant("MAGPIE_LOGGED_USER", settings_container=request)
145152
logged_user_path = s.UserAPI.path.replace("{" + user_name_key + "}", logged_user_name)
146153
if user_name_key not in request.matchdict and request.path_info.startswith(logged_user_path):
@@ -154,7 +161,7 @@ def get_user_matchdict_checked(request, user_name_key="user_name"):
154161
155162
:returns: found user.
156163
:raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request.
157-
:raises HTTPNotFound: if the specified user name or token does not correspond to any existing user.
164+
:raises HTTPNotFound: if the specified user name does not correspond to any existing user.
158165
"""
159166
user_name = get_value_matchdict_checked(request, user_name_key)
160167
return get_user(request, user_name)

‎magpie/api/schemas.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1456,7 +1456,7 @@ class User_PUT_BadRequestResponseSchema(colander.MappingSchema):
14561456

14571457

14581458
class User_PUT_ForbiddenResponseSchema(colander.MappingSchema):
1459-
description = "Failed user verification with db."
1459+
description = "User name update not allowed."
14601460
header = HeaderResponseSchema()
14611461
body = BaseResponseBodySchema(code=HTTPForbidden.code, description=description)
14621462

@@ -1513,7 +1513,7 @@ class User_DELETE_OkResponseSchema(colander.MappingSchema):
15131513

15141514

15151515
class User_DELETE_ForbiddenResponseSchema(colander.MappingSchema):
1516-
description = "Delete user by name refused by db."
1516+
description = "User could not be deleted."
15171517
header = HeaderResponseSchema()
15181518
body = BaseResponseBodySchema(code=HTTPForbidden.code, description=description)
15191519

‎magpie/app.py

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def main(global_config=None, **settings): # noqa: F811
5252
# with a new engine class and logging settings don't get re-evaluated/applied
5353
db_session = get_db_session_from_config_ini(config_ini, settings_override=sa_settings)
5454

55+
print_log("Validate settings that require explicit definitions...", LOGGER)
56+
for req_config in ["MAGPIE_SECRET", "MAGPIE_ADMIN_USER", "MAGPIE_ADMIN_PASSWORD"]:
57+
get_constant(req_config, settings_container=settings, raise_missing=True, raise_not_set=True)
58+
5559
print_log("Register default users...", LOGGER)
5660
register_default_users(db_session=db_session, settings=settings)
5761

‎magpie/constants.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ def _get_default_log_level():
8383
# variables from magpie.env
8484
# ===========================
8585
MAGPIE_URL = os.getenv("MAGPIE_URL", None)
86-
MAGPIE_SECRET = os.getenv("MAGPIE_SECRET", "seekrit")
86+
MAGPIE_SECRET = os.getenv("MAGPIE_SECRET", "")
8787
MAGPIE_COOKIE_NAME = os.getenv("MAGPIE_COOKIE_NAME", "auth_tkt")
8888
MAGPIE_COOKIE_EXPIRE = os.getenv("MAGPIE_COOKIE_EXPIRE", None)
89-
MAGPIE_ADMIN_USER = os.getenv("MAGPIE_ADMIN_USER", "admin")
90-
MAGPIE_ADMIN_PASSWORD = os.getenv("MAGPIE_ADMIN_PASSWORD", "qwerty")
89+
MAGPIE_ADMIN_USER = os.getenv("MAGPIE_ADMIN_USER", "")
90+
MAGPIE_ADMIN_PASSWORD = os.getenv("MAGPIE_ADMIN_PASSWORD", "")
9191
MAGPIE_ADMIN_EMAIL = "{}@mail.com".format(MAGPIE_ADMIN_USER)
9292
MAGPIE_ADMIN_GROUP = os.getenv("MAGPIE_ADMIN_GROUP", "administrators")
9393
MAGPIE_ANONYMOUS_USER = os.getenv("MAGPIE_ANONYMOUS_USER", "anonymous")
@@ -176,10 +176,10 @@ def get_constant(constant_name, # type: Str
176176
:param settings_name: alternative name for `settings` if specified
177177
:param default_value: default value to be returned if not found anywhere, and exception raises are disabled.
178178
:param raise_missing: raise exception if key is not found anywhere
179-
:param print_missing: print message if key is not found anywhere, return `None`
180-
:param raise_not_set: raise an exception if the found key is None, search until last case if previous are `None`
179+
:param print_missing: print message if key is not found anywhere, return ``None``
180+
:param raise_not_set: raise an exception if the found key is ``None``, search until last case if others are ``None``
181181
:returns: found value or `default_value`
182-
:raises: according message based on options (by default raise missing/`None` value)
182+
:raises: according message based on options (by default raise missing/``None`` value)
183183
"""
184184
from magpie.utils import get_settings, raise_log, print_log # pylint: disable=C0415 # avoid circular import error
185185

‎magpie/ui/home/static/style.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ table tr:nth-child(even) {
183183
}
184184

185185
.panel_heading {
186-
background-color: #f5f5f5;
186+
background-color: #F5F5F5;
187187
border-bottom: 1px solid transparent;
188188
border-top-left-radius: 3px;
189189
border-top-right-radius: 3px;

‎magpie/ui/user/templates/edit_current_user.mako

+5-2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111

1212

1313
<div class="panel_box">
14-
<!-- # FIXME: should we have a route that allows user to unregister itself?
14+
<!-- # FIXME: implement with better warning (alert), API route supports operation
15+
(admin is immediate delete, but we should confirm user self-delete beforehand just in case)
16+
-->
17+
<!--
1518
<form id="delete_user" action="${request.path}" method="post">
1619
<div class="panel_heading">
1720
<span class="panel_title">User: </span>
1821
<span class="panel_value">${user_name}</span>
1922
<span class="panel_heading_button">
20-
<input type="submit" value="Delete" name="delete" class="button delete">
23+
<input type="submit" value="Delete Account" name="delete" class="button delete">
2124
</span>
2225
</div>
2326
</form>

‎magpie/ui/utils.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -85,23 +85,26 @@ def wrap(*args, **kwargs):
8585

8686
class BaseViews(object):
8787
"""Base methods for Magpie UI pages."""
88+
MAGPIE_FIXED_GROUP_MEMBERSHIPS = []
89+
MAGPIE_FIXED_GROUP_EDITS = []
8890

8991
def __init__(self, request):
9092
self.request = request
9193
self.magpie_url = get_magpie_url(request.registry)
9294
self.logged_user = get_logged_user(request)
9395

94-
self.MAGPIE_FIXED_GROUP_MEMBERSHIPS = [
95-
get_constant("MAGPIE_ANONYMOUS_GROUP", settings_container=request),
96-
]
97-
"""Special groups membership that cannot be edited."""
96+
anonymous = get_constant("MAGPIE_ANONYMOUS_GROUP", settings_container=request)
97+
admin = get_constant("MAGPIE_ADMIN_GROUP", settings_container=request)
98+
self.MAGPIE_FIXED_GROUP_MEMBERSHIPS = [anonymous] # special groups membership that cannot be edited
99+
self.MAGPIE_FIXED_GROUP_EDITS = [anonymous, admin] # special groups that cannot be edited
98100

99101
def add_template_data(self, data=None):
100102
# type: (Optional[Dict[Str, Any]]) -> Dict[Str, Any]
101103
"""Adds required template data for the 'heading' mako template applied to every UI page."""
102104
all_data = data or {}
103105
all_data.setdefault("MAGPIE_SUB_TITLE", "Administration")
104106
all_data.setdefault("MAGPIE_FIXED_GROUP_MEMBERSHIPS", self.MAGPIE_FIXED_GROUP_MEMBERSHIPS)
107+
all_data.setdefault("MAGPIE_FIXED_GROUP_EDITS", self.MAGPIE_FIXED_GROUP_EDITS)
105108
magpie_logged_user = get_logged_user(self.request)
106109
if magpie_logged_user:
107110
all_data.update({"MAGPIE_LOGGED_USER": magpie_logged_user.user_name})

‎magpie/utils.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
if TYPE_CHECKING:
2929
# pylint: disable=W0611,unused-import
30+
from typing import NoReturn # noqa: F401
3031
from magpie.typedefs import ( # noqa: F401
3132
Any, AnyKey, Str, List, Optional, Type, Union,
3233
AnyResponseType, AnyHeadersType, LoggerType, CookiesType, SettingsType, AnySettingsContainer,
@@ -64,6 +65,10 @@ def get_logger(name, level=None):
6465

6566
def print_log(msg, logger=None, level=logging.INFO):
6667
# type: (Str, Optional[LoggerType], int) -> None
68+
"""
69+
Logs the requested message to the logger and optionally enforce printing to the console according to configuration
70+
value defined by ``MAGPIE_LOG_PRINT``.
71+
"""
6772
# pylint: disable=C0415 # cannot use 'get_constant', recursive call
6873
from magpie.constants import MAGPIE_LOG_PRINT
6974

@@ -79,7 +84,8 @@ def print_log(msg, logger=None, level=logging.INFO):
7984

8085

8186
def raise_log(msg, exception=Exception, logger=None, level=logging.ERROR):
82-
# type: (Str, Optional[Type[Exception]], Optional[LoggerType], int) -> None
87+
# type: (Str, Optional[Type[Exception]], Optional[LoggerType], int) -> NoReturn
88+
"""Logs the provided message to the logger and raises the corresponding exception afterwards."""
8389
if not logger:
8490
logger = get_logger(__name__)
8591
logger.log(level, msg)
@@ -173,16 +179,16 @@ def fuzzy_name(name):
173179
def convert_response(response):
174180
# type: (AnyResponseType) -> Response
175181
"""
176-
Converts a ``response`` implementation (e.g.: ``requests.Response``) to an equivalent ``pyramid.response.Response``
177-
version.
182+
Converts a ``response`` implementation (e.g.: :class:`requests.Response`) to an equivalent
183+
:class:`pyramid.response.Response` object.
178184
"""
179185
if isinstance(response, Response):
180186
return response
181187
json_body = get_json(response)
182188
pyramid_response = Response(body=json_body, headers=response.headers)
183189
if hasattr(response, "cookies"):
184190
for cookie in response.cookies:
185-
pyramid_response.set_cookie(name=cookie.name, value=cookie.value, overwrite=True)
191+
pyramid_response.set_cookie(name=cookie.name, value=cookie.value, overwrite=True) # noqa
186192
if isinstance(response, HTTPException):
187193
for header_name, header_value in response.headers._items: # noqa # pylint: disable=W0212
188194
if header_name.lower() == "set-cookie":
@@ -217,7 +223,7 @@ def get_admin_cookies(container, verify=True, raise_message=None):
217223
def get_settings(container):
218224
# type: (AnySettingsContainer) -> SettingsType
219225
if isinstance(container, (Configurator, Request)):
220-
return container.registry.settings
226+
return container.registry.settings # noqa
221227
if isinstance(container, Registry):
222228
return container.settings
223229
if isinstance(container, dict):

0 commit comments

Comments
 (0)
Failed to load comments.