Skip to content

Commit

Permalink
feat: endpoint to list all identity schemas (#1703)
Browse files Browse the repository at this point in the history
Closes #1699
  • Loading branch information
jld3103 committed Sep 23, 2021
1 parent b9d253e commit aa23d5d
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 41 deletions.
120 changes: 107 additions & 13 deletions schema/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"

"github.com/ory/kratos/driver/config"
"github.com/ory/x/urlx"

"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
Expand Down Expand Up @@ -41,7 +43,8 @@ const SchemasPath string = "schemas"

func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
h.r.CSRFHandler().IgnoreGlobs(fmt.Sprintf("/%s/*", SchemasPath))
public.GET(fmt.Sprintf("/%s/:id", SchemasPath), h.get)
public.GET(fmt.Sprintf("/%s/:id", SchemasPath), h.getByID)
public.GET(fmt.Sprintf("/%s", SchemasPath), h.getAll)
}

func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) {
Expand Down Expand Up @@ -77,34 +80,125 @@ type getJsonSchema struct {
// 200: jsonSchema
// 404: jsonError
// 500: jsonError
func (h *Handler) get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
func (h *Handler) getByID(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s, err := h.r.IdentityTraitsSchemas(r.Context()).GetByID(ps.ByName("id"))
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound.WithDebugf("%+v", err)))
return
}
var src io.ReadCloser

if s.URL.Scheme == "file" {
src, err = os.Open(s.URL.Host + s.URL.Path)
src, err := ReadSchema(s)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err)))
return
}
defer src.Close()

w.Header().Add("Content-Type", "application/json")
if _, err := io.Copy(w, src); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err)))
return
}
}

// Raw identity Schema list
//
// swagger:model identitySchemas
type IdentitySchemas []identitySchema

// swagger:model identitySchema
type identitySchema struct {
// The ID of the Identity JSON Schema
ID string `json:"id"`
// The actual Identity JSON Schema
Schema json.RawMessage `json:"schema"`
}

// nolint:deadcode,unused
// swagger:parameters listIdentitySchemas
type listIdentitySchemas struct {
// Items per Page
//
// This is the number of items per page.
//
// required: false
// in: query
// default: 100
// min: 1
// max: 500
PerPage int `json:"per_page"`

// Pagination Page
//
// required: false
// in: query
// default: 0
// min: 0
Page int `json:"page"`
}

// swagger:route GET /schemas v0alpha2 listIdentitySchemas
//
// Get all Identity Schemas
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: identitySchemas
// 500: jsonError
func (h *Handler) getAll(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
page, itemsPerPage := x.ParsePagination(r)

schemas := h.r.IdentityTraitsSchemas(r.Context()).List(page, itemsPerPage)
total := h.r.IdentityTraitsSchemas(r.Context()).Total()

var ss IdentitySchemas

for _, schema := range schemas {
s, err := h.r.IdentityTraitsSchemas(r.Context()).GetByID(schema.ID)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrNotFound.WithDebugf("%+v", err)))
return
}

src, err := ReadSchema(s)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err)))
return
}
defer src.Close()
} else {
resp, err := http.Get(s.URL.String())

raw, err := ioutil.ReadAll(src)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err)))
return
}
defer resp.Body.Close()
src = resp.Body

ss = append(ss, identitySchema{
ID: s.ID,
Schema: raw,
})
}

w.Header().Add("Content-Type", "application/json")
if _, err := io.Copy(w, src); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err)))
return
x.PaginationHeader(w, urlx.AppendPaths(h.r.Config(r.Context()).SelfPublicURL(r), fmt.Sprintf("/%s", SchemasPath)), int64(total), page, itemsPerPage)
h.r.Writer().Write(w, r, ss)
}

func ReadSchema(schema *Schema) (src io.ReadCloser, err error) {
if schema.URL.Scheme == "file" {
src, err = os.Open(schema.URL.Host + schema.URL.Path)
if err != nil {
return nil, errors.WithStack(err)
}
} else {
resp, err := http.Get(schema.URL.String())
if err != nil {
return nil, errors.WithStack(err)
}
src = resp.Body
}
return src, nil
}
166 changes: 138 additions & 28 deletions schema/handler_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package schema_test

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand All @@ -9,6 +10,7 @@ import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

_ "github.com/ory/jsonschema/v3/fileloader"
Expand Down Expand Up @@ -61,68 +63,176 @@ func TestHandler(t *testing.T) {
return s
}

getFromTS := func(id string, expectCode int) string {
res, err := ts.Client().Get(fmt.Sprintf("%s/schemas/%s", ts.URL, id))
getFromTS := func(url string, expectCode int) []byte {
res, err := ts.Client().Get(url)
require.NoError(t, err)
body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, res.Body.Close())

require.EqualValues(t, expectCode, res.StatusCode, "%s", body)
return string(body)
return body

}

