Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for custom permissions policy (WASM + Node bindings) #1414

Merged
merged 8 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading