kratos/schema/handler.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
}