Skip to content

MM-69161 Adding permission check model to allow/deny requests - #678

Merged
avasconcelos114 merged 5 commits into
masterfrom
MM-69161
Jun 16, 2026
Merged

avasconcelos114 merged 5 commits into
masterfrom
MM-69161

Conversation

@avasconcelos114

@avasconcelos114 avasconcelos114 commented Jun 11, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR adds a layer that checks gitlab's own permission model to prevent any state-changing requests from being made, and failing that it falls back to the current flow for only allowing sysadmins from accessing a particular command. This has been for now only integrated with the /gitlab webhook commands

Ticket Link

Fixes https://mattermost.atlassian.net/browse/MM-69161

Change Impact: 🟠 Medium

Reasoning: The PR changes authorization behavior for /gitlab webhook by removing a local sysadmin gate and relying on GitLab permission enforcement plus namespace allow/deny checks, and it extends audit logging for webhook creation. While this is security-relevant, the blast radius is primarily confined to webhook command flows and is supported by newly added namespace-denial tests.

Regression Risk: Medium—command access semantics change (admin-only → namespace-scoped/remote-permission-scoped), and new early-denial paths can alter behavior for existing configurations; however, the affected logic is localized and key denial/success scenarios are covered by tests.

QA Recommendation: Moderate manual QA. Validate for both admin and non-admin users:

  1. /gitlab webhook list for allowed vs disallowed namespaces (including denial messaging and no hook retrieval),
  2. /gitlab webhook add for allowed vs disallowed namespaces (including denial messaging),
  3. audit logging on successful webhook add (hook id/url/scope present) and on forbidden failures (failure recorded correctly).
    Generated by CodeRabbitAI

- Applied permission model to webhook slash commands
@avasconcelos114 avasconcelos114 self-assigned this Jun 11, 2026
@avasconcelos114
avasconcelos114 requested a review from a team as a code owner June 11, 2026 08:59
@avasconcelos114 avasconcelos114 added the 2: Dev Review Requires review by a core committer label Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0b106191-f587-460c-9643-f238a51ee5fa

📥 Commits

Reviewing files that changed from the base of the PR and between fc8d93f and a33ba22.

📒 Files selected for processing (3)
  • server/command.go
  • server/command_test.go
  • server/gitlab/api.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/command_test.go

📝 Walkthrough

Walkthrough

The system-admin authorization gate is removed from handleWebhookHandler and its autocomplete wiring. Both the list and add webhook subcommands now enforce isNamespaceAllowed checks after resolving the target namespace. The add path additionally wraps the full flow with a structured audit record using two new payload types, AddWebhookAuditParams and AddWebhookAuditResult. A namespaceFromGroupAndProject helper is introduced to derive namespace strings. The GetProjectHooks pagination error handling is fixed to validate HTTP responses before processing errors. Tests are updated to cover the new namespace restriction paths and drop the now-removed admin-gate subtests.

Changes

Namespace-Based Webhook Authorization with Audit Logging

Layer / File(s) Summary
Audit payload structs and namespace helper
server/audit.go, server/utils.go
Adds AddWebhookAuditParams and AddWebhookAuditResult structs with Auditable() methods for the webhook-add audit event. Adds namespaceFromGroupAndProject to construct a namespace string from a group and optional project.
Webhook command: admin gate removal, namespace checks, and audit integration
server/command.go
Removes the system-admin gate from handleWebhookHandler and removes model.SystemAdminRoleId from autocomplete wiring. Adds webhookPermissionMessage helper to format namespace denial messages. Adds isNamespaceAllowed early-return check to the list subcommand and updates error handling to map gitlab.ErrForbidden to the permission message. Adds resolvedNamespace derivation, a deferred audit record initialization with AddWebhookAuditParams, isNamespaceAllowed enforcement with failure logging, and createHook invocation with AddWebhookAuditResult or error recording to the add subcommand.
GitLab API: pagination error handling
server/gitlab/api.go
Adds HTTP response validation (checkResponse(resp)) in the GetProjectHooks pagination loop to properly detect non-success HTTP statuses when the error value is nil.
Tests: namespace enforcement and admin protection updates
server/command_test.go
Adds TestListWebhookCommandNamespaceNotAllowed and TestAddWebhookCommandNamespaceNotAllowed for the new namespace restriction paths. Updates test mocks with optional LogAuditRec expectations. Removes the prior webhook admin-protection subtests from TestAdminProtectedCommands.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hoppity-hop through the namespace gate,
No admin badge needed — just check the state!
The audit trail follows each webhook I place,
With params and results logged neatly in space.
Now only allowed groups may ring the bell,
And every add/list is recorded so well! 🔔

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: implementing a permission check model to allow/deny requests, which is the central theme of removing local admin checks and relying on GitLab's permission model.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@avasconcelos114

