Files
mattermost/server/channels/app/plugin_api.go
T
Nick MisasiandCursor Agent c7eff70026 ABAC: plugin-keyed resource types, trusted plugin PAP/CEL APIs, and AuthZEN-style decision API (#37509)
* MM: add v0.5 plugin access control policy model, registry, and decision outcomes

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: add plugin access control PDP/PAP app-layer methods with fail-closed semantics

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: expose plugin access control API surface (EvaluateAccessControl + PAP/CEL methods)

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: add store-layer round-trip tests for v0.5 plugin access control policies

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: extract plugin access control app code into plugin_access_control.go

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: add atomic type-guarded AccessControlPolicyStore.DeleteIfType

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: plugin PAP hardening — atomic typed delete, indistinguishable 404s, audit every attempt

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: close plugin Get-by-ID TOCTOU via GetPolicyOfType; stamp save audit operation at entry

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Fix gob RPC poisoning from native attribute select options

NativeUserAttributeFields stored bool-select options as
[]map[string]string inside PropertyField.Attrs (map[string]any). gob
requires concrete types inside interface values to be registered, and
[]map[string]string is not registered in client_rpc.go, so encoding the
GetAccessControlFieldsAutocomplete reply failed and net/rpc shut down
the shared plugin API connection, breaking every subsequent plugin API
call.

Build the options from gob-registered containers ([]any/map[string]any)
instead; JSON output is byte-identical. Add gob round-trip regression
tests covering every plugin access control API reply payload: the
autocomplete response including native attribute fields (fails against
the old code), policies with JSON-decoded Props, visual AST condition
values of every runtime shape, expression check errors, query users
responses, and evaluation decisions.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Move plugin access control gob-safety tests into their own file

Pure move: plugin_access_control_test.go crossed 1000 lines; the gob-safety
helper and TestPluginAccessControlGobSafety now live in
plugin_access_control_gob_test.go, unchanged.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* MM: resolve plugin policy existence when ABAC is unavailable (Option B)

Every evaluation-impossible branch of EvaluatePluginAccessRequest
(service nil / unlicensed / flag off / user load or subject build
failure / evaluator infra error / unknown outcome) now performs a raw
open-core store read on the already-validated resource ID: no stored
row returns no_policy so the caller can safely apply legacy behavior;
any stored row (with a Warn on a foreign-type anomaly) or a failed
read returns unavailable so the caller must fail closed. This lets the
plugin drop its local policy index entirely.

Strengthens the EvaluateAccessControl doc contract accordingly and
reworks the fail-closed test matrix with with/without-row splits per
branch, a foreign-type-row case, a store-read-error case on the store
mock, and passthrough rows pinning that the fallback read never runs
when the evaluator answers.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Clarify EvaluateAccessControl failure-mapping doc

A failure with a definitive store miss maps to no_policy under the
Option B semantics, so 'failures never map to allow or no_policy' was
inaccurate. State precisely: never allow; no_policy only on positively
determined non-existence; deny for defensive failures on a resolved
policy; unavailable otherwise.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Tighten ABAC comments

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Document AccessDecision.Outcome as plugin-API-only

The evaluator populates Outcome on every lane, but the only production
reader is the app layer's EvaluatePluginAccessRequest, which maps it
into PluginAccessControlDecision; core channel/team enforcement reads
the collapsed Decision bool alone. State that on the field.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Retrigger CI to rebuild enterprise image with updated enterprise branch

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Document that only the evaluator's plugin lane sets AccessDecision.Outcome

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: key plugin resource types as plugin_id:type and drop the static registry

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: replace AccessDecision.Outcome with the AuthZEN decision context reason

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: drop type-scoped policy get/delete and check the type in the app layer

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: compare plugin policy type ownership exactly instead of case-insensitively

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: bound the whole policy type to the Type column width

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: require an allow before treating a decision as the no-policy fallback

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: treat a colliding foreign-type policy row as no_policy, not a deny

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: gate plugin policy reads on a raw store read before normalization

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: pin policy type immutability on save in the store tests

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: reuse a single unavailable-error constructor in the existence fallback

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: re-check plugin policy ownership on the normalized get read

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* ABAC: confirm plugin policy ownership before surfacing a normalization error

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-04 09:59:48 -04:00

1978 lines
69 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
type PluginAPI struct {
id string
app *App
ctx request.CTX
logger mlog.Sugar
manifest *model.Manifest
}
func NewPluginAPI(a *App, rctx request.CTX, manifest *model.Manifest) *PluginAPI {
return &PluginAPI{
id: manifest.Id,
manifest: manifest,
ctx: rctx,
app: a,
logger: a.Log().Sugar(mlog.String("plugin_id", manifest.Id)),
}
}
func (api *PluginAPI) checkLDAPLicense() error {
license := api.GetLicense()
if license == nil || !*license.Features.LDAPGroups {
return fmt.Errorf("license does not support LDAP groups")
}
return nil
}
func (api *PluginAPI) LoadPluginConfiguration(dest any) error {
finalConfig := make(map[string]any)
// First set final config to defaults
if api.manifest.SettingsSchema != nil {
for _, setting := range api.manifest.SettingsSchema.Settings {
finalConfig[strings.ToLower(setting.Key)] = setting.Default
}
for _, section := range api.manifest.SettingsSchema.Sections {
for _, setting := range section.Settings {
finalConfig[strings.ToLower(setting.Key)] = setting.Default
}
}
}
// If we have settings given we override the defaults with them
for setting, value := range api.app.Config().PluginSettings.Plugins[api.id] {
finalConfig[strings.ToLower(setting)] = value
}
pluginSettingsJsonBytes, err := json.Marshal(finalConfig)
if err != nil {
api.logger.Error("Error marshaling config for plugin", mlog.Err(err))
return nil
}
err = json.Unmarshal(pluginSettingsJsonBytes, dest)
if err != nil {
api.logger.Error("Error unmarshaling config for plugin", mlog.Err(err))
}
return nil
}
func (api *PluginAPI) RegisterCommand(command *model.Command) error {
return api.app.RegisterPluginCommand(api.id, command)
}
func (api *PluginAPI) UnregisterCommand(teamID, trigger string) error {
api.app.UnregisterPluginCommand(api.id, teamID, trigger)
return nil
}
func (api *PluginAPI) ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) {
user, appErr := api.app.GetUser(commandArgs.UserId)
if appErr != nil {
return nil, appErr
}
commandArgs.T = i18n.GetUserTranslations(user.Locale)
commandArgs.SiteURL = api.app.GetSiteURL()
response, appErr := api.app.ExecuteCommand(api.ctx, commandArgs)
if appErr != nil {
return response, appErr
}
return response, nil
}
func (api *PluginAPI) GetConfig() *model.Config {
return api.app.GetSanitizedConfig()
}
// GetUnsanitizedConfig gets the configuration for a system admin without removing secrets.
func (api *PluginAPI) GetUnsanitizedConfig() *model.Config {
return api.app.Config().Clone()
}
func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError {
_, _, err := api.app.SaveConfig(config, true)
return err
}
func (api *PluginAPI) GetPluginConfig() map[string]any {
cfg := api.app.GetSanitizedConfig()
if pluginConfig, isOk := cfg.PluginSettings.Plugins[api.manifest.Id]; isOk {
return pluginConfig
}
return map[string]any{}
}
func (api *PluginAPI) SavePluginConfig(pluginConfig map[string]any) *model.AppError {
cfg := api.app.Config().Clone()
cfg.PluginSettings.Plugins[api.manifest.Id] = pluginConfig
_, _, err := api.app.SaveConfig(cfg, true)
return err
}
func (api *PluginAPI) GetBundlePath() (string, error) {
bundlePath, err := filepath.Abs(filepath.Join(*api.GetConfig().PluginSettings.Directory, api.manifest.Id))
if err != nil {
return "", err
}
return bundlePath, err
}
func (api *PluginAPI) GetLicense() *model.License {
return api.app.Srv().License()
}
func (api *PluginAPI) IsEnterpriseReady() bool {
result, _ := strconv.ParseBool(model.BuildEnterpriseReady)
return result
}
func (api *PluginAPI) GetServerVersion() string {
return model.CurrentVersion
}
func (api *PluginAPI) GetSystemInstallDate() (int64, *model.AppError) {
return api.app.Srv().Platform().GetSystemInstallDate()
}
func (api *PluginAPI) GetDiagnosticId() string {
return api.app.ServerId()
}
func (api *PluginAPI) GetTelemetryId() string {
return api.app.ServerId()
}
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
if model.SafeDereference(api.app.Config().PrivacySettings.UseAnonymousURLs) && model.MinimumEnterpriseAdvancedLicense(api.app.License()) {
team.Name = model.NewId()
}
return api.app.CreateTeam(api.ctx, team)
}
func (api *PluginAPI) DeleteTeam(teamID string) *model.AppError {
return api.app.SoftDeleteTeam(teamID)
}
func (api *PluginAPI) GetTeams() ([]*model.Team, *model.AppError) {
return api.app.GetAllTeams()
}
func (api *PluginAPI) GetTeam(teamID string) (*model.Team, *model.AppError) {
return api.app.GetTeam(teamID)
}
func (api *PluginAPI) SearchTeams(term string) ([]*model.Team, *model.AppError) {
teams, _, err := api.app.SearchAllTeams(&model.TeamSearch{Term: term})
return teams, err
}
func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
return api.app.GetTeamByName(name)
}
func (api *PluginAPI) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) {
return api.app.GetTeamsUnreadForUser("", userID, false)
}
func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
return api.app.UpdateTeam(team)
}
func (api *PluginAPI) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
return api.app.GetTeamsForUser(userID)
}
func (api *PluginAPI) LogAuditRec(rec *model.AuditRecord) {
api.LogAuditRecWithLevel(rec, mlog.LvlAuditCLI)
}
func (api *PluginAPI) LogAuditRecWithLevel(rec *model.AuditRecord, level mlog.Level) {
if rec == nil {
return
}
// Ensure the plugin_id is always logged with the correct ID
model.AddEventParameterToAuditRec(rec, "plugin_id", api.id)
api.app.Srv().Audit.LogRecord(level, *rec)
}
func (api *PluginAPI) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return api.app.AddTeamMember(api.ctx, teamID, userID)
}
func (api *PluginAPI) CreateTeamMembers(teamID string, userIDs []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
members, err := api.app.AddTeamMembers(api.ctx, teamID, userIDs, requestorId, false)
if err != nil {
return nil, err
}
return model.TeamMembersWithErrorToTeamMembers(members), nil
}
func (api *PluginAPI) CreateTeamMembersGracefully(teamID string, userIDs []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) {
return api.app.AddTeamMembers(api.ctx, teamID, userIDs, requestorId, true)
}
func (api *PluginAPI) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError {
return api.app.RemoveUserFromTeam(api.ctx, teamID, userID, requestorId)
}
func (api *PluginAPI) GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
return api.app.GetTeamMembers(teamID, page*perPage, perPage, nil)
}
func (api *PluginAPI) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return api.app.GetTeamMember(api.ctx, teamID, userID)
}
func (api *PluginAPI) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
return api.app.GetTeamMembersForUserWithPagination(userID, page, perPage)
}
func (api *PluginAPI) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) {
return api.app.UpdateTeamMemberRoles(api.ctx, teamID, userID, newRoles)
}
func (api *PluginAPI) GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) {
return api.app.GetTeamStats(teamID, nil)
}
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
return api.app.CreateUser(api.ctx, user)
}
func (api *PluginAPI) DeleteUser(userID string) *model.AppError {
user, err := api.app.GetUser(userID)
if err != nil {
return err
}
_, err = api.app.UpdateActive(api.ctx, user, false)
return err
}
func (api *PluginAPI) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
return api.app.GetUsersFromProfiles(options)
}
func (api *PluginAPI) GetUsersByIds(usersID []string) ([]*model.User, *model.AppError) {
return api.app.GetUsers(api.ctx, usersID)
}
func (api *PluginAPI) GetUser(userID string) (*model.User, *model.AppError) {
return api.app.GetUser(userID)
}
func (api *PluginAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
return api.app.GetUserByEmail(email)
}
func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
return api.app.GetUserByUsername(name)
}
func (api *PluginAPI) GetUserByRemoteID(remoteID string) (*model.User, *model.AppError) {
return api.app.GetUserByRemoteID(remoteID)
}
func (api *PluginAPI) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) {
return api.app.GetUsersByUsernames(usernames, true, nil)
}
func (api *PluginAPI) GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) {
options := &model.UserGetOptions{InTeamId: teamID, Page: page, PerPage: perPage}
return api.app.GetUsersInTeam(options)
}
func (api *PluginAPI) GetPreferenceForUser(userID, category, name string) (model.Preference, *model.AppError) {
pref, err := api.app.GetPreferenceByCategoryAndNameForUser(api.ctx, userID, category, name)
if err != nil {
return model.Preference{}, err
}
return *pref, nil
}
func (api *PluginAPI) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) {
return api.app.GetPreferencesForUser(api.ctx, userID)
}
func (api *PluginAPI) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
return api.app.UpdatePreferences(api.ctx, userID, preferences)
}
func (api *PluginAPI) DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
return api.app.DeletePreferences(api.ctx, userID, preferences)
}
func (api *PluginAPI) GetSession(sessionID string) (*model.Session, *model.AppError) {
return api.app.GetSessionById(api.ctx, sessionID)
}
func (api *PluginAPI) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
return api.app.CreateSession(api.ctx, session)
}
func (api *PluginAPI) ExtendSessionExpiry(sessionID string, expiresAt int64) *model.AppError {
session, err := api.app.ch.srv.platform.GetSessionByID(api.ctx, sessionID)
if err != nil {
return model.NewAppError("extendSessionExpiry", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := api.app.ch.srv.platform.ExtendSessionExpiry(session, expiresAt); err != nil {
return model.NewAppError("extendSessionExpiry", "app.session.extend_session_expiry.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (api *PluginAPI) RevokeSession(sessionID string) *model.AppError {
return api.app.RevokeSessionById(api.ctx, sessionID)
}
func (api *PluginAPI) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
return api.app.CreateUserAccessToken(api.ctx, token)
}
func (api *PluginAPI) RevokeUserAccessToken(tokenID string) *model.AppError {
accessToken, err := api.app.GetUserAccessToken(tokenID, false)
if err != nil {
return err
}
return api.app.RevokeUserAccessToken(api.ctx, accessToken)
}
func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
return api.app.UpdateUser(api.ctx, user, true)
}
func (api *PluginAPI) UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
return api.app.UpdateUserAuth(api.ctx, userID, userAuth)
}
func (api *PluginAPI) UpdateUserActive(userID string, active bool) *model.AppError {
return api.app.UpdateUserActive(api.ctx, userID, active)
}
func (api *PluginAPI) GetUserStatus(userID string) (*model.Status, *model.AppError) {
return api.app.GetStatus(userID)
}
func (api *PluginAPI) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
return api.app.GetUserStatusesByIds(userIDs)
}
func (api *PluginAPI) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) {
switch status {
case model.StatusOnline:
api.app.SetStatusOnline(userID, true)
case model.StatusOffline:
api.app.SetStatusOffline(userID, true, false)
case model.StatusAway:
api.app.SetStatusAwayIfNeeded(userID, true)
case model.StatusDnd:
api.app.SetStatusDoNotDisturb(userID)
default:
return nil, model.NewAppError("UpdateUserStatus", "plugin.api.update_user_status.bad_status", nil, "unrecognized status", http.StatusBadRequest)
}
return api.app.GetStatus(userID)
}
func (api *PluginAPI) SetUserStatusTimedDND(userID string, endTime int64) (*model.Status, *model.AppError) {
// read-after-write bug which will fail if there are replicas.
// it works for now because we have a cache in between.
// FIXME: make SetStatusDoNotDisturbTimed return updated status
api.app.SetStatusDoNotDisturbTimed(userID, endTime)
return api.app.GetStatus(userID)
}
func (api *PluginAPI) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError {
return api.app.SetCustomStatus(api.ctx, userID, customStatus)
}
func (api *PluginAPI) RemoveUserCustomStatus(userID string) *model.AppError {
return api.app.RemoveCustomStatus(api.ctx, userID)
}
func (api *PluginAPI) GetUserCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
return api.app.GetCustomStatus(userID)
}
func (api *PluginAPI) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) {
switch sortBy {
case model.ChannelSortByUsername:
return api.app.GetUsersInChannel(&model.UserGetOptions{
InChannelId: channelID,
Page: page,
PerPage: perPage,
})
case model.ChannelSortByStatus:
return api.app.GetUsersInChannelByStatus(&model.UserGetOptions{
InChannelId: channelID,
Page: page,
PerPage: perPage,
})
default:
return nil, model.NewAppError("GetUsersInChannel", "plugin.api.get_users_in_channel", nil, "invalid sort option", http.StatusBadRequest)
}
}
func (api *PluginAPI) GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) {
if api.app.Ldap() == nil {
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, err := api.app.GetUser(userID)
if err != nil {
return nil, err
}
if user.AuthData == nil {
return map[string]string{}, nil
}
// Only bother running the query if the user's auth service is LDAP or it's SAML and sync is enabled.
if user.AuthService == model.UserAuthServiceLdap ||
(user.AuthService == model.UserAuthServiceSaml && *api.app.Config().SamlSettings.EnableSyncWithLdap) {
return api.app.Ldap().GetUserAttributes(api.ctx, *user.AuthData, attributes)
}
return map[string]string{}, nil
}
func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
UseAnonymousURLs := model.SafeDereference(api.app.Config().PrivacySettings.UseAnonymousURLs) && model.MinimumEnterpriseAdvancedLicense(api.app.License())
// Space backing channels have system-assigned names, not user-visible URLs — anonymous-URL scrambling does not apply.
if !channel.IsGroupOrDirect() && !channel.IsSpace() && UseAnonymousURLs {
channel.Name = model.NewId()
}
return api.app.CreateChannel(api.ctx, channel, false)
}
func (api *PluginAPI) DeleteChannel(channelID string) *model.AppError {
channel, err := api.resolveChannel(channelID)
if err != nil {
return err
}
return api.app.DeleteChannel(api.ctx, channel, "")
}
func (api *PluginAPI) RestoreChannel(channelID string) *model.AppError {
channel, err := api.resolveChannel(channelID)
if err != nil {
return err
}
_, err = api.app.RestoreChannel(api.ctx, channel, "")
return err
}
func (api *PluginAPI) GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) {
channels, err := api.app.GetPublicChannelsForTeam(api.ctx, teamID, page*perPage, perPage)
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) GetChannel(channelID string) (*model.Channel, *model.AppError) {
return api.app.GetChannel(api.ctx, channelID)
}
// GetChannelOfType resolves a channel by ID, requiring it to be of the given type. Generic
// GetChannel excludes opaque backing channel types (e.g. space); plugins that manage such a
// channel resolve it by its exact type here.
func (api *PluginAPI) GetChannelOfType(channelID string, channelType model.ChannelType) (*model.Channel, *model.AppError) {
ctx := api.ctx
// Opaque backing types (e.g. space) are uncached and created without waiting for replica
// replication, so a plugin resolving one immediately after creating it could miss it on a
// lagging replica; read those from master. Cached message-bearing types stay on the replica.
if channelType == model.ChannelTypeSpace {
ctx = RequestContextWithMaster(api.ctx)
}
return api.app.GetChannelOfType(ctx, channelID, channelType)
}
// resolveChannel fetches a channel by ID for plugin API mutation methods. Generic channel
// lookups exclude opaque backing channel types (e.g. space); on 404 this retries the ID
// against each known backing type so plugins can manage backing channels through the standard
// API without a separate resolution step. The retry uses master context to avoid
// read-after-write misses on lagging replicas.
func (api *PluginAPI) resolveChannel(channelID string) (*model.Channel, *model.AppError) {
return resolveChannelByID(
api.ctx,
channelID,
api.app.GetChannel,
api.app.GetChannelOfType,
)
}
// resolveChannelByID is the testable core of resolveChannel.
// It is a package-level function so tests can inject controlled fetch functions.
func resolveChannelByID(
rctx request.CTX,
channelID string,
getChannel func(request.CTX, string) (*model.Channel, *model.AppError),
getChannelOfType func(request.CTX, string, model.ChannelType) (*model.Channel, *model.AppError),
) (*model.Channel, *model.AppError) {
channel, err := getChannel(rctx, channelID)
if err == nil {
return channel, nil
}
if err.StatusCode != http.StatusNotFound {
return nil, err
}
// Generic lookup excludes backing channel types; retry as each known backing type.
if spaceChannel, spErr := getChannelOfType(RequestContextWithMaster(rctx), channelID, model.ChannelTypeSpace); spErr == nil {
return spaceChannel, nil
} else if spErr.StatusCode != http.StatusNotFound {
return nil, spErr
}
return nil, err
}
func (api *PluginAPI) GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) {
return api.app.GetChannelByName(api.ctx, name, teamID, includeDeleted)
}
func (api *PluginAPI) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) {
return api.app.GetChannelByNameForTeamName(api.ctx, channelName, teamName, includeDeleted)
}
func (api *PluginAPI) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) {
channels, err := api.app.GetChannelsForTeamForUser(api.ctx, teamID, userID, &model.ChannelSearchOpts{
IncludeDeleted: includeDeleted,
LastDeleteAt: 0,
})
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) GetChannelStats(channelID string) (*model.ChannelStats, *model.AppError) {
memberCount, err := api.app.GetChannelMemberCount(api.ctx, channelID)
if err != nil {
return nil, err
}
guestCount, err := api.app.GetChannelMemberCount(api.ctx, channelID)
if err != nil {
return nil, err
}
return &model.ChannelStats{ChannelId: channelID, MemberCount: memberCount, GuestCount: guestCount}, nil
}
func (api *PluginAPI) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) {
return api.app.GetOrCreateDirectChannel(api.ctx, userID1, userID2)
}
func (api *PluginAPI) GetGroupChannel(userIDs []string) (*model.Channel, *model.AppError) {
return api.app.CreateGroupChannel(api.ctx, userIDs, "")
}
func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return api.app.UpdateChannel(api.ctx, channel)
}
func (api *PluginAPI) RegisterChannelGuard(channelID string) *model.AppError {
return api.app.RegisterChannelGuard(api.ctx, channelID, strings.ToLower(api.id))
}
func (api *PluginAPI) UnregisterChannelGuard(channelID string) *model.AppError {
return api.app.UnregisterChannelGuard(api.ctx, channelID, strings.ToLower(api.id))
}
func (api *PluginAPI) SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) {
channels, err := api.app.SearchChannels(api.ctx, teamID, term)
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
return api.app.CreateSidebarCategory(api.ctx, userID, teamID, newCategory)
}
func (api *PluginAPI) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
return api.app.GetSidebarCategoriesForTeamForUser(api.ctx, userID, teamID)
}
func (api *PluginAPI) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
return api.app.UpdateSidebarCategories(api.ctx, userID, teamID, categories)
}
func (api *PluginAPI) SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) {
pluginSearchUsersOptions := &model.UserSearchOptions{
IsAdmin: true,
AllowInactive: search.AllowInactive,
Limit: search.Limit,
}
return api.app.SearchUsers(api.ctx, search, pluginSearchUsersOptions)
}
func (api *PluginAPI) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) {
postList, err := api.app.SearchPostsInTeam(teamID, paramsList)
if err != nil {
return nil, err
}
return postList.ForPlugin().ToSlice(), nil
}
func (api *PluginAPI) SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) {
var terms string
if searchParams.Terms != nil {
terms = *searchParams.Terms
}
timeZoneOffset := 0
if searchParams.TimeZoneOffset != nil {
timeZoneOffset = *searchParams.TimeZoneOffset
}
isOrSearch := false
if searchParams.IsOrSearch != nil {
isOrSearch = *searchParams.IsOrSearch
}
page := 0
if searchParams.Page != nil {
page = *searchParams.Page
}
perPage := 100
if searchParams.PerPage != nil {
perPage = *searchParams.PerPage
}
includeDeletedChannels := false
if searchParams.IncludeDeletedChannels != nil {
includeDeletedChannels = *searchParams.IncludeDeletedChannels
}
results, _, appErr := api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
if results != nil {
results = results.ForPlugin()
}
return results, appErr
}
func (api *PluginAPI) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
channel, err := api.resolveChannel(channelID)
if err != nil {
return nil, err
}
ctx := api.ctx
if channel.IsSpace() {
ctx = RequestContextWithMaster(api.ctx)
}
return api.app.AddChannelMember(ctx, userID, channel, ChannelMemberOpts{
// For now, don't allow overriding these via the plugin API.
UserRequestorID: "",
PostRootID: "",
})
}
func (api *PluginAPI) AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError) {
channel, err := api.resolveChannel(channelID)
if err != nil {
return nil, err
}
ctx := api.ctx
if channel.IsSpace() {
ctx = RequestContextWithMaster(api.ctx)
}
return api.app.AddChannelMember(ctx, userID, channel, ChannelMemberOpts{
UserRequestorID: asUserID,
})
}
func (api *PluginAPI) GetChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
return api.app.GetChannelMember(api.ctx, channelID, userID)
}
func (api *PluginAPI) GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
return api.app.GetChannelMembersPage(api.ctx, channelID, page, perPage)
}
func (api *PluginAPI) GetChannelMembersByIds(channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
return api.app.GetChannelMembersByIds(api.ctx, channelID, userIDs)
}
func (api *PluginAPI) GetChannelMembersForUser(_, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
// The team ID parameter was never used in the SQL query.
// But we keep this to maintain compatibility.
return api.app.GetChannelMembersForUserWithPagination(api.ctx, userID, page, perPage)
}
func (api *PluginAPI) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
return api.app.UpdateChannelMemberRoles(api.ctx, channelID, userID, newRoles)
}
func (api *PluginAPI) UpdateChannelMemberNotifications(channelID, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
if appErr := api.rejectSpaceChannel(channelID); appErr != nil {
return nil, appErr
}
return api.app.UpdateChannelMemberNotifyProps(api.ctx, notifications, channelID, userID)
}
func (api *PluginAPI) PatchChannelMembersNotifications(members []*model.ChannelMemberIdentifier, notifications map[string]string) *model.AppError {
for _, member := range members {
if appErr := api.rejectSpaceChannel(member.ChannelId); appErr != nil {
return appErr
}
}
_, err := api.app.PatchChannelMembersNotifyProps(api.ctx, members, notifications)
return err
}
// rejectSpaceChannel returns a bad-request AppError when channelID is a space backing channel.
// Notify-prop mutations carry chat semantics (they emit a channel_member_updated event) that do
// not belong on an internal space backing channel, so they are rejected here. It fails closed by
// propagating any error other than not-found, so a lookup failure cannot let a space through.
func (api *PluginAPI) rejectSpaceChannel(channelID string) *model.AppError {
_, err := api.app.GetChannelOfType(RequestContextWithMaster(api.ctx), channelID, model.ChannelTypeSpace)
if err == nil {
return model.NewAppError("PluginAPI.rejectSpaceChannel", "plugin_api.channel.space_notify_props.app_error", nil, "", http.StatusBadRequest)
}
if err.StatusCode != http.StatusNotFound {
return err
}
return nil
}
func (api *PluginAPI) DeleteChannelMember(channelID, userID string) *model.AppError {
channel, err := api.resolveChannel(channelID)
if err != nil {
return err
}
if channel.IsSpace() {
// Space backing channels resolve outside LeaveChannel's generic GetChannel; remove directly.
return api.app.RemoveUserFromChannel(RequestContextWithMaster(api.ctx), userID, userID, channel)
}
return api.app.LeaveChannel(api.ctx, channelID, userID)
}
func (api *PluginAPI) GetGroup(groupId string) (*model.Group, *model.AppError) {
return api.app.GetGroup(groupId, nil, nil)
}
func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError) {
return api.app.GetGroupByName(name, model.GroupSearchOpts{})
}
func (api *PluginAPI) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
users, _, err := api.app.GetGroupMemberUsersPage(groupID, page, perPage, nil)
return users, err
}
func (api *PluginAPI) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
return api.app.GetGroupsBySource(groupSource)
}
func (api *PluginAPI) GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) {
return api.app.GetGroupsByUserId(userID, model.GroupSearchOpts{})
}
func (api *PluginAPI) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpsertGroupMember", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.UpsertGroupMember(groupID, userID)
}
func (api *PluginAPI) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpsertGroupMembers", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.UpsertGroupMembers(groupID, userIDs)
}
func (api *PluginAPI) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroupByRemoteID", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.GetGroupByRemoteID(remoteID, groupSource)
}
func (api *PluginAPI) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("CreateGroup", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.CreateGroup(group)
}
func (api *PluginAPI) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpdateGroup", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.UpdateGroup(group)
}
func (api *PluginAPI) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("DeleteGroup", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.DeleteGroup(groupID)
}
func (api *PluginAPI) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("RestoreGroup", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.RestoreGroup(groupID)
}
func (api *PluginAPI) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("DeleteGroupMember", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.DeleteGroupMember(groupID, userID)
}
func (api *PluginAPI) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroupSyncable", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.GetGroupSyncable(groupID, syncableID, syncableType)
}
func (api *PluginAPI) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroupSyncables", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.GetGroupSyncables(groupID, syncableType)
}
func (api *PluginAPI) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpsertGroupSyncable", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.UpsertGroupSyncable(groupSyncable)
}
func (api *PluginAPI) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("UpdateGroupSyncable", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.UpdateGroupSyncable(groupSyncable)
}
func (api *PluginAPI) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.DeleteGroupSyncable(groupID, syncableID, syncableType)
}
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
channel, appErr := api.app.GetChannel(api.ctx, post.ChannelId)
if appErr != nil {
return nil, appErr
}
silent := post.HasSilentNotification()
post.SanitizeProps()
post, _, appErr = api.app.CreatePost(api.ctx, post, channel, model.CreatePostFlags{
TriggerWebhooks: true,
SetOnline: true,
FromPlugin: true,
SilentNotification: silent,
})
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) {
return api.app.SaveReactionForPost(api.ctx, reaction)
}
func (api *PluginAPI) RemoveReaction(reaction *model.Reaction) *model.AppError {
return api.app.DeleteReactionForPost(api.ctx, reaction)
}
func (api *PluginAPI) GetReactions(postID string) ([]*model.Reaction, *model.AppError) {
return api.app.GetReactionsForPost(postID)
}
func (api *PluginAPI) SendEphemeralPost(userID string, post *model.Post) *model.Post {
newPost, _ := api.app.SendEphemeralPost(api.ctx, userID, post)
return newPost.ForPlugin()
}
func (api *PluginAPI) UpdateEphemeralPost(userID string, post *model.Post) *model.Post {
newPost, _ := api.app.UpdateEphemeralPost(api.ctx, userID, post)
return newPost.ForPlugin()
}
func (api *PluginAPI) DeleteEphemeralPost(userID, postID string) {
api.app.DeleteEphemeralPost(api.ctx, userID, postID)
}
func (api *PluginAPI) DeletePost(postID string) *model.AppError {
_, err := api.app.DeletePost(api.ctx, postID, api.id)
return err
}
func (api *PluginAPI) GetPostThread(postID string) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostThread(api.ctx, postID, model.GetPostsOptions{}, "")
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPost(postID string) (*model.Post, *model.AppError) {
post, appErr := api.app.GetSinglePost(api.ctx, postID, false)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) GetPostsSince(channelID string, time int64) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsSince(api.ctx, model.GetPostsSinceOptions{ChannelId: channelID, Time: time})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsAfter(channelID, postID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsAfterPost(api.ctx, model.GetPostsOptions{ChannelId: channelID, PostId: postID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsBefore(channelID, postID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsBeforePost(api.ctx, model.GetPostsOptions{ChannelId: channelID, PostId: postID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsForChannel(channelID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsPage(api.ctx, model.GetPostsOptions{ChannelId: channelID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
// Grant mm_blocks_actions write access only when the plugin's update
// actually includes the prop, AND the value passes validation.
// Otherwise the freeze in UpdatePost preserves whatever the original
// post had — plugins that update unrelated fields don't accidentally
// drop or corrupt mm_blocks_actions.
allowMmBlocksActionsUpdate := false
if post.GetProp(model.PostPropsMmBlocksActions) != nil {
if err := model.ValidateMmBlocksActions(post); err != nil {
return nil, model.NewAppError("UpdatePost", "plugin.api.update_post.mm_blocks_actions.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
allowMmBlocksActionsUpdate = true
}
post, _, appErr := api.app.UpdatePost(api.ctx, post, &model.UpdatePostOptions{SafeUpdate: false, AllowMmBlocksActionsUpdate: allowMmBlocksActionsUpdate})
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) GetProfileImage(userID string) ([]byte, *model.AppError) {
user, err := api.app.GetUser(userID)
if err != nil {
return nil, err
}
data, _, err := api.app.GetProfileImage(user)
return data, err
}
func (api *PluginAPI) SetProfileImage(userID string, data []byte) *model.AppError {
if _, err := api.app.GetUser(userID); err != nil {
return err
}
return api.app.SetProfileImageFromFile(api.ctx, userID, bytes.NewReader(data))
}
func (api *PluginAPI) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) {
return api.app.GetEmojiList(api.ctx, page, perPage, sortBy)
}
func (api *PluginAPI) GetEmojiByName(name string) (*model.Emoji, *model.AppError) {
return api.app.GetEmojiByName(api.ctx, name)
}
func (api *PluginAPI) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
return api.app.GetEmoji(api.ctx, emojiId)
}
func (api *PluginAPI) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
return api.app.CopyFileInfos(api.ctx, userID, fileIDs)
}
func (api *PluginAPI) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
return api.app.GetFileInfo(api.ctx, fileID)
}
func (api *PluginAPI) SetFileSearchableContent(fileID string, content string) *model.AppError {
return api.app.SetFileSearchableContent(api.ctx, fileID, content)
}
func (api *PluginAPI) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
return api.app.GetFileInfos(api.ctx, page, perPage, opt)
}
func (api *PluginAPI) GetFileLink(fileID string) (string, *model.AppError) {
if !*api.app.Config().FileSettings.EnablePublicLink {
return "", model.NewAppError("GetFileLink", "plugin_api.get_file_link.disabled.app_error", nil, "", http.StatusNotImplemented)
}
info, err := api.app.GetFileInfo(api.ctx, fileID)
if err != nil {
return "", err
}
if info.PostId == "" {
return "", model.NewAppError("GetFileLink", "plugin_api.get_file_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
}
return api.app.GeneratePublicLink(api.app.GetSiteURL(), info), nil
}
func (api *PluginAPI) ReadFile(path string) ([]byte, *model.AppError) {
return api.app.ReadFile(path)
}
func (api *PluginAPI) GetFile(fileID string) ([]byte, *model.AppError) {
return api.app.GetFile(api.ctx, fileID)
}
func (api *PluginAPI) UploadFile(data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
return api.app.UploadFile(api.ctx, data, channelID, filename)
}
func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) {
return api.app.GetEmojiImage(api.ctx, emojiId)
}
func (api *PluginAPI) GetTeamIcon(teamID string) ([]byte, *model.AppError) {
team, err := api.app.GetTeam(teamID)
if err != nil {
return nil, err
}
data, err := api.app.GetTeamIcon(team)
if err != nil {
return nil, err
}
return data, nil
}
func (api *PluginAPI) SetTeamIcon(teamID string, data []byte) *model.AppError {
team, err := api.app.GetTeam(teamID)
if err != nil {
return err
}
return api.app.SetTeamIconFromFile(api.ctx, team, bytes.NewReader(data))
}
func (api *PluginAPI) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
return api.app.OpenInteractiveDialog(api.ctx, dialog)
}
func (api *PluginAPI) RemoveTeamIcon(teamID string) *model.AppError {
_, err := api.app.GetTeam(teamID)
if err != nil {
return err
}
err = api.app.RemoveTeamIcon(teamID)
if err != nil {
return err
}
return nil
}
// Mail Section
func (api *PluginAPI) SendMail(to, subject, htmlBody string) *model.AppError {
if to == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_to", nil, "", http.StatusBadRequest)
}
if subject == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_subject", nil, "", http.StatusBadRequest)
}
if htmlBody == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusBadRequest)
}
if err := api.app.Srv().EmailService.SendNotificationMail(to, subject, htmlBody); err != nil {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// Plugin Section
func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) {
plugins, err := api.app.GetPlugins()
if err != nil {
return nil, err
}
var manifests []*model.Manifest
for _, manifest := range plugins.Active {
manifests = append(manifests, &manifest.Manifest)
}
for _, manifest := range plugins.Inactive {
manifests = append(manifests, &manifest.Manifest)
}
return manifests, nil
}
func (api *PluginAPI) EnablePlugin(id string) *model.AppError {
return api.app.EnablePlugin(id)
}
func (api *PluginAPI) DisablePlugin(id string) *model.AppError {
return api.app.DisablePlugin(id)
}
func (api *PluginAPI) RemovePlugin(id string) *model.AppError {
return api.app.Channels().RemovePlugin(id)
}
func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
return api.app.GetPluginStatus(id)
}
func (api *PluginAPI) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
if !*api.app.Config().PluginSettings.Enable || !*api.app.Config().PluginSettings.EnableUploads {
return nil, model.NewAppError("installPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
}
fileBuffer, err := io.ReadAll(file)
if err != nil {
return nil, model.NewAppError("InstallPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return api.app.InstallPlugin(bytes.NewReader(fileBuffer), replace)
}
// KV Store Section
func (api *PluginAPI) KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
return api.app.SetPluginKeyWithOptions(api.id, key, value, options)
}
func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError {
return api.app.SetPluginKey(api.id, key, value)
}
func (api *PluginAPI) KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) {
return api.app.CompareAndSetPluginKey(api.id, key, oldValue, newValue)
}
func (api *PluginAPI) KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) {
return api.app.CompareAndDeletePluginKey(api.ctx, api.id, key, oldValue)
}
func (api *PluginAPI) KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError {
return api.app.SetPluginKeyWithExpiry(api.id, key, value, expireInSeconds)
}
func (api *PluginAPI) KVGet(key string) ([]byte, *model.AppError) {
return api.app.GetPluginKey(api.id, key)
}
func (api *PluginAPI) KVDelete(key string) *model.AppError {
return api.app.DeletePluginKey(api.id, key)
}
func (api *PluginAPI) KVDeleteAll() *model.AppError {
return api.app.DeleteAllKeysForPlugin(api.id)
}
func (api *PluginAPI) KVList(page, perPage int) ([]string, *model.AppError) {
return api.app.ListPluginKeys(api.id, page, perPage)
}
func (api *PluginAPI) PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
ev := model.NewWebSocketEvent(model.WebsocketEventType(fmt.Sprintf("custom_%v_%v", api.id, event)), "", "", "", nil, "")
ev = ev.SetBroadcast(broadcast).SetData(payload)
api.app.Publish(ev)
}
func (api *PluginAPI) SendToastMessage(userID, connectionID, message string, options model.SendToastMessageOptions) *model.AppError {
return api.app.SendToastMessage(userID, connectionID, message, options)
}
func (api *PluginAPI) HasPermissionTo(userID string, permission *model.Permission) bool {
return api.app.HasPermissionTo(userID, permission)
}
func (api *PluginAPI) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool {
return api.app.HasPermissionToTeam(api.ctx, userID, teamID, permission)
}
func (api *PluginAPI) HasPermissionToChannel(userID, channelID string, permission *model.Permission) bool {
ok, _ := api.app.HasPermissionToChannel(api.ctx, userID, channelID, permission)
return ok
}
func (api *PluginAPI) RolesGrantPermission(roleNames []string, permissionId string) bool {
return api.app.RolesGrantPermission(roleNames, permissionId)
}
func (api *PluginAPI) UpdateUserRoles(userID string, newRoles string) (*model.User, *model.AppError) {
return api.app.UpdateUserRoles(api.ctx, userID, newRoles, true)
}
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...any) {
api.logger.Debugw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...any) {
api.logger.Infow(msg, keyValuePairs...)
}
func (api *PluginAPI) LogError(msg string, keyValuePairs ...any) {
api.logger.Errorw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...any) {
api.logger.Warnw(msg, keyValuePairs...)
}
func (api *PluginAPI) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
// Bots created by a plugin should use the plugin's ID for the creator field, unless
// otherwise specified by the plugin.
if bot.OwnerId == "" {
bot.OwnerId = api.id
}
// Bots cannot be owners of other bots
if user, err := api.app.GetUser(bot.OwnerId); err == nil {
if user.IsBot {
return nil, model.NewAppError("CreateBot", "plugin_api.bot_cant_create_bot", nil, "", http.StatusBadRequest)
}
}
return api.app.CreateBot(api.ctx, bot)
}
func (api *PluginAPI) PatchBot(userID string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
return api.app.PatchBot(api.ctx, userID, botPatch)
}
func (api *PluginAPI) GetBot(userID string, includeDeleted bool) (*model.Bot, *model.AppError) {
return api.app.GetBot(api.ctx, userID, includeDeleted)
}
func (api *PluginAPI) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) {
bots, err := api.app.GetBots(api.ctx, options)
return []*model.Bot(bots), err
}
func (api *PluginAPI) UpdateBotActive(userID string, active bool) (*model.Bot, *model.AppError) {
return api.app.UpdateBotActive(api.ctx, userID, active)
}
func (api *PluginAPI) PermanentDeleteBot(userID string) *model.AppError {
return api.app.PermanentDeleteBot(api.ctx, userID)
}
func (api *PluginAPI) EnsureBotUser(bot *model.Bot) (string, error) {
// Bots created by a plugin should use the plugin's ID for the creator field.
bot.OwnerId = api.id
return api.app.EnsureBot(api.ctx, api.id, bot)
}
func (api *PluginAPI) PublishUserTyping(userID, channelID, parentId string) *model.AppError {
return api.app.PublishUserTyping(userID, channelID, parentId)
}
func (api *PluginAPI) PluginHTTP(request *http.Request) *http.Response {
split := strings.SplitN(request.URL.Path, "/", 3)
if len(split) != 3 {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString("Not enough URL. Form of URL should be /<pluginid>/*")),
}
}
destinationPluginId := split[1]
newURL, err := url.Parse("/" + split[2])
newURL.RawQuery = request.URL.Query().Encode()
request.URL = newURL
if destinationPluginId == "" || err != nil {
message := "No plugin specified. Form of URL should be /<pluginid>/*"
if err != nil {
message = "Form of URL should be /<pluginid>/* Error: " + err.Error()
}
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(message)),
}
}
// Create pipe for streaming response
pr, pw := io.Pipe()
responseTransfer := NewPluginResponseWriter(pw)
// Serve the request in a goroutine, streaming the response through the pipe
go func() {
defer func() {
// Ensure pipe is closed when request completes
var closeErr error
if err := recover(); err != nil {
closeErr = responseTransfer.CloseWithError(fmt.Errorf("panic in plugin request: %v", err))
} else {
closeErr = responseTransfer.Close()
}
if closeErr != nil {
api.logger.Errorw("Failed to close plugin response pipe", "error", closeErr)
}
}()
api.app.ServeInterPluginRequest(responseTransfer, request, api.id, destinationPluginId)
}()
// Wait for headers to be ready before returning response
<-responseTransfer.ResponseReady
return responseTransfer.GenerateResponse(pr)
}
func (api *PluginAPI) CreateCommand(cmd *model.Command) (*model.Command, error) {
cmd.CreatorId = ""
cmd.PluginId = api.id
cmd, appErr := api.app.createCommand(cmd)
if appErr != nil {
return cmd, appErr
}
return cmd, nil
}
func (api *PluginAPI) ListCommands(teamID string) ([]*model.Command, error) {
ret := make([]*model.Command, 0)
cmds, err := api.ListPluginCommands(teamID)
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
cmds, err = api.ListBuiltInCommands()
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
cmds, err = api.ListCustomCommands(teamID)
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
return ret, nil
}
func (api *PluginAPI) ListCustomCommands(teamID string) ([]*model.Command, error) {
// Plugins are allowed to bypass the a.Config().ServiceSettings.EnableCommands setting.
return api.app.Srv().Store().Command().GetByTeam(teamID)
}
func (api *PluginAPI) ListPluginCommands(teamID string) ([]*model.Command, error) {
commands := make([]*model.Command, 0)
seen := make(map[string]bool)
for _, cmd := range api.app.CommandsForTeam(teamID) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
return commands, nil
}
func (api *PluginAPI) ListBuiltInCommands() ([]*model.Command, error) {
commands := make([]*model.Command, 0)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(api.app, i18n.T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
return commands, nil
}
func (api *PluginAPI) GetCommand(commandID string) (*model.Command, error) {
return api.app.Srv().Store().Command().Get(commandID)
}
func (api *PluginAPI) UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) {
oldCmd, err := api.GetCommand(commandID)
if err != nil {
return nil, err
}
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
updatedCmd.Id = oldCmd.Id
updatedCmd.Token = oldCmd.Token
updatedCmd.CreateAt = oldCmd.CreateAt
updatedCmd.UpdateAt = model.GetMillis()
updatedCmd.DeleteAt = oldCmd.DeleteAt
updatedCmd.PluginId = api.id
if updatedCmd.TeamId == "" {
updatedCmd.TeamId = oldCmd.TeamId
}
if appErr := api.app.validateCommandTriggerUniqueness(updatedCmd.TeamId, updatedCmd.Trigger, updatedCmd.Id); appErr != nil {
return nil, appErr
}
return api.app.Srv().Store().Command().Update(updatedCmd)
}
func (api *PluginAPI) DeleteCommand(commandID string) error {
err := api.app.Srv().Store().Command().Delete(commandID, model.GetMillis())
if err != nil {
return err
}
return nil
}
func (api *PluginAPI) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
return api.app.CreateOAuthApp(app)
}
func (api *PluginAPI) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
return api.app.GetOAuthApp(appID)
}
func (api *PluginAPI) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
oldApp, err := api.GetOAuthApp(app.Id)
if err != nil {
return nil, err
}
return api.app.UpdateOAuthApp(oldApp, app)
}
func (api *PluginAPI) DeleteOAuthApp(appID string) *model.AppError {
return api.app.DeleteOAuthApp(api.ctx, appID)
}
// PublishPluginClusterEvent broadcasts a plugin event to all other running instances of
// the calling plugin.
func (api *PluginAPI) PublishPluginClusterEvent(ev model.PluginClusterEvent,
opts model.PluginClusterEventSendOptions,
) error {
if api.app.Cluster() == nil {
return nil
}
msg := &model.ClusterMessage{
Event: model.ClusterEventPluginEvent,
SendType: opts.SendType,
WaitForAllToSend: false,
Props: map[string]string{
"PluginID": api.id,
"EventID": ev.Id,
},
Data: ev.Data,
}
// If TargetId is empty we broadcast to all other cluster nodes.
if opts.TargetId == "" {
api.app.Cluster().SendClusterMessage(msg)
} else {
if err := api.app.Cluster().SendClusterMessageToNode(opts.TargetId, msg); err != nil {
return fmt.Errorf("failed to send message to cluster node %q: %w", opts.TargetId, err)
}
}
return nil
}
// RequestTrialLicense requests a trial license and installs it in the server
func (api *PluginAPI) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
// Normally, plugins are unrestricted in their abilities, but to maintain backwards compatbilibity with plugins
// that were unaware of the nuances of ExperimentalSettings.RestrictSystemAdmin, we restrict the trial license
// unconditionally.
if *api.app.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
return api.app.Channels().RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
}
// GetCloudLimits returns any limits associated with the cloud instance
func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) {
if api.app.Cloud() == nil {
return &model.ProductLimits{}, nil
}
limits, err := api.app.Cloud().GetCloudLimits("")
return limits, err
}
// RegisterCollectionAndTopic is no longer supported.
func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error {
return nil
}
func (api *PluginAPI) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) {
us, err := api.app.CreateUploadSession(api.ctx, us)
if err != nil {
return nil, err
}
return us, nil
}
func (api *PluginAPI) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
fi, err := api.app.UploadData(api.ctx, us, rd)
if err != nil {
return nil, err
}
return fi, nil
}
func (api *PluginAPI) GetUploadSession(uploadID string) (*model.UploadSession, error) {
// We want to fetch from master DB to avoid a potential read-after-write on the plugin side.
api.ctx = api.ctx.With(RequestContextWithMaster)
fi, err := api.app.GetUploadSession(api.ctx, uploadID)
if err != nil {
return nil, err
}
return fi, nil
}
func (api *PluginAPI) SendPushNotification(notification *model.PushNotification, userID string) *model.AppError {
// Ignoring skipSessionId because it's only used internally to clear push notifications
return api.app.sendPushNotificationToAllSessions(api.ctx, notification, userID, "")
}
func (api *PluginAPI) RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error) {
return api.app.RegisterPluginForSharedChannels(api.ctx, opts)
}
func (api *PluginAPI) UnregisterPluginForSharedChannels(pluginID string) error {
return api.app.UnregisterPluginForSharedChannels(pluginID)
}
func (api *PluginAPI) UnregisterPluginRemoteForSharedChannels(remoteID string) error {
return api.app.UnregisterPluginRemoteForSharedChannels(api.id, remoteID)
}
func (api *PluginAPI) ShareChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
scShared, err := api.app.ShareChannel(api.ctx, sc)
if errors.Is(err, model.ErrChannelAlreadyShared) {
// sharing an already shared channel is not an error; treat as idempotent and return the existing shared channel
return api.app.GetSharedChannel(sc.ChannelId)
}
return scShared, err
}
func (api *PluginAPI) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
return api.app.UpdateSharedChannel(sc)
}
func (api *PluginAPI) UnshareChannel(channelID string) (unshared bool, err error) {
return api.app.UnshareChannel(channelID)
}
func (api *PluginAPI) UpdateSharedChannelCursor(channelID, remoteID string, cusror model.GetPostsSinceForSyncCursor) error {
return api.app.UpdateSharedChannelCursor(channelID, remoteID, cusror)
}
func (api *PluginAPI) SyncSharedChannel(channelID string) error {
return api.app.SyncSharedChannel(channelID)
}
func (api *PluginAPI) InviteRemoteToChannel(channelID string, remoteID, userID string, shareIfNotShared bool) error {
return api.app.InviteRemoteToChannel(channelID, remoteID, userID, shareIfNotShared)
}
func (api *PluginAPI) UninviteRemoteFromChannel(channelID string, remoteID string) error {
return api.app.UninviteRemoteFromChannel(channelID, remoteID)
}
func (api *PluginAPI) ReceiveSharedChannelSyncMsg(remoteID string, msg *model.SyncMsg) (model.SyncResponse, error) {
return api.app.ReceiveSharedChannelSyncMsg(api.ctx, api.id, remoteID, msg)
}
func (api *PluginAPI) ReceiveSharedChannelAttachmentSyncMsg(remoteID, channelID string, fi *model.FileInfo, data io.Reader) (*model.FileInfo, error) {
return api.app.ReceiveSharedChannelAttachmentSyncMsg(api.ctx, api.id, remoteID, channelID, fi, data)
}
func (api *PluginAPI) ReceiveSharedChannelProfileImageSyncMsg(remoteID, userID string, image []byte) error {
return api.app.ReceiveSharedChannelProfileImageSyncMsg(api.ctx, api.id, remoteID, userID, image)
}
func (api *PluginAPI) GetPluginID() string {
return api.id
}
func (api *PluginAPI) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
if err := api.checkLDAPLicense(); err != nil {
return nil, model.NewAppError("GetGroups", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
return api.app.GetGroups(page, perPage, opts, viewRestrictions)
}
func (api *PluginAPI) CreateDefaultSyncableMemberships(params model.CreateDefaultMembershipParams) *model.AppError {
if err := api.checkLDAPLicense(); err != nil {
return model.NewAppError("CreateDefaultSyncableMemberships", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
err := api.app.CreateDefaultMemberships(api.ctx, params)
if err != nil {
return model.NewAppError("CreateDefaultSyncableMemberships", "app.group.create_syncable_memberships.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (api *PluginAPI) DeleteGroupConstrainedMemberships() *model.AppError {
if err := api.checkLDAPLicense(); err != nil {
return model.NewAppError("DeleteGroupConstrainedMemberships", "app.group.license_error", nil, "", http.StatusForbidden).Wrap(err)
}
err := api.app.DeleteGroupConstrainedMemberships(api.ctx)
if err != nil {
return model.NewAppError("DeleteGroupConstrainedMemberships", "app.group.delete_invalid_syncable_memberships.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (api *PluginAPI) psaPluginContext() request.CTX {
return RequestContextWithCallerID(api.ctx, api.manifest.Id)
}
// psaPluginContextWithOptions is psaPluginContext plus the plugin's per-call
// declarations, used by the *WithOptions property methods so owner-based
// access control can match the scope against a field's owners list.
func (api *PluginAPI) psaPluginContextWithOptions(options model.PropertyRequestOptions) request.CTX {
return requestContextWithCallerIDAndOptions(api.ctx, api.manifest.Id, options)
}
func (api *PluginAPI) CreatePropertyField(field *model.PropertyField) (*model.PropertyField, error) {
createdField, appErr := api.app.CreatePropertyField(api.psaPluginContext(), field, false, "")
if appErr != nil {
return nil, appErr
}
return createdField, nil
}
func (api *PluginAPI) GetPropertyField(groupID, fieldID string) (*model.PropertyField, error) {
field, appErr := api.app.GetPropertyField(api.psaPluginContext(), groupID, fieldID)
if appErr != nil {
return nil, appErr
}
return field, nil
}
func (api *PluginAPI) GetPropertyFields(groupID string, ids []string) ([]*model.PropertyField, error) {
fields, appErr := api.app.GetPropertyFields(api.psaPluginContext(), groupID, ids)
if appErr != nil {
return nil, appErr
}
return fields, nil
}
func (api *PluginAPI) UpdatePropertyField(groupID string, field *model.PropertyField) (*model.PropertyField, error) {
updatedField, _, appErr := api.app.UpdatePropertyField(api.psaPluginContext(), groupID, field, false, "")
if appErr != nil {
return nil, appErr
}
return updatedField, nil
}
func (api *PluginAPI) DeletePropertyField(groupID, fieldID string) error {
if appErr := api.app.DeletePropertyField(api.psaPluginContext(), groupID, fieldID, false, ""); appErr != nil {
return appErr
}
return nil
}
func (api *PluginAPI) SearchPropertyFields(groupID string, opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
fields, appErr := api.app.SearchPropertyFields(api.psaPluginContext(), groupID, opts)
if appErr != nil {
return nil, appErr
}
return fields, nil
}
func (api *PluginAPI) CountPropertyFields(groupID string, includeDeleted bool) (int64, error) {
count, appErr := api.app.CountPropertyFieldsForGroup(api.psaPluginContext(), groupID, includeDeleted)
if appErr != nil {
return 0, appErr
}
return count, nil
}
func (api *PluginAPI) CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
count, appErr := api.app.CountPropertyFieldsForTarget(api.psaPluginContext(), groupID, targetType, targetID, includeDeleted)
if appErr != nil {
return 0, appErr
}
return count, nil
}
func (api *PluginAPI) CreatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
createdValue, appErr := api.app.CreatePropertyValue(api.psaPluginContext(), value)
if appErr != nil {
return nil, appErr
}
return createdValue, nil
}
func (api *PluginAPI) GetPropertyValue(groupID, valueID string) (*model.PropertyValue, error) {
value, appErr := api.app.GetPropertyValue(api.psaPluginContext(), groupID, valueID)
if appErr != nil {
return nil, appErr
}
return value, nil
}
func (api *PluginAPI) GetPropertyValues(groupID string, ids []string) ([]*model.PropertyValue, error) {
values, appErr := api.app.GetPropertyValues(api.psaPluginContext(), groupID, ids)
if appErr != nil {
return nil, appErr
}
return values, nil
}
func (api *PluginAPI) UpdatePropertyValue(groupID string, value *model.PropertyValue) (*model.PropertyValue, error) {
updatedValue, appErr := api.app.UpdatePropertyValue(api.psaPluginContext(), groupID, value)
if appErr != nil {
return nil, appErr
}
return updatedValue, nil
}
func (api *PluginAPI) UpsertPropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
upsertedValue, appErr := api.app.UpsertPropertyValue(api.psaPluginContext(), value)
if appErr != nil {
return nil, appErr
}
return upsertedValue, nil
}
func (api *PluginAPI) DeletePropertyValue(groupID, valueID string) error {
if appErr := api.app.DeletePropertyValue(api.psaPluginContext(), groupID, valueID); appErr != nil {
return appErr
}
return nil
}
func (api *PluginAPI) SearchPropertyValues(groupID string, opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
values, appErr := api.app.SearchPropertyValues(api.psaPluginContext(), groupID, opts)
if appErr != nil {
return nil, appErr
}
return values, nil
}
func (api *PluginAPI) RegisterPropertyGroup(name string) (*model.PropertyGroup, error) {
if name == model.DeprecatedCPAPropertyGroupName {
return nil, fmt.Errorf(
"%q is a version 1 PSA group that has been deprecated; use the version 2 PSA group %q instead",
model.DeprecatedCPAPropertyGroupName,
model.AccessControlPropertyGroupName,
)
}
group, appErr := api.app.RegisterPropertyGroup(api.psaPluginContext(), &model.PropertyGroup{
Name: name,
Version: model.PropertyGroupVersionV1,
})
if appErr != nil {
return nil, appErr
}
return group, nil
}
func (api *PluginAPI) GetPropertyGroup(name string) (*model.PropertyGroup, error) {
if name == model.DeprecatedCPAPropertyGroupName {
return nil, fmt.Errorf(
"%q is a version 1 PSA group that has been deprecated; use the version 2 PSA group %q instead",
model.DeprecatedCPAPropertyGroupName,
model.AccessControlPropertyGroupName,
)
}
group, appErr := api.app.GetPropertyGroup(api.psaPluginContext(), name)
if appErr != nil {
return nil, appErr
}
return group, nil
}
func (api *PluginAPI) GetPropertyFieldByName(groupID, targetID, name string) (*model.PropertyField, error) {
field, appErr := api.app.GetPropertyFieldByName(api.psaPluginContext(), groupID, targetID, name)
if appErr != nil {
return nil, appErr
}
return field, nil
}
func (api *PluginAPI) UpdatePropertyFields(groupID string, fields []*model.PropertyField) ([]*model.PropertyField, error) {
updatedFields, _, appErr := api.app.UpdatePropertyFields(api.psaPluginContext(), groupID, fields, false, "")
if appErr != nil {
return nil, appErr
}
return updatedFields, nil
}
func (api *PluginAPI) UpdatePropertyValues(groupID string, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
updatedValues, appErr := api.app.UpdatePropertyValues(api.psaPluginContext(), groupID, values)
if appErr != nil {
return nil, appErr
}
return updatedValues, nil
}
func (api *PluginAPI) UpsertPropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
upsertedValues, appErr := api.app.UpsertPropertyValues(api.psaPluginContext(), values, "", "", "")
if appErr != nil {
return nil, appErr
}
return upsertedValues, nil
}
func (api *PluginAPI) DeletePropertyValuesForTarget(groupID, targetType, targetID string) error {
if appErr := api.app.DeletePropertyValuesForTarget(api.psaPluginContext(), groupID, targetType, targetID); appErr != nil {
return appErr
}
return nil
}
func (api *PluginAPI) DeletePropertyValuesForField(groupID, fieldID string) error {
if appErr := api.app.DeletePropertyValuesForField(api.psaPluginContext(), groupID, fieldID); appErr != nil {
return appErr
}
return nil
}
func (api *PluginAPI) EvaluateAccessControl(userID, resourceType, resourceID, action string) (*model.AccessDecision, *model.AppError) {
return api.app.EvaluatePluginAccessRequest(api.ctx, api.id, userID, resourceType, resourceID, action)
}
func (api *PluginAPI) SaveAccessControlPolicy(actingUserID string, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
return api.app.SavePluginAccessControlPolicy(api.ctx, api.id, actingUserID, policy)
}
func (api *PluginAPI) GetAccessControlPolicy(id string) (*model.AccessControlPolicy, *model.AppError) {
return api.app.GetPluginAccessControlPolicy(api.ctx, api.id, id)
}
func (api *PluginAPI) DeleteAccessControlPolicy(actingUserID, resourceType, id string) *model.AppError {
return api.app.DeletePluginAccessControlPolicy(api.ctx, api.id, actingUserID, resourceType, id)
}
func (api *PluginAPI) CheckAccessControlExpression(actingUserID, resourceType, expression string) ([]model.CELExpressionError, *model.AppError) {
return api.app.CheckPluginAccessControlExpression(api.ctx, api.id, actingUserID, resourceType, expression)
}
func (api *PluginAPI) QueryUsersForAccessControlExpression(actingUserID, resourceType, expression, term, cursorID string, limit int) (*model.AccessControlPolicyTestResponse, *model.AppError) {
return api.app.QueryUsersForPluginAccessControlExpression(api.ctx, api.id, actingUserID, resourceType, expression, term, cursorID, limit)
}
func (api *PluginAPI) GetAccessControlFieldsAutocomplete(actingUserID, after string, limit int) ([]*model.PropertyField, *model.AppError) {
return api.app.GetPluginAccessControlFieldsAutocomplete(api.ctx, api.id, actingUserID, after, limit)
}
func (api *PluginAPI) GetAccessControlVisualAST(actingUserID, resourceType, expression string) (*model.VisualExpression, *model.AppError) {
return api.app.GetPluginAccessControlVisualAST(api.ctx, api.id, actingUserID, resourceType, expression)
}
func (api *PluginAPI) UpsertPropertyValuesWithOptions(values []*model.PropertyValue, options model.PropertyRequestOptions) ([]*model.PropertyValue, error) {
upsertedValues, appErr := api.app.UpsertPropertyValues(api.psaPluginContextWithOptions(options), values, "", "", "")
if appErr != nil {
return nil, appErr
}
return upsertedValues, nil
}
func (api *PluginAPI) UpsertPropertyValueWithOptions(value *model.PropertyValue, options model.PropertyRequestOptions) (*model.PropertyValue, error) {
upsertedValue, appErr := api.app.UpsertPropertyValue(api.psaPluginContextWithOptions(options), value)
if appErr != nil {
return nil, appErr
}
return upsertedValue, nil
}
func (api *PluginAPI) DeletePropertyValueWithOptions(groupID, valueID string, options model.PropertyRequestOptions) error {
if appErr := api.app.DeletePropertyValue(api.psaPluginContextWithOptions(options), groupID, valueID); appErr != nil {
return appErr
}
return nil
}
func (api *PluginAPI) DeletePropertyValuesForTargetWithOptions(groupID, targetType, targetID string, options model.PropertyRequestOptions) error {
if appErr := api.app.DeletePropertyValuesForTarget(api.psaPluginContextWithOptions(options), groupID, targetType, targetID); appErr != nil {
return appErr
}
return nil
}
func (api *PluginAPI) DeletePropertyValuesForFieldWithOptions(groupID, fieldID string, options model.PropertyRequestOptions) error {
if appErr := api.app.DeletePropertyValuesForField(api.psaPluginContextWithOptions(options), groupID, fieldID); appErr != nil {
return appErr
}
return nil
}