snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

67
cmd/cleanup/handler.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cleanup
import (
"github.com/pkg/errors"
"github.com/ory/x/contextx"
"github.com/ory/x/configx"
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/flagx"
)
type CleanupHandler struct{}
func NewCleanupHandler() *CleanupHandler {
return &CleanupHandler{}
}
func (h *CleanupHandler) CleanupSQL(cmd *cobra.Command, args []string) error {
opts := []configx.OptionModifier{
configx.WithFlags(cmd.Flags()),
configx.SkipValidation(),
}
if !flagx.MustGetBool(cmd, "read-from-env") {
if len(args) != 1 {
return errors.New(`expected to get the DSN as an argument, or the "read-from-env" flag`)
}
opts = append(opts, configx.WithValue(config.ViperKeyDSN, args[0]))
}
d, err := driver.NewWithoutInit(
cmd.Context(),
cmd.ErrOrStderr(),
driver.WithConfigOptions(opts...),
)
if len(d.Config().DSN(cmd.Context())) == 0 {
return errors.New(`required config value "dsn" was not set`)
} else if err != nil {
return errors.Wrap(err, "An error occurred initializing cleanup")
}
err = d.Init(cmd.Context(), &contextx.Default{})
if err != nil {
return errors.Wrap(err, "An error occurred initializing cleanup")
}
keepLast := flagx.MustGetDuration(cmd, "keep-last")
err = d.Persister().CleanupDatabase(
cmd.Context(),
d.Config().DatabaseCleanupSleepTables(cmd.Context()),
keepLast,
d.Config().DatabaseCleanupBatchSize(cmd.Context()))
if err != nil {
return errors.Wrap(err, "An error occurred while cleaning up expired data")
}
return nil
}

25
cmd/cleanup/root.go Normal file
View File

@@ -0,0 +1,25 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cleanup
import (
"github.com/spf13/cobra"
"github.com/ory/x/configx"
)
func NewCleanupCmd() *cobra.Command {
c := &cobra.Command{
Use: "cleanup",
Short: "Various cleanup helpers",
}
configx.RegisterFlags(c.PersistentFlags())
return c
}
func RegisterCommandRecursive(parent *cobra.Command) {
c := NewCleanupCmd()
parent.AddCommand(c)
c.AddCommand(NewCleanupSQLCmd())
}

49
cmd/cleanup/sql.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cleanup
import (
"fmt"
"time"
"github.com/ory/x/cmdx"
"github.com/spf13/cobra"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/configx"
)
// cleanupSqlCmd represents the sql command
func NewCleanupSQLCmd() *cobra.Command {
c := &cobra.Command{
Use: "sql <database-url>",
Short: "Cleanup sql database from expired flows and sessions",
Long: `Run this command as frequently as you need.
It is recommended to run this command close to the SQL instance (e.g. same subnet) instead of over the public internet.
This decreases risk of failure and decreases time required.
You can read in the database URL using the -e flag, for example:
export DSN=...
kratos cleanup sql -e
### WARNING ###
Before running this command on an existing database, create a back up!
`,
RunE: func(cmd *cobra.Command, args []string) error {
err := NewCleanupHandler().CleanupSQL(cmd, args)
if err != nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), err)
return cmdx.FailSilently(cmd)
}
return nil
},
}
configx.RegisterFlags(c.PersistentFlags())
c.Flags().BoolP("read-from-env", "e", true, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")
c.Flags().Duration(config.ViperKeyDatabaseCleanupSleepTables, time.Minute, "How long to wait between each table cleanup")
c.Flags().IntP(config.ViperKeyDatabaseCleanupBatchSize, "b", 100, "Set the number of records to be cleaned per run")
c.Flags().Duration("keep-last", 0, "Don't remove records younger than")
return c
}

27
cmd/cleanup/sql_test.go Normal file
View File

@@ -0,0 +1,27 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cleanup
import (
"bytes"
"io"
"strings"
"testing"
)
func Test_ExecuteCleanupFailedDSN(t *testing.T) {
cmd := NewCleanupSQLCmd()
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"--read-from-env=false"})
_ = cmd.Execute()
out, err := io.ReadAll(b)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(out), "expected to get the DSN as an argument") {
t.Fatalf("expected \"%s\" got \"%s\"", "expected to get the DSN as an argument", string(out))
}
_ = cmd.Execute()
}

82
cmd/cliclient/client.go Normal file
View File

@@ -0,0 +1,82 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cliclient
import (
"fmt"
"net/http"
"net/url"
"os"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/x/httpx"
)
const (
envKeyEndpoint = "KRATOS_ADMIN_URL"
FlagEndpoint = "endpoint"
)
type ContextKey int
const (
ClientContextKey ContextKey = iota + 1
)
type ClientContext struct {
Endpoint string
HTTPClient *http.Client
}
func NewClient(cmd *cobra.Command) (*kratos.APIClient, error) {
if f, ok := cmd.Context().Value(ClientContextKey).(func(cmd *cobra.Command) (*ClientContext, error)); ok {
cc, err := f(cmd)
if err != nil {
return nil, err
}
conf := kratos.NewConfiguration()
conf.HTTPClient = cc.HTTPClient
conf.Servers = kratos.ServerConfigurations{{URL: cc.Endpoint}}
return kratos.NewAPIClient(conf), nil
} else if f != nil {
return nil, errors.Errorf("ClientContextKey was expected to be *client.OryKratos but it contained an invalid type %T ", f)
}
endpoint, err := cmd.Flags().GetString(FlagEndpoint)
if err != nil {
return nil, errors.WithStack(err)
}
if endpoint == "" {
endpoint = os.Getenv(envKeyEndpoint)
}
if endpoint == "" {
return nil, errors.Errorf("you have to set the remote endpoint, try --help for details")
}
u, err := url.Parse(endpoint)
if err != nil {
return nil, errors.Wrapf(err, `could not parse the endpoint URL "%s"`, endpoint)
}
conf := kratos.NewConfiguration()
conf.HTTPClient = httpx.NewResilientClient(
httpx.ResilientClientWithConnectionTimeout(10 * time.Second),
).StandardClient()
conf.Servers = kratos.ServerConfigurations{{URL: u.String()}}
return kratos.NewAPIClient(conf), nil
}
func RegisterClientFlags(flags *pflag.FlagSet) {
flags.StringP(FlagEndpoint, FlagEndpoint[:1], "", fmt.Sprintf("The URL of Ory Kratos' Admin API. Alternatively set using the %s environmental variable.", envKeyEndpoint))
}

431
cmd/clidoc/main.go Normal file
View File

