diff --git a/createFeatureFlag/main.go b/createFeatureFlag/main.go index 7121d47..382ee0a 100644 --- a/createFeatureFlag/main.go +++ b/createFeatureFlag/main.go @@ -70,7 +70,7 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - // Use enhanced middleware with user verification (Week 2 migration) + // Use enhanced middleware with user verification and RBAC (Week 3) jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) if err != nil || jwtResponse.StatusCode != http.StatusOK { return jwtResponse, err @@ -80,6 +80,13 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return utils.ClientError(http.StatusUnauthorized, "User context not available") } + // Check permission: CREATE_FEATURE_FLAG (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionCreateFeatureFlag) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(req.Headers) + return permResponse, err + } + corsHeaders := middleware.GetCORSHeadersV1(req.Headers) err = json.Unmarshal([]byte(req.Body), &createFeatureFlagRequest) diff --git a/createUserFeatureFlag/main.go b/createUserFeatureFlag/main.go index b32437e..66cf0b1 100644 --- a/createUserFeatureFlag/main.go +++ b/createUserFeatureFlag/main.go @@ -64,13 +64,34 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - jwtResponse, _, err := jwt.JWTMiddleware()(req) + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) if err != nil || jwtResponse.StatusCode != http.StatusOK { return jwtResponse, err } + if userContext == nil { + return utils.ClientError(http.StatusUnauthorized, "User context not available") + } + + // Check permission: CREATE_USER_MAPPING (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionCreateUserMapping) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(req.Headers) + return permResponse, err + } + corsHeaders := middleware.GetCORSHeadersV1(req.Headers) + // Check if user can access this resource (own resources or ADMIN) + if !utils.CanAccessUserResource(userContext, userId) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You can only manage your own feature flag mappings", + Headers: corsHeaders, + }, nil + } + var requestBody utils.CreateFeatureFlagUserMappingRequest err = json.Unmarshal([]byte(req.Body), &requestBody) if err != nil { diff --git a/getAllFeatureFlags/main.go b/getAllFeatureFlags/main.go index 7b3f861..1091116 100644 --- a/getAllFeatureFlags/main.go +++ b/getAllFeatureFlags/main.go @@ -54,9 +54,17 @@ func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events return corsResponse, err } - response, _, err := jwt.JWTMiddleware()(request) - if err != nil || response.StatusCode != http.StatusOK { - return response, err + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(request) + if err != nil || jwtResponse.StatusCode != http.StatusOK { + return jwtResponse, err + } + + // Check permission: READ_FEATURE_FLAG + permResponse, err := utils.RequirePermission(userContext, utils.PermissionReadFeatureFlag) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(request.Headers) + return permResponse, err } corsHeaders := middleware.GetCORSHeadersV1(request.Headers) diff --git a/getFeatureFlagById/main.go b/getFeatureFlagById/main.go index 7e258c8..48d63a4 100644 --- a/getFeatureFlagById/main.go +++ b/getFeatureFlagById/main.go @@ -21,9 +21,17 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - response, _, err := jwt.JWTMiddleware()(req) - if err != nil || response.StatusCode != http.StatusOK { - return response, err + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) + if err != nil || jwtResponse.StatusCode != http.StatusOK { + return jwtResponse, err + } + + // Check permission: READ_FEATURE_FLAG + permResponse, err := utils.RequirePermission(userContext, utils.PermissionReadFeatureFlag) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(req.Headers) + return permResponse, err } corsHeaders := middleware.GetCORSHeadersV1(req.Headers) @@ -56,7 +64,7 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return serverErrorResponse, nil } - response = events.APIGatewayProxyResponse{ + response := events.APIGatewayProxyResponse{ StatusCode: http.StatusOK, Headers: corsHeaders, Body: string(jsonResponse), diff --git a/getUserById/main.go b/getUserById/main.go index 127ba85..0f35ebe 100644 --- a/getUserById/main.go +++ b/getUserById/main.go @@ -56,9 +56,14 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - response, _, err := jwt.JWTMiddleware()(req) - if err != nil || response.StatusCode != http.StatusOK { - return response, err + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) + if err != nil || jwtResponse.StatusCode != http.StatusOK { + return jwtResponse, err + } + + if userContext == nil { + return utils.ClientError(http.StatusUnauthorized, "User context not available") } corsHeaders := middleware.GetCORSHeadersV1(req.Headers) @@ -70,8 +75,21 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return clientErrorResponse, nil } - // TODO: Add role-based access control in Week 3 - // Users can only view their own profile unless they're ADMIN + // Check permission: READ_USER (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionReadUser) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = corsHeaders + return permResponse, err + } + + // Check if user can access this resource (own resources or ADMIN) + if !utils.CanAccessUserResource(userContext, userId) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You can only view your own profile", + Headers: corsHeaders, + }, nil + } user, err := getUserById(ctx, db, userId) if err != nil { @@ -104,7 +122,7 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return serverErrorResponse, nil } - response = events.APIGatewayProxyResponse{ + response := events.APIGatewayProxyResponse{ StatusCode: http.StatusOK, Headers: corsHeaders, Body: string(jsonResponse), diff --git a/getUserFeatureFlag/main.go b/getUserFeatureFlag/main.go index 6597d37..fa465f9 100644 --- a/getUserFeatureFlag/main.go +++ b/getUserFeatureFlag/main.go @@ -62,14 +62,31 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - response, _, err := jwt.JWTMiddleware()(req) - if err != nil || response.StatusCode != http.StatusOK { - return response, err + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) + if err != nil || jwtResponse.StatusCode != http.StatusOK { + return jwtResponse, err + } + + // Check permission: READ_USER_MAPPING (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionReadUserMapping) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(req.Headers) + return permResponse, err } corsHeaders := middleware.GetCORSHeadersV1(req.Headers) userId := req.PathParameters["userId"] + + // Check if user can access this resource (own resources or ADMIN) + if !utils.CanAccessUserResource(userContext, userId) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You can only access your own feature flag mappings", + Headers: corsHeaders, + }, nil + } flagId := req.PathParameters["flagId"] diff --git a/getUserFeatureFlags/main.go b/getUserFeatureFlags/main.go index aa0ce01..5513e85 100644 --- a/getUserFeatureFlags/main.go +++ b/getUserFeatureFlags/main.go @@ -62,14 +62,31 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - response, _, err := jwt.JWTMiddleware()(req) - if err != nil || response.StatusCode != http.StatusOK { - return response, err + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) + if err != nil || jwtResponse.StatusCode != http.StatusOK { + return jwtResponse, err + } + + // Check permission: READ_USER_MAPPING (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionReadUserMapping) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(req.Headers) + return permResponse, err } corsHeaders := middleware.GetCORSHeadersV1(req.Headers) userId := req.PathParameters["userId"] + + // Check if user can access this resource (own resources or ADMIN) + if !utils.CanAccessUserResource(userContext, userId) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You can only access your own feature flag mappings", + Headers: corsHeaders, + }, nil + } result, err := processGetById(ctx, userId) if err != nil { return utils.ServerError(err) diff --git a/layer/utils/RBAC.go b/layer/utils/RBAC.go new file mode 100644 index 0000000..0c0f6b8 --- /dev/null +++ b/layer/utils/RBAC.go @@ -0,0 +1,148 @@ +package utils + +import ( + "log" + "net/http" + + "github.com/aws/aws-lambda-go/events" +) + +// Permission represents an action that can be performed +type Permission string + +const ( + // Feature Flag Permissions + PermissionCreateFeatureFlag Permission = "CREATE_FEATURE_FLAG" + PermissionUpdateFeatureFlag Permission = "UPDATE_FEATURE_FLAG" + PermissionReadFeatureFlag Permission = "READ_FEATURE_FLAG" + PermissionDeleteFeatureFlag Permission = "DELETE_FEATURE_FLAG" + + // User Feature Flag Mapping Permissions + PermissionCreateUserMapping Permission = "CREATE_USER_MAPPING" + PermissionUpdateUserMapping Permission = "UPDATE_USER_MAPPING" + PermissionReadUserMapping Permission = "READ_USER_MAPPING" + + // User Management Permissions + PermissionReadUser Permission = "READ_USER" + PermissionUpdateUser Permission = "UPDATE_USER" + PermissionDeleteUser Permission = "DELETE_USER" +) + +// RolePermissions maps roles to their allowed permissions +var RolePermissions = map[string][]Permission{ + ROLE_ADMIN: { + // Feature Flags - Full Access + PermissionCreateFeatureFlag, + PermissionUpdateFeatureFlag, + PermissionReadFeatureFlag, + PermissionDeleteFeatureFlag, + // User Mappings - Full Access + PermissionCreateUserMapping, + PermissionUpdateUserMapping, + PermissionReadUserMapping, + // User Management - Full Access + PermissionReadUser, + PermissionUpdateUser, + PermissionDeleteUser, + }, + ROLE_DEVELOPER: { + // Feature Flags - Create and Update + PermissionCreateFeatureFlag, + PermissionUpdateFeatureFlag, + PermissionReadFeatureFlag, + // User Mappings - Full Access + PermissionCreateUserMapping, + PermissionUpdateUserMapping, + PermissionReadUserMapping, + // User Management - Read Only + PermissionReadUser, + }, + ROLE_VIEWER: { + // Feature Flags - Read Only + PermissionReadFeatureFlag, + // User Mappings - Read Only (own mappings) + PermissionReadUserMapping, + // User Management - Read Own Profile + PermissionReadUser, + }, +} + +// HasPermission checks if a role has a specific permission +func HasPermission(role string, permission Permission) bool { + permissions, exists := RolePermissions[role] + if !exists { + log.Printf("Unknown role: %s", role) + return false + } + + for _, p := range permissions { + if p == permission { + return true + } + } + + return false +} + +// RequirePermission is a middleware helper that checks if user has required permission +func RequirePermission(userContext *UserContext, permission Permission) (events.APIGatewayProxyResponse, error) { + if userContext == nil { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusUnauthorized, + Body: "User context not available", + }, nil + } + + if !HasPermission(userContext.Role, permission) { + log.Printf("User %s with role %s does not have permission %s", userContext.UserId, userContext.Role, permission) + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "Insufficient permissions", + }, nil + } + + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + }, nil +} + +// RequireAnyPermission checks if user has any of the provided permissions +func RequireAnyPermission(userContext *UserContext, permissions ...Permission) (events.APIGatewayProxyResponse, error) { + if userContext == nil { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusUnauthorized, + Body: "User context not available", + }, nil + } + + for _, permission := range permissions { + if HasPermission(userContext.Role, permission) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + }, nil + } + } + + log.Printf("User %s with role %s does not have any of the required permissions", userContext.UserId, userContext.Role) + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "Insufficient permissions", + }, nil +} + +// CanAccessUserResource checks if user can access a resource belonging to another user +// Users can access their own resources, or if they're ADMIN +func CanAccessUserResource(userContext *UserContext, resourceUserId string) bool { + if userContext == nil { + return false + } + + // Users can always access their own resources + if userContext.UserId == resourceUserId { + return true + } + + // Only ADMIN can access other users' resources + return userContext.Role == ROLE_ADMIN +} + diff --git a/layer/utils/RequestResponse.go b/layer/utils/RequestResponse.go index c3ee687..37f1094 100644 --- a/layer/utils/RequestResponse.go +++ b/layer/utils/RequestResponse.go @@ -2,7 +2,7 @@ package utils type UpdateFeatureFlagRequest struct { Status string `json:"status" validate:"required"` - UserId string `json:"userId" validate:"required"` + UserId string `json:"userId"` // Optional - will be set from authenticated user context } type CreateFeatureFlagRequest struct { @@ -24,12 +24,12 @@ type FeatureFlagResponse struct { type CreateFeatureFlagUserMappingRequest struct { Status string `json:"status" validate:"required"` - UserId string `json:"userId" validate:"required"` + UserId string `json:"userId"` // Optional - will be set from authenticated user context } type UpdateFeatureFlagUserMappingRequest struct { Status string `json:"status" validate:"required"` - UserId string `json:"userId" validate:"required"` + UserId string `json:"userId"` // Optional - will be set from authenticated user context } type FeatureFlagUserMappingResponse struct { diff --git a/updateFeatureFlag/main.go b/updateFeatureFlag/main.go index 5166df5..8d0383f 100644 --- a/updateFeatureFlag/main.go +++ b/updateFeatureFlag/main.go @@ -100,10 +100,23 @@ func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events return corsResponse, err } - jwtResponse, _, err := jwt.JWTMiddleware()(request) + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(request) if err != nil || jwtResponse.StatusCode != http.StatusOK { return jwtResponse, err } + + if userContext == nil { + return utils.ClientError(http.StatusUnauthorized, "User context not available") + } + + // Check permission: UPDATE_FEATURE_FLAG (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionUpdateFeatureFlag) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(request.Headers) + return permResponse, err + } + corsHeaders := middleware.GetCORSHeadersV1(request.Headers) updateFeatureFlagRequest := utils.UpdateFeatureFlagRequest{} @@ -117,14 +130,18 @@ func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events } if err := validate.Struct(&updateFeatureFlagRequest); err != nil { - errorMessage := "Check the request body passed status and userId are required." + errorMessage := "Check the request body passed status is required." response := events.APIGatewayProxyResponse{ Body: errorMessage, StatusCode: http.StatusBadRequest, + Headers: corsHeaders, } return response, nil } + // Use userId from authenticated user context (Week 2 migration) + updateFeatureFlagRequest.UserId = userContext.UserId + found := utils.ValidateFeatureFlagStatus(updateFeatureFlagRequest.Status) if !found { response := events.APIGatewayProxyResponse{ diff --git a/updateUser/main.go b/updateUser/main.go index 8ff150f..093cea8 100644 --- a/updateUser/main.go +++ b/updateUser/main.go @@ -116,9 +116,14 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - response, userIdFromToken, err := jwt.JWTMiddleware()(req) - if err != nil || response.StatusCode != http.StatusOK { - return response, err + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) + if err != nil || jwtResponse.StatusCode != http.StatusOK { + return jwtResponse, err + } + + if userContext == nil { + return utils.ClientError(http.StatusUnauthorized, "User context not available") } corsHeaders := middleware.GetCORSHeadersV1(req.Headers) @@ -130,6 +135,43 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return clientErrorResponse, nil } + // Check permission: UPDATE_USER (Week 3 RBAC) + // Only ADMIN can update other users, users can update themselves + if userId != userContext.UserId && userContext.Role != utils.ROLE_ADMIN { + // Check if trying to update role (only ADMIN can do this) + var tempRequest UpdateUserRequest + json.Unmarshal([]byte(req.Body), &tempRequest) + if tempRequest.Role != "" { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "Only ADMIN can update user roles", + Headers: corsHeaders, + }, nil + } + } + + // Check if user can access this resource (own resources or ADMIN) + if !utils.CanAccessUserResource(userContext, userId) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You can only update your own profile", + Headers: corsHeaders, + }, nil + } + + // For non-ADMIN users updating themselves, restrict role updates + if userId == userContext.UserId && userContext.Role != utils.ROLE_ADMIN { + var tempRequest UpdateUserRequest + json.Unmarshal([]byte(req.Body), &tempRequest) + if tempRequest.Role != "" && tempRequest.Role != userContext.Role { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You cannot change your own role", + Headers: corsHeaders, + }, nil + } + } + err = json.Unmarshal([]byte(req.Body), &updateRequest) if err != nil { log.Printf("Error unmarshal request body: %v", err) @@ -144,7 +186,7 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API }, nil } - user, err := updateUser(ctx, db, userId, updateRequest, userIdFromToken) + user, err := updateUser(ctx, db, userId, updateRequest, userContext.UserId) if err != nil { log.Printf("Error while updating user: %v", err) return utils.ServerError(err) @@ -177,7 +219,7 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return utils.ServerError(err) } - response = events.APIGatewayProxyResponse{ + response := events.APIGatewayProxyResponse{ StatusCode: http.StatusOK, Headers: corsHeaders, Body: string(responseBody), diff --git a/updateUserFeatureFlag/main.go b/updateUserFeatureFlag/main.go index 49fd5e4..5ba4c70 100644 --- a/updateUserFeatureFlag/main.go +++ b/updateUserFeatureFlag/main.go @@ -92,13 +92,34 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API return corsResponse, err } - jwtResponse, _, err := jwt.JWTMiddleware()(req) + // Use enhanced middleware with user verification and RBAC (Week 3) + jwtResponse, userContext, err := jwt.JWTMiddlewareWithUserVerification()(req) if err != nil || jwtResponse.StatusCode != http.StatusOK { return jwtResponse, err } + if userContext == nil { + return utils.ClientError(http.StatusUnauthorized, "User context not available") + } + + // Check permission: UPDATE_USER_MAPPING (Week 3 RBAC) + permResponse, err := utils.RequirePermission(userContext, utils.PermissionUpdateUserMapping) + if err != nil || permResponse.StatusCode != http.StatusOK { + permResponse.Headers = middleware.GetCORSHeadersV1(req.Headers) + return permResponse, err + } + corsHeaders := middleware.GetCORSHeadersV1(req.Headers) + // Check if user can access this resource (own resources or ADMIN) + if !utils.CanAccessUserResource(userContext, userId) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusForbidden, + Body: "You can only manage your own feature flag mappings", + Headers: corsHeaders, + }, nil + } + var requestBody utils.UpdateFeatureFlagUserMappingRequest err = json.Unmarshal([]byte(req.Body), &requestBody) if err != nil { @@ -107,14 +128,18 @@ func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.API } if err := validate.Struct(&requestBody); err != nil { - errorMessage := "Check the request body passed status and userId are required." + errorMessage := "Check the request body passed status is required." response := events.APIGatewayProxyResponse{ Body: errorMessage, StatusCode: http.StatusBadRequest, + Headers: corsHeaders, } return response, nil } + // Use userId from authenticated user context (Week 2 migration) + requestBody.UserId = userContext.UserId + found := utils.ValidateFeatureFlagStatus(requestBody.Status) if !found { response := events.APIGatewayProxyResponse{