From 6a5ac8916815578004b6bdc6b1f8286d672fda11 Mon Sep 17 00:00:00 2001 From: Ruslan Aliev Date: Tue, 5 Mar 2024 16:18:35 -0600 Subject: [PATCH] Introduce RBAC support & deckhand manifests fetching Small other improvements included. Change-Id: Ibcf3fc2f5383a4b1faacff814d492d13d2a5a8e5 Signed-off-by: Ruslan Aliev --- go.mod | 3 +- go.sum | 2 + pkg/apply/apply.go | 35 +++++++++++++- pkg/{server => auth}/auth.go | 59 ++++++++++++++++++++++- pkg/server/server.go | 91 +++++++++++++++++++++++++++++------- 5 files changed, 169 insertions(+), 21 deletions(-) rename pkg/{server => auth}/auth.go (82%) diff --git a/go.mod b/go.mod index 9326766..50235b8 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module opendev.org/airship/armada-go go 1.20 require ( + github.com/databus23/goslo.policy v0.0.0-20210929125152-81bf2876dbdb github.com/gin-gonic/gin v1.9.1 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.18.2 golang.org/x/sync v0.5.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.28.4 k8s.io/apiextensions-apiserver v0.28.3 k8s.io/apimachinery v0.28.4 @@ -80,7 +82,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/go.sum b/go.sum index 842a0b3..65b48a7 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhD github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/databus23/goslo.policy v0.0.0-20210929125152-81bf2876dbdb h1:8JB2G8t3o1iCL8vCzssUj2Nn2qjqSab2/G3xXhvkpPQ= +github.com/databus23/goslo.policy v0.0.0-20210929125152-81bf2876dbdb/go.mod h1:tRj172JgwQmUmEqZZJBWzYWFStitMFTtb95NtUnmpkw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/pkg/apply/apply.go b/pkg/apply/apply.go index 2f63b57..506f37e 100644 --- a/pkg/apply/apply.go +++ b/pkg/apply/apply.go @@ -21,7 +21,11 @@ import ( "flag" "fmt" "io" + "net/http" + "net/url" + "opendev.org/airship/armada-go/pkg/auth" "os" + "regexp" "strings" "time" @@ -326,10 +330,39 @@ func (c *RunCommand) ValidateManifests() error { func (c *RunCommand) ParseManifests() error { klog.V(5).Infof("parsing manifests started, path: %s", c.Manifests) - f, err := os.Open(c.Manifests) + + var f io.ReadCloser + u, err := url.Parse(c.Manifests) if err != nil { return err } + if u.Scheme == "" { + f, err = os.Open(c.Manifests) + if err != nil { + return err + } + } else if u.Scheme == "deckhand+http" { + reg, err := regexp.Compile("^[^+]+\\+") + if err != nil { + return err + } + deckhandUrl := reg.ReplaceAllString(c.Manifests, "") + req, err := http.NewRequest("GET", deckhandUrl, nil) + if err != nil { + return err + } + token, err := auth.Authenticate() + if err != nil { + return err + } + req.Header.Set("X-Auth-Token", token) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + f = resp.Body + } defer f.Close() c.airCharts = map[string]*AirshipChart{} diff --git a/pkg/server/auth.go b/pkg/auth/auth.go similarity index 82% rename from pkg/server/auth.go rename to pkg/auth/auth.go index 8ea43e7..e0ee729 100644 --- a/pkg/server/auth.go +++ b/pkg/auth/auth.go @@ -1,9 +1,11 @@ -package server +package auth import ( + "bytes" "encoding/json" "errors" "fmt" + "github.com/spf13/viper" "net/http" "strings" "time" @@ -13,7 +15,7 @@ import ( "opendev.org/airship/armada-go/pkg/log" ) -var Log func(string, ...interface{}) = func(format string, a ...interface{}) { +var Log = func(format string, a ...interface{}) { log.Printf(format, a...) } @@ -287,3 +289,56 @@ func filterIncomingHeaders(req *http.Request) { req.Header.Del("X-User") req.Header.Del("X-Role") } + +func Authenticate() (string, error) { + authUrl := viper.Sub("keystone_authtoken").GetString("auth_url") + username := viper.Sub("keystone_authtoken").GetString("username") + password := viper.Sub("keystone_authtoken").GetString("password") + projectDomainName := viper.Sub("keystone_authtoken").GetString("project_domain_name") + projectName := viper.Sub("keystone_authtoken").GetString("project_name") + userDomainName := viper.Sub("keystone_authtoken").GetString("user_domain_name") + + jsonData := []byte(fmt.Sprintf(`{ + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "name": "%s", + "domain": { "id": "%s" }, + "password": "%s" + } + } + }, + "scope": { + "project": { + "name": "%s", + "domain": { "id": "%s" } + } + } + } + }`, username, userDomainName, password, projectName, projectDomainName)) + + req, err := http.NewRequest("POST", authUrl+"/auth/tokens", bytes.NewBuffer(jsonData)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != 201 { + return "", errors.New("http: not authorized") + } + + token := resp.Header.Get("X-Subject-Token") + if token == "" { + return "", errors.New("http: keystone token is empty") + } + + return token, nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 652905a..ed28180 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -15,13 +15,18 @@ package server import ( - "net/http" - + "fmt" + policy "github.com/databus23/goslo.policy" "github.com/gin-gonic/gin" "github.com/spf13/viper" - + "gopkg.in/yaml.v3" + "net/http" + "opendev.org/airship/armada-go/pkg/apply" + auth2 "opendev.org/airship/armada-go/pkg/auth" "opendev.org/airship/armada-go/pkg/config" "opendev.org/airship/armada-go/pkg/log" + "os" + "strings" ) // RunCommand phase run command @@ -29,17 +34,53 @@ type RunCommand struct { Factory config.Factory } +type JsonDataRequest struct { + Href string `json:"hrefs" binding:"required"` + Overrides []any `json:"overrides"` +} + +func PolicyEnforcer(enforcer *policy.Enforcer, rule string) gin.HandlerFunc { + return func(context *gin.Context) { + ctx := policy.Context{ + Roles: strings.Split(context.GetHeader("X-Roles"), ","), + Logger: log.Printf, + } + if enforcer.Enforce(rule, ctx) { + context.Next() + } else { + context.String(401, "oslo policy error") + } + } +} + func Apply(c *gin.Context) { if c.GetHeader("X-Identity-Status") == "Confirmed" { - c.JSON(200, gin.H{ - "message": gin.H{ - "install": []any{}, - "upgrade": []any{}, - "diff": []any{}, - "purge": []any{}, - "protected": []any{}, - }, - }) + if c.ContentType() == "application/json" { + targetManifest := c.Query("target_manifest") + var dataReq JsonDataRequest + if err := c.BindJSON(&dataReq); err != nil { + c.String(500, "internal error", err.Error()) + return + } + + runOpts := apply.RunCommand{Manifests: dataReq.Href, TargetManifest: targetManifest, Out: os.Stdout} + if err := runOpts.RunE(); err != nil { + c.String(500, "apply error", err.Error()) + return + } + + c.JSON(200, gin.H{ + "message": gin.H{ + "install": []any{}, + "upgrade": []any{}, + "diff": []any{}, + "purge": []any{}, + "protected": []any{}, + }, + }) + } else { + c.Status(500) + } } else { c.Status(401) } @@ -66,7 +107,7 @@ func Releases(c *gin.Context) { if c.GetHeader("X-Identity-Status") == "Confirmed" { c.JSON(200, gin.H{ "releases": gin.H{ - "ucp": []string{"clcp-ucp-armada"}, + "ucp": []string{}, }, }) } else { @@ -88,11 +129,27 @@ func (c *RunCommand) RunE() error { log.Printf("armada-go server has been started") r := gin.Default() r.Use(gin.Logger()) - auth := New(viper.Sub("keystone_authtoken").GetString("auth_url")) + auth := auth2.New(viper.Sub("keystone_authtoken").GetString("auth_url")) - r.POST("/api/v1.0/apply", auth.Handler(r.Handler()), Apply) - r.POST("/api/v1.0/validatedesign", auth.Handler(r.Handler()), Validate) - r.GET("/api/v1.0/releases", auth.Handler(r.Handler()), Releases) + buf, err := os.ReadFile("/etc/armada/policy.yaml") + if err != nil { + return err + } + + var pol map[string]string + err = yaml.Unmarshal(buf, &pol) + if err != nil { + return fmt.Errorf("in file %q: %w", "policy", err) + } + + enf, err := policy.NewEnforcer(pol) + if err != nil { + return err + } + + r.POST("/api/v1.0/apply", auth.Handler(r.Handler()), PolicyEnforcer(enf, "armada:create_endpoints"), Apply) + r.POST("/api/v1.0/validatedesign", auth.Handler(r.Handler()), PolicyEnforcer(enf, "armada:validate_manifest"), Validate) + r.GET("/api/v1.0/releases", auth.Handler(r.Handler()), PolicyEnforcer(enf, "armada:get_release"), Releases) r.GET("/api/v1.0/health", Health) return r.Run(":8000") }