Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] AAP integration #1528

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,6 @@ chatbot-test:
.PHONY: chatbot-lint
chatbot-lint:
npm --prefix ./ansible_ai_connect_chatbot run eslint

pre-commit:
pre-commit run --color=always --all-files
22 changes: 22 additions & 0 deletions ansible_ai_connect/ai/api/openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


def preprocessing_filter_spec(endpoints):
filtered = []
for path, path_regex, method, callback in endpoints:
# do not add internal endpoints to schema
if not path.startswith("/api/v1/service-index"):
filtered.append((path, path_regex, method, callback))
return filtered
3 changes: 2 additions & 1 deletion ansible_ai_connect/ai/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
from django.urls import include, path

from .versions.v0 import urls as v0_urls
Expand All @@ -20,4 +20,5 @@
urlpatterns = [
path("v0/", include((v0_urls, "ai"), namespace="v0")),
path("v1/", include((v1_urls, "ai"), namespace="v1")),
path("v1/", include(resource_api_urls)),
]
7 changes: 7 additions & 0 deletions ansible_ai_connect/ai/api/versions/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

from django.urls import include, path

from ansible_ai_connect.healthcheck.views import (
WisdomServiceHealthView,
WisdomServiceLivenessProbeView,
)

from .ai import urls as ai_urls
from .telemetry import urls as telemetry_urls
from .users import urls as me_urls
Expand All @@ -24,4 +29,6 @@
path("me/", include(me_urls)),
path("telemetry/", include(telemetry_urls)),
path("wca/", include(wca_urls)),
path("health/", WisdomServiceLivenessProbeView.as_view(), name="health"),
path("health/status/", WisdomServiceHealthView.as_view(), name="health_status"),
]
4 changes: 2 additions & 2 deletions ansible_ai_connect/ai/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
)
from ansible_ai_connect.users.models import User

from ...main.permissions import IsRHInternalUser, IsTestUser
from ...main.permissions import IsAAPUser, IsRHInternalUser, IsTestUser
from ...users.throttling import EndpointRateThrottle
from ..feature_flags import FeatureFlags
from .data.data_model import ContentMatchPayloadData, ContentMatchResponseDto
Expand Down Expand Up @@ -1041,7 +1041,7 @@ class ChatEndpointThrottle(EndpointRateThrottle):
permission_classes = [
permissions.IsAuthenticated,
IsAuthenticatedOrTokenHasScope,
IsRHInternalUser | IsTestUser,
IsRHInternalUser | IsTestUser | IsAAPUser,
]
required_scopes = ["read", "write"]
schema1_event = schema1.ChatBotOperationalEvent
Expand Down
35 changes: 35 additions & 0 deletions ansible_ai_connect/ai/resource_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from ansible_base.resource_registry.registry import (
ResourceConfig,
ServiceAPIConfig,
SharedResource,
)
from ansible_base.resource_registry.shared_types import UserType
from ansible_base.resource_registry.utils.resource_type_processor import (
ResourceTypeProcessor,
)
from django.contrib.auth import get_user_model


class UserProcessor(ResourceTypeProcessor):
def pre_serialize_additional(self):
# These fields aren't supported in app, so we'll set them to blank
setattr(self.instance, "external_auth_provider", None)
setattr(self.instance, "external_auth_uid", None)
setattr(self.instance, "organizations", [])
setattr(self.instance, "organizations_administered", [])

return self.instance


class APIConfig(ServiceAPIConfig):
custom_resource_processors = {"shared.user": UserProcessor}
service_type = "lightspeed"


RESOURCE_LIST = [
ResourceConfig(
get_user_model(),
shared_resource=SharedResource(serializer=UserType, is_provider=False),
name_field="username",
),
]
13 changes: 13 additions & 0 deletions ansible_ai_connect/main/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,16 @@ class IsTestUser(BasePermission):
def has_permission(self, request, view):
user = request.user
return user.is_authenticated and user.groups.filter(name="test").exists()


