Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1710e08b8 | ||
|
|
3fe1547a5b |
@@ -2,6 +2,29 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## v0.1.0-preview.21 — 2026-09-04
|
||||
|
||||
- Derive the registered credential algorithm from the verified COSE public key
|
||||
embedded in authenticator data instead of the optional browser
|
||||
`publicKeyAlgorithm` convenience member.
|
||||
- Preserve the ES256-only policy while accepting standards-compliant response
|
||||
serializers that omit redundant response conveniences, including the
|
||||
Bitwarden/Vaultwarden passkey flow exercised through Gamertan.
|
||||
- Add regression coverage for an ES256 credential whose convenience algorithm
|
||||
is absent, plus malformed and non-ES256 credential rejection.
|
||||
|
||||
## v0.1.0-preview.20 — 2026-09-04
|
||||
|
||||
- Extend the direct-owner transaction boundary to invitations. Creating or
|
||||
revoking an invitation that grants the configured owner role now requires
|
||||
the actor to remain an active direct owner after the SQLite write lock is
|
||||
acquired.
|
||||
- Preserve application-owned permission policy for ordinary invitations while
|
||||
preventing a broad access-management role, stale ceremony, or alternate
|
||||
repository call from creating or cancelling owner access.
|
||||
- Pass the configured owner role explicitly through invitation repository
|
||||
mutations so non-SQLite adapters cannot silently omit the invariant.
|
||||
|
||||
## v0.1.0-preview.19 — 2026-09-04
|
||||
|
||||
- Require a current active direct owner for every direct-role transition to or
|
||||
|
||||
@@ -17,7 +17,7 @@ router, handlers, HTML, authorization decisions, cache behavior, and
|
||||
deployment. Adopt one boundary at a time; Go compiles and links only the
|
||||
packages you import.
|
||||
|
||||
> **Public preview:** `v0.1.0-preview.19`. APIs may change before a stable
|
||||
> **Public preview:** `v0.1.0-preview.21`. APIs may change before a stable
|
||||
> release. Linux is the maintained release platform.
|
||||
|
||||
## Why Web Foundations?
|
||||
@@ -57,14 +57,14 @@ owns—and, just as importantly, what remains application policy.
|
||||
Pin the preview in an application module:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web@v0.1.0-preview.19
|
||||
go get gamertan.com/web@v0.1.0-preview.21
|
||||
go mod verify
|
||||
```
|
||||
|
||||
An application may name the first package it intends to adopt:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.19
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.21
|
||||
```
|
||||
|
||||
The version belongs to the `gamertan.com/web` module. See the
|
||||
|
||||
@@ -123,8 +123,8 @@ func (store *Store) CreateApplicationService(ctx context.Context, application or
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) CreateInvitation(ctx context.Context, invitation organizations.Invitation, audit organizations.AuditEvent) error {
|
||||
if !opaqueID(invitation.ID) || zeroDigest(invitation.Digest) || !opaqueID(invitation.OrganizationID) || !text(invitation.Email, 320, false) || !opaqueID(invitation.InvitedByUserID) || invitation.DirectRole != "" && !safeName(invitation.DirectRole) || !validInvitationTeamIDs(invitation.TeamIDs) || invitation.CreatedAt.IsZero() || !invitation.ExpiresAt.After(invitation.CreatedAt) || !invitation.UsedAt.IsZero() || !invitation.RevokedAt.IsZero() || !validOrganizationAudit(audit, invitation.OrganizationID) {
|
||||
func (store *Store) CreateInvitation(ctx context.Context, invitation organizations.Invitation, ownerRole string, audit organizations.AuditEvent) error {
|
||||
if !opaqueID(invitation.ID) || zeroDigest(invitation.Digest) || !opaqueID(invitation.OrganizationID) || !text(invitation.Email, 320, false) || !opaqueID(invitation.InvitedByUserID) || invitation.DirectRole != "" && !safeName(invitation.DirectRole) || ownerRole != "" && !safeName(ownerRole) || !validInvitationTeamIDs(invitation.TeamIDs) || invitation.CreatedAt.IsZero() || !invitation.ExpiresAt.After(invitation.CreatedAt) || !invitation.UsedAt.IsZero() || !invitation.RevokedAt.IsZero() || !validOrganizationAudit(audit, invitation.OrganizationID) {
|
||||
return errors.New("authsqlite: invalid invitation")
|
||||
}
|
||||
teamIDs, err := json.Marshal(invitation.TeamIDs)
|
||||
@@ -136,6 +136,18 @@ func (store *Store) CreateInvitation(ctx context.Context, invitation organizatio
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err = lockActiveMembershipActor(ctx, tx, invitation.OrganizationID, invitation.InvitedByUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
if ownerRole != "" && invitation.DirectRole == ownerRole {
|
||||
actorIsOwner, ownerErr := hasDirectOwnerRole(ctx, tx, invitation.OrganizationID, invitation.InvitedByUserID, ownerRole)
|
||||
if ownerErr != nil {
|
||||
return ownerErr
|
||||
}
|
||||
if !actorIsOwner {
|
||||
return organizations.ErrOwnerAuthority
|
||||
}
|
||||
}
|
||||
if err = validateInvitationTeams(ctx, tx, invitation.OrganizationID, invitation.TeamIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -743,8 +755,8 @@ func validateInvitationTeams(ctx context.Context, tx *sql.Tx, organizationID str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) RevokeInvitation(ctx context.Context, organizationID, invitationID string, revokedAt time.Time, audit organizations.AuditEvent) error {
|
||||
if !opaqueID(organizationID) || !opaqueID(invitationID) || revokedAt.IsZero() || !validOrganizationAudit(audit, organizationID) {
|
||||
func (store *Store) RevokeInvitation(ctx context.Context, organizationID, invitationID, ownerRole string, revokedAt time.Time, audit organizations.AuditEvent) error {
|
||||
if !opaqueID(organizationID) || !opaqueID(invitationID) || ownerRole != "" && !safeName(ownerRole) || revokedAt.IsZero() || !validOrganizationAudit(audit, organizationID) {
|
||||
return organizations.ErrInvitationNotFound
|
||||
}
|
||||
tx, err := store.db.BeginTx(ctx, nil)
|
||||
@@ -752,6 +764,25 @@ func (store *Store) RevokeInvitation(ctx context.Context, organizationID, invita
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err = lockActiveMembershipActor(ctx, tx, organizationID, audit.ActorUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
var directRole string
|
||||
if err = tx.QueryRowContext(ctx, `SELECT direct_role FROM gwf_organization_invitations WHERE organization_id=? AND id=? AND used_at IS NULL AND revoked_at IS NULL`, organizationID, invitationID).Scan(&directRole); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return organizations.ErrInvitationNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ownerRole != "" && directRole == ownerRole {
|
||||
actorIsOwner, ownerErr := hasDirectOwnerRole(ctx, tx, organizationID, audit.ActorUserID, ownerRole)
|
||||
if ownerErr != nil {
|
||||
return ownerErr
|
||||
}
|
||||
if !actorIsOwner {
|
||||
return organizations.ErrOwnerAuthority
|
||||
}
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `UPDATE gwf_organization_invitations SET revoked_at=? WHERE organization_id=? AND id=? AND used_at IS NULL AND revoked_at IS NULL`, revokedAt.Unix(), organizationID, invitationID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -653,6 +653,80 @@ func TestOrganizationRoleAdministrationIsAtomicAndProtectsOwners(t *testing.T) {
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_access_bindings WHERE id=?`, replacement.ID, 0)
|
||||
}
|
||||
|
||||
func TestOwnerInvitationsRequireDirectOwnerAuthority(t *testing.T) {
|
||||
store, err := Open(filepath.Join(t.TempDir(), "accounts.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
now := time.Date(2026, 9, 4, 12, 0, 0, 0, time.UTC)
|
||||
authService, err := auth.New(store, auth.Options{Now: func() time.Time { return now }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := authService.CreateUser(t.Context(), auth.CreateUser{Username: "invitation.owner", Email: "invitation-owner@example.test", DisplayName: "Invitation Owner", Password: "correct horse battery staple"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manager, err := authService.CreateUser(t.Context(), auth.CreateUser{Username: "invitation.manager", Email: "invitation-manager@example.test", DisplayName: "Invitation Manager", Password: "correct horse battery staple"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
organizationService, err := organizations.New(store, organizations.Options{Now: func() time.Time { return now }, OwnerRole: "owner"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
organization, err := organizationService.CreateOrganization(t.Context(), organizations.CreateOrganization{Slug: "invitation-authority", Name: "Invitation Authority", OwnerUserID: owner.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _, err := organizationService.Invite(t.Context(), organization.ID, manager.Email, owner.ID, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = organizationService.AcceptInvitation(t.Context(), raw, manager.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policy := access.Policy{
|
||||
Roles: map[string]string{"owner": "Owner", "site-admin": "Site administrator", "viewer": "Viewer"},
|
||||
Permissions: map[string]string{"site.access.manage": "Manage site access"},
|
||||
Grants: map[string][]string{"owner": {"site.access.manage"}, "site-admin": {"site.access.manage"}, "viewer": {}},
|
||||
}
|
||||
accessService, err := access.New(store, policy, access.Options{Now: func() time.Time { return now }, OwnerRole: "owner"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = accessService.Seed(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = accessService.Grant(t.Context(), access.Grant{SubjectKind: access.User, SubjectID: owner.ID, Role: "owner", Scope: access.Scope{OrganizationID: organization.ID}, GrantedBy: owner.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = accessService.Grant(t.Context(), access.Grant{SubjectKind: access.User, SubjectID: manager.ID, Role: "site-admin", Scope: access.Scope{OrganizationID: organization.ID}, GrantedBy: owner.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err = organizationService.InviteWithAccess(t.Context(), organizations.InviteWithAccess{OrganizationID: organization.ID, Email: "blocked-owner@example.test", InvitedByUserID: manager.ID, DirectRole: "owner", Lifetime: time.Hour}); !errors.Is(err, organizations.ErrOwnerAuthority) {
|
||||
t.Fatalf("non-owner owner invitation err=%v", err)
|
||||
}
|
||||
_, viewerInvitation, err := organizationService.InviteWithAccess(t.Context(), organizations.InviteWithAccess{OrganizationID: organization.ID, Email: "viewer@example.test", InvitedByUserID: manager.ID, DirectRole: "viewer", Lifetime: time.Hour})
|
||||
if err != nil {
|
||||
t.Fatalf("non-owner ordinary invitation err=%v", err)
|
||||
}
|
||||
_, ownerInvitation, err := organizationService.InviteWithAccess(t.Context(), organizations.InviteWithAccess{OrganizationID: organization.ID, Email: "new-owner@example.test", InvitedByUserID: owner.ID, DirectRole: "owner", Lifetime: time.Hour})
|
||||
if err != nil {
|
||||
t.Fatalf("owner invitation err=%v", err)
|
||||
}
|
||||
if err = organizationService.RevokeInvitation(t.Context(), organization.ID, ownerInvitation.ID, manager.ID, "request-manager-owner-revoke"); !errors.Is(err, organizations.ErrOwnerAuthority) {
|
||||
t.Fatalf("non-owner owner invitation revocation err=%v", err)
|
||||
}
|
||||
if err = organizationService.RevokeInvitation(t.Context(), organization.ID, viewerInvitation.ID, manager.ID, "request-manager-viewer-revoke"); err != nil {
|
||||
t.Fatalf("ordinary invitation revocation err=%v", err)
|
||||
}
|
||||
if err = organizationService.RevokeInvitation(t.Context(), organization.ID, ownerInvitation.ID, owner.ID, "request-owner-owner-revoke"); err != nil {
|
||||
t.Fatalf("owner invitation revocation err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptimisticMembershipLifecycleIsSerializedAndAtomic(t *testing.T) {
|
||||
store, err := Open(filepath.Join(t.TempDir(), "accounts.db"))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authwebauthn
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gamertan.com/web/internal/webauthnvendored/protocol/webauthncbor"
|
||||
"gamertan.com/web/internal/webauthnvendored/protocol/webauthncose"
|
||||
wa "gamertan.com/web/internal/webauthnvendored/webauthn"
|
||||
)
|
||||
|
||||
func TestEnforceCredentialAlgorithmUsesVerifiedCOSEKey(t *testing.T) {
|
||||
privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKey := privateKey.PublicKey().Bytes()
|
||||
encoded, err := webauthncbor.Marshal(map[int64]any{
|
||||
1: int64(webauthncose.EllipticKey),
|
||||
3: int64(webauthncose.AlgES256),
|
||||
-1: int64(webauthncose.P256),
|
||||
-2: publicKey[1:33],
|
||||
-3: publicKey[33:65],
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
credential := &wa.Credential{
|
||||
PublicKey: encoded,
|
||||
// This value is absent when a standards-compliant client serializes the
|
||||
// mandatory attestation object without optional response conveniences.
|
||||
Attestation: wa.CredentialAttestation{PublicKeyAlgorithm: 0},
|
||||
}
|
||||
if err = enforceCredentialAlgorithm(credential); err != nil {
|
||||
t.Fatalf("verified ES256 COSE key rejected when convenience value was absent: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnforceCredentialAlgorithmRejectsOtherOrInvalidKeys(t *testing.T) {
|
||||
rsaKey, err := webauthncbor.Marshal(map[int64]any{
|
||||
1: int64(webauthncose.RSAKey),
|
||||
3: int64(webauthncose.AlgRS256),
|
||||
-1: []byte{0xff},
|
||||
-2: []byte{0x01, 0x00, 0x01},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, credential := range map[string]*wa.Credential{
|
||||
"nil": nil,
|
||||
"malformed": {PublicKey: []byte("not-cose")},
|
||||
"rsa": {PublicKey: rsaKey},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := enforceCredentialAlgorithm(credential); !errors.Is(err, ErrUnsupportedCredential) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -375,7 +375,7 @@ func (service *Service) finishRegistrationCeremony(ctx context.Context, ceremony
|
||||
if err != nil {
|
||||
return Credential{}, fmt.Errorf("authwebauthn: verify registration: %w", err)
|
||||
}
|
||||
if verified.Attestation.PublicKeyAlgorithm != int64(webauthncose.AlgES256) {
|
||||
if err = enforceCredentialAlgorithm(verified); err != nil {
|
||||
return Credential{}, ErrUnsupportedCredential
|
||||
}
|
||||
encoded, err := json.Marshal(verified)
|
||||
@@ -407,6 +407,26 @@ func (service *Service) finishRegistrationCeremony(ctx context.Context, ceremony
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// enforceCredentialAlgorithm derives the algorithm from the verified COSE key
|
||||
// carried inside authenticator data. AuthenticatorAttestationResponse's
|
||||
// publicKeyAlgorithm member is an optional browser convenience value: clients
|
||||
// that serialize the mandatory attestation object directly may omit it, and it
|
||||
// is not the cryptographically authoritative representation.
|
||||
func enforceCredentialAlgorithm(credential *wa.Credential) error {
|
||||
if credential == nil {
|
||||
return ErrUnsupportedCredential
|
||||
}
|
||||
parsed, err := webauthncose.ParsePublicKey(credential.PublicKey)
|
||||
if err != nil {
|
||||
return ErrUnsupportedCredential
|
||||
}
|
||||
key, ok := parsed.(webauthncose.EC2PublicKeyData)
|
||||
if !ok || key.Algorithm != int64(webauthncose.AlgES256) {
|
||||
return ErrUnsupportedCredential
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) BeginLogin(ctx context.Context) (BeginResult, error) {
|
||||
challenge, err := service.randomBytes(32)
|
||||
if err != nil {
|
||||
|
||||
@@ -79,3 +79,17 @@ application concern belongs in the shared module.
|
||||
that invariant into the same SQLite transactions as direct-role and
|
||||
membership changes, while leaving the application's role vocabulary and UI
|
||||
policy application-owned.
|
||||
- Gamertan's invitation work found the same authority boundary before a route
|
||||
was exposed: Site Admin must be able to invite ordinary staff without being
|
||||
able to grant or cancel Owner access. Preview 20 passes the configured owner
|
||||
role into invitation mutations and rechecks a current active direct Owner
|
||||
after acquiring the SQLite write lock. The application still owns fresh
|
||||
authentication, recipient delivery, and the one-time secret presentation.
|
||||
- A real Bitwarden/Vaultwarden owner enrollment reached successful WebAuthn
|
||||
verification but was rejected by a redundant algorithm check because the
|
||||
application's direct response serializer omitted the optional browser
|
||||
`publicKeyAlgorithm` convenience member. Preview 21 keeps ES256-only policy
|
||||
enforcement but derives it from the verified COSE key embedded in
|
||||
authenticator data. This makes the server independent of serializer-specific
|
||||
convenience fields without weakening origin, challenge, user-verification,
|
||||
or algorithm validation.
|
||||
|
||||
@@ -26,7 +26,7 @@ The packages are ordinary Go imports. Pin the current preview and verify its
|
||||
module checksum:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.19
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.21
|
||||
go mod verify
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import "gamertan.com/web/requestmeta"
|
||||
and request the containing module at an exact version:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.19
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.21
|
||||
```
|
||||
|
||||
Only imported packages are compiled and linked. The packages nevertheless
|
||||
|
||||
@@ -12,6 +12,10 @@ expiring, single-use invitations. An invitation may carry one direct role and
|
||||
up to sixteen reviewed team memberships. Acceptance verifies that the
|
||||
authenticated user's normalized email matches and applies the membership,
|
||||
role, teams, consumption marker, and audit event in one transaction.
|
||||
When `OwnerRole` is configured, creating or revoking an invitation carrying
|
||||
that role additionally requires a current active direct owner inside the same
|
||||
SQLite transaction. A broad access-management permission may administer
|
||||
ordinary invitations but cannot create or cancel owner access.
|
||||
Applications own invitation pages, email or out-of-band delivery, active-source
|
||||
checks before archival, and account recovery.
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ authorization decisions, session cookie, HTML, and local recovery command.
|
||||
- Request no attestation conveyance.
|
||||
- Permit ES256 only until another algorithm has explicit interoperability and
|
||||
security evidence.
|
||||
- Enforce that policy from the verified COSE public key embedded in
|
||||
authenticator data. Do not rely on the optional browser
|
||||
`publicKeyAlgorithm` convenience member: direct standards-compliant response
|
||||
serializers may omit it even when the attested credential is ES256.
|
||||
- Store random challenges and verifier session data only behind opaque,
|
||||
single-use ceremony tokens.
|
||||
- Treat clone warnings as audit signals rather than automatic lockout for
|
||||
|
||||
@@ -27,7 +27,7 @@ var (
|
||||
ErrRevisionConflict = errors.New("organizations: revision conflict")
|
||||
ErrPersonalOrganization = errors.New("organizations: personal organization lifecycle is fixed")
|
||||
ErrLastOwner = errors.New("organizations: the last active direct owner must be preserved")
|
||||
ErrOwnerAuthority = errors.New("organizations: a current direct owner must manage owner memberships")
|
||||
ErrOwnerAuthority = errors.New("organizations: a current direct owner must manage owner access")
|
||||
slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,62}$`)
|
||||
idPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{8,128}$`)
|
||||
)
|
||||
@@ -104,10 +104,10 @@ type Repository interface {
|
||||
CreateProject(context.Context, Project) error
|
||||
CreateEnvironment(context.Context, Environment) error
|
||||
CreateApplicationService(context.Context, ApplicationService) error
|
||||
CreateInvitation(context.Context, Invitation, AuditEvent) error
|
||||
CreateInvitation(context.Context, Invitation, string, AuditEvent) error
|
||||
InvitationByDigest(context.Context, [32]byte, time.Time) (Invitation, error)
|
||||
Invitations(context.Context, string, int) ([]Invitation, error)
|
||||
RevokeInvitation(context.Context, string, string, time.Time, AuditEvent) error
|
||||
RevokeInvitation(context.Context, string, string, string, time.Time, AuditEvent) error
|
||||
AcceptInvitation(context.Context, [32]byte, string, time.Time, AuditEvent) error
|
||||
OrganizationMemberships(context.Context, string, int) ([]Membership, error)
|
||||
MembershipsForUser(context.Context, string) ([]Membership, error)
|
||||
@@ -314,7 +314,7 @@ func (service *Service) InviteWithAccess(ctx context.Context, input InviteWithAc
|
||||
if err != nil {
|
||||
return "", Invitation{}, err
|
||||
}
|
||||
if err = service.repository.CreateInvitation(ctx, invitation, audit); err != nil {
|
||||
if err = service.repository.CreateInvitation(ctx, invitation, service.ownerRole, audit); err != nil {
|
||||
return "", Invitation{}, err
|
||||
}
|
||||
return raw, invitation, nil
|
||||
@@ -559,7 +559,7 @@ func (service *Service) RevokeInvitation(ctx context.Context, organizationID, in
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return service.repository.RevokeInvitation(ctx, organizationID, invitationID, now, audit)
|
||||
return service.repository.RevokeInvitation(ctx, organizationID, invitationID, service.ownerRole, now, audit)
|
||||
}
|
||||
|
||||
func (service *Service) Repository() Repository { return service.repository }
|
||||
|
||||
@@ -100,7 +100,7 @@ func (*repositoryStub) CreateEnvironment(context.Context, Environment) error { r
|
||||
func (*repositoryStub) CreateApplicationService(context.Context, ApplicationService) error {
|
||||
return nil
|
||||
}
|
||||
func (repository *repositoryStub) CreateInvitation(_ context.Context, invitation Invitation, _ AuditEvent) error {
|
||||
func (repository *repositoryStub) CreateInvitation(_ context.Context, invitation Invitation, _ string, _ AuditEvent) error {
|
||||
repository.invitation = invitation
|
||||
return nil
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func (repository *repositoryStub) InvitationByDigest(context.Context, [32]byte,
|
||||
func (*repositoryStub) Invitations(context.Context, string, int) ([]Invitation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*repositoryStub) RevokeInvitation(context.Context, string, string, time.Time, AuditEvent) error {
|
||||
func (*repositoryStub) RevokeInvitation(context.Context, string, string, string, time.Time, AuditEvent) error {
|
||||
return nil
|
||||
}
|
||||
func (repository *repositoryStub) AcceptInvitation(_ context.Context, _ [32]byte, userID string, _ time.Time, _ AuditEvent) error {
|
||||
|
||||
Reference in New Issue
Block a user