@@ -0,0 +1,431 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package main
import (
"bytes"
"encoding/json"
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"github.com/ory/kratos/cmd"
"github.com/ory/kratos/text"
"github.com/ory/x/clidoc"
"github.com/ory/x/clock"
)
var (
aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second)
inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute)
// Fixed clocks make the rendered "expired N minutes ago" / "within the next
// N minutes" example messages deterministic for doc generation. They are
// anchored so the durations come out as exactly one minute.
docExpiredClock = clock.NewMock(aSecondAgo.Add(time.Minute))
docUntilClock = clock.NewMock(inAMinute.Add(-time.Minute))
)
var messages map[string]*text.Message
func init() {
messages = map[string]*text.Message{
"NewInfoNodeLabelVerifyOTP": text.NewInfoNodeLabelVerifyOTP(),
"NewInfoNodeLabelVerificationCode": text.NewInfoNodeLabelVerificationCode(),
"NewInfoNodeLabelRecoveryCode": text.NewInfoNodeLabelRecoveryCode(),
"NewInfoNodeInputPassword": text.NewInfoNodeInputPassword(),
"NewInfoNodeInputPhoneNumber": text.NewInfoNodeInputPhoneNumber(),
"NewInfoNodeLabelGenerated": text.NewInfoNodeLabelGenerated("{title}", "{name}"),
"NewInfoNodeLabelSave": text.NewInfoNodeLabelSave(),
"NewInfoNodeLabelSubmit": text.NewInfoNodeLabelSubmit(),
"NewInfoNodeLabelID": text.NewInfoNodeLabelID(),
"NewErrorValidationSettingsFlowExpired": text.NewErrorValidationSettingsFlowExpired(docExpiredClock, aSecondAgo),
"NewInfoSelfServiceSettingsTOTPQRCode": text.NewInfoSelfServiceSettingsTOTPQRCode(),
"NewInfoSelfServiceSettingsTOTPSecret": text.NewInfoSelfServiceSettingsTOTPSecret("{secret}"),
"NewInfoSelfServiceSettingsTOTPSecretLabel": text.NewInfoSelfServiceSettingsTOTPSecretLabel(),
"NewInfoSelfServiceSettingsUpdateSuccess": text.NewInfoSelfServiceSettingsUpdateSuccess(),
"NewInfoSelfServiceSettingsUpdateUnlinkTOTP": text.NewInfoSelfServiceSettingsUpdateUnlinkTOTP(),
"NewInfoSelfServiceSettingsRevealLookup": text.NewInfoSelfServiceSettingsRevealLookup(),
"NewInfoSelfServiceSettingsRegenerateLookup": text.NewInfoSelfServiceSettingsRegenerateLookup(),
"NewInfoSelfServiceSettingsDisableLookup": text.NewInfoSelfServiceSettingsDisableLookup(),
"NewInfoSelfServiceSettingsLookupConfirm": text.NewInfoSelfServiceSettingsLookupConfirm(),
"NewInfoSelfServiceSettingsManagedByOrganization": text.NewInfoSelfServiceSettingsManagedByOrganization(),
"NewInfoSelfServiceSettingsLookupSecretList": text.NewInfoSelfServiceSettingsLookupSecretList([]string{"{secrets_list}"}, []interface{}{
text.NewInfoSelfServiceSettingsLookupSecret("{secret}"),
text.NewInfoSelfServiceSettingsLookupSecretUsed(aSecondAgo),
}),
"NewInfoSelfServiceSettingsLookupSecret": text.NewInfoSelfServiceSettingsLookupSecret("{secret}"),
"NewInfoSelfServiceSettingsLookupSecretUsed": text.NewInfoSelfServiceSettingsLookupSecretUsed(aSecondAgo),
"NewInfoSelfServiceSettingsLookupSecretsLabel": text.NewInfoSelfServiceSettingsLookupSecretsLabel(),
"NewInfoSelfServiceSettingsUpdateLinkOIDC": text.NewInfoSelfServiceSettingsUpdateLinkOIDC("{provider}"),
"NewInfoSelfServiceSettingsUpdateUnlinkOIDC": text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC("{provider}"),
"NewInfoSelfServiceRegisterWebAuthnDisplayName": text.NewInfoSelfServiceRegisterWebAuthnDisplayName(),
"NewInfoSelfServiceRemoveWebAuthn": text.NewInfoSelfServiceRemoveWebAuthn("{display_name}", aSecondAgo),
"NewInfoSelfServiceRemovePasskey": text.NewInfoSelfServiceRemovePasskey("{display_name}", aSecondAgo),
"NewErrorValidationVerificationFlowExpired": text.NewErrorValidationVerificationFlowExpired(docExpiredClock, aSecondAgo),
"NewInfoSelfServiceVerificationSuccessful": text.NewInfoSelfServiceVerificationSuccessful(),
"NewVerificationEmailSent": text.NewVerificationEmailSent(),
"NewVerificationEmailWithCodeSent": text.NewVerificationEmailWithCodeSent(),
"NewVerificationPhoneWithCodeSent": text.NewVerificationPhoneWithCodeSent(),
"NewInfoSelfServiceVerificationPhoneSuccessful": text.NewInfoSelfServiceVerificationPhoneSuccessful(),
"NewErrorValidationVerificationTokenInvalidOrAlreadyUsed": text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed(),
"NewErrorValidationVerificationRetrySuccess": text.NewErrorValidationVerificationRetrySuccess(),
"NewErrorValidationVerificationStateFailure": text.NewErrorValidationVerificationStateFailure(),
"NewErrorValidationVerificationCodeInvalidOrAlreadyUsed": text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(),
"NewErrorSystemGeneric": text.NewErrorSystemGeneric("{reason}"),
"NewErrorSystemNoAuthenticationMethodsAvailable": text.NewErrorSystemNoAuthenticationMethodsAvailable(),
"NewErrorSystemOrganizationNoSSOProvidersAvailable": text.NewErrorSystemOrganizationNoSSOProvidersAvailable(),
"NewValidationErrorGeneric": text.NewValidationErrorGeneric("{reason}"),
"NewValidationErrorRequired": text.NewValidationErrorRequired("{property}"),
"NewErrorValidationMinLength": text.NewErrorValidationMinLength(5, 3),
"NewErrorValidationMaxLength": text.NewErrorValidationMaxLength(5, 6),
"NewErrorValidationInvalidFormat": text.NewErrorValidationInvalidFormat("{pattern}"),
"NewErrorValidationMinimum": text.NewErrorValidationMinimum(5, 3),
"NewErrorValidationExclusiveMinimum": text.NewErrorValidationExclusiveMinimum(5, 5),
"NewErrorValidationMaximum": text.NewErrorValidationMaximum(5, 6),
"NewErrorValidationExclusiveMaximum": text.NewErrorValidationExclusiveMaximum(5, 5),
"NewErrorValidationMultipleOf": text.NewErrorValidationMultipleOf(7, 3),
"NewErrorValidationMaxItems": text.NewErrorValidationMaxItems(3, 4),
"NewErrorValidationMinItems": text.NewErrorValidationMinItems(3, 2),
"NewErrorValidationUniqueItems": text.NewErrorValidationUniqueItems(0, 2),
"NewErrorValidationWrongType": text.NewErrorValidationWrongType([]string{"{allowed_types_list}"}, "{actual_type}"),
"NewErrorValidationConst": text.NewErrorValidationConst("{expected}"),
"NewErrorValidationConstGeneric": text.NewErrorValidationConstGeneric(),
"NewErrorValidationPasswordPolicyViolationGeneric": text.NewErrorValidationPasswordPolicyViolationGeneric("{reason}"),
"NewErrorValidationPasswordIdentifierTooSimilar": text.NewErrorValidationPasswordIdentifierTooSimilar(),
"NewErrorValidationPasswordMinLength": text.NewErrorValidationPasswordMinLength(6, 5),
"NewErrorValidationPasswordMaxLength": text.NewErrorValidationPasswordMaxLength(72, 80),
"NewErrorValidationPasswordTooManyBreaches": text.NewErrorValidationPasswordTooManyBreaches(101),
"NewErrorValidationPasswordNewSameAsOld": text.NewErrorValidationPasswordNewSameAsOld(),
"NewErrorValidationInvalidCredentials": text.NewErrorValidationInvalidCredentials(),
"NewErrorValidationDuplicateCredentials": text.NewErrorValidationDuplicateCredentials(),
"NewErrorValidationDuplicateCredentialsWithHints": text.NewErrorValidationDuplicateCredentialsWithHints([]string{"{available_credential_types_list}"}, []string{"{available_oidc_providers_list}"}, "{credential_identifier_hint}"),
"NewErrorValidationDuplicateCredentialsOnOIDCLink": text.NewErrorValidationDuplicateCredentialsOnOIDCLink(),
"NewErrorValidationTOTPVerifierWrong": text.NewErrorValidationTOTPVerifierWrong(),
"NewErrorValidationWebAuthnVerifierWrong": text.NewErrorValidationWebAuthnVerifierWrong(),
"NewErrorValidationDeviceAuthnVerifierWrong": text.NewErrorValidationDeviceAuthnVerifierWrong(),
"NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid": text.NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid(),
"NewErrorValidationDeviceAuthnKeyReenrollmentRequired": text.NewErrorValidationDeviceAuthnKeyReenrollmentRequired(),
"NewErrorValidationLookupAlreadyUsed": text.NewErrorValidationLookupAlreadyUsed(),
"NewErrorValidationLookupInvalid": text.NewErrorValidationLookupInvalid(),
"NewErrorValidationIdentifierMissing": text.NewErrorValidationIdentifierMissing(),
"NewErrorValidationAddressNotVerified": text.NewErrorValidationAddressNotVerified(),
"NewErrorValidationNoTOTPDevice": text.NewErrorValidationNoTOTPDevice(),
"NewErrorValidationNoLookup": text.NewErrorValidationNoLookup(),
"NewErrorValidationNoWebAuthnDevice": text.NewErrorValidationNoWebAuthnDevice(),
"NewInfoLoginReAuth": text.NewInfoLoginReAuth(),
"NewInfoLoginMFA": text.NewInfoLoginMFA(),
"NewInfoLoginTOTPLabel": text.NewInfoLoginTOTPLabel(),
"NewInfoLoginLookupLabel": text.NewInfoLoginLookupLabel(),
"NewInfoLogin": text.NewInfoLogin(),
"NewInfoLoginAndLink": text.NewInfoLoginAndLink(),
"NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicateIdentifier}", "{provider}", "{newLoginUrl}", []string{"{available_credential_types_list}"}, []string{"{available_oidc_providers_list}"}),
"NewInfoLoginTOTP": text.NewInfoLoginTOTP(),
"NewInfoLoginLookup": text.NewInfoLoginLookup(),
"NewInfoLoginVerify": text.NewInfoLoginVerify(),
"NewInfoLoginWith": text.NewInfoLoginWith("{provider}", "{providerID}"),
"NewInfoLoginWithAndLink": text.NewInfoLoginWithAndLink("{provider}"),
"NewErrorValidationNoDeviceAuthnDevice": text.NewErrorValidationNoDeviceAuthnDevice(),
"NewErrorValidationLoginFlowExpired": text.NewErrorValidationLoginFlowExpired(docExpiredClock, aSecondAgo),
"NewErrorValidationLoginNoStrategyFound": text.NewErrorValidationLoginNoStrategyFound(),
"NewErrorValidationRegistrationNoStrategyFound": text.NewErrorValidationRegistrationNoStrategyFound(),
"NewErrorValidationSettingsNoStrategyFound": text.NewErrorValidationSettingsNoStrategyFound(),
"NewErrorValidationRecoveryNoStrategyFound": text.NewErrorValidationRecoveryNoStrategyFound(),
"NewErrorValidationVerificationNoStrategyFound": text.NewErrorValidationVerificationNoStrategyFound(),
"NewInfoSelfServiceLoginWebAuthn": text.NewInfoSelfServiceLoginWebAuthn(),
"NewInfoSelfServiceLoginDeviceAuthn": text.NewInfoSelfServiceLoginDeviceAuthn(),
"NewInfoRegistration": text.NewInfoRegistration(),
"NewInfoRegistrationWith": text.NewInfoRegistrationWith("{provider}", "{providerID}"),
"NewInfoRegistrationContinue": text.NewInfoRegistrationContinue(),
"NewInfoRegistrationBack": text.NewInfoRegistrationBack(),
"NewInfoSelfServiceChooseCredentials": text.NewInfoSelfServiceChooseCredentials(),
"NewErrorValidationRegistrationFlowExpired": text.NewErrorValidationRegistrationFlowExpired(docExpiredClock, aSecondAgo),
"NewErrorValidationRecoveryFlowExpired": text.NewErrorValidationRecoveryFlowExpired(docExpiredClock, aSecondAgo),
"NewRecoverySuccessful": text.NewRecoverySuccessful(docUntilClock, inAMinute),
"NewRecoveryEmailSent": text.NewRecoveryEmailSent(),
"NewRecoveryEmailWithCodeSent": text.NewRecoveryEmailWithCodeSent(),
"NewRecoveryCodeRecoverySelectAddressSent": text.NewRecoveryCodeRecoverySelectAddressSent("{masked_address}"),
"NewRecoveryAskAnyRecoveryAddress": text.NewRecoveryAskAnyRecoveryAddress(),
"NewRecoveryAskForFullAddress": text.NewRecoveryAskForFullAddress(),
"NewRecoveryAskToChooseAddress": text.NewRecoveryAskToChooseAddress(),
"NewRecoveryBack": text.NewRecoveryBack(),
"NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed(),
"NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed(),
"NewErrorValidationRecoveryRetrySuccess": text.NewErrorValidationRecoveryRetrySuccess(),
"NewErrorValidationRecoveryStateFailure": text.NewErrorValidationRecoveryStateFailure(),
"NewInfoNodeInputEmail": text.NewInfoNodeInputEmail(),
"NewInfoNodeInputEmailOrPhone": text.NewInfoNodeInputEmailOrPhone(),
"NewInfoNodeResendOTP": text.NewInfoNodeResendOTP(),
"NewInfoNodeLoginAndLinkCredential": text.NewInfoNodeLoginAndLinkCredential(),
"NewInfoNodeLabelContinue": text.NewInfoNodeLabelContinue(),
"NewInfoSelfServiceSettingsRegisterWebAuthn": text.NewInfoSelfServiceSettingsRegisterWebAuthn(),
"NewInfoSelfServiceSettingsRegisterPasskey": text.NewInfoSelfServiceSettingsRegisterPasskey(),
"NewInfoLoginWebAuthnPasswordless": text.NewInfoLoginWebAuthnPasswordless(),
"NewInfoSelfServiceRegistrationRegisterWebAuthn": text.NewInfoSelfServiceRegistrationRegisterWebAuthn(),
"NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(),
"NewInfoSelfServiceLoginPasskey": text.NewInfoSelfServiceLoginPasskey(),
"NewInfoSelfServiceRegistrationRegisterPasskey": text.NewInfoSelfServiceRegistrationRegisterPasskey(),
"NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(),
"NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(),
"NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(),
"NewLoginCodeSent": text.NewLoginCodeSent(),
"NewLoginCodeSentForAuthenticatedUser": text.NewLoginCodeSentForAuthenticatedUser(),
"NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(),
"NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(),
"NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(),
"NewInfoNodeLabelRegistrationCode": text.NewInfoNodeLabelRegistrationCode(),
"NewInfoNodeLabelLoginCode": text.NewInfoNodeLabelLoginCode(),
"NewErrorValidationLoginRetrySuccessful": text.NewErrorValidationLoginRetrySuccessful(),
"NewErrorValidationTraitsMismatch": text.NewErrorValidationTraitsMismatch(),
"NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(),
"NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(),
"NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(),
"NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(),
"NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(),
"NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(),
"NewInfoLoginPassword": text.NewInfoLoginPassword(),
"NewErrorValidationAccountNotFound": text.NewErrorValidationAccountNotFound(),
"NewInfoSelfServiceLoginAAL2CodeAddress": text.NewInfoSelfServiceLoginAAL2CodeAddress("{channel}", "{address}"),
"NewErrorCaptchaFailed": text.NewErrorCaptchaFailed(),
"NewCaptchaContainerMessage": text.NewCaptchaContainerMessage(),
"NewErrorValidationEmail": text.NewErrorValidationEmail("{value}"),
"NewErrorValidationPhone": text.NewErrorValidationPhone("{value}"),
"NewErrorValidationIdentityDisabled": text.NewErrorValidationIdentityDisabled(),
"NewErrorValidationSettingsTooManyAddressChanges": text.NewErrorValidationSettingsTooManyAddressChanges(),
}
}
func main() {
if os.Args[1] == "elements" {
path, err := filepath.Abs(os.Args[2])
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to determine absolute path for elements generation: %+v", err)
os.Exit(1)
}
if err := generateElements(path); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to generate locales for elements: %+v", err)
os.Exit(1)
}
return
}
if err := clidoc.Generate(cmd.NewRootCmd(nil, nil), []string{filepath.Join(os.Args[2], "cli")}); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to generate CLI docs: %+v", err)
os.Exit(1)
}
if err := validateAllMessages(filepath.Join(os.Args[1], "text")); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to validate messages: %+v\n", err)
os.Exit(1)
}
sortedMessages := sortMessages()
for i := 1; i < len(sortedMessages); i++ {
if sortedMessages[i].ID == sortedMessages[i-1].ID {
_, _ = fmt.Fprintf(os.Stderr, "Message ID %d is used more than once: %q %q\n", sortedMessages[i].ID, sortedMessages[i].Text, sortedMessages[i-1].Text)
os.Exit(1)
}
}
if err := writeMessages(filepath.Join(os.Args[2], "concepts/ui-messages.md"), sortedMessages); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to generate message table: %+v\n", err)
os.Exit(1)
}
if err := writeMessagesJson(filepath.Join(os.Args[2], "concepts/messages.json"), sortedMessages); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to generate messages.json: %+v\n", err)
os.Exit(1)
}
fmt.Println("All files have been generated and updated.")
}
func codeEncode(in interface{}) string {
out, err := json.MarshalIndent(in, "", " ")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to encode to JSON: %+v", err)
os.Exit(1)
}
return string(out)
}
func sortMessages() []*text.Message {
var toSort []*text.Message
for _, m := range messages {
toSort = append(toSort, m)
}
sort.Slice(toSort, func(i, j int) bool {
if toSort[i].ID == toSort[j].ID {
return toSort[i].Text < toSort[j].Text
}
return toSort[i].ID < toSort[j].ID
})
return toSort
}
func writeMessages(path string, sortedMessages []*text.Message) error {
//nolint:gosec
content, err := os.ReadFile(path) // #nosec
if err != nil {
return err
}
var w bytes.Buffer
for _, m := range sortedMessages {
fmt.Fprintf(&w, `###### %s (%d)
%s
`, m.Text, m.ID, "```json\n"+codeEncode(m)+"\n```")
}
r := regexp.MustCompile(`(?s)<!-- START MESSAGE TABLE -->(.*?)<!-- END MESSAGE TABLE -->`)
result := r.ReplaceAllString(string(content), "<!-- START MESSAGE TABLE -->\n"+w.String()+"\n<!-- END MESSAGE TABLE -->")
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) // #nosec
if err != nil {
return err
}
if _, err := f.WriteString(result); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return nil
}
func writeMessagesJson(path string, sortedMessages []*text.Message) error {
result := codeEncode(sortedMessages)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) // #nosec
if err != nil {
return err
}
if _, err := f.WriteString(result); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return nil
}
func validateAllMessages(path string) error {
type message struct {
ID, Name string
}
usedIDs := make([]message, 0, len(messages))
set := token.NewFileSet()
packs, err := parser.ParseDir(set, path, nil, 0)
if err != nil {
return errors.Wrapf(err, "unable to parse text directory")
}
info := &types.Info{
Defs: make(map[*ast.Ident]types.Object),
}
//nolint:staticcheck
var pack *ast.Package
for _, p := range packs {
if p.Name == "text" {
pack = p
break
}
}
allFiles := make([]*ast.File, 0)
for fn, f := range pack.Files {
if strings.HasSuffix(fn, "/id.go") {
allFiles = append(allFiles, f)
}
}
_, err = (&types.Config{Importer: importer.Default()}).Check("text", set, allFiles, info)
if err != nil {
return err // type error
}
for _, f := range pack.Files {
for _, d := range f.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
if name := decl.Name.String(); decl.Name.IsExported() && strings.HasPrefix(name, "New") {
if _, ok := messages[name]; !ok {
return errors.Errorf("expected to find message %s in the list for the documentation generation but could not", name)
}
}
case *ast.GenDecl:
if decl.Tok == token.CONST {
for _, spec := range decl.Specs {
value := spec.(*ast.ValueSpec) // safe because decl.Tok is token.CONST
if t, ok := value.Type.(*ast.Ident); ok {
if t.Name == "ID" {
for _, name := range value.Names {
c := info.ObjectOf(name)
if c == nil {
return errors.Errorf("expected to find const %s in text/id.go", name.Name)
}
usedIDs = append(usedIDs, message{
ID: c.(*types.Const).Val().ExactString(),
Name: name.Name,
})
}
}
}
}
}
}
}
}
sort.Slice(usedIDs, func(i, j int) bool {
return usedIDs[i].ID < usedIDs[j].ID
})
for i := 1; i < len(usedIDs); i++ {
if usedIDs[i].ID == usedIDs[i-1].ID {
return errors.Errorf("message ID %s is used more than once: %s %s", usedIDs[i].ID, usedIDs[i].Name, usedIDs[i-1].Name)
}
}
return nil
}
func generateElements(messageFilePath string) error {
// If the file at messageFilePath does not exist, create it
f, err := os.OpenFile(messageFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) // #nosec
if err != nil {
return errors.Wrapf(err, "unable to open or create message file at %s", messageFilePath)
}
if err := f.Close(); err != nil {
return errors.Wrapf(err, "unable to close message file at %s", messageFilePath)
}
sortedMessages := sortMessages()
if err := writeMessagesJson(messageFilePath, sortedMessages); err != nil {
return errors.Wrapf(err, "unable to write messages json to %s", messageFilePath)
}
return nil
}

14
cmd/clidoc/main_test.go Normal file
View File

@@ -0,0 +1,14 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMessages(t *testing.T) {
require.NoError(t, validateAllMessages("../../text"))
}

27
cmd/courier/root.go Normal file
View File

@@ -0,0 +1,27 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package courier
import (
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/x/configx"
)
// NewCourierCmd creates a new courier command
func NewCourierCmd() *cobra.Command {
c := &cobra.Command{
Use: "courier",
Short: "Commands related to the Ory Kratos message courier",
}
configx.RegisterFlags(c.PersistentFlags())
return c
}
func RegisterCommandRecursive(parent *cobra.Command, dOpts []driver.RegistryOption) {
c := NewCourierCmd()
parent.AddCommand(c)
c.AddCommand(NewWatchCmd(dOpts))
}

108
cmd/courier/watch.go Normal file
View File

@@ -0,0 +1,108 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package courier
import (
"context"
"net/http"
"github.com/spf13/cobra"
"github.com/urfave/negroni"
"golang.org/x/sync/errgroup"
"github.com/ory/graceful"
"github.com/ory/kratos/driver"
"github.com/ory/x/configx"
"github.com/ory/x/prometheusx"
"github.com/ory/x/reqlog"
)
func NewWatchCmd(dOpts []driver.RegistryOption) *cobra.Command {
c := &cobra.Command{
Use: "watch",
Short: "Starts the Ory Kratos message courier",
RunE: func(cmd *cobra.Command, args []string) error {
r, err := driver.New(cmd.Context(), cmd.ErrOrStderr(), append(dOpts, driver.WithConfigOptions(configx.WithFlags(cmd.Flags())))...)
if err != nil {
return err
}
return StartCourier(cmd.Context(), r)
},
}
c.PersistentFlags().Int("expose-metrics-port", 0, "The port to expose the metrics endpoint on (not exposed by default)")
return c
}
func StartCourier(ctx context.Context, r driver.Registry) error {
eg, ctx := errgroup.WithContext(ctx)
if port := r.Config().CourierExposeMetricsPort(ctx); port != 0 {
eg.Go(func() error {
return ServeMetrics(ctx, r, port)
})
}
eg.Go(func() error {
return Watch(ctx, r)
})
return eg.Wait()
}
func ServeMetrics(ctx context.Context, r driver.Registry, port int) error {
cfg := r.Config().ServeAdmin(ctx)
l := r.Logger()
recovery := negroni.NewRecovery()
recovery.Logger = l
n := negroni.New(
recovery,
reqlog.NewMiddlewareFromLogger(l, "admin#"+cfg.BaseURL.String()),
)
router := http.NewServeMux()
router.Handle(prometheusx.MetricsPrometheusPath, prometheusx.Handler())
n.UseHandler(router)
var handler http.Handler = n
//#nosec G112 -- the correct settings are set by graceful.WithDefaults
server := graceful.WithDefaults(&http.Server{
Addr: configx.GetAddress(cfg.Host, port),
Handler: handler,
})
l.WithField("addr", server.Addr).Info("Starting the metrics httpd")
if err := graceful.GracefulContext(ctx, server.ListenAndServe, server.Shutdown); err != nil {
l.Errorln("Failed to gracefully shutdown metrics httpd")
return err
}
l.Println("Metrics httpd was shutdown gracefully")
return nil
}
func Watch(ctx context.Context, r driver.Registry) error {
ctx, cancel := context.WithCancel(ctx)
r.Logger().Println("Courier worker started.")
if err := graceful.Graceful(func() error {
c, err := r.Courier(ctx)
if err != nil {
return err
}
return c.Work(ctx)
}, func(_ context.Context) error {
cancel()
return nil
}); err != nil {
r.Logger().WithError(err).Error("Failed to run courier worker.")
return err
}
r.Logger().Println("Courier worker was shutdown gracefully.")
return nil
}

47
cmd/courier/watch_test.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package courier
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/phayes/freeport"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/pkg"
"github.com/ory/x/configx"
)
func TestStartCourier(t *testing.T) {
t.Run("case=without metrics", func(t *testing.T) {
_, r := pkg.NewFastRegistryWithMocks(t)
go func() { _ = StartCourier(t.Context(), r) }()
require.Equal(t, r.Config().CourierExposeMetricsPort(t.Context()), 0)
})
t.Run("case=with metrics", func(t *testing.T) {
port, err := freeport.GetFreePort()
require.NoError(t, err)
_, r := pkg.NewFastRegistryWithMocks(t, configx.WithValue("expose-metrics-port", port))
go func() { _ = StartCourier(t.Context(), r) }()
transport := http.DefaultTransport.(*http.Transport).Clone()
t.Cleanup(transport.CloseIdleConnections)
client := &http.Client{Transport: transport}
url := fmt.Sprintf("http://127.0.0.1:%d/metrics/prometheus", port)
require.EventuallyWithT(t, func(t *assert.CollectT) {
res, err := client.Get(url)
if !assert.NoError(t, err) {
return
}
defer func() {
assert.NoError(t, res.Body.Close())
}()
assert.Equal(t, 200, res.StatusCode)
}, 10*time.Second, 10*time.Millisecond)
})
}

