Managing client access across multiple SaaS tools (Linear, Slack, GitHub, Notion) requires centralized permission architecture to prevent drift, enable safe onboarding, and ensure offboarding security. SCIM provisioning and API-driven access management automate client permission sync across platforms. This guide covers permission models, provisioning scripts, and audit strategies for distributed team access control.
Understanding the Permission Management Challenge
Remote teams typically use a stack of tools: project management (Linear, Asana, Jira), communication (Slack, Discord), documentation (Notion, Confluence), code hosting (GitHub, GitLab), and file storage (Google Drive, Dropbox). Each platform has its own permission model, and clients often need access to some—but not all—of these tools.
The core problems emerge quickly: permissions drift as team members add new tools, onboarding new clients requires manual configuration across each platform, and offboarding becomes a security risk when access isn’t systematically revoked.
Build a Centralized Permission Matrix
Start by documenting your permission requirements in a structured format. This becomes your source of truth for both manual configuration and programmatic implementation.
# permission-matrix.yaml
roles:
client_viewer:
description: "Read-only access to project progress"
tools:
linear: "viewer"
slack: "guest"
notion: "read"
github: "read"
drive: "reader"
client_contributor:
description: "Can comment and approve deliverables"
tools:
linear: "commenter"
slack: "member"
notion: "can_edit"
github: "triager"
drive: "commenter"
client_admin:
description: "Full project oversight access"
tools:
linear: "admin"
slack: "admin"
notion: "full_access"
github: "maintain"
drive: "writer"
This YAML structure serves two purposes: it documents your intended permissions and can be processed by automation scripts to configure new client accounts.
Automate Provisioning with Scripted Onboarding
Manual provisioning across five or more tools introduces errors and inconsistencies. A simple script can iterate through your tools and apply the correct permissions based on the assigned role.
#!/usr/bin/env python3
"""Client onboarding script - applies permissions across tools."""
import os
import requests
# Configuration for each tool's API
TOOL_CONFIGS = {
"linear": {
"api_key": os.environ["LINEAR_API_KEY"],
"org": os.environ["LINEAR_ORG"],
"base_url": "https://api.linear.app/graphql"
},
"slack": {
"token": os.environ["SLACK_BOT_TOKEN"],
"base_url": "https://slack.com/api"
},
"notion": {
"token": os.environ["NOTION_TOKEN"],
"base_url": "https://api.notion.com/v1"
}
}
ROLE_PERMISSIONS = {
"client_viewer": {
"linear_team_role": "viewer",
"notion_permission": "read_content",
"slack_channel_access": "invite"
},
"client_contributor": {
"linear_team_role": "commenter",
"notion_permission": "edit_content",
"slack_channel_access": "invite"
}
}
def provision_client(email: str, role: str, client_name: str) -> dict:
"""Provision a new client with the specified role."""
results = {}
permissions = ROLE_PERMISSIONS.get(role, {})
# Provision in Linear
linear_config = TOOL_CONFIGS["linear"]
headers = {"Authorization": linear_config["api_key"]}
# GraphQL mutation to add member
query = """
mutation AddMember($email: String!, $role: String!) {
organizationMembershipInvite(email: $email, role: $role) {
success
}
}
"""
results["linear"] = requests.post(
linear_config["base_url"],
json={"query": query, "variables": {"email": email, "role": permissions.get("linear_team_role")}},
headers=headers
).json()
# Provision in Slack
slack_config = TOOL_CONFIGS["slack"]
headers = {"Authorization": f"Bearer {slack_config['token']}"}
# Invite to client channel
channel_id = f"client-{client_name.lower().replace(' ', '-')}"
results["slack"] = requests.post(
f"{slack_config['base_url']}/conversations.invite",
json={"channel": channel_id, "users": email},
headers=headers
).json()
return results
if __name__ == "__main__":
import sys
email, role, name = sys.argv[1], sys.argv[2], sys.argv[3]
result = provision_client(email, role, name)
print(f"Provisioning complete: {result}")
This script demonstrates the pattern—you’ll need to adapt it to your specific tool versions and APIs. The key principle is centralizing role definitions and applying them consistently.
Implement Time-Bounded Access
Client projects have natural lifecycles, and permissions should expire automatically. Most enterprise tools support temporal access controls.
For GitHub organization access, use team membership expiration:
# .github/team-expiry.yml
client-teams:
- name: "client-acme-q1-2026"
description: "ACME Corp Q1 2026 project team"
members:
- "client-john@acme.com"
- "client-sarah@acme.com"
access_expiry: "2026-03-31"
repositories:
- "project-acme-frontend"
- "project-acme-backend"
For Notion, set up periodic access reviews using their audit logs:
// Notion access review script
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_TOKEN });
async function reviewClientAccess() {
const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
// Query audit logs for client workspace access
const response = await notion.security.analyze({
filter: {
timestamp: { after: ninetyDaysAgo.toISOString() },
actor: { condition: 'ends_with', value: '@client.com' }
}
});
const inactiveClients = response.data.filter(user =>
user.last_activity === null ||
new Date(user.last_activity) < ninetyDaysAgo
);
console.log('Inactive clients requiring access review:', inactiveClients);
return inactiveClients;
}
Audit and Monitor Access Patterns
Regular access audits catch permission drift before it becomes a security issue. Set up quarterly reviews that check three things: whether active clients still need access, whether permissions match their current role, and whether departed clients have been fully removed.
#!/bin/bash
# Quarterly access audit script
echo "=== Client Access Audit Report ==="
echo "Generated: $(date)"
echo ""
# Check GitHub organization members
echo "## GitHub Organization Members"
gh org member list --role outside collaborator --jq '.[] | "\(.login) - Last active: \(.updated_at)"'
# Check Slack guest accounts
echo ""
echo "## Slack Guest Accounts"
curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
"https://slack.com/api/users.list" | jq -r '
.members[] | select(.is_workflow_bot == false) |
select(.is_bot == false) |
select(.enterprise_id != null) |
"\(.name) - ID: \(.id) - Created: \(.created)"
'
# Check Notion shared pages
echo ""
echo "## Notion External Shares"
# Use Notion API to list pages shared externally
Document Your Permission Strategy
Create an internal reference document that answers these questions for each tool:
- What role levels does this tool support?
- Which role should each client tier receive?
- Who has permission to modify client access?
- What is the offboarding checklist for each tool?
This documentation prevents knowledge silos and ensures consistent security practices regardless of who performs onboarding.
Related Articles
- teleport-db-config.yaml
- infrastructure-pods.yaml
- How to Manage Remote Journalism Team Across International
- How to Manage Remote Team Across More Than 8 Timezones Guide
- How to Manage Remote Team Handoffs Across Time Zones: A
Built by theluckystrike — More at zovo.one