mirror of https://github.com/ory/kratos
257 lines
7.2 KiB
Go
257 lines
7.2 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package schema
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/kratos/x/nosurfx"
|
|
"github.com/ory/kratos/x/redir"
|
|
"github.com/ory/x/errorsx"
|
|
"github.com/ory/x/otelx"
|
|
"github.com/ory/x/pagination/migrationpagination"
|
|
)
|
|
|
|
type (
|
|
handlerDependencies interface {
|
|
x.WriterProvider
|
|
x.LoggingProvider
|
|
IdentitySchemaProvider
|
|
nosurfx.CSRFProvider
|
|
config.Provider
|
|
x.TracingProvider
|
|
x.HTTPClientProvider
|
|
}
|
|
Handler struct {
|
|
r handlerDependencies
|
|
}
|
|
HandlerProvider interface {
|
|
SchemaHandler() *Handler
|
|
}
|
|
)
|
|
|
|
func NewHandler(r handlerDependencies) *Handler {
|
|
return &Handler{r: r}
|
|
}
|
|
|
|
const (
|
|
SchemasPath string = "schemas"
|
|
maxSchemaSize = 1024 * 1024 // 1 MB
|
|
)
|
|
|
|
func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
|
|
h.r.CSRFHandler().IgnoreGlobs(
|
|
"/"+SchemasPath+"/*",
|
|
x.AdminPrefix+"/"+SchemasPath+"/*",
|
|
)
|
|
public.GET(fmt.Sprintf("/%s/{id}", SchemasPath), h.getIdentitySchema)
|
|
public.GET(fmt.Sprintf("/%s", SchemasPath), h.getAll)
|
|
public.GET(fmt.Sprintf("%s/%s/{id}", x.AdminPrefix, SchemasPath), h.getIdentitySchema)
|
|
public.GET(fmt.Sprintf("%s/%s", x.AdminPrefix, SchemasPath), h.getAll)
|
|
}
|
|
|
|
func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) {
|
|
admin.GET(fmt.Sprintf("/%s/{id}", SchemasPath), redir.RedirectToPublicRoute(h.r))
|
|
admin.GET(fmt.Sprintf("/%s", SchemasPath), redir.RedirectToPublicRoute(h.r))
|
|
}
|
|
|
|
// Raw JSON Schema
|
|
//
|
|
// swagger:model identitySchema
|
|
type _ json.RawMessage
|
|
|
|
// Get Identity JSON Schema Response
|
|
//
|
|
// swagger:parameters getIdentitySchema
|
|
type _ struct {
|
|
// ID must be set to the ID of schema you want to get
|
|
//
|
|
// required: true
|
|
// in: path
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// swagger:route GET /schemas/{id} identity getIdentitySchema
|
|
//
|
|
// # Get Identity JSON Schema
|
|
//
|
|
// Return a specific identity schema.
|
|
//
|
|
// Produces:
|
|
// - application/json
|
|
//
|
|
// Schemes: http, https
|
|
//
|
|
// Responses:
|
|
// 200: identitySchema
|
|
// 404: errorGeneric
|
|
// default: errorGeneric
|
|
func (h *Handler) getIdentitySchema(w http.ResponseWriter, r *http.Request) {
|
|
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "schema.Handler.getIdentitySchema")
|
|
defer span.End()
|
|
|
|
ss, err := h.r.IdentityTraitsSchemas(ctx)
|
|
if err != nil {
|
|
h.r.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
|
|
id := r.PathValue("id")
|
|
s, err := ss.GetByID(id)
|
|
if err != nil {
|
|
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound.WithReasonf("Identity schema `%s` could not be found.", id)))
|
|
return
|
|
}
|
|
|
|
raw, err := h.ReadSchema(ctx, s.URL)
|
|
if err != nil {
|
|
code, ok := errorsx.GetCodeFromHerodotError(err)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) || (ok && code == http.StatusNotFound) {
|
|
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrMisconfiguration.WithReason("The file for this JSON Schema ID could not be found/fetched. This is a configuration issue.").WithDebugf("%+v", err)))
|
|
} else if ok && code == http.StatusBadGateway {
|
|
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrUpstreamError.WithReason("The file for this JSON Schema ID could not be fetched. This is an upstream issue.").WithDebugf("%+v", err)))
|
|
} else {
|
|
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReason("The file for this JSON Schema ID could not be read. This is an I/O issue.").WithDebugf("%+v", err)))
|
|
}
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Type", "application/json")
|
|
h.r.Writer().Write(w, r, json.RawMessage(raw))
|
|
}
|
|
|
|
// List of Identity JSON Schemas
|
|
//
|
|
// swagger:model identitySchemas
|
|
type IdentitySchemas []identitySchemaContainer
|
|
|
|
// An Identity JSON Schema Container
|
|
//
|
|
// swagger:model identitySchemaContainer
|
|
type identitySchemaContainer struct {
|
|
// The ID of the Identity JSON Schema
|
|
// required: true
|
|
ID string `json:"id"`
|
|
// The actual Identity JSON Schema
|
|
// required: true
|
|
Schema json.RawMessage `json:"schema"`
|
|
}
|
|
|
|
// List Identity JSON Schemas Response
|
|
//
|
|
// swagger:parameters listIdentitySchemas
|
|
type _ struct {
|
|
migrationpagination.RequestParameters
|
|
}
|
|
|
|
// List Identity JSON Schemas Response
|
|
//
|
|
// swagger:response identitySchemas
|
|
type _ struct {
|
|
migrationpagination.ResponseHeaderAnnotation
|
|
|
|
// in: body
|
|
Body IdentitySchemas
|
|
}
|
|
|
|
// swagger:route GET /schemas identity listIdentitySchemas
|
|
//
|
|
// # Get all Identity Schemas
|
|
//
|
|
// Returns a list of all identity schemas currently in use.
|
|
//
|
|
// Produces:
|
|
// - application/json
|
|
//
|
|
// Schemes: http, https
|
|
//
|
|
// Responses:
|
|
// 200: identitySchemas
|
|
// default: errorGeneric
|
|
func (h *Handler) getAll(w http.ResponseWriter, r *http.Request) {
|
|
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "schema.Handler.getAll")
|
|
defer span.End()
|
|
|
|
page, itemsPerPage := x.ParsePagination(r)
|
|
|
|
allSchemas, err := h.r.IdentityTraitsSchemas(r.Context())
|
|
if err != nil {
|
|
h.r.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
total := allSchemas.Total()
|
|
schemas := allSchemas.List(page, itemsPerPage)
|
|
|
|
ss := make(IdentitySchemas, len(schemas))
|
|
for i, schema := range schemas {
|
|
raw, err := h.ReadSchema(ctx, schema.URL)
|
|
if err != nil {
|
|
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrMisconfiguration.WithReasonf("The file for a JSON Schema ID could not be found or opened. This is a configuration issue.").WithWrap(err)))
|
|
return
|
|
}
|
|
ss[i] = identitySchemaContainer{
|
|
ID: schema.ID,
|
|
Schema: raw,
|
|
}
|
|
}
|
|
|
|
x.PaginationHeader(w, *r.URL, int64(total), page, itemsPerPage)
|
|
h.r.Writer().Write(w, r, ss)
|
|
}
|
|
|
|
func (h *Handler) ReadSchema(ctx context.Context, uri *url.URL) (data []byte, err error) {
|
|
ctx, span := h.r.Tracer(ctx).Tracer().Start(ctx, "schema.Handler.ReadSchema")
|
|
defer otelx.End(span, &err)
|
|
|
|
switch uri.Scheme {
|
|
case "file":
|
|
data, err = os.ReadFile(uri.Host + uri.Path)
|
|
if err != nil {
|
|
return nil, errors.WithStack(fmt.Errorf("could not read schema file: %w", err))
|
|
}
|
|
case "base64":
|
|
data, err = base64.StdEncoding.DecodeString(strings.TrimPrefix(uri.String(), "base64://"))
|
|
if err != nil {
|
|
return nil, errors.WithStack(fmt.Errorf("could not decode schema file: %w", err))
|
|
}
|
|
default:
|
|
req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
|
|
if err != nil {
|
|
return nil, errors.WithStack(fmt.Errorf("could not create request: %w", err))
|
|
}
|
|
resp, err := h.r.HTTPClient(ctx).Do(req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(herodot.ErrUpstreamError.WithReason("could not fetch schema").WithError(err.Error()).WithDetail("uri", uri))
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, herodot.ErrNotFound.WithDetail("url", uri)
|
|
}
|
|
return nil, errors.WithStack(herodot.ErrUpstreamError.WithError("upstream error").WithDetail("status_code", resp.StatusCode).WithDetail("uri", uri))
|
|
}
|
|
data, err = io.ReadAll(io.LimitReader(resp.Body, maxSchemaSize))
|
|
if err != nil {
|
|
return nil, errors.WithStack(herodot.ErrUpstreamError.WithReason("could not read schema response").WithError(err.Error()).WithDetail("uri", uri))
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|