Rewrite identity and access management

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-03 19:23:15 +01:00
parent 4ed3f5a067
commit 74fc3b8cd1
201 changed files with 32895 additions and 23649 deletions

View File

@@ -22,85 +22,84 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"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"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/securecookie"
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
serverauth "go.probo.inc/probo/pkg/server/auth"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/session"
"go.probo.inc/probo/pkg/statelesstoken"
)
type (
AuthConfig struct {
CookieName string
CookieDomain string
SessionDuration time.Duration
CookieSecret string
CookieSecure bool
}
Resolver struct {
proboSvc *probo.Service
authSvc *auth.Service
authzSvc *authz.Service
samlSvc *auth.SAMLService
authCfg AuthConfig
probo *probo.Service
iam *iam.Service
customDomainCname string
schema *ast.Schema
}
)
func ensureAuthenticated(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
identity := connect_v1.UserFromContext(ctx)
if identity == nil {
return func(ctx context.Context) *graphql.Response {
return &graphql.Response{
Errors: gqlerror.List{
gqlutils.Unauthorized(),
},
}
}
}
ctxKey struct{ name string }
)
var (
sessionContextKey = &ctxKey{name: "session"}
)
func SessionFromContext(ctx context.Context) *coredata.Session {
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
return session
}
func UserFromContext(ctx context.Context) *coredata.User {
return serverauth.UserFromContext(ctx)
}
func UserAPIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
return serverauth.UserAPIKeyFromContext(ctx)
return next(ctx)
}
func NewMux(
logger *log.Logger,
proboSvc *probo.Service,
authSvc *auth.Service,
authzSvc *authz.Service,
authCfg AuthConfig,
iamSvc *iam.Service,
cookieConfig securecookie.Config,
tokenSecret string,
connectorRegistry *connector.ConnectorRegistry,
safeRedirect *saferedirect.SafeRedirect,
baseURL *baseurl.BaseURL,
customDomainCname string,
samlSvc *auth.SAMLService,
) *chi.Mux {
r := chi.NewMux()
safeRedirect := &saferedirect.SafeRedirect{AllowedHost: baseURL.Host()}
sessionMiddleware := connect_v1.NewSessionMiddleware(iamSvc, cookieConfig)
apiKeyMiddleware := connect_v1.NewAPIKeyMiddleware(iamSvc)
r.Use(sessionMiddleware)
r.Use(apiKeyMiddleware)
config := schema.Config{
Resolvers: &Resolver{
probo: proboSvc,
iam: iamSvc,
customDomainCname: customDomainCname,
},
}
es := schema.NewExecutableSchema(config)
h := gqlutils.NewHandler(es, logger)
h.AroundOperations(ensureAuthenticated)
r.Handle("/graphql", h)
r.Get(
"/documents/signing-requests",
func(w http.ResponseWriter, r *http.Request) {
@@ -111,7 +110,7 @@ func NewMux(
}
token = strings.TrimPrefix(token, "Bearer ")
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](tokenSecret, probo.TokenTypeSigningRequest, token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
@@ -139,7 +138,7 @@ func NewMux(
return
}
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](tokenSecret, probo.TokenTypeSigningRequest, token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
@@ -194,7 +193,7 @@ func NewMux(
}
token = strings.TrimPrefix(token, "Bearer ")
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](tokenSecret, probo.TokenTypeSigningRequest, token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
@@ -217,7 +216,7 @@ func NewMux(
},
)
r.Get("/connectors/initiate", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) {
r.Get("/connectors/initiate", func(w http.ResponseWriter, r *http.Request) {
provider := r.URL.Query().Get("provider")
if provider != "SLACK" {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider"))
@@ -229,7 +228,23 @@ func NewMux(
panic(fmt.Errorf("cannot parse organization id: %w", err))
}
_ = GetTenantService(r.Context(), proboSvc, organizationID.TenantID())
identity := connect_v1.UserFromContext(r.Context())
apiKey := connect_v1.APIKeyFromContext(r.Context())
if identity == nil {
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
return
}
var credentialID *gid.GID
if apiKey != nil {
credentialID = &apiKey.ID
}
// Ensure the actor (and optional API key) can access this organization.
if err := iamSvc.AccessManagementService.Authorize(r.Context(), identity.ID, credentialID, organizationID, iam.ActionGet); err != nil {
httpserver.RenderError(w, http.StatusForbidden, err)
return
}
redirectURL, err := connectorRegistry.Initiate(r.Context(), provider, organizationID, r)
if err != nil {
@@ -239,7 +254,7 @@ func NewMux(
// Allow external redirects for Slack OAuth only for now
slackSafeRedirect := &saferedirect.SafeRedirect{AllowedHost: "slack.com"}
slackSafeRedirect.Redirect(w, r, redirectURL, "/", http.StatusSeeOther)
}))
})
r.Get("/connectors/complete", func(w http.ResponseWriter, r *http.Request) {
provider := r.URL.Query().Get("provider")
@@ -288,146 +303,16 @@ func NewMux(
if continueURL != "" {
safeRedirect.Redirect(w, r, continueURL, "/", http.StatusSeeOther)
} else {
redirectURL := fmt.Sprintf("/organizations/%s", organizationID.String())
redirectURL := baseURL.WithPath("/organizations/" + organizationID.String()).MustString()
safeRedirect.Redirect(w, r, redirectURL, "/", http.StatusSeeOther)
}
})
r.Get("/", playground.Handler("GraphQL", "/api/console/v1/query"))
r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, samlSvc, authCfg, customDomainCname))
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
// 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(
transport.MultipartForm{
MaxMemory: 32 * mb,
MaxUploadSize: 50 * mb,
},
)
srv.Use(extension.Introspection{})
srv.Use(gqlutils.NewTracingExtension(logger))
srv.SetRecoverFunc(gqlutils.RecoverFunc)
srv.AroundOperations(
func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
user := UserFromContext(ctx)
if user == nil {
return func(ctx context.Context) *graphql.Response {
return &graphql.Response{
Errors: gqlerror.List{
&gqlerror.Error{
Message: "authentication required",
Extensions: map[string]any{
"code": "UNAUTHENTICATED",
},
},
},
}
}
}
return next(ctx)
},
)
return WithSession(authSvc, authzSvc, authCfg, srv.ServeHTTP)
}
func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if authCtx := serverauth.AuthenticateWithAPIKey(ctx, r, authSvc, authzSvc); authCtx != nil {
next(w, r.WithContext(authCtx))
return
}
sessionAuthCfg := session.AuthConfig{
CookieName: authCfg.CookieName,
CookieSecret: authCfg.CookieSecret,
CookieSecure: authCfg.CookieSecure,
}
errorHandler := session.ErrorHandler{
OnCookieError: func(err error) {
panic(fmt.Errorf("cannot get session: %w", err))
},
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
session.ClearCookie(w, authCfg)
},
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
session.ClearCookie(w, authCfg)
},
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
session.ClearCookie(w, authCfg)
},
OnTenantError: func(err error) {
panic(fmt.Errorf("cannot list tenants for user: %w", err))
},
}
authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
if authResult == nil {
next(w, r)
return
}
ctx = context.WithValue(ctx, sessionContextKey, authResult.Session)
ctx = context.WithValue(ctx, serverauth.UserContextKey, authResult.User)
ctx = context.WithValue(ctx, serverauth.UserTenantContextKey, &serverauth.UserTenantAccess{
TenantIDs: authResult.TenantIDs,
AuthErrors: authResult.AuthErrors,
})
next(w, r.WithContext(ctx))
// Update session after the handler completes
if _, err := authSvc.UpdateSession(ctx, authResult.Session.ID); err != nil {
panic(fmt.Errorf("cannot update session: %w", err))
}
}
}
func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *probo.TenantService {
return GetTenantService(ctx, r.proboSvc, tenantID)
}
func (r *Resolver) AuthzService(ctx context.Context, tenantID gid.TenantID) *authz.TenantAuthzService {
return GetTenantAuthzService(ctx, r.authzSvc, tenantID)
}
func (r *Resolver) AuthService(ctx context.Context, tenantID gid.TenantID) *auth.TenantAuthService {
return GetTenantAuthService(ctx, r.authSvc, tenantID)
return GetTenantService(ctx, r.probo, tenantID)
}
func UnwrapOmittable[T any](field graphql.Omittable[T]) *T {
@@ -439,26 +324,19 @@ func UnwrapOmittable[T any](field graphql.Omittable[T]) *T {
}
func GetTenantService(ctx context.Context, proboSvc *probo.Service, tenantID gid.TenantID) *probo.TenantService {
serverauth.RequireTenantAccess(ctx, tenantID)
return proboSvc.WithTenant(tenantID)
}
func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantID gid.TenantID) *authz.TenantAuthzService {
serverauth.RequireTenantAccess(ctx, tenantID)
return authzSvc.WithTenant(tenantID)
}
func (r *Resolver) MustBeAuthorized(ctx context.Context, entityID gid.GID, action iam.Action) {
user := connect_v1.UserFromContext(ctx)
apiKey := connect_v1.APIKeyFromContext(ctx)
func GetTenantAuthService(ctx context.Context, authSvc *auth.Service, tenantID gid.TenantID) *auth.TenantAuthService {
serverauth.RequireTenantAccess(ctx, tenantID)
return authSvc.WithTenant(tenantID)
}
var credentialID *gid.GID
if apiKey != nil {
credentialID = &apiKey.ID
}
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)
err := r.iam.AccessManagementService.Authorize(ctx, user.ID, credentialID, entityID, action)
if err != nil {
panic(err)
}

