diff --git a/.github/ISSUE_TEMPLATE/docs_issue.md b/.github/ISSUE_TEMPLATE/docs_issue.md new file mode 100644 index 0000000000..47eaa2a668 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_issue.md @@ -0,0 +1,22 @@ +--- +name: Documentation issue +about: Suggest an improvement or report a problem +title: "" +labels: documentation +assignees: "" +--- + +**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.** +A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...] + +**Provide the URL or link to the exact page in the documentation to which you are referring.** +If there are multiple pages, list them all, and be sure to state the header or section where the content is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the documentation issue here. + +**Consider opening a PR!** +If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation). \ No newline at end of file diff --git a/.github/workflows/ci-outpost.yml b/.github/workflows/ci-outpost.yml index 209e7d4da1..4369d97404 100644 --- a/.github/workflows/ci-outpost.yml +++ b/.github/workflows/ci-outpost.yml @@ -29,7 +29,7 @@ jobs: - name: Generate API run: make gen-client-go - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: version: latest args: --timeout 5000s --verbose diff --git a/Dockerfile b/Dockerfile index 900efb0bc3..ef5cc06571 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" # Stage 5: Download uv -FROM ghcr.io/astral-sh/uv:0.6.9 AS uv +FROM ghcr.io/astral-sh/uv:0.6.10 AS uv # Stage 6: Base python image FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index f62265d931..f6c9e15240 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -1,5 +1,20 @@ -# update website/docs/install-config/configuration/configuration.mdx -# This is the default configuration file +# authentik configuration +# +# https://docs.goauthentik.io/docs/install-config/configuration/ +# +# To override the settings in this file, run the following command from the repository root: +# +# ```shell +# make gen-dev-config +# ``` +# +# You may edit the generated file to override the configuration below. +# +# When making modifying the default configuration file, +# ensure that the corresponding documentation is updated to match. +# +# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information. + postgresql: host: localhost name: authentik diff --git a/authentik/stages/email/tests/test_sending.py b/authentik/stages/email/tests/test_sending.py index 631983ab67..fc36cc7db6 100644 --- a/authentik/stages/email/tests/test_sending.py +++ b/authentik/stages/email/tests/test_sending.py @@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend from django.urls import reverse from authentik.core.models import User -from authentik.core.tests.utils import create_test_admin_user, create_test_flow +from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user from authentik.events.models import Event, EventAction from authentik.flows.markers import StageMarker from authentik.flows.models import FlowDesignation, FlowStageBinding @@ -67,6 +67,36 @@ class TestEmailStageSending(FlowTestCase): self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"]) self.assertEqual(event.context["from_email"], "system@authentik.local") + def test_newlines_long_name(self): + """Test with pending user""" + plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) + long_user = create_test_user() + long_user.name = "Test User\r\n Many Words\r\n" + long_user.save() + plan.context[PLAN_CONTEXT_PENDING_USER] = long_user + session = self.client.session + session[SESSION_KEY_PLAN] = plan + session.save() + Event.objects.filter(action=EventAction.EMAIL_SENT).delete() + + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + with patch( + "authentik.stages.email.models.EmailStage.backend_class", + PropertyMock(return_value=EmailBackend), + ): + response = self.client.post(url) + self.assertEqual(response.status_code, 200) + self.assertStageResponse( + response, + self.flow, + response_errors={ + "non_field_errors": [{"string": "email-sent", "code": "email-sent"}] + }, + ) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, "authentik") + self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"]) + def test_pending_fake_user(self): """Test with pending (fake) user""" self.flow.designation = FlowDesignation.RECOVERY diff --git a/authentik/stages/email/utils.py b/authentik/stages/email/utils.py index 22beb1294c..f19d9c1ea6 100644 --- a/authentik/stages/email/utils.py +++ b/authentik/stages/email/utils.py @@ -32,7 +32,14 @@ class TemplateEmailMessage(EmailMultiAlternatives): sanitized_to = [] # Ensure that all recipients are valid for recipient_name, recipient_email in to: - sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8")) + # Remove any newline characters from name and email before sanitizing + clean_name = ( + recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else "" + ) + clean_email = ( + recipient_email.replace("\n", "").replace("\r", "") if recipient_email else "" + ) + sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8")) super().__init__(to=sanitized_to, **kwargs) if not template_name: return diff --git a/internal/config/config.go b/internal/config/config.go index a7d3eeb5d0..ca1ec09424 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -162,13 +162,14 @@ func (c *Config) parseScheme(rawVal string) string { if err != nil { return rawVal } - if u.Scheme == "env" { + switch u.Scheme { + case "env": e, ok := os.LookupEnv(u.Host) if ok { return e } return u.RawQuery - } else if u.Scheme == "file" { + case "file": d, err := os.ReadFile(u.Path) if err != nil { return u.RawQuery diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0687e84a2b..066592f48e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,7 +10,7 @@ import ( ) func TestConfigEnv(t *testing.T) { - os.Setenv("AUTHENTIK_SECRET_KEY", "bar") + assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar")) cfg = nil if err := Get().fromEnv(); err != nil { panic(err) @@ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) { } func TestConfigEnv_Scheme(t *testing.T) { - os.Setenv("foo", "bar") - os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo") + assert.NoError(t, os.Setenv("foo", "bar")) + assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")) cfg = nil if err := Get().fromEnv(); err != nil { panic(err) @@ -33,13 +33,15 @@ func TestConfigEnv_File(t *testing.T) { if err != nil { log.Fatal(err) } - defer os.Remove(file.Name()) + defer func() { + assert.NoError(t, os.Remove(file.Name())) + }() _, err = file.Write([]byte("bar")) if err != nil { panic(err) } - os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())) + assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))) cfg = nil if err := Get().fromEnv(); err != nil { panic(err) diff --git a/internal/debug/debug.go b/internal/debug/debug.go index ddd90ffa9e..3897df12e5 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -35,7 +35,7 @@ func EnableDebugServer() { if err != nil { return nil } - _, err = w.Write([]byte(fmt.Sprintf("%[1]s", tpl))) + _, err = fmt.Fprintf(w, "%[1]s", tpl) if err != nil { l.WithError(err).Warning("failed to write index") return nil diff --git a/internal/gounicorn/gounicorn.go b/internal/gounicorn/gounicorn.go index a3692f3106..6478b91981 100644 --- a/internal/gounicorn/gounicorn.go +++ b/internal/gounicorn/gounicorn.go @@ -44,10 +44,11 @@ func New(healthcheck func() bool) *GoUnicorn { signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2) go func() { for sig := range c { - if sig == syscall.SIGHUP { + switch sig { + case syscall.SIGHUP: g.log.Info("SIGHUP received, forwarding to gunicorn") g.Reload() - } else if sig == syscall.SIGUSR2 { + case syscall.SIGUSR2: g.log.Info("SIGUSR2 received, restarting gunicorn") g.Restart() } diff --git a/internal/outpost/ak/api_utils.go b/internal/outpost/ak/api_utils.go index d732ba7556..9b477903ae 100644 --- a/internal/outpost/ak/api_utils.go +++ b/internal/outpost/ak/api_utils.go @@ -35,13 +35,19 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]]( req PaginatorRequest[Treq, Tres], opts PaginatorOptions, ) ([]Tobj, error) { + if opts.Logger == nil { + opts.Logger = log.NewEntry(log.StandardLogger()) + } var bfreq, cfreq interface{} fetchOffset := func(page int32) (Tres, error) { bfreq = req.Page(page) cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize)) - res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() + res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() if err != nil { opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page") + if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 { + return res, err + } } return res, err } @@ -51,6 +57,9 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]]( for { apiObjects, err := fetchOffset(page) if err != nil { + if page == 1 { + return objects, err + } errs = append(errs, err) continue } diff --git a/internal/outpost/ak/api_utils_test.go b/internal/outpost/ak/api_utils_test.go index ac751f943a..7ee84e13f4 100644 --- a/internal/outpost/ak/api_utils_test.go +++ b/internal/outpost/ak/api_utils_test.go @@ -1,5 +1,64 @@ package ak +import ( + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "goauthentik.io/api/v3" +) + +type fakeAPIType struct{} + +type fakeAPIResponse struct { + results []fakeAPIType + pagination api.Pagination +} + +func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results } +func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination } + +type fakeAPIRequest struct { + res *fakeAPIResponse + http *http.Response + err error +} + +func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi } +func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi } +func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) { + return fapi.res, fapi.http, fapi.err +} + +func Test_Simple(t *testing.T) { + req := &fakeAPIRequest{ + res: &fakeAPIResponse{ + results: []fakeAPIType{ + {}, + }, + pagination: api.Pagination{ + TotalPages: 1, + }, + }, + } + res, err := Paginator(req, PaginatorOptions{}) + assert.NoError(t, err) + assert.Len(t, res, 1) +} + +func Test_BadRequest(t *testing.T) { + req := &fakeAPIRequest{ + http: &http.Response{ + StatusCode: 400, + }, + err: errors.New("foo"), + } + res, err := Paginator(req, PaginatorOptions{}) + assert.Error(t, err) + assert.Equal(t, []fakeAPIType{}, res) +} + // func Test_PaginatorCompile(t *testing.T) { // req := api.ApiCoreUsersListRequest{} // Paginator(req, PaginatorOptions{ diff --git a/internal/outpost/ak/api_ws.go b/internal/outpost/ak/api_ws.go index d92941f760..62f4e9ea48 100644 --- a/internal/outpost/ak/api_ws.go +++ b/internal/outpost/ak/api_ws.go @@ -148,7 +148,8 @@ func (ac *APIController) startWSHandler() { "outpost_type": ac.Server.Type(), "uuid": ac.instanceUUID.String(), }).Set(1) - if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { + switch wsMsg.Instruction { + case WebsocketInstructionTriggerUpdate: time.Sleep(ac.reloadOffset) logger.Debug("Got update trigger...") err := ac.OnRefresh() @@ -163,7 +164,7 @@ func (ac *APIController) startWSHandler() { "build": constants.BUILD(""), }).SetToCurrentTime() } - } else if wsMsg.Instruction == WebsocketInstructionProviderSpecific { + case WebsocketInstructionProviderSpecific: for _, h := range ac.wsHandlers { h(context.Background(), wsMsg.Args) } diff --git a/internal/outpost/ldap/ldap.go b/internal/outpost/ldap/ldap.go index 383682c78a..f9d4ad61bb 100644 --- a/internal/outpost/ldap/ldap.go +++ b/internal/outpost/ldap/ldap.go @@ -66,7 +66,12 @@ func (ls *LDAPServer) StartLDAPServer() error { return err } proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} - defer proxyListener.Close() + defer func() { + err := proxyListener.Close() + if err != nil { + ls.log.WithError(err).Warning("failed to close proxy listener") + } + }() ls.log.WithField("listen", listen).Info("Starting LDAP server") err = ls.s.Serve(proxyListener) diff --git a/internal/outpost/ldap/ldap_tls.go b/internal/outpost/ldap/ldap_tls.go index 48d4bcf8d9..40dfc25d9b 100644 --- a/internal/outpost/ldap/ldap_tls.go +++ b/internal/outpost/ldap/ldap_tls.go @@ -49,7 +49,12 @@ func (ls *LDAPServer) StartLDAPTLSServer() error { } proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} - defer proxyListener.Close() + defer func() { + err := proxyListener.Close() + if err != nil { + ls.log.WithError(err).Warning("failed to close proxy listener") + } + }() tln := tls.NewListener(proxyListener, tlsConfig) diff --git a/internal/outpost/ldap/search/memory/memory.go b/internal/outpost/ldap/search/memory/memory.go index 0236cd9f28..c4f23a60e8 100644 --- a/internal/outpost/ldap/search/memory/memory.go +++ b/internal/outpost/ldap/search/memory/memory.go @@ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, entries := make([]*ldap.Entry, 0) - scope := req.SearchRequest.Scope + scope := req.Scope needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass) if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { diff --git a/internal/outpost/proxyv2/application/endpoint.go b/internal/outpost/proxyv2/application/endpoint.go index c9cc50d40c..6137de2254 100644 --- a/internal/outpost/proxyv2/application/endpoint.go +++ b/internal/outpost/proxyv2/application/endpoint.go @@ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo if !embedded && hostBrowser == "" { return ep } - var newHost *url.URL = aku + var newHost = aku var newBrowserHost *url.URL if embedded { if authentikHost == "" { diff --git a/internal/outpost/proxyv2/proxyv2.go b/internal/outpost/proxyv2/proxyv2.go index eed0ef18ac..1a83081d35 100644 --- a/internal/outpost/proxyv2/proxyv2.go +++ b/internal/outpost/proxyv2/proxyv2.go @@ -130,7 +130,12 @@ func (ps *ProxyServer) ServeHTTP() { return } proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()} - defer proxyListener.Close() + defer func() { + err := proxyListener.Close() + if err != nil { + ps.log.WithError(err).Warning("failed to close proxy listener") + } + }() ps.log.WithField("listen", listenAddress).Info("Starting HTTP server") ps.serve(proxyListener) @@ -149,7 +154,12 @@ func (ps *ProxyServer) ServeHTTPS() { return } proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()} - defer proxyListener.Close() + defer func() { + err := proxyListener.Close() + if err != nil { + ps.log.WithError(err).Warning("failed to close proxy listener") + } + }() tlsListener := tls.NewListener(proxyListener, tlsConfig) ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server") diff --git a/internal/outpost/proxyv2/redisstore/redisstore.go b/internal/outpost/proxyv2/redisstore/redisstore.go index 21c812412a..643e2d11da 100644 --- a/internal/outpost/proxyv2/redisstore/redisstore.go +++ b/internal/outpost/proxyv2/redisstore/redisstore.go @@ -72,11 +72,13 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error session.ID = c.Value err = s.load(r.Context(), session) - if err == nil { - session.IsNew = false - } else if err == redis.Nil { - err = nil // no data stored + if err != nil { + if errors.Is(err, redis.Nil) { + return session, nil + } + return session, err } + session.IsNew = false return session, err } diff --git a/internal/web/web.go b/internal/web/web.go index ecf2da149e..d5b2ae88ef 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -158,7 +158,12 @@ func (ws *WebServer) listenPlain() { return } proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} - defer proxyListener.Close() + defer func() { + err := proxyListener.Close() + if err != nil { + ws.log.WithError(err).Warning("failed to close proxy listener") + } + }() ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server") ws.serve(proxyListener) diff --git a/internal/web/web_tls.go b/internal/web/web_tls.go index 7ccaf4dff9..9e006fbdd8 100644 --- a/internal/web/web_tls.go +++ b/internal/web/web_tls.go @@ -46,7 +46,12 @@ func (ws *WebServer) listenTLS() { return } proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()} - defer proxyListener.Close() + defer func() { + err := proxyListener.Close() + if err != nil { + ws.log.WithError(err).Warning("failed to close proxy listener") + } + }() tlsListener := tls.NewListener(proxyListener, tlsConfig) ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server") diff --git a/lifecycle/aws/package-lock.json b/lifecycle/aws/package-lock.json index 29913c2f02..2d73b7fdfe 100644 --- a/lifecycle/aws/package-lock.json +++ b/lifecycle/aws/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "devDependencies": { - "aws-cdk": "^2.1005.0", + "aws-cdk": "^2.1006.0", "cross-env": "^7.0.3" }, "engines": { @@ -17,9 +17,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.1005.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz", - "integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==", + "version": "2.1006.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz", + "integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/lifecycle/aws/package.json b/lifecycle/aws/package.json index d99249d1d1..912bed315c 100644 --- a/lifecycle/aws/package.json +++ b/lifecycle/aws/package.json @@ -10,7 +10,7 @@ "node": ">=20" }, "devDependencies": { - "aws-cdk": "^2.1005.0", + "aws-cdk": "^2.1006.0", "cross-env": "^7.0.3" } } diff --git a/scripts/generate_config.py b/scripts/generate_config.py index cc7cc30fd1..6cb49e0aea 100755 --- a/scripts/generate_config.py +++ b/scripts/generate_config.py @@ -5,48 +5,88 @@ from yaml import safe_dump from authentik.lib.generators import generate_id -with open("local.env.yml", "w", encoding="utf-8") as _config: - safe_dump( - { - "debug": True, - "log_level": "debug", - "secret_key": generate_id(), - "postgresql": { - "user": "postgres", - }, - "outposts": { - "container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s", - "disable_embedded_outpost": False, - }, - "blueprints_dir": "./blueprints", - "cert_discovery_dir": "./certs", - "events": { - "processors": { - "geoip": "tests/GeoLite2-City-Test.mmdb", - "asn": "tests/GeoLite2-ASN-Test.mmdb", - } - }, - "storage": { - "media": { - "backend": "file", - "s3": { - "endpoint": "http://localhost:8020", - "access_key": "accessKey1", - "secret_key": "secretKey1", - "bucket_name": "authentik-media", - "custom_domain": "localhost:8020/authentik-media", - "secure_urls": False, - }, + +def generate_local_config(): + """Generate a local development configuration""" + # TODO: This should be generated and validated against a schema, such as Pydantic. + + return { + "debug": True, + "log_level": "debug", + "secret_key": generate_id(), + "postgresql": { + "user": "postgres", + }, + "outposts": { + "container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s", + "disable_embedded_outpost": False, + }, + "blueprints_dir": "./blueprints", + "cert_discovery_dir": "./certs", + "events": { + "processors": { + "geoip": "tests/GeoLite2-City-Test.mmdb", + "asn": "tests/GeoLite2-ASN-Test.mmdb", + } + }, + "storage": { + "media": { + "backend": "file", + "s3": { + "endpoint": "http://localhost:8020", + "access_key": "accessKey1", + "secret_key": "secretKey1", + "bucket_name": "authentik-media", + "custom_domain": "localhost:8020/authentik-media", + "secure_urls": False, }, }, - "tenants": { - "enabled": False, - "api_key": generate_id(), - }, - "worker": { - "embedded": True, - }, }, - _config, - default_flow_style=False, + "tenants": { + "enabled": False, + "api_key": generate_id(), + }, + "worker": { + "embedded": True, + }, + } + + +if __name__ == "__main__": + config_file_name = "local.env.yml" + + with open(config_file_name, "w", encoding="utf-8") as _config: + _config.write( + """ +# Local authentik configuration overrides +# +# https://docs.goauthentik.io/docs/install-config/configuration/ +# +# To regenerate this file, run the following command from the repository root: +# +# ```shell +# make gen-dev-config +# ``` + +""" + ) + + safe_dump( + generate_local_config(), + _config, + default_flow_style=False, + ) + + print( + f""" +--- + +Generated configuration file: {config_file_name} + +For more information on how to use this configuration, see: + +https://docs.goauthentik.io/docs/install-config/configuration/ + +--- +""" ) diff --git a/web/package-lock.json b/web/package-lock.json index 335438a5b4..bbbfaff646 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -24760,9 +24760,9 @@ } }, "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "version": "5.4.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", + "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/web/src/elements/sync/SyncStatusCard.ts b/web/src/elements/sync/SyncStatusCard.ts index ce329dd61a..fb75d9995a 100644 --- a/web/src/elements/sync/SyncStatusCard.ts +++ b/web/src/elements/sync/SyncStatusCard.ts @@ -11,6 +11,7 @@ import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, css, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFTable from "@patternfly/patternfly/components/Table/table.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; @@ -34,6 +35,9 @@ export class SyncStatusTable extends Table { } async apiEndpoint(): Promise> { + if (this.tasks.length === 1) { + this.expandedElements = this.tasks; + } return { pagination: { next: 0, @@ -104,7 +108,7 @@ export class SyncStatusCard extends AKElement { triggerSync!: () => Promise; static get styles(): CSSResult[] { - return [PFBase, PFCard, PFTable]; + return [PFBase, PFButton, PFCard, PFTable]; } firstUpdated() { @@ -133,7 +137,20 @@ export class SyncStatusCard extends AKElement { render(): TemplateResult { return html` - ${msg("Sync status")} + + + { + this.fetch(); + }} + > + + + + ${msg("Sync status")} + ${this.renderSyncStatus()}