416 lines
12 KiB
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
|
|
}
|