Add Nitrate advanced permissions delete (#9416)

*  Add Nitrate advanced permissions delete

* 📎 Code review
This commit is contained in:
María Valderrama 2026-05-07 21:14:30 +02:00 committed by GitHub
parent d84685c0cb
commit 7c5fa038c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 241 additions and 75 deletions

View File

@ -360,7 +360,7 @@
[:map [:map
[:organization-id ::sm/uuid] [:organization-id ::sm/uuid]
[:owner-id ::sm/uuid] [:owner-id ::sm/uuid]
[:create-teams [:enum "any" "onlyMe"]]] [:permissions [:map-of :keyword :string]]]
params))) params)))
(defn- redeem-activation-code-api (defn- redeem-activation-code-api

View File

@ -12,6 +12,7 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.nitrate :as nitrate] [app.nitrate :as nitrate]
@ -313,12 +314,12 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :not-allowed :code :not-allowed
:hint "Unable to verify organization permissions") :hint "Unable to verify organization permissions")
(let [create-perm (:create-teams org-perms) (when-not (nitrate-perms/allowed? :create-team
is-owner? (= profile-id (:owner-id org-perms))] {:org-perms org-perms
(when (and (= create-perm "onlyMe") (not is-owner?)) :profile-id profile-id})
(ex/raise :type :validation (ex/raise :type :validation
:code :not-allowed :code :not-allowed
:hint "You are not allowed to add teams in this organization")))))) :hint "You are not allowed to add teams in this organization")))))
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})] (let [team-members (db/query cfg :team-profile-rel {:team-id team-id})]
;; Add teammates to the org if needed ;; Add teammates to the org if needed

View File

@ -12,6 +12,7 @@
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.team :as types.team] [app.common.types.team :as types.team]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
@ -520,12 +521,12 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :not-allowed :code :not-allowed
:hint "Unable to verify organization permissions") :hint "Unable to verify organization permissions")
(let [create-perm (:create-teams org-perms) (when-not (nitrate-perms/allowed? :create-team
is-owner? (= profile-id (:owner-id org-perms))] {:org-perms org-perms
(when (and (= create-perm "onlyMe") (not is-owner?)) :profile-id profile-id})
(ex/raise :type :validation (ex/raise :type :validation
:code :not-allowed :code :not-allowed
:hint "You are not allowed to create teams in this organization")))))) :hint "You are not allowed to create teams in this organization")))))
(let [features (-> (cfeat/get-enabled-features cf/flags) (let [features (-> (cfeat/get-enabled-features cf/flags)
(set/difference cfeat/frontend-only-features) (set/difference cfeat/frontend-only-features)
@ -773,20 +774,35 @@
(defn delete-team (defn delete-team
"Mark a team for deletion" "Mark a team for deletion"
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id]}] [{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
(let [team (get-team conn :profile-id profile-id :team-id team-id) (let [team (get-team conn :profile-id profile-id :team-id team-id)
team (if (contains? cf/flags :nitrate)
(nitrate/add-org-info-to-team cfg team params)
team)
perms (get team :permissions)] perms (get team :permissions)]
(when-not (:is-owner perms)
(ex/raise :type :validation
:code :only-owner-can-delete-team))
(when (:is-default team) (when (:is-default team)
(ex/raise :type :validation (ex/raise :type :validation
:code :non-deletable-team :code :non-deletable-team
:hint "impossible to delete default team")) :hint "impossible to delete default team"))
;; Check delete permissions based on organization settings.
;; For non-org teams or when nitrate is disabled, only owners can delete.
(if (and (:organization-id team) (contains? cf/flags :nitrate))
(let [org-perms {:owner-id (:organization-owner-id team)
:permissions (:organization-permissions team)}]
(when-not (nitrate-perms/allowed? :delete-team
{:org-perms org-perms
:profile-id profile-id
:team-perms perms})
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to delete teams in this organization")))
(when-not (:is-owner perms)
(ex/raise :type :validation
:code :only-owner-can-delete-team)))
(let [delay (ldel/get-deletion-delay team) (let [delay (ldel/get-deletion-delay team)
team (db/update! conn :team team (db/update! conn :team
{:deleted-at (ct/in-future delay)} {:deleted-at (ct/in-future delay)}

View File

@ -0,0 +1,41 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.nitrate-permissions)
(def ^:private defaults
{:create-teams "any"
:delete-teams "ownersAndAdmins"})
(def ^:private action-rules
{:create-team {:permission-key :create-teams
:allowed-values #{"any"}
:requires-admin? false}
:delete-team {:permission-key :delete-teams
:allowed-values #{"ownersAndAdmins"}
:requires-admin? true}})
(defn- normalize-org-permissions
[org-perms]
(merge defaults (or (:permissions org-perms) {})))
(defn- owner?
[org-perms profile-id]
(= profile-id (:owner-id org-perms)))
(defn allowed?
"Returns true only for explicitly allowed actions (fail-closed)."
[action {:keys [org-perms profile-id team-perms]}]
(let [{:keys [permission-key allowed-values requires-admin?] :as rule}
(get action-rules action)
permissions (normalize-org-permissions org-perms)
is-owner? (owner? org-perms profile-id)
is-admin? (boolean (:is-admin team-perms))]
(cond
(nil? rule) false
is-owner? true
(and requires-admin? (not is-admin?)) false
:else (contains? allowed-values (get permissions permission-key)))))

View File

@ -16,7 +16,10 @@
[:owner-id ::sm/uuid] [:owner-id ::sm/uuid]
[:avatar-bg-url ::sm/uri] [:avatar-bg-url ::sm/uri]
[:logo-id {:optional true} [:maybe ::sm/uuid]] [:logo-id {:optional true} [:maybe ::sm/uuid]]
[:create-teams {:optional true} [:maybe [:enum "any" "onlyMe"]]]]) [:permissions {:optional true}
[:maybe [:map
[:create-teams {:optional true} [:maybe [:enum "any" "onlyMe"]]]
[:delete-teams {:optional true} [:maybe [:enum "ownersAndAdmins" "onlyOwners"]]]]]]])
(def schema:team-with-organization (def schema:team-with-organization
@ -33,7 +36,7 @@
[:slug :organization-slug] [:slug :organization-slug]
[:avatar-bg-url :organization-avatar-bg-url] [:avatar-bg-url :organization-avatar-bg-url]
[:owner-id :organization-owner-id] [:owner-id :organization-owner-id]
[:create-teams :organization-create-teams]]) [:permissions :organization-permissions]])
(defn apply-organization (defn apply-organization
"Updates a team map with organization fields sourced from org. "Updates a team map with organization fields sourced from org.

View File

@ -67,6 +67,7 @@
[common-tests.types.container-test] [common-tests.types.container-test]
[common-tests.types.fill-test] [common-tests.types.fill-test]
[common-tests.types.modifiers-test] [common-tests.types.modifiers-test]
[common-tests.types.nitrate-permissions-test]
[common-tests.types.objects-map-test] [common-tests.types.objects-map-test]
[common-tests.types.path-data-test] [common-tests.types.path-data-test]
[common-tests.types.shape-decode-encode-test] [common-tests.types.shape-decode-encode-test]
@ -147,6 +148,7 @@
'common-tests.types.container-test 'common-tests.types.container-test
'common-tests.types.fill-test 'common-tests.types.fill-test
'common-tests.types.modifiers-test 'common-tests.types.modifiers-test
'common-tests.types.nitrate-permissions-test
'common-tests.types.objects-map-test 'common-tests.types.objects-map-test
'common-tests.types.path-data-test 'common-tests.types.path-data-test
'common-tests.types.shape-decode-encode-test 'common-tests.types.shape-decode-encode-test

View File

@ -0,0 +1,57 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.types.nitrate-permissions-test
(:require
[app.common.types.nitrate-permissions :as nitrate-perms]
[clojure.test :as t]))
(def org-perms
{:owner-id :owner
:permissions {:create-teams "any"
:delete-teams "ownersAndAdmins"}})
(t/deftest unknown-action-is-denied
(t/is (false? (nitrate-perms/allowed? :unknown
{:org-perms org-perms
:profile-id :member
:team-perms {:is-admin true}}))))
(t/deftest owner-is-always-allowed
(t/is (true? (nitrate-perms/allowed? :create-team
{:org-perms org-perms
:profile-id :owner
:team-perms {:is-admin false}})))
(t/is (true? (nitrate-perms/allowed? :delete-team
{:org-perms org-perms
:profile-id :owner
:team-perms {:is-admin false}}))))
(t/deftest create-team-permission-rules
(t/is (true? (nitrate-perms/allowed? :create-team
{:org-perms org-perms
:profile-id :member
:team-perms {:is-admin false}})))
(t/is (false? (nitrate-perms/allowed? :create-team
{:org-perms (assoc org-perms :permissions {:create-teams "none"
:delete-teams "ownersAndAdmins"})
:profile-id :member
:team-perms {:is-admin false}}))))
(t/deftest delete-team-requires-admin-and-policy
(t/is (false? (nitrate-perms/allowed? :delete-team
{:org-perms org-perms
:profile-id :member
:team-perms {:is-admin false}})))
(t/is (true? (nitrate-perms/allowed? :delete-team
{:org-perms org-perms
:profile-id :member
:team-perms {:is-admin true}})))
(t/is (false? (nitrate-perms/allowed? :delete-team
{:org-perms (assoc org-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"})
:profile-id :member
:team-perms {:is-admin true}}))))

View File

@ -189,7 +189,7 @@
(let [all-orgs (map dt/team->organization (let [all-orgs (map dt/team->organization
(filter #(and (:is-default %) (:organization-id %)) teams)) (filter #(and (:is-default %) (:organization-id %)) teams))
orgs (filter (fn [org] orgs (filter (fn [org]
(let [perm (:create-teams org) (let [perm (get-in org [:permissions :create-teams])
is-own? (= profile-id (:owner-id org))] is-own? (= profile-id (:owner-id org))]
(or (= perm "any") is-own?))) all-orgs) (or (= perm "any") is-own?))) all-orgs)
team (first (filter #(= (:id %) team-id) teams)) team (first (filter #(= (:id %) team-id) teams))
@ -198,7 +198,7 @@
:organization-id organization-id})))] :organization-id organization-id})))]
(rx/of (dt/teams-fetched teams) (rx/of (dt/teams-fetched teams)
(if (empty? orgs) (if (empty? orgs)
(modal/show :no-org-allows-create-team {}) (modal/show :no-permission-modal {:type :no-orgs-create})
(let [has-filtered? (< (count orgs) (count all-orgs)) (let [has-filtered? (< (count orgs) (count all-orgs))
extra-props (when has-filtered? extra-props (when has-filtered?
{:info-message-key "dashboard.select-org-modal.permission-info"})] {:info-message-key "dashboard.select-org-modal.permission-info"})]
@ -228,7 +228,7 @@
(let [all-orgs (map dt/team->organization (let [all-orgs (map dt/team->organization
(filter #(and (:is-default %) (:organization-id %)) teams)) (filter #(and (:is-default %) (:organization-id %)) teams))
orgs (filter (fn [org] orgs (filter (fn [org]
(let [perm (:create-teams org) (let [perm (get-in org [:permissions :create-teams])
is-own? (= profile-id (:owner-id org))] is-own? (= profile-id (:owner-id org))]
(or (= perm "any") is-own?))) all-orgs) (or (= perm "any") is-own?))) all-orgs)
team (first (filter #(= (:id %) team-id) teams)) team (first (filter #(= (:id %) team-id) teams))
@ -237,7 +237,7 @@
:organization-id organization-id})))] :organization-id organization-id})))]
(rx/of (dt/teams-fetched teams) (rx/of (dt/teams-fetched teams)
(if (empty? orgs) (if (empty? orgs)
(modal/show :no-org-allows-create-team {}) (modal/show :no-permission-modal {:type :no-orgs-change})
(let [has-filtered? (< (count orgs) (count all-orgs)) (let [has-filtered? (< (count orgs) (count all-orgs))
extra-props (when has-filtered? extra-props (when has-filtered?
{:info-message-key "dashboard.select-org-modal.permission-info"})] {:info-message-key "dashboard.select-org-modal.permission-info"})]

View File

@ -11,6 +11,7 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.team :as ctt] [app.common.types.team :as ctt]
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
@ -22,6 +23,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.util.clipboard :as clipboard] [app.util.clipboard :as clipboard]
[app.util.i18n :refer [tr]]
[app.util.storage :as storage] [app.util.storage :as storage]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[clojure.string :as str] [clojure.string :as str]
@ -52,7 +54,7 @@
:organization-slug :organization-slug
:organization-owner-id :organization-owner-id
:organization-avatar-bg-url :organization-avatar-bg-url
:organization-create-teams))] :organization-permissions))]
(update state :teams assoc id team-updated))) (update state :teams assoc id team-updated)))
state state
teams))))) teams)))))
@ -78,16 +80,55 @@
(fn [teams] (fn [teams]
(let [team (d/seek #(= (:id %) team-id) teams) (let [team (d/seek #(= (:id %) team-id) teams)
in-org? (and (contains? cf/flags :nitrate) (:organization-id team)) in-org? (and (contains? cf/flags :nitrate) (:organization-id team))
create-perm (:organization-create-teams team) can-create? (if in-org?
is-owner? (= profile-id (:organization-owner-id team)) (nitrate-perms/allowed? :create-team
can-create? (or (not in-org?) (= create-perm "any") is-owner?)] {:org-perms {:owner-id (:organization-owner-id team)
:permissions (:organization-permissions team)}
:profile-id profile-id
:team-perms (:permissions team)})
true)]
(rx/of (teams-fetched teams) (rx/of (teams-fetched teams)
(if can-create? (if can-create?
(modal/show :team-form (if in-org? (modal/show :team-form (if in-org?
{:organization-id (:organization-id team) {:organization-id (:organization-id team)
:organization-name (:organization-name team)} :organization-name (:organization-name team)}
{})) {}))
(modal/show :no-permission-create-team {:organization-name (:organization-name team)}))))))))))) (modal/show :no-permission-modal {:type :create-team
:organization-name (:organization-name team)})))))))))))
(defn check-and-delete-team
"Fetches fresh team data from the server to ensure up-to-date org
permissions, then shows the confirmation modal or a no-permission modal."
[{:keys [team-id delete-fn]}]
(ptk/reify ::check-and-delete-team
ptk/WatchEvent
(watch [_ state _]
(let [profile-id (dm/get-in state [:profile :id])]
(->> (rp/cmd! :get-teams)
(rx/mapcat
(fn [teams]
(let [team (d/seek #(= (:id %) team-id) teams)
in-org? (and (contains? cf/flags :nitrate) (:organization-id team))
can-delete? (if in-org?
(nitrate-perms/allowed? :delete-team
{:org-perms {:owner-id (:organization-owner-id team)
:permissions (:organization-permissions team)}
:profile-id profile-id
:team-perms (:permissions team)})
(boolean (dm/get-in team [:permissions :is-owner])))
message (if in-org?
(tr "modals.delete-org-team-confirm.message" (:organization-name team))
(tr "modals.delete-team-confirm.message"))]
(rx/of (teams-fetched teams)
(if can-delete?
(modal/show
{:type :confirm
:title (tr "modals.delete-team-confirm.title")
:message message
:accept-label (tr "modals.delete-team-confirm.accept")
:on-accept delete-fn})
(modal/show :no-permission-modal {:type :delete-team
:organization-name (:organization-name team)})))))))))))
;; --- EVENT: fetch-members ;; --- EVENT: fetch-members
@ -625,5 +666,5 @@
:custom-photo (:organization-custom-photo team) :custom-photo (:organization-custom-photo team)
:name (:organization-name team) :name (:organization-name team)
:default-team-id (:id team) :default-team-id (:id team)
:create-teams (:organization-create-teams team)}) :permissions (:organization-permissions team)})