371
cmd/daemon/serve.go Normal file
View File

@@ -0,0 +1,371 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package daemon
import (
"context"
"crypto/tls"
"net/http"
"time"
"github.com/pkg/errors"
"github.com/rs/cors"
"github.com/spf13/cobra"
"github.com/urfave/negroni"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"golang.org/x/sync/errgroup"
"github.com/ory/analytics-go/v5"
"github.com/ory/graceful"
"github.com/ory/kratos/cmd/courier"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/errorx"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/logout"
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/flow/verification"
"github.com/ory/kratos/selfservice/strategy/link"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/x/healthx"
"github.com/ory/x/httprouterx"
"github.com/ory/x/metricsx"
"github.com/ory/x/networkx"
"github.com/ory/x/otelx"
"github.com/ory/x/otelx/semconv"
"github.com/ory/x/popx"
"github.com/ory/x/prometheusx"
"github.com/ory/x/reqlog"
"github.com/ory/x/urlx"
)
func init() {
graceful.DefaultShutdownTimeout = 120 * time.Second
}
var httpMetrics = prometheusx.NewHTTPMetrics("kratos", prometheusx.HTTPPrefix, config.Version, config.Commit, config.Date)
func servePublic(ctx context.Context, r *driver.RegistryDefault, cmd *cobra.Command) (func() error, error) {
cfg := r.Config().ServePublic(ctx)
l := r.Logger()
rec := negroni.NewRecovery()
rec.Logger = l
router := httprouterx.NewRouterPublic()
n := negroni.New(
rec,
httpMetrics,
)
for _, mw := range r.HTTPMiddlewares() {
n.Use(mw)
}
publicLogger := reqlog.NewMiddlewareFromLogger(l, "public#"+cfg.BaseURL.String())
if cfg.RequestLog.DisableHealth {
publicLogger.ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath)
}
n.UseFunc(semconv.Middleware)
n.Use(publicLogger)
n.Use(x.HTTPLoaderContextMiddleware(r))
n.UseFunc(httprouterx.NoCacheNegroni)
n.Use(sqa(ctx, cmd, r))
csrf := nosurfx.NewCSRFHandler(router, r)
// we need to always load the CORS middleware even if it is disabled, to allow hot-enabling CORS
n.UseFunc(func(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
cfg, enabled := r.Config().CORSPublic(req.Context())
if !enabled {
next(w, req)
return
}
cors.New(cfg).ServeHTTP(w, req, next)
})
r.WithCSRFHandler(csrf)
n.UseHandler(r.CSRFHandler())
// Disable CSRF for these endpoints
csrf.DisablePath(healthx.AliveCheckPath)
csrf.DisablePath(healthx.ReadyCheckPath)
csrf.DisablePath(healthx.VersionPath)
csrf.DisablePath(prometheusx.MetricsPrometheusPath)
r.RegisterPublicRoutes(ctx, router)
var handler http.Handler = n
if tracer := r.Tracer(ctx); tracer.IsLoaded() {
handler = otelx.NewMiddleware(handler, "servePublic",
otelhttp.WithTracerProvider(tracer.Provider()),
)
}
handler = http.MaxBytesHandler(handler, 5*1024*1024 /* 5 MB */) // Important: this must be the outermost handler or our tracing breaks
certFunc, err := cfg.TLS.GetCertFunc(ctx, l, "public")
if err != nil {
return nil, err
}
//#nosec G112 -- the correct settings are set by graceful.WithDefaults
server := graceful.WithDefaults(&http.Server{
Handler: handler,
TLSConfig: &tls.Config{GetCertificate: certFunc, MinVersion: tls.VersionTLS12},
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
})
addr := cfg.GetAddress()
return func() error {
l.WithField("addr", addr).Info("Starting the public httpd")
if err := graceful.GracefulContext(ctx, func() error {
listener, err := networkx.MakeListener(addr, &cfg.Socket)
if err != nil {
return err
}
if certFunc == nil {
return server.Serve(listener)
}
return server.ServeTLS(listener, "", "")
}, server.Shutdown); err != nil {
if !errors.Is(err, context.Canceled) {
l.Errorf("Failed to gracefully shutdown public httpd: %s", err)
return err
}
}
l.Println("Public httpd was shutdown gracefully")
return nil
}, nil
}
func serveAdmin(ctx context.Context, r *driver.RegistryDefault, cmd *cobra.Command) (func() error, error) {
cfg := r.Config().ServeAdmin(ctx)
l := r.Logger()
rec := negroni.NewRecovery()
rec.Logger = l
router := httprouterx.NewRouterAdminWithPrefix()
n := negroni.New(
rec,
httpMetrics,
)
for _, mw := range r.HTTPMiddlewares() {
n.Use(mw)
}
adminLogger := reqlog.NewMiddlewareFromLogger(l, "admin#"+cfg.BaseURL.String())
if cfg.RequestLog.DisableHealth {
adminLogger.ExcludePaths(
httprouterx.AdminPrefix+healthx.AliveCheckPath,
httprouterx.AdminPrefix+healthx.ReadyCheckPath,
httprouterx.AdminPrefix+prometheusx.MetricsPrometheusPath,
)
}
n.UseFunc(semconv.Middleware)
n.Use(adminLogger)
n.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni)
n.UseFunc(httprouterx.NoCacheNegroni)
n.Use(x.HTTPLoaderContextMiddleware(r))
n.Use(sqa(ctx, cmd, r))
r.RegisterAdminRoutes(ctx, router)
n.UseHandler(router)
var handler http.Handler = n
if tracer := r.Tracer(ctx); tracer.IsLoaded() {
handler = otelx.NewMiddleware(handler, "serveAdmin",
otelhttp.WithTracerProvider(tracer.Provider()),
)
}
handler = http.MaxBytesHandler(handler, 5*1024*1024 /* 5 MB */) // Important: this must be the outermost handler or our tracing breaks
certFunc, err := cfg.TLS.GetCertFunc(ctx, l, "admin")
if err != nil {
return nil, err
}
//#nosec G112 -- the correct settings are set by graceful.WithDefaults
server := graceful.WithDefaults(&http.Server{
Handler: handler,
TLSConfig: &tls.Config{GetCertificate: certFunc, MinVersion: tls.VersionTLS12},
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 600 * time.Second,
})
addr := cfg.GetAddress()
return func() error {
l.WithField("addr", addr).Info("Starting the admin httpd")
if err := graceful.GracefulContext(ctx, func() error {
listener, err := networkx.MakeListener(addr, &cfg.Socket)
if err != nil {
return err
}
if certFunc == nil {
return server.Serve(listener)
}
return server.ServeTLS(listener, "", "")
}, server.Shutdown); err != nil {
if !errors.Is(err, context.Canceled) {
l.Errorf("Failed to gracefully shutdown admin httpd: %s", err)
return err
}
}
l.Println("Admin httpd was shutdown gracefully")
return nil
}, nil
}
func sqa(ctx context.Context, cmd *cobra.Command, d driver.Registry) *metricsx.Service {
urls := []string{
d.Config().ServePublic(ctx).BaseURL.Host,
d.Config().ServeAdmin(ctx).BaseURL.Host,
d.Config().SelfServiceFlowLoginUI(ctx).Host,
d.Config().SelfServiceFlowSettingsUI(ctx).Host,
d.Config().SelfServiceFlowErrorURL(ctx).Host,
d.Config().SelfServiceFlowRegistrationUI(ctx).Host,
d.Config().SelfServiceFlowRecoveryUI(ctx).Host,
d.Config().ServePublic(ctx).Host,
d.Config().ServeAdmin(ctx).Host,
}
if c, y := d.Config().CORSPublic(ctx); y {
urls = append(urls, c.AllowedOrigins...)
}
host := urlx.ExtractPublicAddress(urls...)
// Creates only ones
// instance
return metricsx.New(
cmd,
d.Logger(),
d.Config().GetProvider(ctx),
&metricsx.Options{
Service: "kratos",
DeploymentId: metricsx.Hash(d.Persister().NetworkID(ctx).String()),
DBDialect: d.Persister().GetConnection(ctx).Dialect.Name(),
IsDevelopment: d.Config().IsInsecureDevMode(ctx),
WriteKey: "qQlI6q8Q4WvkzTjKQSor4sHYOikHIvvi",
WhitelistedPaths: []string{
"/",
healthx.AliveCheckPath,
healthx.ReadyCheckPath,
healthx.VersionPath,
oidc.RouteBase,
login.RouteInitBrowserFlow,
login.RouteInitAPIFlow,
login.RouteGetFlow,
login.RouteSubmitFlow,
logout.RouteInitBrowserFlow,
logout.RouteSubmitFlow,
logout.RouteAPIFlow,
registration.RouteInitBrowserFlow,
registration.RouteInitAPIFlow,
registration.RouteGetFlow,
registration.RouteSubmitFlow,
session.RouteWhoami,
httprouterx.AdminPrefix + "/" + schema.SchemasPath,
httprouterx.AdminPrefix + identity.RouteCollection,
settings.RouteInitBrowserFlow,
settings.RouteInitAPIFlow,
settings.RouteGetFlow,
settings.RouteSubmitFlow,
verification.RouteInitAPIFlow,
verification.RouteInitBrowserFlow,
verification.RouteGetFlow,
verification.RouteSubmitFlow,
recovery.RouteInitAPIFlow,
recovery.RouteInitBrowserFlow,
recovery.RouteGetFlow,
recovery.RouteSubmitFlow,
link.RouteAdminCreateRecoveryLink,
errorx.RouteGet,
prometheusx.MetricsPrometheusPath,
},
BuildVersion: config.Version,
BuildHash: config.Commit,
BuildTime: config.Date,
Config: &analytics.Config{
Endpoint: "https://sqa.ory.sh",
GzipCompressionLevel: 6,
BatchMaxSize: 500 * 1000,
BatchSize: 1000,
Interval: time.Hour * 6,
},
Hostname: host,
},
)
}
func courierTask(ctx context.Context, d driver.Registry) func() error {
return func() error {
if d.Config().IsBackgroundCourierEnabled(ctx) {
return courier.Watch(ctx, d)
}
return nil
}
}
func ServeAll(d *driver.RegistryDefault) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
g, ctx := errgroup.WithContext(ctx)
cmd.SetContext(ctx)
if err := popx.VerifyDialect(ctx, d.Persister().GetConnection(ctx)); err != nil {
return errors.WithStack(err)
}
// construct all tasks upfront to avoid race conditions
publicSrv, err := servePublic(ctx, d, cmd)
if err != nil {
return errors.WithStack(err)
}
adminSrv, err := serveAdmin(ctx, d, cmd)
if err != nil {
return errors.WithStack(err)
}
tasks := []func() error{
publicSrv,
adminSrv,
courierTask(ctx, d),
}
for _, task := range tasks {
g.Go(task)
}
return g.Wait()
}
}

76
cmd/daemon/serve_test.go Normal file
View File

@@ -0,0 +1,76 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package daemon
import (
"fmt"
"io"
"net/http"
"testing"
"time"
"github.com/phayes/freeport"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/ory/x/configx"
"github.com/ory/kratos/pkg"
)
func TestMetricsRouterPaths(t *testing.T) {
// TODO refactor this test to be parallelizable once we rewrite the server setup to be properly testable
ports, err := freeport.GetFreePorts(2)
require.NoError(t, err)
adminPort, publicPort := ports[0], ports[1]
cmd := &cobra.Command{}
cmd.Flags().Bool("sqa-opt-out", true, "")
_, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{
"serve.admin.port": adminPort,
"serve.public.port": publicPort,
}))
startAdmin, err := serveAdmin(t.Context(), reg, cmd)
require.NoError(t, err)
startPublic, err := servePublic(t.Context(), reg, cmd)
require.NoError(t, err)
eg, _ := errgroup.WithContext(t.Context())
eg.Go(startAdmin)
eg.Go(startPublic)
transport := http.DefaultTransport.(*http.Transport).Clone()
t.Cleanup(transport.CloseIdleConnections)
client := &http.Client{Transport: transport}
require.EventuallyWithT(t, func(t *assert.CollectT) {
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/health/ready", publicPort))
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equalf(t, http.StatusOK, resp.StatusCode, "%s", body)
}, 2*time.Second, 10*time.Millisecond)
// Make some requests that should be recorded in the metrics
req, _ := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://127.0.0.1:%d/sessions/session-id", publicPort), nil)
resp, err := client.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
resp, err = client.Get(fmt.Sprintf("http://127.0.0.1:%d/admin/identities/8e5f29ec-db29-4e56-8517-75f7d4e2f1bc/sessions", adminPort))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
res, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/admin/metrics/prometheus", adminPort))
require.NoError(t, err)
require.EqualValues(t, http.StatusOK, res.StatusCode)
respBody, err := io.ReadAll(res.Body)
body := string(respBody)
require.NoError(t, err)
assert.Contains(t, body, `endpoint="/sessions/{param}"`)
assert.Contains(t, body, `endpoint="/admin/identities/{param}/sessions"`)
}, 5*time.Second, 100*time.Millisecond)
}

View File

@@ -0,0 +1,332 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package argon2
import (
"fmt"
"io"
"runtime"
"strconv"
"time"
"github.com/ory/x/configx"
"github.com/ory/x/cmdx"
"github.com/fatih/color"
"github.com/inhies/go-bytesize"
"github.com/spf13/cobra"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hash"
)
const (
FlagStartMemory = "start-memory"
FlagMaxMemory = "max-memory"
FlagAdjustMemory = "adjust-memory-by"
FlagStartIterations = "start-iterations"
FlagMaxConcurrent = "max-concurrent"
FlagRuns = "probe-runs"
)
type (
colorWriter struct {
c *color.Color
w io.Writer
}
loadResult struct {
r *resultTable
v *config.Argon2
}
loadResults []*loadResult
)
var _ cmdx.Table = loadResults{}
func (l loadResults) Header() []string {
return append((&resultTable{}).Header(), "MEMOry PARAM", "ITERATIONS PARAM")
}
func (l loadResults) Table() [][]string {
t := make([][]string, len(l))
for i, r := range l {
t[i] = append(r.r.Columns(), fmt.Sprintf("%d", r.v.Memory), fmt.Sprintf("%d", r.v.Iterations))
}
return t
}
func (l loadResults) Interface() interface{} {
return l
}
func (l loadResults) Len() int {
return len(l)
}
func (c *colorWriter) Write(o []byte) (int, error) {
return c.c.Fprintf(c.w, "%s", o)
}
func newCalibrateCmd() *cobra.Command {
var (
maxMemory, adjustMemory bytesize.ByteSize = 0, 512 * bytesize.MB
runs int
)
flagConfig := &argon2Config{
localConfig: config.Argon2{},
}
cmd := &cobra.Command{
Use: "calibrate <requests-per-minute>",
Args: cobra.ExactArgs(1),
Short: "Computes Optimal Argon2 Parameters",
Long: `This command helps you calibrate the configuration parameters for Argon2. Password hashing is a trade-off between security, resource consumption, and user experience. Resource consumption should not be too high and the login should not take too long.
We recommend that the login process takes between half a second and one second for password hashing, giving a good balance between security and user experience.
Please note that the values depend on the machine you run the hashing on. If you have RAM constraints, please set the memory dedicated to Ory Kratos to avoid out of memory panics.`,
RunE: func(cmd *cobra.Command, args []string) error {
progressPrinter := cmdx.NewLoudErrPrinter(cmd)
resultPrinter := cmdx.NewLoudPrinter(cmd, &colorWriter{c: color.New(color.FgGreen), w: cmd.ErrOrStderr()})
conf, err := configProvider(cmd, flagConfig)
if err != nil {
return err
}
// always take start flags, or their default
conf.localConfig.Memory = flagConfig.localConfig.Memory
conf.localConfig.Iterations = flagConfig.localConfig.Iterations
desiredDuration := conf.localConfig.ExpectedDuration
reqPerMin, err := strconv.ParseInt(args[0], 0, 0)
if err != nil {
// we want the error and usage string printed so just return
return err
}
hasher := hash.NewHasherArgon2(conf)
var currentDuration time.Duration
_, _ = progressPrinter.Printf("Increasing memory to get over %s:\n", desiredDuration)
for {
if maxMemory != 0 && conf.localConfig.Memory > maxMemory {
// don't further increase memory
_, _ = progressPrinter.Println(" ouch, hit the memory limit there")
conf.localConfig.Memory = maxMemory
break
}
currentDuration, err = probe(cmd, hasher, runs, progressPrinter)
if err != nil {
return err
}
_, _ = progressPrinter.Printf(" took %s with %s of memory\n", currentDuration, conf.localConfig.Memory)
if currentDuration > desiredDuration {
if conf.localConfig.Memory <= adjustMemory {
// adjusting the memory would now result in <= 0B
adjustMemory = adjustMemory >> 1
}
conf.localConfig.Memory -= adjustMemory
break
}
// adjust config
conf.localConfig.Memory += adjustMemory
}
_, _ = progressPrinter.Printf("Decreasing memory to get under %s:\n", desiredDuration)
for {
currentDuration, err = probe(cmd, hasher, runs, progressPrinter)
if err != nil {
return err
}
_, _ = progressPrinter.Printf(" took %s with %s of memory\n", currentDuration, conf.localConfig.Memory)
if currentDuration < desiredDuration {
break
}
for conf.localConfig.Memory <= adjustMemory {
// adjusting the memory would now result in <= 0B
adjustMemory = adjustMemory >> 1
}
// adjust config
conf.localConfig.Memory -= adjustMemory
}
_, _ = resultPrinter.Printf("Settled on %s of memory.\n", conf.localConfig.Memory)
_, _ = progressPrinter.Printf("Increasing iterations to get over %s:\n", desiredDuration)
for {
currentDuration, err = probe(cmd, hasher, runs, progressPrinter)
if err != nil {
return err
}
_, _ = progressPrinter.Printf(" took %s with %d iterations\n", currentDuration, conf.localConfig.Iterations)
if currentDuration > desiredDuration {
if conf.localConfig.Iterations > 1 {
conf.localConfig.Iterations -= 1
}
break
}
// adjust config
conf.localConfig.Iterations += 1
}
_, _ = progressPrinter.Printf("Decreasing iterations to get under %s:\n", desiredDuration)
for {
currentDuration, err = probe(cmd, hasher, runs, progressPrinter)
if err != nil {
return err
}
_, _ = progressPrinter.Printf(" took %s with %d iterations\n", currentDuration, conf.localConfig.Iterations)
// break also when iterations is 1; this catches the case where 1 was only slightly under the desired time and took longer a bit longer on another run
if currentDuration < desiredDuration || conf.localConfig.Iterations == 1 {
break
}
// adjust config
conf.localConfig.Iterations -= 1
}
_, _ = resultPrinter.Printf("Settled on %d iterations.\n", conf.localConfig.Iterations)
results := make(loadResults, 5)
for i := 0; i < len(results); i++ {
_, _ = progressPrinter.Printf("\nRunning load test to simulate %d login requests per minute.\n", reqPerMin)
res, err := runLoadTest(cmd, conf, int(reqPerMin))
if err != nil {
return err
}
cv, _ := conf.HasherArgon2()
ccv := *cv
results[i] = &loadResult{r: res, v: &ccv}
_, _ = progressPrinter.Println("The load test result is:")
_, _ = progressPrinter.Println()
cmdx.PrintRow(cmd, res)
switch {
// too fast
case res.MedianTime < conf.localConfig.ExpectedDuration:
_, _ = progressPrinter.Printf("The median was %s under the minimal duration of %s, going to increase the hash cost.\n", conf.localConfig.ExpectedDuration-res.MedianTime, conf.localConfig.ExpectedDuration)
// try to increase memory first
if res.MaxMem+64*bytesize.MB < maxMemory {
// only small amounts of memory are sensible as we already benchmarked a single, non-concurrent request
conf.localConfig.Memory += 64 * bytesize.MB
_, _ = progressPrinter.Printf("Increasing memory to %s\n", conf.localConfig.Memory)
} else {
// increasing memory is not allowed by maxMemory, therefore increase CPU load
conf.localConfig.Iterations++
_, _ = progressPrinter.Printf("Increasing iterations to %d\n", conf.localConfig.Iterations)
}
// too much memory
case res.MaxMem > conf.localConfig.DedicatedMemory:
_, _ = progressPrinter.Printf("The required memory was %s more than the maximum allowed of %s.\n", res.MaxMem-maxMemory, conf.localConfig.DedicatedMemory)
//nolint:gosec // disable G115
conf.localConfig.Memory -= (res.MaxMem - conf.localConfig.DedicatedMemory) / bytesize.ByteSize(reqPerMin)
_, _ = progressPrinter.Printf("Decreasing memory to %s\n", conf.localConfig.Memory)
// too slow
case res.MaxTime > conf.localConfig.ExpectedDeviation+conf.localConfig.ExpectedDuration:
_, _ = progressPrinter.Printf("The longest request took %s longer than the longest acceptable time of %s, going to decrease the hash cost.\n", res.MaxTime-conf.localConfig.ExpectedDeviation+conf.localConfig.ExpectedDuration, conf.localConfig.ExpectedDeviation+conf.localConfig.ExpectedDuration)
// try to decrease iterations first
if conf.localConfig.Iterations > 1 {
conf.localConfig.Iterations--
_, _ = progressPrinter.Printf("Decreasing iterations to %d\n", conf.localConfig.Iterations)
} else {
// decreasing iterations is not possible anymore, decreasing memory
// only small amounts of memory are sensible as we already benchmarked a single, non-concurrent request
conf.localConfig.Memory -= 64 * bytesize.MB
_, _ = progressPrinter.Printf("Decreasing memory to %s\n", conf.localConfig.Memory)
}
// too high deviation
case res.StdDev > conf.localConfig.ExpectedDeviation:
_, _ = progressPrinter.Printf("The deviation was %s more than the expected deviation of %s.\n", res.StdDev-conf.localConfig.ExpectedDeviation, conf.localConfig.ExpectedDeviation)
// try to decrease iterations first
if conf.localConfig.Iterations > 1 {
conf.localConfig.Iterations--
_, _ = progressPrinter.Printf("Decreasing iterations to %d\n", conf.localConfig.Iterations)
} else {
// decreasing iterations is not possible anymore, decreasing memory
// only small amounts of memory are sensible as we already benchmarked a single, non-concurrent request
conf.localConfig.Memory -= 64 * bytesize.MB
_, _ = progressPrinter.Printf("Decreasing memory to %s\n", conf.localConfig.Memory)
}
// all values seem reasonable
default:
_, _ = progressPrinter.Println("These values look good to me.")
_, _ = progressPrinter.Println()
cmdx.PrintRow(cmd, conf)
return nil
}
}
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Could not automatically determine good parameters. Have a look at all the measurements taken and select acceptable values yourself. Have a look in the docs for more information: https://www.ory.com/kratos/docs/debug/performance-out-of-memory-password-hashing-argon2")
cmdx.PrintTable(cmd, results)
return nil
},
}
flags := cmd.Flags()
flags.IntVarP(&runs, FlagRuns, "r", 2, "Runs per probe, median of all runs is taken as the result.")
flags.VarP(&flagConfig.localConfig.Memory, FlagStartMemory, "m", "Amount of memory to start probing at.")
flags.Var(&maxMemory, FlagMaxMemory, "Maximum memory allowed (0 means no limit).")
flags.Var(&adjustMemory, FlagAdjustMemory, "Amount by which the memory is adjusted in every step while probing.")
flags.Uint32VarP(&flagConfig.localConfig.Iterations, FlagStartIterations, "i", 1, "Number of iterations to start probing at.")
flags.Uint8(FlagMaxConcurrent, 16, "Maximum number of concurrent hashing operations.")
registerArgon2ConstantConfigFlags(flags, flagConfig)
cmdx.RegisterFormatFlags(flags)
configx.RegisterFlags(flags)
return cmd
}
func probe(cmd *cobra.Command, hasher hash.Hasher, runs int, progressPrinter *cmdx.ConditionalPrinter) (time.Duration, error) {
// force GC at the start of the experiment
runtime.GC()
start := time.Now()
var mid time.Time
for i := 0; i < runs; i++ {
mid = time.Now()
_, err := hasher.Generate(cmd.Context(), []byte("password"))
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate a hash: %s\n", err)
return 0, cmdx.FailSilently(cmd)
}
_, _ = progressPrinter.Printf(" took %s in try %d\n", time.Since(mid), i)
}
return time.Duration(int64(time.Since(start)) / int64(runs)), nil
}

View File

@@ -0,0 +1,83 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package argon2
import (
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/ory/kratos/hash"
"github.com/ory/x/cmdx"
"github.com/ory/x/configx"
"github.com/ory/x/flagx"
)
const (
FlagParallel = "parallel"
)
func newHashCmd() *cobra.Command {
flagConfig := &argon2Config{}
cmd := &cobra.Command{
Use: "hash <password1> [<password2> ...]",
Short: "Hash a list of passwords for benchmarking the hashing parameters",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
conf, err := configProvider(cmd, flagConfig)
if err != nil {
return err
}
flagConfig.ctx = cmd.Context()
hasher := hash.NewHasherArgon2(conf)
hashes := make([][]byte, len(args))
errs := make(chan error, len(args))
start := time.Now()
for i, pw := range args {
go func(i int, pw string) {
start := time.Now()
h, err := hasher.Generate(cmd.Context(), []byte(pw))
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "password %d: %s\n", i, time.Since(start))
hashes[i] = h
errs <- err
}(i, pw)
if !flagx.MustGetBool(cmd, FlagParallel) {
if err := <-errs; err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate hash: %s\n", err.Error())
return cmdx.FailSilently(cmd)
}
}
}
if flagx.MustGetBool(cmd, FlagParallel) {
for i := 0; i < len(args); i++ {
if err := <-errs; err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not generate hash: %s\n", err.Error())
return cmdx.FailSilently(cmd)
}
}
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "total: %s\n", time.Since(start))
return nil
},
}
flags := cmd.Flags()
flags.Bool(FlagParallel, false, "Run all hashing operations in parallel.")
registerArgon2ConfigFlags(flags, flagConfig)
configx.RegisterFlags(flags)
return cmd
}

View File

@@ -0,0 +1,269 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package argon2
import (
"context"
"fmt"
"math/rand"
"runtime"
"strconv"
"time"
"github.com/ory/x/flagx"
"github.com/fatih/color"
"github.com/inhies/go-bytesize"
"github.com/montanaflynn/stats"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"github.com/ory/kratos/hash"
"github.com/ory/x/cmdx"
"github.com/ory/x/configx"
)
type resultTable struct {
TotalTime time.Duration `json:"total_time"`
MedianTime time.Duration `json:"median_request_time"`
StdDev time.Duration `json:"std_deviation"`
MinTime time.Duration `json:"min_request_time"`
MaxTime time.Duration `json:"max_request_time"`
MaxMem bytesize.ByteSize `json:"mem_used"`
}
var (
ErrSampleTimeExceeded = fmt.Errorf("the sample time was exceeded")
ErrMemoryConsumptionExceeded = fmt.Errorf("the memory consumption was exceeded")
_ cmdx.TableRow = &resultTable{}
)
func (r *resultTable) Header() []string {
return []string{"TOTAL SAMPLE TIME", "MEDIAN REQUEST TIME", "STANDARD DEVIATION", "MIN REQUEST TIME", "MAX REQUEST TIME", "MEMOry USED"}
}
func (r *resultTable) Columns() []string {
return []string{
r.TotalTime.String(),
r.MedianTime.String(),
r.StdDev.String(),
r.MinTime.String(),
r.MaxTime.String(),
r.MaxMem.String(),
}
}
func (r *resultTable) Interface() interface{} {
return r
}
func newLoadTestCmd() *cobra.Command {
flagConf := &argon2Config{}
cmd := &cobra.Command{
Use: "load-test <authentication-requests-per-minute>",
Short: "Simulate the password hashing with a number of concurrent requests/minute.",
Long: "Simulates a number of concurrent authentication requests per minute. Gives statistical data about the measured performance and resource consumption. Can be used to tune and test the hashing parameters for peak demand situations.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
perMinute, err := strconv.ParseInt(args[0], 0, 0)
if err != nil {
return err
}
flagConf.ctx = cmd.Context()
conf, err := configProvider(cmd, flagConf)
if err != nil {
return err
}
if !flagx.MustGetBool(cmd, cmdx.FlagQuiet) {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "The hashing configuration used is:")
cmdx.PrintRow(cmd, conf)
}
res, err := runLoadTest(cmd, conf, int(perMinute))
if err != nil {
return err
}
cmdx.PrintRow(cmd, res)
return nil
},
}
registerArgon2ConfigFlags(cmd.Flags(), flagConf)
configx.RegisterFlags(cmd.Flags())
cmdx.RegisterFormatFlags(cmd.Flags())
return cmd
}
func runLoadTest(cmd *cobra.Command, conf *argon2Config, reqPerMin int) (*resultTable, error) {
// force GC at the start of the experiment
runtime.GC()
sampleTime := time.Minute / 3
reqNum := reqPerMin / int(time.Minute/sampleTime)
progressPrinter := cmdx.NewLoudErrPrinter(cmd)
_, _ = progressPrinter.Printf("It takes about %s to collect all necessary data, please be patient.\n", sampleTime)
ctx, cancel := context.WithCancel(cmd.Context())
hasher := hash.NewHasherArgon2(conf)
allDone := make(chan struct{})
startAll := time.Now()
var cancelReason error
var memStats []uint64
go func() {
clock := time.NewTicker(time.Second)
defer func() {
clock.Stop()
}()
for {
select {
case <-cmd.Context().Done():
return
case <-allDone:
return
case <-clock.C:
// cancel if the allowed time is exceeded by 110%
if time.Since(startAll) > ((sampleTime+conf.localConfig.ExpectedDuration+conf.localConfig.ExpectedDeviation)/100)*110 {
cancelReason = ErrSampleTimeExceeded
cancel()
return
}
ms := runtime.MemStats{}
runtime.ReadMemStats(&ms)
// cancel if memory is exceeded by 110%
if ms.HeapAlloc > (uint64(conf.localConfig.DedicatedMemory)/100)*110 {
cancelReason = ErrMemoryConsumptionExceeded
cancel()
return
}
memStats = append(memStats, ms.HeapAlloc)
}
}
}()
go func() {
// don't read std_in when quiet
if flagx.MustGetBool(cmd, cmdx.FlagQuiet) {
return
}
input := make([]byte, 1)
for {
n, err := cmd.InOrStdin().Read(input)
if err != nil {
return
}
if n != 0 {
_, _ = color.New(color.FgRed).Fprintln(cmd.ErrOrStderr(), "I SAID BE PATIENT!!!")
return
}
select {
case <-allDone:
return
case <-cmd.Context().Done():
return
case <-time.After(time.Millisecond):
}
}
}()
calcTimes := make([]time.Duration, reqNum)
eg, _ := errgroup.WithContext(ctx)
for i := 0; i < reqNum; i++ {
eg.Go(func(i int) func() error {
return func() error {
// wait randomly before starting, max. sample time
//#nosec G404 -- just a timeout to collect statistical data
t := time.Duration(rand.Intn(int(sampleTime)))
timer := time.NewTimer(t)
defer timer.Stop()
select {
case <-ctx.Done():
return nil
case <-timer.C:
}
start := time.Now()
done := make(chan struct{})
var err error
go func() {
_, err = hasher.Generate(ctx, []byte("password"))
close(done)
}()
select {
case <-ctx.Done():
return nil
case <-done:
if err != nil {
return err
}
calcTimes[i] = time.Since(start)
return nil
}
}
}(i))
}
if err := eg.Wait(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Error during hashing: %+v\n", err)
return nil, cmdx.FailSilently(cmd)
}
switch cancelReason {
case ErrSampleTimeExceeded:
memUsed, err2 := stats.LoadRawData(memStats).Max()
if err2 != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unexpected maths error: %+v\nRaw Data: %+v\n", cancelReason, memStats)
}
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "The hashing load test took too long. This indicates that you don't have enough resources to handle %d login requests per minute with the desired minimal time of %s. The memory used was %s. Either dedicate more CPU/memory, or decrease the hashing cost (memory and iterations parameters).\n", reqPerMin, conf.localConfig.ExpectedDuration, bytesize.ByteSize(memUsed))
return nil, cmdx.FailSilently(cmd)
case ErrMemoryConsumptionExceeded:
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "The hashing load test exceeded the memory limit of %s. This indicates that you don't have enough resources to handle %d login requests per minute with the desired minimal time of %s. Either dedicate more memory, or decrease the hashing cost (memory and iterations parameters).\n", conf.localConfig.DedicatedMemory, reqPerMin, conf.localConfig.ExpectedDuration)
return nil, cmdx.FailSilently(cmd)
}
totalTime := time.Since(startAll)
close(allDone)
calcData := stats.LoadRawData(calcTimes)
duration := func(f func() (float64, error)) time.Duration {
v, err := f()
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unexpected maths error: %+v\nRaw Data: %+v\n", err, calcTimes)
}
return time.Duration(int64(v))
}
memUsed, err := stats.LoadRawData(memStats).Max()
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unexpected maths error: %+v\nRaw Data: %+v\n", err, memStats)
}
return &resultTable{
TotalTime: totalTime,
MedianTime: duration(calcData.Mean),
StdDev: duration(calcData.StandardDeviation),
MinTime: duration(calcData.Min),
MaxTime: duration(calcData.Max),
MaxMem: bytesize.ByteSize(memUsed),
}, nil
}

