Hello from MCP server
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) },
)
}