move api/server to api/edge/bff

This commit is contained in:
Stephan D
2026-02-28 00:39:20 +01:00
parent 34182af3b8
commit 98db0e4e9e
248 changed files with 406 additions and 18 deletions

View File

@@ -0,0 +1,94 @@
package permissionsimp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func (a *PermissionsAPI) changePolicies(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
var req srequest.ChangePolicies
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Warn("Failed to decode role policies change request", zap.Error(err))
return response.BadPayload(a.logger, mservice.Roles, err)
}
if req.Add != nil && req.Remove != nil {
for _, addItem := range *req.Add {
for _, removeItem := range *req.Remove {
if addItem == removeItem {
a.logger.Debug("Duplicate policies found, rejecting policies update request", zap.Any("add", &addItem), zap.Any("remove", &removeItem))
return response.BadRequest(a.logger, a.Name(), "invalid_policies_change_request", "duplicate policies found in 'add' and 'remove' fields")
}
}
}
}
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
return a.changePoliciesImp(ctx, account, &req)
}); err != nil {
a.logger.Debug("Rolling policies changes back", zap.Error(err))
return response.Auto(a.logger, a.Name(), err)
}
return response.Success(a.logger)
}
func (a *PermissionsAPI) changePoliciesImp(
ctx context.Context,
account *model.Account,
req *srequest.ChangePolicies,
) (any, error) {
// helper that runs through each change-item, enforces the right action,
// and then calls apply(item) if enforcement passes.
handle := func(items *[]model.RolePolicy, action model.Action, opName string, apply func(context.Context, *model.RolePolicy) error) error {
for _, it := range *items {
// 1) permission check
ok, err := a.enforcer.Enforce(ctx, a.policiesPermissionRef, account.ID, it.OrganizationRef, bson.NilObjectID, action)
if err != nil {
a.logger.Warn(fmt.Sprintf("failed to enforce permission while %s policy", opName), zap.Error(err), zap.Any(opName, &it))
return err
}
if !ok {
a.logger.Debug(fmt.Sprintf("policy %s denied", opName))
return merrors.AccessDenied(mservice.Policies, string(action), bson.NilObjectID)
}
// 2) perform the add/remove
if err := apply(ctx, &it); err != nil {
a.logger.Warn(fmt.Sprintf("failed to %s role policy", opName), zap.Error(err), zap.Any("policy", &it))
return err
}
}
return nil
}
// REMOVE
if req.Remove != nil {
if err := handle(req.Remove, model.ActionDelete, "remove", func(ctx context.Context, it *model.RolePolicy) error {
return a.auth.Permission().RevokeFromRole(ctx, it)
}); err != nil {
return nil, err
}
}
// ADD
if req.Add != nil {
if err := handle(req.Add, model.ActionCreate, "add", func(ctx context.Context, it *model.RolePolicy) error {
return a.auth.Permission().GrantToRole(ctx, it)
}); err != nil {
return nil, err
}
}
return nil, nil
}

View File

