Skip to content

Commit a1a91f5

Browse files
committed
Feature: roles & permissions
1 parent 138cfe5 commit a1a91f5

File tree

13 files changed

+808
-109
lines changed

13 files changed

+808
-109
lines changed

datastore/datastore.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var (
1414
ErrNodeNotFound = errors.New("node not found")
1515
ErrJobNotFound = errors.New("job not found")
1616
ErrUserNotFound = errors.New("user not found")
17+
ErrRoleNotFound = errors.New("role not found")
1718
ErrContextNotFound = errors.New("context not found")
1819
)
1920

@@ -39,11 +40,18 @@ type Datastore interface {
3940
UpdateJob(ctx context.Context, id string, modify func(u *tork.Job) error) error
4041
GetJobByID(ctx context.Context, id string) (*tork.Job, error)
4142
GetJobLogParts(ctx context.Context, jobID string, page, size int) (*Page[*tork.TaskLogPart], error)
42-
GetJobs(ctx context.Context, q string, page, size int) (*Page[*tork.JobSummary], error)
43+
GetJobs(ctx context.Context, currentUser, q string, page, size int) (*Page[*tork.JobSummary], error)
4344

4445
CreateUser(ctx context.Context, u *tork.User) error
4546
GetUser(ctx context.Context, username string) (*tork.User, error)
4647

48+
CreateRole(ctx context.Context, r *tork.Role) error
49+
GetRole(ctx context.Context, id string) (*tork.Role, error)
50+
GetRoles(ctx context.Context) ([]*tork.Role, error)
51+
GetUserRoles(ctx context.Context, userID string) ([]*tork.Role, error)
52+
AssignRole(ctx context.Context, userID, roleID string) error
53+
UnassignRole(ctx context.Context, userID, roleID string) error
54+
4755
GetMetrics(ctx context.Context) (*tork.Metrics, error)
4856

4957
WithTx(ctx context.Context, f func(tx Datastore) error) error

datastore/inmemory/inmemory.go

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/runabol/tork/datastore"
1515
"github.com/runabol/tork/internal/cache"
1616
"github.com/runabol/tork/internal/slices"
17+
"github.com/runabol/tork/internal/uuid"
1718
)
1819