177
cmd/hashers/argon2/root.go Normal file
View File

@@ -0,0 +1,177 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package argon2
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/ory/x/contextx"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/cmdx"
"github.com/ory/x/configx"
"github.com/ory/x/logrusx"
)
const (
FlagIterations = "iterations"
FlagParallelism = "parallelism"
FlagSaltLength = "salt-length"
FlagKeyLength = "key-length"
FlagMemory = "memory"
FlagDedicatedMemory = "dedicated-memory"
FlagMinimalDuration = "min-duration"
FlagExpectedDeviation = "expected-deviation"
)
var rootCmd = &cobra.Command{
Use: "argon2",
}
func RegisterCommandRecursive(parent *cobra.Command) {
parent.AddCommand(rootCmd)
rootCmd.AddCommand(newCalibrateCmd(), newHashCmd(), newLoadTestCmd())
}
func registerArgon2ConstantConfigFlags(flags *pflag.FlagSet, c *argon2Config) {
// set default value first
c.localConfig.DedicatedMemory = config.Argon2DefaultDedicatedMemory
flags.Var(&c.localConfig.DedicatedMemory, FlagDedicatedMemory, "Amount of memory dedicated for password hashing. Kratos will try to not consume more memory.")
flags.DurationVar(&c.localConfig.ExpectedDuration, FlagMinimalDuration, config.Argon2DefaultDuration, "Minimal duration a hashing operation (~login request) takes.")
flags.DurationVar(&c.localConfig.ExpectedDeviation, FlagExpectedDeviation, config.Argon2DefaultDeviation, "Expected deviation of the time a hashing operation (~login request) takes.")
flags.Uint8Var(&c.localConfig.Parallelism, FlagParallelism, config.Argon2DefaultParallelism, "Number of threads to use.")
flags.Uint32Var(&c.localConfig.SaltLength, FlagSaltLength, config.Argon2DefaultSaltLength, "Length of the salt in bytes.")
flags.Uint32Var(&c.localConfig.KeyLength, FlagKeyLength, config.Argon2DefaultKeyLength, "Length of the key in bytes.")
}
func registerArgon2ConfigFlags(flags *pflag.FlagSet, c *argon2Config) {
flags.Uint32Var(&c.localConfig.Iterations, FlagIterations, 1, "Number of iterations to start probing at.")
// set default value first
c.localConfig.Memory = config.Argon2DefaultMemory
flags.Var(&c.localConfig.Memory, FlagMemory, "Memory to use.")
registerArgon2ConstantConfigFlags(flags, c)
}
func configProvider(cmd *cobra.Command, flagConf *argon2Config) (*argon2Config, error) {
l := logrusx.New("Ory Kratos", config.Version)
conf := &argon2Config{}
var err error
conf.config, err = config.New(
cmd.Context(),
l,
cmd.ErrOrStderr(),
&contextx.Default{},
configx.WithFlags(cmd.Flags()),
configx.SkipValidation(),
configx.WithContext(cmd.Context()),
configx.WithImmutables("hashers"),
)
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to initialize the config provider: %s\n", err)
return nil, cmdx.FailSilently(cmd)
}
conf.localConfig = *conf.config.HasherArgon2(cmd.Context())
if cmd.Flags().Changed(FlagIterations) {
conf.localConfig.Iterations = flagConf.localConfig.Iterations
}
if cmd.Flags().Changed(FlagParallelism) {
conf.localConfig.Parallelism = flagConf.localConfig.Parallelism
}
if cmd.Flags().Changed(FlagMemory) {
conf.localConfig.Memory = flagConf.localConfig.Memory
}
if cmd.Flags().Changed(FlagDedicatedMemory) {
conf.localConfig.DedicatedMemory = flagConf.localConfig.DedicatedMemory
}
if cmd.Flags().Changed(FlagKeyLength) {
conf.localConfig.KeyLength = flagConf.localConfig.KeyLength
}
if cmd.Flags().Changed(FlagSaltLength) {
conf.localConfig.SaltLength = flagConf.localConfig.SaltLength
}
if cmd.Flags().Changed(FlagExpectedDeviation) {
conf.localConfig.ExpectedDeviation = flagConf.localConfig.ExpectedDeviation
}
if cmd.Flags().Changed(FlagMinimalDuration) {
conf.localConfig.ExpectedDuration = flagConf.localConfig.ExpectedDuration
}
return conf, nil
}
type (
argon2Config struct {
localConfig config.Argon2
config *config.Config
ctx context.Context
}
)
var _ cmdx.TableRow = (*argon2Config)(nil)
func (c *argon2Config) Header() []string {
var header []string
t := reflect.TypeOf(c.localConfig)
for i := 0; i < t.NumField(); i++ {
header = append(header, strings.ReplaceAll(strings.ToUpper(t.Field(i).Tag.Get("json")), "_", " "))
}
return header
}
func (c *argon2Config) Columns() []string {
conf, _ := c.HasherArgon2()
return []string{
conf.Memory.String(),
fmt.Sprintf("%d", conf.Iterations),
fmt.Sprintf("%d", conf.Parallelism),
fmt.Sprintf("%d", conf.SaltLength),
fmt.Sprintf("%d", conf.KeyLength),
conf.ExpectedDuration.String(),
conf.ExpectedDeviation.String(),
conf.DedicatedMemory.String(),
}
}
func (c *argon2Config) Interface() interface{} {
i, _ := c.HasherArgon2()
return i
}
func (c *argon2Config) Config() *config.Config {
ac, _ := c.HasherArgon2()
for k, v := range map[string]interface{}{
config.ViperKeyHasherArgon2ConfigIterations: ac.Iterations,
config.ViperKeyHasherArgon2ConfigMemory: ac.Memory,
config.ViperKeyHasherArgon2ConfigParallelism: ac.Parallelism,
config.ViperKeyHasherArgon2ConfigDedicatedMemory: ac.DedicatedMemory,
config.ViperKeyHasherArgon2ConfigKeyLength: ac.KeyLength,
config.ViperKeyHasherArgon2ConfigSaltLength: ac.SaltLength,
config.ViperKeyHasherArgon2ConfigExpectedDuration: ac.ExpectedDuration,
config.ViperKeyHasherArgon2ConfigExpectedDeviation: ac.ExpectedDeviation,
} {
_ = c.config.Set(c.ctx, k, v)
}
return c.config
}
func (c *argon2Config) HasherArgon2() (*config.Argon2, error) {
if c.localConfig.Memory == 0 {
c.localConfig.Memory = config.Argon2DefaultMemory
}
return &c.localConfig, nil
}

25
cmd/hashers/root.go Normal file
View File

@@ -0,0 +1,25 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hashers
import (
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/hashers/argon2"
)
func NewRootCmd() *cobra.Command {
c := &cobra.Command{
Use: "hashers",
Short: "This command contains helpers around hashing",
}
return c
}
func RegisterCommandRecursive(parent *cobra.Command) {
rootCmd := NewRootCmd()
parent.AddCommand(rootCmd)
argon2.RegisterCommandRecursive(rootCmd)
}

View File

@@ -0,0 +1,90 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"strings"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/x/cmdx"
)
type (
outputIdentity kratos.Identity
outputIdentityCollection struct {
Identities []kratos.Identity `json:"identities"`
NextPageToken string `json:"next_page_token"`
includePageToken bool
}
)
func (outputIdentity) Header() []string {
return []string{"ID", "EXTERNAL ID", "VERIFIED ADDRESSES", "RECOVERY ADDRESSES", "SCHEMA ID", "SCHEMA URL"}
}
func (i outputIdentity) Columns() []string {
data := [6]string{
i.Id,
cmdx.None,
cmdx.None,
cmdx.None,
cmdx.None,
cmdx.None,
}
if i.ExternalId != nil && len(*i.ExternalId) > 0 {
data[1] = *i.ExternalId
}
addresses := make([]string, 0, len(i.VerifiableAddresses))
for _, a := range i.VerifiableAddresses {
if len(a.Value) > 0 {
addresses = append(addresses, a.Value)
}
}
data[2] = strings.Join(addresses, ", ")
addresses = addresses[:0]
for _, a := range i.RecoveryAddresses {
if len(a.Value) > 0 {
addresses = append(addresses, a.Value)
}
}
data[3] = strings.Join(addresses, ", ")
data[4] = i.SchemaId
data[5] = i.SchemaUrl
return data[:]
}
func (i outputIdentity) Interface() interface{} {
return i
}
func (outputIdentityCollection) Header() []string {
return outputIdentity{}.Header()
}
func (c outputIdentityCollection) Table() [][]string {
rows := make([][]string, len(c.Identities))
for i, ident := range c.Identities {
rows[i] = outputIdentity(ident).Columns()
}
return append(rows,
[]string{""},
[]string{"NEXT PAGE TOKEN", c.NextPageToken},
)
}
func (c outputIdentityCollection) Interface() interface{} {
if c.includePageToken {
return c
}
return c.Identities
}
func (c *outputIdentityCollection) Len() int {
return len(c.Identities)
}

72
cmd/identities/delete.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"fmt"
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/cliclient"
"github.com/ory/kratos/pkg/clihelpers"
"github.com/ory/x/cmdx"
)
func NewDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Delete resources",
}
cmd.AddCommand(NewDeleteIdentityCmd())
cliclient.RegisterClientFlags(cmd.PersistentFlags())
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
return cmd
}
func NewDeleteIdentityCmd() *cobra.Command {
return &cobra.Command{
Use: "identity id-0 [id-1] [id-2] [id-n]",
Short: "Delete one or more identities by their ID(s)",
Long: fmt.Sprintf(`This command deletes one or more identities by ID. To delete an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
%s`, clihelpers.WarningJQIsComplicated),
Example: `To delete the identity with the recovery email address "foo@bar.com", run:
{{ .CommandPath }} $({{ .Root.Name }} list identities --format json | jq -r 'map(select(.recovery_addresses[].value == "foo@bar.com")) | .[].id')`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
var (
deleted = make([]cmdx.OutputIder, 0, len(args))
failed = make(map[string]error)
)
for _, a := range args {
_, err := c.IdentityAPI.DeleteIdentity(cmd.Context(), a).Execute()
if err != nil {
failed[a] = cmdx.PrintOpenAPIError(cmd, err)
continue
}
deleted = append(deleted, cmdx.OutputIder(a))
}
if len(deleted) == 1 {
cmdx.PrintRow(cmd, &deleted[0])
} else if len(deleted) > 1 {
cmdx.PrintTable(cmd, &cmdx.OutputIderCollection{Items: deleted})
}
cmdx.PrintErrors(cmd, failed)
if len(failed) != 0 {
return cmdx.FailSilently(cmd)
}
return nil
},
}
}

View File

@@ -0,0 +1,61 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities_test
import (
"context"
"errors"
"strings"
"testing"
"github.com/tidwall/gjson"
"github.com/ory/kratos/cmd/identities"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/x"
"github.com/ory/x/sqlcon"
)
func TestDeleteCmd(t *testing.T) {
reg, cmd := setup(t, identities.NewDeleteIdentityCmd)
t.Run("case=deletes successfully", func(t *testing.T) {
// create identity to delete
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
stdOut := cmd.ExecNoErr(t, i.ID.String())
// expect ID and no error
assert.Equal(t, i.ID.String(), gjson.Parse(stdOut).String())
// expect identity to be deleted
_, err := reg.Persister().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
assert.True(t, errors.Is(err, sqlcon.ErrNoRows()))
})
t.Run("case=deletes three identities", func(t *testing.T) {
is, ids := makeIdentities(t, reg, 3)
stdOut := cmd.ExecNoErr(t, ids...)
assert.Equal(t, `["`+strings.Join(ids, "\",\"")+"\"]\n", stdOut)
for _, i := range is {
_, err := reg.Persister().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
assert.Error(t, err)
}
})
t.Run("case=fails with unknown ID", func(t *testing.T) {
stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String())
assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
})
}

116
cmd/identities/get.go Normal file
View File

@@ -0,0 +1,116 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"fmt"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/x/cmdx"
"github.com/ory/x/stringsx"
"github.com/ory/kratos/pkg/clihelpers"
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/cliclient"
)
const (
FlagIncludeCreds = "include-credentials"
FlagExternalID = "external-id"
)
func NewGetCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "Get resources",
}
cmd.AddCommand(NewGetIdentityCmd())
cliclient.RegisterClientFlags(cmd.PersistentFlags())
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
return cmd
}
func NewGetIdentityCmd() *cobra.Command {
var (
includeCreds []string
byExternalID bool
)
cmd := &cobra.Command{
Use: "identity [id-1] [id-2] [id-n]",
Short: "Get one or more identities by their ID(s)",
Long: fmt.Sprintf(`This command gets all the details about an identity. To get an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
Use the --%s flag to look up identities by their external ID instead of their Ory-internal ID.
%s`, FlagExternalID, clihelpers.WarningJQIsComplicated),
Example: `To get the identities with the recovery email address at the domain "ory.sh", run:
{{ .CommandPath }} $({{ .Root.Name }} ls identities --format json | jq -r 'map(select(.recovery_addresses[].value | endswith("@ory.sh"))) | .[].id')
To get an identity by its external ID, run:
{{ .CommandPath }} --external-id customer-12345`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
// we check includeCreds argument is valid
for _, opt := range includeCreds {
e := stringsx.SwitchExact(opt)
if !e.AddCase("oidc") {
cmd.PrintErrln(`You have to put a valid value of credentials type to be included, try --help for details.`)
return cmdx.FailSilently(cmd)
}
}
identities := make([]kratos.Identity, 0, len(args))
failed := make(map[string]error)
for _, id := range args {
var identity *kratos.Identity
if byExternalID {
identity, _, err = c.IdentityAPI.
GetIdentityByExternalID(cmd.Context(), id).
IncludeCredential(includeCreds).
Execute()
} else {
identity, _, err = c.IdentityAPI.
GetIdentity(cmd.Context(), id).
IncludeCredential(includeCreds).
Execute()
}
if clihelpers.SDKError(err) != nil {
failed[id] = cmdx.PrintOpenAPIError(cmd, err)
continue
}
identities = append(identities, *identity)
}
if len(identities) == 1 {
cmdx.PrintRow(cmd, (*outputIdentity)(&identities[0]))
} else if len(identities) > 1 {
cmdx.PrintTable(cmd, &outputIdentityCollection{Identities: identities, includePageToken: false})
}
cmdx.PrintErrors(cmd, failed)
if len(failed) != 0 {
return cmdx.FailSilently(cmd)
}
return nil
},
}
flags := cmd.Flags()
// include credential flag to add third party tokens in returned data
flags.StringArrayVarP(&includeCreds, FlagIncludeCreds, "i", []string{}, `Include third party tokens (only "oidc" supported) `)
flags.BoolVar(&byExternalID, FlagExternalID, false, "Treat the provided ID(s) as external IDs and look up the identities by their external ID instead of their Ory-internal ID.")
return cmd
}

132
cmd/identities/get_test.go Normal file
View File

