Skip to content

feat: add B2B organization directory group synchronization through RabbitMQ.#4828

Merged
shepilov merged 10 commits into
masterfrom
feat/b2b_group_sync
Jul 9, 2026
Merged

feat: add B2B organization directory group synchronization through RabbitMQ.#4828
shepilov merged 10 commits into
masterfrom
feat/b2b_group_sync

Conversation

@shepilov

@shepilov shepilov commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

This introduces support for b2b.group.* lifecycle events on the b2b exchange and replicates managed contact groups, group memberships, and related external contacts across all instances of an organization.

What Changed

  • Added a new stack.b2b.group.lifecycle queue bound to:

    • b2b.group.created
    • b2b.group.updated
    • b2b.group.deleted
    • b2b.group.member.added
    • b2b.group.member.removed
  • Added organization directory group sync logic:

    • creates/updates/deletes managed io.cozy.contacts.groups documents
    • uses stable group IDs derived from organizationId + external group id
    • stores external metadata under twakeDirectory
    • refuses to overwrite/delete unmanaged groups
  • Added membership sync:

    • creates or reuses managed external contacts by email / Cozy URL
    • attaches/removes group relationships on contacts
    • supports members that only have an email, with workplace FQDN as optional data
  • Extended user.created organization contact sync:

    • supports organizationId or organization domain resolution
    • copies existing managed groups and contacts from the organization instance to newly created member instances
  • Added managed directory protections in the data API:

    • managed contacts/groups cannot be created, updated, or deleted through regular data routes
    • only the organization directory sync path owns those documents
  • Added organization instance lookup by org_id:

    • new global by-orgid index
    • org instance and managed directory queries now paginate with CouchDB bookmarks

Fixes #4821

@shepilov shepilov requested a review from a team as a code owner July 2, 2026 08:41
Comment thread model/instance/service.go
Comment thread pkg/couchdb/couchdb.go
Comment thread web/data/data.go Outdated
}

func managedDirectoryWriteError() error {
return jsonapi.Errorf(http.StatusForbidden, "managed organization directory documents are read-only")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a log? Because today it's a nightmare to have a 403 in graylog, but we are not being able to see the reason it was rejected. it's kind of complicated to debug

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be logged upper in the handler

Comment thread model/orgdirectory/metadata.go Outdated
Comment on lines +40 to +45
doc.M[DirectoryMetadataKey] = map[string]interface{}{
"managed": true,
"kind": metadataKindGroup,
"organizationId": organizationID,
"externalId": externalID,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could create a new struct for this directory metadata.

Comment thread web/data/data.go Outdated
Comment on lines +324 to +326
if orgdirectory.IsManagedDirectoryDoctype(doctype) && orgdirectory.IsManagedDirectoryDoc(&doc) {
return managedDirectoryWriteError()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a rejectManagedDirectoryDelete function?

Comment thread model/orgdirectory/organizations.go Outdated
resolvedID := organizationID
if resolvedID == "" {
for _, inst := range list {
if inst == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove these from the list before returning it?

Comment thread model/orgdirectory/contact_patch.go Outdated
Comment thread model/orgdirectory/groups.go
Comment thread model/orgdirectory/groups.go
Comment thread model/orgdirectory/groups.go
Comment thread model/orgdirectory/groups.go Outdated
if err := ctx.Err(); err != nil {
return err
}
copy := cloneForCreateOrUpdate(g, consts.Groups)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the copy here?

Comment thread pkg/rabbitmq/handlers.go
Comment thread pkg/rabbitmq/handlers.go
@shepilov shepilov force-pushed the feat/b2b_group_sync branch from b68696d to b82f6e6 Compare July 9, 2026 13:36
@shepilov shepilov merged commit 71fd016 into master Jul 9, 2026
4 checks passed
@shepilov shepilov deleted the feat/b2b_group_sync branch July 9, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

B2B: RabbitMQ handler to sync organization groups to stack

3 participants