Skip to content

Commit ba13524

Browse files
Alex-Izquierdobzweijshimkus-rhkdelee
authored
Merge devel into main (#1063)
Signed-off-by: Alex <[email protected]> Co-authored-by: Bill Wei <[email protected]> Co-authored-by: Joe Shimkus <[email protected]> Co-authored-by: Elijah DeLee <[email protected]>
1 parent 9d07152 commit ba13524

File tree

10 files changed

+396
-17
lines changed

10 files changed

+396
-17
lines changed

src/aap_eda/conf/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2024 Red Hat, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from aap_eda.conf.registry import settings_registry
16+
from aap_eda.conf.settings import application_settings
17+
18+
__all__ = [
19+
"settings_registry",
20+
"application_settings",
21+
]

src/aap_eda/conf/registry.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Copyright 2024 Red Hat, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections import OrderedDict
16+
from dataclasses import dataclass
17+
from typing import Any
18+
19+
import yaml
20+
from django.conf import settings
21+
from django.core.exceptions import ImproperlyConfigured
22+
from django.db import transaction
23+
24+
from aap_eda.core.models import Setting
25+
from aap_eda.core.utils.crypto.base import SecretValue
26+
27+
ENCRYPTED_STRING = "$encrypted$"
28+
29+
30+
class InvalidKeyError(Exception):
31+
...
32+
33+
34+
class InvalidValueError(Exception):
35+
...
36+
37+
38+
@dataclass
39+
class RegistryData(object):
40+
name: str
41+
default: Any
42+
type: type = str
43+
read_only: bool = False
44+
is_secret: bool = False
45+
46+
47+
_APPLICATION_SETTING_REGISTRIES = [
48+
RegistryData(
49+
name="INSIGHTS_TRACKING_STATE",
50+
type=bool,
51+
default=False,
52+
),
53+
RegistryData(
54+
name="AUTOMATION_ANALYTICS_URL",
55+
read_only=True,
56+
default=settings.AUTOMATION_ANALYTICS_URL,
57+
),
58+
RegistryData(
59+
name="REDHAT_USERNAME",
60+
default="",
61+
),
62+
RegistryData(
63+
name="REDHAT_PASSWORD",
64+
is_secret=True,
65+
default="",
66+
),
67+
RegistryData(
68+
name="SUBSCRIPTIONS_USERNAME",
69+
default="",
70+
),
71+
RegistryData(
72+
name="SUBSCRIPTIONS_PASSWORD",
73+
is_secret=True,
74+
default="",
75+
),
76+
RegistryData(
77+
name="INSIGHTS_CERT_PATH",
78+
read_only=True,
79+
default=settings.INSIGHTS_CERT_PATH,
80+
),
81+
RegistryData(
82+
name="AUTOMATION_ANALYTICS_LAST_GATHER",
83+
default="",
84+
),
85+
RegistryData(
86+
name="AUTOMATION_ANALYTICS_LAST_ENTRIES",
87+
type=dict,
88+
default={},
89+
),
90+
RegistryData(
91+
name="AUTOMATION_ANALYTICS_GATHER_INTERVAL",
92+
type=int,
93+
default=14400,
94+
),
95+
]
96+
97+
98+
class SettingsRegistry(object):
99+
def __init__(self):
100+
self._registry = OrderedDict()
101+
for registry_data in _APPLICATION_SETTING_REGISTRIES:
102+
self.register(registry_data)
103+
104+
def register(self, registry_data: RegistryData) -> None:
105+
if registry_data.name in self._registry:
106+
raise ImproperlyConfigured(
107+
f"Setting {registry_data.name} is already registered."
108+
)
109+
self._registry[registry_data.name] = registry_data
110+
111+
def persist_registry_data(self):
112+
for key, data in self._registry.items():
113+
if data.read_only:
114+
update_method = Setting.objects.update_or_create
115+
else:
116+
update_method = Setting.objects.get_or_create
117+
update_method(key=key, defaults={"value": data.default})
118+
119+
def get_registered_settings(
120+
self, skip_read_only: bool = False
121+
) -> list[str]:
122+
setting_names = []
123+
124+
for setting, data in self._registry.items():
125+
if data.read_only and skip_read_only:
126+
continue
127+
setting_names.append(setting)
128+
return setting_names
129+
130+
def is_setting_secret(self, key: str) -> bool:
131+
return self._registry[key].is_secret
132+
133+
def is_setting_read_only(self, key: str) -> bool:
134+
return self._registry[key].read_only
135+
136+
def get_setting_type(self, key: str) -> type:
137+
return self._registry[key].type
138+
139+
def db_update_setting(self, key: str, value: Any) -> None:
140+
self._validate_key(key, writable=True)
141+
Setting.objects.filter(key=key).update(
142+
value=self._setting_value(key, value)
143+
)
144+
145+
def db_update_settings(self, settings: dict[str, Any]) -> None:
146+
with transaction.atomic():
147+
for key, value in settings.items():
148+
self._validate_key(key, writable=True)
149+
Setting.objects.filter(key=key).update(
150+
value=self._setting_value(key, value)
151+
)
152+
153+
def db_get_settings_for_display(self) -> dict[str, Any]:
154+
keys = self.get_registered_settings()
155+
settings = Setting.objects.filter(key__in=keys)
156+
return {
157+
obj.key: self._value_for_display(obj.key, obj.value)
158+
for obj in settings
159+
}
160+
161+
def db_get_setting(self, key: str) -> Any:
162+
self._validate_key(key)
163+
setting = Setting.objects.filter(key=key).first()
164+
return self._decrypt_value(key, setting.value)
165+
166+
def _validate_key(self, key: str, writable: bool = False) -> None:
167+
if key not in self._registry:
168+
raise InvalidKeyError(f"{key} is not a preset key")
169+
if writable and self.is_setting_read_only(key):
170+
raise InvalidKeyError(f"{key} is readonly")
171+
172+
def _decrypt_value(self, key: str, db_value: SecretValue) -> Any:
173+
val = db_value.get_secret_value()
174+
if val != "":
175+
val = yaml.safe_load(val)
176+
return self._setting_value(key, val)
177+
178+
def _setting_value(self, key: str, value: Any) -> Any:
179+
try:
180+
return self.get_setting_type(key)(value)
181+
except (ValueError, TypeError):
182+
raise InvalidValueError(
183+
f"Attempt to set an invalid value to key {key}"
184+
)
185+
186+
def _value_for_display(self, key: str, db_value: SecretValue) -> Any:
187+
value = self._decrypt_value(key, db_value)
188+
if self.is_setting_secret(key) and value:
189+
return ENCRYPTED_STRING
190+
return value
191+
192+
193+
settings_registry = SettingsRegistry()

src/aap_eda/conf/settings.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2024 Red Hat, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Any
16+
17+
from aap_eda.conf.registry import settings_registry
18+
19+
20+
class ApplicationSettings(object):
21+
def __setattr__(self, name: str, value: Any) -> None:
22+
settings_registry.db_update_setting(name, value)
23+
24+
def __getattr__(self, name: str) -> Any:
25+
return settings_registry.db_get_setting(name)
26+
27+
28+
application_settings = ApplicationSettings()

src/aap_eda/core/management/commands/create_initial_data.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from django.db import transaction
2424
from django.db.models import Q
2525

26+
from aap_eda.conf import settings_registry
2627
from aap_eda.core import enums, models
2728
from aap_eda.core.tasking import enable_redis_prefix
2829
from aap_eda.core.utils.credentials import inputs_to_store
@@ -1081,6 +1082,7 @@ class Command(BaseCommand):
10811082

10821083
@transaction.atomic
10831084
def handle(self, *args, **options):
1085+
settings_registry.persist_registry_data()
10841086
self._preload_credential_types()
10851087
self._copy_registry_credentials()
10861088
self._copy_scm_credentials()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 4.2.7 on 2024-09-16 19:40
2+
3+
from django.db import migrations, models
4+
5+
import aap_eda.core.utils.crypto.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("core", "0049_alter_eventstream_name"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Setting",
16+
fields=[
17+
(
18+
"id",
19+
models.BigAutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
("key", models.CharField(max_length=255, unique=True)),
27+
(
28+
"value",
29+
aap_eda.core.utils.crypto.fields.EncryptedTextField(
30+
blank=True
31+
),
32+
),
33+
("created_at", models.DateTimeField(auto_now_add=True)),
34+
("modified_at", models.DateTimeField(auto_now=True)),
35+
],
36+
options={
37+
"db_table": "core_setting",
38+
"indexes": [
39+
models.Index(
40+
fields=["key"], name="core_settin_key_53fa74_idx"
41+
)
42+
],
43+
},
44+
),
45+
]

src/aap_eda/core/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
RulebookProcessLog,
4444
RulebookProcessQueue,
4545
)
46+
from .setting import Setting
4647
from .team import Team
4748
from .user import AwxToken, User
4849

