mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM-69063 - Add team ABAC model and constants foundation * Add team ABAC store EXISTS, channel Type retrofit, policy count split, and index migration * Add team ABAC app layer: access gate, hydrators, assign/unassign, cleanup, and GetTeamMembersToRemove store * Enforce team membership ABAC on join and hide policy governed teams from non-qualifying users in the directory * Add team_ids to access policy assign/unassign, expose per-team policy GET, and support abac_match_only for not_in_team user listing * Add team ABAC client methods, websocket handler, per-team System Console policy UI, and hide policy-governed teams from non-qualifying users * Make team ABAC mode-aware: advisory on public teams, strict on private, and surface governed private teams to qualifying users in directory listings * Flag-gate team ABAC mutation/read APIs and fix policy-save error handling, member-removal limit, team-id validation, export, and audit cleanup * coderabbit feedback; Broadcast team policy enforcement updates on policy create/update and activation, not only on delete * Update team access control policy schema to allow nullable policies and enhance test cases with channel counts * Enhance access control policy tests to include team policy search alongside channel policy search * Add team membership access control feature flag to docker-compose generation * Implement team access control policy checks and refactor related components * MM-69100 - add team membership ABAC sync worker with mode-aware removal and auto-add * Add team ABAC removal/auto-add notifications, cascade audit, and team custom-rules save backend * Add Team Settings team membership tab with custom-rules editor, auto-add toggle, save confirmation, and mode-flip sync trigger * Add team access control policy panels, job details team list, and e2e coverage for membership tab and discoverability cards * ix save double-submit with loading state, team privacy via updateTeamPrivacy, mode-flip count accuracy, sync trigger logic, i18n keys, and e2e test cleanup * Add team membership policy notices to invite and add-to-team flows, team access control system messages, and a team policy attributes endpoint * Add team membership recommended tag for qualifying users and team requirements notice in the members modal * temp * adjust styling, add new e2e, fix team admin job permissions issues * e2e clean items created * fix directory and team integration e2e tests * Surface team sync results in the Sync Job Details modal via a Teams tab linked from the chained channel job * code clean up, adjust styling * Add access control attributes endpoint and enhance E2E tests for team membership * Enhance team access control by updating permission checks and syncing jobs * Add API-level enforcement gate tests for ABAC team membership * Fix formatting in team channel settings component by adding missing semicolon * Fix formatting in Client4 class by adjusting type annotations for clarity * revert unwanted changes in package-lock * Refactor job permission test and simplify error handling in team details component * Update test assertions and enhance auto-add functionality in team membership tab * Update onboarding tests to use 'Public Team' card for team access settings * Refactor AccessSettings and TeamPolicyEditor components to replace allowOpenInviteCheckbox with public and private team buttons, and update access control job dispatching logic. * Remove redundant state updates for channel assignments in access control policy reducer * Implement email suppression for team membership notifications and add related tests * Authorize ABAC team self-join by attribute match instead of the join_private_teams role * comments clean up * Gate team privacy type normalization behind active ABAC, keep legacy allow_open_invite-only path otherwise * adjust job details styles * Add team membership policy disconnect confirmation, fix linked-policy affected-member count, and polish job details modal * Combine custom rules with system policy expressions for accurate confirm counts * Implement child resource count stamping and deletion gating for access control policies * Enhance team sync job triggering logic for membership changes without auto-add * Refactor team privacy handling to align with open-directory model; update comments for clarity on allow_open_invite logic. * Deduplicate team ABAC sync jobs per policy to prevent concurrent runs emitting duplicate DMs and audit records on HA clusters * fix linter warnings * Add confirmation for policy removal in TeamDetails tests * Update delete policy message to include Teams in the warning * Add linked teams warning to policy deletion and update translations * Add Auto-add feature for policy selection and team management - Enhanced PolicySelectionModal to include an Auto-add checkbox for each policy. - Updated PolicyList to manage Auto-add state and reflect changes in the UI. - Modified TeamAccessControl to handle Auto-add functionality and persist changes. - Added tests to ensure Auto-add behavior works as expected in various scenarios. - Updated translations for Auto-add related strings. * Add team membership sync footer and enhance job fetching with policy ID * Refactor team membership policy handling and sync footer implementation * Add team sync channel cascade test for private channel membership --------- Co-authored-by: Mattermost Build <[email protected]>
317 lines
8.7 KiB
Go
317 lines
8.7 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const (
|
|
TeamOpen = "O"
|
|
TeamInvite = "I"
|
|
TeamAllowedDomainsMaxLength = 500
|
|
TeamCompanyNameMaxLength = 64
|
|
TeamDescriptionMaxLength = 255
|
|
TeamDisplayNameMaxRunes = 64
|
|
TeamEmailMaxLength = 128
|
|
TeamNameMaxLength = 64
|
|
TeamNameMinLength = 2
|
|
)
|
|
|
|
type Team struct {
|
|
Id string `json:"id"`
|
|
CreateAt int64 `json:"create_at"`
|
|
UpdateAt int64 `json:"update_at"`
|
|
DeleteAt int64 `json:"delete_at"`
|
|
DisplayName string `json:"display_name"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Email string `json:"email"`
|
|
Type string `json:"type"`
|
|
CompanyName string `json:"company_name"`
|
|
AllowedDomains string `json:"allowed_domains"`
|
|
InviteId string `json:"invite_id"`
|
|
AllowOpenInvite bool `json:"allow_open_invite"`
|
|
LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
|
|
SchemeId *string `json:"scheme_id"`
|
|
GroupConstrained *bool `json:"group_constrained"`
|
|
PolicyID *string `json:"policy_id"` // Data Retention policy — unrelated to ABAC below
|
|
CloudLimitsArchived bool `json:"cloud_limits_archived"`
|
|
|
|
// Not persisted; derived by the store via EXISTS on AccessControlPolicies(Type='team').
|
|
// Use HasMembershipPolicyAction for enforcement — this is a read-path signal only.
|
|
PolicyEnforced bool `json:"policy_enforced"`
|
|
PolicyActions map[string]bool `json:"policy_actions,omitempty"` // hydrated lazily; nil when not hydrated
|
|
PolicyIsActive bool `json:"policy_is_active"`
|
|
|
|
// Not persisted; a transient per-viewer hint set on public, policy-enforced
|
|
// teams the requesting user qualifies to join and is not already a member of.
|
|
// Never set on private teams. Carries no policy detail.
|
|
Recommended bool `json:"recommended,omitempty"`
|
|
}
|
|
|
|
// HasPolicyAction is nil-safe; returns false when PolicyActions is nil or empty.
|
|
func (o *Team) HasPolicyAction(action string) bool {
|
|
if o == nil || len(o.PolicyActions) == 0 {
|
|
return false
|
|
}
|
|
return o.PolicyActions[action]
|
|
}
|
|
|
|
func (o *Team) HasMembershipPolicyAction() bool {
|
|
return o.HasPolicyAction(AccessControlPolicyActionMembership)
|
|
}
|
|
|
|
func (o *Team) Auditable() map[string]any {
|
|
return map[string]any{
|
|
"id": o.Id,
|
|
"create_at": o.CreateAt,
|
|
"update_at": o.UpdateAt,
|
|
"delete_at": o.DeleteAt,
|
|
"type": o.Type,
|
|
"invite_id": o.InviteId,
|
|
"allow_open_invite": o.AllowOpenInvite,
|
|
"scheme_id": o.SchemeId,
|
|
"group_constrained": o.GroupConstrained,
|
|
"policy_id": o.PolicyID,
|
|
"cloud_limits_archived": o.CloudLimitsArchived,
|
|
}
|
|
}
|
|
|
|
func (o *Team) LogClone() any {
|
|
return o.Auditable()
|
|
}
|
|
|
|
type TeamPatch struct {
|
|
DisplayName *string `json:"display_name"`
|
|
Description *string `json:"description"`
|
|
CompanyName *string `json:"company_name"`
|
|
AllowedDomains *string `json:"allowed_domains"`
|
|
AllowOpenInvite *bool `json:"allow_open_invite"`
|
|
GroupConstrained *bool `json:"group_constrained"`
|
|
CloudLimitsArchived *bool `json:"cloud_limits_archived"`
|
|
}
|
|
|
|
func (o *TeamPatch) Auditable() map[string]any {
|
|
return map[string]any{
|
|
"allow_open_invite": o.AllowOpenInvite,
|
|
"group_constrained": o.GroupConstrained,
|
|
"cloud_limits_archived": o.CloudLimitsArchived,
|
|
}
|
|
}
|
|
|
|
type TeamForExport struct {
|
|
Team
|
|
SchemeName *string
|
|
}
|
|
|
|
type Invites struct {
|
|
Invites []map[string]string `json:"invites"`
|
|
}
|
|
|
|
type TeamsWithCount struct {
|
|
Teams []*Team `json:"teams"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
func (o *Invites) ToEmailList() []string {
|
|
emailList := make([]string, 0, len(o.Invites))
|
|
for _, invite := range o.Invites {
|
|
emailList = append(emailList, invite["email"])
|
|
}
|
|
return emailList
|
|
}
|
|
|
|
func (o *Team) Etag() string {
|
|
return Etag(o.Id, o.UpdateAt)
|
|
}
|
|
|
|
func (o *Team) IsValid() *AppError {
|
|
if !IsValidId(o.Id) {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.id.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if o.CreateAt == 0 {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if o.UpdateAt == 0 {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if len(o.Email) > TeamEmailMaxLength {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if o.Email != "" && !IsValidEmail(o.Email) {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if utf8.RuneCountInString(o.DisplayName) == 0 || utf8.RuneCountInString(o.DisplayName) > TeamDisplayNameMaxRunes {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.name.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if len(o.Name) > TeamNameMaxLength {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if len(o.Description) > TeamDescriptionMaxLength {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.description.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if o.InviteId == "" {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.invite_id.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if IsReservedTeamName(o.Name) {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidTeamName(o.Name) {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.characters.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if !(o.Type == TeamOpen || o.Type == TeamInvite) {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if len(o.CompanyName) > TeamCompanyNameMaxLength {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.company.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if len(o.AllowedDomains) > TeamAllowedDomainsMaxLength {
|
|
return NewAppError("Team.IsValid", "model.team.is_valid.domains.app_error", nil, "id="+o.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *Team) PreSave() {
|
|
if o.Id == "" {
|
|
o.Id = NewId()
|
|
}
|
|
|
|
o.CreateAt = GetMillis()
|
|
o.UpdateAt = o.CreateAt
|
|
|
|
o.Name = SanitizeUnicode(o.Name)
|
|
o.DisplayName = SanitizeUnicode(o.DisplayName)
|
|
o.Description = SanitizeUnicode(o.Description)
|
|
o.CompanyName = SanitizeUnicode(o.CompanyName)
|
|
|
|
if o.InviteId == "" {
|
|
o.InviteId = NewId()
|
|
}
|
|
}
|
|
|
|
func (o *Team) PreUpdate() {
|
|
o.UpdateAt = GetMillis()
|
|
o.Name = SanitizeUnicode(o.Name)
|
|
o.DisplayName = SanitizeUnicode(o.DisplayName)
|
|
o.Description = SanitizeUnicode(o.Description)
|
|
o.CompanyName = SanitizeUnicode(o.CompanyName)
|
|
}
|
|
|
|
func IsReservedTeamName(s string) bool {
|
|
s = strings.ToLower(s)
|
|
|
|
for _, value := range reservedName {
|
|
if strings.Index(s, value) == 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func IsValidTeamName(s string) bool {
|
|
if !isValidAlphaNum(s) {
|
|
return false
|
|
}
|
|
|
|
if len(s) < TeamNameMinLength {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
var validTeamNameCharacter = regexp.MustCompile(`^[a-z0-9-]$`)
|
|
|
|
func CleanTeamName(s string) string {
|
|
s = strings.ToLower(strings.Replace(s, " ", "-", -1))
|
|
|
|
for _, value := range reservedName {
|
|
if strings.Index(s, value) == 0 {
|
|
s = strings.Replace(s, value, "", -1)
|
|
}
|
|
}
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
for _, c := range s {
|
|
char := fmt.Sprintf("%c", c)
|
|
if !validTeamNameCharacter.MatchString(char) {
|
|
s = strings.Replace(s, char, "", -1)
|
|
}
|
|
}
|
|
|
|
s = strings.Trim(s, "-")
|
|
|
|
if !IsValidTeamName(s) {
|
|
s = NewId()
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (o *Team) Sanitize() {
|
|
o.Email = ""
|
|
o.InviteId = ""
|
|
}
|
|
|
|
func (o *Team) Patch(patch *TeamPatch) {
|
|
if patch.DisplayName != nil {
|
|
o.DisplayName = *patch.DisplayName
|
|
}
|
|
|
|
if patch.Description != nil {
|
|
o.Description = *patch.Description
|
|
}
|
|
|
|
if patch.CompanyName != nil {
|
|
o.CompanyName = *patch.CompanyName
|
|
}
|
|
|
|
if patch.AllowedDomains != nil {
|
|
o.AllowedDomains = *patch.AllowedDomains
|
|
}
|
|
|
|
if patch.AllowOpenInvite != nil {
|
|
o.AllowOpenInvite = *patch.AllowOpenInvite
|
|
}
|
|
|
|
if patch.GroupConstrained != nil {
|
|
o.GroupConstrained = patch.GroupConstrained
|
|
}
|
|
|
|
if patch.CloudLimitsArchived != nil {
|
|
o.CloudLimitsArchived = *patch.CloudLimitsArchived
|
|
}
|
|
}
|
|
|
|
func (o *Team) IsGroupConstrained() bool {
|
|
return o.GroupConstrained != nil && *o.GroupConstrained
|
|
}
|
|
|
|
// ShallowCopy returns a shallow copy of team.
|
|
func (o *Team) ShallowCopy() *Team {
|
|
c := *o
|
|
return &c
|
|
}
|