View File

@@ -34,15 +34,6 @@ type PageInfo {
endCursor: CursorKey
}
# Roles
enum Role {
OWNER
ADMIN
VIEWER
AUDITOR
FULL
}
# Enums
enum OrderDirection
@goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") {
@@ -83,31 +74,6 @@ enum PeopleKind @goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleKind") {
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindServiceAccount")
}
enum InvitationStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") {
PENDING
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending")
ACCEPTED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted")
EXPIRED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired")
}
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")
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer")
AUDITOR
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor")
}
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")
@@ -143,26 +109,6 @@ enum AuditState @goModel(model: "go.probo.inc/probo/pkg/coredata.AuditState") {
OUTDATED @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateOutdated")
}
enum SAMLEnforcementPolicy
@goModel(model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy") {
OFF @goEnum(value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff")
OPTIONAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
)
REQUIRED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
)
}
enum UserAuthMethod
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserAuthMethod") {
PASSWORD
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodPassword")
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodSAML")
}
enum TrustCenterVisibility
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility") {
NONE
@@ -226,8 +172,7 @@ enum ObligationStatus
enum ObligationType
@goModel(model: "go.probo.inc/probo/pkg/coredata.ObligationType") {
LEGAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeLegal")
LEGAL @goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeLegal")
CONTRACTUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeContractual")
}
@@ -269,17 +214,11 @@ enum ContinualImprovementPriority
}
enum RightsRequestType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.RightsRequestType"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.RightsRequestType") {
ACCESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeAccess"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeAccess")
DELETION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeDeletion"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeDeletion")
PORTABILITY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypePortability"
@@ -287,21 +226,13 @@ enum RightsRequestType
}
enum RightsRequestState
@goModel(
model: "go.probo.inc/probo/pkg/coredata.RightsRequestState"
) {
TODO
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateTodo"
)
@goModel(model: "go.probo.inc/probo/pkg/coredata.RightsRequestState") {
TODO @goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateTodo")
IN_PROGRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateInProgress"
)
DONE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateDone"
)
DONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateDone")
}
enum ProcessingActivitySpecialOrCriminalDatum
@@ -429,9 +360,7 @@ enum DataProtectionImpactAssessmentResidualRisk
}
enum ProcessingActivityRole
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityRole"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityRole") {
CONTROLLER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityRoleController"
@@ -443,12 +372,6 @@ enum ProcessingActivityRole
}
# Order Field Enums
enum UserOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserOrderField") {
CREATED_AT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserOrderFieldCreatedAt")
}
enum PeopleOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleOrderField") {
FULL_NAME
@@ -1110,9 +1033,7 @@ enum ContinualImprovementOrderField
}
enum RightsRequestOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.RightsRequestOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.RightsRequestOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RightsRequestOrderFieldCreatedAt"
@@ -1259,57 +1180,7 @@ enum SnapshotOrderField
TYPE @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType")
}
enum MembershipOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") {
FULL_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName"
)
EMAIL_ADDRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress"
)
ROLE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldCreatedAt"
)
}
enum InvitationOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") {
FULL_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldFullName"
)
EMAIL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail")
ROLE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt"
)
EXPIRES_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldExpiresAt"
)
ACCEPTED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldAcceptedAt"
)
}
# Input Types
input UserOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.UserOrderBy"
) {
direction: OrderDirection!
field: UserOrderField!
}
input PeopleOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.PeopleOrderBy"
@@ -1531,23 +1402,6 @@ input SnapshotOrder
field: SnapshotOrderField!
}
input MembershipOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MembershipOrderBy"
) {
direction: OrderDirection!
field: MembershipOrderField!
}
input InvitationOrder {
direction: OrderDirection!
field: InvitationOrderField!
}
input InvitationFilter {
statuses: [InvitationStatus!]
}
input DocumentVersionFilter {
status: DocumentStatus
}
@@ -1595,7 +1449,6 @@ input ContinualImprovementFilter {
snapshotId: ID
}
input ProcessingActivityFilter {
snapshotId: ID
}
@@ -1655,23 +1508,6 @@ type Organization implements Node {
headquarterAddress: String
context: OrganizationContext @goField(forceResolver: true)
memberships(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: MembershipOrder
): MembershipConnection! @goField(forceResolver: true)
invitations(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: InvitationOrder
filter: InvitationFilter
): InvitationConnection! @goField(forceResolver: true)
slackConnections(
first: Int
after: CursorKey
@@ -1874,44 +1710,10 @@ type Organization implements Node {
customDomain: CustomDomain @goField(forceResolver: true)
samlConfigurations: [SAMLConfiguration!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type User implements Node {
id: ID!
fullName: String!
email: EmailAddr!
createdAt: Datetime!
updatedAt: Datetime!
}
type Membership implements Node {
id: ID!
userID: ID!
organizationID: ID!
role: MembershipRole!
fullName: String!
emailAddress: EmailAddr!
authMethod: UserAuthMethod! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type Invitation implements Node {
id: ID!
email: EmailAddr!
fullName: String!
role: MembershipRole!
status: InvitationStatus!
expiresAt: Datetime!
acceptedAt: Datetime
createdAt: Datetime!
organization: Organization! @goField(forceResolver: true)
}
type SlackConnection {
id: ID!
channel: String
@@ -2324,7 +2126,8 @@ type StateOfApplicability implements Node {
orderBy: ControlOrder
filter: ControlFilter
): ControlConnection! @goField(forceResolver: true)
availableControls: [AvailableStateOfApplicabilityControl!]! @goField(forceResolver: true)
availableControls: [AvailableStateOfApplicabilityControl!]!
@goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -2510,8 +2313,10 @@ type ProcessingActivity implements Node {
before: CursorKey
orderBy: VendorOrder
): VendorConnection! @goField(forceResolver: true)
dataProtectionImpactAssessment: DataProtectionImpactAssessment @goField(forceResolver: true)
transferImpactAssessment: TransferImpactAssessment @goField(forceResolver: true)
dataProtectionImpactAssessment: DataProtectionImpactAssessment
@goField(forceResolver: true)
transferImpactAssessment: TransferImpactAssessment
@goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -2580,15 +2385,6 @@ type Session {
type Viewer {
id: ID!
user: User!
organizations(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: OrganizationOrder
): OrganizationConnection! @goField(forceResolver: true)
signableDocuments(
organizationId: ID!
@@ -2602,17 +2398,6 @@ type Viewer {
signableDocument(id: ID!): SignableDocument @goField(forceResolver: true)
}
# Connection Types
type OrganizationConnection {
edges: [OrganizationEdge!]!
pageInfo: PageInfo!
}
type OrganizationEdge {
cursor: CursorKey!
node: Organization!
}
type TrustCenterConnection {
edges: [TrustCenterEdge!]!
pageInfo: PageInfo!
@@ -2729,34 +2514,6 @@ type TrustCenterFileEdge {
node: TrustCenterFile!
}
type UserConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.UserConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type MembershipConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MembershipConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [MembershipEdge!]!
pageInfo: PageInfo!
}
type MembershipEdge {
cursor: CursorKey!
node: Membership!
}
type UserEdge {
cursor: CursorKey!
node: User!
}
type PeopleConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.PeopleConnection"
@@ -3126,20 +2883,6 @@ type File {
updatedAt: Datetime!
}
type InvitationConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.InvitationConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [InvitationEdge!]!
pageInfo: PageInfo!
}
type InvitationEdge {
cursor: CursorKey!
node: Invitation!
}
# Root Types
type Query {
node(id: ID!): Node!
@@ -3147,22 +2890,9 @@ type Query {
}
type Mutation {
# Organization mutations
createOrganization(
input: CreateOrganizationInput!
): CreateOrganizationPayload!
updateOrganization(
input: UpdateOrganizationInput!
): UpdateOrganizationPayload!
updateOrganizationContext(
input: UpdateOrganizationContextInput!
): UpdateOrganizationContextPayload!
deleteOrganizationHorizontalLogo(
input: DeleteOrganizationHorizontalLogoInput!
): DeleteOrganizationHorizontalLogoPayload!
deleteOrganization(
input: DeleteOrganizationInput!
): DeleteOrganizationPayload!
updateTrustCenter(input: UpdateTrustCenterInput!): UpdateTrustCenterPayload!
uploadTrustCenterNDA(
input: UploadTrustCenterNDAInput!
@@ -3203,13 +2933,7 @@ type Mutation {
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!
updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload!
# People mutations
createPeople(input: CreatePeopleInput!): CreatePeoplePayload!
updatePeople(input: UpdatePeopleInput!): UpdatePeoplePayload!
@@ -3407,6 +3131,7 @@ type Mutation {
input: CancelSignatureRequestInput!
): CancelSignatureRequestPayload!
signDocument(input: SignDocumentInput!): SignDocumentPayload!
exportDocumentVersionPDF(
input: ExportDocumentVersionPDFInput!
): ExportDocumentVersionPDFPayload!
@@ -3515,25 +3240,6 @@ type Mutation {
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(
input: UpdateSAMLConfigurationInput!
): UpdateSAMLConfigurationPayload!
deleteSAMLConfiguration(
input: DeleteSAMLConfigurationInput!
): DeleteSAMLConfigurationPayload!
enableSAML(input: EnableSAMLInput!): EnableSAMLPayload!
disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!
}
# Input Types
@@ -3545,34 +3251,11 @@ type GenerateFrameworkStateOfApplicabilityPayload {
data: String!
}
input CreateOrganizationInput {
name: String!
}
input UpdateOrganizationInput {
organizationId: ID!
name: String
description: String @goField(omittable: true)
websiteUrl: String @goField(omittable: true)
email: String @goField(omittable: true)
headquarterAddress: String @goField(omittable: true)
logoFile: Upload
horizontalLogoFile: Upload
}
input UpdateOrganizationContextInput {
organizationId: ID!
summary: String @goField(omittable: true)
}
input DeleteOrganizationHorizontalLogoInput {
organizationId: ID!
}
input DeleteOrganizationInput {
organizationId: ID!
}
input UpdateTrustCenterInput {
trustCenterId: ID!
active: Boolean
@@ -3763,7 +3446,7 @@ input UpdatePeopleInput {
id: ID!
fullName: String
primaryEmailAddress: EmailAddr
additionalEmailAddresses: [EmailAddr!]
additionalEmailAddresses: [EmailAddr!] @goField(omittable: true)
kind: PeopleKind
position: String @goField(omittable: true)
contractStartDate: Datetime @goField(omittable: true)
@@ -3844,7 +3527,6 @@ input DeleteTaskInput {
taskId: ID!
}
input CreateControlMeasureMappingInput {
controlId: ID!
measureId: ID!
@@ -4152,7 +3834,6 @@ input DeleteStateOfApplicabilityInput {
stateOfApplicabilityId: ID!
}
type StateOfApplicabilityControl {
id: ID!
stateOfApplicabilityId: ID!
@@ -4162,13 +3843,19 @@ type StateOfApplicabilityControl {
justification: String
}
type StateOfApplicabilityControlConnection @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlConnection") {
type StateOfApplicabilityControlConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlConnection"
) {
totalCount: Int!
edges: [StateOfApplicabilityControlEdge!]!
pageInfo: PageInfo!
}
type StateOfApplicabilityControlEdge @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlEdge") {
type StateOfApplicabilityControlEdge
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlEdge"
) {
cursor: CursorKey!
node: StateOfApplicabilityControl!
}
@@ -4202,38 +3889,6 @@ enum StateOfApplicabilityOrderField
value: "go.probo.inc/probo/pkg/coredata.StateOfApplicabilityOrderFieldCreatedAt"
)
}
input ConfirmEmailInput {
token: String!
}
input InviteUserInput {
organizationId: ID!
email: EmailAddr!
fullName: String!
role: MembershipRole!
createPeople: Boolean!
}
input AcceptInvitationInput {
invitationId: ID!
}
input DeleteInvitationInput {
invitationId: ID!
}
input RemoveMemberInput {
organizationId: ID!
memberId: ID!
}
input UpdateMembershipInput {
organizationId: ID!
memberId: ID!
role: MembershipRole!
}
input CreateControlInput {
frameworkId: ID!
sectionTitle: String!
@@ -4517,13 +4172,6 @@ input DeleteSnapshotInput {
}
# Payload Types
type CreateOrganizationPayload {
organizationEdge: OrganizationEdge!
}
type UpdateOrganizationPayload {
organization: Organization!
}
type UpdateOrganizationContextPayload {
context: OrganizationContext!
@@ -4534,14 +4182,6 @@ type OrganizationContext {
summary: String
}
type DeleteOrganizationHorizontalLogoPayload {
organization: Organization!
}
type DeleteOrganizationPayload {
deletedOrganizationId: ID!
}
type UpdateTrustCenterPayload {
trustCenter: TrustCenter!
}
@@ -4698,7 +4338,6 @@ type DeleteTaskPayload {
deletedTaskId: ID!
}
type CreateControlMeasureMappingPayload {
controlEdge: ControlEdge!
measureEdge: MeasureEdge!
@@ -4906,30 +4545,6 @@ type DeleteStateOfApplicabilityPayload {
deletedStateOfApplicabilityId: ID!
}
type ConfirmEmailPayload {
success: Boolean!
}
type InviteUserPayload {
invitationEdge: InvitationEdge!
}
type AcceptInvitationPayload {
invitation: Invitation!
}
type DeleteInvitationPayload {
deletedInvitationId: ID!
}
type RemoveMemberPayload {
deletedMemberId: ID!
}
type UpdateMembershipPayload {
membership: Membership!
}
input VendorRiskAssessmentOrder {
field: VendorRiskAssessmentOrderField!
direction: OrderDirection!
@@ -5213,7 +4828,7 @@ type Asset implements Node {
before: CursorKey
orderBy: VendorOrder
): VendorConnection! @goField(forceResolver: true)
assetType: AssetType! @goField(forceResolver: true)
assetType: AssetType!
dataTypesStored: String!
organization: Organization! @goField(forceResolver: true)
createdAt: Datetime!
@@ -5513,162 +5128,3 @@ type CreateCustomDomainPayload {
type DeleteCustomDomainPayload {
deletedCustomDomainId: ID!
}
# ============================================
# SAML Configuration Types
# ============================================
type SAMLConfiguration implements Node {
id: ID!
organization: Organization! @goField(forceResolver: true)
emailDomain: String!
enabled: Boolean!
enforcementPolicy: SAMLEnforcementPolicy!
# Domain verification (required before SAML can be configured)
domainVerified: Boolean!
domainVerificationToken: String
domainVerifiedAt: Datetime
# Service Provider metadata (read-only, auto-generated)
spEntityId: String!
spAcsUrl: String!
spMetadataUrl: String! @goField(forceResolver: true)
# Identity Provider configuration
idpEntityId: String!
idpSsoUrl: String!
idpCertificate: String!
idpMetadataUrl: String
# Attribute mapping
attributeEmail: String!
attributeFirstname: String!
attributeLastname: String!
attributeRole: String!
# Auto-signup
autoSignupEnabled: Boolean!
# Test login URL for this configuration
testLoginUrl: String! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
# ============================================
# SAML Configuration Inputs
# ============================================
input CreateSAMLConfigurationInput {
organizationId: ID!
# Email domain this config applies to
emailDomain: String!
# Enforcement policy for this SAML configuration
enforcementPolicy: SAMLEnforcementPolicy!
# SP configuration (optional - auto-generated if not provided)
spCertificate: String
spPrivateKey: String
# IdP configuration - Option 1: Provide metadata XML (recommended for Google Workspace)
# This will automatically extract entityId, ssoUrl, and certificate from the metadata
idpMetadataXml: String
# IdP configuration - Option 2: Provide individual fields manually
# Required if idpMetadataXml is not provided
idpEntityId: String
idpSsoUrl: String
idpCertificate: String
idpMetadataUrl: String
# Attribute mapping (optional, defaults provided)
attributeEmail: String
attributeFirstname: String
attributeLastname: String
attributeRole: String
autoSignupEnabled: Boolean
}
input UpdateSAMLConfigurationInput {
id: ID!
enabled: Boolean
enforcementPolicy: SAMLEnforcementPolicy
spCertificate: String
spPrivateKey: String
idpEntityId: String
idpSsoUrl: String
idpCertificate: String
idpMetadataUrl: String
attributeEmail: String
attributeFirstname: String
attributeLastname: String
attributeRole: String
autoSignupEnabled: Boolean
}
# ============================================
# Domain Verification Inputs
# ============================================
input InitiateDomainVerificationInput {
organizationId: ID!
emailDomain: String!
}
input VerifyDomainInput {
id: ID!
}
input DeleteSAMLConfigurationInput {
id: ID!
}
input EnableSAMLInput {
id: ID!
}
input DisableSAMLInput {
id: ID!
}
# ============================================
# SAML Configuration Payloads
# ============================================
type InitiateDomainVerificationPayload {
samlConfiguration: SAMLConfiguration!
# The TXT record value that needs to be added to DNS
# Format: probo-verification={token}
dnsRecord: String!
}
type VerifyDomainPayload {
samlConfiguration: SAMLConfiguration!
verified: Boolean!
}
type CreateSAMLConfigurationPayload {
samlConfiguration: SAMLConfiguration!
}
type UpdateSAMLConfigurationPayload {
samlConfiguration: SAMLConfiguration!
}
type DeleteSAMLConfigurationPayload {
deletedSAMLConfigurationId: ID!
}
type EnableSAMLPayload {
samlConfiguration: SAMLConfiguration!
}
type DisableSAMLPayload {
samlConfiguration: SAMLConfiguration!
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +0,0 @@
// 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 (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
InvitationConnection struct {
TotalCount int `json:"totalCount"`
Edges []*InvitationEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
Resolver any
ParentID gid.GID
Filter *InvitationFilter
}
)
func NewInvitationConnection(
p *page.Page[*coredata.Invitation, coredata.InvitationOrderField],
resolver any,
parentID gid.GID,
filter *InvitationFilter,
) *InvitationConnection {
var edges = make([]*InvitationEdge, len(p.Data))
for i := range edges {
edges[i] = NewInvitationEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &InvitationConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
Filter: filter,
}
}
func NewInvitationEdge(invitation *coredata.Invitation, orderBy coredata.InvitationOrderField) *InvitationEdge {
return &InvitationEdge{
Cursor: invitation.CursorKey(orderBy),
Node: NewInvitation(invitation),
}
}
func NewInvitation(i *coredata.Invitation) *Invitation {
return &Invitation{
ID: i.ID,
Email: i.Email,
FullName: i.FullName,
Role: i.Role,
Status: i.Status,
ExpiresAt: i.ExpiresAt,
AcceptedAt: i.AcceptedAt,
CreatedAt: i.CreatedAt,
}
}

View File

@@ -1,73 +0,0 @@
// 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 (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
MembershipConnection struct {
TotalCount int `json:"totalCount"`
Edges []*MembershipEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
Resolver any
ParentID gid.GID
}
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
)
func NewMembershipConnection(
p *page.Page[*coredata.Membership, coredata.MembershipOrderField],
resolver any,
parentID gid.GID,
) *MembershipConnection {
var edges = make([]*MembershipEdge, len(p.Data))
for i := range edges {
edges[i] = NewMembershipEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &MembershipConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}
func NewMembershipEdge(membership *coredata.Membership, orderBy coredata.MembershipOrderField) *MembershipEdge {
return &MembershipEdge{
Cursor: membership.CursorKey(orderBy),
Node: NewMembership(membership),
}
}
func NewMembership(m *coredata.Membership) *Membership {
return &Membership{
ID: m.ID,
UserID: m.UserID,
OrganizationID: m.OrganizationID,
Role: m.Role,
FullName: m.FullName,
EmailAddress: m.EmailAddress,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}

View File

@@ -16,33 +16,8 @@ package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/page"
)
type (
OrganizationOrderBy OrderBy[coredata.OrganizationOrderField]
)
func NewOrganizationConnection(page *page.Page[*coredata.Organization, coredata.OrganizationOrderField]) *OrganizationConnection {
var edges = make([]*OrganizationEdge, len(page.Data))
for i := range edges {
edges[i] = NewOrganizationEdge(page.Data[i], page.Cursor.OrderBy.Field)
}
return &OrganizationConnection{
Edges: edges,
PageInfo: NewPageInfo(page),
}
}
func NewOrganizationEdge(o *coredata.Organization, orderBy coredata.OrganizationOrderField) *OrganizationEdge {
return &OrganizationEdge{
Cursor: o.CursorKey(orderBy),
Node: NewOrganization(o),
}
}
func NewOrganization(o *coredata.Organization) *Organization {
return &Organization{
ID: o.ID,

View File

@@ -64,7 +64,6 @@ func NewPeopleEdge(p *coredata.People, orderBy coredata.PeopleOrderField) *Peopl
}
func NewPeople(p *coredata.People) *People {
return &People{
ID: p.ID,
FullName: p.FullName,

View File

@@ -1,44 +0,0 @@
// 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 (
"go.probo.inc/probo/pkg/coredata"
)
func NewSAMLConfigurationWithURLs(c *coredata.SAMLConfiguration, spEntityID, spAcsURL string) *SAMLConfiguration {
return &SAMLConfiguration{
ID: c.ID,
EmailDomain: c.EmailDomain,
Enabled: c.Enabled,
EnforcementPolicy: c.EnforcementPolicy,
DomainVerified: c.DomainVerified,
DomainVerificationToken: c.DomainVerificationToken,
DomainVerifiedAt: c.DomainVerifiedAt,
SpEntityID: spEntityID,
SpAcsURL: spAcsURL,
IdpEntityID: c.IdPEntityID,
IdpSsoURL: c.IdPSsoURL,
IdpCertificate: c.IdPCertificate,
IdpMetadataURL: c.IdPMetadataURL,
AttributeEmail: c.AttributeEmail,
AttributeFirstname: c.AttributeFirstname,
AttributeLastname: c.AttributeLastname,
AttributeRole: c.AttributeRole,
AutoSignupEnabled: c.AutoSignupEnabled,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}

View File

@@ -65,7 +65,13 @@ func NewStateOfApplicabilityEdge(soa *coredata.StateOfApplicability, orderBy cor
func NewStateOfApplicability(soa *coredata.StateOfApplicability) *StateOfApplicability {
return &StateOfApplicability{
ID: soa.ID,
ID: soa.ID,
Organization: &Organization{
ID: soa.OrganizationID,
},
Owner: &People{
ID: soa.OwnerID,
},
Name: soa.Name,
SourceID: soa.SourceID,
SnapshotID: soa.SnapshotID,

View File

@@ -61,7 +61,7 @@ func NewTaskEdge(t *coredata.Task, orderBy coredata.TaskOrderField) *TaskEdge {
}
func NewTask(t *coredata.Task) *Task {
return &Task{
node := &Task{
ID: t.ID,
Name: t.Name,
Description: t.Description,
@@ -71,4 +71,12 @@ func NewTask(t *coredata.Task) *Task {
UpdatedAt: t.UpdatedAt,
Deadline: t.Deadline,
}
if t.MeasureID != nil {
node.Measure = &Measure{
ID: *t.MeasureID,
}
}
return node
}

View File

@@ -3,10 +3,6 @@
package types
import (
"bytes"
"fmt"
"io"
"strconv"
"time"
"github.com/99designs/gqlgen/graphql"
@@ -21,14 +17,6 @@ type Node interface {
GetID() gid.GID
}
type AcceptInvitationInput struct {
InvitationID gid.GID `json:"invitationId"`
}
type AcceptInvitationPayload struct {
Invitation *Invitation `json:"invitation"`
}
type AssessVendorInput struct {
ID gid.GID `json:"id"`
WebsiteURL string `json:"websiteUrl"`
@@ -150,14 +138,6 @@ type CancelSignatureRequestPayload struct {
DeletedDocumentVersionSignatureID gid.GID `json:"deletedDocumentVersionSignatureId"`
}
type ConfirmEmailInput struct {
Token string `json:"token"`
}
type ConfirmEmailPayload struct {
Success bool `json:"success"`
}
type ContinualImprovement struct {
ID gid.GID `json:"id"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
@@ -441,14 +421,6 @@ type CreateObligationPayload struct {
ObligationEdge *ObligationEdge `json:"obligationEdge"`
}
type CreateOrganizationInput struct {
Name string `json:"name"`
}
type CreateOrganizationPayload struct {
OrganizationEdge *OrganizationEdge `json:"organizationEdge"`
}
type CreatePeopleInput struct {
OrganizationID gid.GID `json:"organizationId"`
FullName string `json:"fullName"`
@@ -555,28 +527,6 @@ type CreateRiskPayload struct {
RiskEdge *RiskEdge `json:"riskEdge"`
}
type CreateSAMLConfigurationInput struct {
OrganizationID gid.GID `json:"organizationId"`
EmailDomain string `json:"emailDomain"`
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
SpCertificate *string `json:"spCertificate,omitempty"`
SpPrivateKey *string `json:"spPrivateKey,omitempty"`
IdpMetadataXML *string `json:"idpMetadataXml,omitempty"`
IdpEntityID *string `json:"idpEntityId,omitempty"`
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
IdpCertificate *string `json:"idpCertificate,omitempty"`
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
AttributeEmail *string `json:"attributeEmail,omitempty"`
AttributeFirstname *string `json:"attributeFirstname,omitempty"`
AttributeLastname *string `json:"attributeLastname,omitempty"`
AttributeRole *string `json:"attributeRole,omitempty"`
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
}
type CreateSAMLConfigurationPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
}
type CreateSnapshotInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -936,14 +886,6 @@ type DeleteFrameworkPayload struct {
DeletedFrameworkID gid.GID `json:"deletedFrameworkId"`
}
type DeleteInvitationInput struct {
InvitationID gid.GID `json:"invitationId"`
}
type DeleteInvitationPayload struct {
DeletedInvitationID gid.GID `json:"deletedInvitationId"`
}
type DeleteMeasureInput struct {
MeasureID gid.GID `json:"measureId"`
}
@@ -976,22 +918,6 @@ type DeleteObligationPayload struct {
DeletedObligationID gid.GID `json:"deletedObligationId"`
}
type DeleteOrganizationHorizontalLogoInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
type DeleteOrganizationHorizontalLogoPayload struct {
Organization *Organization `json:"organization"`
}
type DeleteOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
type DeleteOrganizationPayload struct {
DeletedOrganizationID gid.GID `json:"deletedOrganizationId"`
}
type DeletePeopleInput struct {
PeopleID gid.GID `json:"peopleId"`
}
@@ -1054,14 +980,6 @@ type DeleteRiskPayload struct {
DeletedRiskID gid.GID `json:"deletedRiskId"`
}
type DeleteSAMLConfigurationInput struct {
ID gid.GID `json:"id"`
}
type DeleteSAMLConfigurationPayload struct {
DeletedSAMLConfigurationID gid.GID `json:"deletedSAMLConfigurationId"`
}
type DeleteSnapshotInput struct {
SnapshotID gid.GID `json:"snapshotId"`
}
@@ -1185,14 +1103,6 @@ type DeleteVendorServicePayload struct {
DeletedVendorServiceID gid.GID `json:"deletedVendorServiceId"`
}
type DisableSAMLInput struct {
ID gid.GID `json:"id"`
}
type DisableSAMLPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
}
type Document struct {
ID gid.GID `json:"id"`
Title string `json:"title"`
@@ -1268,14 +1178,6 @@ type DocumentVersionSignatureOrder struct {
Direction page.OrderDirection `json:"direction"`
}
type EnableSAMLInput struct {
ID gid.GID `json:"id"`
}
type EnableSAMLPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
}
type Evidence struct {
ID gid.GID `json:"id"`
Size int `json:"size"`
@@ -1443,57 +1345,6 @@ type ImportMeasurePayload struct {
MeasureEdges []*MeasureEdge `json:"measureEdges"`
}
type InitiateDomainVerificationInput struct {
OrganizationID gid.GID `json:"organizationId"`
EmailDomain string `json:"emailDomain"`
}
type InitiateDomainVerificationPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
DNSRecord string `json:"dnsRecord"`
}
type Invitation struct {
ID gid.GID `json:"id"`
Email mail.Addr `json:"email"`
FullName string `json:"fullName"`
Role coredata.MembershipRole `json:"role"`
Status coredata.InvitationStatus `json:"status"`
ExpiresAt time.Time `json:"expiresAt"`
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
Organization *Organization `json:"organization"`
}
func (Invitation) IsNode() {}
func (this Invitation) GetID() gid.GID { return this.ID }
type InvitationEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Invitation `json:"node"`
}
type InvitationFilter struct {
Statuses []coredata.InvitationStatus `json:"statuses,omitempty"`
}
type InvitationOrder struct {
Direction page.OrderDirection `json:"direction"`
Field coredata.InvitationOrderField `json:"field"`
}
type InviteUserInput struct {
OrganizationID gid.GID `json:"organizationId"`
Email mail.Addr `json:"email"`
FullName string `json:"fullName"`
Role coredata.MembershipRole `json:"role"`
CreatePeople bool `json:"createPeople"`
}
type InviteUserPayload struct {
InvitationEdge *InvitationEdge `json:"invitationEdge"`
}
type Measure struct {
ID gid.GID `json:"id"`
Category string `json:"category"`
@@ -1540,26 +1391,6 @@ type MeetingEdge struct {
Node *Meeting `json:"node"`
}
type Membership struct {
ID gid.GID `json:"id"`
UserID gid.GID `json:"userID"`
OrganizationID gid.GID `json:"organizationID"`
Role coredata.MembershipRole `json:"role"`
FullName string `json:"fullName"`
EmailAddress mail.Addr `json:"emailAddress"`
AuthMethod coredata.UserAuthMethod `json:"authMethod"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Membership) IsNode() {}
func (this Membership) GetID() gid.GID { return this.ID }
type MembershipEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Membership `json:"node"`
}
type Mutation struct {
}
@@ -1634,8 +1465,6 @@ type Organization struct {
Email *string `json:"email,omitempty"`
HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
Context *OrganizationContext `json:"context,omitempty"`
Memberships *MembershipConnection `json:"memberships"`
Invitations *InvitationConnection `json:"invitations"`
SlackConnections *SlackConnectionConnection `json:"slackConnections"`
Frameworks *FrameworkConnection `json:"frameworks"`
Controls *ControlConnection `json:"controls"`
@@ -1661,7 +1490,6 @@ type Organization struct {
TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"`
TrustCenter *TrustCenter `json:"trustCenter,omitempty"`
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
SamlConfigurations []*SAMLConfiguration `json:"samlConfigurations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
@@ -1669,21 +1497,11 @@ type Organization struct {
func (Organization) IsNode() {}
func (this Organization) GetID() gid.GID { return this.ID }
type OrganizationConnection struct {
Edges []*OrganizationEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type OrganizationContext struct {
OrganizationID gid.GID `json:"organizationId"`
Summary *string `json:"summary,omitempty"`
}
type OrganizationEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Organization `json:"node"`
}
type OrganizationOrder struct {
Direction page.OrderDirection `json:"direction"`
Field coredata.OrganizationOrderField `json:"field"`
@@ -1777,15 +1595,6 @@ type PublishDocumentVersionPayload struct {
type Query struct {
}
type RemoveMemberInput struct {
OrganizationID gid.GID `json:"organizationId"`
MemberID gid.GID `json:"memberId"`
}
type RemoveMemberPayload struct {
DeletedMemberID gid.GID `json:"deletedMemberId"`
}
type Report struct {
ID gid.GID `json:"id"`
ObjectKey string `json:"objectKey"`
@@ -1880,35 +1689,6 @@ type RiskFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
type SAMLConfiguration struct {
ID gid.GID `json:"id"`
Organization *Organization `json:"organization"`
EmailDomain string `json:"emailDomain"`
Enabled bool `json:"enabled"`
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
DomainVerified bool `json:"domainVerified"`
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
SpEntityID string `json:"spEntityId"`
SpAcsURL string `json:"spAcsUrl"`
SpMetadataURL string `json:"spMetadataUrl"`
IdpEntityID string `json:"idpEntityId"`
IdpSsoURL string `json:"idpSsoUrl"`
IdpCertificate string `json:"idpCertificate"`
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
AttributeEmail string `json:"attributeEmail"`
AttributeFirstname string `json:"attributeFirstname"`
AttributeLastname string `json:"attributeLastname"`
AttributeRole string `json:"attributeRole"`
AutoSignupEnabled bool `json:"autoSignupEnabled"`
TestLoginURL string `json:"testLoginUrl"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (SAMLConfiguration) IsNode() {}
func (this SAMLConfiguration) GetID() gid.GID { return this.ID }
type SendSigningNotificationsInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
@@ -2289,16 +2069,6 @@ 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"`
@@ -2344,30 +2114,15 @@ type UpdateOrganizationContextPayload struct {
Context *OrganizationContext `json:"context"`
}
type UpdateOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name *string `json:"name,omitempty"`
Description graphql.Omittable[*string] `json:"description,omitempty"`
WebsiteURL graphql.Omittable[*string] `json:"websiteUrl,omitempty"`
Email graphql.Omittable[*string] `json:"email,omitempty"`
HeadquarterAddress graphql.Omittable[*string] `json:"headquarterAddress,omitempty"`
LogoFile *graphql.Upload `json:"logoFile,omitempty"`
HorizontalLogoFile *graphql.Upload `json:"horizontalLogoFile,omitempty"`
}
type UpdateOrganizationPayload struct {
Organization *Organization `json:"organization"`
}
type UpdatePeopleInput struct {
ID gid.GID `json:"id"`
FullName *string `json:"fullName,omitempty"`
PrimaryEmailAddress *mail.Addr `json:"primaryEmailAddress,omitempty"`
AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses,omitempty"`
Kind *coredata.PeopleKind `json:"kind,omitempty"`
Position graphql.Omittable[*string] `json:"position,omitempty"`
ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"`
ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"`
ID gid.GID `json:"id"`
FullName *string `json:"fullName,omitempty"`
PrimaryEmailAddress *mail.Addr `json:"primaryEmailAddress,omitempty"`
AdditionalEmailAddresses graphql.Omittable[[]mail.Addr] `json:"additionalEmailAddresses,omitempty"`
Kind *coredata.PeopleKind `json:"kind,omitempty"`
Position graphql.Omittable[*string] `json:"position,omitempty"`
ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"`
ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"`
}
type UpdatePeoplePayload struct {
@@ -2435,27 +2190,6 @@ type UpdateRiskPayload struct {
Risk *Risk `json:"risk"`
}
type UpdateSAMLConfigurationInput struct {
ID gid.GID `json:"id"`
Enabled *bool `json:"enabled,omitempty"`
EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
SpCertificate *string `json:"spCertificate,omitempty"`
SpPrivateKey *string `json:"spPrivateKey,omitempty"`
IdpEntityID *string `json:"idpEntityId,omitempty"`
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
IdpCertificate *string `json:"idpCertificate,omitempty"`
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
AttributeEmail *string `json:"attributeEmail,omitempty"`
AttributeFirstname *string `json:"attributeFirstname,omitempty"`
AttributeLastname *string `json:"attributeLastname,omitempty"`
AttributeRole *string `json:"attributeRole,omitempty"`
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
}
type UpdateSAMLConfigurationPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
}
type UpdateStateOfApplicabilityInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
@@ -2676,22 +2410,6 @@ type UploadVendorDataPrivacyAgreementPayload struct {
VendorDataPrivacyAgreement *VendorDataPrivacyAgreement `json:"vendorDataPrivacyAgreement"`
}
type User struct {
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
Email mail.Addr `json:"email"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (User) IsNode() {}
func (this User) GetID() gid.GID { return this.ID }
type UserEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *User `json:"node"`
}
type Vendor struct {
ID gid.GID `json:"id"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
@@ -2867,80 +2585,8 @@ type VendorServiceEdge struct {
Node *VendorService `json:"node"`
}
type VerifyDomainInput struct {
ID gid.GID `json:"id"`
}
type VerifyDomainPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
Verified bool `json:"verified"`
}
type Viewer struct {
ID gid.GID `json:"id"`
User *User `json:"user"`
Organizations *OrganizationConnection `json:"organizations"`
SignableDocuments *SignableDocumentConnection `json:"signableDocuments"`
SignableDocument *SignableDocument `json:"signableDocument,omitempty"`
}
type Role string
const (
RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN"
RoleViewer Role = "VIEWER"
RoleAuditor Role = "AUDITOR"
RoleFull Role = "FULL"
)
var AllRole = []Role{
RoleOwner,
RoleAdmin,
RoleViewer,
RoleAuditor,
RoleFull,
}
func (e Role) IsValid() bool {
switch e {
case RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, 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
}

View File

@@ -1,70 +0,0 @@
// 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 (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
UserConnection struct {
TotalCount int `json:"totalCount"`
Edges []*UserEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
Resolver any
ParentID gid.GID
}
UserOrderBy OrderBy[coredata.UserOrderField]
)
func NewUserConnection(
p *page.Page[*coredata.User, coredata.UserOrderField],
resolver any,
parentID gid.GID,
) *UserConnection {
var edges = make([]*UserEdge, len(p.Data))
for i := range edges {
edges[i] = NewUserEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &UserConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}
func NewUserEdge(user *coredata.User, orderBy coredata.UserOrderField) *UserEdge {
return &UserEdge{
Cursor: user.CursorKey(orderBy),
Node: NewUser(user),
}
}
func NewUser(u *coredata.User) *User {
return &User{
ID: u.ID,
Email: u.EmailAddress,
FullName: u.FullName,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}

File diff suppressed because it is too large Load Diff