Skip to content

Commit

Permalink
feat: list project/group permissions over current user
Browse files Browse the repository at this point in the history
Signed-off-by: Kush Sharma <[email protected]>
  • Loading branch information
kushsharma committed Sep 16, 2023
1 parent 89f7f62 commit 08553a1
Show file tree
Hide file tree
Showing 33 changed files with 6,623 additions and 5,659 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
VERSION := $(shell git describe --tags ${TAG})
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto ui
.DEFAULT_GOAL := build
PROTON_COMMIT := "92eacbcf84c2f82f5e605845bb21948fe1b05bdd"
PROTON_COMMIT := "f8d2c72515a5d10205a493b75df9374e0db9c524"

ui:
@echo " > generating ui build"
Expand Down
2 changes: 1 addition & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func buildAPIDependencies(
policyService := policy.NewService(policyPGRepository, relationService, roleService)

userRepository := postgres.NewUserRepository(dbc)
userService := user.NewService(userRepository, relationService, permissionRepository)
userService := user.NewService(userRepository, relationService)

svUserRepo := postgres.NewServiceUserRepository(dbc)
scUserCredRepo := postgres.NewServiceUserCredentialRepository(dbc)
Expand Down
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"errors"
"fmt"
"os"
"path/filepath"

Expand Down Expand Up @@ -63,6 +64,9 @@ func Load(serverConfigFileFromFlag string) (*Frontier, error) {

// backward compatibility
conf = postHook(conf)
if conf.App.IdentityProxyHeader != "" {
fmt.Println("WARNING: running in development mode, bypassing all authorization checks")
}

return conf, nil
}
Expand Down
2 changes: 1 addition & 1 deletion core/permission/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import "errors"
var (
ErrInvalidID = errors.New("permission id is invalid")
ErrNotExist = errors.New("permission doesn't exist")
ErrInvalidDetail = errors.New("invalid action detail")
ErrInvalidDetail = errors.New("invalid permission detail")
)
7 changes: 7 additions & 0 deletions core/permission/permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,10 @@ func convertDotPermissionToSlug(s string) string {
}
return s
}

func AddNamespaceIfRequired(namespace string, name string) string {
if strings.Contains(name, ".") || strings.Contains(name, "_") || strings.Contains(name, "/") {
return name
}
return fmt.Sprintf("%s:%s", namespace, name)
}
2 changes: 1 addition & 1 deletion core/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var OwnerRole = schema.RoleProjectOwner

