Skip to content
Snippets Groups Projects
Commit 6238df53 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '839-authorized-applications-spawns-mini-aleksis' into 'release-3.0'

Resolve "Authorized Applications spawns "mini-AlekSIS""

See merge request !1218
parents 90044556 d676e4a4
No related branches found
No related tags found
3 merge requests!1237Release 3.0,!1218Resolve "Authorized Applications spawns "mini-AlekSIS"",!1183Release 3.0
Pipeline #125869 canceled
Showing
with 271 additions and 72 deletions
......@@ -19,6 +19,13 @@ const dateTimeFormats = {
minute: "numeric",
second: "numeric",
},
longNumeric: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
},
},
de: {
short: {
......@@ -39,6 +46,13 @@ const dateTimeFormats = {
minute: "numeric",
second: "numeric",
},
longNumeric: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
},
},
};
......
<template>
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<div class="d-flex justify-start align-center">
<v-avatar
x-large
v-if="accessToken.application.icon.absoluteUrl"
class="mr-4"
>
<img
:src="accessToken.application.icon.absoluteUrl"
:alt="accessToken.application.name"
/>
</v-avatar>
<v-avatar x-large v-else class="mr-4" color="secondary">
<v-icon color="white">mdi-apps</v-icon>
</v-avatar>
<div class="subtitle-1 font-weight-medium">
{{ accessToken.application.name }}
</div>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-list dense class="pa-0">
<v-list-item>
<v-list-item-content class="body-2">
{{
$t("oauth.authorized_application.access_since", {
date: $d(new Date(accessToken.created), "longNumeric"),
})
}}
·
{{
$t("oauth.authorized_application.valid_until", {
date: $d(new Date(accessToken.expires), "longNumeric"),
})
}}
</v-list-item-content>
<v-list-item-action>
<v-btn color="primary" @click="deleteItem(accessToken)">
{{ $t("oauth.authorized_application.revoke") }}
</v-btn>
</v-list-item-action>
</v-list-item>
<v-list-item v-if="accessToken.scopes && accessToken.scopes.length > 0">
<div class="pr-4">
<v-list-item-content class="body-2">
{{ $t("oauth.authorized_application.has_access_to") }}
</v-list-item-content>
</div>
<v-list dense class="pa-0 flex-grow-1">
<div v-for="(scope, idx) in accessToken.scopes" :key="scope.name">
<v-list-item>
<v-list-item-content class="body-2">
{{ scope.description }}
</v-list-item-content>
</v-list-item>
<v-divider v-if="idx < accessToken.scopes.length - 1" />
</div>
</v-list>
</v-list-item>
</v-list>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
export default {
name: "AuthorizedApplication",
props: {
accessToken: {
type: Object,
required: true,
},
},
methods: {
deleteItem(item) {
this.$emit("delete-item", item);
},
},
};
</script>
<template>
<div>
<h1 class="mb-4">{{ $t("oauth.authorized_application.title") }}</h1>
<div v-if="$apollo.queries.accessTokens.loading">
<v-skeleton-loader type="card"></v-skeleton-loader>
</div>
<div v-else-if="accessTokens">
<v-card class="mb-4">
<v-card-title>
{{ $t("oauth.authorized_application.subtitle") }}
</v-card-title>
<v-card-text>
{{ $t("oauth.authorized_application.description") }}
</v-card-text>
<v-expansion-panels flat>
<authorized-application
v-for="(accessToken, index) in accessTokens"
:key="accessToken.id"
:access-token="accessToken"
@delete-item="openDeleteDialog"
/>
</v-expansion-panels>
</v-card>
</div>
<delete-dialog
:item="deleteItem"
:gql-mutation="require('./revokeOauthToken.graphql')"
:gql-query="require('./accessTokens.graphql')"
v-model="deleteDialog"
>
<template #title>
{{ $t("oauth.authorized_application.revoke_question") }}
</template>
<template #body>
<span v-if="deleteItem">{{ deleteItem.application.name }}</span>
</template>
<template #deleteContent>
{{ $t("oauth.authorized_application.revoke") }}
</template>
</delete-dialog>
</div>
</template>
<script>
import gqlAccessTokens from "./accessTokens.graphql";
import AuthorizedApplication from "./AuthorizedApplication.vue";
import DeleteDialog from "../generic/dialogs/DeleteDialog.vue";
export default {
name: "AuthorizedApplications",
components: { DeleteDialog, AuthorizedApplication },
data() {
return {
deleteDialog: false,
deleteItem: null,
};
},
methods: {
openDeleteDialog(item) {
this.deleteItem = item;
this.deleteDialog = true;
},
},
apollo: {
accessTokens: {
query: gqlAccessTokens,
},
},
};
</script>
{
accessTokens: oauthAccessTokens {
id
created
updated
expires
scopes {
name
description
}
application {
id
name
icon {
absoluteUrl
}
}
}
}
mutation ($id: ID!) {
revokeOauthToken(id: $id) {
ok
}
}
......@@ -167,8 +167,16 @@
"title": "OAuth Application",
"title_plural": "OAuth Applications"
},
"authorized_token": {
"menu_title": "Authorized Applications"
"authorized_application": {
"menu_title": "Third-party Applications",
"title": "Third-party Applications",
"subtitle": "Third-party Applications With Access to Your Account",
"description": "The following third-party applications have access to your account. You can revoke access at any time for those you don't need or trust anymore.",
"valid_until": "Valid until {date}",
"access_since": "Access since {date}",
"has_access_to": "Has access to:",
"revoke": "Revoke Access",
"revoke_question": "Are you sure you want to revoke access for this application?"
}
},
"people": "People",
......
......@@ -920,14 +920,14 @@ const routes = [
},
{
path: "/oauth/authorized_tokens/",
component: () => import("./components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
component: () =>
import(
"./components/authorized_oauth_applications/AuthorizedApplications.vue"
),
name: "core.oauth.authorizedTokens",
meta: {
inAccountMenu: true,
titleKey: "oauth.authorized_token.menu_title",
titleKey: "oauth.authorized_application.menu_title",
icon: "mdi-gesture-tap-hold",
permission: "core.manage_authorized_tokens_rule",
},
......
......@@ -9,7 +9,15 @@ from haystack.inputs import AutoQuery
from haystack.query import SearchQuerySet
from haystack.utils.loading import UnifiedIndex
from ..models import CustomMenu, DynamicRoute, Notification, PDFFile, Person, TaskUserAssignment
from ..models import (
CustomMenu,
DynamicRoute,
Notification,
OAuthAccessToken,
PDFFile,
Person,
TaskUserAssignment,
)
from ..util.apps import AppConfig
from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person
from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType
......@@ -19,6 +27,7 @@ from .group import GroupType # noqa
from .installed_apps import AppType
from .message import MessageType
from .notification import MarkNotificationReadMutation, NotificationType
from .oauth import OAuthAccessTokenType, OAuthRevokeTokenMutation
from .pdf import PDFFileType
from .person import PersonMutation, PersonType
from .school_term import SchoolTermType # noqa
......@@ -59,6 +68,8 @@ class Query(graphene.ObjectType):
two_factor = graphene.Field(TwoFactorType)
oauth_access_tokens = graphene.List(OAuthAccessTokenType)
def resolve_ping(root, info, payload) -> str:
return payload
......@@ -157,6 +168,10 @@ class Query(graphene.ObjectType):
return None
return info.context.user
@staticmethod
def resolve_oauth_access_tokens(root, info, **kwargs):
return OAuthAccessToken.objects.filter(user=info.context.user)
class Mutation(graphene.ObjectType):
update_person = PersonMutation.Field()
......@@ -165,6 +180,8 @@ class Mutation(graphene.ObjectType):
celery_progress_fetched = CeleryProgressFetchedMutation.Field()
revoke_oauth_token = OAuthRevokeTokenMutation.Field()
def build_global_schema():
"""Build global GraphQL schema from all apps."""
......
import graphene
from graphene_django import DjangoObjectType
from aleksis.core.models import OAuthAccessToken, OAuthApplication
from .base import FieldFileType
class OAuthScope(graphene.ObjectType):
name = graphene.String()
description = graphene.String()
class OAuthApplicationType(DjangoObjectType):
icon = graphene.Field(FieldFileType)
class Meta:
model = OAuthApplication
fields = ["id", "name", "icon"]
class OAuthAccessTokenType(DjangoObjectType):
scopes = graphene.List(OAuthScope)
@staticmethod
def resolve_scopes(root: OAuthAccessToken, info, **kwargs):
return [OAuthScope(name=key, description=value) for key, value in root.scopes.items()]
class Meta:
model = OAuthAccessToken
fields = ["id", "application", "expires", "created", "updated"]
class OAuthRevokeTokenMutation(graphene.Mutation):
class Arguments:
id = graphene.ID() # noqa
ok = graphene.Boolean()
@staticmethod
def mutate(root, info, id): # noqa
token = OAuthAccessToken.objects.get(id=id, user=info.context.user)
token.delete()
return OAuthRevokeTokenMutation(ok=True)
{% extends "core/base.html" %}
{% load i18n %}
{% block browser_title %}{% trans "Revoke access" %}{% endblock %}
{% block page_title %}{% trans "Revoke access" %}{% endblock %}
{% block content %}
<div class="alert info">
<p>
<i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>
{% trans "Are you sure to revoke the access for this application?" %}
</p>
</div>
<form method="post">
{% csrf_token %}
<a class="btn waves-effect waves-light red" href="{% url "oauth2_applications" %}">
<i class="material-icons iconify left" data-icon="mdi:delete-outline"></i>
{% trans "Revoke" %}
</a>
<a class="btn waves-effect waves-light" href="{% url "oauth2_applications" %}">
<i class="material-icons iconify left" data-icon="mdi:close"></i>
{% trans "Cancel" %}
</a>
</form>
{% endblock %}
{% extends "core/base.html" %}
{% load i18n %}
{% block browser_title %}{% blocktrans %}Authorized applications{% endblocktrans %}{% endblock %}
{% block page_title %}{% trans "Authorized applications" %}{% endblock %}
{% block content %}
{% if authorized_tokens %}
<div class="row">
{% for authorized_token in authorized_tokens %}
<div class="col s12 m6 l4 xl3">
<div class="card">
<div class="card-content">
<div class="card-title">{{ authorized_token.application }}</div>
{% for scope_name, scope_description in authorized_token.scopes.items %}
<p>
{{ scope_name }}: {{ scope_description }}
</p>
{% endfor %}
</div>
<div class="card-action">
<a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">{% trans "Revoke access" %}</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert info">
<p>
<i class="material-icons iconify left" data-icon="mdi:information-outline"></i>
{% trans "No authorized applications." %}
</p>
</div>
{% endif %}
{% endblock %}
......@@ -33,6 +33,10 @@ urlpatterns = [
ConnectDiscoveryInfoView.as_view(),
name="oidc_configuration",
),
path("oauth/applications/", views.TemplateView.as_view(template_name="core/vue_index.html")),
path(
"oauth/authorized_tokens/", views.TemplateView.as_view(template_name="core/vue_index.html")
),
path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path(
"django/",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment