Skip to content

Commit

Permalink
Css 10675/authz middleware (#1364)
Browse files Browse the repository at this point in the history
* add authorization for models middleware
  • Loading branch information
SimoneDutto authored Sep 17, 2024
1 parent 14feb2e commit b201bc3
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 0 deletions.
File renamed without changes.
File renamed without changes.
55 changes: 55 additions & 0 deletions internal/middleware/authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2024 Canonical.

package middleware

import (
"net/http"

cofga "github.com/canonical/ofga"
"github.com/go-chi/chi/v5"
"github.com/juju/names/v5"

"github.com/canonical/jimm/v3/internal/jujuapi"
ofganames "github.com/canonical/jimm/v3/internal/openfga/names"
)

// AuthorizeUserForModelAccess extract the user from the context, and checks for permission on the model uuid extracted from the path.
func AuthorizeUserForModelAccess(next http.Handler, jimm jujuapi.JIMM, accessNeeded cofga.Relation) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := IdentityFromContext(ctx)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(err.Error()))
return
}
modelUUID := chi.URLParam(r, "uuid")
if modelUUID == "" {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("cannot find uuid in URL path"))
return
}
modelTag := names.NewModelTag(modelUUID)
switch accessNeeded {
case ofganames.ReaderRelation:
ok, err := user.IsModelReader(ctx, modelTag)
if !ok || err != nil {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("no access to the resource"))
return
}
case ofganames.WriterRelation:
ok, err := user.IsModelWriter(ctx, modelTag)
if !ok || err != nil {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("no access to the resource"))
return
}
default:
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("no access to the resource"))
return
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
112 changes: 112 additions & 0 deletions internal/middleware/authz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2024 Canonical.

package middleware_test

import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"

cofga "github.com/canonical/ofga"
qt "github.com/frankban/quicktest"
"github.com/go-chi/chi/v5"
"github.com/juju/names/v5"

"github.com/canonical/jimm/v3/internal/dbmodel"
jimm_errors "github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/jimmtest"
"github.com/canonical/jimm/v3/internal/jimmtest/mocks"
"github.com/canonical/jimm/v3/internal/middleware"
"github.com/canonical/jimm/v3/internal/openfga"
ofganames "github.com/canonical/jimm/v3/internal/openfga/names"
)

func TestAuthorizeUserForModelAccess(t *testing.T) {
c := qt.New(t)
ctx := context.Background()
testUser := "[email protected]"
jt := jimmtest.JIMM{
LoginService: mocks.LoginService{
LoginWithSessionToken_: func(ctx context.Context, sessionToken string) (*openfga.User, error) {
if sessionToken != "good" {
return nil, jimm_errors.E(jimm_errors.CodeSessionTokenInvalid)
}
user := dbmodel.Identity{Name: testUser}
return &openfga.User{Identity: &user, JimmAdmin: true}, nil
},
},
}
ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(t.Name())
c.Assert(err, qt.IsNil)
bobIdentity, err := dbmodel.NewIdentity("[email protected]")
c.Assert(err, qt.IsNil)
bob := openfga.NewUser(bobIdentity, ofgaClient)
validModelUUID := "54d9f921-c45a-4825-8253-74e7edc28066"
notvalidModelUUID := "54d9f921-c45a-4825-8253-74e7edc28065"
tuples := []openfga.Tuple{
{
Object: ofganames.ConvertTag(bob.ResourceTag()),
Relation: ofganames.AdministratorRelation,
Target: ofganames.ConvertTag(names.NewModelTag(validModelUUID)),
},
{
Object: ofganames.ConvertTag(bob.ResourceTag()),
Relation: ofganames.ReaderRelation,
Target: ofganames.ConvertTag(names.NewModelTag(notvalidModelUUID)),
},
}
err = ofgaClient.AddRelation(ctx, tuples...)
c.Assert(err, qt.IsNil)
tests := []struct {
name string
expectedStatus int
uuidInPath string
permissionRequired cofga.Relation
errorExpected string
}{
{
name: "success",
expectedStatus: http.StatusOK,
permissionRequired: ofganames.WriterRelation,
uuidInPath: validModelUUID,
},
{
name: "no uuid from path",
expectedStatus: http.StatusUnauthorized,
errorExpected: "cannot find uuid in URL path",
},
{
name: "not enough permission",
expectedStatus: http.StatusForbidden,
uuidInPath: notvalidModelUUID,
permissionRequired: ofganames.WriterRelation,
errorExpected: "no access to the resource",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := qt.New(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
h := middleware.AuthorizeUserForModelAccess(handler, &jt, tt.permissionRequired)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("uuid", tt.uuidInPath)
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
h.ServeHTTP(w, req.WithContext(middleware.WithIdentity(ctx, bob)))
c.Assert(w.Code, qt.Equals, tt.expectedStatus)
b := w.Result().Body
defer b.Close()
body, err := io.ReadAll(b)
c.Assert(err, qt.IsNil)
if tt.errorExpected != "" {
c.Assert(string(body), qt.Matches, tt.errorExpected)
}
})
}
}
6 changes: 6 additions & 0 deletions internal/middleware/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2024 Canonical.
package middleware

var (
WithIdentity = withIdentity
)

0 comments on commit b201bc3

Please sign in to comment.