avasconcelos114 commented Jun 11, 2026 •

Copy link
Copy Markdown
Member Author

@edgarbellot This PR here adds the general flow we had previously discussed, but now that I see it in action I can't help but wonder if we should just remove the admin check from the webhook commands instead as Gitlab's own API would be rejecting a call made with the oauth token of a user that doesn't have the needed permissions anyway :D

Unless we see any value in preventing an API call from being made to Gitlab until we're sure the user has due access beforehand, but for now I kept the changes and flow until we reach a decision, it would be quite quick and easy to revert the previous commit and add a new one that doesn't enforce sys admin checks for these two commands

@avasconcelos114 avasconcelos114 added the 3: Security Review Review requested from Security Team label Jun 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/gitlab/permissions_test.go (1)

55-63: ⚡ Quick win

Add explicit 403 coverage for both access-level lookups.

These tests currently pin 404 and 500, but not the 403 mapping path that influences downstream authorization fallback behavior. Adding a focused 403 case for each method will lock this contract down.

Suggested test additions
 func TestGetProjectAccessLevel(t *testing.T) {
@@
 	t.Run("returns error on server failure", func(t *testing.T) {
@@
 	})
+
+	t.Run("returns ErrForbidden on forbidden response", func(t *testing.T) {
+		client, token := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusForbidden)
+			_, _ = w.Write([]byte(`{"message":"403 Forbidden"}`))
+		})
+
+		level, err := client.GetProjectAccessLevel(context.Background(), user, token, "group", "project")
+		assert.ErrorIs(t, err, ErrForbidden)
+		assert.Equal(t, internGitlab.NoPermissions, level)
+	})
 }
@@
 func TestGetGroupAccessLevel(t *testing.T) {
@@
 	t.Run("returns error on server failure", func(t *testing.T) {
@@
 	})
+
+	t.Run("returns ErrForbidden on forbidden response", func(t *testing.T) {
+		client, token := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusForbidden)
+			_, _ = w.Write([]byte(`{"message":"403 Forbidden"}`))
+		})
+
+		level, err := client.GetGroupAccessLevel(context.Background(), user, token, "group")
+		assert.ErrorIs(t, err, ErrForbidden)
+		assert.Equal(t, internGitlab.NoPermissions, level)
+	})
 }

Also applies to: 92-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/gitlab/permissions_test.go` around lines 55 - 63, Add explicit 403
test cases for both access-level lookup methods: when the test HTTP handler
returns http.StatusForbidden, call client.GetProjectAccessLevel(...) and
client.GetGroupAccessLevel(...) and assert the returned error is the forbidden
sentinel (use require.ErrorIs/ assert.ErrorIs with ErrForbidden or the
appropriate forbidden error constant). Place one test alongside the existing
"returns error on server failure" block for the project path and a matching one
alongside the analogous group path (the block around lines 92-100) so the
403->forbidden mapping is covered for both GetProjectAccessLevel and
GetGroupAccessLevel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/gitlab/permissions_test.go`:
- Around line 55-63: Add explicit 403 test cases for both access-level lookup
methods: when the test HTTP handler returns http.StatusForbidden, call
client.GetProjectAccessLevel(...) and client.GetGroupAccessLevel(...) and assert
the returned error is the forbidden sentinel (use require.ErrorIs/
assert.ErrorIs with ErrForbidden or the appropriate forbidden error constant).
Place one test alongside the existing "returns error on server failure" block
for the project path and a matching one alongside the analogous group path (the
block around lines 92-100) so the 403->forbidden mapping is covered for both
GetProjectAccessLevel and GetGroupAccessLevel.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 78bfbbbe-1c0b-4e13-9926-a5d8a4ee7aa5