@@ -0,0 +1,132 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities_test
import (
"context"
"encoding/json"
"testing"
"github.com/ory/kratos/cmd/identities"
"github.com/ory/x/assertx"
"github.com/ory/x/sqlxx"
"github.com/ory/kratos/x"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
)
func TestGetCmd(t *testing.T) {
reg, cmd := setup(t, identities.NewGetIdentityCmd)
t.Run("case=gets a single identity", func(t *testing.T) {
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.MetadataPublic = []byte(`"public"`)
i.MetadataAdmin = []byte(`"admin"`)
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
stdOut := cmd.ExecNoErr(t, i.ID.String())
ij, err := json.Marshal(identity.WithCredentialsNoConfigAndAdminMetadataInJSON(*i))
require.NoError(t, err)
assertx.EqualAsJSONExcept(t, json.RawMessage(ij), json.RawMessage(stdOut), []string{"created_at", "updated_at", "AdditionalProperties"})
})
t.Run("case=gets three identities", func(t *testing.T) {
is, ids := makeIdentities(t, reg, 3)
stdOut := cmd.ExecNoErr(t, ids...)
isj, err := json.Marshal(is)
require.NoError(t, err)
assertx.EqualAsJSONExcept(t, json.RawMessage(isj), json.RawMessage(stdOut), []string{"created_at", "updated_at", "AdditionalProperties"})
})
t.Run("case=fails with unknown ID", func(t *testing.T) {
stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String())
assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
})
t.Run("case=gets a single identity by its external ID", func(t *testing.T) {
externalID := "customer-" + x.NewUUID().String()
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.ExternalID = sqlxx.NullString(externalID)
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
stdOut := cmd.ExecNoErr(t, "--"+identities.FlagExternalID, externalID)
assert.Equal(t, i.ID.String(), gjson.Get(stdOut, "id").String())
assert.Equal(t, externalID, gjson.Get(stdOut, "external_id").String())
})
t.Run("case=fails with unknown external ID", func(t *testing.T) {
stdErr := cmd.ExecExpectedErr(t, "--"+identities.FlagExternalID, "does-not-exist")
assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
})
t.Run("case=gets a single identity with oidc credentials", func(t *testing.T) {
applyCredentials := func(identifier, accessToken, refreshToken, idToken string, encrypt bool) identity.Credentials {
toJson := func(c identity.CredentialsOIDC) []byte {
out, err := json.Marshal(&c)
require.NoError(t, err)
return out
}
transform := func(token string) string {
if encrypt {
s, err := reg.Cipher(context.Background()).Encrypt(context.Background(), []byte(token))
require.NoError(t, err)
return s
}
return token
}
return identity.Credentials{
Type: identity.CredentialsTypeOIDC,
Identifiers: []string{"bar:" + identifier},
Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{
{
Subject: "foo",
Provider: "bar",
InitialAccessToken: transform(accessToken + "0"),
InitialRefreshToken: transform(refreshToken + "0"),
InitialIDToken: transform(idToken + "0"),
Organization: "foo-org-id",
},
{
Subject: "baz",
Provider: "zab",
InitialAccessToken: transform(accessToken + "1"),
InitialRefreshToken: transform(refreshToken + "1"),
InitialIDToken: transform(idToken + "1"),
Organization: "bar-org-id",
},
}}),
}
}
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.MetadataPublic = []byte(`"public"`)
i.MetadataAdmin = []byte(`"admin"`)
i.SetCredentials(identity.CredentialsTypeOIDC, applyCredentials("uniqueIdentifier", "accessBar", "refreshBar", "idBar", true))
// duplicate identity with decrypted tokens
di := i.CopyWithoutCredentials()
di.SetCredentials(identity.CredentialsTypeOIDC, applyCredentials("uniqueIdentifier", "accessBar", "refreshBar", "idBar", false))
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
stdOut := cmd.ExecNoErr(t, "--"+identities.FlagIncludeCreds, "oidc", i.ID.String())
ij, err := json.Marshal(identity.WithCredentialsAndAdminMetadataInJSON(*di))
require.NoError(t, err)
ii := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at", "credentials.oidc.created_at", "credentials.oidc.updated_at", "credentials.oidc.version", "AdditionalProperties"}
assertx.EqualAsJSONExcept(t, json.RawMessage(ij), json.RawMessage(stdOut), ii)
})
}

53
cmd/identities/helpers.go Normal file
View File

@@ -0,0 +1,53 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/tidwall/gjson"
"github.com/ory/x/cmdx"
)
func parseIdentities(raw []byte) (rawIdentities []string) {
res := gjson.ParseBytes(raw)
if !res.IsArray() {
return []string{res.Raw}
}
res.ForEach(func(_, v gjson.Result) bool {
rawIdentities = append(rawIdentities, v.Raw)
return true
})
return
}
func readIdentities(cmd *cobra.Command, args []string) (map[string]string, error) {
rawIdentities := make(map[string]string)
if len(args) == 0 {
fc, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "STD_IN: Could not read: %s\n", err)
return nil, cmdx.FailSilently(cmd)
}
for i, id := range parseIdentities(fc) {
rawIdentities[fmt.Sprintf("STD_IN[%d]", i)] = id
}
return rawIdentities, nil
}
for _, fn := range args {
fc, err := os.ReadFile(fn) // #nosec G304 -- file is supplied by user
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not open identity file: %s\n", fn, err)
return nil, cmdx.FailSilently(cmd)
}
for i, id := range parseIdentities(fc) {
rawIdentities[fmt.Sprintf("%s[%d]", fn, i)] = id
}
}
return rawIdentities, nil
}

View File

@@ -0,0 +1,49 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities_test
import (
"context"
"testing"
"github.com/ory/x/cmdx"
"github.com/ory/kratos/identity"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/cmd/cliclient"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
)
func setup(t *testing.T, newCmd func() *cobra.Command) (*driver.RegistryDefault, *cmdx.CommandExecuter) {
conf, reg := pkg.NewFastRegistryWithMocks(t)
_, admin := testhelpers.NewKratosServerWithCSRF(t, reg)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stubs/identity.schema.json")
// setup command
return reg, &cmdx.CommandExecuter{
New: func() *cobra.Command {
cmd := newCmd()
cliclient.RegisterClientFlags(cmd.Flags())
cmdx.RegisterFormatFlags(cmd.Flags())
return cmd
},
PersistentArgs: []string{"--" + cliclient.FlagEndpoint, admin.URL, "--" + cmdx.FlagFormat, string(cmdx.FormatJSON)},
}
}
func makeIdentities(t *testing.T, reg driver.Registry, n int) (is []*identity.Identity, ids []string) {
for j := 0; j < n; j++ {
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.MetadataPublic = []byte(`{"foo":"bar"}`)
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
is = append(is, i)
ids = append(ids, i.ID.String())
}
return
}

98
cmd/identities/import.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"encoding/json"
"fmt"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/x/cmdx"
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/cliclient"
)
func NewImportCmd() *cobra.Command {
var cmd = &cobra.Command{
Use: "import",
Short: "Import resources",
}
cmd.AddCommand(NewImportIdentitiesCmd())
cliclient.RegisterClientFlags(cmd.PersistentFlags())
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
return cmd
}
// NewImportIdentitiesCmd represents the import command
func NewImportIdentitiesCmd() *cobra.Command {
return &cobra.Command{
Use: "identities file-1.json [file-2.json] [file-3.json] [file-n.json]",
Short: "Import one or more identities from files or STD_IN",
Example: `Create an example identity:
cat > ./file.json <<EOF
{
"schema_id": "default",
"traits": {
"email": "foo@example.com"
}
}
EOF
{{ .CommandPath }} file.json
Alternatively:
cat file.json | {{ .CommandPath }}`,
Long: `Import identities from files or STD_IN.
Files can contain only a single or an array of identities. The validity of files can be tested beforehand using "... identities validate".`,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
imported := make([]kratos.Identity, 0, len(args))
failed := make(map[string]error)
is, err := readIdentities(cmd, args)
if err != nil {
return err
}
for src, i := range is {
var params kratos.CreateIdentityBody
err = json.Unmarshal([]byte(i), &params)
if err != nil {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "STD_IN: Could not parse identity")
return cmdx.FailSilently(cmd)
}
ident, _, err := c.IdentityAPI.CreateIdentity(cmd.Context()).CreateIdentityBody(params).Execute()
if err != nil {
failed[src] = cmdx.PrintOpenAPIError(cmd, err)
} else {
imported = append(imported, *ident)
}
}
if len(imported) == 1 {
cmdx.PrintRow(cmd, (*outputIdentity)(&imported[0]))
} else {
cmdx.PrintTable(cmd, &outputIdentityCollection{Identities: imported})
}
cmdx.PrintErrors(cmd, failed)
if len(failed) != 0 {
return cmdx.FailSilently(cmd)
}
return nil
},
}
}

View File

@@ -0,0 +1,150 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities_test
import (
"bytes"
"context"
"encoding/json"
"os"
"testing"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/cmd/identities"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
kratos "github.com/ory/kratos/pkg/httpclient"
)
func TestImportCmd(t *testing.T) {
reg, cmd := setup(t, identities.NewImportIdentitiesCmd)
t.Run("case=imports a new identity from file", func(t *testing.T) {
i := kratos.CreateIdentityBody{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
}
ij, err := json.Marshal(i)
require.NoError(t, err)
f, err := os.CreateTemp("", "")
require.NoError(t, err)
_, err = f.Write(ij)
require.NoError(t, err)
require.NoError(t, f.Close())
stdOut := cmd.ExecNoErr(t, f.Name())
id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
require.NoError(t, err)
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
assert.NoError(t, err)
})
t.Run("case=imports multiple identities from single file", func(t *testing.T) {
i := []kratos.CreateIdentityBody{
{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
},
{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
},
}
ij, err := json.Marshal(i)
require.NoError(t, err)
f, err := os.CreateTemp("", "")
require.NoError(t, err)
_, err = f.Write(ij)
require.NoError(t, err)
require.NoError(t, f.Close())
stdOut := cmd.ExecNoErr(t, f.Name())
id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String())
require.NoError(t, err)
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
assert.NoError(t, err)
id, err = uuid.FromString(gjson.Get(stdOut, "1.id").String())
require.NoError(t, err)
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
assert.NoError(t, err)
})
t.Run("case=imports a new identity from STD_IN", func(t *testing.T) {
i := []kratos.CreateIdentityBody{
{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
},
{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
},
}
ij, err := json.Marshal(i)
require.NoError(t, err)
stdOut, stdErr, err := cmd.Exec(bytes.NewBuffer(ij))
require.NoError(t, err, "%s %s", stdOut, stdErr)
id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String())
require.NoError(t, err)
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
assert.NoError(t, err)
id, err = uuid.FromString(gjson.Get(stdOut, "1.id").String())
require.NoError(t, err)
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
assert.NoError(t, err)
})
t.Run("case=imports multiple identities from STD_IN", func(t *testing.T) {
i := kratos.CreateIdentityBody{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
}
ij, err := json.Marshal(i)
require.NoError(t, err)
stdOut, stdErr, err := cmd.Exec(bytes.NewBuffer(ij))
require.NoError(t, err, "%s %s", stdOut, stdErr)
id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
require.NoError(t, err)
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
assert.NoError(t, err)
})
t.Run("case=imports an identity with an external ID", func(t *testing.T) {
externalID := "customer-12345"
i := kratos.CreateIdentityBody{
SchemaId: config.DefaultIdentityTraitsSchemaID,
Traits: map[string]interface{}{},
ExternalId: kratos.PtrString(externalID),
}
ij, err := json.Marshal(i)
require.NoError(t, err)
stdOut, stdErr, err := cmd.Exec(bytes.NewBuffer(ij))
require.NoError(t, err, "%s %s", stdOut, stdErr)
// the external ID must be reflected in the command output
assert.Equal(t, externalID, gjson.Get(stdOut, "external_id").String())
// and it must be persisted on the identity
id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
require.NoError(t, err)
stored, err := reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
require.NoError(t, err)
assert.Equal(t, externalID, stored.ExternalID.String())
})
}

80
cmd/identities/list.go Normal file
View File

@@ -0,0 +1,80 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"fmt"
"github.com/spf13/cobra"
"github.com/ory/x/flagx"
"github.com/ory/x/pagination/keysetclient"
"github.com/ory/kratos/cmd/cliclient"
"github.com/ory/x/cmdx"
)
func NewListCmd() *cobra.Command {
c := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List resources",
}
c.AddCommand(NewListIdentitiesCmd())
cliclient.RegisterClientFlags(c.PersistentFlags())
cmdx.RegisterFormatFlags(c.PersistentFlags())
return c
}
func NewListIdentitiesCmd() *cobra.Command {
c := &cobra.Command{
Use: "identities",
Short: "List identities",
Long: `Return a list of identities.
The consistency defaults to ` + "`eventual`" + ` and can be set to ` + "`strong`" + ` or ` + "`eventual`" + `.
Eventual consistency means that the list operation will return faster and might not include recently created or updated identities. Replication lag is about 5 seconds.`,
Example: "{{ .CommandPath }} --page-size 100 --consistency eventual",
Args: func(cmd *cobra.Command, args []string) error {
// zero or exactly two args
if len(args) != 0 && len(args) != 2 {
return fmt.Errorf("expected zero or two args, got %d: %+v", len(args), args)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
consistency := flagx.MustGetString(cmd, "consistency")
req := c.IdentityAPI.ListIdentities(cmd.Context()).Consistency(consistency)
page, perPage, err := cmdx.ParseTokenPaginationArgs(cmd)
if err != nil {
return err
}
req = req.PageToken(page)
req = req.PageSize(int64(perPage))
identities, res, err := req.Execute()
if err != nil {
return cmdx.PrintOpenAPIError(cmd, err)
}
pages := keysetclient.ParseHeader(res)
cmdx.PrintTable(cmd, &outputIdentityCollection{
Identities: identities,
NextPageToken: pages.NextToken,
includePageToken: true,
})
return nil
},
}
c.Flags().String("consistency", "eventual", "The read consistency to use. Can be either \"strong\" or \"eventual\". Defaults to \"eventual\".")
cmdx.RegisterTokenPaginationFlags(c)
return c
}

View File

@@ -0,0 +1,66 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities_test
import (
"context"
"testing"
"github.com/tidwall/gjson"
"github.com/ory/kratos/cmd/identities"
"github.com/ory/x/cmdx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/identity"
)
func TestListCmd(t *testing.T) {
reg, cmd := setup(t, identities.NewListIdentitiesCmd)
var deleteIdentities = func(t *testing.T, is []*identity.Identity) {
for _, i := range is {
require.NoError(t, reg.Persister().DeleteIdentity(context.Background(), i.ID))
}
}
t.Run("case=lists all identities with default pagination", func(t *testing.T) {
is, ids := makeIdentities(t, reg, 5)
t.Cleanup(func() {
deleteIdentities(t, is)
})
stdOut := cmd.ExecNoErr(t, "--"+cmdx.FlagQuiet)
for _, i := range ids {
assert.Contains(t, stdOut, i)
}
})
t.Run("case=lists all identities with pagination", func(t *testing.T) {
is, ids := makeIdentities(t, reg, 10)
t.Cleanup(func() {
deleteIdentities(t, is)
})
first := cmd.ExecNoErr(t, "--format", "json-pretty", "--page-size", "2")
nextPageToken := gjson.Get(first, "next_page_token").String()
actualIDs := gjson.Get(first, "identities.#.id").Array()
for nextPageToken != "" {
next := cmd.ExecNoErr(t, "--page-size", "2", "--page-token", nextPageToken)
actualIDs = append(actualIDs, gjson.Get(next, "identities.#.id").Array()...)
nextPageToken = gjson.Get(next, "next_page_token").String()
}
actualIDsString := make([]string, len(actualIDs))
for i, id := range actualIDs {
actualIDsString[i] = id.Str
}
assert.ElementsMatch(t, ids, actualIDsString)
})
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"traits": {
"additionalProperties": false,
"type": "object",
"properties": {
"testKey": {
"type": "string"
}
}
}
}
}

152
cmd/identities/validate.go Normal file
View File

