add "only platform admin can create groups" and "only groups can create events" restrictions

This commit is contained in:
setop 2021-10-06 18:00:50 +02:00 committed by Thomas Citharel
parent 7885151220
commit 7940d69d5a
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
13 changed files with 135 additions and 34 deletions

View file

@ -40,9 +40,11 @@ config :mobilizon, :instance,
email_reply_to: "noreply@localhost" email_reply_to: "noreply@localhost"
config :mobilizon, :groups, enabled: true config :mobilizon, :groups, enabled: true
config :mobilizon, :events, creation: true config :mobilizon, :events, creation: true
config :mobilizon, :restrictions, only_admin_can_create_groups: false
config :mobilizon, :restrictions, only_groups_can_create_events: false
# Configures the endpoint # Configures the endpoint
config :mobilizon, Mobilizon.Web.Endpoint, config :mobilizon, Mobilizon.Web.Endpoint,
url: [ url: [

View file

@ -44,6 +44,7 @@
" "
> >
<b-button <b-button
v-if="!hideCreateEventsButton"
tag="router-link" tag="router-link"
:to="{ name: RouteName.CREATE_EVENT }" :to="{ name: RouteName.CREATE_EVENT }"
type="is-primary" type="is-primary"
@ -313,6 +314,10 @@ export default class NavBar extends Vue {
}); });
return changeIdentity(this.$apollo.provider.defaultClient, identity); return changeIdentity(this.$apollo.provider.defaultClient, identity);
} }
get hideCreateEventsButton(): boolean {
return !!this.config?.restrictions?.onlyGroupsCanCreateEvents;
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -69,6 +69,10 @@ export const CONFIG = gql`
eventCreation eventCreation
koenaConnect koenaConnect
} }
restrictions {
onlyAdminCanCreateGroups
onlyGroupsCanCreateEvents
}
auth { auth {
ldap ldap
oauthProviders { oauthProviders {

View file

@ -84,6 +84,10 @@ export interface IConfig {
groups: boolean; groups: boolean;
koenaConnect: boolean; koenaConnect: boolean;
}; };
restrictions: {
onlyAdminCanCreateGroups: boolean;
onlyGroupsCanCreateEvents: boolean;
};
federating: boolean; federating: boolean;
version: string; version: string;
auth: { auth: {

View file

@ -14,6 +14,13 @@
</li> </li>
</ul> </ul>
</nav> </nav>
<div class="buttons" v-if="showCreateGroupsButton">
<router-link
class="button is-primary"
:to="{ name: RouteName.CREATE_GROUP }"
>{{ $t("Create group") }}</router-link
>
</div>
<div v-if="groups"> <div v-if="groups">
<b-switch v-model="local">{{ $t("Local") }}</b-switch> <b-switch v-model="local">{{ $t("Local") }}</b-switch>
<b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch> <b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch>
@ -100,6 +107,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model";
import { LIST_GROUPS } from "@/graphql/group"; import { LIST_GROUPS } from "@/graphql/group";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import EmptyContent from "../../components/Utils/EmptyContent.vue"; import EmptyContent from "../../components/Utils/EmptyContent.vue";
@ -110,6 +119,7 @@ const PROFILES_PER_PAGE = 10;
@Component({ @Component({
apollo: { apollo: {
config: CONFIG,
groups: { groups: {
query: LIST_GROUPS, query: LIST_GROUPS,
variables() { variables() {
@ -139,6 +149,7 @@ export default class GroupProfiles extends Vue {
PROFILES_PER_PAGE = PROFILES_PER_PAGE; PROFILES_PER_PAGE = PROFILES_PER_PAGE;
config!: IConfig;
RouteName = RouteName; RouteName = RouteName;
async onPageChange(): Promise<void> { async onPageChange(): Promise<void> {
@ -185,6 +196,10 @@ export default class GroupProfiles extends Vue {
this.pushRouter({ suspended: suspended ? "1" : "0" }); this.pushRouter({ suspended: suspended ? "1" : "0" });
} }
get showCreateGroupsButton(): boolean {
return !!this.config?.restrictions?.onlyAdminCanCreateGroups;
}
onFiltersChange({ onFiltersChange({
preferredUsername, preferredUsername,
domain, domain,

View file

@ -10,7 +10,7 @@
) )
}} }}
</p> </p>
<div class="buttons"> <div class="buttons" v-if="!hideCreateEventButton">
<router-link <router-link
class="button is-primary" class="button is-primary"
:to="{ name: RouteName.CREATE_EVENT }" :to="{ name: RouteName.CREATE_EVENT }"
@ -126,6 +126,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { ParticipantRole } from "@/types/enums"; import { ParticipantRole } from "@/types/enums";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
@ -147,6 +149,7 @@ import Subtitle from "../../components/Utils/Subtitle.vue";
EventListCard, EventListCard,
}, },
apollo: { apollo: {
config: CONFIG,
futureParticipations: { futureParticipations: {
query: LOGGED_USER_PARTICIPATIONS, query: LOGGED_USER_PARTICIPATIONS,
fetchPolicy: "cache-and-network", fetchPolicy: "cache-and-network",
@ -197,6 +200,8 @@ export default class MyEvents extends Vue {
limit = 10; limit = 10;
config!: IConfig;
futureParticipations: IParticipant[] = []; futureParticipations: IParticipant[] = [];
hasMoreFutureParticipations = true; hasMoreFutureParticipations = true;
@ -286,6 +291,10 @@ export default class MyEvents extends Vue {
(participation) => participation.event.id !== eventid (participation) => participation.event.id !== eventid
); );
} }
get hideCreateEventButton(): boolean {
return !!this.config?.restrictions?.onlyGroupsCanCreateEvents;
}
} }
</script> </script>

View file

@ -8,7 +8,7 @@
) )
}} }}
</p> </p>
<div class="buttons"> <div class="buttons" v-if="!hideCreateGroupButton">
<router-link <router-link
class="button is-primary" class="button is-primary"
:to="{ name: RouteName.CREATE_GROUP }" :to="{ name: RouteName.CREATE_GROUP }"
@ -72,6 +72,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor"; import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import { LEAVE_GROUP } from "@/graphql/group"; import { LEAVE_GROUP } from "@/graphql/group";
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue"; import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
@ -90,6 +92,9 @@ import RouteName from "../../router/name";
Invitations, Invitations,
}, },
apollo: { apollo: {
config: {
query: CONFIG,
},
membershipsPages: { membershipsPages: {
query: LOGGED_USER_MEMBERSHIPS, query: LOGGED_USER_MEMBERSHIPS,
fetchPolicy: "cache-and-network", fetchPolicy: "cache-and-network",
@ -114,6 +119,8 @@ export default class MyGroups extends Vue {
RouteName = RouteName; RouteName = RouteName;
config!: IConfig;
page = 1; page = 1;
limit = 10; limit = 10;
@ -177,6 +184,10 @@ export default class MyGroups extends Vue {
![MemberRole.INVITED, MemberRole.REJECTED].includes(member.role) ![MemberRole.INVITED, MemberRole.REJECTED].includes(member.role)
); );
} }
get hideCreateGroupButton(): boolean {
return !!this.config?.restrictions?.onlyAdminCanCreateGroups;
}
} }
</script> </script>

View file

@ -56,6 +56,11 @@ export const configMock = {
groups: true, groups: true,
koenaConnect: false, koenaConnect: false,
}, },
restrictions: {
__typename: "Restrictions",
onlyAdminCanCreateGroups: false,
onlyGroupsCanCreateEvents: false,
},
geocoding: { geocoding: {
__typename: "Geocoding", __typename: "Geocoding",
autocomplete: true, autocomplete: true,

View file

@ -134,6 +134,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
event_creation: Config.instance_event_creation_enabled?(), event_creation: Config.instance_event_creation_enabled?(),
koena_connect: Config.get([:instance, :koena_connect_link], false) koena_connect: Config.get([:instance, :koena_connect_link], false)
}, },
restrictions: %{
only_admin_can_create_groups: Config.only_admin_can_create_groups?(),
only_groups_can_create_events: Config.only_groups_can_create_events?()
},
rules: Config.instance_rules(), rules: Config.instance_rules(),
version: Config.instance_version(), version: Config.instance_version(),
federating: Config.instance_federating(), federating: Config.instance_federating(),

View file

@ -265,29 +265,33 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
%{context: %{current_user: user}} = _resolution %{context: %{current_user: user}} = _resolution
) do ) do
# See https://github.com/absinthe-graphql/absinthe/issues/490 # See https://github.com/absinthe-graphql/absinthe/issues/490
with {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id), if Config.only_groups_can_create_events?() and Map.get(args, :attributed_to_id) == nil do
args <- Map.put(args, :options, args[:options] || %{}), {:error, "only groups can create events"}
{:group_check, true} <- {:group_check, is_organizer_group_member?(args)},
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
API.Events.create_event(args_with_organizer) do
{:ok, event}
else else
{:group_check, false} -> with {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
{:error, args <- Map.put(args, :options, args[:options] || %{}),
dgettext( {:group_check, true} <- {:group_check, is_organizer_group_member?(args)},
"errors", args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
"Organizer profile doesn't have permission to create an event on behalf of this group" {:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
)} API.Events.create_event(args_with_organizer) do
{:ok, event}
else
{:group_check, false} ->
{:error,
dgettext(
"errors",
"Organizer profile doesn't have permission to create an event on behalf of this group"
)}
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, dgettext("errors", "Organizer profile is not owned by the user")} {:error, dgettext("errors", "Organizer profile is not owned by the user")}
{:error, _, %Ecto.Changeset{} = error, _} -> {:error, _, %Ecto.Changeset{} = error, _} ->
{:error, error} {:error, error}
{:error, %Ecto.Changeset{} = error} -> {:error, %Ecto.Changeset{} = error} ->
{:error, error} {:error, error}
end
end end
end end

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.Config
alias Mobilizon.{Actors, Events} alias Mobilizon.{Actors, Events}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Federation.ActivityPub.Actions
@ -137,23 +138,29 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
args, args,
%{ %{
context: %{ context: %{
current_actor: %Actor{id: creator_actor_id} = creator_actor current_actor: %Actor{id: creator_actor_id} = creator_actor,
current_user: %User{role: role} = _resolution
} }
} }
) do ) do
with args when is_map(args) <- Map.update(args, :preferred_username, "", &String.downcase/1), if Config.only_admin_can_create_groups?() and not is_admin(role) do
args when is_map(args) <- Map.put(args, :creator_actor, creator_actor), {:error, "only admins can create groups"}
args when is_map(args) <- Map.put(args, :creator_actor_id, creator_actor_id),
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.create_group(args) do
{:ok, group}
else else
{:picture, {:error, :file_too_large}} -> with args when is_map(args) <-
{:error, dgettext("errors", "The provided picture is too heavy")} Map.update(args, :preferred_username, "", &String.downcase/1),
args when is_map(args) <- Map.put(args, :creator_actor, creator_actor),
args when is_map(args) <- Map.put(args, :creator_actor_id, creator_actor_id),
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.create_group(args) do
{:ok, group}
else
{:picture, {:error, :file_too_large}} ->
{:error, dgettext("errors", "The provided picture is too heavy")}
{:error, err} when is_binary(err) -> {:error, err} when is_binary(err) ->
{:error, err} {:error, err}
end
end end
end end

View file

@ -37,6 +37,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:timezones, list_of(:string), description: "The instance's available timezones") field(:timezones, list_of(:string), description: "The instance's available timezones")
field(:features, :features, description: "The instance's features") field(:features, :features, description: "The instance's features")
field(:restrictions, :restrictions, description: "The instance's restrictions")
field(:version, :string, description: "The instance's version") field(:version, :string, description: "The instance's version")
field(:federating, :boolean, description: "Whether this instance is federation") field(:federating, :boolean, description: "Whether this instance is federation")
@ -275,6 +276,19 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:koena_connect, :boolean, description: "Activate link to Koena Connect") field(:koena_connect, :boolean, description: "Activate link to Koena Connect")
end end
@desc """
The instance's restrictions
"""
object :restrictions do
field(:only_admin_can_create_groups, :boolean,
description: "Whether groups creation is allowed only for admin, not for all users"
)
field(:only_groups_can_create_events, :boolean,
description: "Whether events creation is allowed only for groups, not for persons"
)
end
@desc """ @desc """
The instance's auth configuration The instance's auth configuration
""" """

View file

@ -288,6 +288,9 @@ defmodule Mobilizon.Config do
end end
end end
# config :mobilizon, :groups, enabled: true
# config :mobilizon, :events, creation: true
@spec instance_group_feature_enabled? :: boolean @spec instance_group_feature_enabled? :: boolean
def instance_group_feature_enabled?, def instance_group_feature_enabled?,
do: :mobilizon |> Application.get_env(:groups) |> Keyword.get(:enabled) do: :mobilizon |> Application.get_env(:groups) |> Keyword.get(:enabled)
@ -303,6 +306,20 @@ defmodule Mobilizon.Config do
} }
end end
@spec only_admin_can_create_groups? :: boolean
def only_admin_can_create_groups?,
do:
:mobilizon
|> Application.get_env(:restrictions)
|> Keyword.get(:only_admin_can_create_groups)
@spec only_groups_can_create_events? :: boolean
def only_groups_can_create_events?,
do:
:mobilizon
|> Application.get_env(:restrictions)
|> Keyword.get(:only_groups_can_create_events)
@spec anonymous_actor_id :: integer @spec anonymous_actor_id :: integer
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id) def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
@spec relay_actor_id :: integer @spec relay_actor_id :: integer