diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 97e7e9c8c..5f3f09b2f 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -27,7 +27,7 @@ jobs: -f Dockerfile . docker-compose up --no-start docker-compose start postgresql redis - docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" + docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" - name: Extract version number id: get_version uses: actions/github-script@0.2.0 diff --git a/authentik/events/api/notification_rule.py b/authentik/events/api/notification_rule.py index 996216e2a..fbe0788c3 100644 --- a/authentik/events/api/notification_rule.py +++ b/authentik/events/api/notification_rule.py @@ -2,22 +2,25 @@ from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet +from authentik.core.api.groups import GroupSerializer from authentik.events.models import NotificationRule class NotificationRuleSerializer(ModelSerializer): """NotificationRule Serializer""" + group_obj = GroupSerializer(read_only=True, source="group") + class Meta: model = NotificationRule - depth = 2 fields = [ "pk", "name", "transports", "severity", "group", + "group_obj", ] diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 6f4aa46e8..24d3fcf2c 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -42,6 +42,7 @@ outposts: # Placeholders: # %(type)s: Outpost type; proxy, ldap, etc # %(version)s: Current version; 2021.4.1 + # %(build_hash)s: Build hash if you're running a beta version docker_image_base: "beryju/authentik-%(type)s:%(version)s" authentik: diff --git a/authentik/outposts/api/outposts.py b/authentik/outposts/api/outposts.py index 7268cfdf6..d026fa503 100644 --- a/authentik/outposts/api/outposts.py +++ b/authentik/outposts/api/outposts.py @@ -18,8 +18,6 @@ class OutpostSerializer(ModelSerializer): """Outpost Serializer""" config = JSONField(validators=[is_dict], source="_config") - # TODO: Remove _config again, this is only here for legacy with older outposts - _config = JSONField(validators=[is_dict], read_only=True) providers_obj = ProviderSerializer(source="providers", many=True, read_only=True) def validate_config(self, config) -> dict: @@ -42,7 +40,6 @@ class OutpostSerializer(ModelSerializer): "service_connection", "token_identifier", "config", - "_config", ] diff --git a/authentik/outposts/channels.py b/authentik/outposts/channels.py index 918b79881..f084b4a3d 100644 --- a/authentik/outposts/channels.py +++ b/authentik/outposts/channels.py @@ -82,6 +82,7 @@ class OutpostConsumer(AuthJsonConsumer): ) if msg.instruction == WebsocketMessageInstruction.HELLO: state.version = msg.args.get("version", None) + state.build_hash = msg.args.get("buildHash", "") elif msg.instruction == WebsocketMessageInstruction.ACK: return state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5) diff --git a/authentik/outposts/controllers/base.py b/authentik/outposts/controllers/base.py index ef22b79d5..fe342cb0f 100644 --- a/authentik/outposts/controllers/base.py +++ b/authentik/outposts/controllers/base.py @@ -1,11 +1,12 @@ """Base Controller""" from dataclasses import dataclass +from os import environ from typing import Optional from structlog.stdlib import get_logger from structlog.testing import capture_logs -from authentik import __version__ +from authentik import ENV_GIT_HASH_KEY, __version__ from authentik.lib.config import CONFIG from authentik.lib.sentry import SentryIgnoredException from authentik.outposts.models import Outpost, OutpostServiceConnection @@ -69,4 +70,8 @@ class BaseController: def get_container_image(self) -> str: """Get container image to use for this outpost""" image_name_template: str = CONFIG.y("outposts.docker_image_base") - return image_name_template % {"type": self.outpost.type, "version": __version__} + return image_name_template % { + "type": self.outpost.type, + "version": __version__, + "build_hash": environ.get(ENV_GIT_HASH_KEY, ""), + } diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index ed21eca3d..c7de05794 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -1,6 +1,7 @@ """Outpost models""" from dataclasses import asdict, dataclass, field from datetime import datetime +from os import environ from typing import Iterable, Optional, Union from uuid import uuid4 @@ -26,7 +27,7 @@ from packaging.version import LegacyVersion, Version, parse from structlog.stdlib import get_logger from urllib3.exceptions import HTTPError -from authentik import __version__ +from authentik import ENV_GIT_HASH_KEY, __version__ from authentik.core.models import USER_ATTRIBUTE_SA, Provider, Token, TokenIntents, User from authentik.crypto.models import CertificateKeyPair from authentik.lib.config import CONFIG @@ -411,6 +412,7 @@ class OutpostState: last_seen: Optional[datetime] = field(default=None) version: Optional[str] = field(default=None) version_should: Union[Version, LegacyVersion] = field(default=OUR_VERSION) + build_hash: str = field(default="") _outpost: Optional[Outpost] = field(default=None) @@ -419,6 +421,8 @@ class OutpostState: """Check if outpost version matches our version""" if not self.version: return False + if self.build_hash != environ.get(ENV_GIT_HASH_KEY, ""): + return False return parse(self.version) < OUR_VERSION @staticmethod diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 63314ab06..5060215fe 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -320,6 +320,7 @@ CELERY_RESULT_BACKEND = ( # Database backup DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} +DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql" if CONFIG.y("postgresql.s3_backup"): DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" DBBACKUP_STORAGE_OPTIONS = { diff --git a/outpost/azure-pipelines.yml b/outpost/azure-pipelines.yml index c3df49c67..0417353bd 100644 --- a/outpost/azure-pipelines.yml +++ b/outpost/azure-pipelines.yml @@ -116,7 +116,10 @@ stages: command: 'buildAndPush' Dockerfile: 'outpost/proxy.Dockerfile' buildContext: 'outpost/' - tags: "gh-$(branchName)" + tags: | + gh-$(branchName) + gh-$(Build.SourceVersion) + arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' - job: ldap_build_docker pool: vmImage: 'ubuntu-latest' @@ -141,4 +144,7 @@ stages: command: 'buildAndPush' Dockerfile: 'outpost/ldap.Dockerfile' buildContext: 'outpost/' - tags: "gh-$(branchName)" + tags: | + gh-$(branchName) + gh-$(Build.SourceVersion) + arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' diff --git a/outpost/ldap.Dockerfile b/outpost/ldap.Dockerfile index 08149fe05..a204c15be 100644 --- a/outpost/ldap.Dockerfile +++ b/outpost/ldap.Dockerfile @@ -1,4 +1,6 @@ FROM golang:1.16.4 AS builder +ARG GIT_BUILD_HASH +ENV GIT_BUILD_HASH=$GIT_BUILD_HASH WORKDIR /work diff --git a/outpost/pkg/ak/api.go b/outpost/pkg/ak/api.go index c897003a4..413a8afaf 100644 --- a/outpost/pkg/ak/api.go +++ b/outpost/pkg/ak/api.go @@ -1,7 +1,6 @@ package ak import ( - "fmt" "math/rand" "net/url" "os" @@ -43,7 +42,7 @@ type APIController struct { // NewAPIController initialise new API Controller instance from URL and API token func NewAPIController(akURL url.URL, token string) *APIController { transport := httptransport.New(akURL.Host, client.DefaultBasePath, []string{akURL.Scheme}) - transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)) + transport.Transport = SetUserAgent(getTLSTransport(), pkg.UserAgent()) // create the transport auth := httptransport.BearerToken(token) diff --git a/outpost/pkg/ak/api_ws.go b/outpost/pkg/ak/api_ws.go index fb5526961..898579418 100644 --- a/outpost/pkg/ak/api_ws.go +++ b/outpost/pkg/ak/api_ws.go @@ -23,7 +23,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) { header := http.Header{ "Authorization": []string{authHeader}, - "User-Agent": []string{fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)}, + "User-Agent": []string{pkg.UserAgent()}, } value, set := os.LookupEnv("AUTHENTIK_INSECURE") @@ -46,8 +46,9 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) { msg := websocketMessage{ Instruction: WebsocketInstructionHello, Args: map[string]interface{}{ - "version": pkg.VERSION, - "uuid": ac.instanceUUID.String(), + "version": pkg.VERSION, + "buildHash": pkg.BUILD(), + "uuid": ac.instanceUUID.String(), }, } err := ws.WriteJSON(msg) @@ -76,7 +77,7 @@ func (ac *APIController) startWSHandler() { var wsMsg websocketMessage err := ac.wsConn.ReadJSON(&wsMsg) if err != nil { - logger.Println("read:", err) + logger.WithError(err).Warning("ws write error, reconnecting") ac.wsConn.CloseAndReconnect() continue } @@ -100,14 +101,15 @@ func (ac *APIController) startWSHealth() { aliveMsg := websocketMessage{ Instruction: WebsocketInstructionHello, Args: map[string]interface{}{ - "version": pkg.VERSION, - "uuid": ac.instanceUUID.String(), + "version": pkg.VERSION, + "buildHash": pkg.BUILD(), + "uuid": ac.instanceUUID.String(), }, } err := ac.wsConn.WriteJSON(aliveMsg) ac.logger.WithField("loop", "ws-health").Trace("hello'd") if err != nil { - ac.logger.WithField("loop", "ws-health").Println("write:", err) + ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error, reconnecting") ac.wsConn.CloseAndReconnect() continue } diff --git a/outpost/pkg/ak/global.go b/outpost/pkg/ak/global.go index 108803ec3..b446075c5 100644 --- a/outpost/pkg/ak/global.go +++ b/outpost/pkg/ak/global.go @@ -33,7 +33,7 @@ func doGlobalSetup(config map[string]interface{}) { default: log.SetLevel(log.DebugLevel) } - log.WithField("version", pkg.VERSION).Info("Starting authentik outpost") + log.WithField("buildHash", pkg.BUILD()).WithField("version", pkg.VERSION).Info("Starting authentik outpost") var dsn string if config[ConfigErrorReportingEnabled].(bool) { diff --git a/outpost/pkg/ldap/bind.go b/outpost/pkg/ldap/bind.go index 1fcc6e781..c050b7184 100644 --- a/outpost/pkg/ldap/bind.go +++ b/outpost/pkg/ldap/bind.go @@ -2,20 +2,22 @@ package ldap import ( "net" + "strings" "github.com/nmcclain/ldap" ) func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { - ls.log.WithField("boundDN", bindDN).Info("bind") + ls.log.WithField("bindDN", bindDN).Info("bind") + bindDN = strings.ToLower(bindDN) for _, instance := range ls.providers { username, err := instance.getUsername(bindDN) if err == nil { - return instance.Bind(username, bindPW, conn) + return instance.Bind(username, bindDN, bindPW, conn) } else { ls.log.WithError(err).Debug("Username not for instance") } } - ls.log.WithField("boundDN", bindDN).WithField("request", "bind").Warning("No provider found for request") + ls.log.WithField("bindDN", bindDN).WithField("request", "bind").Warning("No provider found for request") return ldap.LDAPResultOperationsError, nil } diff --git a/outpost/pkg/ldap/instance_bind.go b/outpost/pkg/ldap/instance_bind.go index e9cb527f0..b9a860854 100644 --- a/outpost/pkg/ldap/instance_bind.go +++ b/outpost/pkg/ldap/instance_bind.go @@ -47,7 +47,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) { return "", errors.New("failed to find cn") } -func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { +func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { jar, err := cookiejar.New(nil) if err != nil { pi.log.WithError(err).Warning("Failed to create cookiejar") @@ -67,9 +67,9 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) } params := url.Values{} params.Add("goauthentik.io/outpost/ldap", "true") - passed, err := pi.solveFlowChallenge(username, bindPW, client, params.Encode()) + passed, err := pi.solveFlowChallenge(username, bindPW, client, params.Encode(), 1) if err != nil { - pi.log.WithField("boundDN", username).WithError(err).Warning("failed to solve challenge") + pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to solve challenge") return ldap.LDAPResultOperationsError, nil } if !passed { @@ -82,25 +82,25 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) }, httptransport.PassThroughAuth) if err != nil { if _, denied := err.(*core.CoreApplicationsCheckAccessForbidden); denied { - pi.log.WithField("boundDN", username).Info("Access denied for user") + pi.log.WithField("bindDN", bindDN).Info("Access denied for user") return ldap.LDAPResultInsufficientAccessRights, nil } - pi.log.WithField("boundDN", username).WithError(err).Warning("failed to check access") + pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to check access") return ldap.LDAPResultOperationsError, nil } - pi.log.WithField("boundDN", username).Info("User has access") + pi.log.WithField("bindDN", bindDN).Info("User has access") // Get user info to store in context userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{ Context: context.Background(), HTTPClient: client, }, httptransport.PassThroughAuth) if err != nil { - pi.log.WithField("boundDN", username).WithError(err).Warning("failed to get user info") + pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to get user info") return ldap.LDAPResultOperationsError, nil } pi.boundUsersMutex.Lock() - pi.boundUsers[username] = UserFlags{ - UserInfo: userInfo.Payload.User, + pi.boundUsers[bindDN] = UserFlags{ + UserInfo: *userInfo.Payload.User, CanSearch: pi.SearchAccessCheck(userInfo.Payload.User), } defer pi.boundUsersMutex.Unlock() @@ -112,7 +112,8 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) func (pi *ProviderInstance) SearchAccessCheck(user *models.User) bool { for _, group := range user.Groups { for _, allowedGroup := range pi.searchAllowedGroups { - if &group.Pk == allowedGroup { + pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access") + if group.Pk.String() == allowedGroup.String() { pi.log.WithField("group", group.Name).Info("Allowed access to search") return true } @@ -139,7 +140,7 @@ func (pi *ProviderInstance) delayDeleteUserInfo(dn string) { }() } -func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client, urlParams string) (bool, error) { +func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client, urlParams string, depth int) (bool, error) { challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ FlowSlug: pi.flowSlug, Query: urlParams, @@ -169,6 +170,10 @@ func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, c } response, err := pi.s.ac.Client.Flows.FlowsExecutorSolve(responseParams, pi.s.ac.Auth) pi.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response") + switch response.Payload.Component { + case "ak-stage-access-denied": + return false, errors.New("got ak-stage-access-denied") + } if *response.Payload.Type == "redirect" { return true, nil } @@ -184,5 +189,8 @@ func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, c } } } - return pi.solveFlowChallenge(bindDN, password, client, urlParams) + if depth >= 10 { + return false, errors.New("exceeded stage recursion depth") + } + return pi.solveFlowChallenge(bindDN, password, client, urlParams, depth+1) } diff --git a/outpost/pkg/ldap/instance_search.go b/outpost/pkg/ldap/instance_search.go index b7102c347..913adcbb2 100644 --- a/outpost/pkg/ldap/instance_search.go +++ b/outpost/pkg/ldap/instance_search.go @@ -29,10 +29,13 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, pi.boundUsersMutex.RLock() defer pi.boundUsersMutex.RUnlock() flags, ok := pi.boundUsers[bindDN] + pi.log.WithField("bindDN", bindDN).WithField("ok", ok).Debugf("%+v\n", flags) if !ok { + pi.log.Debug("User info not cached") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") } if !flags.CanSearch { + pi.log.Debug("User can't search") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") } diff --git a/outpost/pkg/ldap/ldap.go b/outpost/pkg/ldap/ldap.go index a22348639..f7253108e 100644 --- a/outpost/pkg/ldap/ldap.go +++ b/outpost/pkg/ldap/ldap.go @@ -31,7 +31,7 @@ type ProviderInstance struct { } type UserFlags struct { - UserInfo *models.User + UserInfo models.User CanSearch bool } diff --git a/outpost/pkg/ldap/search.go b/outpost/pkg/ldap/search.go index ecc5f35e6..ab00a71bc 100644 --- a/outpost/pkg/ldap/search.go +++ b/outpost/pkg/ldap/search.go @@ -8,8 +8,8 @@ import ( "github.com/nmcclain/ldap" ) -func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { - ls.log.WithField("boundDN", boundDN).WithField("baseDN", searchReq.BaseDN).Info("search") +func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { + ls.log.WithField("bindDN", bindDN).WithField("baseDN", searchReq.BaseDN).Info("search") if searchReq.BaseDN == "" { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil } @@ -21,7 +21,7 @@ func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn for _, provider := range ls.providers { providerBase, _ := goldap.ParseDN(provider.BaseDN) if providerBase.AncestorOf(bd) { - return provider.Search(boundDN, searchReq, conn) + return provider.Search(bindDN, searchReq, conn) } } return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request") diff --git a/outpost/pkg/version.go b/outpost/pkg/version.go index b86f726ab..de6ee06ae 100644 --- a/outpost/pkg/version.go +++ b/outpost/pkg/version.go @@ -1,3 +1,16 @@ package pkg +import ( + "fmt" + "os" +) + const VERSION = "2021.5.1-rc8" + +func BUILD() string { + return os.Getenv("GIT_BUILD_HASH") +} + +func UserAgent() string { + return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD()) +} diff --git a/outpost/proxy.Dockerfile b/outpost/proxy.Dockerfile index 8943b5a51..15861aed0 100644 --- a/outpost/proxy.Dockerfile +++ b/outpost/proxy.Dockerfile @@ -1,4 +1,6 @@ FROM golang:1.16.4 AS builder +ARG GIT_BUILD_HASH +ENV GIT_BUILD_HASH=$GIT_BUILD_HASH WORKDIR /work diff --git a/swagger.yaml b/swagger.yaml index b324adae6..26411c5ae 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -15694,6 +15694,7 @@ definitions: NotificationRule: required: - name + - transports type: object properties: pk: @@ -15706,38 +15707,17 @@ definitions: type: string minLength: 1 transports: + description: Select which transports should be used to notify the user. If + none are selected, the notification will only be shown in the authentik + UI. type: array items: - required: - - name - - mode - type: object - properties: - uuid: - title: Uuid - type: string - format: uuid - readOnly: true - name: - title: Name - type: string - minLength: 1 - mode: - title: Mode - type: string - enum: - - webhook - - webhook_slack - - email - webhook_url: - title: Webhook url - type: string - send_once: - title: Send once - description: Only send notification once, for example when sending a - webhook into a chat channel. - type: boolean - readOnly: true + description: Select which transports should be used to notify the user. + If none are selected, the notification will only be shown in the authentik + UI. + type: string + format: uuid + uniqueItems: true severity: title: Severity description: Controls which severity level the created notifications will @@ -15748,57 +15728,14 @@ definitions: - warning - alert group: - required: - - name - type: object - properties: - group_uuid: - title: Group uuid - type: string - format: uuid - readOnly: true - name: - title: Name - type: string - maxLength: 80 - minLength: 1 - is_superuser: - title: Is superuser - description: Users added to this group will be superusers. - type: boolean - attributes: - title: Attributes - type: object - parent: - required: - - name - - parent - type: object - properties: - group_uuid: - title: Group uuid - type: string - format: uuid - readOnly: true - name: - title: Name - type: string - maxLength: 80 - minLength: 1 - is_superuser: - title: Is superuser - description: Users added to this group will be superusers. - type: boolean - attributes: - title: Attributes - type: object - parent: - title: Parent - type: string - format: uuid - x-nullable: true - readOnly: true - readOnly: true + title: Group + description: Define which group of users this notification should be sent + and shown to. If left empty, Notification won't ben sent. + type: string + format: uuid + x-nullable: true + group_obj: + $ref: '#/definitions/Group' NotificationTransport: required: - name diff --git a/web/src/pages/events/RuleForm.ts b/web/src/pages/events/RuleForm.ts index 7e922ac0c..d931d4bdc 100644 --- a/web/src/pages/events/RuleForm.ts +++ b/web/src/pages/events/RuleForm.ts @@ -67,7 +67,7 @@ export class RuleForm extends ModelForm { ${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then(groups => { return groups.results.map(group => { - return html``; + return html``; }); }), html``)} @@ -80,7 +80,7 @@ export class RuleForm extends ModelForm { ${until(new EventsApi(DEFAULT_CONFIG).eventsTransportsList({}).then(transports => { return transports.results.map(transport => { const selected = Array.from(this.instance?.transports || []).some(su => { - return su.uuid == transport.pk; + return su == transport.pk; }); return html``; }); diff --git a/web/src/pages/events/RuleListPage.ts b/web/src/pages/events/RuleListPage.ts index 7ffcd4d30..3852fd293 100644 --- a/web/src/pages/events/RuleListPage.ts +++ b/web/src/pages/events/RuleListPage.ts @@ -55,7 +55,7 @@ export class RuleListPage extends TablePage { return [ html`${item.name}`, html`${item.severity}`, - html`${item.group?.name || t`None (rule disabled)`}`, + html`${item.groupObj?.name || t`None (rule disabled)`}`, html` diff --git a/web/src/pages/outposts/OutpostHealth.ts b/web/src/pages/outposts/OutpostHealth.ts index 9ec2654c2..eee784756 100644 --- a/web/src/pages/outposts/OutpostHealth.ts +++ b/web/src/pages/outposts/OutpostHealth.ts @@ -42,13 +42,12 @@ export class OutpostHealthElement extends LitElement { return html``; } if (this.outpostHealth.length === 0) { - return html`
  • + return html`
    • -
    -
  • `; + `; } return html`
      ${this.outpostHealth.map((h) => { return html`
    • diff --git a/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts b/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts index 5d38d8276..8314a5f08 100644 --- a/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +++ b/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts @@ -70,7 +70,7 @@ export class AuthenticatorValidateStageForm extends ModelForm + name="notConfiguredAction">