commit 096bf36c2f54907cb0db43791f84692726765bbd Author: Farhan Khan Date: Thu Mar 26 23:30:07 2026 -0400 raw draft of policy-manager diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2725acd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# ---- Build Stage ---- +FROM golang:latest AS builder + +WORKDIR /app + +# Copy go mod files first (better caching) +COPY go.mod ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build static binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server + +# ---- Runtime Stage ---- +FROM gcr.io/distroless/base-debian12 + +WORKDIR /app + +# 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"] + diff --git a/createpolicy.go b/createpolicy.go new file mode 100644 index 0000000..bec3d27 --- /dev/null +++ b/createpolicy.go @@ -0,0 +1,113 @@ +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/db.go b/db.go new file mode 100644 index 0000000..fc677c2 --- /dev/null +++ b/db.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/jackc/pgx/v5/pgxpool" +) + +var pool *pgxpool.Pool + +func getDbPool() *pgxpool.Pool { + // Construct the connection string + // Note: Ensure your Docker Compose env vars match these keys! + dburl := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", + os.Getenv("POSTGRES_USER"), + os.Getenv("POSTGRES_PASSWORD"), + os.Getenv("POSTGRES_HOSTNAME"), + os.Getenv("POSTGRES_DB"), + ) + + var err error + // Use pgxpool.New instead of Connect for v5 + pool, err = pgxpool.New(context.Background(), dburl) + if err != nil { + log.Fatalf("Unable to create connection pool: %v", err) + } + + // Ping the database to verify the connection is actually live + err = pool.Ping(context.Background()) + if err != nil { + log.Fatalf("Unable to ping database: %v", err) + } + + return pool +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7a8fc2c --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module policy-manager + +go 1.25.0 + +require github.com/jackc/pgx/v5 v5.8.0 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..87a6c8a --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +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/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= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/listpolicy.go b/listpolicy.go new file mode 100644 index 0000000..a3a1f8e --- /dev/null +++ b/listpolicy.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type PolicyList struct { + Status string `json:"status"` + Policies map[string]Policy `json:"policies"` + Count int `json:"count"` + Timestamp string `json:"timestamp"` +} + +func ListPolicy(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) + + // Check if policy with the same name already exists for the account + rows, err := pool.Query(context.Background(), + "SELECT policyname, document FROM policies WHERE accountid = $1", accountid) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var policylist PolicyList + policylist.Policies = make(map[string]Policy) + + for rows.Next() { + var policyname string + var document Policy + err = rows.Scan(&policyname, &document) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + policylist.Count = policylist.Count + 1 + policylist.Policies["pri:iam::"+accountid+":policy/"+policyname] = document + } + + policylist.Status = "success" + policylist.Timestamp = fmt.Sprintf("%d", time.Now().Unix()) + + json.NewEncoder(w).Encode(policylist) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..35ddf73 --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "net/http" +) + +// 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"` +} + +func main() { + + pool = getDbPool() + log.Println("Policy Manager service started") + + http.HandleFunc("/iam/create-policy", CreatePolicy) + http.HandleFunc("/iam/list-policy", ListPolicy) + log.Println("Server running on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..0e5ede1 --- /dev/null +++ b/notes.md @@ -0,0 +1,34 @@ +## Create Policy + +``` +curl -X POST \ + -H "Content-Type: application/json" \ + -H "Account: 8715694634136" \ + -d '{ + "policyname": "MyReadOnlyPolicy", + "policy": { + "pid": "p-12345678", + "comment": "Allows read-only access to S3 buckets", + "effect": "Allow", + "actions": [ + "s3:GetObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "resources": [ + "arn:aws:s3:::my-company-data/*", + "arn:aws:s3:::my-company-data" + ] + } + }' \ + http://localhost:8080/iam/create-policy +``` + +## List Policies + +``` +curl -X POST \ + -H "Content-Type: application/json" \ + -H "Account: 8715694634136" \ + http://localhost:8080/iam/list-policy +``` \ No newline at end of file