class IsAAPUser(BasePermission):
"""
Allow access only to authenticated AAP users
"""

code = "permission_denied_user_not_from_aap"
message = "The user is not an AAP User"

def has_permission(self, request, view):
user = request.user
return user.is_authenticated and user.aap_user
6 changes: 6 additions & 0 deletions ansible_ai_connect/main/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"ansible_ai_connect.healthcheck",
"oauth2_provider",
"import_export",
"ansible_base.resource_registry",
"ansible_base.jwt_consumer",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -262,6 +264,7 @@ def is_ssl_enabled(value: str) -> bool:
"DEFAULT_AUTHENTICATION_CLASSES": [
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
"rest_framework.authentication.SessionAuthentication",
"ansible_ai_connect.users.authentication.LightspeedJWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
Expand All @@ -276,6 +279,9 @@ def is_ssl_enabled(value: str) -> bool:

API_VERSION = "1.0.0"

ANSIBLE_BASE_ORGANIZATION_MODEL = "ansible_ai_connect.organizations.models.Organization"
ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = "ansible_ai_connect.ai.resource_api"

# Current RHSSOAuthentication implementation is incompatible with tech preview terms partial
if not ANSIBLE_AI_ENABLE_TECH_PREVIEW:
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].insert(
Expand Down
1 change: 1 addition & 0 deletions ansible_ai_connect/main/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
{"name": "wca", "description": "watsonx Code Assistant"},
],
"SCHEMA_PATH_PREFIX": r"/api/v[0-9]+",
"PREPROCESSING_HOOKS": ["ansible_ai_connect.ai.api.openapi.preprocessing_filter_spec"],
}

# social_django does not process auth exceptions when DEBUG=True by default.
Expand Down
4 changes: 2 additions & 2 deletions ansible_ai_connect/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
IsOrganisationLightspeedSubscriber,
)
from ansible_ai_connect.main.base_views import ProtectedTemplateView
from ansible_ai_connect.main.permissions import IsRHInternalUser, IsTestUser
from ansible_ai_connect.main.permissions import IsAAPUser, IsRHInternalUser, IsTestUser
from ansible_ai_connect.main.settings.base import SOCIAL_AUTH_OIDC_KEY

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -118,7 +118,7 @@ class ChatbotView(ProtectedTemplateView):
template_name = "chatbot/index.html"

permission_classes = [
IsRHInternalUser | IsTestUser,
IsRHInternalUser | IsTestUser | IsAAPUser,
]

llm: ModelPipelineChatBot
Expand Down
12 changes: 12 additions & 0 deletions ansible_ai_connect/users/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from ansible_base.jwt_consumer.common.auth import JWTAuthentication


class LightspeedJWTAuthentication(JWTAuthentication):

def authenticate(self, request):
userdata = super().authenticate(request)
if userdata:
user, _ = userdata
user.aap_user = True
user.save()
return userdata
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 4.2.18 on 2025-02-11 09:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0014_user_rh_internal_user"),
]

operations = [
migrations.AlterField(
model_name="user",
name="email",
field=models.CharField(default=None, max_length=150, null=True),
),
migrations.AlterField(
model_name="user",
name="external_username",
field=models.CharField(default="", max_length=150),
),
migrations.AlterField(
model_name="user",
name="family_name",
field=models.CharField(default=None, max_length=150, null=True),
),
migrations.AlterField(
model_name="user",
name="given_name",
field=models.CharField(default=None, max_length=150, null=True),
),
migrations.AlterField(
model_name="user",
name="name",
field=models.CharField(default=None, max_length=150, null=True),
),
]
18 changes: 18 additions & 0 deletions ansible_ai_connect/users/migrations/0016_user_aap_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.18 on 2025-02-17 08:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0015_alter_user_email_alter_user_external_username_and_more"),
]

