// SPDX-License-Identifier: MPL-2.0 package organizations import ( "context" "errors" "gamertan.com/web/access" ) // OwnedOrganization is one atomic creation command. Implementations must commit // the organization, membership, direct owner binding, and both audits together. type OwnedOrganization struct { Organization Organization Membership Membership OwnerBinding access.Binding OrganizationAudit AuditEvent AccessAudit access.AuditEvent } // OwnedOrganizationRepository extends Repository without changing the legacy // membership-only CreateOrganization contract. There is no non-atomic fallback. type OwnedOrganizationRepository interface { CreateOwnedOrganization(context.Context, OwnedOrganization) error } // CreateOwnedOrganization grants the configured OwnerRole to the initial owner // inside the creation transaction. Applications authorize creation and choose // OwnerRole when constructing the service, never from a submitted role name. // The role must already be seeded in the repository. func (service *Service) CreateOwnedOrganization(ctx context.Context, input CreateOrganization) (Organization, error) { if service.ownerRole == "" { return Organization{}, errors.New("organizations: owned creation requires a configured owner role") } repository, ok := service.repository.(OwnedOrganizationRepository) if !ok { return Organization{}, ErrOwnedCreationUnsupported } organization, membership, audit, err := service.prepareOrganization(input) if err != nil { return Organization{}, err } bindingID, err := token(service.random, 18) if err != nil { return Organization{}, err } accessAuditID, err := token(service.random, 18) if err != nil { return Organization{}, err } binding := access.Binding{ ID: bindingID, SubjectKind: access.User, SubjectID: input.OwnerUserID, Role: service.ownerRole, Scope: access.Scope{OrganizationID: organization.ID}, GrantedBy: input.OwnerUserID, GrantedAt: organization.CreatedAt, } accessAudit := access.AuditEvent{ ID: accessAuditID, OrganizationID: organization.ID, ActorUserID: input.OwnerUserID, Action: "access.binding.grant", ResourceType: "binding", ResourceID: bindingID, RequestID: input.RequestID, Summary: "Initial organization owner granted", CreatedAt: organization.CreatedAt, } if err = repository.CreateOwnedOrganization(ctx, OwnedOrganization{ Organization: organization, Membership: membership, OwnerBinding: binding, OrganizationAudit: audit, AccessAudit: accessAudit, }); err != nil { return Organization{}, err } return organization, nil }