@@ -0,0 +1,152 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/ory/kratos/spec"
"github.com/ory/x/jsonschemax"
"github.com/ory/x/cmdx"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/tidwall/gjson"
"github.com/ory/jsonschema/v3"
"github.com/ory/kratos/cmd/cliclient"
)
func NewValidateCmd() *cobra.Command {
var cmd = &cobra.Command{
Use: "validate",
Short: "Validate resources",
}
cmd.AddCommand(NewValidateIdentityCmd())
cliclient.RegisterClientFlags(cmd.PersistentFlags())
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
return cmd
}
func NewValidateIdentityCmd() *cobra.Command {
var c = &cobra.Command{
Use: "identity file.json [file-2.json] [file-3.json] [file-n.json]",
Short: "Validate local identity files",
Long: `This command allows validation of identity files.
It validates against the payload of the API and the identity schema as configured in Ory Kratos.
Identities can be supplied via STD_IN or JSON files containing a single or an array of identities.`,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
is, err := readIdentities(cmd, args)
if err != nil {
return err
}
for src, i := range is {
err = ValidateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) {
return c.IdentityAPI.GetIdentitySchema(ctx, id).Execute()
})
if err != nil {
return err
}
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "All identity files are valid.")
return nil
},
}
return c
}
var schemas = make(map[string]*jsonschema.Schema)
const createIdentityPath = "api.json#/components/schemas/createIdentityBody"
type SchemaGetter = func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error)
// ValidateIdentity validates the json payload fc against
// 1. the swagger payload definition and
// 2. the remote custom identity schema.
func ValidateIdentity(cmd *cobra.Command, src, i string, getRemoteSchema SchemaGetter) error {
swaggerSchema, ok := schemas[createIdentityPath]
if !ok {
// add swagger schema
schemaCompiler := jsonschema.NewCompiler()
err := schemaCompiler.AddResource("api.json", bytes.NewReader(spec.API))
if err != nil {
return errors.Wrap(err, "Could not add swagger schema to the schema compiler. This is an error with the binary you use and should be reported. Thanks ;)")
}
// compile swagger payload definition
swaggerSchema, err = schemaCompiler.Compile(cmd.Context(), createIdentityPath)
if err != nil {
return errors.Wrap(err, "Could not compile the identity schema. This is an error with the binary you use and should be reported. Thanks ;)")
}
// force additional properties to false because swagger does not render this
swaggerSchema.AdditionalProperties = false
schemas[createIdentityPath] = swaggerSchema
}
// validate against swagger definition
var foundValidationErrors bool
err := swaggerSchema.Validate(bytes.NewBufferString(i))
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: not valid\n", src)
jsonschemax.FormatValidationErrorForCLI(cmd.ErrOrStderr(), []byte(i), err)
foundValidationErrors = true
}
// get custom identity schema id
sid := gjson.Get(i, "schema_id")
if !sid.Exists() {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), `%s: Expected key "schema_id" to be defined in identity file`, src)
return cmdx.FailSilently(cmd)
}
customSchema, ok := schemas[sid.String()]
if !ok {
ts, _, err := getRemoteSchema(cmd.Context(), sid.String())
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not fetch schema with ID \"%s\": %s\n", src, sid.String(), err)
return cmdx.FailSilently(cmd)
}
sf, err := json.Marshal(ts)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("%s: Could not marshal the traits schema. This usually means there is a problem with your upstream service as it served an invalid response.", src))
}
// compile custom identity schema
customSchema, err = jsonschema.CompileString(cmd.Context(), "identity_traits.schema.json", string(sf))
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not compile the traits schema: %s\n", src, err)
return cmdx.FailSilently(cmd)
}
schemas[sid.String()] = customSchema
}
// validate against custom identity schema
err = customSchema.Validate(bytes.NewBufferString(i))
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: not valid\n", src)
jsonschemax.FormatValidationErrorForCLI(cmd.ErrOrStderr(), []byte(i), err)
foundValidationErrors = true
}
if foundValidationErrors {
return cmdx.FailSilently(cmd)
}
return nil
}

View File

@@ -0,0 +1,78 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identities_test
import (
"bytes"
"context"
"fmt"
"net/http"
"testing"
"github.com/ory/kratos/cmd/identities"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
func TestValidateIdentity(t *testing.T) {
var testCmd = func() (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
out, err := &bytes.Buffer{}, &bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(out)
cmd.SetErr(err)
return cmd, out, err
}
var testSchemaGetter = func(resp map[string]interface{}) identities.SchemaGetter {
return func(_ context.Context, _ string) (map[string]interface{}, *http.Response, error) {
return resp, nil, nil
}
}
for i, tc := range []struct {
description string
payload string
expectedRequiredError string
identitySchema map[string]interface{}
}{
{
description: "requires schema_id from swagger schema",
payload: "{}",
expectedRequiredError: "schema_id",
},
{
description: "requires traits from swagger schema",
payload: `{"schema_id": "case2"}`,
expectedRequiredError: "traits",
},
{
description: "requires custom trait key",
payload: `{"schema_id": "case3", "traits": {}}`,
expectedRequiredError: "random-property",
identitySchema: map[string]interface{}{
"type": "object",
"required": []string{"random-property"},
"properties": map[string]interface{}{
"random-property": map[string]interface{}{
"type": "string",
},
},
},
},
} {
t.Run(fmt.Sprintf("case=%d/description=%s", i, tc.description), func(t *testing.T) {
cmd, stdOut, stdErr := testCmd()
if tc.identitySchema == nil {
tc.identitySchema = map[string]interface{}{}
}
err := identities.ValidateIdentity(cmd, "test identity", tc.payload, testSchemaGetter(tc.identitySchema))
assert.Error(t, err, stdOut.String(), stdErr.String())
assert.Len(t, stdOut.String(), 0, stdErr.String())
assert.Contains(t, stdErr.String(), "required", stdOut.String())
assert.Contains(t, stdErr.String(), tc.expectedRequiredError, stdOut.String())
})
}
}

61
cmd/jsonnet/format.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package jsonnet
import (
"fmt"
"os"
"path/filepath"
"github.com/google/go-jsonnet/formatter"
"github.com/spf13/cobra"
"github.com/ory/x/cmdx"
"github.com/ory/x/flagx"
)
func NewFormatCmd() *cobra.Command {
c := &cobra.Command{
Use: "format",
Short: "Helpers for formatting code",
}
c.AddCommand(NewJsonnetFormatCmd())
return c
}
func NewJsonnetFormatCmd() *cobra.Command {
c := &cobra.Command{
Use: "jsonnet path/to/files/*.jsonnet [more/files.jsonnet] [supports/**/{foo,bar}.jsonnet]",
Long: `Formats JSONNet files using the official JSONNet formatter.
Use -w or --write to write output back to files instead of stdout.
` + GlobHelp,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for _, pattern := range args {
files, err := filepath.Glob(pattern)
cmdx.Must(err, `Glob path "%s" is not valid: %s`, pattern, err)
shouldWrite := flagx.MustGetBool(cmd, "write")
for _, file := range files {
content, err := os.ReadFile(file) // #nosec G304 -- the file is provided by the user
cmdx.Must(err, `Unable to read file "%s" because: %s`, file, err)
output, err := formatter.Format(file, string(content), formatter.DefaultOptions())
cmdx.Must(err, `JSONNet file "%s" could not be formatted: %s`, file, err)
if shouldWrite {
err := os.WriteFile(file, []byte(output), 0o644) //#nosec
cmdx.Must(err, `Could not write to file "%s" because: %s`, file, err)
} else {
fmt.Println(output)
}
}
}
},
}
c.Flags().BoolP("write", "w", false, "Write formatted output back to file.")
return c
}

57
cmd/jsonnet/lint.go Normal file
View File

@@ -0,0 +1,57 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package jsonnet
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/google/go-jsonnet/linter"
"github.com/spf13/cobra"
"github.com/ory/x/cmdx"
"github.com/ory/x/jsonnetsecure"
)
func NewLintCmd() *cobra.Command {
c := &cobra.Command{
Use: "lint",
Short: "Helpers for linting code",
}
c.AddCommand(NewJsonnetLintCmd())
return c
}
func NewJsonnetLintCmd() *cobra.Command {
return &cobra.Command{
Use: "lint path/to/files/*.jsonnet [more/files.jsonnet] [supports/**/{foo,bar}.jsonnet]",
Long: `Lints JSONNet files using the official JSONNet linter and exits with a status code of 1 when issues are detected.
` + GlobHelp,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
vm := jsonnetsecure.MakeInProcessVM()
for _, pattern := range args {
files, err := filepath.Glob(pattern)
cmdx.Must(err, `Glob path "%s" is not valid: %s`, pattern, err)
for _, file := range files {
content, err := os.ReadFile(file) // #nosec G304 -- file is supplied by user
cmdx.Must(err, `Unable to read file "%s" because: %s`, file, err)
var outBuilder strings.Builder
errorsFound := linter.LintSnippet(vm, &outBuilder, []linter.Snippet{{FileName: file, Code: string(content)}})
if errorsFound {
_, _ = fmt.Fprintf(os.Stderr, "Linter found issues.")
os.Exit(1)
}
}
}
},
}
}

29
cmd/jsonnet/root.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package jsonnet
const GlobHelp = `Glob Syntax:
pattern:
{ term }
term:
'*' matches any sequence of non-separator characters
'**' matches any sequence of characters
'?' matches any single non-separator character
'[' [ '!' ] { character-range } ']'
character class (must be non-empty)
'{' pattern-list '}'
pattern alternatives
c matches character c (c != '*', '**', '?', '\', '[', '{', '}')
'\' c matches character c
character-range:
c matches character c (c != '\\', '-', ']')
'\' c matches character c
lo '-' hi matches character c for lo <= c <= hi
pattern-list:
pattern { ',' pattern }
comma-separated (without spaces) patterns`

105
cmd/migrate/handler.go Normal file
View File

@@ -0,0 +1,105 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"fmt"
"github.com/pkg/errors"
"github.com/ory/x/popx"
"github.com/ory/x/contextx"
"github.com/ory/x/configx"
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/cmdx"
"github.com/ory/x/flagx"
)
type MigrateHandler struct{}
func NewMigrateHandler() *MigrateHandler {
return &MigrateHandler{}
}
func (h *MigrateHandler) getPersister(cmd *cobra.Command, args []string, opts []driver.RegistryOption) (driver.Registry, error) {
return getPersister(cmd, args, opts)
}
func getPersister(cmd *cobra.Command, args []string, opts []driver.RegistryOption) (d driver.Registry, err error) {
if flagx.MustGetBool(cmd, "read-from-env") {
d, err = driver.NewWithoutInit(
cmd.Context(),
cmd.ErrOrStderr(),
driver.WithConfigOptions(
configx.WithFlags(cmd.Flags()),
configx.SkipValidation(),
))
if err != nil {
return nil, err
}
if len(d.Config().DSN(cmd.Context())) == 0 {
fmt.Println(cmd.UsageString())
fmt.Println("")
fmt.Println("When using flag -e, environment variable DSN must be set")
return nil, cmdx.FailSilently(cmd)
}
} else {
if len(args) != 1 {
fmt.Println(cmd.UsageString())
return nil, cmdx.FailSilently(cmd)
}
d, err = driver.NewWithoutInit(
cmd.Context(),
cmd.ErrOrStderr(),
driver.WithConfigOptions(
configx.WithFlags(cmd.Flags()),
configx.SkipValidation(),
configx.WithValue(config.ViperKeyDSN, args[0]),
))
if err != nil {
return nil, err
}
}
err = d.Init(cmd.Context(), &contextx.Default{}, append(opts, driver.SkipNetworkInit)...)
if err != nil {
return nil, errors.Wrap(err, "an error occurred initializing migrations")
}
if err := popx.VerifyDialect(cmd.Context(), d.Persister().GetConnection(cmd.Context())); err != nil {
return nil, err
}
return d, nil
}
func (h *MigrateHandler) MigrateSQLDown(cmd *cobra.Command, args []string, opts ...driver.RegistryOption) error {
p, err := h.getPersister(cmd, args, opts)
if err != nil {
return err
}
return popx.MigrateSQLDown(cmd, p.Persister())
}
func (h *MigrateHandler) MigrateSQLStatus(cmd *cobra.Command, args []string, opts ...driver.RegistryOption) error {
p, err := h.getPersister(cmd, args, opts)
if err != nil {
return err
}
return popx.MigrateStatus(cmd, p.Persister())
}
func (h *MigrateHandler) MigrateSQLUp(cmd *cobra.Command, args []string, opts ...driver.RegistryOption) error {
p, err := h.getPersister(cmd, args, opts)
if err != nil {
return err
}
return popx.MigrateSQLUp(cmd, p.Persister())
}

View File

@@ -0,0 +1,55 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/x/configx"
)
func NewNormalizePhoneCmd(opts ...driver.RegistryOption) *cobra.Command {
c := &cobra.Command{
Use: "normalize-phone-numbers [database-url]",
Short: "Normalize phone numbers to E.164 format in the database",
Long: `Normalizes all phone numbers in identity_credential_identifiers,
identity_verifiable_addresses, and identity_recovery_addresses to E.164 format.
This command uses keyset pagination to iterate over the database in batches.
It is safe to run multiple times (idempotent) and can be interrupted and resumed
using the --start-after flag with the last ID printed in the progress output.
Run this command AFTER deploying the code changes that normalize phone numbers
on write, to ensure all legacy data is also normalized.
You can read in the database URL using the -e flag, for example:
export DSN=...
kratos migrate normalize-phone-numbers -e
### WARNING ###
Before running this command on an existing database, create a back up!
`,
RunE: func(cmd *cobra.Command, args []string) error {
err := NewNormalizePhoneHandler().NormalizePhoneNumbers(cmd, args, opts...)
if err != nil {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), err)
return err
}
return nil
},
}
configx.RegisterFlags(c.PersistentFlags())
c.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")
c.Flags().IntP("batch-size", "b", 1000, "Number of rows to process per batch")
c.Flags().Bool("dry-run", false, "If set, only report what would change without writing")
c.Flags().Duration("batch-delay", time.Second, "Delay between batches to reduce database load (e.g. 100ms, 1s)")
c.Flags().StringSlice("start-after", nil, "Resume after a table's last processed ID, e.g. --start-after credentials=<id> --start-after verifiable=<id>")
return c
}

View File

@@ -0,0 +1,221 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"fmt"
"regexp"
"strings"
"text/tabwriter"
"time"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/x"
"github.com/ory/pop/v6"
"github.com/ory/x/flagx"
)
// phonePattern matches values that look like phone numbers: + followed by at least one digit,
// possibly with spaces, dashes, or parens. This filters out OIDC subjects and other
// identifiers that happen to start with +.
var phonePattern = regexp.MustCompile(`^\+[\d\s\-().]+$`)
type NormalizePhoneHandler struct{}
func NewNormalizePhoneHandler() *NormalizePhoneHandler {
return &NormalizePhoneHandler{}
}
type normalizeStats struct {
scanned int
updated int
skipped int
errors int
}
func (h *NormalizePhoneHandler) NormalizePhoneNumbers(cmd *cobra.Command, args []string, opts ...driver.RegistryOption) error {
d, err := getPersister(cmd, args, opts)
if err != nil {
return err
}
conn := d.Persister().GetConnection(cmd.Context())
batchSize := flagx.MustGetInt(cmd, "batch-size")
dryRun := flagx.MustGetBool(cmd, "dry-run")
batchDelay := flagx.MustGetDuration(cmd, "batch-delay")
if dryRun {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Dry run mode enabled. No changes will be written.")
}
tables := []tableConfig{
{
key: "ci",
name: "Credential identifiers",
selectQuery: `
SELECT id, identifier AS value
FROM identity_credential_identifiers
WHERE identifier LIKE '+%'
AND id > ?
ORDER BY id ASC LIMIT ?`,
updateQuery: `UPDATE identity_credential_identifiers SET identifier = ?, updated_at = ? WHERE id = ? AND identifier = ?`,
},
{
key: "va",
name: "Verifiable addresses",
selectQuery: `
SELECT id, value
FROM identity_verifiable_addresses
WHERE via = 'sms'
AND id > ?
AND value LIKE '+%'
ORDER BY id ASC LIMIT ?`,
updateQuery: `UPDATE identity_verifiable_addresses SET value = ?, updated_at = ? WHERE id = ? AND value = ?`,
},
{
key: "ra",
name: "Recovery addresses",
selectQuery: `
SELECT id, value
FROM identity_recovery_addresses
WHERE via = 'sms'
AND id > ?
AND value LIKE '+%'
ORDER BY id ASC LIMIT ?`,
updateQuery: `UPDATE identity_recovery_addresses SET value = ?, updated_at = ? WHERE id = ? AND value = ?`,
},
}
startAfterMap, err := parseStartAfter(flagx.MustGetStringSlice(cmd, "start-after"))
if err != nil {
return err
}
allStats := make([]normalizeStats, len(tables))
for i, table := range tables {
startAfter := startAfterMap[table.key]
if startAfter != uuid.Nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: resuming after ID %s\n", table.name, startAfter)
}
stats, err := normalizeTable(conn, batchSize, batchDelay, startAfter, dryRun, cmd, table)
if err != nil {
return errors.Wrapf(err, "normalizing %s", table.name)
}
allStats[i] = stats
}
printSummary(cmd, tables, allStats)
return nil
}
type tableConfig struct {
key string
name string
selectQuery string
updateQuery string
}
// parseStartAfter parses --start-after flags of the form "key=uuid".
func parseStartAfter(args []string) (map[string]uuid.UUID, error) {
result := make(map[string]uuid.UUID)
for _, arg := range args {
key, val, ok := strings.Cut(arg, "=")
if !ok {
return nil, errors.Errorf("invalid --start-after format %q: expected key=uuid (e.g. credentials=<id>)", arg)
}
id, err := uuid.FromString(val)
if err != nil {
return nil, errors.Wrapf(err, "invalid UUID in --start-after %q", arg)
}
result[key] = id
}
return result, nil
}
func printSummary(cmd *cobra.Command, tables []tableConfig, allStats []normalizeStats) {
out := cmd.OutOrStdout()
_, _ = fmt.Fprintln(out)
tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintln(tw, "\tSCANNED\tUPDATED\tSKIPPED\tERRORS")
var total normalizeStats
for i, table := range tables {
s := allStats[i]
_, _ = fmt.Fprintf(tw, "%s\t%d\t%d\t%d\t%d\n", table.name, s.scanned, s.updated, s.skipped, s.errors)
total.scanned += s.scanned
total.updated += s.updated
total.skipped += s.skipped
total.errors += s.errors
}
_, _ = fmt.Fprintf(tw, "TOTAL\t%d\t%d\t%d\t%d\n", total.scanned, total.updated, total.skipped, total.errors)
_ = tw.Flush()
}
func normalizeTable(conn *pop.Connection, batchSize int, batchDelay time.Duration, startAfter uuid.UUID, dryRun bool, cmd *cobra.Command, table tableConfig) (normalizeStats, error) {
var stats normalizeStats
lastID := startAfter
for {
var rows []struct {
ID uuid.UUID `db:"id"`
Value string `db:"value"`
}
if err := conn.RawQuery(table.selectQuery, lastID, batchSize).All(&rows); err != nil {
return stats, errors.Wrapf(err, "querying %s", table.name)
}
if len(rows) == 0 {
break
}
for _, row := range rows {
lastID = row.ID
stats.scanned++
// The SQL query pre-filters with LIKE '+%'. This Go-side regex additionally
// rejects non-phone identifiers (e.g. OIDC subjects) that happen to start
// with '+'. We cannot use SQL regex because SQLite does not support it.
if !phonePattern.MatchString(row.Value) {
stats.skipped++
continue
}
normalized := x.NormalizePhoneIdentifier(row.Value)
if normalized == row.Value {
stats.skipped++
continue
}
if dryRun {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "[dry-run] %s %s: %q -> %q\n", table.name, row.ID, row.Value, normalized)
stats.updated++
continue
}
now := time.Now().UTC().Truncate(time.Second)
if err := conn.RawQuery(table.updateQuery, normalized, now, row.ID, row.Value).Exec(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "error updating %s %s (%q -> %q): %v\n", table.name, row.ID, row.Value, normalized, err)
stats.errors++
continue
}
stats.updated++
}
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\tprocessed %d rows so far (--start-after %s=%s)\n", table.name, stats.scanned, table.key, lastID)
if batchDelay > 0 {
time.Sleep(batchDelay)
}
}
return stats, nil
}

