Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Authz enhancement #48

Draft
wants to merge 16 commits into
base: releases/v4.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/api/contexts/contexts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"gohan/api/services"
variantsService "gohan/api/services/variants"

authzModels "gohan/api/models/authorization"

es7 "github.com/elastic/go-elasticsearch/v7"
"github.com/labstack/echo"
)
Expand All @@ -14,9 +16,11 @@ type (
// an elasticsearch client and other variables
GohanContext struct {
echo.Context
Es7Client *es7.Client
Config *models.Config
IngestionService *services.IngestionService
VariantService *variantsService.VariantService
Es7Client *es7.Client
Config *models.Config
IngestionService *services.IngestionService
VariantService *variantsService.VariantService
RequestedResource authzModels.Resource
RequiredPermissions []authzModels.Permission
}
)
90 changes: 72 additions & 18 deletions src/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
variantsMvc "gohan/api/mvc/variants"
workflowsMvc "gohan/api/mvc/workflows"
"gohan/api/services"
authzServices "gohan/api/services/authorization"
"gohan/api/services/sanitation"
variantsService "gohan/api/services/variants"
"gohan/api/utils"
Expand Down Expand Up @@ -56,8 +57,7 @@ func main() {
"\tDRS Username : %s\n\n"+

"\tAuthorization Enabled : %t\n"+
"\tOIDC Public JWKS Url : %s\n"+
"\tOPA Url : %s\n"+
"\tAuthorization Url : %s\n"+
"\tRequired HTTP Headers: %s\n\n"+

"Running on Port : %s\n",
Expand All @@ -74,8 +74,7 @@ func main() {
cfg.Api.BridgeDirectory, cfg.Drs.BridgeDirectory,
cfg.Drs.Url, cfg.Drs.Username,
cfg.AuthX.IsAuthorizationEnabled,
cfg.AuthX.OidcPublicJwksUrl,
cfg.AuthX.OpaUrl,
cfg.AuthX.AuthorizationUrl,
strings.Split(cfg.AuthX.RequiredHeadersCommaSep, ","),
cfg.Api.Port)
// --
Expand All @@ -91,7 +90,7 @@ func main() {
// rather than have one global http client ?)

// Service Singletons
az := services.NewAuthzService(&cfg)
az := authzServices.NewAuthzService(&cfg)
iz := services.NewIngestionService(es, &cfg)
vs := variantsService.NewVariantService(&cfg)

Expand Down Expand Up @@ -120,9 +119,6 @@ func main() {
}
})

// Global Middleware (optional)
e.Use(az.MandateAuthorizationTokensMiddleware)

