Skip to content

Commit 89f6c51

Browse files
committed
TT-98 feat: add feature flags
1 parent 508171b commit 89f6c51

File tree

5 files changed

+108
-10
lines changed

5 files changed

+108
-10
lines changed

commons/feature_flags/__init__.py

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from azure.appconfiguration import AzureAppConfigurationClient
2+
from time_tracker_api.security import current_user_email
3+
import json
4+
import os
5+
6+
7+
class FeatureFlags:
8+
AZURE_CONNECTION_STRING = os.environ.get(
9+
'AZURE_APP_CONFIGURATION_CONNECTION_STRING'
10+
)
11+
12+
def __init__(self, key: str, label: str = None):
13+
self.key = key
14+
self.label = label
15+
self.configuration = self.get_configuration(self.key, self.label)
16+
17+
def get_configuration(self, key: str, label: str):
18+
connection_str = self.AZURE_CONNECTION_STRING
19+
client = AzureAppConfigurationClient.from_connection_string(
20+
connection_str
21+
)
22+
configuration = client.get_configuration_setting(
23+
key=f".appconfig.featureflag/{self.key}", label=self.label
24+
)
25+
26+
return configuration
27+
28+
def is_toggle_enabled(self):
29+
self.configuration = self.get_configuration(self.key, self.label)
30+
data = json.loads(self.configuration.value)
31+
result = data["enabled"]
32+
33+
return result
34+
35+
def is_toggle_enabled_for_user(self):
36+
self.configuration = self.get_configuration(self.key, self.label)
37+
data = json.loads(self.configuration.value)
38+
list_data = data["conditions"]["client_filters"]
39+
data2 = list_data[0]
40+
list_users = data2["parameters"]["Audience"]["Users"]
41+
user = current_user_email()
42+
43+
return (
44+
True if user in list_users and self.is_toggle_enabled() else False
45+
)

requirements/commons.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
requests==2.23.0
77

88
# To create sample content in tests and API documentation
9-
Faker==4.0.2
9+
Faker==4.0.2
10+
11+
# For feature toggles
12+
azure-appconfiguration==1.1.1

time_tracker_api/security.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,43 @@
2525
}
2626

2727
# For matching UUIDs
28-
UUID_REGEX = '[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}'
28+
UUID_REGEX = (
29+
'[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}'
30+
)
2931

30-
iss_claim_pattern = re.compile(r"(.*).b2clogin.com/(?P<tenant_id>%s)" % UUID_REGEX)
32+
iss_claim_pattern = re.compile(
33+
r"(.*).b2clogin.com/(?P<tenant_id>%s)" % UUID_REGEX
34+
)
3135

3236
roles = {
3337
"admin": {"name": "time-tracker-admin"},
34-
"client": {"name": "client-role"}
38+
"client": {"name": "client-role"},
3539
}
3640

3741

3842
def current_user_id() -> str:
3943
oid_claim = get_token_json().get("oid")
4044
if oid_claim is None:
41-
abort(message='The claim "oid" is missing in the JWT', code=HTTPStatus.UNAUTHORIZED)
45+
abort(
46+
message='The claim "oid" is missing in the JWT',
47+
code=HTTPStatus.UNAUTHORIZED,
48+
)
4249

4350
return oid_claim
4451

4552

53+
def current_user_email() -> str:
54+
email_list_claim = get_token_json().get("emails")
55+
if email_list_claim is None:
56+
abort(
57+
message='The claim "emails" is missing in the JWT',
58+
code=HTTPStatus.UNAUTHORIZED,
59+
)
60+
61+
email_user = email_list_claim[0]
62+
return email_user
63+
64+
4665
def current_role_user() -> str:
4766
role_user = get_token_json().get("extension_role", None)
4867
return role_user if role_user else roles.get("client").get("name")
@@ -51,13 +70,18 @@ def current_role_user() -> str:
5170
def current_user_tenant_id() -> str:
5271
iss_claim = get_token_json().get("iss")
5372
if iss_claim is None:
54-
abort(message='The claim "iss" is missing in the JWT', code=HTTPStatus.UNAUTHORIZED)
73+
abort(
74+
message='The claim "iss" is missing in the JWT',
75+
code=HTTPStatus.UNAUTHORIZED,
76+
)
5577

5678
tenant_id = parse_tenant_id_from_iss_claim(iss_claim)
5779
if tenant_id is None:
58-
abort(message='The format of the claim "iss" cannot be understood. '
59-
'Please contact the development team.',
60-
code=HTTPStatus.UNAUTHORIZED)
80+
abort(
81+
message='The format of the claim "iss" cannot be understood. '
82+
'Please contact the development team.',
83+
code=HTTPStatus.UNAUTHORIZED,
84+
)
6185

6286
return tenant_id
6387

@@ -66,11 +90,18 @@ def get_or_generate_dev_secret_key():
6690
global dev_secret_key
6791
if dev_secret_key is None:
6892
from time_tracker_api import flask_app as app
93+
6994
"""
7095
Generates a security key for development purposes
7196
:return: str
7297
"""
73-
dev_secret_key = fake.password(length=16, special_chars=True, digits=True, upper_case=True, lower_case=True)
98+
dev_secret_key = fake.password(
99+
length=16,
100+
special_chars=True,
101+
digits=True,
102+
upper_case=True,
103+
lower_case=True,
104+
)
74105
if app.config.get("FLASK_DEBUG", False): # pragma: no cover
75106
print('*********************************************************')
76107
print("The generated secret is \"%s\"" % dev_secret_key)

time_tracker_api/time_entries/time_entries_namespace.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
remove_required_constraint,
1818
)
1919
from time_tracker_api.time_entries.time_entries_dao import create_dao
20+
from commons.feature_flags.features_flags import FeatureFlags
2021

2122
faker = Faker()
2223

@@ -267,6 +268,24 @@ def get(self):
267268
return time_entries_dao.get_lastest_entries_by_project(conditions={})
268269

269270

271+
@ns.route('/featureFlags')
272+
class featuresFlags(Resource):
273+
@ns.doc('flags')
274+
@ns.marshal_list_with(time_entry)
275+
@ns.response(HTTPStatus.NOT_FOUND, 'No time entries found')
276+
def get(self):
277+
"""List the latest time entries"""
278+
featureTest = FeatureFlags("ui-list-test-users")
279+
test = featureTest.is_toggle_enabled_for_user()
280+
if test:
281+
return time_entries_dao.get_lastest_entries_by_project(
282+
conditions={}
283+
)
284+
else:
285+
conditions = attributes_filter.parse_args()
286+
return time_entries_dao.get_all(conditions=conditions)
287+
288+
270289
@ns.route('/<string:id>')
271290
@ns.response(HTTPStatus.NOT_FOUND, 'This time entry does not exist')
272291
@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format')

0 commit comments

Comments
 (0)