myxce/src/frontend/apiserver/controllers/rbac_check.go

416 lines
12 KiB
Go

package controllers
import (
"apiserver/common"
"apiserver/models"
"apiserver/store"
"apiserver/utils"
logger "apiserver/utils"
"errors"
"os"
"sync"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
type RBACConfig struct {
Permissions []string `yaml:"permissions"`
Roles map[string]Role `yaml:"roles"`
Users map[string]User `yaml:"users"`
}
// define Role
type Role struct {
Includes []string `yaml:"includes,omitempty"`
Permissions []Permission `yaml:"permissions"`
}
type Permission struct {
Action string `yaml:"action"`
Domain []Domain `yaml:"domain"`
}
// define User
type User struct {
Roles []UserRole `yaml:"roles"`
}
type UserRole struct {
Name string `yaml:"name"`
Domain []Domain `yaml:"domain"`
}
type Domain struct {
Type string `yaml:"type"`
Values []string `yaml:"values,omitempty"`
}
var (
rbacConfig RBACConfig
once sync.Once
)
func LoadRBACConfig(path string) error {
logger.Debug("Enter LoadRBACConfig() function")
data, err := os.ReadFile(path)
if err != nil {
return err
}
if err := yaml.Unmarshal(data, &rbacConfig); err != nil {
return err
}
// Populate store.Roles from parsed config
for roleName, role := range rbacConfig.Roles {
var perms []store.RolePermission
for _, perm := range role.Permissions {
for _, domain := range perm.Domain {
perms = append(perms, store.RolePermission{
Action: perm.Action,
DomainType: domain.Type,
DomainValues: domain.Values,
})
}
}
store.AddRoleNames(roleName)
if len(perms) > 0 {
store.AddPermissionsForRole(roleName, perms)
}
}
// Resolve role inheritance (includes)
resolveRoleIncludes()
// Load user-role mappings from users section
for userName, user := range rbacConfig.Users {
for _, role := range user.Roles {
store.AddRoleForUser(userName, role.Name)
}
}
return nil
}
func resolveRoleIncludes() {
for roleName, rbacRole := range rbacConfig.Roles {
if len(rbacRole.Includes) == 0 {
continue
}
inherited := collectRolePermissions(
UserRole{Name: roleName},
rbacConfig.Roles,
make(map[string]struct{}),
)
for _, perm := range inherited {
for _, domain := range perm.Domain {
store.AddPermissionsForRole(roleName, []store.RolePermission{
{Action: perm.Action, DomainType: domain.Type, DomainValues: domain.Values},
})
}
}
}
}
// validate token and check permission
func RequirePermission(perm string) gin.HandlerFunc {
return func(ctx *gin.Context) {
logger.Debug("Enter RequirePermission() function", zap.String("perm", perm))
token := ctx.GetHeader("Authorization")
logonUser, err := GetUsernameFromToken(token)
if err != nil {
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
return
}
logger.Debug("Logon user is ", zap.String("logonUser", logonUser))
// check if user has cluster_admin role
userRoles := store.GetUserRoleNames(logonUser)
if utils.ContainsField(userRoles, "cluster_admin") {
return
}
permsWithType := GetUserAllPermissionsWithType(logonUser)
logger.Debug("In RequirePermission after call GetUserAllPermissionsWithType from cache",
zap.Any("permsWithType", permsWithType))
var allPerms []string
for _, perm := range permsWithType {
allPerms = append(allPerms, perm.Permission)
}
found := false
for _, p := range allPerms {
if p == perm {
found = true
ctx.Next()
return
}
}
if !found {
logger.Error("Permission denied", zap.String("perm", perm), zap.String("logonUser", logonUser))
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
ctx.Abort()
return
}
}
}
// get all permissions from rbac.yaml file
func GetAllPermissions() []string {
permissions := rbacConfig.Permissions
return permissions
}
// get role permissions
func GetBuiltinRolePermissions(roleName string) []Permission {
role, ok := rbacConfig.Roles[roleName]
if !ok {
return []Permission{}
}
return role.Permissions
}
// get all user permission,
// if user not exist in store, use basic_user permission
func GetUserAllPermissionsWithType(userName string) []models.RolePermission {
userRoles := store.GetUserRoleNames(userName)
if len(userRoles) == 0 {
// case 1: user not in store, use basic_user
builtInPerms := GetBuiltinRolePermissions("basic_user")
permissionTransform := make([]models.RolePermission, 0)
for _, p := range builtInPerms {
for _, domain := range p.Domain {
rp := models.RolePermission{
Permission: p.Action,
DomainType: domain.Type,
DomainValues: domain.Values,
}
permissionTransform = append(permissionTransform, rp)
}
}
logger.Debug("case 1, assign basic_user permission to unregistered user")
return permissionTransform
}
builtInRoles := utils.IncludeBuiltinRoles(userRoles)
var permissionListsType []models.RolePermission
if len(builtInRoles) > 0 {
permissionTransform := make([]models.RolePermission, 0)
for _, role := range builtInRoles {
builtinPerms := GetBuiltinRolePermissions(role)
for _, p := range builtinPerms {
for _, domain := range p.Domain {
rp := models.RolePermission{
Permission: p.Action,
DomainType: domain.Type,
DomainValues: domain.Values,
}
permissionTransform = append(permissionTransform, rp)
}
}
}
permissionListsType = append(permissionListsType, permissionTransform...)
logger.Debug("case 2, builtin role permissions", zap.Any("permissionListsType", permissionListsType))
}
for _, userRole := range userRoles {
permissionListsType = append(permissionListsType, GetRoleAllPermissionsWithType(userRole)...)
}
logger.Debug("case 3, before remove duplicated permissions", zap.Any("permissionListsType", permissionListsType))
permissionListsType = DeduplicatePermissions(permissionListsType)
logger.Debug("case 4 DeduplicatePermissions", zap.Any("permissionListsType", permissionListsType))
return permissionListsType
}
func GetRoleAllPermissionsWithType(roleName string) []models.RolePermission {
var permissionsType []models.RolePermission
perms := store.GetRolePermissions(roleName)
for _, perm := range perms {
permissionsType = append(permissionsType, models.RolePermission{
Permission: perm.Action,
DomainType: perm.DomainType,
DomainValues: perm.DomainValues,
})
}
return permissionsType
}
// DeduplicatePermissions removes duplicate permissions based on the specified rules:
// - If permission name and domain type are the same, keep only one
// - If domain values contain "all", keep the one without "all" when both exist
// DeduplicatePermissions removes duplicate permissions based on the specified rules:
// - If permission names are the same, keep only one
// - If one has specific domain values and another has "all" domain type, keep the specific one
func DeduplicatePermissions(permissions []models.RolePermission) []models.RolePermission {
permissionMap := make(map[string]models.RolePermission) // key: permission_name
for _, perm := range permissions {
if existingPerm, exists := permissionMap[perm.Permission]; exists {
// Check if one has more specific domain than the other
existingHasAll := (existingPerm.DomainType == models.DomainTypeAll)
currentHasAll := (perm.DomainType == models.DomainTypeAll)
// If existing has "all" and current has specific domain, replace it
if existingHasAll && !currentHasAll {
permissionMap[perm.Permission] = perm
} else if !existingHasAll && currentHasAll {
// Keep existing specific domain, discard current "all"
// No action needed
} else if existingHasAll && currentHasAll {
// Both have "all", keep existing
// No action needed
} else {
// Both have specific domains, check if current has values and existing doesn't
existingHasValues := len(existingPerm.DomainValues) > 0
currentHasValues := len(perm.DomainValues) > 0
if !existingHasValues && currentHasValues {
permissionMap[perm.Permission] = perm
} else if existingHasValues && !currentHasValues {
// Keep existing with values
// No action needed
} else {
// Both have values or both don't have values, merge or keep existing
mergedValues := mergeDomainValues(existingPerm.DomainValues, perm.DomainValues)
permissionMap[perm.Permission] = models.RolePermission{
Permission: existingPerm.Permission,
DomainType: existingPerm.DomainType,
DomainValues: mergedValues,
}
}
}
} else {
permissionMap[perm.Permission] = perm
}
}
// Convert map back to slice
result := make([]models.RolePermission, 0, len(permissionMap))
for _, perm := range permissionMap {
result = append(result, perm)
}
return result
}
// Helper function to check if domain values contain "all"
func hasAllDomain(domainValues []string) bool {
for _, value := range domainValues {
if value == models.DomainTypeAll {
return true
}
}
return false
}
func mergeDomainValues(values1, values2 []string) []string {
// Use a map to track unique values
valueMap := make(map[string]bool)
// Add all values from first slice
for _, v := range values1 {
valueMap[v] = true
}
// Add all values from second slice
for _, v := range values2 {
valueMap[v] = true
}
// Convert back to slice
merged := make([]string, 0, len(valueMap))
for v := range valueMap {
merged = append(merged, v)
}
return merged
}
func GetRequireResourcePermission(logonUser string, permId string) (models.RolePermission, error) {
logger.Debug("Enter GetRequireResourcePermission() function",
zap.Any("logonUser", logonUser), zap.Any("permId", permId))
availablePerm := models.RolePermission{}
perms := GetUserAllPermissionsWithType(logonUser)
logger.Debug("In GetRequireResourcePermission function user permissions are:",
zap.Any("perms", perms))
if len(perms) == 0 {
return availablePerm, errors.New("no permission")
}
for _, perm := range perms {
if perm.Permission != permId {
continue
}
availablePerm.Permission = perm.Permission
availablePerm.DomainType = perm.DomainType
availablePerm.DomainValues = perm.DomainValues
return availablePerm, nil
}
return availablePerm, errors.New("no permission")
}
func collectRolePermissions(userRole UserRole, rbacRoles map[string]Role, visited map[string]struct{}) []Permission {
var perms []Permission
var collect func(roleName string, userDomains []Domain)
collect = func(roleName string, userDomains []Domain) {
if _, ok := visited[roleName]; ok {
return
}
visited[roleName] = struct{}{}
role, ok := rbacRoles[roleName]
if !ok {
return
}
for _, perm := range role.Permissions {
mergedDomains := mergeDomains(perm.Domain, userDomains)
if mergedDomains == nil {
mergedDomains = []Domain{{Type: "all"}} // default to all if no
}
perms = append(perms, Permission{
Action: perm.Action,
Domain: mergedDomains,
})
}
for _, inc := range role.Includes {
collect(inc, userDomains)
}
}
collect(userRole.Name, userRole.Domain)
return perms
}
func mergeDomains(roleDomains, userDomains []Domain) []Domain {
domainMap := make(map[string]map[string]struct{})
if len(userDomains) == 1 && userDomains[0].Type == "all" {
return roleDomains
}
if len(roleDomains) == 1 && roleDomains[0].Type == "all" {
return userDomains
}
for _, d := range roleDomains {
if d.Type == "all" {
continue
}
if _, ok := domainMap[d.Type]; !ok {
domainMap[d.Type] = make(map[string]struct{})
}
for _, v := range d.Values {
domainMap[d.Type][v] = struct{}{}
}
}
for _, d := range userDomains {
if d.Type == "all" {
continue
}
if _, ok := domainMap[d.Type]; !ok {
domainMap[d.Type] = make(map[string]struct{})
}
for _, v := range d.Values {
domainMap[d.Type][v] = struct{}{}
}
}
var merged []Domain
for t, vals := range domainMap {
var values []string
for v := range vals {
values = append(values, v)
}
merged = append(merged, Domain{Type: t, Values: values})
}
return merged
}