Skip to content

Commit

Permalink
Merge branch 'feature-rebac' into CSS-4936-postgres-as-secret-backend
Browse files Browse the repository at this point in the history
  • Loading branch information
kian99 committed Jul 26, 2023
2 parents 8844dff + fe55d77 commit 8895228
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ local/vault/roleid.txt
*.crt
*.key
*.csr
jimmctl
/jimmctl
qa-controller
7 changes: 7 additions & 0 deletions api/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,10 @@ func (c *Client) CrossModelQuery(req *params.CrossModelQueryRequest) (*params.Cr
err := c.caller.APICall("JIMM", 4, "", "CrossModelQuery", req, &response)
return &response, err
}

// PurgeLogs purges logs from the database before the given date.
func (c *Client) PurgeLogs(req *params.PurgeLogsRequest) (*params.PurgeLogsResponse, error) {
var response params.PurgeLogsResponse
err := c.caller.APICall("JIMM", 4, "", "PurgeLogs", req, &response)
return &response, err
}
13 changes: 13 additions & 0 deletions api/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,16 @@ type CrossModelQueryResponse struct {
Results map[string][]any `json:"results"`
Errors map[string][]string `json:"errors"`
}

// PurgeLogsRequest is the request used to purge logs.
type PurgeLogsRequest struct {
// Date is the date before which logs should be purged.
Date time.Time `json:"date"`
}

// PurgeLogsResponse is the response returned by the PurgeLogs method.
// It has one field:
// - DeletedCount - the number of logs that were deleted.
type PurgeLogsResponse struct {
DeletedCount int64 `json:"deleted-count" yaml:"deleted-count"`
}
12 changes: 12 additions & 0 deletions cmd/jimmctl/cmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,15 @@ func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, bClient *

return modelcmd.WrapBase(cmd)
}

func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command {
cmd := &purgeLogsCommand{
store: store,
dialOpts: &jujuapi.DialOpts{
InsecureSkipVerify: true,
BakeryClient: bClient,
},
}

return modelcmd.WrapBase(cmd)
}
127 changes: 127 additions & 0 deletions cmd/jimmctl/cmd/purge_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package cmd

import (
"time"

"github.com/canonical/jimm/api"
apiparams "github.com/canonical/jimm/api/params"
"github.com/canonical/jimm/internal/errors"
"github.com/juju/cmd/v3"
"github.com/juju/gnuflag"
jujuapi "github.com/juju/juju/api"
jujucmd "github.com/juju/juju/cmd"
"github.com/juju/juju/cmd/modelcmd"
"github.com/juju/juju/jujuclient"
)

const purgeLogsDoc = `
purge-audit-logs purges logs from the database before the given date.
Examples:
jimmctl purge-audit-logs 2021-02-03
jimmctl purge-audit-logs 2021-02-03T00
jimmctl purge-audit-logs 2021-02-03T15:04:05Z
`

// NewPurgeLogsCommand returns a command to purge logs.
func NewPurgeLogsCommand() cmd.Command {
cmd := &purgeLogsCommand{}
return modelcmd.WrapBase(cmd)
}

// purgeLogsCommand purges logs.
type purgeLogsCommand struct {
modelcmd.ControllerCommandBase
store jujuclient.ClientStore
dialOpts *jujuapi.DialOpts
out cmd.Output

date time.Time
}

// Info implements Command.Info. It returns the command information.
func (c *purgeLogsCommand) Info() *cmd.Info {
return jujucmd.Info(&cmd.Info{
Name: "purge-audit-logs",
Args: "<ISO8601 date>",
Purpose: "purges audit logs from the database before the given date",
Doc: purgeLogsDoc,
})
}

// Init implements Command.Init. It checks the number of arguments and validates
// the date.
func (c *purgeLogsCommand) Init(args []string) error {
if len(args) != 1 {
return errors.E("expected one argument (ISO8601 date)")
}
// validate date
var err error
c.date, err = parseDate(args[0])
if err != nil {
return errors.E("invalid date. Expected ISO8601 date")
}
return nil
}

// SetFlags implements Command.SetFlags.
func (c *purgeLogsCommand) SetFlags(f *gnuflag.FlagSet) {
c.CommandBase.SetFlags(f)
c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{
"yaml": cmd.FormatYaml,
"json": cmd.FormatJson,
})
}

// Run implements Command.Run. It purges logs from the database before the given
// date.
func (c *purgeLogsCommand) Run(ctx *cmd.Context) error {
currentController, err := c.store.CurrentController()
if err != nil {
return errors.E(err, "could not determine controller")
}

apiCaller, err := c.NewAPIRootWithDialOpts(c.store, currentController, "", c.dialOpts)
if err != nil {
return err
}

client := api.NewClient(apiCaller)
response, err := client.PurgeLogs(&apiparams.PurgeLogsRequest{
Date: c.date,
})
if err != nil {
return errors.E(err)
}
err = c.out.Write(ctx, response)
if err != nil {
return errors.E(err)
}
return nil
}

