Skip to content

Commit

Permalink
Merge branch 'main' into grady/startrunbe
Browse files Browse the repository at this point in the history
  • Loading branch information
gbdubs committed Jan 2, 2024
2 parents 3bbb73c + b530d5d commit 3dd90da
Show file tree
Hide file tree
Showing 26 changed files with 1,014 additions and 27 deletions.
20 changes: 11 additions & 9 deletions azure/azblob/azblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,47 +103,49 @@ func (c *Client) DeleteBlob(ctx context.Context, uri string) error {

// SignedUploadURL returns a URL that is allowed to upload to the given URI.
// See https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/[email protected]/sas#example-package-UserDelegationSAS
func (c *Client) SignedUploadURL(ctx context.Context, uri string) (string, error) {
func (c *Client) SignedUploadURL(ctx context.Context, uri string) (string, time.Time, error) {
return c.signBlob(ctx, uri, &sas.BlobPermissions{Create: true, Write: true})
}

// SignedDownloadURL returns a URL that is allowed to download the file at the given URI.
// See https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/[email protected]/sas#example-package-UserDelegationSAS
func (c *Client) SignedDownloadURL(ctx context.Context, uri string) (string, error) {
func (c *Client) SignedDownloadURL(ctx context.Context, uri string) (string, time.Time, error) {
return c.signBlob(ctx, uri, &sas.BlobPermissions{Read: true})
}

func (c *Client) signBlob(ctx context.Context, uri string, perms *sas.BlobPermissions) (string, error) {
func (c *Client) signBlob(ctx context.Context, uri string, perms *sas.BlobPermissions) (string, time.Time, error) {
ctr, blb, ok := blob.SplitURI(Scheme, uri)
if !ok {
return "", fmt.Errorf("malformed URI %q is not for Azure", uri)
return "", time.Time{}, fmt.Errorf("malformed URI %q is not for Azure", uri)
}

// The blob component is important, otherwise the signed URL is applicable to the whole container.
if blb == "" {
return "", fmt.Errorf("uri %q did not contain a blob component", uri)
return "", time.Time{}, fmt.Errorf("uri %q did not contain a blob component", uri)
}

now := c.now().UTC().Add(-10 * time.Second)
udc, err := c.getUserDelegationCredential(ctx, now)
if err != nil {
return "", fmt.Errorf("failed to get udc: %w", err)
return "", time.Time{}, fmt.Errorf("failed to get udc: %w", err)
}

expiry := now.Add(15 * time.Minute)

// Create Blob Signature Values with desired permissions and sign with user delegation credential
sasQueryParams, err := sas.BlobSignatureValues{
Protocol: sas.ProtocolHTTPS,
StartTime: now,
ExpiryTime: now.Add(15 * time.Minute),
ExpiryTime: expiry,
Permissions: perms.String(),
ContainerName: ctr,
BlobName: blb,
}.SignWithUserDelegation(udc)
if err != nil {
return "", fmt.Errorf("failed to sign blob: %w", err)
return "", time.Time{}, fmt.Errorf("failed to sign blob: %w", err)
}

return fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s?%s", c.storageAccount, ctr, blb, sasQueryParams.Encode()), nil
return fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s?%s", c.storageAccount, ctr, blb, sasQueryParams.Encode()), expiry, nil
}

func (c *Client) ListBlobs(ctx context.Context, uriPrefix string) ([]string, error) {
Expand Down
2 changes: 2 additions & 0 deletions cmd/server/pactasrv/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ go_library(
name = "pactasrv",
srcs = [
"analysis.go",
"audit_logs.go",
"blobs.go",
"incomplete_upload.go",
"initiative.go",
"initiative_invitation.go",
Expand Down
39 changes: 39 additions & 0 deletions cmd/server/pactasrv/audit_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pactasrv

import (
"context"

"github.com/RMI/pacta/cmd/server/pactasrv/conv"
"github.com/RMI/pacta/oapierr"
api "github.com/RMI/pacta/openapi/pacta"
"go.uber.org/zap"
)

// queries the platform's audit logs
// (POST /audit-logs)
func (s *Server) ListAuditLogs(ctx context.Context, request api.ListAuditLogsRequestObject) (api.ListAuditLogsResponseObject, error) {
// TODO(#12) implement authorization
query, err := conv.AuditLogQueryFromOAPI(request.Body)
if err != nil {
return nil, err
}
// TODO(#12) implement additional authorizations, ensuring for example that:
// - every generated query has reasonable limits + only filters by allowed search terms
// - the actor is allowed to see the audit logs of the actor_owner, but not of other actor_owners
// - initiative admins should be able to see audit logs of the initiative, but not initiative members
// - admins should be able to see all
// This is probably our most important piece of authz-ery, so it should be thoroughly tested.
als, pi, err := s.DB.AuditLogs(s.DB.NoTxn(ctx), query)
if err != nil {
return nil, oapierr.Internal("querying audit logs failed", zap.Error(err))
}
results, err := dereference(conv.AuditLogsToOAPI(als))
if err != nil {
return nil, err
}
return api.ListAuditLogs200JSONResponse{
AuditLogs: results,
Cursor: string(pi.Cursor),
HasNextPage: pi.HasNextPage,
}, nil
}
103 changes: 103 additions & 0 deletions cmd/server/pactasrv/blobs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package pactasrv

import (
"context"
"fmt"

"github.com/RMI/pacta/db"
"github.com/RMI/pacta/oapierr"
api "github.com/RMI/pacta/openapi/pacta"
"github.com/RMI/pacta/pacta"
"go.uber.org/zap"
)

func (s *Server) AccessBlobContent(ctx context.Context, request api.AccessBlobContentRequestObject) (api.AccessBlobContentResponseObject, error) {
actorInfo, err := s.getActorInfoOrFail(ctx)
if err != nil {
return nil, err
}

blobIDs := []pacta.BlobID{}
for _, item := range request.Body.Items {
blobIDs = append(blobIDs, pacta.BlobID(item.BlobId))
}
err404 := oapierr.NotFound("blob not found", zap.Strings("blob_ids", asStrs(blobIDs)))
bos, err := s.DB.BlobContexts(s.DB.NoTxn(ctx), blobIDs)
if err != nil {
if db.IsNotFound(err) {
return nil, err404
}
return nil, oapierr.Internal("error getting blob owners", zap.Error(err), zap.Strings("blob_ids", asStrs(blobIDs)))
}
asMap := map[pacta.BlobID]*pacta.BlobContext{}
for _, boi := range bos {
asMap[boi.BlobID] = boi
}
auditLogs := []*pacta.AuditLog{}
for _, blobID := range blobIDs {
boi := asMap[blobID]
accessAsOwner := boi.PrimaryTargetOwnerID == actorInfo.OwnerID
accessAsAdmin := boi.AdminDebugEnabled && actorInfo.IsAdmin
accessAsSuperAdmin := boi.AdminDebugEnabled && actorInfo.IsSuperAdmin
var actorType pacta.AuditLogActorType
if accessAsOwner {
actorType = pacta.AuditLogActorType_Owner
} else if accessAsAdmin {
actorType = pacta.AuditLogActorType_Admin
} else if accessAsSuperAdmin {
actorType = pacta.AuditLogActorType_SuperAdmin
} else {
// DENY CASE
return nil, err404
}
auditLogs = append(auditLogs, &pacta.AuditLog{
Action: pacta.AuditLogAction_Download,
ActorID: string(actorInfo.UserID),
ActorOwner: &pacta.Owner{ID: actorInfo.OwnerID},
ActorType: actorType,
PrimaryTargetType: boi.PrimaryTargetType,
PrimaryTargetID: boi.PrimaryTargetID,
PrimaryTargetOwner: &pacta.Owner{ID: boi.PrimaryTargetOwnerID},
})
}

blobs, err := s.DB.Blobs(s.DB.NoTxn(ctx), blobIDs)
if err != nil {
if db.IsNotFound(err) {
return nil, err404
}
return nil, oapierr.Internal("error getting blobs", zap.Error(err), zap.Strings("blob_ids", asStrs(blobIDs)))
}

err = s.DB.Transactional(ctx, func(tx db.Tx) error {
for i, al := range auditLogs {
_, err := s.DB.CreateAuditLog(tx, al)
if err != nil {
return fmt.Errorf("creating audit log %d/%d: %w", i+1, len(auditLogs), err)
}
}
return nil
})
if err != nil {
return nil, oapierr.Internal("error creating audit logs - no download URLs generated", zap.Error(err), zap.Strings("blob_ids", asStrs(blobIDs)))
}

// Note, we're not parallelizing this because it is probably not nescessary.
// The majority use case of this endpoint will be the user clicking a download
// button, which will spin as it gets the URL, then turn into a dial as the
// download starts. That allows us to only generate audit logs for true accesses,
// and will typically happen on a single-file basis.
response := api.AccessBlobContentResp{}
for _, blob := range blobs {
url, expiryTime, err := s.Blob.SignedDownloadURL(ctx, string(blob.BlobURI))
if err != nil {
return nil, oapierr.Internal("error getting signed download url", zap.Error(err), zap.String("blob_uri", string(blob.BlobURI)))
}
response.Items = append(response.Items, api.AccessBlobContentRespItem{
BlobId: string(blob.ID),
DownloadUrl: url,
ExpirationTime: expiryTime,
})
}
return api.AccessBlobContent200JSONResponse(response), nil
}
1 change: 1 addition & 0 deletions cmd/server/pactasrv/conv/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ go_library(
importpath = "github.com/RMI/pacta/cmd/server/pactasrv/conv",
visibility = ["//visibility:public"],
deps = [
"//db",
"//oapierr",
"//openapi:pacta_generated",
"//pacta",
Expand Down
8 changes: 8 additions & 0 deletions cmd/server/pactasrv/conv/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ func strPtr[T ~string](t T) *string {
return ptr(string(t))
}

func fromStrs[T ~string](ss []string) []T {
result := make([]T, len(ss))
for i, s := range ss {
result[i] = T(s)
}
return result
}

func ifNil[T any](t *T, fallback T) T {
if t == nil {
return fallback
Expand Down
108 changes: 108 additions & 0 deletions cmd/server/pactasrv/conv/oapi_to_pacta.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package conv

import (
"fmt"
"regexp"

"github.com/RMI/pacta/db"
"github.com/RMI/pacta/oapierr"
api "github.com/RMI/pacta/openapi/pacta"
"github.com/RMI/pacta/pacta"
Expand Down Expand Up @@ -105,3 +107,109 @@ func PortfolioGroupCreateFromOAPI(pg *api.PortfolioGroupCreate, ownerID pacta.Ow
Owner: &pacta.Owner{ID: ownerID},
}, nil
}

func auditLogActionFromOAPI(i api.AuditLogAction) (pacta.AuditLogAction, error) {
return pacta.ParseAuditLogAction(string(i))
}

func auditLogActorTypeFromOAPI(i api.AuditLogActorType) (pacta.AuditLogActorType, error) {
return pacta.ParseAuditLogActorType(string(i))
}

func auditLogTargetTypeFromOAPI(i api.AuditLogTargetType) (pacta.AuditLogTargetType, error) {
return pacta.ParseAuditLogTargetType(string(i))
}

func auditLogQueryWhereFromOAPI(i api.AuditLogQueryWhere) (*db.AuditLogQueryWhere, error) {
result := &db.AuditLogQueryWhere{}
if i.InId != nil {
result.InID = fromStrs[pacta.AuditLogID](*i.InId)
}
if i.MinCreatedAt != nil {
result.MinCreatedAt = *i.MinCreatedAt
}
if i.MaxCreatedAt != nil {
result.MaxCreatedAt = *i.MaxCreatedAt
}
if i.InAction != nil {
as, err := convAll(*i.InAction, auditLogActionFromOAPI)
if err != nil {
return nil, fmt.Errorf("converting audit log query where in action: %w", err)
}
result.InAction = as
}
if i.InActorType != nil {
at, err := convAll(*i.InActorType, auditLogActorTypeFromOAPI)
if err != nil {
return nil, fmt.Errorf("converting audit log query where in actor type: %w", err)
}
result.InActorType = at
}
if i.InActorId != nil {
result.InActorID = *i.InActorId
}
if i.InActorOwnerId != nil {
result.InActorOwnerID = fromStrs[pacta.OwnerID](*i.InActorOwnerId)
}
if i.InTargetType != nil {
tt, err := convAll(*i.InTargetType, auditLogTargetTypeFromOAPI)
if err != nil {
return nil, fmt.Errorf("converting audit log query where in target type: %w", err)
}
result.InTargetType = tt
}
if i.InTargetId != nil {
result.InTargetID = *i.InTargetId
}
if i.InTargetOwnerId != nil {
result.InTargetOwnerID = fromStrs[pacta.OwnerID](*i.InTargetOwnerId)
}
return result, nil
}

func auditLogQuerySortByFromOAPI(i api.AuditLogQuerySortBy) (db.AuditLogQuerySortBy, error) {
return db.ParseAuditLogQuerySortBy(string(i))
}

func auditLogQuerySortFromOAPI(i api.AuditLogQuerySort) (*db.AuditLogQuerySort, error) {
by, err := auditLogQuerySortByFromOAPI(i.By)
if err != nil {
return nil, fmt.Errorf("converting audit log query sort by: %w", err)
}
return &db.AuditLogQuerySort{
By: by,
Ascending: i.Ascending,
}, nil
}

func AuditLogQueryFromOAPI(q *api.AuditLogQueryReq) (*db.AuditLogQuery, error) {
limit := 25
if q.Limit != nil {
limit = *q.Limit
}
if limit > 100 {
limit = 100
}
cursor := ""
if q.Cursor != nil {
cursor = *q.Cursor
}
sorts := []*db.AuditLogQuerySort{}
if q.Sorts != nil {
ss, err := convAll(*q.Sorts, auditLogQuerySortFromOAPI)
if err != nil {
return nil, oapierr.BadRequest("error converting audit log query sorts", zap.Error(err))
}
sorts = ss
}
wheres, err := convAll(q.Wheres, auditLogQueryWhereFromOAPI)
if err != nil {
return nil, oapierr.BadRequest("error converting audit log query wheres", zap.Error(err))
}
return &db.AuditLogQuery{
Cursor: db.Cursor(cursor),
Limit: limit,
Wheres: wheres,
Sorts: sorts,
}, nil
}
Loading

0 comments on commit 3dd90da

Please sign in to comment.