verify / verify (push) Successful in 4m25s
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
102 lines
4.3 KiB
Go
102 lines
4.3 KiB
Go
// 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
|
|
}
|
|
|
|
var ErrOwnedManagementUnsupported = errors.New("organizations: atomic owner-managed updates are unsupported")
|
|
|
|
// OwnerManagedRepository rechecks the configured direct owner in the same
|
|
// transaction as profile and membership writes. These explicit operations do
|
|
// not change legacy methods used by applications with delegated administrators.
|
|
// Implementations must also preserve optimistic state, last-owner protection,
|
|
// and audit atomicity. There is no preflight-only fallback.
|
|
type OwnerManagedRepository interface {
|
|
UpdateOwnedOrganization(context.Context, Organization, int64, string, AuditEvent) error
|
|
ChangeOwnedMembershipStatus(context.Context, MembershipStatusChange, string, AuditEvent) error
|
|
RemoveOwnedMembershipIfCurrent(context.Context, MembershipRemoval, string, AuditEvent) error
|
|
}
|
|
|
|
// UpdateOwnedOrganization changes a non-personal organization's name and slug.
|
|
// OwnerRole comes from trusted service configuration, not submitted form data.
|
|
func (service *Service) UpdateOwnedOrganization(ctx context.Context, input UpdateOrganization) (Organization, error) {
|
|
return service.updateOrganization(ctx, input, true)
|
|
}
|
|
|
|
// ChangeOwnedMembershipStatus requires a current owner even when the target
|
|
// member is not an owner. It preserves the last active owner.
|
|
func (service *Service) ChangeOwnedMembershipStatus(ctx context.Context, input MembershipStatusChange) error {
|
|
return service.changeMembershipStatus(ctx, input, true)
|
|
}
|
|
|
|
// RemoveOwnedMembershipIfCurrent removes only the displayed membership state,
|
|
// with current owner authority checked in the write transaction.
|
|
func (service *Service) RemoveOwnedMembershipIfCurrent(ctx context.Context, input MembershipRemoval) error {
|
|
return service.removeMembershipIfCurrent(ctx, input, true)
|
|
}
|
|
|
|
// 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
|
|
}
|