Skip to content

Commit

Permalink
Refactors Initiative FE to support Authz + Anon Access (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
gbdubs authored Jan 24, 2024
1 parent 5c8f7fd commit 3528656
Show file tree
Hide file tree
Showing 25 changed files with 361 additions and 355 deletions.
35 changes: 34 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"net/http"
"os"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -314,7 +315,7 @@ func run(args []string) error {
zaphttplog.NewMiddleware(logger, zaphttplog.WithConcise(false)),
chimiddleware.Recoverer,
jwtauth.Verifier(jwtauth.New("EdDSA", nil, jwKey)),
jwtauth.Authenticator,
requireJWTIfNotPublicEndpoint,
session.WithAuthn(logger, db),
}, addl...)
}
Expand Down Expand Up @@ -372,6 +373,34 @@ func run(args []string) error {
return nil
}

type allowFn func(r *http.Request) bool

var publicEndpoints = []allowFn{
allowPublicInitiativeLookups,
}

var allowPublicInitiativeLookupsRegexp = regexp.MustCompile(`^/initiative/[^/]*$`)

func allowPublicInitiativeLookups(r *http.Request) bool {
if r.Method != http.MethodGet {
return false
}
return allowPublicInitiativeLookupsRegexp.MatchString(r.URL.Path)
}

func requireJWTIfNotPublicEndpoint(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, fn := range publicEndpoints {
if fn(r) {
r = r.WithContext(session.WithAllowedAnonymous(r.Context()))
next.ServeHTTP(w, r)
return
}
}
jwtauth.Authenticator(next).ServeHTTP(w, r)
})
}

