Autorisation
Documentation detaillee des mecanismes d'autorisation de MyTelevision API.
Vue d'ensemble
MyTelevision API implemente un systeme d'autorisation multi-couches :
- Role-Based Access Control (RBAC) -- Roles utilisateur avec hierarchie
- Profile-Based Access -- Restrictions multi-profil
- Content Access Control -- Contenu premium/abonnement
- Parental Controls -- Restrictions pour profils enfants
- Rate Limiting -- Limites de requetes par profil
- Multi-Tenant Isolation -- Isolation par tenant
Architecture d'autorisation
Role-Based Access Control (RBAC)
Roles utilisateur
| Role | Niveau | Description |
|---|---|---|
USER | 1 | Utilisateur standard |
MODERATOR | 2 | Moderation du contenu |
ADMIN | 3 | Acces administratif |
SUPER_ADMIN | 4 | Acces systeme complet |
Hierarchie des roles
const roleHierarchy = {
SUPER_ADMIN: ['ADMIN', 'MODERATOR', 'USER'],
ADMIN: ['MODERATOR', 'USER'],
MODERATOR: ['USER'],
USER: [],
};
// SUPER_ADMIN herite de toutes les permissions
// ADMIN peut faire tout ce que MODERATOR et USER peuvent
Implementation du RolesGuard
// roles.decorator.ts
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
'roles',
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return this.hasRequiredRole(user.role, requiredRoles);
}
private hasRequiredRole(
userRole: UserRole,
requiredRoles: UserRole[],
): boolean {
const inheritedRoles = roleHierarchy[userRole] || [];
const allUserRoles = [userRole, ...inheritedRoles];
return requiredRoles.some((role) => allUserRoles.includes(role));
}
}
Utilisation des guards de roles
// Endpoint reserve aux admins
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@Get('admin/users')
async getUsers() { ... }
// Moderateur ou superieur
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.MODERATOR)
@Patch('content/:id/moderate')
async moderateContent() { ... }
Matrice de controle d'acces
| Ressource | USER | MODERATOR | ADMIN | SUPER_ADMIN |
|---|---|---|---|---|
| Propre profil | CRUD | CRUD | CRUD | CRUD |
| Autres profils | - | R | CRUD | CRUD |
| Contenu | R | RU | CRUD | CRUD |
| Utilisateurs | - | R | CRUD | CRUD |
| Parametres | - | - | CRUD | CRUD |
| Logs d'audit | - | R | R | CRUD |
PermissionGuard -- Permissions fines
Le PermissionGuard offre un controle plus granulaire que le RolesGuard, avec support des modes any et all :
@Injectable()
export class PermissionGuard implements CanActivate {
// Mode ANY : l'utilisateur doit avoir au moins UNE permission
// Mode ALL : l'utilisateur doit avoir TOUTES les permissions
}
Tenant context
Le PermissionGuard genere un warning log si un tenant context est detecte mais non valide (fix PRIV-001), permettant d'identifier les cas de mauvaise utilisation.
Tests : 24 tests unitaires couvrent le PermissionGuard (modes any/all, detection de mauvaise utilisation).
Acces base sur les profils
Types de profils
| Type | Description | Restrictions |
|---|---|---|
STANDARD | Profil acces complet | Aucune |
KIDS | Profil enfant | Controles parentaux actifs |
Contexte de profil
interface ProfileContext {
accountId: string;
profileId: string;
profileType: ProfileType;
restrictions?: ProfileRestrictions;
}
// Attache a la requete apres authentification
interface RequestWithProfile extends Request {
user: User;
profile: ProfileContext;
}
Isolation des profils
// Assurer que les utilisateurs n'accedent qu'a leurs propres profils
@Injectable()
export class ProfileOwnershipGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const { accountId } = request.user;
const profileId = request.params.profileId || request.body.profileId;
if (!profileId) return true;
return this.profileBelongsToAccount(profileId, accountId);
}
}
Invariants critiques des profils
- Maximum 4 profils par compte (configurable par tenant)
- Profil par defaut ne peut pas etre supprime
- Restrictions des profils enfants appliquees cote serveur (jamais faire confiance au client)
Controle d'acces au contenu
Types d'acces
| Type | Description | Exigence |
|---|---|---|
FREE | Disponible a tous | Aucune |
PREMIUM | Contenu premium | Abonnement premium actif |
SUBSCRIPTION | Contenu abonne | Tout abonnement actif |
ContentAccessGuard
@Injectable()
export class ContentAccessGuard implements CanActivate {
constructor(private readonly subscriptionService: SubscriptionService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const contentId = request.params.id;
const { accountId } = request.user;
const content = await this.getContent(contentId);
if (content.accessType === AccessType.FREE) {
return true;
}
const subscription = await this.subscriptionService.getActive(accountId);
if (!subscription) {
throw new ForbiddenException({
code: 'SUBSCRIPTION_REQUIRED',
message: 'This content requires an active subscription',
});
}
if (content.accessType === AccessType.PREMIUM) {
if (!this.isPremiumPlan(subscription.planId)) {
throw new ForbiddenException({
code: 'PREMIUM_REQUIRED',
message: 'This content requires a premium subscription',
});
}
}
return true;
}
}
Controles parentaux
Types de restrictions
interface ProfileRestrictions {
maxAgeRating: string; // ex: '7+', '13+', '16+', '18+'
blockedCategories: string[]; // ex: ['horror', 'violence']
allowedMediaTypes: MediaType[]; // ex: ['MOVIE', 'SERIES']
timeWindows: TimeWindow[]; // Horaires de visionnage autorises
dailyLimitMinutes: number; // Temps max de visionnage par jour
}
interface TimeWindow {
startTime: string; // "08:00"
endTime: string; // "20:00"
daysOfWeek: number[]; // [1,2,3,4,5] = Lun-Ven
}
KidsProfileGuard
@Injectable()
export class KidsProfileGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { profile } = request;
// Verifier uniquement pour les profils KIDS
if (profile.profileType !== ProfileType.KIDS) {
return true;
}
const restrictions = profile.restrictions;
const contentId = request.params.id;
const content = await this.getContent(contentId);
// Verifier la classification d'age
if (!this.isAgeAppropriate(content.ageRating, restrictions.maxAgeRating)) {
throw new ForbiddenException({
code: 'AGE_RESTRICTED',
message: 'This content is not appropriate for this profile',
});
}
// Verifier les categories bloquees
if (
this.hasBlockedCategory(
content.categories,
restrictions.blockedCategories,
)
) {
throw new ForbiddenException({
code: 'CATEGORY_BLOCKED',
message: 'This content category is blocked for this profile',
});
}
// Verifier les horaires autorises
if (!this.isWithinAllowedHours(restrictions.timeWindows)) {
throw new ForbiddenException({
code: 'OUTSIDE_VIEWING_HOURS',
message: 'Viewing is not allowed at this time',
});
}
// Verifier la limite quotidienne
if (
await this.hasExceededDailyLimit(
profile.id,
restrictions.dailyLimitMinutes,
)
) {
throw new ForbiddenException({
code: 'DAILY_LIMIT_EXCEEDED',
message: 'Daily viewing limit has been reached',
});
}
return true;
}
}
Configuration par defaut des profils enfants (par tenant)
{
kidsDefaults: {
maxAgeRating: '7+',
allowedCategories: ['animation', 'education', 'family'],
dailyLimitMinutes: 120
}
}
Rate Limiting
Tiers de rate limiting
| Tier | Limite | Fenetre | Cas d'usage |
|---|---|---|---|
| Short | 3 requetes | 1 seconde | Protection burst |
| Medium | 20 requetes | 10 secondes | Endpoints generaux |
| Long | 100 requetes | 1 minute | Endpoints lourds |
Cle de rate limiting
Le rate limiting est applique par profil, pas par utilisateur :
const rateLimitKey = `ratelimit:${tenantId}:${profileId}:${tier}`;
Cela empeche :
- Le contournement par changement de profil
- L'abus de partage de compte
- Les attaques par bots
Implementation
@Injectable()
export class ProfileRateLimitGuard implements CanActivate {
constructor(private readonly redis: Redis) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { profile, tenant } = request;
const limits = this.getTierLimits(tenant);
for (const tier of ['short', 'medium', 'long']) {
const key = `ratelimit:${tenant.id}:${profile.id}:${tier}`;
const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, limits[tier].window);
}
if (current > limits[tier].limit) {
throw new ThrottlerException(`Rate limit exceeded (${tier})`);
}
}
return true;
}
}
Limites specifiques par endpoint
// Limites plus strictes sur les endpoints sensibles
@Throttle({ short: { ttl: 1000, limit: 1 } })
@Post('auth/login')
async login() { ... }
// Limites assouplies pour les lectures
@Throttle({ long: { ttl: 60000, limit: 200 } })
@Get('movies')
async listMovies() { ... }
Isolation multi-tenant
TenantGuard
@Injectable()
export class TenantGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const tenantId = this.resolveTenant(request);
if (!tenantId) {
throw new BadRequestException('Tenant not identified');
}
// Attacher le tenant a la requete
request.tenant = tenantId;
return true;
}
private resolveTenant(request: Request): string | null {
// Priorite : Header > Sous-domaine > Mapping domaine
return (
request.headers['x-tenant-id'] ||
this.extractFromSubdomain(request.hostname) ||
this.lookupByDomain(request.hostname)
);
}
}
Requetes scopees par tenant
// Toujours filtrer par tenant
async findMovies(tenantId: string, pagination: PaginationDto) {
return this.prisma.movie.findMany({
where: { tenantId },
...pagination,
});
}
Configuration multi-tenant
{
maxProfilesPerAccount: 4, // Par defaut : 4
maxDevicesPerAccount: 5, // Par defaut : 5
maxConcurrentSessions: 4, // Par defaut : 4
rateLimitOverrides: {
short: 100, // Par minute
medium: 1000, // Par heure
long: 10000 // Par jour
}
}
Propriete des ressources
Validation de propriete
@Injectable()
export class OwnershipGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const resourceId = request.params.id;
const { user } = request;
const resource = await this.getResource(resourceId);
if (resource.userId !== user.id && user.role !== UserRole.ADMIN) {
throw new ForbiddenException('You do not own this resource');
}
return true;
}
}
Patterns courants de propriete
// L'utilisateur ne peut acceder qu'a ses propres favoris
@Get('favorites')
@UseGuards(JwtAuthGuard)
async getFavorites(@CurrentUser() user: User) {
return this.favoritesService.findByUserId(user.id);
}
// L'utilisateur ne peut supprimer que son propre profil (sauf default)
@Delete('profiles/:id')
@UseGuards(JwtAuthGuard, ProfileOwnershipGuard)
async deleteProfile(
@Param('id') id: string,
@CurrentProfile() profile: Profile,
) {
if (profile.isDefault) {
throw new ForbiddenException('Cannot delete default profile');
}
return this.profilesService.delete(id);
}
Defense en profondeur
// Empilement de guards multiples
@UseGuards(
TenantGuard, // 1. Identifier le tenant
AccountAuthGuard, // 2. Verifier l'authentification
RolesGuard, // 3. Verifier le role
ProfileGuard, // 4. Valider le profil
ContentAccessGuard, // 5. Verifier l'acces au contenu
KidsProfileGuard, // 6. Appliquer les restrictions
ProfileRateLimitGuard, // 7. Rate limiting
)
@Get('premium-content/:id')
async getPremiumContent() { ... }
Principe Fail Secure
// Refus par defaut -- exiger des permissions explicites
@Injectable()
export class SecureGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// Si quelque chose manque, refuser l'acces
if (!request.user || !request.profile || !request.tenant) {
return false;
}
return true;
}
}
Codes d'erreur d'autorisation
| Code | HTTP Status | Message |
|---|---|---|
| AUTH_001 | 401 | Invalid or expired token |
| AUTHZ_001 | 403 | Insufficient role |
| AUTHZ_002 | 403 | Subscription required |
| AUTHZ_003 | 403 | Premium required |
| AUTHZ_004 | 403 | Content restricted |
| AUTHZ_005 | 403 | Outside viewing hours |
| AUTHZ_006 | 403 | Daily limit exceeded |
| AUTHZ_007 | 429 | Rate limit exceeded |
Piste d'audit
// Logger toutes les decisions d'autorisation
async logAuthzDecision(context: AuthzContext) {
await this.auditService.log({
timestamp: new Date(),
event: 'AUTHORIZATION',
userId: context.userId,
profileId: context.profileId,
resource: context.resource,
action: context.action,
decision: context.allowed ? 'ALLOW' : 'DENY',
reason: context.reason,
ip: context.ip,
});
}