// parseDate validates the date string is in ISO8601 format. If it is, it
// sets the date field in the command.
func parseDate(date string) (time.Time, error) {
// Define the possible ISO8601 date layouts
layouts := []string{
"2006-01-02T15:04:05-0700",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05",
"2006-01-02T15:04Z",
"2006-01-02",
}

// Try to parse the date string using the defined layouts
for _, layout := range layouts {
date_time, err := time.Parse(layout, date)
if err == nil {
// If parsing was successful, the date is valid
// You can use the parsed time t if needed
return date_time, nil
}
}

// If none of the layouts match, the date is not in the correct format
return time.Time{}, errors.E("invalid date. Expected ISO8601 date")
}
93 changes: 93 additions & 0 deletions cmd/jimmctl/cmd/purge_logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cmd_test

import (
"bytes"
"context"
"time"

"github.com/canonical/jimm/cmd/jimmctl/cmd"
"github.com/canonical/jimm/internal/dbmodel"
"github.com/juju/cmd/v3/cmdtesting"
"github.com/juju/names/v4"
gc "gopkg.in/check.v1"
)

type purgeLogsSuite struct {
jimmSuite
}

var _ = gc.Suite(&purgeLogsSuite{})

func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) {
// alice is superuser
bClient := s.userBakeryClient("alice")
datastring := "2021-01-01T00:00:00Z"
cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring)
c.Assert(err, gc.IsNil)
expected := "deleted-count: 0\n"
actual := cmdCtx.Stdout.(*bytes.Buffer).String()
c.Assert(actual, gc.Equals, expected)
}

func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) {
// alice is superuser
bClient := s.userBakeryClient("alice")
datastring := "13/01/2021"
_, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring)
c.Assert(err, gc.ErrorMatches, `invalid date. Expected ISO8601 date`)

}

