Skip to content

Commit 65d85ca

Browse files
authored
Merge branch 'master' into pipeline
2 parents 3f3e5a0 + 18cd3da commit 65d85ca

File tree

9 files changed

+206
-33
lines changed

9 files changed

+206
-33
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
<!--next-version-placeholder-->
44

5+
## v0.29.0 (2021-02-19)
6+
### Feature
7+
* TT-154 add is user member of a group endpoint ([#259](https://github.com/ioet/time-tracker-backend/issues/259)) ([`2381cf2`](https://github.com/ioet/time-tracker-backend/commit/2381cf2a76f6bdcc174ae8433a63d4121ac4acd5))
8+
9+
## v0.28.0 (2021-02-11)
10+
### Feature
11+
* TT-147 user endpoint ([#257](https://github.com/ioet/time-tracker-backend/issues/257)) ([`6abc3c2`](https://github.com/ioet/time-tracker-backend/commit/6abc3c24dfa1bfadd9c23558260dba14949fea22))
12+
13+
### Documentation
14+
* TT-137: Create-Feature Toggles dictionary ([`5b0deaf`](https://github.com/ioet/time-tracker-backend/commit/5b0deaf4ebad89bdf9730ca3e5faf10761ae67e5))
15+
516
## v0.27.1 (2021-01-27)
617
### Fix
718
* TT-131 remove feature toggle for user role field ([#255](https://github.com/ioet/time-tracker-backend/issues/255)) ([`a3439c3`](https://github.com/ioet/time-tracker-backend/commit/a3439c3b0a2c3b6007f87b1157e38773968fc00d))

commons/feature_toggles/feature_toggle_manager.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,11 @@
22
import json
33
from time_tracker_api.security import current_user_email
44
from azure.appconfiguration import AzureAppConfigurationClient
5+
from utils.environment_variables import check_variables_are_defined
56

67

78
class FeatureToggleConfig:
8-
def check_variables_are_defined():
9-
azure_app_variable = 'AZURE_APP_CONFIGURATION_CONNECTION_STRING'
10-
if azure_app_variable not in os.environ:
11-
raise EnvironmentError(
12-
"{} is not defined in the environment".format(
13-
azure_app_variable
14-
)
15-
)
16-
17-
check_variables_are_defined()
9+
check_variables_are_defined(['AZURE_APP_CONFIGURATION_CONNECTION_STRING'])
1810
AZURE_APP_CONFIGURATION_CONNECTION_STRING = os.environ.get(
1911
'AZURE_APP_CONFIGURATION_CONNECTION_STRING'
2012
)

requirements/time_tracker_api/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ pytest-mock==2.0.0
1515
coverage==4.5.1
1616

1717
# Git hooks
18-
pre-commit==2.2.0
18+
pre-commit==2.2.0

tests/time_tracker_api/users/users_namespace_test.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,45 @@
11
from unittest.mock import Mock, patch
22
from flask import json
3+
from faker import Faker
34
from flask.testing import FlaskClient
45
from flask_restplus._http import HTTPStatus
56
from pytest import mark
67

78

9+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
10+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
11+
@patch('utils.azure_users.AzureConnection.get_user')
12+
def test_get_user_response_contains_expected_props(
13+
get_user_mock,
14+
client: FlaskClient,
15+
valid_header: dict,
16+
):
17+
get_user_mock.return_value = {
18+
'name': 'dummy',
19+
'email': 'dummy',
20+
'roles': ['dummy-role'],
21+
}
22+
user_id = (Faker().uuid4(),)
23+
response = client.get(f'/users/{user_id}', headers=valid_header)
24+
25+
get_user_mock.assert_called()
26+
assert HTTPStatus.OK == response.status_code
27+
assert 'name' in json.loads(response.data)
28+
assert 'email' in json.loads(response.data)
29+
assert 'roles' in json.loads(response.data)
30+
assert ['dummy-role'] == json.loads(response.data)['roles']
31+
32+
833
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
934
@patch('utils.azure_users.AzureConnection.get_token', Mock())
1035
@patch(
1136
'utils.azure_users.AzureConnection.is_test_user', Mock(return_value=True)
1237
)
1338
@patch('utils.azure_users.AzureConnection.users')
1439
def test_users_response_contains_expected_props(
15-
users_mock, client: FlaskClient, valid_header: dict,
40+
users_mock,
41+
client: FlaskClient,
42+
valid_header: dict,
1643
):
1744
users_mock.return_value = [
1845
{'name': 'dummy', 'email': 'dummy', 'roles': ['dummy-role']}
@@ -31,7 +58,8 @@ def test_users_response_contains_expected_props(
3158
@patch('utils.azure_users.AzureConnection.get_token', Mock())
3259
@patch('utils.azure_users.AzureConnection.update_role')
3360
@mark.parametrize(
34-
'role_id,action', [('test', 'grant'), ('admin', 'revoke')],
61+
'role_id,action',
62+
[('test', 'grant'), ('admin', 'revoke')],
3563
)
3664
def test_update_role_response_contains_expected_props(
3765
update_role_mock,
@@ -47,7 +75,8 @@ def test_update_role_response_contains_expected_props(
4775
'roles': [],
4876
}
4977
response = client.post(
50-
f'/users/{user_id}/roles/{role_id}/{action}', headers=valid_header,
78+
f'/users/{user_id}/roles/{role_id}/{action}',
79+
headers=valid_header,
5180
)
5281
assert HTTPStatus.OK == response.status_code
5382
assert 'name' in json.loads(response.data)
@@ -78,10 +107,34 @@ def test_update_role_is_called_properly_on_each_action(
78107
):
79108
update_role_mock.return_value = {}
80109
response = client.post(
81-
f'/users/{user_id}/roles/{role_id}/{action}', headers=valid_header,
110+
f'/users/{user_id}/roles/{role_id}/{action}',
111+
headers=valid_header,
82112
)
83113

84114
assert HTTPStatus.OK == response.status_code
85115
update_role_mock.assert_called_once_with(
86116
user_id, role_id, is_grant=is_grant
87117
)
118+
119+
120+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
121+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
122+
@patch('utils.azure_users.AzureConnection.is_user_in_group')
123+
@mark.parametrize(
124+
'group_name, expected_value', [('admin', True), ('admin', False)]
125+
)
126+
def test_if_user_is_in_group(
127+
is_user_in_group_mock,
128+
client: FlaskClient,
129+
valid_header: dict,
130+
user_id: str,
131+
group_name,
132+
expected_value,
133+
):
134+
is_user_in_group_mock.return_value = {'value': expected_value}
135+
response = client.get(
136+
f'/users/{user_id}/groups/{group_name}/is-member-of',
137+
headers=valid_header,
138+
)
139+
assert HTTPStatus.OK == response.status_code
140+
assert 'value' in json.loads(response.data)

tests/utils/azure_users_test.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
],
1515
)
1616
def test_azure_connection_is_test_user(
17-
get_mock, field_name, field_value, is_test_user_expected_value,
17+
get_mock,
18+
field_name,
19+
field_value,
20+
is_test_user_expected_value,
1821
):
1922
response_mock = Mock()
2023
response_mock.status_code = 200
@@ -33,7 +36,12 @@ def test_azure_connection_get_test_user_ids(get_mock):
3336
response_mock = Mock()
3437
response_mock.status_code = 200
3538
response_mock.json = Mock(
36-
return_value={'value': [{'objectId': 'ID1'}, {'objectId': 'ID2'},]}
39+
return_value={
40+
'value': [
41+
{'objectId': 'ID1'},
42+
{'objectId': 'ID2'},
43+
]
44+
}
3745
)
3846
get_mock.return_value = response_mock
3947

@@ -56,3 +64,42 @@ def test_azure_connection_get_non_test_users(
5664
non_test_users = [non_test_user]
5765
az_conn = AzureConnection()
5866
assert az_conn.get_non_test_users() == non_test_users
67+
68+
69+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
70+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
71+
@patch('requests.get')
72+
def test_azure_connection_get_group_id_by_group_name(get_mock):
73+
response_mock = Mock()
74+
response_mock.status_code = 200
75+
response_mock.json = Mock(return_value={'value': [{'objectId': 'ID1'}]})
76+
get_mock.return_value = response_mock
77+
78+
group_id = 'ID1'
79+
azure_connection = AzureConnection()
80+
assert (
81+
azure_connection.get_group_id_by_group_name('group_name') == group_id
82+
)
83+
84+
85+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
86+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
87+
@patch('utils.azure_users.AzureConnection.get_group_id_by_group_name')
88+
@patch('requests.post')
89+
@mark.parametrize('expected_value', [True, False])
90+
def test_is_user_in_group(
91+
post_mock, get_group_id_by_group_name_mock, expected_value
92+
):
93+
response_expected = {'value': expected_value}
94+
response_mock = Mock()
95+
response_mock.status_code = 200
96+
response_mock.json = Mock(return_value=response_expected)
97+
post_mock.return_value = response_mock
98+
99+
get_group_id_by_group_name_mock.return_value = 'group_id'
100+
101+
azure_connection = AzureConnection()
102+
assert (
103+
azure_connection.is_user_in_group('user_id', 'group_name')
104+
== response_expected
105+
)

time_tracker_api/users/users_namespace.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
description='List of the roles assigned to the user by the tenant',
3232
),
3333
example=Faker().words(
34-
3, ['time-tracker-admin', 'test-user', 'guest',],
34+
3,
35+
[
36+
'time-tracker-admin',
37+
'test-user',
38+
'guest',
39+
],
3540
),
3641
),
3742
},
@@ -40,6 +45,16 @@
4045
user_response_fields.update(common_fields)
4146

4247

48+
@ns.route('/<string:id>')
49+
@ns.param('id', 'The unique identifier of the user')
50+
class User(Resource):
51+
@ns.doc('get_user')
52+
@ns.marshal_list_with(user_response_fields)
53+
def get(self, id):
54+
"""Get an user"""
55+
return AzureConnection().get_user(id)
56+
57+
4358
@ns.route('')
4459
class Users(Resource):
4560
@ns.doc('list_users')
@@ -84,3 +99,13 @@ class RevokeRole(Resource):
8499
def post(self, user_id, role_id):
85100
"""Revoke role to user"""
86101
return AzureConnection().update_role(user_id, role_id, is_grant=False)
102+
103+
104+
@ns.route('/<string:user_id>/groups/<string:group_id>/is-member-of')
105+
@ns.param('user_id', 'The user identifier')
106+
@ns.param('group_id', 'The group name identifier')
107+
class UserInGroup(Resource):
108+
@ns.doc('user_in_group')
109+
def get(self, user_id, group_id):
110+
"""Is User in the Group"""
111+
return AzureConnection().is_user_in_group(user_id, group_id)

time_tracker_api/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.27.1'
1+
__version__ = '0.29.0'

utils/azure_users.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@
33
import requests
44
import json
55
from typing import List
6+
from utils.environment_variables import check_variables_are_defined
67

78

89
class MSConfig:
9-
def check_variables_are_defined():
10-
auth_variables = [
11-
'MS_CLIENT_ID',
12-
'MS_AUTHORITY',
13-
'MS_SECRET',
14-
'MS_SCOPE',
15-
'MS_ENDPOINT',
16-
]
17-
for var in auth_variables:
18-
if var not in os.environ:
19-
raise EnvironmentError(
20-
"{} is not defined in the environment".format(var)
21-
)
10+
ms_variables = [
11+
'MS_CLIENT_ID',
12+
'MS_AUTHORITY',
13+
'MS_SECRET',
14+
'MS_SCOPE',
15+
'MS_ENDPOINT',
16+
]
17+
18+
check_variables_are_defined(ms_variables)
2219

23-
check_variables_are_defined()
2420
CLIENT_ID = os.environ.get('MS_CLIENT_ID')
2521
AUTHORITY = os.environ.get('MS_AUTHORITY')
2622
SECRET = os.environ.get('MS_SECRET')
@@ -86,6 +82,14 @@ def get_token(self):
8682
error_info = f"{response['error']} {response['error_description']}"
8783
raise ValueError(error_info)
8884

85+
def get_user(self, user_id) -> AzureUser:
86+
endpoint = "{endpoint}/users/{user_id}?api-version=1.6".format(
87+
endpoint=self.config.ENDPOINT, user_id=user_id
88+
)
89+
response = requests.get(endpoint, auth=BearerAuth(self.access_token))
90+
assert 200 == response.status_code
91+
return self.to_azure_user(response.json())
92+
8993
def users(self) -> List[AzureUser]:
9094
role_fields_params = ','.join(
9195
[field_name for field_name, _ in ROLE_FIELD_VALUES.values()]
@@ -165,3 +169,35 @@ def get_test_user_ids(self):
165169
assert 200 == response.status_code
166170
assert 'value' in response.json()
167171
return [item['objectId'] for item in response.json()['value']]
172+
173+
def get_group_id_by_group_name(self, group_name):
174+
endpoint = "{endpoint}/groups?api-version=1.6&$select=objectId&$filter=displayName eq '{group_name}'".format(
175+
endpoint=self.config.ENDPOINT, group_name=group_name
176+
)
177+
178+
response = requests.get(endpoint, auth=BearerAuth(self.access_token))
179+
180+
assert 200 == response.status_code
181+
182+
return response.json()['value'][0]['objectId']
183+
184+
def is_user_in_group(self, user_id, group_name):
185+
group_id = self.get_group_id_by_group_name(group_name=group_name)
186+
187+
endpoint = "{endpoint}/isMemberOf?api-version=1.6".format(
188+
endpoint=self.config.ENDPOINT
189+
)
190+
191+
data = {"groupId": group_id, "memberId": user_id}
192+
193+
response = requests.post(
194+
endpoint,
195+
auth=BearerAuth(self.access_token),
196+
data=json.dumps(data),
197+
headers=HTTP_PATCH_HEADERS,
198+
)
199+
200+
assert 200 == response.status_code
201+
202+
item = response.json()['value']
203+
return {'value': item}

utils/environment_variables.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import os
2+
3+
4+
def check_variables_are_defined(variables):
5+
for var in variables:
6+
if var not in os.environ:
7+
raise EnvironmentError(
8+
"{} is not defined in the environment".format(var)
9+
)

0 commit comments

Comments
 (0)