📥 Commits

Reviewing files that changed from the base of the PR and between 4f98c2a and 24a987e.

📒 Files selected for processing (9)
  • server/command.go
  • server/command_test.go
  • server/gitlab/gitlab.go
  • server/gitlab/mocks/mock_gitlab.go
  • server/gitlab/permissions.go
  • server/gitlab/permissions_test.go
  • server/mocks/mock_gitlab.go
  • server/permissions.go
  • server/permissions_test.go

Comment thread server/gitlab/permissions.go Outdated
}
if err != nil {
return internGitlab.NoPermissions, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If the request errored out at the transport layer resp could be nil here depending on checkResponse's contract. I would swap the order so you check err != nil first when resp == nil. Same pattern repeats in GetGroupAccessLevel.

Comment thread server/gitlab/permissions.go Outdated
return internGitlab.NoPermissions, err
}

endpoint := fmt.Sprintf("groups/%s/members/all/%d", internGitlab.PathEscape(group), user.GitlabUserID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: here we use internGitlab.PathEscape for the group identifier but GetProjectAccessLevel above does fmt.Sprintf("%s/%s", namespace, project) with no escaping. Can we align both to use PathEscape for symmetry?

Comment thread server/permissions.go Outdated

required, ok := minimumAccessLevels[action][scope]
if !ok {
return false, errors.Errorf("no minimum access level configured for action %q scope %q", action, scope)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making this a build time guarantee like a func init() that verifies every permissionAction constant has a complete Project/Group entry in minimumAccessLevels. That way its caught at plugin boot isntead when we run the command in production.

Comment thread server/permissions.go Outdated
}
if !allowed {
return insufficientPermissionsMessage
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The err is logged but then discarded when allowed's zero value is false so a misconfig silently surfaces to the user as insufficientPermissionsMessage which might be confusing UX

Comment thread server/command.go
gitlab.AddCommand(settings)

webhook := model.NewAutocompleteData("webhook", "[command]", "Available Commands: list, add")
webhook.RoleID = model.SystemAdminRoleId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this mean that if a non-Maintainer will see and try the command they will hit the denial message?

Comment thread server/permissions_test.go Outdated
@@ -0,0 +1,110 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice coverage on the matrix. I would add a couple cases for full safety but living up to your consideration!

  1. Action with no min-access-level configured which exercises the errors.Errorf("no minimum access level configured for action %q scope %q", ...) branch.
  2. requireWebhookPermission directly as currently only hasGitlabPermission is unit-tested.

@nang2049 nang2049 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing change thanks @avasconcelos114 a few minor comments!

@edgarbellot

Copy link
Copy Markdown
Contributor

@edgarbellot This PR here adds the general flow we had previously discussed, but now that I see it in action I can't help but wonder if we should just remove the admin check from the webhook commands instead as Gitlab's own API would be rejecting a call made with the oauth token of a user that doesn't have the needed permissions anyway :D

Unless we see any value in preventing an API call from being made to Gitlab until we're sure the user has due access beforehand, but for now I kept the changes and flow until we reach a decision, it would be quite quick and easy to revert the previous commit and add a new one that doesn't enforce sys admin checks for these two commands

I agree, let's remove the check and rely on GitLab.

Since these calls use the user's own OAuth token, GitLab is already the source of truth and rejects anything they can't do. That holds for sysadmins too.

@edgarbellot edgarbellot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! just one nit. Thanks Andre!

Comment thread server/command.go Outdated
return namespaceErr.Error()
}

if msg := p.requireWebhookPermission(ctx, info, group, project); msg != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: webhook add creates a state-changing resource but doesn't emit an audit record, unlike other state-changing ops in the repo (createIssue). Since you're touching the auth flow here it could be a good spot to add a MakeAuditRecord / LogAuditRec capturing the actor and target. Non-blocking :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yep sounds good! so for this PR I'll now:

  1. Revert the previous PR with the explicit permission checks
  2. Remove the sysadmin checks for webhook commands in order to allow Gitlab's own permission checks to kick in (with some graceful error handling so it's clear why was the call rejected)
  3. Add audit logging for the webhook command handlers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with some graceful error handling so it's clear why was the call rejected

good call!

Sounds good - happy to review the changes once they are ready.

@edgarbellot edgarbellot removed the 3: Security Review Review requested from Security Team label Jun 15, 2026
- Removed active permission checks and allowing gitlab to dictate outcome of request

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/command.go (1)

1162-1162: 💤 Low value

Autocomplete visibility restricts to sysadmin but command execution does not.

The RoleID = model.SystemAdminRoleId hides the webhook commands from non-admin users in autocomplete suggestions, but the actual command handler no longer enforces admin authorization (relying on GitLab API permissions instead). This creates a UX gap where eligible users (GitLab maintainers who aren't Mattermost admins) won't discover the command through autocomplete.

Consider removing the RoleID restriction to align autocomplete visibility with the new permission model, or document that users can manually type /gitlab webhook even if it doesn't appear in suggestions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/command.go` at line 1162, The webhook command's RoleID is set to
model.SystemAdminRoleId, which restricts autocomplete visibility to system
admins only, but the command handler no longer enforces admin authorization and
instead relies on GitLab API permissions. Remove the RoleID restriction (or set
it to an empty/default value) at the line where webhook.RoleID is assigned to
align autocomplete visibility with the actual permission model that checks
GitLab maintainer status rather than Mattermost admin status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/command.go`:
- Line 1162: The webhook command's RoleID is set to model.SystemAdminRoleId,
which restricts autocomplete visibility to system admins only, but the command
handler no longer enforces admin authorization and instead relies on GitLab API
permissions. Remove the RoleID restriction (or set it to an empty/default value)
at the line where webhook.RoleID is assigned to align autocomplete visibility
with the actual permission model that checks GitLab maintainer status rather
than Mattermost admin status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: eba732ac-ca9d-47d2-8632-7fc4ed0a324a

📥 Commits

Reviewing files that changed from the base of the PR and between 24a987e and d2c40ad.

📒 Files selected for processing (4)
  • server/audit.go
  • server/command.go
  • server/command_test.go
  • server/utils.go

@avasconcelos114

Copy link
Copy Markdown
Member Author

Made the changes necessary to simplify this quite a bit by simply allowing Gitlab to reject the request made with the user's oauth token and removed the sysadmin checks previously in place.

I've also gone ahead and enforced the same checks we had in other commands for namespace limiting via the GitLab Group: option + Added audit logging for the transaction

@edgarbellot edgarbellot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good :)

Comment thread server/command.go
}
model.AddEventParameterAuditableToAuditRec(auditRec, "add_webhook", auditParams)

if err := p.isNamespaceAllowed(resolvedNamespace); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

@nang2049 nang2049 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@avasconcelos114
avasconcelos114 merged commit 84f5a3e into master Jun 16, 2026
17 checks passed
@avasconcelos114
avasconcelos114 deleted the MM-69161 branch June 16, 2026 13:16
avasconcelos114 added a commit that referenced this pull request Jun 22, 2026
…680)

* MM-69161 Adding permission check model to allow/deny requests

- Applied permission model to webhook slash commands

* Revert "MM-69161 Adding permission check model to allow/deny requests"

This reverts commit 24a987e.

* Adding audit logging to webhook command

- Removed active permission checks and allowing gitlab to dictate outcome of request

* Allowing webhook autocomplete to be visible to standard users

* Added more human-readable error responses to webhook cmd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2: Dev Review Requires review by a core committer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants