73 lines
3.6 KiB
Go
73 lines
3.6 KiB
Go
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package authsqlite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"gamertan.com/web/organizations"
|
|
)
|
|
|
|
// CreateOwnedOrganization atomically creates a new organization and its first
|
|
// direct owner. It never grants authority in an existing organization.
|
|
func (store *Store) CreateOwnedOrganization(ctx context.Context, setup organizations.OwnedOrganization) error {
|
|
organization, membership, binding := setup.Organization, setup.Membership, setup.OwnerBinding
|
|
audit, accessAudit := setup.OrganizationAudit, setup.AccessAudit
|
|
if !validOrganization(organization) || organization.Status != "active" || organization.Revision != 1 ||
|
|
membership.OrganizationID != organization.ID || !opaqueID(membership.UserID) || membership.Status != "active" || !membership.JoinedAt.Equal(organization.CreatedAt) ||
|
|
!validOwnerBinding(binding, organization.ID, membership.UserID) || !binding.GrantedAt.Equal(organization.CreatedAt) ||
|
|
!validOrganizationAudit(audit, organization.ID) || audit.ActorUserID != membership.UserID || audit.Action != "organization.create" || audit.ResourceType != "organization" || audit.ResourceID != organization.ID ||
|
|
!validAccessAudit(accessAudit) || accessAudit.OrganizationID != organization.ID || accessAudit.ActorUserID != membership.UserID || accessAudit.Action != "access.binding.grant" || accessAudit.ResourceType != "binding" || accessAudit.ResourceID != binding.ID || accessAudit.RequestID != audit.RequestID {
|
|
return errors.New("authsqlite: invalid owned organization")
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var personalOwner any
|
|
if organization.Personal {
|
|
personalOwner = membership.UserID
|
|
}
|
|
// The first statement acquires the writer lock and validates active, completed
|
|
// identity inside the transaction; account suspension cannot race the grant.
|
|
result, err := tx.ExecContext(ctx, `INSERT INTO gwf_organizations(id,slug,name,personal,personal_owner_user_id,created_at,status,revision,updated_at)
|
|
SELECT ?,?,?,?,?,?,?,?,? FROM gwf_users WHERE id=? AND status='active' AND registration_pending=0`,
|
|
organization.ID, organization.Slug, organization.Name, organization.Personal, personalOwner,
|
|
organization.CreatedAt.Unix(), organization.Status, organization.Revision, organization.UpdatedAt.Unix(), membership.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed, err := result.RowsAffected(); err != nil || changed != 1 {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return organizations.ErrOwnerAuthority
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO gwf_organization_memberships(organization_id,user_id,status,joined_at) VALUES(?,?,?,?)`, organization.ID, membership.UserID, membership.Status, membership.JoinedAt.Unix()); err != nil {
|
|
return err
|
|
}
|
|
result, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_bindings(id,organization_id,subject_kind,subject_id,role_name,project_id,environment_id,service_id,granted_by_user_id,granted_at)
|
|
SELECT ?,?,'user',?,?,NULL,NULL,NULL,?,? FROM gwf_access_roles WHERE name=?`,
|
|
binding.ID, organization.ID, membership.UserID, binding.Role, membership.UserID, binding.GrantedAt.Unix(), binding.Role)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed, err := result.RowsAffected(); err != nil || changed != 1 {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors.New("authsqlite: initial owner role has not been seeded")
|
|
}
|
|
if err = appendOrganizationAudit(ctx, tx, audit); err != nil {
|
|
return err
|
|
}
|
|
if err = appendAccessAudit(ctx, tx, accessAudit); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
var _ organizations.OwnedOrganizationRepository = (*Store)(nil)
|