diff --git a/Dockerfile b/Dockerfile index 2725acd..6d53e60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,37 @@ # ---- Build Stage ---- -FROM golang:latest AS builder +FROM golang:1.26 AS builder WORKDIR /app -# Copy go mod files first (better caching) -COPY go.mod ./ -RUN go mod download +# ---- Step 1: Copy go.mod/go.sum for caching ---- +# Copy only the minimal files first for layer caching +COPY libshared/go.mod libshared/go.sum ./libshared/ +COPY policy-manager/go.mod policy-manager/go.sum ./policy-manager/ -# Copy source code -COPY . . +# ---- Step 2: Download dependencies ---- +RUN go -C libshared mod download +RUN go -C policy-manager mod download -# Build static binary +# ---- Step 3: Copy full source code ---- +COPY libshared ./libshared +COPY policy-manager ./policy-manager + +# ---- Step 4: Create Go workspace inside container ---- +RUN go work init ./policy-manager ./libshared + +## Optional: verify workspace +#RUN go work list + +# ---- Step 5: Build binary ---- +WORKDIR /app/policy-manager RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server # ---- Runtime Stage ---- FROM gcr.io/distroless/base-debian12 WORKDIR /app +COPY --from=builder /app/policy-manager/server . -# Copy only the binary from builder -COPY --from=builder /app/server . - -# Expose port EXPOSE 8080 - -# Run as non-root user USER nonroot:nonroot - -ENTRYPOINT ["/app/server"] - +ENTRYPOINT ["/app/server"] \ No newline at end of file diff --git a/create-policy.go b/create-policy.go new file mode 100644 index 0000000..b2c31d5 --- /dev/null +++ b/create-policy.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "libshared" + "log" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v5" +) + +// Custom claims struct +type MyClaims struct { + Sub string `json:"sub"` + Purpose string `json:"purpose"` + Account string `json:"account"` + jwt.RegisteredClaims +} + +type PolicyRequest struct { + Name string `json:"name"` + Description string `json:"description"` + PolicyDocument Policy `json:"policydocument"` +} + +type PolicyResponse struct { + // PolicyID string `json:"policy_id"` +} + +func CreatePolicy(w http.ResponseWriter, r *http.Request) { + + log.Println("Create Policy Request") + + w.Header().Set("Content-Type", "application/json") + + // Only allow POST method + if r.Method != http.MethodPost { + log.Println("Invalid method:", r.Method) + //w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Method not allowed", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + return + } + // Optional: enforce content type + if r.Header.Get("Content-Type") != "application/json" { + log.Println("Invalid Content-Type:", r.Header.Get("Content-Type")) + //w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnsupportedMediaType) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Content-Type must be application/json", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + //http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType) + return + } + + // Get JWT from Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + log.Println("Missing Authorization header") + //w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Missing Authorization header", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + //http.Error(w, "Missing Authorization header", http.StatusUnauthorized) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + log.Println("Invalid Authorization header format") + //w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Invalid Authorization format", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + //http.Error(w, "Invalid Authorization format", http.StatusUnauthorized) + return + } + + tokenString := parts[1] + + // Replace with your actual secret or keyfunc + secret := []byte("super-secret-key") + + claims := &MyClaims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + // Validate signing method if needed + return secret, nil + }) + + if err != nil || !token.Valid { + log.Println("Invalid token:", err) + //http.Error(w, "Invalid token", http.StatusUnauthorized) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Invalid token", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + return + } + + // Access parsed values + log.Println("sub:", claims.Sub) + log.Println("purpose:", claims.Purpose) + log.Println("account:", claims.Account) + // exp and iat come from RegisteredClaims + log.Println("exp:", claims.ExpiresAt) + log.Println("iat:", claims.IssuedAt) + + // Read body with size limit (protect against huge requests) + const maxBodyBytes = 1 << 20 // 1 MB + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) + + // Decode JSON into our struct + var policydocumentrequest PolicyRequest + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() // strict mode - reject unknown fields + + if err := decoder.Decode(&policydocumentrequest); err != nil { + switch { + case err == io.EOF: + log.Println("Empty request body") + w.WriteHeader(http.StatusBadRequest) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Empty request body", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + //http.Error(w, "Empty request body", http.StatusBadRequest) + return + case err.Error() == "http: request body too large": + log.Println("Request body too large") + w.WriteHeader(http.StatusRequestEntityTooLarge) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Request body too large (max 1MB)", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + //http.Error(w, "Request body too large (max 1MB)", http.StatusRequestEntityTooLarge) + return + default: + log.Println("Invalid JSON:", err) + w.WriteHeader(http.StatusBadRequest) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", fmt.Sprintf("Invalid JSON: %v", err), PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + //http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + } + + checkExisting := libshared.Pool.QueryRow( + context.Background(), + `SELECT FROM policies WHERE name=$1 AND accountid=$2`, + policydocumentrequest.Name, + claims.Account, + ) + err = checkExisting.Scan() + if err == nil { + log.Println("Policy with this name already exists for this account") + w.WriteHeader(http.StatusConflict) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Policy with this name already exists", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + return + } + + _, err = libshared.Pool.Exec( + context.Background(), + `INSERT INTO policies (name, accountid, description, document) VALUES ($1, $2, $3, $4)`, + policydocumentrequest.Name, + claims.Account, + policydocumentrequest.Description, + policydocumentrequest.PolicyDocument, + ) + + if err != nil { + log.Println("Database error:", err) + w.WriteHeader(http.StatusInternalServerError) + apiresponse := libshared.NewAPIResponse[PolicyResponse]("fail", "Internal server error", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + return + } + + // Success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + apiresponse := libshared.NewAPIResponse[PolicyResponse]("success", "Policy created", PolicyResponse{}) + json.NewEncoder(w).Encode(apiresponse) + + // arnservice:region:account-id:resource-type:resource-id + //pri := "pri:iam::" + accountid + ":policy/" + policydocumentrequest.PolicyName + + // response := map[string]interface{}{ + // "status": "success", + // //"pri": pri, + // "message": "Policy created", + // "timestamp": fmt.Sprintf("%d", time.Now().Unix()), + // } + + return +} diff --git a/createpolicy.go b/createpolicy.go deleted file mode 100644 index bec3d27..0000000 --- a/createpolicy.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "time" -) - -type CreatePolicyRequest struct { - PolicyName string `json:"policyname"` - PolicyDocument Policy `json:"policy"` -} - -func CreatePolicy(w http.ResponseWriter, r *http.Request) { - // Only allow POST method - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // XXX Temporary Account Handler - accountid := r.Header.Get("Account") - if accountid == "" { - http.Error(w, "Account header is required", http.StatusUnauthorized) - return - } - - // Optional: enforce content type - if r.Header.Get("Content-Type") != "application/json" { - http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType) - return - } - - // Read body with size limit (protect against huge requests) - const maxBodyBytes = 1 << 20 // 1 MB - r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) - - // Decode JSON into our struct - var policydocumentrequest CreatePolicyRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // strict mode - reject unknown fields - - if err := decoder.Decode(&policydocumentrequest); err != nil { - switch { - case err == io.EOF: - http.Error(w, "Empty request body", http.StatusBadRequest) - case err.Error() == "http: request body too large": - http.Error(w, "Request body too large (max 1MB)", http.StatusRequestEntityTooLarge) - default: - http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) - } - return - } - - // Optional: basic validation - if policydocumentrequest.PolicyDocument.Actions == nil || policydocumentrequest.PolicyDocument.Effect == "" || policydocumentrequest.PolicyDocument.Resources == nil { - http.Error(w, "Missing required fields: id and title", http.StatusBadRequest) - return - } - - // Process the results - log.Printf("New Policy: Account:%s, PolicyIdentifier=%s, Actions=%q, Effect=%q, Resources=%q, Comment=%q", - accountid, policydocumentrequest.PolicyDocument.PolicyIdentifier, policydocumentrequest.PolicyDocument.Actions, - policydocumentrequest.PolicyDocument.Effect, policydocumentrequest.PolicyDocument.Resources, policydocumentrequest.PolicyDocument.Comment) - - // Check if policy with the same name already exists for the account - checkExisting := pool.QueryRow(context.Background(), - "SELECT FROM policies WHERE accountid = $1 AND policyname = $2", - accountid, policydocumentrequest.PolicyName) - err := checkExisting.Scan() - if err == nil { - pri := "pri:iam::" + accountid + ":policy/" + policydocumentrequest.PolicyName - response := map[string]interface{}{ - "status": "fail", - "pri": pri, - "message": "Policy " + policydocumentrequest.PolicyName + " already exists: " + pri, - "timestamp": fmt.Sprintf("%d", time.Now().Unix()), - } - - json.NewEncoder(w).Encode(response) - http.Error(w, "Policy with the same name already exists for this account", http.StatusConflict) - return - } - - _, err = pool.Exec(context.Background(), - "INSERT INTO policies (accountid, policyname, document) VALUES($1, $2, $3) ON CONFLICT DO NOTHING", - accountid, policydocumentrequest.PolicyName, policydocumentrequest.PolicyDocument) - if err != nil { - log.Printf("Error inserting policy: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - // Success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - - // arnservice:region:account-id:resource-type:resource-id - pri := "pri:iam::" + accountid + ":policy/" + policydocumentrequest.PolicyName - - response := map[string]interface{}{ - "status": "success", - "pri": pri, - "message": "Policy created", - "timestamp": fmt.Sprintf("%d", time.Now().Unix()), - } - - json.NewEncoder(w).Encode(response) -} diff --git a/go.mod b/go.mod index 7a8fc2c..609926b 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module policy-manager go 1.25.0 -require github.com/jackc/pgx/v5 v5.8.0 +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/jackc/pgx/v5 v5.8.0 +) require ( github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/go.sum b/go.sum index 87a6c8a..90386e5 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= diff --git a/main.go b/main.go index 35ddf73..d443a43 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "libshared" "log" "net/http" ) @@ -8,20 +9,28 @@ import ( // Document represents the expected JSON structure // You can modify this struct to match your actual data needs type Policy struct { - PolicyIdentifier string `json:"pid"` - Comment string `json:"comment"` - Effect string `json:"effect"` - Actions []string `json:"actions"` - Resources []string `json:"resources"` + Name string `json:"name"` + Description string `json:"description"` + Conditions []Condition `json:"conditions"` +} + +// Condition represents each item in the "conditions" array +type Condition struct { + StatementID string `json:"statementid"` + Principal []string `json:"principals"` + Actions []string `json:"actions"` + Source []string `json:"source"` + Effect string `json:"effect"` + Operator string `json:"operator"` } func main() { - pool = getDbPool() + pool = libshared.GetDbPool() log.Println("Policy Manager service started") - http.HandleFunc("/iam/create-policy", CreatePolicy) - http.HandleFunc("/iam/list-policy", ListPolicy) + http.HandleFunc("/policy/create-policy", CreatePolicy) + http.HandleFunc("/policy/list-policy", ListPolicy) log.Println("Server running on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) }