From 0d85c8d1737d33054a9def9ca15d8a174436c995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Proen=C3=A7a?= Date: Wed, 21 May 2025 00:00:31 +0100 Subject: [PATCH] Revert "Authentication & Authorization with Keycloak" --- code/docker-compose.yml | 21 -- code/grpc-gateway/main.go | 103 ++----- code/grpc-gateway/middleware/auth.go | 163 ----------- code/grpc-gateway/middleware/auth_routes.go | 274 ------------------ code/keycloak/realm-export.json | 53 ---- code/kubernetes/keycloak/configmap.yaml | 10 - code/kubernetes/keycloak/deployment.yaml | 63 ---- code/kubernetes/keycloak/realm-configmap.yaml | 46 --- code/kubernetes/keycloak/secrets.yaml | 11 - code/kubernetes/scripts/deploy.sh | 19 +- code/kubernetes/scripts/keycloak-ops.sh | 65 ----- code/kubernetes/traefik/ingress-routes.yaml | 54 ---- code/services/auth/auth.go | 120 -------- code/traefik/traefik.yml | 51 ---- 14 files changed, 28 insertions(+), 1025 deletions(-) delete mode 100644 code/grpc-gateway/middleware/auth.go delete mode 100644 code/grpc-gateway/middleware/auth_routes.go delete mode 100644 code/keycloak/realm-export.json delete mode 100644 code/kubernetes/keycloak/configmap.yaml delete mode 100644 code/kubernetes/keycloak/deployment.yaml delete mode 100644 code/kubernetes/keycloak/realm-configmap.yaml delete mode 100644 code/kubernetes/keycloak/secrets.yaml delete mode 100644 code/kubernetes/scripts/keycloak-ops.sh delete mode 100644 code/kubernetes/traefik/ingress-routes.yaml delete mode 100644 code/services/auth/auth.go diff --git a/code/docker-compose.yml b/code/docker-compose.yml index ba4b038..261f24f 100644 --- a/code/docker-compose.yml +++ b/code/docker-compose.yml @@ -194,27 +194,6 @@ services: networks: - threadit-network - keycloak: - image: quay.io/keycloak/keycloak:21.1 - container_name: keycloak - restart: always - command: - - start-dev - - --import-realm - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} - KC_HOSTNAME_STRICT: false - KC_HOSTNAME_STRICT_HTTPS: false - KC_HTTP_ENABLED: "true" - KC_PROXY: edge - volumes: - - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro - ports: - - "${KEYCLOAK_PORT}:8080" - networks: - - threadit-network - volumes: db_data: driver: local diff --git a/code/grpc-gateway/main.go b/code/grpc-gateway/main.go index e58e362..3f5bbfe 100644 --- a/code/grpc-gateway/main.go +++ b/code/grpc-gateway/main.go @@ -17,7 +17,6 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "threadit/grpc-gateway/middleware" "google.golang.org/protobuf/types/known/emptypb" ) @@ -109,24 +108,8 @@ func handleHealthCheck(w http.ResponseWriter, r *http.Request) { } func main() { - ctx := context.Background() - mux := runtime.NewServeMux() - - // Initialize auth handler - authHandler := middleware.NewAuthHandler( - os.Getenv("KEYCLOAK_URL"), - os.Getenv("KEYCLOAK_CLIENT_ID"), - os.Getenv("KEYCLOAK_CLIENT_SECRET"), - os.Getenv("KEYCLOAK_REALM"), - ) - - // Create a new ServeMux for both gRPC-Gateway and auth routes - httpMux := http.NewServeMux() + gwmux := runtime.NewServeMux() - // Register auth routes - authHandler.RegisterRoutes(httpMux) - - // gRPC dial options with message size configurations opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions( @@ -135,80 +118,48 @@ func main() { ), } - // Register gRPC-Gateway routes with auth middleware - httpMux.Handle("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Auth middleware for API routes - authMiddleware := middleware.NewAuthMiddleware(middleware.KeycloakConfig{ - Realm: os.Getenv("KEYCLOAK_REALM"), - ClientID: os.Getenv("KEYCLOAK_CLIENT_ID"), - ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"), - KeycloakURL: os.Getenv("KEYCLOAK_URL"), - }) - - authMiddleware.Handler(mux).ServeHTTP(w, r) - })) - - // Register service handlers - if err := registerServices(ctx, mux, opts); err != nil { - log.Fatalf("Failed to register services: %v", err) - } - - port := os.Getenv("GRPC_GATEWAY_PORT") - if port == "" { - log.Fatalf("missing GRPC_GATEWAY_PORT env var") + err := communitypb.RegisterCommunityServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - log.Printf("gRPC Gateway server listening on :%s", port) - if err := http.ListenAndServe(":"+port, httpMux); err != nil { - log.Fatalf("Failed to serve: %v", err) + err = threadpb.RegisterThreadServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } -} -func registerServices(ctx context.Context, mux *runtime.ServeMux, opts []grpc.DialOption) error { - // Register Community Service - if err := communitypb.RegisterCommunityServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("COMMUNITY_SERVICE_HOST", "COMMUNITY_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register community service: %v", err) + err = commentpb.RegisterCommentServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - // Register Thread Service - if err := threadpb.RegisterThreadServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("THREAD_SERVICE_HOST", "THREAD_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register thread service: %v", err) + err = votepb.RegisterVoteServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - // Register Comment Service - if err := commentpb.RegisterCommentServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("COMMENT_SERVICE_HOST", "COMMENT_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register comment service: %v", err) + err = searchpb.RegisterSearchServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } - // Register Vote Service - if err := votepb.RegisterVoteServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("VOTE_SERVICE_HOST", "VOTE_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register vote service: %v", err) + err = popularpb.RegisterPopularServiceHandlerFromEndpoint(context.Background(), gwmux, getGrpcServerAddress("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT"), opts) + if err != nil { + log.Fatalf("Failed to register gRPC gateway: %v", err) } http.HandleFunc("/health", handleHealthCheck) + http.Handle("/", gwmux) - // Register Search Service - if err := searchpb.RegisterSearchServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("SEARCH_SERVICE_HOST", "SEARCH_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register search service: %v", err) + port := os.Getenv("GRPC_GATEWAY_PORT") + if port == "" { + log.Fatalf("missing GRPC_GATEWAY_PORT env var") } - // Register Popular Service - if err := popularpb.RegisterPopularServiceHandlerFromEndpoint( - ctx, mux, getGrpcServerAddress("POPULAR_SERVICE_HOST", "POPULAR_SERVICE_PORT"), opts, - ); err != nil { - return fmt.Errorf("failed to register popular service: %v", err) + log.Printf("gRPC Gateway server listening on :%s", port) + err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) + if err != nil { + log.Fatalf("Failed to start HTTP server: %v", err) } - - return nil } diff --git a/code/grpc-gateway/middleware/auth.go b/code/grpc-gateway/middleware/auth.go deleted file mode 100644 index 95607b1..0000000 --- a/code/grpc-gateway/middleware/auth.go +++ /dev/null @@ -1,163 +0,0 @@ -package middleware - -import ( - "context" - "net/http" - "strings" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "google.golang.org/grpc/metadata" - "your-module/code/services/auth" -) - -type AuthMiddleware struct { - keycloak *auth.KeycloakClient -} - -func NewAuthMiddleware(config auth.KeycloakConfig) (*AuthMiddleware, error) { - kc, err := auth.NewKeycloakClient(config) - if err != nil { - return nil, err - } - return &AuthMiddleware{keycloak: kc}, nil -} - -func (am *AuthMiddleware) Handler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip auth for public endpoints - if isPublicEndpoint(r.URL.Path, r.Method) { - next.ServeHTTP(w, r) - return - } - - // Extract token from Authorization header - token, err := auth.ExtractBearerToken(r.Header.Get("Authorization")) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Validate token - claims, err := am.keycloak.ValidateToken(r.Context(), token) - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - // Check required roles for protected endpoints - if !hasRequiredRole(r.URL.Path, claims) { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - - // Add user info to context - ctx := context.WithValue(r.Context(), "user_claims", claims) - - // Forward token to gRPC services - md := metadata.Pairs("authorization", "Bearer "+token) - ctx = metadata.NewOutgoingContext(ctx, md) - - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func isPublicEndpoint(path, method string) bool { - // Auth endpoints are always public - authPaths := []string{ - "/auth/login", - "/auth/register", - "/auth/logout", - } - for _, ap := range authPaths { - if path == ap { - return true - } - } - - // Only GET requests can be public for these paths - if method != http.MethodGet { - return false - } - - publicGetPaths := []string{ - "/communities", - "/threads", - "/comments", - "/search", - "/search/thread", - "/search/community", - "/popular/threads", - "/popular/comments", - } - - // Check exact matches for list endpoints - for _, pp := range publicGetPaths { - if path == pp { - return true - } - } - - // Check id based paths - idBasedPaths := []string{ - "/communities/", - "/threads/", - "/comments/", - } - - for _, pp := range idBasedPaths { - if strings.HasPrefix(path, pp) && path != pp { - return true - } - } - - return false -} - -func hasRequiredRole(path string, claims *auth.TokenClaims) bool { - roleRequirements := map[string]string{ - // Communities - "POST /communities": "user", - "PATCH /communities/": "moderator", - "DELETE /communities/": "moderator", - - // Threads - "POST /threads": "user", - "PATCH /threads/": "user", - "DELETE /threads/": "user", - - // Comment sdpoints - "POST /comments": "user", - "PATCH /comments/": "user", - "DELETE /comments/": "user", - - // Votes - "POST /votes/thread/": "user", - "POST /votes/comment/": "user", - - // Admin - "POST /admin/": "admin", - "PUT /admin/": "admin", - "DELETE /admin/": "admin", - } - - // Check each role requirement - for pathPattern, requiredRole := range roleRequirements { - parts := strings.SplitN(pathPattern, " ", 2) - method, pattern := parts[0], parts[1] - if strings.HasPrefix(path, pattern) { - return claims.RealmAccess.Roles != nil && contains(claims.RealmAccess.Roles, requiredRole) - } - } - - // If no specific role requirement, allow access - return true -} - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} \ No newline at end of file diff --git a/code/grpc-gateway/middleware/auth_routes.go b/code/grpc-gateway/middleware/auth_routes.go deleted file mode 100644 index 0c2799d..0000000 --- a/code/grpc-gateway/middleware/auth_routes.go +++ /dev/null @@ -1,274 +0,0 @@ -package middleware - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "regexp" - "strings" - "time" -) - -type AuthHandler struct { - keycloakURL string - clientID string - clientSecret string - realm string - adminClientID string -} - -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type RegisterRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` -} - -type ErrorResponse struct { - Error string `json:"error"` - Description string `json:"error_description,omitempty"` -} - -func NewAuthHandler(keycloakURL, clientID, clientSecret, realm string) *AuthHandler { - return &AuthHandler{ - keycloakURL: keycloakURL, - clientID: clientID, - clientSecret: clientSecret, - realm: realm, - adminClientID: "admin-cli", - } -} - -func (h *AuthHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("/auth/register", h.handleRegister) - mux.HandleFunc("/auth/login", h.handleLogin) - mux.HandleFunc("/auth/logout", h.handleLogout) -} - -func (h *AuthHandler) validateRegister(req *RegisterRequest) error { - // Username validation - if len(req.Username) < 3 || len(req.Username) > 30 { - return fmt.Errorf("username must be between 3 and 30 characters") - } - if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(req.Username) { - return fmt.Errorf("username can only contain letters, numbers, underscores, and hyphens") - } - - // Email validation - emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - if !emailRegex.MatchString(req.Email) { - return fmt.Errorf("invalid email format") - } - - // Password validation - if len(req.Password) < 8 { - return fmt.Errorf("password must be at least 8 characters long") - } - if !regexp.MustCompile(`[A-Z]`).MatchString(req.Password) { - return fmt.Errorf("password must contain at least one uppercase letter") - } - if !regexp.MustCompile(`[a-z]`).MatchString(req.Password) { - return fmt.Errorf("password must contain at least one lowercase letter") - } - if !regexp.MustCompile(`[0-9]`).MatchString(req.Password) { - return fmt.Errorf("password must contain at least one number") - } - - return nil -} - -func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req RegisterRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - // Validate registration data - if err := h.validateRegister(&req); err != nil { - h.sendError(w, err.Error(), http.StatusBadRequest) - return - } - - // Create user in Keycloak - keycloakURL := fmt.Sprintf("%s/auth/admin/realms/%s/users", h.keycloakURL, h.realm) - userData := map[string]interface{}{ - "username": req.Username, - "email": req.Email, - "enabled": true, - "credentials": []map[string]interface{}{ - { - "type": "password", - "value": req.Password, - "temporary": false, - }, - }, - } - - jsonData, err := json.Marshal(userData) - if err != nil { - h.sendError(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Get admin token - adminToken, err := h.getAdminToken() - if err != nil { - h.sendError(w, "Failed to authenticate with Keycloak", http.StatusInternalServerError) - return - } - - request, err := http.NewRequest(http.MethodPost, keycloakURL, strings.NewReader(string(jsonData))) - if err != nil { - h.sendError(w, "Internal server error", http.StatusInternalServerError) - return - } - - request.Header.Set("Content-Type", "application/json") - request.Header.Set("Authorization", "Bearer "+adminToken) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(request) - if err != nil { - h.sendError(w, "Failed to register user", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - body, _ := io.ReadAll(resp.Body) - h.sendError(w, fmt.Sprintf("Failed to register user: %s", string(body)), resp.StatusCode) - return - } - - // Auto login after registration - h.performLogin(w, req.Username, req.Password) -} - -func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - h.performLogin(w, req.Username, req.Password) -} - -func (h *AuthHandler) performLogin(w http.ResponseWriter, username, password string) { - tokenURL := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", h.keycloakURL, h.realm) - data := url.Values{} - data.Set("grant_type", "password") - data.Set("client_id", h.clientID) - data.Set("client_secret", h.clientSecret) - data.Set("username", username) - data.Set("password", password) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.PostForm(tokenURL, data) - if err != nil { - h.sendError(w, "Failed to login", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - h.sendError(w, "Invalid credentials", http.StatusUnauthorized) - return - } - - // Forward Keycloak response (tokens) to client - w.Header().Set("Content-Type", "application/json") - io.Copy(w, resp.Body) -} - -func (h *AuthHandler) handleLogout(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - h.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - token := r.Header.Get("Authorization") - if token == "" { - h.sendError(w, "No token provided", http.StatusBadRequest) - return - } - - logoutURL := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/logout", h.keycloakURL, h.realm) - request, err := http.NewRequest(http.MethodPost, logoutURL, nil) - if err != nil { - h.sendError(w, "Internal server error", http.StatusInternalServerError) - return - } - - request.Header.Set("Authorization", token) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(request) - if err != nil { - h.sendError(w, "Failed to logout", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - h.sendError(w, "Failed to logout", resp.StatusCode) - return - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"message": "Logged out successfully"}) -} - -func (h *AuthHandler) getAdminToken() (string, error) { - tokenURL := fmt.Sprintf("%s/auth/realms/master/protocol/openid-connect/token", h.keycloakURL) - data := url.Values{} - data.Set("grant_type", "client_credentials") - data.Set("client_id", h.adminClientID) - data.Set("client_secret", h.clientSecret) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.PostForm(tokenURL, data) - if err != nil { - return "", fmt.Errorf("failed to get admin token: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get admin token: status %d", resp.StatusCode) - } - - var result struct { - AccessToken string `json:"access_token"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", fmt.Errorf("failed to decode admin token response: %v", err) - } - - return result.AccessToken, nil -} - -func (h *AuthHandler) sendError(w http.ResponseWriter, message string, status int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(ErrorResponse{ - Error: http.StatusText(status), - Description: message, - }) -} \ No newline at end of file diff --git a/code/keycloak/realm-export.json b/code/keycloak/realm-export.json deleted file mode 100644 index 9452d7e..0000000 --- a/code/keycloak/realm-export.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "id": "threadit", - "realm": "threadit", - "enabled": true, - "roles": { - "realm": [ - { - "name": "user", - "description": "User role" - }, - { - "name": "moderator", - "description": "Community moderator role" - }, - { - "name": "admin", - "description": "Admin role" - } - ] - }, - "defaultRoles": ["user"], - "clients": [ - { - "clientId": "threadit-api", - "enabled": true, - "protocol": "openid-connect", - "publicClient": false, - "clientAuthenticatorType": "client-secret", - "secret": "${CLIENT_SECRET}", - "redirectUris": ["*"], - "webOrigins": ["*"], - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true - } - ], - "users": [ - { - "username": "admin", - "enabled": true, - "credentials": [ - { - "type": "password", - "value": "${ADMIN_PASSWORD}", - "temporary": false - } - ], - "realmRoles": ["admin"] - } - ] -} \ No newline at end of file diff --git a/code/kubernetes/keycloak/configmap.yaml b/code/kubernetes/keycloak/configmap.yaml deleted file mode 100644 index ea226ad..0000000 --- a/code/kubernetes/keycloak/configmap.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: keycloak-config - namespace: threadit -data: - KC_HOSTNAME_STRICT: "false" - KC_HOSTNAME_STRICT_HTTPS: "false" - KC_HTTP_ENABLED: "true" - KC_PROXY: "edge" \ No newline at end of file diff --git a/code/kubernetes/keycloak/deployment.yaml b/code/kubernetes/keycloak/deployment.yaml deleted file mode 100644 index d2b08bc..0000000 --- a/code/kubernetes/keycloak/deployment.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: keycloak - namespace: threadit - labels: - app: keycloak -spec: - ports: - - port: 8080 - targetPort: 8080 - protocol: TCP - selector: - app: keycloak ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: keycloak - namespace: threadit - labels: - app: keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: keycloak - template: - metadata: - labels: - app: keycloak - spec: - containers: - - name: keycloak - image: quay.io/keycloak/keycloak:21.1 - args: ["start-dev", "--import-realm"] - ports: - - containerPort: 8080 - envFrom: - - configMapRef: - name: keycloak-config - - secretRef: - name: keycloak-secrets - volumeMounts: - - name: realm-config - mountPath: /opt/keycloak/data/import - readOnly: true - readinessProbe: - httpGet: - path: /auth/realms/master - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /auth/realms/master - port: 8080 - initialDelaySeconds: 60 - periodSeconds: 15 - volumes: - - name: realm-config - configMap: - name: keycloak-realm-config \ No newline at end of file diff --git a/code/kubernetes/keycloak/realm-configmap.yaml b/code/kubernetes/keycloak/realm-configmap.yaml deleted file mode 100644 index 5a1a905..0000000 --- a/code/kubernetes/keycloak/realm-configmap.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: keycloak-realm-config - namespace: threadit -data: - realm.json: | - { - "id": "threadit", - "realm": "threadit", - "enabled": true, - "roles": { - "realm": [ - { - "name": "user", - "description": "User role" - }, - { - "name": "moderator", - "description": "Community moderator role" - }, - { - "name": "admin", - "description": "Admin role" - } - ] - }, - "defaultRoles": ["user"], - "clients": [ - { - "clientId": "threadit-api", - "enabled": true, - "protocol": "openid-connect", - "publicClient": false, - "clientAuthenticatorType": "client-secret", - "secret": "${CLIENT_SECRET}", - "redirectUris": ["*"], - "webOrigins": ["*"], - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true - } - ] - } \ No newline at end of file diff --git a/code/kubernetes/keycloak/secrets.yaml b/code/kubernetes/keycloak/secrets.yaml deleted file mode 100644 index fc4baa5..0000000 --- a/code/kubernetes/keycloak/secrets.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: keycloak-secrets - namespace: threadit -type: Opaque -data: - KC_DB_PASSWORD: a2V5Y2xvYWtfcGFzc3dvcmQ= - KEYCLOAK_ADMIN: YWRtaW4= - KEYCLOAK_ADMIN_PASSWORD: YWRtaW5fcGFzc3dvcmQ= - CLIENT_SECRET: eW91ci1jbGllbnQtc2VjcmV0 \ No newline at end of file diff --git a/code/kubernetes/scripts/deploy.sh b/code/kubernetes/scripts/deploy.sh index f0348cd..854279f 100644 --- a/code/kubernetes/scripts/deploy.sh +++ b/code/kubernetes/scripts/deploy.sh @@ -54,7 +54,6 @@ helm upgrade --install traefik traefik/traefik -n $CLUSTER_NAME -f traefik/value kubectl apply -n $CLUSTER_NAME -f traefik/cors.yaml kubectl apply -n $CLUSTER_NAME -f traefik/strip-prefix.yaml -kubectl apply -n $CLUSTER_NAME -f traefik/ingress-routes.yaml # Deploy threadit application kubectl create secret generic "bucket-secret" \ @@ -69,24 +68,8 @@ kubectl create secret generic "mongo-secret" \ kubectl apply -n $CLUSTER_NAME -f config.yaml kubectl apply -n $CLUSTER_NAME -f mongo/ -# Keycloak -echo "Deploying Keycloak..." -kubectl apply -n $CLUSTER_NAME -f keycloak/configmap.yaml -kubectl apply -n $CLUSTER_NAME -f keycloak/secrets.yaml -kubectl apply -n $CLUSTER_NAME -f keycloak/realm-configmap.yaml -kubectl apply -n $CLUSTER_NAME -f keycloak/deployment.yaml - -# Services for SERVICE in "${SERVICES[@]}"; do kubectl apply -n $CLUSTER_NAME -f services/"$SERVICE-service"/ done -# gRPC Gateway -kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ - -echo "Waiting for Keycloak to be ready..." -kubectl wait --for=condition=ready pod -l app=keycloak -n $CLUSTER_NAME --timeout=300s - -echo "Deployment complete!" -kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ - +kubectl apply -n $CLUSTER_NAME -f grpc-gateway/ \ No newline at end of file diff --git a/code/kubernetes/scripts/keycloak-ops.sh b/code/kubernetes/scripts/keycloak-ops.sh deleted file mode 100644 index 170a5f4..0000000 --- a/code/kubernetes/scripts/keycloak-ops.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -set -e - -CLUSTER_NAME="threadit-cluster" - -function help() { - echo "Usage: $0 " - echo "Commands:" - echo " status - Check Keycloak status" - echo " logs - Show Keycloak logs" - echo " restart - Restart Keycloak deployment" - echo " reload - Reload realm configuration" - echo " port-forward - Start port forwarding to access Keycloak locally" -} - -function check_status() { - echo "Checking Keycloak status..." - kubectl get pods -n $CLUSTER_NAME -l app=keycloak -} - -function show_logs() { - echo "Fetching Keycloak logs..." - kubectl logs -n $CLUSTER_NAME -l app=keycloak --tail=100 -f -} - -function restart_keycloak() { - echo "Restarting Keycloak..." - kubectl rollout restart deployment/keycloak -n $CLUSTER_NAME - kubectl rollout status deployment/keycloak -n $CLUSTER_NAME -} - -function reload_realm() { - echo "Reloading realm configuration..." - # Delete the existing pod to force a reload of the realm config - kubectl delete pod -n $CLUSTER_NAME -l app=keycloak - echo "Waiting for new pod to be ready..." - kubectl wait --for=condition=ready pod -l app=keycloak -n $CLUSTER_NAME --timeout=300s -} - -function port_forward() { - echo "Starting port forward to Keycloak on localhost:8080..." - kubectl port-forward -n $CLUSTER_NAME svc/keycloak 8080:8080 -} - -case "$1" in - "status") - check_status - ;; - "logs") - show_logs - ;; - "restart") - restart_keycloak - ;; - "reload") - reload_realm - ;; - "port-forward") - port_forward - ;; - *) - help - exit 1 - ;; -esac \ No newline at end of file diff --git a/code/kubernetes/traefik/ingress-routes.yaml b/code/kubernetes/traefik/ingress-routes.yaml deleted file mode 100644 index 5620952..0000000 --- a/code/kubernetes/traefik/ingress-routes.yaml +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: traefik.containo.us/v1alpha1 -kind: Middleware -metadata: - name: cors-headers - namespace: threadit -spec: - headers: - accessControlAllowMethods: - - GET - - POST - - PUT - - DELETE - - PATCH - accessControlAllowHeaders: - - "Authorization" - - "Content-Type" - accessControlAllowOriginList: - - "*" - accessControlMaxAge: 100 - addVaryHeader: true ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: api - namespace: threadit -spec: - entryPoints: - - web - routes: - - match: PathPrefix(`/api/v1`) - kind: Rule - services: - - name: grpc-gateway - port: 8080 - middlewares: - - name: cors-headers ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: keycloak - namespace: threadit -spec: - entryPoints: - - web - routes: - - match: PathPrefix(`/auth`) - kind: Rule - services: - - name: keycloak - port: 8080 - middlewares: - - name: cors-headers \ No newline at end of file diff --git a/code/services/auth/auth.go b/code/services/auth/auth.go deleted file mode 100644 index faab083..0000000 --- a/code/services/auth/auth.go +++ /dev/null @@ -1,120 +0,0 @@ -package auth - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "time" - "github.com/golang-jwt/jwt/v4" -) - -var ( - ErrNoToken = errors.New("no token provided") - ErrInvalidToken = errors.New("invalid token") - ErrInsufficientRole = errors.New("insufficient role") -) - -type KeycloakConfig struct { - Realm string - ClientID string - ClientSecret string - KeycloakURL string -} - -type TokenClaims struct { - jwt.StandardClaims - RealmAccess struct { - Roles []string `json:"roles"` - } `json:"realm_access"` -} - -type KeycloakClient struct { - config KeycloakConfig - keys map[string]interface{} -} - -func NewKeycloakClient(config KeycloakConfig) (*KeycloakClient, error) { - kc := &KeycloakClient{ - config: config, - keys: make(map[string]interface{}), - } - if err := kc.fetchKeys(); err != nil { - return nil, err - } - return kc, nil -} - -func (kc *KeycloakClient) fetchKeys() error { - resp, err := http.Get(fmt.Sprintf("%s/realms/%s/protocol/openid-connect/certs", kc.config.KeycloakURL, kc.config.Realm)) - if err != nil { - return err - } - defer resp.Body.Close() - - var jwks struct { - Keys []struct { - Kid string `json:"kid"` - Kty string `json:"kty"` - Alg string `json:"alg"` - Use string `json:"use"` - N string `json:"n"` - E string `json:"e"` - } `json:"keys"` - } - - if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { - return err - } - - for _, key := range jwks.Keys { - kc.keys[key.Kid] = key - } - - return nil -} - -func (kc *KeycloakClient) ValidateToken(ctx context.Context, tokenString string) (*TokenClaims, error) { - token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { - if kid, ok := token.Header["kid"].(string); ok { - if key, exists := kc.keys[kid]; exists { - return key, nil - } - } - return nil, ErrInvalidToken - }) - - if err != nil { - return nil, fmt.Errorf("failed to parse token: %w", err) - } - - if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid { - return claims, nil - } - - return nil, ErrInvalidToken -} - -func (kc *KeycloakClient) HasRole(claims *TokenClaims, requiredRole string) bool { - for _, role := range claims.RealmAccess.Roles { - if role == requiredRole { - return true - } - } - return false -} - -func ExtractBearerToken(header string) (string, error) { - if header == "" { - return "", ErrNoToken - } - - parts := strings.Split(header, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - return "", ErrInvalidToken - } - - return parts[1], nil -} \ No newline at end of file diff --git a/code/traefik/traefik.yml b/code/traefik/traefik.yml index 3ac99dd..58366a4 100644 --- a/code/traefik/traefik.yml +++ b/code/traefik/traefik.yml @@ -3,63 +3,12 @@ global: sendAnonymousUsage: false api: - dashboard: true insecure: true entryPoints: web: address: ":80" - forwardedHeaders: - insecure: true providers: - docker: - exposedByDefault: false file: filename: "/etc/traefik/dynamic.yml" - -http: - middlewares: - cors-headers: - headers: - accessControlAllowMethods: - - GET - - POST - - PUT - - DELETE - - PATCH - accessControlAllowHeaders: - - "Authorization" - - "Content-Type" - accessControlAllowOriginList: - - "*" - accessControlMaxAge: 100 - addVaryHeader: true - - routers: - api: - rule: "PathPrefix(`/api/v1`)" - service: "grpc-gateway" - middlewares: - - "cors-headers" - entryPoints: - - "web" - - keycloak: - rule: "PathPrefix(`/auth`)" - service: "keycloak" - middlewares: - - "cors-headers" - entryPoints: - - "web" - - services: - grpc-gateway: - loadBalancer: - servers: - - url: "http://grpc-gateway:8080" - - keycloak: - loadBalancer: - servers: - - url: "http://keycloak:8080"