Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 29 additions & 95 deletions _examples/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,29 +329,14 @@ func main() {
Tags: []string{"user", "status"},
})

// ====== ROUTES AVEC CONTRÔLE DE RÔLES ======
// ====== ROUTES AVEC CONTRÔLE DE RÔLES (déclaratif via RequiredRoles) ======

// Route pour les utilisateurs (rôle minimum)
// RequiredRoles: vérifié automatiquement avant le handler
// RequiredPermissions: documenté dans la spec OpenAPI
fiberoapi.Get(oapi, "/documents/:documentId",
func(c *fiber.Ctx, input DocumentRequest) (DocumentResponse, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)

// Vérification manuelle des rôles et scopes
if !authService.HasRole(authCtx, "user") {
return DocumentResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'user' role",
Type: "authorization_error",
}
}
if !authService.HasScope(authCtx, "read") {
return DocumentResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'read' scope",
Type: "authorization_error",
}
}

fmt.Printf("📖 User %s (roles: %v) accessing document %s\n", authCtx.UserID, authCtx.Roles, input.DocumentID)

return DocumentResponse{
Expand All @@ -362,32 +347,17 @@ func main() {
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Get document",
Description: "Récupère un document. Nécessite le rôle 'user' et scope 'read'",
Tags: []string{"documents"},
Summary: "Get document",
Description: "Récupère un document. Nécessite le rôle 'user'",
Tags: []string{"documents"},
RequiredRoles: []string{"user"},
RequiredPermissions: []string{"document:read"},
})

// Route pour les éditeurs (peuvent modifier)
fiberoapi.Put(oapi, "/documents/:documentId",
func(c *fiber.Ctx, input UpdateDocumentRequest) (DocumentResponse, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)

// Vérification manuelle des rôles et scopes
if !authService.HasRole(authCtx, "user") {
return DocumentResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'user' role",
Type: "authorization_error",
}
}
if !authService.HasScope(authCtx, "write") {
return DocumentResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'write' scope",
Type: "authorization_error",
}
}

fmt.Printf("✏️ User %s (scopes: %v) updating document %s\n", authCtx.UserID, authCtx.Scopes, input.DocumentID)

return DocumentResponse{
Expand All @@ -398,91 +368,53 @@ func main() {
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Update document",
Description: "Met à jour un document. Nécessite le rôle 'user' et scope 'write'",
Tags: []string{"documents"},
Summary: "Update document",
Description: "Met à jour un document. Nécessite le rôle 'editor'",
Tags: []string{"documents"},
RequiredRoles: []string{"editor"},
RequiredPermissions: []string{"document:write"},
})

// Route pour partager (éditeurs et admins)
// Route pour partager (éditeurs seulement)
fiberoapi.Post(oapi, "/documents/:documentId/share",
func(c *fiber.Ctx, input DocumentRequest) (DocumentShareResponse, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)

// Vérification du scope share
if !authService.HasScope(authCtx, "share") {
return DocumentShareResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'share' scope",
Type: "authorization_error",
}
}

fmt.Printf("🔗 User %s sharing document %s\n", authCtx.UserID, input.DocumentID)

return DocumentShareResponse{
ShareLink: fmt.Sprintf("https://example.com/shared/%s", input.DocumentID),
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Share document",
Description: "Partage un document. Nécessite le scope 'share'",
Tags: []string{"documents", "sharing"},
Summary: "Share document",
Description: "Partage un document. Nécessite le rôle 'editor'",
Tags: []string{"documents", "sharing"},
RequiredRoles: []string{"editor"},
RequiredPermissions: []string{"document:share"},
})

// Route réservée aux administrateurs
fiberoapi.Delete(oapi, "/documents/:documentId",
func(c *fiber.Ctx, input DocumentRequest) (DocumentDeleteResponse, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)

// Vérification du rôle admin et scope delete
if !authService.HasRole(authCtx, "admin") {
return DocumentDeleteResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'admin' role",
Type: "authorization_error",
}
}
if !authService.HasScope(authCtx, "delete") {
return DocumentDeleteResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'delete' scope",
Type: "authorization_error",
}
}

fmt.Printf("🗑️ Admin %s deleting document %s\n", authCtx.UserID, input.DocumentID)

return DocumentDeleteResponse{
Success: true,
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Delete document",
Description: "Supprime un document. Réservé aux administrateurs",
Tags: []string{"documents", "admin"},
Summary: "Delete document",
Description: "Supprime un document. Réservé aux administrateurs",
Tags: []string{"documents", "admin"},
RequiredRoles: []string{"admin"},
RequiredPermissions: []string{"document:delete"},
})

// Route de création d'utilisateur (admin seulement)
fiberoapi.Post(oapi, "/users",
func(c *fiber.Ctx, input CreateUserRequest) (CreateUserResponse, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)

// Vérification du rôle admin et scope write
if !authService.HasRole(authCtx, "admin") {
return CreateUserResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'admin' role",
Type: "authorization_error",
}
}
if !authService.HasScope(authCtx, "write") {
return CreateUserResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Access denied: requires 'write' scope",
Type: "authorization_error",
}
}

fmt.Printf("👤 Admin %s creating user: %s\n", authCtx.UserID, input.Name)

return CreateUserResponse{
Expand All @@ -492,9 +424,11 @@ func main() {
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Create user",
Description: "Crée un nouvel utilisateur. Réservé aux administrateurs",
Tags: []string{"users", "admin"},
Summary: "Create user",
Description: "Crée un nouvel utilisateur. Réservé aux administrateurs",
Tags: []string{"users", "admin"},
RequiredRoles: []string{"admin"},
RequiredPermissions: []string{"user:create"},
})

fmt.Println("🚀 Serveur avec authentification et rôles démarré sur port 3002")
Expand Down
26 changes: 25 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,11 @@ func RoleGuard(validator AuthorizationService, requiredRoles ...string) fiber.Ha

// validateAuthorization validates permissions based on configured security schemes.
// When SecuritySchemes is empty, it falls back to Bearer-only validation for backward compatibility.
func validateAuthorization(c *fiber.Ctx, input interface{}, authService AuthorizationService, config *Config) error {
func validateAuthorization(c *fiber.Ctx, input interface{}, authService AuthorizationService, config *Config, requiredRoles []string) error {
if authService == nil {
if len(requiredRoles) > 0 {
return &AuthError{StatusCode: 500, Message: "authorization service not configured"}
}
return nil
}

Expand All @@ -165,6 +168,11 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz
return &AuthError{StatusCode: 401, Message: err.Error()}
}
c.Locals("auth", authCtx)

// Check roles before resource access
if err := checkRequiredRoles(authCtx, authService, requiredRoles); err != nil {
return err
}
return validateResourceAccess(c, authCtx, input, authService)
}

Expand All @@ -182,6 +190,11 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz
authCtx, err := validateSecurityRequirement(c, requirement, config.SecuritySchemes, authService)
if err == nil {
c.Locals("auth", authCtx)

// Check roles before resource access
if err := checkRequiredRoles(authCtx, authService, requiredRoles); err != nil {
return err
}
return validateResourceAccess(c, authCtx, input, authService)
}
var authErr *AuthError
Expand All @@ -203,6 +216,17 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz
return &AuthError{StatusCode: 401, Message: lastErr.Error()}
}

// checkRequiredRoles checks that the authenticated user has all required roles.
// Called inside validateAuthorization after auth context is established, before resource access checks.
func checkRequiredRoles(authCtx *AuthContext, authService AuthorizationService, requiredRoles []string) error {
for _, role := range requiredRoles {
if !authService.HasRole(authCtx, role) {
return &AuthError{StatusCode: 403, Message: fmt.Sprintf("required role missing: %s", role)}
}
}
return nil
}

// validateResourceAccess validates resource access based on tags
func validateResourceAccess(c *fiber.Ctx, authCtx *AuthContext, input interface{}, authService AuthorizationService) error {
inputValue := reflect.ValueOf(input)
Expand Down
6 changes: 6 additions & 0 deletions auth_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ func WithSecurityDisabled(options OpenAPIOptions) OpenAPIOptions {
return options
}

// WithRoles adds required roles to a route (checked automatically during auth)
func WithRoles(options OpenAPIOptions, roles ...string) OpenAPIOptions {
options.RequiredRoles = append(options.RequiredRoles, roles...)
return options
}

// WithPermissions adds required permissions for documentation
func WithPermissions(options OpenAPIOptions, permissions ...string) OpenAPIOptions {
options.RequiredPermissions = append(options.RequiredPermissions, permissions...)
Expand Down
Loading
Loading