Aller au contenu principal

Autorisation

Documentation detaillee des mecanismes d'autorisation de MyTelevision API.

Vue d'ensemble

MyTelevision API implemente un systeme d'autorisation multi-couches :

  1. Role-Based Access Control (RBAC) -- Roles utilisateur avec hierarchie
  2. Profile-Based Access -- Restrictions multi-profil
  3. Content Access Control -- Contenu premium/abonnement
  4. Parental Controls -- Restrictions pour profils enfants
  5. Rate Limiting -- Limites de requetes par profil
  6. Multi-Tenant Isolation -- Isolation par tenant

Architecture d'autorisation

Role-Based Access Control (RBAC)

Roles utilisateur

RoleNiveauDescription
USER1Utilisateur standard
MODERATOR2Moderation du contenu
ADMIN3Acces administratif
SUPER_ADMIN4Acces 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

RessourceUSERMODERATORADMINSUPER_ADMIN
Propre profilCRUDCRUDCRUDCRUD
Autres profils-RCRUDCRUD
ContenuRRUCRUDCRUD
Utilisateurs-RCRUDCRUD
Parametres--CRUDCRUD
Logs d'audit-RRCRUD

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

TypeDescriptionRestrictions
STANDARDProfil acces completAucune
KIDSProfil enfantControles 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

TypeDescriptionExigence
FREEDisponible a tousAucune
PREMIUMContenu premiumAbonnement premium actif
SUBSCRIPTIONContenu abonneTout 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

TierLimiteFenetreCas d'usage
Short3 requetes1 secondeProtection burst
Medium20 requetes10 secondesEndpoints generaux
Long100 requetes1 minuteEndpoints 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

CodeHTTP StatusMessage
AUTH_001401Invalid or expired token
AUTHZ_001403Insufficient role
AUTHZ_002403Subscription required
AUTHZ_003403Premium required
AUTHZ_004403Content restricted
AUTHZ_005403Outside viewing hours
AUTHZ_006403Daily limit exceeded
AUTHZ_007429Rate 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,
});
}