Skip to content
31 changes: 31 additions & 0 deletions api/gen_server.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

687 changes: 344 additions & 343 deletions api/gen_spec.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions api/gen_types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions api/patients.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/tidepool-org/clinic/auth"
"github.com/tidepool-org/clinic/clinics"
"github.com/tidepool-org/clinic/export"
"github.com/tidepool-org/clinic/patients"
"github.com/tidepool-org/clinic/store"
)
Expand Down Expand Up @@ -527,3 +528,34 @@ func (h *Handler) ConnectProvider(ec echo.Context, clinicId ClinicId, patientId

return ec.NoContent(http.StatusNoContent)
}

func (h *Handler) ExportPatientList(ec echo.Context, clinicId ClinicId, params ExportPatientListParams) error {
ctx := ec.Request().Context()
authData := auth.GetAuthData(ctx)
if authData == nil || authData.SubjectId == "" {
return &echo.HTTPError{
Code: http.StatusBadRequest,
Message: "expected authenticated user id",
}
}
if authData.ServerAccess {
return &echo.HTTPError{
Code: http.StatusBadRequest,
Message: "expected user access token",
}
}
filter := patients.ExportParams{
Period: params.Period,
ExporterClinicianID: authData.SubjectId,
WorkspaceID: clinicId,
ReportDate: time.Now(),
}
exporter, err := export.NewPatientExport(ctx, h.Clinics, h.Clinicians, h.Patients, filter)
if err != nil {
return err
}
disposition := fmt.Sprintf("attachment; filename=patients-%d.csv", time.Now().Unix())
ec.Response().Header().Set(echo.HeaderContentDisposition, disposition)
ec.Response().Header().Set(echo.HeaderContentType, "text/csv")
return exporter.Write(ctx, ec.Response())
}
30 changes: 29 additions & 1 deletion auth/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/tidepool-org/clinic/clinicians"
"github.com/tidepool-org/clinic/clinics"
internalErrs "github.com/tidepool-org/clinic/errors"
"go.uber.org/zap"

Expand All @@ -36,7 +37,7 @@ type RequestAuthorizer interface {
EvaluatePolicy(context.Context, map[string]interface{}) error
}

