Skip to content

Commit

Permalink
Added support for custom permissions policy (WASM + Node bindings) (#…
Browse files Browse the repository at this point in the history
…1414)

* Add custom permissions policy to Node bindings

* Add custom permissions policy to WASM bindings

* Prepare node bindings release

* Prepare WASM bindings release

* Add test

* Support permissions policy updates in WASM bindings

* Support permissions policy updates in Node bindings

* Add test
  • Loading branch information
rygine authored Dec 13, 2024
1 parent 7c7dbdb commit 4336127
Show file tree
Hide file tree
Showing 11 changed files with 425 additions and 27 deletions.
4 changes: 4 additions & 0 deletions bindings_node/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# @xmtp/node-bindings

## 0.0.29

- Added support for custom permission policy sets

## 0.0.28

- Removed `is_installation_authorized` and `is_address_authorized` from `Client`
Expand Down
2 changes: 1 addition & 1 deletion bindings_node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@xmtp/node-bindings",
"version": "0.0.28",
"version": "0.0.29",
"repository": {
"type": "git",
"url": "git+https://[email protected]/xmtp/libxmtp.git",
Expand Down
31 changes: 30 additions & 1 deletion bindings_node/src/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use xmtp_cryptography::signature::ed25519_public_key_to_address;
use xmtp_mls::{
groups::{
group_metadata::GroupMetadata as XmtpGroupMetadata,
group_mutable_metadata::MetadataField as XmtpMetadataField,
intents::PermissionUpdateType as XmtpPermissionUpdateType,
members::PermissionLevel as XmtpPermissionLevel, MlsGroup, UpdateAdminListType,
},
storage::{
Expand All @@ -23,7 +25,7 @@ use crate::{
consent_state::ConsentState,
encoded_content::EncodedContent,
message::{ListMessagesOptions, Message},
permissions::GroupPermissions,
permissions::{GroupPermissions, MetadataField, PermissionPolicy, PermissionUpdateType},
streams::StreamCloser,
ErrorWrapper,
};
Expand Down Expand Up @@ -654,4 +656,31 @@ impl Conversation {

Ok(group.dm_inbox_id().map_err(ErrorWrapper::from)?)
}

#[napi]
pub async fn update_permission_policy(
&self,
permission_update_type: PermissionUpdateType,
permission_policy_option: PermissionPolicy,
metadata_field: Option<MetadataField>,
) -> Result<()> {
let group = MlsGroup::new(
self.inner_client.clone(),
self.group_id.clone(),
self.created_at_ns,
);

group
.update_permission_policy(
XmtpPermissionUpdateType::from(&permission_update_type),
permission_policy_option
.try_into()
.map_err(ErrorWrapper::from)?,
metadata_field.map(|field| XmtpMetadataField::from(&field)),
)
.await
.map_err(ErrorWrapper::from)?;

Ok(())
}
}
29 changes: 26 additions & 3 deletions bindings_node/src/conversations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use xmtp_mls::storage::group::GroupMembershipState as XmtpGroupMembershipState;
use xmtp_mls::storage::group::GroupQueryArgs;

use crate::message::Message;
use crate::permissions::GroupPermissionsOptions;
use crate::permissions::{GroupPermissionsOptions, PermissionPolicySet};
use crate::ErrorWrapper;
use crate::{client::RustXmtpClient, conversation::Conversation, streams::StreamCloser};

Expand Down Expand Up @@ -105,6 +105,7 @@ pub struct CreateGroupOptions {
pub group_image_url_square: Option<String>,
pub group_description: Option<String>,
pub group_pinned_frame_url: Option<String>,
pub custom_permission_policy_set: Option<PermissionPolicySet>,
}

impl CreateGroupOptions {
Expand Down Expand Up @@ -143,21 +144,43 @@ impl Conversations {
group_image_url_square: None,
group_description: None,
group_pinned_frame_url: None,
custom_permission_policy_set: None,
},
};

if let Some(GroupPermissionsOptions::CustomPolicy) = options.permissions {
if options.custom_permission_policy_set.is_none() {
return Err(Error::from_reason("CustomPolicy must include policy set"));
}
} else if options.custom_permission_policy_set.is_some() {
return Err(Error::from_reason(
"Only CustomPolicy may specify a policy set",
));
}

let metadata_options = options.clone().into_group_metadata_options();

let group_permissions = match options.permissions {
Some(GroupPermissionsOptions::AllMembers) => {
Some(PreconfiguredPolicies::AllMembers.to_policy_set())
}
Some(GroupPermissionsOptions::AdminOnly) => {
Some(PreconfiguredPolicies::AdminsOnly.to_policy_set())
}
Some(GroupPermissionsOptions::CustomPolicy) => {
if let Some(policy_set) = options.custom_permission_policy_set {
Some(
policy_set
.try_into()
.map_err(|e| Error::from_reason(format!("{}", e).as_str()))?,
)
} else {
None
}
}
_ => None,
};

let metadata_options = options.clone().into_group_metadata_options();

let convo = if account_addresses.is_empty() {
self
.inner_client
Expand Down
123 changes: 112 additions & 11 deletions bindings_node/src/permissions.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use napi::bindgen_prelude::{Error, Result};
use napi::bindgen_prelude::Result;
use napi_derive::napi;
use std::collections::HashMap;
use xmtp_mls::groups::{
group_mutable_metadata::MetadataField,
group_mutable_metadata::MetadataField as XmtpMetadataField,
group_permissions::{
BasePolicies, GroupMutablePermissions, MembershipPolicies, MetadataBasePolicies,
MetadataPolicies, PermissionsBasePolicies, PermissionsPolicies,
BasePolicies, GroupMutablePermissions, GroupMutablePermissionsError, MembershipPolicies,
MetadataBasePolicies, MetadataPolicies, PermissionsBasePolicies, PermissionsPolicies,
PolicySet,
},
intents::{PermissionPolicyOption, PermissionUpdateType as XmtpPermissionUpdateType},
PreconfiguredPolicies,
Expand Down Expand Up @@ -49,15 +51,15 @@ pub enum PermissionPolicy {
}

impl TryInto<PermissionPolicyOption> for PermissionPolicy {
type Error = Error;
type Error = GroupMutablePermissionsError;

fn try_into(self) -> Result<PermissionPolicyOption> {
fn try_into(self) -> std::result::Result<PermissionPolicyOption, Self::Error> {
match self {
PermissionPolicy::Allow => Ok(PermissionPolicyOption::Allow),
PermissionPolicy::Deny => Ok(PermissionPolicyOption::Deny),
PermissionPolicy::Admin => Ok(PermissionPolicyOption::AdminOnly),
PermissionPolicy::SuperAdmin => Ok(PermissionPolicyOption::SuperAdminOnly),
_ => Err(Error::from_reason("InvalidPermissionPolicyOption")),
_ => Err(GroupMutablePermissionsError::InvalidPermissionPolicyOption),
}
}
}
Expand Down Expand Up @@ -107,7 +109,49 @@ impl From<&PermissionsPolicies> for PermissionPolicy {
}
}

impl TryInto<MetadataPolicies> for PermissionPolicy {
type Error = GroupMutablePermissionsError;

fn try_into(self) -> std::result::Result<MetadataPolicies, GroupMutablePermissionsError> {
match self {
PermissionPolicy::Allow => Ok(MetadataPolicies::allow()),
PermissionPolicy::Deny => Ok(MetadataPolicies::deny()),
PermissionPolicy::Admin => Ok(MetadataPolicies::allow_if_actor_admin()),
PermissionPolicy::SuperAdmin => Ok(MetadataPolicies::allow_if_actor_super_admin()),
_ => Err(GroupMutablePermissionsError::InvalidPermissionPolicyOption),
}
}
}

impl TryInto<PermissionsPolicies> for PermissionPolicy {
type Error = GroupMutablePermissionsError;

fn try_into(self) -> std::result::Result<PermissionsPolicies, Self::Error> {
match self {
PermissionPolicy::Deny => Ok(PermissionsPolicies::deny()),
PermissionPolicy::Admin => Ok(PermissionsPolicies::allow_if_actor_admin()),
PermissionPolicy::SuperAdmin => Ok(PermissionsPolicies::allow_if_actor_super_admin()),
_ => Err(GroupMutablePermissionsError::InvalidPermissionPolicyOption),
}
}
}

impl TryInto<MembershipPolicies> for PermissionPolicy {
type Error = GroupMutablePermissionsError;

fn try_into(self) -> std::result::Result<MembershipPolicies, Self::Error> {
match self {
PermissionPolicy::Allow => Ok(MembershipPolicies::allow()),
PermissionPolicy::Deny => Ok(MembershipPolicies::deny()),
PermissionPolicy::Admin => Ok(MembershipPolicies::allow_if_actor_admin()),
PermissionPolicy::SuperAdmin => Ok(MembershipPolicies::allow_if_actor_super_admin()),
_ => Err(GroupMutablePermissionsError::InvalidPermissionPolicyOption),
}
}
}

#[napi(object)]
#[derive(Clone)]
pub struct PermissionPolicySet {
pub add_member_policy: PermissionPolicy,
pub remove_member_policy: PermissionPolicy,
Expand Down Expand Up @@ -163,10 +207,67 @@ impl GroupPermissions {
remove_member_policy: PermissionPolicy::from(&policy_set.remove_member_policy),
add_admin_policy: PermissionPolicy::from(&policy_set.add_admin_policy),
remove_admin_policy: PermissionPolicy::from(&policy_set.remove_admin_policy),
update_group_name_policy: get_policy(MetadataField::GroupName.as_str()),
update_group_description_policy: get_policy(MetadataField::Description.as_str()),
update_group_image_url_square_policy: get_policy(MetadataField::GroupImageUrlSquare.as_str()),
update_group_pinned_frame_url_policy: get_policy(MetadataField::GroupPinnedFrameUrl.as_str()),
update_group_name_policy: get_policy(XmtpMetadataField::GroupName.as_str()),
update_group_description_policy: get_policy(XmtpMetadataField::Description.as_str()),
update_group_image_url_square_policy: get_policy(
XmtpMetadataField::GroupImageUrlSquare.as_str(),
),
update_group_pinned_frame_url_policy: get_policy(
XmtpMetadataField::GroupPinnedFrameUrl.as_str(),
),
})
}
}

impl TryFrom<PermissionPolicySet> for PolicySet {
type Error = GroupMutablePermissionsError;
fn try_from(
policy_set: PermissionPolicySet,
) -> std::result::Result<Self, GroupMutablePermissionsError> {
let mut metadata_permissions_map: HashMap<String, MetadataPolicies> = HashMap::new();
metadata_permissions_map.insert(
XmtpMetadataField::GroupName.to_string(),
policy_set.update_group_name_policy.try_into()?,
);
metadata_permissions_map.insert(
XmtpMetadataField::Description.to_string(),
policy_set.update_group_description_policy.try_into()?,
);
metadata_permissions_map.insert(
XmtpMetadataField::GroupImageUrlSquare.to_string(),
policy_set.update_group_image_url_square_policy.try_into()?,
);
metadata_permissions_map.insert(
XmtpMetadataField::GroupPinnedFrameUrl.to_string(),
policy_set.update_group_pinned_frame_url_policy.try_into()?,
);

Ok(PolicySet {
add_member_policy: policy_set.add_member_policy.try_into()?,
remove_member_policy: policy_set.remove_member_policy.try_into()?,
add_admin_policy: policy_set.add_admin_policy.try_into()?,
remove_admin_policy: policy_set.remove_admin_policy.try_into()?,
update_metadata_policy: metadata_permissions_map,
update_permissions_policy: PermissionsPolicies::allow_if_actor_super_admin(),
})
}
}

#[napi]
pub enum MetadataField {
GroupName,
Description,
ImageUrlSquare,
PinnedFrameUrl,
}

impl From<&MetadataField> for XmtpMetadataField {
fn from(field: &MetadataField) -> Self {
match field {
MetadataField::GroupName => XmtpMetadataField::GroupName,
MetadataField::Description => XmtpMetadataField::Description,
MetadataField::ImageUrlSquare => XmtpMetadataField::GroupImageUrlSquare,
MetadataField::PinnedFrameUrl => XmtpMetadataField::GroupPinnedFrameUrl,
}
}
}
93 changes: 93 additions & 0 deletions bindings_node/test/Conversations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
Conversation,
GroupPermissionsOptions,
Message,
MetadataField,
PermissionPolicy,
PermissionUpdateType,
} from '../dist'

const SLEEP_MS = 100
Expand Down Expand Up @@ -80,6 +83,96 @@ describe('Conversations', () => {
expect((await client2.conversations().listGroups()).length).toBe(1)
})

it('should create a group with custom permissions', async () => {
const user1 = createUser()
const user2 = createUser()
const client1 = await createRegisteredClient(user1)
const client2 = await createRegisteredClient(user2)
const group = await client1
.conversations()
.createGroup([user2.account.address], {
permissions: GroupPermissionsOptions.CustomPolicy,
customPermissionPolicySet: {
addAdminPolicy: 2,
addMemberPolicy: 3,
removeAdminPolicy: 1,
removeMemberPolicy: 0,
updateGroupNamePolicy: 2,
updateGroupDescriptionPolicy: 1,
updateGroupImageUrlSquarePolicy: 0,
updateGroupPinnedFrameUrlPolicy: 3,
},
})
expect(group).toBeDefined()
expect(group.groupPermissions().policyType()).toBe(
GroupPermissionsOptions.CustomPolicy
)
expect(group.groupPermissions().policySet()).toEqual({
addAdminPolicy: 2,
addMemberPolicy: 3,
removeAdminPolicy: 1,
removeMemberPolicy: 0,
updateGroupNamePolicy: 2,
updateGroupDescriptionPolicy: 1,
updateGroupImageUrlSquarePolicy: 0,
updateGroupPinnedFrameUrlPolicy: 3,
})
})

it('should update group permission policy', async () => {
const user1 = createUser()
const user2 = createUser()
const client1 = await createRegisteredClient(user1)
const client2 = await createRegisteredClient(user2)
const group = await client1
.conversations()
.createGroup([user2.account.address])

expect(group.groupPermissions().policySet()).toEqual({
addMemberPolicy: 0,
removeMemberPolicy: 2,
addAdminPolicy: 3,
removeAdminPolicy: 3,
updateGroupNamePolicy: 0,
updateGroupDescriptionPolicy: 0,
updateGroupImageUrlSquarePolicy: 0,
updateGroupPinnedFrameUrlPolicy: 0,
})

await group.updatePermissionPolicy(
PermissionUpdateType.AddAdmin,
PermissionPolicy.Deny
)

expect(group.groupPermissions().policySet()).toEqual({
addMemberPolicy: 0,
removeMemberPolicy: 2,
addAdminPolicy: 1,
removeAdminPolicy: 3,
updateGroupNamePolicy: 0,
updateGroupDescriptionPolicy: 0,
updateGroupImageUrlSquarePolicy: 0,
updateGroupPinnedFrameUrlPolicy: 0,
})

await group.updatePermissionPolicy(
PermissionUpdateType.UpdateMetadata,
PermissionPolicy.Deny,
MetadataField.GroupName
)

expect(group.groupPermissions().policySet()).toEqual({
addMemberPolicy: 0,
removeMemberPolicy: 2,
addAdminPolicy: 1,
removeAdminPolicy: 3,
updateGroupNamePolicy: 1,
updateGroupDescriptionPolicy: 0,
updateGroupImageUrlSquarePolicy: 0,
updateGroupPinnedFrameUrlPolicy: 0,
})
})

it('should create a dm group', async () => {
const user1 = createUser()
const user2 = createUser()
Expand Down
Loading

0 comments on commit 4336127

Please sign in to comment.