This shows the green banner with a tick when cancelling a user's
invitation to a service or organisation. The accessibility audit noted
that 'When cancelling an invite a new page loads, however, there is no
immediate indication that the invite has been cancelled.'
In order to display the invited user's email address as part of the
flash message, this adds new methods to the api clients for invites to get
a single invite.
API gives an error if it tries to add a user to a service and that user is
already a member of the service. This situation shouldn't occur - admin checks
if an invited user is a member of a service before calling API, but we
have seen this error occurring when there are two requests processing at
the same time.
This change catches the errors from API if a user is already a member of
a service and redirects the user to the service dashboard so that they
don't see an error page.
flask_login sets a bunch of variables in the session object. We only use
one of them, `user_id`. We set that to the user id from the database,
and refer to it all over the place.
However, in flask_login 0.5.0 they prefix this with an underscore to
prevent people accidentally overwriting it etc. So when a user logs in
we need to make sure that we set user_id manually so we can still use
it.
flask_login sets a bunch of variables on the `flask.session` object.
However, this session object isn't the one that gets passed in to the
request context by flask - that one can only be modified outside of
requests from within the session_transaction context manager (see [1]).
So, flask_login populates the normal session and then we need to copy
all of those values across.
We didn't need to do this previously because we already set the
`user_id` value on line 20 of tests/__init__.py, but now that
flask_login is looking for `_user_id` instead we need to do this
properly.
[1] https://flask.palletsprojects.com/en/1.1.x/testing/#accessing-and-modifying-sessions
flask_login sets a bunch of variables in the session object. We only use
one of them, `user_id`. We set that to the user id from the database,
and refer to it all over the place.
However, in flask_login 0.5.0 they prefix this with an underscore to
prevent people accidentally overwriting it etc. So when a user logs in
we need to make sure that we set user_id manually so we can still use
it.
flask_login sets a bunch of variables on the `flask.session` object.
However, this session object isn't the one that gets passed in to the
request context by flask - that one can only be modified outside of
requests from within the session_transaction context manager (see [1]).
So, flask_login populates the normal session and then we need to copy
all of those values across.
We didn't need to do this previously because we already set the
`user_id` value on line 20 of tests/__init__.py, but now that
flask_login is looking for `_user_id` instead we need to do this
properly.
[1] https://flask.palletsprojects.com/en/1.1.x/testing/#accessing-and-modifying-sessions
We found another scenario where signing out of the db can cause a 500.
If the user archives their trial mode service, current_service.active = false, then signs out, the current user was being signed out client side first, meaning current_user is now an Anonymous user, next the call to the API is made to log out user on db, all calls to NotifyApiClient `check_inactive_service`, which is only authorised if user is platform_admin, but an AnonymousUser does not have that attribute, so a 500 is raise.
Seemed a bit cleaner to change the User.signout method to rather than the `check_inactive_service` method for current_user.is_authenticated.
Anytime a user clicks "sign out" we should be signing them out server side as well. This can be accomplished by setting the Users.current_session_id = null.
I found that the method User.logged_in_elsewhere doesn't need to check if the current_session_id is None. The current_session_ids in the cookie and db (redis or postgres) then the user should be forced to log in again.
Rather than hard-coding a format string in a bunch of different places
we can use the function we already have in utils.
This commit also refactors some logic around password resets to put the
date-parsing changes in the most sensible bit of the codebase, so it’s
clearer what the intention of the view-layer code is.
The property doesn’t represent the whole client, but just one method on
it. So this commit renames the property to better describe what it is
designed to store.
Because it means you often have to cast to string in your application
code just to get your tests passing.
The method being monkey patched is originally defined here: b81aa0f18c/src/werkzeug/routing.py (L1272)
This commit introduces a slightly hacky way of putting usernames against
events, given that the API only returns user IDs.
It does so without:
- making changes to the API
- making a pages that could potentially fire off dozens of API calls (ie
one per user)
This comes with the limitation that it can only get names for those team
members who are still in the team. Otherwise it will say ‘Unknown’.
In the future the API should probably return the name and email address
for the user who initiated the event, and whether that user was acting
in a platform admin capacity.
An invited user can’t be added to an organisation or service, only a
real user can. So the methods to do this should be on the user model,
and take the details of the invite as arguments.
Organisation team members will be ultimately interested in the detailed
usage of each service, but shouldn't necessarily have access to the
personal data of that services users.
So we should allow these organisation team members to navigate to live
services usage page from the organisation page. They may need to contact
the team so they should also be able to view the team members page.
So they'll then see just usage and team members pages.
If they are actually a team member of the service they're viewing, then
they'll see the full range of options as usual.
This commit implement the above by adding an extra flag to the
`user.has_permissions` decorator which allows certain pages to be marked
as viewable by an organisation user. The default (for all other existing
pages) is that organisation users don’t have permission.
It’s a bit more concise to use these methods, rather than access the
lists directly.
And because it’s easier to read it will make later refactoring less
bothersome.
so that platform admins (us) can view pages as regular users do easily.
Simply adds a flag in the session cookie that overrides the actual
platform admin flag on the user model if set. This way it's safe, since
this only downgrades existing functionality, so if someone managed to
alter it they could only get less permissions, not more.
You can change this value from the user profile page if either:
* you're a platform admin
* the flag is set (to any value) on the cookie.
This slightly weird check means that we don't check the underlying
`user._platform_admin` flag anywhere in the code, even when toggling
the suppression.
The view code shouldn’t need to know the internals of a service’s data
structure; the idea of having a service model is to abstract this kind
of thing.
This makes it:
- nicer, by having access to sensibly named things like
`Service.trial_mode` instead of `service['restricted']`.
- less likely to write Jinja code like `service.trail_mode`, which would
fail silently if `service` was a dictionary
For consistency with `.organisations`/`.organisation_ids`.
`.services` returns a list of semi-rich dictionaries for each service.
`.service_ids` returns service IDs only.
In reality we shouldn’t have any live services that don’t have an
organisation. But we probably do locally, in preview, etc., and we
shouldn’t lose a way of accessing them.
At the moment the service list doesn’t disambiguate between live and
trial mode services. This makes it hard to tell which of the things are
important and which aren’t.
The first step towards making this page clearer is to list trial mode
services separately.
The API needs the id of the user, not the id of the invite.
The problem with the tests is that the update mock returned a different
user ID than the user it was being passed. So the tests didn’t catch
this.
the api always returns exactly:
```
id
name
email_address
auth_type
current_session_id
failed_login_count
logged_in_at
mobile_number
organisations
password_changed_at
permissions
platform_admin
services
state
```
it does this through `models.py::User.serialize` – there is an old
Marshmallow `user_schema` in `schemas.py` but this isn’t used for
dumping return data, only parsing the json in the create user rest
endpoint.
This means we can rely on these keys always being in the dictionary.
Our other models inherit from `JSONModel`, rather than manually doing
lookups of the JSON in the `__init__` method. This commit changes the
user model to work in the same way.
I had to add a new concept (`DEFAULTS`) to account for some properties
not always being present in the (mocked) JSON. In reality it might be
that the API does always return these values. This should be looked at
in future work, to see which defaults can be safely removed. At least
now they:
- do not mean any changes are needed to the existing tests
- are explicitly separated from the attributes that we do expect to
always be in the JSON response
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
This removes the edit_folder_permission checks from the code, enabling
the folder permissions for all services.
This also fixes folder-related tests to set up appropriate user
permissions.
This should only be merged right after alphagov/notifications-api#2428,
when all other permission stories are done.
We get people signing up for Notify who work for the NHS, but whose
organisation we don’t know about. For example
`name@gloshospitals.nhs.uk` will be someone working for Gloucestershire
Hospitals NHS Foundation Trust, which is not an organisation we have in
the database.
Currently we rely on knowing the specific organisation (NHS as a whole
isn’t an organisation) in order to set the organisation type for any
services they create. This commit adds a special case for anyone with an
NHS email address to set the organisation type to be NHS, even when we
don’t know which specific part of the NHS they work for.
This is the same thing we do on the API side for NHS email and letter
branding:
a4ae5a0a90/app/dao/services_dao.py (L310-L313)
If you define a route with the service ID as a typed parameter, ie
```
@main.route('/services/<uuid:service_id>/agreement')
```
then `type(service_id)` returns `<class 'uuid.UUID'>`.
This is a problem when the permissions dictionary stores service IDs as
strings, because trying to look up a user’s permissions with the UUID
fails silently (that key isn’t in the dictionary).
This commit makes sure we always cast the service ID to a string before
using it to check permissions.