diff --git a/Makefile b/Makefile index e23a2c9ff..edac5dc10 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ lint: bandit -r authentik tests lifecycle -x node_modules pylint authentik tests lifecycle -gen: coverage +gen: ./manage.py generate_swagger -o swagger.yaml -f yaml local-stack: @@ -31,7 +31,5 @@ local-stack: docker-compose up -d docker-compose run --rm server migrate -build-static: - docker-compose -f scripts/ci.docker-compose.yml up -d - docker build -t beryju/authentik-static -f static.Dockerfile --network=scripts_default . - docker-compose -f scripts/ci.docker-compose.yml down -v +run: + go run -v cmd/server/main.go diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 1099232d8..6f23700d5 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -1,14 +1,18 @@ """User API Views""" +from json import loads + from django.http.response import Http404 from django.urls import reverse_lazy from django.utils.http import urlencode +from django_filters.filters import CharFilter +from django_filters.filterset import FilterSet from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method from guardian.utils import get_anonymous_user from rest_framework.decorators import action from rest_framework.fields import CharField, JSONField, SerializerMethodField from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import BooleanField, ModelSerializer +from rest_framework.serializers import BooleanField, ModelSerializer, ValidationError from rest_framework.viewsets import ModelViewSet from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h @@ -84,13 +88,42 @@ class UserMetricsSerializer(PassiveSerializer): ) +class UsersFilter(FilterSet): + """Filter for users""" + + attributes = CharFilter( + field_name="attributes", + lookup_expr="", + label="Attributes", + method="filter_attributes", + ) + + # pylint: disable=unused-argument + def filter_attributes(self, queryset, name, value): + """Filter attributes by query args""" + try: + value = loads(value) + except ValueError: + raise ValidationError(detail="filter: failed to parse JSON") + if not isinstance(value, dict): + raise ValidationError(detail="filter: value must be key:value mapping") + qs = {} + for key, _value in value.items(): + qs[f"attributes__{key}"] = _value + return queryset.filter(**qs) + + class Meta: + model = User + fields = ["username", "name", "is_active", "attributes"] + + class UserViewSet(ModelViewSet): """User Viewset""" queryset = User.objects.none() serializer_class = UserSerializer search_fields = ["username", "name", "is_active"] - filterset_fields = ["username", "name", "is_active"] + filterset_class = UsersFilter def get_queryset(self): return User.objects.all().exclude(pk=get_anonymous_user().pk) diff --git a/internal/web/web_proxy.go b/internal/web/web_proxy.go index 03f5232da..af0d8d694 100644 --- a/internal/web/web_proxy.go +++ b/internal/web/web_proxy.go @@ -1,6 +1,7 @@ package web import ( + "net/http" "net/http/httputil" "net/url" ) @@ -9,5 +10,11 @@ func (ws *WebServer) configureProxy() { // Reverse proxy to the application server u, _ := url.Parse("http://localhost:8000") rp := httputil.NewSingleHostReverseProxy(u) + rp.ErrorHandler = ws.proxyErrorHandler ws.m.PathPrefix("/").Handler(rp) } + +func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { + ws.log.WithError(err).Warning("proxy error") + rw.WriteHeader(http.StatusBadGateway) +} diff --git a/swagger.yaml b/swagger.yaml index 4e3173804..9c755dec6 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1994,6 +1994,11 @@ paths: description: '' required: false type: string + - name: attributes + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. diff --git a/web/src/pages/sources/plex/PlexSourceForm.ts b/web/src/pages/sources/plex/PlexSourceForm.ts index ec49ce8e0..45e582c5c 100644 --- a/web/src/pages/sources/plex/PlexSourceForm.ts +++ b/web/src/pages/sources/plex/PlexSourceForm.ts @@ -56,7 +56,7 @@ export class PlexSourceForm extends Form { }; async doAuth(): Promise { - const authInfo = await PlexAPIClient.getPin(this.source?.clientId); + const authInfo = await PlexAPIClient.getPin(this.source?.clientId || ""); const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700); PlexAPIClient.pinPoll(this.source?.clientId || "", authInfo.pin.id).then(token => { authWindow?.close(); diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts index e3b2bbe38..13666a5fe 100644 --- a/web/src/pages/users/UserListPage.ts +++ b/web/src/pages/users/UserListPage.ts @@ -35,12 +35,18 @@ export class UserListPage extends TablePage { @property() order = "last_login"; + @property({ type: Boolean }) + hideServiceAccounts = true; + apiEndpoint(page: number): Promise> { return new CoreApi(DEFAULT_CONFIG).coreUsersList({ ordering: this.order, page: page, pageSize: PAGE_SIZE, search: this.search || "", + attributes: this.hideServiceAccounts ? JSON.stringify({ + "goauthentik.io/user/service-account__isnull": "true" + }) : undefined }); } @@ -163,4 +169,29 @@ export class UserListPage extends TablePage { `; } + renderToolbarAfter(): TemplateResult { + return html`  +
+
+
+
+ { + this.hideServiceAccounts = !this.hideServiceAccounts; + this.page = 1; + this.fetch(); + }} /> + +
+
+
+
`; + } + }