From 01af3812c4cda72e5c07db68a04c7cbda343fb74 Mon Sep 17 00:00:00 2001 From: gearnode Date: Wed, 12 Mar 2025 10:14:49 +0100 Subject: [PATCH] Enforce allowed tenant in each request Signed-off-by: gearnode --- pkg/coredata/migrations/20250312T094600Z.sql | 1 + pkg/coredata/organization.go | 2 + pkg/coredata/user_organization.go | 80 ++++++++++ pkg/server/api/console/v1/resolver.go | 20 +++ pkg/server/api/console/v1/v1_resolver.go | 85 +++++------ pkg/usrmgr/usrmgr.go | 152 ++++++------------- 6 files changed, 186 insertions(+), 154 deletions(-) create mode 100644 pkg/coredata/migrations/20250312T094600Z.sql create mode 100644 pkg/coredata/user_organization.go diff --git a/pkg/coredata/migrations/20250312T094600Z.sql b/pkg/coredata/migrations/20250312T094600Z.sql new file mode 100644 index 000000000..b3253e2d2 --- /dev/null +++ b/pkg/coredata/migrations/20250312T094600Z.sql @@ -0,0 +1 @@ +ALTER TABLE usrmgr_user_organizations RENAME TO users_organizations; \ No newline at end of file diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index 47da0d8a3..bec39eed9 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -34,6 +34,8 @@ type ( CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + + Organizations []*Organization ) func (o *Organization) LoadByID( diff --git a/pkg/coredata/user_organization.go b/pkg/coredata/user_organization.go new file mode 100644 index 000000000..5322b9ef5 --- /dev/null +++ b/pkg/coredata/user_organization.go @@ -0,0 +1,80 @@ +// 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 ( + "context" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" +) + +type ( + UserOrganization struct { + UserID gid.GID `db:"user_id"` + OrganizationID gid.GID `db:"organization_id"` + CreatedAt time.Time `db:"created_at"` + } + + UserOrganizations []*UserOrganization +) + +func (uo UserOrganization) Insert( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +INSERT INTO users_organizations (user_id, organization_id, created_at) +VALUES (@user_id, @organization_id, @created_at) +` + + _, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"user_id": uo.UserID, "organization_id": uo.OrganizationID, "created_at": uo.CreatedAt}) + return err +} + +func (uo UserOrganization) Delete(ctx context.Context, conn pg.Conn) error { + q := ` +DELETE FROM users_organizations WHERE user_id = @user_id AND organization_id = @organization_id +` + + _, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"user_id": uo.UserID, "organization_id": uo.OrganizationID}) + return err +} + +func (uo *UserOrganizations) ForUserID( + ctx context.Context, + conn pg.Conn, + userID gid.GID, +) error { + q := ` +SELECT user_id, organization_id, created_at FROM users_organizations WHERE user_id = @user_id +` + + rows, err := conn.Query(ctx, q, pgx.StrictNamedArgs{"user_id": userID}) + if err != nil { + return err + } + + userOrganizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserOrganization]) + if err != nil { + return err + } + + *uo = userOrganizations + + return nil +} diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index c5eebbe84..3a22b50df 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -58,6 +58,7 @@ type ( var ( sessionContextKey = &ctxKey{name: "session"} userContextKey = &ctxKey{name: "user"} + userTenantContextKey = &ctxKey{name: "user_tenants"} ) func SessionFromContext(ctx context.Context) *coredata.Session { @@ -109,6 +110,7 @@ func graphqlHandler(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg } user := UserFromContext(ctx) + if user == nil { return func(ctx context.Context) *graphql.Response { return &graphql.Response{ @@ -176,8 +178,14 @@ func graphqlHandler(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg return } + tenantIDs, err := usrmgrSvc.ListTenantsForUserID(ctx, user.ID) + if err != nil { + panic(fmt.Errorf("failed to list tenants for user: %w", err)) + } + ctx = context.WithValue(ctx, sessionContextKey, session) ctx = context.WithValue(ctx, userContextKey, user) + ctx = context.WithValue(ctx, userTenantContextKey, tenantIDs) srv.ServeHTTP(w, r.WithContext(ctx)) @@ -187,3 +195,15 @@ func graphqlHandler(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg } } + +func (r *Resolver) GetTenantServiceIfAuthorized(ctx context.Context, tenantID gid.TenantID) *probo.TenantService { + tenantIDs, _ := ctx.Value(userTenantContextKey).([]gid.TenantID) + + for _, id := range tenantIDs { + if id == tenantID { + return r.proboSvc.WithTenant(tenantID) + } + } + + panic(fmt.Errorf("tenant not found")) +} diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 4a3c281c3..8cfd60abd 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -20,7 +20,7 @@ import ( // Tasks is the resolver for the tasks field. func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) page, err := svc.Tasks.ListForControlID(ctx, obj.ID, cursor) @@ -33,7 +33,7 @@ func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first * // FileURL is the resolver for the fileUrl field. func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (string, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) fileURL, err := svc.Evidences.GenerateFileURL(ctx, obj.ID, 15*time.Minute) if err != nil { @@ -45,7 +45,7 @@ func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (st // Controls is the resolver for the controls field. func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) page, err := svc.Controls.ListForFrameworkID(ctx, obj.ID, cursor) @@ -58,7 +58,7 @@ func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, // CreateVendor is the resolver for the createVendor field. func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.CreateVendorPayload, error) { - svc := r.proboSvc.WithTenant(input.OrganizationID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID()) vendor, err := svc.Vendors.Create(ctx, probo.CreateVendorRequest{ OrganizationID: input.OrganizationID, @@ -82,7 +82,7 @@ func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateV // UpdateVendor is the resolver for the updateVendor field. func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateVendorInput) (*types.UpdateVendorPayload, error) { - svc := r.proboSvc.WithTenant(input.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID()) vendor, err := svc.Vendors.Update(ctx, probo.UpdateVendorRequest{ ID: input.ID, @@ -108,7 +108,7 @@ func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateV // DeleteVendor is the resolver for the deleteVendor field. func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (*types.DeleteVendorPayload, error) { - svc := r.proboSvc.WithTenant(input.VendorID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.VendorID.TenantID()) err := svc.Vendors.Delete(ctx, input.VendorID) if err != nil { @@ -122,7 +122,7 @@ func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteV // CreatePeople is the resolver for the createPeople field. func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.CreatePeoplePayload, error) { - svc := r.proboSvc.WithTenant(input.OrganizationID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID()) people, err := svc.Peoples.Create(ctx, probo.CreatePeopleRequest{ OrganizationID: input.OrganizationID, @@ -143,7 +143,7 @@ func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreateP // UpdatePeople is the resolver for the updatePeople field. func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdatePeopleInput) (*types.UpdatePeoplePayload, error) { - svc := r.proboSvc.WithTenant(input.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID()) people, err := svc.Peoples.Update(ctx, probo.UpdatePeopleRequest{ ID: input.ID, @@ -164,7 +164,7 @@ func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdateP // DeletePeople is the resolver for the deletePeople field. func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) { - svc := r.proboSvc.WithTenant(input.PeopleID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.PeopleID.TenantID()) err := svc.Peoples.Delete(ctx, input.PeopleID) if err != nil { @@ -178,7 +178,8 @@ func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeleteP // CreateOrganization is the resolver for the createOrganization field. func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) { - svc := r.proboSvc.WithTenant(gid.NewTenantID()) + // TODO: fix does not work now + svc := r.GetTenantServiceIfAuthorized(ctx, gid.NewTenantID()) organization, err := svc.Organizations.Create(ctx, probo.CreateOrganizationRequest{ Name: input.Name, @@ -187,7 +188,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C return nil, fmt.Errorf("cannot create organization: %w", err) } - err = r.usrmgrSvc.AddUserToOrganization(ctx, UserFromContext(ctx).ID, organization.ID) + err = r.usrmgrSvc.EnrollUserInOrganization(ctx, UserFromContext(ctx).ID, organization.ID) if err != nil { return nil, fmt.Errorf("cannot add user to organization: %w", err) } @@ -204,7 +205,7 @@ func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.D // CreateTask is the resolver for the createTask field. func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) { - svc := r.proboSvc.WithTenant(input.ControlID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.ControlID.TenantID()) task, err := svc.Tasks.Create(ctx, probo.CreateTaskRequest{ ControlID: input.ControlID, @@ -222,7 +223,7 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTas // UpdateTask is the resolver for the updateTask field. func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTaskInput) (*types.UpdateTaskPayload, error) { - svc := r.proboSvc.WithTenant(input.TaskID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID()) task, err := svc.Tasks.Update(ctx, probo.UpdateTaskRequest{ ID: input.TaskID, @@ -242,7 +243,7 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas // DeleteTask is the resolver for the deleteTask field. func (r *mutationResolver) DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) { - svc := r.proboSvc.WithTenant(input.TaskID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID()) err := svc.Tasks.Delete(ctx, input.TaskID) if err != nil { @@ -256,7 +257,7 @@ func (r *mutationResolver) DeleteTask(ctx context.Context, input types.DeleteTas // CreateFramework is the resolver for the createFramework field. func (r *mutationResolver) CreateFramework(ctx context.Context, input types.CreateFrameworkInput) (*types.CreateFrameworkPayload, error) { - svc := r.proboSvc.WithTenant(input.OrganizationID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID()) framework, err := svc.Frameworks.Create(ctx, probo.CreateFrameworkRequest{ OrganizationID: input.OrganizationID, @@ -274,7 +275,7 @@ func (r *mutationResolver) CreateFramework(ctx context.Context, input types.Crea // CreateControl is the resolver for the createControl field. func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) { - svc := r.proboSvc.WithTenant(input.FrameworkID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.FrameworkID.TenantID()) control, err := svc.Controls.Create(ctx, probo.CreateControlRequest{ FrameworkID: input.FrameworkID, @@ -293,7 +294,7 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create // UpdateFramework is the resolver for the updateFramework field. func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error) { - svc := r.proboSvc.WithTenant(input.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID()) framework, err := svc.Frameworks.Update(ctx, probo.UpdateFrameworkRequest{ ID: input.ID, @@ -312,7 +313,7 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda // UpdateControl is the resolver for the updateControl field. func (r *mutationResolver) UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error) { - svc := r.proboSvc.WithTenant(input.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID()) control, err := svc.Controls.Update(ctx, probo.UpdateControlRequest{ ID: input.ID, @@ -333,7 +334,7 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update // UploadEvidence is the resolver for the uploadEvidence field. func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) { - svc := r.proboSvc.WithTenant(input.TaskID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID()) req := probo.CreateEvidenceRequest{ TaskID: input.TaskID, @@ -353,7 +354,7 @@ func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.Uploa // DeleteEvidence is the resolver for the deleteEvidence field. func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.DeleteEvidenceInput) (*types.DeleteEvidencePayload, error) { - svc := r.proboSvc.WithTenant(input.EvidenceID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.EvidenceID.TenantID()) err := svc.Evidences.Delete(ctx, input.EvidenceID) if err != nil { @@ -367,7 +368,7 @@ func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.Delet // CreatePolicy is the resolver for the createPolicy field. func (r *mutationResolver) CreatePolicy(ctx context.Context, input types.CreatePolicyInput) (*types.CreatePolicyPayload, error) { - svc := r.proboSvc.WithTenant(input.OrganizationID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID()) policy, err := svc.Policies.Create(ctx, probo.CreatePolicyRequest{ OrganizationID: input.OrganizationID, @@ -388,7 +389,7 @@ func (r *mutationResolver) CreatePolicy(ctx context.Context, input types.CreateP // UpdatePolicy is the resolver for the updatePolicy field. func (r *mutationResolver) UpdatePolicy(ctx context.Context, input types.UpdatePolicyInput) (*types.UpdatePolicyPayload, error) { - svc := r.proboSvc.WithTenant(input.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID()) policy, err := svc.Policies.Update(ctx, probo.UpdatePolicyRequest{ ID: input.ID, @@ -410,7 +411,7 @@ func (r *mutationResolver) UpdatePolicy(ctx context.Context, input types.UpdateP // DeletePolicy is the resolver for the deletePolicy field. func (r *mutationResolver) DeletePolicy(ctx context.Context, input types.DeletePolicyInput) (*types.DeletePolicyPayload, error) { - svc := r.proboSvc.WithTenant(input.PolicyID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, input.PolicyID.TenantID()) err := svc.Policies.Delete(ctx, input.PolicyID) if err != nil { @@ -424,7 +425,7 @@ func (r *mutationResolver) DeletePolicy(ctx context.Context, input types.DeleteP // Frameworks is the resolver for the frameworks field. func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) @@ -438,7 +439,7 @@ func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organi // Vendors is the resolver for the vendors field. func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) @@ -452,7 +453,7 @@ func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organizat // Peoples is the resolver for the peoples field. func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PeopleConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) @@ -466,7 +467,7 @@ func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organizat // Policies is the resolver for the policies field. func (r *organizationResolver) Policies(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PolicyConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) page, err := svc.Policies.ListByOrganizationID(ctx, obj.ID, cursor) @@ -479,7 +480,7 @@ func (r *organizationResolver) Policies(ctx context.Context, obj *types.Organiza // Owner is the resolver for the owner field. func (r *policyResolver) Owner(ctx context.Context, obj *types.Policy) (*types.People, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) policy, err := svc.Policies.Get(ctx, obj.ID) if err != nil { @@ -497,7 +498,7 @@ func (r *policyResolver) Owner(ctx context.Context, obj *types.Policy) (*types.P // Node is the resolver for the node field. func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) { - svc := r.proboSvc.WithTenant(id.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, id.TenantID()) switch id.EntityType() { case coredata.OrganizationEntityType: @@ -569,12 +570,12 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.User, error) { // Evidences is the resolver for the evidences field. func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error) { - svc := r.proboSvc.WithTenant(obj.ID.TenantID()) + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) cursor := types.NewCursor(first, after, last, before) page, err := svc.Evidences.ListForTaskID(ctx, obj.ID, cursor) if err != nil { - return nil, fmt.Errorf("cannot list task evidences: %w", err) + panic(fmt.Errorf("failed to list task evidences: %w", err)) } return types.NewEvidenceConnection(page), nil @@ -582,29 +583,13 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in // Organizations is the resolver for the organizations field. func (r *userResolver) Organizations(ctx context.Context, obj *types.User, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.OrganizationConnection, error) { - // Get the user's organization IDs - organizationIDs, err := r.usrmgrSvc.GetUserOrganizations(ctx, obj.ID) + organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, obj.ID) if err != nil { - return nil, fmt.Errorf("failed to get user organizations: %w", err) + panic(fmt.Errorf("failed to list organizations for user: %w", err)) } - // If the user doesn't have any organizations, return an empty connection - if len(organizationIDs) == 0 { - return &types.OrganizationConnection{ - Edges: []*types.OrganizationEdge{}, - PageInfo: &types.PageInfo{}, - }, nil - } - - // Get the organization details for each organization ID var edges []*types.OrganizationEdge - for _, organizationID := range organizationIDs { - svc := r.proboSvc.WithTenant(organizationID.TenantID()) - - organization, err := svc.Organizations.Get(ctx, organizationID) - if err != nil { - return nil, fmt.Errorf("failed to get organization details: %w", err) - } + for _, organization := range organizations { edges = append(edges, types.NewOrganizationEdge(organization)) } diff --git a/pkg/usrmgr/usrmgr.go b/pkg/usrmgr/usrmgr.go index 535dc0a86..94b6b8c15 100644 --- a/pkg/usrmgr/usrmgr.go +++ b/pkg/usrmgr/usrmgr.go @@ -264,44 +264,6 @@ func (s Service) GetSession( return session, nil } -func (s Service) RefreshSession( - ctx context.Context, - sessionID gid.GID, -) (*coredata.Session, error) { - session := &coredata.Session{} - - err := s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - if err := session.LoadByID(ctx, tx, sessionID); err != nil { - return &ErrSessionNotFound{message: "session not found"} - } - - // Check if session is expired - if time.Now().After(session.ExpiredAt) { - return &ErrSessionExpired{message: "session expired"} - } - - // Update session expiration - now := time.Now() - session.ExpiredAt = now.Add(24 * time.Hour) - session.UpdatedAt = now - - if err := session.Update(ctx, tx); err != nil { - return fmt.Errorf("cannot update session: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return session, nil -} - func (s Service) GetUserByID( ctx context.Context, userID gid.GID, @@ -337,43 +299,28 @@ func (s Service) GetUserBySession( return s.GetUserByID(ctx, session.UserID) } -// GetUserOrganizations gets all organizations for a user -func (s Service) GetUserOrganizations( +func (s Service) ListOrganizationsForUserID( ctx context.Context, userID gid.GID, -) ([]gid.GID, error) { - q := ` -SELECT - organization_id -FROM - usrmgr_user_organizations -WHERE - user_id = @user_id; -` +) (coredata.Organizations, error) { - args := pgx.StrictNamedArgs{"user_id": userID} + uos := coredata.UserOrganizations{} + organizations := []*coredata.Organization{} - var organizationIDs []gid.GID - - err := s.pg.WithTx( + err := s.pg.WithConn( ctx, - func(tx pg.Conn) error { - rows, err := tx.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("failed to query user organizations: %w", err) + func(conn pg.Conn) error { + if err := uos.ForUserID(ctx, conn, userID); err != nil { + return fmt.Errorf("cannot list user organizations: %w", err) } - defer rows.Close() - for rows.Next() { - var organizationID gid.GID - if err := rows.Scan(&organizationID); err != nil { - return fmt.Errorf("failed to scan organization ID: %w", err) + for _, uo := range uos { + scope := coredata.NewScope(uo.OrganizationID.TenantID()) + organization := &coredata.Organization{} + if err := organization.LoadByID(ctx, conn, scope, uo.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization by id: %w", err) } - organizationIDs = append(organizationIDs, organizationID) - } - - if err := rows.Err(); err != nil { - return fmt.Errorf("error iterating over rows: %w", err) + organizations = append(organizations, organization) } return nil @@ -384,58 +331,55 @@ WHERE return nil, err } - return organizationIDs, nil + return organizations, nil } -// AddUserToOrganization adds a user to an organization -func (s Service) AddUserToOrganization( +func (s Service) ListTenantsForUserID( + ctx context.Context, + userID gid.GID, +) ([]gid.TenantID, error) { + + uos := coredata.UserOrganizations{} + + err := s.pg.WithConn( + ctx, + func(tx pg.Conn) error { + return uos.ForUserID(ctx, tx, userID) + }, + ) + + if err != nil { + return nil, err + } + + tenantIDs := make([]gid.TenantID, len(uos)) + for _, uo := range uos { + tenantIDs = append(tenantIDs, uo.OrganizationID.TenantID()) + } + + return tenantIDs, nil +} + +func (s Service) EnrollUserInOrganization( ctx context.Context, userID gid.GID, organizationID gid.GID, ) error { - q := ` -INSERT INTO - usrmgr_user_organizations (user_id, organization_id, created_at) -VALUES - (@user_id, @organization_id, NOW()) -ON CONFLICT (user_id, organization_id) DO NOTHING; -` - args := pgx.StrictNamedArgs{ - "user_id": userID, - "organization_id": organizationID, + uo := coredata.UserOrganization{ + UserID: userID, + OrganizationID: organizationID, + CreatedAt: time.Now(), } - return s.pg.WithTx( + return s.pg.WithConn( ctx, func(tx pg.Conn) error { - _, err := tx.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("failed to add user to organization: %w", err) - } - return nil + return uo.Insert(ctx, tx) }, ) } -// GetUserIDFromContext gets the user ID from the context -func (s Service) GetUserIDFromContext(ctx context.Context) (gid.GID, error) { - // Get the session ID from the context - sessionID, ok := ctx.Value("session_id").(gid.GID) - if !ok { - return gid.GID{}, fmt.Errorf("no session ID in context") - } - - // Get the session - session, err := s.GetSession(ctx, sessionID) - if err != nil { - return gid.GID{}, fmt.Errorf("failed to get session: %w", err) - } - - return session.UserID, nil -} - -// UpdateSession updates a session in the database func (s Service) UpdateSession( ctx context.Context, session *coredata.Session,