diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index f3725a1db..fc25c2158 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -57,7 +57,10 @@ from authentik.providers.proxy.api import ( ) from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet -from authentik.sources.oauth.api import OAuthSourceViewSet +from authentik.sources.oauth.api.source import OAuthSourceViewSet +from authentik.sources.oauth.api.source_connection import ( + UserOAuthSourceConnectionViewSet, +) from authentik.sources.saml.api import SAMLSourceViewSet from authentik.stages.authenticator_static.api import ( AuthenticatorStaticStageViewSet, @@ -104,6 +107,7 @@ router.register("core/applications", ApplicationViewSet) router.register("core/groups", GroupViewSet) router.register("core/users", UserViewSet) router.register("core/user_consent", UserConsentViewSet) +router.register("core/source_user_connections_oauth", UserOAuthSourceConnectionViewSet) router.register("core/tokens", TokenViewSet) router.register("outposts/outposts", OutpostViewSet) diff --git a/authentik/sources/oauth/api/__init__.py b/authentik/sources/oauth/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/sources/oauth/api.py b/authentik/sources/oauth/api/source.py similarity index 100% rename from authentik/sources/oauth/api.py rename to authentik/sources/oauth/api/source.py diff --git a/authentik/sources/oauth/api/source_connection.py b/authentik/sources/oauth/api/source_connection.py new file mode 100644 index 000000000..4133b897a --- /dev/null +++ b/authentik/sources/oauth/api/source_connection.py @@ -0,0 +1,32 @@ +"""OAuth Source Serializer""" +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.sources import SourceSerializer +from authentik.sources.oauth.models import UserOAuthSourceConnection + + +class UserOAuthSourceConnectionSerializer(SourceSerializer): + """OAuth Source Serializer""" + + class Meta: + model = UserOAuthSourceConnection + fields = [ + "user", + "source", + "identifier", + "access_token", + ] + + +class UserOAuthSourceConnectionViewSet(ModelViewSet): + """Source Viewset""" + + queryset = UserOAuthSourceConnection.objects.all() + serializer_class = UserOAuthSourceConnectionSerializer + + def get_queryset(self): + if not self.request: + return super().get_queryset() + if self.request.user.is_superuser: + return super().get_queryset() + return super().get_queryset().filter(user=self.request.user) diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index 234af4776..9067aae79 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -51,7 +51,7 @@ class OAuthSource(Source): @property def serializer(self) -> Type[Serializer]: - from authentik.sources.oauth.api import OAuthSourceSerializer + from authentik.sources.oauth.api.source import OAuthSourceSerializer return OAuthSourceSerializer diff --git a/swagger.yaml b/swagger.yaml index d6ab656d7..815c6a13b 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1188,6 +1188,147 @@ paths: required: true type: string format: uuid + /core/source_user_connections_oauth/: + get: + operationId: core_source_user_connections_oauth_list + description: Source Viewset + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/UserOAuthSourceConnection' + tags: + - core + post: + operationId: core_source_user_connections_oauth_create + description: Source Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + tags: + - core + parameters: [] + /core/source_user_connections_oauth/{id}/: + get: + operationId: core_source_user_connections_oauth_read + description: Source Viewset + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + tags: + - core + put: + operationId: core_source_user_connections_oauth_update + description: Source Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + tags: + - core + patch: + operationId: core_source_user_connections_oauth_partial_update + description: Source Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/UserOAuthSourceConnection' + tags: + - core + delete: + operationId: core_source_user_connections_oauth_delete + description: Source Viewset + parameters: [] + responses: + '204': + description: '' + tags: + - core + parameters: + - name: id + in: path + description: A unique integer value identifying this User OAuth Source Connection. + required: true + type: integer /core/tokens/: get: operationId: core_tokens_list @@ -10807,6 +10948,29 @@ definitions: attributes: title: Attributes type: object + UserOAuthSourceConnection: + description: OAuth Source Serializer + required: + - user + - source + - identifier + type: object + properties: + user: + title: User + type: integer + source: + title: Source + type: string + identifier: + title: Identifier + type: string + maxLength: 255 + minLength: 1 + access_token: + title: Access token + type: string + x-nullable: true User: title: User description: User Serializer diff --git a/tests/e2e/test_provider_oauth2_oidc_implicit.py b/tests/e2e/test_provider_oauth2_oidc_implicit.py index e3e207607..27dce41fe 100644 --- a/tests/e2e/test_provider_oauth2_oidc_implicit.py +++ b/tests/e2e/test_provider_oauth2_oidc_implicit.py @@ -152,6 +152,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): self.container = self.setup_client() self.driver.get("http://localhost:9009/implicit/") + sleep(2) self.login() self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre"))) sleep(1) @@ -264,6 +265,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): self.container = self.setup_client() self.driver.get("http://localhost:9009/implicit/") + sleep(2) self.login() self.wait.until( ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index f6314db79..b332f6784 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -32,6 +32,7 @@ from authentik.core.api.users import UserSerializer from authentik.core.models import User from authentik.managed.manager import ObjectManager +RETRIES = int(environ.get("RETRIES", "3")) # pylint: disable=invalid-name def USER() -> User: # noqa @@ -205,7 +206,7 @@ def object_manager(func: Callable): return wrapper -def retry(max_retires=3, exceptions=None): +def retry(max_retires=RETRIES, exceptions=None): """Retry test multiple times. Default to catching Selenium Timeout Exception""" if not exceptions: