Compare commits
	
		
			4 Commits
		
	
	
		
			version/20
			...
			outposts/s
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 396925d1f0 | |||
| 10a8ed164e | |||
| 445dc01dca | |||
| 441916703d | 
| @ -128,6 +128,12 @@ class OutpostConsumer(JsonWebsocketConsumer): | ||||
|             state.args.update(msg.args) | ||||
|         elif msg.instruction == WebsocketMessageInstruction.ACK: | ||||
|             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( | ||||
|             tenant=connection.schema_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" | ||||
|     RADIUS = "radius" | ||||
|     RAC = "rac" | ||||
|     SCIM = "scim" | ||||
|  | ||||
|  | ||||
| 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.radius.controllers.docker import RadiusDockerController | ||||
| 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 | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| 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""" | ||||
|     if not outpost.service_connection: | ||||
|         return None | ||||
| @ -74,6 +76,11 @@ def controller_for_outpost(outpost: Outpost) -> type[BaseController] | None: | ||||
|             return RACDockerController | ||||
|         if isinstance(service_connection, KubernetesServiceConnection): | ||||
|             return RACKubernetesController | ||||
|     if outpost.type == OutpostType.SCIM: | ||||
|         if isinstance(service_connection, DockerServiceConnection): | ||||
|             return SCIMDockerController | ||||
|         if isinstance(service_connection, KubernetesServiceConnection): | ||||
|             return SCIMKubernetesController | ||||
|     return None | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -19,6 +19,7 @@ from authentik.lib.sync.outgoing.exceptions import ( | ||||
|     TransientSyncException, | ||||
| ) | ||||
| 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.schema import ServiceProviderConfiguration | ||||
| from authentik.providers.scim.models import SCIMProvider | ||||
| @ -41,8 +42,7 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"]( | ||||
|  | ||||
|     def __init__(self, provider: SCIMProvider): | ||||
|         super().__init__(provider) | ||||
|         self._session = get_http_session() | ||||
|         self._session.verify = provider.verify_certificates | ||||
|         self._session = self.get_session(provider) | ||||
|         self.provider = provider | ||||
|         # Remove trailing slashes as we assume the URL doesn't have any | ||||
|         base_url = provider.url | ||||
| @ -52,6 +52,15 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"]( | ||||
|         self.token = provider.token | ||||
|         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: | ||||
|         """Wrapper to send a request to the full URL""" | ||||
|         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", | ||||
|                         "ldap", | ||||
|                         "radius", | ||||
|                         "rac" | ||||
|                         "rac", | ||||
|                         "scim" | ||||
|                     ], | ||||
|                     "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) | ||||
| 	} | ||||
| 	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] | ||||
|  | ||||
|  | ||||
| @ -233,15 +233,19 @@ func (a *APIController) AddWSHandler(handler WSHandler) { | ||||
| 	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 { | ||||
| 	allArgs := a.getWebsocketPingArgs() | ||||
| 	for key, value := range args { | ||||
| 		allArgs[key] = value | ||||
| 	} | ||||
| 	aliveMsg := websocketMessage{ | ||||
| 		Instruction: WebsocketInstructionHello, | ||||
| 		Args:        allArgs, | ||||
| 	} | ||||
| 	err := a.wsConn.WriteJSON(aliveMsg) | ||||
| 	return err | ||||
| 	return a.SendWS(WebsocketInstructionHello, args) | ||||
| } | ||||
|  | ||||
| @ -1,19 +1,19 @@ | ||||
| package ak | ||||
|  | ||||
| type websocketInstruction int | ||||
| type WebsocketInstruction int | ||||
|  | ||||
| const ( | ||||
| 	// WebsocketInstructionAck Code used to acknowledge a previous message | ||||
| 	WebsocketInstructionAck websocketInstruction = 0 | ||||
| 	WebsocketInstructionAck WebsocketInstruction = 0 | ||||
| 	// WebsocketInstructionHello Code used to send a healthcheck keepalive | ||||
| 	WebsocketInstructionHello websocketInstruction = 1 | ||||
| 	WebsocketInstructionHello WebsocketInstruction = 1 | ||||
| 	// WebsocketInstructionTriggerUpdate Code received to trigger a config update | ||||
| 	WebsocketInstructionTriggerUpdate websocketInstruction = 2 | ||||
| 	WebsocketInstructionTriggerUpdate WebsocketInstruction = 2 | ||||
| 	// WebsocketInstructionProviderSpecific Code received to trigger some provider specific function | ||||
| 	WebsocketInstructionProviderSpecific websocketInstruction = 3 | ||||
| 	WebsocketInstructionProviderSpecific WebsocketInstruction = 3 | ||||
| ) | ||||
|  | ||||
| type websocketMessage struct { | ||||
| 	Instruction websocketInstruction   `json:"instruction"` | ||||
| 	Instruction WebsocketInstruction   `json:"instruction"` | ||||
| 	Args        map[string]interface{} `json:"args"` | ||||
| } | ||||
|  | ||||
| @ -46705,6 +46705,7 @@ components: | ||||
|       - ldap | ||||
|       - radius | ||||
|       - rac | ||||
|       - scim | ||||
|       type: string | ||||
|     PaginatedApplicationEntitlementList: | ||||
|       type: object | ||||
|  | ||||
| @ -73,6 +73,9 @@ const radiusListFetch = async (page: number, search = "") => | ||||
| const racListProvider = async (page: number, 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 { | ||||
|     switch (type) { | ||||
|         case OutpostTypeEnum.Proxy: | ||||
| @ -83,6 +86,8 @@ function providerProvider(type: OutpostTypeEnum): DataProvider { | ||||
|             return radiusListFetch; | ||||
|         case OutpostTypeEnum.Rac: | ||||
|             return racListProvider; | ||||
|         case OutpostTypeEnum.Scim: | ||||
|             return scimListProvider; | ||||
|         default: | ||||
|             throw new Error(`Unrecognized OutputType: ${type}`); | ||||
|     } | ||||
| @ -142,6 +147,7 @@ export class OutpostForm extends ModelForm<Outpost, string> { | ||||
|             [OutpostTypeEnum.Ldap, msg("LDAP")], | ||||
|             [OutpostTypeEnum.Radius, msg("Radius")], | ||||
|             [OutpostTypeEnum.Rac, msg("RAC")], | ||||
|             [OutpostTypeEnum.Scim, msg("SCIM")], | ||||
|         ]; | ||||
|  | ||||
|         return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name"> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	