operations = [
migrations.AddField(
model_name="user",
name="aap_user",
field=models.BooleanField(default=False),
),
]
11 changes: 6 additions & 5 deletions ansible_ai_connect/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ class User(ExportModelOperationsMixin("user"), AbstractUser):
)
rh_user_is_org_admin = models.BooleanField(default=False)
rh_internal = models.BooleanField(default=False)
external_username = models.CharField(default="", null=False)
name = models.CharField(default=None, null=True)
given_name = models.CharField(default=None, null=True)
family_name = models.CharField(default=None, null=True)
email = models.CharField(default=None, null=True)
external_username = models.CharField(default="", null=False, max_length=150)
name = models.CharField(default=None, null=True, max_length=150)
given_name = models.CharField(default=None, null=True, max_length=150)
family_name = models.CharField(default=None, null=True, max_length=150)
email = models.CharField(default=None, null=True, max_length=150)
email_verified = models.BooleanField(default=False, null=True)
aap_user = models.BooleanField(default=False)

@property
def org_id(self) -> int | None:
Expand Down
24 changes: 21 additions & 3 deletions requirements-aarch64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ anyio==4.6.2.post1
argparse==1.4.0
# via uwsgi-readiness-check
asgiref==3.8.1
# via django
# via
# django
# django-ansible-base
asttokens==2.4.1
# via stack-data
attrs==23.2.0
Expand Down Expand Up @@ -72,6 +74,7 @@ cryptography==43.0.1
# via
# -r requirements.in
# ansible-core
# django-ansible-base
# jwcrypto
# pyopenssl
# social-auth-core
Expand All @@ -88,6 +91,8 @@ django==4.2.18
# via
# -r requirements.in
# django-allow-cidr
# django-ansible-base
# django-crum
# django-csp
# django-deprecate-fields
# django-extensions
Expand All @@ -99,6 +104,10 @@ django==4.2.18
# social-auth-app-django
django-allow-cidr==0.6.0
# via -r requirements.in
django-ansible-base[jwt-consumer,resource-registry] @ git+https://github.com/ansible/django-ansible-base@devel
# via -r requirements.in
django-crum==0.7.9
# via django-ansible-base
django-csp==3.7
# via -r requirements.in
django-deprecate-fields==0.1.1
Expand All @@ -113,11 +122,14 @@ django-oauth-toolkit==2.3.0
# via -r requirements.in
django-prometheus==2.2.0
# via -r requirements.in
django-split-settings==1.3.2
# via django-ansible-base
django-test-migrations==1.3.0
# via -r requirements.in
djangorestframework==3.15.2
# via
# -r requirements.in
# django-ansible-base
# drf-spectacular
drf-spectacular==0.27.2
# via -r requirements.in
Expand Down Expand Up @@ -175,7 +187,9 @@ idna==3.7
# requests
# yarl
inflection==0.5.1
# via drf-spectacular
# via
# django-ansible-base
# drf-spectacular
ipython==8.10.0
# via -r requirements.in
jedi==0.19.1
Expand Down Expand Up @@ -331,6 +345,7 @@ pygments==2.17.2
pyjwt==2.8.0
# via
# -r requirements.in
# django-ansible-base
# social-auth-core
pyopenssl==24.2.1
# via
Expand Down Expand Up @@ -373,6 +388,7 @@ requests==2.32.0
# via
# -r requirements.in
# ansible-risk-insight
# django-ansible-base
# django-oauth-toolkit
# google-api-core
# langchain
Expand Down Expand Up @@ -433,10 +449,11 @@ social-auth-core==4.5.4
# social-auth-app-django
sqlalchemy==2.0.29
# via langchain
sqlparse==0.5.0
sqlparse==0.5.2
# via
# -r requirements.in
# django
# django-ansible-base
stack-data==0.6.3
# via ipython
subprocess-tee==0.4.1
Expand Down Expand Up @@ -472,6 +489,7 @@ urllib3==1.26.19
# via
# -r requirements.in
# botocore
# django-ansible-base
# launchdarkly-server-sdk
# requests
uwsgi==2.0.22
Expand Down
Loading
Loading