/
users.py
233 lines (194 loc) · 8.2 KB
/
users.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
from flask_classful import FlaskView, route
from marshmallow import fields
from sqlalchemy.exc import IntegrityError
from webargs.flaskparser import use_kwargs
from flask_security import current_user
from flask_security.recoverable import send_reset_password_instructions
from flask_json import as_json
from werkzeug.exceptions import Forbidden
from flexmeasures.data.models.user import User as UserModel, Account
from flexmeasures.api.common.schemas.users import AccountIdField, UserIdField
from flexmeasures.data.schemas.users import UserSchema
from flexmeasures.data.services.users import (
get_users,
set_random_password,
remove_cookie_and_token_access,
)
from flexmeasures.auth.decorators import permission_required_for_context
from flexmeasures.data import db
"""
API endpoints to manage users.
Both POST (to create) and DELETE are not accessible via the API, but as CLI functions.
"""
# Instantiate schemas outside of endpoint logic to minimize response time
user_schema = UserSchema()
users_schema = UserSchema(many=True)
partial_user_schema = UserSchema(partial=True)
class UserAPI(FlaskView):
route_base = "/users"
trailing_slash = False
@route("", methods=["GET"])
@use_kwargs(
{
"account": AccountIdField(
data_key="account_id", load_default=AccountIdField.load_current
),
"include_inactive": fields.Bool(load_default=False),
},
location="query",
)
@permission_required_for_context("read", arg_name="account")
@as_json
def index(self, account: Account, include_inactive: bool = False):
"""API endpoint to list all users of an account.
.. :quickref: User; Download user list
This endpoint returns all accessible users.
By default, only active users are returned.
The `include_inactive` query parameter can be used to also fetch
inactive users.
Accessible users are users in the same account as the current user.
Only admins can use this endpoint to fetch users from a different account (by using the `account_id` query parameter).
**Example response**
An example of one user being returned:
.. sourcecode:: json
[
{
'active': True,
'email': 'test_prosumer@seita.nl',
'account_id': 13,
'flexmeasures_roles': [1, 3],
'id': 1,
'timezone': 'Europe/Amsterdam',
'username': 'Test Prosumer User'
}
]
:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
users = get_users(account_name=account.name, only_active=not include_inactive)
return users_schema.dump(users), 200
@route("/<id>")
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("read", arg_name="user")
@as_json
def get(self, id: int, user: UserModel):
"""API endpoint to get a user.
.. :quickref: User; Get a user
This endpoint gets a user.
Only admins or the user themselves can use this endpoint.
**Example response**
.. sourcecode:: json
{
'account_id': 1,
'active': True,
'email': 'test_prosumer@seita.nl',
'flexmeasures_roles': [1, 3],
'id': 1,
'timezone': 'Europe/Amsterdam',
'username': 'Test Prosumer User'
}
:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
return user_schema.dump(user), 200
@route("/<id>", methods=["PATCH"])
@use_kwargs(partial_user_schema)
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("update", arg_name="user")
@as_json
def patch(self, id: int, user: UserModel, **user_data):
"""API endpoint to patch user data.
.. :quickref: User; Patch data for an existing user
This endpoint sets data for an existing user.
Any subset of user fields can be sent.
Only the user themselves or admins are allowed to update its data,
while a non-admin can only edit a few of their own fields.
Several fields are not allowed to be updated, e.g. id and account_id.
**Example request**
.. sourcecode:: json
{
"active": false,
}
**Example response**
The following user fields are returned:
.. sourcecode:: json
{
'account_id': 1,
'active': True,
'email': 'test_prosumer@seita.nl',
'flexmeasures_roles': [1, 3],
'id': 1,
'timezone': 'Europe/Amsterdam',
'username': 'Test Prosumer User'
}
:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: UPDATED
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
allowed_fields = [
"email",
"username",
"active",
"timezone",
"flexmeasures_roles",
]
for k, v in [(k, v) for k, v in user_data.items() if k in allowed_fields]:
if current_user.id == user.id and k in ("active", "flexmeasures_roles"):
raise Forbidden(
"Users who edit themselves cannot edit security-sensitive fields."
)
setattr(user, k, v)
if k == "active" and v is False:
remove_cookie_and_token_access(user)
db.session.add(user)
try:
db.session.commit()
except IntegrityError as ie:
return (
dict(message="Duplicate user already exists", detail=ie._message()),
400,
)
return user_schema.dump(user), 200
@route("/<id>/password-reset", methods=["PATCH"])
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("update", arg_name="user")
@as_json
def reset_user_password(self, id: int, user: UserModel):
"""API endpoint to reset the user's current password, cookies and auth tokens, and to email a password reset link to the user.
.. :quickref: User; Password reset
Reset the user's password, and send them instructions on how to reset the password.
This endpoint is useful from a security standpoint, in case of worries the password might be compromised.
It sets the current password to something random, invalidates cookies and auth tokens,
and also sends an email for resetting the password to the user.
Users can reset their own passwords. Only admins can use this endpoint to reset passwords of other users.
:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
set_random_password(user)
remove_cookie_and_token_access(user)
send_reset_password_instructions(user)
# commit only if sending instructions worked, as well
db.session.commit()