getFromFS := func(id string) string {
f, err := os.Open(strings.TrimPrefix(getSchemaById(id).RawURL, "file://"))
require.NoError(t, err)
raw, err := ioutil.ReadAll(f)
getFromTSById := func(id string, expectCode int) []byte {
return getFromTS(fmt.Sprintf("%s/schemas/%s", ts.URL, id), expectCode)
}

getFromTSPaginated := func(page, perPage, expectCode int) []byte {
return getFromTS(fmt.Sprintf("%s/schemas?page=%d&per_page=%d", ts.URL, page, perPage), expectCode)
}

getFromFS := func(id string) []byte {
raw, err := os.ReadFile(strings.TrimPrefix(getSchemaById(id).RawURL, "file://"))
require.NoError(t, err)
require.NoError(t, f.Close())
return string(raw)
return raw
}

var schemasConfig []config.Schema
for _, s := range schemas {
if s.ID != config.DefaultIdentityTraitsSchemaID {
schemasConfig = append(schemasConfig, config.Schema{
ID: s.ID,
URL: s.RawURL,
})
setSchemas := func(newSchemas schema.Schemas) {
schemas = newSchemas
var schemasConfig []config.Schema
for _, s := range schemas {
if s.ID != config.DefaultIdentityTraitsSchemaID {
schemasConfig = append(schemasConfig, config.Schema{
ID: s.ID,
URL: s.RawURL,
})
}
}
conf.MustSet(config.ViperKeyIdentitySchemas, schemasConfig)
}

conf.MustSet(config.ViperKeyPublicBaseURL, ts.URL)
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, getSchemaById(config.DefaultIdentityTraitsSchemaID).RawURL)
conf.MustSet(config.ViperKeyIdentitySchemas, schemasConfig)
setSchemas(schemas)

t.Run("case=get default schema", func(t *testing.T) {
server := getFromTS(config.DefaultIdentityTraitsSchemaID, http.StatusOK)
server := getFromTSById(config.DefaultIdentityTraitsSchemaID, http.StatusOK)
file := getFromFS(config.DefaultIdentityTraitsSchemaID)
require.Equal(t, file, server)
require.JSONEq(t, string(file), string(server))
})

t.Run("case=get other schema", func(t *testing.T) {
server := getFromTS("identity2", http.StatusOK)
server := getFromTSById("identity2", http.StatusOK)
file := getFromFS("identity2")
require.Equal(t, file, server)
require.JSONEq(t, string(file), string(server))
})

t.Run("case=get unreachable schema", func(t *testing.T) {
reason := getFromTS("unreachable", http.StatusInternalServerError)
require.Contains(t, reason, "could not be found or opened")
reason := getFromTSById("unreachable", http.StatusInternalServerError)
require.Contains(t, string(reason), "could not be found or opened")
})

t.Run("case=get no-file schema", func(t *testing.T) {
reason := getFromTS("no-file", http.StatusInternalServerError)
require.Contains(t, reason, "could not be found or opened")
reason := getFromTSById("no-file", http.StatusInternalServerError)
require.Contains(t, string(reason), "could not be found or opened")
})

t.Run("case=get directory schema", func(t *testing.T) {
reason := getFromTS("directory", http.StatusInternalServerError)
require.Contains(t, reason, "could not be found or opened")
reason := getFromTSById("directory", http.StatusInternalServerError)
require.Contains(t, string(reason), "could not be found or opened")
})

t.Run("case=get not-existing schema", func(t *testing.T) {
_ = getFromTS("not-existing", http.StatusNotFound)
_ = getFromTSById("not-existing", http.StatusNotFound)
})

t.Run("case=get all schemas", func(t *testing.T) {
setSchemas(schema.Schemas{
{
ID: "default",
URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"),
RawURL: "file://./stub/identity.schema.json",
},
{
ID: "identity2",
URL: urlx.ParseOrPanic("file://./stub/identity-2.schema.json"),
RawURL: "file://./stub/identity-2.schema.json",
},
})

body := getFromTSPaginated(0, 2, http.StatusOK)

var result schema.IdentitySchemas
require.NoError(t, json.Unmarshal(body, &result))

ids_orig := []string{}
for _, s := range schemas {
ids_orig = append(ids_orig, s.ID)
}
ids_list := []string{}
for _, s := range result {
ids_list = append(ids_list, s.ID)
}
for _, id := range ids_orig {
require.Contains(t, ids_list, id)
}

for _, s := range schemas {
for _, r := range result {
if r.ID == s.ID {
assert.JSONEq(t, string(getFromFS(s.ID)), string(r.Schema))
}
}
}
})

t.Run("case=get paginated schemas", func(t *testing.T) {
setSchemas(schema.Schemas{
{
ID: "default",
URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"),
RawURL: "file://./stub/identity.schema.json",
},
{
ID: "identity2",
URL: urlx.ParseOrPanic("file://./stub/identity-2.schema.json"),
RawURL: "file://./stub/identity-2.schema.json",
},
})

body1, body2 := getFromTSPaginated(0, 1, http.StatusOK), getFromTSPaginated(1, 1, http.StatusOK)

var result1, result2 schema.IdentitySchemas
require.NoError(t, json.Unmarshal(body1, &result1))
require.NoError(t, json.Unmarshal(body2, &result2))

result := append(result1, result2...)

ids_orig := []string{}
for _, s := range schemas {
ids_orig = append(ids_orig, s.ID)
}
ids_list := []string{}
for _, s := range result {
ids_list = append(ids_list, s.ID)
}
for _, id := range ids_orig {
require.Contains(t, ids_list, id)
}
})

t.Run("case=read schema", func(t *testing.T) {
setSchemas(schema.Schemas{
{
ID: "default",
URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"),
RawURL: "file://./stub/identity.schema.json",
},
{
ID: "default",
URL: urlx.ParseOrPanic(fmt.Sprintf("%s/schemas/default", ts.URL)),
RawURL: fmt.Sprintf("%s/schemas/default", ts.URL),
},
})

src, err := schema.ReadSchema(&schemas[0])
require.NoError(t, err)
defer src.Close()

src, err = schema.ReadSchema(&schemas[1])
require.NoError(t, err)
defer src.Close()
})
}

0 comments on commit aa23d5d

Please sign in to comment.