Adds Remote Cluster related API endpoints (#27432)

* Adds Remote Cluster related API endpoints

New endpoints for the following routes are added:

- Get Remote Clusters at `GET /api/v4/remotecluster`
- Create Remote Cluster at `POST /api/v4/remotecluster`
- Accept Remote Cluster invite at `POST
/api/v4/remotecluster/accept_invite`
- Generate Remote Cluster invite at `POST
/api/v4/remotecluster/{remote_id}/generate_invite`
- Get Remote Cluster at `GET /api/v4/remotecluster/{remote_id}`
- Patch Remote Cluster at `PATCH /api/v4/remotecluster/{remote_id}`
- Delete Remote Cluster at `DELETE /api/v4/remotecluster/{remote_id}`

These endpoints are planned to be used from the system console, and
gated through the `manage_secure_connections` permission.

* Update server/channels/api4/remote_cluster_test.go

Co-authored-by: Doug Lauder <wiggin77@warpmail.net>

* Fix AppError names

---------

Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Miguel de la Cruz
2024-07-04 10:35:26 +02:00
коммит произвёл GitHub
родитель cc5e87ae24
Коммит 809ad4f76d
29 изменённых файлов: 1702 добавлений и 77 удалений

Просмотреть файл

@@ -22,6 +22,14 @@ func (api *API) InitRemoteCluster() {
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData, handlerParamFileAPI)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage, handlerParamFileAPI)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("", api.APISessionRequired(getRemoteClusters)).Methods("GET")
api.BaseRoutes.RemoteCluster.Handle("", api.APISessionRequired(createRemoteCluster)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/accept_invite", api.APISessionRequired(remoteClusterAcceptInvite)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}/generate_invite", api.APISessionRequired(generateRemoteClusterInvite)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteCluster)).Methods("GET")
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(patchRemoteCluster)).Methods("PATCH")
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteRemoteCluster)).Methods("DELETE")
}
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -293,3 +301,359 @@ func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func getRemoteClusters(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: c.Params.ExcludeOffline,
InChannel: c.Params.InChannel,
NotInChannel: c.Params.NotInChannel,
Topic: c.Params.Topic,
CreatorId: c.Params.CreatorId,
OnlyConfirmed: c.Params.OnlyConfirmed,
PluginID: c.Params.PluginId,
OnlyPlugins: c.Params.OnlyPlugins,
ExcludePlugins: c.Params.ExcludePlugins,
}
rcs, appErr := c.App.GetAllRemoteClusters(c.Params.Page, c.Params.PerPage, filter)
if appErr != nil {
c.Err = appErr
return
}
for _, rc := range rcs {
rc.Sanitize()
}
b, err := json.Marshal(rcs)
if err != nil {
c.Err = model.NewAppError("getRemoteClusters", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("createRemoteCluster", audit.Fail)
defer c.LogAuditRec(auditRec)
var rcWithTeamAndPassword model.RemoteClusterWithPassword
if jsonErr := json.NewDecoder(r.Body).Decode(&rcWithTeamAndPassword); jsonErr != nil {
c.SetInvalidParamWithErr("remoteCluster", jsonErr)
return
}
if rcWithTeamAndPassword.Password == "" {
c.SetInvalidParam("password")
return
}
url := c.App.GetSiteURL()
if url == "" {
c.Err = model.NewAppError("createRemoteCluster", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity)
return
}
if rcWithTeamAndPassword.DisplayName == "" {
rcWithTeamAndPassword.DisplayName = rcWithTeamAndPassword.Name
}
rc := &model.RemoteCluster{
Name: rcWithTeamAndPassword.Name,
DisplayName: rcWithTeamAndPassword.DisplayName,
SiteURL: model.SiteURLPending + model.NewId(),
Token: model.NewId(),
CreatorId: c.AppContext.Session().UserId,
}
audit.AddEventParameterAuditable(auditRec, "remotecluster", rc)
rcSaved, appErr := c.App.AddRemoteCluster(rc)
if appErr != nil {
c.Err = appErr
return
}
rcSaved.Sanitize()
inviteCode, iErr := c.App.CreateRemoteClusterInvite(rcSaved.RemoteId, url, rcSaved.Token, rcWithTeamAndPassword.Password)
if iErr != nil {
c.Err = iErr
return
}
auditRec.Success()
auditRec.AddEventResultState(rcSaved)
auditRec.AddEventObjectType("remotecluster")
b, err := json.Marshal(model.RemoteClusterWithInvite{RemoteCluster: rcSaved, Invite: inviteCode})
if err != nil {
c.Err = model.NewAppError("createRemoteCluster", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.WriteHeader(http.StatusCreated)
w.Write(b)
}
func remoteClusterAcceptInvite(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
// make sure remote cluster service is enabled.
rcs, appErr := c.App.GetRemoteClusterService()
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
defer c.LogAuditRec(auditRec)
var rcAcceptInvite model.RemoteClusterAcceptInvite
if jsonErr := json.NewDecoder(r.Body).Decode(&rcAcceptInvite); jsonErr != nil {
c.SetInvalidParamWithErr("remoteCluster", jsonErr)
return
}
audit.AddEventParameter(auditRec, "name", rcAcceptInvite.Name)
audit.AddEventParameter(auditRec, "display_name", rcAcceptInvite.DisplayName)
if rcAcceptInvite.DisplayName == "" {
rcAcceptInvite.DisplayName = rcAcceptInvite.Name
}
invite, dErr := c.App.DecryptRemoteClusterInvite(rcAcceptInvite.Invite, rcAcceptInvite.Password)
if dErr != nil {
c.Err = dErr
return
}
audit.AddEventParameter(auditRec, "site_url", invite.SiteURL)
url := c.App.GetSiteURL()
if url == "" {
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity)
return
}
rc, aErr := rcs.AcceptInvitation(invite, rcAcceptInvite.Name, rcAcceptInvite.DisplayName, c.AppContext.Session().UserId, url)
if aErr != nil {
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.remote_cluster.accept_invitation_error", nil, "", http.StatusInternalServerError).Wrap(aErr)
if appErr, ok := aErr.(*model.AppError); ok {
c.Err = appErr
}
return
}
rc.Sanitize()
auditRec.Success()
auditRec.AddEventResultState(rc)
auditRec.AddEventObjectType("remotecluster")
b, err := json.Marshal(rc)
if err != nil {
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.WriteHeader(http.StatusCreated)
w.Write(b)
}
func generateRemoteClusterInvite(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("generateRemoteClusterInvite", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
props := model.MapFromJSON(r.Body)
password := props["password"]
if password == "" {
c.SetInvalidParam("password")
return
}
url := c.App.GetSiteURL()
if url == "" {
c.Err = model.NewAppError("generateRemoteClusterInvite", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity)
return
}
rc, appErr := c.App.GetRemoteCluster(c.Params.RemoteId)
if appErr != nil {
c.Err = appErr
return
}
inviteCode, invErr := c.App.CreateRemoteClusterInvite(rc.RemoteId, url, rc.Token, password)
if invErr != nil {
c.Err = invErr
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(inviteCode))
}
func getRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
c.RequireRemoteId()
if c.Err != nil {
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
rc, err := c.App.GetRemoteCluster(c.Params.RemoteId)
if err != nil {
c.Err = err
return
}
rc.Sanitize()
if err := json.NewEncoder(w).Encode(rc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
c.RequireRemoteId()
if c.Err != nil {
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var patch model.RemoteClusterPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("remotecluster", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchRemoteCluster", audit.Fail)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
audit.AddEventParameterAuditable(auditRec, "remotecluster_patch", &patch)
defer c.LogAuditRec(auditRec)
orc, err := c.App.GetRemoteCluster(c.Params.RemoteId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(orc)
auditRec.AddEventObjectType("remotecluster")
updatedRC, err := c.App.PatchRemoteCluster(c.Params.RemoteId, &patch)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedRC)
if err := json.NewEncoder(w).Encode(updatedRC); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("deleteRemoteCluster", audit.Fail)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
defer c.LogAuditRec(auditRec)
orc, err := c.App.GetRemoteCluster(c.Params.RemoteId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(orc)
auditRec.AddEventObjectType("remotecluster")
deleted, err := c.App.DeleteRemoteCluster(c.Params.RemoteId)
if err != nil {
c.Err = err
return
}
if !deleted {
c.Err = model.NewAppError("deleteRemoteCluster", "api.remote_cluster.cluster_not_deleted", nil, "", http.StatusInternalServerError)
return
}
auditRec.Success()
w.WriteHeader(http.StatusNoContent)
}