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

CSS-5672 Adds declared caveats to the discharge macaroon. #1047

Merged
Merged
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
159 changes: 159 additions & 0 deletions discharger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright 2023 Canonical Ltd.

package jimm

import (
"context"
"net/http"
"strings"
"time"

"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
jjmacaroon "github.com/juju/juju/core/macaroon"
"github.com/juju/names/v4"
"github.com/juju/zaputil/zapctx"
"go.uber.org/zap"

"github.com/canonical/jimm/internal/db"
"github.com/canonical/jimm/internal/dbmodel"
"github.com/canonical/jimm/internal/errors"
"github.com/canonical/jimm/internal/openfga"
ofganames "github.com/canonical/jimm/internal/openfga/names"
jimmnames "github.com/canonical/jimm/pkg/names"
)

var defaultDischargeExpiry = 15 * time.Minute
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better having these functions moved out


func newMacaroonDischarger(p Params, db *db.Database, ofgaClient *openfga.OFGAClient) (*macaroonDischarger, error) {
var kp bakery.KeyPair
if p.PublicKey == "" || p.PrivateKey == "" {
generatedKP, err := bakery.GenerateKey()
if err != nil {
return nil, errors.E(err, "failed to generate a bakery keypair")
}
kp = *generatedKP
} else {
if err := kp.Private.UnmarshalText([]byte(p.PrivateKey)); err != nil {
return nil, errors.E(err, "cannot unmarshal private key")
}
if err := kp.Public.UnmarshalText([]byte(p.PublicKey)); err != nil {
return nil, errors.E(err, "cannot unmarshal public key")
}
}

checker := checkers.New(jjmacaroon.MacaroonNamespace)
b := bakery.New(
bakery.BakeryParams{
Checker: checker,
RootKeyStore: dbrootkeystore.NewRootKeys(100, nil).NewStore(
db,
dbrootkeystore.Policy{
ExpiryDuration: p.MacaroonExpiryDuration,
},
),
Key: &kp,
Location: "jimm " + p.ControllerUUID,
},
)

return &macaroonDischarger{
ofgaClient: ofgaClient,
bakery: b,
kp: kp,
}, nil
}

type macaroonDischarger struct {
ofgaClient *openfga.OFGAClient
bakery *bakery.Bakery
kp bakery.KeyPair
}

// thirdPartyCaveatCheckerFunction returns a function that
// checks third party caveats addressed to this service.
// Caveat format is:
//
// is-<relation name> <user tag> <resource tag>
//
// Examples of caveats are:
//
// is-reader <user tag> <offer tag containing uuid>
// is-consumer <user tag> <offer tag containing uuid>
// is-administrator <user tag> <offer tag containing uuid>
// is-reader <user tag> <model tag containing uuid>
// is-writer <user tag> <model tag containing uuid>
// is-admininistrator <user tag> <model tag containing uuid>
// is-admininistrator <user tag> <controller tag containing uuid>
//
// The discharged macaroon will contain a time-before first party caveat and
// a declared caveat declaring relation to the required entity in form of:
//
// <relation> <entity tag>
//
// Example:
// 1. if the third party caveat condition is:
// is-reader <user tag> <offer tag containing uuid>
// the declared caveat will contain
// reader <offer tag>
// 2. if the third party caveat condition is:
// is-writer <user tag> <model tag containing uuid>
// the declared caveat will contain
// writer <model tag>
func (md *macaroonDischarger) checkThirdPartyCaveat(ctx context.Context, req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo, _ *httpbakery.DischargeToken) ([]checkers.Caveat, error) {
caveatTokens := strings.Split(string(cavInfo.Condition), " ")
if len(caveatTokens) != 3 {
zapctx.Error(ctx, "caveat token length incorrect", zap.Int("length", len(caveatTokens)))
return nil, checkers.ErrCaveatNotRecognized
}
relationString := caveatTokens[0]
userTagString := caveatTokens[1]
objectTagString := caveatTokens[2]

if !strings.HasPrefix(relationString, "is-") {
zapctx.Error(ctx, "caveat token relation string missing prefix")
return nil, checkers.ErrCaveatNotRecognized
}
relationString = strings.TrimPrefix(relationString, "is-")
relation, err := ofganames.ParseRelation(relationString)
if err != nil {
zapctx.Error(ctx, "caveat token relation invalid", zap.Error(err))
return nil, checkers.ErrCaveatNotRecognized
}

userTag, err := names.ParseUserTag(userTagString)
if err != nil {
zapctx.Error(ctx, "failed to parse caveat user tag", zap.Error(err))
return nil, checkers.ErrCaveatNotRecognized
}

objectTag, err := jimmnames.ParseTag(objectTagString)
if err != nil {
zapctx.Error(ctx, "failed to parse caveat object tag", zap.Error(err))
return nil, checkers.ErrCaveatNotRecognized
}

user := openfga.NewUser(
&dbmodel.User{
Username: userTag.Id(),
},
md.ofgaClient,
)

allowed, err := openfga.CheckRelation(ctx, user, objectTag, relation)
if err != nil {
zapctx.Error(ctx, "failed to check request caveat relation", zap.Error(err))
return nil, errors.E(err)
}

if allowed {
return []checkers.Caveat{
checkers.DeclaredCaveat(relationString, objectTagString),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main bit that changed right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

checkers.TimeBeforeCaveat(time.Now().Add(defaultDischargeExpiry)),
}, nil
}
zapctx.Debug(ctx, "macaroon dishcharge denied", zap.String("user", user.Username), zap.String("object", objectTag.Id()))
return nil, httpbakery.ErrPermissionDenied
}
100 changes: 7 additions & 93 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
cofga "github.com/canonical/ofga"
"github.com/go-chi/chi/v5"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
Expand Down Expand Up @@ -49,7 +48,6 @@ import (
"github.com/canonical/jimm/internal/servermon"
"github.com/canonical/jimm/internal/vault"
"github.com/canonical/jimm/internal/wellknownapi"
jimmnames "github.com/canonical/jimm/pkg/names"
)

