Introduce authorizations with Rajska

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2023-03-17 18:10:59 +01:00
parent b6875f6a4b
commit 8984bd7636
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
95 changed files with 4560 additions and 1505 deletions

View File

@ -78,7 +78,7 @@ config :tesla, Mobilizon.Service.HTTP.HostMetaClient,
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
config :mobilizon, Oban, queues: false, plugins: false
config :mobilizon, Oban, testing: :manual
config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret"

View File

@ -45,6 +45,7 @@
"@tiptap/extension-strike": "^2.0.0-beta.26",
"@tiptap/extension-text": "^2.0.0-beta.15",
"@tiptap/extension-underline": "^2.0.0-beta.7",
"@tiptap/pm": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.195",
"@tiptap/vue-3": "^2.0.0-beta.96",
"@vue-a11y/announcer": "^2.1.0",
@ -59,7 +60,7 @@
"autoprefixer": "^10",
"blurhash": "^2.0.0",
"date-fns": "^2.16.0",
"date-fns-tz": "^1.1.6",
"date-fns-tz": "^2.0.0",
"floating-vue": "^2.0.0-beta.17",
"graphql": "^15.8.0",
"graphql-tag": "^2.10.3",
@ -74,16 +75,6 @@
"p-debounce": "^4.0.0",
"phoenix": "^1.6",
"postcss": "^8",
"prosemirror-commands": "^1.5.0",
"prosemirror-dropcursor": "^1.6.1",
"prosemirror-gapcursor": "^1.3.1",
"prosemirror-history": "^1.3.0",
"prosemirror-keymap": "^1.2.0",
"prosemirror-model": "^1.19.0",
"prosemirror-schema-list": "^1.2.2",
"prosemirror-state": "^1.4.2",
"prosemirror-transform": "^1.7.1",
"prosemirror-view": "^1.30.0",
"register-service-worker": "^1.7.2",
"sanitize-html": "^2.5.3",
"tailwindcss": "^3",
@ -100,7 +91,7 @@
"zhyswan-vuedraggable": "^4.1.3"
},
"devDependencies": {
"@histoire/plugin-vue": "^0.12.4",
"@histoire/plugin-vue": "^0.15.8",
"@playwright/test": "^1.25.1",
"@rushstack/eslint-patch": "^1.1.4",
"@tailwindcss/forms": "^0.5.2",
@ -114,8 +105,8 @@
"@types/phoenix": "^1.5.2",
"@types/sanitize-html": "^2.5.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vitest/coverage-c8": "^0.28.2",
"@vitest/ui": "^0.28.2",
"@vitest/coverage-c8": "^0.29.2",
"@vitest/ui": "^0.29.2",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.0.2",
@ -125,7 +116,7 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.3.0",
"flush-promises": "^1.0.2",
"histoire": "^0.12.4",
"histoire": "^0.15.8",
"jsdom": "^21.1.0",
"mock-apollo-client": "^1.1.0",
"prettier": "^2.2.1",
@ -135,7 +126,7 @@
"typescript": "~4.9.4",
"vite": "^4.0.4",
"vite-plugin-pwa": "^0.14.1",
"vitest": "^0.28.2",
"vitest": "^0.29.2",
"vue-i18n-extract": "^2.0.4"
}
}

View File