View File

@ -468,6 +468,10 @@
:owner-cant-leave-team :owner-cant-leave-team
(rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave"))) (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave")))
:not-allowed
(rx/of (modal/show :no-permission-modal {:type :delete-team
:organization-name (:organization-name team)}))
(rx/throw error)))) (rx/throw error))))
leave-fn leave-fn
@ -526,17 +530,9 @@
(mf/use-fn (mf/use-fn
(mf/deps team delete-fn) (mf/deps team delete-fn)
(fn [] (fn []
(let [is-org-team? (some? (:organization-id team)) ;; Fetch fresh team data to check current permission, then show appropriate modal
message (if is-org-team? (st/emit! (dtm/check-and-delete-team {:team-id (:id team)
(tr "modals.delete-org-team-confirm.message" (:organization-name team)) :delete-fn delete-fn}))))]
(tr "modals.delete-team-confirm.message"))]
(st/emit!
(modal/show
{:type :confirm
:title (tr "modals.delete-team-confirm.title")
:message message
:accept-label (tr "modals.delete-team-confirm.accept")
:on-accept delete-fn})))))]
[:> dropdown-menu* props [:> dropdown-menu* props
[:> dropdown-menu-item* {:on-click go-members [:> dropdown-menu-item* {:on-click go-members
@ -583,11 +579,19 @@
:class (stl/css :team-options-item)} :class (stl/css :team-options-item)}
(tr "dashboard.leave-team")]) (tr "dashboard.leave-team")])
(when (get-in team [:permissions :is-owner]) (let [is-owner? (get-in team [:permissions :is-owner])
is-admin? (get-in team [:permissions :is-admin])
is-org-team? (some? (:organization-id team))
in-org? (and (contains? cf/flags :nitrate) is-org-team?)
show-delete? (if in-org?
(or is-owner? is-admin?)
is-owner?)]
(when show-delete?
[:> dropdown-menu-item* {:on-click on-delete-clicked [:> dropdown-menu-item* {:on-click on-delete-clicked
:class (stl/css :team-options-item :warning) :class (stl/css :team-options-item :warning)
:data-testid "delete-team"} :data-testid "delete-team"}
(tr "dashboard.delete-team")])])) (tr "dashboard.delete-team")]))]))
(mf/defc org-options-dropdown* (mf/defc org-options-dropdown*
{::mf/private true} {::mf/private true}

View File

@ -1381,7 +1381,7 @@
organizations (mf/with-memo [all-organizations profile-id] organizations (mf/with-memo [all-organizations profile-id]
(->> all-organizations (->> all-organizations
(filter (fn [org] (filter (fn [org]
(let [perm (:create-teams org) (let [perm (get-in org [:permissions :create-teams])
is-owner? (= profile-id (:owner-id org))] is-owner? (= profile-id (:owner-id org))]
(or (= perm "any") is-owner?)))))) (or (= perm "any") is-owner?))))))

View File

@ -46,7 +46,8 @@
(let [id (get-in @form [:clean-data :id]) (let [id (get-in @form [:clean-data :id])
code (-> response ex-data :code)] code (-> response ex-data :code)]
(if (= code :not-allowed) (if (= code :not-allowed)
(rx/of (modal/show :no-permission-create-team {:organization-name organization-name})) (rx/of (modal/show :no-permission-modal {:type :create-team
:organization-name organization-name}))
(if id (if id
(rx/of (ntf/error "Error on updating team.")) (rx/of (ntf/error "Error on updating team."))
(rx/of (ntf/error "Error on creating team.")))))) (rx/of (ntf/error "Error on creating team."))))))
@ -139,31 +140,25 @@
:class (stl/css :accept-btn)}]]]]]])) :class (stl/css :accept-btn)}]]]]]]))
(mf/defc no-permission-create-team-modal* (mf/defc no-permission-modal*
"Generic modal for displaying permission-related messages based on error type"
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :no-permission-create-team} ::mf/register-as :no-permission-modal}
[{:keys [organization-name]}] [{:keys [type organization-name]}]
(let [[title message] (case type
:create-team [(tr "labels.create-team")
(tr "dashboard.no-permission-create-team.message" organization-name)]
:delete-team [(tr "dashboard.delete-team")
(tr "dashboard.no-permission-delete-team.message" organization-name)]
:no-orgs-create [(tr "dashboard.select-org-modal.title")
(tr "dashboard.no-org-allows-create-team.message")]
:no-orgs-change [(tr "dashboard.change-org-modal.title")
(tr "dashboard.no-org-allows-create-team.message")])]
[:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)} [:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} [:h2 {:class (stl/css :modal-title)} title]
(tr "labels.create-team")]
[:button {:class (stl/css :modal-close-btn) [:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]] :on-click modal/hide!} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)} [:div {:class (stl/css :modal-content)}
[:div (tr "dashboard.no-permission-create-team.message" organization-name)]]]]) [:div message]]]]))
(mf/defc no-org-allows-create-team-modal*
{::mf/register modal/components
::mf/register-as :no-org-allows-create-team}
[_props]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)}
(tr "dashboard.select-org-modal.title")]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
[:div (tr "dashboard.no-org-allows-create-team.message")]]]])

View File

@ -9423,3 +9423,6 @@ msgstr "You don't have permission to add teams to any of your organizations."
msgid "dashboard.select-org-modal.permission-info" msgid "dashboard.select-org-modal.permission-info"
msgstr "Here you find all your organizations where you are allowed to create or add teams." msgstr "Here you find all your organizations where you are allowed to create or add teams."
msgid "dashboard.no-permission-delete-team.message"
msgstr "You are not allowed to delete teams that are part of %s organization. If you need more information, contact the owner."

View File

@ -9128,3 +9128,6 @@ msgstr "No tienes permiso para añadir equipos a ninguna de tus organizaciones."
msgid "dashboard.select-org-modal.permission-info" msgid "dashboard.select-org-modal.permission-info"
msgstr "Aquí encontrarás todas las organizaciones en las que tienes permiso para crear o añadir equipos." msgstr "Aquí encontrarás todas las organizaciones en las que tienes permiso para crear o añadir equipos."
msgid "dashboard.no-permission-delete-team.message"
msgstr "No tienes permiso para eliminar equipos que pertenecen a la organización %s. Si necesitas más información, contacta con el propietario."