Skip to main content
Glama
cbcoutinho

Nextcloud MCP Server

by cbcoutinho
test_oauth_deck_permissions.py12.9 kB
""" Multi-user OAuth tests for Nextcloud Deck board permissions. Tests verify that the MCP server respects Nextcloud Deck board ACL permissions when accessed via OAuth authentication with different users. """ import json import logging import pytest logger = logging.getLogger(__name__) pytestmark = [pytest.mark.integration, pytest.mark.oauth] async def add_board_acl(nc_client, board_id: int, user: str, permission_type: int = 0): """ Helper to add ACL entry to a Deck board. Args: nc_client: Admin NextcloudClient board_id: Board ID user: Username to grant access permission_type: 0=view, 1=edit, 2=manage Returns: ACL entry ID """ acl = await nc_client.deck.add_acl_rule( board_id=board_id, type=0, # 0 = user, 1 = group participant=user, permission_edit=permission_type >= 1, permission_share=permission_type >= 2, permission_manage=permission_type >= 2, ) logger.info(f"Added ACL for board {board_id}: {user} (type={permission_type})") return acl.id async def delete_board_acl(nc_client, board_id: int, acl_id: int): """Helper to delete a board ACL entry.""" await nc_client.deck.delete_acl_rule(board_id, acl_id) logger.info(f"Deleted ACL {acl_id} from board {board_id}") async def test_deck_board_view_permissions( nc_client, alice_mcp_client, bob_mcp_client, diana_mcp_client ): """ Test that Deck boards respect view permissions. Scenario: 1. Admin creates a board as alice 2. Admin adds bob to board with view-only permissions 3. Bob can view the board via MCP tools 4. Diana cannot access the board (no ACL entry) """ # Create a board as alice logger.info("Creating Deck board as alice...") board = await nc_client.deck.create_board( "Alice's Shared Board - View Test", "FF0000" ) board_id = board.id bob_acl_id = None try: # Add bob to board with view-only permission logger.info("Adding bob to board with view permission...") bob_acl_id = await add_board_acl(nc_client, board_id, "bob", permission_type=0) # Test: Bob can view the board via MCP logger.info("Bob attempting to list boards via MCP...") result = await bob_mcp_client.call_tool("deck_get_boards", arguments={}) if not result.isError: response_data = json.loads(result.content[0].text) # The response is directly a list of boards if not isinstance(response_data, list): response_data = [response_data] if response_data else [] board_ids = [b["id"] for b in response_data] logger.info(f"Bob can see {len(response_data)} boards: {board_ids}") # Bob should see the shared board if board_id in board_ids: logger.info(f"Bob can see shared board {board_id}") else: logger.warning(f"Bob cannot see shared board {board_id}") else: logger.warning(f"Bob could not list boards: {result.content}") # Test: Diana cannot see the board logger.info("Diana attempting to list boards via MCP...") result = await diana_mcp_client.call_tool("deck_get_boards", arguments={}) if not result.isError: response_data = json.loads(result.content[0].text) # The response is directly a list of boards if not isinstance(response_data, list): response_data = [response_data] if response_data else [] board_ids = [b["id"] for b in response_data] logger.info(f"Diana can see {len(response_data)} boards") # Diana should NOT see the board assert board_id not in board_ids, "Diana should not see board without ACL" logger.info("Diana correctly cannot see board without ACL") else: logger.warning(f"Diana could not list boards: {result.content}") finally: # Cleanup if bob_acl_id: await delete_board_acl(nc_client, board_id, bob_acl_id) logger.info(f"Deleting board {board_id}") await nc_client.deck.delete_board(board_id) async def test_deck_board_edit_permissions( nc_client, alice_mcp_client, charlie_mcp_client, bob_mcp_client ): """ Test that Deck boards respect edit permissions. Scenario: 1. Admin creates a board as alice with a stack 2. Admin adds charlie with edit permission 3. Admin adds bob with view-only permission 4. Charlie can create cards via MCP tools 5. Bob cannot create cards """ # Create a board as alice logger.info("Creating Deck board as alice...") board = await nc_client.deck.create_board( "Alice's Shared Board - Edit Test", "00FF00" ) board_id = board.id # Create a stack in the board logger.info("Creating stack in board...") stack = await nc_client.deck.create_stack(board_id, "Test Stack", 1) stack_id = stack.id charlie_acl_id = None bob_acl_id = None try: # Add charlie with edit permission logger.info("Adding charlie to board with edit permission...") charlie_acl_id = await add_board_acl( nc_client, board_id, "charlie", permission_type=1 ) # Add bob with view-only permission logger.info("Adding bob to board with view permission...") bob_acl_id = await add_board_acl(nc_client, board_id, "bob", permission_type=0) # Test: Charlie can create a card logger.info("Charlie attempting to create card via MCP...") result = await charlie_mcp_client.call_tool( "deck_create_card", arguments={ "board_id": board_id, "stack_id": stack_id, "title": "Charlie's Card", "description": "Created by Charlie with edit permission", }, ) if not result.isError: response_data = json.loads(result.content[0].text) card_id = response_data.get("id") logger.info(f"Charlie successfully created card {card_id}") # Cleanup the card await nc_client.deck.delete_card(board_id, stack_id, card_id) else: logger.warning(f"Charlie could not create card: {result.content}") # Test: Bob attempts to create a card (should fail) logger.info("Bob attempting to create card via MCP...") result = await bob_mcp_client.call_tool( "deck_create_card", arguments={ "board_id": board_id, "stack_id": stack_id, "title": "Bob's Card", "description": "Bob trying to create a card", }, ) if result.isError: logger.info("Bob correctly denied card creation (view-only)") else: logger.warning("Bob unexpectedly succeeded in creating card") # Cleanup if bob somehow created a card response_data = json.loads(result.content[0].text) if "id" in response_data: await nc_client.deck.delete_card( board_id, stack_id, response_data["id"] ) finally: # Cleanup if charlie_acl_id: await delete_board_acl(nc_client, board_id, charlie_acl_id) if bob_acl_id: await delete_board_acl(nc_client, board_id, bob_acl_id) logger.info(f"Deleting board {board_id}") await nc_client.deck.delete_board(board_id) async def test_deck_board_manage_permissions( nc_client, alice_mcp_client, charlie_mcp_client ): """ Test that Deck boards respect manage permissions. Scenario: 1. Admin creates a board as alice 2. Admin adds charlie with manage permission 3. Charlie can create stacks and modify board settings """ # Create a board as alice logger.info("Creating Deck board as alice...") board = await nc_client.deck.create_board( "Alice's Shared Board - Manage Test", "0000FF" ) board_id = board.id charlie_acl_id = None try: # Add charlie with manage permission logger.info("Adding charlie to board with manage permission...") charlie_acl_id = await add_board_acl( nc_client, board_id, "charlie", permission_type=2 ) # Test: Charlie can create a stack logger.info("Charlie attempting to create stack via MCP...") result = await charlie_mcp_client.call_tool( "deck_create_stack", arguments={"board_id": board_id, "title": "Charlie's Stack", "order": 1}, ) if not result.isError: response_data = json.loads(result.content[0].text) stack_id = response_data.get("id") logger.info(f"Charlie successfully created stack {stack_id}") # Cleanup the stack await nc_client.deck.delete_stack(board_id, stack_id) else: logger.warning(f"Charlie could not create stack: {result.content}") # Test: Charlie can delete a stack (manage permission) logger.info("Charlie attempting to delete stack via MCP...") # First create a temporary stack to delete temp_stack = await nc_client.deck.create_stack( board_id, "Temp Stack for Deletion", 99 ) result = await charlie_mcp_client.call_tool( "deck_delete_stack", arguments={"board_id": board_id, "stack_id": temp_stack.id}, ) if not result.isError: logger.info("Charlie successfully deleted stack") else: logger.warning(f"Charlie could not delete stack: {result.content}") # Cleanup if deletion via MCP failed try: await nc_client.deck.delete_stack(board_id, temp_stack.id) except Exception: pass finally: # Cleanup if charlie_acl_id: await delete_board_acl(nc_client, board_id, charlie_acl_id) logger.info(f"Deleting board {board_id}") await nc_client.deck.delete_board(board_id) async def test_deck_user_isolation(nc_client, alice_mcp_client, bob_mcp_client): """ Test that users can only see their own boards when not shared. Scenario: 1. Admin creates a board as alice (not shared) 2. Admin creates a board as bob (not shared) 3. Alice can only see her own board 4. Bob can only see his own board """ # Create alice's board logger.info("Creating alice's private board...") alice_board = await nc_client.deck.create_board("Alice's Private Board", "FF00FF") alice_board_id = alice_board.id # Create bob's board logger.info("Creating bob's private board...") bob_board = await nc_client.deck.create_board("Bob's Private Board", "00FFFF") bob_board_id = bob_board.id try: # Test: Alice lists boards logger.info("Alice listing boards via MCP...") result = await alice_mcp_client.call_tool("deck_get_boards", arguments={}) if not result.isError: response_data = json.loads(result.content[0].text) # The response is directly a list of boards if not isinstance(response_data, list): response_data = [response_data] if response_data else [] board_ids = [b["id"] for b in response_data] logger.info(f"Alice can see boards: {board_ids}") # Alice should NOT see Bob's board assert bob_board_id not in board_ids, ( "Alice should not see Bob's private board" ) else: logger.warning(f"Alice could not list boards: {result.content}") # Test: Bob lists boards logger.info("Bob listing boards via MCP...") result = await bob_mcp_client.call_tool("deck_get_boards", arguments={}) if not result.isError: response_data = json.loads(result.content[0].text) # The response is directly a list of boards if not isinstance(response_data, list): response_data = [response_data] if response_data else [] board_ids = [b["id"] for b in response_data] logger.info(f"Bob can see boards: {board_ids}") # Bob should NOT see Alice's board assert alice_board_id not in board_ids, ( "Bob should not see Alice's private board" ) else: logger.warning(f"Bob could not list boards: {result.content}") logger.info("User isolation test passed: users can only see their own boards") finally: # Cleanup logger.info("Cleaning up test boards...") await nc_client.deck.delete_board(alice_board_id) await nc_client.deck.delete_board(bob_board_id)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbcoutinho/nextcloud-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server