1920
const (
@@ -35,6 +36,8 @@ type InMemoryDatastore struct {
3536
jobs *cache.Cache[*tork.Job]
3637
usersByID *cache.Cache[*tork.User]
3738
usersByUsername *cache.Cache[*tork.User]
39+
roles *cache.Cache[*tork.Role]
40+
userRoles *cache.Cache[[]*tork.UserRole]
3841
logs *cache.Cache[[]*tork.TaskLogPart]
3942
logsMu sync.RWMutex
4043
nodeExpiration *time.Duration
@@ -80,6 +83,8 @@ func NewInMemoryDatastore(opts ...Option) *InMemoryDatastore {
8083
ds.logs = cache.New[[]*tork.TaskLogPart](cache.NoExpiration, ci)
8184
ds.usersByID = cache.New[*tork.User](cache.NoExpiration, ci)
8285
ds.usersByUsername = cache.New[*tork.User](cache.NoExpiration, ci)
86+
ds.roles = cache.New[*tork.Role](cache.NoExpiration, ci)
87+
ds.userRoles = cache.New[[]*tork.UserRole](cache.NoExpiration, ci)
8388
ds.jobs.OnEvicted(ds.onJobEviction)
8489
return ds
8590
}
@@ -255,7 +260,7 @@ func (ds *InMemoryDatastore) GetActiveTasks(ctx context.Context, jobID string) (
255260
return result, nil
256261
}
257262

258-
func (ds *InMemoryDatastore) GetJobs(ctx context.Context, q string, page, size int) (*datastore.Page[*tork.JobSummary], error) {
263+
func (ds *InMemoryDatastore) GetJobs(ctx context.Context, currentUser, q string, page, size int) (*datastore.Page[*tork.JobSummary], error) {
259264
parseQuery := func(query string) (string, []string) {
260265
terms := []string{}
261266
tags := []string{}
@@ -272,10 +277,46 @@ func (ds *InMemoryDatastore) GetJobs(ctx context.Context, q string, page, size i
272277
return strings.Join(terms, " "), tags
273278
}
274279

280+
var urs []*tork.Role
281+
var user *tork.User
282+
if currentUser != "" {
283+
u, err := ds.GetUser(ctx, currentUser)
284+
if err != nil {
285+
return nil, err
286+
}
287+
user = u
288+
ur, err := ds.GetUserRoles(ctx, u.ID)
289+
if err != nil {
290+
return nil, err
291+
}
292+
urs = ur
293+
}
294+
275295
searchTerm, tags := parseQuery(q)
276296
offset := (page - 1) * size
277297
filtered := make([]*tork.Job, 0)
298+
hasPermission := func(user *tork.User, uroles []*tork.Role, job *tork.Job) bool {
299+
if len(job.Permissions) == 0 {
300+
return true
301+
}
302+
for _, p := range job.Permissions {
303+
if p.User != nil && p.User.Username == user.Username {
304+
return true
305+
}
306+
if p.Role != nil {
307+
for _, ur := range uroles {
308+
if p.Role.Slug == ur.Slug {
309+
return true
310+
}
311+
}
312+
}
313+
}
314+
return false
315+
}
278316
ds.jobs.Iterate(func(_ string, j *tork.Job) {
317+
if currentUser != "" && !hasPermission(user, urs, j) {
318+
return
319+
}
279320
if searchTerm != "" {
280321
if strings.Contains(strings.ToLower(j.Name), strings.ToLower(searchTerm)) ||
281322
strings.Contains(strings.ToLower(string(j.State)), strings.ToLower(searchTerm)) {
@@ -458,6 +499,19 @@ func (ds *InMemoryDatastore) GetUser(ctx context.Context, uid string) (*tork.Use
458499
return nil, datastore.ErrUserNotFound
459500
}
460501

502+
func (ds *InMemoryDatastore) GetRole(ctx context.Context, id string) (*tork.Role, error) {
503+
var r *tork.Role
504+
ds.roles.Iterate(func(key string, v *tork.Role) {
505+
if key == id || v.Slug == id {
506+
r = v
507+
}
508+
})
509+
if r == nil {
510+
return nil, datastore.ErrRoleNotFound
511+
}
512+
return r, nil
513+
}
514+
461515
func (ds *InMemoryDatastore) CreateUser(ctx context.Context, u *tork.User) error {
462516
if _, ok := ds.usersByID.Get(u.ID); ok {
463517
return errors.New("user already exists")
@@ -470,6 +524,73 @@ func (ds *InMemoryDatastore) CreateUser(ctx context.Context, u *tork.User) error
470524
return nil
471525
}
472526

527+
func (ds *InMemoryDatastore) CreateRole(ctx context.Context, r *tork.Role) error {
528+
r.ID = uuid.NewUUID()
529+
now := time.Now().UTC()
530+
r.CreatedAt = &now
531+
ds.roles.Set(r.ID, r)
532+
return nil
533+
}
534+
535+
func (ds *InMemoryDatastore) GetRoles(ctx context.Context) ([]*tork.Role, error) {
536+
roles := make([]*tork.Role, 0)
537+
ds.roles.Iterate(func(_ string, v *tork.Role) {
538+
roles = append(roles, v)
539+
})
540+
sort.Slice(roles, func(i, j int) bool {
541+
return roles[i].Name < roles[j].Name
542+
})
543+
return roles, nil
544+
}
545+
546+
func (ds *InMemoryDatastore) AssignRole(ctx context.Context, userID, roleID string) error {
547+
now := time.Now().UTC()
548+
ur := &tork.UserRole{
549+
ID: uuid.NewUUID(),
550+
UserID: userID,
551+
RoleID: roleID,
552+
CreatedAt: &now,
553+
}
554+
urs, ok := ds.userRoles.Get(userID)
555+
if !ok {
556+
urs = make([]*tork.UserRole, 0)
557+
}
558+
urs = append(urs, ur)
559+
ds.userRoles.Set(userID, urs)
560+
return nil
561+
}
562+
563+
func (ds *InMemoryDatastore) UnassignRole(ctx context.Context, userID, roleID string) error {
564+
urs, ok := ds.userRoles.Get(userID)
565+
if !ok {
566+
return nil
567+
}
568+
nurs := make([]*tork.UserRole, 0)
569+
for _, v := range urs {
570+
if v.UserID != userID || v.RoleID != roleID {
571+
nurs = append(nurs, v)
572+
}
573+
}
574+
ds.userRoles.Set(userID, nurs)
575+
return nil
576+
}
577+
578+
func (ds *InMemoryDatastore) GetUserRoles(ctx context.Context, userID string) ([]*tork.Role, error) {
579+
uroles, ok := ds.userRoles.Get(userID)
580+
if !ok {
581+
return make([]*tork.Role, 0), nil
582+
}
583+
result := make([]*tork.Role, len(uroles))
584+
for i, ur := range uroles {
585+
r, ok := ds.roles.Get(ur.RoleID)
586+
if !ok {
587+
return nil, datastore.ErrRoleNotFound
588+
}
589+
result[i] = r
590+
}
591+
return result, nil
592+
}
593+
473594
func (ds *InMemoryDatastore) WithTx(ctx context.Context, f func(tx datastore.Datastore) error) error {
474595
return f(ds)
475596
}

datastore/inmemory/inmemory_test.go

Lines changed: 136 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -512,16 +512,77 @@ func TestInMemoryGetJobLogParts(t *testing.T) {
512512
func TestInMemorySearchJobs(t *testing.T) {
513513
ctx := context.Background()
514514
ds := inmemory.NewInMemoryDatastore()
515-
for i := 0; i < 101; i++ {
515+
516+
u1 := &tork.User{
517+
ID: uuid.NewUUID(),
518+
Username: uuid.NewShortUUID(),
519+
Name: "Tester",
520+
}
521+
err := ds.CreateUser(ctx, u1)
522+
assert.NoError(t, err)
523+
524+
u2 := &tork.User{
525+
ID: uuid.NewUUID(),
526+
Username: uuid.NewShortUUID(),
527+
Name: "Tester",
528+
}
529+
err = ds.CreateUser(ctx, u2)
530+
assert.NoError(t, err)
531+
532+
r := &tork.Role{
533+
Slug: "test-role",
534+
Name: "Test Role",
535+
}
536+
err = ds.CreateRole(ctx, r)
537+
assert.NoError(t, err)
538+
539+
err = ds.AssignRole(ctx, u2.ID, r.ID)
540+
assert.NoError(t, err)
541+
542+
u3 := &tork.User{
543+
ID: uuid.NewUUID(),
544+
Username: uuid.NewShortUUID(),
545+
Name: "Tester",
546+
}
547+
err = ds.CreateUser(ctx, u3)
548+
assert.NoError(t, err)
549+
550+
for i := 0; i < 100; i++ {
551+
j1 := tork.Job{
552+
ID: uuid.NewUUID(),
553+
Name: fmt.Sprintf("Job %d", (i + 1)),
554+
State: tork.JobStateRunning,
555+
Tasks: []*tork.Task{{
556+
Name: "some task",
557+
}},
558+
Tags: []string{fmt.Sprintf("tag-%d", i)},
559+
Permissions: []*tork.Permission{{
560+
User: u1,
561+
}, {
562+
Role: r,
563+
}},
564+
}
565+
err := ds.CreateJob(ctx, &j1)
566+
assert.NoError(t, err)
567+
568+
now := time.Now().UTC()
569+
err = ds.CreateTask(ctx, &tork.Task{
570+
ID: uuid.NewUUID(),
571+
JobID: j1.ID,
572+
State: tork.TaskStateRunning,
573+
CreatedAt: &now,
574+
})
575+
assert.NoError(t, err)
576+
}
577+
578+
for i := 100; i < 101; i++ {
516579
j1 := tork.Job{
517580
ID: uuid.NewUUID(),
518581
Name: fmt.Sprintf("Job %d", (i + 1)),
519582
State: tork.JobStateRunning,
520-
Tasks: []*tork.Task{
521-
{
522-
Name: "some task",
523-
},
524-
},
583+
Tasks: []*tork.Task{{
584+
Name: "some task",
585+
}},
525586
Tags: []string{fmt.Sprintf("tag-%d", i)},
526587
}
527588
err := ds.CreateJob(ctx, &j1)
@@ -537,38 +598,100 @@ func TestInMemorySearchJobs(t *testing.T) {
537598
assert.NoError(t, err)
538599
}
539600

540-
p1, err := ds.GetJobs(ctx, "", 1, 10)
601+
p1, err := ds.GetJobs(ctx, "", "", 1, 10)
541602
assert.NoError(t, err)
542603
assert.Equal(t, 10, p1.Size)
543604
assert.Equal(t, 101, p1.TotalItems)
544605

545-
p1, err = ds.GetJobs(ctx, "101", 1, 10)
606+
p1, err = ds.GetJobs(ctx, "", "101", 1, 10)
546607
assert.NoError(t, err)
547608
assert.Equal(t, 1, p1.Size)
548609
assert.Equal(t, 1, p1.TotalItems)
549610

550-
p1, err = ds.GetJobs(ctx, "tag:tag-1", 1, 10)
611+
p1, err = ds.GetJobs(ctx, "", "tag:tag-1", 1, 10)
551612
assert.NoError(t, err)
552613
assert.Equal(t, 1, p1.Size)
553614
assert.Equal(t, 1, p1.TotalItems)
554615

555-
p1, err = ds.GetJobs(ctx, "tag:not-a-tag", 1, 10)
616+
p1, err = ds.GetJobs(ctx, "", "tag:not-a-tag", 1, 10)
556617
assert.NoError(t, err)
557618
assert.Equal(t, 0, p1.Size)
558619
assert.Equal(t, 0, p1.TotalItems)
559620

560-
p1, err = ds.GetJobs(ctx, "tags:not-a-tag,tag-1", 1, 10)
621+
p1, err = ds.GetJobs(ctx, "", "tags:not-a-tag,tag-1", 1, 10)
561622
assert.NoError(t, err)
562623
assert.Equal(t, 1, p1.Size)
563624
assert.Equal(t, 1, p1.TotalItems)
564625

565-
p1, err = ds.GetJobs(ctx, "Job", 1, 10)
626+
p1, err = ds.GetJobs(ctx, "", "Job", 1, 10)
627+
assert.NoError(t, err)
628+
assert.Equal(t, 10, p1.Size)
629+
assert.Equal(t, 101, p1.TotalItems)
630+
631+
p1, err = ds.GetJobs(ctx, "", "running", 1, 10)
566632
assert.NoError(t, err)
567633
assert.Equal(t, 10, p1.Size)
568634
assert.Equal(t, 101, p1.TotalItems)
569635

570-
p1, err = ds.GetJobs(ctx, "running", 1, 10)
636+
p1, err = ds.GetJobs(ctx, u1.Username, "running", 1, 10)
571637
assert.NoError(t, err)
572638
assert.Equal(t, 10, p1.Size)
573639
assert.Equal(t, 101, p1.TotalItems)
640+
641+
p1, err = ds.GetJobs(ctx, u2.Username, "running", 1, 10)
642+
assert.NoError(t, err)
643+
assert.Equal(t, 10, p1.Size)
644+
assert.Equal(t, 101, p1.TotalItems)
645+
646+
p1, err = ds.GetJobs(ctx, u3.Username, "running", 1, 10)
647+
assert.NoError(t, err)
648+
assert.Equal(t, 1, p1.Size)
649+
assert.Equal(t, 1, p1.TotalItems)
650+
}
651+
652+
func TestInMemoryCreateRole(t *testing.T) {
653+
ctx := context.Background()
654+
ds := inmemory.NewInMemoryDatastore()
655+
now := time.Now().UTC()
656+
r := &tork.Role{
657+
ID: uuid.NewUUID(),
658+
Slug: "test-role",
659+
Name: "Test Role",
660+
CreatedAt: &now,
661+
}
662+
err := ds.CreateRole(ctx, r)
663+
assert.NoError(t, err)
664+
665+
role, err := ds.GetRole(ctx, r.Slug)
666+
assert.NoError(t, err)
667+
assert.Equal(t, r.Slug, role.Slug)
668+
669+
roles, err := ds.GetRoles(ctx)
670+
assert.NoError(t, err)
671+
assert.Len(t, roles, 1)
672+
assert.Equal(t, "Test Role", roles[0].Name)
673+
674+
u := &tork.User{
675+
ID: uuid.NewUUID(),
676+
Username: uuid.NewShortUUID(),
677+
Name: "Tester",
678+
CreatedAt: &now,
679+
}
680+
err = ds.CreateUser(ctx, u)
681+
assert.NoError(t, err)
682+
683+
err = ds.AssignRole(ctx, u.ID, r.ID)
684+
assert.NoError(t, err)
685+
686+
uroles, err := ds.GetUserRoles(ctx, u.ID)
687+
assert.NoError(t, err)
688+
assert.Len(t, uroles, 1)
689+
assert.Equal(t, r.ID, uroles[0].ID)
690+
691+
err = ds.UnassignRole(ctx, u.ID, r.ID)
692+
assert.NoError(t, err)
693+
694+
uroles, err = ds.GetUserRoles(ctx, u.ID)
695+
assert.NoError(t, err)
696+
assert.Len(t, uroles, 0)
574697
}

0 commit comments

Comments
 (0)