From f375171b13737beb878a6f7bf98f57cc2981da9f Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 13:36:41 -0800 Subject: [PATCH] docs: expand Spring Boot skills and add Go microservice example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - springboot-security: add code examples for authorization, input validation, SQL injection prevention, password encoding, CORS, rate limiting, and secrets management (119 → 261 lines) - springboot-verification: add unit test, Testcontainers integration test, MockMvc API test patterns, and security scan grep commands (100 → 222 lines) - Add Go microservice example (gRPC + PostgreSQL + clean architecture) - Update README directory tree with new example --- README.md | 3 +- examples/go-microservice-CLAUDE.md | 267 ++++++++++++++++++++++++ skills/springboot-security/SKILL.md | 142 +++++++++++++ skills/springboot-verification/SKILL.md | 124 ++++++++++- 4 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 examples/go-microservice-CLAUDE.md diff --git a/README.md b/README.md index ef78ea3c..8c6dc3bc 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,8 @@ everything-claude-code/ |-- examples/ # Example configurations and sessions | |-- CLAUDE.md # Example project-level config | |-- user-CLAUDE.md # Example user-level config -| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe) +| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe) +| |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL) | |-- mcp-configs/ # MCP server configurations | |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc. diff --git a/examples/go-microservice-CLAUDE.md b/examples/go-microservice-CLAUDE.md new file mode 100644 index 00000000..62ad81f1 --- /dev/null +++ b/examples/go-microservice-CLAUDE.md @@ -0,0 +1,267 @@ +# Go Microservice — Project CLAUDE.md + +> Real-world example for a Go microservice with PostgreSQL, gRPC, and Docker. +> Copy this to your project root and customize for your service. + +## Project Overview + +**Stack:** Go 1.22+, PostgreSQL, gRPC + REST (grpc-gateway), Docker, sqlc (type-safe SQL), Wire (dependency injection) + +**Architecture:** Clean architecture with domain, repository, service, and handler layers. gRPC as primary transport with REST gateway for external clients. + +## Critical Rules + +### Go Conventions + +- Follow Effective Go and the Go Code Review Comments guide +- Use `errors.New` / `fmt.Errorf` with `%w` for wrapping — never string matching on errors +- No `init()` functions — explicit initialization in `main()` or constructors +- No global mutable state — pass dependencies via constructors +- Context must be the first parameter and propagated through all layers + +### Database + +- All queries in `queries/` as plain SQL — sqlc generates type-safe Go code +- Migrations in `migrations/` using golang-migrate — never alter the database directly +- Use transactions for multi-step operations via `pgx.Tx` +- All queries must use parameterized placeholders (`$1`, `$2`) — never string formatting + +### Error Handling + +- Return errors, don't panic — panics are only for truly unrecoverable situations +- Wrap errors with context: `fmt.Errorf("creating user: %w", err)` +- Define sentinel errors in `domain/errors.go` for business logic +- Map domain errors to gRPC status codes in the handler layer + +```go +// Domain layer — sentinel errors +var ( + ErrUserNotFound = errors.New("user not found") + ErrEmailTaken = errors.New("email already registered") +) + +// Handler layer — map to gRPC status +func toGRPCError(err error) error { + switch { + case errors.Is(err, domain.ErrUserNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, domain.ErrEmailTaken): + return status.Error(codes.AlreadyExists, err.Error()) + default: + return status.Error(codes.Internal, "internal error") + } +} +``` + +### Code Style + +- No emojis in code or comments +- Exported types and functions must have doc comments +- Keep functions under 50 lines — extract helpers +- Use table-driven tests for all logic with multiple cases +- Prefer `struct{}` for signal channels, not `bool` + +## File Structure + +``` +cmd/ + server/ + main.go # Entrypoint, Wire injection, graceful shutdown +internal/ + domain/ # Business types and interfaces + user.go # User entity and repository interface + errors.go # Sentinel errors + service/ # Business logic + user_service.go + user_service_test.go + repository/ # Data access (sqlc-generated + custom) + postgres/ + user_repo.go + user_repo_test.go # Integration tests with testcontainers + handler/ # gRPC + REST handlers + grpc/ + user_handler.go + rest/ + user_handler.go + config/ # Configuration loading + config.go +proto/ # Protobuf definitions + user/v1/ + user.proto +queries/ # SQL queries for sqlc + user.sql +migrations/ # Database migrations + 001_create_users.up.sql + 001_create_users.down.sql +``` + +## Key Patterns + +### Repository Interface + +```go +type UserRepository interface { + Create(ctx context.Context, user *User) error + FindByID(ctx context.Context, id uuid.UUID) (*User, error) + FindByEmail(ctx context.Context, email string) (*User, error) + Update(ctx context.Context, user *User) error + Delete(ctx context.Context, id uuid.UUID) error +} +``` + +### Service with Dependency Injection + +```go +type UserService struct { + repo domain.UserRepository + hasher PasswordHasher + logger *slog.Logger +} + +func NewUserService(repo domain.UserRepository, hasher PasswordHasher, logger *slog.Logger) *UserService { + return &UserService{repo: repo, hasher: hasher, logger: logger} +} + +func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*domain.User, error) { + existing, err := s.repo.FindByEmail(ctx, req.Email) + if err != nil && !errors.Is(err, domain.ErrUserNotFound) { + return nil, fmt.Errorf("checking email: %w", err) + } + if existing != nil { + return nil, domain.ErrEmailTaken + } + + hashed, err := s.hasher.Hash(req.Password) + if err != nil { + return nil, fmt.Errorf("hashing password: %w", err) + } + + user := &domain.User{ + ID: uuid.New(), + Name: req.Name, + Email: req.Email, + Password: hashed, + } + if err := s.repo.Create(ctx, user); err != nil { + return nil, fmt.Errorf("creating user: %w", err) + } + return user, nil +} +``` + +### Table-Driven Tests + +```go +func TestUserService_Create(t *testing.T) { + tests := []struct { + name string + req CreateUserRequest + setup func(*MockUserRepo) + wantErr error + }{ + { + name: "valid user", + req: CreateUserRequest{Name: "Alice", Email: "alice@example.com", Password: "secure123"}, + setup: func(m *MockUserRepo) { + m.On("FindByEmail", mock.Anything, "alice@example.com").Return(nil, domain.ErrUserNotFound) + m.On("Create", mock.Anything, mock.Anything).Return(nil) + }, + wantErr: nil, + }, + { + name: "duplicate email", + req: CreateUserRequest{Name: "Alice", Email: "taken@example.com", Password: "secure123"}, + setup: func(m *MockUserRepo) { + m.On("FindByEmail", mock.Anything, "taken@example.com").Return(&domain.User{}, nil) + }, + wantErr: domain.ErrEmailTaken, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := new(MockUserRepo) + tt.setup(repo) + svc := NewUserService(repo, &bcryptHasher{}, slog.Default()) + + _, err := svc.Create(context.Background(), tt.req) + + if tt.wantErr != nil { + assert.ErrorIs(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + } + }) + } +} +``` + +## Environment Variables + +```bash +# Database +DATABASE_URL=postgres://user:pass@localhost:5432/myservice?sslmode=disable + +# gRPC +GRPC_PORT=50051 +REST_PORT=8080 + +# Auth +JWT_SECRET= # Load from vault in production +TOKEN_EXPIRY=24h + +# Observability +LOG_LEVEL=info # debug, info, warn, error +OTEL_ENDPOINT= # OpenTelemetry collector +``` + +## Testing Strategy + +```bash +/go-test # TDD workflow for Go +/go-review # Go-specific code review +/go-build # Fix build errors +``` + +### Test Commands + +```bash +# Unit tests (fast, no external deps) +go test ./internal/... -short -count=1 + +# Integration tests (requires Docker for testcontainers) +go test ./internal/repository/... -count=1 -timeout 120s + +# All tests with coverage +go test ./... -coverprofile=coverage.out -count=1 +go tool cover -func=coverage.out # summary +go tool cover -html=coverage.out # browser + +# Race detector +go test ./... -race -count=1 +``` + +## ECC Workflow + +```bash +# Planning +/plan "Add rate limiting to user endpoints" + +# Development +/go-test # TDD with Go-specific patterns + +# Review +/go-review # Go idioms, error handling, concurrency +/security-scan # Secrets and vulnerabilities + +# Before merge +go vet ./... +staticcheck ./... +``` + +## Git Workflow + +- `feat:` new features, `fix:` bug fixes, `refactor:` code changes +- Feature branches from `main`, PRs required +- CI: `go vet`, `staticcheck`, `go test -race`, `golangci-lint` +- Deploy: Docker image built in CI, deployed to Kubernetes diff --git a/skills/springboot-security/SKILL.md b/skills/springboot-security/SKILL.md index f9dc6a29..6ca80d40 100644 --- a/skills/springboot-security/SKILL.md +++ b/skills/springboot-security/SKILL.md @@ -42,17 +42,88 @@ public class JwtAuthFilter extends OncePerRequestFilter { - Use `@PreAuthorize("hasRole('ADMIN')")` or `@PreAuthorize("@authz.canEdit(#id)")` - Deny by default; expose only required scopes +```java +@RestController +@RequestMapping("/api/admin") +public class AdminController { + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/users") + public List listUsers() { + return userService.findAll(); + } + + @PreAuthorize("@authz.isOwner(#id, authentication)") + @DeleteMapping("/users/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + userService.delete(id); + return ResponseEntity.noContent().build(); + } +} +``` + ## Input Validation - Use Bean Validation with `@Valid` on controllers - Apply constraints on DTOs: `@NotBlank`, `@Email`, `@Size`, custom validators - Sanitize any HTML with a whitelist before rendering +```java +// BAD: No validation +@PostMapping("/users") +public User createUser(@RequestBody UserDto dto) { + return userService.create(dto); +} + +// GOOD: Validated DTO +public record CreateUserDto( + @NotBlank @Size(max = 100) String name, + @NotBlank @Email String email, + @NotNull @Min(0) @Max(150) Integer age +) {} + +@PostMapping("/users") +public ResponseEntity createUser(@Valid @RequestBody CreateUserDto dto) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(userService.create(dto)); +} +``` + ## SQL Injection Prevention - Use Spring Data repositories or parameterized queries - For native queries, use `:param` bindings; never concatenate strings +```java +// BAD: String concatenation in native query +@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true) + +// GOOD: Parameterized native query +@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true) +List findByName(@Param("name") String name); + +// GOOD: Spring Data derived query (auto-parameterized) +List findByEmailAndActiveTrue(String email); +``` + +## Password Encoding + +- Always hash passwords with BCrypt or Argon2 — never store plaintext +- Use `PasswordEncoder` bean, not manual hashing + +```java +@Bean +public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); // cost factor 12 +} + +// In service +public User register(CreateUserDto dto) { + String hashedPassword = passwordEncoder.encode(dto.password()); + return userRepository.save(new User(dto.email(), hashedPassword)); +} +``` + ## CSRF Protection - For browser session apps, keep CSRF enabled; include token in forms/headers @@ -70,6 +141,25 @@ http - Keep `application.yml` free of credentials; use placeholders - Rotate tokens and DB credentials regularly +```yaml +# BAD: Hardcoded in application.yml +spring: + datasource: + password: mySecretPassword123 + +# GOOD: Environment variable placeholder +spring: + datasource: + password: ${DB_PASSWORD} + +# GOOD: Spring Cloud Vault integration +spring: + cloud: + vault: + uri: https://vault.example.com + token: ${VAULT_TOKEN} +``` + ## Security Headers ```java @@ -82,11 +172,63 @@ http .referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER))); ``` +## CORS Configuration + +- Configure CORS at the security filter level, not per-controller +- Restrict allowed origins — never use `*` in production + +```java +@Bean +public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("https://app.example.com")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(true); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", config); + return source; +} + +// In SecurityFilterChain: +http.cors(cors -> cors.configurationSource(corsConfigurationSource())); +``` + ## Rate Limiting - Apply Bucket4j or gateway-level limits on expensive endpoints - Log and alert on bursts; return 429 with retry hints +```java +// Using Bucket4j for per-endpoint rate limiting +@Component +public class RateLimitFilter extends OncePerRequestFilter { + private final Map buckets = new ConcurrentHashMap<>(); + + private Bucket createBucket() { + return Bucket.builder() + .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))) + .build(); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String clientIp = request.getRemoteAddr(); + Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createBucket()); + + if (bucket.tryConsume(1)) { + chain.doFilter(request, response); + } else { + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.getWriter().write("{\"error\": \"Rate limit exceeded\"}"); + } + } +} +``` + ## Dependency Security - Run OWASP Dependency Check / Snyk in CI diff --git a/skills/springboot-verification/SKILL.md b/skills/springboot-verification/SKILL.md index abec6e8e..0f280446 100644 --- a/skills/springboot-verification/SKILL.md +++ b/skills/springboot-verification/SKILL.md @@ -42,6 +42,111 @@ Report: - Total tests, passed/failed - Coverage % (lines/branches) +### Unit Tests + +Test service logic in isolation with mocked dependencies: + +```java +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock private UserRepository userRepository; + @InjectMocks private UserService userService; + + @Test + void createUser_validInput_returnsUser() { + var dto = new CreateUserDto("Alice", "alice@example.com"); + var expected = new User(1L, "Alice", "alice@example.com"); + when(userRepository.save(any(User.class))).thenReturn(expected); + + var result = userService.create(dto); + + assertThat(result.name()).isEqualTo("Alice"); + verify(userRepository).save(any(User.class)); + } + + @Test + void createUser_duplicateEmail_throwsException() { + var dto = new CreateUserDto("Alice", "existing@example.com"); + when(userRepository.existsByEmail(dto.email())).thenReturn(true); + + assertThatThrownBy(() -> userService.create(dto)) + .isInstanceOf(DuplicateEmailException.class); + } +} +``` + +### Integration Tests with Testcontainers + +Test against a real database instead of H2: + +```java +@SpringBootTest +@Testcontainers +class UserRepositoryIntegrationTest { + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine") + .withDatabaseName("testdb"); + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + } + + @Autowired private UserRepository userRepository; + + @Test + void findByEmail_existingUser_returnsUser() { + userRepository.save(new User("Alice", "alice@example.com")); + + var found = userRepository.findByEmail("alice@example.com"); + + assertThat(found).isPresent(); + assertThat(found.get().getName()).isEqualTo("Alice"); + } +} +``` + +### API Tests with MockMvc + +Test controller layer with full Spring context: + +```java +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired private MockMvc mockMvc; + @MockBean private UserService userService; + + @Test + void createUser_validInput_returns201() throws Exception { + var user = new UserDto(1L, "Alice", "alice@example.com"); + when(userService.create(any())).thenReturn(user); + + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"name": "Alice", "email": "alice@example.com"} + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("Alice")); + } + + @Test + void createUser_invalidEmail_returns400() throws Exception { + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"name": "Alice", "email": "not-an-email"} + """)) + .andExpect(status().isBadRequest()); + } +} +``` + ## Phase 4: Security Scan ```bash @@ -50,10 +155,27 @@ mvn org.owasp:dependency-check-maven:check # or ./gradlew dependencyCheckAnalyze -# Secrets (git) +# Secrets in source +grep -rn "password\s*=\s*\"" src/ --include="*.java" --include="*.yml" --include="*.properties" +grep -rn "sk-\|api_key\|secret" src/ --include="*.java" --include="*.yml" + +# Secrets (git history) git secrets --scan # if configured ``` +### Common Security Findings + +``` +# Check for System.out.println (use logger instead) +grep -rn "System\.out\.print" src/main/ --include="*.java" + +# Check for raw exception messages in responses +grep -rn "e\.getMessage()" src/main/ --include="*.java" + +# Check for wildcard CORS +grep -rn "allowedOrigins.*\*" src/main/ --include="*.java" +``` + ## Phase 5: Lint/Format (optional gate) ```bash