From 3eb854c58da33196a0d7051238b10ab3307f40a5 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 2 Apr 2025 11:04:27 +0200 Subject: [PATCH] [MM-63421] add openID Authorization API-compliant PDP interface (#30462) --- server/channels/app/platform/enterprise.go | 6 ++ server/channels/app/platform/service.go | 6 ++ .../mocks/PolicyDecisionPointInterface.go | 62 +++++++++++++++++++ server/einterfaces/pdp.go | 16 +++++ server/public/model/access_request.go | 43 +++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 server/einterfaces/mocks/PolicyDecisionPointInterface.go create mode 100644 server/einterfaces/pdp.go create mode 100644 server/public/model/access_request.go diff --git a/server/channels/app/platform/enterprise.go b/server/channels/app/platform/enterprise.go index b66a458d35..ee158b8f58 100644 --- a/server/channels/app/platform/enterprise.go +++ b/server/channels/app/platform/enterprise.go @@ -37,3 +37,9 @@ var metricsInterfaceFn func(*PlatformService, string, string) einterfaces.Metric func RegisterMetricsInterface(f func(*PlatformService, string, string) einterfaces.MetricsInterface) { metricsInterfaceFn = f } + +var pdpInterface func(*PlatformService) einterfaces.PolicyDecisionPointInterface + +func RegisterPdpInterface(f func(*PlatformService) einterfaces.PolicyDecisionPointInterface) { + pdpInterface = f +} diff --git a/server/channels/app/platform/service.go b/server/channels/app/platform/service.go index 36cde084ae..5130f1c641 100644 --- a/server/channels/app/platform/service.go +++ b/server/channels/app/platform/service.go @@ -111,6 +111,8 @@ type PlatformService struct { // This is a test mode setting used to enable Redis // without a license. forceEnableRedis bool + + pdpService einterfaces.PolicyDecisionPointInterface } type HookRunner interface { @@ -474,6 +476,10 @@ func (ps *PlatformService) initEnterprise() { if licenseInterface != nil { ps.licenseManager = licenseInterface(ps) } + + if pdpInterface != nil { + ps.pdpService = pdpInterface(ps) + } } func (ps *PlatformService) TotalWebsocketConnections() int { diff --git a/server/einterfaces/mocks/PolicyDecisionPointInterface.go b/server/einterfaces/mocks/PolicyDecisionPointInterface.go new file mode 100644 index 0000000000..b2b6cb483e --- /dev/null +++ b/server/einterfaces/mocks/PolicyDecisionPointInterface.go @@ -0,0 +1,62 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +// Regenerate this file using `make einterfaces-mocks`. + +package mocks + +import ( + model "github.com/mattermost/mattermost/server/public/model" + request "github.com/mattermost/mattermost/server/public/shared/request" + mock "github.com/stretchr/testify/mock" +) + +// PolicyDecisionPointInterface is an autogenerated mock type for the PolicyDecisionPointInterface type +type PolicyDecisionPointInterface struct { + mock.Mock +} + +// AccessEvaluation provides a mock function with given fields: rctx, accessRequest +func (_m *PolicyDecisionPointInterface) AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (*model.AccessDecision, *model.AppError) { + ret := _m.Called(rctx, accessRequest) + + if len(ret) == 0 { + panic("no return value specified for AccessEvaluation") + } + + var r0 *model.AccessDecision + var r1 *model.AppError + if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) (*model.AccessDecision, *model.AppError)); ok { + return rf(rctx, accessRequest) + } + if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) *model.AccessDecision); ok { + r0 = rf(rctx, accessRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AccessDecision) + } + } + + if rf, ok := ret.Get(1).(func(request.CTX, model.AccessRequest) *model.AppError); ok { + r1 = rf(rctx, accessRequest) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// NewPolicyDecisionPointInterface creates a new instance of PolicyDecisionPointInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPolicyDecisionPointInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *PolicyDecisionPointInterface { + mock := &PolicyDecisionPointInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/einterfaces/pdp.go b/server/einterfaces/pdp.go new file mode 100644 index 0000000000..ea526b1d29 --- /dev/null +++ b/server/einterfaces/pdp.go @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package einterfaces + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" +) + +// PolicyDecisionPointInterface is the service that evaluates access requests +// using the OpenID Auth API spec. It determines whether a subject can perform +// an action on a resource based on the resource policy. +type PolicyDecisionPointInterface interface { + AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (*model.AccessDecision, *model.AppError) +} diff --git a/server/public/model/access_request.go b/server/public/model/access_request.go new file mode 100644 index 0000000000..9165e87752 --- /dev/null +++ b/server/public/model/access_request.go @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// Subject represents the user or a virtual entity for which the Authorization +// API is called. +type Subject struct { + // ID is the unique identifier of the Subject. + // it can be a user ID, bot ID, etc and it is scoped to the Type. + ID string `json:"id"` + // Type specifies the type of the Subject, eg. user, bot, etc. + Type string `json:"type"` + // Properties are the key-value pairs assicuated with the subject. + // An attribute may be single-valued or multi-valued and can be a primitive type + // (string, boolean, number) or a complex type like a JSON object or array. + Properties map[string]any `json:"properties"` +} + +// Resource is the target of an access request. +type Resource struct { + // ID is the unique identifier of the Resource. + // It can be a channel ID, post ID, etc and it is scoped to the Type. + ID string `json:"id"` + // Type specifies the type of the Resource, eg. channel, post, etc. + Type string `json:"type"` +} + +// AccessRequest represents the input to the Policy Decision Point (PDP). +// It contains the Subject, Resource, Action and optional Context attributes. +type AccessRequest struct { + Subject Subject `json:"subject"` + Resource Resource `json:"resource"` + Action string `json:"action"` + Context map[string]any `json:"context,omitempty"` +} + +// The PDP evaluates the request and returns an AccessDecision. +// The Decision field is a boolean indicating whether the request is allowed or not. +type AccessDecision struct { + Decision bool `json:"decision"` + Context map[string]any `json:"context,omitempty"` +}