diff --git a/models/permission.go b/models/permission.go new file mode 100644 index 0000000..933b16d --- /dev/null +++ b/models/permission.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type Permission struct { + BaseModel + Name string + Slug string +} + +// TableName sets the table name +func (Permission) TableName() string { + return "permissions" +} diff --git a/models/role-permissions.go b/models/role-permissions.go new file mode 100644 index 0000000..99cf3fe --- /dev/null +++ b/models/role-permissions.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type RolePermission struct { + BaseModel + RoleID uint // Role id + PermissionID uint // Permission id +} + +// TableName sets the table name +func (RolePermission) TableName() string { + return "role_permissions" +} diff --git a/models/roles.go b/models/roles.go new file mode 100644 index 0000000..decd675 --- /dev/null +++ b/models/roles.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type Role struct { + BaseModel + Name string // The name of the role + Slug string // String based unique identifier of the role, (use hyphen seperated role name '-', instead of space) +} + +// TableName sets the table name +func (Role) TableName() string { + return "roles" +} diff --git a/models/user-roles.go b/models/user-roles.go new file mode 100644 index 0000000..9c69ba7 --- /dev/null +++ b/models/user-roles.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type UserRole struct { + BaseModel + UserID string // The user id + RoleID uint // The role id +} + +// TableName sets the table name +func (UserRole) TableName() string { + return "user_roles" +} diff --git a/run-auto-migrations.go b/run-auto-migrations.go index 8410637..119a44c 100644 --- a/run-auto-migrations.go +++ b/run-auto-migrations.go @@ -18,4 +18,8 @@ func RunAutoMigrations() { // Add auto migrations for your models here... db.AutoMigrate(&models.User{}) + db.AutoMigrate(&models.UserRole{}) + db.AutoMigrate(&models.Role{}) + db.AutoMigrate(&models.RolePermission{}) + db.AutoMigrate(&models.Permission{}) } diff --git a/utils/authority.go b/utils/authority.go new file mode 100644 index 0000000..401f36b --- /dev/null +++ b/utils/authority.go @@ -0,0 +1,408 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "errors" + "fmt" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" + "gorm.io/gorm" +) + +type Authority struct{} + +var auth *Authority + +var ( + ErrPermissionInUse = errors.New("cannot delete assigned permission") + ErrPermissionNotFound = errors.New("permission not found") + ErrRoleInUse = errors.New("cannot delete assigned role") + ErrRoleNotFound = errors.New("role not found") +) + +// Add a new role to the database +func (a *Authority) CreateRole(c *core.Context, r models.Role) error { + roleSlug := r.Slug + var dbRole models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&dbRole) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + // create + createRes := c.GetGorm().Create(&r) + if createRes.Error != nil { + return createRes.Error + } + return nil + } + return res.Error + } + + return errors.New(fmt.Sprintf("role '%v' already exists", roleSlug)) +} + +// Add a new permission to the database +func (a *Authority) CreatePermission(c *core.Context, p models.Permission) error { + permSlug := p.Slug + var dbPerm models.Permission + res := c.GetGorm().Where("slug = ?", permSlug).First(&dbPerm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + // create + createRes := c.GetGorm().Create(&p) + if createRes.Error != nil { + return createRes.Error + } + return nil + } + return res.Error + } + + return errors.New(fmt.Sprintf("permission '%v' already exists", permSlug)) +} + +// Assigns a group of permissions to a given role +func (a *Authority) AssignPermissionsToRole(c *core.Context, roleSlug string, permSlugs []string) error { + var role models.Role + rRes := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if rRes.Error != nil { + if errors.Is(rRes.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return rRes.Error + } + var perms []models.Permission + for _, permSlug := range permSlugs { + var perm models.Permission + pRes := c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if pRes.Error != nil { + if errors.Is(pRes.Error, gorm.ErrRecordNotFound) { + return ErrPermissionNotFound + } + return pRes.Error + } + perms = append(perms, perm) + } + tx := c.GetGorm().Begin() + for _, perm := range perms { + var rolePerm models.RolePermission + res := c.GetGorm().Where("role_id = ?", role.ID).Where("permission_id =?", perm.ID).First(&rolePerm) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + cRes := tx.Create(&models.RolePermission{RoleID: role.ID, PermissionID: perm.ID}) + if cRes.Error != nil { + tx.Rollback() + return cRes.Error + } + } + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + tx.Rollback() + return res.Error + } + if rolePerm != (models.RolePermission{}) { + tx.Rollback() + return errors.New(fmt.Sprintf("permission '%v' is aleady assigned to the role '%v'", perm.Name, role.Name)) + } + rolePerm = models.RolePermission{} + } + return tx.Commit().Error +} + +// Assigns a role to a given user +func (a *Authority) AssignRoleToUser(c *core.Context, userID interface{}, roleSlug string) error { + userIDStr := fmt.Sprintf("%v", userID) + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return res.Error + } + var userRole models.UserRole + res = c.GetGorm().Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + c.GetGorm().Create(&models.UserRole{UserID: userIDStr, RoleID: role.ID}) + return nil + } + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return res.Error + } + + return errors.New(fmt.Sprintf("this role '%v' is aleady assigned to the user", roleSlug)) +} + +// Checks if a role is assigned to a user +func (a *Authority) CheckUserRole(c *core.Context, userID interface{}, roleSlug string) (bool, error) { + userIDStr := fmt.Sprintf("%v", userID) + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrRoleNotFound + } + return false, res.Error + } + + // check if the role is a assigned + var userRole models.UserRole + res = c.GetGorm().Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, res.Error + } + + return true, nil +} + +// Checks if a permission is assigned to a user +func (a *Authority) CheckUserPermission(c *core.Context, userID interface{}, permSlug string) (bool, error) { + userIDStr := fmt.Sprintf("%v", userID) + // the user role + var userRoles []models.UserRole + res := c.GetGorm().Where("user_id = ?", userIDStr).Find(&userRoles) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, res.Error + } + + //prepare an array of role ids + var roleIDs []interface{} + for _, r := range userRoles { + roleIDs = append(roleIDs, r.RoleID) + } + + // find the permission + var perm models.Permission + res = c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrPermissionNotFound + } + return false, res.Error + } + + // find the role permission + var rolePermission models.RolePermission + res = c.GetGorm().Where("role_id IN (?)", roleIDs).Where("permission_id = ?", perm.ID).First(&rolePermission) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, res.Error + } + + return true, nil +} + +// Checks if a permission is assigned to a role +func (a *Authority) CheckRolePermission(c *core.Context, roleSlug string, permSlug string) (bool, error) { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrRoleNotFound + } + return false, res.Error + } + + // find the permission + var perm models.Permission + res = c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrPermissionNotFound + } + return false, res.Error + } + + // find the rolePermission + var rolePermission models.RolePermission + res = c.GetGorm().Where("role_id = ?", role.ID).Where("permission_id = ?", perm.ID).First(&rolePermission) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, res.Error + } + + return true, nil +} + +// Revokes a roles's permission +func (a *Authority) RevokeRolePermission(c *core.Context, roleSlug string, permSlug string) error { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return res.Error + } + + // find the permission + var perm models.Permission + res = c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrPermissionNotFound + } + return res.Error + } + + // revoke the permission + rRes := c.GetGorm().Where("role_id = ?", role.ID).Where("permission_id = ?", perm.ID).Delete(models.RolePermission{}) + if rRes.Error != nil { + return rRes.Error + } + + return nil +} + +// Returns all stored roles +func (a *Authority) GetAllRoles(c *core.Context) ([]models.Role, error) { + var roles []models.Role + res := c.GetGorm().Find(&roles) + if res.Error != nil { + return nil, res.Error + } + + return roles, nil +} + +// Returns all user assigned roles +func (a *Authority) GetUserRoles(c *core.Context, userID interface{}) ([]models.Role, error) { + userIDStr := fmt.Sprintf("%v", userID) + var userRoles []models.UserRole + res := c.GetGorm().Where("user_id = ?", userIDStr).Find(&userRoles) + if res.Error != nil { + return nil, res.Error + } + + var roleIDs []interface{} + for _, r := range userRoles { + roleIDs = append(roleIDs, r.RoleID) + } + + var roles []models.Role + res = c.GetGorm().Where("id IN (?)", roleIDs).Find(&roles) + if res.Error != nil { + return nil, res.Error + } + + return roles, nil +} + +// Returns all role assigned permissions +func (a *Authority) GetRolePermissions(c *core.Context, roleSlug string) ([]models.Permission, error) { + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).Find(&role) + if res.Error != nil { + return nil, res.Error + } + + var rolePerms []models.RolePermission + res = c.GetGorm().Where("role_id = ?", role.ID).Find(&rolePerms) + if res.Error != nil { + return nil, res.Error + } + var permIDs []interface{} + for _, rolePerm := range rolePerms { + permIDs = append(permIDs, rolePerm.PermissionID) + } + + var perms []models.Permission + res = c.GetGorm().Where("id IN (?)", permIDs).Find(&perms) + if res.Error != nil { + return nil, res.Error + } + + return perms, nil +} + +// Returns all stored permissions +func (a *Authority) GetAllPermissions(c *core.Context) ([]models.Permission, error) { + var perms []models.Permission + res := c.GetGorm().Find(&perms) + if res.Error != nil { + return nil, res.Error + } + + return perms, nil +} + +// Deletes a given role even if it's has assigned permissions +func (a *Authority) DeleteRole(c *core.Context, roleSlug string) error { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + return res.Error + } + + // check if the role is assigned to a user + var ca int64 + res = c.GetGorm().Model(models.UserRole{}).Where("role_id = ?", role.ID).Count(&ca) + if res.Error != nil { + return res.Error + } + + if ca != 0 { + // role is assigned + return ErrRoleInUse + } + tx := c.GetGorm().Begin() + // revoke the assignment of permissions before deleting the role + dRes := tx.Where("role_id = ?", role.ID).Delete(models.RolePermission{}) + if dRes.Error != nil { + tx.Rollback() + return dRes.Error + } + + // delete the role + dRes = c.GetGorm().Where("slug = ?", roleSlug).Delete(models.Role{}) + if dRes.Error != nil { + tx.Rollback() + return dRes.Error + } + + return tx.Commit().Error +} + +// Deletes a given permission +func (a *Authority) DeletePermission(c *core.Context, permSlug string) error { + // find the permission + var perm models.Permission + res := c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + return res.Error + } + + // check if the permission is assigned to a role + var rolePermission models.RolePermission + res = c.GetGorm().Where("permission_id = ?", perm.ID).First(&rolePermission) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return res.Error + } + + if res.Error == nil { + return ErrPermissionInUse + } + + // delete the permission + dRes := c.GetGorm().Where("slug = ?", permSlug).Delete(models.Permission{}) + if dRes.Error != nil { + return dRes.Error + } + + return nil +}