View File

@@ -0,0 +1,70 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/ory/kratos/x"
)
func TestNormalizePhoneIdentifier(t *testing.T) {
for _, tc := range []struct {
name string
input string
expected string
shouldSkip bool
}{
{
name: "German phone with spaces",
input: "+49 176 671 11 638",
expected: "+4917667111638",
},
{
name: "German phone with dashes",
input: "+49-176-671-11-638",
expected: "+4917667111638",
},
{
name: "Already normalized E.164",
input: "+4917667111638",
expected: "+4917667111638",
shouldSkip: true,
},
{
name: "US phone with parens",
input: "+1 (415) 555-2671",
expected: "+14155552671",
},
{
name: "Malaysian phone with extra zero",
input: "+600136866982",
expected: "+60136866982",
},
{
name: "Not a valid phone number",
input: "+invalid",
expected: "+invalid",
shouldSkip: true,
},
{
name: "Email address (not a phone)",
input: "+foo@bar.com",
expected: "+foo@bar.com",
shouldSkip: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
normalized := x.NormalizePhoneIdentifier(tc.input)
assert.Equal(t, tc.expected, normalized)
if tc.shouldSkip {
assert.Equal(t, tc.input, normalized, "should be unchanged (skip)")
} else {
assert.NotEqual(t, tc.input, normalized, "should be changed (update)")
}
})
}
}

47
cmd/migrate/root.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/x/configx"
"github.com/ory/x/popx"
)
func NewMigrateCmd() *cobra.Command {
return &cobra.Command{
Use: "migrate",
Short: "Various migration helpers",
}
}
func RegisterCommandRecursive(parent *cobra.Command) {
c := NewMigrateCmd()
configx.RegisterFlags(c.PersistentFlags())
c.AddCommand(NewMigrateSQLCmd())
c.AddCommand(NewNormalizePhoneCmd())
parent.AddCommand(c)
}
func NewMigrateSQLDownCmd(opts ...driver.RegistryOption) *cobra.Command {
return popx.NewMigrateSQLDownCmd(func(cmd *cobra.Command, args []string) error {
return NewMigrateHandler().MigrateSQLDown(cmd, args, opts...)
})
}
func NewMigrateSQLUpCmd(opts ...driver.RegistryOption) *cobra.Command {
return popx.NewMigrateSQLUpCmd(func(cmd *cobra.Command, args []string) error {
return NewMigrateHandler().MigrateSQLUp(cmd, args, opts...)
})
}
func NewMigrateSQLStatusCmd(opts ...driver.RegistryOption) *cobra.Command {
return popx.NewMigrateSQLStatusCmd(func(cmd *cobra.Command, args []string) error {
return NewMigrateHandler().MigrateSQLStatus(cmd, args, opts...)
})
}

45
cmd/migrate/sql.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"github.com/spf13/cobra"
"github.com/ory/kratos/driver"
"github.com/ory/x/configx"
)
func NewMigrateSQLCmd(opts ...driver.RegistryOption) *cobra.Command {
c := &cobra.Command{
Use: "sql [database-url]",
Deprecated: "Please use `kratos migrate sql up|down|status` instead.",
Short: "Create SQL schemas and apply migration plans",
Long: `Run this command on a fresh SQL installation and when you upgrade Ory Kratos to a new minor version.
It is recommended to run this command close to the SQL instance (e.g. same subnet) instead of over the public internet.
This decreases risk of failure and decreases time required.
You can read in the database URL using the -e flag, for example:
export DSN=...
kratos migrate sql -e
### WARNING ###
Before running this command on an existing database, create a back up!
`,
RunE: func(cmd *cobra.Command, args []string) error {
return NewMigrateHandler().MigrateSQLUp(cmd, args, opts...)
},
}
configx.RegisterFlags(c.PersistentFlags())
c.PersistentFlags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")
c.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.")
c.AddCommand(NewMigrateSQLStatusCmd(opts...))
c.AddCommand(NewMigrateSQLUpCmd(opts...))
c.AddCommand(NewMigrateSQLDownCmd(opts...))
return c
}

29
cmd/remote/root.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package remote
import (
"github.com/spf13/cobra"
"github.com/ory/x/cmdx"
"github.com/ory/kratos/cmd/cliclient"
)
var remoteCmd = &cobra.Command{
Use: "remote",
Short: "Helpers and management for remote Ory Kratos instances",
}
func RegisterCommandRecursive(parent *cobra.Command) {
parent.AddCommand(remoteCmd)
remoteCmd.AddCommand(versionCmd)
remoteCmd.AddCommand(statusCmd)
}
func init() {
cliclient.RegisterClientFlags(remoteCmd.PersistentFlags())
cmdx.RegisterFormatFlags(remoteCmd.PersistentFlags())
}

69
cmd/remote/status.go Normal file
View File

@@ -0,0 +1,69 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package remote
import (
"github.com/ory/x/cmdx"
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/cliclient"
)
type statusState struct {
Alive bool `json:"alive"`
Ready bool `json:"ready"`
}
func (s *statusState) Header() []string {
return []string{"ALIVE", "READY"}
}
func (s *statusState) Columns() []string {
f := [2]string{
"false",
"false",
}
if s.Alive {
f[0] = "true"
}
if s.Ready {
f[1] = "true"
}
return f[:]
}
func (s *statusState) Interface() interface{} {
return s
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "Print the alive and readiness status of a Ory Kratos instance",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
state := &statusState{}
defer cmdx.PrintRow(cmd, state)
alive, _, err := c.MetadataAPI.IsAlive(cmd.Context()).Execute()
if err != nil {
return err
}
state.Alive = alive.Status == "ok"
ready, _, err := c.MetadataAPI.IsReady(cmd.Context()).Execute()
if err != nil {
return err
}
state.Ready = ready.Status == "ok"
return nil
},
}

47
cmd/remote/version.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package remote
import (
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/cliclient"
"github.com/ory/x/cmdx"
)
type versionValue struct {
Version string `json:"version"`
}
func (v *versionValue) Header() []string {
return []string{"VERSION"}
}
func (v *versionValue) Columns() []string {
return []string{v.Version}
}
func (v *versionValue) Interface() interface{} {
return v
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version of an Ory Kratos instance",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
resp, _, err := c.MetadataAPI.GetVersion(cmd.Context()).Execute()
if err != nil {
return err
}
cmdx.PrintRow(cmd, &versionValue{Version: resp.Version})
return nil
},
}

72
cmd/root.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"errors"
"fmt"
"runtime"
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/cleanup"
"github.com/ory/kratos/cmd/courier"
"github.com/ory/kratos/cmd/hashers"
"github.com/ory/kratos/cmd/identities"
"github.com/ory/kratos/cmd/jsonnet"
"github.com/ory/kratos/cmd/migrate"
"github.com/ory/kratos/cmd/remote"
"github.com/ory/kratos/cmd/serve"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/cmdx"
"github.com/ory/x/jsonnetsecure"
"github.com/ory/x/profilex"
)
func NewRootCmd(driverOpts ...driver.RegistryOption) (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "kratos",
}
cmdx.EnableUsageTemplating(cmd)
courier.RegisterCommandRecursive(cmd, driverOpts)
cmd.AddCommand(identities.NewGetCmd())
cmd.AddCommand(identities.NewDeleteCmd())
cmd.AddCommand(jsonnet.NewFormatCmd())
hashers.RegisterCommandRecursive(cmd)
cmd.AddCommand(identities.NewImportCmd())
cmd.AddCommand(jsonnet.NewLintCmd())
cmd.AddCommand(identities.NewListCmd())
migrate.RegisterCommandRecursive(cmd)
serve.RegisterCommandRecursive(cmd, driverOpts)
cleanup.RegisterCommandRecursive(cmd)
remote.RegisterCommandRecursive(cmd)
cmd.AddCommand(identities.NewValidateCmd())
cmd.AddCommand(cmdx.Version(&config.Version, &config.Commit, &config.Date))
// Registers a hidden "jsonnet" subcommand for process-isolated Jsonnet VMs.
cmd.AddCommand(jsonnetsecure.NewJsonnetCmd())
return cmd
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the RootCmd.
func Execute() int {
defer profilex.Profile().Stop()
jsonnetPool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0))
defer jsonnetPool.Close()
c := NewRootCmd(driver.WithJsonnetPool(jsonnetPool))
if err := c.Execute(); err != nil {
if !errors.Is(err, cmdx.ErrNoPrintButFail) {
_, _ = fmt.Fprintln(c.ErrOrStderr(), err)
}
return 1
}
return 0
}

14
cmd/root_test.go Normal file
View File

@@ -0,0 +1,14 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"testing"
"github.com/ory/x/cmdx"
)
func TestUsageStrings(t *testing.T) {
cmdx.AssertUsageTemplates(t, NewRootCmd())
}

58
cmd/serve/root.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package serve
import (
"github.com/spf13/cobra"
"github.com/ory/kratos/cmd/daemon"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/configx"
)
// NewServeCmd returns the serve command
func NewServeCmd(dOpts ...driver.RegistryOption) (serveCmd *cobra.Command) {
serveCmd = &cobra.Command{
Use: "serve",
Short: "Run the Ory Kratos server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
d, err := driver.New(ctx, cmd.ErrOrStderr(), append(dOpts, driver.WithConfigOptions(configx.WithFlags(cmd.Flags())))...)
if err != nil {
return err
}
if d.Config().IsInsecureDevMode(ctx) {
d.Logger().Warn(`
YOU ARE RUNNING Ory KRATOS IN DEV MODE.
SECURITY IS DISABLED.
DON'T DO THIS IN PRODUCTION!
`)
}
configVersion := d.Config().ConfigVersion(ctx)
if configVersion == config.UnknownVersion {
d.Logger().Warn("The config has no version specified. Add the version to improve your development experience.")
} else if config.Version != "" &&
configVersion != config.Version {
d.Logger().Warnf("Config version is '%s' but kratos runs on version '%s'", configVersion, config.Version)
}
return daemon.ServeAll(d)(cmd, args)
},
}
configx.RegisterFlags(serveCmd.PersistentFlags())
serveCmd.PersistentFlags().Bool("sqa-opt-out", false, "Disable anonymized telemetry reports - for more information please visit https://www.ory.com/docs/ecosystem/sqa")
serveCmd.PersistentFlags().Bool("dev", false, "Disables critical security features to make development easier")
serveCmd.PersistentFlags().Bool("watch-courier", false, "Run the message courier as a background task, to simplify single-instance setup")
return serveCmd
}
func RegisterCommandRecursive(parent *cobra.Command, dOpts []driver.RegistryOption) {
parent.AddCommand(NewServeCmd(dOpts...))
}

45
cmd/serve/root_test.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package serve_test
import (
"testing"
"github.com/ory/kratos/pkg/testhelpers"
)
func TestServe(t *testing.T) {
_, _ = testhelpers.StartE2EServer(t, "./stub/kratos.yml", nil)
}
func TestServeTLSBase64(t *testing.T) {
_, _, certBase64, keyBase64 := testhelpers.GenerateTLSCertificateFilesForTests(t)
publicPort, adminPort := testhelpers.StartE2EServerOnly(t,
"./stub/kratos.yml",
true,
testhelpers.ConfigOptions{
"serve.public.tls.key.base64": keyBase64,
"serve.public.tls.cert.base64": certBase64,
"serve.admin.tls.key.base64": keyBase64,
"serve.admin.tls.cert.base64": certBase64,
},
)
testhelpers.CheckE2EServerOnHTTPS(t, publicPort, adminPort)
}
func TestServeTLSPaths(t *testing.T) {
certPath, keyPath, _, _ := testhelpers.GenerateTLSCertificateFilesForTests(t)
publicPort, adminPort := testhelpers.StartE2EServerOnly(t,
"./stub/kratos.yml",
true,
testhelpers.ConfigOptions{
"serve.public.tls.key.path": keyPath,
"serve.public.tls.cert.path": certPath,
"serve.admin.tls.key.path": keyPath,
"serve.admin.tls.cert.path": certPath,
},
)
testhelpers.CheckE2EServerOnHTTPS(t, publicPort, adminPort)
}

View File

@@ -0,0 +1,49 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"title": "First Name",
"type": "string"
},
"last": {
"title": "Last Name",
"type": "string"
}
}
}
},
"required": [
"email"
],
"additionalProperties": false
}
}
}

81
cmd/serve/stub/kratos.yml Normal file
View File

@@ -0,0 +1,81 @@
dsn: memory
serve:
public:
base_url: http://127.0.0.1:4433/
cors:
enabled: true
admin:
base_url: http://kratos:4434/
selfservice:
default_browser_return_url: http://127.0.0.1:4455/
allowed_return_urls:
- http://127.0.0.1:4455
methods:
password:
enabled: true
flows:
error:
ui_url: http://127.0.0.1:4455/error
settings:
ui_url: http://127.0.0.1:4455/settings
privileged_session_max_age: 15m
recovery:
enabled: true
ui_url: http://127.0.0.1:4455/recovery
verification:
enabled: true
ui_url: http://127.0.0.1:4455/verify
after:
default_browser_return_url: http://127.0.0.1:4455/
logout:
after:
default_browser_return_url: http://127.0.0.1:4455/auth/login
login:
ui_url: http://127.0.0.1:4455/auth/login
lifespan: 10m
registration:
lifespan: 10m
ui_url: http://127.0.0.1:4455/auth/registration
after:
password:
hooks:
-
hook: session
log:
level: debug
format: text
leak_sensitive_values: true
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
pagination:
- "test pagination secret"
hashers:
argon2:
parallelism: 1
memory: 128MB
iterations: 2
salt_length: 16
key_length: 16
identity:
schemas:
- id: default
url: file://stub/identity.schema.json
courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true&legacy_ssl=true