func (s *purgeLogsSuite) TestPurgeLogs(c *gc.C) {
// bob is not superuser
bClient := s.userBakeryClient("bob")
_, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), "2021-01-01T00:00:00Z")
c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`)
}

func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) {
// create logs
layouts := []string{
"2006-01-02T15:04:05-0700",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05",
"2006-01-02T15:04Z",
"2006-01-02",
}
for _, layout := range layouts {

ctx := context.Background()
relativeNow := time.Now().AddDate(-1, 0, 0)
ale := dbmodel.AuditLogEntry{
Time: relativeNow.UTC().Round(time.Millisecond),
UserTag: names.NewUserTag("alice@external").String(),
}
ale_past := dbmodel.AuditLogEntry{
Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond),
UserTag: names.NewUserTag("alice@external").String(),
}
ale_future := dbmodel.AuditLogEntry{
Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond),
UserTag: names.NewUserTag("alice@external").String(),
}

err := s.JIMM.Database.Migrate(context.Background(), false)
c.Assert(err, gc.IsNil)
err = s.JIMM.Database.AddAuditLogEntry(ctx, &ale)
c.Assert(err, gc.IsNil)
err = s.JIMM.Database.AddAuditLogEntry(ctx, &ale_past)
c.Assert(err, gc.IsNil)
err = s.JIMM.Database.AddAuditLogEntry(ctx, &ale_future)
c.Assert(err, gc.IsNil)

tomorrow := relativeNow.AddDate(0, 0, 1).Format(layout)
//alice is superuser
bClient := s.userBakeryClient("alice")
cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), tomorrow)
c.Assert(err, gc.IsNil)
// check that logs have been deleted
expectedOutput := "deleted-count: 2\n"
actual := cmdCtx.Stdout.(*bytes.Buffer).String()
c.Assert(actual, gc.Equals, expectedOutput)
}
}
1 change: 1 addition & 0 deletions cmd/jimmctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func NewSuperCommand() *jujucmd.SuperCommand {
jimmcmd.Register(cmd.NewRemoveCloudFromControllerCommand())
jimmcmd.Register(cmd.NewAuthCommand())
jimmcmd.Register(cmd.NewCrossModelQueryCommand())
jimmcmd.Register(cmd.NewPurgeLogsCommand())
return jimmcmd
}

Expand Down
1 change: 1 addition & 0 deletions internal/db/applicationoffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func (d *Database) DeleteApplicationOffer(ctx context.Context, offer *dbmodel.Ap
return nil
}


// ApplicationOfferFilter can be used to find application offers that match certain criteria.
type ApplicationOfferFilter func(*gorm.DB) *gorm.DB

Expand Down
31 changes: 31 additions & 0 deletions internal/db/auditlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,34 @@ func (s *dbSuite) TestDeleteAuditLogsBefore(c *qt.C) {
c.Assert(err, qt.IsNil)
c.Assert(logs, qt.HasLen, 1)
}

func (s *dbSuite) TestPurgeLogsFromDb(c *qt.C) {

ctx := context.Background()
relativeNow := time.Now().AddDate(-1, 0, 0)
ale := dbmodel.AuditLogEntry{
Time: relativeNow.UTC().Round(time.Millisecond),
UserTag: names.NewUserTag("alice@external").String(),
}
ale_past := dbmodel.AuditLogEntry{
Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond),
UserTag: names.NewUserTag("alice@external").String(),
}
ale_future := dbmodel.AuditLogEntry{
Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond),
UserTag: names.NewUserTag("alice@external").String(),
}

err := s.Database.Migrate(context.Background(), false)
c.Assert(err, qt.IsNil)
err = s.Database.AddAuditLogEntry(ctx, &ale)
c.Assert(err, qt.IsNil)
err = s.Database.AddAuditLogEntry(ctx, &ale_past)
c.Assert(err, qt.IsNil)
err = s.Database.AddAuditLogEntry(ctx, &ale_future)
c.Assert(err, qt.IsNil)
deleted_count, err := s.Database.DeleteAuditLogsBefore(ctx, relativeNow.AddDate(0, 0, 1))
// check that logs have been deleted
c.Assert(err, qt.IsNil)
c.Assert(deleted_count, qt.Equals, int64(2))
}
34 changes: 34 additions & 0 deletions internal/jimm/purge_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023 Canonical Ltd.

package jimm

import (
"context"
"time"

"github.com/canonical/jimm/internal/errors"
"github.com/canonical/jimm/internal/openfga"
"github.com/juju/zaputil/zapctx"
"go.uber.org/zap"
)

// PurgeLogs removes all audit logs before the given timestamp. Only JIMM
// administrators can perform this operation. The number of logs purged is
// returned.
func (j *JIMM) PurgeLogs(ctx context.Context, user *openfga.User, before time.Time) (int64, error) {
op := errors.Op("jimm.PurgeLogs")
isJIMMAdmin, err := openfga.IsAdministrator(ctx, user, j.ResourceTag())
if err != nil {
zapctx.Error(ctx, "failed administrator check", zap.Error(err))
return 0, errors.E(op, "failed administrator check", err)
}
if !isJIMMAdmin {
return 0, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}
count, err := j.Database.DeleteAuditLogsBefore(ctx, before)
if err != nil {
zapctx.Error(ctx, "failed to purge logs", zap.Error(err))
return 0, errors.E(op, "failed to purge logs", err)
}
return count, nil
}
15 changes: 15 additions & 0 deletions internal/jujuapi/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func init() {
checkRelationMethod := rpc.Method(r.CheckRelation)
listRelationshipTuplesMethod := rpc.Method(r.ListRelationshipTuples)
crossModelQueryMethod := rpc.Method(r.CrossModelQuery)
purgeLogsMethod := rpc.Method(r.PurgeLogs)

r.AddMethod("JIMM", 2, "DisableControllerUUIDMasking", disableControllerUUIDMaskingMethod)
r.AddMethod("JIMM", 2, "ListControllers", listControllersMethod)
Expand Down Expand Up @@ -79,6 +80,7 @@ func init() {
r.AddMethod("JIMM", 4, "UpdateMigratedModel", updateMigratedModelMethod)
r.AddMethod("JIMM", 4, "AddCloudToController", addCloudToControllerMethod)
r.AddMethod("JIMM", 4, "RemoveCloudFromController", removeCloudFromControllerMethod)
r.AddMethod("JIMM", 4, "PurgeLogs", purgeLogsMethod)
// JIMM ReBAC RPC
r.AddMethod("JIMM", 4, "AddGroup", addGroupMethod)
r.AddMethod("JIMM", 4, "RenameGroup", renameGroupMethod)
Expand Down Expand Up @@ -498,3 +500,16 @@ func (r *controllerRoot) CrossModelQuery(ctx context.Context, req apiparams.Cros
return apiparams.CrossModelQueryResponse{}, errors.E(op, errors.Code("invalid query type"), "unable to query models")
}
}

// PurgeLogs removes all audit log entries older than the specified date.
func (r *controllerRoot) PurgeLogs(ctx context.Context, req apiparams.PurgeLogsRequest) (apiparams.PurgeLogsResponse, error) {
const op = errors.Op("jujuapi.PurgeLogs")

deleted_count, err := r.jimm.PurgeLogs(ctx, r.user, req.Date)
if err != nil {
return apiparams.PurgeLogsResponse{}, errors.E(op, err)
}
return apiparams.PurgeLogsResponse{
DeletedCount: deleted_count,
}, nil
}

0 comments on commit 8895228

Please sign in to comment.