This gist is associated with the blog post "Building safe-by-default tools in our Go web application".
It contains the goa middleware that we use to ensure our queries are correctly scoped by organisation.
package mw | |
import ( | |
"context" | |
"reflect" | |
"github.com/incident-io/core/server/api/gen/billing" | |
"github.com/incident-io/core/server/api/gen/insights" | |
"github.com/incident-io/core/server/api/gen/system" | |
"github.com/incident-io/core/server/api/gen/typeaheads" | |
"github.com/incident-io/core/server/db" | |
"github.com/incident-io/core/server/internal/errors" | |
"github.com/incident-io/core/server/log" | |
goa "goa.design/goa/v3/pkg" | |
) | |
// CheckOrganisationScope generates errors whenever we return resources that don't belong | |
// to the organisation associated with the API request scope. | |
// | |
// It assumes our responses are struct pointers with a field that is either: | |
// | |
// - A pointer to a struct which should have an OrganisationID field | |
// - A pointer to a slice of pointers to structs which should have an OrganisationID field | |
func CheckOrganisationScope(db *db.Postgres) func(goa.Endpoint) goa.Endpoint { | |
return func(e goa.Endpoint) goa.Endpoint { | |
return goa.Endpoint(func(ctx context.Context, req interface{}) (interface{}, error) { | |
res, err := e(ctx, req) | |
if err != nil { | |
return res, err | |
} | |
if res == nil { | |
return res, err // There is nothing to check! | |
} | |
// If we don't have an org, we're an unauthenticated request, so running this | |
// middleware doesn't make sense. | |
id, _, _ := GetIdentity(ctx) | |
if id.OrganisationID == "" { | |
return res, err | |
} | |
if err := CheckOrganisationScopeResponse(id.OrganisationID, res); err != nil { | |
log.Error(ctx, err, map[string]interface{}{ | |
"event": "check_organisation_scope_violation", | |
"endpoint_service": ctx.Value(goa.ServiceKey).(string), | |
"endpoint_method": ctx.Value(goa.MethodKey).(string), | |
}) | |
} | |
return res, err | |
}) | |
} | |
} | |
func CheckOrganisationScopeResponse(orgID string, res interface{}) error { | |
val := reflect.ValueOf(res).Elem() | |
if !val.IsValid() || val.IsZero() { | |
return nil // there is nothing to check! | |
} | |
for idx := 0; idx < val.NumField(); idx++ { | |
fieldVal := val.Field(idx) | |
// If we're wrapped in an interface, unpack it to get the real pointer type. | |
if fieldVal.Kind() == reflect.Interface { | |
fieldVal = fieldVal.Elem() | |
} | |
switch fieldVal.Kind() { | |
case reflect.Slice: | |
for elemIdx := 0; elemIdx < fieldVal.Len(); elemIdx++ { | |
if err := checkOrganisationScope(orgID, fieldVal.Index(elemIdx)); err != nil { | |
return err | |
} | |
} | |
case reflect.Ptr: | |
return checkOrganisationScope(orgID, fieldVal) | |
} | |
} | |
return nil | |
} | |
var ErrCheckOrganisationScopeMissingID = errors.New("this response does not have any organisation ID") | |
type ErrCheckOrganisationScopeIncorrectID struct { | |
ExpectedOrganisationID string | |
ResourceOrganisationID string | |
} | |
func (e ErrCheckOrganisationScopeIncorrectID) Error() string { | |
return "response includes data for an organisation outside of this API scope" | |
} | |
// checkOrganisationScope ensures a val, which is expected to be a pointer to a struct, | |
// has a valid OrganisationID field. | |
func checkOrganisationScope(orgID string, val reflect.Value) error { | |
if val.Elem().Kind() != reflect.Struct { | |
return nil | |
} | |
organisationField := val.Elem().FieldByName("OrganisationID") | |
if !organisationField.IsValid() || organisationField.IsZero() { | |
return ErrCheckOrganisationScopeMissingID | |
} | |
if resourceOrgID := organisationField.Interface().(string); resourceOrgID != orgID { | |
return ErrCheckOrganisationScopeIncorrectID{ | |
ExpectedOrganisationID: orgID, | |
ResourceOrganisationID: resourceOrgID, | |
} | |
} | |
return nil | |
} |
package mw_test | |
import ( | |
"github.com/incident-io/core/server/domain" | |
"github.com/incident-io/core/server/api/mw" | |
. "github.com/incident-io/core/server/spec" | |
. "github.com/nauyey/factory" | |
. "github.com/onsi/ginkgo" | |
. "github.com/onsi/gomega" | |
) | |
var _ = Describe("CheckOrganisationScopeResponse", func() { | |
var ( | |
org domain.Organisation | |
) | |
BeforeEach(func() { | |
MustCreate(ctx, tx, &org, Build(domain.OrganisationFactory)) | |
}) | |
type envelope struct { | |
Envelope interface{} | |
} | |
type resWithOrg struct { | |
ID string | |
OrganisationID string | |
} | |
type resWithoutOrg struct { | |
ID string | |
} | |
check := func(res interface{}) error { | |
return mw.CheckOrganisationScopeResponse(org.ID, res) | |
} | |
Context("when singular response matches organisation", func() { | |
It("returns no error", func() { | |
err := check(&envelope{ | |
Envelope: &resWithOrg{ | |
ID: "my-id", | |
OrganisationID: org.ID, | |
}, | |
}) | |
Expect(err).NotTo(HaveOccurred()) | |
}) | |
}) | |
Context("when singular response has different organisation", func() { | |
It("returns no error", func() { | |
err := check(&envelope{ | |
Envelope: &resWithOrg{ | |
ID: "my-id", | |
OrganisationID: "different-org-id", | |
}, | |
}) | |
Expect(err).To(MatchError(mw.ErrCheckOrganisationScopeIncorrectID{ | |
ExpectedOrganisationID: org.ID, | |
ResourceOrganisationID: "different-org-id", | |
})) | |
}) | |
}) | |
Context("when singular response has no organisation", func() { | |
It("returns no error", func() { | |
err := check(&envelope{ | |
Envelope: &resWithoutOrg{ | |
ID: "my-id", | |
}, | |
}) | |
Expect(err).To(MatchError(mw.ErrCheckOrganisationScopeMissingID)) | |
}) | |
}) | |
Context("when slice response matches organisation", func() { | |
It("returns no error", func() { | |
err := check(&envelope{ | |
Envelope: []*resWithOrg{ | |
{ | |
ID: "my-id", | |
OrganisationID: org.ID, | |
}, | |
}, | |
}) | |
Expect(err).NotTo(HaveOccurred()) | |
}) | |
}) | |
Context("when slice response has different organisation", func() { | |
It("returns no error", func() { | |
err := check(&envelope{ | |
Envelope: []*resWithOrg{ | |
{ | |
ID: "my-id", | |
OrganisationID: "different-org-id", | |
}, | |
}, | |
}) | |
Expect(err).To(MatchError(mw.ErrCheckOrganisationScopeIncorrectID{ | |
ExpectedOrganisationID: org.ID, | |
ResourceOrganisationID: "different-org-id", | |
})) | |
}) | |
}) | |
Context("when singular response has no organisation", func() { | |
It("returns no error", func() { | |
err := check(&envelope{ | |
Envelope: []*resWithoutOrg{ | |
{ | |
ID: "my-id", | |
}, | |
}, | |
}) | |
Expect(err).To(MatchError(mw.ErrCheckOrganisationScopeMissingID)) | |
}) | |
}) | |
}) |