Skip to content

Commit

Permalink
manage user permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
dwradcliffe committed Oct 6, 2024
1 parent 8d28b50 commit 8229977
Show file tree
Hide file tree
Showing 19 changed files with 471 additions and 47 deletions.
4 changes: 4 additions & 0 deletions backend/pkg/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type DatabaseRepositoryType string
type InstallationVerificationStatus string
type InstallationQuotaStatus string
type UserRole string
type Permission string

const (
ResourceListPageSize int = 20
Expand Down Expand Up @@ -54,4 +55,7 @@ const (

UserRoleUser UserRole = "user"
UserRoleAdmin UserRole = "admin"

PermissionManageSources Permission = "manage_sources"
PermissionRead Permission = "read"
)
104 changes: 104 additions & 0 deletions backend/pkg/database/gorm_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,110 @@ func (gr *GormRepository) GetUsers(ctx context.Context) ([]models.User, error) {
return sanitizedUsers, result.Error
}

func (gr *GormRepository) GetUser(ctx context.Context, userID uuid.UUID) (*models.User, error) {
var dbUser models.User
var user models.User
result := gr.GormClient.WithContext(ctx).First(&dbUser, userID)
if result.Error != nil {
return nil, result.Error
}

user.ID = dbUser.ID
user.FullName = dbUser.FullName
user.Email = dbUser.Email
user.Username = dbUser.Username
user.Role = dbUser.Role

// Populate ACLs for the user
var acls []models.UserPermission
if err := gr.GormClient.WithContext(ctx).
Where("user_id = ?", user.ID).
Find(&acls).Error; err != nil {
return nil, err
}
user.Permissions = make(map[string]map[string]bool)

for _, acl := range acls {
if _, exists := user.Permissions[acl.TargetUserID.String()]; !exists {
user.Permissions[acl.TargetUserID.String()] = make(map[string]bool)
}
user.Permissions[acl.TargetUserID.String()][string(acl.Permission)] = true
}

return &user, nil
}

func (gr *GormRepository) UpdateUserAndPermissions(ctx context.Context, user models.User) error {
// Lookup user from the db
var dbUser models.User
result := gr.GormClient.WithContext(ctx).First(&dbUser, user.ID)
if result.Error != nil {
return result.Error
}
// Update fields on User
updates := map[string]interface{}{
"full_name": user.FullName,
"username": user.Username,
"email": user.Email,
"role": user.Role,
}
result = gr.GormClient.WithContext(ctx).Model(dbUser).Updates(updates)
if result.Error != nil {
return result.Error
}
// Update User Permissions
var existingPermissions []models.UserPermission
if err := gr.GormClient.WithContext(ctx).
Where("user_id = ?", user.ID).
Find(&existingPermissions).Error; err != nil {
return err
}
for targetUserId, permissions := range user.Permissions {
for permission, value := range permissions {
if !value {
continue
}
// Check if the permission already exists
exists := false
for _, existingPermission := range existingPermissions {
if existingPermission.TargetUserID.String() == targetUserId && string(existingPermission.Permission) == permission {
exists = true
break
}
}
if !exists {
// Add new permission
p := models.UserPermission{
UserID: user.ID,
TargetUserID: uuid.Must(uuid.Parse(targetUserId)),
Permission: pkg.Permission(permission),
}
err := gr.GormClient.WithContext(ctx).Create(&p).Error
if err != nil {
return err
}
}
}
}

// Remove permissions that are no longer in user.Permissions
for _, existingPermission := range existingPermissions {
targetUserId := existingPermission.TargetUserID.String()
permission := string(existingPermission.Permission)

// Check if the permission still exists in the new user.Permissions
if _, exists := user.Permissions[targetUserId]; !exists || !user.Permissions[targetUserId][permission] {
// Permission no longer exists, so delete it
err := gr.GormClient.WithContext(ctx).Delete(&existingPermission).Error
if err != nil {
return err
}
}
}

return nil
}

//</editor-fold>

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
15 changes: 15 additions & 0 deletions backend/pkg/database/gorm_repository_migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
_20240813222836 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240813222836"
_20240827214347 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240827214347"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
Expand Down Expand Up @@ -225,6 +226,20 @@ func (gr *GormRepository) Migrate() error {
return nil
},
},
{
ID: "20240827214347", // add UserPermission model
Migrate: func(tx *gorm.DB) error {

err := tx.AutoMigrate(
&_20240827214347.UserPermission{},
)
if err != nil {
return err
}

return nil
},
},
})

