Rework roles as enum, change AuthSource to enum

This commit is contained in:
Christoph Kluge
2023-03-06 11:44:38 +01:00
parent df44bd9621
commit f37e7c26f6
21 changed files with 205 additions and 141 deletions

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/ClusterCockpit/cc-backend/pkg/log"
@@ -19,36 +20,83 @@ import (
"github.com/jmoiron/sqlx"
)
const (
RoleAdmin string = "admin"
RoleSupport string = "support"
RoleManager string = "manager"
RoleUser string = "user"
RoleApi string = "api"
)
var validRoles = [5]string{RoleUser, RoleManager, RoleSupport, RoleAdmin, RoleApi}
type AuthSource int
const (
AuthViaLocalPassword int8 = 0
AuthViaLDAP int8 = 1
AuthViaToken int8 = 2
AuthViaLocalPassword AuthSource = iota
AuthViaLDAP
AuthViaToken
)
type User struct {
Username string `json:"username"`
Password string `json:"-"`
Name string `json:"name"`
Roles []string `json:"roles"`
AuthSource int8 `json:"via"`
Email string `json:"email"`
Projects []string `json:"projects"`
Username string `json:"username"`
Password string `json:"-"`
Name string `json:"name"`
Roles []string `json:"roles"`
AuthSource AuthSource `json:"via"`
Email string `json:"email"`
Projects []string `json:"projects"`
Expiration time.Time
}
func (u *User) HasRole(role string) bool {
type Role int
const (
RoleAnonymous Role = iota
RoleApi
RoleUser
RoleManager
RoleSupport
RoleAdmin
RoleError
)
func GetRoleString(roleInt Role) string {
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
}
func getRoleEnum(roleStr string) Role {
switch strings.ToLower(roleStr) {
case "admin":
return RoleAdmin
case "support":
return RoleSupport
case "manager":
return RoleManager
case "user":
return RoleUser
case "api":
return RoleApi
case "anonymous":
return RoleAnonymous
default:
return RoleError
}
}
func isValidRole(role string) bool {
if getRoleEnum(role) == RoleError {
log.Errorf("Unknown Role %s", role)
return false
}
return true
}
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
if isValidRole(role) {
for _, r := range u.Roles {
if r == role {
return true, true
}
}
return false, true
}
return false, false
}
func (u *User) HasRole(role Role) bool {
for _, r := range u.Roles {
if r == role {
if r == GetRoleString(role) {
return true
}
}
@@ -56,10 +104,10 @@ func (u *User) HasRole(role string) bool {
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAnyRole(queryroles []string) bool {
func (u *User) HasAnyRole(queryroles []Role) bool {
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == qr {
if ur == GetRoleString(qr) {
return true
}
}
@@ -68,12 +116,12 @@ func (u *User) HasAnyRole(queryroles []string) bool {
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAllRoles(queryroles []string) bool {
func (u *User) HasAllRoles(queryroles []Role) bool {
target := len(queryroles)
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == qr {
if ur == GetRoleString(qr) {
matches += 1
break
}
@@ -88,11 +136,11 @@ func (u *User) HasAllRoles(queryroles []string) bool {
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasNotRoles(queryroles []string) bool {
func (u *User) HasNotRoles(queryroles []Role) bool {
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == qr {
if ur == GetRoleString(qr) {
matches += 1
break
}
@@ -106,20 +154,47 @@ func (u *User) HasNotRoles(queryroles []string) bool {
}
}
// Find highest role, returns integer
func (u *User) GetAuthLevel() int {
// Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role
func GetValidRoles(user *User) ([]string, error) {
var vals []string
if user.HasRole(RoleAdmin) {
for i := RoleApi; i < RoleError; i++ {
vals = append(vals, GetRoleString(i))
}
return vals, nil
}
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
}
// Called by routerConfig web.page setup in backend: Only requires known user and/or not API user
func GetValidRolesMap(user *User) (map[string]Role, error) {
named := make(map[string]Role)
if user.HasNotRoles([]Role{RoleApi, RoleAnonymous}) {
for i := RoleApi; i < RoleError; i++ {
named[GetRoleString(i)] = i
}
return named, nil
}
return named, fmt.Errorf("Only known users are allowed to fetch a list of roles")
}
// Find highest role
func (u *User) GetAuthLevel() Role {
if u.HasRole(RoleAdmin) {
return 5
return RoleAdmin
} else if u.HasRole(RoleSupport) {
return 4
return RoleSupport
} else if u.HasRole(RoleManager) {
return 3
return RoleManager
} else if u.HasRole(RoleUser) {
return 2
return RoleUser
} else if u.HasRole(RoleApi) {
return 1
return RoleApi
} else if u.HasRole(RoleAnonymous) {
return RoleAnonymous
} else {
return 0
return RoleError
}
}
@@ -132,24 +207,6 @@ func (u *User) HasProject(project string) bool {
return false
}
func IsValidRole(role string) bool {
for _, r := range validRoles {
if r == role {
return true
}
}
return false
}
func GetValidRoles(user *User) ([5]string, error) {
var vals [5]string
if !user.HasRole(RoleAdmin) {
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
} else {
return validRoles, nil
}
}
func GetUser(ctx context.Context) *User {
x := ctx.Value(ContextUserKey)
if x == nil {

View File

@@ -146,12 +146,16 @@ func (ja *JWTAuthenticator) Login(
if rawroles, ok := claims["roles"].([]interface{}); ok {
for _, rr := range rawroles {
if r, ok := rr.(string); ok {
roles = append(roles, r)
if isValidRole(r) {
roles = append(roles, r)
}
}
}
}
if rawrole, ok := claims["roles"].(string); ok {
roles = append(roles, rawrole)
if isValidRole(rawrole) {
roles = append(roles, rawrole)
}
}
if user == nil {

View File

@@ -164,7 +164,7 @@ func (la *LdapAuthenticator) Sync() error {
name := newnames[username]
log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name)
if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
username, 1, name, "[\""+RoleUser+"\"]"); err != nil {
username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil {
log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
return err
}

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log"
@@ -133,23 +134,25 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
func (auth *Authentication) AddRole(
ctx context.Context,
username string,
role string) error {
queryrole string) error {
newRole := strings.ToLower(queryrole)
user, err := auth.GetUser(username)
if err != nil {
log.Warnf("Could not load user '%s'", username)
return err
}
if !IsValidRole(role) {
return fmt.Errorf("Invalid user role: %v", role)
exists, valid := user.HasValidRole(newRole)
if !valid {
return fmt.Errorf("Supplied role is no valid option : %v", newRole)
}
if exists {
return fmt.Errorf("User %v already has role %v", username, newRole)
}
if user.HasRole(role) {
return fmt.Errorf("user %#v already has role %#v", username, role)
}
roles, _ := json.Marshal(append(user.Roles, role))
roles, _ := json.Marshal(append(user.Roles, newRole))
if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
log.Errorf("Error while adding new role for user '%s'", user.Username)
return err
@@ -157,41 +160,40 @@ func (auth *Authentication) AddRole(
return nil
}
func (auth *Authentication) RemoveRole(ctx context.Context, username string, role string) error {
func (auth *Authentication) RemoveRole(ctx context.Context, username string, queryrole string) error {
oldRole := strings.ToLower(queryrole)
user, err := auth.GetUser(username)
if err != nil {
log.Warnf("Could not load user '%s'", username)
return err
}
if !IsValidRole(role) {
return fmt.Errorf("Invalid user role: %#v", role)
exists, valid := user.HasValidRole(oldRole)
if !valid {
return fmt.Errorf("Supplied role is no valid option : %v", oldRole)
}
if !exists {
return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole)
}
if role == RoleManager && len(user.Projects) != 0 {
if oldRole == GetRoleString(RoleManager) && len(user.Projects) != 0 {
return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects)
}
var exists bool
var newroles []string
for _, r := range user.Roles {
if r != role {
if r != oldRole {
newroles = append(newroles, r) // Append all roles not matching requested to be deleted role
} else {
exists = true
}
}
if exists == true {
var mroles, _ = json.Marshal(newroles)
if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
log.Errorf("Error while removing role for user '%s'", user.Username)
return err
}
return nil
} else {
return fmt.Errorf("User '%v' already does not have role: %v", username, role)
var mroles, _ = json.Marshal(newroles)
if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
log.Errorf("Error while removing role for user '%s'", user.Username)
return err
}
return nil
}
func (auth *Authentication) AddProject(
@@ -262,7 +264,7 @@ func (auth *Authentication) RemoveProject(ctx context.Context, username string,
func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
me := GetUser(ctx)
if me != nil && me.Username != username && me.HasNotRoles([]string{RoleAdmin, RoleSupport, RoleManager}) {
if me != nil && me.Username != username && me.HasNotRoles([]Role{RoleAdmin, RoleSupport, RoleManager}) {
return nil, errors.New("forbidden")
}