@@ -0,0 +1,85 @@
package permissionsimp
import (
"context"
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func (a *PermissionsAPI) changeRole(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
orgRef, err := mutil.GetOrganizationRef(r)
if err != nil {
a.logger.Warn("Failed to restore organization reference", zap.Error(err), zap.String("organization_ref", mutil.GetOrganizationID(r)))
return response.BadReference(a.logger, a.Name(), mutil.OrganizationRefName(), mutil.GetOrganizationID(r), err)
}
var req srequest.ChangeRole
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Warn("Failed to decode change role request", zap.Error(err))
return response.BadPayload(a.logger, mservice.Roles, err)
}
ctx := r.Context()
res, err := a.enforcer.Enforce(ctx, a.rolesPermissionRef, account.ID, orgRef, req.AccountRef, model.ActionUpdate)
if err != nil {
a.logger.Warn("Failed to check permissions while assigning new role", zap.Error(err),
mzap.ObjRef("requesting_account_ref", account.ID), mzap.AccRef(req.AccountRef),
mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
}
if !res {
a.logger.Debug("Permission denied to set new role", mzap.ObjRef("requesting_account_ref", account.ID),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
return response.AccessDenied(a.logger, a.Name(), "no permission to change user roles")
}
var roleDescription model.RoleDescription
if err := a.rdb.Get(ctx, req.NewRoleDescriptionRef, &roleDescription); err != nil {
a.logger.Warn("Failed to fetch and validate role description", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}
return a.changeRoleImp(ctx, &req, orgRef, account)
}
func (a *PermissionsAPI) changeRoleImp(ctx context.Context, req *srequest.ChangeRole, organizationRef bson.ObjectID, account *model.Account) http.HandlerFunc {
roles, err := a.enforcer.GetRoles(ctx, req.AccountRef, organizationRef)
// TODO: add check that role revocation won't leave venue without the owner
if err != nil {
a.logger.Warn("Failed to fetch account roles", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}
for _, role := range roles {
if err := a.manager.Role().Revoke(ctx, role.DescriptionRef, req.AccountRef, organizationRef); err != nil {
a.logger.Warn("Failed to revoke old role", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef),
mzap.ObjRef("role_ref", role.DescriptionRef))
// continue...
}
}
role := model.Role{
AccountRef: req.AccountRef,
OrganizationRef: organizationRef,
DescriptionRef: req.NewRoleDescriptionRef,
}
if err := a.manager.Role().Assign(ctx, &role); err != nil {
a.logger.Warn("Failed to assign new role", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef),
mzap.ObjRef("role_ref", req.NewRoleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}
return response.Success(a.logger)
}

View File

@@ -0,0 +1,29 @@
package permissionsimp
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (a *PermissionsAPI) createRoleDescription(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
var req model.RoleDescription
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Warn("Failed to decode role creation request", zap.Error(err))
return response.BadPayload(a.logger, mservice.Roles, err)
}
if err := a.rdb.Create(r.Context(), &req); err != nil {
a.logger.Warn("Failed to create role description", zap.Error(err),
mzap.ObjRef("requesting_account_ref", account.ID), zap.String("role_name", req.Name))
return response.Auto(a.logger, a.Name(), err)
}
return response.Success(a.logger)
}

View File

@@ -0,0 +1,28 @@
package permissionsimp
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *PermissionsAPI) deleteRoleDescription(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
roleDescriptionRef, err := a.Rph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to restore object reference", zap.Error(err), mutil.PLog(a.Rph, r))
return response.BadReference(a.logger, a.Name(), a.Rph.Name(), a.Rph.GetID(r), err)
}
if err := a.rdb.Delete(r.Context(), roleDescriptionRef); err != nil {
a.logger.Warn("Failed to delete role description", zap.Error(err),
mzap.ObjRef("requesting_account_ref", account.ID), mzap.ObjRef("role_ref", roleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}
return response.Success(a.logger)
}

View File

@@ -0,0 +1,51 @@
package permissionsimp
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *PermissionsAPI) get(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
orgRef, err := mutil.GetOrganizationRef(r)
if err != nil {
a.logger.Warn("Failed to restore organization reference", zap.Error(err), zap.String("organization_ref", mutil.GetOrganizationID(r)))
return response.BadReference(a.logger, a.Name(), mutil.OrganizationRefName(), mutil.GetOrganizationID(r), err)
}
ctx := r.Context()
roles, permissions, err := a.enforcer.GetPermissions(ctx, *account.GetID(), orgRef)
if len(roles) == 0 {
a.logger.Warn("No roles defined for account", mzap.StorableRef(account), mzap.ObjRef("organization_ref", orgRef))
return response.AccessDenied(a.logger, a.Name(), "User has no roles assigned")
}
if err != nil {
a.logger.Warn("Failed to fetch account policies", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Internal(a.logger, a.Name(), err)
}
roleDescs, err := a.rdb.List(ctx, orgRef, nil)
if err != nil {
a.logger.Warn("Failed to fetch organization roles", mzap.ObjRef("organization_ref", orgRef))
return response.Internal(a.logger, a.Name(), err)
}
policies, err := a.getRolePolicies(ctx, roleDescs)
if err != nil {
a.logger.Warn("Failed to fetch roles policies", zap.Error(err))
return response.Auto(a.logger, a.Name(), err)
}
permDescs, err := a.pdb.All(ctx, orgRef)
if err != nil {
a.logger.Warn("Failed to fetch organization permissions", mzap.ObjRef("organization_ref", orgRef))
return response.Internal(a.logger, a.Name(), err)
}
return sresponse.Permisssions(a.logger,
roleDescs, permDescs,
roles, policies, permissions,
accessToken,
)
}

View File

@@ -0,0 +1,75 @@
package permissionsimp
import (
"context"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func (a *PermissionsAPI) getRolePolicies(ctx context.Context, roles []model.RoleDescription) ([]model.RolePolicy, error) {
policies := make([]model.RolePolicy, 0)
uniqueRefs := make(map[bson.ObjectID]struct{})
for _, role := range roles {
uniqueRefs[*role.GetID()] = struct{}{}
}
for ref := range uniqueRefs {
plcs, err := a.auth.Permission().GetPolicies(ctx, ref)
if err != nil {
a.logger.Warn("Failed to fetch role permissions", zap.Error(err), mzap.ObjRef("role_ref", ref))
return nil, err
}
policies = append(policies, plcs...)
}
return policies, nil
}
func (a *PermissionsAPI) getAll(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
orgRef, err := mutil.GetOrganizationRef(r)
if err != nil {
a.logger.Warn("Failed to restore organization reference", zap.Error(err), zap.String("organization_ref", mutil.GetOrganizationID(r)))
return response.BadReference(a.logger, a.Name(), mutil.ObjRefName(), mutil.GetOrganizationID(r), err)
}
ctx := r.Context()
res, err := a.enforcer.Enforce(ctx, a.rolesPermissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Debug("Error occurred", zap.Error(err))
response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access to permissions denied")
response.AccessDenied(a.logger, a.Name(), "no required permissiosn to read account permissions data")
}
var org model.Organization
if err := a.db.Get(ctx, account.ID, orgRef, &org); err != nil {
a.logger.Warn("Failed to fetch venue", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, a.Name(), err)
}
roles := make([]model.Role, 0)
permissions := make([]model.Permission, 0)
for _, employee := range org.Members {
rls, prms, err := a.enforcer.GetPermissions(ctx, employee, orgRef)
if len(rls) == 0 {
a.logger.Warn("No roles defined for account", mzap.ObjRef("employee_ref", employee), mzap.ObjRef("organization_ref", orgRef))
return response.NotFound(a.logger, a.Name(), "User has no roles assigned")
}
if err != nil {
a.logger.Warn("Failed to fetch account policies", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, a.Name(), err)
}
roles = append(roles, rls...)
permissions = append(permissions, prms...)
}
return a.permissions(ctx, orgRef, roles, permissions, accessToken)
}

View File

@@ -0,0 +1,33 @@
package permissionsimp
import (
"context"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func (a *PermissionsAPI) permissions(ctx context.Context, organizationRef bson.ObjectID, roles []model.Role, permissions []model.Permission, accessToken *sresponse.TokenData) http.HandlerFunc {
roleDescs, err := a.rdb.List(ctx, organizationRef, nil)
if err != nil {
a.logger.Warn("Failed to fetch organization roles", zap.Error(err), mzap.ObjRef("organization_ref", organizationRef))
return response.Internal(a.logger, a.Name(), err)
}
permDescs, err := a.pdb.All(ctx, organizationRef)
if err != nil {
a.logger.Warn("Failed to fetch organization permissions", zap.Error(err), mzap.ObjRef("organization_ref", organizationRef))
return response.Internal(a.logger, a.Name(), err)
}
policies, err := a.getRolePolicies(ctx, roleDescs)
if err != nil {
a.logger.Warn("Failed to fetch roles policies", zap.Error(err))
return response.Auto(a.logger, a.Name(), err)
}
return sresponse.Permisssions(a.logger, roleDescs, permDescs, roles, policies, permissions, accessToken)
}

View File

@@ -0,0 +1,87 @@
package permissionsimp
import (
"context"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/organization"
"github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/role"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
eapi "github.com/tech/sendico/server/interface/api"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
type PermissionsAPI struct {
logger mlogger.Logger
db organization.DB
pdb policy.DB
rdb role.DB
enforcer auth.Enforcer
manager auth.Manager
rolesPermissionRef bson.ObjectID
policiesPermissionRef bson.ObjectID
Rph mutil.ParamHelper
tf transaction.Factory
auth auth.Manager
}
func (a *PermissionsAPI) Name() mservice.Type {
return mservice.Permissions
}
func (a *PermissionsAPI) Finish(_ context.Context) error {
return nil
}
func CreateAPI(a eapi.API) (*PermissionsAPI, error) {
p := &PermissionsAPI{
enforcer: a.Permissions().Enforcer(),
manager: a.Permissions().Manager(),
Rph: mutil.CreatePH("role"),
tf: a.DBFactory().TransactionFactory(),
auth: a.Permissions().Manager(),
}
p.logger = a.Logger().Named(p.Name())
var err error
if p.db, err = a.DBFactory().NewOrganizationDB(); err != nil {
p.logger.Error("Failed to create organizations database", zap.Error(err))
return nil, err
}
if p.rdb, err = a.DBFactory().NewRolesDB(); err != nil {
p.logger.Error("Failed to create roles database", zap.Error(err))
return nil, err
}
if p.pdb, err = a.DBFactory().NewPoliciesDB(); err != nil {
p.logger.Error("Failed to create policies database", zap.Error(err))
return nil, err
}
var pdesc model.PolicyDescription
if err := p.pdb.GetBuiltInPolicy(context.Background(), mservice.Roles, &pdesc); err != nil {
p.logger.Warn("Failed to fetch roles management permission description", zap.Error(err))
return nil, err
}
p.rolesPermissionRef = pdesc.ID
if err := p.pdb.GetBuiltInPolicy(context.Background(), mservice.Policies, &pdesc); err != nil {
p.logger.Warn("Failed to fetch policies management permission description", zap.Error(err))
return nil, err
}
p.policiesPermissionRef = pdesc.ID
a.Register().AccountHandler(p.Name(), mutil.AddOrganizaztionRef("/"), api.Get, p.get)
a.Register().AccountHandler(p.Name(), mutil.AddOrganizaztionRef("/all"), api.Get, p.getAll)
a.Register().AccountHandler(p.Name(), mutil.AddOrganizaztionRef("/change_role"), api.Post, p.changeRole)
a.Register().AccountHandler(p.Name(), "/policies", api.Put, p.changePolicies)
a.Register().AccountHandler(p.Name(), "/role", api.Post, p.createRoleDescription)
a.Register().AccountHandler(p.Name(), p.Rph.AddRef("/role"), api.Delete, p.deleteRoleDescription)
return p, nil
}