From d1c31978da4c33d7dda998224e278abc945fe54d Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 30 Oct 2025 22:42:17 +0100 Subject: [PATCH] Fix role and organization logo Signed-off-by: Bryan Frimin --- apps/console/src/layouts/MainLayout.tsx | 13 ++-- .../settings/GeneralSettingsTab.tsx | 52 ++++++++------- pkg/auth/saml_mapper.go | 7 ++- pkg/auth/saml_service.go | 2 +- pkg/auth/service.go | 8 +-- pkg/authz/service.go | 21 ++----- pkg/coredata/invitation.go | 4 +- pkg/coredata/membership.go | 2 +- pkg/coredata/role.go | 63 +++++++++++++++++++ pkg/server/api/console/v1/schema.graphql | 11 +++- pkg/server/api/console/v1/schema/schema.go | 59 ++++++++++++++--- pkg/server/api/console/v1/types/types.go | 4 +- pkg/server/api/console/v1/v1_resolver.go | 3 +- pkg/server/auth/list_invitations_handler.go | 2 +- 14 files changed, 177 insertions(+), 74 deletions(-) create mode 100644 pkg/coredata/role.go diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index ab69ef10c..e91d2eb30 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -384,14 +384,14 @@ function OrganizationSelector({ const isSAMLUrl = targetUrl.includes("/connect/saml/"); + // Use organization endpoint for all logos for consistency + const logoUrl = organization.logoUrl; + return ( {isSAMLUrl ? ( - + {organization.name} {isAuthenticated && ( @@ -405,10 +405,7 @@ function OrganizationSelector({ ) : ( - + {organization.name} {isAuthenticated && ( diff --git a/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx b/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx index 8792b5e34..12d915cdb 100644 --- a/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx +++ b/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx @@ -175,19 +175,23 @@ export default function GeneralSettingsTab() { const reader = new FileReader(); reader.onload = () => { setLogoPreview(reader.result as string); - updateOrganization({ - variables: { - input: { - organizationId: organization.id, - logo: file, - }, - }, - onCompleted: () => { - setLogoPreview(null); - }, - }); }; reader.readAsDataURL(file); + + updateOrganization({ + variables: { + input: { + organizationId: organization.id, + logoFile: null, + }, + }, + uploadables: { + "input.logoFile": file, + }, + onCompleted: () => { + setLogoPreview(null); + }, + }); }; const handleHorizontalLogoChange: ChangeEventHandler = ( @@ -199,19 +203,23 @@ export default function GeneralSettingsTab() { const reader = new FileReader(); reader.onload = () => { setHorizontalLogoPreview(reader.result as string); - updateOrganization({ - variables: { - input: { - organizationId: organization.id, - horizontalLogo: file, - }, - }, - onCompleted: () => { - setHorizontalLogoPreview(null); - }, - }); }; reader.readAsDataURL(file); + + updateOrganization({ + variables: { + input: { + organizationId: organization.id, + horizontalLogoFile: null, + }, + }, + uploadables: { + "input.horizontalLogoFile": file, + }, + onCompleted: () => { + setHorizontalLogoPreview(null); + }, + }); }; const handleDeleteHorizontalLogo = () => { diff --git a/pkg/auth/saml_mapper.go b/pkg/auth/saml_mapper.go index ca5e75f00..84019f2b1 100644 --- a/pkg/auth/saml_mapper.go +++ b/pkg/auth/saml_mapper.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/crewjam/saml" + "github.com/getprobo/probo/pkg/coredata" ) func ExtractAttributeValue(assertion *saml.Assertion, attributeName string) (string, error) { @@ -76,12 +77,12 @@ func ExtractEmailDomain(email string) (string, error) { return domain, nil } -func MapSAMLRoleToSystemRole(samlRole string) string { +func MapSAMLRoleToSystemRole(samlRole string) coredata.Role { if samlRole != "" && isValidRole(samlRole) { - return samlRole + return coredata.Role(samlRole) } - return "MEMBER" + return coredata.RoleMember } func isValidRole(role string) bool { diff --git a/pkg/auth/saml_service.go b/pkg/auth/saml_service.go index efa63bded..e3cb884d6 100644 --- a/pkg/auth/saml_service.go +++ b/pkg/auth/saml_service.go @@ -407,7 +407,7 @@ func (s *SAMLService) InitiateSAMLLogin( type SAMLUserInfo struct { Email string FullName string - Role string + Role coredata.Role SAMLSubject string OrganizationID gid.GID SAMLConfigID gid.GID diff --git a/pkg/auth/service.go b/pkg/auth/service.go index bd0fbb517..089f82041 100644 --- a/pkg/auth/service.go +++ b/pkg/auth/service.go @@ -1055,15 +1055,9 @@ func (s Service) GetOrganizationLogoFile( organizationID gid.GID, session *coredata.Session, ) (*coredata.File, error) { - // Check authentication requirements before allowing access to logo - err := s.CheckSingleOrganizationAccess(ctx, user, organizationID, session) - if err != nil { - return nil, fmt.Errorf("access denied: %w", err) - } - var logoFile *coredata.File - err = s.pg.WithConn( + err := s.pg.WithConn( ctx, func(conn pg.Conn) error { scope := coredata.NewScope(organizationID.TenantID()) diff --git a/pkg/authz/service.go b/pkg/authz/service.go index 8049ef27c..8c6e3260f 100644 --- a/pkg/authz/service.go +++ b/pkg/authz/service.go @@ -44,15 +44,6 @@ type ( invitationTokenValidity time.Duration scope coredata.Scoper } - - Role string -) - -const ( - RoleOwner Role = "OWNER" - RoleAdmin Role = "ADMIN" - RoleMember Role = "MEMBER" - RoleViewer Role = "VIEWER" ) const ( @@ -284,7 +275,7 @@ type UserInvitation struct { ID gid.GID Email string FullName string - Role string + Role coredata.Role ExpiresAt time.Time AcceptedAt *time.Time CreatedAt time.Time @@ -398,7 +389,7 @@ func (s *TenantAuthzService) AddUserToOrganization( ctx context.Context, userID gid.GID, orgID gid.GID, - role string, + role coredata.Role, ) error { now := time.Now() membershipID := gid.New(s.scope.GetTenantID(), coredata.MembershipEntityType) @@ -624,7 +615,7 @@ func (s *TenantAuthzService) GetUserRoleInOrganization( ctx context.Context, userID gid.GID, orgID gid.GID, -) (string, error) { +) (coredata.Role, error) { membership := &coredata.Membership{} err := s.pg.WithConn( @@ -675,7 +666,7 @@ func (s *TenantAuthzService) UpdateUserRole( ctx context.Context, userID gid.GID, orgID gid.GID, - newRole string, + newRole coredata.Role, ) error { return s.pg.WithTx( ctx, @@ -702,7 +693,7 @@ func (s *TenantAuthzService) InviteUserToOrganization( organizationID gid.GID, emailAddress string, fullName string, - role string, + role coredata.Role, ) (*coredata.Invitation, error) { var invitation *coredata.Invitation @@ -808,7 +799,7 @@ func (s *TenantAuthzService) EnsureSAMLMembership( ctx context.Context, userID gid.GID, organizationID gid.GID, - role string, + role coredata.Role, ) error { now := time.Now() diff --git a/pkg/coredata/invitation.go b/pkg/coredata/invitation.go index 7636eba48..45e479cbc 100644 --- a/pkg/coredata/invitation.go +++ b/pkg/coredata/invitation.go @@ -33,7 +33,7 @@ type ( OrganizationID gid.GID `db:"organization_id"` Email string `db:"email"` FullName string `db:"full_name"` - Role string `db:"role"` + Role Role `db:"role"` Status InvitationStatus `db:"status"` ExpiresAt time.Time `db:"expires_at"` AcceptedAt *time.Time `db:"accepted_at"` @@ -47,7 +47,7 @@ type ( OrganizationID gid.GID `json:"organization_id"` Email string `json:"email"` FullName string `json:"full_name"` - Role string `json:"role"` + Role Role `json:"role"` } ErrInvitationNotFound struct { diff --git a/pkg/coredata/membership.go b/pkg/coredata/membership.go index 3383bae1e..a7ff6e861 100644 --- a/pkg/coredata/membership.go +++ b/pkg/coredata/membership.go @@ -33,7 +33,7 @@ type ( ID gid.GID `db:"id"` UserID gid.GID `db:"user_id"` OrganizationID gid.GID `db:"organization_id"` - Role string `db:"role"` + Role Role `db:"role"` FullName string `db:"full_name"` EmailAddress string `db:"email_address"` CreatedAt time.Time `db:"created_at"` diff --git a/pkg/coredata/role.go b/pkg/coredata/role.go new file mode 100644 index 000000000..faa4839d8 --- /dev/null +++ b/pkg/coredata/role.go @@ -0,0 +1,63 @@ +// Copyright (c) 2025 Probo Inc . +// +// 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 coredata + +import ( + "database/sql/driver" + "fmt" +) + +type Role string + +const ( + RoleOwner Role = "OWNER" + RoleAdmin Role = "ADMIN" + RoleMember Role = "MEMBER" + RoleViewer Role = "VIEWER" +) + +func (r Role) String() string { + return string(r) +} + +func (r *Role) Scan(value any) error { + var s string + switch v := value.(type) { + case string: + s = v + case []byte: + s = string(v) + default: + return fmt.Errorf("unsupported type for Role: %T", value) + } + + switch s { + case "OWNER": + *r = RoleOwner + case "ADMIN": + *r = RoleAdmin + case "MEMBER": + *r = RoleMember + case "VIEWER": + *r = RoleViewer + default: + return fmt.Errorf("invalid Role value: %q", s) + } + return nil +} + +func (r Role) Value() (driver.Value, error) { + return r.String(), nil +} diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 16838acce..edc655e3f 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -108,6 +108,13 @@ enum InvitationStatus ) } +enum Role @goModel(model: "github.com/getprobo/probo/pkg/coredata.Role") { + OWNER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleOwner") + ADMIN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleAdmin") + MEMBER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleMember") + VIEWER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleViewer") +} + enum DocumentStatus @goModel(model: "github.com/getprobo/probo/pkg/coredata.DocumentStatus") { DRAFT @@ -1837,7 +1844,7 @@ type Membership implements Node { id: ID! userID: ID! organizationID: ID! - role: String! + role: Role! fullName: String! emailAddress: String! authMethod: UserAuthMethod! @goField(forceResolver: true) @@ -1849,7 +1856,7 @@ type Invitation implements Node { id: ID! email: String! fullName: String! - role: String! + role: Role! status: InvitationStatus! expiresAt: Datetime! acceptedAt: Datetime diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index ee82a7d1e..8af6c919c 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -9834,6 +9834,13 @@ enum InvitationStatus ) } +enum Role @goModel(model: "github.com/getprobo/probo/pkg/coredata.Role") { + OWNER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleOwner") + ADMIN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleAdmin") + MEMBER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleMember") + VIEWER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleViewer") +} + enum DocumentStatus @goModel(model: "github.com/getprobo/probo/pkg/coredata.DocumentStatus") { DRAFT @@ -11563,7 +11570,7 @@ type Membership implements Node { id: ID! userID: ID! organizationID: ID! - role: String! + role: Role! fullName: String! emailAddress: String! authMethod: UserAuthMethod! @goField(forceResolver: true) @@ -11575,7 +11582,7 @@ type Invitation implements Node { id: ID! email: String! fullName: String! - role: String! + role: Role! status: InvitationStatus! expiresAt: Datetime! acceptedAt: Datetime @@ -38001,9 +38008,9 @@ func (ec *executionContext) _Invitation_role(ctx context.Context, field graphql. } return graphql.Null } - res := resTmp.(string) + res := resTmp.(coredata.Role) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Invitation_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -38013,7 +38020,7 @@ func (ec *executionContext) fieldContext_Invitation_role(_ context.Context, fiel IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("field of type Role does not have child fields") }, } return fc, nil @@ -39588,9 +39595,9 @@ func (ec *executionContext) _Membership_role(ctx context.Context, field graphql. } return graphql.Null } - res := resTmp.(string) + res := resTmp.(coredata.Role) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Membership_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -39600,7 +39607,7 @@ func (ec *executionContext) fieldContext_Membership_role(_ context.Context, fiel IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("field of type Role does not have child fields") }, } return fc, nil @@ -56563,6 +56570,8 @@ func (ec *executionContext) fieldContext_SAMLConfiguration_organization(_ contex return ec.fieldContext_Organization_processingActivities(ctx, field) case "snapshots": return ec.fieldContext_Organization_snapshots(ctx, field) + case "trustCenterFiles": + return ec.fieldContext_Organization_trustCenterFiles(ctx, field) case "trustCenter": return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": @@ -61954,6 +61963,8 @@ func (ec *executionContext) fieldContext_TrustCenterFile_organization(_ context. return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "samlConfigurations": + return ec.fieldContext_Organization_samlConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -104604,6 +104615,38 @@ var ( } ) +func (ec *executionContext) unmarshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx context.Context, v any) (coredata.Role, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx context.Context, sel ast.SelectionSet, v coredata.Role) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole = map[string]coredata.Role{ + "OWNER": coredata.RoleOwner, + "ADMIN": coredata.RoleAdmin, + "MEMBER": coredata.RoleMember, + "VIEWER": coredata.RoleViewer, + } + marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole = map[coredata.Role]string{ + coredata.RoleOwner: "OWNER", + coredata.RoleAdmin: "ADMIN", + coredata.RoleMember: "MEMBER", + coredata.RoleViewer: "VIEWER", + } +) + func (ec *executionContext) marshalNSAMLConfiguration2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfigurationᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.SAMLConfiguration) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index b2bd7cda7..449d561be 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1276,7 +1276,7 @@ type Invitation struct { ID gid.GID `json:"id"` Email string `json:"email"` FullName string `json:"fullName"` - Role string `json:"role"` + Role coredata.Role `json:"role"` Status coredata.InvitationStatus `json:"status"` ExpiresAt time.Time `json:"expiresAt"` AcceptedAt *time.Time `json:"acceptedAt,omitempty"` @@ -1343,7 +1343,7 @@ type Membership struct { ID gid.GID `json:"id"` UserID gid.GID `json:"userID"` OrganizationID gid.GID `json:"organizationID"` - Role string `json:"role"` + Role coredata.Role `json:"role"` FullName string `json:"fullName"` EmailAddress string `json:"emailAddress"` AuthMethod coredata.UserAuthMethod `json:"authMethod"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index b8fd7da35..b30275fcc 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -14,7 +14,6 @@ import ( "time" "github.com/getprobo/probo/pkg/auth" - "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" @@ -1489,7 +1488,7 @@ func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.Confirm // InviteUser is the resolver for the inviteUser field. func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) { authzSvc := r.AuthzService(ctx, input.OrganizationID.TenantID()) - invitation, err := authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember)) + invitation, err := authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, coredata.RoleMember) if err != nil { panic(fmt.Errorf("cannot invite user to organization: %w", err)) } diff --git a/pkg/server/auth/list_invitations_handler.go b/pkg/server/auth/list_invitations_handler.go index 4c0f8e5a4..486f63b15 100644 --- a/pkg/server/auth/list_invitations_handler.go +++ b/pkg/server/auth/list_invitations_handler.go @@ -64,7 +64,7 @@ func ListInvitationsHandler(authzSvc *authz.Service) http.HandlerFunc { ID: invitation.ID, Email: invitation.Email, FullName: invitation.FullName, - Role: invitation.Role, + Role: invitation.Role.String(), ExpiresAt: invitation.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"), CreatedAt: invitation.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), Organization: OrganizationResponseSummary{