Compare commits
4 Commits
policies/p
...
outposts/s
| Author | SHA1 | Date | |
|---|---|---|---|
| 396925d1f0 | |||
| 10a8ed164e | |||
| 445dc01dca | |||
| 441916703d |
@ -128,6 +128,12 @@ class OutpostConsumer(JsonWebsocketConsumer):
|
|||||||
state.args.update(msg.args)
|
state.args.update(msg.args)
|
||||||
elif msg.instruction == WebsocketMessageInstruction.ACK:
|
elif msg.instruction == WebsocketMessageInstruction.ACK:
|
||||||
return
|
return
|
||||||
|
elif msg.instruction == WebsocketMessageInstruction.PROVIDER_SPECIFIC:
|
||||||
|
if "response_channel" not in msg.args:
|
||||||
|
return
|
||||||
|
self.logger.debug("Posted response to channel", msg=msg)
|
||||||
|
async_to_sync(self.channel_layer.send)(msg.args.get("response_channel"), content)
|
||||||
|
return
|
||||||
GAUGE_OUTPOSTS_LAST_UPDATE.labels(
|
GAUGE_OUTPOSTS_LAST_UPDATE.labels(
|
||||||
tenant=connection.schema_name,
|
tenant=connection.schema_name,
|
||||||
outpost=self.outpost.name,
|
outpost=self.outpost.name,
|
||||||
|
|||||||
86
authentik/outposts/http.py
Normal file
86
authentik/outposts/http.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from base64 import b64decode
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from random import choice
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
|
from channels_redis.pubsub import RedisPubSubChannelLayer
|
||||||
|
from requests.adapters import BaseAdapter
|
||||||
|
from requests.models import PreparedRequest, Response
|
||||||
|
from requests.utils import CaseInsensitiveDict
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.outposts.models import Outpost
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OutpostPreparedRequest:
|
||||||
|
uid: str
|
||||||
|
method: str
|
||||||
|
url: str
|
||||||
|
headers: dict[str, str]
|
||||||
|
body: Any
|
||||||
|
ssl_verify: bool
|
||||||
|
timeout: int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_requests(req: PreparedRequest) -> "OutpostPreparedRequest":
|
||||||
|
return OutpostPreparedRequest(
|
||||||
|
uid=str(uuid4()),
|
||||||
|
method=req.method,
|
||||||
|
url=req.url,
|
||||||
|
headers=req.headers._store,
|
||||||
|
body=req.body,
|
||||||
|
ssl_verify=True,
|
||||||
|
timeout=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def response_channel(self) -> str:
|
||||||
|
return f"authentik_outpost_http_response_{self.uid}"
|
||||||
|
|
||||||
|
|
||||||
|
class OutpostHTTPAdapter(BaseAdapter):
|
||||||
|
"""Requests Adapter that sends HTTP requests via a specified Outpost"""
|
||||||
|
|
||||||
|
def __init__(self, outpost: Outpost, default_timeout=10):
|
||||||
|
super().__init__()
|
||||||
|
self.__outpost = outpost
|
||||||
|
self.__logger = get_logger().bind()
|
||||||
|
self.__layer: RedisPubSubChannelLayer = get_channel_layer()
|
||||||
|
self.default_timeout = default_timeout
|
||||||
|
|
||||||
|
def parse_response(self, raw_response: dict, req: PreparedRequest) -> Response:
|
||||||
|
res = Response()
|
||||||
|
res.request = req
|
||||||
|
res.status_code = raw_response.get("status")
|
||||||
|
res.url = raw_response.get("final_url")
|
||||||
|
res.headers = CaseInsensitiveDict(raw_response.get("headers"))
|
||||||
|
res._content = b64decode(raw_response.get("body"))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
|
||||||
|
# Convert request so we can send it to the outpost
|
||||||
|
converted = OutpostPreparedRequest.from_requests(request)
|
||||||
|
converted.ssl_verify = verify
|
||||||
|
converted.timeout = timeout if timeout else self.default_timeout
|
||||||
|
# Pick one of the outpost instances
|
||||||
|
state = choice(self.__outpost.state) # nosec
|
||||||
|
self.__logger.debug("sending HTTP request to outpost", uid=converted.uid)
|
||||||
|
async_to_sync(self.__layer.send)(
|
||||||
|
state.uid,
|
||||||
|
{
|
||||||
|
"type": "event.provider.specific",
|
||||||
|
"sub_type": "http_request",
|
||||||
|
"response_channel": converted.response_channel,
|
||||||
|
"request": asdict(converted),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.__logger.debug("receiving HTTP response from outpost", uid=converted.uid)
|
||||||
|
raw_response = async_to_sync(self.__layer.receive)(
|
||||||
|
converted.response_channel,
|
||||||
|
)
|
||||||
|
self.__logger.debug("received HTTP response from outpost", uid=converted.uid)
|
||||||
|
return self.parse_response(raw_response.get("args", {}).get("response", {}), request)
|
||||||
@ -98,6 +98,7 @@ class OutpostType(models.TextChoices):
|
|||||||
LDAP = "ldap"
|
LDAP = "ldap"
|
||||||
RADIUS = "radius"
|
RADIUS = "radius"
|
||||||
RAC = "rac"
|
RAC = "rac"
|
||||||
|
SCIM = "scim"
|
||||||
|
|
||||||
|
|
||||||
def default_outpost_config(host: str | None = None):
|
def default_outpost_config(host: str | None = None):
|
||||||
|
|||||||
@ -43,13 +43,15 @@ from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
|||||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||||
from authentik.providers.radius.controllers.docker import RadiusDockerController
|
from authentik.providers.radius.controllers.docker import RadiusDockerController
|
||||||
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
|
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
|
||||||
|
from authentik.providers.scim.controllers.docker import SCIMDockerController
|
||||||
|
from authentik.providers.scim.controllers.kubernetes import SCIMKubernetesController
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s"
|
CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s"
|
||||||
|
|
||||||
|
|
||||||
def controller_for_outpost(outpost: Outpost) -> type[BaseController] | None:
|
def controller_for_outpost(outpost: Outpost) -> type[BaseController] | None: # noqa: PLR0911
|
||||||
"""Get a controller for the outpost, when a service connection is defined"""
|
"""Get a controller for the outpost, when a service connection is defined"""
|
||||||
if not outpost.service_connection:
|
if not outpost.service_connection:
|
||||||
return None
|
return None
|
||||||
@ -74,6 +76,11 @@ def controller_for_outpost(outpost: Outpost) -> type[BaseController] | None:
|
|||||||
return RACDockerController
|
return RACDockerController
|
||||||
if isinstance(service_connection, KubernetesServiceConnection):
|
if isinstance(service_connection, KubernetesServiceConnection):
|
||||||
return RACKubernetesController
|
return RACKubernetesController
|
||||||
|
if outpost.type == OutpostType.SCIM:
|
||||||
|
if isinstance(service_connection, DockerServiceConnection):
|
||||||
|
return SCIMDockerController
|
||||||
|
if isinstance(service_connection, KubernetesServiceConnection):
|
||||||
|
return SCIMKubernetesController
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,11 @@
|
|||||||
"""Expression Policy API"""
|
"""Expression Policy API"""
|
||||||
|
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.fields import CharField
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.events.logs import LogEventSerializer, capture_logs
|
|
||||||
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
|
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.expression.evaluator import PolicyEvaluator
|
from authentik.policies.expression.evaluator import PolicyEvaluator
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
|
||||||
from authentik.policies.process import PolicyProcess
|
|
||||||
from authentik.policies.types import PolicyRequest
|
|
||||||
from authentik.rbac.decorators import permission_required
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class ExpressionPolicySerializer(PolicySerializer):
|
class ExpressionPolicySerializer(PolicySerializer):
|
||||||
@ -45,50 +30,3 @@ class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
|||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
|
|
||||||
class ExpressionPolicyTestSerializer(PolicyTestSerializer):
|
|
||||||
"""Expression policy test serializer"""
|
|
||||||
|
|
||||||
expression = CharField()
|
|
||||||
|
|
||||||
@permission_required("authentik_policies.view_policy")
|
|
||||||
@extend_schema(
|
|
||||||
request=ExpressionPolicyTestSerializer(),
|
|
||||||
responses={
|
|
||||||
200: PolicyTestResultSerializer(),
|
|
||||||
400: OpenApiResponse(description="Invalid parameters"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
|
||||||
def test(self, request: Request, pk: str) -> Response:
|
|
||||||
"""Test policy"""
|
|
||||||
policy = self.get_object()
|
|
||||||
test_params = self.ExpressionPolicyTestSerializer(data=request.data)
|
|
||||||
if not test_params.is_valid():
|
|
||||||
return Response(test_params.errors, status=400)
|
|
||||||
|
|
||||||
# User permission check, only allow policy testing for users that are readable
|
|
||||||
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
|
|
||||||
pk=test_params.validated_data["user"].pk
|
|
||||||
)
|
|
||||||
if not users.exists():
|
|
||||||
return Response(status=400)
|
|
||||||
|
|
||||||
policy.expression = test_params.validated_data["expression"]
|
|
||||||
|
|
||||||
p_request = PolicyRequest(users.first())
|
|
||||||
p_request.debug = True
|
|
||||||
p_request.set_http_request(self.request)
|
|
||||||
p_request.context = test_params.validated_data.get("context", {})
|
|
||||||
|
|
||||||
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
|
|
||||||
with capture_logs() as logs:
|
|
||||||
result = proc.execute()
|
|
||||||
log_messages = []
|
|
||||||
for log in logs:
|
|
||||||
if log.attributes.get("process", "") == "PolicyProcess":
|
|
||||||
continue
|
|
||||||
log_messages.append(LogEventSerializer(log).data)
|
|
||||||
result.log_messages = log_messages
|
|
||||||
response = PolicyTestResultSerializer(result)
|
|
||||||
return Response(response.data)
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
|||||||
TransientSyncException,
|
TransientSyncException,
|
||||||
)
|
)
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
|
from authentik.outposts.http import OutpostHTTPAdapter
|
||||||
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
||||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
||||||
from authentik.providers.scim.models import SCIMProvider
|
from authentik.providers.scim.models import SCIMProvider
|
||||||
@ -41,8 +42,7 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
|||||||
|
|
||||||
def __init__(self, provider: SCIMProvider):
|
def __init__(self, provider: SCIMProvider):
|
||||||
super().__init__(provider)
|
super().__init__(provider)
|
||||||
self._session = get_http_session()
|
self._session = self.get_session(provider)
|
||||||
self._session.verify = provider.verify_certificates
|
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
# Remove trailing slashes as we assume the URL doesn't have any
|
# Remove trailing slashes as we assume the URL doesn't have any
|
||||||
base_url = provider.url
|
base_url = provider.url
|
||||||
@ -52,6 +52,15 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
|||||||
self.token = provider.token
|
self.token = provider.token
|
||||||
self._config = self.get_service_provider_config()
|
self._config = self.get_service_provider_config()
|
||||||
|
|
||||||
|
def get_session(self, provider: SCIMProvider):
|
||||||
|
session = get_http_session()
|
||||||
|
if self.provider.outpost_set.exists():
|
||||||
|
adapter = OutpostHTTPAdapter()
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
session.mount("http://", adapter)
|
||||||
|
session.verify = provider.verify_certificates
|
||||||
|
return session
|
||||||
|
|
||||||
def _request(self, method: str, path: str, **kwargs) -> dict:
|
def _request(self, method: str, path: str, **kwargs) -> dict:
|
||||||
"""Wrapper to send a request to the full URL"""
|
"""Wrapper to send a request to the full URL"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
0
authentik/providers/scim/controllers/__init__.py
Normal file
0
authentik/providers/scim/controllers/__init__.py
Normal file
12
authentik/providers/scim/controllers/docker.py
Normal file
12
authentik/providers/scim/controllers/docker.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"""SCIM Provider Docker Controller"""
|
||||||
|
|
||||||
|
from authentik.outposts.controllers.docker import DockerController
|
||||||
|
from authentik.outposts.models import DockerServiceConnection, Outpost
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMDockerController(DockerController):
|
||||||
|
"""SCIM Provider Docker Controller"""
|
||||||
|
|
||||||
|
def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
|
||||||
|
super().__init__(outpost, connection)
|
||||||
|
self.deployment_ports = []
|
||||||
14
authentik/providers/scim/controllers/kubernetes.py
Normal file
14
authentik/providers/scim/controllers/kubernetes.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""SCIM Provider Kubernetes Controller"""
|
||||||
|
|
||||||
|
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
||||||
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
|
from authentik.outposts.models import KubernetesServiceConnection, Outpost
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMKubernetesController(KubernetesController):
|
||||||
|
"""SCIM Provider Kubernetes Controller"""
|
||||||
|
|
||||||
|
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
|
||||||
|
super().__init__(outpost, connection)
|
||||||
|
self.deployment_ports = []
|
||||||
|
del self.reconcilers[ServiceReconciler.reconciler_name()]
|
||||||
@ -4381,7 +4381,8 @@
|
|||||||
"proxy",
|
"proxy",
|
||||||
"ldap",
|
"ldap",
|
||||||
"radius",
|
"radius",
|
||||||
"rac"
|
"rac",
|
||||||
|
"scim"
|
||||||
],
|
],
|
||||||
"title": "Type"
|
"title": "Type"
|
||||||
},
|
},
|
||||||
|
|||||||
185
cmd/scim/main.go
Normal file
185
cmd/scim/main.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/common"
|
||||||
|
"goauthentik.io/internal/debug"
|
||||||
|
"goauthentik.io/internal/outpost/ak"
|
||||||
|
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
const helpMessage = `authentik SCIM
|
||||||
|
|
||||||
|
Required environment variables:
|
||||||
|
- AUTHENTIK_HOST: URL to connect to (format "http://authentik.company")
|
||||||
|
- AUTHENTIK_TOKEN: Token to authenticate with
|
||||||
|
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Long: helpMessage,
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
log.SetFormatter(&log.JSONFormatter{
|
||||||
|
FieldMap: log.FieldMap{
|
||||||
|
log.FieldKeyMsg: "event",
|
||||||
|
log.FieldKeyTime: "timestamp",
|
||||||
|
},
|
||||||
|
DisableHTMLEscape: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
debug.EnableDebugServer()
|
||||||
|
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||||
|
if !found {
|
||||||
|
fmt.Println("env AUTHENTIK_HOST not set!")
|
||||||
|
fmt.Println(helpMessage)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
|
||||||
|
if !found {
|
||||||
|
fmt.Println("env AUTHENTIK_TOKEN not set!")
|
||||||
|
fmt.Println(helpMessage)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
akURLActual, err := url.Parse(akURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Println(helpMessage)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ex := common.Init()
|
||||||
|
defer common.Defer()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
<-ex
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ac := ak.NewAPIController(*akURLActual, akToken)
|
||||||
|
if ac == nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer ac.Shutdown()
|
||||||
|
|
||||||
|
ac.Server = &SCIMOutpost{
|
||||||
|
ac: ac,
|
||||||
|
log: log.WithField("logger", "authentik.outpost.scim"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ac.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Panic("Failed to run server")
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
<-ex
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPRequest struct {
|
||||||
|
Uid string `mapstructure:"uid"`
|
||||||
|
Method string `mapstructure:"method"`
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
Headers map[string][]string `mapstructure:"headers"`
|
||||||
|
Body interface{} `mapstructure:"body"`
|
||||||
|
SSLVerify bool `mapstructure:"ssl_verify"`
|
||||||
|
Timeout int `mapstructure:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestArgs struct {
|
||||||
|
Request HTTPRequest `mapstructure:"request"`
|
||||||
|
ResponseChannel string `mapstructure:"response_channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SCIMOutpost struct {
|
||||||
|
ac *ak.APIController
|
||||||
|
log *log.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SCIMOutpost) Type() string { return "SCIM" }
|
||||||
|
func (s *SCIMOutpost) Stop() error { return nil }
|
||||||
|
func (s *SCIMOutpost) Refresh() error { return nil }
|
||||||
|
func (s *SCIMOutpost) TimerFlowCacheExpiry(context.Context) {}
|
||||||
|
|
||||||
|
func (s *SCIMOutpost) Start() error {
|
||||||
|
s.ac.AddWSHandler(func(ctx context.Context, args map[string]interface{}) {
|
||||||
|
rd := RequestArgs{}
|
||||||
|
err := mapstructure.Decode(args, &rd)
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Warning("failed to parse http request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.log.WithField("rd", rd).WithField("raw", args).Debug("request data")
|
||||||
|
ctx, canc := context.WithTimeout(ctx, time.Duration(rd.Request.Timeout)*time.Second)
|
||||||
|
defer canc()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, rd.Request.Method, rd.Request.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Warning("failed to create request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: !rd.Request.SSLVerify},
|
||||||
|
TLSHandshakeTimeout: time.Duration(rd.Request.Timeout) * time.Second,
|
||||||
|
IdleConnTimeout: time.Duration(rd.Request.Timeout) * time.Second,
|
||||||
|
ResponseHeaderTimeout: time.Duration(rd.Request.Timeout) * time.Second,
|
||||||
|
ExpectContinueTimeout: time.Duration(rd.Request.Timeout) * time.Second,
|
||||||
|
}
|
||||||
|
c := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
}
|
||||||
|
s.log.WithField("url", req.URL.Host).Debug("sending HTTP request")
|
||||||
|
res, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Warning("failed to send request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Warning("failed to read body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.log.WithField("res", res.StatusCode).Debug("sending HTTP response")
|
||||||
|
err = s.ac.SendWS(ak.WebsocketInstructionProviderSpecific, map[string]interface{}{
|
||||||
|
"sub_type": "http_response",
|
||||||
|
"response_channel": rd.ResponseChannel,
|
||||||
|
"response": map[string]interface{}{
|
||||||
|
"status": res.StatusCode,
|
||||||
|
"final_url": res.Request.URL.String(),
|
||||||
|
"headers": res.Header,
|
||||||
|
"body": base64.StdEncoding.EncodeToString(body),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Warning("failed to send http response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rootCmd.AddCommand(healthcheck.Command)
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -95,7 +95,7 @@ func NewAPIController(akURL url.URL, token string) *APIController {
|
|||||||
time.Sleep(time.Second * 3)
|
time.Sleep(time.Second * 3)
|
||||||
}
|
}
|
||||||
if len(outposts.Results) < 1 {
|
if len(outposts.Results) < 1 {
|
||||||
panic("No outposts found with given token, ensure the given token corresponds to an authenitk Outpost")
|
panic("No outposts found with given token, ensure the given token corresponds to an authentik Outpost")
|
||||||
}
|
}
|
||||||
outpost := outposts.Results[0]
|
outpost := outposts.Results[0]
|
||||||
|
|
||||||
|
|||||||
@ -233,15 +233,19 @@ func (a *APIController) AddWSHandler(handler WSHandler) {
|
|||||||
a.wsHandlers = append(a.wsHandlers, handler)
|
a.wsHandlers = append(a.wsHandlers, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *APIController) SendWS(inst WebsocketInstruction, args map[string]interface{}) error {
|
||||||
|
msg := websocketMessage{
|
||||||
|
Instruction: inst,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
err := a.wsConn.WriteJSON(msg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (a *APIController) SendWSHello(args map[string]interface{}) error {
|
func (a *APIController) SendWSHello(args map[string]interface{}) error {
|
||||||
allArgs := a.getWebsocketPingArgs()
|
allArgs := a.getWebsocketPingArgs()
|
||||||
for key, value := range args {
|
for key, value := range args {
|
||||||
allArgs[key] = value
|
allArgs[key] = value
|
||||||
}
|
}
|
||||||
aliveMsg := websocketMessage{
|
return a.SendWS(WebsocketInstructionHello, args)
|
||||||
Instruction: WebsocketInstructionHello,
|
|
||||||
Args: allArgs,
|
|
||||||
}
|
|
||||||
err := a.wsConn.WriteJSON(aliveMsg)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
package ak
|
package ak
|
||||||
|
|
||||||
type websocketInstruction int
|
type WebsocketInstruction int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// WebsocketInstructionAck Code used to acknowledge a previous message
|
// WebsocketInstructionAck Code used to acknowledge a previous message
|
||||||
WebsocketInstructionAck websocketInstruction = 0
|
WebsocketInstructionAck WebsocketInstruction = 0
|
||||||
// WebsocketInstructionHello Code used to send a healthcheck keepalive
|
// WebsocketInstructionHello Code used to send a healthcheck keepalive
|
||||||
WebsocketInstructionHello websocketInstruction = 1
|
WebsocketInstructionHello WebsocketInstruction = 1
|
||||||
// WebsocketInstructionTriggerUpdate Code received to trigger a config update
|
// WebsocketInstructionTriggerUpdate Code received to trigger a config update
|
||||||
WebsocketInstructionTriggerUpdate websocketInstruction = 2
|
WebsocketInstructionTriggerUpdate WebsocketInstruction = 2
|
||||||
// WebsocketInstructionProviderSpecific Code received to trigger some provider specific function
|
// WebsocketInstructionProviderSpecific Code received to trigger some provider specific function
|
||||||
WebsocketInstructionProviderSpecific websocketInstruction = 3
|
WebsocketInstructionProviderSpecific WebsocketInstruction = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type websocketMessage struct {
|
type websocketMessage struct {
|
||||||
Instruction websocketInstruction `json:"instruction"`
|
Instruction WebsocketInstruction `json:"instruction"`
|
||||||
Args map[string]interface{} `json:"args"`
|
Args map[string]interface{} `json:"args"`
|
||||||
}
|
}
|
||||||
|
|||||||
53
schema.yml
53
schema.yml
@ -12813,43 +12813,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
/policies/expression/{policy_uuid}/test/:
|
|
||||||
post:
|
|
||||||
operationId: policies_expression_test_create
|
|
||||||
description: Test policy
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: policy_uuid
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: A UUID string identifying this Expression Policy.
|
|
||||||
required: true
|
|
||||||
tags:
|
|
||||||
- policies
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ExpressionPolicyTestRequest'
|
|
||||||
required: true
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/PolicyTestResult'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
description: Invalid parameters
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
/policies/expression/{policy_uuid}/used_by/:
|
/policies/expression/{policy_uuid}/used_by/:
|
||||||
get:
|
get:
|
||||||
operationId: policies_expression_used_by_list
|
operationId: policies_expression_used_by_list
|
||||||
@ -42317,21 +42280,6 @@ components:
|
|||||||
required:
|
required:
|
||||||
- expression
|
- expression
|
||||||
- name
|
- name
|
||||||
ExpressionPolicyTestRequest:
|
|
||||||
type: object
|
|
||||||
description: Expression policy test serializer
|
|
||||||
properties:
|
|
||||||
user:
|
|
||||||
type: integer
|
|
||||||
context:
|
|
||||||
type: object
|
|
||||||
additionalProperties: {}
|
|
||||||
expression:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
|
||||||
- expression
|
|
||||||
- user
|
|
||||||
ExtraRoleObjectPermission:
|
ExtraRoleObjectPermission:
|
||||||
type: object
|
type: object
|
||||||
description: User permission with additional object-related data
|
description: User permission with additional object-related data
|
||||||
@ -46757,6 +46705,7 @@ components:
|
|||||||
- ldap
|
- ldap
|
||||||
- radius
|
- radius
|
||||||
- rac
|
- rac
|
||||||
|
- scim
|
||||||
type: string
|
type: string
|
||||||
PaginatedApplicationEntitlementList:
|
PaginatedApplicationEntitlementList:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@ -73,6 +73,9 @@ const radiusListFetch = async (page: number, search = "") =>
|
|||||||
const racListProvider = async (page: number, search = "") =>
|
const racListProvider = async (page: number, search = "") =>
|
||||||
provisionMaker(await api().providersRacList(providerListArgs(page, search)));
|
provisionMaker(await api().providersRacList(providerListArgs(page, search)));
|
||||||
|
|
||||||
|
const scimListProvider = async (page: number, search = "") =>
|
||||||
|
provisionMaker(await api().providersScimList(providerListArgs(page, search)));
|
||||||
|
|
||||||
function providerProvider(type: OutpostTypeEnum): DataProvider {
|
function providerProvider(type: OutpostTypeEnum): DataProvider {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case OutpostTypeEnum.Proxy:
|
case OutpostTypeEnum.Proxy:
|
||||||
@ -83,6 +86,8 @@ function providerProvider(type: OutpostTypeEnum): DataProvider {
|
|||||||
return radiusListFetch;
|
return radiusListFetch;
|
||||||
case OutpostTypeEnum.Rac:
|
case OutpostTypeEnum.Rac:
|
||||||
return racListProvider;
|
return racListProvider;
|
||||||
|
case OutpostTypeEnum.Scim:
|
||||||
|
return scimListProvider;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unrecognized OutputType: ${type}`);
|
throw new Error(`Unrecognized OutputType: ${type}`);
|
||||||
}
|
}
|
||||||
@ -142,6 +147,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
|||||||
[OutpostTypeEnum.Ldap, msg("LDAP")],
|
[OutpostTypeEnum.Ldap, msg("LDAP")],
|
||||||
[OutpostTypeEnum.Radius, msg("Radius")],
|
[OutpostTypeEnum.Radius, msg("Radius")],
|
||||||
[OutpostTypeEnum.Rac, msg("RAC")],
|
[OutpostTypeEnum.Rac, msg("RAC")],
|
||||||
|
[OutpostTypeEnum.Scim, msg("SCIM")],
|
||||||
];
|
];
|
||||||
|
|
||||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import "@goauthentik/admin/policies/password/PasswordPolicyForm";
|
|||||||
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
|
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
|
||||||
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { PFSize } from "@goauthentik/common/enums";
|
|
||||||
import { PFColor } from "@goauthentik/elements/Label";
|
import { PFColor } from "@goauthentik/elements/Label";
|
||||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
@ -22,6 +21,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
|||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import { PoliciesApi, Policy } from "@goauthentik/api";
|
import { PoliciesApi, Policy } from "@goauthentik/api";
|
||||||
|
|
||||||
@ -71,12 +71,7 @@ export class PolicyListPage extends TablePage<Policy> {
|
|||||||
${msg("Warning: Policy is not assigned.")}
|
${msg("Warning: Policy is not assigned.")}
|
||||||
</ak-label>`}`,
|
</ak-label>`}`,
|
||||||
html`${item.verboseName}`,
|
html`${item.verboseName}`,
|
||||||
html` <ak-forms-modal
|
html` <ak-forms-modal>
|
||||||
size=${item.component === "ak-policy-expression-form"
|
|
||||||
? PFSize.XLarge
|
|
||||||
: PFSize.Large}
|
|
||||||
?fullHeight=${item.component === "ak-policy-expression-form"}
|
|
||||||
>
|
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span slot="header"> ${msg(str`Update ${item.verboseName}`)} </span>
|
<span slot="header"> ${msg(str`Update ${item.verboseName}`)} </span>
|
||||||
<ak-proxy-form
|
<ak-proxy-form
|
||||||
@ -84,7 +79,7 @@ export class PolicyListPage extends TablePage<Policy> {
|
|||||||
.args=${{
|
.args=${{
|
||||||
instancePk: item.pk,
|
instancePk: item.pk,
|
||||||
}}
|
}}
|
||||||
type=${item.component}
|
type=${ifDefined(item.component)}
|
||||||
>
|
>
|
||||||
</ak-proxy-form>
|
</ak-proxy-form>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||||
|
|||||||
@ -87,10 +87,7 @@ export class PolicyWizard extends AKElement {
|
|||||||
slot=${`type-${type.component}-${type.modelName}`}
|
slot=${`type-${type.component}-${type.modelName}`}
|
||||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||||
>
|
>
|
||||||
<ak-proxy-form
|
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||||
?showPreview=${false}
|
|
||||||
type=${type.component}
|
|
||||||
></ak-proxy-form>
|
|
||||||
</ak-wizard-page-form>
|
</ak-wizard-page-form>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,64 +1,25 @@
|
|||||||
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
|
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { docLink } from "@goauthentik/common/global";
|
import { docLink } from "@goauthentik/common/global";
|
||||||
import { me } from "@goauthentik/common/users";
|
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||||
import "@goauthentik/elements/EmptyState";
|
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import YAML from "yaml";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, html, nothing } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api";
|
||||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
|
||||||
import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CoreApi,
|
|
||||||
CoreUsersListRequest,
|
|
||||||
ExpressionPolicy,
|
|
||||||
PoliciesApi,
|
|
||||||
PolicyTestResult,
|
|
||||||
ResponseError,
|
|
||||||
SessionUser,
|
|
||||||
User,
|
|
||||||
ValidationErrorFromJSON,
|
|
||||||
} from "@goauthentik/api";
|
|
||||||
|
|
||||||
@customElement("ak-policy-expression-form")
|
@customElement("ak-policy-expression-form")
|
||||||
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||||
@property({ type: Boolean })
|
loadInstance(pk: string): Promise<ExpressionPolicy> {
|
||||||
showPreview = true;
|
return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionRetrieve({
|
||||||
|
|
||||||
@state()
|
|
||||||
preview?: PolicyTestResult;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
previewError?: string[];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
user?: SessionUser;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
previewLoading = false;
|
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
|
||||||
return super.styles.concat(PFGrid, PFStack, PFTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadInstance(pk: string): Promise<ExpressionPolicy> {
|
|
||||||
const policy = await new PoliciesApi(DEFAULT_CONFIG).policiesExpressionRetrieve({
|
|
||||||
policyUuid: pk,
|
policyUuid: pk,
|
||||||
});
|
});
|
||||||
this.user = await me();
|
|
||||||
await this.refreshPreview(policy);
|
|
||||||
return policy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(data: ExpressionPolicy): Promise<ExpressionPolicy> {
|
async send(data: ExpressionPolicy): Promise<ExpressionPolicy> {
|
||||||
@ -74,196 +35,10 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldRefresh = false;
|
|
||||||
_timer = 0;
|
|
||||||
|
|
||||||
connectedCallback(): void {
|
|
||||||
super.connectedCallback();
|
|
||||||
if (!this.showPreview) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Only check if we should update once a second, to prevent spamming API requests
|
|
||||||
// when many fields are edited
|
|
||||||
const minUpdateDelay = 1000;
|
|
||||||
this._timer = setInterval(() => {
|
|
||||||
if (this._shouldRefresh) {
|
|
||||||
this.refreshPreview();
|
|
||||||
this._shouldRefresh = false;
|
|
||||||
}
|
|
||||||
}, minUpdateDelay) as unknown as number;
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
if (!this.showPreview) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearTimeout(this._timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshPreview(policy?: ExpressionPolicy): Promise<void> {
|
|
||||||
if (!policy) {
|
|
||||||
policy = this.serializeForm();
|
|
||||||
if (!policy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.previewLoading = true;
|
|
||||||
try {
|
|
||||||
interface testpolicy {
|
|
||||||
expression: string;
|
|
||||||
user?: number;
|
|
||||||
context?: { [key: string]: unknown };
|
|
||||||
}
|
|
||||||
const tp = policy as unknown as testpolicy;
|
|
||||||
this.preview = await new PoliciesApi(DEFAULT_CONFIG).policiesExpressionTestCreate({
|
|
||||||
expressionPolicyTestRequest: {
|
|
||||||
expression: tp.expression,
|
|
||||||
user: tp.user || this.user?.user.pk || 0,
|
|
||||||
context: tp.context || {},
|
|
||||||
},
|
|
||||||
policyUuid: this.instancePk || "",
|
|
||||||
});
|
|
||||||
this.previewError = undefined;
|
|
||||||
} catch (exc) {
|
|
||||||
const errorMessage = ValidationErrorFromJSON(
|
|
||||||
await (exc as ResponseError).response.json(),
|
|
||||||
);
|
|
||||||
this.previewError = errorMessage.nonFieldErrors;
|
|
||||||
} finally {
|
|
||||||
this.previewLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
return html`<div class="pf-l-grid pf-m-gutter">
|
|
||||||
<div class="pf-l-grid__item pf-m-6-col pf-l-stack">
|
|
||||||
<div class="pf-c-form pf-m-horizontal pf-l-stack__item">
|
|
||||||
${this.renderEditForm()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-l-grid__item pf-m-6-col">${this.renderPreview()}</div>
|
|
||||||
</div> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPreview(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
|
||||||
<div class="pf-c-card__title">${msg("Test parameters")}</div>
|
|
||||||
<div class="pf-c-card__body pf-c-form pf-m-horizontal">
|
|
||||||
<ak-form-element-horizontal label=${msg("User")} name="user">
|
|
||||||
<ak-search-select
|
|
||||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
|
||||||
const args: CoreUsersListRequest = {
|
|
||||||
ordering: "username",
|
|
||||||
};
|
|
||||||
if (query !== undefined) {
|
|
||||||
args.search = query;
|
|
||||||
}
|
|
||||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
return users.results;
|
|
||||||
}}
|
|
||||||
.renderElement=${(user: User): string => {
|
|
||||||
return user.username;
|
|
||||||
}}
|
|
||||||
.renderDescription=${(user: User): TemplateResult => {
|
|
||||||
return html`${user.name}`;
|
|
||||||
}}
|
|
||||||
.value=${(user: User | undefined): number | undefined => {
|
|
||||||
return user?.pk;
|
|
||||||
}}
|
|
||||||
.selected=${(user: User): boolean => {
|
|
||||||
return this.user?.user.pk === user.pk;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</ak-search-select>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal label=${msg("Context")} name="context">
|
|
||||||
<ak-codemirror mode=${CodeMirrorMode.YAML} value=${YAML.stringify({})}>
|
|
||||||
</ak-codemirror>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__footer">
|
|
||||||
<button
|
|
||||||
class="pf-c-button pf-m-primary"
|
|
||||||
@click=${() => {
|
|
||||||
this.refreshPreview();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Execute")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
|
||||||
<div class="pf-c-card__title">${msg("Test results")}</div>
|
|
||||||
${this.previewLoading
|
|
||||||
? html`<ak-empty-state loading></ak-empty-state>`
|
|
||||||
: html`<div class="pf-c-card__body pf-c-form pf-m-horizontal">
|
|
||||||
<ak-form-element-horizontal label=${msg("Passing")}>
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<div class="c-form__horizontal-group">
|
|
||||||
<span class="pf-c-form__label-text">
|
|
||||||
<ak-status-label
|
|
||||||
?good=${this.preview?.passing}
|
|
||||||
></ak-status-label>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal label=${msg("Messages")}>
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<div class="c-form__horizontal-group">
|
|
||||||
<ul>
|
|
||||||
${(this.preview?.messages || []).length > 0
|
|
||||||
? this.preview?.messages?.map((m) => {
|
|
||||||
return html`<li>
|
|
||||||
<span class="pf-c-form__label-text"
|
|
||||||
>${m}</span
|
|
||||||
>
|
|
||||||
</li>`;
|
|
||||||
})
|
|
||||||
: html`<li>
|
|
||||||
<span class="pf-c-form__label-text">-</span>
|
|
||||||
</li>`}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
|
|
||||||
<ak-form-element-horizontal label=${msg("Log messages")}>
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<div class="c-form__horizontal-group">
|
|
||||||
<dl class="pf-c-description-list pf-m-horizontal">
|
|
||||||
<ak-log-viewer
|
|
||||||
.logs=${this.preview?.logMessages}
|
|
||||||
></ak-log-viewer>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
</div>`}
|
|
||||||
</div>
|
|
||||||
${this.previewError
|
|
||||||
? html`
|
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
|
||||||
<div class="pf-c-card__body">${msg("Preview errors")}</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
${this.previewError.map((err) => html`<pre>${err}</pre>`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEditForm(): TemplateResult {
|
|
||||||
return html` <span>
|
return html` <span>
|
||||||
${msg(
|
${msg(
|
||||||
"Executes the Python snippet to determine whether to allow or deny a request.",
|
"Executes the python snippet to determine whether to allow or deny a request.",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||||
@ -305,9 +80,6 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
|||||||
<ak-codemirror
|
<ak-codemirror
|
||||||
mode=${CodeMirrorMode.Python}
|
mode=${CodeMirrorMode.Python}
|
||||||
value="${ifDefined(this.instance?.expression)}"
|
value="${ifDefined(this.instance?.expression)}"
|
||||||
@change=${() => {
|
|
||||||
this._shouldRefresh = true;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
</ak-codemirror>
|
</ak-codemirror>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
|
|||||||
@ -20,11 +20,8 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStatusBanner() {
|
renderStatusBanner() {
|
||||||
if (!this.licenseSummary) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
// Check if we're in the correct interface to render a banner
|
// Check if we're in the correct interface to render a banner
|
||||||
switch (this.licenseSummary?.status) {
|
switch (this.licenseSummary.status) {
|
||||||
// user warning is both on admin interface and user interface
|
// user warning is both on admin interface and user interface
|
||||||
case LicenseSummaryStatusEnum.LimitExceededUser:
|
case LicenseSummaryStatusEnum.LimitExceededUser:
|
||||||
if (
|
if (
|
||||||
@ -49,7 +46,7 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let message = "";
|
let message = "";
|
||||||
switch (this.licenseSummary?.status) {
|
switch (this.licenseSummary.status) {
|
||||||
case LicenseSummaryStatusEnum.LimitExceededAdmin:
|
case LicenseSummaryStatusEnum.LimitExceededAdmin:
|
||||||
case LicenseSummaryStatusEnum.LimitExceededUser:
|
case LicenseSummaryStatusEnum.LimitExceededUser:
|
||||||
message = msg(
|
message = msg(
|
||||||
@ -86,16 +83,13 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFlagBanner() {
|
renderFlagBanner() {
|
||||||
if (!this.licenseSummary) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
return html`
|
return html`
|
||||||
${this.licenseSummary?.licenseFlags.includes(LicenseFlagsEnum.Trial)
|
${this.licenseSummary.licenseFlags.includes(LicenseFlagsEnum.Trial)
|
||||||
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
||||||
${msg("This authentik instance uses a Trial license.")}
|
${msg("This authentik instance uses a Trial license.")}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this.licenseSummary?.licenseFlags.includes(LicenseFlagsEnum.NonProduction)
|
${this.licenseSummary.licenseFlags.includes(LicenseFlagsEnum.NonProduction)
|
||||||
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
||||||
${msg("This authentik instance uses a Non-production license.")}
|
${msg("This authentik instance uses a Non-production license.")}
|
||||||
</div>`
|
</div>`
|
||||||
|
|||||||
@ -40,9 +40,6 @@ export class ModalButton extends AKElement {
|
|||||||
@property()
|
@property()
|
||||||
size: PFSize = PFSize.Large;
|
size: PFSize = PFSize.Large;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
fullHeight = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
open = false;
|
open = false;
|
||||||
|
|
||||||
@ -72,9 +69,6 @@ export class ModalButton extends AKElement {
|
|||||||
.pf-c-modal-box.pf-m-xl {
|
.pf-c-modal-box.pf-m-xl {
|
||||||
--pf-c-modal-box--Width: calc(1.5 * var(--pf-c-modal-box--m-lg--lg--MaxWidth));
|
--pf-c-modal-box--Width: calc(1.5 * var(--pf-c-modal-box--m-lg--lg--MaxWidth));
|
||||||
}
|
}
|
||||||
:host([fullHeight]) .pf-c-modal-box {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,20 +14,7 @@ import { LogEvent, LogLevelEnum } from "@goauthentik/api";
|
|||||||
@customElement("ak-log-viewer")
|
@customElement("ak-log-viewer")
|
||||||
export class LogViewer extends Table<LogEvent> {
|
export class LogViewer extends Table<LogEvent> {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
set logs(val: LogEvent[]) {
|
logs?: LogEvent[] = [];
|
||||||
this.data = {
|
|
||||||
pagination: {
|
|
||||||
next: 0,
|
|
||||||
previous: 0,
|
|
||||||
count: val.length || 0,
|
|
||||||
current: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
startIndex: 1,
|
|
||||||
endIndex: val.length || 0,
|
|
||||||
},
|
|
||||||
results: val,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
expandable = true;
|
expandable = true;
|
||||||
paginated = false;
|
paginated = false;
|
||||||
@ -37,20 +24,18 @@ export class LogViewer extends Table<LogEvent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async apiEndpoint(): Promise<PaginatedResponse<LogEvent>> {
|
async apiEndpoint(): Promise<PaginatedResponse<LogEvent>> {
|
||||||
return (
|
return {
|
||||||
this.data || {
|
pagination: {
|
||||||
pagination: {
|
next: 0,
|
||||||
next: 0,
|
previous: 0,
|
||||||
previous: 0,
|
count: this.logs?.length || 0,
|
||||||
count: this.logs?.length || 0,
|
current: 1,
|
||||||
current: 1,
|
totalPages: 1,
|
||||||
totalPages: 1,
|
startIndex: 1,
|
||||||
startIndex: 1,
|
endIndex: this.logs?.length || 0,
|
||||||
endIndex: this.logs?.length || 0,
|
},
|
||||||
},
|
results: this.logs || [],
|
||||||
results: this.logs || [],
|
};
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmpty(): TemplateResult {
|
renderEmpty(): TemplateResult {
|
||||||
|
|||||||
@ -38,10 +38,6 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get instancePk(): PKT | undefined {
|
|
||||||
return this._instancePk;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _instancePk?: PKT;
|
private _instancePk?: PKT;
|
||||||
|
|
||||||
// Keep track if we've loaded the model instance
|
// Keep track if we've loaded the model instance
|
||||||
|
|||||||
Reference in New Issue
Block a user