Add role management

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-11-07 09:20:38 +01:00
parent 696adb7d79
commit 21c4b7cd9d
143 changed files with 5976 additions and 2094 deletions

View File

@@ -77,9 +77,9 @@ func ExtractEmailDomain(email string) (string, error) {
return domain, nil
}
func MapSAMLRoleToSystemRole(samlRole string) *coredata.Role {
func MapSAMLRoleToSystemRole(samlRole string) *coredata.MembershipRole {
if samlRole != "" && isValidRole(samlRole) {
role := coredata.Role(samlRole)
role := coredata.MembershipRole(samlRole)
return &role
}
@@ -88,7 +88,7 @@ func MapSAMLRoleToSystemRole(samlRole string) *coredata.Role {
func isValidRole(role string) bool {
switch role {
case "OWNER", "ADMIN", "MEMBER", "VIEWER":
case "OWNER", "ADMIN", "VIEWER":
return true
default:
return false

View File

@@ -407,7 +407,7 @@ func (s *SAMLService) InitiateSAMLLogin(
type SAMLUserInfo struct {
Email string
FullName string
Role *coredata.Role
Role *coredata.MembershipRole
SAMLSubject string
OrganizationID gid.GID
SAMLConfigID gid.GID

View File

@@ -46,6 +46,8 @@ type (
invitationTokenValidity time.Duration
}
ErrCreateOrganizationDisabled struct{}
TenantAuthService struct {
pg *pg.Client
encryptionKey cipher.EncryptionKey
@@ -192,6 +194,10 @@ func (e ErrSAMLAutoSignupDisabled) Error() string {
return "SAML auto-signup is disabled for this organization"
}
func (e ErrCreateOrganizationDisabled) Error() string {
return "organization creation is disabled for users without existing admin or owner membership"
}
func (e ErrSAMLAuthRequired) Error() string {
return "SAML authentication required for this organization"
}
@@ -232,6 +238,28 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthService {
}
}
func (s Service) CanCreateOrganization(ctx context.Context, userID gid.GID) error {
if !s.disableSignup {
return nil
}
var memberships coredata.Memberships
err := s.pg.WithConn(ctx, func(conn pg.Conn) error {
return memberships.LoadByUserID(ctx, conn, coredata.NewNoScope(), userID)
})
if err != nil {
return fmt.Errorf("cannot load user memberships: %w", err)
}
for _, membership := range memberships {
if membership.Role == coredata.MembershipRoleOwner || membership.Role == coredata.MembershipRoleAdmin {
return nil
}
}
return &ErrCreateOrganizationDisabled{}
}
func (s Service) ForgetPassword(
ctx context.Context,
email string,
@@ -1351,13 +1379,20 @@ func (s *Service) CreateUserAPIKey(
for _, membership := range memberships {
scope := coredata.NewScope(membership.MembershipID.TenantID())
var m coredata.Membership
if err := m.LoadByID(ctx, tx, scope, membership.MembershipID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
UserAPIKeyID: userAPIKey.ID,
MembershipID: membership.MembershipID,
Role: membership.Role,
CreatedAt: now,
UpdatedAt: now,
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
UserAPIKeyID: userAPIKey.ID,
MembershipID: membership.MembershipID,
Role: membership.Role,
OrganizationID: m.OrganizationID,
CreatedAt: now,
UpdatedAt: now,
}
if err := userAPIKeyMembership.Insert(ctx, tx, scope); err != nil {
@@ -1513,13 +1548,20 @@ func (s *Service) UpdateUserAPIKeyMemberships(
now := time.Now()
for _, membership := range memberships {
scope := coredata.NewScope(membership.MembershipID.TenantID())
var m coredata.Membership
if err := m.LoadByID(ctx, tx, scope, membership.MembershipID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
UserAPIKeyID: userAPIKey.ID,
MembershipID: membership.MembershipID,
Role: membership.Role,
CreatedAt: now,
UpdatedAt: now,
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
UserAPIKeyID: userAPIKey.ID,
MembershipID: membership.MembershipID,
Role: membership.Role,
OrganizationID: m.OrganizationID,
CreatedAt: now,
UpdatedAt: now,
}
if err := userAPIKeyMembership.Insert(ctx, tx, scope); err != nil {

707
pkg/authz/permissions.go Normal file
View File

@@ -0,0 +1,707 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package authz
import (
"slices"
"go.probo.inc/probo/pkg/coredata"
)
type (
Role string
Action string
)
const (
RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN"
RoleViewer Role = "VIEWER"
RoleFull Role = "FULL"
)
const (
ActionGet Action = "get"
ActionGetAssetType Action = "getAssetType"
ActionGetAssignedTo Action = "getAssignedTo"
ActionGetAuthMethod Action = "getAuthMethod"
ActionGetBusinessAssociateAgreement Action = "getBusinessAssociateAgreement"
ActionGetBusinessOwner Action = "getBusinessOwner"
ActionGetCustomDomain Action = "getCustomDomain"
ActionGetDataPrivacyAgreement Action = "getDataPrivacyAgreement"
ActionGetFile Action = "getFile"
ActionGetFileUrl Action = "getFileUrl"
ActionGetFramework Action = "getFramework"
ActionGetHorizontalLogoUrl Action = "getHorizontalLogoUrl"
ActionGetLogoUrl Action = "getLogoUrl"
ActionGetMeasure Action = "getMeasure"
ActionGetNdaFileUrl Action = "getNdaFileUrl"
ActionGetOrganization Action = "getOrganization"
ActionGetOwner Action = "getOwner"
ActionGetSecurityOwner Action = "getSecurityOwner"
ActionGetSnapshot Action = "getSnapshot"
ActionGetTask Action = "getTask"
ActionGetTrustCenter Action = "getTrustCenter"
ActionGetTrustCenterFile Action = "getTrustCenterFile"
ActionGetVendor Action = "getVendor"
ActionActiveCount Action = "activeCount"
ActionAudit Action = "audit"
ActionAvailableDocumentAccesses Action = "availableDocumentAccesses"
ActionDocument Action = "document"
ActionDocumentVersion Action = "documentVersion"
ActionDownloadUrl Action = "downloadUrl"
ActionMemberships Action = "memberships"
ActionPendingRequestCount Action = "pendingRequestCount"
ActionPeoples Action = "peoples"
ActionReport Action = "report"
ActionReportUrl Action = "reportUrl"
ActionSignatures Action = "signatures"
ActionSignedBy Action = "signedBy"
ActionSpMetadataUrl Action = "spMetadataUrl"
ActionTestLoginUrl Action = "testLoginUrl"
ActionTotalCount Action = "totalCount"
ActionTrustCenterFile Action = "trustCenterFile"
ActionListAccesses Action = "listAccesses"
ActionListAssets Action = "listAssets"
ActionListAudits Action = "listAudits"
ActionListComplianceReports Action = "listComplianceReports"
ActionListContacts Action = "listContacts"
ActionListContinualImprovements Action = "listContinualImprovements"
ActionListControls Action = "listControls"
ActionListData Action = "listData"
ActionListDocuments Action = "listDocuments"
ActionListEvidences Action = "listEvidences"
ActionListFrameworks Action = "listFrameworks"
ActionListInvitations Action = "listInvitations"
ActionListMeasures Action = "listMeasures"
ActionListMeetings Action = "listMeetings"
ActionListMembers Action = "listMembers"
ActionListNonconformities Action = "listNonconformities"
ActionListObligations Action = "listObligations"
ActionListPeople Action = "listPeople"
ActionListProcessingActivities Action = "listProcessingActivities"
ActionListReferences Action = "listReferences"
ActionListRiskAssessments Action = "listRiskAssessments"
ActionListRisks Action = "listRisks"
ActionListSAMLConfigurations Action = "listSAMLConfigurations"
ActionListServices Action = "listServices"
ActionListSlackConnections Action = "listSlackConnections"
ActionListSnapshots Action = "listSnapshots"
ActionListTasks Action = "listTasks"
ActionListTrustCenterFiles Action = "listTrustCenterFiles"
ActionListVendors Action = "listVendors"
ActionListVersions Action = "listVersions"
ActionCreateAsset Action = "createAsset"
ActionCreateAudit Action = "createAudit"
ActionCreateContinualImprovement Action = "createContinualImprovement"
ActionCreateControl Action = "createControl"
ActionCreateControlAuditMapping Action = "createControlAuditMapping"
ActionCreateControlDocumentMapping Action = "createControlDocumentMapping"
ActionCreateControlMeasureMapping Action = "createControlMeasureMapping"
ActionCreateControlSnapshotMapping Action = "createControlSnapshotMapping"
ActionCreateCustomDomain Action = "createCustomDomain"
ActionCreateDatum Action = "createDatum"
ActionCreateDocument Action = "createDocument"
ActionCreateDraftDocumentVersion Action = "createDraftDocumentVersion"
ActionCreateFramework Action = "createFramework"
ActionCreateMeasure Action = "createMeasure"
ActionCreateMeeting Action = "createMeeting"
ActionCreateNonconformity Action = "createNonconformity"
ActionCreateObligation Action = "createObligation"
ActionCreatePeople Action = "createPeople"
ActionCreateProcessingActivity Action = "createProcessingActivity"
ActionCreateRisk Action = "createRisk"
ActionCreateRiskDocumentMapping Action = "createRiskDocumentMapping"
ActionCreateRiskMeasureMapping Action = "createRiskMeasureMapping"
ActionCreateRiskObligationMapping Action = "createRiskObligationMapping"
ActionCreateSAMLConfiguration Action = "createSAMLConfiguration"
ActionCreateSnapshot Action = "createSnapshot"
ActionCreateTask Action = "createTask"
ActionCreateTrustCenter Action = "createTrustCenter"
ActionCreateTrustCenterAccess Action = "createTrustCenterAccess"
ActionCreateTrustCenterFile Action = "createTrustCenterFile"
ActionCreateTrustCenterReference Action = "createTrustCenterReference"
ActionCreateVendor Action = "createVendor"
ActionCreateVendorContact Action = "createVendorContact"
ActionCreateVendorRiskAssessment Action = "createVendorRiskAssessment"
ActionCreateVendorService Action = "createVendorService"
ActionUpdateAsset Action = "updateAsset"
ActionUpdateAudit Action = "updateAudit"
ActionUpdateContinualImprovement Action = "updateContinualImprovement"
ActionUpdateControl Action = "updateControl"
ActionUpdateDatum Action = "updateDatum"
ActionUpdateDocument Action = "updateDocument"
ActionUpdateDocumentVersion Action = "updateDocumentVersion"
ActionUpdateFramework Action = "updateFramework"
ActionUpdateMeasure Action = "updateMeasure"
ActionUpdateMeeting Action = "updateMeeting"
ActionUpdateMembership Action = "updateMembership"
ActionUpdateNonconformity Action = "updateNonconformity"
ActionUpdateObligation Action = "updateObligation"
ActionUpdateOrganization Action = "updateOrganization"
ActionUpdatePeople Action = "updatePeople"
ActionUpdateProcessingActivity Action = "updateProcessingActivity"
ActionUpdateRisk Action = "updateRisk"
ActionUpdateSAMLConfiguration Action = "updateSAMLConfiguration"
ActionUpdateTask Action = "updateTask"
ActionUpdateTrustCenter Action = "updateTrustCenter"
ActionUpdateTrustCenterAccess Action = "updateTrustCenterAccess"
ActionUpdateTrustCenterFile Action = "updateTrustCenterFile"
ActionUpdateTrustCenterReference Action = "updateTrustCenterReference"
ActionUpdateVendor Action = "updateVendor"
ActionUpdateVendorBusinessAssociateAgreement Action = "updateVendorBusinessAssociateAgreement"
ActionUpdateVendorContact Action = "updateVendorContact"
ActionUpdateVendorDataPrivacyAgreement Action = "updateVendorDataPrivacyAgreement"
ActionUpdateVendorService Action = "updateVendorService"
ActionDeleteAsset Action = "deleteAsset"
ActionDeleteAudit Action = "deleteAudit"
ActionDeleteAuditReport Action = "deleteAuditReport"
ActionDeleteContinualImprovement Action = "deleteContinualImprovement"
ActionDeleteControl Action = "deleteControl"
ActionDeleteControlAuditMapping Action = "deleteControlAuditMapping"
ActionDeleteControlDocumentMapping Action = "deleteControlDocumentMapping"
ActionDeleteControlMeasureMapping Action = "deleteControlMeasureMapping"
ActionDeleteControlSnapshotMapping Action = "deleteControlSnapshotMapping"
ActionDeleteCustomDomain Action = "deleteCustomDomain"
ActionDeleteDatum Action = "deleteDatum"
ActionDeleteDocument Action = "deleteDocument"
ActionDeleteDraftDocumentVersion Action = "deleteDraftDocumentVersion"
ActionDeleteEvidence Action = "deleteEvidence"
ActionDeleteFramework Action = "deleteFramework"
ActionDeleteInvitation Action = "deleteInvitation"
ActionDeleteMeasure Action = "deleteMeasure"
ActionDeleteMeeting Action = "deleteMeeting"
ActionDeleteNonconformity Action = "deleteNonconformity"
ActionDeleteObligation Action = "deleteObligation"
ActionDeleteOrganization Action = "deleteOrganization"
ActionDeleteOrganizationHorizontalLogo Action = "deleteOrganizationHorizontalLogo"
ActionDeletePeople Action = "deletePeople"
ActionDeleteProcessingActivity Action = "deleteProcessingActivity"
ActionDeleteRisk Action = "deleteRisk"
ActionDeleteRiskDocumentMapping Action = "deleteRiskDocumentMapping"
ActionDeleteRiskMeasureMapping Action = "deleteRiskMeasureMapping"
ActionDeleteRiskObligationMapping Action = "deleteRiskObligationMapping"
ActionDeleteSAMLConfiguration Action = "deleteSAMLConfiguration"
ActionDeleteSnapshot Action = "deleteSnapshot"
ActionDeleteTask Action = "deleteTask"
ActionDeleteTrustCenterAccess Action = "deleteTrustCenterAccess"
ActionDeleteTrustCenterFile Action = "deleteTrustCenterFile"
ActionDeleteTrustCenterNDA Action = "deleteTrustCenterNDA"
ActionDeleteTrustCenterReference Action = "deleteTrustCenterReference"
ActionDeleteVendor Action = "deleteVendor"
ActionDeleteVendorBusinessAssociateAgreement Action = "deleteVendorBusinessAssociateAgreement"
ActionDeleteVendorComplianceReport Action = "deleteVendorComplianceReport"
ActionDeleteVendorContact Action = "deleteVendorContact"
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
ActionDeleteVendorService Action = "deleteVendorService"
ActionAcceptInvitation Action = "acceptInvitation"
ActionAssessVendor Action = "assessVendor"
ActionAssignTask Action = "assignTask"
ActionBulkDeleteDocuments Action = "bulkDeleteDocuments"
ActionBulkExportDocuments Action = "bulkExportDocuments"
ActionBulkPublishDocumentVersions Action = "bulkPublishDocumentVersions"
ActionBulkRequestSignatures Action = "bulkRequestSignatures"
ActionCancelSignatureRequest Action = "cancelSignatureRequest"
ActionConfirmEmail Action = "confirmEmail"
ActionDisableSAML Action = "disableSAML"
ActionEnableSAML Action = "enableSAML"
ActionExportDocumentVersionPDF Action = "exportDocumentVersionPDF"
ActionExportFramework Action = "exportFramework"
ActionGenerateDocumentChangelog Action = "generateDocumentChangelog"
ActionGenerateFrameworkStateOfApplicability Action = "generateFrameworkStateOfApplicability"
ActionImportFramework Action = "importFramework"
ActionImportMeasure Action = "importMeasure"
ActionInitiateDomainVerification Action = "initiateDomainVerification"
ActionInviteUser Action = "inviteUser"
ActionPublishDocumentVersion Action = "publishDocumentVersion"
ActionRemoveMember Action = "removeMember"
ActionRequestSignature Action = "requestSignature"
ActionSendSigningNotifications Action = "sendSigningNotifications"
ActionUnassignTask Action = "unassignTask"
ActionUploadAuditReport Action = "uploadAuditReport"
ActionUploadMeasureEvidence Action = "uploadMeasureEvidence"
ActionUploadTrustCenterNDA Action = "uploadTrustCenterNDA"
ActionUploadVendorBusinessAssociateAgreement Action = "uploadVendorBusinessAssociateAgreement"
ActionUploadVendorComplianceReport Action = "uploadVendorComplianceReport"
ActionUploadVendorDataPrivacyAgreement Action = "uploadVendorDataPrivacyAgreement"
ActionVerifyDomain Action = "verifyDomain"
)
var (
AllRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull}
EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull}
)
var Permissions = map[uint16]map[Action][]Role{
coredata.OrganizationEntityType: {
ActionGet: AllRoles,
ActionGetLogoUrl: AllRoles,
ActionGetHorizontalLogoUrl: AllRoles,
ActionMemberships: AllRoles,
ActionPeoples: AllRoles,
ActionTotalCount: AllRoles,
ActionListMembers: AllRoles,
ActionListInvitations: AllRoles,
ActionListSlackConnections: AllRoles,
ActionListFrameworks: AllRoles,
ActionListControls: AllRoles,
ActionListVendors: AllRoles,
ActionListPeople: AllRoles,
ActionListDocuments: AllRoles,
ActionListMeetings: AllRoles,
ActionListMeasures: AllRoles,
ActionListRisks: AllRoles,
ActionListTasks: AllRoles,
ActionListAssets: AllRoles,
ActionListData: AllRoles,
ActionListAudits: AllRoles,
ActionListNonconformities: AllRoles,
ActionListObligations: AllRoles,
ActionListContinualImprovements: AllRoles,
ActionListProcessingActivities: AllRoles,
ActionListSnapshots: AllRoles,
ActionListTrustCenterFiles: AllRoles,
ActionGetTrustCenter: AllRoles,
ActionGetCustomDomain: AllRoles,
ActionListSAMLConfigurations: AllRoles,
ActionConfirmEmail: AllRoles,
ActionAcceptInvitation: AllRoles,
ActionUpdateOrganization: EditRoles,
ActionDeleteOrganizationHorizontalLogo: EditRoles,
ActionCreateTrustCenter: EditRoles,
ActionInviteUser: EditRoles,
ActionDeleteInvitation: EditRoles,
ActionUpdateMembership: EditRoles,
ActionCreatePeople: EditRoles,
ActionCreateVendor: EditRoles,
ActionCreateFramework: EditRoles,
ActionImportFramework: EditRoles,
ActionCreateControl: EditRoles,
ActionCreateMeasure: EditRoles,
ActionImportMeasure: EditRoles,
ActionCreateMeeting: EditRoles,
ActionCreateTask: EditRoles,
ActionCreateRisk: EditRoles,
ActionCreateDocument: EditRoles,
ActionCreateAsset: EditRoles,
ActionCreateDatum: EditRoles,
ActionCreateAudit: EditRoles,
ActionCreateNonconformity: EditRoles,
ActionCreateObligation: EditRoles,
ActionCreateContinualImprovement: EditRoles,
ActionCreateProcessingActivity: EditRoles,
ActionCreateSnapshot: EditRoles,
ActionCreateTrustCenterFile: EditRoles,
ActionSendSigningNotifications: EditRoles,
ActionRemoveMember: {RoleOwner, RoleFull},
ActionCreateCustomDomain: {RoleOwner},
ActionInitiateDomainVerification: {RoleOwner},
ActionVerifyDomain: {RoleOwner},
ActionCreateSAMLConfiguration: {RoleOwner},
ActionDeleteOrganization: {RoleOwner},
},
coredata.TrustCenterEntityType: {
ActionGet: AllRoles,
ActionGetNdaFileUrl: AllRoles,
ActionGetOrganization: AllRoles,
ActionListAccesses: AllRoles,
ActionListReferences: AllRoles,
ActionUpdateTrustCenter: EditRoles,
ActionUploadTrustCenterNDA: EditRoles,
ActionDeleteTrustCenterNDA: EditRoles,
ActionCreateTrustCenterAccess: EditRoles,
ActionCreateTrustCenterReference: EditRoles,
},
coredata.TrustCenterAccessEntityType: {
ActionGet: AllRoles,
ActionActiveCount: AllRoles,
ActionPendingRequestCount: AllRoles,
ActionAvailableDocumentAccesses: AllRoles,
ActionUpdateTrustCenterAccess: EditRoles,
ActionDeleteTrustCenterAccess: EditRoles,
},
coredata.TrustCenterReferenceEntityType: {
ActionGet: AllRoles,
ActionGetLogoUrl: AllRoles,
ActionUpdateTrustCenterReference: EditRoles,
ActionDeleteTrustCenterReference: EditRoles,
},
coredata.TrustCenterFileEntityType: {
ActionGet: AllRoles,
ActionGetFileUrl: AllRoles,
ActionUpdateTrustCenterFile: EditRoles,
ActionGetTrustCenterFile: EditRoles,
ActionDeleteTrustCenterFile: EditRoles,
},
coredata.UserEntityType: {
ActionGet: AllRoles,
},
coredata.MembershipEntityType: {
ActionGet: AllRoles,
ActionGetAuthMethod: AllRoles,
},
coredata.InvitationEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
},
coredata.PeopleEntityType: {
ActionGet: AllRoles,
ActionUpdatePeople: EditRoles,
ActionDeletePeople: EditRoles,
},
coredata.VendorEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
ActionListComplianceReports: AllRoles,
ActionGetBusinessAssociateAgreement: AllRoles,
ActionGetDataPrivacyAgreement: AllRoles,
ActionListContacts: AllRoles,
ActionListServices: AllRoles,
ActionListRiskAssessments: AllRoles,
ActionGetBusinessOwner: AllRoles,
ActionGetSecurityOwner: AllRoles,
ActionUpdateVendor: EditRoles,
ActionDeleteVendor: EditRoles,
ActionCreateVendorContact: EditRoles,
ActionCreateVendorService: EditRoles,
ActionUploadVendorComplianceReport: EditRoles,
ActionUploadVendorBusinessAssociateAgreement: EditRoles,
ActionDeleteVendorBusinessAssociateAgreement: EditRoles,
ActionUploadVendorDataPrivacyAgreement: EditRoles,
ActionCreateVendorRiskAssessment: EditRoles,
ActionAssessVendor: EditRoles,
},
coredata.VendorComplianceReportEntityType: {
ActionGet: AllRoles,
ActionGetVendor: AllRoles,
ActionGetFile: AllRoles,
ActionDeleteVendorComplianceReport: EditRoles,
},
coredata.VendorBusinessAssociateAgreementEntityType: {
ActionGet: AllRoles,
ActionGetVendor: AllRoles,
ActionGetFileUrl: AllRoles,
ActionUpdateVendorBusinessAssociateAgreement: EditRoles,
ActionDeleteVendorBusinessAssociateAgreement: EditRoles,
},
coredata.VendorContactEntityType: {
ActionGet: AllRoles,
ActionGetVendor: AllRoles,
ActionUpdateVendorContact: EditRoles,
ActionDeleteVendorContact: EditRoles,
},
coredata.VendorServiceEntityType: {
ActionGet: AllRoles,
ActionGetVendor: AllRoles,
ActionUpdateVendorService: EditRoles,
ActionDeleteVendorService: EditRoles,
},
coredata.VendorDataPrivacyAgreementEntityType: {
ActionGet: AllRoles,
ActionGetVendor: AllRoles,
ActionGetFileUrl: AllRoles,
ActionUpdateVendorDataPrivacyAgreement: EditRoles,
ActionDeleteVendorDataPrivacyAgreement: EditRoles,
},
coredata.VendorRiskAssessmentEntityType: {
ActionGet: AllRoles,
},
coredata.FrameworkEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
ActionListControls: AllRoles,
ActionCreateControl: EditRoles,
ActionUpdateFramework: EditRoles,
ActionDeleteFramework: EditRoles,
ActionGenerateFrameworkStateOfApplicability: EditRoles,
ActionExportFramework: EditRoles,
},
coredata.ControlEntityType: {
ActionGet: AllRoles,
ActionGetFramework: AllRoles,
ActionListMeasures: AllRoles,
ActionListDocuments: AllRoles,
ActionListAudits: AllRoles,
ActionListSnapshots: AllRoles,
ActionUpdateControl: EditRoles,
ActionDeleteControl: EditRoles,
ActionCreateControlMeasureMapping: EditRoles,
ActionCreateControlDocumentMapping: EditRoles,
ActionDeleteControlMeasureMapping: EditRoles,
ActionDeleteControlDocumentMapping: EditRoles,
ActionCreateControlAuditMapping: EditRoles,
ActionDeleteControlAuditMapping: EditRoles,
ActionCreateControlSnapshotMapping: EditRoles,
ActionDeleteControlSnapshotMapping: EditRoles,
},
coredata.MeasureEntityType: {
ActionGet: AllRoles,
ActionListEvidences: AllRoles,
ActionListTasks: AllRoles,
ActionListRisks: AllRoles,
ActionListControls: AllRoles,
ActionTotalCount: AllRoles,
ActionUpdateMeasure: EditRoles,
ActionDeleteMeasure: EditRoles,
ActionUploadMeasureEvidence: EditRoles,
},
coredata.TaskEntityType: {
ActionGet: AllRoles,
ActionGetAssignedTo: AllRoles,
ActionGetOrganization: AllRoles,
ActionGetMeasure: AllRoles,
ActionListEvidences: AllRoles,
ActionUpdateTask: EditRoles,
ActionDeleteTask: EditRoles,
ActionAssignTask: EditRoles,
ActionUnassignTask: EditRoles,
},
coredata.EvidenceEntityType: {
ActionGet: AllRoles,
ActionGetFile: AllRoles,
ActionGetTask: AllRoles,
ActionGetMeasure: AllRoles,
ActionDeleteEvidence: EditRoles,
},
coredata.DocumentEntityType: {
ActionGet: AllRoles,
ActionExportDocumentVersionPDF: AllRoles,
ActionGetOwner: AllRoles,
ActionGetOrganization: AllRoles,
ActionListVersions: AllRoles,
ActionListControls: AllRoles,
ActionTotalCount: AllRoles,
ActionUpdateDocument: EditRoles,
ActionDeleteDocument: EditRoles,
ActionPublishDocumentVersion: EditRoles,
ActionBulkPublishDocumentVersions: EditRoles,
ActionBulkDeleteDocuments: EditRoles,
ActionBulkExportDocuments: EditRoles,
ActionGenerateDocumentChangelog: EditRoles,
ActionCreateDraftDocumentVersion: EditRoles,
ActionDeleteDraftDocumentVersion: EditRoles,
ActionUpdateDocumentVersion: EditRoles,
ActionRequestSignature: EditRoles,
ActionBulkRequestSignatures: EditRoles,
ActionSendSigningNotifications: EditRoles,
ActionCancelSignatureRequest: EditRoles,
},
coredata.DocumentVersionEntityType: {
ActionGet: AllRoles,
ActionGetFile: AllRoles,
ActionGetOwner: AllRoles,
ActionDocument: AllRoles,
ActionSignatures: AllRoles,
ActionUpdateDocumentVersion: EditRoles,
},
coredata.DocumentVersionSignatureEntityType: {
ActionGet: AllRoles,
ActionDocumentVersion: AllRoles,
ActionSignedBy: AllRoles,
},
coredata.RiskEntityType: {
ActionGet: AllRoles,
ActionGetOwner: AllRoles,
ActionGetOrganization: AllRoles,
ActionTotalCount: AllRoles,
ActionListControls: AllRoles,
ActionListMeasures: AllRoles,
ActionListDocuments: AllRoles,
ActionListObligations: AllRoles,
ActionUpdateRisk: EditRoles,
ActionDeleteRisk: EditRoles,
ActionCreateRiskMeasureMapping: EditRoles,
ActionDeleteRiskMeasureMapping: EditRoles,
ActionCreateRiskDocumentMapping: EditRoles,
ActionDeleteRiskDocumentMapping: EditRoles,
ActionCreateRiskObligationMapping: EditRoles,
ActionDeleteRiskObligationMapping: EditRoles,
},
coredata.AssetEntityType: {
ActionGet: AllRoles,
ActionGetOwner: AllRoles,
ActionListVendors: AllRoles,
ActionGetAssetType: AllRoles,
ActionGetOrganization: AllRoles,
ActionUpdateAsset: EditRoles,
ActionDeleteAsset: EditRoles,
},
coredata.DatumEntityType: {
ActionGet: AllRoles,
ActionGetOwner: AllRoles,
ActionGetOrganization: AllRoles,
ActionListVendors: AllRoles,
ActionUpdateDatum: EditRoles,
ActionDeleteDatum: EditRoles,
},
coredata.AuditEntityType: {
ActionGet: AllRoles,
ActionGetFile: AllRoles,
ActionGetFramework: AllRoles,
ActionGetOrganization: AllRoles,
ActionReport: AllRoles,
ActionReportUrl: AllRoles,
ActionListControls: AllRoles,
ActionUpdateAudit: EditRoles,
ActionDeleteAudit: EditRoles,
ActionUploadAuditReport: EditRoles,
ActionDeleteAuditReport: EditRoles,
},
coredata.ReportEntityType: {
ActionGet: AllRoles,
ActionGetFile: AllRoles,
ActionGetOrganization: AllRoles,
ActionGetSnapshot: AllRoles,
ActionDownloadUrl: AllRoles,
},
coredata.NonconformityEntityType: {
ActionGet: AllRoles,
ActionGetOwner: AllRoles,
ActionGetOrganization: AllRoles,
ActionAudit: AllRoles,
ActionUpdateNonconformity: EditRoles,
ActionDeleteNonconformity: EditRoles,
},
coredata.ObligationEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
ActionGetOwner: AllRoles,
ActionListRisks: AllRoles,
ActionUpdateObligation: EditRoles,
ActionDeleteObligation: EditRoles,
},
coredata.ContinualImprovementEntityType: {
ActionGet: AllRoles,
ActionGetOwner: AllRoles,
ActionGetOrganization: AllRoles,
ActionUpdateContinualImprovement: EditRoles,
ActionDeleteContinualImprovement: EditRoles,
},
coredata.ProcessingActivityEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
ActionListVendors: AllRoles,
ActionUpdateProcessingActivity: EditRoles,
ActionDeleteProcessingActivity: EditRoles,
},
coredata.SnapshotEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
ActionListControls: AllRoles,
ActionDeleteSnapshot: EditRoles,
},
coredata.CustomDomainEntityType: {
ActionGet: {RoleOwner, RoleAdmin},
ActionDeleteCustomDomain: {RoleOwner},
},
coredata.SAMLConfigurationEntityType: {
ActionGet: {RoleOwner, RoleAdmin},
ActionSpMetadataUrl: {RoleOwner, RoleAdmin},
ActionTestLoginUrl: {RoleOwner, RoleAdmin},
ActionUpdateSAMLConfiguration: {RoleOwner, RoleAdmin},
ActionDeleteSAMLConfiguration: {RoleOwner, RoleAdmin},
ActionEnableSAML: {RoleOwner, RoleAdmin},
ActionDisableSAML: {RoleOwner, RoleAdmin},
},
coredata.FileEntityType: {
ActionGet: AllRoles,
ActionDownloadUrl: AllRoles,
},
coredata.TrustCenterDocumentAccessEntityType: {
ActionGet: AllRoles,
ActionReport: AllRoles,
ActionTrustCenterFile: AllRoles,
},
coredata.MeetingEntityType: {
ActionGet: AllRoles,
ActionGetOrganization: AllRoles,
ActionTotalCount: AllRoles,
ActionUpdateMeeting: EditRoles,
ActionDeleteMeeting: EditRoles,
},
}
func GetPermissionsForAction(entityType uint16, action Action) []Role {
if entityActions, ok := Permissions[entityType]; ok {
if roles, ok := entityActions[action]; ok {
return roles
}
}
return nil
}
func GetPermissionsByRole(userRole Role) map[string]map[Action]bool {
permissions := make(map[string]map[Action]bool)
for entityType, actions := range Permissions {
entityTypeName, ok := coredata.EntityModel(entityType)
if !ok {
continue
}
if permissions[entityTypeName] == nil {
permissions[entityTypeName] = make(map[Action]bool)
}
for action, allowedRoles := range actions {
if slices.Contains(allowedRoles, userRole) {
permissions[entityTypeName][action] = true
}
}
}
return permissions
}

View File

@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"net/url"
"slices"
"time"
"go.gearno.de/kit/pg"
@@ -37,6 +38,14 @@ func (e *TenantAccessError) Error() string {
return "not authorized"
}
type PermissionDeniedError struct {
Message string
}
func (e *PermissionDeniedError) Error() string {
return e.Message
}
type (
Service struct {
pg *pg.Client
@@ -103,6 +112,27 @@ func (s *Service) GetAllUserOrganizations(
return organizations, err
}
func (s *Service) GetUserOrganizationsWithRole(
ctx context.Context,
userID gid.GID,
role coredata.MembershipRole,
) (coredata.Organizations, error) {
organizations := coredata.Organizations{}
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := organizations.LoadAllByUserIDWithRole(ctx, conn, userID, role); err != nil {
return fmt.Errorf("cannot load user organizations with role: %w", err)
}
return nil
},
)
return organizations, err
}
func (s *Service) GetAllOrganizationsForUserAPIKeyId(
ctx context.Context,
userAPIKeyID gid.GID,
@@ -143,70 +173,6 @@ func (s *Service) GetUserOrganizations(
return organizations, err
}
func (s *Service) AcceptInvitation(
ctx context.Context,
token string,
userID gid.GID,
) error {
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
s.tokenSecret,
TokenTypeOrganizationInvitation,
token,
)
if err != nil {
return fmt.Errorf("invalid invitation token: %w", err)
}
invitationData := payload.Data
scope := coredata.NewScope(invitationData.InvitationID.TenantID())
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
invitation := &coredata.Invitation{}
if err := invitation.LoadByID(ctx, tx, scope, invitationData.InvitationID); err != nil {
var errInvitationNotFound *coredata.ErrInvitationNotFound
if errors.As(err, &errInvitationNotFound) {
return fmt.Errorf("invitation was deleted or no longer exists")
}
return fmt.Errorf("cannot load invitation: %w", err)
}
if invitation.AcceptedAt != nil {
return fmt.Errorf("invitation already accepted")
}
if time.Now().After(invitation.ExpiresAt) {
return fmt.Errorf("invitation expired")
}
now := time.Now()
membershipID := gid.New(scope.GetTenantID(), coredata.MembershipEntityType)
membership := &coredata.Membership{
ID: membershipID,
UserID: userID,
OrganizationID: invitation.OrganizationID,
Role: invitation.Role,
CreatedAt: now,
UpdatedAt: now,
}
if err := membership.Create(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot add user to organization: %w", err)
}
invitation.AcceptedAt = &now
if err := invitation.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot mark invitation as accepted: %w", err)
}
return nil
},
)
}
func (s *Service) AcceptInvitationByID(
ctx context.Context,
invitationID gid.GID,
@@ -274,36 +240,11 @@ func (s *Service) AcceptInvitationByID(
return acceptedInvitation, nil
}
func (s *Service) GetUserInvitations(
ctx context.Context,
email string,
cursor *page.Cursor[coredata.InvitationOrderField],
filter *coredata.InvitationFilter,
) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) {
var invitations coredata.Invitations
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := invitations.LoadByEmail(ctx, conn, coredata.NewNoScope(), email, cursor, filter); err != nil {
return fmt.Errorf("cannot load invitations: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(invitations, cursor), nil
}
type UserInvitation struct {
ID gid.GID
Email string
FullName string
Role coredata.Role
Role coredata.MembershipRole
ExpiresAt time.Time
AcceptedAt *time.Time
CreatedAt time.Time
@@ -417,7 +358,7 @@ func (s *TenantAuthzService) AddUserToOrganization(
ctx context.Context,
userID gid.GID,
orgID gid.GID,
role coredata.Role,
role coredata.MembershipRole,
) error {
now := time.Now()
membershipID := gid.New(s.scope.GetTenantID(), coredata.MembershipEntityType)
@@ -538,6 +479,30 @@ func (s *TenantAuthzService) DeleteInvitation(
)
}
func (s *TenantAuthzService) GetMembershipByUserAndOrganizationID(
ctx context.Context,
userID gid.GID,
orgID gid.GID,
) (*coredata.Membership, error) {
membership := &coredata.Membership{}
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := membership.LoadByUserAndOrg(ctx, conn, s.scope, userID, orgID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return membership, nil
}
func (s *TenantAuthzService) GetMembershipsByOrganizationID(
ctx context.Context,
orgID gid.GID,
@@ -609,41 +574,11 @@ func (s *TenantAuthzService) CountOrganizationUsers(
return count, nil
}
func (s *TenantAuthzService) CanUserAccessOrganization(
ctx context.Context,
userID gid.GID,
orgID gid.GID,
) (bool, error) {
membership := &coredata.Membership{}
haveAccess := false
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := membership.LoadByUserAndOrg(ctx, conn, s.scope, userID, orgID); err != nil {
if _, ok := err.(coredata.ErrMembershipNotFound); ok {
return nil // Not an error, just no access
}
return fmt.Errorf("cannot check organization access: %w", err)
}
haveAccess = true
return nil
},
)
if err != nil {
return false, err
}
return haveAccess, nil
}
func (s *TenantAuthzService) GetUserRoleInOrganization(
ctx context.Context,
userID gid.GID,
orgID gid.GID,
) (coredata.Role, error) {
) (coredata.MembershipRole, error) {
membership := &coredata.Membership{}
err := s.pg.WithConn(
@@ -690,30 +625,55 @@ func (s *TenantAuthzService) RemoveMemberFromOrganization(
)
}
func (s *TenantAuthzService) UpdateUserRole(
func (s *TenantAuthzService) UpdateMembershipRole(
ctx context.Context,
userID gid.GID,
orgID gid.GID,
newRole coredata.Role,
) error {
return s.pg.WithTx(
memberID gid.GID,
newRole coredata.MembershipRole,
) (*coredata.Membership, error) {
membership := &coredata.Membership{}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
membership := &coredata.Membership{}
if err := membership.LoadByUserAndOrg(ctx, tx, s.scope, userID, orgID); err != nil {
return fmt.Errorf("cannot find membership: %w", err)
if err := membership.LoadByID(ctx, tx, s.scope, memberID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.OrganizationID != orgID {
return fmt.Errorf("membership does not belong to organization")
}
// If the new role cannot create API keys, delete all related API key memberships
if newRole != coredata.MembershipRoleOwner {
var apiKeyMemberships coredata.UserAPIKeyMemberships
if err := apiKeyMemberships.LoadByMembershipID(ctx, tx, s.scope, memberID); err != nil {
return fmt.Errorf("cannot load api key memberships: %w", err)
}
for _, apiKeyMembership := range apiKeyMemberships {
if err := apiKeyMembership.Delete(ctx, tx, s.scope); err != nil {
return fmt.Errorf("cannot delete api key membership: %w", err)
}
}
}
membership.Role = newRole
membership.UpdatedAt = time.Now()
if err := membership.Update(ctx, tx, s.scope); err != nil {
return fmt.Errorf("cannot update user role: %w", err)
return fmt.Errorf("cannot update membership role: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return membership, nil
}
func (s *TenantAuthzService) InviteUserToOrganization(
@@ -721,7 +681,7 @@ func (s *TenantAuthzService) InviteUserToOrganization(
organizationID gid.GID,
emailAddress string,
fullName string,
role coredata.Role,
role coredata.MembershipRole,
) (*coredata.Invitation, error) {
var invitation *coredata.Invitation
@@ -827,7 +787,7 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
ctx context.Context,
userID gid.GID,
organizationID gid.GID,
role *coredata.Role,
role *coredata.MembershipRole,
) error {
now := time.Now()
@@ -842,7 +802,7 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
return fmt.Errorf("cannot load membership: %w", err)
}
membershipRole := coredata.RoleMember
membershipRole := coredata.MembershipRoleViewer
if role != nil {
membershipRole = *role
}
@@ -878,15 +838,94 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
)
}
// This is a placeholder for future permission system
func (s *TenantAuthzService) HasPermission(
func (s *TenantAuthzService) Authorize(
ctx context.Context,
userID gid.GID,
orgID gid.GID,
resource string,
action string,
) (bool, error) {
// For now, just check if user is a member
// In the future, this will check specific permissions based on role
return s.CanUserAccessOrganization(ctx, userID, orgID)
user *coredata.User,
apiKey *coredata.UserAPIKey,
entityGID gid.GID,
action Action,
) error {
requiredRoles := GetPermissionsForAction(entityGID.EntityType(), action)
if requiredRoles == nil {
return &PermissionDeniedError{
Message: fmt.Sprintf("no permissions defined for action %s on entity type %d", action, entityGID.EntityType()),
}
}
role, err := s.GetUserOrAPIKeyRole(ctx, user, apiKey, entityGID)
if err != nil {
return fmt.Errorf("cannot get user or API key role: %w", err)
}
if !slices.Contains(requiredRoles, role) {
return &PermissionDeniedError{
Message: fmt.Sprintf("role %s not authorized for action %s, requires one of %v", role, action, requiredRoles),
}
}
return nil
}
func (s *TenantAuthzService) CanAssignRole(
ctx context.Context,
user *coredata.User,
apiKey *coredata.UserAPIKey,
entityGID gid.GID,
targetRole coredata.MembershipRole,
) error {
currentRole, err := s.GetUserOrAPIKeyRole(ctx, user, apiKey, entityGID)
if err != nil {
return fmt.Errorf("cannot get user or API key role: %w", err)
}
if currentRole == RoleOwner || currentRole == RoleFull {
return nil
}
if currentRole == RoleAdmin {
if targetRole == coredata.MembershipRoleOwner {
return &PermissionDeniedError{Message: "admin users cannot assign owner role"}
}
return nil
}
return &PermissionDeniedError{Message: fmt.Sprintf("role %s cannot assign roles", currentRole)}
}
func (s *TenantAuthzService) GetUserOrAPIKeyRole(
ctx context.Context,
user *coredata.User,
apiKey *coredata.UserAPIKey,
entityGID gid.GID,
) (Role, error) {
var role Role
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if user != nil {
membership := &coredata.Membership{}
if err := membership.LoadRoleByUserAndEntityID(ctx, conn, s.scope, user.ID, entityGID); err != nil {
return fmt.Errorf("cannot get user role: %w", err)
}
role = Role(membership.Role.String())
return nil
}
if apiKey != nil {
apiKeyMembership := &coredata.UserAPIKeyMembership{}
if err := apiKeyMembership.LoadRoleByAPIKeyAndEntityID(ctx, conn, s.scope, apiKey.ID, entityGID); err != nil {
return fmt.Errorf("cannot get API key role: %w", err)
}
role = Role(apiKeyMembership.Role.String())
return nil
}
return fmt.Errorf("no user or API key provided")
},
)
if err != nil {
return "", err
}
return role, nil
}

View File

@@ -47,13 +47,14 @@ func (a *UserAPIKeyMembership) Insert(
) error {
q := `
INSERT INTO
authz_api_keys_memberships (id, tenant_id, auth_user_api_key_id, membership_id, role, created_at, updated_at)
authz_api_keys_memberships (id, tenant_id, auth_user_api_key_id, membership_id, role, organization_id, created_at, updated_at)
VALUES (
@id,
@tenant_id,
@auth_user_api_key_id,
@membership_id,
@role,
@organization_id,
@created_at,
@updated_at
)
@@ -65,6 +66,7 @@ VALUES (
"auth_user_api_key_id": a.UserAPIKeyID,
"membership_id": a.MembershipID,
"role": a.Role,
"organization_id": a.OrganizationID,
"created_at": a.CreatedAt,
"updated_at": a.UpdatedAt,
}
@@ -127,6 +129,133 @@ ORDER BY akm.created_at DESC
return nil
}
// LoadRoleByAPIKeyAndEntityID loads an API key's role by querying any entity to extract its organization_id
func (a *UserAPIKeyMembership) LoadRoleByAPIKeyAndEntityID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
apiKeyID gid.GID,
entityID gid.GID,
) error {
entityType := entityID.EntityType()
// For organization, the entity ID is the organization ID
if entityType == OrganizationEntityType {
return a.LoadByAPIKeyIDAndOrganizationID(ctx, conn, scope, apiKeyID, entityID)
}
tableName, ok := EntityTable(entityType)
if !ok {
return fmt.Errorf("unsupported entity type for API key role lookup: %d", entityType)
}
query := fmt.Sprintf(`
SELECT
akm.id,
akm.auth_user_api_key_id,
akm.membership_id,
akm.role,
akm.created_at,
akm.updated_at
FROM
authz_api_keys_memberships akm
INNER JOIN authz_memberships m ON m.id = akm.membership_id
INNER JOIN %s e ON e.id = @entity_id
WHERE
%s
AND akm.auth_user_api_key_id = @api_key_id
AND m.organization_id = e.organization_id
LIMIT 1;
`, tableName, scope.SQLFragment())
args := pgx.NamedArgs{
"api_key_id": apiKeyID,
"entity_id": entityID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot query API key membership by entity: %w", err)
}
defer rows.Close()
if !rows.Next() {
return fmt.Errorf("API key membership not found for key %s and entity %s", apiKeyID, entityID)
}
var membership UserAPIKeyMembership
err = rows.Scan(
&membership.ID,
&membership.UserAPIKeyID,
&membership.MembershipID,
&membership.Role,
&membership.CreatedAt,
&membership.UpdatedAt,
)
if err != nil {
return fmt.Errorf("cannot scan API key membership: %w", err)
}
*a = membership
return nil
}
func (a *UserAPIKeyMembership) LoadByAPIKeyIDAndOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
apiKeyID gid.GID,
organizationID gid.GID,
) error {
q := `
SELECT
akm.id,
akm.auth_user_api_key_id,
akm.membership_id,
akm.role,
akm.created_at,
akm.updated_at,
m.organization_id,
o.name as organization_name
FROM
authz_api_keys_memberships akm
JOIN
authz_memberships m ON akm.membership_id = m.id
JOIN
organizations o ON m.organization_id = o.id
WHERE
akm.auth_user_api_key_id = @api_key_id
AND m.organization_id = @organization_id
AND m.%s
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"api_key_id": apiKeyID,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query user api key membership: %w", err)
}
membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[UserAPIKeyMembership])
if err != nil {
if err == pgx.ErrNoRows {
return fmt.Errorf("API key does not have access to organization")
}
return fmt.Errorf("cannot collect user api key membership: %w", err)
}
*a = membership
return nil
}
func (a *UserAPIKeyMembership) Delete(
ctx context.Context,
conn pg.Conn,
@@ -155,6 +284,56 @@ WHERE
return nil
}
func (a *UserAPIKeyMemberships) LoadByMembershipID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
membershipID gid.GID,
) error {
q := `
SELECT
akm.id,
akm.auth_user_api_key_id,
akm.membership_id,
akm.role,
akm.created_at,
akm.updated_at,
m.organization_id,
o.name as organization_name
FROM
authz_api_keys_memberships akm
JOIN
authz_memberships m ON akm.membership_id = m.id
JOIN
organizations o ON m.organization_id = o.id
WHERE
akm.membership_id = @membership_id
AND m.%s
ORDER BY akm.created_at DESC
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"membership_id": membershipID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query user api key memberships by membership id: %w", err)
}
memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserAPIKeyMembership])
if err != nil {
return fmt.Errorf("cannot collect user api key memberships: %w", err)
}
*a = memberships
return nil
}
func DeleteAllUserAPIKeyMembershipsByUserAPIKeyID(
ctx context.Context,
conn pg.Conn,

View File

@@ -46,6 +46,7 @@ func (av AssetVendors) Merge(
conn pg.Conn,
scope Scoper,
assetID gid.GID,
organizationID gid.GID,
vendorIDs []gid.GID,
) error {
q := `
@@ -54,6 +55,7 @@ WITH vendor_ids AS (
unnest(@vendor_ids::text[]) AS vendor_id,
@tenant_id AS tenant_id,
@asset_id AS asset_id,
@organization_id AS organization_id,
@created_at::timestamptz AS created_at
)
MERGE INTO asset_vendors AS tgt
@@ -62,18 +64,19 @@ ON tgt.tenant_id = src.tenant_id
AND tgt.asset_id = src.asset_id
AND tgt.vendor_id = src.vendor_id
WHEN NOT MATCHED
THEN INSERT (tenant_id, asset_id, vendor_id, created_at)
VALUES (src.tenant_id, src.asset_id, src.vendor_id, src.created_at)
THEN INSERT (tenant_id, asset_id, vendor_id, organization_id, created_at)
VALUES (src.tenant_id, src.asset_id, src.vendor_id, src.organization_id, src.created_at)
WHEN NOT MATCHED BY SOURCE
AND tgt.tenant_id = @tenant_id AND tgt.asset_id = @asset_id
THEN DELETE
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"asset_id": assetID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
"tenant_id": scope.GetTenantID(),
"asset_id": assetID,
"organization_id": organizationID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
}
_, err := conn.Exec(ctx, q, args)
@@ -89,26 +92,29 @@ func (av AssetVendors) Insert(
conn pg.Conn,
scope Scoper,
assetID gid.GID,
organizationID gid.GID,
vendorIDs []gid.GID,
) error {
q := `
WITH vendor_ids AS (
SELECT unnest(@vendor_ids::text[]) AS vendor_id
)
INSERT INTO asset_vendors (tenant_id, asset_id, vendor_id, created_at)
INSERT INTO asset_vendors (tenant_id, asset_id, vendor_id, organization_id, created_at)
SELECT
@tenant_id AS tenant_id,
@asset_id AS asset_id,
vendor_id,
@organization_id AS organization_id,
@created_at AS created_at
FROM vendor_ids
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"asset_id": assetID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
"tenant_id": scope.GetTenantID(),
"asset_id": assetID,
"organization_id": organizationID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -31,6 +31,7 @@ import (
type (
Control struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
SectionTitle string `db:"section_title"`
FrameworkID gid.GID `db:"framework_id"`
Name string `db:"name"`
@@ -127,6 +128,7 @@ WITH ctrl AS (
c.id,
c.section_title,
c.framework_id,
c.organization_id,
c.tenant_id,
c.name,
c.description,
@@ -146,6 +148,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -236,6 +239,7 @@ WITH ctrl AS (
c.id,
c.section_title,
c.framework_id,
c.organization_id,
c.tenant_id,
c.name,
c.description,
@@ -255,6 +259,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -351,6 +356,7 @@ WITH ctrl AS (
c.id,
c.section_title,
c.framework_id,
c.organization_id,
c.tenant_id,
c.name,
c.description,
@@ -376,6 +382,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -455,6 +462,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -548,6 +556,7 @@ WITH ctrl AS (
c.id,
c.section_title,
c.framework_id,
c.organization_id,
c.tenant_id,
c.name,
c.description,
@@ -567,6 +576,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -613,6 +623,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -661,6 +672,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -707,6 +719,7 @@ INSERT INTO
controls (
tenant_id,
id,
organization_id,
framework_id,
section_title,
name,
@@ -719,6 +732,7 @@ INSERT INTO
VALUES (
@tenant_id,
@control_id,
@organization_id,
@framework_id,
@section_title,
@name,
@@ -733,6 +747,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"control_id": c.ID,
"organization_id": c.OrganizationID,
"framework_id": c.FrameworkID,
"section_title": c.SectionTitle,
"name": c.Name,
@@ -884,6 +899,7 @@ WITH ctrl AS (
c.id,
c.section_title,
c.framework_id,
c.organization_id,
c.tenant_id,
c.name,
c.description,
@@ -903,6 +919,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,
@@ -994,6 +1011,7 @@ WITH ctrl AS (
c.id,
c.section_title,
c.framework_id,
c.organization_id,
c.tenant_id,
c.name,
c.description,
@@ -1013,6 +1031,7 @@ SELECT
id,
section_title,
framework_id,
organization_id,
name,
description,
status,

View File

@@ -27,9 +27,10 @@ import (
type (
ControlAudit struct {
ControlID gid.GID `db:"control_id"`
AuditID gid.GID `db:"audit_id"`
CreatedAt time.Time `db:"created_at"`
ControlID gid.GID `db:"control_id"`
AuditID gid.GID `db:"audit_id"`
OrganizationID gid.GID `db:"organization_id"`
CreatedAt time.Time `db:"created_at"`
}
ControlAudits []*ControlAudit
@@ -45,12 +46,14 @@ INSERT INTO
controls_audits (
control_id,
audit_id,
organization_id,
tenant_id,
created_at
)
VALUES (
@control_id,
@audit_id,
@organization_id,
@tenant_id,
@created_at
)
@@ -58,10 +61,11 @@ ON CONFLICT (control_id, audit_id) DO NOTHING;
`
args := pgx.StrictNamedArgs{
"control_id": ca.ControlID,
"audit_id": ca.AuditID,
"tenant_id": scope.GetTenantID(),
"created_at": ca.CreatedAt,
"control_id": ca.ControlID,
"audit_id": ca.AuditID,
"organization_id": ca.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": ca.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err

View File

@@ -29,10 +29,11 @@ import (
type (
ControlDocument struct {
ControlID gid.GID `db:"control_id"`
DocumentID gid.GID `db:"document_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
ControlID gid.GID `db:"control_id"`
DocumentID gid.GID `db:"document_id"`
OrganizationID gid.GID `db:"organization_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
}
ControlDocuments []*ControlDocument
@@ -57,22 +58,25 @@ INSERT INTO
controls_documents (
control_id,
document_id,
organization_id,
tenant_id,
created_at
)
VALUES (
@control_id,
@document_id,
@organization_id,
@tenant_id,
@created_at
);
`
args := pgx.StrictNamedArgs{
"control_id": cp.ControlID,
"document_id": cp.DocumentID,
"tenant_id": scope.GetTenantID(),
"created_at": cp.CreatedAt,
"control_id": cp.ControlID,
"document_id": cp.DocumentID,
"organization_id": cp.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": cp.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -20,17 +20,18 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (
ControlMeasure struct {
ControlID gid.GID `db:"control_id"`
MeasureID gid.GID `db:"measure_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
ControlID gid.GID `db:"control_id"`
MeasureID gid.GID `db:"measure_id"`
OrganizationID gid.GID `db:"organization_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
}
ControlMeasures []*ControlMeasure
@@ -46,12 +47,14 @@ INSERT INTO
controls_measures (
control_id,
measure_id,
organization_id,
tenant_id,
created_at
)
VALUES (
@control_id,
@measure_id,
@organization_id,
@tenant_id,
@created_at
)
@@ -59,10 +62,11 @@ ON CONFLICT (control_id, measure_id) DO NOTHING;
`
args := pgx.StrictNamedArgs{
"control_id": cm.ControlID,
"measure_id": cm.MeasureID,
"tenant_id": scope.GetTenantID(),
"created_at": cm.CreatedAt,
"control_id": cm.ControlID,
"measure_id": cm.MeasureID,
"organization_id": cm.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": cm.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err

View File

@@ -27,9 +27,10 @@ import (
type (
ControlSnapshot struct {
ControlID gid.GID `db:"control_id"`
SnapshotID gid.GID `db:"snapshot_id"`
CreatedAt time.Time `db:"created_at"`
ControlID gid.GID `db:"control_id"`
SnapshotID gid.GID `db:"snapshot_id"`
OrganizationID gid.GID `db:"organization_id"`
CreatedAt time.Time `db:"created_at"`
}
ControlSnapshots []*ControlSnapshot
@@ -45,12 +46,14 @@ INSERT INTO
controls_snapshots (
control_id,
snapshot_id,
organization_id,
tenant_id,
created_at
)
VALUES (
@control_id,
@snapshot_id,
@organization_id,
@tenant_id,
@created_at
)
@@ -58,10 +61,11 @@ ON CONFLICT (control_id, snapshot_id) DO NOTHING;
`
args := pgx.StrictNamedArgs{
"control_id": cs.ControlID,
"snapshot_id": cs.SnapshotID,
"tenant_id": scope.GetTenantID(),
"created_at": cs.CreatedAt,
"control_id": cs.ControlID,
"snapshot_id": cs.SnapshotID,
"organization_id": cs.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": cs.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err

View File

@@ -22,17 +22,18 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/crypto/cipher"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/crypto/cipher"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
CustomDomain struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Domain string `db:"domain"`
HTTPChallengeToken *string `db:"http_challenge_token"`
HTTPChallengeKeyAuth *string `db:"http_challenge_key_auth"`
@@ -159,6 +160,7 @@ func (cd *CustomDomain) LoadByID(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -211,6 +213,7 @@ func (cd *CustomDomain) LoadByIDForUpdate(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -263,6 +266,7 @@ func (cd *CustomDomain) LoadByDomain(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -320,6 +324,7 @@ func (cd *CustomDomain) Insert(
INSERT INTO custom_domains (
id,
tenant_id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -337,6 +342,7 @@ INSERT INTO custom_domains (
) VALUES (
@id,
@tenant_id,
@organization_id,
@domain,
@http_challenge_token,
@http_challenge_key_auth,
@@ -357,6 +363,7 @@ INSERT INTO custom_domains (
args := pgx.NamedArgs{
"id": cd.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": cd.OrganizationID,
"domain": cd.Domain,
"http_challenge_token": cd.HTTPChallengeToken,
"http_challenge_key_auth": cd.HTTPChallengeKeyAuth,
@@ -487,6 +494,7 @@ func (cd *CustomDomain) LoadByHTTPChallengeToken(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -537,6 +545,7 @@ func (domains *CustomDomains) ListDomainsForRenewal(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -589,6 +598,7 @@ func (domains *CustomDomains) ListDomainsWithPendingHTTPChallenges(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -644,6 +654,7 @@ func (domains *CustomDomains) LoadActiveCertificates(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,
@@ -693,6 +704,7 @@ func (domains *CustomDomains) ListStaleProvisioningDomains(
q := `
SELECT
id,
organization_id,
domain,
http_challenge_token,
http_challenge_key_auth,

View File

@@ -41,6 +41,7 @@ func (dv DatumVendors) Merge(
conn pg.Conn,
scope Scoper,
datumID gid.GID,
organizationID gid.GID,
vendorIDs []gid.GID,
) error {
q := `
@@ -49,6 +50,7 @@ WITH vendor_ids AS (
unnest(@vendor_ids::text[]) AS vendor_id,
@tenant_id AS tenant_id,
@datum_id AS datum_id,
@organization_id AS organization_id,
@created_at::timestamptz AS created_at
)
MERGE INTO data_vendors AS tgt
@@ -57,18 +59,19 @@ ON tgt.tenant_id = src.tenant_id
AND tgt.datum_id = src.datum_id
AND tgt.vendor_id = src.vendor_id
WHEN NOT MATCHED THEN
INSERT (tenant_id, datum_id, vendor_id, created_at)
VALUES (src.tenant_id, src.datum_id, src.vendor_id, src.created_at)
INSERT (tenant_id, datum_id, vendor_id, organization_id, created_at)
VALUES (src.tenant_id, src.datum_id, src.vendor_id, src.organization_id, src.created_at)
WHEN NOT MATCHED BY SOURCE
AND tgt.tenant_id = @tenant_id AND tgt.datum_id = @datum_id
THEN DELETE
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"datum_id": datumID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
"tenant_id": scope.GetTenantID(),
"datum_id": datumID,
"organization_id": organizationID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
}
_, err := conn.Exec(ctx, q, args)
@@ -84,26 +87,29 @@ func (dv DatumVendors) Insert(
conn pg.Conn,
scope Scoper,
datumID gid.GID,
organizationID gid.GID,
vendorIDs []gid.GID,
) error {
q := `
WITH vendor_ids AS (
SELECT unnest(@vendor_ids::text[]) AS vendor_id
)
INSERT INTO data_vendors (tenant_id, datum_id, vendor_id, created_at)
INSERT INTO data_vendors (tenant_id, datum_id, vendor_id, organization_id, created_at)
SELECT
@tenant_id::text AS tenant_id,
@datum_id::text AS datum_id,
vendor_id,
@organization_id::text AS organization_id,
@created_at::timestamptz AS created_at
FROM vendor_ids
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"datum_id": datumID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
"tenant_id": scope.GetTenantID(),
"datum_id": datumID,
"organization_id": organizationID,
"created_at": time.Now(),
"vendor_ids": vendorIDs,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -21,16 +21,17 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
DocumentVersion struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
DocumentID gid.GID `db:"document_id"`
Title string `db:"title"`
OwnerID gid.GID `db:"owner_id"`
@@ -81,6 +82,7 @@ func (p *DocumentVersions) LoadByDocumentID(
q := `
SELECT
id,
organization_id,
document_id,
title,
owner_id,
@@ -140,6 +142,7 @@ func (p *DocumentVersion) LoadByID(
q := `
SELECT
id,
organization_id,
document_id,
title,
owner_id,
@@ -190,6 +193,7 @@ func (p DocumentVersion) Insert(
INSERT INTO document_versions (
tenant_id,
id,
organization_id,
document_id,
title,
owner_id,
@@ -204,6 +208,7 @@ INSERT INTO document_versions (
VALUES (
@tenant_id,
@id,
@organization_id,
@document_id,
@title,
@owner_id,
@@ -217,18 +222,19 @@ VALUES (
)
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"id": p.ID,
"document_id": p.DocumentID,
"title": p.Title,
"owner_id": p.OwnerID,
"version_number": p.VersionNumber,
"classification": p.Classification,
"content": p.Content,
"changelog": p.Changelog,
"status": p.Status,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
"tenant_id": scope.GetTenantID(),
"id": p.ID,
"organization_id": p.OrganizationID,
"document_id": p.DocumentID,
"title": p.Title,
"owner_id": p.OwnerID,
"version_number": p.VersionNumber,
"classification": p.Classification,
"content": p.Content,
"changelog": p.Changelog,
"status": p.Status,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
@@ -264,6 +270,7 @@ func (p *DocumentVersion) LoadByDocumentIDAndVersionNumber(
q := `
SELECT
id,
organization_id,
document_id,
title,
owner_id,
@@ -316,6 +323,7 @@ func (p *DocumentVersion) LoadLatestVersion(
q := `
SELECT
id,
organization_id,
document_id,
title,
owner_id,
@@ -366,6 +374,7 @@ func (p *DocumentVersion) LoadLatestPublishedVersion(
q := `
SELECT
id,
organization_id,
document_id,
title,
owner_id,

View File

@@ -21,16 +21,17 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
DocumentVersionSignature struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"-"`
DocumentVersionID gid.GID `json:"document_version_id"`
State DocumentVersionSignatureState `json:"state"`
SignedBy gid.GID `json:"signed_by"`
@@ -87,6 +88,7 @@ func (pvs *DocumentVersionSignature) LoadByDocumentVersionIDAndSignatory(
q := `
SELECT
id,
organization_id,
document_version_id,
state,
signed_by,
@@ -132,6 +134,7 @@ func (pvs *DocumentVersionSignature) LoadByID(
q := `
SELECT
id,
organization_id,
document_version_id,
state,
signed_by,
@@ -175,6 +178,7 @@ func (pvs DocumentVersionSignature) Insert(
INSERT INTO document_version_signatures (
id,
tenant_id,
organization_id,
document_version_id,
state,
signed_by,
@@ -185,6 +189,7 @@ INSERT INTO document_version_signatures (
) VALUES (
@id,
@tenant_id,
@organization_id,
@document_version_id,
@state,
@signed_by,
@@ -198,6 +203,7 @@ INSERT INTO document_version_signatures (
args := pgx.StrictNamedArgs{
"id": pvs.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": pvs.OrganizationID,
"document_version_id": pvs.DocumentVersionID,
"state": pvs.State,
"signed_by": pvs.SignedBy,
@@ -234,6 +240,7 @@ func (pvss *DocumentVersionSignatures) LoadByDocumentVersionID(
q := `
SELECT
id,
organization_id,
document_version_id,
state,
signed_by,
@@ -346,6 +353,7 @@ func (pvss *DocumentVersionSignaturesWithPeople) LoadByDocumentVersionIDWithPeop
WITH sigs AS (
SELECT
dvs.id,
dvs.organization_id,
dvs.tenant_id,
dvs.document_version_id,
dvs.state,
@@ -367,6 +375,7 @@ WITH sigs AS (
)
SELECT
id,
organization_id,
document_version_id,
state,
signed_by,

View File

@@ -20,9 +20,9 @@ import (
"fmt"
"time"
"go.probo.inc/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (

View File

@@ -68,3 +68,211 @@ const (
UserAPIKeyMembershipEntityType uint16 = 44
MeetingEntityType uint16 = 45
)
type EntityInfo struct {
Model string
Table string
}
var entityRegistry = map[uint16]EntityInfo{
OrganizationEntityType: {
Model: "Organization",
Table: "organizations",
},
FrameworkEntityType: {
Model: "Framework",
Table: "frameworks",
},
MeasureEntityType: {
Model: "Measure",
Table: "measures",
},
TaskEntityType: {
Model: "Task",
Table: "tasks",
},
EvidenceEntityType: {
Model: "Evidence",
Table: "evidences",
},
ConnectorEntityType: {
Model: "Connector",
Table: "connectors",
},
VendorRiskAssessmentEntityType: {
Model: "VendorRiskAssessment",
Table: "vendor_risk_assessments",
},
VendorEntityType: {
Model: "Vendor",
Table: "vendors",
},
PeopleEntityType: {
Model: "People",
Table: "peoples",
},
VendorComplianceReportEntityType: {
Model: "VendorComplianceReport",
Table: "vendor_compliance_reports",
},
DocumentEntityType: {
Model: "Document",
Table: "documents",
},
UserEntityType: {
Model: "User",
Table: "auth_users",
},
SessionEntityType: {
Model: "Session",
Table: "auth_sessions",
},
EmailEntityType: {
Model: "Email",
Table: "auth_emails",
},
ControlEntityType: {
Model: "Control",
Table: "controls",
},
RiskEntityType: {
Model: "Risk",
Table: "risks",
},
DocumentVersionEntityType: {
Model: "DocumentVersion",
Table: "document_versions",
},
DocumentVersionSignatureEntityType: {
Model: "DocumentVersionSignature",
Table: "document_version_signatures",
},
AssetEntityType: {
Model: "Asset",
Table: "assets",
},
DatumEntityType: {
Model: "Datum",
Table: "data",
},
AuditEntityType: {
Model: "Audit",
Table: "audits",
},
ReportEntityType: {
Model: "Report",
Table: "reports",
},
TrustCenterEntityType: {
Model: "TrustCenter",
Table: "trust_centers",
},
TrustCenterAccessEntityType: {
Model: "TrustCenterAccess",
Table: "trust_center_accesses",
},
VendorBusinessAssociateAgreementEntityType: {
Model: "VendorBusinessAssociateAgreement",
Table: "vendor_business_associate_agreements",
},
FileEntityType: {
Model: "File",
Table: "files",
},
VendorContactEntityType: {
Model: "VendorContact",
Table: "vendor_contacts",
},
VendorDataPrivacyAgreementEntityType: {
Model: "VendorDataPrivacyAgreement",
Table: "vendor_data_privacy_agreements",
},
NonconformityEntityType: {
Model: "Nonconformity",
Table: "nonconformities",
},
ObligationEntityType: {
Model: "Obligation",
Table: "obligations",
},
VendorServiceEntityType: {
Model: "VendorService",
Table: "vendor_services",
},
SnapshotEntityType: {
Model: "Snapshot",
Table: "snapshots",
},
ContinualImprovementEntityType: {
Model: "ContinualImprovement",
Table: "continual_improvements",
},
ProcessingActivityEntityType: {
Model: "ProcessingActivity",
Table: "processing_activities",
},
ExportJobEntityType: {
Model: "ExportJob",
Table: "export_jobs",
},
TrustCenterReferenceEntityType: {
Model: "TrustCenterReference",
Table: "trust_center_references",
},
TrustCenterDocumentAccessEntityType: {
Model: "",
Table: "trust_center_document_accesses",
},
CustomDomainEntityType: {
Model: "CustomDomain",
Table: "custom_domains",
},
InvitationEntityType: {
Model: "Invitation",
Table: "authz_invitations",
},
MembershipEntityType: {
Model: "Membership",
Table: "authz_memberships",
},
SlackMessageEntityType: {
Model: "SlackMessage",
Table: "slack_messages",
},
TrustCenterFileEntityType: {
Model: "TrustCenterFile",
Table: "trust_center_files",
},
SAMLConfigurationEntityType: {
Model: "SAMLConfiguration",
Table: "auth_saml_configurations",
},
UserAPIKeyEntityType: {
Model: "UserAPIKey",
Table: "auth_user_api_keys",
},
UserAPIKeyMembershipEntityType: {
Model: "UserAPIKeyMembership",
Table: "authz_api_keys_memberships",
},
MeetingEntityType: {
Model: "Meeting",
Table: "meetings",
},
}
func EntityTable(entityType uint16) (string, bool) {
info, ok := entityRegistry[entityType]
if !ok {
return "", false
}
return info.Table, true
}
func EntityModel(entityType uint16) (string, bool) {
info, ok := entityRegistry[entityType]
if !ok {
return "", false
}
return info.Model, true
}

View File

@@ -31,6 +31,7 @@ import (
type (
Evidence struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
MeasureID gid.GID `db:"measure_id"`
TaskID *gid.GID `db:"task_id"`
State EvidenceState `db:"state"`
@@ -141,6 +142,7 @@ INSERT INTO
evidences (
tenant_id,
id,
organization_id,
measure_id,
task_id,
reference_id,
@@ -155,6 +157,7 @@ INSERT INTO
VALUES (
@tenant_id,
@evidence_id,
@organization_id,
@measure_id,
@task_id,
@reference_id,
@@ -171,6 +174,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"evidence_id": e.ID,
"organization_id": e.OrganizationID,
"measure_id": e.MeasureID,
"task_id": e.TaskID,
"reference_id": e.ReferenceID,
@@ -208,6 +212,7 @@ func (e *Evidence) LoadByID(
q := `
SELECT
id,
organization_id,
task_id,
measure_id,
reference_id,
@@ -288,6 +293,7 @@ func (e *Evidences) LoadByMeasureID(
q := `
SELECT
id,
organization_id,
measure_id,
task_id,
reference_id,
@@ -369,6 +375,7 @@ func (e *Evidences) LoadByTaskID(
q := `
SELECT
id,
organization_id,
measure_id,
task_id,
reference_id,

View File

@@ -8,14 +8,15 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (
ExportJob struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Type ExportJobType `db:"type"`
Arguments json.RawMessage `db:"arguments"`
Error *string `db:"error"`
@@ -54,6 +55,7 @@ func (ej *ExportJob) Insert(
q := `
INSERT INTO export_jobs (
id,
organization_id,
tenant_id,
type,
arguments,
@@ -63,6 +65,7 @@ INSERT INTO export_jobs (
created_at
) VALUES (
@id,
@organization_id,
@tenant_id,
@type,
@arguments,
@@ -73,6 +76,7 @@ INSERT INTO export_jobs (
)`
args := pgx.StrictNamedArgs{
"id": ej.ID,
"organization_id": ej.OrganizationID,
"tenant_id": scope.GetTenantID(),
"type": ej.Type,
"arguments": ej.Arguments,
@@ -126,6 +130,7 @@ func (ej *ExportJob) LoadByID(
q := `
SELECT
id,
organization_id,
type,
arguments,
error,
@@ -167,6 +172,7 @@ func (ej *ExportJob) LoadNextPendingForUpdateSkipLocked(
q := `
SELECT
id,
organization_id,
type,
arguments,
error,

View File

@@ -21,23 +21,24 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (
File struct {
ID gid.GID `db:"id"`
BucketName string `db:"bucket_name"`
MimeType string `db:"mime_type"`
FileName string `db:"file_name"`
FileKey string `db:"file_key"`
FileSize int64 `db:"file_size"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt *time.Time `db:"deleted_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
BucketName string `db:"bucket_name"`
MimeType string `db:"mime_type"`
FileName string `db:"file_name"`
FileKey string `db:"file_key"`
FileSize int64 `db:"file_size"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt *time.Time `db:"deleted_at"`
}
Files []*File
@@ -68,6 +69,7 @@ func (f *File) LoadByID(
q := `
SELECT
id,
organization_id,
bucket_name,
mime_type,
file_name,
@@ -119,6 +121,7 @@ INSERT INTO
files (
id,
tenant_id,
organization_id,
bucket_name,
mime_type,
file_name,
@@ -131,6 +134,7 @@ INSERT INTO
VALUES (
@file_id,
@tenant_id,
@organization_id,
@bucket_name,
@mime_type,
@file_name,
@@ -143,16 +147,17 @@ VALUES (
`
args := pgx.StrictNamedArgs{
"file_id": f.ID,
"tenant_id": scope.GetTenantID(),
"bucket_name": f.BucketName,
"mime_type": f.MimeType,
"file_name": f.FileName,
"file_key": f.FileKey,
"file_size": f.FileSize,
"created_at": f.CreatedAt,
"updated_at": f.UpdatedAt,
"deleted_at": f.DeletedAt,
"file_id": f.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": f.OrganizationID,
"bucket_name": f.BucketName,
"mime_type": f.MimeType,
"file_name": f.FileName,
"file_key": f.FileKey,
"file_size": f.FileSize,
"created_at": f.CreatedAt,
"updated_at": f.UpdatedAt,
"deleted_at": f.DeletedAt,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -21,10 +21,10 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
@@ -33,7 +33,7 @@ type (
OrganizationID gid.GID `db:"organization_id"`
Email string `db:"email"`
FullName string `db:"full_name"`
Role Role `db:"role"`
Role MembershipRole `db:"role"`
Status InvitationStatus `db:"status"`
ExpiresAt time.Time `db:"expires_at"`
AcceptedAt *time.Time `db:"accepted_at"`
@@ -43,11 +43,11 @@ type (
Invitations []*Invitation
InvitationData struct {
InvitationID gid.GID `json:"invitation_id"`
OrganizationID gid.GID `json:"organization_id"`
Email string `json:"email"`
FullName string `json:"full_name"`
Role Role `json:"role"`
InvitationID gid.GID `json:"invitation_id"`
OrganizationID gid.GID `json:"organization_id"`
Email string `json:"email"`
FullName string `json:"full_name"`
Role MembershipRole `json:"role"`
}
ErrInvitationNotFound struct {

View File

@@ -19,20 +19,19 @@ import (
"fmt"
)
type Role string
type MembershipRole string
const (
RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN"
RoleMember Role = "MEMBER"
RoleViewer Role = "VIEWER"
MembershipRoleOwner MembershipRole = "OWNER"
MembershipRoleAdmin MembershipRole = "ADMIN"
MembershipRoleViewer MembershipRole = "VIEWER"
)
func (r Role) String() string {
func (r MembershipRole) String() string {
return string(r)
}
func (r *Role) Scan(value any) error {
func (r *MembershipRole) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
@@ -40,24 +39,22 @@ func (r *Role) Scan(value any) error {
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for Role: %T", value)
return fmt.Errorf("unsupported type for MembershipRole: %T", value)
}
switch s {
case "OWNER":
*r = RoleOwner
*r = MembershipRoleOwner
case "ADMIN":
*r = RoleAdmin
case "MEMBER":
*r = RoleMember
*r = MembershipRoleAdmin
case "VIEWER":
*r = RoleViewer
*r = MembershipRoleViewer
default:
return fmt.Errorf("invalid Role value: %q", s)
return fmt.Errorf("invalid MembershipRole value: %q", s)
}
return nil
}
func (r Role) Value() (driver.Value, error) {
func (r MembershipRole) Value() (driver.Value, error) {
return r.String(), nil
}

View File

@@ -19,25 +19,26 @@ import (
"errors"
"fmt"
"maps"
"strings"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
Membership struct {
ID gid.GID `db:"id"`
UserID gid.GID `db:"user_id"`
OrganizationID gid.GID `db:"organization_id"`
Role Role `db:"role"`
FullName string `db:"full_name"`
EmailAddress string `db:"email_address"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
UserID gid.GID `db:"user_id"`
OrganizationID gid.GID `db:"organization_id"`
Role MembershipRole `db:"role"`
FullName string `db:"full_name"`
EmailAddress string `db:"email_address"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Memberships []*Membership
@@ -185,6 +186,83 @@ JOIN
return nil
}
// LoadRoleByUserAndEntityID loads a user's role by querying any entity to extract its organization_id
func (m *Membership) LoadRoleByUserAndEntityID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
userID gid.GID,
entityID gid.GID,
) error {
entityType := entityID.EntityType()
// For organization, the entity ID is the organization ID
if entityType == OrganizationEntityType {
return m.LoadByUserAndOrg(ctx, conn, scope, userID, entityID)
}
tableName, ok := EntityTable(entityType)
if !ok {
return fmt.Errorf("unsupported entity type for role lookup: %d", entityType)
}
// Build scope fragment with table alias to avoid ambiguity
scopeFragment := scope.SQLFragment()
// Replace column references with table-qualified versions
scopeFragment = strings.ReplaceAll(scopeFragment, "tenant_id =", "m.tenant_id =")
query := fmt.Sprintf(`
SELECT
m.id,
m.user_id,
m.organization_id,
m.role,
m.created_at,
m.updated_at
FROM
authz_memberships m
INNER JOIN %s e ON e.id = @entity_id
WHERE
%s
AND m.user_id = @user_id
AND m.organization_id = e.organization_id
LIMIT 1;
`, tableName, scopeFragment)
args := pgx.NamedArgs{
"user_id": userID,
"entity_id": entityID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot query membership by entity: %w", err)
}
defer rows.Close()
if !rows.Next() {
return &ErrMembershipNotFound{UserID: userID, OrgID: entityID}
}
var membership Membership
err = rows.Scan(
&membership.ID,
&membership.UserID,
&membership.OrganizationID,
&membership.Role,
&membership.CreatedAt,
&membership.UpdatedAt,
)
if err != nil {
return fmt.Errorf("cannot scan membership: %w", err)
}
*m = membership
return nil
}
func (m *Membership) LoadByUserAndOrg(
ctx context.Context,
conn pg.Conn,
@@ -195,17 +273,17 @@ func (m *Membership) LoadByUserAndOrg(
query := `
WITH mbr AS (
SELECT
id,
user_id,
organization_id,
role,
created_at,
updated_at
am.id,
am.user_id,
am.organization_id,
am.role,
am.created_at,
am.updated_at
FROM
authz_memberships
authz_memberships am
WHERE
user_id = @user_id
AND organization_id = @organization_id
am.user_id = @user_id
AND am.organization_id = @organization_id
AND %s
)
SELECT
@@ -223,7 +301,12 @@ JOIN
users u ON mbr.user_id = u.id
`
query = fmt.Sprintf(query, scope.SQLFragment())
// Build scope fragment with table alias
scopeFragment := scope.SQLFragment()
// Replace column references with table-qualified versions
scopeFragment = strings.ReplaceAll(scopeFragment, "tenant_id =", "am.tenant_id =")
query = fmt.Sprintf(query, scopeFragment)
args := pgx.StrictNamedArgs{
"user_id": userID,
@@ -468,67 +551,3 @@ WHERE
}
return count, nil
}
func LoadUserIDsByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
) ([]gid.GID, error) {
query := `
SELECT user_id
FROM authz_memberships
WHERE organization_id = @organization_id AND %s
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, query, args)
if err != nil {
return nil, fmt.Errorf("cannot query memberships: %w", err)
}
var userIDs []gid.GID
for rows.Next() {
var userID gid.GID
if err := rows.Scan(&userID); err != nil {
rows.Close()
return nil, fmt.Errorf("cannot scan user_id: %w", err)
}
userIDs = append(userIDs, userID)
}
rows.Close()
return userIDs, nil
}
func UpdateMembershipUserID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
oldUserID gid.GID,
newUserID gid.GID,
organizationID gid.GID,
) error {
query := `
UPDATE authz_memberships
SET user_id = @new_user_id, updated_at = @updated_at
WHERE user_id = @old_user_id AND organization_id = @organization_id AND %s
`
query = fmt.Sprintf(query, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"new_user_id": newUserID,
"old_user_id": oldUserID,
"organization_id": organizationID,
"updated_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot update membership: %w", err)
}
return nil
}

View File

@@ -0,0 +1,210 @@
-- Set all existing memberships to OWNER role
UPDATE authz_memberships SET role = 'OWNER';
ALTER TABLE controls ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE controls
SET organization_id = organizations.id
FROM organizations
WHERE controls.tenant_id = organizations.tenant_id
AND controls.organization_id IS NULL;
ALTER TABLE controls ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE evidences ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE evidences
SET organization_id = organizations.id
FROM organizations
WHERE evidences.tenant_id = organizations.tenant_id
AND evidences.organization_id IS NULL;
ALTER TABLE evidences ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE files ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE files
SET organization_id = organizations.id
FROM organizations
WHERE files.tenant_id = organizations.tenant_id
AND files.organization_id IS NULL;
ALTER TABLE files ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE document_versions ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE document_versions
SET organization_id = organizations.id
FROM organizations
WHERE document_versions.tenant_id = organizations.tenant_id
AND document_versions.organization_id IS NULL;
ALTER TABLE document_versions ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE document_version_signatures ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE document_version_signatures
SET organization_id = organizations.id
FROM organizations
WHERE document_version_signatures.tenant_id = organizations.tenant_id
AND document_version_signatures.organization_id IS NULL;
ALTER TABLE document_version_signatures ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE trust_center_references ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE trust_center_references
SET organization_id = organizations.id
FROM organizations
WHERE trust_center_references.tenant_id = organizations.tenant_id
AND trust_center_references.organization_id IS NULL;
ALTER TABLE trust_center_references ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE trust_center_accesses ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE trust_center_accesses
SET organization_id = organizations.id
FROM organizations
WHERE trust_center_accesses.tenant_id = organizations.tenant_id
AND trust_center_accesses.organization_id IS NULL;
ALTER TABLE trust_center_accesses ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE trust_center_document_accesses ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE trust_center_document_accesses
SET organization_id = organizations.id
FROM organizations
WHERE trust_center_document_accesses.tenant_id = organizations.tenant_id
AND trust_center_document_accesses.organization_id IS NULL;
ALTER TABLE trust_center_document_accesses ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE vendor_services ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE vendor_services
SET organization_id = organizations.id
FROM organizations
WHERE vendor_services.tenant_id = organizations.tenant_id
AND vendor_services.organization_id IS NULL;
ALTER TABLE vendor_services ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE vendor_contacts ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE vendor_contacts
SET organization_id = organizations.id
FROM organizations
WHERE vendor_contacts.tenant_id = organizations.tenant_id
AND vendor_contacts.organization_id IS NULL;
ALTER TABLE vendor_contacts ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE vendor_risk_assessments ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE vendor_risk_assessments
SET organization_id = organizations.id
FROM organizations
WHERE vendor_risk_assessments.tenant_id = organizations.tenant_id
AND vendor_risk_assessments.organization_id IS NULL;
ALTER TABLE vendor_risk_assessments ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE reports ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE reports
SET organization_id = organizations.id
FROM organizations
WHERE reports.tenant_id = organizations.tenant_id
AND reports.organization_id IS NULL;
ALTER TABLE reports ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE custom_domains ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE custom_domains
SET organization_id = organizations.id
FROM organizations
WHERE custom_domains.tenant_id = organizations.tenant_id
AND custom_domains.organization_id IS NULL;
ALTER TABLE custom_domains ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE asset_vendors ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE asset_vendors
SET organization_id = organizations.id
FROM organizations
WHERE asset_vendors.tenant_id = organizations.tenant_id
AND asset_vendors.organization_id IS NULL;
ALTER TABLE asset_vendors ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE authz_api_keys_memberships ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE authz_api_keys_memberships
SET organization_id = organizations.id
FROM organizations
WHERE authz_api_keys_memberships.tenant_id = organizations.tenant_id
AND authz_api_keys_memberships.organization_id IS NULL;
ALTER TABLE authz_api_keys_memberships ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE controls_audits ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE controls_audits
SET organization_id = organizations.id
FROM organizations
WHERE controls_audits.tenant_id = organizations.tenant_id
AND controls_audits.organization_id IS NULL;
ALTER TABLE controls_audits ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE controls_documents ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE controls_documents
SET organization_id = organizations.id
FROM organizations
WHERE controls_documents.tenant_id = organizations.tenant_id
AND controls_documents.organization_id IS NULL;
ALTER TABLE controls_documents ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE controls_measures ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE controls_measures
SET organization_id = organizations.id
FROM organizations
WHERE controls_measures.tenant_id = organizations.tenant_id
AND controls_measures.organization_id IS NULL;
ALTER TABLE controls_measures ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE controls_snapshots ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE controls_snapshots
SET organization_id = organizations.id
FROM organizations
WHERE controls_snapshots.tenant_id = organizations.tenant_id
AND controls_snapshots.organization_id IS NULL;
ALTER TABLE controls_snapshots ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE data_vendors ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE data_vendors
SET organization_id = organizations.id
FROM organizations
WHERE data_vendors.tenant_id = organizations.tenant_id
AND data_vendors.organization_id IS NULL;
ALTER TABLE data_vendors ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE export_jobs ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE export_jobs
SET organization_id = organizations.id
FROM organizations
WHERE export_jobs.tenant_id = organizations.tenant_id
AND export_jobs.organization_id IS NULL;
ALTER TABLE export_jobs ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE processing_activity_vendors ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE processing_activity_vendors
SET organization_id = organizations.id
FROM organizations
WHERE processing_activity_vendors.tenant_id = organizations.tenant_id
AND processing_activity_vendors.organization_id IS NULL;
ALTER TABLE processing_activity_vendors ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE risks_documents ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE risks_documents
SET organization_id = organizations.id
FROM organizations
WHERE risks_documents.tenant_id = organizations.tenant_id
AND risks_documents.organization_id IS NULL;
ALTER TABLE risks_documents ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE risks_measures ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE risks_measures
SET organization_id = organizations.id
FROM organizations
WHERE risks_measures.tenant_id = organizations.tenant_id
AND risks_measures.organization_id IS NULL;
ALTER TABLE risks_measures ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE risks_obligations ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE risks_obligations
SET organization_id = organizations.id
FROM organizations
WHERE risks_obligations.tenant_id = organizations.tenant_id
AND risks_obligations.organization_id IS NULL;
ALTER TABLE risks_obligations ALTER COLUMN organization_id SET NOT NULL;
ALTER TABLE vendor_compliance_reports ADD COLUMN IF NOT EXISTS organization_id TEXT;
UPDATE vendor_compliance_reports
SET organization_id = organizations.id
FROM organizations
WHERE vendor_compliance_reports.tenant_id = organizations.tenant_id
AND vendor_compliance_reports.organization_id IS NULL;
ALTER TABLE vendor_compliance_reports ALTER COLUMN organization_id SET NOT NULL;

View File

@@ -237,6 +237,63 @@ ORDER BY
return nil
}
func (o *Organizations) LoadAllByUserIDWithRole(
ctx context.Context,
conn pg.Conn,
userID gid.GID,
role MembershipRole,
) error {
q := `
WITH user_org AS (
SELECT
organization_id
FROM
authz_memberships
WHERE
user_id = @user_id
AND role = @role
)
SELECT
tenant_id,
id,
name,
description,
website_url,
email,
headquarter_address,
custom_domain_id,
logo_file_id,
horizontal_logo_file_id,
created_at,
updated_at
FROM
organizations
INNER JOIN
user_org ON organizations.id = user_org.organization_id
ORDER BY
name ASC
`
args := pgx.StrictNamedArgs{
"user_id": userID,
"role": role,
}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query organizations: %w", err)
}
organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization])
if err != nil {
return fmt.Errorf("cannot collect organizations: %w", err)
}
*o = organizations
return nil
}
func (o *Organizations) LoadAllByUserAPIKeyID(
ctx context.Context,
conn pg.Conn,

View File

@@ -20,21 +20,22 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
Report struct {
ID gid.GID `db:"id"`
ObjectKey string `db:"object_key"`
MimeType string `db:"mime_type"`
Filename string `db:"filename"`
Size int64 `db:"size"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
ObjectKey string `db:"object_key"`
MimeType string `db:"mime_type"`
Filename string `db:"filename"`
Size int64 `db:"size"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Reports []*Report
@@ -49,6 +50,7 @@ func (r *Report) LoadByID(
q := `
SELECT
id,
organization_id,
object_key,
mime_type,
filename,
@@ -92,6 +94,7 @@ func (r *Report) Insert(
INSERT INTO reports (
id,
tenant_id,
organization_id,
object_key,
mime_type,
filename,
@@ -101,6 +104,7 @@ INSERT INTO reports (
) VALUES (
@id,
@tenant_id,
@organization_id,
@object_key,
@mime_type,
@filename,
@@ -111,14 +115,15 @@ INSERT INTO reports (
`
args := pgx.StrictNamedArgs{
"id": r.ID,
"tenant_id": scope.GetTenantID(),
"object_key": r.ObjectKey,
"mime_type": r.MimeType,
"filename": r.Filename,
"size": r.Size,
"created_at": r.CreatedAt,
"updated_at": r.UpdatedAt,
"id": r.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": r.OrganizationID,
"object_key": r.ObjectKey,
"mime_type": r.MimeType,
"filename": r.Filename,
"size": r.Size,
"created_at": r.CreatedAt,
"updated_at": r.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -27,10 +27,11 @@ import (
type (
RiskDocument struct {
RiskID gid.GID `db:"risk_id"`
DocumentID gid.GID `db:"document_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
RiskID gid.GID `db:"risk_id"`
DocumentID gid.GID `db:"document_id"`
OrganizationID gid.GID `db:"organization_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
}
RiskDocuments []*RiskDocument
@@ -46,22 +47,25 @@ INSERT INTO
risks_documents (
risk_id,
document_id,
organization_id,
tenant_id,
created_at
)
VALUES (
@risk_id,
@document_id,
@organization_id,
@tenant_id,
@created_at
);
`
args := pgx.StrictNamedArgs{
"risk_id": rp.RiskID,
"document_id": rp.DocumentID,
"tenant_id": scope.GetTenantID(),
"created_at": rp.CreatedAt,
"risk_id": rp.RiskID,
"document_id": rp.DocumentID,
"organization_id": rp.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": rp.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err

View File

@@ -20,17 +20,18 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (
RiskMeasure struct {
RiskID gid.GID `db:"risk_id"`
MeasureID gid.GID `db:"measure_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
RiskID gid.GID `db:"risk_id"`
MeasureID gid.GID `db:"measure_id"`
OrganizationID gid.GID `db:"organization_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
}
RiskMeasures []*RiskMeasure
@@ -46,22 +47,25 @@ INSERT INTO
risks_measures (
risk_id,
measure_id,
organization_id,
tenant_id,
created_at
)
VALUES (
@risk_id,
@measure_id,
@organization_id,
@tenant_id,
@created_at
);
`
args := pgx.StrictNamedArgs{
"risk_id": rm.RiskID,
"measure_id": rm.MeasureID,
"tenant_id": scope.GetTenantID(),
"created_at": rm.CreatedAt,
"risk_id": rm.RiskID,
"measure_id": rm.MeasureID,
"organization_id": rm.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": rm.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err

View File

@@ -27,9 +27,10 @@ import (
type (
RiskObligation struct {
RiskID gid.GID `db:"risk_id"`
ObligationID gid.GID `db:"obligation_id"`
CreatedAt time.Time `db:"created_at"`
RiskID gid.GID `db:"risk_id"`
ObligationID gid.GID `db:"obligation_id"`
OrganizationID gid.GID `db:"organization_id"`
CreatedAt time.Time `db:"created_at"`
}
RiskObligations []*RiskObligation
@@ -44,21 +45,24 @@ func (ro RiskObligation) Insert(
INSERT INTO risks_obligations (
risk_id,
obligation_id,
organization_id,
tenant_id,
created_at
) VALUES (
@risk_id,
@obligation_id,
@organization_id,
@tenant_id,
@created_at
)
`
args := pgx.StrictNamedArgs{
"risk_id": ro.RiskID,
"obligation_id": ro.ObligationID,
"tenant_id": scope.GetTenantID(),
"created_at": ro.CreatedAt,
"risk_id": ro.RiskID,
"obligation_id": ro.ObligationID,
"organization_id": ro.OrganizationID,
"tenant_id": scope.GetTenantID(),
"created_at": ro.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -21,9 +21,9 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (

View File

@@ -22,16 +22,17 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
TrustCenterAccess struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
TenantID gid.TenantID `db:"tenant_id"`
TrustCenterID gid.GID `db:"trust_center_id"`
Email string `db:"email"`
@@ -82,6 +83,7 @@ func (tca *TrustCenterAccess) LoadByID(
q := `
SELECT
id,
organization_id,
tenant_id,
trust_center_id,
email,
@@ -135,6 +137,7 @@ func (tca *TrustCenterAccess) LoadByTrustCenterIDAndEmail(
q := `
SELECT
id,
organization_id,
tenant_id,
trust_center_id,
email,
@@ -191,6 +194,7 @@ func (tca *TrustCenterAccess) Insert(
INSERT INTO trust_center_accesses (
id,
tenant_id,
organization_id,
trust_center_id,
email,
name,
@@ -201,6 +205,7 @@ INSERT INTO trust_center_accesses (
) VALUES (
@id,
@tenant_id,
@organization_id,
@trust_center_id,
@email,
@name,
@@ -214,6 +219,7 @@ INSERT INTO trust_center_accesses (
args := pgx.StrictNamedArgs{
"id": tca.ID,
"tenant_id": tca.TenantID,
"organization_id": tca.OrganizationID,
"trust_center_id": tca.TrustCenterID,
"email": tca.Email,
"name": tca.Name,
@@ -317,6 +323,7 @@ func (tcas *TrustCenterAccesses) LoadByTrustCenterID(
q := `
SELECT
id,
organization_id,
tenant_id,
trust_center_id,
email,

View File

@@ -21,16 +21,17 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
TrustCenterDocumentAccess struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
TrustCenterAccessID gid.GID `db:"trust_center_access_id"`
DocumentID *gid.GID `db:"document_id"`
ReportID *gid.GID `db:"report_id"`
@@ -78,6 +79,7 @@ func (tcda *TrustCenterDocumentAccess) LoadByID(
q := `
SELECT
id,
organization_id,
trust_center_access_id,
document_id,
report_id,
@@ -127,6 +129,7 @@ func (tcda *TrustCenterDocumentAccess) LoadByTrustCenterAccessIDAndDocumentID(
q := `
SELECT
id,
organization_id,
trust_center_access_id,
document_id,
report_id,
@@ -177,6 +180,7 @@ func (tcda *TrustCenterDocumentAccess) LoadByTrustCenterAccessIDAndReportID(
q := `
SELECT
id,
organization_id,
trust_center_access_id,
document_id,
report_id,
@@ -226,6 +230,7 @@ func (tcda *TrustCenterDocumentAccess) Insert(
INSERT INTO trust_center_document_accesses (
id,
tenant_id,
organization_id,
trust_center_access_id,
document_id,
report_id,
@@ -237,6 +242,7 @@ INSERT INTO trust_center_document_accesses (
) VALUES (
@id,
@tenant_id,
@organization_id,
@trust_center_access_id,
@document_id,
@report_id,
@@ -251,6 +257,7 @@ INSERT INTO trust_center_document_accesses (
args := pgx.StrictNamedArgs{
"id": tcda.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": tcda.OrganizationID,
"trust_center_access_id": tcda.TrustCenterAccessID,
"document_id": tcda.DocumentID,
"report_id": tcda.ReportID,
@@ -512,6 +519,7 @@ final_items AS (
SELECT
COALESCE(tcda.id, ai.item_id) AS id,
tcda.tenant_id,
(SELECT organization_id FROM organization) AS organization_id,
@trust_center_access_id AS trust_center_access_id,
ai.document_id,
ai.report_id,
@@ -532,6 +540,7 @@ final_items AS (
)
SELECT
id,
organization_id,
trust_center_access_id,
document_id,
report_id,
@@ -576,6 +585,7 @@ func (tcdas *TrustCenterDocumentAccesses) LoadAllByTrustCenterAccessID(
q := `
SELECT
id,
organization_id,
trust_center_access_id,
document_id,
report_id,
@@ -717,6 +727,7 @@ func (tcdas TrustCenterDocumentAccesses) BulkInsertDocumentAccesses(
conn pg.Conn,
scope Scoper,
trustCenterAccessID gid.GID,
organizationID gid.GID,
documentIDs []gid.GID,
requested bool,
createdAt time.Time,
@@ -730,6 +741,7 @@ WITH document_access_data AS (
SELECT
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
@tenant_id AS tenant_id,
@organization_id AS organization_id,
@trust_center_access_id AS trust_center_access_id,
unnest(@document_ids::text[]) AS document_id,
null::text AS report_id,
@@ -740,7 +752,7 @@ WITH document_access_data AS (
@updated_at::timestamptz AS updated_at
)
INSERT INTO trust_center_document_accesses (
id, tenant_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
id, tenant_id, organization_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
)
SELECT * FROM document_access_data
ON CONFLICT DO NOTHING
@@ -748,6 +760,7 @@ ON CONFLICT DO NOTHING
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"organization_id": organizationID,
"trust_center_access_id": trustCenterAccessID,
"document_ids": documentIDs,
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
@@ -768,6 +781,7 @@ func (tcdas TrustCenterDocumentAccesses) BulkInsertReportAccesses(
conn pg.Conn,
scope Scoper,
trustCenterAccessID gid.GID,
organizationID gid.GID,
reportIDs []gid.GID,
requested bool,
createdAt time.Time,
@@ -781,6 +795,7 @@ WITH report_access_data AS (
SELECT
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
@tenant_id AS tenant_id,
@organization_id AS organization_id,
@trust_center_access_id AS trust_center_access_id,
null::text AS document_id,
unnest(@report_ids::text[]) AS report_id,
@@ -791,14 +806,15 @@ WITH report_access_data AS (
@updated_at::timestamptz AS updated_at
)
INSERT INTO trust_center_document_accesses (
id, tenant_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
id, tenant_id, organization_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
)
SELECT * FROM report_access_data
ON CONFLICT DO NOTHING
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"tenant_id": scope.GetTenantID(),
"organization_id": organizationID,
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
"trust_center_access_id": trustCenterAccessID,
"report_ids": reportIDs,
@@ -903,6 +919,7 @@ func (tcdas TrustCenterDocumentAccesses) BulkInsertTrustCenterFileAccesses(
conn pg.Conn,
scope Scoper,
trustCenterAccessID gid.GID,
organizationID gid.GID,
trustCenterFileIDs []gid.GID,
requested bool,
createdAt time.Time,
@@ -912,6 +929,7 @@ WITH trust_center_file_access_data AS (
SELECT
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
@tenant_id AS tenant_id,
@organization_id AS organization_id,
@trust_center_access_id AS trust_center_access_id,
null::text AS document_id,
null::text AS report_id,
@@ -922,14 +940,15 @@ WITH trust_center_file_access_data AS (
@updated_at::timestamptz AS updated_at
)
INSERT INTO trust_center_document_accesses (
id, tenant_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
id, tenant_id, organization_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
)
SELECT * FROM trust_center_file_access_data
ON CONFLICT DO NOTHING
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"tenant_id": scope.GetTenantID(),
"organization_id": organizationID,
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
"trust_center_access_id": trustCenterAccessID,
"trust_center_file_ids": trustCenterFileIDs,

View File

@@ -30,15 +30,16 @@ import (
type (
TrustCenterReference struct {
ID gid.GID `db:"id"`
TrustCenterID gid.GID `db:"trust_center_id"`
Name string `db:"name"`
Description *string `db:"description"`
WebsiteURL string `db:"website_url"`
LogoFileID gid.GID `db:"logo_file_id"`
Rank int `db:"rank"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
TrustCenterID gid.GID `db:"trust_center_id"`
Name string `db:"name"`
Description *string `db:"description"`
WebsiteURL string `db:"website_url"`
LogoFileID gid.GID `db:"logo_file_id"`
Rank int `db:"rank"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
TrustCenterReferences []*TrustCenterReference
@@ -83,6 +84,7 @@ func (t *TrustCenterReference) LoadByID(
q := `
SELECT
id,
organization_id,
trust_center_id,
name,
description,
@@ -128,6 +130,7 @@ INSERT INTO
trust_center_references (
tenant_id,
id,
organization_id,
trust_center_id,
name,
description,
@@ -140,6 +143,7 @@ INSERT INTO
VALUES (
@tenant_id,
@id,
@organization_id,
@trust_center_id,
@name,
@description,
@@ -155,6 +159,7 @@ RETURNING rank;
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"id": t.ID,
"organization_id": t.OrganizationID,
"trust_center_id": t.TrustCenterID,
"name": t.Name,
"description": t.Description,
@@ -308,6 +313,7 @@ func (t *TrustCenterReferences) LoadByTrustCenterID(
q := `
SELECT
id,
organization_id,
trust_center_id,
name,
description,

View File

@@ -28,16 +28,17 @@ import (
type (
VendorComplianceReport struct {
ID gid.GID
VendorID gid.GID
ReportDate time.Time
ValidUntil *time.Time
ReportName string
ReportFileId *gid.GID
SnapshotID *gid.GID
SourceID *gid.GID
CreatedAt time.Time
UpdatedAt time.Time
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
VendorID gid.GID `db:"vendor_id"`
ReportDate time.Time `db:"report_date"`
ValidUntil *time.Time `db:"valid_until"`
ReportName string `db:"report_name"`
ReportFileId *gid.GID `db:"report_file_id"`
SnapshotID *gid.GID `db:"snapshot_id"`
SourceID *gid.GID `db:"source_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
VendorComplianceReports []*VendorComplianceReport
@@ -157,6 +158,7 @@ func (vcr *VendorComplianceReport) Insert(
INSERT INTO
vendor_compliance_reports (
id,
organization_id,
tenant_id,
vendor_id,
report_date,
@@ -168,6 +170,7 @@ INSERT INTO
)
VALUES (
@id,
@organization_id,
@tenant_id,
@vendor_id,
@report_date,
@@ -179,15 +182,16 @@ VALUES (
)
`
args := pgx.NamedArgs{
"id": vcr.ID,
"tenant_id": scope.GetTenantID(),
"vendor_id": vcr.VendorID,
"report_date": vcr.ReportDate,
"valid_until": vcr.ValidUntil,
"report_name": vcr.ReportName,
"report_file_id": vcr.ReportFileId,
"created_at": vcr.CreatedAt,
"updated_at": vcr.UpdatedAt,
"id": vcr.ID,
"organization_id": vcr.OrganizationID,
"tenant_id": scope.GetTenantID(),
"vendor_id": vcr.VendorID,
"report_date": vcr.ReportDate,
"valid_until": vcr.ValidUntil,
"report_name": vcr.ReportName,
"report_file_id": vcr.ReportFileId,
"created_at": vcr.CreatedAt,
"updated_at": vcr.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -21,24 +21,25 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
VendorContact struct {
ID gid.GID `db:"id"`
VendorID gid.GID `db:"vendor_id"`
FullName *string `db:"full_name"`
Email *string `db:"email"`
Phone *string `db:"phone"`
Role *string `db:"role"`
SnapshotID *gid.GID `db:"snapshot_id"`
SourceID *gid.GID `db:"source_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
VendorID gid.GID `db:"vendor_id"`
FullName *string `db:"full_name"`
Email *string `db:"email"`
Phone *string `db:"phone"`
Role *string `db:"role"`
SnapshotID *gid.GID `db:"snapshot_id"`
SourceID *gid.GID `db:"source_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
VendorContacts []*VendorContact
@@ -74,6 +75,7 @@ func (vc *VendorContact) LoadByID(
q := `
SELECT
id,
organization_id,
vendor_id,
full_name,
email,
@@ -126,6 +128,7 @@ func (vc *VendorContacts) LoadByVendorID(
q := `
SELECT
id,
organization_id,
vendor_id,
full_name,
email,
@@ -176,6 +179,7 @@ INSERT INTO
vendor_contacts (
tenant_id,
id,
organization_id,
vendor_id,
full_name,
email,
@@ -187,6 +191,7 @@ INSERT INTO
VALUES (
@tenant_id,
@vendor_contact_id,
@organization_id,
@vendor_id,
@full_name,
@email,
@@ -200,6 +205,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"vendor_contact_id": vc.ID,
"organization_id": vc.OrganizationID,
"vendor_id": vc.VendorID,
"full_name": vc.FullName,
"email": vc.Email,

View File

@@ -20,16 +20,17 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
// RiskAssessment represents a point-in-time risk assessment for a vendor
VendorRiskAssessment struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
VendorID gid.GID `db:"vendor_id"`
ExpiresAt time.Time `db:"expires_at"`
DataSensitivity DataSensitivity `db:"data_sensitivity"`
@@ -66,6 +67,7 @@ INSERT INTO
vendor_risk_assessments (
tenant_id,
id,
organization_id,
vendor_id,
expires_at,
data_sensitivity,
@@ -77,6 +79,7 @@ INSERT INTO
VALUES (
@tenant_id,
@id,
@organization_id,
@vendor_id,
@expires_at,
@data_sensitivity,
@@ -90,6 +93,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"id": r.ID,
"organization_id": r.OrganizationID,
"vendor_id": r.VendorID,
"expires_at": r.ExpiresAt,
"data_sensitivity": r.DataSensitivity,
@@ -112,6 +116,7 @@ func (r *VendorRiskAssessment) LoadByID(
q := `
SELECT
id,
organization_id,
vendor_id,
expires_at,
data_sensitivity,
@@ -160,6 +165,7 @@ func (r *VendorRiskAssessment) LoadLatestByVendorID(
q := `
SELECT
id,
organization_id,
vendor_id,
expires_at,
data_sensitivity,
@@ -211,6 +217,7 @@ func (r *VendorRiskAssessments) LoadByVendorID(
q := `
SELECT
id,
organization_id,
vendor_id,
expires_at,
data_sensitivity,

View File

@@ -21,22 +21,23 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
VendorService struct {
ID gid.GID `db:"id"`
VendorID gid.GID `db:"vendor_id"`
Name string `db:"name"`
Description *string `db:"description"`
SnapshotID *gid.GID `db:"snapshot_id"`
SourceID *gid.GID `db:"source_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
VendorID gid.GID `db:"vendor_id"`
Name string `db:"name"`
Description *string `db:"description"`
SnapshotID *gid.GID `db:"snapshot_id"`
SourceID *gid.GID `db:"source_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
VendorServices []*VendorService
@@ -70,6 +71,7 @@ func (vs *VendorService) LoadByID(
q := `
SELECT
id,
organization_id,
vendor_id,
name,
description,
@@ -120,6 +122,7 @@ func (vs *VendorServices) LoadByVendorID(
q := `
SELECT
id,
organization_id,
vendor_id,
name,
description,
@@ -168,6 +171,7 @@ INSERT INTO
vendor_services (
tenant_id,
id,
organization_id,
vendor_id,
name,
description,
@@ -177,6 +181,7 @@ INSERT INTO
VALUES (
@tenant_id,
@vendor_service_id,
@organization_id,
@vendor_id,
@name,
@description,
@@ -188,6 +193,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"vendor_service_id": vs.ID,
"organization_id": vs.OrganizationID,
"vendor_id": vs.VendorID,
"name": vs.Name,
"description": vs.Description,

View File

@@ -203,7 +203,7 @@ func (s AssetService) Update(
}
if req.VendorIDs != nil {
if err := assetVendors.Merge(ctx, conn, s.svc.scope, asset.ID, req.VendorIDs); err != nil {
if err := assetVendors.Merge(ctx, conn, s.svc.scope, asset.ID, asset.OrganizationID, req.VendorIDs); err != nil {
return fmt.Errorf("cannot update asset vendors: %w", err)
}
}
@@ -248,7 +248,7 @@ func (s AssetService) Create(
}
if len(req.VendorIDs) > 0 {
if err := assetVendors.Insert(ctx, conn, s.svc.scope, asset.ID, req.VendorIDs); err != nil {
if err := assetVendors.Insert(ctx, conn, s.svc.scope, asset.ID, asset.OrganizationID, req.VendorIDs); err != nil {
return fmt.Errorf("cannot create asset vendors: %w", err)
}
}

View File

@@ -54,13 +54,12 @@ type (
func (ccr *CreateControlRequest) Validate() error {
v := validator.New()
v.Check(ccr.ID, "id", validator.Required(), validator.GID(coredata.ControlEntityType))
v.Check(ccr.FrameworkID, "framework_id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(ccr.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(ccr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(ccr.SectionTitle, "section_title", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(ccr.Status, "status", validator.Required(), validator.OneOfSlice(coredata.ControlStatuses()))
v.Check(ccr.ExclusionJustification, "exclusion_justification", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ccr.ExclusionJustification, "exclusion_justification", validator.SafeText(TitleMaxLength))
return v.Error()
}
@@ -367,13 +366,6 @@ func (s ControlService) CreateMeasureMapping(
controlID gid.GID,
measureID gid.GID,
) (*coredata.Control, *coredata.Measure, error) {
controlMeasure := &coredata.ControlMeasure{
ControlID: controlID,
MeasureID: measureID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
control := &coredata.Control{}
measure := &coredata.Measure{}
@@ -388,6 +380,14 @@ func (s ControlService) CreateMeasureMapping(
return fmt.Errorf("cannot load measure: %w", err)
}
controlMeasure := &coredata.ControlMeasure{
ControlID: controlID,
MeasureID: measureID,
OrganizationID: control.OrganizationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
return controlMeasure.Upsert(ctx, conn, s.svc.scope)
},
)
@@ -454,10 +454,11 @@ func (s ControlService) CreateDocumentMapping(
}
controlDocument := &coredata.ControlDocument{
ControlID: control.ID,
DocumentID: document.ID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
ControlID: control.ID,
DocumentID: document.ID,
OrganizationID: control.OrganizationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
if err := controlDocument.Insert(ctx, conn, s.svc.scope); err != nil {
@@ -515,12 +516,6 @@ func (s ControlService) CreateAuditMapping(
controlID gid.GID,
auditID gid.GID,
) (*coredata.Control, *coredata.Audit, error) {
controlAudit := &coredata.ControlAudit{
ControlID: controlID,
AuditID: auditID,
CreatedAt: time.Now(),
}
control := &coredata.Control{}
audit := &coredata.Audit{}
@@ -535,6 +530,13 @@ func (s ControlService) CreateAuditMapping(
return fmt.Errorf("cannot load audit: %w", err)
}
controlAudit := &coredata.ControlAudit{
ControlID: controlID,
AuditID: auditID,
OrganizationID: control.OrganizationID,
CreatedAt: time.Now(),
}
if err := controlAudit.Upsert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot create control audit mapping: %w", err)
}
@@ -620,12 +622,6 @@ func (s ControlService) CreateSnapshotMapping(
controlID gid.GID,
snapshotID gid.GID,
) (*coredata.Control, *coredata.Snapshot, error) {
controlSnapshot := &coredata.ControlSnapshot{
ControlID: controlID,
SnapshotID: snapshotID,
CreatedAt: time.Now(),
}
control := &coredata.Control{}
snapshot := &coredata.Snapshot{}
@@ -636,6 +632,13 @@ func (s ControlService) CreateSnapshotMapping(
return fmt.Errorf("cannot load control: %w", err)
}
controlSnapshot := &coredata.ControlSnapshot{
ControlID: controlID,
SnapshotID: snapshotID,
OrganizationID: control.OrganizationID,
CreatedAt: time.Now(),
}
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID); err != nil {
return fmt.Errorf("cannot load snapshot: %w", err)
}
@@ -751,6 +754,7 @@ func (s ControlService) Create(
}
control.FrameworkID = framework.ID
control.OrganizationID = framework.OrganizationID
return control.Insert(ctx, conn, s.svc.scope)
},

View File

@@ -78,6 +78,7 @@ func (s *CustomDomainService) CreateCustomDomain(
ctx,
func(tx pg.Conn) error {
domain = coredata.NewCustomDomain(s.svc.scope.GetTenantID(), req.Domain)
domain.OrganizationID = req.OrganizationID
if err := domain.Insert(ctx, tx, s.svc.scope, s.encryptionKey); err != nil {
return fmt.Errorf("cannot insert custom domain: %w", err)

View File

@@ -205,7 +205,7 @@ func (s DatumService) Update(
}
if req.VendorIDs != nil {
if err := datumVendors.Merge(ctx, conn, s.svc.scope, datum.ID, req.VendorIDs); err != nil {
if err := datumVendors.Merge(ctx, conn, s.svc.scope, datum.ID, datum.OrganizationID, req.VendorIDs); err != nil {
return fmt.Errorf("cannot update data vendors: %w", err)
}
}
@@ -250,7 +250,7 @@ func (s DatumService) Create(
}
if len(req.VendorIDs) > 0 {
if err := datumVendors.Insert(ctx, conn, s.svc.scope, datum.ID, req.VendorIDs); err != nil {
if err := datumVendors.Insert(ctx, conn, s.svc.scope, datum.ID, datum.OrganizationID, req.VendorIDs); err != nil {
return fmt.Errorf("cannot create data vendors: %w", err)
}
}

View File

@@ -405,6 +405,8 @@ func (s *DocumentService) Create(
return fmt.Errorf("cannot insert document: %w", err)
}
documentVersion.OrganizationID = organization.ID
if err := documentVersion.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot create document version: %w", err)
}
@@ -716,6 +718,11 @@ func (s *DocumentService) createSignatureRequestInTx(
ignoreExisting bool,
) (*coredata.DocumentVersionSignature, error) {
signatory := &coredata.People{}
documentVersion := &coredata.DocumentVersion{}
if err := documentVersion.LoadByID(ctx, tx, s.svc.scope, documentVersionID); err != nil {
return nil, fmt.Errorf("cannot load document version: %w", err)
}
if err := signatory.LoadByID(ctx, tx, s.svc.scope, signatoryID); err != nil {
return nil, fmt.Errorf("cannot load signatory: %w", err)
@@ -731,6 +738,7 @@ func (s *DocumentService) createSignatureRequestInTx(
now := time.Now()
documentVersionSignature := &coredata.DocumentVersionSignature{
ID: documentVersionSignatureID,
OrganizationID: documentVersion.OrganizationID,
DocumentVersionID: documentVersionID,
State: coredata.DocumentVersionSignatureStateRequested,
RequestedAt: now,
@@ -829,6 +837,7 @@ func (s *DocumentService) CreateDraft(
}
draftVersion.ID = draftVersionID
draftVersion.OrganizationID = document.OrganizationID
draftVersion.DocumentID = documentID
draftVersion.Title = document.Title
draftVersion.OwnerID = document.OwnerID
@@ -936,11 +945,14 @@ func (s *DocumentService) RequestExport(
}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
var organizationID gid.GID
for _, documentID := range documentIDs {
document := &coredata.Document{}
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document %q: %w", documentID, err)
}
organizationID = document.OrganizationID
}
now := time.Now()
@@ -959,6 +971,7 @@ func (s *DocumentService) RequestExport(
exportJob = &coredata.ExportJob{
ID: exportJobID,
OrganizationID: organizationID,
Type: coredata.ExportJobTypeDocument,
Arguments: argsJSON,
Status: coredata.ExportJobStatusPending,

View File

@@ -126,6 +126,7 @@ func (s EvidenceService) UploadMeasureEvidence(
return fmt.Errorf("cannot upload or file: %w", err)
}
evidence.OrganizationID = measure.OrganizationID
evidence.EvidenceFileId = &file.ID
evidence.MeasureID = req.MeasureID

View File

@@ -120,18 +120,30 @@ func (s FileService) UploadAndSaveFile(
fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType)
var file *coredata.File
// Extract organization ID from S3 metadata
organizationIDStr, hasOrgID := s3Metadata["organization-id"]
var organizationID gid.GID
if hasOrgID {
var err error
organizationID, err = gid.ParseGID(organizationIDStr)
if err != nil {
return nil, fmt.Errorf("invalid organization-id in metadata: %w", err)
}
}
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
file = &coredata.File{
ID: fileID,
BucketName: s.svc.bucket,
MimeType: mimeType,
FileName: req.Filename,
FileKey: objectKey.String(),
FileSize: *headOutput.ContentLength,
CreatedAt: now,
UpdatedAt: now,
ID: fileID,
OrganizationID: organizationID,
BucketName: s.svc.bucket,
MimeType: mimeType,
FileName: req.Filename,
FileKey: objectKey.String(),
FileSize: *headOutput.ContentLength,
CreatedAt: now,
UpdatedAt: now,
}
if err := file.Insert(ctx, conn, s.svc.scope); err != nil {

View File

@@ -122,6 +122,7 @@ func (s FrameworkService) RequestExport(
exportJob = &coredata.ExportJob{
ID: exportJobID,
OrganizationID: framework.OrganizationID,
Type: coredata.ExportJobTypeFramework,
Arguments: argsJSON,
Status: coredata.ExportJobStatusPending,
@@ -518,14 +519,15 @@ func (s FrameworkService) Import(
now := time.Now()
description := control.Description
control := &coredata.Control{
ID: controlID,
FrameworkID: frameworkID,
SectionTitle: control.ID,
Name: control.Name,
Description: &description,
Status: coredata.ControlStatusIncluded,
CreatedAt: now,
UpdatedAt: now,
ID: controlID,
FrameworkID: frameworkID,
OrganizationID: organization.ID,
SectionTitle: control.ID,
Name: control.Name,
Description: &description,
Status: coredata.ControlStatusIncluded,
CreatedAt: now,
UpdatedAt: now,
}
if err := control.Insert(ctx, tx, s.svc.scope); err != nil {

View File

@@ -386,15 +386,16 @@ func (s MeasureService) Import(
continue
}
controlMeasure := &coredata.ControlMeasure{
ControlID: control.ID,
MeasureID: measure.ID,
CreatedAt: now,
}
controlMeasure := &coredata.ControlMeasure{
ControlID: control.ID,
MeasureID: measure.ID,
OrganizationID: measure.OrganizationID,
CreatedAt: now,
}
if err := controlMeasure.Upsert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert control measure: %w", err)
}
if err := controlMeasure.Upsert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert control measure: %w", err)
}
}
}

View File

@@ -221,9 +221,10 @@ func (s RiskService) CreateDocumentMapping(
}
riskDocument := &coredata.RiskDocument{
RiskID: risk.ID,
DocumentID: document.ID,
CreatedAt: time.Now(),
RiskID: risk.ID,
DocumentID: document.ID,
OrganizationID: risk.OrganizationID,
CreatedAt: time.Now(),
}
return riskDocument.Insert(ctx, conn, s.svc.scope)
@@ -288,9 +289,10 @@ func (s RiskService) CreateMeasureMapping(
}
riskMeasure := &coredata.RiskMeasure{
RiskID: risk.ID,
MeasureID: measure.ID,
CreatedAt: time.Now(),
RiskID: risk.ID,
MeasureID: measure.ID,
OrganizationID: risk.OrganizationID,
CreatedAt: time.Now(),
}
return riskMeasure.Insert(ctx, conn, s.svc.scope)
@@ -324,9 +326,10 @@ func (s RiskService) DeleteMeasureMapping(
}
riskMeasure := &coredata.RiskMeasure{
RiskID: riskID,
MeasureID: measureID,
CreatedAt: time.Now(),
RiskID: riskID,
MeasureID: measureID,
OrganizationID: risk.OrganizationID,
CreatedAt: time.Now(),
}
return riskMeasure.Delete(ctx, conn, s.svc.scope, risk.ID, measure.ID)
@@ -360,9 +363,10 @@ func (s RiskService) CreateObligationMapping(
}
riskObligation := &coredata.RiskObligation{
RiskID: risk.ID,
ObligationID: obligation.ID,
CreatedAt: time.Now(),
RiskID: risk.ID,
ObligationID: obligation.ID,
OrganizationID: risk.OrganizationID,
CreatedAt: time.Now(),
}
return riskObligation.Insert(ctx, conn, s.svc.scope)

View File

@@ -268,7 +268,7 @@ func (s *Service) ExportJob(ctx context.Context) error {
return unknownTypeErr
}
exportJob, buildErr := exportService.BuildAndUploadExport(ctx, exportJob.ID)
updatedExportJob, buildErr := exportService.BuildAndUploadExport(ctx, exportJob.ID)
if buildErr != nil {
if err := s.commitFailedExport(ctx, exportJob, buildErr); err != nil {
return fmt.Errorf(
@@ -280,6 +280,7 @@ func (s *Service) ExportJob(ctx context.Context) error {
}
return fmt.Errorf("cannot build and upload %s export: %w", exportJob.Type, buildErr)
}
exportJob = updatedExportJob
if emailErr := exportService.SendExportEmail(ctx, *exportJob.FileID, exportJob.RecipientName, exportJob.RecipientEmail); emailErr != nil {
if err := s.commitFailedExport(ctx, exportJob, emailErr); err != nil {

View File

@@ -272,8 +272,14 @@ func (s TrustCenterAccessService) Create(
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
trustCenter := &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, tx, s.svc.scope, req.TrustCenterID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
access = &coredata.TrustCenterAccess{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.TrustCenterAccessEntityType),
OrganizationID: trustCenter.OrganizationID,
TenantID: s.svc.scope.GetTenantID(),
TrustCenterID: req.TrustCenterID,
Email: req.Email,
@@ -332,9 +338,9 @@ func (s TrustCenterAccessService) Update(
return fmt.Errorf("cannot update trust center access: %w", err)
}
if err := s.upsertDocumentAccesses(ctx, tx, access.ID, req.DocumentIDs, req.ReportIDs, req.TrustCenterFileIDs, now); err != nil {
return fmt.Errorf("cannot upsert document accesses: %w", err)
}
if err := s.upsertDocumentAccesses(ctx, tx, access.ID, access.OrganizationID, req.DocumentIDs, req.ReportIDs, req.TrustCenterFileIDs, now); err != nil {
return fmt.Errorf("cannot upsert document accesses: %w", err)
}
if req.ReportIDs != nil {
if err := coredata.ActivateByReportIDs(ctx, tx, s.svc.scope, access.ID, req.ReportIDs, now); err != nil {
@@ -387,6 +393,7 @@ func (s TrustCenterAccessService) upsertDocumentAccesses(
ctx context.Context,
tx pg.Conn,
accessID gid.GID,
organizationID gid.GID,
documentIDs []gid.GID,
reportIDs []gid.GID,
trustCenterFileIDs []gid.GID,
@@ -402,7 +409,7 @@ func (s TrustCenterAccessService) upsertDocumentAccesses(
if documentIDs != nil {
var documentAccesses coredata.TrustCenterDocumentAccesses
if err := documentAccesses.BulkInsertDocumentAccesses(ctx, tx, s.svc.scope, accessID, documentIDs, false, now); err != nil {
if err := documentAccesses.BulkInsertDocumentAccesses(ctx, tx, s.svc.scope, accessID, organizationID, documentIDs, false, now); err != nil {
return fmt.Errorf("cannot create document accesses: %w", err)
}
if err := coredata.ActivateByDocumentIDs(ctx, tx, s.svc.scope, accessID, documentIDs, now); err != nil {
@@ -412,7 +419,7 @@ func (s TrustCenterAccessService) upsertDocumentAccesses(
if reportIDs != nil {
var documentAccesses coredata.TrustCenterDocumentAccesses
if err := documentAccesses.BulkInsertReportAccesses(ctx, tx, s.svc.scope, accessID, reportIDs, false, now); err != nil {
if err := documentAccesses.BulkInsertReportAccesses(ctx, tx, s.svc.scope, accessID, organizationID, reportIDs, false, now); err != nil {
return fmt.Errorf("cannot create report accesses: %w", err)
}
if err := coredata.ActivateByReportIDs(ctx, tx, s.svc.scope, accessID, reportIDs, now); err != nil {
@@ -422,7 +429,7 @@ func (s TrustCenterAccessService) upsertDocumentAccesses(
if trustCenterFileIDs != nil {
var documentAccesses coredata.TrustCenterDocumentAccesses
if err := documentAccesses.BulkInsertTrustCenterFileAccesses(ctx, tx, s.svc.scope, accessID, trustCenterFileIDs, false, now); err != nil {
if err := documentAccesses.BulkInsertTrustCenterFileAccesses(ctx, tx, s.svc.scope, accessID, organizationID, trustCenterFileIDs, false, now); err != nil {
return fmt.Errorf("cannot create trust center file accesses: %w", err)
}
if err := coredata.ActivateByTrustCenterFileIDs(ctx, tx, s.svc.scope, accessID, trustCenterFileIDs, now); err != nil {

View File

@@ -164,6 +164,11 @@ func (s TrustCenterReferenceService) Create(
var logoKey string
err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error {
trustCenter := &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, tx, s.svc.scope, req.TrustCenterID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
fileID, s3Key, err := s.uploadLogoFile(ctx, tx, req.LogoFile, referenceID, req.TrustCenterID, now)
if err != nil {
return fmt.Errorf("cannot upload logo file: %w", err)
@@ -171,14 +176,15 @@ func (s TrustCenterReferenceService) Create(
logoKey = s3Key
reference = &coredata.TrustCenterReference{
ID: referenceID,
TrustCenterID: req.TrustCenterID,
Name: req.Name,
Description: req.Description,
WebsiteURL: req.WebsiteURL,
LogoFileID: fileID,
CreatedAt: now,
UpdatedAt: now,
ID: referenceID,
OrganizationID: trustCenter.OrganizationID,
TrustCenterID: req.TrustCenterID,
Name: req.Name,
Description: req.Description,
WebsiteURL: req.WebsiteURL,
LogoFileID: fileID,
CreatedAt: now,
UpdatedAt: now,
}
if err := reference.Insert(ctx, tx, s.svc.scope); err != nil {

View File

@@ -103,14 +103,15 @@ func (s VendorComplianceReportService) Upload(
vendorComplianceReportID := gid.New(s.svc.scope.GetTenantID(), coredata.VendorComplianceReportEntityType)
vendorComplianceReport := &coredata.VendorComplianceReport{
ID: vendorComplianceReportID,
VendorID: vendorID,
ReportDate: req.ReportDate,
ValidUntil: req.ValidUntil,
ReportName: req.ReportName,
ReportFileId: &f.ID,
CreatedAt: now,
UpdatedAt: now,
ID: vendorComplianceReportID,
OrganizationID: vendor.OrganizationID,
VendorID: vendorID,
ReportDate: req.ReportDate,
ValidUntil: req.ValidUntil,
ReportName: req.ReportName,
ReportFileId: &f.ID,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithConn(

View File

@@ -143,9 +143,16 @@ func (s VendorContactService) Create(
UpdatedAt: now,
}
err := s.svc.pg.WithConn(
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
vendor := &coredata.Vendor{}
if err := vendor.LoadByID(ctx, conn, s.svc.scope, req.VendorID); err != nil {
return fmt.Errorf("cannot load vendor: %w", err)
}
vendorContact.OrganizationID = vendor.OrganizationID
if err := vendorContact.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert vendor contact: %w", err)
}

View File

@@ -638,7 +638,13 @@ func (s VendorService) CreateRiskAssessment(
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
vendor := coredata.Vendor{ID: req.VendorID}
vendor := coredata.Vendor{}
if err := vendor.LoadByID(ctx, tx, s.svc.scope, req.VendorID); err != nil {
return fmt.Errorf("cannot load vendor: %w", err)
}
vendorRiskAssessment.OrganizationID = vendor.OrganizationID
if err := vendor.ExpireNonExpiredRiskAssessments(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot expire vendor risk assessments: %w", err)
}

View File

@@ -133,9 +133,16 @@ func (s VendorServiceService) Create(
UpdatedAt: now,
}
err := s.svc.pg.WithConn(
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
vendor := &coredata.Vendor{}
if err := vendor.LoadByID(ctx, conn, s.svc.scope, req.VendorID); err != nil {
return fmt.Errorf("cannot load vendor: %w", err)
}
vendorService.OrganizationID = vendor.OrganizationID
if err := vendorService.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert vendor service: %w", err)
}

View File

@@ -17,11 +17,11 @@ package api
import (
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/vektah/gqlparser/v2/ast"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth"
@@ -92,6 +92,11 @@ var (
ErrMissingAuthzService = errors.New("server configuration requires a valid authz.Service instance")
)
// GetConsoleSchema returns the GraphQL schema for the console API
func GetConsoleSchema() *ast.Schema {
return console_v1.GetSchema()
}
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

View File

@@ -17,6 +17,10 @@ resolver:
autobind: []
call_argument_directives_with_null: true
directives:
mustBeAuthorized:
skip_runtime: false
models:
ID:
model:

View File

@@ -31,6 +31,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi/v5"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/httpserver"
@@ -64,6 +65,7 @@ type (
samlSvc *auth.SAMLService
authCfg AuthConfig
customDomainCname string
schema *ast.Schema
}
ctxKey struct{ name string }
@@ -307,21 +309,32 @@ func NewMux(
return r
}
// GetSchema returns the parsed GraphQL schema for the console API
// This is used by other services like authz to extract permissions from @mustBeAuthorized directives
func GetSchema() *ast.Schema {
execSchema := schema.NewExecutableSchema(schema.Config{})
return execSchema.Schema()
}
func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, samlSvc *auth.SAMLService, authCfg AuthConfig, customDomainCname string) http.HandlerFunc {
var mb int64 = 1 << 20
es := schema.NewExecutableSchema(
schema.Config{
Resolvers: &Resolver{
proboSvc: proboSvc,
authSvc: authSvc,
authzSvc: authzSvc,
samlSvc: samlSvc,
authCfg: authCfg,
customDomainCname: customDomainCname,
},
// Parse the schema first to make it available to resolvers
execSchema := schema.NewExecutableSchema(schema.Config{})
cfg := schema.Config{
Resolvers: &Resolver{
proboSvc: proboSvc,
authSvc: authSvc,
authzSvc: authzSvc,
samlSvc: samlSvc,
authCfg: authCfg,
customDomainCname: customDomainCname,
schema: execSchema.Schema(),
},
)
}
es := schema.NewExecutableSchema(cfg)
srv := handler.New(es)
srv.AddTransport(transport.POST{})
srv.AddTransport(
@@ -504,3 +517,14 @@ func validateTenantAccess(ctx context.Context, tenantID gid.TenantID) {
panic(&authz.TenantAccessError{Message: "tenant not found"})
}
}
func (r *Resolver) MustBeAuthorized(ctx context.Context, entityID gid.GID, action authz.Action) {
user := UserFromContext(ctx)
apiKey := UserAPIKeyFromContext(ctx)
authzSvc := r.AuthzService(ctx, entityID.TenantID())
err := authzSvc.Authorize(ctx, user, apiKey, entityID, action)
if err != nil {
panic(err)
}
}

View File

@@ -33,6 +33,14 @@ type PageInfo {
endCursor: CursorKey
}
# Roles
enum Role {
OWNER
ADMIN
VIEWER
FULL
}
# Enums
enum OrderDirection
@goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") {
@@ -83,13 +91,17 @@ enum InvitationStatus
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired")
}
enum Role @goModel(model: "go.probo.inc/probo/pkg/coredata.Role") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.RoleOwner")
ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.RoleAdmin")
MEMBER @goEnum(value: "go.probo.inc/probo/pkg/coredata.RoleMember")
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.RoleViewer")
enum MembershipRole @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner")
ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin")
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer")
}
enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") {
FULL @goEnum(value: "go.probo.inc/probo/pkg/coredata.APIRoleFull")
}
enum DocumentStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") {
DRAFT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
@@ -1662,7 +1674,7 @@ type Membership implements Node {
id: ID!
userID: ID!
organizationID: ID!
role: Role!
role: MembershipRole!
fullName: String!
emailAddress: String!
authMethod: UserAuthMethod! @goField(forceResolver: true)
@@ -1674,7 +1686,7 @@ type Invitation implements Node {
id: ID!
email: String!
fullName: String!
role: Role!
role: MembershipRole!
status: InvitationStatus!
expiresAt: Datetime!
acceptedAt: Datetime
@@ -1731,7 +1743,7 @@ type Vendor implements Node {
): VendorComplianceReportConnection! @goField(forceResolver: true)
businessAssociateAgreement: VendorBusinessAssociateAgreement
@goField(forceResolver: true)
@goField(forceResolver: true)
dataPrivacyAgreement: VendorDataPrivacyAgreement @goField(forceResolver: true)
contacts(
@@ -1855,7 +1867,9 @@ type Framework implements Node {
updatedAt: Datetime!
}
type Control implements Node {
type Control implements Node @goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.Control"
) {
id: ID!
sectionTitle: String!
name: String!
@@ -2349,7 +2363,7 @@ type TrustCenterReferenceEdge {
node: TrustCenterReference!
}
type TrustCenterFile implements Node {
type TrustCenterFile {
id: ID!
name: String!
category: String!
@@ -2723,8 +2737,7 @@ type Mutation {
# Organization mutations
createOrganization(
input: CreateOrganizationInput!
): CreateOrganizationPayload!
updateOrganization(
): CreateOrganizationPayload! updateOrganization(
input: UpdateOrganizationInput!
): UpdateOrganizationPayload!
updateOrganizationContext(
@@ -2732,218 +2745,145 @@ type Mutation {
): UpdateOrganizationContextPayload!
deleteOrganizationHorizontalLogo(
input: DeleteOrganizationHorizontalLogoInput!
): DeleteOrganizationHorizontalLogoPayload!
deleteOrganization(
): DeleteOrganizationHorizontalLogoPayload! deleteOrganization(
input: DeleteOrganizationInput!
): DeleteOrganizationPayload!
updateTrustCenter(input: UpdateTrustCenterInput!): UpdateTrustCenterPayload!
uploadTrustCenterNDA(
input: UploadTrustCenterNDAInput!
): UploadTrustCenterNDAPayload!
deleteTrustCenterNDA(
input: DeleteTrustCenterNDAInput!
): DeleteTrustCenterNDAPayload!
# Trust Center Access CRUD mutations
createTrustCenterAccess(
input: CreateTrustCenterAccessInput!
): CreateTrustCenterAccessPayload!
updateTrustCenterAccess(
input: UpdateTrustCenterAccessInput!
): UpdateTrustCenterAccessPayload!
deleteTrustCenterAccess(
input: DeleteTrustCenterAccessInput!
): DeleteTrustCenterAccessPayload!
# Trust Center Reference mutations
createTrustCenterReference(
input: CreateTrustCenterReferenceInput!
): CreateTrustCenterReferencePayload!
updateTrustCenterReference(
input: UpdateTrustCenterReferenceInput!
): UpdateTrustCenterReferencePayload!
deleteTrustCenterReference(
input: DeleteTrustCenterReferenceInput!
): DeleteTrustCenterReferencePayload!
# Trust Center File mutations
createTrustCenterFile(
input: CreateTrustCenterFileInput!
): CreateTrustCenterFilePayload!
updateTrustCenterFile(
input: UpdateTrustCenterFileInput!
): UpdateTrustCenterFilePayload!
getTrustCenterFile(
input: GetTrustCenterFileInput!
): GetTrustCenterFilePayload!
deleteTrustCenterFile(
input: DeleteTrustCenterFileInput!
): DeleteTrustCenterFilePayload!
# User mutations
confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload!
inviteUser(input: InviteUserInput!): InviteUserPayload!
acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload!
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload! removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload!
# People mutations
createPeople(input: CreatePeopleInput!): CreatePeoplePayload!
updatePeople(input: UpdatePeopleInput!): UpdatePeoplePayload!
deletePeople(input: DeletePeopleInput!): DeletePeoplePayload!
createPeople(input: CreatePeopleInput!): CreatePeoplePayload! updatePeople(input: UpdatePeopleInput!): UpdatePeoplePayload! deletePeople(input: DeletePeopleInput!): DeletePeoplePayload!
# Vendor mutations
createVendor(input: CreateVendorInput!): CreateVendorPayload!
updateVendor(input: UpdateVendorInput!): UpdateVendorPayload!
deleteVendor(input: DeleteVendorInput!): DeleteVendorPayload!
createVendor(input: CreateVendorInput!): CreateVendorPayload! updateVendor(input: UpdateVendorInput!): UpdateVendorPayload! deleteVendor(input: DeleteVendorInput!): DeleteVendorPayload!
# Vendor Contact mutations
createVendorContact(
input: CreateVendorContactInput!
): CreateVendorContactPayload!
updateVendorContact(
): CreateVendorContactPayload! updateVendorContact(
input: UpdateVendorContactInput!
): UpdateVendorContactPayload!
deleteVendorContact(
): UpdateVendorContactPayload! deleteVendorContact(
input: DeleteVendorContactInput!
): DeleteVendorContactPayload!
# Vendor Service mutations
createVendorService(
input: CreateVendorServiceInput!
): CreateVendorServicePayload!
updateVendorService(
): CreateVendorServicePayload! updateVendorService(
input: UpdateVendorServiceInput!
): UpdateVendorServicePayload!
deleteVendorService(
): UpdateVendorServicePayload! deleteVendorService(
input: DeleteVendorServiceInput!
): DeleteVendorServicePayload!
# Framework mutations
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload!
deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload!
generateFrameworkStateOfApplicability(
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload! updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload! deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload! generateFrameworkStateOfApplicability(
input: GenerateFrameworkStateOfApplicabilityInput!
): GenerateFrameworkStateOfApplicabilityPayload!
exportFramework(input: ExportFrameworkInput!): ExportFrameworkPayload!
): GenerateFrameworkStateOfApplicabilityPayload! exportFramework(input: ExportFrameworkInput!): ExportFrameworkPayload!
# Control mutations
createControl(input: CreateControlInput!): CreateControlPayload!
updateControl(input: UpdateControlInput!): UpdateControlPayload!
deleteControl(input: DeleteControlInput!): DeleteControlPayload!
createControl(input: CreateControlInput!): CreateControlPayload! updateControl(input: UpdateControlInput!): UpdateControlPayload! deleteControl(input: DeleteControlInput!): DeleteControlPayload!
# Measure mutations
createMeasure(input: CreateMeasureInput!): CreateMeasurePayload!
updateMeasure(input: UpdateMeasureInput!): UpdateMeasurePayload!
importMeasure(input: ImportMeasureInput!): ImportMeasurePayload!
deleteMeasure(input: DeleteMeasureInput!): DeleteMeasurePayload!
createMeasure(input: CreateMeasureInput!): CreateMeasurePayload! updateMeasure(input: UpdateMeasureInput!): UpdateMeasurePayload! importMeasure(input: ImportMeasureInput!): ImportMeasurePayload! deleteMeasure(input: DeleteMeasureInput!): DeleteMeasurePayload!
# Control mutations
createControlMeasureMapping(
input: CreateControlMeasureMappingInput!
): CreateControlMeasureMappingPayload!
createControlDocumentMapping(
): CreateControlMeasureMappingPayload! createControlDocumentMapping(
input: CreateControlDocumentMappingInput!
): CreateControlDocumentMappingPayload!
deleteControlMeasureMapping(
): CreateControlDocumentMappingPayload! deleteControlMeasureMapping(
input: DeleteControlMeasureMappingInput!
): DeleteControlMeasureMappingPayload!
deleteControlDocumentMapping(
): DeleteControlMeasureMappingPayload! deleteControlDocumentMapping(
input: DeleteControlDocumentMappingInput!
): DeleteControlDocumentMappingPayload!
createControlAuditMapping(
): DeleteControlDocumentMappingPayload! createControlAuditMapping(
input: CreateControlAuditMappingInput!
): CreateControlAuditMappingPayload!
deleteControlAuditMapping(
): CreateControlAuditMappingPayload! deleteControlAuditMapping(
input: DeleteControlAuditMappingInput!
): DeleteControlAuditMappingPayload!
createControlSnapshotMapping(
): DeleteControlAuditMappingPayload! createControlSnapshotMapping(
input: CreateControlSnapshotMappingInput!
): CreateControlSnapshotMappingPayload!
deleteControlSnapshotMapping(
): CreateControlSnapshotMappingPayload! deleteControlSnapshotMapping(
input: DeleteControlSnapshotMappingInput!
): DeleteControlSnapshotMappingPayload!
# Task mutations
createTask(input: CreateTaskInput!): CreateTaskPayload!
updateTask(input: UpdateTaskInput!): UpdateTaskPayload!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
assignTask(input: AssignTaskInput!): AssignTaskPayload!
unassignTask(input: UnassignTaskInput!): UnassignTaskPayload!
createTask(input: CreateTaskInput!): CreateTaskPayload! updateTask(input: UpdateTaskInput!): UpdateTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! assignTask(input: AssignTaskInput!): AssignTaskPayload! unassignTask(input: UnassignTaskInput!): UnassignTaskPayload!
# Risk mutations
createRisk(input: CreateRiskInput!): CreateRiskPayload!
updateRisk(input: UpdateRiskInput!): UpdateRiskPayload!
deleteRisk(input: DeleteRiskInput!): DeleteRiskPayload!
createRiskMeasureMapping(
createRisk(input: CreateRiskInput!): CreateRiskPayload! updateRisk(input: UpdateRiskInput!): UpdateRiskPayload! deleteRisk(input: DeleteRiskInput!): DeleteRiskPayload! createRiskMeasureMapping(
input: CreateRiskMeasureMappingInput!
): CreateRiskMeasureMappingPayload!
deleteRiskMeasureMapping(
): CreateRiskMeasureMappingPayload! deleteRiskMeasureMapping(
input: DeleteRiskMeasureMappingInput!
): DeleteRiskMeasureMappingPayload!
createRiskDocumentMapping(
input: CreateRiskDocumentMappingInput!
): CreateRiskDocumentMappingPayload!
deleteRiskDocumentMapping(
): CreateRiskDocumentMappingPayload! deleteRiskDocumentMapping(
input: DeleteRiskDocumentMappingInput!
): DeleteRiskDocumentMappingPayload!
createRiskObligationMapping(
input: CreateRiskObligationMappingInput!
): CreateRiskObligationMappingPayload!
deleteRiskObligationMapping(
): CreateRiskObligationMappingPayload! deleteRiskObligationMapping(
input: DeleteRiskObligationMappingInput!
): DeleteRiskObligationMappingPayload!
# Evidence mutations
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload!
uploadMeasureEvidence(
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload! uploadMeasureEvidence(
input: UploadMeasureEvidenceInput!
): UploadMeasureEvidencePayload!
# Vendor Compliance Report mutations
uploadVendorComplianceReport(
input: UploadVendorComplianceReportInput!
): UploadVendorComplianceReportPayload!
deleteVendorComplianceReport(
): UploadVendorComplianceReportPayload! deleteVendorComplianceReport(
input: DeleteVendorComplianceReportInput!
): DeleteVendorComplianceReportPayload!
# Vendor Business Associate Agreement mutations
uploadVendorBusinessAssociateAgreement(
input: UploadVendorBusinessAssociateAgreementInput!
): UploadVendorBusinessAssociateAgreementPayload!
updateVendorBusinessAssociateAgreement(
): UploadVendorBusinessAssociateAgreementPayload! updateVendorBusinessAssociateAgreement(
input: UpdateVendorBusinessAssociateAgreementInput!
): UpdateVendorBusinessAssociateAgreementPayload!
deleteVendorBusinessAssociateAgreement(
): UpdateVendorBusinessAssociateAgreementPayload! deleteVendorBusinessAssociateAgreement(
input: DeleteVendorBusinessAssociateAgreementInput!
): DeleteVendorBusinessAssociateAgreementPayload!
# Vendor Data Privacy Agreement mutations
uploadVendorDataPrivacyAgreement(
input: UploadVendorDataPrivacyAgreementInput!
): UploadVendorDataPrivacyAgreementPayload!
updateVendorDataPrivacyAgreement(
): UploadVendorDataPrivacyAgreementPayload! updateVendorDataPrivacyAgreement(
input: UpdateVendorDataPrivacyAgreementInput!
): UpdateVendorDataPrivacyAgreementPayload!
deleteVendorDataPrivacyAgreement(
): UpdateVendorDataPrivacyAgreementPayload! deleteVendorDataPrivacyAgreement(
input: DeleteVendorDataPrivacyAgreementInput!
): DeleteVendorDataPrivacyAgreementPayload!
# Document mutations
createDocument(input: CreateDocumentInput!): CreateDocumentPayload!
updateDocument(input: UpdateDocumentInput!): UpdateDocumentPayload!
@@ -2954,134 +2894,85 @@ type Mutation {
deleteMeeting(input: DeleteMeetingInput!): DeleteMeetingPayload!
publishDocumentVersion(
input: PublishDocumentVersionInput!
): PublishDocumentVersionPayload!
bulkPublishDocumentVersions(
): PublishDocumentVersionPayload! bulkPublishDocumentVersions(
input: BulkPublishDocumentVersionsInput!
): BulkPublishDocumentVersionsPayload!
bulkDeleteDocuments(
): BulkPublishDocumentVersionsPayload! bulkDeleteDocuments(
input: BulkDeleteDocumentsInput!
): BulkDeleteDocumentsPayload!
bulkExportDocuments(
): BulkDeleteDocumentsPayload! bulkExportDocuments(
input: BulkExportDocumentsInput!
): BulkExportDocumentsPayload!
generateDocumentChangelog(
): BulkExportDocumentsPayload! generateDocumentChangelog(
input: GenerateDocumentChangelogInput!
): GenerateDocumentChangelogPayload!
createDraftDocumentVersion(
): GenerateDocumentChangelogPayload! createDraftDocumentVersion(
input: CreateDraftDocumentVersionInput!
): CreateDraftDocumentVersionPayload!
deleteDraftDocumentVersion(
): CreateDraftDocumentVersionPayload! deleteDraftDocumentVersion(
input: DeleteDraftDocumentVersionInput!
): DeleteDraftDocumentVersionPayload!
updateDocumentVersion(
): DeleteDraftDocumentVersionPayload! updateDocumentVersion(
input: UpdateDocumentVersionInput!
): UpdateDocumentVersionPayload!
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
bulkRequestSignatures(
): UpdateDocumentVersionPayload! requestSignature(input: RequestSignatureInput!): RequestSignaturePayload! bulkRequestSignatures(
input: BulkRequestSignaturesInput!
): BulkRequestSignaturesPayload!
sendSigningNotifications(
): BulkRequestSignaturesPayload! sendSigningNotifications(
input: SendSigningNotificationsInput!
): SendSigningNotificationsPayload!
cancelSignatureRequest(
): SendSigningNotificationsPayload! cancelSignatureRequest(
input: CancelSignatureRequestInput!
): CancelSignatureRequestPayload!
exportDocumentVersionPDF(
): CancelSignatureRequestPayload! exportDocumentVersionPDF(
input: ExportDocumentVersionPDFInput!
): ExportDocumentVersionPDFPayload!
createVendorRiskAssessment(
input: CreateVendorRiskAssessmentInput!
): CreateVendorRiskAssessmentPayload!
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
createAsset(input: CreateAssetInput!): CreateAssetPayload!
updateAsset(input: UpdateAssetInput!): UpdateAssetPayload!
deleteAsset(input: DeleteAssetInput!): DeleteAssetPayload!
createDatum(input: CreateDatumInput!): CreateDatumPayload!
updateDatum(input: UpdateDatumInput!): UpdateDatumPayload!
deleteDatum(input: DeleteDatumInput!): DeleteDatumPayload!
createAudit(input: CreateAuditInput!): CreateAuditPayload!
updateAudit(input: UpdateAuditInput!): UpdateAuditPayload!
deleteAudit(input: DeleteAuditInput!): DeleteAuditPayload!
uploadAuditReport(input: UploadAuditReportInput!): UploadAuditReportPayload!
deleteAuditReport(input: DeleteAuditReportInput!): DeleteAuditReportPayload!
createAsset(input: CreateAssetInput!): CreateAssetPayload! updateAsset(input: UpdateAssetInput!): UpdateAssetPayload! deleteAsset(input: DeleteAssetInput!): DeleteAssetPayload!
createDatum(input: CreateDatumInput!): CreateDatumPayload! updateDatum(input: UpdateDatumInput!): UpdateDatumPayload! deleteDatum(input: DeleteDatumInput!): DeleteDatumPayload!
createAudit(input: CreateAuditInput!): CreateAuditPayload! updateAudit(input: UpdateAuditInput!): UpdateAuditPayload! deleteAudit(input: DeleteAuditInput!): DeleteAuditPayload! uploadAuditReport(input: UploadAuditReportInput!): UploadAuditReportPayload! deleteAuditReport(input: DeleteAuditReportInput!): DeleteAuditReportPayload!
# Nonconformity mutations
createNonconformity(
input: CreateNonconformityInput!
): CreateNonconformityPayload!
updateNonconformity(
): CreateNonconformityPayload! updateNonconformity(
input: UpdateNonconformityInput!
): UpdateNonconformityPayload!
deleteNonconformity(
): UpdateNonconformityPayload! deleteNonconformity(
input: DeleteNonconformityInput!
): DeleteNonconformityPayload!
# Obligation mutations
createObligation(input: CreateObligationInput!): CreateObligationPayload!
updateObligation(input: UpdateObligationInput!): UpdateObligationPayload!
deleteObligation(input: DeleteObligationInput!): DeleteObligationPayload!
createObligation(input: CreateObligationInput!): CreateObligationPayload! updateObligation(input: UpdateObligationInput!): UpdateObligationPayload! deleteObligation(input: DeleteObligationInput!): DeleteObligationPayload!
# Continual Improvement mutations
createContinualImprovement(
input: CreateContinualImprovementInput!
): CreateContinualImprovementPayload!
updateContinualImprovement(
): CreateContinualImprovementPayload! updateContinualImprovement(
input: UpdateContinualImprovementInput!
): UpdateContinualImprovementPayload!
deleteContinualImprovement(
): UpdateContinualImprovementPayload! deleteContinualImprovement(
input: DeleteContinualImprovementInput!
): DeleteContinualImprovementPayload!
# Processing Activity mutations
createProcessingActivity(
input: CreateProcessingActivityInput!
): CreateProcessingActivityPayload!
updateProcessingActivity(
): CreateProcessingActivityPayload! updateProcessingActivity(
input: UpdateProcessingActivityInput!
): UpdateProcessingActivityPayload!
deleteProcessingActivity(
): UpdateProcessingActivityPayload! deleteProcessingActivity(
input: DeleteProcessingActivityInput!
): DeleteProcessingActivityPayload!
# Snapshot mutations
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload!
deleteSnapshot(input: DeleteSnapshotInput!): DeleteSnapshotPayload!
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload! deleteSnapshot(input: DeleteSnapshotInput!): DeleteSnapshotPayload!
# Custom Domain mutations
createCustomDomain(
input: CreateCustomDomainInput!
): CreateCustomDomainPayload!
deleteCustomDomain(
): CreateCustomDomainPayload! deleteCustomDomain(
input: DeleteCustomDomainInput!
): DeleteCustomDomainPayload!
# SAML Configuration mutations (OWNER/ADMIN only)
# Step 1: Initiate domain verification (creates SAML config with unverified domain)
initiateDomainVerification(
input: InitiateDomainVerificationInput!
): InitiateDomainVerificationPayload!
# Step 2: Verify domain ownership via DNS TXT record
verifyDomain(input: VerifyDomainInput!): VerifyDomainPayload!
# Step 3: Configure SAML (only allowed after domain is verified)
createSAMLConfiguration(
input: CreateSAMLConfigurationInput!
): CreateSAMLConfigurationPayload!
updateSAMLConfiguration(
): CreateSAMLConfigurationPayload! updateSAMLConfiguration(
input: UpdateSAMLConfigurationInput!
): UpdateSAMLConfigurationPayload!
deleteSAMLConfiguration(
): UpdateSAMLConfigurationPayload! deleteSAMLConfiguration(
input: DeleteSAMLConfigurationInput!
): DeleteSAMLConfigurationPayload!
enableSAML(input: EnableSAMLInput!): EnableSAMLPayload!
disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!
}
): DeleteSAMLConfigurationPayload! enableSAML(input: EnableSAMLInput!): EnableSAMLPayload! disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!}
# Input Types
input GenerateFrameworkStateOfApplicabilityInput {
@@ -3629,6 +3520,7 @@ input InviteUserInput {
organizationId: ID!
email: String!
fullName: String!
role: MembershipRole!
createPeople: Boolean!
}
@@ -3645,6 +3537,12 @@ input RemoveMemberInput {
memberId: ID!
}
input UpdateMembershipInput {
organizationId: ID!
memberId: ID!
role: MembershipRole!
}
input CreateControlInput {
frameworkId: ID!
sectionTitle: String!
@@ -4214,6 +4112,10 @@ type RemoveMemberPayload {
deletedMemberId: ID!
}
type UpdateMembershipPayload {
membership: Membership!
}
input VendorRiskAssessmentOrder {
field: VendorRiskAssessmentOrderField!
direction: OrderDirection!
@@ -4264,7 +4166,7 @@ type DeleteMeasurePayload {
deletedMeasureId: ID!
}
type DocumentVersion implements Node {
type DocumentVersion implements Node @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.DocumentVersion") {
id: ID!
document: Document! @goField(forceResolver: true)
status: DocumentStatus!
@@ -4549,7 +4451,7 @@ type DeleteAssetPayload {
deletedAssetId: ID!
}
type Datum implements Node {
type Datum implements Node @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.Datum") {
id: ID!
snapshotId: ID
name: String!

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,33 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type Control struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"-"`
SectionTitle string `json:"sectionTitle"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Status coredata.ControlStatus `json:"status"`
ExclusionJustification *string `json:"exclusionJustification,omitempty"`
Framework *Framework `json:"framework"`
Measures *MeasureConnection `json:"measures"`
Documents *DocumentConnection `json:"documents"`
Audits *AuditConnection `json:"audits"`
Snapshots *SnapshotConnection `json:"snapshots"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Control) IsNode() {}
func (c Control) GetID() gid.GID { return c.ID }
type (
ControlOrderBy OrderBy[coredata.ControlOrderField]
@@ -36,42 +44,42 @@ type (
func NewControlConnection(
p *page.Page[*coredata.Control, coredata.ControlOrderField],
parentType any,
resolver any,
parentID gid.GID,
filters *coredata.ControlFilter,
filter *coredata.ControlFilter,
) *ControlConnection {
var edges = make([]*ControlEdge, len(p.Data))
for i := range edges {
edges[i] = NewControlEdge(p.Data[i], p.Cursor.OrderBy.Field)
edges := make([]*ControlEdge, len(p.Data))
for i, control := range p.Data {
edges[i] = NewControlEdge(control, p.Cursor.OrderBy.Field)
}
return &ControlConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: parentType,
Resolver: resolver,
ParentID: parentID,
Filters: filters,
Filters: filter,
}
}
func NewControlEdge(c *coredata.Control, orderBy coredata.ControlOrderField) *ControlEdge {
return &ControlEdge{
Cursor: c.CursorKey(orderBy),
Node: NewControl(c),
}
}
func NewControl(c *coredata.Control) *Control {
func NewControl(control *coredata.Control) *Control {
return &Control{
ID: c.ID,
SectionTitle: c.SectionTitle,
Name: c.Name,
Description: c.Description,
Status: c.Status,
ExclusionJustification: c.ExclusionJustification,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
ID: control.ID,
OrganizationID: control.OrganizationID,
SectionTitle: control.SectionTitle,
Name: control.Name,
Description: control.Description,
Status: control.Status,
ExclusionJustification: control.ExclusionJustification,
CreatedAt: control.CreatedAt,
UpdatedAt: control.UpdatedAt,
}
}
func NewControlEdge(control *coredata.Control, orderField coredata.ControlOrderField) *ControlEdge {
return &ControlEdge{
Node: NewControl(control),
Cursor: control.CursorKey(orderField),
}
}

View File

@@ -15,11 +15,29 @@
package types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type Datum struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"-"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
Name string `json:"name"`
DataClassification coredata.DataClassification `json:"dataClassification"`
Owner *People `json:"owner"`
Vendors *VendorConnection `json:"vendors"`
Organization *Organization `json:"organization"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Datum) IsNode() {}
func (this Datum) GetID() gid.GID { return this.ID }
type (
DatumOrderBy OrderBy[coredata.DatumOrderField]
@@ -58,12 +76,12 @@ func NewDataConnection(
func NewDatum(d *coredata.Datum) *Datum {
return &Datum{
ID: d.ID,
OrganizationID: d.OrganizationID,
Name: d.Name,
SnapshotID: d.SnapshotID,
DataClassification: d.DataClassification,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
Organization: &Organization{ID: d.OrganizationID},
}
}

View File

@@ -15,14 +15,40 @@
package types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
DocumentVersion struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"-"`
Document *Document `json:"document"`
Status coredata.DocumentStatus `json:"status"`
Version int `json:"version"`
Content string `json:"content"`
Changelog string `json:"changelog"`
Title string `json:"title"`
Classification coredata.DocumentClassification `json:"classification"`
Owner *People `json:"owner"`
Signatures *DocumentVersionSignatureConnection `json:"signatures"`
PublishedAt *time.Time `json:"publishedAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
DocumentVersionOrderBy OrderBy[coredata.DocumentVersionOrderField]
)
func (DocumentVersion) IsNode() {}
func (d DocumentVersion) GetID() gid.GID {
return d.ID
}
func NewDocumentVersionConnection(page *page.Page[*coredata.DocumentVersion, coredata.DocumentVersionOrderField]) *DocumentVersionConnection {
edges := make([]*DocumentVersionEdge, len(page.Data))
for i, documentVersion := range page.Data {
@@ -55,6 +81,7 @@ func NewDocumentVersionEdge(documentVersion *coredata.DocumentVersion, orderBy c
func NewDocumentVersion(documentVersion *coredata.DocumentVersion) *DocumentVersion {
return &DocumentVersion{
ID: documentVersion.ID,
OrganizationID: documentVersion.OrganizationID,
Version: documentVersion.VersionNumber,
Title: documentVersion.Title,
Content: documentVersion.Content,

View File

@@ -59,6 +59,7 @@ func NewProcessingActivity(par *coredata.ProcessingActivity) *ProcessingActivity
return &ProcessingActivity{
ID: par.ID,
SnapshotID: par.SnapshotID,
SourceID: par.SourceID,
Name: par.Name,
Purpose: par.Purpose,
DataSubjectCategory: par.DataSubjectCategory,

View File

@@ -30,6 +30,7 @@ func NewTrustCenterAccess(tca *coredata.TrustCenterAccess) *TrustCenterAccess {
HasAcceptedNonDisclosureAgreement: tca.HasAcceptedNonDisclosureAgreement,
CreatedAt: tca.CreatedAt,
UpdatedAt: tca.UpdatedAt,
LastTokenExpiresAt: tca.LastTokenExpiresAt,
}
}

View File

@@ -36,6 +36,7 @@ type (
TrustCenterDocumentAccess struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"-"`
Active bool `json:"active"`
Requested bool `json:"requested"`
CreatedAt time.Time `json:"createdAt"`
@@ -55,6 +56,7 @@ type (
func NewTrustCenterDocumentAccess(tcda *coredata.TrustCenterDocumentAccess) *TrustCenterDocumentAccess {
return &TrustCenterDocumentAccess{
ID: tcda.ID,
OrganizationID: tcda.OrganizationID,
Active: tcda.Active,
Requested: tcda.Requested,
CreatedAt: tcda.CreatedAt,

View File

@@ -3,6 +3,10 @@
package types
import (
"bytes"
"fmt"
"io"
"strconv"
"time"
"github.com/99designs/gqlgen/graphql"
@@ -174,25 +178,6 @@ type ContinualImprovementFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
type Control struct {
ID gid.GID `json:"id"`
SectionTitle string `json:"sectionTitle"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Status coredata.ControlStatus `json:"status"`
ExclusionJustification *string `json:"exclusionJustification,omitempty"`
Framework *Framework `json:"framework"`
Measures *MeasureConnection `json:"measures"`
Documents *DocumentConnection `json:"documents"`
Audits *AuditConnection `json:"audits"`
Snapshots *SnapshotConnection `json:"snapshots"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Control) IsNode() {}
func (this Control) GetID() gid.GID { return this.ID }
type ControlEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Control `json:"node"`
@@ -685,21 +670,6 @@ type DNSRecordInstruction struct {
Purpose string `json:"purpose"`
}
type Datum struct {
ID gid.GID `json:"id"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
Name string `json:"name"`
DataClassification coredata.DataClassification `json:"dataClassification"`
Owner *People `json:"owner"`
Vendors *VendorConnection `json:"vendors"`
Organization *Organization `json:"organization"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Datum) IsNode() {}
func (this Datum) GetID() gid.GID { return this.ID }
type DatumEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Datum `json:"node"`
@@ -1087,25 +1057,6 @@ type DocumentFilter struct {
Query *string `json:"query,omitempty"`
}
type DocumentVersion struct {
ID gid.GID `json:"id"`
Document *Document `json:"document"`
Status coredata.DocumentStatus `json:"status"`
Version int `json:"version"`
Content string `json:"content"`
Changelog string `json:"changelog"`
Title string `json:"title"`
Classification coredata.DocumentClassification `json:"classification"`
Owner *People `json:"owner"`
Signatures *DocumentVersionSignatureConnection `json:"signatures"`
PublishedAt *time.Time `json:"publishedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (DocumentVersion) IsNode() {}
func (this DocumentVersion) GetID() gid.GID { return this.ID }
type DocumentVersionConnection struct {
Edges []*DocumentVersionEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
@@ -1297,7 +1248,7 @@ type Invitation struct {
ID gid.GID `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Role coredata.Role `json:"role"`
Role coredata.MembershipRole `json:"role"`
Status coredata.InvitationStatus `json:"status"`
ExpiresAt time.Time `json:"expiresAt"`
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
@@ -1323,10 +1274,11 @@ type InvitationOrder struct {
}
type InviteUserInput struct {
OrganizationID gid.GID `json:"organizationId"`
Email string `json:"email"`
FullName string `json:"fullName"`
CreatePeople bool `json:"createPeople"`
OrganizationID gid.GID `json:"organizationId"`
Email string `json:"email"`
FullName string `json:"fullName"`
Role coredata.MembershipRole `json:"role"`
CreatePeople bool `json:"createPeople"`
}
type InviteUserPayload struct {
@@ -1383,7 +1335,7 @@ type Membership struct {
ID gid.GID `json:"id"`
UserID gid.GID `json:"userID"`
OrganizationID gid.GID `json:"organizationID"`
Role coredata.Role `json:"role"`
Role coredata.MembershipRole `json:"role"`
FullName string `json:"fullName"`
EmailAddress string `json:"emailAddress"`
AuthMethod coredata.UserAuthMethod `json:"authMethod"`
@@ -1855,9 +1807,6 @@ type TrustCenterFile struct {
Organization *Organization `json:"organization"`
}
func (TrustCenterFile) IsNode() {}
func (this TrustCenterFile) GetID() gid.GID { return this.ID }
type TrustCenterFileEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *TrustCenterFile `json:"node"`
@@ -2014,6 +1963,16 @@ type UpdateMeetingPayload struct {
Meeting *Meeting `json:"meeting"`
}
type UpdateMembershipInput struct {
OrganizationID gid.GID `json:"organizationId"`
MemberID gid.GID `json:"memberId"`
Role coredata.MembershipRole `json:"role"`
}
type UpdateMembershipPayload struct {
Membership *Membership `json:"membership"`
}
type UpdateNonconformityInput struct {
ID gid.GID `json:"id"`
ReferenceID *string `json:"referenceId,omitempty"`
@@ -2551,3 +2510,62 @@ type Viewer struct {
User *User `json:"user"`
Organizations *OrganizationConnection `json:"organizations"`
}
type Role string
const (
RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN"
RoleViewer Role = "VIEWER"
RoleFull Role = "FULL"
)
var AllRole = []Role{
RoleOwner,
RoleAdmin,
RoleViewer,
RoleFull,
}
func (e Role) IsValid() bool {
switch e {
case RoleOwner, RoleAdmin, RoleViewer, RoleFull:
return true
}
return false
}
func (e Role) String() string {
return string(e)
}
func (e *Role) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = Role(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid Role", str)
}
return nil
}
func (e Role) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *Role) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e Role) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -58,9 +58,9 @@ func NewServer(cfg Config) (*Server, error) {
router.Post("/invitations/accept", AcceptInvitationHandler(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
router.Get("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, ListUserAPIKeysHandler(cfg.Auth, cfg.Authz)))
router.Post("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, CreateUserAPIKeyHandler(cfg.Auth)))
router.Post("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, CreateUserAPIKeyHandler(cfg.Auth, cfg.Authz)))
router.Get("/api-keys/{id}", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, GetUserAPIKeyHandler(cfg.Auth)))
router.Put("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, UpdateUserAPIKeyHandler(cfg.Auth)))
router.Put("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, UpdateUserAPIKeyHandler(cfg.Auth, cfg.Authz)))
router.Delete("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, DeleteUserAPIKeyHandler(cfg.Auth)))
router.Get("/saml/login/{samlConfigID}", SAMLLoginHandler(cfg.SAML, cfg.Auth, cfg.Logger))

View File

@@ -22,6 +22,7 @@ import (
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
@@ -44,7 +45,7 @@ type (
}
)
func CreateUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
func CreateUserAPIKeyHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
@@ -92,6 +93,24 @@ func CreateUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
})
return
}
// Check if user is an OWNER for this organization
tenantAuthzSvc := authzSvc.WithTenant(orgID.TenantID())
role, err := tenantAuthzSvc.GetUserRoleInOrganization(ctx, user.ID, orgID)
if err != nil {
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
"error": "user does not have access to this organization",
})
return
}
if role != coredata.MembershipRoleOwner {
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
"error": "only owners can create API keys for this organization",
})
return
}
orgInputs[i] = authsvc.UserAPIKeyOrganizationRequest{
OrganizationID: orgID,
Role: coredata.APIRole(org.Role),

View File

@@ -18,11 +18,11 @@ import (
"fmt"
"net/http"
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/kit/httpserver"
)
type (
@@ -106,9 +106,21 @@ func ListOrganizationsHandler(authSvc *authsvc.Service, authzSvc *authz.Service)
user := UserFromContext(ctx)
sess := SessionFromContext(ctx)
organizations, err := authzSvc.GetAllUserOrganizations(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot list organizations for user: %w", err))
var organizations coredata.Organizations
var err error
roleFilter := r.URL.Query().Get("role")
if roleFilter != "" {
role := coredata.MembershipRole(roleFilter)
organizations, err = authzSvc.GetUserOrganizationsWithRole(ctx, user.ID, role)
if err != nil {
panic(fmt.Errorf("cannot list organizations for user with role: %w", err))
}
} else {
organizations, err = authzSvc.GetAllUserOrganizations(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot list organizations for user: %w", err))
}
}
orgIDs := make([]gid.GID, len(organizations))

View File

@@ -22,6 +22,7 @@ import (
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
@@ -32,7 +33,7 @@ type UpdateUserAPIKeyRequest struct {
Organizations []UserAPIKeyOrganizationMembershipRequest `json:"organizations"`
}
func UpdateUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
func UpdateUserAPIKeyHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
@@ -100,6 +101,22 @@ func UpdateUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
return
}
// Check if user is an OWNER for this organization
tenantAuthzSvc := authzSvc.WithTenant(orgID.TenantID())
role, err := tenantAuthzSvc.GetUserRoleInOrganization(ctx, user.ID, orgID)
if err != nil {
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
"error": "user does not have access to this organization",
})
return
}
if role != coredata.MembershipRoleOwner {
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
"error": "only owners can update API keys for this organization",
})
return
}
orgInputs[i] = authsvc.UserAPIKeyOrganizationRequest{
OrganizationID: orgID,
Role: coredata.APIRole(org.Role),

121
pkg/server/authz/authz.go Normal file
View File

@@ -0,0 +1,121 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package authz
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/server/session"
)
type Config struct {
Auth *authsvc.Service
Authz *authz.Service
Logger *log.Logger
CookieName string
CookieSecret string
CookieSecure bool
}
type Server struct {
router *chi.Mux
}
type ctxKey struct{ name string }
var (
userContextKey = &ctxKey{name: "user"}
)
func NewServer(cfg Config) (*Server, error) {
router := chi.NewRouter()
// Apply authentication middleware to all routes
router.Use(requireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
router.Get("/{organizationID}/permissions", PermissionsHandler(cfg.Authz, UserFromContext, cfg.Logger))
return &Server{
router: router,
}, nil
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
// requireAuth is a middleware that requires authentication
func requireAuth(
authService *authsvc.Service,
authzService *authz.Service,
cookieName string,
cookieSecret string,
cookieSecure bool,
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
sessionAuthCfg := session.AuthConfig{
CookieName: cookieName,
CookieSecret: cookieSecret,
CookieSecure: cookieSecure,
}
errorHandler := session.ErrorHandler{
OnCookieError: func(err error) {
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
},
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
session.ClearCookie(w, authCfg)
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
},
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
session.ClearCookie(w, authCfg)
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
},
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
session.ClearCookie(w, authCfg)
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
},
OnTenantError: func(err error) {
panic(fmt.Errorf("cannot list tenants for user: %w", err))
},
}
authResult := session.TryAuth(ctx, w, r, authService, authzService, sessionAuthCfg, errorHandler)
if authResult == nil {
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
return
}
ctx = context.WithValue(ctx, userContextKey, authResult.User)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func UserFromContext(ctx context.Context) *coredata.User {
user, _ := ctx.Value(userContextKey).(*coredata.User)
return user
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package authz
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
// PermissionsHandler returns permissions for the current user's role in an organization
// It uses the centralized permissions map
func PermissionsHandler(
authzService *authz.Service,
userFromContext func(ctx context.Context) *coredata.User,
logger *log.Logger,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orgIDStr := chi.URLParam(r, "organizationID")
if orgIDStr == "" {
http.Error(w, "organizationID parameter required", http.StatusBadRequest)
return
}
orgID, err := gid.ParseGID(orgIDStr)
if err != nil {
http.Error(w, "invalid organizationID", http.StatusBadRequest)
return
}
user := userFromContext(ctx)
if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
tenantAuthzSvc := authzService.WithTenant(orgID.TenantID())
memberRole, err := tenantAuthzSvc.GetUserRoleInOrganization(ctx, user.ID, orgID)
if err != nil {
http.Error(w, fmt.Sprintf("cannot get user role: %v", err), http.StatusInternalServerError)
return
}
userRole := authz.Role(memberRole.String())
permissions := authz.GetPermissionsByRole(userRole)
response := map[string]any{
"permissions": permissions,
"role": memberRole.String(),
}
httpserver.RenderJSON(w, http.StatusOK, response)
}
}

View File

@@ -29,6 +29,15 @@ func Unauthorized() *gqlerror.Error {
}
}
func Forbidden(err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
Extensions: map[string]any{
"code": "FORBIDDEN",
},
}
}
func AuthenticationRequired(details map[string]any) *gqlerror.Error {
extensions := map[string]any{"code": "AUTHENTICATION_REQUIRED"}
maps.Copy(extensions, details)

View File

@@ -77,6 +77,11 @@ func RecoverFunc(ctx context.Context, err any) error {
return Unauthorized()
}
var permissionDeniedErr *authz.PermissionDeniedError
if errTyped, ok := err.(error); ok && errors.As(errTyped, &permissionDeniedErr) {
return Forbidden(permissionDeniedErr)
}
logger := httpserver.LoggerFromContext(ctx)
logger.Error("resolver panic", log.Any("error", err), log.Any("stack", string(debug.Stack())))

View File

@@ -35,6 +35,7 @@ import (
"go.probo.inc/probo/pkg/server/api"
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
auth_server "go.probo.inc/probo/pkg/server/auth"
authz_server "go.probo.inc/probo/pkg/server/authz"
"go.probo.inc/probo/pkg/server/trust"
"go.probo.inc/probo/pkg/server/web"
trust_pkg "go.probo.inc/probo/pkg/trust"
@@ -65,6 +66,7 @@ type Server struct {
webServer *web.Server
trustServer *trust.Server
authServer *auth_server.Server
authzServer *authz_server.Server
router *chi.Mux
extraHeaderFields map[string]string
proboService *probo.Service
@@ -118,6 +120,18 @@ func NewServer(cfg Config) (*Server, error) {
return nil, err
}
authzServer, err := authz_server.NewServer(authz_server.Config{
Auth: cfg.Auth,
Authz: cfg.Authz,
Logger: cfg.Logger.Named("authz"),
CookieName: cfg.ConsoleAuth.CookieName,
CookieSecret: cfg.ConsoleAuth.CookieSecret,
CookieSecure: cfg.ConsoleAuth.CookieSecure,
})
if err != nil {
return nil, err
}
router := chi.NewRouter()
server := &Server{
@@ -125,6 +139,7 @@ func NewServer(cfg Config) (*Server, error) {
webServer: webServer,
trustServer: trustServer,
authServer: authServer,
authzServer: authzServer,
router: router,
extraHeaderFields: cfg.ExtraHeaderFields,
proboService: cfg.Probo,
@@ -139,6 +154,7 @@ func NewServer(cfg Config) (*Server, error) {
func (s *Server) setupRoutes() {
s.router.Mount("/api", s.apiServer)
s.router.Mount("/connect", s.authServer)
s.router.Mount("/authz", s.authzServer)
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
r.Use(s.loadTrustCenterBySlugOrID)

View File

@@ -179,17 +179,18 @@ func (s TrustCenterAccessService) Request(
return fmt.Errorf("invalid email address")
}
access = &coredata.TrustCenterAccess{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.TrustCenterAccessEntityType),
TenantID: s.svc.scope.GetTenantID(),
TrustCenterID: req.TrustCenterID,
Email: req.Email,
Name: *req.Name,
Active: false,
HasAcceptedNonDisclosureAgreement: false,
CreatedAt: now,
UpdatedAt: now,
}
access = &coredata.TrustCenterAccess{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.TrustCenterAccessEntityType),
OrganizationID: organizationID,
TenantID: s.svc.scope.GetTenantID(),
TrustCenterID: req.TrustCenterID,
Email: req.Email,
Name: *req.Name,
Active: false,
HasAcceptedNonDisclosureAgreement: false,
CreatedAt: now,
UpdatedAt: now,
}
if err := access.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert trust center access: %w", err)
@@ -206,19 +207,19 @@ func (s TrustCenterAccessService) Request(
newReportIDs := filterExistingIDs(reportIDs, existingReportIDs)
newTrustCenterFileIDs := filterExistingIDs(trustCenterFileIDs, existingTrustCenterFileIDs)
var accesses coredata.TrustCenterDocumentAccesses
var accesses coredata.TrustCenterDocumentAccesses
if err := accesses.BulkInsertDocumentAccesses(ctx, tx, s.svc.scope, access.ID, newDocumentIDs, true, now); err != nil {
return fmt.Errorf("cannot bulk insert trust center document accesses: %w", err)
}
if err := accesses.BulkInsertDocumentAccesses(ctx, tx, s.svc.scope, access.ID, access.OrganizationID, newDocumentIDs, true, now); err != nil {
return fmt.Errorf("cannot bulk insert trust center document accesses: %w", err)
}
if err := accesses.BulkInsertReportAccesses(ctx, tx, s.svc.scope, access.ID, newReportIDs, true, now); err != nil {
return fmt.Errorf("cannot bulk insert trust center report accesses: %w", err)
}
if err := accesses.BulkInsertReportAccesses(ctx, tx, s.svc.scope, access.ID, access.OrganizationID, newReportIDs, true, now); err != nil {
return fmt.Errorf("cannot bulk insert trust center report accesses: %w", err)
}
if err := accesses.BulkInsertTrustCenterFileAccesses(ctx, tx, s.svc.scope, access.ID, newTrustCenterFileIDs, true, now); err != nil {
return fmt.Errorf("cannot bulk insert trust center file accesses: %w", err)
}
if err := accesses.BulkInsertTrustCenterFileAccesses(ctx, tx, s.svc.scope, access.ID, access.OrganizationID, newTrustCenterFileIDs, true, now); err != nil {
return fmt.Errorf("cannot bulk insert trust center file accesses: %w", err)
}
return nil
})