diff options
| author | mo khan <mo@mokhan.ca> | 2025-03-15 15:20:53 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-03-15 15:20:53 -0600 |
| commit | b27894fcfee8a8422ca191ccd87f641eb8befcf0 (patch) | |
| tree | 503b19478f05ca2433082a3c9838e0c6ae401772 | |
| parent | 80f1b83544b3482cbcdab8cdf521a92f2afdfa16 (diff) | |
refactor: authorize unsigned JWT in requests
| -rwxr-xr-x | bin/api | 26 | ||||
| -rwxr-xr-x | bin/idp | 14 | ||||
| -rwxr-xr-x | bin/ui | 8 | ||||
| -rw-r--r-- | cmd/gtwy/main.go | 49 | ||||
| -rw-r--r-- | go.mod | 17 | ||||
| -rw-r--r-- | go.sum | 40 | ||||
| -rw-r--r-- | pkg/app/app.go | 44 | ||||
| -rw-r--r-- | pkg/app/routes.go | 17 | ||||
| -rw-r--r-- | pkg/authz/token.go | 30 | ||||
| -rw-r--r-- | policy.csv | 15 | ||||
| -rw-r--r-- | test/e2e_test.go | 43 |
11 files changed, 201 insertions, 102 deletions
@@ -55,6 +55,10 @@ class Entity def to_h @attributes end + + def to_gid + ::GlobalID.create(self, app: "example") + end end class Organization < Entity @@ -73,11 +77,11 @@ module HTTPHelpers authorization = Rack::Auth::AbstractRequest.new(request.env) return false unless authorization.provided? - response = rpc.allowed( + response = rpc.allowed({ subject: authorization.params, permission: permission, - resource: ::GlobalID.create(resource, app: "example").to_s - ) + resource: resource.to_gid.to_s, + }, headers: { 'Authorization' => "Bearer #{authorization.params}"}) response.error.nil? && response.data.result end @@ -93,11 +97,11 @@ module HTTPHelpers http_response(code: 201, body: JSON.pretty_generate(body.to_h)) end - def json_unauthorized(permission) + def json_unauthorized(permission, resource) http_response(code: 401, body: JSON.pretty_generate({ error: { code: 401, - message: "`#{permission}` is required", + message: "`#{permission}` is required on `#{resource.to_gid}`", } })) end @@ -128,15 +132,21 @@ class API when "/organizations", "/organizations.json" return json_ok(Organization.all.map(&:to_h)) when "/projects", "/projects.json" - return json_ok(Project.all.map(&:to_h)) + resource = Organization.default + if authorized?(request, :read_project, resource) + return json_ok(Project.all.map(&:to_h)) + else + return json_unauthorized(:read_project, resource) + end end when Rack::POST case request.path when "/projects", "/projects.json" - if authorized?(request, :create_project, Organization.default) + resource = Organization.default + if authorized?(request, :create_project, resource) return json_created(Project.create!(JSON.parse(request.body.read, symbolize_names: true))) else - return json_unauthorized(:create_project) + return json_unauthorized(:create_project, resource) end end end @@ -322,6 +322,7 @@ module Authz class OrganizationPolicy < DeclarativePolicy::Base condition(:owner) { true } + rule { owner }.enable :read_project rule { owner }.enable :create_project end @@ -334,9 +335,9 @@ module Authz def to_jwt [ - Base64.strict_encode64(JSON.generate({alg: "RS256", typ: "JWT"})), + Base64.strict_encode64(JSON.generate(alg: "none")), Base64.strict_encode64(JSON.generate(claims)), - Base64.strict_encode64(JSON.generate({})), + "" ].join(".") end end @@ -354,8 +355,13 @@ module Authz def can?(request) subject = subject_of(request.subject) resource = resource_from(request.resource) + permission = request.permission.to_sym + policy = DeclarativePolicy.policy_for(subject, resource) - policy.can?(request.permission.to_sym) + policy.can?(permission) + rescue StandardError => error + puts error.inspect + false end def subject_of(token) @@ -371,7 +377,7 @@ module Authz def from_jwt(token) token .split('.', 3) - .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) } + .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) rescue {} } end end end @@ -108,13 +108,7 @@ class UI } ) end - if response.code.to_i == 200 - [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate( - request.params.merge(JSON.parse(response.body)) - )]] - else - [response.code, response.header, [response.body]] - end + [response.code, response.header, [response.body]] end def saml_post_to_idp(request) diff --git a/cmd/gtwy/main.go b/cmd/gtwy/main.go index 3563d93..f2a7d2e 100644 --- a/cmd/gtwy/main.go +++ b/cmd/gtwy/main.go @@ -1,57 +1,12 @@ package main import ( - "fmt" "log" - "net" - "net/http" - "github.com/casbin/casbin/v2" "github.com/xlgmokha/x/pkg/env" - "github.com/xlgmokha/x/pkg/x" - "gitlab.com/mokhax/spike/pkg/authz" - "gitlab.com/mokhax/spike/pkg/cfg" - "gitlab.com/mokhax/spike/pkg/prxy" - "gitlab.com/mokhax/spike/pkg/srv" + "gitlab.com/mokhax/spike/pkg/app" ) -func WithCasbin() authz.Authorizer { - enforcer := x.Must(casbin.NewEnforcer("model.conf", "policy.csv")) - - return authz.AuthorizerFunc(func(r *http.Request) bool { - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - return false - } - - subject := "71cbc18e-bd41-4229-9ad2-749546a2a4a7" // TODO:: unpack sub claim in JWT - ok, err := enforcer.Enforce(subject, host, r.Method, r.URL.Path) - if err != nil { - fmt.Printf("%v\n", err) - return false - } - - fmt.Printf("%v: %v %v%v\n", ok, r.Method, host, r.URL.Path) - return ok - }) -} - -func WithRoutes() cfg.Option { - return func(c *cfg.Config) { - mux := http.NewServeMux() - mux.Handle("/", authz.HTTP(WithCasbin(), prxy.New(map[string]string{ - "idp.example.com": "http://localhost:8282", - "ui.example.com": "http://localhost:8283", - "api.example.com": "http://localhost:8284", - }))) - - cfg.WithMux(mux)(c) - } -} - func main() { - log.Fatal(srv.Run(cfg.New( - env.Fetch("BIND_ADDR", ":8080"), - WithRoutes(), - ))) + log.Fatal(app.Start(env.Fetch("BIND_ADDR", ":8080"))) } @@ -3,27 +3,36 @@ module gitlab.com/mokhax/spike go 1.24.0 require ( - github.com/casbin/casbin/v2 v2.103.0 + github.com/casbin/casbin/v3 v3.0.0-beta.7 + github.com/lestrrat-go/jwx/v3 v3.0.0-alpha3 github.com/magefile/mage v1.15.0 github.com/playwright-community/playwright-go v0.5001.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.10.0 github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8 golang.org/x/oauth2 v0.28.0 ) require ( + github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect github.com/arthurnn/twirp-ruby v1.13.0 // indirect - github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect - github.com/casbin/govaluate v1.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.7.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/jsonapi v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sys v0.31.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -1,23 +1,23 @@ +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/arthurnn/twirp-ruby v1.13.0 h1:j0T7I5oxe2niKFdfjiiCmkiydwYeegrbwVMs+Gajm6M= github.com/arthurnn/twirp-ruby v1.13.0/go.mod h1:1fVOQuSLzwXoPi9/ejlDYG3roilJIPAZN2sw+A3o48o= -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic= -github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= -github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= -github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/casbin/v3 v3.0.0-beta.7 h1:siS3e6cRtuyFlshUgJfw0wnWuK3z3U/ald0C8Jtof24= +github.com/casbin/casbin/v3 v3.0.0-beta.7/go.mod h1:69HoI+h4yMUTydUMxT7VQh7FgGpoJsB/ZskkVGcvasQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k= github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -30,6 +30,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 h1:pzDjP9dSONCFQC/AE3mWUnHILGiYPiMKzQIS+weKJXA= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1/go.mod h1:wdsgouffPvWPEYh8t7PRH/PidR5sfVqt0na4Nhj60Ms= +github.com/lestrrat-go/jwx/v3 v3.0.0-alpha3 h1:HHT8iW+UcPBgBr5A3soZQQsL5cBor/u6BkLB+wzY/R0= +github.com/lestrrat-go/jwx/v3 v3.0.0-alpha3/go.mod h1:ak32WoNtHE0aLowVWBcCvXngcAnW4tuC0YhFwOr/kwc= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -42,19 +52,24 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8 h1:Hmyf8pgNUs3l8TW0YdUarBVAU+hWX87efBukspg4nWc= github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8/go.mod h1:C9MUZ3A7PTPbrLNTvu2lKhpM0dFpPHt5yH8YGuYzmKQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -73,6 +88,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -85,7 +102,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..fd6a3f1 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,44 @@ +package app + +import ( + "fmt" + "net" + "net/http" + + "github.com/casbin/casbin/v3" + "github.com/xlgmokha/x/pkg/x" + "gitlab.com/mokhax/spike/pkg/authz" + "gitlab.com/mokhax/spike/pkg/cfg" + "gitlab.com/mokhax/spike/pkg/srv" +) + +func WithCasbin() authz.Authorizer { + enforcer := x.Must(casbin.NewEnforcer("model.conf", "policy.csv")) + + return authz.AuthorizerFunc(func(r *http.Request) bool { + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + return false + } + + subject, found := authz.TokenFrom(r).Subject() + if !found { + subject = "*" + } + ok, err := enforcer.Enforce(subject, host, r.Method, r.URL.Path) + if err != nil { + fmt.Printf("%v\n", err) + return false + } + + fmt.Printf("%v: %v -> %v %v%v\n", ok, subject, r.Method, host, r.URL.Path) + return ok + }) +} + +func Start(bindAddr string) error { + return srv.Run(cfg.New( + bindAddr, + cfg.WithMux(authz.HTTP(WithCasbin(), Routes())), + )) +} diff --git a/pkg/app/routes.go b/pkg/app/routes.go new file mode 100644 index 0000000..9cfa979 --- /dev/null +++ b/pkg/app/routes.go @@ -0,0 +1,17 @@ +package app + +import ( + "net/http" + + "gitlab.com/mokhax/spike/pkg/prxy" +) + +func Routes() http.Handler { + mux := http.NewServeMux() + mux.Handle("/", prxy.New(map[string]string{ + "idp.example.com": "http://localhost:8282", + "ui.example.com": "http://localhost:8283", + "api.example.com": "http://localhost:8284", + })) + return mux +} diff --git a/pkg/authz/token.go b/pkg/authz/token.go new file mode 100644 index 0000000..1822a21 --- /dev/null +++ b/pkg/authz/token.go @@ -0,0 +1,30 @@ +package authz + +import ( + "fmt" + "net/http" + "strings" + + "github.com/lestrrat-go/jwx/v3/jwt" +) + +func TokenFrom(r *http.Request) jwt.Token { + authorization := r.Header.Get("Authorization") + if authorization == "" || !strings.Contains(authorization, "Bearer") { + return jwt.New() + } + + token, err := jwt.ParseRequest(r, + jwt.WithContext(r.Context()), + jwt.WithHeaderKey("Authorization"), + jwt.WithValidate(false), // TODO:: Connect this to a JSON Web Key Set + jwt.WithVerify(false), // TODO:: Connect this to a JSON Web Key Set + ) + + if err != nil { + fmt.Printf("error: %v\n", err) + return jwt.New() + } + + return token +} @@ -1,8 +1,11 @@ -p, "\A[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /* +p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /*.json p, *, *, (GET)|(HEAD), /health p, *, *, GET, /.well-known/* -p, *, idp.example.com, (GET)|(POST), /oauth/* -p, *, idp.example.com, (GET)|(POST), /saml/* -p, *, ui.example.com, (GET)|(POST), /oauth/* -p, *, ui.example.com, (GET)|(POST), /saml/* -p, 71cbc18e-bd41-4229-9ad2-749546a2a4a7, *, *, /* +p, *, *, GET, /favicon.ico +p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", idp.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /twirp/authx.rpc.* +p, *, idp.example.com, (GET)|(POST), /oauth* +p, *, idp.example.com, (GET)|(POST), /saml* +p, *, idp.example.com, (GET)|(POST), /sessions* +p, *, ui.example.com, (GET)|(POST), /oauth* +p, *, ui.example.com, (GET)|(POST), /oidc* +p, *, ui.example.com, (GET)|(POST), /saml* diff --git a/test/e2e_test.go b/test/e2e_test.go index 7e98b1b..16ea4be 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -11,6 +11,7 @@ import ( "github.com/playwright-community/playwright-go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/xlgmokha/x/pkg/env" "github.com/xlgmokha/x/pkg/serde" "github.com/xlgmokha/x/pkg/x" @@ -76,20 +77,34 @@ func TestAuthx(t *testing.T) { assert.Contains(t, page.URL(), "http://ui.example.com:8080/oauth/callback") content := x.Must(page.Locator("pre").First().InnerText()) item := x.Must(serde.FromJSON[oauth2.Token](strings.NewReader(content))) - assert.NotEmpty(t, item.AccessToken) - assert.Equal(t, "Bearer", item.TokenType) - assert.NotEmpty(t, item.RefreshToken) + require.NotEmpty(t, item.AccessToken) + require.Equal(t, "Bearer", item.TokenType) + require.NotEmpty(t, item.RefreshToken) - t.Run("lists all the organizations", func(t *testing.T) { + t.Run("GET http://api.example.com:8080/organizations.json", func(t *testing.T) { response := x.Must(http.Get("http://api.example.com:8080/organizations.json")) - assert.Equal(t, http.StatusOK, response.StatusCode) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + }) + + t.Run("GET http://api.example.com:8080/organizations.json with Authorization", func(t *testing.T) { + request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/organizations.json", nil)) + request.Header.Add("Authorization", "Bearer "+item.AccessToken) + response := x.Must(client.Do(request)) + require.Equal(t, http.StatusOK, response.StatusCode) organizations := x.Must(serde.FromJSON[[]map[string]string](response.Body)) assert.NotNil(t, organizations) }) - t.Run("lists all the projects", func(t *testing.T) { + t.Run("GET http://api.example.com:8080/projects.json", func(t *testing.T) { response := x.Must(http.Get("http://api.example.com:8080/projects.json")) - assert.Equal(t, http.StatusOK, response.StatusCode) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + }) + + t.Run("GET http://api.example.com:8080/projects.json with Authorization", func(t *testing.T) { + request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/projects.json", nil)) + request.Header.Add("Authorization", "Bearer "+item.AccessToken) + response := x.Must(client.Do(request)) + require.Equal(t, http.StatusOK, response.StatusCode) projects := x.Must(serde.FromJSON[[]map[string]string](response.Body)) assert.NotNil(t, projects) }) @@ -100,18 +115,18 @@ func TestAuthx(t *testing.T) { request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://api.example.com:8080/projects", io)) request.Header.Add("Authorization", "Bearer "+item.AccessToken) response := x.Must(client.Do(request)) - assert.Equal(t, http.StatusCreated, response.StatusCode) + require.Equal(t, http.StatusCreated, response.StatusCode) project := x.Must(serde.FromJSON[map[string]string](response.Body)) assert.Equal(t, "example", project["name"]) }) - t.Run("creates another projects", func(t *testing.T) { + t.Run("creates another project", func(t *testing.T) { io := bytes.NewBuffer(nil) assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "example2"})) request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://api.example.com:8080/projects.json", io)) request.Header.Add("Authorization", "Bearer "+item.AccessToken) response := x.Must(client.Do(request)) - assert.Equal(t, http.StatusCreated, response.StatusCode) + require.Equal(t, http.StatusCreated, response.StatusCode) project := x.Must(serde.FromJSON[map[string]string](response.Body)) assert.Equal(t, "example2", project["name"]) }) @@ -121,7 +136,7 @@ func TestAuthx(t *testing.T) { t.Run("OAuth", func(t *testing.T) { t.Run("GET /.well-known/oauth-authorization-server", func(t *testing.T) { response := x.Must(client.Get("http://idp.example.com:8080/.well-known/oauth-authorization-server")) - assert.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, http.StatusOK, response.StatusCode) metadata := x.Must(serde.FromJSON[map[string]interface{}](response.Body)) assert.Equal(t, "http://idp.example.com:8080/.well-known/oauth-authorization-server", metadata["issuer"]) assert.Equal(t, "http://idp.example.com:8080/oauth/authorize", metadata["authorization_endpoint"]) @@ -149,7 +164,7 @@ func TestAuthx(t *testing.T) { t.Run("GET /.well-known/openid-configuration", func(t *testing.T) { response := x.Must(client.Get("http://idp.example.com:8080/.well-known/openid-configuration")) - assert.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, http.StatusOK, response.StatusCode) metadata := x.Must(serde.FromJSON[map[string]interface{}](response.Body)) assert.Equal(t, "http://idp.example.com:8080/.well-known/oauth-authorization-server", metadata["issuer"]) assert.Equal(t, "http://idp.example.com:8080/oauth/authorize", metadata["authorization_endpoint"]) @@ -230,14 +245,14 @@ func TestAuthx(t *testing.T) { t.Run("token is usable against REST API", func(t *testing.T) { client := conf.Client(ctx, credentials) response := x.Must(client.Get("http://api.example.com:8080/projects.json")) - assert.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, http.StatusOK, response.StatusCode) projects := x.Must(serde.FromJSON[[]map[string]string](response.Body)) assert.NotNil(t, projects) io := bytes.NewBuffer(nil) assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "foo"})) response = x.Must(client.Post("http://api.example.com:8080/projects", "application/json", io)) - assert.Equal(t, http.StatusCreated, response.StatusCode) + require.Equal(t, http.StatusCreated, response.StatusCode) project := x.Must(serde.FromJSON[map[string]string](response.Body)) assert.Equal(t, "foo", project["name"]) }) |
