From 427565c521ba34e9b7b20a63bc7f07bb04df19bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 16 Dec 2025 19:02:57 +0100 Subject: [PATCH] Add light and dark logo files for frameworks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- pkg/authz/permissions.go | 1 + pkg/coredata/framework.go | 46 ++++-- pkg/coredata/migrations/20251216T104900Z.sql | 17 ++ pkg/probo/framework_service.go | 141 ++++++++++++++++- pkg/server/api/console/v1/schema.graphql | 2 + pkg/server/api/console/v1/schema/schema.go | 158 +++++++++++++++++++ pkg/server/api/console/v1/types/types.go | 2 + pkg/server/api/console/v1/v1_resolver.go | 18 +++ pkg/server/api/trust/v1/schema.graphql | 2 + pkg/server/api/trust/v1/schema/schema.go | 157 +++++++++++++++++- pkg/server/api/trust/v1/types/types.go | 6 +- pkg/server/api/trust/v1/v1_resolver.go | 18 +++ pkg/trust/framework_service.go | 87 +++++++++- 13 files changed, 630 insertions(+), 25 deletions(-) create mode 100644 pkg/coredata/migrations/20251216T104900Z.sql diff --git a/pkg/authz/permissions.go b/pkg/authz/permissions.go index 8419a2199..542ff2a34 100644 --- a/pkg/authz/permissions.go +++ b/pkg/authz/permissions.go @@ -467,6 +467,7 @@ var Permissions = map[uint16]map[Action][]Role{ ActionGet: NonEmployeeRoles, ActionGetOrganization: NonEmployeeRoles, ActionListControls: NonEmployeeRoles, + ActionGetLogoUrl: NonEmployeeRoles, ActionCreateControl: EditRoles, ActionUpdateFramework: EditRoles, diff --git a/pkg/coredata/framework.go b/pkg/coredata/framework.go index 11430a277..b7da578e1 100644 --- a/pkg/coredata/framework.go +++ b/pkg/coredata/framework.go @@ -30,13 +30,15 @@ import ( type ( Framework struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - ReferenceID string `db:"reference_id"` - Name string `db:"name"` - Description *string `db:"description"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + ReferenceID string `db:"reference_id"` + Name string `db:"name"` + Description *string `db:"description"` + LightLogoFileID *gid.GID `db:"light_logo_file_id"` + DarkLogoFileID *gid.GID `db:"dark_logo_file_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } Frameworks []*Framework @@ -121,6 +123,8 @@ SELECT reference_id, name, description, + light_logo_file_id, + dark_logo_file_id, created_at, updated_at FROM @@ -128,7 +132,7 @@ FROM WHERE %s AND organization_id = @organization_id - AND %s + AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) @@ -164,6 +168,8 @@ SELECT reference_id, name, description, + light_logo_file_id, + dark_logo_file_id, created_at, updated_at FROM @@ -210,6 +216,8 @@ SELECT reference_id, name, description, + light_logo_file_id, + dark_logo_file_id, created_at, updated_at FROM @@ -257,6 +265,8 @@ INSERT INTO reference_id, name, description, + light_logo_file_id, + dark_logo_file_id, created_at, updated_at ) @@ -267,20 +277,24 @@ VALUES ( @reference_id, @name, @description, + @light_logo_file_id, + @dark_logo_file_id, @created_at, @updated_at ); ` args := pgx.StrictNamedArgs{ - "tenant_id": scope.GetTenantID(), - "framework_id": f.ID, - "organization_id": f.OrganizationID, - "reference_id": f.ReferenceID, - "name": f.Name, - "description": f.Description, - "created_at": f.CreatedAt, - "updated_at": f.UpdatedAt, + "tenant_id": scope.GetTenantID(), + "framework_id": f.ID, + "organization_id": f.OrganizationID, + "reference_id": f.ReferenceID, + "name": f.Name, + "description": f.Description, + "light_logo_file_id": f.LightLogoFileID, + "dark_logo_file_id": f.DarkLogoFileID, + "created_at": f.CreatedAt, + "updated_at": f.UpdatedAt, } _, err := conn.Exec(ctx, q, args) diff --git a/pkg/coredata/migrations/20251216T104900Z.sql b/pkg/coredata/migrations/20251216T104900Z.sql new file mode 100644 index 000000000..bf44b7b91 --- /dev/null +++ b/pkg/coredata/migrations/20251216T104900Z.sql @@ -0,0 +1,17 @@ +ALTER TABLE + frameworks +ADD + COLUMN light_logo_file_id TEXT, +ADD + COLUMN dark_logo_file_id TEXT; + +ALTER TABLE + frameworks +ADD + CONSTRAINT frameworks_light_logo_file_id_fkey FOREIGN KEY (light_logo_file_id) REFERENCES files(id) ON DELETE +SET + NULL, +ADD + CONSTRAINT frameworks_dark_logo_file_id_fkey FOREIGN KEY (dark_logo_file_id) REFERENCES files(id) ON DELETE +SET + NULL; \ No newline at end of file diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 65a55459e..feb7d7fe7 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "os" + "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -64,8 +65,12 @@ type ( ImportFrameworkRequest struct { Framework struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id"` + Name string `json:"name"` + Logo *struct { + Light string `json:"light"` + Dark string `json:"dark"` + } `json:"logo,omitempty"` Controls []struct { ID string `json:"id"` Name string `json:"name"` @@ -510,6 +515,54 @@ func (s FrameworkService) Import( UpdatedAt: now, } + if req.Framework.Logo != nil { + for name, logo := range map[string]string{ + "light": req.Framework.Logo.Light, + "dark": req.Framework.Logo.Dark, + } { + fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) + objectKey, err := uuid.NewV7() + if err != nil { + return fmt.Errorf("cannot generate object key: %w", err) + } + + filename := "logo_" + name + contentType := "image/svg+xml" + + fileRecord := &coredata.File{ + ID: fileID, + BucketName: s.svc.bucket, + MimeType: contentType, + FileName: filename, + FileKey: objectKey.String(), + CreatedAt: now, + UpdatedAt: now, + } + + fileSize, err := s.svc.fileManager.PutFile(ctx, fileRecord, strings.NewReader(logo), map[string]string{ + "type": "framework-logo", + "theme": name, + "framework-id": framework.ID.String(), + "organization-id": organization.ID.String(), + }) + if err != nil { + return fmt.Errorf("cannot upload logo file: %w", err) + } + + fileRecord.FileSize = fileSize + + if err := fileRecord.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert file: %w", err) + } + + if name == "light" { + framework.LightLogoFileID = &fileID + } else { + framework.DarkLogoFileID = &fileID + } + } + } + if err := framework.Insert(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot insert framework: %w", err) } @@ -890,3 +943,87 @@ func (s *FrameworkService) BuildAndUploadExport(ctx context.Context, exportJobID return exportJob, nil } + +func (s FrameworkService) GenerateLightLogoURL( + ctx context.Context, + frameworkID gid.GID, + expiresIn time.Duration, +) (*string, error) { + file := &coredata.File{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + framework := &coredata.Framework{} + if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } + + if framework.LightLogoFileID == nil { + return nil + } + + if err := file.LoadByID(ctx, conn, s.svc.scope, *framework.LightLogoFileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + if file.FileKey == "" { + return nil, nil + } + + presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, file, expiresIn) + if err != nil { + return nil, fmt.Errorf("cannot generate file URL: %w", err) + } + + return &presignedURL, nil +} + +func (s FrameworkService) GenerateDarkLogoURL( + ctx context.Context, + frameworkID gid.GID, + expiresIn time.Duration, +) (*string, error) { + file := &coredata.File{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + framework := &coredata.Framework{} + if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } + + if framework.DarkLogoFileID == nil { + return nil + } + + if err := file.LoadByID(ctx, conn, s.svc.scope, *framework.DarkLogoFileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + if file.FileKey == "" { + return nil, nil + } + + presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, file, expiresIn) + if err != nil { + return nil, fmt.Errorf("cannot generate file URL: %w", err) + } + + return &presignedURL, nil +} diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 7acb44b17..b27fb53bb 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1886,6 +1886,8 @@ type Framework implements Node { orderBy: ControlOrder filter: ControlFilter ): ControlConnection! @goField(forceResolver: true) + lightLogoURL: String @goField(forceResolver: true) + darkLogoURL: String @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index f646b481c..cf45041a2 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -754,8 +754,10 @@ type ComplexityRoot struct { Framework struct { Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int CreatedAt func(childComplexity int) int + DarkLogoURL func(childComplexity int) int Description func(childComplexity int) int ID func(childComplexity int) int + LightLogoURL func(childComplexity int) int Name func(childComplexity int) int Organization func(childComplexity int) int UpdatedAt func(childComplexity int) int @@ -1933,6 +1935,8 @@ type FileResolver interface { type FrameworkResolver interface { Organization(ctx context.Context, obj *types.Framework) (*types.Organization, error) Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) + LightLogoURL(ctx context.Context, obj *types.Framework) (*string, error) + DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) } type FrameworkConnectionResolver interface { TotalCount(ctx context.Context, obj *types.FrameworkConnection) (int, error) @@ -4143,6 +4147,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Framework.CreatedAt(childComplexity), true + case "Framework.darkLogoURL": + if e.complexity.Framework.DarkLogoURL == nil { + break + } + + return e.complexity.Framework.DarkLogoURL(childComplexity), true case "Framework.description": if e.complexity.Framework.Description == nil { break @@ -4155,6 +4165,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Framework.ID(childComplexity), true + case "Framework.lightLogoURL": + if e.complexity.Framework.LightLogoURL == nil { + break + } + + return e.complexity.Framework.LightLogoURL(childComplexity), true case "Framework.name": if e.complexity.Framework.Name == nil { break @@ -11442,6 +11458,8 @@ type Framework implements Node { orderBy: ControlOrder filter: ControlFilter ): ControlConnection! @goField(forceResolver: true) + lightLogoURL: String @goField(forceResolver: true) + darkLogoURL: String @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! @@ -18952,6 +18970,10 @@ func (ec *executionContext) fieldContext_Audit_framework(_ context.Context, fiel return ec.fieldContext_Framework_organization(ctx, field) case "controls": return ec.fieldContext_Framework_controls(ctx, field) + case "lightLogoURL": + return ec.fieldContext_Framework_lightLogoURL(ctx, field) + case "darkLogoURL": + return ec.fieldContext_Framework_darkLogoURL(ctx, field) case "createdAt": return ec.fieldContext_Framework_createdAt(ctx, field) case "updatedAt": @@ -20542,6 +20564,10 @@ func (ec *executionContext) fieldContext_Control_framework(_ context.Context, fi return ec.fieldContext_Framework_organization(ctx, field) case "controls": return ec.fieldContext_Framework_controls(ctx, field) + case "lightLogoURL": + return ec.fieldContext_Framework_lightLogoURL(ctx, field) + case "darkLogoURL": + return ec.fieldContext_Framework_darkLogoURL(ctx, field) case "createdAt": return ec.fieldContext_Framework_createdAt(ctx, field) case "updatedAt": @@ -28103,6 +28129,64 @@ func (ec *executionContext) fieldContext_Framework_controls(ctx context.Context, return fc, nil } +func (ec *executionContext) _Framework_lightLogoURL(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Framework_lightLogoURL, + func(ctx context.Context) (any, error) { + return ec.resolvers.Framework().LightLogoURL(ctx, obj) + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Framework_lightLogoURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Framework", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Framework_darkLogoURL(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Framework_darkLogoURL, + func(ctx context.Context) (any, error) { + return ec.resolvers.Framework().DarkLogoURL(ctx, obj) + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Framework_darkLogoURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Framework", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Framework_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -28327,6 +28411,10 @@ func (ec *executionContext) fieldContext_FrameworkEdge_node(_ context.Context, f return ec.fieldContext_Framework_organization(ctx, field) case "controls": return ec.fieldContext_Framework_controls(ctx, field) + case "lightLogoURL": + return ec.fieldContext_Framework_lightLogoURL(ctx, field) + case "darkLogoURL": + return ec.fieldContext_Framework_darkLogoURL(ctx, field) case "createdAt": return ec.fieldContext_Framework_createdAt(ctx, field) case "updatedAt": @@ -48961,6 +49049,10 @@ func (ec *executionContext) fieldContext_UpdateFrameworkPayload_framework(_ cont return ec.fieldContext_Framework_organization(ctx, field) case "controls": return ec.fieldContext_Framework_controls(ctx, field) + case "lightLogoURL": + return ec.fieldContext_Framework_lightLogoURL(ctx, field) + case "darkLogoURL": + return ec.fieldContext_Framework_darkLogoURL(ctx, field) case "createdAt": return ec.fieldContext_Framework_createdAt(ctx, field) case "updatedAt": @@ -71372,6 +71464,72 @@ func (ec *executionContext) _Framework(ctx context.Context, sel ast.SelectionSet continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "lightLogoURL": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Framework_lightLogoURL(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "darkLogoURL": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Framework_darkLogoURL(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdAt": out.Values[i] = ec._Framework_createdAt(ctx, field, obj) diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 7dd604ff4..2674ba74c 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1178,6 +1178,8 @@ type Framework struct { Description *string `json:"description,omitempty"` Organization *Organization `json:"organization"` Controls *ControlConnection `json:"controls"` + LightLogoURL *string `json:"lightLogoURL,omitempty"` + DarkLogoURL *string `json:"darkLogoURL,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 7de68052c..74ad5457c 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1100,6 +1100,24 @@ func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, return types.NewControlConnection(page, r, obj.ID, controlFilter), nil } +// LightLogoURL is the resolver for the lightLogoURL field. +func (r *frameworkResolver) LightLogoURL(ctx context.Context, obj *types.Framework) (*string, error) { + r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetLogoUrl) + + prb := r.ProboService(ctx, obj.ID.TenantID()) + + return prb.Frameworks.GenerateLightLogoURL(ctx, obj.ID, 1*time.Hour) +} + +// DarkLogoURL is the resolver for the darkLogoURL field. +func (r *frameworkResolver) DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) { + r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetLogoUrl) + + prb := r.ProboService(ctx, obj.ID.TenantID()) + + return prb.Frameworks.GenerateDarkLogoURL(ctx, obj.ID, 1*time.Hour) +} + // TotalCount is the resolver for the totalCount field. func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types.FrameworkConnection) (int, error) { r.MustBeAuthorized(ctx, obj.ParentID, authz.ActionTotalCount) diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index d24aacf9f..6cbe275b5 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -75,6 +75,8 @@ type DocumentEdge { type Framework implements Node { id: ID! name: String! + lightLogoURL: String @goField(forceResolver: true) + darkLogoURL: String @goField(forceResolver: true) } type Report implements Node { diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index ef4484cbf..f703d2831 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -48,6 +48,7 @@ type Config struct { type ResolverRoot interface { Audit() AuditResolver Document() DocumentResolver + Framework() FrameworkResolver Mutation() MutationResolver Organization() OrganizationResolver Query() QueryResolver @@ -113,8 +114,10 @@ type ComplexityRoot struct { } Framework struct { - ID func(childComplexity int) int - Name func(childComplexity int) int + DarkLogoURL func(childComplexity int) int + ID func(childComplexity int) int + LightLogoURL func(childComplexity int) int + Name func(childComplexity int) int } Mutation struct { @@ -250,6 +253,10 @@ type DocumentResolver interface { IsUserAuthorized(ctx context.Context, obj *types.Document) (bool, error) HasUserRequestedAccess(ctx context.Context, obj *types.Document) (bool, error) } +type FrameworkResolver interface { + LightLogoURL(ctx context.Context, obj *types.Framework) (*string, error) + DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) +} type MutationResolver interface { RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error) @@ -440,12 +447,24 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.ExportTrustCenterFilePayload.Data(childComplexity), true + case "Framework.darkLogoURL": + if e.complexity.Framework.DarkLogoURL == nil { + break + } + + return e.complexity.Framework.DarkLogoURL(childComplexity), true case "Framework.id": if e.complexity.Framework.ID == nil { break } return e.complexity.Framework.ID(childComplexity), true + case "Framework.lightLogoURL": + if e.complexity.Framework.LightLogoURL == nil { + break + } + + return e.complexity.Framework.LightLogoURL(childComplexity), true case "Framework.name": if e.complexity.Framework.Name == nil { break @@ -1173,6 +1192,8 @@ type DocumentEdge { type Framework implements Node { id: ID! name: String! + lightLogoURL: String @goField(forceResolver: true) + darkLogoURL: String @goField(forceResolver: true) } type Report implements Node { @@ -2138,6 +2159,10 @@ func (ec *executionContext) fieldContext_Audit_framework(_ context.Context, fiel return ec.fieldContext_Framework_id(ctx, field) case "name": return ec.fieldContext_Framework_name(ctx, field) + case "lightLogoURL": + return ec.fieldContext_Framework_lightLogoURL(ctx, field) + case "darkLogoURL": + return ec.fieldContext_Framework_darkLogoURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Framework", field.Name) }, @@ -2758,6 +2783,64 @@ func (ec *executionContext) fieldContext_Framework_name(_ context.Context, field return fc, nil } +func (ec *executionContext) _Framework_lightLogoURL(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Framework_lightLogoURL, + func(ctx context.Context) (any, error) { + return ec.resolvers.Framework().LightLogoURL(ctx, obj) + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Framework_lightLogoURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Framework", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Framework_darkLogoURL(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Framework_darkLogoURL, + func(ctx context.Context) (any, error) { + return ec.resolvers.Framework().DarkLogoURL(ctx, obj) + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Framework_darkLogoURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Framework", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation_requestAllAccesses(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7976,13 +8059,79 @@ func (ec *executionContext) _Framework(ctx context.Context, sel ast.SelectionSet case "id": out.Values[i] = ec._Framework_id(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "name": out.Values[i] = ec._Framework_name(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } + case "lightLogoURL": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Framework_lightLogoURL(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "darkLogoURL": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Framework_darkLogoURL(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index 4cb54aa06..f63e07d83 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -93,8 +93,10 @@ type ExportTrustCenterFilePayload struct { } type Framework struct { - ID gid.GID `json:"id"` - Name string `json:"name"` + ID gid.GID `json:"id"` + Name string `json:"name"` + LightLogoURL *string `json:"lightLogoURL,omitempty"` + DarkLogoURL *string `json:"darkLogoURL,omitempty"` } func (Framework) IsNode() {} diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 6085ef29a..647788b77 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -120,6 +120,20 @@ func (r *documentResolver) HasUserRequestedAccess(ctx context.Context, obj *type return false, nil } +// LightLogoURL is the resolver for the lightLogoURL field. +func (r *frameworkResolver) LightLogoURL(ctx context.Context, obj *types.Framework) (*string, error) { + publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + + return publicTrustService.Frameworks.GenerateLightLogoURL(ctx, obj.ID, 1*time.Hour) +} + +// DarkLogoURL is the resolver for the darkLogoURL field. +func (r *frameworkResolver) DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) { + privateTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + + return privateTrustService.Frameworks.GenerateDarkLogoURL(ctx, obj.ID, 1*time.Hour) +} + // RequestAllAccesses is the resolver for the requestAllAccesses field. func (r *mutationResolver) RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) { publicTrustService := r.PublicTrustService(ctx, input.TrustCenterID.TenantID()) @@ -990,6 +1004,9 @@ func (r *Resolver) Audit() schema.AuditResolver { return &auditResolver{r} } // Document returns schema.DocumentResolver implementation. func (r *Resolver) Document() schema.DocumentResolver { return &documentResolver{r} } +// Framework returns schema.FrameworkResolver implementation. +func (r *Resolver) Framework() schema.FrameworkResolver { return &frameworkResolver{r} } + // Mutation returns schema.MutationResolver implementation. func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} } @@ -1017,6 +1034,7 @@ func (r *Resolver) TrustCenterReference() schema.TrustCenterReferenceResolver { type auditResolver struct{ *Resolver } type documentResolver struct{ *Resolver } +type frameworkResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type organizationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } diff --git a/pkg/trust/framework_service.go b/pkg/trust/framework_service.go index 5776e5181..a11f4a1af 100644 --- a/pkg/trust/framework_service.go +++ b/pkg/trust/framework_service.go @@ -16,12 +16,13 @@ package trust import ( "context" + "time" "fmt" + "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" - "go.gearno.de/kit/pg" ) type FrameworkService struct { @@ -49,3 +50,87 @@ func (s FrameworkService) Get( return framework, nil } + +func (s FrameworkService) GenerateLightLogoURL( + ctx context.Context, + frameworkID gid.GID, + expiresIn time.Duration, +) (*string, error) { + file := &coredata.File{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + framework := &coredata.Framework{} + if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } + + if framework.LightLogoFileID == nil { + return nil + } + + if err := file.LoadByID(ctx, conn, s.svc.scope, *framework.LightLogoFileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + if file.FileKey == "" { + return nil, nil + } + + presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, file, expiresIn) + if err != nil { + return nil, fmt.Errorf("cannot generate file URL: %w", err) + } + + return &presignedURL, nil +} + +func (s FrameworkService) GenerateDarkLogoURL( + ctx context.Context, + frameworkID gid.GID, + expiresIn time.Duration, +) (*string, error) { + file := &coredata.File{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + framework := &coredata.Framework{} + if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } + + if framework.DarkLogoFileID == nil { + return nil + } + + if err := file.LoadByID(ctx, conn, s.svc.scope, *framework.DarkLogoFileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + if file.FileKey == "" { + return nil, nil + } + + presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, file, expiresIn) + if err != nil { + return nil, fmt.Errorf("cannot generate file URL: %w", err) + } + + return &presignedURL, nil +}