authelia/cmd/authelia-gen/cmd_docs_jsonschema.go

402 lines
11 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"github.com/authelia/jsonschema"
"github.com/spf13/cobra"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/utils"
)
func newDocsJSONSchemaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: pathJSONSchema,
Short: "Generate docs JSON schema",
RunE: rootSubCommandsRunE,
DisableAutoGenTag: true,
}
cmd.AddCommand(newDocsJSONSchemaConfigurationCmd(), newDocsJSONSchemaUserDatabaseCmd(), newDocsJSONSchemaExportsCmd())
return cmd
}
func newDocsJSONSchemaExportsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "exports",
Short: "Generate docs JSON schema for the various exports",
RunE: rootSubCommandsRunE,
DisableAutoGenTag: true,
}
cmd.AddCommand(newDocsJSONSchemaExportsTOTPCmd(), newDocsJSONSchemaExportsWebAuthnCmd(), newDocsJSONSchemaExportsIdentifiersCmd())
return cmd
}
func newDocsJSONSchemaExportsTOTPCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "totp",
Short: "Generate docs JSON schema for the TOTP exports",
RunE: docsJSONSchemaExportsTOTPRunE,
DisableAutoGenTag: true,
}
return cmd
}
func newDocsJSONSchemaExportsWebAuthnCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "webauthn",
Short: "Generate docs JSON schema for the WebAuthn exports",
RunE: docsJSONSchemaExportsWebAuthnRunE,
DisableAutoGenTag: true,
}
return cmd
}
func newDocsJSONSchemaExportsIdentifiersCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "identifiers",
Short: "Generate docs JSON schema for the identifiers exports",
RunE: docsJSONSchemaExportsIdentifiersRunE,
DisableAutoGenTag: true,
}
return cmd
}
func newDocsJSONSchemaConfigurationCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "configuration",
Short: "Generate docs JSON schema for the configuration",
RunE: docsJSONSchemaConfigurationRunE,
DisableAutoGenTag: true,
}
return cmd
}
func newDocsJSONSchemaUserDatabaseCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "user-database",
Short: "Generate docs JSON schema for the user database",
RunE: docsJSONSchemaUserDatabaseRunE,
DisableAutoGenTag: true,
}
return cmd
}
func docsJSONSchemaExportsTOTPRunE(cmd *cobra.Command, args []string) (err error) {
var version *model.SemanticVersion
if version, err = readVersion(cmd); err != nil {
return err
}
var (
dir, file, schemaDir string
)
if schemaDir, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDirSchema); err != nil {
return err
}
if dir, file, err = getJSONSchemaOutputPath(cmd, cmdFlagDocsStaticJSONSchemaExportsTOTP); err != nil {
return err
}
return docsJSONSchemaGenerateRunE(cmd, args, version, schemaDir, &model.TOTPConfigurationDataExport{}, dir, file, nil)
}
func docsJSONSchemaExportsWebAuthnRunE(cmd *cobra.Command, args []string) (err error) {
var version *model.SemanticVersion
if version, err = readVersion(cmd); err != nil {
return err
}
var (
dir, file, schemaDir string
)
if schemaDir, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDirSchema); err != nil {
return err
}
if dir, file, err = getJSONSchemaOutputPath(cmd, cmdFlagDocsStaticJSONSchemaExportsWebAuthn); err != nil {
return err
}
return docsJSONSchemaGenerateRunE(cmd, args, version, schemaDir, &model.WebAuthnCredentialDataExport{}, dir, file, nil)
}
func docsJSONSchemaExportsIdentifiersRunE(cmd *cobra.Command, args []string) (err error) {
var version *model.SemanticVersion
if version, err = readVersion(cmd); err != nil {
return err
}
var (
dir, file, schemaDir string
)
if schemaDir, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDirSchema); err != nil {
return err
}
if dir, file, err = getJSONSchemaOutputPath(cmd, cmdFlagDocsStaticJSONSchemaExportsIdentifiers); err != nil {
return err
}
return docsJSONSchemaGenerateRunE(cmd, args, version, schemaDir, &model.UserOpaqueIdentifiersExport{}, dir, file, nil)
}
func docsJSONSchemaConfigurationRunE(cmd *cobra.Command, args []string) (err error) {
var version *model.SemanticVersion
if version, err = readVersion(cmd); err != nil {
return err
}
var (
dir, file, schemaDir string
)
if schemaDir, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDirSchema); err != nil {
return err
}
if dir, file, err = getJSONSchemaOutputPath(cmd, cmdFlagDocsStaticJSONSchemaConfiguration); err != nil {
return err
}
return docsJSONSchemaGenerateRunE(cmd, args, version, schemaDir, &schema.Configuration{}, dir, file, jsonschemaKoanfMapper)
}
func docsJSONSchemaUserDatabaseRunE(cmd *cobra.Command, args []string) (err error) {
var version *model.SemanticVersion
if version, err = readVersion(cmd); err != nil {
return err
}
var (
dir, file, schemaDir string
)
if schemaDir, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDirAuthentication); err != nil {
return err
}
if dir, file, err = getJSONSchemaOutputPath(cmd, cmdFlagDocsStaticJSONSchemaUserDatabase); err != nil {
return err
}
return docsJSONSchemaGenerateRunE(cmd, args, version, schemaDir, &authentication.FileUserDatabase{}, dir, file, jsonschemaKoanfMapper)
}
//nolint:gocyclo
func docsJSONSchemaGenerateRunE(cmd *cobra.Command, _ []string, version *model.SemanticVersion, schemaDir string, v any, dir, file string, mapper func(reflect.Type) *jsonschema.Schema) (err error) {
r := &jsonschema.Reflector{
RequiredFromJSONSchemaTags: true,
Mapper: mapper,
}
if runtime.GOOS == windows {
mapComments := map[string]string{}
if err = jsonschema.ExtractGoComments(goModuleBase, schemaDir, mapComments); err != nil {
return err
}
if r.CommentMap == nil {
r.CommentMap = map[string]string{}
}
for key, comment := range mapComments {
r.CommentMap[strings.ReplaceAll(key, `\`, `/`)] = comment
}
} else {
if err = r.AddGoComments(goModuleBase, schemaDir); err != nil {
return err
}
}
var (
versions []string
)
versions, _ = cmd.Flags().GetStringSlice(cmdFlagVersions)
if len(versions) == 0 {
versions = []string{metaVersionLatest, metaVersionCurrent}
}
next := utils.IsStringInSlice(metaVersionNext, versions)
if next && utils.IsStringInSlice(metaVersionCurrent, versions) {
return fmt.Errorf("failed to generate: meta version next and current are mutually exclusive")
}
schema := r.Reflect(v)
for _, versionName := range versions {
var out string
switch versionName {
case metaVersionNext:
out = fmt.Sprintf("v%d.%d", version.Major, version.Minor+1)
schema.ID = jsonschema.ID(fmt.Sprintf(model.FormatJSONSchemaIdentifier, out, file))
case metaVersionCurrent:
out = fmt.Sprintf("v%d.%d", version.Major, version.Minor)
schema.ID = jsonschema.ID(fmt.Sprintf(model.FormatJSONSchemaIdentifier, out, file))
case metaVersionLatest:
out = metaVersionLatest
if next {
schema.ID = jsonschema.ID(fmt.Sprintf(model.FormatJSONSchemaIdentifier, fmt.Sprintf("v%d.%d", version.Major, version.Minor+1), file))
} else {
schema.ID = jsonschema.ID(fmt.Sprintf(model.FormatJSONSchemaIdentifier, fmt.Sprintf("v%d.%d", version.Major, version.Minor), file))
}
default:
var parsed *model.SemanticVersion
if parsed, err = model.NewSemanticVersion(versionName); err != nil {
return fmt.Errorf("failed to parse version: %w", err)
}
out = fmt.Sprintf("v%d.%d", parsed.Major, parsed.Minor)
schema.ID = jsonschema.ID(fmt.Sprintf(model.FormatJSONSchemaIdentifier, fmt.Sprintf("v%d.%d", version.Major, version.Minor), file))
}
if err = writeJSONSchema(schema, dir, out, file); err != nil {
return err
}
}
return nil
}
func writeJSONSchema(schema *jsonschema.Schema, dir, version, file string) (err error) {
var (
data []byte
f *os.File
)
if data, err = json.MarshalIndent(schema, "", " "); err != nil {
return err
}
if _, err = os.Stat(filepath.Join(dir, version, pathJSONSchema)); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Join(dir, version, pathJSONSchema), 0755); err != nil {
return err
}
}
if f, err = os.Create(filepath.Join(dir, version, pathJSONSchema, file+extJSON)); err != nil {
return err
}
if _, err = f.Write(data); err != nil {
return err
}
return f.Close()
}
func getJSONSchemaOutputPath(cmd *cobra.Command, flag string) (dir, file string, err error) {
if dir, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDocs, cmdFlagDocsStatic, cmdFlagDocsStaticJSONSchemas); err != nil {
return "", "", err
}
if file, err = cmd.Flags().GetString(flag); err != nil {
return "", "", err
}
return dir, file, nil
}
func jsonschemaKoanfMapper(t reflect.Type) *jsonschema.Schema {
switch t.String() {
case "regexp.Regexp", "*regexp.Regexp":
return &jsonschema.Schema{
Type: jsonschema.TypeString,
Format: jsonschema.FormatStringRegex,
}
case "time.Duration", "*time.Duration":
return &jsonschema.Schema{
OneOf: []*jsonschema.Schema{
{
Type: jsonschema.TypeString,
Pattern: `^\d+\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\s*\d+\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$`,
},
{
Type: jsonschema.TypeInteger,
Description: "The duration in seconds",
},
},
}
case "schema.CryptographicKey":
return &jsonschema.Schema{
Type: jsonschema.TypeString,
Pattern: `^-{5}BEGIN (((RSA|EC) )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-{5}\n([a-zA-Z0-9\/+]{1,64}\n)+([a-zA-Z0-9\/+]{1,64}[=]{0,2})\n-{5}END (((RSA|EC) )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-{5}\n?$`,
}
case "schema.CryptographicPrivateKey":
return &jsonschema.Schema{
Type: jsonschema.TypeString,
Pattern: `^-{5}BEGIN ((RSA|EC) )?PRIVATE KEY-{5}\n([a-zA-Z0-9\/+]{1,64}\n)+([a-zA-Z0-9\/+]{1,64}[=]{0,2})\n-{5}END ((RSA|EC) )?PRIVATE KEY-{5}\n?$`,
}
case "rsa.PrivateKey", "*rsa.PrivateKey":
return &jsonschema.Schema{
Type: jsonschema.TypeString,
Pattern: `^-{5}(BEGIN (RSA )?PRIVATE KEY-{5}\n([a-zA-Z0-9\/+]{1,64}\n)+([a-zA-Z0-9\/+]{1,64}[=]{0,2})\n-{5}END (RSA )?PRIVATE KEY-{5}\n?)+$`,
}
case "ecdsa.PrivateKey", "*.ecdsa.PrivateKey":
return &jsonschema.Schema{
Type: jsonschema.TypeString,
Pattern: `^-{5}(BEGIN ((EC )?PRIVATE KEY-{5}\n([a-zA-Z0-9\/+]{1,64}\n)+([a-zA-Z0-9\/+]{1,64}[=]{0,2})\n-{5}END (EC )?PRIVATE KEY-{5}\n?)+$`,
}
case "mail.Address", "*mail.Address":
return &jsonschema.Schema{
OneOf: []*jsonschema.Schema{
{
Type: jsonschema.TypeString,
Format: jsonschema.FormatStringEmail,
},
{
Type: jsonschema.TypeString,
Pattern: `^[^<]+\s\<[a-zA-Z0-9._~!#$%&'*/=?^{|}+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]+\>$`,
},
},
}
case "schema.CSPTemplate":
return &jsonschema.Schema{
Type: jsonschema.TypeString,
Default: buildCSP(codeCSPProductionDefaultSrc, codeCSPValuesCommon, codeCSPValuesProduction),
}
}
return nil
}