Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"
)

// Look up by org -> role -> action -> resource -> type
// where type is the string "allow" or "deny" (typeFromDb.Get("name"))
// e.g. { orgId: {roleId1: {actionId1: {resourceId1: typeIdAllow }}}}
// Permissions.Cache[org][role][action][resource]"allow"
var ResourcesCache map[string]string
var ActionsCache map[string]string
var TypesCache map[string]string
var PermissionsCache map[string]map[string]map[string]map[string]string

func updatePermissionsCache(orgId string, app core.App) error {
	if ResourcesCache == nil {
		ResourcesCache = make(map[string]string)
		resources, err := app.FindAllRecords("resources")
		if err != nil {
			return err
		}
		for _, resource := range resources {
			ResourcesCache[resource.Id] = resource.GetString("name")
		}
	}

	if ActionsCache == nil {
		ActionsCache = make(map[string]string)
		actions, err := app.FindAllRecords("permissionActions")
		if err != nil {
			return err
		}
		for _, action := range actions {
			ActionsCache[action.Id] = action.GetString("name")
		}

	}

	if TypesCache == nil {
		TypesCache = make(map[string]string)
		permTypes, err := app.FindAllRecords("permissionTypes")
		if err != nil {
			return err
		}

		for _, permType := range permTypes {
			TypesCache[permType.Id] = permType.GetString("name")
		}

	}

	if PermissionsCache == nil {
		PermissionsCache = make(map[string]map[string]map[string]map[string]string)
	}

	permissions, err := app.FindAllRecords("permissions", dbx.NewExp("org = {:org}", dbx.Params{"org": orgId}))
	if err != nil {
		return err
	}

	if _, ok := PermissionsCache[orgId]; !ok {
		PermissionsCache[orgId] = make(map[string]map[string]map[string]string)
	}

	for _, permission := range permissions {
		role := permission.GetString("role")
		if _, ok := PermissionsCache[orgId][role]; !ok {
			PermissionsCache[orgId][role] = make(map[string]map[string]string)
		}

		action := ActionsCache[permission.GetString("action")]
		if _, ok := PermissionsCache[orgId][role][action]; !ok {
			PermissionsCache[orgId][role][action] = make(map[string]string)
		}

		resource := ResourcesCache[permission.GetString("resource")]
		PermissionsCache[orgId][role][action][resource] = TypesCache[permission.GetString("type")]
	}

	// PrintPermissionsCacheJSON(PermissionsCache)

	return nil
}

func PrintPermissionsCacheJSON(cache map[string]map[string]map[string]map[string]string) {
	b, err := json.MarshalIndent(cache, "", "  ")
	if err != nil {
		fmt.Println("Error marshaling PermissionsCache:", err)
		return
	}
	fmt.Println(string(b))
}

func CanAccessResource(userId string, resourceOrg string, resource string, action string, app core.App) (bool, error) {
	// NOTE: Finding the first profile record for a user means that users
	// can only belong to one organziation. If we want to support multi-org
	// users, then we need to change this logic. We can use the activeOrg field

	user, err := app.FindFirstRecordByData("users", "id", userId)
	if err != nil {
		return false, err
	}

	profile, err := app.FindFirstRecordByFilter(
		"profiles",
		"user = {:userId} && org = {:orgId}",
		dbx.Params{"userId": userId, "orgId": user.GetString("activeOrg")},
	)

	if err != nil {
		return false, err
	}

	errs := app.ExpandRecord(profile, []string{"roles"}, nil)
	if len(errs) > 0 {
		return false, fmt.Errorf("failed to expand %v", errs)
	}

	roles := profile.ExpandedAll("roles")
	profileOrg := profile.GetString("org")

	updatePermissionsCache(resourceOrg, app)

	// Check if the resource belongs to org
	if profileOrg != resourceOrg {
		return false, nil
	}

	// Get only the profile roles that belong to this org (just in case)
	var orgRoles []*core.Record
	for _, role := range roles {
		if role.Get("org") == resourceOrg {
			orgRoles = append(orgRoles, role)
		}
	}

	for _, role := range orgRoles {
		// If they're an admin, let them do whatever
		if role.Get("name") == "admin" {
			return true, nil
		}
	}

	// If they're not an admin, then we need to check if there is a permission
	// on the requested resource and action that allows one of their roles
	for _, role := range orgRoles {
		permType := PermissionsCache[profileOrg][role.Id][action][resource]
		if permType == "allow" {
			return true, nil
		}
	}

	return false, nil
}

func authRequest(
	userId, resourceOrg, resource, action string,
	app core.App,
	onAllow func() error,
	onDeny func() error,
) error {
	// NOTE: If org is nil, then we let the PocketBase API rules handle permissions
	if resourceOrg == "" {
		return onAllow()
	}

	allow, err := CanAccessResource(userId, resourceOrg, resource, action, app)
	if err != nil {
		return err
	}
	if allow {
		return onAllow()
	} else {
		return onDeny()
	}
}

func skipAuth(auth *core.Record, onAllow func() error) error {
	// If auth is nil, then we let the PocketBase API rules handle permissions
	if auth == nil {
		return onAllow()
	}
	//
	// If superuser then skip authorization
	if auth.Collection().Name == "_superusers" {
		return onAllow()
	}
	return nil
}

func authRecordRequest(e *core.RecordRequestEvent, action string) error {
	skip := skipAuth(e.Auth, e.Next)
	if skip != nil {
		return skip
	}

	// e.g. books are available to subscribers outside of the org
	// there are still persmission checks enforeced in the Pocketbase
	// API rules
	publicPermissions := [][]string{
		{"books", "view"},
	}

	// it doesn't matter, but this should be more efficient, like a
	// hash map lookup instead of the loop, it's just here for now...
	for _, permission := range publicPermissions {
		if permission[0] == e.Record.Collection().Name && permission[1] == action {
			return e.Next()
		}
	}

	return authRequest(
		e.Auth.Id,
		e.Record.GetString("org"),
		e.Record.Collection().Name,
		action,
		e.App,
		e.Next,
		func() error { return e.UnauthorizedError("Unauthorized", nil) },
	)
}

func authRecordsListRequest(e *core.RecordsListRequestEvent, action string) error {
	log.Println("auth records list", action, e.Auth.Collection().Name)
	if e.Auth == nil {
		return e.Next()
	}

	if e.Auth != nil && e.Auth.Collection().Name == "_superusers" {
		return e.Next()
	}

	// e.g. profiles are available to users when they have a different
	// active org but the Pocketbase API rules allow them to see all of
	// their profiles
	publicPermissions := []string{
		"profiles",
	}

	// it doesn't matter, but this should be more efficient, like a
	// hash map lookup instead of the loop, it's just here for now...
	for _, collectionName := range publicPermissions {
		if collectionName == e.Collection.Name {
			return e.Next()
		}
	}

	// We can't check the record org for a list action, so all lists are either public
	// or protected by the Pocketbase API rules
	//
	// Within an organzation, the list action can be protected by roles, but for potentially
	// public resources, like books, we check the user's role in their organiztion, not in the
	// resource owner's organization.
	// i.e. an admin in any org will be able to list books that their org subscribes to
	// but a support role in any org should not be able to list books
	return authRequest(
		e.Auth.Id,
		e.Auth.GetString("activeOrg"),
		e.Collection.Name,
		action,
		e.App,
		e.Next,
		func() error { return e.UnauthorizedError("Unauthorized", nil) },
	)
}