diff --git a/.github/workflows/ready_for_review.yml b/.github/workflows/ready_for_review.yml index 2ead263dc9..db756633f4 100644 --- a/.github/workflows/ready_for_review.yml +++ b/.github/workflows/ready_for_review.yml @@ -13,7 +13,7 @@ jobs: Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review: - - [ ] I am happy with the code + - [ ] I have reviewed my changes and would approve it - [ ] Documentations and examples are up-to-date - [ ] Logical behavior changes are tested automatically - [ ] No debug or dead code @@ -28,4 +28,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: content - }) \ No newline at end of file + }) diff --git a/docs/buf.gen.yaml b/docs/buf.gen.yaml index b507a2fb9c..d23040f416 100644 --- a/docs/buf.gen.yaml +++ b/docs/buf.gen.yaml @@ -9,7 +9,7 @@ plugins: - allow_delete_body - remove_internal_comments=true - preserve_rpc_order=true - - local: ./protoc-gen-connect-openapi + - local: ./protoc-gen-connect-openapi/protoc-gen-connect-openapi out: .artifacts/openapi3 strategy: all opt: diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index a22b1b80fc..3ca8247a57 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -218,7 +218,6 @@ module.exports = { showLastUpdateTime: true, editUrl: "https://github.com/zitadel/zitadel/edit/main/docs/", remarkPlugins: [require("mdx-mermaid")], - docItemComponent: "@theme/ApiItem", }, theme: { @@ -243,6 +242,17 @@ module.exports = { }, }, ], + [ + "@signalwire/docusaurus-plugin-llms-txt", + { + depth: 3, + logLevel: 1, + content: { + excludeRoutes: ["/search"], + enableMarkdownFiles: true, + }, + }, + ], [ "docusaurus-plugin-openapi-docs", { diff --git a/docs/package.json b/docs/package.json index 7aa75b9ae0..b9df844b51 100644 --- a/docs/package.json +++ b/docs/package.json @@ -30,6 +30,7 @@ "@docusaurus/theme-search-algolia": "^3.8.1", "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", + "@signalwire/docusaurus-plugin-llms-txt": "^1.2.0", "@inkeep/cxkit-docusaurus": "^0.5.89", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", diff --git a/docs/plugin-download.sh b/docs/plugin-download.sh index c6de8d702f..499326a1e7 100644 --- a/docs/plugin-download.sh +++ b/docs/plugin-download.sh @@ -1,5 +1,6 @@ echo $(uname -m) - +mkdir protoc-gen-connect-openapi +cd ./protoc-gen-connect-openapi/ if [ "$(uname)" = "Darwin" ]; then curl -L -o protoc-gen-connect-openapi.tar.gz https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_darwin_all.tar.gz else diff --git a/internal/api/grpc/admin/instance.go b/internal/api/grpc/admin/instance.go index 74f1576ae1..a7b0316ec4 100644 --- a/internal/api/grpc/admin/instance.go +++ b/internal/api/grpc/admin/instance.go @@ -30,6 +30,7 @@ func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInst } return &admin_pb.ListInstanceDomainsResponse{ Result: instance_grpc.DomainsToPb(domains.Domains), + SortingColumn: req.SortingColumn, Details: object.ToListDetails( domains.Count, domains.Sequence, @@ -49,6 +50,7 @@ func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.L } return &admin_pb.ListInstanceTrustedDomainsResponse{ Result: instance_grpc.TrustedDomainsToPb(domains.Domains), + SortingColumn: req.SortingColumn, Details: object.ToListDetails( domains.Count, domains.Sequence, diff --git a/internal/api/grpc/admin/instance_converter.go b/internal/api/grpc/admin/instance_converter.go index 397845c9e3..603d544cb8 100644 --- a/internal/api/grpc/admin/instance_converter.go +++ b/internal/api/grpc/admin/instance_converter.go @@ -51,8 +51,23 @@ func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedD Offset: offset, Limit: limit, Asc: asc, - SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn), + SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.SortingColumn), }, Queries: queries, }, nil } + +func fieldNameToInstanceTrustedDomainColumn(fieldName instance.DomainFieldName) query.Column { + switch fieldName { + case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceTrustedDomainDomainCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceTrustedDomainCreationDateCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED, + instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY, + instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED: + return query.InstanceTrustedDomainCreationDateCol + default: + return query.Column{} + } +} diff --git a/internal/api/ui/login/password_reset_handler.go b/internal/api/ui/login/password_reset_handler.go index 5bdee7904c..11a50d942d 100644 --- a/internal/api/ui/login/password_reset_handler.go +++ b/internal/api/ui/login/password_reset_handler.go @@ -17,7 +17,11 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } - user, err := l.query.GetUserByLoginName(setContext(r.Context(), authReq.UserOrgID), true, authReq.LoginName) + // We check if the user really exists or if it is just a placeholder or an unknown user. + // In theory, we could also check for the unknownUserID constant. However, that could disclose + // information about the existence of a user to an attacker if they check response times, + // since those requests would take shorter than the ones for real users. + user, err := l.query.GetUserByID(setContext(r.Context(), authReq.UserOrgID), true, authReq.UserID) if err != nil { if authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) { err = nil diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 7c335a752f..bf0609673b 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -1055,6 +1055,10 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth if err != nil { return nil, err } + // in case the user was set automatically, we might not have the org set + if request.UserOrgID == "" { + request.UserOrgID = user.ResourceOwner + } userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user) if err != nil { return nil, err diff --git a/login/apps/login/src/app/(login)/saml-post/route.ts b/login/apps/login/src/app/(login)/saml-post/route.ts index f2834f3884..a2061a18e2 100644 --- a/login/apps/login/src/app/(login)/saml-post/route.ts +++ b/login/apps/login/src/app/(login)/saml-post/route.ts @@ -1,22 +1,41 @@ +import { getSAMLFormCookie } from "@/lib/saml"; import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const url = searchParams.get("url"); - const relayState = searchParams.get("RelayState"); - const samlResponse = searchParams.get("SAMLResponse"); + const id = searchParams.get("id"); - if (!url || !relayState || !samlResponse) { - return new NextResponse("Missing required parameters", { status: 400 }); + if (!url) { + return new NextResponse("Missing url parameter", { status: 400 }); } + if (!id) { + return new NextResponse("Missing id parameter", { status: 400 }); + } + + const formData = await getSAMLFormCookie(id); + + const formDataParsed = formData ? JSON.parse(formData) : null; + + if (!formDataParsed) { + return new NextResponse("SAML form data not found", { status: 404 }); + } + + // Generate hidden input fields for all key-value pairs in formDataParsed + const hiddenInputs = Object.entries(formDataParsed) + .map( + ([key, value]) => + ``, + ) + .join("\n "); + // Respond with an HTML form that auto-submits via POST const html = `
+ + + `; - redirectUrl.searchParams.set("url", url); - redirectUrl.searchParams.set("RelayState", binding.value.relayState); - redirectUrl.searchParams.set( - "SAMLResponse", - binding.value.samlResponse, - ); - - return NextResponse.redirect(redirectUrl.toString()); + return new NextResponse(html, { + headers: { "Content-Type": "text/html" }, + }); } else { console.log( "could not create response, redirect user to choose other account", diff --git a/login/apps/login/src/lib/saml.ts b/login/apps/login/src/lib/saml.ts index e85084f022..e1b5f4c080 100644 --- a/login/apps/login/src/lib/saml.ts +++ b/login/apps/login/src/lib/saml.ts @@ -4,7 +4,9 @@ import { createResponse, getLoginSettings } from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { v4 as uuidv4 } from "uuid"; import { constructUrl } from "./service-url"; import { isSessionValid } from "./session"; @@ -17,6 +19,37 @@ type LoginWithSAMLAndSession = { request: NextRequest; }; +export async function getSAMLFormUID() { + return uuidv4(); +} + +export async function setSAMLFormCookie(value: string): Promise