| name | speak-enterprise-rbac |
| description | Configure Speak enterprise SSO, role-based access control, and organization management for language schools.
Use when implementing SSO integration, configuring role-based permissions,
or setting up organization-level controls for enterprise language learning.
Trigger with phrases like "speak SSO", "speak RBAC",
"speak enterprise", "speak roles", "speak permissions", "speak SAML".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Speak Enterprise RBAC
Overview
Configure enterprise-grade access control for Speak language learning integrations in schools, businesses, and organizations.
Prerequisites
- Speak Enterprise tier subscription
- Identity Provider (IdP) with SAML/OIDC support
- Understanding of role-based access patterns
- Audit logging infrastructure
Role Definitions for Language Learning
| Role | Permissions | Use Case |
|---|
| Admin | Full access | Organization administrators |
| Instructor | Create/manage courses, view learner progress | Teachers, tutors |
| Manager | View reports, manage teams | Department heads |
| Learner | Access assigned courses, track own progress | Students, employees |
| Observer | Read-only access to progress | Parents, supervisors |
| Service | API access only | Automated systems |
Role Implementation
enum SpeakRole {
Admin = 'admin',
Instructor = 'instructor',
Manager = 'manager',
Learner = 'learner',
Observer = 'observer',
Service = 'service',
}
interface SpeakPermissions {
createLessons: boolean;
accessAllLanguages: boolean;
assignCourses: boolean;
viewLearnerProgress: boolean;
viewAllProgress: boolean;
manageUsers: boolean;
createContent: boolean;
editContent: boolean;
deleteContent: boolean;
manageBilling: boolean;
manageSettings: boolean;
viewAuditLogs: boolean;
}
const ROLE_PERMISSIONS: Record<SpeakRole, SpeakPermissions> = {
admin: {
createLessons: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
};
(): {
[role][permission];
}
SSO Integration
SAML Configuration
const samlConfig = {
entryPoint: 'https://idp.school.edu/saml/sso',
issuer: 'https://speak.com/saml/metadata',
cert: process.env.SAML_CERT,
callbackUrl: 'https://app.yourschool.com/auth/speak/callback',
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
};
const groupRoleMapping: Record<string, SpeakRole> = {
'Faculty': SpeakRole.Instructor,
'Students': SpeakRole.Learner,
'Staff': SpeakRole.Learner,
'Department-Heads': SpeakRole.Manager,
'IT-Admins': SpeakRole.Admin,
'Parents': SpeakRole.Observer,
};
function mapSamlToRole(samlAttributes: SamlAttributes): SpeakRole {
const groups = samlAttributes.memberOf || [];
for ( [group, role] .(groupRoleMapping)) {
(groups.(group)) {
role;
}
}
.;
}
OAuth2/OIDC Integration
import { OAuth2Client } from '@speak/sdk';
const oauthClient = new OAuth2Client({
clientId: process.env.SPEAK_OAUTH_CLIENT_ID!,
clientSecret: process.env.SPEAK_OAUTH_CLIENT_SECRET!,
redirectUri: 'https://app.yourschool.com/auth/speak/callback',
scopes: [
'lessons:read',
'lessons:write',
'progress:read',
'users:read',
],
});
async function handleOAuthCallback(code: string): Promise<AuthResult> {
const tokens = await oauthClient.exchangeCode(code);
const userInfo = await oauthClient.getUserInfo(tokens.accessToken);
return {
user: {
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
role: mapOidcToRole(userInfo),
},
tokens,
};
}
Organization Management
interface SpeakOrganization {
id: string;
name: string;
type: 'school' | 'business' | 'individual';
ssoEnabled: boolean;
enforceSso: boolean;
allowedDomains: string[];
defaultRole: SpeakRole;
enabledLanguages: string[];
maxSeats: number;
features: {
customContent: boolean;
progressReports: boolean;
instructorDashboard: boolean;
parentPortal: boolean;
apiAccess: boolean;
};
}
async function createOrganization(
config: Partial<SpeakOrganization>
): Promise<SpeakOrganization> {
const org = await speakClient.organizations.create({
name: config.name!,
type: config.type || 'business',
settings: {
: {
: config. || ,
: config. || ,
: config. || [],
},
: {
: config. || .,
: config. || [],
},
: config.,
},
});
({
: ,
: org.,
config,
});
org;
}
Team and Class Management
interface Team {
id: string;
organizationId: string;
name: string;
type: 'class' | 'department' | 'cohort';
instructorIds: string[];
learnerIds: string[];
languages: string[];
curriculum?: CurriculumConfig;
}
class TeamManager {
async createTeam(config: Partial<Team>): Promise<Team> {
const team = await db.teams.insert({
...config,
id: crypto.randomUUID(),
createdAt: new Date(),
});
if (config.learnerIds) {
await this.assignLearnersToTeam(team.id, config.learnerIds);
}
return team;
}
async assignLearnersToTeam(teamId: string, : []): <> {
team = db..({ : teamId });
( learnerId learnerIds) {
db..({
teamId,
: learnerId,
: ,
: (),
: team.,
});
(team.) {
(learnerId, team.);
}
}
}
(: ): <> {
team = db..({ : teamId });
members = db..({ teamId });
progressData = .(
members.( (m) => ({
: m.,
: speakClient..(m.),
}))
);
{
team,
: members.,
: (progressData, ),
: (progressData, ),
: (progressData, ),
: (progressData),
};
}
}
Access Control Middleware
function requireSpeakPermission(
requiredPermission: keyof SpeakPermissions
) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as { speakRole: SpeakRole; organizationId: string };
if (!checkPermission(user.speakRole, requiredPermission)) {
await auditLog({
action: 'permission_denied',
userId: user.id,
permission: requiredPermission,
resource: req.path,
});
return res.status(403).json({
error: 'Forbidden',
message: `Missing permission: ${requiredPermission}`,
});
}
next();
};
}
function requireResourceAccess(resourceType: 'team' | 'learner' | 'content') {
(: , : , : ) => {
user = req.;
resourceId = req..;
hasAccess = (user, resourceType, resourceId);
(!hasAccess) {
res.().({
: ,
: ,
});
}
();
};
}
app.(,
(),
(),
getTeamProgress
);
app.(,
(),
(),
deleteContent
);
Audit Trail
interface SpeakAuditEntry {
timestamp: Date;
userId: string;
role: SpeakRole;
organizationId: string;
action: string;
resource: string;
resourceId?: string;
success: boolean;
ipAddress: string;
userAgent: string;
metadata?: Record<string, any>;
}
async function logSpeakAccess(entry: Omit<SpeakAuditEntry, 'timestamp'>): Promise<void> {
const log: SpeakAuditEntry = { ...entry, timestamp: new Date() };
await auditDb.insert(log);
if (entry.action.includes('delete') && !entry.success) {
await alertOnSuspiciousActivity(entry);
}
(entry);
}
(): <> {
logs = auditDb.({
organizationId,
: { : dateRange., : dateRange. },
});
{
organizationId,
: dateRange,
: logs.,
: (logs, ),
: (logs, ),
: logs.( !l.),
: (logs.( l.)).,
};
}
Output
- Role definitions for education/enterprise
- SSO integration (SAML/OIDC)
- Team and class management
- Permission middleware
- Audit trail enabled
Error Handling
| Issue | Cause | Solution |
|---|
| SSO login fails | Wrong callback URL | Verify IdP config |
| Permission denied | Missing role mapping | Update group mappings |
| Token expired | Short TTL | Refresh token logic |
| Team access denied | Not a member | Check team membership |
Examples
Quick Permission Check
if (!checkPermission(user.role, 'viewLearnerProgress')) {
throw new ForbiddenError('Cannot view learner progress');
}
Instructor Dashboard Access
app.get('/instructor/dashboard',
requireSpeakPermission('viewLearnerProgress'),
async (req, res) => {
const teams = await teamManager.getInstructorTeams(req.user.id);
const progress = await Promise.all(
teams.map(t => teamManager.getTeamProgress(t.id))
);
res.json({ teams, progress });
}
);
Resources
Next Steps
For major migrations, see speak-migration-deep-dive.