// run when database is empty
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/database/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type DatabaseRepository interface {
GetCurrentUser(ctx context.Context) (*models.User, error)
DeleteCurrentUser(ctx context.Context) error
GetUsers(ctx context.Context) ([]models.User, error)
GetUser(ctx context.Context, userId uuid.UUID) (*models.User, error)
UpdateUserAndPermissions(ctx context.Context, user models.User) error

GetSummary(ctx context.Context) (*models.Summary, error)

Expand Down
20 changes: 20 additions & 0 deletions backend/pkg/database/migrations/20240827214347/user_permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package _20240827214347

import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/google/uuid"
)

type Permission string

const (
PermissionManageSources Permission = "manage_sources"
PermissionRead Permission = "read"
)

type UserPermission struct {
models.ModelBase
UserID uuid.UUID `json:"user_id" gorm:"type:uuid"`
TargetUserID uuid.UUID `json:"target_user_id" gorm:"type:uuid"`
Permission Permission `json:"permission"`
}
29 changes: 29 additions & 0 deletions backend/pkg/database/mock/mock_database.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 8 additions & 10 deletions backend/pkg/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import (
"fmt"
"strings"

"golang.org/x/crypto/bcrypt"

"github.com/fastenhealth/fasten-onprem/backend/pkg"
"golang.org/x/crypto/bcrypt"
)

type User struct {
ModelBase
FullName string `json:"full_name"`
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`

//additional optional metadata that Fasten stores with users
Picture string `json:"picture"`
Email string `json:"email"`
Role pkg.UserRole `json:"role"`
FullName string `json:"full_name"`
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`
Picture string `json:"picture"`
Email string `json:"email"`
Role pkg.UserRole `json:"role"`
Permissions map[string]map[string]bool `json:"permissions" gorm:"-:all"`
}

func (user *User) HashPassword(password string) error {
Expand Down
13 changes: 13 additions & 0 deletions backend/pkg/models/user_permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

import (
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/google/uuid"
)

type UserPermission struct {
ModelBase
UserID uuid.UUID `json:"user_id" gorm:"type:uuid"`
TargetUserID uuid.UUID `json:"target_user_id" gorm:"type:uuid"`
Permission pkg.Permission `json:"permission"`
}
41 changes: 41 additions & 0 deletions backend/pkg/web/handler/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -54,3 +55,43 @@ func CreateUser(c *gin.Context) {

c.JSON(http.StatusOK, gin.H{"success": true, "data": newUser})
}

func GetUser(c *gin.Context) {
if !IsAdmin(c) {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "Unauthorized"})
return
}

databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)

user, err := databaseRepo.GetUser(c, uuid.MustParse(c.Param("userId")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}

c.JSON(200, gin.H{"success": true, "data": user})
}

func UpdateUser(c *gin.Context) {
if !IsAdmin(c) {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "Unauthorized"})
return
}

databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)

var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
return
}

err := databaseRepo.UpdateUserAndPermissions(c, user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}

c.JSON(200, gin.H{"success": true, "data": user})
}
2 changes: 2 additions & 0 deletions backend/pkg/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {

secure.GET("/users", handler.GetUsers)
secure.POST("/users", handler.CreateUser)
secure.GET("/users/:userId", handler.GetUser)
secure.POST("/users/:userId", handler.UpdateUser)

//server-side-events handler (only supported on mac/linux)
// TODO: causes deadlock on Windows
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ResourceCreatorComponent } from './pages/resource-creator/resource-crea
import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component';
import { SourceDetailComponent } from './pages/source-detail/source-detail.component';
import { UserCreateComponent } from './pages/user-create/user-create.component';
import { UserEditComponent } from "./pages/user-edit/user-edit.component";
import { UserListComponent } from './pages/user-list/user-list.component';

const routes: Routes = [
Expand Down Expand Up @@ -55,6 +56,7 @@ const routes: Routes = [

{ path: 'users', component: UserListComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
{ path: 'users/new', component: UserCreateComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },
{ path: 'users/:user_id', component: UserEditComponent, canActivate: [ IsAuthenticatedAuthGuard, IsAdminAuthGuard ] },

// { path: 'general-pages', loadChildren: () => import('./general-pages/general-pages.module').then(m => m.GeneralPagesModule) },
// { path: 'ui-elements', loadChildren: () => import('./ui-elements/ui-elements.module').then(m => m.UiElementsModule) },
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/app/models/fasten/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
export const POSSIBLE_PERMISSIONS = [
{ name: 'Manage Sources', value: 'manage_sources' },
{ name: 'Read', value: 'read' },
]

export class User {
user_id?: number
id?: string
full_name?: string
username?: string
email?: string
password?: string
role?: string
permissions?: {
[targetUserId: string]: {
[key in typeof POSSIBLE_PERMISSIONS[number]['value']]: boolean;
}
}
}
Loading

0 comments on commit 8229977

Please sign in to comment.