const (
Expand Down Expand Up @@ -348,31 +346,21 @@ func NewService(ctx context.Context, p Params) (*Service, error) {
// to enable Juju controllers to check for permissions using a macaroon-based workflow (atm only
// for cross model relations).
func (s *Service) setupDischarger(p Params, openFGAclient *openfga.OFGAClient) (*bakery.KeyPair, *http.ServeMux, error) {
var kp bakery.KeyPair
if p.PublicKey == "" || p.PrivateKey == "" {
generatedKP, err := bakery.GenerateKey()
if err != nil {
return nil, nil, errors.E(err, "failed to generate a bakery keypair")
}
kp = *generatedKP
} else {
if err := kp.Private.UnmarshalText([]byte(p.PrivateKey)); err != nil {
return nil, nil, errors.E(err, "cannot unmarshal private key")
}
if err := kp.Public.UnmarshalText([]byte(p.PublicKey)); err != nil {
return nil, nil, errors.E(err, "cannot unmarshal public key")
}
macaroonDischarger, err := newMacaroonDischarger(p, &s.jimm.Database, openFGAclient)
if err != nil {
return nil, nil, errors.E(err)
}

discharger := httpbakery.NewDischarger(
httpbakery.DischargerParams{
Key: &kp,
Checker: httpbakery.ThirdPartyCaveatCheckerFunc(s.thirdPartyCaveatCheckerFunction(openFGAclient)),
Key: &macaroonDischarger.kp,
Checker: httpbakery.ThirdPartyCaveatCheckerFunc(macaroonDischarger.checkThirdPartyCaveat),
},
)
dischargeMux := http.NewServeMux()
discharger.AddMuxHandlers(dischargeMux, localDischargePath)

return &kp, dischargeMux, nil
return &macaroonDischarger.kp, dischargeMux, nil
}

func openDB(ctx context.Context, dsn string) (*gorm.DB, error) {
Expand Down Expand Up @@ -576,77 +564,3 @@ func ensureControllerAdministrators(ctx context.Context, client *openfga.OFGACli
}
return client.AddRelation(ctx, tuples...)
}

var defaultDischargeExpiry = 15 * time.Minute

// thirdPartyCaveatCheckerFunction returns a function that
// checks third party caveats addressed to this service.
// Caveat format is:
//
// is-<relation name> <user tag> <resource tag>
//
// Examples of caveats are:
//
// is-reader <user tag> <offer tag containing uuid>
// is-consumer <user tag> <offer tag containing uuid>
// is-administrator <user tag> <offer tag containing uuid>
// is-reader <user tag> <model tag containing uuid>
// is-writer <user tag> <model tag containing uuid>
// is-admininistrator <user tag> <model tag containing uuid>
// is-admininistrator <user tag> <controller tag containing uuid>
func (s *Service) thirdPartyCaveatCheckerFunction(ofgaClient *openfga.OFGAClient) func(ctx context.Context, req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo, _ *httpbakery.DischargeToken) ([]checkers.Caveat, error) {
return func(ctx context.Context, req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo, _ *httpbakery.DischargeToken) ([]checkers.Caveat, error) {
caveatTokens := strings.Split(string(cavInfo.Condition), " ")
if len(caveatTokens) != 3 {
zapctx.Error(ctx, "caveat token length incorrect", zap.Int("length", len(caveatTokens)))
return nil, checkers.ErrCaveatNotRecognized
}
relationString := caveatTokens[0]
userTagString := caveatTokens[1]
objectTagString := caveatTokens[2]

if !strings.HasPrefix(relationString, "is-") {
zapctx.Error(ctx, "caveat token relation string missing prefix")
return nil, checkers.ErrCaveatNotRecognized
}
relationString = strings.TrimPrefix(relationString, "is-")
relation, err := ofganames.ParseRelation(relationString)
if err != nil {
zapctx.Error(ctx, "caveat token relation invalid", zap.Error(err))
return nil, checkers.ErrCaveatNotRecognized
}

userTag, err := names.ParseUserTag(userTagString)
if err != nil {
zapctx.Error(ctx, "failed to parse caveat user tag", zap.Error(err))
return nil, checkers.ErrCaveatNotRecognized
}

objectTag, err := jimmnames.ParseTag(objectTagString)
if err != nil {
zapctx.Error(ctx, "failed to parse caveat object tag", zap.Error(err))
return nil, checkers.ErrCaveatNotRecognized
}

user := openfga.NewUser(
&dbmodel.User{
Username: userTag.Id(),
},
ofgaClient,
)

allowed, err := openfga.CheckRelation(ctx, user, objectTag, relation)
if err != nil {
zapctx.Error(ctx, "failed to check request caveat relation", zap.Error(err))
return nil, errors.E(err)
}

if allowed {
return []checkers.Caveat{
checkers.TimeBeforeCaveat(time.Now().Add(defaultDischargeExpiry)),
}, nil
}
zapctx.Debug(ctx, "macaroon dishcharge denied", zap.String("user", user.Username), zap.String("object", objectTag.Id()))
return nil, httpbakery.ErrPermissionDenied
}
}
Loading