func rateLimitMiddleware(maxReq int, windowLength time.Duration) func(http.Handler) http.Handler {
// This example uses an in-memory rate limiter for simplicity, an application
// that will be running multiple API instances should likely use something like
Expand All @@ -381,6 +410,10 @@ func rateLimitMiddleware(maxReq int, windowLength time.Duration) func(http.Handl
maxReq,
windowLength,
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
if session.IsAllowedAnonymous(r.Context()) {
// Rate limit by IP address if the user is anonymous.
return r.RemoteAddr, nil
}
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return "", fmt.Errorf("failed to get claims from context: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/server/pactasrv/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ type initiativeAnalysis struct {

func (ia *initiativeAnalysis) checkAuth(ctx context.Context, tx db.Tx) error {
// This crudely tests whether or not a user is a manager of the initiative.
if err := ia.s.initiativeDoAuthzAndAuditLog(ctx, ia.iID, pacta.AuditLogAction_Update); err != nil {
if _, err := ia.s.initiativeDoAuthzAndAuditLog(ctx, ia.iID, pacta.AuditLogAction_Update); err != nil {
return err
}
i, err := ia.s.DB.Initiative(tx, ia.iID)
Expand Down
11 changes: 11 additions & 0 deletions cmd/server/pactasrv/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,26 @@ func notFoundOrUnauthorized[T ~string](actorInfo actorInfo, action pacta.AuditLo
}

func (s *Server) auditLogIfAuthorizedOrFail(ctx context.Context, status *authzStatus) error {
zapFields := func(others ...zap.Field) []zap.Field {
return append([]zap.Field{
zap.String("actor_id", status.actorUserID()),
zap.String("action", string(status.action)),
zap.String("target_type", string(status.primaryTargetType)),
zap.String("target_id", status.primaryTargetID),
}, others...)
}
if !status.isAuthorized {
s.Logger.Warn("not authorized", zapFields(zap.String("reason", "is_authorized_false"))...)
return notFoundOrUnauthorized(status.actorInfo, status.action, status.primaryTargetType, status.primaryTargetID)
}
al, err := status.ToAuditLog()
if err != nil {
s.Logger.Warn("not authorized", zapFields(zap.String("reason", "to_audit_log_failure"), zap.Error(err))...)
return err
}
_, err = s.DB.CreateAuditLog(s.DB.NoTxn(ctx), al)
if err != nil {
s.Logger.Warn("not authorized", zapFields(zap.String("reason", "create_audit_log_failure"), zap.Error(err))...)
return oapierr.Internal("creating audit log failed", zap.Error(err))
}
return nil
Expand Down
18 changes: 15 additions & 3 deletions cmd/server/pactasrv/conv/helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package conv

import "time"
import (
"fmt"
"time"

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

func ptr[T any](t T) *T {
return &t
Expand Down Expand Up @@ -52,10 +58,16 @@ func convAll[I any, O any](is []I, f func(I) (O, error)) ([]O, error) {
return os, nil
}

func dereferenceAll[T any](ts []*T) []T {
func dereference[T any](ts []*T, e error) ([]T, error) {
if e != nil {
return nil, e
}
result := make([]T, len(ts))
for i, t := range ts {
if t == nil {
return nil, oapierr.Internal("nil pointer found in derference", zap.String("type", fmt.Sprintf("%T", t)), zap.Int("index", i))
}
result[i] = *t
}
return result
return result, nil
}
11 changes: 8 additions & 3 deletions cmd/server/pactasrv/conv/pacta_to_oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func InitiativeToOAPI(i *pacta.Initiative) (*api.Initiative, error) {
if err != nil {
return nil, oapierr.Internal("initiativeToOAPI: portfolioInitiativeMembershipToOAPIInitiative failed", zap.Error(err))
}
iurs, err := dereference(convAll(i.InitiativeUserRelationships, InitiativeUserRelationshipToOAPI))
if err != nil {
return nil, oapierr.Internal("initiativeToOAPI: initiativeUserRelationshipToOAPI failed", zap.Error(err))
}
lang, err := LanguageToOAPI(i.Language)
if err != nil {
return nil, oapierr.Internal("initiativeToOAPI: languageToOAPI failed", zap.Error(err))
Expand All @@ -59,6 +63,7 @@ func InitiativeToOAPI(i *pacta.Initiative) (*api.Initiative, error) {
PublicDescription: i.PublicDescription,
RequiresInvitationToJoin: i.RequiresInvitationToJoin,
PortfolioInitiativeMemberships: pims,
InitiativeUserRelationships: iurs,
}, nil
}

Expand Down Expand Up @@ -250,7 +255,7 @@ func PortfolioToOAPI(p *pacta.Portfolio) (*api.Portfolio, error) {
}
pims, err := convAll(p.PortfolioInitiativeMemberships, portfolioInitiativeMembershipToOAPIInitiative)
if err != nil {
return nil, oapierr.Internal("initiativeToOAPI: portfolioInitiativeMembershipToOAPIInitiative failed", zap.Error(err))
return nil, oapierr.Internal("portfolioToOAPI: portfolioInitiativeMembershipToOAPIInitiative failed", zap.Error(err))
}
hd, err := HoldingsDateToOAPI(p.Properties.HoldingsDate)
if err != nil {
Expand Down Expand Up @@ -408,7 +413,7 @@ func AnalysisToOAPI(a *pacta.Analysis) (*api.Analysis, error) {
if a == nil {
return nil, oapierr.Internal("analysisToOAPI: can't convert nil pointer")
}
aas, err := convAll(a.Artifacts, AnalysisArtifactToOAPI)
aas, err := dereference(convAll(a.Artifacts, AnalysisArtifactToOAPI))
if err != nil {
return nil, oapierr.Internal("analysisToOAPI: analysisArtifactsToOAPI failed", zap.Error(err))
}
Expand Down Expand Up @@ -440,7 +445,7 @@ func AnalysisToOAPI(a *pacta.Analysis) (*api.Analysis, error) {
CompletedAt: timeToNilable(a.CompletedAt),
FailureCode: fc,
FailureMessage: fm,
Artifacts: dereferenceAll(aas),
Artifacts: aas,
OwnerId: string(a.Owner.ID),
}, nil
}
Expand Down
107 changes: 74 additions & 33 deletions cmd/server/pactasrv/initiative.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (s *Server) CreateInitiative(ctx context.Context, request api.CreateInitiat
// (PATCH /initiative/{id})
func (s *Server) UpdateInitiative(ctx context.Context, request api.UpdateInitiativeRequestObject) (api.UpdateInitiativeResponseObject, error) {
id := pacta.InitiativeID(request.Id)
if err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Update); err != nil {
if _, err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Update); err != nil {
return nil, err
}
mutations := []db.UpdateInitiativeFn{}
Expand Down Expand Up @@ -97,7 +97,7 @@ func (s *Server) UpdateInitiative(ctx context.Context, request api.UpdateInitiat
// (DELETE /initiative/{id})
func (s *Server) DeleteInitiative(ctx context.Context, request api.DeleteInitiativeRequestObject) (api.DeleteInitiativeResponseObject, error) {
id := pacta.InitiativeID(request.Id)
if err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Delete); err != nil {
if _, err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Delete); err != nil {
return nil, err
}
buris, err := s.DB.DeleteInitiative(s.DB.NoTxn(ctx), id)
Expand All @@ -113,9 +113,13 @@ func (s *Server) DeleteInitiative(ctx context.Context, request api.DeleteInitiat
// Returns an initiative by ID
// (GET /initiative/{id})
func (s *Server) FindInitiativeById(ctx context.Context, request api.FindInitiativeByIdRequestObject) (api.FindInitiativeByIdResponseObject, error) {
// TODO(#12) Allow Anonymous Access
actorInfo, err := s.getActorInfoOrAnon(ctx)
if err != nil {
return nil, err
}
id := pacta.InitiativeID(request.Id)
if err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_ReadMetadata); err != nil {
info, err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_ReadMetadata)
if err != nil {
return nil, err
}
i, err := s.DB.Initiative(s.DB.NoTxn(ctx), id)
Expand All @@ -125,11 +129,27 @@ func (s *Server) FindInitiativeById(ctx context.Context, request api.FindInitiat
}
return nil, oapierr.Internal("failed to load initiative", zap.String("initiative_id", request.Id), zap.Error(err))
}
portfolios, err := s.DB.PortfolioInitiativeMembershipsByInitiative(s.DB.NoTxn(ctx), i.ID)
if err != nil {
return nil, oapierr.Internal("failed to load portfolios for initiative", zap.String("initiative_id", string(i.ID)), zap.Error(err))
if info.CanManageUsersAndPortfolios {
portfolios, err := s.DB.PortfolioInitiativeMembershipsByInitiative(s.DB.NoTxn(ctx), i.ID)
if err != nil {
return nil, oapierr.Internal("failed to load portfolios for initiative", zap.String("initiative_id", string(i.ID)), zap.Error(err))
}
i.PortfolioInitiativeMemberships = portfolios
relationships, err := s.DB.InitiativeUserRelationshipsByInitiative(s.DB.NoTxn(ctx), i.ID)
if err != nil {
return nil, oapierr.Internal("failed to load initiative user relationships for initiative", zap.String("initiative_id", string(i.ID)), zap.Error(err))
}
i.InitiativeUserRelationships = relationships
} else if actorInfo.UserID != "" {
iur, err := s.DB.InitiativeUserRelationship(s.DB.NoTxn(ctx), i.ID, actorInfo.UserID)
if err != nil {
return nil, oapierr.Internal("failed to load singular initiative user relationship for initiative", zap.String("initiative_id", string(i.ID)), zap.Error(err))
}
i.InitiativeUserRelationships = []*pacta.InitiativeUserRelationship{iur}
}
if !info.CanSeeInternalInfo {
i.InternalDescription = ""
}
i.PortfolioInitiativeMemberships = portfolios
resp, err := conv.InitiativeToOAPI(i)
if err != nil {
return nil, err
Expand All @@ -144,6 +164,9 @@ func (s *Server) ListInitiatives(ctx context.Context, request api.ListInitiative
if err != nil {
return nil, oapierr.Internal("failed to load all initiatives", zap.Error(err))
}
for _, i := range is {
i.InternalDescription = ""
}
result, err := dereference(mapAll(is, conv.InitiativeToOAPI))
if err != nil {
return nil, err
Expand All @@ -158,25 +181,22 @@ func (s *Server) AllInitiativeData(ctx context.Context, request api.AllInitiativ
if err != nil {
return nil, err
}
// TODO(#12) Implement Authorization, along the lines of #121
i, err := s.DB.Initiative(s.DB.NoTxn(ctx), pacta.InitiativeID(request.Id))
id := pacta.InitiativeID(request.Id)
_, err = s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Download)
if err != nil {
if db.IsNotFound(err) {
return nil, oapierr.NotFound("initiative not found", zap.String("initiative_id", request.Id))
}
return nil, oapierr.Internal("failed to load initiative", zap.String("initiative_id", request.Id), zap.Error(err))
return nil, err
}
portfolioMembers, err := s.DB.PortfolioInitiativeMembershipsByInitiative(s.DB.NoTxn(ctx), i.ID)
portfolioMembers, err := s.DB.PortfolioInitiativeMembershipsByInitiative(s.DB.NoTxn(ctx), id)
if err != nil {
return nil, oapierr.Internal("failed to load portfolio memberships for initiative", zap.String("initiative_id", string(i.ID)), zap.Error(err))
return nil, oapierr.Internal("failed to load portfolio memberships for initiative", zap.String("initiative_id", string(id)), zap.Error(err))
}
portfolioIDs := []pacta.PortfolioID{}
for _, pm := range portfolioMembers {
portfolioIDs = append(portfolioIDs, pm.Portfolio.ID)
}
portfolios, err := s.DB.Portfolios(s.DB.NoTxn(ctx), portfolioIDs)
if err != nil {
return nil, oapierr.Internal("failed to load portfolios for initiative", zap.String("initiative_id", string(i.ID)), zap.Error(err))
return nil, oapierr.Internal("failed to load portfolios for initiative", zap.String("initiative_id", string(id)), zap.Error(err))
}
if err := s.populateBlobsInPortfolios(ctx, values(portfolios)...); err != nil {
return nil, err
Expand All @@ -192,7 +212,7 @@ func (s *Server) AllInitiativeData(ctx context.Context, request api.AllInitiativ
PrimaryTargetID: string(p.ID),
PrimaryTargetOwner: p.Owner,
SecondaryTargetType: pacta.AuditLogTargetType_Initiative,
SecondaryTargetID: string(i.ID),
SecondaryTargetID: string(id),
SecondaryTargetOwner: &pacta.Owner{ID: "SYSTEM"}, // TODO(#12) When merging with #121, use the const type.
})
}
Expand All @@ -216,24 +236,30 @@ func (s *Server) AllInitiativeData(ctx context.Context, request api.AllInitiativ
ExpirationTime: expiryTime,
})
}

return api.AllInitiativeData200JSONResponse(response), nil
}

func (s *Server) initiativeDoAuthzAndAuditLog(ctx context.Context, iID pacta.InitiativeID, action pacta.AuditLogAction) error {
actorInfo, err := s.getActorInfoOrErrIfAnon(ctx)
if err != nil {
return err
}
iurs, err := s.DB.InitiativeUserRelationshipsByInitiative(s.DB.NoTxn(ctx), iID)
type initiativeAuthzVisibilityInfo struct {
CanManageUsersAndPortfolios bool
CanSeeInternalInfo bool
}

func (s *Server) initiativeDoAuthzAndAuditLog(ctx context.Context, iID pacta.InitiativeID, action pacta.AuditLogAction) (*initiativeAuthzVisibilityInfo, error) {
actorInfo, err := s.getActorInfoOrAnon(ctx)
if err != nil {
return oapierr.Internal("failed to list initiative user relationships", zap.Error(err))
return nil, err
}
userIsInitiativeManager := false
for _, iur := range iurs {
if iur.User.ID == actorInfo.UserID && iur.Manager {
userIsInitiativeManager = true
break
userIsInitiativeMember := false
if actorInfo.UserID != "" {
iur, err := s.DB.InitiativeUserRelationship(s.DB.NoTxn(ctx), iID, actorInfo.UserID)
if err != nil {
if !db.IsNotFound(err) {
return nil, oapierr.Internal("failed to find initiative user relationships", zap.Error(err))
}
} else {
userIsInitiativeMember = iur.Member
userIsInitiativeManager = iur.Manager
}
}
as := &authzStatus{
Expand All @@ -244,15 +270,30 @@ func (s *Server) initiativeDoAuthzAndAuditLog(ctx context.Context, iID pacta.Ini
action: action,
}
switch action {
case pacta.AuditLogAction_Delete, pacta.AuditLogAction_Create, pacta.AuditLogAction_ReadMetadata, pacta.AuditLogAction_Update:
case pacta.AuditLogAction_ReadMetadata:
as.isAuthorized = true
if userIsInitiativeManager {
as.authorizedAsActorType = ptr(pacta.AuditLogActorType_Owner)
} else if actorInfo.IsAdmin || actorInfo.IsSuperAdmin {
as.authorizedAsActorType = ptr(pacta.AuditLogActorType_Admin)
} else {
as.authorizedAsActorType = ptr(pacta.AuditLogActorType_Public)
}
case pacta.AuditLogAction_Delete, pacta.AuditLogAction_Create, pacta.AuditLogAction_Update:
if userIsInitiativeManager {
as.authorizedAsActorType = ptr(pacta.AuditLogActorType_Owner)
as.isAuthorized = true
} else {
as.isAuthorized, as.authorizedAsActorType = allowIfAdmin(actorInfo)
}
default:
return fmt.Errorf("unknown action %q for initiative authz", action)
return nil, fmt.Errorf("unknown action %q for initiative authz", action)
}
if err := s.auditLogIfAuthorizedOrFail(ctx, as); err != nil {
return nil, err
}
return s.auditLogIfAuthorizedOrFail(ctx, as)
return &initiativeAuthzVisibilityInfo{
CanSeeInternalInfo: userIsInitiativeMember || userIsInitiativeManager || actorInfo.IsAdmin || actorInfo.IsSuperAdmin,
CanManageUsersAndPortfolios: userIsInitiativeManager || actorInfo.IsAdmin || actorInfo.IsSuperAdmin,
}, nil
}
Loading

0 comments on commit 3528656

Please sign in to comment.