@@ -73,6 +74,7 @@
7374
"Organization",
7475
"Team",
7576
"EventStream",
77+
"Setting",
7678
]
7779

7880
permission_registry.register(

src/aap_eda/core/models/setting.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2024 Red Hat, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from django.db import models
16+
17+
from aap_eda.core.utils.crypto.fields import EncryptedTextField
18+
19+
__all__ = ("Setting",)
20+
21+
22+
class Setting(models.Model):
23+
class Meta:
24+
db_table = "core_setting"
25+
indexes = [models.Index(fields=["key"])]
26+
27+
key = models.CharField(max_length=255, unique=True)
28+
value = EncryptedTextField(blank=True, null=False)
29+
created_at = models.DateTimeField(auto_now_add=True, null=False)
30+
modified_at = models.DateTimeField(auto_now=True, null=False)

src/aap_eda/core/utils/crypto/fields.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,3 @@ def from_db_value(self, value, expression, connection):
8181
if value is None:
8282
return None
8383
return SecretValue(decrypt_string(value))
84-
85-
86-
class EncryptedJsonField(BaseEncryptedField, models.JSONField):
87-
def get_db_prep_save(self, value, connection):
88-
if value is None:
89-
return None
90-
if isinstance(value, SecretValue):
91-
value = value.get_secret_value()
92-
value = super().get_db_prep_save(value, connection)
93-
return encrypt_string(value)
94-
95-
def from_db_value(self, value, expression, connection):
96-
if value is None:
97-
return None
98-
value = decrypt_string(value)
99-
value = super().from_db_value(value, expression, connection)
100-
return SecretValue(value)

src/aap_eda/settings/default.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def _get_boolean(name: str, default=False) -> bool:
206206
"django.middleware.csrf.CsrfViewMiddleware",
207207
"django.contrib.auth.middleware.AuthenticationMiddleware",
208208
"django.middleware.clickjacking.XFrameOptionsMiddleware",
209+
"ansible_base.lib.middleware.logging.log_request.LogTracebackMiddleware",
209210
"crum.CurrentRequestUserMiddleware",
210211
]
211212

@@ -789,3 +790,6 @@ def get_rulebook_process_log_level() -> RulebookProcessLogLevel:
789790
MAX_PG_NOTIFY_MESSAGE_SIZE = int(
790791
settings.get("MAX_PG_NOTIFY_MESSAGE_SIZE", 6144)
791792
)
793+
794+
AUTOMATION_ANALYTICS_URL = settings.get("AUTOMATION_ANALYTICS_URL", "")
795+
INSIGHTS_CERT_PATH = settings.get("INSIGHTS_CERT_PATH", "")

0 commit comments

Comments
 (0)