type Repository interface {
GetByID(ctx context.Context, id string) (Project, error)
GetByIDs(ctx context.Context, ids []string) ([]Project, error)
GetByIDs(ctx context.Context, ids []string, flt Filter) ([]Project, error)
GetByName(ctx context.Context, slug string) (Project, error)
Create(ctx context.Context, org Project) (Project, error)
List(ctx context.Context, f Filter) ([]Project, error)
Expand Down
8 changes: 4 additions & 4 deletions core/project/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ func (s Service) Get(ctx context.Context, idOrName string) (Project, error) {
return s.repository.GetByName(ctx, idOrName)
}

func (s Service) GetByIDs(ctx context.Context, ids []string) ([]Project, error) {
return s.repository.GetByIDs(ctx, ids)
func (s Service) GetByIDs(ctx context.Context, ids []string, flt Filter) ([]Project, error) {
return s.repository.GetByIDs(ctx, ids, flt)
}

func (s Service) Create(ctx context.Context, prj Project) (Project, error) {
Expand Down Expand Up @@ -98,7 +98,7 @@ func (s Service) List(ctx context.Context, f Filter) ([]Project, error) {
return s.repository.List(ctx, f)
}

func (s Service) ListByUser(ctx context.Context, userID string) ([]Project, error) {
func (s Service) ListByUser(ctx context.Context, userID string, flt Filter) ([]Project, error) {
requestedUser, err := s.userService.GetByID(ctx, userID)
if err != nil {
return nil, err
Expand All @@ -119,7 +119,7 @@ func (s Service) ListByUser(ctx context.Context, userID string) ([]Project, erro
if len(projIDs) == 0 {
return []Project{}, nil
}
return s.GetByIDs(ctx, projIDs)
return s.GetByIDs(ctx, projIDs, flt)
}

func (s Service) Update(ctx context.Context, prj Project) (Project, error) {
Expand Down
85 changes: 4 additions & 81 deletions core/user/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ package user

import (
"context"
"fmt"
"net/mail"
"strings"
"time"

"github.com/raystack/frontier/core/permission"

"github.com/raystack/frontier/pkg/utils"

"github.com/raystack/frontier/core/relation"
Expand All @@ -23,29 +20,21 @@ type RelationService interface {
Delete(ctx context.Context, rel relation.Relation) error
LookupSubjects(ctx context.Context, rel relation.Relation) ([]string, error)
LookupResources(ctx context.Context, rel relation.Relation) ([]string, error)
BatchCheckPermission(ctx context.Context, relations []relation.Relation) ([]relation.CheckPair, error)
}

type PermissionService interface {
List(ctx context.Context, flt permission.Filter) ([]permission.Permission, error)
}

type Service struct {
repository Repository
relationService RelationService
Now func() time.Time
permissionService PermissionService
repository Repository
relationService RelationService
Now func() time.Time
}

func NewService(repository Repository, relationRepo RelationService,
permissionService PermissionService) *Service {
func NewService(repository Repository, relationRepo RelationService) *Service {
return &Service{
repository: repository,
relationService: relationRepo,
Now: func() time.Time {
return time.Now().UTC()
},
permissionService: permissionService,
}
}

Expand Down Expand Up @@ -172,72 +161,6 @@ func (s Service) ListByGroup(ctx context.Context, groupID string, permissionFilt
return s.repository.GetByIDs(ctx, userIDs)
}

func (s Service) ListByGroupWithAccessPairs(ctx context.Context, groupID string, permissions []string) ([]AccessPair, error) {
users, err := s.ListByGroup(ctx, groupID, schema.MembershipPermission)
if err != nil {
return nil, fmt.Errorf("fetching users: %w", err)
}

// ensure all permissions exist
permModels, err := s.permissionService.List(ctx, permission.Filter{Namespace: schema.GroupNamespace})
if err != nil {
return nil, fmt.Errorf("fetching permission: %w", err)
}
for _, perm := range permissions {
if !utils.ContainsFunc(permModels, func(p permission.Permission) bool {
return p.Name == perm
}) {
return nil, fmt.Errorf("invalid %s: %w", perm, permission.ErrNotExist)
}
}

var accessPairs []AccessPair
var checks []relation.Relation
// fetch access pairs permissions
for _, user := range users {
relSubject := relation.Subject{
ID: user.ID,
Namespace: schema.UserPrincipal,
}
relObj := relation.Object{
ID: groupID,
Namespace: schema.GroupNamespace,
}
for _, permission := range permissions {
checks = append(checks, relation.Relation{
Subject: relSubject,
Object: relObj,
RelationName: permission,
})
}

accessPairs = append(accessPairs, AccessPair{
User: user,
Can: []string{},
On: schema.JoinNamespaceAndResourceID(schema.GroupNamespace, groupID),
})
}

// create permission check requests
checkPairs, err := s.relationService.BatchCheckPermission(ctx, checks)
if err != nil {
return nil, fmt.Errorf("checking permissions: %w", err)
}
successChecks := utils.Filter(checkPairs, func(pair relation.CheckPair) bool {
return pair.Status
})

for _, checkPair := range successChecks {
accessPairs = utils.Map(accessPairs, func(pair AccessPair) AccessPair {
if pair.User.ID == checkPair.Relation.Subject.ID {
pair.Can = append(pair.Can, checkPair.Relation.RelationName)
}
return pair
})
}
return accessPairs, nil
}

func (s Service) Sudo(ctx context.Context, id string) error {
currentUser, err := s.GetByID(ctx, id)
if errors.Is(err, ErrNotExist) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ api:
"permission":
{
"type": "string",
"description": "the permission name to check. <br/> *Example:* `get` or `list`",
"description": "the permission name to check. <br/> *Example:* `get`, `list`, `compute.instance.create`",
},
"resource":
{
Expand Down Expand Up @@ -339,7 +339,7 @@ import TabItem from "@theme/TabItem";

Returns true if a principal has required permissions to access a resource and false otherwise.<br/> Note the principal can be a user or a service account, and Frontier will the credentials from the current logged in principal from the session cookie (if any), or the client id and secret (in case of service users) or the access token (in case of human user accounts).

<MimeTabs><TabItem label={"application/json"} value={"application/json-schema"}><details style={{}} data-collapsed={false} open={true}><summary style={{"textAlign":"left"}}><strong>Request Body</strong><strong style={{"fontSize":"var(--ifm-code-font-size)","color":"var(--openapi-required)"}}> required</strong></summary><div style={{"textAlign":"left","marginLeft":"1rem"}}></div><ul style={{"marginLeft":"1rem"}}><SchemaItem collapsible={false} name={"objectId"} required={false} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"Deprecated. Use `resource` field instead."}}></SchemaItem><SchemaItem collapsible={false} name={"objectNamespace"} required={false} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"Deprecated. Use `resource` field instead."}}></SchemaItem><SchemaItem collapsible={false} name={"permission"} required={true} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"the permission name to check. <br/> *Example:* `get` or `list`"}}></SchemaItem><SchemaItem collapsible={false} name={"resource"} required={false} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"`namespace:uuid` or `namespace:name` of the org or project, and `namespace:urn` of a resource under a project. In case of an org/project either provide the complete namespace (app/organization) or Frontier can also parse aliases for the same as `org` or `project`. <br/> *Example:* `organization:92f69c3a-334b-4f25-90b8-4d4f3be6b825` or `app/project:project-name` or `compute/instance:92f69c3a-334b-4f25-90b8-4d4f3be6b825`"}}></SchemaItem></ul></details></TabItem></MimeTabs><div><ApiTabs><TabItem label={"200"} value={"200"}><div>
<MimeTabs><TabItem label={"application/json"} value={"application/json-schema"}><details style={{}} data-collapsed={false} open={true}><summary style={{"textAlign":"left"}}><strong>Request Body</strong><strong style={{"fontSize":"var(--ifm-code-font-size)","color":"var(--openapi-required)"}}> required</strong></summary><div style={{"textAlign":"left","marginLeft":"1rem"}}></div><ul style={{"marginLeft":"1rem"}}><SchemaItem collapsible={false} name={"objectId"} required={false} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"Deprecated. Use `resource` field instead."}}></SchemaItem><SchemaItem collapsible={false} name={"objectNamespace"} required={false} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"Deprecated. Use `resource` field instead."}}></SchemaItem><SchemaItem collapsible={false} name={"permission"} required={true} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"the permission name to check. <br/> *Example:* `get`, `list`, `compute.instance.create`"}}></SchemaItem><SchemaItem collapsible={false} name={"resource"} required={false} schemaName={"string"} qualifierMessage={undefined} schema={{"type":"string","description":"`namespace:uuid` or `namespace:name` of the org or project, and `namespace:urn` of a resource under a project. In case of an org/project either provide the complete namespace (app/organization) or Frontier can also parse aliases for the same as `org` or `project`. <br/> *Example:* `organization:92f69c3a-334b-4f25-90b8-4d4f3be6b825` or `app/project:project-name` or `compute/instance:92f69c3a-334b-4f25-90b8-4d4f3be6b825`"}}></SchemaItem></ul></details></TabItem></MimeTabs><div><ApiTabs><TabItem label={"200"} value={"200"}><div>

A successful response.

Expand Down
Loading

0 comments on commit 08553a1

Please sign in to comment.