// Begin MVC Routes
// -- Root
e.GET("/", func(c echo.Context) error {
Expand All @@ -140,24 +136,56 @@ func main() {
e.GET("/data-types/variant/metadata_schema", dataTypesMvc.GetVariantDataTypeMetadataSchema)

// -- Tables
e.GET("/tables", tablesMvc.GetTables)
e.POST("/tables", tablesMvc.CreateTable)
e.GET("/tables/:id", tablesMvc.GetTables)
e.DELETE("/tables/:id", tablesMvc.DeleteTable)
e.GET("/tables/:id/summary", tablesMvc.GetTableSummary)
e.GET("/tables", tablesMvc.GetTables,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)
e.POST("/tables", tablesMvc.CreateTable,
// middleware
// - authorization
gam.CreateEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)
e.GET("/tables/:id", tablesMvc.GetTables,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)
e.DELETE("/tables/:id", tablesMvc.DeleteTable,
// middleware
// - authorization
gam.DeleteEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)
e.GET("/tables/:id/summary", tablesMvc.GetTableSummary,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)

// -- Variants
e.GET("/variants/overview", variantsMvc.GetVariantsOverview)
e.GET("/variants/overview", variantsMvc.GetVariantsOverview,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)

e.GET("/variants/get/by/variantId", variantsMvc.VariantsGetByVariantId,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.ValidateOptionalChromosomeAttribute,
gam.MandateCalibratedBounds,
gam.MandateCalibratedAlleles,
gam.MandateAssemblyIdAttribute,
gam.ValidatePotentialGenotypeQueryParameter)
e.GET("/variants/get/by/sampleId", variantsMvc.VariantsGetBySampleId,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.ValidateOptionalChromosomeAttribute,
gam.MandateCalibratedBounds,
gam.MandateCalibratedAlleles,
Expand All @@ -167,13 +195,19 @@ func main() {
e.GET("/variants/get/by/documentId", variantsMvc.VariantsGetByDocumentId)

e.GET("/variants/count/by/variantId", variantsMvc.VariantsCountByVariantId,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.ValidateOptionalChromosomeAttribute,
gam.MandateCalibratedBounds,
gam.MandateAssemblyIdAttribute,
gam.ValidatePotentialGenotypeQueryParameter)
e.GET("/variants/count/by/sampleId", variantsMvc.VariantsCountBySampleId,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.ValidateOptionalChromosomeAttribute,
gam.MandateCalibratedBounds,
gam.MandateAssemblyIdAttribute,
Expand All @@ -183,25 +217,45 @@ func main() {
// TODO: refactor (deduplicate) --
e.GET("/variants/ingestion/run", variantsMvc.VariantsIngest,
// middleware
// - authorization
gam.IngestEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.MandateAssemblyIdAttribute,
gam.MandateTableIdAttribute)
e.GET("/variants/ingestion/requests", variantsMvc.GetAllVariantIngestionRequests)
e.GET("/variants/ingestion/stats", variantsMvc.VariantsIngestionStats)

e.GET("/private/variants/ingestion/run", variantsMvc.VariantsIngest,
// middleware
// - authorization
gam.IngestEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.MandateAssemblyIdAttribute,
gam.MandateTableIdAttribute)
e.GET("/private/variants/ingestion/requests", variantsMvc.GetAllVariantIngestionRequests)
// --

// -- Genes
e.GET("/genes/overview", genesMvc.GetGenesOverview)
e.GET("/genes/overview", genesMvc.GetGenesOverview,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)
e.GET("/genes/search", genesMvc.GenesGetByNomenclatureWildcard,
// middleware
// - authorization
gam.QueryEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute,
// - everything else
gam.ValidateOptionalChromosomeAttribute)
e.GET("/genes/ingestion/run", genesMvc.GenesIngest,
// middleware
// - authorization
gam.IngestEverythingPermissionAttribute,
az.ValidateTokenPermissionsAttribute)
e.GET("/genes/ingestion/requests", genesMvc.GetAllGeneIngestionRequests)
e.GET("/genes/ingestion/run", genesMvc.GenesIngest)
e.GET("/genes/ingestion/stats", genesMvc.GenesIngestionStats)

// -- Workflows
Expand Down
55 changes: 55 additions & 0 deletions src/api/middleware/authorizationMiddleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package middleware

import (
"gohan/api/contexts"
authzModels "gohan/api/models/authorization"
authzConstants "gohan/api/models/constants/authorization"

"github.com/labstack/echo"
)

func QueryEverythingPermissionAttribute(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
gc := c.(*contexts.GohanContext)
addResourceEverything(gc)
addPermissions(gc, authzConstants.QUERY, authzConstants.DATA)
return next(gc)
}
}
func CreateEverythingPermissionAttribute(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
gc := c.(*contexts.GohanContext)
addResourceEverything(gc)
addPermissions(gc, authzConstants.CREATE, authzConstants.DATA)
return next(gc)
}
}
func IngestEverythingPermissionAttribute(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
gc := c.(*contexts.GohanContext)
addResourceEverything(gc)
addPermissions(gc, authzConstants.INGEST, authzConstants.DATA)
return next(gc)
}
}
func DeleteEverythingPermissionAttribute(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
gc := c.(*contexts.GohanContext)
addResourceEverything(gc)
addPermissions(gc, authzConstants.DELETE, authzConstants.DATA)
return next(gc)
}
}

// -- helper functions
func addResourceEverything(gc *contexts.GohanContext) {
gc.RequestedResource = authzModels.ResourceEverything{
Everything: true,
}
}
func addPermissions(gc *contexts.GohanContext, verb authzConstants.PermissionVerb, noun authzConstants.PermissionNoun) {
gc.RequiredPermissions = []authzModels.Permission{{
Verb: verb,
Noun: noun,
}}
}
48 changes: 48 additions & 0 deletions src/api/models/authorization/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package authorization

import (
authzConstants "gohan/api/models/constants/authorization"
)

type Resource interface{}
type ResourceEverything struct {
Resource
Everything bool `json:"everything"`
}
type ResourceSpecific struct {
Resource
Project string `json:"project"` // cannot be empty, should be a UUID
Dataset string `json:"dataset"` // maybe empty, use "" to check for 'zero' value
DataType string `json:"data_type"` // maybe empty, use "" to check for 'zero' value
}

type Permission struct {
Verb authzConstants.PermissionVerb
Noun authzConstants.PermissionNoun
Level authzConstants.PermissionLevel
}

type Issuer struct {
Iss string `json:"iss"`
}
type IssuerClient struct {
Issuer
Client string `json:"client"`
}
type IssuerSubject struct {
Issuer
Sub string `json:"sub"`
}

type SubjectEveryone struct {
Everyone bool `json:"everyone"`
}
type SubjectGroup struct {
Group int `json:"group"`
}

type GroupMembershipExpr struct {
Expr []interface{} `json:"expr"`
// TODO: ensure each element is either
// of type '[]interface{}' or 'string'
}
20 changes: 20 additions & 0 deletions src/api/models/constants/authorization/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package authorization

type PermissionVerb string
type PermissionNoun string
type PermissionLevel string

const (
QUERY PermissionVerb = "query"
DOWNLOAD PermissionVerb = "download"
CREATE PermissionVerb = "create"
EDIT PermissionVerb = "edit"
DELETE PermissionVerb = "delete"
INGEST PermissionVerb = "ingest"
ANALYZE PermissionVerb = "analyze"
EXPORT PermissionVerb = "export"
)

const (
DATA PermissionNoun = "data"
)
37 changes: 37 additions & 0 deletions src/api/models/dtos/authorization/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package authorization

import (
"encoding/json"
"fmt"
mauthz "gohan/api/models/authorization"
)

type PermissionRequestDto struct {
RequestedResource mauthz.Resource
RequiredPermissions []mauthz.Permission
}

func (p *PermissionRequestDto) MarshalJSON() ([]byte, error) {
// customize the request serialized format:

// serialize permissions as a simple json list of the contents
// concatenated with a colon ':'
reqPermStrArr := make([]string, 0)
for _, rp := range p.RequiredPermissions {
reqPermStrArr = append(reqPermStrArr, fmt.Sprintf("%s:%s", rp.Verb, rp.Noun))
}
// i.e : '["verb1:noun1", "verb2:noun2", ...]'
// instead of '{"requiredPermissions": [{"verb":<verb1>, "noun": <noun1>},{"verb":<verb2>, "noun": <noun2>}]}'

// - serialize and then deserialize requested resource
rrl, _ := json.Marshal(&p.RequestedResource)
var reqResMap map[string]interface{}
json.Unmarshal([]byte(rrl), &reqResMap)
// - structure the request body using snake case
res := map[string]interface{}{
"requested_resource": reqResMap,
"required_permissions": reqPermStrArr,
}
// - reserialize the request body
return json.Marshal(&res)
}
3 changes: 1 addition & 2 deletions src/api/models/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ type Config struct {

AuthX struct {
IsAuthorizationEnabled bool `yaml:"isAuthorizationEnabled" envconfig:"GOHAN_AUTHZ_ENABLED"`
OidcPublicJwksUrl string `yaml:"oidcPublicJwksUrl" envconfig:"GOHAN_PUBLIC_AUTHN_JWKS_URL"`
OpaUrl string `yaml:"opaUrl" envconfig:"GOHAN_PRIVATE_AUTHZ_URL"`
AuthorizationUrl string `yaml:"authorizationUrl" envconfig:"GOHAN_AUTHZ_URL"`
RequiredHeadersCommaSep string `yaml:"requiredHeadersCommaSep" envconfig:"GOHAN_AUTHZ_REQHEADS"`
} `yaml:"authx"`
}
Loading