func NewRequestAuthorizer(clinicians clinicians.Service, logger *zap.SugaredLogger) (RequestAuthorizer, error) {
func NewRequestAuthorizer(clinicians clinicians.Service, clinics clinics.Service, logger *zap.SugaredLogger) (RequestAuthorizer, error) {
compiler, err := ast.CompileModules(map[string]string{
"policy.rego": authzPolicy,
})
Expand All @@ -46,13 +47,15 @@ func NewRequestAuthorizer(clinicians clinicians.Service, logger *zap.SugaredLogg

return &embeddedOpaAuthorizer{
clinicians: clinicians,
clinics: clinics,
logger: logger,
policy: compiler,
}, nil
}

type embeddedOpaAuthorizer struct {
clinicians clinicians.Service
clinics clinics.Service
logger *zap.SugaredLogger
policy *ast.Compiler
}
Expand All @@ -75,6 +78,18 @@ func (e *embeddedOpaAuthorizer) Authorize(ctx context.Context, input *openapi3fi
in["clinician"] = clinicianStruct.Map()
}

if input.RequestValidationInput.PathParams[clinicIdPathParameter] != "" && strings.HasSuffix(input.RequestValidationInput.Route.Path, "/export/patients") {
clinic, err := e.getClinicRecord(ctx, input)
if err != nil {
return err
}
if clinic != nil {
clinicStruct := structs.New(*clinic)
clinicStruct.TagName = "bson"
in["clinic"] = clinicStruct.Map()
}
}

return e.EvaluatePolicy(ctx, in)
}

Expand Down Expand Up @@ -134,3 +149,16 @@ func (e *embeddedOpaAuthorizer) getClinicianRecord(ctx context.Context, input *o

return clinician, nil
}

func (e *embeddedOpaAuthorizer) getClinicRecord(ctx context.Context, input *openapi3filter.AuthenticationInput) (*clinics.Clinic, error) {
clinicId := input.RequestValidationInput.PathParams[clinicIdPathParameter]
if clinicId == "" {
return nil, nil
}
clinic, err := e.clinics.Get(ctx, clinicId)
if err != nil && !errors.Is(err, clinics.ErrNotFound) {
return nil, err
}

return clinic, nil
}
69 changes: 68 additions & 1 deletion auth/authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ var clinicMember = map[string]interface{}{
"roles": []string{"CLINIC_MEMBER"},
}

var freeTierClinic = map[string]interface{}{
"tier": "tier0100",
}

var standardTierClinic = map[string]interface{}{
"tier": "tier0200",
}

var _ = Describe("Request Authorizer", func() {
var authorizer auth.RequestAuthorizer

BeforeEach(func() {
var err error
authorizer, err = auth.NewRequestAuthorizer(nil, zap.NewNop().Sugar())
authorizer, err = auth.NewRequestAuthorizer(nil, nil, zap.NewNop().Sugar())
Expect(err).ToNot(HaveOccurred())
})

Expand Down Expand Up @@ -184,6 +192,65 @@ var _ = Describe("Request Authorizer", func() {
Expect(err).ToNot(HaveOccurred())
})

It("allow clinic admins of supported tiers to export a patient list", func() {
input := map[string]interface{}{
"path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"},
"method": "GET",
"auth": map[string]interface{}{
"subjectId": "999999999",
"serverAccess": false,
},
"clinician": clinicAdmin,
"clinic": standardTierClinic,
}
err := authorizer.EvaluatePolicy(context.Background(), input)
Expect(err).ToNot(HaveOccurred())
})

It("allow clinic members of supported tiers to export a patient list", func() {
input := map[string]interface{}{
"path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"},
"method": "GET",
"auth": map[string]interface{}{
"subjectId": "999999999",
"serverAccess": false,
},
"clinician": clinicMember,
"clinic": standardTierClinic,
}
err := authorizer.EvaluatePolicy(context.Background(), input)
Expect(err).ToNot(HaveOccurred())
})

It("prevents clinicians in unsupported tiers from generating a patient list export", func() {
input := map[string]interface{}{
"path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"},
"method": "GET",
"auth": map[string]interface{}{
"subjectId": "999999999",
"serverAccess": false,
},
"clinician": clinicMember,
"clinic": freeTierClinic,
}
err := authorizer.EvaluatePolicy(context.Background(), input)
Expect(err).To(Equal(auth.ErrUnauthorized))
})

It("prevents users from generating a patient list export", func() {
input := map[string]interface{}{
"path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"},
"method": "GET",
"auth": map[string]interface{}{
"subjectId": "999999999",
"serverAccess": false,
},
"clinic": standardTierClinic,
}
err := authorizer.EvaluatePolicy(context.Background(), input)
Expect(err).To(Equal(auth.ErrUnauthorized))
})

It("prevents users to migrate patients to a clinic", func() {
input := map[string]interface{}{
"path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "patients", "12345"},
Expand Down
20 changes: 20 additions & 0 deletions auth/policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ write_access_roles := {
"CLINIC_ADMIN"
}

patient_export_supported_tiers := {
"tier0200",
"tier0300",
"tier0400",
}
clinic_tier := { input.clinic.tier }

# convert clinician roles to set
clinician_roles := { x | x = input.clinician.roles[_] }

Expand All @@ -28,6 +35,10 @@ clinician_has_write_access {
count(clinician_roles & write_access_roles) > 0
}

clinic_tier_supports_patient_export {
count(clinic_tier & patient_export_supported_tiers) > 0
}

default allow = false

# Allow backend services to list all clinics
Expand Down Expand Up @@ -426,6 +437,15 @@ allow {
input.path = ["v1", "clinics", _, "patients", _]
}

# Allow currently authenticated clinician to get patient list export
# GET /v1/clinics/:clinicId/export/patients
allow {
input.method == "GET"
input.path = ["v1", "clinics", _, "export", "patients"]
clinician_has_read_access
clinic_tier_supports_patient_export
}

# Allow backend services to create a patient from existing user
# POST /v1/clinics/:clinicId/patients/:patientId
allow {
Expand Down
Loading