Files
KubePi/internal/api/v1/v1.go
2022-06-06 11:36:20 +08:00

428 lines
12 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/KubeOperator/kubepi/internal/api/v1/mfa"
"io/ioutil"
"strings"
"github.com/KubeOperator/kubepi/internal/api/v1/file"
"github.com/kataras/iris/v12/middleware/jwt"
"github.com/KubeOperator/kubepi/internal/api/v1/chart"
"github.com/KubeOperator/kubepi/internal/api/v1/cluster"
"github.com/KubeOperator/kubepi/internal/api/v1/imagerepo"
"github.com/KubeOperator/kubepi/internal/api/v1/ldap"
"github.com/KubeOperator/kubepi/internal/api/v1/proxy"
"github.com/KubeOperator/kubepi/internal/api/v1/role"
"github.com/KubeOperator/kubepi/internal/api/v1/session"
"github.com/KubeOperator/kubepi/internal/api/v1/system"
"github.com/KubeOperator/kubepi/internal/api/v1/user"
"github.com/KubeOperator/kubepi/internal/api/v1/webkubectl"
"github.com/KubeOperator/kubepi/internal/api/v1/ws"
v1 "github.com/KubeOperator/kubepi/internal/model/v1"
v1Role "github.com/KubeOperator/kubepi/internal/model/v1/role"
v1System "github.com/KubeOperator/kubepi/internal/model/v1/system"
"github.com/KubeOperator/kubepi/internal/service/v1/common"
v1RoleService "github.com/KubeOperator/kubepi/internal/service/v1/role"
v1RoleBindingService "github.com/KubeOperator/kubepi/internal/service/v1/rolebinding"
v1SystemService "github.com/KubeOperator/kubepi/internal/service/v1/system"
pkgV1 "github.com/KubeOperator/kubepi/pkg/api/v1"
"github.com/KubeOperator/kubepi/pkg/collectons"
"github.com/KubeOperator/kubepi/pkg/i18n"
"github.com/asdine/storm/v3"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"
"github.com/kataras/iris/v12/sessions"
)
var resourceWhiteList = WhiteList{"sessions", "proxy", "ws", "charts", "webkubectl", "apps", "mfa", "pod"}
type WhiteList []string
func (w WhiteList) In(name string) bool {
for i := range w {
if w[i] == name {
return true
}
}
return false
}
func authHandler() iris.Handler {
return func(ctx *context.Context) {
var p session.UserProfile
if ctx.GetHeader("Authorization") != "" {
pr := jwt.Get(ctx).(*session.UserProfile)
p = *pr
} else {
p = sessions.Get(ctx).Get("profile").(session.UserProfile)
}
if p.Name == "" {
ctx.Values().Set("message", "please login")
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
ctx.Values().Set("profile", p)
ctx.Next()
}
}
func langHandler() iris.Handler {
return func(ctx *context.Context) {
p := ctx.Values().Get("profile")
u, ok := p.(session.UserProfile)
lang := i18n.LanguageZhCN
if ok {
lang = u.Language
}
ctx.Values().Set("language", lang)
ctx.Next()
}
}
func pageHandler() iris.Handler {
return func(ctx *context.Context) {
if ctx.URLParamExists(pkgV1.PageSize) && ctx.URLParamExists(pkgV1.PageNum) {
pageNum, err := ctx.URLParamInt(pkgV1.PageNum)
if err != nil {
ctx.Values().Set("message", fmt.Sprintf("page num format err %s", err.Error()))
ctx.StopWithStatus(iris.StatusBadRequest)
return
}
pageSize, err := ctx.URLParamInt(pkgV1.PageSize)
if err != nil {
ctx.Values().Set("message", fmt.Sprintf("page size format err %s", err.Error()))
ctx.StopWithStatus(iris.StatusBadRequest)
return
}
ctx.Values().Set(pkgV1.PageNum, pageNum)
ctx.Values().Set(pkgV1.PageSize, pageSize)
}
ctx.Next()
}
}
type logHelper struct {
Name string `json:"name"`
Metadata v1.Metadata
}
func logHandler() iris.Handler {
return func(ctx *context.Context) {
method := strings.ToLower(ctx.Method())
if method != "post" && method != "delete" && method != "put" {
ctx.Next()
return
}
resourceName := ctx.Values().GetString("resource")
if resourceName == "" || resourceWhiteList.In(resourceName) {
ctx.Next()
return
}
currentPath := ctx.GetCurrentRoute().Path()
path := strings.Replace(ctx.Request().URL.Path, "/kubepi/api/v1/", "", 1)
currentPath = strings.Replace(currentPath, "/kubepi/api/v1/", "", 1)
if strings.HasSuffix(path, "search") {
ctx.Next()
return
}
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
var log v1System.OperationLog
log.Operator = profile.Name
log.Operation = method
pathResource := strings.Split(path, "/")
if strings.HasPrefix(currentPath, "clusters/:name") {
if len(pathResource) < 3 {
log.OperationDomain = pathResource[0]
if method != "post" {
log.SpecificInformation = pathResource[1]
}
} else {
log.OperationDomain = fmt.Sprintf("%s_%s", pathResource[0], pathResource[2])
if method != "post" {
if len(pathResource) > 3 {
log.SpecificInformation = fmt.Sprintf("[%s] %s", pathResource[1], pathResource[3])
} else {
log.SpecificInformation = fmt.Sprintf("[%s] %s", pathResource[1], "-")
}
}
}
} else {
log.OperationDomain = strings.Split(currentPath, "/")[0]
if method != "post" {
if len(pathResource) > 1 {
log.SpecificInformation = pathResource[1]
} else {
log.SpecificInformation = "-"
}
}
}
if !strings.Contains(currentPath, "upload") {
if method == "post" {
var req logHelper
data, _ := ctx.GetBody()
if err := json.Unmarshal(data, &req); err != nil {
ctx.Next()
}
if len(req.Name) == 0 {
req.Name = req.Metadata.Name
}
if strings.HasPrefix(currentPath, "clusters/:name") {
log.SpecificInformation = fmt.Sprintf("[%s] %s", pathResource[1], req.Name)
} else {
log.SpecificInformation = req.Name
}
ctx.Request().Body = ioutil.NopCloser(bytes.NewBuffer(data))
}
}
systemService := v1SystemService.NewService()
go systemService.CreateOperationLog(&log, common.DBOptions{})
ctx.Next()
}
}
func resourceExtractHandler() iris.Handler {
return func(ctx *context.Context) {
path := ctx.Request().URL.Path
ss := strings.Split(path, "/")
// "" "api" "v1" "resource"
if len(ss) >= 5 {
ctx.Values().Set("resource", ss[4])
}
ctx.Next()
}
}
func roleHandler() iris.Handler {
return func(ctx *context.Context) {
// 查询当前用户的角色
// 查询角色的 rolebinding 获取 roles
p := ctx.Values().Get("profile")
u := p.(session.UserProfile)
if u.IsAdministrator {
ctx.Next()
return
}
roleBindingService := v1RoleBindingService.NewService()
rbs, err := roleBindingService.GetRoleBindingBySubject(v1Role.Subject{
Kind: "User",
Name: u.Name,
}, common.DBOptions{})
if err != nil {
if !errors.As(err, &storm.ErrNotFound) {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
return
}
}
roleNameHash := map[string]struct{}{}
for i := range rbs {
roleName := rbs[i].RoleRef
roleNameHash[roleName] = struct{}{}
}
var roleNames []string
for key := range roleNameHash {
roleNames = append(roleNames, key)
}
roleService := v1RoleService.NewService()
rs, err := roleService.GetByNames(roleNames, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
return
}
ctx.Values().Set("roles", rs)
ctx.Next()
}
}
func getVerbByRoute(path, method string) string {
switch strings.ToLower(method) {
case "put":
return "update"
case "delete":
return "delete"
case "get":
if strings.Contains(path, "/:name") {
return "get"
} else {
return "list"
}
case "post":
if strings.HasSuffix(path, "search") {
return "list"
}
return "create"
}
return ""
}
func apiResourceHandler(party iris.Party) iris.Handler {
return func(ctx *context.Context) {
//1. 确定所有的api资源有哪些
apiBuilder := party.(*router.APIBuilder)
routes := apiBuilder.GetRoutes()
resourceMap := map[string]*collectons.StringSet{}
for i := range routes {
if strings.HasPrefix(routes[i].Path, "/kubepi/api/v1/") {
ss := strings.Split(routes[i].Path, "/")
if len(ss) >= 5 {
resourceName := ss[4]
//过滤session资源
if resourceWhiteList.In(resourceName) {
continue
}
if _, ok := resourceMap[resourceName]; !ok {
resourceMap[resourceName] = collectons.NewStringSet()
}
resourceMap[resourceName].Add(getVerbByRoute(routes[i].Path, routes[i].Method))
}
}
}
displayMap := map[string][]string{}
for k := range resourceMap {
verbs := resourceMap[k]
if len(verbs.ToSlice()) > 0 {
displayMap[k] = verbs.ToSlice()
}
}
ctx.Values().Set("data", displayMap)
}
}
func roleAccessHandler() iris.Handler {
return func(ctx *context.Context) {
//// 查询角色的 resources
//// 通过api resource 过滤出来资源主体,method 过滤操作
p := ctx.Values().Get("profile")
u := p.(session.UserProfile)
isInWhiteList := false
for _, path := range resourceWhiteList {
if strings.Contains(ctx.Request().URL.Path, fmt.Sprintf("/%s", path)) && path != "sessions" {
isInWhiteList = true
break
}
}
if !isInWhiteList {
// 放通admin权限
if u.IsAdministrator {
ctx.Next()
return
}
rs := ctx.Values().Get("roles")
roles := rs.([]v1Role.Role)
requestResource := ctx.Values().GetString("resource")
if requestResource != "" {
currentRoute := ctx.GetCurrentRoute()
requestVerb := getVerbByRoute(currentRoute.Path(), currentRoute.Method())
resourceMatched, methodMatch := matchRoles(requestResource, requestVerb, roles)
if !(resourceMatched && methodMatch) {
ctx.StopWithStatus(iris.StatusForbidden)
ctx.Values().Set("message", []string{"user %s can not access resource %s %s", u.Name, requestResource, requestVerb})
return
}
}
}
ctx.Next()
}
}
func matchRoles(requestResource, requestMethod string, rs []v1Role.Role) (bool, bool) {
resourceMatch := false
methodMatch := false
for i := range rs {
for j := range rs[i].Rules {
for k := range rs[i].Rules[j].Resource {
if rs[i].Rules[j].Resource[k] == requestResource || rs[i].Rules[j].Resource[k] == "*" {
resourceMatch = true
for x := range rs[i].Rules[j].Verbs {
if rs[i].Rules[j].Verbs[x] == requestMethod || rs[i].Rules[j].Verbs[x] == "*" {
methodMatch = true
}
}
}
}
}
}
return resourceMatch, methodMatch
}
func resourceNameInvalidHandler() iris.Handler {
return func(ctx *context.Context) {
r := ctx.GetCurrentRoute()
if strings.Contains(r.Path(), "/:name") {
resourceName := ctx.Params().GetString("name")
if resourceName == "" {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("invalid resource name %s", resourceName))
return
}
}
ctx.Next()
}
}
func WarpedJwtHandler() iris.Handler {
verifier := jwt.NewVerifier(jwt.HS256, session.JwtSigKey)
verifier.WithDefaultBlocklist()
verifyMiddleware := verifier.Verify(func() interface{} {
return new(session.UserProfile)
})
return func(ctx *context.Context) {
sess := sessions.Get(ctx)
if sess.Get("profile") != nil {
ctx.Next()
return
}
verifyMiddleware(ctx)
}
}
func AddV1Route(app iris.Party) {
v1Party := app.Party("/v1")
v1Party.Use(langHandler())
v1Party.Use(pageHandler())
session.Install(v1Party)
mfa.Install(v1Party)
authParty := v1Party.Party("")
authParty.Use(WarpedJwtHandler())
authParty.Use(authHandler())
authParty.Use(resourceExtractHandler())
authParty.Use(roleHandler())
authParty.Use(roleAccessHandler())
authParty.Use(resourceNameInvalidHandler())
authParty.Use(logHandler())
authParty.Get("/", apiResourceHandler(authParty))
user.Install(authParty)
cluster.Install(authParty)
role.Install(authParty)
system.Install(v1Party)
proxy.Install(authParty)
ws.Install(authParty)
chart.Install(authParty)
webkubectl.Install(authParty, v1Party)
ldap.Install(authParty)
imagerepo.Install(authParty)
file.Install(authParty)
}