Introduce RBAC support & deckhand manifests fetching

Small other improvements included.

Change-Id: Ibcf3fc2f5383a4b1faacff814d492d13d2a5a8e5
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
This commit is contained in:
Ruslan Aliev 2024-03-05 16:18:35 -06:00
parent 7094ac6ace
commit 6a5ac89168
5 changed files with 169 additions and 21 deletions

3
go.mod
View File

@ -3,10 +3,12 @@ module opendev.org/airship/armada-go
go 1.20 go 1.20
require ( require (
github.com/databus23/goslo.policy v0.0.0-20210929125152-81bf2876dbdb
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
golang.org/x/sync v0.5.0 golang.org/x/sync v0.5.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.28.4 k8s.io/api v0.28.4
k8s.io/apiextensions-apiserver v0.28.3 k8s.io/apiextensions-apiserver v0.28.3
k8s.io/apimachinery v0.28.4 k8s.io/apimachinery v0.28.4
@ -80,7 +82,6 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.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/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect

2
go.sum
View File

@ -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/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/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/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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=

View File

@ -21,7 +21,11 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"net/http"
"net/url"
"opendev.org/airship/armada-go/pkg/auth"
"os" "os"
"regexp"
"strings" "strings"
"time" "time"
@ -326,10 +330,39 @@ func (c *RunCommand) ValidateManifests() error {
func (c *RunCommand) ParseManifests() error { func (c *RunCommand) ParseManifests() error {
klog.V(5).Infof("parsing manifests started, path: %s", c.Manifests) 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 { if err != nil {
return err 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() defer f.Close()
c.airCharts = map[string]*AirshipChart{} c.airCharts = map[string]*AirshipChart{}

View File

@ -1,9 +1,11 @@
package server package auth
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/spf13/viper"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -13,7 +15,7 @@ import (
"opendev.org/airship/armada-go/pkg/log" "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...) log.Printf(format, a...)
} }
@ -287,3 +289,56 @@ func filterIncomingHeaders(req *http.Request) {
req.Header.Del("X-User") req.Header.Del("X-User")
req.Header.Del("X-Role") 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
}

View File

@ -15,13 +15,18 @@
package server package server
import ( import (
"net/http" "fmt"
policy "github.com/databus23/goslo.policy"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/viper" "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/config"
"opendev.org/airship/armada-go/pkg/log" "opendev.org/airship/armada-go/pkg/log"
"os"
"strings"
) )
// RunCommand phase run command // RunCommand phase run command
@ -29,8 +34,41 @@ type RunCommand struct {
Factory config.Factory 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) { func Apply(c *gin.Context) {
if c.GetHeader("X-Identity-Status") == "Confirmed" { if c.GetHeader("X-Identity-Status") == "Confirmed" {
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{ c.JSON(200, gin.H{
"message": gin.H{ "message": gin.H{
"install": []any{}, "install": []any{},
@ -40,6 +78,9 @@ func Apply(c *gin.Context) {
"protected": []any{}, "protected": []any{},
}, },
}) })
} else {
c.Status(500)
}
} else { } else {
c.Status(401) c.Status(401)
} }
@ -66,7 +107,7 @@ func Releases(c *gin.Context) {
if c.GetHeader("X-Identity-Status") == "Confirmed" { if c.GetHeader("X-Identity-Status") == "Confirmed" {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"releases": gin.H{ "releases": gin.H{
"ucp": []string{"clcp-ucp-armada"}, "ucp": []string{},
}, },
}) })
} else { } else {
@ -88,11 +129,27 @@ func (c *RunCommand) RunE() error {
log.Printf("armada-go server has been started") log.Printf("armada-go server has been started")
r := gin.Default() r := gin.Default()
r.Use(gin.Logger()) 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) buf, err := os.ReadFile("/etc/armada/policy.yaml")
r.POST("/api/v1.0/validatedesign", auth.Handler(r.Handler()), Validate) if err != nil {
r.GET("/api/v1.0/releases", auth.Handler(r.Handler()), Releases) 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) r.GET("/api/v1.0/health", Health)
return r.Run(":8000") return r.Run(":8000")
} }