@ -65,18 +65,15 @@
</o-field>
<o-modal
has-modal-card
v-model="showNewElementModal"
v-model:active="showNewElementModal"
:close-button-aria-label="$t('Close')"
>
<div class="modal-card">
<header class="modal-card-head">
<button
type="button"
class="delete"
@click="showNewElementModal = false"
/>
<div class="">
<header class="">
<h2>{{ t('Create a new metadata element') }}</h2>
<p>{{ t('You can put any arbitrary content in this element. URLs will be clickable.') }}</p>
</header>
<div class="modal-card-body">
<div class="">
<form @submit="addNewElement">
<o-field :label="$t('Element title')">
<o-input v-model="newElement.title" />
@ -84,7 +81,7 @@
<o-field :label="$t('Element value')">
<o-input v-model="newElement.value" />
</o-field>
<o-button variant="primary" native-type="submit">{{
<o-button class="mt-2" variant="primary" native-type="submit">{{
$t("Add")
}}</o-button>
</form>

View File

@ -70,7 +70,7 @@ function setupApp({ app }) {
new Promise((resolve) =>
resolve({
data: {
identities: [{ id: "9", preferredUsername: "sam", name: "Samuel" }],
loggedUser: { actors: [{ id: "9", preferredUsername: "sam", name: "Samuel" }] },
},
})
)

View File

@ -1,44 +1,94 @@
<template>
<h1 class="text-3xl">
{{ t("Autorize this application to access your account?") }}
</h1>
<div>
<h1 class="text-3xl">
{{ t("Autorize this application to access your account?") }}
</h1>
<div
class="rounded-lg bg-mbz-warning dark:text-black shadow-xl my-6 p-4 flex items-center gap-2"
>
<AlertCircle :size="42" />
<p>
{{
t(
"This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust."
)
}}
</p>
</div>
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-xl my-6">
<div class="p-4 pb-0">
<p class="text-3xl font-bold">{{ authApplication.name }}</p>
<p>{{ authApplication.website }}</p>
</div>
<div class="flex gap-3 p-4">
<o-button @click="() => authorize()">{{ t("Authorize") }}</o-button>
<o-button outlined tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Decline")
}}</o-button>
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-xl my-6">
<div class="p-4 pb-0">
<p class="text-3xl font-bold">{{ authApplication.name }}</p>
<p>{{ authApplication.website }}</p>
</div>
<p class="p-4">
{{
t(
"You'll be able to revoke access for this application in your account settings."
)
}}
</p>
<div class="">
<div
v-if="collapses.length === 0"
class="rounded-lg bg-mbz-danger shadow-xl my-6 p-4 flex items-center gap-2"
>
<AlertCircle :size="42" />
<p>
{{
t(
"This application didn't ask for known permissions. It's likely the request is incorrect."
)
}}
</p>
</div>
<p v-else class="px-4 font-bold">
{{ t('This application asks for the following permissions:') }}
</p>
<o-collapse
class="mt-3 border-b pb-2 border-zinc-700 text-black dark:text-white"
:class="{
'bg-mbz-warning dark:!text-black': collapse?.type === 'warning',
}"
animation="slide"
v-for="(collapse, index) of collapses"
:key="index"
:open="isOpen === index"
@open="isOpen = index"
>
<template #trigger="props">
<div class="flex py-1" role="button">
<o-icon :icon="collapse.icon" class="px-2" />
<p class="font-bold text-lg p-2 flex-1">
{{ collapse.title }}
</p>
<a
class="flex items-center cursor-pointer p-3 justify-center self-end"
>
<o-icon :icon="props.open ? 'chevron-up' : 'chevron-down'">
</o-icon>
</a>
</div>
</template>
<div class="p-2">
<div class="content">
{{ collapse.text }}
</div>
</div>
</o-collapse>
</div>
<div class="flex gap-3 p-4">
<o-button
:disabled="collapses.length === 0"
@click="() => authorize()"
>{{ t("Authorize") }}</o-button
>
<o-button outlined tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Decline")
}}</o-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useHead } from "@vueuse/head";
import { computed } from "vue";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable";
import { AUTORIZE_APPLICATION } from "@/graphql/application";
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
import RouteName from "@/router/name";
import { IApplication } from "@/types/application.model";
import { scope } from "./scope";
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
const { t } = useI18n({ useScope: "global" });
@ -49,9 +99,25 @@ const props = defineProps<{
scope?: string | null;
}>();
const isOpen = ref<number>(-1);
const collapses = computed(() =>
(props.scope ?? "")
.split(" ")
.map((scope) => scope[scope])
.filter((scope) => scope)
);
const { mutate: authorizeMutation, onDone: onAuthorizeMutationDone } =
useMutation<
{ authorizeApplication: { code: string; state: string } },
{
authorizeApplication: {
code: string;
state: string;
clientId: string;
scope: string;
};
},
{
applicationClientId: string;
redirectURI: string;
@ -71,13 +137,20 @@ const authorize = () => {
onAuthorizeMutationDone(({ data }) => {
const code = data?.authorizeApplication?.code;
const localClientId = data?.authorizeApplication?.clientId;
const localScope = data?.authorizeApplication?.scope;
const returnedState = data?.authorizeApplication?.state ?? "";
if (!code) return;
if (!code || !localClientId || !localScope) return;
if (props.redirectURI) {
const params = new URLSearchParams(
Object.entries({ code, state: returnedState })
Object.entries({
code,
state: returnedState,
client_id: localClientId,
scope: localScope,
})
);
window.location.assign(
new URL(`${props.redirectURI}?${params.toString()}`)

View File

@ -0,0 +1,283 @@
import { i18n } from "@/utils/i18n";
const t = i18n.global.t;
export const scope: Record<
string,
{ title: string; type?: "warning"; text: string; icon?: string }
> = {
read: {
title: t("Read all of your account's data"),
type: "warning",
text: t(
"This application will be allowed to see all of your events organized, the events you participate to, as well as every data from your groups."
),
icon: "eye-outline",
},
write: {
title: t("Modify all of your account's data"),
text: t(
"This application will be allowed to publish and manage events on your behalf, post and manage comments, participate to events, manage all of your groups, including group events, resources, posts and discussions. It will also be allowed to manage your account and profile settings."
),
type: "warning",
icon: "pencil-outline",
},
"write:event:create": {
title: t("Publish events"),
text: t(
"This application will be allowed to publish events on your behalf"
),
icon: "calendar",
},
"write:event:update": {
title: t("Update events"),
text: t("This application will be allowed to update events on your behalf"),
icon: "calendar",
},
"write:event:delete": {
title: t("Delete events"),
text: t("This application will be allowed to delete events on your behalf"),
icon: "calendar",
},
"write:media:upload": {
title: t("Upload media"),
text: t("This application will be allowed to upload media on your behalf"),
icon: "image",
},
"write:media:remove": {
title: t("Remove uploaded media"),
text: t(
"This application will be allowed to remove uploaded media on your behalf"
),
icon: "image",
},
"write:group:post:create": {
title: t("Publish group posts"),
text: t(
"This application will be allowed to publish group posts on your behalf"
),
icon: "bullhorn",
},
"write:group:post:update": {
title: t("Update group posts"),
text: t(
"This application will be allowed to update group posts on your behalf"
),
icon: "bullhorn",
},
"write:group:post:delete": {
title: t("Delete group posts"),
text: t(
"This application will be allowed to delete group posts on your behalf"
),
icon: "bullhorn",
},
"read:group:resources": {
title: t("Access your group's resources"),
text: t(
"This application will be allowed to access all of the groups you're a member of on your behalf"
),
icon: "link",
},
"write:group:resources:create": {
title: t("Create group resources"),
text: t(
"This application will be allowed to create resources in all of the groups you're a member of on your behalf"
),
icon: "link",
},
"write:group:resources:update": {
title: t("Update group resources"),
text: t(
"This application will be allowed to update resources in all of the groups you're a member of on your behalf"
),
icon: "link",
},
"write:group:resources:delete": {
title: t("Delete group resources"),
text: t(
"This application will be allowed to delete resources in all of the groups you're a member of on your behalf"
),
icon: "link",
},
"read:group:events": {
title: t("Access group events"),
text: t(
"This application will be allowed to list and access group events in all of the groups you're a member of on your behalf"
),
icon: "calendar",
},
"read:group:discussions": {
title: t("Access group discussions"),
text: t(
"This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf"
),
icon: "chat",
},
"read:group:members": {
title: t("Access group members"),
text: t(
"This application will be allowed to list group members in all of the groups you're a member of on your behalf"
),
icon: "account-circle",
},
"read:group:followers": {
title: t("Access group followers"),
text: t(
"This application will be allowed to list group followers in all of the groups you're a member of on your behalf"
),
icon: "account-circle",
},
"read:group:activities": {
title: t("Access group activities"),
text: t(
"This application will be allowed to access group activities in all of the groups you're a member of on your behalf"
),
icon: "timeline-text",
},
"read:group:todo_lists": {
title: t("Access group todo-lists"),
text: t(
"This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf"
),
icon: "checkbox-marked",
},
"write:group:group_membership": {
title: t("Manage group memberships"),
text: t(
"This application will be allowed to join and leave groups on your behalf"
),
icon: "account-circle",
},
"write:group:members": {
title: t("Manage group members"),
text: t(
"This application will be allowed to manage group members in all of the groups you're a member of on your behalf"
),
icon: "account-circle",
},
"read:profile:organized_events": {
title: t("Access organized events"),
text: t(
"This application will be allowed to list and view your organized events"
),
icon: "calendar",
},
"read:profile:participations": {
title: t("Access participations"),
text: t(
"This application will be allowed to list and view the events you're participating to"
),
icon: "account-circle",
},
"read:profile:memberships": {
title: t("Access memberships"),
text: t(
"This application will be allowed to list and view the groups you're a member of"
),
icon: "account-circle",
},
"read:profile:follows": {
title: t("Access followed groups"),
text: t(
"This application will be allowed to list and view the groups you're following"
),
icon: "account-circle",
},
"write:profile:create": {
title: t("Create new profiles"),
text: t(
"This application will be allowed to create new profiles for your account on your behalf"
),
icon: "account-circle",
},
"write:profile:update": {
title: t("Update profiles"),
text: t(
"This application will be allowed to update your profiles on your behalf"
),
icon: "account-circle",
},
"write:profile:delete": {
title: t("Delete profiles"),
text: t(
"This application will be allowed to delete your profiles on your behalf"
),
icon: "account-circle",
},
"write:comment:create": {
title: t("Post comments"),
text: t("This application will be allowed to post comments on your behalf"),
icon: "comment",
},
"write:comment:update": {
title: t("Update comments"),
text: t(
"This application will be allowed to update comments on your behalf"
),
icon: "comment",
},
"write:comment:delete": {
title: t("Delete comments"),
text: t(
"This application will be allowed to delete comments on your behalf"
),
icon: "comment",
},
"write:group:discussion:create": {
title: t("Create group discussions"),
text: t(
"This application will be allowed to create group discussions on your behalf"
),
icon: "comment",
},
"write:group:discussion:update": {
title: t("Update group discussions"),
text: t(
"This application will be allowed to update group discussions on your behalf"
),
icon: "comment",
},
"write:group:discussion:delete": {
title: t("Delete group discussions"),
text: t(
"This application will be allowed to delete group discussions on your behalf"
),
icon: "comment",
},
"write:profile:feed_token:create": {
title: t("Create feed tokens"),
text: t(
"This application will be allowed to create feed tokens on your behalf"
),
icon: "rss",
},
"write:feed_token:delete": {
title: t("Delete feed tokens"),
text: t(
"This application will be allowed to delete feed tokens on your behalf"
),
icon: "rss",
},
"write:participation": {
title: t("Manage event participations"),
text: t(
"This application will be allowed to manage events participations on your behalf"
),
icon: "rss",
},
"write:user:setting:activity": {
title: t("Manage activity settings"),
text: t(
"This application will be allowed to manage your account activity settings"
),
icon: "cog",
},
"write:user:setting:push": {
title: t("Manage push notification settings"),
text: t(
"This application will be allowed to manage your account push notification settings"
),
icon: "cog",
},
};

View File

@ -250,6 +250,14 @@ const icons: Record<string, () => Promise<any>> = {
),
ExitToApp: () =>
import(`../../../node_modules/vue-material-design-icons/ExitToApp.vue`),
CheckboxMarked: () =>
import(
`../../../node_modules/vue-material-design-icons/CheckboxMarked.vue`
),
EyeOutline: () =>
import(`../../../node_modules/vue-material-design-icons/EyeOutline.vue`),
PencilOutline: () =>
import(`../../../node_modules/vue-material-design-icons/PencilOutline.vue`),
};
const props = withDefaults(

View File

@ -5,6 +5,7 @@ import {
PERSON_STATUS_GROUP,
} from "@/graphql/actor";
import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { useQuery } from "@vue/apollo-composable";
import { computed, Ref, unref } from "vue";
import { useCurrentUserClient } from "./user";
@ -24,7 +25,7 @@ export function useCurrentActorClient() {
export function useCurrentUserIdentities() {
const { currentUser } = useCurrentUserClient();
const { result, error, loading } = useQuery<{ identities: IPerson[] }>(
const { result, error, loading } = useQuery<{ loggedUser: Pick<ICurrentUser, 'actors'> }>(
IDENTITIES,
{},
() => ({
@ -35,7 +36,7 @@ export function useCurrentUserIdentities() {
})
);
const identities = computed(() => result.value?.identities);
const identities = computed(() => result.value?.loggedUser?.actors);
return { identities, error, loading };
}

View File

@ -82,11 +82,11 @@ export function registerAccount() {
{ context }
) => {
if (context?.userAlreadyActivated) {
const identitiesData = store.readQuery<{ identities: IPerson[] }>({
const currentUserData = store.readQuery<{ loggedUser: Pick<ICurrentUser, 'actors'> }>({
query: IDENTITIES,
});
if (identitiesData && localData) {
if (currentUserData && localData) {
const newPersonData = {
...localData.registerPerson,
type: ActorType.PERSON,
@ -95,8 +95,10 @@ export function registerAccount() {
store.writeQuery({
query: IDENTITIES,
data: {
...identitiesData,
identities: [...identitiesData.identities, newPersonData],
...currentUserData.loggedUser,
actors: [
[...currentUserData.loggedUser.actors, newPersonData]
]
},
});
}

View File

@ -282,13 +282,17 @@ export const LOGGED_USER_MEMBERSHIPS = gql`
export const IDENTITIES = gql`
query Identities {
identities {
...ActorFragment
loggedUser {
actors {
...ActorFragment
}
}
}
${ACTOR_FRAGMENT}
`;
export const PERSON_MEMBERSHIPS = gql`
query PersonMemberships($id: ID!) {
person(id: $id) {

View File

@ -14,9 +14,9 @@ export const AUTH_APPLICATION = gql`
export const AUTORIZE_APPLICATION = gql`
mutation AuthorizeApplication(
$applicationClientId: String!
$redirectURI: String
$redirectURI: String!
$state: String
$scope: String
$scope: String!
) {
authorizeApplication(
clientId: $applicationClientId
@ -26,6 +26,23 @@ export const AUTORIZE_APPLICATION = gql`
) {
code
state
clientId
scope
}
}
`;
export const AUTORIZE_DEVICE_APPLICATION = gql`
mutation AuthorizeDeviceApplication(
$applicationClientId: String!
$userCode: String
) {
authorizeDeviceApplication(
clientId: $applicationClientId
userCode: $userCode
) {
clientId
scope
}
}
`;

View File

@ -72,6 +72,7 @@ export const CONFIG = gql`
features {
groups
eventCreation
antispam
}
restrictions {
onlyAdminCanCreateGroups

View File

@ -226,15 +226,6 @@ export const FETCH_GROUP = gql`
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;
export const FETCH_GROUP_BY_ID = gql`
query FetchGroupById($id: ID!) {
groupById(id: $name) {
...GroupFullFields
}
}
${GROUP_FIELDS_FRAGMENTS}
`;
export const GET_GROUP = gql`
query GetGroup(
$id: ID!
@ -407,6 +398,7 @@ export const GROUP_TIMELINE = gql`
openness
physicalAddress {
id
originId
}
banner {
id

View File

@ -1457,5 +1457,101 @@
"Autorize this application to access your account?": "Autorize this application to access your account?",
"This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust.": "This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust.",
"Authorize application": "Authorize application",
"Authorize": "Authorize"
"Authorize": "Authorize",
"You'll be able to revoke access for this application in your account settings.": "You'll be able to revoke access for this application in your account settings.",
"Read all of your account's data": "Read all of your account's data",
"This application will be allowed to see all of your events organized, the events you participate to, …": "",
"Modify all of your account's data": "Modify all of your account's data",
"This application will be allowed to publish events on your behalf, participate to events": "",
"Publish events": "Publish events",
"This application will be allowed to publish events on your behalf": "This application will be allowed to publish events on your behalf",
"Update events": "Update events",
"This application will be allowed to update events on your behalf": "This application will be allowed to update events on your behalf",
"Delete events": "Delete events",
"This application will be allowed to delete events on your behalf": "This application will be allowed to delete events on your behalf",
"Upload media": "Upload media",
"This application will be allowed to upload media on your behalf": "This application will be allowed to upload media on your behalf",
"Remove uploaded media": "Remove uploaded media",
"This application will be allowed to remove uploaded media on your behalf": "This application will be allowed to remove uploaded media on your behalf",
"Publish group posts": "Publish group posts",
"This application will be allowed to publish group posts on your behalf": "This application will be allowed to publish group posts on your behalf",
"Update group posts": "Update group posts",
"This application will be allowed to update group posts on your behalf": "This application will be allowed to update group posts on your behalf",
"Delete group posts": "Delete group posts",
"This application will be allowed to delete group posts on your behalf": "This application will be allowed to delete group posts on your behalf",
"Access your group's resources": "Access your group's resources",
"This application will be allowed to access all of the groups you're a member of on your behalf": "This application will be allowed to access all of the groups you're a member of on your behalf",
"Create group resources": "Create group resources",
"This application will be allowed to create resources in all of the groups you're a member of on your behalf": "This application will be allowed to create resources in all of the groups you're a member of on your behalf",
"Update group resources": "Update group resources",
"This application will be allowed to update resources in all of the groups you're a member of on your behalf": "This application will be allowed to update resources in all of the groups you're a member of on your behalf",
"Delete group resources": "Delete group resources",
"This application will be allowed to delete resources in all of the groups you're a member of on your behalf": "This application will be allowed to delete resources in all of the groups you're a member of on your behalf",
"Access group events": "Access group events",
"This application will be allowed to list and access group events in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group events in all of the groups you're a member of on your behalf",
"Access group discussions": "Access group discussions",
"This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf",
"Access group members": "Access group members",
"This application will be allowed to list group members in all of the groups you're a member of on your behalf": "This application will be allowed to list group members in all of the groups you're a member of on your behalf",
"Access group followers": "Access group followers",
"This application will be allowed to list group followers in all of the groups you're a member of on your behalf": "This application will be allowed to list group followers in all of the groups you're a member of on your behalf",
"Access group activities": "Access group activities",
"This application will be allowed to access group activities in all of the groups you're a member of on your behalf": "This application will be allowed to access group activities in all of the groups you're a member of on your behalf",
"Access group todo-lists": "Access group todo-lists",
"This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf",
"Manage group memberships": "Manage group memberships",
"This application will be allowed to join and leave groups on your behalf": "This application will be allowed to join and leave groups on your behalf",
"Manage group members": "Manage group members",
"This application will be allowed to manage group members in all of the groups you're a member of on your behalf": "This application will be allowed to manage group members in all of the groups you're a member of on your behalf",
"Access organized events": "Access organized events",
"This application will be allowed to list and view your organized events": "This application will be allowed to list and view your organized events",
"Access participations": "Access participations",
"This application will be allowed to list and view the events you're participating to": "This application will be allowed to list and view the events you're participating to",
"Access memberships": "Access memberships",
"This application will be allowed to list and view the groups you're a member of": "This application will be allowed to list and view the groups you're a member of",
"Access followed groups": "Access followed groups",
"This application will be allowed to list and view the groups you're following": "This application will be allowed to list and view the groups you're following",
"Create new profiles": "Create new profiles",
"This application will be allowed to create new profiles for your account on your behalf": "This application will be allowed to create new profiles for your account on your behalf",
"Update profiles": "Update profiles",
"This application will be allowed to update your profiles on your behalf": "This application will be allowed to update your profiles on your behalf",
"Delete profiles": "Delete profiles",
"This application will be allowed to delete your profiles on your behalf": "This application will be allowed to delete your profiles on your behalf",
"Post comments": "Post comments",
"This application will be allowed to post comments on your behalf": "This application will be allowed to post comments on your behalf",
"Update comments": "Update comments",
"This application will be allowed to update comments on your behalf": "This application will be allowed to update comments on your behalf",
"Delete comments": "Delete comments",
"This application will be allowed to delete comments on your behalf": "This application will be allowed to delete comments on your behalf",
"Create group discussions": "Create group discussions",
"This application will be allowed to create group discussions on your behalf": "This application will be allowed to create group discussions on your behalf",
"Update group discussions": "Update group discussions",
"This application will be allowed to update group discussions on your behalf": "This application will be allowed to update group discussions on your behalf",
"Delete group discussions": "Delete group discussions",
"This application will be allowed to delete group discussions on your behalf": "This application will be allowed to delete group discussions on your behalf",
"Create feed tokens": "Create feed tokens",
"This application will be allowed to create feed tokens on your behalf": "This application will be allowed to create feed tokens on your behalf",
"Delete feed tokens": "Delete feed tokens",
"This application will be allowed to delete feed tokens on your behalf": "This application will be allowed to delete feed tokens on your behalf",
"Manage event participations": "Manage event participations",
"This application will be allowed to manage events participations on your behalf": "This application will be allowed to manage events participations on your behalf",
"Manage activity settings": "Manage activity settings",
"This application will be allowed to manage your account activity settings": "This application will be allowed to manage your account activity settings",
"Manage push notification settings": "Manage push notification settings",
"This application will be allowed to manage your account push notification settings": "This application will be allowed to manage your account push notification settings",
"Apps": "Apps",
"Device activation": "Device activation",
"Application not found": "Application not found",
"The provided application was not found.": "The provided application was not found.",
"Your application code": "Your application code",
"You need to provide the following code to your application": "You need to provide the following code to your application",
"Enter the code displayed on your device": "Enter the code displayed on your device",
"Continue": "Continue",
"The device code is incorrect or no longer valid.": "The device code is incorrect or no longer valid.",
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.",
"Last used on {last_used_date}": "Last used on {last_used_date}",
"Never used": "Never used",
"Authorized on {authorization_date}": "Authorized on {authorization_date}",
"Revoke": "Revoke",
"Application was revoked": "Application was revoked"
}

View File

@ -1455,5 +1455,99 @@
"Autorize this application to access your account?": "Autoriser cette application à accéder à votre compte ?",
"This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust.": "Cette application sera capable d'accéder à toutes vos informations et poster du contenu en votre nom. Assurez-vous d'approuver uniquement des applications en lesquelles vous avez confiance.",
"Authorize application": "Autoriser l'application",
"Authorize": "Autoriser"
"Authorize": "Autoriser",
"You'll be able to revoke access for this application in your account settings.": "Vous pourrez révoquer l'accès pour cette applications dans les paramètres de votre compte.",
"Read all of your account's data": "Lire toutes les données de votre compte",
"Modify all of your account's data": "Modifier toutes les données de votre compte",
"Publish events": "Publier des événements",
"This application will be allowed to publish events on your behalf": "Cette application pourra publier des événements en votre nom",
"Update events": "Update events",
"This application will be allowed to update events on your behalf": "Cette application pourra mettre à jour des événements en votre nom",
"Delete events": "Delete events",
"This application will be allowed to delete events on your behalf": "Cette application pourra supprimer des événements en votre nom",
"Upload media": "Upload media",
"This application will be allowed to upload media on your behalf": "Cette application pourra téléverser des médias en votre nom",
"Remove uploaded media": "Remove uploaded media",
"This application will be allowed to remove uploaded media on your behalf": "Cette application pourra supprimer des médias téléversés en votre nom",
"Publish group posts": "Publish group posts",
"This application will be allowed to publish group posts on your behalf": "Cette application pourra publier des billets de groupes en votre nom",
"Update group posts": "Update group posts",
"This application will be allowed to update group posts on your behalf": "Cette application pourra mettre à jour des billets de groupes en votre nom",
"Delete group posts": "Delete group posts",
"This application will be allowed to delete group posts on your behalf": "Cette application pourra supprimer des billets de groupes en votre nom",
"Access your group's resources": "Access your group's resources",
"This application will be allowed to access all of the groups you're a member of on your behalf": "Cette application pourra accéder à tous les groupes dont vous êtes membres",
"Create group resources": "Create group resources",
"This application will be allowed to create resources in all of the groups you're a member of on your behalf": "Cette application pourra créer des ressources dans chacun des groupes dont vous êtes membre en votre nom",
"Update group resources": "Update group resources",
"This application will be allowed to update resources in all of the groups you're a member of on your behalf": "Cette application pourra mettre à jour des ressources dans chacun des groupes dont vous êtes membre en votre nom",
"Delete group resources": "Delete group resources",
"This application will be allowed to delete resources in all of the groups you're a member of on your behalf": "Cette application pourra supprimer des ressources dans chacun des groupes dont vous êtes membre en votre nom",
"Access group events": "Access group events",
"This application will be allowed to list and access group events in all of the groups you're a member of on your behalf": "Cette application pourra lister et accéder aux événements des groupes dont vous êtes membre",
"Access group discussions": "Access group discussions",
"This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf",
"Access group members": "Access group members",
"This application will be allowed to list group members in all of the groups you're a member of on your behalf": "This application will be allowed to list group members in all of the groups you're a member of on your behalf",
"Access group followers": "Access group followers",
"This application will be allowed to list group followers in all of the groups you're a member of on your behalf": "This application will be allowed to list group followers in all of the groups you're a member of on your behalf",
"Access group activities": "Access group activities",
"This application will be allowed to access group activities in all of the groups you're a member of on your behalf": "This application will be allowed to access group activities in all of the groups you're a member of on your behalf",
"Access group todo-lists": "Access group todo-lists",
"This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf",
"Manage group memberships": "Manage group memberships",
"This application will be allowed to join and leave groups on your behalf": "This application will be allowed to join and leave groups on your behalf",
"Manage group members": "Manage group members",
"This application will be allowed to manage group members in all of the groups you're a member of on your behalf": "This application will be allowed to manage group members in all of the groups you're a member of on your behalf",
"Access organized events": "Access organized events",
"This application will be allowed to list and view your organized events": "This application will be allowed to list and view your organized events",
"Access participations": "Access participations",
"This application will be allowed to list and view the events you're participating to": "This application will be allowed to list and view the events you're participating to",
"Access memberships": "Access memberships",
"This application will be allowed to list and view the groups you're a member of": "This application will be allowed to list and view the groups you're a member of",
"Access followed groups": "Access followed groups",
"This application will be allowed to list and view the groups you're following": "This application will be allowed to list and view the groups you're following",
"Create new profiles": "Create new profiles",
"This application will be allowed to create new profiles for your account on your behalf": "This application will be allowed to create new profiles for your account on your behalf",
"Update profiles": "Update profiles",
"This application will be allowed to update your profiles on your behalf": "This application will be allowed to update your profiles on your behalf",
"Delete profiles": "Delete profiles",
"This application will be allowed to delete your profiles on your behalf": "This application will be allowed to delete your profiles on your behalf",
"Post comments": "Post comments",
"This application will be allowed to post comments on your behalf": "This application will be allowed to post comments on your behalf",
"Update comments": "Update comments",
"This application will be allowed to update comments on your behalf": "This application will be allowed to update comments on your behalf",
"Delete comments": "Delete comments",
"This application will be allowed to delete comments on your behalf": "This application will be allowed to delete comments on your behalf",
"Create group discussions": "Create group discussions",
"This application will be allowed to create group discussions on your behalf": "This application will be allowed to create group discussions on your behalf",
"Update group discussions": "Update group discussions",
"This application will be allowed to update group discussions on your behalf": "This application will be allowed to update group discussions on your behalf",
"Delete group discussions": "Delete group discussions",
"This application will be allowed to delete group discussions on your behalf": "This application will be allowed to delete group discussions on your behalf",
"Create feed tokens": "Create feed tokens",
"This application will be allowed to create feed tokens on your behalf": "This application will be allowed to create feed tokens on your behalf",
"Delete feed tokens": "Delete feed tokens",
"This application will be allowed to delete feed tokens on your behalf": "This application will be allowed to delete feed tokens on your behalf",
"Manage event participations": "Manage event participations",
"This application will be allowed to manage events participations on your behalf": "This application will be allowed to manage events participations on your behalf",
"Manage activity settings": "Manage activity settings",
"This application will be allowed to manage your account activity settings": "This application will be allowed to manage your account activity settings",
"Manage push notification settings": "Manage push notification settings",
"This application will be allowed to manage your account push notification settings": "This application will be allowed to manage your account push notification settings",
"Apps": "Apps",
"Device activation": "Device activation",
"Application not found": "Application not found",
"The provided application was not found.": "The provided application was not found.",
"Your application code": "Your application code",
"You need to provide the following code to your application": "You need to provide the following code to your application",
"Enter the code displayed on your device": "Enter the code displayed on your device",
"Continue": "Continue",
"The device code is incorrect or no longer valid.": "The device code is incorrect or no longer valid.",
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.",
"Last used on {last_used_date}": "Last used on {last_used_date}",
"Never used": "Never used",
"Authorized on {authorization_date}": "Authorized on {authorization_date}",
"Revoke": "Revoke",
"Application was revoked": "Application was revoked"
}

View File

@ -3,7 +3,7 @@ export interface IApplication {
clientId: string;
clientSecret?: string;
redirectUris?: string;
scopes: string | null;
scope: string | null;
website: string | null;
}

View File

@ -15,6 +15,7 @@ export interface ICurrentUser {
isLoggedIn: boolean;
role: ICurrentUserRole;
defaultActor?: IPerson;
actors: IPerson[];
}
export interface IUserPreferredLocation {

View File

@ -1,6 +1,7 @@
import { AUTH_USER_ACTOR_ID } from "@/constants";
import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor";
import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { apolloClient } from "@/vue-apollo";
import {
provideApolloClient,
@ -36,10 +37,10 @@ export async function initializeCurrentActor(): Promise<void> {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
const { result: identitiesResult } = provideApolloClient(apolloClient)(() =>
useQuery<{ identities: IPerson[] }>(IDENTITIES)
useQuery<{ currentUser: Pick<ICurrentUser, 'actors'> }>(IDENTITIES)
);
const identities = computed(() => identitiesResult.value?.identities);
const identities = computed(() => identitiesResult.value?.currentUser.actors);
watch(identities, async () => {
if (identities.value && identities.value.length < 1) {

View File

@ -234,6 +234,7 @@ import { convertToUsername } from "@/utils/username";
import { Dialog } from "@/plugins/dialog";
import { Notifier } from "@/plugins/notifier";
import { AbsintheGraphQLErrors } from "@/types/errors.model";
import { ICurrentUser } from "@/types/current-user.model";
const { t } = useI18n({ useScope: "global" });
const router = useRouter();
@ -348,7 +349,7 @@ const {
onError: deletePersonError,
} = useMutation(DELETE_PERSON, () => ({
update: (store: ApolloCache<InMemoryCache>) => {
const data = store.readQuery<{ identities: IPerson[] }>({
const data = store.readQuery<{ loggedUser: Pick<ICurrentUser, 'actors'> }>({
query: IDENTITIES,
});
@ -356,7 +357,10 @@ const {
store.writeQuery({
query: IDENTITIES,
data: {
identities: data.identities.filter((i) => i.id !== identity.value.id),
loggedUser: {
...data.loggedUser,
actors: data.loggedUser.actors.filter((i) => i.id !== identity.value.id)
}
},
});
}
@ -379,10 +383,10 @@ deletePersonDone(async () => {
*/
const client = resolveClient();
const data = client.readQuery<{
identities: IPerson[];
loggedUser: Pick<ICurrentUser, 'actors'>
}>({ query: IDENTITIES });
if (data) {
await maybeUpdateCurrentActorCache(data.identities[0]);
await maybeUpdateCurrentActorCache(data.loggedUser.actors[0]);
}
await redirectIfNoIdentitySelected();
@ -408,7 +412,7 @@ const {
store: ApolloCache<InMemoryCache>,
{ data: updateData }: FetchResult
) => {
const data = store.readQuery<{ identities: IPerson[] }>({
const data = store.readQuery<{ loggedUser: Pick<ICurrentUser, 'actors'> }>({
query: IDENTITIES,
});
@ -452,7 +456,7 @@ const {
store: ApolloCache<InMemoryCache>,
{ data: updateData }: FetchResult
) => {
const data = store.readQuery<{ identities: IPerson[] }>({
const data = store.readQuery<{ loggedUser: Pick<ICurrentUser, 'actors'> }>({
query: IDENTITIES,
});
@ -460,10 +464,13 @@ const {
store.writeQuery({
query: IDENTITIES,
data: {
identities: [
...data.identities,
loggedUser: {
...data.loggedUser,
actors: [
...data.loggedUser.actors,
{ ...updateData?.createPerson, type: ActorType.PERSON },
],
]
}
},
});
}

View File

@ -18,27 +18,47 @@
<section class="timeline">
<o-field>
<o-radio v-model="activityType" :native-value="undefined">
<o-radio class="pr-4" v-model="activityType" :native-value="undefined">
<TimelineText />
{{ t("All activities") }}</o-radio
>
<o-radio v-model="activityType" :native-value="ActivityType.MEMBER">
<o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.MEMBER"
>
<o-icon icon="account-multiple-plus"></o-icon>
{{ t("Members") }}</o-radio
>
<o-radio v-model="activityType" :native-value="ActivityType.GROUP">
<o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.GROUP"
>
<o-icon icon="cog"></o-icon>
{{ t("Settings") }}</o-radio
>
<o-radio v-model="activityType" :native-value="ActivityType.EVENT">
<o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.EVENT"
>
<o-icon icon="calendar"></o-icon>
{{ t("Events") }}</o-radio
>
<o-radio v-model="activityType" :native-value="ActivityType.POST">
<o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.POST"
>
<o-icon icon="bullhorn"></o-icon>
{{ t("Posts") }}</o-radio
>
<o-radio v-model="activityType" :native-value="ActivityType.DISCUSSION">
<o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.DISCUSSION"
>
<o-icon icon="chat"></o-icon>
{{ t("Discussions") }}</o-radio
>
@ -48,11 +68,16 @@
>
</o-field>
<o-field>
<o-radio v-model="activityAuthor" :native-value="undefined">
<o-radio
class="pr-4"
v-model="activityAuthor"
:native-value="undefined"
>
<TimelineText />
{{ t("All activities") }}</o-radio
>
<o-radio
class="pr-4"
v-model="activityAuthor"
:native-value="ActivityAuthorFilter.SELF"
>
@ -60,6 +85,7 @@
{{ t("From yourself") }}</o-radio
>
<o-radio
class="pr-4"
v-model="activityAuthor"
:native-value="ActivityAuthorFilter.BY"
>
@ -89,7 +115,7 @@
<h2 v-else>
{{ formatDateString(date) }}
</h2>
<ul>
<ul class="before:opacity-10">
<li v-for="activityItem in activityItems" :key="activityItem.id">
<skeleton-activity-item v-if="activityItem.type === 'skeleton'" />
<component
@ -202,6 +228,7 @@ const page = ref(1);
const {
result: groupTimelineResult,
fetchMore: fetchMoreActivities,
onError: onGroupTLError,
loading,
} = useQuery<{ group: IGroup }>(GROUP_TIMELINE, () => ({
preferredUsername: props.preferredUsername,
@ -211,6 +238,8 @@ const {
author: activityAuthor.value,
}));
onGroupTLError((err) => console.error(err));
const group = computed(() => groupTimelineResult.value?.group);
useHead({

View File

@ -36,8 +36,8 @@
/>
<div v-show="authApplicationError">
<div
class="rounded-lg bg-mbz-danger shadow-xl my-6 p-4 flex items-center gap-2"
v-if="authApplicationGraphError?.status_code === 404"
class="rounded-lg text-white bg-mbz-danger shadow-xl my-6 p-4 flex items-center gap-2"
v-if="authApplicationGraphError?.message === 'not_found'"
>
<AlertCircle :size="42" />
<div>
@ -72,9 +72,6 @@
<p class="text-4xl">{{ resultCode }}</p>
</div>
</div>
<o-button variant="text" tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Back to homepage")
}}</o-button>
</div>
</template>
@ -83,8 +80,8 @@ import { useRouteQuery } from "vue-use-route-query";
import { useHead } from "@vueuse/head";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { AUTH_APPLICATION, AUTORIZE_APPLICATION } from "@/graphql/application";
import { useQuery } from "@vue/apollo-composable";
import { AUTH_APPLICATION } from "@/graphql/application";
import { IApplication } from "@/types/application.model";
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
import type { AbsintheGraphQLError } from "@/types/errors.model";
@ -98,7 +95,6 @@ const redirectURI = useRouteQuery("redirect_uri", null);
const state = useRouteQuery("state", null);
const scope = useRouteQuery("scope", null);
const OUT_OF_BAND_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
const resultCode = ref<string | null>(null);
const {
@ -123,44 +119,6 @@ const authApplicationGraphError = computed(
() => authApplicationError.value?.graphQLErrors[0] as AbsintheGraphQLError
);
const { mutate: authorizeMutation, onDone: onAuthorizeMutationDone } =
useMutation<
{ authorizeApplication: { code: string; state: string } },
{
applicationClientId: string;
redirectURI: string;
state?: string | null;
scope?: string | null;
}
>(AUTORIZE_APPLICATION);
const authorize = () => {
authorizeMutation({
applicationClientId: clientId.value as string,
redirectURI: redirectURI.value as string,
state: state.value,
scope: scope.value,
});
};
onAuthorizeMutationDone(({ data }) => {
const code = data?.authorizeApplication?.code;
const returnedState = data?.authorizeApplication?.state ?? "";
if (!code) return;
if (redirectURI.value !== OUT_OF_BAND_REDIRECT_URI) {
const params = new URLSearchParams(
Object.entries({ code, state: returnedState })
);
window.location.assign(
new URL(`${redirectURI.value}?${params.toString()}`)
);
return;
}
resultCode.value = code;
});
useHead({
title: computed(() => t("Authorize application")),
});

View File

@ -136,10 +136,8 @@ const { mutate: revoke, onDone: onRevokedApplication } = useMutation<
const notifier = inject<Notifier>("notifier");
onRevokedApplication(() => {
notifier?.success(
t("Application was revoked")
);
})
notifier?.success(t("Application was revoked"));
});
useHead({
title: computed(() => t("Apps")),

View File

@ -45,6 +45,13 @@ export default defineConfig(({ command }) => {
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
unfetch: path.resolve(
__dirname,
"node_modules",
"unfetch",
"dist",
"unfetch.mjs"
),
},
},
css: {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
defmodule Mobilizon.GraphQL.Authorization do
@moduledoc """
Check authorizations
"""
use Rajska,
valid_roles: [:user, :moderator, :administrator],
super_role: :administrator,
default_rule: :default
alias Mobilizon.Applications.ApplicationToken
alias Mobilizon.GraphQL.Authorization.AppScope
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext, only: [dgettext: 3]
@impl true
def has_user_access?(%User{}, _scope, _rule), do: true
def has_user_access?(%ApplicationToken{scope: scope} = _current_app_token, _struct, rule)
when rule != :forbid_app_access do
AppScope.has_app_access?(scope, rule)
end
def has_user_access?(_current_user, _scoped_struct, _rule), do: false
@impl true
def get_current_user(%{current_auth_app_token: app_token}), do: app_token
def get_current_user(%{current_user: current_user}), do: current_user
def get_current_user(_ctx), do: nil
@impl true
def role_authorized?(_user_role, :all), do: true
def role_authorized?(role, _allowed_role) when is_super_role(role), do: true
def role_authorized?(user_role, allowed_role) when is_atom(user_role) and is_atom(allowed_role),
do: user_role === allowed_role
def role_authorized?(user_role, allowed_roles)
when is_atom(user_role) and is_list(allowed_roles),
do: user_role in allowed_roles
@impl true
def get_user_role(%ApplicationToken{user: %{role: role}}), do: role
def get_user_role(%{role: role}), do: role
def get_user_role(nil), do: nil
@impl true
def unauthorized_message(resolution) do
case Map.get(resolution.context, :current_user) do
nil ->
"unauthenticated"
_ ->
"unauthorized"
end
end
@impl true
def unauthorized_query_scope_message(_resolution, object_type) do
dgettext("errors", "Not authorized to access this %{object_type}",
object_type: replace_underscore(object_type)
)
end
@impl true
def unauthorized_object_scope_message(_result_object, object) do
dgettext("errors", "Not authorized to access object %{object}", object: object.identifier)
end
@impl true
def unauthorized_object_message(_resolution, object) do
dgettext("errors", "Not authorized to access object %{object}", object: object.identifier)
end
@impl true
def unauthorized_field_message(_resolution, field),
do: dgettext("errors", "Not authorized to access field %{field}", field: field)
defp replace_underscore(string) when is_binary(string), do: String.replace(string, "_", " ")
defp replace_underscore(atom) when is_atom(atom) do
atom
|> Atom.to_string()
|> replace_underscore()
end
end

View File

@ -0,0 +1,118 @@
defmodule Mobilizon.GraphQL.Authorization.AppScope do
@moduledoc """
Module referencing all scopes usable in the Mobilizon API
"""
require Logger
@global_scope %{
"write" => [
# Media
:"write:media:upload",
:"write:media:remove",
# Event permissions
:"write:event:create",
:"write:event:update",
:"write:event:delete",
# Comment permissions
:"write:comment:create",
:"write:comment:update",
:"write:comment:delete",
# Event participation permission
:"write:participation",
# User account permissions
:"write:user:settings",
:"write:user:setting:activity",
:"write:user:setting:push",
# Profile permissions
:"write:profile:create",
:"write:profile:update",
:"write:profile:delete",
:"write:profile:feed_token:create",
:"write:feed_token:delete",
# Membership permissions
:"write:group_membership",
# Group permissions
:"write:group:create",
:"write:group:update",
:"write:group:delete",
# Group discussions permissions
:"write:group:discussion:create",
:"write:group:discussion:update",
:"write:group:discussion:delete",
# Group resources permissions
:"write:group:resources:create",
:"write:group:resources:update",
:"write:group:resources:delete",
# Group members
:"write:group:members",
# Post permissions
:"write:group:post:create",
:"write:group:post:update",
:"write:group:post:delete"
],
"read" => [
:"read:event",
:"read:event:participants",
:"read:event:participants:export",
:"read:user:settings",
# Profile permissions
:"read:profile",
:"read:profile:organized_events",
:"read:profile:participations",
:"read:profile:memberships",
:"read:profile:follows",
# Group details permissions
:"read:group",
:"read:group:events",
:"read:group:discussions",
:"read:group:resources",
:"read:group:members",
:"read:group:followers",
:"read:group:todo_lists",
:"read:group:activities"
]
}
@spec get_scopes :: list(atom())
def get_scopes do
@global_scope
|> Map.values()
|> Enum.concat()
|> Enum.concat([:read, :write])
end
@spec scopes_valid?(String.t()) :: boolean()
def scopes_valid?(scopes) do
scopes
|> String.split(" ")
|> Enum.all?(&scope_valid?/1)
end
@spec scope_valid?(String.t() | atom()) :: boolean()
def scope_valid?(scope) when is_binary(scope) do
scope in Enum.map(get_scopes(), &to_string/1)
end
def scope_valid?(scope) when is_atom(scope) do
scope in get_scopes()
end
@spec has_app_access?(binary, atom | binary) :: boolean
def has_app_access?(scope, rule) do
Logger.debug("Has app token access? scope: #{inspect(scope)}, rule: #{inspect(rule)}")
scope = String.split(scope, " ")
scope_acceptable_for_rule?(scope, rule) or global_scopes_acceptable_for_rule?(scope, rule)
end
@spec scope_acceptable_for_rule?(list(String.t() | atom()), String.t() | atom()) :: boolean()
defp scope_acceptable_for_rule?(scope, rule) when is_list(scope) do
to_string(rule) in Enum.map(scope, &to_string/1)
end
defp global_scopes_acceptable_for_rule?(scope, rule),
do: Enum.any?(scope, &global_scope_acceptable_for_rule?(&1, rule))
defp global_scope_acceptable_for_rule?(global_scope, rule),
do: scope_acceptable_for_rule?(Map.get(@global_scope, global_scope, []), rule)
end

View File

@ -30,6 +30,17 @@ defmodule Mobilizon.GraphQL.Error do
handle(reason)
end
# It's unclear why returned errors are now binaries instead of atoms
# but we can still convert them back
def normalize(string) when is_binary(string) do
string
|> String.to_existing_atom()
|> handle()
rescue
ArgumentError ->
handle(string)
end
# Unhandled errors
def normalize(other) do
handle(other)
@ -65,6 +76,9 @@ defmodule Mobilizon.GraphQL.Error do
end
defp handle(reason) when is_binary(reason) do
Logger.debug("Unknown error")
Logger.debug(reason)
%Error{
code: :unknown_error,
message: reason,
@ -101,6 +115,11 @@ defmodule Mobilizon.GraphQL.Error do
defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")}
defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")}
defp metadata(:discussion_not_found), do: {404, dgettext("errors", "Discussion not found")}
defp metadata(:application_not_found), do: {404, dgettext("errors", "Application not found")}
defp metadata(:application_token_not_found),
do: {404, dgettext("errors", "Application token not found")}
defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")}
defp metadata(code) do

View File

@ -12,7 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do
require Logger
@doc """
Create an application
Authorize an application
"""
@spec authorize(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} | {:error, String.t()}
def authorize(
@ -21,8 +21,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do
%{context: %{current_user: %User{id: user_id}}}
) do
case Applications.autorize(client_id, redirect_uri, scope, user_id) do
{:ok, code} ->
{:ok, %{code: code, state: state}}
{:ok,
%ApplicationToken{
application: %Application{client_id: client_id},
scope: scope,
authorization_code: code
}} ->
{:ok, %{code: code, state: state, client_id: client_id, scope: scope}}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:error, :application_not_found} ->
{:error,
@ -41,18 +49,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do
end
def authorize(_parent, _args, _context) do
{:error, dgettext("errors", "You need to be logged-in to autorize applications")}
{:error, :unauthenticated}
end
@spec get_application(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Application.t()} | {:error, :not_found | :unauthenticated}
{:ok, Application.t()} | {:error, :application_not_found | :unauthenticated}
def get_application(_parent, %{client_id: client_id}, %{context: %{current_user: %User{}}}) do
case ApplicationManager.get_application_by_client_id(client_id) do
%Application{} = application ->
{:ok, application}
nil ->
{:error, :not_found}
{:error, :application_not_found}
end
end
@ -82,7 +90,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do
end
_ ->
{:error, :not_found}
{:error, :application_token_not_found}
end
end
@ -93,29 +101,53 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do
def activate_device(_parent, %{user_code: user_code}, %{
context: %{current_user: %User{} = user}
}) do
with {:ok, %ApplicationDeviceActivation{} = app_device_activation} <-
Applications.activate_device(user_code, user) do
{:ok, app_device_activation |> Map.from_struct() |> Map.take([:application, :id, :scope])}
case Applications.activate_device(user_code, user) do
{:ok, %ApplicationDeviceActivation{} = app_device_activation} ->
{:ok, app_device_activation |> Map.from_struct() |> Map.take([:application, :id, :scope])}
{:error, :expired} ->
{:error, dgettext("errors", "The given user code has expired")}
{:error, :not_found} ->
{:error, dgettext("errors", "The given user code is invalid")}
end
end
def activate_device(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
@spec authorize_device_application(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def authorize_device_application(
_parent,
%{client_id: client_id, user_code: user_code},
%{context: %{current_user: %User{id: user_id}}}
%{context: %{current_user: %User{}}}
) do
case Applications.autorize_device_application(client_id, user_code, user_id) do
{:ok, %Application{} = app} ->
case Applications.autorize_device_application(client_id, user_code) do
{:ok, %ApplicationDeviceActivation{application: app}} ->
{:ok, app}
{:error, :application_not_found} ->
{:error, :not_confirmed} ->
{:error,
dgettext(
"errors",
"No application with this client_id was found"
"The device user code was not provided before approving the application"
)}
{:error, :not_found} ->
{:error,
dgettext(
"errors",
"The given user code is invalid"
)}
{:error, :expired} ->
{:error, dgettext("errors", "The given user code has expired")}
end
end
def authorize_device_application(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
end

View File

@ -44,7 +44,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
def upload_media(
_parent,
%{file: %Plug.Upload{} = file} = args,
%{context: %{current_actor: %Actor{id: actor_id}}}
%{context: %{current_actor: %Actor{id: default_actor_id}}}
) do
with {:ok,
%{
@ -62,7 +62,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
{:ok, media = %Media{}} <-
Medias.create_media(%{
file: args,
actor_id: actor_id,
actor_id: Map.get(args, :actor_id, default_actor_id),
metadata: Map.take(uploaded, [:width, :height, :blurhash])
}) do
{:ok, transform_media(media)}

View File

@ -20,8 +20,8 @@ defmodule Mobilizon.GraphQL.Schema do
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.GraphQL.{Authorization, Schema}
alias Mobilizon.GraphQL.Middleware.{CurrentActorProvider, ErrorHandler, OperationNameLogger}
alias Mobilizon.GraphQL.Schema
alias Mobilizon.GraphQL.Schema.Custom
alias Mobilizon.Storage.Repo
@ -57,11 +57,13 @@ defmodule Mobilizon.GraphQL.Schema do
@desc "A struct containing the id of the deleted object"
object :deleted_object do
meta(:authorize, :all)
field(:id, :id)
end
@desc "A JWT and the associated user ID"
object :login do
meta(:authorize, :all)
field(:access_token, non_null(:string), description: "A JWT Token for this session")
field(:refresh_token, non_null(:string),
@ -75,6 +77,7 @@ defmodule Mobilizon.GraphQL.Schema do
Represents a notification for an user
"""
object :notification do
meta(:authorize, :user)
field(:id, :id, description: "The notification ID")
field(:user, :user, description: "The user to transmit the notification to")
field(:actor, :actor, description: "The notification target profile")
@ -133,7 +136,9 @@ defmodule Mobilizon.GraphQL.Schema do
|> Dataloader.add_source(Resources, default_source)
|> Dataloader.add_source(Todos, default_source)
Map.put(ctx, :loader, loader)
ctx
|> Map.put(:loader, loader)
|> Map.put(:authorization, Authorization)
end
def plugins do
@ -201,11 +206,19 @@ defmodule Mobilizon.GraphQL.Schema do
end
@spec middleware(list(module()), any(), map()) :: list(module())
def middleware(middleware, _field, %{identifier: type}) when type in [:query, :mutation] do
[CurrentActorProvider] ++ middleware ++ [ErrorHandler, OperationNameLogger]
def middleware(middleware, field, %{identifier: type}) when type in [:query, :mutation] do
[CurrentActorProvider | middleware]
|> Enum.map(&fix_middleware_format_for_rajska/1)
|> Rajska.add_query_authorization(field, Authorization)
|> Rajska.add_object_authorization()
|> List.insert_at(-1, ErrorHandler)
|> List.insert_at(-1, OperationNameLogger)
end
def middleware(middleware, _field, _object) do
middleware
def middleware(middleware, field, object) do
Rajska.add_field_authorization(middleware, field, object)
end
defp fix_middleware_format_for_rajska({mod, config}), do: {mod, config}
defp fix_middleware_format_for_rajska(mod), do: {mod, nil}
end

View File

@ -25,11 +25,13 @@ defmodule Mobilizon.GraphQL.Schema.ActivityType do
end
object :activity_param_item do
meta(:authorize, :user)
field(:key, :string)
field(:value, :string)
end
interface :activity_object do
meta(:authorize, :user)
field(:id, :id)
resolve_type(fn
@ -66,11 +68,13 @@ defmodule Mobilizon.GraphQL.Schema.ActivityType do
A paginated list of activity items
"""
object :paginated_activity_list do
meta(:authorize, :user)
field(:elements, list_of(:activity), description: "A list of activities")
field(:total, :integer, description: "The total number of elements in the list")
end
object :activity do
meta(:authorize, :user)
field(:id, :id, description: "The activity item ID")
field(:inserted_at, :datetime, description: "When was the activity inserted")
field(:priority, :integer)

View File

@ -13,6 +13,7 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
@desc "An ActivityPub actor"
interface :actor do
meta(:authorize, :all)
field(:id, :id, description: "Internal ID for this actor")
field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
@ -65,18 +66,21 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
@desc "Suspend an actor"
field :suspend_profile, :deleted_object do
arg(:id, non_null(:id), description: "The remote profile ID to suspend")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&ActorResolver.suspend_profile/3)
end
@desc "Unsuspend an actor"
field :unsuspend_profile, :actor do
arg(:id, non_null(:id), description: "The remote profile ID to unsuspend")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&ActorResolver.unsuspend_profile/3)
end
@desc "Refresh a profile"
field :refresh_profile, :actor do
arg(:id, non_null(:id), description: "The remote profile ID to refresh")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&ActorResolver.refresh_profile/3)
end
end

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
Represents an application
"""
object :application do
meta(:authorize, :all)
interfaces([:actor])
field(:id, :id, description: "Internal ID for this application")

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do
Represents an actor's follower
"""
object :follower do
meta(:authorize, :user)
field(:id, :id, description: "The follow ID")
field(:target_actor, :actor, description: "What or who the profile follows")
field(:actor, :actor, description: "Which profile follows")
@ -30,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do
A paginated list of follower objects
"""
object :paginated_follower_list do
meta(:authorize, :user)
field(:elements, list_of(:follower), description: "A list of followers")
field(:total, :integer, description: "The total number of elements in the list")
end
@ -43,6 +45,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do
description: "Whether the follower has been approved by the target actor or not"
)
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Followers.update_follower/3)
end
end

View File

@ -29,6 +29,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
Represents a group of actors
"""
object :group do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:actor, :interactable, :activity_object, :action_log_object, :group_search_result])
field(:id, :id, description: "Internal ID for this group")
@ -77,7 +80,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
)
# This one should have a privacy setting
field :organized_events, :paginated_event_list do
field :organized_events, :paginated_event_list,
meta: [private: true, rule: :"read:group:events"] do
arg(:after_datetime, :datetime,
default_value: nil,
description: "Filter events that begin after this datetime"
@ -94,7 +98,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A list of the events this actor has organized")
end
field :discussions, :paginated_discussion_list do
field :discussions, :paginated_discussion_list,
meta: [private: true, rule: :"read:group:discussions"] do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated discussion list"
@ -111,7 +116,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description: "Whether the group is opened to all or has restricted access"
)
field :members, :paginated_member_list do
field :members, :paginated_member_list, meta: [private: true, rule: :"read:group:members"] do
arg(:name, :string, description: "A name to filter members by")
arg(:page, :integer, default_value: 1, description: "The page in the paginated member list")
arg(:limit, :integer, default_value: 10, description: "The limit of members per page")
@ -120,7 +125,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of group members")
end
field :resources, :paginated_resource_list do
field :resources, :paginated_resource_list,
meta: [private: true, rule: :"read:group:resources"] do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated resource list"
@ -138,7 +144,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the posts this group has")
end
field :todo_lists, :paginated_todo_list_list do
field :todo_lists, :paginated_todo_list_list,
meta: [private: true, rule: :"read:group:todo_lists"] do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated todo-lists list"
@ -149,7 +156,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the todo lists this group has")
end
field :followers, :paginated_follower_list do
field :followers, :paginated_follower_list,
meta: [private: true, rule: :"read:group:followers"] do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated followers list"
@ -166,7 +174,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the followers this group has")
end
field :activity, :paginated_activity_list do
field :activity, :paginated_activity_list,
meta: [private: true, rule: :"read:group:activities"] do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated activity items list"
@ -204,6 +213,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
A paginated list of groups
"""
object :paginated_group_list do
meta(:authorize, :all)
field(:elements, list_of(:group), description: "A list of groups")
field(:total, :integer, description: "The total number of groups in the list")
end
@ -215,12 +225,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
value(:private, description: "Visible only to people with the link - or invited")
end
object :group_follow do
field(:group, :group, description: "The group followed")
field(:profile, :group, description: "The group followed")
field(:notify, :boolean, description: "Whether to notify profile from group activity")
end
object :group_queries do
@desc "Get all groups"
field :groups, :paginated_group_list do
@ -236,12 +240,25 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
arg(:suspended, :boolean, default_value: false, description: "Filter by suspended status")
arg(:page, :integer, default_value: 1, description: "The page in the paginated group list")
arg(:limit, :integer, default_value: 10, description: "The limit of groups per page")
middleware(Rajska.QueryAuthorization,
permit: [:administrator, :moderator],
scope: Mobilizon.Actors.Actor,
args: %{}
)
resolve(&Group.list_groups/3)
end
@desc "Get a group by its ID"
field :get_group, :group do
arg(:id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization,
permit: [:administrator, :moderator],
scope: Mobilizon.Actors.Actor
)
resolve(&Group.get_group/3)
end
@ -251,15 +268,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description: "The group preferred_username, eventually containing their domain if remote"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Group.find_group/3)
end
@desc "Get a group by its preferred username"
field :group_by_id, :group do
arg(:id, non_null(:id), description: "The group local ID")
resolve(&Group.find_group_by_id/3)
end
end
object :group_mutations do
@ -291,7 +302,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
)
arg(:physical_address, :address_input, description: "The physical address for the group")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.create_group/3)
end
@ -323,14 +334,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
)
arg(:physical_address, :address_input, description: "The physical address for the group")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.update_group/3)
end
@desc "Delete a group"
field :delete_group, :deleted_object do
arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.delete_group/3)
end
@ -343,6 +354,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
default_value: true
)
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.follow_group/3)
end
@ -355,13 +367,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
default_value: true
)
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.update_group_follow/3)
end
@desc "Unfollow a group"
field :unfollow_group, :follower do
arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.unfollow_group/3)
end
end

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
Represents a member of a group
"""
object :member do
meta(:authorize, :user)
interfaces([:activity_object])
field(:id, :id, description: "The member's ID")
field(:parent, :group, description: "Of which the profile is member")
@ -37,6 +38,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
A paginated list of members
"""
object :paginated_member_list do
meta(:authorize, :user)
field(:elements, list_of(:member), description: "A list of members")
field(:total, :integer, description: "The total number of elements in the list")
end
@ -46,6 +48,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field :join_group, :member do
arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership",
args: %{parent_id: :group_id}
)
resolve(&Group.join_group/3)
end
@ -53,9 +62,42 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field :leave_group, :deleted_object do
arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership",
args: %{parent_id: :group_id}
)
resolve(&Group.leave_group/3)
end
@desc "Accept an invitation to a group"
field :accept_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership"
)
resolve(&Member.accept_invitation/3)
end
@desc "Reject an invitation to a group"
field :reject_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership"
)
resolve(&Member.reject_invitation/3)
end
@desc "Invite an actor to join the group"
field :invite_member, :member do
arg(:group_id, non_null(:id), description: "The group ID")
@ -64,29 +106,29 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
description: "The targeted person's federated username"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :group_id}
)
resolve(&Member.invite_member/3)
end
@desc "Accept an invitation to a group"
field :accept_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
resolve(&Member.accept_invitation/3)
end
@desc "Reject an invitation to a group"
field :reject_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
resolve(&Member.reject_invitation/3)
end
@desc """
Approve a membership request
"""
field :approve_member, :member do
arg(:member_id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.approve_member/3)
end
@ -96,6 +138,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field :reject_member, :member do
arg(:member_id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.reject_member/3)
end
@ -106,6 +155,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
arg(:member_id, non_null(:id), description: "The member ID")
arg(:role, non_null(:member_role_enum), description: "The new member role")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.update_member/3)
end
@ -118,6 +174,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
description: "Whether the member should be excluded from the group"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.remove_member/3)
end
end

View File

@ -16,6 +16,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
Represents a person identity
"""
object :person do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:actor, :action_log_object])
field(:id, :id, description: "Internal ID for this person")
@ -72,7 +74,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
# This one should have a privacy setting
field(:organized_events, :paginated_event_list,
description: "A list of the events this actor has organized"
description: "A list of the events this actor has organized",
meta: [private: true, rule: :"read:profile:organized_events"]
) do
arg(:page, :integer, default_value: 1, description: "The page in the paginated event list")
arg(:limit, :integer, default_value: 10, description: "The limit of events per page")
@ -81,7 +84,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
@desc "The list of events this person goes to"
field(:participations, :paginated_participant_list,
description: "The list of events this person goes to"
description: "The list of events this person goes to",
meta: [private: true, rule: :"read:profile:participations"]
) do
arg(:event_id, :id, description: "Filter by event ID")
@ -97,7 +101,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
@desc "The list of groups this person is member of"
field(:memberships, :paginated_member_list,
description: "The list of group this person is member of"
description: "The list of group this person is member of",
meta: [private: true, rule: :"read:profile:memberships"]
) do
arg(:group, :string, description: "Filter by group federated username")
arg(:group_id, :id, description: "Filter by group ID")
@ -113,7 +118,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
@desc "The list of groups this person follows"
field(:follows, :paginated_follower_list,
description: "The list of groups this person follows"
description: "The list of groups this person follows",
meta: [private: true, rule: :"read:profile:follows"]
) do
arg(:group, :string, description: "Filter by group federated username")
@ -131,6 +137,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
A paginated list of persons
"""
object :paginated_person_list do
meta(:authorize, :all)
field(:elements, list_of(:person), description: "A list of persons")
field(:total, :integer, description: "The total number of persons in the list")
end
@ -138,23 +145,46 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
object :person_queries do
@desc "Get the current actor for the logged-in user"
field :logged_person, :person do
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{}
)
resolve(&Person.get_current_person/3)
end
@desc "Get a person by its (federated) username"
field :fetch_person, :person do
arg(:preferred_username, non_null(:string), description: "The person's federated username")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{preferred_username: :preferred_username}
)
resolve(&Person.fetch_person/3)
end
@desc "Get a person by its ID"
field :person, :person do
arg(:id, non_null(:id), description: "The person ID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Person.get_person/3)
end
@desc "Get the persons for an user"
field :identities, list_of(:person) do
deprecate("Use the loggedUser query instead")
middleware(Rajska.QueryAuthorization,
permit: [:user, :moderator, :administrator],
scope: Mobilizon.Actors.Actor,
args: %{},
rule: :user_self_identities
)
resolve(&Person.identities/3)
end
@ -172,6 +202,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
arg(:suspended, :boolean, default_value: false, description: "Filter by suspended status")
arg(:page, :integer, default_value: 1, description: "The page in the paginated person list")
arg(:limit, :integer, default_value: 10, description: "The limit of persons per page")
middleware(Rajska.QueryAuthorization,
permit: [:administrator, :moderator],
scope: Mobilizon.Actors.Actor,
args: %{}
)
resolve(&Person.list_persons/3)
end
end
@ -195,6 +232,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
"The banner for the profile, either as an object or directly the ID of an existing media"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{},
rule: :"write:profile:create"
)
resolve(&Person.create_person/3)
end
@ -216,6 +260,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
"The banner for the profile, either as an object or directly the ID of an existing media"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
rule: :"write:profile:update"
)
resolve(&Person.update_person/3)
end
@ -223,6 +273,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
field :delete_person, :person do
arg(:id, non_null(:id), description: "The person's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
rule: :"write:profile:delete"
)
resolve(&Person.delete_person/3)
end
@ -245,6 +301,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
"The banner for the profile, either as an object or directly the ID of an existing media"
)
middleware(Rajska.QueryAuthorization, permit: :all, scope: Mobilizon.Actors.Actor, args: %{})
resolve(&Person.register_person/3)
end
end
@ -254,6 +312,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
field :event_person_participation_changed, :person do
arg(:person_id, non_null(:id), description: "The person's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{id: :person_id}
)
config(fn args, _ ->
{:ok, topic: args.person_id}
end)
@ -264,6 +328,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
arg(:person_id, non_null(:id), description: "The person's ID")
arg(:group, non_null(:string), description: "The group's federated username")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{id: :person_id}
)
config(fn args, _ ->
{:ok, topic: [args.group, args.person_id]}
end)

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
An address object
"""
object :address do
meta(:authorize, :all)
field(:geom, :point, description: "The geocoordinates for the point where this address is")
field(:street, :string, description: "The address's street name (with number)")
field(:locality, :string, description: "The address's locality")
@ -29,6 +30,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
A phone address
"""
object :phone_address do
meta(:authorize, :all)
field(:phone, :string, description: "The phone number")
field(:info, :string, description: "Additional information about the phone number")
end
@ -37,11 +39,13 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
An online address
"""
object :online_address do
meta(:authorize, :all)
field(:url, :string)
field(:info, :string)
end
object :picture_info_element do
meta(:authorize, :all)
field(:name, :string)
field(:url, :string)
end
@ -50,6 +54,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
A picture associated with an address
"""
object :picture_info do
meta(:authorize, :all)
field(:url, :string)
field(:author, :picture_info_element)
field(:source, :picture_info_element)
@ -100,7 +105,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
arg(:limit, :integer, default_value: 10, description: "The limit of search results per page")
arg(:type, :address_search_type, description: "Filter by type of results")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Address.search/3)
end
@ -115,6 +120,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
description: "The user's locale. Geocoding backends will make use of this value."
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Address.reverse_geocode/3)
end
end

View File

@ -15,6 +15,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
@desc "An action log"
object :action_log do
meta(:authorize, :moderator)
field(:id, :id, description: "Internal ID for this comment")
field(:actor, :actor, description: "The actor that acted")
field(:object, :action_log_object, description: "The object that was acted upon")
@ -26,6 +27,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
A paginated list of action logs
"""
object :paginated_action_log_list do
meta(:authorize, :moderator)
field(:elements, list_of(:action_log), description: "A list of action logs")
field(:total, :integer, description: "The total number of action logs in the list")
end
@ -49,6 +51,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
@desc "The objects that can be in an action log"
interface :action_log_object do
meta(:authorize, [:moderator, :administrator])
field(:id, :id, description: "Internal ID for this object")
resolve_type(fn
@ -82,6 +85,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Language information
"""
object :language do
meta(:authorize, :administrator)
field(:code, :string, description: "The iso-639-3 language code")
field(:name, :string, description: "The language name")
end
@ -90,6 +94,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Dashboard information
"""
object :dashboard do
meta(:authorize, :administrator)
field(:last_public_event_published, :event, description: "Last public event published")
field(:last_group_created, :group, description: "Last public group created")
field(:number_of_users, :integer, description: "The number of local users")
@ -109,6 +114,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Admin settings
"""
object :admin_settings do
meta(:authorize, :administrator)
field(:instance_name, :string, description: "The instance's name")
field(:instance_description, :string, description: "The instance's description")
field(:instance_long_description, :string, description: "The instance's long description")
@ -184,6 +190,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
An instance representation
"""
object :instance do
meta(:authorize, :administrator)
field(:domain, :id, description: "The domain name of the instance")
field(:follower_status, :instance_follow_status, description: "Do we follow this instance")
field(:followed_status, :instance_follow_status, description: "Does this instance follow us?")
@ -226,6 +233,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
A paginated list of instances
"""
object :paginated_instance_list do
meta(:authorize, :administrator)
field(:elements, list_of(:instance), description: "A list of instances")
field(:total, :integer, description: "The total number of instances in the list")
end
@ -235,6 +243,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
field :action_logs, type: :paginated_action_log_list do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&Admin.list_action_logs/3)
end
@ -247,6 +256,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
"The user's locale. The list of languages will be translated with this locale"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Admin.get_list_of_languages/3)
end
@ -254,6 +264,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Get dashboard information
"""
field :dashboard, type: :dashboard do
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_dashboard/3)
end
@ -261,6 +272,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Get admin settings
"""
field :admin_settings, type: :admin_settings do
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_settings/3)
end
@ -278,6 +290,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
description: "The limit of relay followers per page"
)
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.list_relay_followers/3)
end
@ -301,6 +314,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
)
arg(:direction, :string, default_value: :desc, description: "The sorting direction")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.list_relay_followings/3)
end
@ -336,6 +350,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
)
arg(:direction, :string, default_value: :desc, description: "The sorting direction")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_instances/3)
end
@ -344,6 +359,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
"""
field :instance, :instance do
arg(:domain, non_null(:id), description: "The instance domain")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_instance/3)
end
end
@ -352,28 +368,28 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
@desc "Add an instance subscription"
field :add_instance, type: :instance do
arg(:domain, non_null(:string), description: "The instance domain to add")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.create_instance/3)
end
@desc "Delete a relay subscription"
field :remove_relay, type: :follower do
arg(:address, non_null(:string), description: "The relay hostname to delete")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.remove_relay/3)
end
@desc "Accept a relay subscription"
field :accept_relay, type: :follower do
arg(:address, non_null(:string), description: "The accepted relay hostname")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.accept_subscription/3)
end
@desc "Reject a relay subscription"
field :reject_relay, type: :follower do
arg(:address, non_null(:string), description: "The rejected relay hostname")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.reject_subscription/3)
end
@ -402,7 +418,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
arg(:instance_rules, :string, description: "The instance's rules")
arg(:registrations_open, :boolean, description: "Whether the registrations are opened")
arg(:instance_languages, list_of(:string), description: "The instance's languages")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.save_settings/3)
end
@ -420,6 +436,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
description: "Whether or not to notify the user of the change"
)
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.update_user/3)
end
end

View File

@ -7,15 +7,17 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do
@desc "An application"
object :auth_application do
meta(:authorize, :user)
field(:id, :id)
field(:name, :string)
field(:client_id, :string)
field(:scopes, :string)
field(:scope, :string)
field(:website, :string)
end
@desc "An application"
object :auth_application_token do
meta(:authorize, :user)
field(:id, :id)
field(:inserted_at, :string)
field(:last_used_at, :string)
@ -24,11 +26,15 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do
@desc "The informations returned after authorization"
object :application_code_and_state do
meta(:authorize, :user)
field(:code, :string)
field(:state, :string)
field(:client_id, :string)
field(:scope, :string)
end
object :application_device_activation do
meta(:authorize, :user)
field(:id, :id)
field(:application, :auth_application)
field(:scope, :string)
@ -38,6 +44,14 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do
@desc "Get an application"
field :auth_application, :auth_application do
arg(:client_id, non_null(:string), description: "The application's client_id")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.Application,
rule: :forbid_app_access,
args: %{client_id: :client_id}
)
resolve(&Application.get_application/3)
end
end
@ -51,18 +65,33 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do
description: "The URI to redirect to with the code and state"
)
arg(:scope, :string, description: "The scope for the authorization")
arg(:scope, non_null(:string), description: "The scope for the authorization")
arg(:state, :string,
description: "A state parameter to check that the request wasn't altered"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.Application,
rule: :forbid_app_access,
args: %{client_id: :client_id}
)
resolve(&Application.authorize/3)
end
@desc "Revoke an authorized application"
field :revoke_application_token, :deleted_object do
arg(:app_token_id, non_null(:string), description: "The application token's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.ApplicationToken,
rule: :forbid_app_access,
args: %{id: :app_token_id}
)
resolve(&Application.revoke_application_token/3)
end
@ -72,13 +101,30 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do
description: "The code provided by the application entered by the user"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.ApplicationDeviceActivation,
rule: :forbid_app_access,
args: %{id: :user_code}
)
resolve(&Application.activate_device/3)
end
@desc "Activate an user device"
field :authorize_device_application, :auth_application do
arg(:client_id, non_null(:string), description: "The application's client_id")
arg(:scope, :string, description: "The scope for the authorization")
arg(:user_code, non_null(:string),
description: "The code provided by the application entered by the user"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.ApplicationDeviceActivation,
rule: :forbid_app_access,
args: %{id: :client_id}
)
resolve(&Application.authorize_device_application/3)
end

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
@desc "A config object"
object :config do
meta(:authorize, :all)
# Instance name
field(:name, :string, description: "The instance's name")
field(:description, :string, description: "The instance's short description")
@ -87,6 +88,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's terms configuration
"""
object :terms do
meta(:authorize, :all)
field(:url, :string, description: "The instance's terms URL.")
field(:type, :instance_terms_type, description: "The instance's terms type")
field(:body_html, :string, description: "The instance's terms body text")
@ -96,6 +98,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's privacy policy configuration
"""
object :privacy do
meta(:authorize, :all)
field(:url, :string, description: "The instance's privacy policy URL")
field(:type, :instance_privacy_type, description: "The instance's privacy policy type")
field(:body_html, :string, description: "The instance's privacy policy body text")
@ -105,6 +108,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Geographic coordinates
"""
object :lonlat do
meta(:authorize, :all)
field(:longitude, :float, description: "The coordinates longitude")
field(:latitude, :float, description: "The coordinates latitude")
# field(:accuracy_radius, :integer)
@ -114,6 +118,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance geocoding configuration
"""
object :geocoding do
meta(:authorize, :all)
field(:autocomplete, :boolean,
description: "Whether autocomplete in address fields can be enabled"
)
@ -125,6 +131,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance maps configuration
"""
object :maps do
meta(:authorize, :all)
field(:tiles, :tiles, description: "The instance's maps tiles configuration")
field(:routing, :routing, description: "The instance's maps routing configuration")
end
@ -133,6 +140,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance map tiles configuration
"""
object :tiles do
meta(:authorize, :all)
field(:endpoint, :string, description: "The instance's tiles endpoint")
field(:attribution, :string, description: "The instance's tiles attribution text")
end
@ -141,6 +149,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance map routing configuration
"""
object :routing do
meta(:authorize, :all)
field(:type, :routing_type, description: "The instance's routing type")
end
@ -153,6 +162,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous configuration
"""
object :anonymous do
meta(:authorize, :all)
field(:participation, :anonymous_participation,
description: "The instance's anonymous participation settings"
)
@ -172,6 +183,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation configuration
"""
object :anonymous_participation do
meta(:authorize, :all)
field(:allowed, :boolean, description: "Whether anonymous participations are allowed")
field(:validation, :anonymous_participation_validation,
@ -183,6 +195,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation validation configuration
"""
object :anonymous_participation_validation do
meta(:authorize, :all)
field(:email, :anonymous_participation_validation_email,
description: "The policy to validate anonymous participations by email"
)
@ -196,6 +210,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation with validation by email configuration
"""
object :anonymous_participation_validation_email do
meta(:authorize, :all)
field(:enabled, :boolean,
description: "Whether anonymous participation validation by email is enabled"
)
@ -209,6 +225,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation with validation by captcha configuration
"""
object :anonymous_participation_validation_captcha do
meta(:authorize, :all)
field(:enabled, :boolean,
description: "Whether anonymous participation validation by captcha is enabled"
)
@ -218,6 +236,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation configuration
"""
object :anonymous_event_creation do
meta(:authorize, :all)
field(:allowed, :boolean, description: "Whether anonymous event creation is enabled")
field(:validation, :anonymous_event_creation_validation,
@ -229,6 +248,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation validation configuration
"""
object :anonymous_event_creation_validation do
meta(:authorize, :all)
field(:email, :anonymous_event_creation_validation_email,
description: "The policy to validate anonymous event creations by email"
)
@ -242,6 +263,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation email validation configuration
"""
object :anonymous_event_creation_validation_email do
meta(:authorize, :all)
field(:enabled, :boolean,
description: "Whether anonymous event creation with email validation is enabled"
)
@ -255,6 +278,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation captcha validation configuration
"""
object :anonymous_event_creation_validation_captcha do
meta(:authorize, :all)
field(:enabled, :boolean,
description: "Whether anonymous event creation with validation by captcha is enabled"
)
@ -264,6 +289,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous reports
"""
object :anonymous_reports do
meta(:authorize, :all)
field(:allowed, :boolean, description: "Whether anonymous reports are allowed")
end
@ -271,6 +297,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
A resource provider details
"""
object :resource_provider do
meta(:authorize, :all)
field(:type, :string, description: "The resource provider's type")
field(:endpoint, :string, description: "The resource provider's endpoint")
field(:software, :string, description: "The resource provider's software")
@ -280,17 +307,22 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's features
"""
object :features do
meta(:authorize, :all)
field(:groups, :boolean, description: "Whether groups are activated on this instance")
field(:event_creation, :boolean,
description: "Whether event creation is allowed on this instance"
)
field(:antispam, :boolean, description: "Whether anti-spam is activated on this instance")
end
@desc """
The instance's restrictions
"""
object :restrictions do
meta(:authorize, :all)
field(:only_admin_can_create_groups, :boolean,
description: "Whether groups creation is allowed only for admin, not for all users"
)
@ -304,6 +336,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's auth configuration
"""
object :auth do
meta(:authorize, :all)
field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled")
field(:database_login, :boolean, description: "Whether or not database login is enabled")
field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers")
@ -313,6 +346,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
An oAuth Provider
"""
object :oauth_provider do
meta(:authorize, :all)
field(:id, :string, description: "The provider ID")
field(:label, :string, description: "The label for the auth provider")
end
@ -321,21 +355,25 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
An upload limits configuration
"""
object :upload_limits do
meta(:authorize, :all)
field(:default, :integer, description: "The default limitation, in bytes")
field(:avatar, :integer, description: "The avatar limitation, in bytes")
field(:banner, :integer, description: "The banner limitation, in bytes")
end
object :instance_feeds do
meta(:authorize, :all)
field(:enabled, :boolean, description: "Whether the instance-wide feeds are enabled")
end
object :web_push do
meta(:authorize, :all)
field(:enabled, :boolean, description: "Whether the WebPush feature is enabled")
field(:public_key, :string, description: "The server's public WebPush VAPID key")
end
object :analytics do
meta(:authorize, :all)
field(:id, :string, description: "ID of the analytics service")
field(:enabled, :boolean, description: "Whether the service is activated or not")
@ -352,16 +390,19 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
end
object :analytics_configuration do
meta(:authorize, :all)
field(:key, :string, description: "The key for the analytics configuration element")
field(:value, :string, description: "The value for the analytics configuration element")
field(:type, :analytics_configuration_type, description: "The analytics configuration type")
end
object :search_settings do
meta(:authorize, :all)
field(:global, :global_search_settings, description: "The instance's global search settings")
end
object :global_search_settings do
meta(:authorize, :all)
field(:is_enabled, :boolean, description: "Whether global search is enabled")
field(:is_default, :boolean, description: "Whether global search is the default")
end
@ -370,6 +411,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Export formats configuration
"""
object :export_formats do
meta(:authorize, :all)
field(:event_participants, list_of(:string),
description: "The list of formats the event participants can be exported to"
)
@ -379,6 +422,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Event categories list configuration
"""
object :event_category_option do
meta(:authorize, :all)
field(:id, :string, description: "The ID of the event category")
field(:label, :string, description: "The translated name of the event category")
end
@ -386,6 +430,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
object :config_queries do
@desc "Get the instance config"
field :config, :config do
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Config.get_config/3)
end
end

View File

@ -11,6 +11,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "A comment"
object :comment do
meta(:authorize, :all)
interfaces([:action_log_object, :activity_object])
field(:id, :id, description: "Internal ID for this comment")
field(:uuid, :uuid, description: "An UUID for this comment")
@ -73,6 +74,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "A paginated list of comments"
object :paginated_comment_list do
meta(:authorize, :all)
field(:elements, list_of(:comment), description: "A list of comments")
field(:total, :integer, description: "The total number of comments in the list")
end
@ -81,6 +83,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "Get replies for thread"
field :thread, type: list_of(:comment) do
arg(:id, non_null(:id), description: "The comment ID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Comment.get_thread/3)
end
end
@ -95,6 +98,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Comment,
rule: :"write:comment:create",
args: %{event_id: :event_id}
)
resolve(&Comment.create_comment/3)
end
@ -106,6 +116,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Comment,
rule: :"write:comment:update",
args: %{id: :comment_id}
)
resolve(&Comment.update_comment/3)
end
@ -113,6 +130,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
field :delete_comment, type: :comment do
arg(:comment_id, non_null(:id), description: "The comment ID")
middleware(Rajska.QueryAuthorization,
permit: [:user, :moderator],
scope: Mobilizon.Discussions.Comment,
rule: :"write:comment:delete",
args: %{id: :comment_id}
)
resolve(&Comment.delete_comment/3)
end
end

View File

@ -11,6 +11,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
@desc "A discussion"
object :discussion do
meta(:authorize, :user)
interfaces([:activity_object])
field(:id, :id, description: "Internal ID for this discussion")
field(:title, :string, description: "The title for this discussion")
@ -36,6 +37,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
@desc "A paginated list of discussions"
object :paginated_discussion_list do
meta(:authorize, :user)
field(:elements, list_of(:discussion), description: "A list of discussion")
field(:total, :integer, description: "The total number of discussions in the list")
end
@ -45,6 +47,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :discussion, type: :discussion do
arg(:id, :id, description: "The discussion's ID")
arg(:slug, :string, description: "The discussion's slug")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"read:group:discussions"
)
resolve(&Discussion.get_discussion/3)
end
end
@ -56,6 +65,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
arg(:text, non_null(:string), description: "The discussion's first comment body")
arg(:actor_id, non_null(:id), description: "The discussion's group ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:create",
args: %{actor_id: :actor_id}
)
resolve(&Discussion.create_discussion/3)
end
@ -63,6 +79,14 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :reply_to_discussion, type: :discussion do
arg(:discussion_id, non_null(:id), description: "The discussion's ID")
arg(:text, non_null(:string), description: "The discussion's reply body")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:update",
args: %{id: :discussion_id}
)
resolve(&Discussion.reply_to_discussion/3)
end
@ -70,6 +94,14 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :update_discussion, type: :discussion do
arg(:title, non_null(:string), description: "The updated discussion's title")
arg(:discussion_id, non_null(:id), description: "The discussion's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:update",
args: %{id: :discussion_id}
)
resolve(&Discussion.update_discussion/3)
end
@ -77,6 +109,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :delete_discussion, type: :discussion do
arg(:discussion_id, non_null(:id), description: "The discussion's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:delete",
args: %{id: :discussion_id}
)
resolve(&Discussion.delete_discussion/3)
end
end

View File

@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
@desc "An event"
object :event do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:action_log_object, :interactable, :activity_object, :event_search_result])
field(:id, :id, description: "Internal ID for this event")
field(:uuid, :uuid, description: "The Event UUID")
@ -61,10 +63,9 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "The event's organizer (as a person)"
)
field(:tags, list_of(:tag),
resolve: &Tag.list_tags_for_event/3,
description: "The event's tags"
)
field(:tags, list_of(:tag), description: "The event's tags") do
resolve(&Tag.list_tags_for_event/3)
end
field(:category, :event_category, description: "The event's category")
@ -75,7 +76,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
resolve: &Event.stats_participants/3
)
field(:participants, :paginated_participant_list, description: "The event's participants") do
field(:participants, :paginated_participant_list,
description: "The event's participants",
meta: [private: true, rule: :"read:event:participants"]
) do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated participants list"
@ -134,12 +138,14 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
@desc "A paginated list of events"
object :paginated_event_list do
meta(:authorize, :all)
field(:elements, list_of(:event), description: "A list of events")
field(:total, :integer, description: "The total number of events in the list")
end
@desc "Participation statistics"
object :participant_stats do
meta(:authorize, :all)
field(:going, :integer, description: "The number of approved participants")
field(:not_approved, :integer, description: "The number of not approved participants")
field(:not_confirmed, :integer, description: "The number of not confirmed participants")
@ -158,6 +164,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
An event offer
"""
object :event_offer do
meta(:authorize, :all)
field(:price, :float, description: "The price amount for this offer")
field(:price_currency, :string, description: "The currency for this price offer")
field(:url, :string, description: "The URL to access to this offer")
@ -167,6 +174,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
An event participation condition
"""
object :event_participation_condition do
meta(:authorize, :all)
field(:title, :string, description: "The title for this condition")
field(:content, :string, description: "The content for this condition")
field(:url, :string, description: "The URL to access this condition")
@ -201,6 +209,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
Event options
"""
object :event_options do
meta(:authorize, :all)
field(:maximum_attendee_capacity, :integer,
description: "The maximum attendee capacity for this event"
)
@ -307,6 +317,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
end
object :event_metadata do
meta(:authorize, :all)
field(:key, :string, description: "The key for the metadata")
field(:title, :string, description: "The title for the metadata")
field(:value, :string, description: "The value for the metadata")
@ -350,12 +361,15 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "Direction for the sort"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Event.list_events/3)
end
@desc "Get an event by uuid"
field :event, :event do
arg(:uuid, non_null(:uuid), description: "The event's UUID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Event.find_event/3)
end
end
@ -416,6 +430,13 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts")
arg(:language, :string, description: "The event language", default_value: "und")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Event,
rule: :"write:event:create",
args: %{organizer_actor_id: :organizer_actor_id}
)
resolve(&Event.create_event/3)
end
@ -460,6 +481,13 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts")
arg(:language, :string, description: "The event language", default_value: "und")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Event,
args: %{id: :event_id},
rule: :"write:event:update"
)
resolve(&Event.update_event/3)
end
@ -467,6 +495,13 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
field :delete_event, :deleted_object do
arg(:event_id, non_null(:id), description: "The event ID to delete")
middleware(Rajska.QueryAuthorization,
permit: [:user, :moderator, :administrator],
scope: Mobilizon.Events.Event,
rule: :"write:event:delete",
args: %{id: :event_id}
)
resolve(&Event.delete_event/3)
end
end

View File

@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
or an Atom feed for just a profile.
"""
object :feed_token do
meta(:authorize, :user)
field(
:actor,
:actor,
@ -36,6 +38,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
@desc "Represents a deleted feed_token"
object :deleted_feed_token do
meta(:authorize, :user)
field(:user, :deleted_object, description: "The user that owned the deleted feed token")
field(:actor, :deleted_object, description: "The actor that owned the deleted feed token")
end
@ -45,6 +48,13 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
field :create_feed_token, :feed_token do
arg(:actor_id, :id, description: "The actor ID for the feed token")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.FeedToken,
rule: :"write:profile:feed_token:create",
args: %{}
)
resolve(&FeedToken.create_feed_token/3)
end
@ -52,6 +62,13 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
field :delete_feed_token, :deleted_feed_token do
arg(:token, non_null(:string), description: "The token to delete")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.FeedToken,
rule: :"write:feed_token:delete",
args: %{token: :token}
)
resolve(&FeedToken.delete_feed_token/3)
end
end

View File

@ -12,6 +12,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
@desc "Represents a participant to an event"
object :participant do
meta(:authorize, :all)
field(:id, :id, description: "The participation ID")
field(
@ -41,6 +42,8 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
Metadata about a participant
"""
object :participant_metadata do
meta(:authorize, :all)
field(:cancellation_token, :string,
description: "The eventual token to leave an event when user is anonymous"
)
@ -53,6 +56,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
A paginated list of participants
"""
object :paginated_participant_list do
meta(:authorize, :user)
field(:elements, list_of(:participant), description: "A list of participants")
field(:total, :integer, description: "The total number of participants in the list")
end
@ -78,6 +82,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
@desc "Represents a deleted participant"
object :deleted_participant do
meta(:authorize, :all)
field(:id, :id, description: "The participant ID")
field(:event, :deleted_object, description: "The participant's event")
field(:actor, :deleted_object, description: "The participant's actor")
@ -92,7 +97,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:message, :string, description: "The anonymous participant's message")
arg(:locale, :string, description: "The anonymous participant's locale")
arg(:timezone, :string, description: "The anonymous participant's timezone")
middleware(Rajska.QueryAuthorization, permit: :all, rule: :"write:participation")
resolve(&Participant.actor_join_event/3)
end
@ -101,7 +106,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:event_id, non_null(:id), description: "The event ID the participant left")
arg(:actor_id, non_null(:id), description: "The actor ID for the participant")
arg(:token, :string, description: "The anonymous participant participation token")
middleware(Rajska.QueryAuthorization, permit: :all, rule: :"write:participation")
resolve(&Participant.actor_leave_event/3)
end
@ -110,12 +115,19 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:id, non_null(:id), description: "The participant ID")
arg(:role, non_null(:participant_role_enum), description: "The participant new role")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Participant,
rule: :"write:participation"
)
resolve(&Participant.update_participation/3)
end
@desc "Confirm a participation"
field :confirm_participation, :participant do
arg(:confirmation_token, non_null(:string), description: "The participation token")
middleware(Rajska.QueryAuthorization, permit: :all, rule: :"write:participation")
resolve(&Participant.confirm_participation_from_token/3)
end
@ -131,6 +143,14 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
)
arg(:format, :export_format_enum, description: "The format in which to return the file")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Event,
rule: :"read:event:participants:export",
args: %{id: :event_id}
)
resolve(&Participant.export_event_participants/3)
end
end

View File

@ -6,12 +6,14 @@ defmodule Mobilizon.GraphQL.Schema.FollowedGroupActivityType do
@desc "A paginated list of follow group events"
object :paginated_followed_group_events do
meta(:authorize, :user)
field(:elements, list_of(:followed_group_event), description: "A list of follow group events")
field(:total, :integer, description: "The total number of follow group events in the list")
end
@desc "A follow group event"
object :followed_group_event do
meta(:authorize, :user)
field(:user, :user)
field(:profile, :person)
field(:group, :group)

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
@desc "A media"
object :media do
meta(:authorize, :all)
field(:id, :id, description: "The media's ID")
field(:alt, :string, description: "The media's alternative text")
field(:name, :string, description: "The media's name")
@ -21,6 +22,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
A paginated list of medias
"""
object :paginated_media_list do
meta(:authorize, :all)
field(:elements, list_of(:media), description: "The list of medias")
field(:total, :integer, description: "The total number of medias in the list")
end
@ -29,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
Some metadata associated with a media
"""
object :media_metadata do
meta(:authorize, :all)
field(:width, :integer, description: "The media width (if a picture)")
field(:height, :integer, description: "The media width (if a height)")
field(:blurhash, :string, description: "The media blurhash (if a picture")
@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
@desc "Get a media"
field :media, :media do
arg(:id, non_null(:id), description: "The media ID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Media.media/3)
end
end
@ -64,6 +68,15 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
arg(:name, non_null(:string), description: "The media's name")
arg(:alt, :string, description: "The media's alternative text")
arg(:file, non_null(:upload), description: "The media file")
arg(:actor_id, :id, description: "The actor that uploads the media")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Medias.Media,
rule: :"write:media:upload",
args: %{}
)
resolve(&Media.upload_media/3)
end
@ -72,6 +85,13 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
"""
field :remove_media, :deleted_object do
arg(:id, non_null(:id), description: "The media's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Medias.Media,
rule: :"write:media:remove"
)
resolve(&Media.remove_media/3)
end
end

View File

@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
@desc "A post"
object :post do
meta(:authorize, :all)
interfaces([:activity_object])
field(:id, :id, description: "The post's ID")
field(:title, :string, description: "The post's title")
@ -22,21 +23,20 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
field(:updated_at, :datetime, description: "The post's last update date")
field(:language, :string, description: "The post language")
field(:tags, list_of(:tag),
resolve: &Tag.list_tags_for_post/3,
description: "The post's tags"
)
field(:tags, list_of(:tag), description: "The post's tags") do
resolve(&Tag.list_tags_for_post/3)
end
field(:picture, :media,
description: "The posts's media",
resolve: &Media.media/3
)
field(:picture, :media, description: "The posts's media") do
resolve(&Media.media/3)
end
end
@desc """
A paginated list of posts
"""
object :paginated_post_list do
meta(:authorize, :all)
field(:elements, list_of(:post), description: "A list of posts")
field(:total, :integer, description: "The total number of posts in the list")
end
@ -56,6 +56,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
@desc "Get a post"
field :post, :post do
arg(:slug, non_null(:string), description: "The post's slug")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Post.get_post/3)
end
end
@ -84,6 +85,13 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
"The banner for the post, either as an object or directly the ID of an existing media"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Posts.Post,
rule: :"write:group:post:create",
args: %{}
)
resolve(&Post.create_post/3)
end
@ -108,12 +116,25 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
"The banner for the post, either as an object or directly the ID of an existing media"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Posts.Post,
rule: :"write:group:post:update"
)
resolve(&Post.update_post/3)
end
@desc "Delete a post"
field :delete_post, :deleted_object do
arg(:id, non_null(:id), description: "The post's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Posts.Post,
rule: :"write:group:post:delete"
)
resolve(&Post.delete_post/3)
end
end

View File

@ -11,11 +11,12 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
@desc "A report object"
object :report do
meta(:authorize, :all)
interfaces([:action_log_object])
field(:id, :id, description: "The internal ID of the report")
field(:content, :string, description: "The comment the reporter added about this report")
field(:status, :report_status, description: "Whether the report is still active")
field(:uri, :string, description: "The URI of the report")
field(:uri, :string, description: "The URI of the report", meta: [private: true])
field(:reported, :actor, description: "The actor that is being reported")
field(:reporter, :actor, description: "The actor that created the report")
field(:event, :event, description: "The event that is being reported")
@ -23,6 +24,7 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
field(:notes, list_of(:report_note),
description: "The notes made on the event",
meta: [private: true],
resolve: dataloader(Reports)
)
@ -31,12 +33,14 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
end
object :paginated_report_list do
meta(:authorize, :moderator)
field(:elements, list_of(:report), description: "A list of reports")
field(:total, :integer, description: "The total number of reports in the list")
end
@desc "A report note object"
object :report_note do
meta(:authorize, :moderator)
interfaces([:action_log_object])
field(:id, :id, description: "The internal ID of the report note")
field(:content, :string, description: "The content of the note")
@ -73,12 +77,20 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
arg(:limit, :integer, default_value: 10, description: "The limit of reports per page")
arg(:status, :report_status, default_value: :open, description: "Filter reports by status")
arg(:domain, :string, default_value: nil, description: "Filter reports by domain name")
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Report,
args: %{}
)
resolve(&Report.list_reports/3)
end
@desc "Get a report by id"
field :report, :report do
arg(:id, non_null(:id), description: "The report ID")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: Mobilizon.Reports.Report)
resolve(&Report.get_report/3)
end
end
@ -101,6 +113,8 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
"Whether to forward the report to the original instance if the content is remote"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Report.create_report/3)
end
@ -113,6 +127,12 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
description: "The feedback to send to the anti-spam system"
)
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Report,
args: %{id: :report_id}
)
resolve(&Report.update_report/3)
end
@ -120,12 +140,26 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
field :create_report_note, type: :report_note do
arg(:content, :string, description: "The note's content")
arg(:report_id, non_null(:id), description: "The report's ID")
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Report,
args: %{id: :report_id}
)
resolve(&Report.create_report_note/3)
end
@desc "Delete a note on a report"
field :delete_report_note, type: :deleted_object do
arg(:note_id, non_null(:id), description: "The note's ID")
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Note,
args: %{id: :note_id}
)
resolve(&Report.delete_report_note/3)
end
end

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
@desc "A resource"
object :resource do
meta(:authorize, :user)
interfaces([:activity_object])
field(:id, :id, description: "The resource's ID")
field(:title, :string, description: "The resource's title")
@ -44,6 +45,7 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
A paginated list of resources
"""
object :paginated_resource_list do
meta(:authorize, :user)
field(:elements, list_of(:resource), description: "A list of resources")
field(:total, :integer, description: "The total number of resources in the list")
end
@ -52,6 +54,7 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
The metadata associated to the resource
"""
object :resource_metadata do
meta(:authorize, :user)
field(:type, :string, description: "The type of the resource")
field(:title, :string, description: "The resource's metadata title")
field(:description, :string, description: "The resource's metadata description")
@ -84,6 +87,13 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
description: "The federated username for the group resource"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"read:group:resources",
args: %{path: :path}
)
resolve(&Resource.get_resource/3)
end
end
@ -101,6 +111,13 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
arg(:resource_url, :string, description: "This resource's own original URL")
arg(:type, :string, default_value: "link", description: "The type for this resource")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"write:group:resources:create",
args: %{actor_id: :actor_id}
)
resolve(&Resource.create_resource/3)
end
@ -112,18 +129,39 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
arg(:parent_id, :id, description: "The new resource parent ID (if the resource is moved)")
arg(:resource_url, :string, description: "The new resource URL")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"write:group:resources:update"
)
resolve(&Resource.update_resource/3)
end
@desc "Delete a resource"
field :delete_resource, :deleted_object do
arg(:id, non_null(:id), description: "The resource ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"write:group:resources:delete"
)
resolve(&Resource.delete_resource/3)
end
@desc "Get a preview for a resource link"
field :preview_resource_link, :resource_metadata do
arg(:resource_url, non_null(:string), description: "The link to crawl to get of preview of")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"read:group:resources",
args: %{}
)
resolve(&Resource.preview_resource_link/3)
end
end

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult}
interface :event_search_result do
meta(:authorize, :all)
field(:id, :id, description: "Internal ID for this event")
field(:uuid, :uuid, description: "The Event UUID")
field(:url, :string, description: "The ActivityPub Event URL")
@ -43,6 +44,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
@desc "Search event result"
object :event_result do
meta(:authorize, :all)
interfaces([:event_search_result])
field(:id, :id, description: "Internal ID for this event")
field(:uuid, :uuid, description: "The Event UUID")
@ -65,6 +67,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
end
interface :group_search_result do
meta(:authorize, :all)
field(:id, :id, description: "Internal ID for this group")
field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
@ -92,6 +95,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
@desc "Search group result"
object :group_result do
meta(:authorize, :all)
interfaces([:group_search_result])
field(:id, :id, description: "Internal ID for this group")
field(:url, :string, description: "The ActivityPub actor's URL")
@ -109,18 +113,21 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
@desc "Search persons result"
object :persons do
meta(:authorize, [:administrator, :moderator])
field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:person)), description: "Person elements")
end
@desc "Search groups result"
object :groups do
meta(:authorize, :all)
field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:group_search_result)), description: "Group elements")
end
@desc "Search events result"
object :events do
meta(:authorize, :all)
field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:event_search_result)), description: "Event elements")
end
@ -179,7 +186,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
arg(:term, :string, default_value: "", description: "Search term")
arg(:page, :integer, default_value: 1, description: "Result page")
arg(:limit, :integer, default_value: 10, description: "Results limit per page")
middleware(Rajska.QueryAuthorization, permit: [:administrator, :moderator], scope: false)
resolve(&Search.search_persons/3)
end
@ -225,6 +232,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "How to sort search results"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Search.search_groups/3)
end
@ -275,13 +283,14 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "How to sort search results"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Search.search_events/3)
end
@desc "Interact with an URI"
field :interact, :interactable do
arg(:uri, non_null(:string), description: "The URI for to interact with")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Search.interact/3)
end
end

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
@desc "A statistics object"
object :statistics do
meta(:authorize, :all)
# Instance name
field(:number_of_users, :integer, description: "The number of local users")
field(:number_of_events, :integer, description: "The total number of events")
@ -27,6 +28,7 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
end
object :category_statistics do
meta(:authorize, :all)
field(:key, :string, description: "The key for the category")
field(:number, :integer, description: "The number of events for the given category")
end
@ -34,11 +36,13 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
object :statistics_queries do
@desc "Get the instance statistics"
field :statistics, :statistics do
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Statistics.get_statistics/3)
end
@desc "Get the instance's category statistics"
field :category_statistics, list_of(:category_statistics) do
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Statistics.get_category_statistics/3)
end
end

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.TagType do
@desc "A tag"
object :tag do
meta(:authorize, :all)
field(:id, :id, description: "The tag's ID")
field(:slug, :string, description: "The tags's slug")
field(:title, :string, description: "The tag's title")
@ -26,6 +27,7 @@ defmodule Mobilizon.GraphQL.Schema.TagType do
arg(:filter, :string, description: "The filter to apply to the search")
arg(:page, :integer, default_value: 1, description: "The page in the paginated tags list")
arg(:limit, :integer, default_value: 10, description: "The limit of tags per page")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Tag.list_tags/3)
end
end

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
@desc "A todo"
object :todo do
meta(:authorize, :user)
field(:id, :id, description: "The todo's ID")
field(:title, :string, description: "The todo's title")
field(:status, :boolean, description: "The todo's status")
@ -30,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
A paginated list of todos
"""
object :paginated_todo_list do
meta(:authorize, :user)
field(:elements, list_of(:todo), description: "A list of todos")
field(:total, :integer, description: "The total number of todos in the list")
end
@ -38,6 +40,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
@desc "Get a todo"
field :todo, :todo do
arg(:id, non_null(:id), description: "The todo ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&TodoResolver.get_todo/3)
end
end
@ -50,6 +53,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
arg(:status, :boolean, description: "The todo status")
arg(:due_date, :datetime, description: "The todo due date")
arg(:assigned_to_id, :id, description: "The actor this todo is assigned to")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&TodoResolver.create_todo/3)
end
@ -62,7 +66,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
arg(:status, :boolean, description: "The new todo status")
arg(:due_date, :datetime, description: "The new todo due date")
arg(:assigned_to_id, :id, description: "The new id of the actor this todo is assigned to")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&TodoResolver.update_todo/3)
end

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
@desc "A todo list"
object :todo_list do
meta(:authorize, :user)
field(:id, :id, description: "The todo list's ID")
field(:title, :string, description: "The todo list's title")
@ -37,6 +38,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
A paginated list of todo-lists
"""
object :paginated_todo_list_list do
meta(:authorize, :user)
field(:elements, list_of(:todo_list), description: "A list of todo lists")
field(:total, :integer, description: "The total number of todo lists in the list")
end
@ -45,6 +47,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
@desc "Get a todo list"
field :todo_list, :todo_list do
arg(:id, non_null(:id), description: "The todo-list ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Todos.get_todo_list/3)
end
end
@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
field :create_todo_list, :todo_list do
arg(:title, non_null(:string), description: "The todo list title")
arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Todos.create_todo_list/3)
end
end

View File

@ -15,6 +15,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "A local user of Mobilizon"
object :user do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:action_log_object])
field(:id, :id, description: "The user's ID")
field(:email, non_null(:string), description: "The user's email")
@ -63,7 +65,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field(:disabled, :boolean, description: "Whether the user is disabled")
field(:participations, :paginated_participant_list,
description: "The list of participations this user has"
description: "The list of participations this user has",
meta: [private: true]
) do
arg(:after_datetime, :datetime, description: "Filter participations by event start datetime")
@ -83,7 +86,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end
field(:memberships, :paginated_member_list,
description: "The list of memberships for this user"
description: "The list of memberships for this user",
meta: [private: true]
) do
arg(:name, :string, description: "A name to filter members by")
@ -97,7 +101,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end
field(:drafts, :paginated_event_list,
description: "The list of draft events this user has created"
description: "The list of draft events this user has created",
meta: [private: true]
) do
arg(:page, :integer,
default_value: 1,
@ -109,7 +114,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end
field(:followed_group_events, :paginated_followed_group_events,
description: "The suggested events from the groups this user follows"
description: "The suggested events from the groups this user follows",
meta: [private: true]
) do
arg(:page, :integer,
default_value: 1,
@ -128,7 +134,10 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
resolve(&User.user_followed_group_events/3)
end
field(:settings, :user_settings, description: "The list of settings for this user") do
field(:settings, :user_settings,
description: "The list of settings for this user",
meta: [private: true]
) do
resolve(&User.user_settings/3)
end
@ -142,7 +151,10 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "The IP adress the user's currently signed-in with"
)
field(:media, :paginated_media_list, description: "The user's media objects") do
field(:media, :paginated_media_list,
description: "The user's media objects",
meta: [private: true]
) do
arg(:page, :integer,
default_value: 1,
description: "The page in the paginated user media list"
@ -158,14 +170,18 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
)
field(:activity_settings, list_of(:activity_setting),
resolve: &ActivitySettings.user_activity_settings/3,
description: "The user's activity settings"
)
description: "The user's activity settings",
meta: [private: true]
) do
resolve(&ActivitySettings.user_activity_settings/3)
end
field(:auth_authorized_applications, list_of(:auth_application_token),
resolve: &Application.get_user_applications/3,
description: "The user's authorized authentication apps"
)
description: "The user's authorized authentication apps",
meta: [private: true, rule: :forbid_app_access]
) do
resolve(&Application.get_user_applications/3)
end
end
@desc "The list of roles an user can have"
@ -177,12 +193,14 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "Token"
object :refreshed_token do
meta(:authorize, :all)
field(:access_token, non_null(:string), description: "Generated access token")
field(:refresh_token, non_null(:string), description: "Generated refreshed token")
end
@desc "Users list"
object :users do
meta(:authorize, [:administrator, :moderator])
field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:user)), description: "User elements")
end
@ -196,6 +214,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
A set of user settings
"""
object :user_settings do
meta(:authorize, :user)
field(:timezone, :string, description: "The timezone for this user")
field(:notification_on_day, :boolean,
@ -254,6 +273,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end
object :location do
meta(:authorize, :user)
field(:range, :integer, description: "The range in kilometers the user wants to see events")
field(:geohash, :string, description: "A geohash representing the user's preferred location")
@ -276,11 +296,13 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "Get an user"
field :user, :user do
arg(:id, non_null(:id))
middleware(Rajska.QueryAuthorization, permit: [:administrator, :moderator], scope: false)
resolve(&User.find_user/3)
end
@desc "Get the current user"
field :logged_user, :user do
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.get_current_user/3)
end
@ -297,7 +319,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
arg(:sort, :sortable_user_field, default_value: :id, description: "Sort column")
arg(:direction, :sort_direction, default_value: :desc, description: "Sort direction")
middleware(Rajska.QueryAuthorization, permit: [:administrator, :moderator], scope: false)
resolve(&User.list_users/3)
end
end
@ -308,7 +330,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
arg(:email, non_null(:string), description: "The new user's email")
arg(:password, non_null(:string), description: "The new user's password")
arg(:locale, :string, description: "The new user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.create_user/3)
end
@ -318,6 +340,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "The token that will be used to validate the user"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.validate_user/3)
end
@ -325,6 +348,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :resend_confirmation_email, type: :string do
arg(:email, non_null(:string), description: "The email used to register")
arg(:locale, :string, description: "The user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.resend_confirmation_email/3)
end
@ -332,6 +356,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :send_reset_password, type: :string do
arg(:email, non_null(:string), description: "The user's email")
arg(:locale, :string, description: "The user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.send_reset_password/3)
end
@ -343,6 +368,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
arg(:password, non_null(:string), description: "The new password")
arg(:locale, :string, default_value: "en", description: "The user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.reset_password/3)
end
@ -350,24 +376,28 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :login, type: :login do
arg(:email, non_null(:string), description: "The user's email")
arg(:password, non_null(:string), description: "The user's password")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.login_user/3)
end
@desc "Refresh a token"
field :refresh_token, type: :refreshed_token do
arg(:refresh_token, non_null(:string), description: "A refresh token")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.refresh_token/3)
end
@desc "Logout an user, deleting a refresh token"
field :logout, :string do
arg(:refresh_token, non_null(:string))
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.logout/3)
end
@desc "Change default actor for user"
field :change_default_actor, :user do
arg(:preferred_username, non_null(:string), description: "The actor preferred_username")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.change_default_actor/3)
end
@ -375,6 +405,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :change_password, :user do
arg(:old_password, non_null(:string), description: "The user's current password")
arg(:new_password, non_null(:string), description: "The user's new password")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.change_password/3)
end
@ -382,6 +413,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :change_email, :user do
arg(:email, non_null(:string), description: "The user's new email")
arg(:password, non_null(:string), description: "The user's current password")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.change_email/3)
end
@ -391,6 +423,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "The token that will be used to validate the email change"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.validate_email/3)
end
@ -398,6 +431,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :delete_account, :deleted_object do
arg(:password, :string, description: "The user's password")
arg(:user_id, :id, description: "The user's ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.delete_account/3)
end
@ -435,12 +469,14 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "A geohash of the user's preferred location, where they want to see events"
)
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.set_user_setting/3)
end
@desc "Update the user's locale"
field :update_locale, :user do
arg(:locale, :string, description: "The user's new locale")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.update_locale/3)
end
end

View File

@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.Users.ActivitySetting do
alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
object :activity_setting do
meta(:authorize, :user)
field(:key, :string)
field(:method, :string)
field(:enabled, :boolean)
@ -17,6 +18,13 @@ defmodule Mobilizon.GraphQL.Schema.Users.ActivitySetting do
arg(:key, non_null(:string))
arg(:method, non_null(:string))
arg(:enabled, non_null(:boolean))
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: false,
rule: :"write:user:setting:activity"
)
resolve(&ActivitySettings.upsert_user_activity_setting/3)
end
end

View File

@ -26,11 +26,25 @@ defmodule Mobilizon.GraphQL.Schema.Users.PushSubscription do
arg(:endpoint, non_null(:string))
arg(:auth, non_null(:string))
arg(:p256dh, non_null(:string))
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: false,
rule: :"write:user:setting:push"
)
resolve(&PushSubscription.register_push_subscription/3)
end
field :unregister_push, :string do
arg(:endpoint, non_null(:string))
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: false,
rule: :"write:user:setting:push"
)
resolve(&PushSubscription.unregister_push_subscription/3)
end
end

View File

@ -12,6 +12,7 @@ defmodule Mobilizon.Applications do
defenum(ApplicationDeviceActivationStatus, [
"success",
"pending",
"confirmed",
"incorrect_device_code",
"access_denied"
])
@ -144,12 +145,15 @@ defmodule Mobilizon.Applications do
"""
def list_application_tokens do
Repo.all(ApplicationToken)
ApplicationToken
|> Repo.all()
|> Repo.preload(:application)
end
@doc """
Returns the list of application tokens for a given user_id
"""
@spec list_application_tokens_for_user_id(number() | String.t()) :: list(ApplicationToken.t())
def list_application_tokens_for_user_id(user_id) do
ApplicationToken
|> where(user_id: ^user_id)
@ -172,7 +176,11 @@ defmodule Mobilizon.Applications do
** (Ecto.NoResultsError)
"""
def get_application_token!(id), do: Repo.get!(ApplicationToken, id)
def get_application_token!(id) do
ApplicationToken
|> Repo.get!(id)
|> Repo.preload([:application])
end
@doc """
Gets a single application_token.
@ -211,6 +219,13 @@ defmodule Mobilizon.Applications do
%ApplicationToken{}
|> ApplicationToken.changeset(attrs)
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:user_id, :application_id])
|> case do
{:ok, application_token} ->
{:ok, Repo.preload(application_token, :application)}
error ->
error
end
end
@doc """
@ -303,10 +318,14 @@ defmodule Mobilizon.Applications do
def get_application_device_activation(id), do: Repo.get(ApplicationDeviceActivation, id)
def get_application_device_activation_by_user_code(user_code),
do: Repo.get_by(ApplicationDeviceActivation, user_code: user_code)
def get_application_device_activation_by_user_code(user_code) do
ApplicationDeviceActivation
|> where([ada], ada.user_code == ^user_code)
|> preload(:application)
|> Repo.one()
end
def get_application_device_activation(client_id, device_code) do
def get_application_device_activation_by_device_code(client_id, device_code) do
ApplicationDeviceActivation
|> join(:left, [ada], a in assoc(ada, :application))
|> where([_, a], a.client_id == ^client_id)

View File

@ -6,16 +6,16 @@ defmodule Mobilizon.Applications.Application do
use Ecto.Schema
import Ecto.Changeset
@required_attrs [:name, :client_id, :client_secret, :redirect_uris]
@optional_attrs [:scopes, :website, :owner_type, :owner_id]
@required_attrs [:name, :client_id, :client_secret, :redirect_uris, :scope]
@optional_attrs [:website, :owner_type, :owner_id]
@attrs @required_attrs ++ @optional_attrs
schema "applications" do
field(:name, :string)
field(:client_id, :string)
field(:client_secret, :string)
field(:redirect_uris, :string)
field(:scopes, :string)
field(:redirect_uris, {:array, :string})
field(:scope, :string)
field(:website, :string)
field(:owner_type, :string)
field(:owner_id, :integer)

View File

@ -19,7 +19,7 @@ defmodule Mobilizon.Applications.ApplicationDeviceActivation do
timestamps()
end
@required_attrs [:user_code, :device_code, :expires_in, :application_id]
@required_attrs [:user_code, :device_code, :expires_in, :application_id, :scope]
@optional_attrs [:status, :user_id]
@attrs @required_attrs ++ @optional_attrs

View File

@ -11,7 +11,7 @@ defmodule Mobilizon.Applications.ApplicationToken do
belongs_to(:user, User)
belongs_to(:application, Application)
field(:authorization_code, :string)
field(:status, ApplicationTokenStatus)
field(:status, ApplicationTokenStatus, default: :pending)
field(:scope, :string)
timestamps()

View File

@ -4,9 +4,12 @@ defmodule Mobilizon.Service.Auth.Applications do
"""
alias Mobilizon.Applications
alias Mobilizon.Applications.{Application, ApplicationDeviceActivation, ApplicationToken}
alias Mobilizon.GraphQL.Authorization.AppScope
alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Users.User
alias Mobilizon.Web.Auth.Guardian
alias Mobilizon.Web.Router.Helpers, as: Routes
require Logger
@app_access_tokens_ttl {8, :hour}
@app_refresh_tokens_ttl {26, :week}
@ -20,37 +23,43 @@ defmodule Mobilizon.Service.Auth.Applications do
required(:token_type) => String.t()
}
def create(name, redirect_uris, scopes, website) do
client_id = :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42)
client_secret = :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42)
@spec create(String.t(), list(String.t()), String.t(), String.t() | nil) ::
{:ok, Application.t()} | {:error, Ecto.Changeset.t()} | {:error, :invalid_scope}
def create(name, redirect_uris, scope, website \\ nil) do
if AppScope.scopes_valid?(scope) do
client_id = :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42)
client_secret = :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42)
Applications.create_application(%{
name: name,
redirect_uris: redirect_uris,
scopes: scopes,
website: website,
client_id: client_id,
client_secret: client_secret
})
Applications.create_application(%{
name: name,
redirect_uris: redirect_uris,
scope: scope,
website: website,
client_id: client_id,
client_secret: client_secret
})
else
{:error, :invalid_scope}
end
end
@spec autorize(String.t(), String.t(), String.t(), integer()) ::
{:ok, String.t()}
{:ok, ApplicationToken.t()}
| {:error, :application_not_found}
| {:error, :redirect_uri_not_in_allowed}
def autorize(client_id, redirect_uri, _scope, user_id) do
| {:error, Ecto.Changeset.t()}
def autorize(client_id, redirect_uri, scope, user_id) do
with %Application{redirect_uris: redirect_uris, id: app_id} <-
Applications.get_application_by_client_id(client_id),
{:redirect_uri, true} <-
{:redirect_uri, redirect_uri in String.split(redirect_uris, "\n")},
code <- :crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16),
{:ok, %ApplicationToken{}} <-
Applications.create_application_token(%{
user_id: user_id,
application_id: app_id,
authorization_code: code
}) do
{:ok, code}
{:redirect_uri, redirect_uri in redirect_uris},
code <- :crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16) do
Applications.create_application_token(%{
user_id: user_id,
application_id: app_id,
authorization_code: code,
scope: scope
})
else
nil ->
{:error, :application_not_found}
@ -60,35 +69,62 @@ defmodule Mobilizon.Service.Auth.Applications do
end
end
@spec autorize_device_application(String.t(), String.t()) ::
{:ok, ApplicationDeviceActivation.t()}
| {:error, Ecto.Changeset.t()}
| {:error, :expired}
| {:error, :access_denied}
| {:error, :not_found}
def autorize_device_application(client_id, user_code) do
case Applications.get_application_device_activation(client_id, user_code) do
%ApplicationDeviceActivation{status: :confirmed} = app_device_activation ->
Applications.update_application_device_activation(app_device_activation, %{
status: :success
})
Logger.debug(
"Authorizing device application client_id: #{client_id}, user_code: #{user_code}"
)
case Applications.get_application_device_activation_by_user_code(user_code) do
%ApplicationDeviceActivation{
status: :confirmed,
application: %Application{client_id: ^client_id}
} = app_device_activation ->
if device_activation_expired?(app_device_activation) do
{:error, :expired}
else
Applications.update_application_device_activation(app_device_activation, %{
status: :success
})
end
# The device activation is confirmed, but does not match the given app client_id, so we say it's not found
%ApplicationDeviceActivation{status: :confirmed} ->
{:error, :not_found}
%ApplicationDeviceActivation{} ->
{:error, :not_confirmed}
nil ->
{:error, :not_found}
end
end
@spec generate_access_token(String.t(), String.t(), String.t(), String.t()) ::
@spec generate_access_token(String.t(), String.t(), String.t(), String.t(), String.t()) ::
{:ok, access_token_details()}
| {:error,
:application_not_found
| :redirect_uri_not_in_allowed
| :provided_code_does_not_match
| :invalid_client_secret
| :app_token_not_found
| :invalid_or_expired
| any()}
def generate_access_token(client_id, client_secret, code, redirect_uri) do
def generate_access_token(client_id, client_secret, code, redirect_uri, scope) do
with {:application,
%Application{
id: application_id,
client_secret: app_client_secret,
scopes: scopes,
redirect_uris: redirect_uris
}} <-
{:application, Applications.get_application_by_client_id(client_id)},
# TODO: check that app token scope still are acceptable
{:redirect_uri, true} <-
{:redirect_uri, redirect_uri in String.split(redirect_uris, "\n")},
{:redirect_uri, redirect_uri in redirect_uris},
{:app_token, %ApplicationToken{} = app_token} <-
{:app_token, Applications.get_application_token_by_authorization_code(code)},
{:ok, %ApplicationToken{application_id: application_id_from_token} = app_token} <-
@ -105,7 +141,7 @@ defmodule Mobilizon.Service.Auth.Applications do
expires_in: ttl_to_seconds(@app_access_tokens_ttl),
refresh_token: refresh_token,
refresh_token_expires_in: ttl_to_seconds(@app_refresh_tokens_ttl),
scope: scopes,
scope: scope,
token_type: "bearer"
}}
else
@ -122,15 +158,20 @@ defmodule Mobilizon.Service.Auth.Applications do
{:error, :redirect_uri_not_in_allowed}
{:app_token, _} ->
{:error, :app_token_not_found}
{:error, :invalid_or_expired}
{:error, err} ->
{:error, err}
end
end
def generate_access_token(client_id, device_code) do
case Applications.get_application_device_activation(client_id, device_code) do
def generate_access_token_for_device_flow(client_id, device_code) do
Logger.debug("Generating access token for application device with",
client_id: client_id,
device_code: device_code
)
case Applications.get_application_device_activation_by_device_code(client_id, device_code) do
%ApplicationDeviceActivation{status: :success, scope: scope, user_id: user_id} =
app_device_activation ->
if device_activation_expired?(app_device_activation) do
@ -164,13 +205,17 @@ defmodule Mobilizon.Service.Auth.Applications do
end
%ApplicationDeviceActivation{status: :incorrect_device_code} ->
Logger.error("Incorrect device code set")
{:error, :incorrect_device_code}
%ApplicationDeviceActivation{status: :access_denied} ->
{:error, :access_denied}
nil ->
Logger.error("nil returned")
{:error, :incorrect_device_code}
err ->
require Logger
Logger.error(inspect(err))
{:error, :incorrect_device_code}
end
@ -191,35 +236,42 @@ defmodule Mobilizon.Service.Auth.Applications do
@spec register_device_code(String.t(), String.t() | nil) ::
{:ok, ApplicationDeviceActivation.t()}
| {:error, :application_not_found}
| {:error, :scope_not_included}
| {:error, Ecto.Changeset.t()}
def register_device_code(client_id, scope) do
%Application{} = application = Applications.get_application_by_client_id(client_id)
device_code = string_of_length(40)
user_code = string_of_length(8)
verification_uri = Routes.page_url(Mobilizon.Web.Endpoint, :auth_device)
expires_in = @expires_in
interval = @interval
case Applications.create_application_device_activation(%{
device_code: device_code,
user_code: user_code,
expires_in: expires_in,
application_id: application.id,
scope: scope
}) do
{:ok, %ApplicationDeviceActivation{} = application_device_activation} ->
{:ok,
application_device_activation
|> Map.from_struct()
|> Map.take([:device_code, :user_code, :expires_in])
|> Map.update!(:user_code, &user_code_displayed/1)
|> Map.merge(%{
interval: interval,
verification_uri: verification_uri
})}
with {:app, %Application{scope: app_scope} = application} <-
{:app, Applications.get_application_by_client_id(client_id)},
{device_code, user_code, verification_uri} <-
{string_of_length(40), string_of_length(8),
Routes.page_url(Mobilizon.Web.Endpoint, :auth_device)},
{:scope_included, true} <- {:scope_included, request_scope_valid?(app_scope, scope)},
{:ok, %ApplicationDeviceActivation{} = application_device_activation} <-
Applications.create_application_device_activation(%{
device_code: device_code,
user_code: user_code,
expires_in: @expires_in,
application_id: application.id,
scope: scope
}) do
{:ok,
application_device_activation
|> Map.from_struct()
|> Map.take([:device_code, :user_code, :expires_in])
|> Map.update!(:user_code, &user_code_displayed/1)
|> Map.merge(%{
interval: @interval,
verification_uri: verification_uri
})}
else
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:app, nil} ->
{:error, :application_not_found}
{:scope_included, false} ->
{:error, :scope_not_included}
end
end
@ -245,6 +297,45 @@ defmodule Mobilizon.Service.Auth.Applications do
end
end
@spec refresh_tokens(String.t(), String.t(), String.t()) ::
{:ok, access_token_details()}
| {:error, :invalid_client_credentials}
| {:error, :invalid_refresh_token}
| {:error, any()}
def refresh_tokens(refresh_token, user_client_id, user_client_secret) do
with {:resource_from_token,
{:ok,
%ApplicationToken{
application: %Application{client_id: app_client_id, client_secret: app_client_secret},
scope: scope
} = app_token,
_claims}} <- {:resource_from_token, Guardian.resource_from_token(refresh_token)},
{:valid_client_credentials, true} <-
{:valid_client_credentials,
app_client_id == user_client_id and app_client_secret == user_client_secret},
{:ok, _old, {exchanged_token, _claims}} <-
Guardian.exchange(refresh_token, "refresh", "access", ttl: @app_access_tokens_ttl),
{:ok, new_refresh_token} <-
Authenticator.generate_refresh_token(app_token, @app_refresh_tokens_ttl),
{:ok, _claims} <- Guardian.revoke(refresh_token) do
{:ok,
%{
access_token: exchanged_token,
expires_in: ttl_to_seconds(@app_access_tokens_ttl),
refresh_token: new_refresh_token,
refresh_token_expires_in: ttl_to_seconds(@app_refresh_tokens_ttl),
scope: scope,
token_type: "bearer"
}}
else
{:valid_client_credentials, false} ->
{:error, :invalid_client_credentials}
{:resource_from_token, _} ->
{:error, :invalid_refresh_token}
end
end
defp user_code_displayed(user_code) do
String.slice(user_code, 0..3) <> "-" <> String.slice(user_code, 4..7)
end
@ -265,6 +356,12 @@ defmodule Mobilizon.Service.Auth.Applications do
expires_in: expires_in
}) do
NaiveDateTime.compare(NaiveDateTime.add(inserted_at, expires_in), NaiveDateTime.utc_now()) ==
:gt
:lt
end
defp request_scope_valid?(app_scope, request_scope) do
app_scopes = app_scope |> String.split(" ") |> MapSet.new()
request_scopes = request_scope |> String.split(" ") |> MapSet.new()
MapSet.subset?(request_scopes, app_scopes)
end
end

View File

@ -38,7 +38,7 @@ defmodule Mobilizon.Web.Auth.Context do
|> set_app_token_context(context, app_token)
|> set_user_context(user)
nil ->
_ ->
{conn, context}
end

View File

@ -3,11 +3,12 @@ defmodule Mobilizon.Web.Auth.ErrorHandler do
In case we have an auth error
"""
import Plug.Conn
require Logger
# sobelow_skip ["XSS.SendResp"]
@spec auth_error(Plug.Conn.t(), any(), any()) :: Plug.Conn.t()
def auth_error(conn, {type, _reason}, _opts) do
body = Jason.encode!(%{message: to_string(type)})
def auth_error(conn, {type, reason}, _opts) do
body = Jason.encode!(%{message: to_string(type), details: inspect(reason)})
send_resp(conn, 401, body)
end
end

View File

@ -43,7 +43,9 @@ defmodule Mobilizon.Web.Auth.Guardian do
{:error, :invalid_id}
end
rescue
Ecto.NoResultsError -> {:error, :no_result}
e in Ecto.NoResultsError ->
Logger.warn("Received token claim for non existing user: #{inspect(e)}")
{:error, :no_result}
end
end
@ -62,7 +64,9 @@ defmodule Mobilizon.Web.Auth.Guardian do
{:error, :invalid_id}
end
rescue
Ecto.NoResultsError -> {:error, :no_result}
e in Ecto.NoResultsError ->
Logger.info("Received token claim for non existing app token: #{inspect(e.message)}")
{:error, :no_result}
end
end
@ -79,6 +83,8 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_verify(any(), any(), any()) :: {:ok, map()} | {:error, :token_not_found}
def on_verify(claims, token, _options) do
Logger.debug("[Guardian] Called on_verify")
with {:ok, _} <- Guardian.DB.on_verify(claims, token) do
{:ok, claims}
end
@ -86,6 +92,8 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_revoke(any(), any(), any()) :: {:ok, map()} | {:error, :could_not_revoke_token}
def on_revoke(claims, token, _options) do
Logger.debug("[Guardian] Called on_revoke")
with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do
{:ok, claims}
end
@ -94,6 +102,8 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_refresh({any(), any()}, {any(), any()}, any()) ::
{:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()}
def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do
Logger.debug("[Guardian] Called on_refresh")
with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do
{:ok, {old_token, old_claims}, {new_token, new_claims}}
end
@ -101,7 +111,10 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_exchange(any(), any(), any()) ::
{:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()}
def on_exchange(old_stuff, new_stuff, options), do: on_refresh(old_stuff, new_stuff, options)
def on_exchange(old_stuff, new_stuff, options) do
Logger.debug("[Guardian] Called on_exchange")
on_refresh(old_stuff, new_stuff, options)
end
# def build_claims(claims, _resource, opts) do
# claims = claims

View File

@ -1,7 +1,7 @@
defmodule Mobilizon.Web.ApplicationController do
use Mobilizon.Web, :controller
alias Mobilizon.Applications.{Application, ApplicationDeviceActivation}
alias Mobilizon.Applications.Application
alias Mobilizon.Service.Auth.Applications
plug(:put_layout, false)
import Mobilizon.Web.Gettext, only: [dgettext: 2]
@ -11,11 +11,14 @@ defmodule Mobilizon.Web.ApplicationController do
Create an application
"""
@spec create_application(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create_application(conn, %{"name" => name, "redirect_uris" => redirect_uris} = args) do
def create_application(
conn,
%{"name" => name, "redirect_uris" => redirect_uris, "scope" => scope} = args
) do
case Applications.create(
name,
redirect_uris,
Map.get(args, "scopes"),
String.split(redirect_uris, "\n"),
scope,
Map.get(args, "website")
) do
{:ok, %Application{} = app} ->
@ -24,7 +27,19 @@ defmodule Mobilizon.Web.ApplicationController do
Map.take(app, [:name, :website, :redirect_uris, :client_id, :client_secret, :scope])
)
{:error, _error} ->
{:error, :invalid_scope} ->
send_resp(
conn,
400,
dgettext(
"errors",
"The scope parameter is not a space separated list of valid scopes"
)
)
{:error, error} ->
Logger.error(inspect(error))
send_resp(
conn,
500,
@ -42,7 +57,7 @@ defmodule Mobilizon.Web.ApplicationController do
400,
dgettext(
"errors",
"Both name and redirect_uri parameters are required to create an application"
"All of name, scope and redirect_uri parameters are required to create an application"
)
)
end
@ -60,14 +75,15 @@ defmodule Mobilizon.Web.ApplicationController do
client_id = conn.query_params["client_id"]
redirect_uri = conn.query_params["redirect_uri"]
state = conn.query_params["state"]
scope = conn.query_params["scope"]
if is_binary(client_id) and is_binary(redirect_uri) and is_binary(state) do
if is_binary(client_id) and is_binary(redirect_uri) and is_binary(state) and is_binary(scope) do
redirect(conn,
to:
Routes.page_path(conn, :authorize,
client_id: client_id,
redirect_uri: redirect_uri,
scope: conn.query_params["scope"],
scope: scope,
state: state
)
)
@ -77,14 +93,14 @@ defmodule Mobilizon.Web.ApplicationController do
400,
dgettext(
"errors",
"You need to specify client_id, redirect_uri and state to autorize an application"
"You need to specify client_id, redirect_uri, scope and state to autorize an application"
)
)
end
end
def device_code(conn, %{"client_id" => client_id} = args) do
case Applications.register_device_code(client_id, Map.get(args, "scope")) do
def device_code(conn, %{"client_id" => client_id, "scope" => scope}) do
case Applications.register_device_code(client_id, scope) do
{:ok, res} when is_map(res) ->
case get_format(conn) do
"json" ->
@ -94,6 +110,12 @@ defmodule Mobilizon.Web.ApplicationController do
send_resp(conn, 200, URI.encode_query(res))
end
{:error, :scope_not_included} ->
send_resp(conn, 400, "The given scope is not in the list of the app declared scopes")
{:error, :application_not_found} ->
send_resp(conn, 400, "No application with this client_id was found")
{:error, %Ecto.Changeset{} = err} ->
Logger.error(inspect(err))
send_resp(conn, 500, "Unable to produce device code")
@ -101,7 +123,11 @@ defmodule Mobilizon.Web.ApplicationController do
end
def device_code(conn, _args) do
send_resp(conn, 400, "You need to send to send at least client_id to obtain a device code")
send_resp(
conn,
400,
"You need to pass both client_id and scope as parameters to obtain a device code"
)
end
@spec generate_access_token(Plug.Conn.t(), map()) :: Plug.Conn.t()
@ -109,59 +135,106 @@ defmodule Mobilizon.Web.ApplicationController do
"client_id" => client_id,
"client_secret" => client_secret,
"code" => code,
"redirect_uri" => redirect_uri
"redirect_uri" => redirect_uri,
"scope" => scope,
"grant_type" => "authorization_code"
}) do
case Applications.generate_access_token(client_id, client_secret, code, redirect_uri) do
case do_generate_access_token(client_id, client_secret, code, redirect_uri, scope) do
{:ok, token} ->
redirect(conn, external: generate_redirect_with_query_params(redirect_uri, token))
json(conn, token)
{:error, :application_not_found} ->
send_resp(conn, 400, dgettext("errors", "No application was found with this client_id"))
{:error, :redirect_uri_not_in_allowed} ->
send_resp(conn, 400, dgettext("errors", "This redirect URI is not allowed"))
{:error, :invalid_or_expired} ->
send_resp(conn, 400, dgettext("errors", "The provided code is invalid or expired"))
{:error, :invalid_client_id} ->
send_resp(
conn,
400,
dgettext("errors", "The provided client_id does not match the provided code")
)
{:error, :invalid_client_secret} ->
send_resp(conn, 400, dgettext("errors", "The provided client_secret is invalid"))
{:error, :user_not_found} ->
send_resp(conn, 400, dgettext("errors", "The user for this code was not found"))
{:error, msg} ->
Logger.debug(msg)
json(conn, %{error: true, details: msg})
end
end
def generate_access_token(conn, %{
"client_id" => client_id,
"device_code" => device_code,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
"_format" => "json"
}) do
json(conn, Applications.generate_access_token(client_id, device_code))
end
def generate_access_token(conn, %{
"client_id" => client_id,
"device_code" => device_code,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
}) do
case Applications.generate_access_token_for_device_flow(client_id, device_code) do
{:ok, res} ->
case get_format(conn) do
"json" ->
json(conn, res)
_ ->
send_resp(
conn,
200,
URI.encode_query(res)
)
end
{:error, :incorrect_device_code} ->
send_resp(conn, 400, "The client_id provided or the device_code associated is invalid")
{:error, :access_denied} ->
send_resp(conn, 401, "The user rejected the requested authorization")
{:error, :expired} ->
send_resp(conn, 400, "The given device_code has expired")
end
end
def generate_access_token(conn, %{
"refresh_token" => refresh_token,
"grant_type" => "refresh_token",
"client_id" => client_id,
"client_secret" => client_secret
}) do
case Applications.refresh_tokens(refresh_token, client_id, client_secret) do
{:ok, res} ->
json(conn, res)
{:error, :invalid_client_credentials} ->
send_resp(conn, 400, "Invalid client credentials provided")
{:error, :invalid_refresh_token} ->
send_resp(conn, 400, "Invalid refresh token provided")
{:error, err} when is_atom(err) ->
send_resp(conn, 500, to_string(err))
end
end
def generate_access_token(conn, _args) do
send_resp(
conn,
200,
URI.encode_query(Applications.generate_access_token(client_id, device_code))
400,
"Incorrect parameters sent. You need to provide at least the grant_type and client_id parameters, depending on the grant type being used."
)
end
@spec generate_redirect_with_query_params(String.t(), map()) :: String.t()
defp generate_redirect_with_query_params(redirect_uri, query_params) do
redirect_uri |> URI.parse() |> URI.merge("?" <> URI.encode_query(query_params)) |> to_string()
@spec do_generate_access_token(String.t(), String.t(), String.t(), String.t(), String.t()) ::
{:ok, Applications.access_token_details()} | {:error, String.t()}
defp do_generate_access_token(client_id, client_secret, code, redirect_uri, scope) do
case Applications.generate_access_token(
client_id,
client_secret,
code,
redirect_uri,
scope
) do
{:ok, token} ->
{:ok, token}
{:error, :application_not_found} ->
{:error, dgettext("errors", "No application was found with this client_id")}
{:error, :redirect_uri_not_in_allowed} ->
{:error, dgettext("errors", "This redirect URI is not allowed")}
{:error, :invalid_or_expired} ->
{:error, dgettext("errors", "The provided code is invalid or expired")}
{:error, :provided_code_does_not_match} ->
{:error, dgettext("errors", "The provided client_id does not match the provided code")}
{:error, :invalid_client_secret} ->
{:error, dgettext("errors", "The provided client_secret is invalid")}
end
end
end

View File

@ -208,7 +208,6 @@ defmodule Mobilizon.Web.Router do
post("/apps", ApplicationController, :create_application)
get("/oauth/authorize", ApplicationController, :authorize)
post("/oauth/token", ApplicationController, :generate_access_token)
get("/oauth/autorize_approve", PageController, :authorize)
get("/login/device", PageController, :auth_device)
end
@ -217,10 +216,11 @@ defmodule Mobilizon.Web.Router do
plug(:accepts, ["html", "json"])
end
scope "/login", Mobilizon.Web do
scope "/", Mobilizon.Web do
pipe_through(:login)
post("/device/code", ApplicationController, :device_code)
post("/login/device/code", ApplicationController, :device_code)
post("/oauth/token", ApplicationController, :generate_access_token)
end
scope "/proxy/", Mobilizon.Web do

View File

@ -209,6 +209,7 @@ defmodule Mobilizon.Mixfile do
{:unplug, "~> 1.0.0"},
{:replug, "~> 0.1.0"},
{:exkismet, github: "tcitworld/exkismet"},
{:rajska, github: "churcho/rajska", branch: "fix/update-absinthe"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]},

View File

@ -118,6 +118,7 @@
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"},
"rajska": {:git, "https://github.com/churcho/rajska.git", "5da424969d5f40dcab690d3a25b248f85f712823", [branch: "fix/update-absinthe"]},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
"replug": {:hex, :replug, "0.1.0", "61d35f8c873c0078a23c49579a48f36e45789414b1ec0daee3fd5f4e34221f23", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f71f7a57e944e854fe4946060c6964098e53958074c69fb844b96e0bd58cfa60"},

View File

@ -6,8 +6,8 @@ defmodule Mobilizon.Repo.Migrations.CreateApplications do
add(:name, :string, null: false)
add(:client_id, :string, null: false)
add(:client_secret, :string, null: false)
add(:redirect_uris, :string, null: false)
add(:scopes, :string, null: true)
add(:redirect_uris, {:array, :string}, null: false)
add(:scope, :string, null: true)
add(:website, :string, null: true)
add(:owner_type, :string, null: true)
add(:owner_id, :integer, null: true)

View File

@ -6,6 +6,8 @@ defmodule Mobilizon.Repo.Migrations.CreateApplicationTokens do
add(:user_id, references(:users, on_delete: :delete_all), null: false)
add(:application_id, references(:applications, on_delete: :delete_all), null: false)
add(:authorization_code, :string, null: true)
add(:status, :string, default: "pending", null: false)
add(:scope, :string)
timestamps()
end

View File

@ -1,10 +0,0 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddDeviceFlowSupport do
use Ecto.Migration
def change do
alter table(:application_tokens) do
add(:status, :string, default: :pending, null: false)
add(:scope, :string)
end
end
end

View File

@ -125,7 +125,8 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do
variables: %{preferredUsername: preferred_username}
)
assert hd(res["errors"])["message"] == "unauthenticated"
assert "Not authorized to access object paginated_activity_list" ==
hd(res["errors"])["message"]
end
test "without being a member", %{

View File

@ -12,6 +12,29 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
alias Mobilizon.GraphQL.{AbsintheHelpers, API}
describe "Resolver: List the action logs" do
@action_logs_query """
query ActionLogs {
actionLogs {
total
elements {
action,
actor {
preferredUsername
},
object {
... on Report {
id,
status
},
... on ReportNote {
content
}
}
}
}
}
"""
@note_content "This a note on a report"
test "list_action_logs/3 list action logs", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
@ -26,48 +49,22 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
{:ok, %Note{} = note} = API.Reports.create_report_note(report, moderator_2, @note_content)
API.Reports.delete_report_note(note, moderator_2)
res = AbsintheHelpers.graphql_query(conn, query: @action_logs_query)
query = """
{
actionLogs {
total
elements {
action,
actor {
preferredUsername
},
object {
... on Report {
id,
status
},
... on ReportNote {
content
}
}
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to list action logs"
assert res["errors"] |> hd |> Map.get("message") ==
"You need to be logged in"
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
|> AbsintheHelpers.graphql_query(query: @action_logs_query)
assert json_response(res, 200)["errors"] == nil
assert is_nil(res["errors"])
assert json_response(res, 200)["data"]["actionLogs"]["total"] == 3
assert json_response(res, 200)["data"]["actionLogs"]["elements"] |> length == 3
assert res["data"]["actionLogs"]["total"] == 3
assert res["data"]["actionLogs"]["elements"] |> length == 3
assert json_response(res, 200)["data"]["actionLogs"]["elements"] == [
assert res["data"]["actionLogs"]["elements"] == [
%{
"action" => "NOTE_DELETION",
"actor" => %{"preferredUsername" => moderator_2.preferred_username},
@ -88,13 +85,8 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
end
describe "Resolver: Get the dashboard statistics" do
test "get_dashboard/3 gets dashboard information", %{conn: conn} do
%Event{title: title} = insert(:event)
%User{} = user_admin = insert(:user, role: :administrator)
query = """
{
@dashbord_information_query """
query Dashboard {
dashboard {
lastPublicEventPublished {
title
@ -105,24 +97,25 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
numberOfReports
}
}
"""
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
test "get_dashboard/3 gets dashboard information", %{conn: conn} do
%Event{title: title} = insert(:event)
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and an administrator to access dashboard statistics"
%User{} = user_admin = insert(:user, role: :administrator)
res = AbsintheHelpers.graphql_query(conn, query: @dashbord_information_query)
assert res["errors"] |> hd |> Map.get("message") == "You need to be logged in"
res =
conn
|> auth_conn(user_admin)
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
|> AbsintheHelpers.graphql_query(query: @dashbord_information_query)
assert json_response(res, 200)["errors"] == nil
assert is_nil(res["errors"])
assert json_response(res, 200)["data"]["dashboard"]["lastPublicEventPublished"]["title"] ==
title
assert title == res["data"]["dashboard"]["lastPublicEventPublished"]["title"]
end
end
@ -175,7 +168,6 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @relay_followers_query)
assert hd(res["errors"])["message"] == "You need to be logged in"
assert hd(res["errors"])["status_code"] == 401
end
test "test list_relay_followers/3 returns nothing when not an admin", %{conn: conn} do
@ -200,7 +192,6 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @relay_followers_query)
assert hd(res["errors"])["message"] == "You don't have permission to do this"
assert hd(res["errors"])["status_code"] == 403
res =
conn
@ -208,7 +199,6 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @relay_followers_query)
assert hd(res["errors"])["message"] == "You don't have permission to do this"
assert hd(res["errors"])["status_code"] == 403
end
test "test list_relay_followers/3 returns relay followers", %{conn: conn} do
@ -258,7 +248,6 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @relay_followings_query)
assert hd(res["errors"])["message"] == "You need to be logged in"
assert hd(res["errors"])["status_code"] == 401
end
test "test list_relay_followings/3 returns nothing when not an admin", %{conn: conn} do
@ -284,7 +273,6 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @relay_followings_query)
assert hd(res["errors"])["message"] == "You don't have permission to do this"
assert hd(res["errors"])["status_code"] == 403
res =
conn
@ -292,7 +280,6 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @relay_followings_query)
assert hd(res["errors"])["message"] == "You don't have permission to do this"
assert hd(res["errors"])["status_code"] == 403
end
test "test list_relay_followings/3 returns relay followings", %{conn: conn} do
@ -403,7 +390,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|> AbsintheHelpers.graphql_query(query: @admin_settings_query)
assert hd(res["errors"])["message"] ==
"You need to be logged-in and an administrator to access admin settings"
"You don't have permission to do this"
end
end
@ -490,7 +477,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
)
assert hd(res["errors"])["message"] ==
"You need to be logged-in and an administrator to save admin settings"
"You don't have permission to do this"
end
end
@ -524,7 +511,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
)
assert hd(res["errors"])["message"] ==
"You need to be logged-in and an administrator to edit an user's details"
"You don't have permission to do this"
end
test "when putting same email", %{conn: conn, user: user, admin: admin} do

View File

@ -0,0 +1,533 @@
defmodule Mobilizon.GraphQL.Resolvers.ApplicationTest do
use Mobilizon.Web.ConnCase
import Mobilizon.Factory
require Logger
alias Mobilizon.Applications.{Application, ApplicationDeviceActivation}
alias Mobilizon.GraphQL.AbsintheHelpers
@identities_query """
query LoggedUser {
loggedUser {
actors {
id
}
}
}
"""
describe "Authorize an application" do
@authorize_mutation """
mutation AuthorizeApplication(
$applicationClientId: String!
$redirectURI: String!
$state: String
$scope: String!
) {
authorizeApplication(
clientId: $applicationClientId
redirectURI: $redirectURI
state: $state
scope: $scope
) {
code
state
clientId
scope
}
}
"""
test "while being not logged-in", %{conn: conn} do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @authorize_mutation,
variables: [
applicationClientId: "an invalid client_id",
redirectURI: "doesn't matter",
state: "hello",
scope: "read"
]
)
assert "You need to be logged in" = hd(res["errors"])["message"]
end
test "with incorrect client_id", %{conn: conn} do
user = insert(:user)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @authorize_mutation,
variables: [
applicationClientId: "an invalid client_id",
redirectURI: "doesn't matter",
state: "hello",
scope: "read"
]
)
assert "No application with this client_id was found" = hd(res["errors"])["message"]
end
test "with incorrect redirect_uri", %{conn: conn} do
user = insert(:user)
app = insert(:auth_application)
client_id = app.client_id
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @authorize_mutation,
variables: [
applicationClientId: client_id,
redirectURI: "something not in app's redirect URIs",
state: "hello",
scope: "read"
]
)
assert "The given redirect_uri is not in the list of allowed redirect URIs" =
hd(res["errors"])["message"]
end
test "with correct params", %{conn: conn} do
user = insert(:user)
app = insert(:auth_application)
client_id = app.client_id
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @authorize_mutation,
variables: [
applicationClientId: client_id,
redirectURI: hd(app.redirect_uris),
state: "hello",
scope: "read"
]
)
assert %{
"scope" => "read",
"state" => "hello",
"clientId" => ^client_id,
"code" => _code
} = res["data"]["authorizeApplication"]
end
end
describe "Revoke an application token" do
@revoke_mutation """
mutation RevokeApplicationToken($appTokenId: String!) {
revokeApplicationToken(appTokenId: $appTokenId) {
id
}
}
"""
test "while not authenticated", %{conn: conn} do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @revoke_mutation,
variables: [
appTokenId: "not an actual token ID"
]
)
assert "You need to be logged in" = hd(res["errors"])["message"]
end
test "with an invalid token", %{conn: conn} do
user = insert(:user)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @revoke_mutation,
variables: [
appTokenId: "5846"
]
)
assert "Application token not found" == hd(res["errors"])["message"]
end
test "with valid token", %{conn: conn} do
user = insert(:user)
app_token = insert(:auth_application_token, user: user)
app_token_id = to_string(app_token.id)
authed_conn = auth_conn(conn, app_token)
res = AbsintheHelpers.graphql_query(authed_conn, query: @identities_query)
assert res["errors"] == nil
assert res["data"]["loggedUser"]["actors"]
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @revoke_mutation,
variables: [
appTokenId: app_token_id
]
)
assert app_token_id == res["data"]["revokeApplicationToken"]["id"]
# Asserting the token can't be used anymore
res = AbsintheHelpers.graphql_query(authed_conn, query: @identities_query)
assert "You need to be logged in" == hd(res["errors"])["message"]
end
end
describe "Get an application" do
@application_query """
query AuthApplication($clientId: String!) {
authApplication(clientId: $clientId) {
id
clientId
name
website
}
}
"""
test "while not authenticated", %{conn: conn} do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @application_query,
variables: [
clientId: "not an actual client ID"
]
)
assert "You need to be logged in" = hd(res["errors"])["message"]
end
test "with incorrect client_id", %{conn: conn} do
user = insert(:user)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @application_query,
variables: [
clientId: "nonsense"
]
)
assert "Application not found" = hd(res["errors"])["message"]
end
test "with valid client_id", %{conn: conn} do
user = insert(:user)
%Application{id: app_id, client_id: app_client_id, name: app_name, website: app_website} =
insert(:auth_application)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @application_query,
variables: [
clientId: app_client_id
]
)
assert is_nil(res["errors"])
app_id = to_string(app_id)
assert %{
"id" => ^app_id,
"clientId" => ^app_client_id,
"name" => ^app_name,
"website" => ^app_website
} = res["data"]["authApplication"]
end
end
describe "Get user applications" do
@user_apps_query """
query AuthAuthorizedApplications {
loggedUser {
id
authAuthorizedApplications {
id
application {
name
website
}
lastUsedAt
insertedAt
}
}
}
"""
test "without being logged in", %{conn: conn} do
res =
conn
|> AbsintheHelpers.graphql_query(query: @user_apps_query)
assert "You need to be logged in" = hd(res["errors"])["message"]
end
test "with an app token", %{conn: conn} do
user = insert(:user)
app_token = insert(:auth_application_token, user: user)
insert(:auth_application_token, user: user, status: :success, authorization_code: nil)
insert(:auth_application_token, user: user, status: :success, authorization_code: nil)
res =
conn
|> auth_conn(app_token)
|> AbsintheHelpers.graphql_query(query: @user_apps_query)
assert is_nil(res["data"]["loggedUser"]["authAuthorizedApplications"])
refute is_nil(res["data"]["loggedUser"]["id"])
assert hd(res["errors"])["message"] =~ "Not authorized to access field"
assert hd(res["errors"])["path"] == ["loggedUser", "authAuthorizedApplications"]
end
test "with authorized applications", %{conn: conn} do
user = insert(:user)
app_token_1 =
insert(:auth_application_token, user: user, status: :success, authorization_code: nil)
app_token_2 =
insert(:auth_application_token, user: user, status: :success, authorization_code: nil)
# Someone else's app token
app_token_3 = insert(:auth_application_token, status: :success, authorization_code: nil)
# An app token not activated
app_token_4 = insert(:auth_application_token, user: user)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @user_apps_query)
assert is_nil(res["errors"])
assert 2 = length(res["data"]["loggedUser"]["authAuthorizedApplications"])
found_app_token_ids =
res["data"]["loggedUser"]["authAuthorizedApplications"]
|> Enum.map(&String.to_integer(&1["id"]))
|> MapSet.new()
assert MapSet.subset?(MapSet.new([app_token_1.id, app_token_2.id]), found_app_token_ids)
refute MapSet.member?(found_app_token_ids, app_token_3.id)
refute MapSet.member?(found_app_token_ids, app_token_4.id)
end
end
describe "Device activation" do
@device_activation_mutation """
mutation DeviceActivation($userCode: String!) {
deviceActivation(userCode: $userCode) {
id
application {
id
clientId
name
website
}
scope
}
}
"""
test "without being logged-in", %{conn: conn} do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @device_activation_mutation,
variables: [userCode: "hi"]
)
assert "You need to be logged in" = hd(res["errors"])["message"]
end
test "with a bad code", %{conn: conn} do
user = insert(:user)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_activation_mutation,
variables: [userCode: "hi"]
)
assert "The given user code is invalid" = hd(res["errors"])["message"]
end
test "with an expired code", %{conn: conn} do
user = insert(:user)
auth_application_device_activation =
insert(:auth_application_device_activation, user: user, expires_in: -100)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_activation_mutation,
variables: [userCode: auth_application_device_activation.user_code]
)
assert "The given user code has expired" = hd(res["errors"])["message"]
end
test "with a valid code", %{conn: conn} do
user = insert(:user)
auth_application_device_activation = insert(:auth_application_device_activation, user: nil)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_activation_mutation,
variables: [userCode: auth_application_device_activation.user_code]
)
assert is_nil(res["errors"])
assert res["data"]["deviceActivation"]["application"]["id"] ==
to_string(auth_application_device_activation.application.id)
end
end
describe "Device authorization" do
@device_authorization_mutation """
mutation AuthorizeDeviceApplication(
$applicationClientId: String!
$userCode: String!
) {
authorizeDeviceApplication(
clientId: $applicationClientId
userCode: $userCode
) {
clientId
scope
}
}
"""
test "without being logged in", %{conn: conn} do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @device_authorization_mutation,
variables: [applicationClientId: "something", userCode: "wrong"]
)
assert "You need to be logged in" = hd(res["errors"])["message"]
end
test "with a bad code", %{conn: conn} do
user = insert(:user)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_authorization_mutation,
variables: [applicationClientId: "something", userCode: "wrong"]
)
assert "The given user code is invalid" = hd(res["errors"])["message"]
end
test "with some code that isn't approved", %{conn: conn} do
user = insert(:user)
auth_application_device_activation =
insert(:auth_application_device_activation, user: user, status: :pending)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_authorization_mutation,
variables: [
applicationClientId: auth_application_device_activation.application.client_id,
userCode: auth_application_device_activation.user_code
]
)
assert "The device user code was not provided before approving the application" =
hd(res["errors"])["message"]
end
test "with some expired code", %{conn: conn} do
user = insert(:user)
auth_application_device_activation =
insert(:auth_application_device_activation,
user: user,
status: :confirmed,
expires_in: -100
)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_authorization_mutation,
variables: [
applicationClientId: auth_application_device_activation.application.client_id,
userCode: auth_application_device_activation.user_code
]
)
assert "The given user code has expired" = hd(res["errors"])["message"]
end
test "with a valid code", %{conn: conn} do
user = insert(:user)
%ApplicationDeviceActivation{
application: %Application{client_id: client_id},
user_code: user_code
} = insert(:auth_application_device_activation, user: user, status: :confirmed)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @device_authorization_mutation,
variables: [
applicationClientId: client_id,
userCode: user_code
]
)
assert is_nil(res["errors"])
assert %{
"clientId" => ^client_id,
"scope" => _scope
} = res["data"]["authorizeDeviceApplication"]
end
end
end

View File

@ -99,7 +99,7 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
)
assert hd(res["errors"])["message"] ==
"You are not allowed to create a comment if not connected"
"You need to be logged in"
end
test "create_comment/3 creates a reply to a comment", %{
@ -166,7 +166,7 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
)
assert hd(res["errors"])["message"] ==
"You are not allowed to delete a comment if not connected"
"You need to be logged in"
# Change the current actor for user
actor2 = insert(:actor, user: user)
@ -218,10 +218,11 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
variables: %{commentId: comment.id}
)
assert res["errors"] == nil
assert res["data"]["deleteComment"]["id"] == to_string(comment.id)
query = """
{
query ActionLogs {
actionLogs {
total
elements {
@ -254,11 +255,11 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
|> AbsintheHelpers.graphql_query(query: query)
refute json_response(res, 200)["errors"]
refute res["errors"]
assert hd(json_response(res, 200)["data"]["actionLogs"]["elements"]) == %{
assert hd(res["data"]["actionLogs"]["elements"]) == %{
"action" => "COMMENT_DELETION",
"actor" => %{"preferredUsername" => actor_moderator.preferred_username},
"object" => %{"text" => comment.text, "id" => to_string(comment.id)}

View File

@ -1510,53 +1510,51 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
end
describe "delete_event/3" do
@delete_event_mutation """
mutation DeleteEvent($eventId: ID!) {
deleteEvent(
eventId: $eventId
) {
id
}
}
"""
test "delete_event/3 deletes an event", %{conn: conn, user: user, actor: actor} do
event = insert(:event, organizer_actor: actor)
mutation = """
mutation {
deleteEvent(
event_id: #{event.id}
) {
id
}
}
"""
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @delete_event_mutation,
variables: [eventId: event.id]
)
assert res["errors"] == nil
assert res["data"]["deleteEvent"]["id"] == to_string(event.id)
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|> AbsintheHelpers.graphql_query(
query: @delete_event_mutation,
variables: [eventId: event.id]
)
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["deleteEvent"]["id"] == to_string(event.id)
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] =~ "not found"
assert hd(res["errors"])["message"] =~ "not found"
end
test "delete_event/3 should check the user is authenticated", %{conn: conn, actor: actor} do
event = insert(:event, organizer_actor: actor)
mutation = """
mutation {
deleteEvent(
event_id: #{event.id}
) {
id
}
}
"""
res =
conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
AbsintheHelpers.graphql_query(conn,
query: @delete_event_mutation,
variables: [eventId: event.id]
)
assert hd(json_response(res, 200)["errors"])["message"] =~ "logged-in"
assert hd(res["errors"])["message"] =~ "logged in"
end
test "delete_event/3 should check the event can be deleted by the user", %{
@ -1567,22 +1565,15 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
actor2 = insert(:actor)
event = insert(:event, organizer_actor: actor2)
mutation = """
mutation {
deleteEvent(
event_id: #{event.id}
) {
id
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|> AbsintheHelpers.graphql_query(
query: @delete_event_mutation,
variables: [eventId: event.id]
)
assert hd(json_response(res, 200)["errors"])["message"] =~ "cannot delete"
assert hd(res["errors"])["message"] =~ "cannot delete"
end
test "delete_event/3 allows a event being deleted by a moderator and creates a entry in actionLogs",
@ -1597,22 +1588,16 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
actor2 = insert(:actor)
event = insert(:event, organizer_actor: actor2)
mutation = """
mutation {
deleteEvent(
event_id: #{event.id}
) {
id
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|> AbsintheHelpers.graphql_query(
query: @delete_event_mutation,
variables: [eventId: event.id]
)
assert json_response(res, 200)["data"]["deleteEvent"]["id"] == to_string(event.id)
assert res["errors"] == nil
assert res["data"]["deleteEvent"]["id"] == to_string(event.id)
query = """
{

View File

@ -13,124 +13,115 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
{:ok, conn: conn, actor: actor, user: user}
end
@user_feed_tokens_query """
query LoggedUserFeedTokens {
loggedUser {
feedTokens {
token
}
}
}
"""
@logged_person_feed_tokens_query """
query LoggedPersonFeedTokens {
loggedPerson {
feedTokens {
token
}
}
}
"""
describe "Feed Token Resolver" do
test "create_feed_token/3 should create a feed token", %{conn: conn, user: user} do
actor2 = insert(:actor, user: user)
mutation = """
mutation {
createFeedToken(
actor_id: #{actor2.id},
) {
token,
actor {
id
},
user {
id
}
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
token = json_response(res, 200)["data"]["createFeedToken"]["token"]
assert is_binary(token)
# TODO: Investigate why user id is a string when actor id is a number
assert json_response(res, 200)["data"]["createFeedToken"]["user"]["id"] ==
to_string(user.id)
assert json_response(res, 200)["data"]["createFeedToken"]["actor"]["id"] ==
to_string(actor2.id)
# The token is present for the user
query = """
{
loggedUser {
feedTokens {
token
@create_feed_token_for_actor_mutation """
mutation CreatePersonFeedToken($actorId: ID!) {
createFeedToken(actorId: $actorId) {
token
actor {
id
}
user {
id
}
}
}
"""
"""
@create_feed_token_for_user_mutation """
mutation CreateFeedToken {
createFeedToken {
token
user {
id
}
}
}
"""
test "create_feed_token/3 should create a feed token", %{conn: conn, user: user} do
actor2 = insert(:actor, user: user)
res =
conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "loggedUser"))
|> AbsintheHelpers.graphql_query(
query: @create_feed_token_for_actor_mutation,
variables: [actorId: actor2.id]
)
assert json_response(res, 200)["data"]["loggedUser"] ==
assert res["errors"] == nil
token = res["data"]["createFeedToken"]["token"]
assert is_binary(token)
assert res["data"]["createFeedToken"]["user"]["id"] ==
to_string(user.id)
assert res["data"]["createFeedToken"]["actor"]["id"] ==
to_string(actor2.id)
# The token is present for the user
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @user_feed_tokens_query)
assert res["data"]["loggedUser"] ==
%{
"feedTokens" => [%{"token" => token}]
}
# But not for this identity
query = """
{
loggedPerson {
feedTokens {
token
}
}
}
"""
res =
conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "loggedPerson"))
|> AbsintheHelpers.graphql_query(query: @logged_person_feed_tokens_query)
assert json_response(res, 200)["data"]["loggedPerson"] ==
assert res["data"]["loggedPerson"] ==
%{
"feedTokens" => []
}
mutation = """
mutation {
createFeedToken {
token,
user {
id
}
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|> AbsintheHelpers.graphql_query(query: @create_feed_token_for_user_mutation)
assert json_response(res, 200)["errors"] == nil
token2 = json_response(res, 200)["data"]["createFeedToken"]["token"]
assert res["errors"] == nil
token2 = res["data"]["createFeedToken"]["token"]
assert is_binary(token2)
assert is_nil(json_response(res, 200)["data"]["createFeedToken"]["actor"])
assert is_nil(res["data"]["createFeedToken"]["actor"])
assert json_response(res, 200)["data"]["createFeedToken"]["user"]["id"] ==
assert res["data"]["createFeedToken"]["user"]["id"] ==
to_string(user.id)
# The token is present for the user
query = """
{
loggedUser {
feedTokens {
token
}
}
}
"""
res =
conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "loggedUser"))
|> AbsintheHelpers.graphql_query(query: @user_feed_tokens_query)
assert json_response(res, 200)["data"]["loggedUser"] ==
assert res["data"]["loggedUser"] ==
%{
"feedTokens" => [%{"token" => token}, %{"token" => token2}]
}
@ -142,22 +133,15 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
} do
actor = insert(:actor)
mutation = """
mutation {
createFeedToken(
actor_id: #{actor.id}
) {
token
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|> AbsintheHelpers.graphql_query(
query: @create_feed_token_for_actor_mutation,
variables: [actorId: actor.id]
)
assert hd(json_response(res, 200)["errors"])["message"] =~ "not owned"
assert hd(res["errors"])["message"] =~ "not owned"
end
test "delete_feed_token/3 should delete a feed token", %{
@ -257,7 +241,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] =~ "if not connected"
assert "You need to be logged in" == hd(json_response(res, 200)["errors"])["message"]
end
test "delete_feed_token/3 should check the correct user is logged in", %{

View File

@ -70,9 +70,8 @@ defmodule Mobilizon.Web.Resolvers.FollowerTest do
variables: %{name: preferred_username}
)
assert res["errors"] == nil
assert res["data"]["group"]["followers"]["total"] == 1
assert res["data"]["group"]["followers"]["elements"] == []
assert hd(res["errors"])["message"] ==
"Not authorized to access object paginated_follower_list"
end
test "without being a member", %{

View File

@ -107,7 +107,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
res = AbsintheHelpers.graphql_query(conn, query: @list_groups_query)
assert hd(res["errors"])["message"] == "You may not list groups unless moderator."
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "list_groups/3 doesn't return all groups if not a moderator", %{conn: conn} do
@ -121,7 +121,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @list_groups_query)
assert hd(res["errors"])["message"] == "You may not list groups unless moderator."
assert hd(res["errors"])["message"] == "You don't have permission to do this"
end
test "list_groups/3 returns all groups if a moderator", %{conn: conn} do
@ -146,6 +146,14 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
describe "find a group" do
@group_query """
query Group($preferredUsername: String!) {
group(preferredUsername: $preferredUsername) {
preferredUsername
}
}
"""
@group_with_member_query """
query Group($preferredUsername: String!) {
group(preferredUsername: $preferredUsername) {
preferredUsername,
@ -173,19 +181,14 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @group_query,
query: @group_with_member_query,
variables: %{
preferredUsername: group.preferred_username
}
)
assert res["errors"] == nil
assert res["data"]["group"]["preferredUsername"] ==
group.preferred_username
assert res["data"]["group"]["members"]["total"] == 2
assert res["data"]["group"]["members"]["elements"] == []
assert hd(res["errors"])["message"] ==
"Not authorized to access object paginated_member_list"
# Login with non-member
res =
@ -203,15 +206,12 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
assert res["data"]["group"]["preferredUsername"] ==
group.preferred_username
assert res["data"]["group"]["members"]["total"] == 2
assert res["data"]["group"]["members"]["elements"] == []
# Login with member
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @group_query,
query: @group_with_member_query,
variables: %{
preferredUsername: group.preferred_username,
actorId: actor.id
@ -252,18 +252,14 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
res =
conn
|> AbsintheHelpers.graphql_query(
query: @group_query,
query: @group_with_member_query,
variables: %{
preferredUsername: group.preferred_username
}
)
assert res["errors"] == nil
assert res["data"]["group"]["preferredUsername"] ==
group.preferred_username
assert res["data"]["group"]["members"] == %{"elements" => [], "total" => 1}
assert hd(res["errors"])["message"] ==
"Not authorized to access object paginated_member_list"
end
end
@ -334,7 +330,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
variables: %{id: group.id, name: @new_group_name}
)
assert hd(res["errors"])["message"] == "You need to be logged-in to update a group"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "update_group/3 requires to be an admin of the group to update a group", %{
@ -436,7 +432,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
variables: %{groupId: group.id}
)
assert hd(res["errors"])["message"] =~ "logged-in"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "delete_group/3 should check the actor is owned by the user", %{
@ -515,7 +511,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
variables: %{groupId: group.id}
)
assert hd(res["errors"])["message"] == "You need to be logged-in to follow a group"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "when group doesn't exist", %{conn: conn, user: user} do
@ -564,7 +560,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
variables: %{groupId: group.id}
)
assert hd(res["errors"])["message"] == "You need to be logged-in to unfollow a group"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "when group doesn't exist", %{conn: conn, user: user} do
@ -631,7 +627,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
variables: %{followId: follow.id}
)
assert hd(res["errors"])["message"] == "You need to be logged-in to update a group follow"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "when follow doesn't exist", %{conn: conn, user: user} do

View File

@ -434,7 +434,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MediaTest do
variables: %{email: user.email}
)
assert is_nil(res["errors"])
assert res["errors"] == nil
assert hd(res["data"]["users"]["elements"])["mediaSize"] == 0
res = upload_media(conn, user)

View File

@ -152,7 +152,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
variables: %{groupId: group.id}
)
assert hd(res["errors"])["message"] =~ "logged-in"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "leave_group/3 should check the group exists", %{
@ -432,7 +432,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
}
)
assert hd(res["errors"])["message"] == "You must be logged-in to update a member"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "update_member/3 fails when not a member of the group", %{
@ -575,7 +575,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
}
)
assert hd(res["errors"])["message"] == "You must be logged-in to remove a member"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "remove_member/3 fails when not a member of the group", %{

View File

@ -29,6 +29,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
}
"""
# TODO: Remove this
@fetch_identities_query """
{
identities {
@ -824,7 +825,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
)
assert hd(res["errors"])["message"] ==
"Only moderators and administrators can suspend a profile"
"You don't have permission to do this"
end
end
end

View File

@ -165,7 +165,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do
)
assert res["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to update a report"
"You need to be logged in"
end
test "update_report/3 without being a moderator doesn't update any report", %{conn: conn} do
@ -181,7 +181,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do
)
assert res["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to update a report"
"You don't have permission to do this"
end
end
@ -209,7 +209,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do
res = AbsintheHelpers.graphql_query(conn, query: @reports_query)
assert hd(res["errors"])["message"] ==
"You need to be logged-in and a moderator to list reports"
"You need to be logged in"
res =
conn
@ -296,7 +296,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to view a report"
"You need to be logged in"
res =
conn

View File

@ -260,10 +260,8 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do
}
)
assert is_nil(res["errors"])
assert res["data"]["group"]["resources"]["total"] == 0
assert res["data"]["group"]["resources"]["elements"] == []
assert hd(res["errors"])["message"] ==
"Not authorized to access object paginated_resource_list"
end
end
@ -380,7 +378,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do
}
)
assert hd(res["errors"])["message"] == "You need to be logged-in to access resources"
assert hd(res["errors"])["message"] == "You need to be logged in"
end
end

View File

@ -239,21 +239,44 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
}
"""
test "finds persons with basic search", %{
conn: conn,
user: user
test "without being logged-in", %{
conn: conn
} do
actor = insert(:actor, user: user, preferred_username: "test_person")
insert(:actor, type: :Group, preferred_username: "test_group")
event = insert(:event, title: "test_event")
Workers.BuildSearch.insert_search_event(event)
res =
AbsintheHelpers.graphql_query(conn,
query: @search_persons_query,
variables: %{term: "test"}
)
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "without being a moderator", %{
conn: conn,
user: user
} do
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @search_persons_query, variables: %{term: "test"})
assert hd(res["errors"])["message"] == "You don't have permission to do this"
end
test "finds persons with basic search", %{
conn: conn
} do
user = insert(:user, role: :moderator)
actor = insert(:actor, preferred_username: "test_person")
insert(:actor, type: :Group, preferred_username: "test_group")
event = insert(:event, title: "test_event")
Workers.BuildSearch.insert_search_event(event)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @search_persons_query, variables: %{term: "test"})
assert res["errors"] == nil
assert res["data"]["searchPersons"]["total"] == 1
assert res["data"]["searchPersons"]["elements"] |> length == 1
@ -263,10 +286,10 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
end
test "finds persons with word search", %{
conn: conn,
user: user
conn: conn
} do
actor = insert(:actor, user: user, preferred_username: "person", name: "I like pineapples")
user = insert(:user, role: :moderator)
actor = insert(:actor, preferred_username: "person", name: "I like pineapples")
insert(:actor, preferred_username: "group", type: :Group, name: "pineapple group")
event1 = insert(:event, title: "Pineapple fashion week")
event2 = insert(:event, title: "I love pineAPPLE")
@ -276,7 +299,9 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
Workers.BuildSearch.insert_search_event(event3)
res =
AbsintheHelpers.graphql_query(conn,
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @search_persons_query,
variables: %{term: "pineapple"}
)

View File

@ -5,7 +5,12 @@ defmodule Mobilizon.GraphQL.Resolvers.TagTest do
alias Mobilizon.GraphQL.AbsintheHelpers
describe "Tag Resolver" do
setup do
user = insert(:user)
{:ok, user: user}
end
describe "list_tags/3" do
@tags_query """
query Tags($filter: String) {
tags(filter: $filter) {
@ -21,7 +26,16 @@ defmodule Mobilizon.GraphQL.Resolvers.TagTest do
}
"""
test "list_tags/3 returns the list of tags", %{conn: conn} do
test "requires being logged-in", %{conn: conn, user: user} do
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @tags_query)
assert res["errors"] == nil
end
test "returns the list of tags", %{conn: conn, user: user} do
tag1 = insert(:tag)
tag2 = insert(:tag)
tag3 = insert(:tag)
@ -30,8 +44,10 @@ defmodule Mobilizon.GraphQL.Resolvers.TagTest do
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @tags_query)
assert res["errors"] == nil
tags = res["data"]["tags"]
assert tags |> length == 3
@ -46,15 +62,17 @@ defmodule Mobilizon.GraphQL.Resolvers.TagTest do
|> MapSet.new()
end
test "list_tags/3 returns tags for a filter", %{conn: conn} do
test "returns tags for a filter", %{conn: conn, user: user} do
tag1 = insert(:tag, title: "PineApple", slug: "pineapple")
tag2 = insert(:tag, title: "sexy pineapple", slug: "sexy-pineapple")
_tag3 = insert(:tag)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(query: @tags_query, variables: %{filter: "apple"})
assert res["errors"] == nil
tags = res["data"]["tags"]
assert tags |> length == 2
assert [tag1.id, tag2.id] == tags |> Enum.map(&String.to_integer(&1["id"]))

View File

@ -949,63 +949,57 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
end
describe "Resolver: Refresh a token" do
test "test refresh_token/3 with a bad token", context do
mutation = """
mutation {
refreshToken(
refreshToken: "bad_token"
) {
accessToken
}
}
"""
@refresh_token_mutation """
mutation RefreshToken($refreshToken: String!) {
refreshToken(
refreshToken: $refreshToken
) {
accessToken
}
}
"""
@logged_person_query """
query LoggedPerson {
loggedPerson {
preferredUsername,
}
}
"""
test "test refresh_token/3 with a bad token", %{conn: conn} do
res =
context.conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
AbsintheHelpers.graphql_query(conn,
query: @refresh_token_mutation,
variables: [refreshToken: "bad_token"]
)
assert hd(json_response(res, 200)["errors"])["message"] ==
assert hd(res["errors"])["message"] ==
"Cannot refresh the token"
end
test "test refresh_token/3 with an appropriate token", context do
test "test refresh_token/3 with an appropriate token", %{conn: conn} do
user = insert(:user)
insert(:actor, user: user)
{:ok, refresh_token} = Authenticator.generate_refresh_token(user)
mutation = """
mutation {
refreshToken(
refreshToken: "#{refresh_token}"
) {
accessToken
}
}
"""
res =
context.conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
AbsintheHelpers.graphql_query(conn,
query: @refresh_token_mutation,
variables: [refreshToken: refresh_token]
)
assert json_response(res, 200)["errors"] == nil
assert res["errors"] == nil
access_token = json_response(res, 200)["data"]["refreshToken"]["accessToken"]
access_token = res["data"]["refreshToken"]["accessToken"]
assert String.length(access_token) > 10
query = """
{
loggedPerson {
preferredUsername,
}
}
"""
res =
context.conn
conn
|> Plug.Conn.put_req_header("authorization", "Bearer #{access_token}")
|> post("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
|> AbsintheHelpers.graphql_query(query: @logged_person_query)
assert json_response(res, 200)["errors"] == nil
assert res["errors"] == nil
end
end
@ -1246,7 +1240,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] ==
"You need to be logged-in to change your password"
"You need to be logged in"
end
end
@ -1443,7 +1437,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
)
assert hd(res["errors"])["message"] ==
"You need to be logged-in to change your email"
"You need to be logged in"
end
end
@ -1566,7 +1560,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
)
assert hd(res["errors"])["message"] ==
"You need to be logged-in to delete your account"
"You need to be logged in"
end
end
end

View File

@ -25,14 +25,15 @@ defmodule Mobilizon.ApplicationsTest do
name: "some name",
client_id: "hello",
client_secret: "secret",
redirect_uris: "somewhere\nelse"
redirect_uris: ["somewhere", "else"],
scope: "read"
}
assert {:ok, %Application{} = application} = Applications.create_application(valid_attrs)
assert application.name == "some name"
assert application.client_id == "hello"
assert application.client_secret == "secret"
assert application.redirect_uris == "somewhere\nelse"
assert application.redirect_uris == ["somewhere", "else"]
end
test "create_application/1 with invalid data returns error changeset" do
@ -95,7 +96,8 @@ defmodule Mobilizon.ApplicationsTest do
valid_attrs = %{
user_id: user.id,
application_id: application.id,
authorization_code: "hey hello"
authorization_code: "hey hello",
scope: "read"
}
assert {:ok, %ApplicationToken{} = application_token} =
@ -149,7 +151,11 @@ defmodule Mobilizon.ApplicationsTest do
import Mobilizon.ApplicationsFixtures
@invalid_attrs %{}
@invalid_attrs %{
application_id: nil,
scope: nil,
expires_in: nil
}
test "list_application_device_activation/0 returns all application_device_activation" do
application_device_activation = application_device_activation_fixture()
@ -164,10 +170,20 @@ defmodule Mobilizon.ApplicationsTest do
end
test "create_application_device_activation/1 with valid data creates a application_device_activation" do
valid_attrs = %{}
application = application_fixture()
valid_attrs = %{
user_code: "hello",
device_code: "some code",
expires_in: 900,
application_id: application.id,
scope: "read"
}
assert {:ok, %ApplicationDeviceActivation{} = application_device_activation} =
Applications.create_application_device_activation(valid_attrs)
assert application_device_activation == "read"
end
test "create_application_device_activation/1 with invalid data returns error changeset" do
@ -177,13 +193,18 @@ defmodule Mobilizon.ApplicationsTest do
test "update_application_device_activation/2 with valid data updates the application_device_activation" do
application_device_activation = application_device_activation_fixture()
update_attrs = %{}
update_attrs = %{
status: "success"
}
assert {:ok, %ApplicationDeviceActivation{} = application_device_activation} =
Applications.update_application_device_activation(
application_device_activation,
update_attrs
)
assert application_device_activation == "success"
end
test "update_application_device_activation/2 with invalid data returns error changeset" do

View File

@ -18,7 +18,6 @@ defmodule Mobilizon.Web.ConnCase do
alias Ecto.Adapters.SQL.Sandbox, as: Adapter
alias Mobilizon.Storage.Repo
alias Mobilizon.Users.User
alias Mobilizon.Web.Auth
@ -33,13 +32,20 @@ defmodule Mobilizon.Web.ConnCase do
# The default endpoint for testing
@endpoint Mobilizon.Web.Endpoint
def auth_conn(%Plug.Conn{} = conn, %User{} = user) do
def auth_conn(%Plug.Conn{} = conn, user) do
{:ok, token, _claims} = Auth.Guardian.encode_and_sign(user)
conn
|> Plug.Conn.put_req_header("authorization", "Bearer #{token}")
|> Plug.Conn.put_req_header("accept", "application/json")
end
@spec set_token(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
def set_token(%Plug.Conn{} = conn, token) do
conn
|> Plug.Conn.put_req_header("authorization", "Bearer #{token}")
|> Plug.Conn.put_req_header("accept", "application/json")
end
end
end

View File

@ -503,4 +503,37 @@ defmodule Mobilizon.Factory do
type: :string
}
end
def auth_application_factory do
%Mobilizon.Applications.Application{
name: sequence("My app"),
client_id: :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42),
client_secret: :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42),
redirect_uris: [sequence("https://someredir.uri")],
scope: "read write",
website: "https://somewebsite.com"
}
end
def auth_application_token_factory do
%Mobilizon.Applications.ApplicationToken{
authorization_code: sequence("some code"),
status: "pending",
scope: "read write",
user: build(:user),
application: build(:auth_application)
}
end
def auth_application_device_activation_factory do
%Mobilizon.Applications.ApplicationDeviceActivation{
user_code: :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8),
device_code: :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8),
status: "pending",
scope: "read write",
expires_in: 600,
user: build(:user),
application: build(:auth_application)
}
end
end

View File

@ -16,7 +16,8 @@ defmodule Mobilizon.ApplicationsFixtures do
name: "some name",
client_id: "hello",
client_secret: "secret",
redirect_uris: "somewhere\nelse"
redirect_uris: ["somewhere", "else"],
scope: "read"
})
|> Mobilizon.Applications.create_application()
@ -34,7 +35,9 @@ defmodule Mobilizon.ApplicationsFixtures do
|> Enum.into(%{
application_id: application_fixture().id,
user_id: user.id,
authorization_code: "some code"
authorization_code: "some code",
scope: "read",
status: :pending
})
|> Mobilizon.Applications.create_application_token()
@ -47,7 +50,13 @@ defmodule Mobilizon.ApplicationsFixtures do
def application_device_activation_fixture(attrs \\ %{}) do
{:ok, application_device_activation} =
attrs
|> Enum.into(%{})
|> Enum.into(%{
user_code: "hello",
device_code: "computers",
expires_in: 600,
application_id: application_fixture().id,
scope: "read"
})
|> Mobilizon.Applications.create_application_device_activation()
application_device_activation

View File

@ -0,0 +1,563 @@
defmodule Mobilizon.Web.ApplicationControllerTest do
use Mobilizon.Web.ConnCase
alias Mobilizon.Service.Auth.Applications
alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Factory
describe "create application" do
test "requires all parameters",
%{conn: conn} do
conn =
conn
|> post("/apps", %{"name" => "hello"})
assert response(conn, 400) ==
"All of name, scope and redirect_uri parameters are required to create an application"
end
test "requires valid scopes",
%{conn: conn} do
conn =
conn
|> post("/apps", %{
"name" => "hello",
"redirect_uris" => "hello",
"scope" => "write nothing"
})
assert response(conn, 400) ==
"The scope parameter is not a space separated list of valid scopes"
end
test "works",
%{conn: conn} do
name = "hello"
redirect_uris = ["hello", "world"]
scope = "read write:event:create"
website = "hi"
conn =
conn
|> post("/apps", %{
"name" => name,
"redirect_uris" => Enum.join(redirect_uris, "\n"),
"scope" => scope,
"website" => website
})
assert %{
"name" => ^name,
"redirect_uris" => ^redirect_uris,
"scope" => ^scope,
"website" => ^website,
"client_id" => _client_id,
"client_secret" => _client_secret
} = json_response(conn, 200)
end
end
describe "authorize" do
test "without all required params", %{conn: conn} do
conn = get(conn, "/oauth/authorize?client_id=hello")
assert response(conn, 400) ==
"You need to specify client_id, redirect_uri, scope and state to autorize an application"
end
test "with all required params redirects to authorization page", %{conn: conn} do
conn =
get(
conn,
"/oauth/authorize?client_id=hello&redirect_uri=somewhere&state=something&scope=everything"
)
assert redirected_to(conn) =~ "/oauth/autorize_approve"
end
end
describe "generate device code" do
test "without all required params", %{conn: conn} do
conn = post(conn, "/login/device/code", client_id: "hello")
assert response(conn, 400) ==
"You need to pass both client_id and scope as parameters to obtain a device code"
end
test "with an invalid client_id", %{conn: conn} do
conn = post(conn, "/login/device/code", client_id: "hello", scope: "write:event:create")
assert response(conn, 400) == "No application with this client_id was found"
end
test "with a scope not matching app registered scopes", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
conn =
post(conn, "/login/device/code", client_id: app.client_id, scope: "write:event:delete")
assert response(conn, 400) ==
"The given scope is not in the list of the app declared scopes"
end
test "with valid params gives a URL-encoded code", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
conn =
post(conn, "/login/device/code", client_id: app.client_id, scope: "write:event:create")
res = conn |> response(200) |> URI.decode_query()
verification_uri = Routes.page_url(Mobilizon.Web.Endpoint, :auth_device)
assert %{
"device_code" => _device_code,
"expires_in" => "900",
"interval" => "5",
"user_code" => user_code,
"verification_uri" => ^verification_uri
} = res
assert Regex.match?(~r/^[A-Z]{4}-[A-Z]{4}$/, user_code)
end
test "with valid params and a JSON Accept header gives a JSON-encoded struct", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
conn =
conn
|> Plug.Conn.put_req_header("accept", "application/json")
|> post("/login/device/code", client_id: app.client_id, scope: "write:event:create")
res = json_response(conn, 200)
verification_uri = Routes.page_url(Mobilizon.Web.Endpoint, :auth_device)
assert %{
"device_code" => _device_code,
"expires_in" => 900,
"interval" => 5,
"user_code" => user_code,
"verification_uri" => ^verification_uri
} = res
assert Regex.match?(~r/^[A-Z]{4}-[A-Z]{4}$/, user_code)
end
end
describe "generate access code for device flow" do
test "without valid parameters", %{conn: conn} do
conn = post(conn, "/oauth/token")
assert response(conn, 400) ==
"Incorrect parameters sent. You need to provide at least the grant_type and client_id parameters, depending on the grant type being used."
end
test "with invalid client_id", %{conn: conn} do
conn =
post(conn, "/oauth/token",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: "some_client_id",
device_code: "hello"
)
assert response(conn, 400) ==
"The client_id provided or the device_code associated is invalid"
end
test "with rejected authorization", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
assert {:ok, _res} =
Mobilizon.Applications.create_application_device_activation(%{
device_code: "hello",
user_code: "world",
expires_in: 900,
application_id: app.id,
scope: "write:event:create write:event:update",
status: :access_denied
})
conn =
post(conn, "/oauth/token",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: app.client_id,
device_code: "hello"
)
assert response(conn, 401) == "The user rejected the requested authorization"
end
test "with incorrect device code", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
assert {:ok, _res} =
Mobilizon.Applications.create_application_device_activation(%{
device_code: "hello",
user_code: "world",
expires_in: 900,
application_id: app.id,
scope: "write:event:create write:event:update",
status: "incorrect_device_code"
})
conn =
post(conn, "/oauth/token",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: app.client_id,
device_code: "hello"
)
assert response(conn, 400) ==
"The client_id provided or the device_code associated is invalid"
end
test "with an expired device activation", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
assert {:ok, _res} =
Mobilizon.Applications.create_application_device_activation(%{
device_code: "hello",
user_code: "world",
expires_in: -40,
application_id: app.id,
scope: "write:event:create write:event:update",
status: "success",
user_id: user.id
})
conn =
post(conn, "/oauth/token",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: app.client_id,
device_code: "hello"
)
assert response(conn, 400) ==
"The given device_code has expired"
end
test "with valid params", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
assert {:ok, _res} =
Mobilizon.Applications.create_application_device_activation(%{
device_code: "hello",
user_code: "world",
expires_in: 600,
application_id: app.id,
scope: "write:event:create write:event:update",
status: "success",
user_id: user.id
})
conn =
post(conn, "/oauth/token",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: app.client_id,
device_code: "hello"
)
res = conn |> response(200) |> URI.decode_query()
assert %{
"access_token" => _access_token,
"expires_in" => "28800",
"refresh_token" => _refresh_token,
"refresh_token_expires_in" => "15724800",
"scope" => "write:event:create write:event:update",
"token_type" => "bearer"
} = res
end
test "with valid params as JSON", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
assert {:ok, _res} =
Mobilizon.Applications.create_application_device_activation(%{
device_code: "hello",
user_code: "world",
expires_in: 600,
application_id: app.id,
scope: "write:event:create write:event:update",
status: "success",
user_id: user.id
})
conn =
conn
|> Plug.Conn.put_req_header("accept", "application/json")
|> post("/oauth/token",
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: app.client_id,
device_code: "hello"
)
res = json_response(conn, 200)
assert %{
"access_token" => _access_token,
"expires_in" => 28_800,
"refresh_token" => _refresh_token,
"refresh_token_expires_in" => 15_724_800,
"scope" => "write:event:create write:event:update",
"token_type" => "bearer"
} = res
end
end
describe "generate access code for authorization flow" do
test "with invalid client_id", %{conn: conn} do
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: "some_client_id",
client_secret: "some_client_secret",
code: "hello",
redirect_uri: "some redirect uri",
scope: "hello"
)
assert json_response(conn, 200)["details"] ==
"No application was found with this client_id"
end
test "with invalid redirect_uri", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: app.client_secret,
code: "hello",
redirect_uri: "nope",
scope: "hello"
)
assert json_response(conn, 200)["details"] ==
"This redirect URI is not allowed"
end
test "with invalid code", %{conn: conn} do
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: app.client_secret,
code: "hello",
redirect_uri: "hello",
scope: "hello"
)
assert json_response(conn, 200)["details"] ==
"The provided code is invalid or expired"
end
test "with invalid client secret", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
Mobilizon.Applications.create_application_token(%{
user_id: user.id,
application_id: app.id,
authorization_code: "hi there",
scope: "write:event:create write:event:update"
})
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: "not the client secret",
code: "hi there",
redirect_uri: "hello",
scope: "write:event:create write:event:update"
)
assert json_response(conn, 200)["details"] ==
"The provided client_secret is invalid"
end
test "with an authorization code matching a different app", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
{:ok, app2} =
Applications.create("My other app", ["hello"], "write:event:create write:event:update")
Mobilizon.Applications.create_application_token(%{
user_id: user.id,
application_id: app2.id,
authorization_code: "hi there",
scope: "write:event:create write:event:update"
})
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: app.client_id,
code: "hi there",
redirect_uri: "hello",
scope: "write:event:create write:event:update"
)
assert json_response(conn, 200)["details"] ==
"The provided client_id does not match the provided code"
end
test "with valid params", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
Mobilizon.Applications.create_application_token(%{
user_id: user.id,
application_id: app.id,
authorization_code: "hi there",
scope: "write:event:create write:event:update"
})
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: app.client_secret,
code: "hi there",
redirect_uri: "hello",
scope: "write:event:create write:event:update"
)
res = json_response(conn, 200)
assert %{
"access_token" => _access_token,
"expires_in" => 28_800,
"refresh_token" => _refresh_token,
"refresh_token_expires_in" => 15_724_800,
"scope" => "write:event:create write:event:update",
"token_type" => "bearer"
} = res
end
end
describe "generate new access code from refresh code" do
test "with invalid refresh token", %{conn: conn} do
conn =
post(conn, "/oauth/token",
grant_type: "refresh_token",
client_id: "hello",
client_secret: "secret",
refresh_token: "none"
)
assert response(conn, 400) ==
"Invalid refresh token provided"
end
test "with invalid client credentials", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
Mobilizon.Applications.create_application_token(%{
user_id: user.id,
application_id: app.id,
authorization_code: "hi there",
scope: "write:event:create write:event:update"
})
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: app.client_secret,
code: "hi there",
redirect_uri: "hello",
scope: "write:event:create write:event:update"
)
res = json_response(conn, 200)
conn =
post(conn, "/oauth/token",
grant_type: "refresh_token",
client_id: "hello",
client_secret: "secret",
refresh_token: res["refresh_token"]
)
assert response(conn, 400) ==
"Invalid client credentials provided"
end
test "with valid params", %{conn: conn} do
user = insert(:user)
{:ok, app} =
Applications.create("My app", ["hello"], "write:event:create write:event:update")
Mobilizon.Applications.create_application_token(%{
user_id: user.id,
application_id: app.id,
authorization_code: "hi there",
scope: "write:event:create write:event:update"
})
conn =
post(conn, "/oauth/token",
grant_type: "authorization_code",
client_id: app.client_id,
client_secret: app.client_secret,
code: "hi there",
redirect_uri: "hello",
scope: "write:event:create write:event:update"
)
res = json_response(conn, 200)
conn =
post(conn, "/oauth/token",
grant_type: "refresh_token",
client_id: app.client_id,
client_secret: app.client_secret,
refresh_token: res["refresh_token"]
)
res = json_response(conn, 200)
assert %{
"access_token" => _access_token,
"expires_in" => 28_800,
"refresh_token" => _refresh_token,
"refresh_token_expires_in" => 15_724_800,
"scope" => "write:event:create write:event:update",
"token_type" => "bearer"
} = res
end
end
end