Skip to content
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
4 changes: 2 additions & 2 deletions buf.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ deps:
- remote: buf.build
owner: agynio
repository: api
commit: b67ca28ce2c94ca1a95f27e18051641b
digest: shake256:fc939b54bfabac248969211ca6f70d6c4efe48c6e971644470c1a1eb2e49719208d270ce7b7319bace0ac279a1a8b2e842b146204a3d2600ab974c807c727ea3
commit: bbd8e6ab60974e9eb324ba9c4eeda240
digest: shake256:803c10c71ad07ed7b1f2cc531c499f4fdbf7598937f51ca7ed353ae67d8950f8f789ac23ac5bd3b2c2e1c623b1a68ff2835925bf3bd4c36356912f54804aebb7
- remote: buf.build
owner: opentelemetry
repository: opentelemetry
Expand Down
50 changes: 48 additions & 2 deletions internal/server/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ func toProtoInstallation(installation store.Installation) (*appsv1.Installation,
if err != nil {
return nil, err
}
return &appsv1.Installation{
protoInstallation := &appsv1.Installation{
Meta: toProtoEntityMeta(installation.Meta),
AppId: installation.AppID.String(),
OrganizationId: installation.OrganizationID.String(),
Slug: installation.Slug,
Configuration: configuration,
}, nil
}
if installation.Status != nil {
status := *installation.Status
protoInstallation.Status = &status
}
return protoInstallation, nil
}

func toProtoVisibility(visibility store.AppVisibility) appsv1.AppVisibility {
Expand All @@ -69,6 +74,32 @@ func toStoreVisibility(visibility appsv1.AppVisibility) (store.AppVisibility, er
}
}

func toProtoAuditLogLevel(level store.InstallationAuditLogLevel) appsv1.InstallationAuditLogLevel {
switch level {
case store.InstallationAuditLogLevelInfo:
return appsv1.InstallationAuditLogLevel_INSTALLATION_AUDIT_LOG_LEVEL_INFO
case store.InstallationAuditLogLevelWarning:
return appsv1.InstallationAuditLogLevel_INSTALLATION_AUDIT_LOG_LEVEL_WARNING
case store.InstallationAuditLogLevelError:
return appsv1.InstallationAuditLogLevel_INSTALLATION_AUDIT_LOG_LEVEL_ERROR
default:
panic("unknown audit log level")
}
}

func toStoreAuditLogLevel(level appsv1.InstallationAuditLogLevel) (store.InstallationAuditLogLevel, error) {
switch level {
case appsv1.InstallationAuditLogLevel_INSTALLATION_AUDIT_LOG_LEVEL_INFO:
return store.InstallationAuditLogLevelInfo, nil
case appsv1.InstallationAuditLogLevel_INSTALLATION_AUDIT_LOG_LEVEL_WARNING:
return store.InstallationAuditLogLevelWarning, nil
case appsv1.InstallationAuditLogLevel_INSTALLATION_AUDIT_LOG_LEVEL_ERROR:
return store.InstallationAuditLogLevelError, nil
default:
return "", fmt.Errorf("unknown audit log level %v", level)
}
}

func protoStructToMap(value *structpb.Struct) map[string]any {
if value == nil {
return map[string]any{}
Expand Down Expand Up @@ -96,3 +127,18 @@ func toProtoAppProfile(app store.App) *appsv1.AppProfile {
Icon: app.Icon,
}
}

func toProtoInstallationAuditLogEntry(entry store.InstallationAuditLogEntry) *appsv1.InstallationAuditLogEntry {
protoEntry := &appsv1.InstallationAuditLogEntry{
Id: entry.ID.String(),
InstallationId: entry.InstallationID.String(),
Message: entry.Message,
Level: toProtoAuditLogLevel(entry.Level),
CreatedAt: timestamppb.New(entry.CreatedAt),
}
if entry.IdempotencyKey != nil {
idempotencyKey := *entry.IdempotencyKey
protoEntry.IdempotencyKey = &idempotencyKey
}
return protoEntry
}
128 changes: 121 additions & 7 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"log"
"strings"

appsv1 "github.com/agynio/apps/.gen/go/agynio/api/apps/v1"
authorizationv1 "github.com/agynio/apps/.gen/go/agynio/api/authorization/v1"
Expand Down Expand Up @@ -46,7 +47,10 @@ type AppStore interface {
GetInstallationBySlug(ctx context.Context, organizationID uuid.UUID, slug string) (store.Installation, error)
ListInstallations(ctx context.Context, pageSize int, pageToken string, filter store.ListInstallationsFilter) ([]store.Installation, string, error)
UpdateInstallation(ctx context.Context, input store.UpdateInstallationInput) (store.Installation, error)
UpdateInstallationStatus(ctx context.Context, id uuid.UUID, status *string) (store.Installation, error)
DeleteInstallation(ctx context.Context, id uuid.UUID) error
AppendInstallationAuditLogEntry(ctx context.Context, input store.AppendInstallationAuditLogEntryInput) (store.InstallationAuditLogEntry, error)
ListInstallationAuditLogEntries(ctx context.Context, installationID uuid.UUID, pageSize int, pageToken string) ([]store.InstallationAuditLogEntry, string, error)
}

type Server struct {
Expand Down Expand Up @@ -627,22 +631,132 @@ func (s *Server) GetInstallationConfiguration(ctx context.Context, req *appsv1.G
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "id: %v", err)
}
installation, err := s.store.GetInstallation(ctx, id)
installation, err := s.requireInstallationForAppIdentity(ctx, callerID, id)
if err != nil {
return nil, err
}
configuration, err := mapToProtoStruct(installation.Configuration)
if err != nil {
return nil, status.Errorf(codes.Internal, "convert configuration: %v", err)
}
return &appsv1.GetInstallationConfigurationResponse{Configuration: configuration}, nil
}

