Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/orm/orm.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ type Object interface {
const OwnerIDKey = "owner_id"
const TenantIDKey = "tenant_id"
const TeamsIDKey = "teams_id"
const TeamIDKey = "team_id"
const ProjectIDKey = "project_id"
const SystemFieldsKey = "_system"

func GetSystemFieldKey(field string) string {
Expand Down
21 changes: 21 additions & 0 deletions core/security/param_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* Copyright © INFINI LTD. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */

package security

import "infini.sh/framework/core/param"

// Standard parameter keys for user session context.
// These keys are used with UserSessionInfo.GetString() / GetStringArray()
// since UserSessionInfo embeds param.Parameters.
const (
ParamTenantID param.ParaKey = "tenant_id"
ParamTenantName param.ParaKey = "tenant_name"
ParamTeamID param.ParaKey = "team_id"
ParamTeamName param.ParaKey = "team_name"
ParamProjectID param.ParaKey = "project_id"
ParamProjectName param.ParaKey = "project_name"
ParamTeamIDs param.ParaKey = "team_ids"
ParamProjectIDs param.ParaKey = "project_ids"
)
10 changes: 10 additions & 0 deletions modules/security/orm_hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,16 @@ func init() {

bq.ShouldClauses = append(bq.ShouldClauses, orm.TermQuery(orm.GetSystemFieldKey(orm.OwnerIDKey), userID))

// Include resources shared with user's teams.
if teamIDs, ok := sessionUser.GetStringArray(security.ParamTeamIDs); ok && len(teamIDs) > 0 {
bq.ShouldClauses = append(bq.ShouldClauses, orm.TermsQuery(orm.GetSystemFieldKey(orm.TeamIDKey), teamIDs))
}

// Include resources shared with user's projects.
if projectIDs, ok := sessionUser.GetStringArray(security.ParamProjectIDs); ok && len(projectIDs) > 0 {
bq.ShouldClauses = append(bq.ShouldClauses, orm.TermsQuery(orm.GetSystemFieldKey(orm.ProjectIDKey), projectIDs))
}

if len(bq.ShouldClauses) > 1 {
bq.Parameter("minimum_should_match", 1)
}
Expand Down
25 changes: 19 additions & 6 deletions modules/security/share/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"infini.sh/framework/core/api"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/orm"
"infini.sh/framework/core/rate"
"infini.sh/framework/core/security"
)

Expand Down Expand Up @@ -69,6 +70,15 @@ func (r *BulkOpResponses[T]) AddUnchanged(item *T) {
}

func (h APIHandler) createOrUpdateShare(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
// Rate limit: 1 req/sec per user to prevent abuse
sessionUser := security.MustGetUserFromRequest(req)
userID := sessionUser.MustGetUserID()
limiter := rate.GetRateLimiterPerSecond("share_api", userID, 1)
if !limiter.Allow() {
h.WriteError(w, "rate limit exceeded, please try again later", http.StatusTooManyRequests)
return
}

op := ShareRequest{}
h.MustDecodeJSON(req, &op)

Expand All @@ -78,9 +88,6 @@ func (h APIHandler) createOrUpdateShare(w http.ResponseWriter, req *http.Request
ctx := orm.NewContextWithParent(req.Context())
ctx.Refresh = orm.WaitForRefresh

sessionUser := security.MustGetUserFromContext(ctx.Context)
userID := sessionUser.MustGetUserID()

newOp := ShareRequest{}
for _, v := range op.Shares {
v.ResourceType = resourceType
Expand All @@ -104,15 +111,21 @@ func (h APIHandler) createOrUpdateShare(w http.ResponseWriter, req *http.Request
}

func (h APIHandler) batchCreateOrUpdateShare(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
// Rate limit: 1 req/sec per user to prevent abuse
sessionUser := security.MustGetUserFromRequest(req)
userID := sessionUser.MustGetUserID()
limiter := rate.GetRateLimiterPerSecond("share_batch_api", userID, 1)
if !limiter.Allow() {
h.WriteError(w, "rate limit exceeded, please try again later", http.StatusTooManyRequests)
return
}

op := ShareRequest{}
h.MustDecodeJSON(req, &op)

ctx := orm.NewContextWithParent(req.Context())
ctx.Refresh = orm.WaitForRefresh

sessionUser := security.MustGetUserFromContext(ctx.Context)
userID := sessionUser.MustGetUserID()

service := NewSharingService()
lists, err := service.CreateOrUpdateShares(ctx, userID, &op)
if err != nil {
Expand Down
120 changes: 120 additions & 0 deletions modules/security/share/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package share

import (
"sync"

"infini.sh/framework/core/elastic"
"infini.sh/framework/core/orm"
"infini.sh/framework/core/security"
)

type ResolvedResource struct {
Exists bool
OwnerID string
Attributes map[string]string
Resource ResourceEntity
}

type ResourceResolver func(ctx *orm.Context, resource ResourceEntity) (*ResolvedResource, error)

type ResolvedPrincipal struct {
Exists bool
Attributes map[string]string
}

type PrincipalResolver func(ctx *orm.Context, principalID string) (*ResolvedPrincipal, error)

type RuntimeExtension struct {
BuildIdentityScope func(ctx *orm.Context, user *security.UserSessionInfo, record *SharingRecord) []string
ValidateResolvedResource func(ctx *orm.Context, user *security.UserSessionInfo, resource *ResolvedResource) error
ValidateResolvedPrincipal func(ctx *orm.Context, user *security.UserSessionInfo, principalType string, principalID string, principal *ResolvedPrincipal) error
PrepareShareForWrite func(ctx *orm.Context, user *security.UserSessionInfo, resource *ResolvedResource, record *SharingRecord) error
PrepareExistingShareUpdate func(ctx *orm.Context, user *security.UserSessionInfo, record *SharingRecord) error
}

var resourceResolvers sync.Map
var principalResolvers sync.Map
var runtimeExtensions []RuntimeExtension
var runtimeExtensionsLock sync.RWMutex

func RegisterResourceResolver(resourceType string, resolver ResourceResolver) {
resourceResolvers.Store(resourceType, resolver)
}

func RegisterPrincipalResolver(principalType string, resolver PrincipalResolver) {
principalResolvers.Store(principalType, resolver)
}

func RegisterRuntimeExtension(ext RuntimeExtension) {
runtimeExtensionsLock.Lock()
defer runtimeExtensionsLock.Unlock()
runtimeExtensions = append(runtimeExtensions, ext)
}

func getResourceResolver(resourceType string) ResourceResolver {
if value, ok := resourceResolvers.Load(resourceType); ok {
if resolver, ok := value.(ResourceResolver); ok {
return resolver
}
}
return nil
}

func getPrincipalResolver(principalType string) PrincipalResolver {
if value, ok := principalResolvers.Load(principalType); ok {
if resolver, ok := value.(PrincipalResolver); ok {
return resolver
}
}
return nil
}

func getRuntimeExtensions() []RuntimeExtension {
runtimeExtensionsLock.RLock()
defer runtimeExtensionsLock.RUnlock()
if len(runtimeExtensions) == 0 {
return nil
}
out := make([]RuntimeExtension, len(runtimeExtensions))
copy(out, runtimeExtensions)
return out
}

func newResolverReadContext(parent *orm.Context) *orm.Context {
var ctx *orm.Context
if parent != nil && parent.Context != nil {
ctx = orm.NewContextWithParent(parent.Context)
ctx.Refresh = parent.Refresh
} else {
ctx = orm.NewContext()
}
ctx.DirectReadAccess()
ctx.PermissionScope(security.PermissionScopePlatform)
return ctx
}

func defaultUserPrincipalResolver(ctx *orm.Context, principalID string) (*ResolvedPrincipal, error) {

if _, user, err := security.GetUserByID(principalID); err == nil && user != nil {
return &ResolvedPrincipal{Exists: true}, nil
}

lookupCtx := newResolverReadContext(ctx)
orm.WithModel(lookupCtx, &security.UserAccount{})

qb := orm.NewQuery()
qb.Filter(orm.TermQuery("id", principalID))
qb.Size(1)

docs := []security.UserAccount{}
err, _ := elastic.SearchV2WithResultItemMapper(lookupCtx, &docs, qb, nil)
if err != nil {
return &ResolvedPrincipal{Exists: false}, nil
}

return &ResolvedPrincipal{Exists: len(docs) > 0}, nil
}

func init() {
RegisterPrincipalResolver(security.PrincipalTypeUser, defaultUserPrincipalResolver)
}
Loading
Loading