func (s *Server) ReportInstallationStatus(ctx context.Context, req *appsv1.ReportInstallationStatusRequest) (*appsv1.ReportInstallationStatusResponse, error) {
callerID, err := identityFromMetadata(ctx)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err)
}
installationID, err := parseUUID(req.GetInstallationId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "installation_id: %v", err)
}
installation, err := s.requireInstallationForAppIdentity(ctx, callerID, installationID)
if err != nil {
return nil, err
}

statusValue := req.GetStatus()
var normalizedStatus *string
if strings.TrimSpace(statusValue) != "" {
normalizedStatus = &statusValue
}

installation, err = s.store.UpdateInstallationStatus(ctx, installation.Meta.ID, normalizedStatus)
if err != nil {
return nil, toStatusError(err)
}
app, err := s.store.GetApp(ctx, installation.AppID)
protoInstallation, err := toProtoInstallation(installation)
if err != nil {
return nil, status.Errorf(codes.Internal, "convert installation: %v", err)
}
return &appsv1.ReportInstallationStatusResponse{Installation: protoInstallation}, nil
}

func (s *Server) AppendInstallationAuditLogEntry(ctx context.Context, req *appsv1.AppendInstallationAuditLogEntryRequest) (*appsv1.AppendInstallationAuditLogEntryResponse, error) {
callerID, err := identityFromMetadata(ctx)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err)
}
installationID, err := parseUUID(req.GetInstallationId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "installation_id: %v", err)
}
if _, err := s.requireInstallationForAppIdentity(ctx, callerID, installationID); err != nil {
return nil, err
}

message := req.GetMessage()
if err := validateAuditLogMessage(message); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "message: %v", err)
}
level, err := toStoreAuditLogLevel(req.GetLevel())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "level: %v", err)
}
var idempotencyKey *string
if trimmedKey := strings.TrimSpace(req.GetIdempotencyKey()); trimmedKey != "" {
idempotencyKey = &trimmedKey
}

entry, err := s.store.AppendInstallationAuditLogEntry(ctx, store.AppendInstallationAuditLogEntryInput{
InstallationID: installationID,
Message: message,
Level: level,
IdempotencyKey: idempotencyKey,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "append audit log entry: %v", err)
}
protoEntry := toProtoInstallationAuditLogEntry(entry)
return &appsv1.AppendInstallationAuditLogEntryResponse{Entry: protoEntry}, nil
}

func (s *Server) ListInstallationAuditLogEntries(ctx context.Context, req *appsv1.ListInstallationAuditLogEntriesRequest) (*appsv1.ListInstallationAuditLogEntriesResponse, error) {
callerID, err := identityFromMetadata(ctx)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err)
}
installationID, err := parseUUID(req.GetInstallationId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "installation_id: %v", err)
}
installation, err := s.store.GetInstallation(ctx, installationID)
if err != nil {
return nil, toStatusError(err)
}
if app.IdentityID != callerID {
return nil, status.Error(codes.PermissionDenied, "permission denied")
if err := s.requireOrgMember(ctx, callerID, installation.OrganizationID); err != nil {
return nil, err
}
configuration, err := mapToProtoStruct(installation.Configuration)

entries, nextToken, err := s.store.ListInstallationAuditLogEntries(ctx, installationID, int(req.GetPageSize()), req.GetPageToken())
if err != nil {
return nil, status.Errorf(codes.Internal, "convert configuration: %v", err)
var invalidToken *store.InvalidPageTokenError
if errors.As(err, &invalidToken) {
return nil, status.Errorf(codes.InvalidArgument, "invalid page_token: %v", invalidToken.Err)
}
return nil, status.Errorf(codes.Internal, "list audit log entries: %v", err)
}
return &appsv1.GetInstallationConfigurationResponse{Configuration: configuration}, nil
protoEntries := make([]*appsv1.InstallationAuditLogEntry, 0, len(entries))
for _, entry := range entries {
protoEntries = append(protoEntries, toProtoInstallationAuditLogEntry(entry))
}
return &appsv1.ListInstallationAuditLogEntriesResponse{Entries: protoEntries, NextPageToken: nextToken}, nil
}

func (s *Server) requireInstallationForAppIdentity(ctx context.Context, identityID uuid.UUID, installationID uuid.UUID) (store.Installation, error) {
installation, err := s.store.GetInstallation(ctx, installationID)
if err != nil {
return store.Installation{}, toStatusError(err)
}
app, err := s.store.GetApp(ctx, installation.AppID)
if err != nil {
return store.Installation{}, toStatusError(err)
}
if app.IdentityID != identityID {
return store.Installation{}, status.Error(codes.PermissionDenied, "permission denied")
}
return installation, nil
}

func (s *Server) requireOrgOwner(ctx context.Context, identityID uuid.UUID, organizationID uuid.UUID) error {
Expand Down
Loading
Loading