enable authentication, disable kratos stuff

pull/1/head
Philipp Rothmann 2022-10-25 12:42:35 +02:00
parent 4c6b7e4414
commit d1838267ea
11 changed files with 171 additions and 46 deletions

View File

@ -3,6 +3,7 @@ HYDRA_CLIENT_SECRET=
HYDRA_AUTHORIZATION_BASE_URL="https://sso.example.org/application/o/authorize/"
HYDRA_PUBLIC_URL="https://sso.example.org/application/o/"
TOKEN_URL="https://sso.example.org/application/o/token/"
REDIRECT_URL="https://example.org/login-callback"
SECRET_KEY=
LOAD_INCLUSTER_CONFIG=false
DATABASE_URL=sqlite:///database.db

1
backend/.gitignore vendored
View File

@ -8,3 +8,4 @@ __pycache__
.envrc
.direnv
run_app.local.sh
*.db

11
backend/Makefile 100644
View File

@ -0,0 +1,11 @@
clean:
rm database.db
flask db upgrade
demo:
flask cli app create nextcloud Dateiablage "https://cloud.dev.local-it.cloud"
flask cli app create vikunja Projekte "https://vikunja.dev.local-it.cloud"
run:
flask run

View File

@ -1,21 +1,7 @@
"""Everything to do with Apps"""
import os
import base64
from sqlalchemy import ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship
from database import db
from .models import App
# import helpers.kubernetes as k8s
DEFAULT_APP_SUBDOMAINS = {
"nextcloud": "files",
"wordpress": "www",
"monitoring": "grafana",
}
class LITApp(App):
"""

View File

@ -30,38 +30,39 @@ def hydra_callback():
token = HydraOauth.get_token(state, code)
user_info = HydraOauth.get_user_info()
# Match Kratos identity with Hydra
identities = KratosApi.get("/identities")
identity = None
for i in identities.json():
if i["traits"]["email"] == user_info["email"]:
identity = i
# identities = KratosApi.get("/identities")
# identity = None
# for i in identities.json():
# if i["traits"]["email"] == user_info["email"]:
# identity = i
access_token = create_access_token(
identity=token, expires_delta=timedelta(days=365), additional_claims={"user_id": identity["id"]}
identity=token, expires_delta=timedelta(days=365),
#additional_claims={"user_id": identity["id"]}
)
apps = App.query.all()
app_roles = []
for app in apps:
tmp_app_role = AppRole.query.filter_by(
user_id=identity["id"], app_id=app.id
).first()
app_roles.append(
{
"name": app.slug,
"role_id": tmp_app_role.role_id if tmp_app_role else None,
}
)
# apps = App.query.all()
# app_roles = []
# for app in apps:
# tmp_app_role = AppRole.query.filter_by(
# user_id=identity["id"], app_id=app.id
# ).first()
# app_roles.append(
# {
# "name": app.slug,
# "role_id": tmp_app_role.role_id if tmp_app_role else None,
# }
# )
return jsonify(
{
"accessToken": access_token,
"userInfo": {
"id": identity["id"],
"id": user_info["email"],
"email": user_info["email"],
"name": user_info["name"],
"preferredUsername": user_info["preferred_username"],
"app_roles": app_roles,
# "app_roles": app_roles,
},
}
)

View File

@ -5,6 +5,7 @@ HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID")
HYDRA_CLIENT_SECRET = os.environ.get("HYDRA_CLIENT_SECRET")
HYDRA_AUTHORIZATION_BASE_URL = os.environ.get("HYDRA_AUTHORIZATION_BASE_URL")
TOKEN_URL = os.environ.get("TOKEN_URL")
REDIRECT_URL = os.environ.get("REDIRECT_URL")
LOGIN_PANEL_URL = os.environ.get("LOGIN_PANEL_URL")

View File

@ -9,7 +9,7 @@ class HydraOauth:
@staticmethod
def authorize():
try:
hydra = OAuth2Session(HYDRA_CLIENT_ID)
hydra = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL)
authorization_url, state = hydra.authorization_url(
HYDRA_AUTHORIZATION_BASE_URL
)

View File

@ -1,18 +1,25 @@
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { Toaster } from 'react-hot-toast';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { Layout } from './components';
import { Dashboard } from './modules';
import { AppIframe } from './modules/dashboard/AppIframe';
import { Login } from './modules/login';
import { LoginCallback } from './modules/login/LoginCallback';
import { useApps } from './services/apps';
import { useAuth } from './services/auth';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function App() {
const host = window.location.hostname;
const splitedDomain = host.split('.');
splitedDomain.shift();
const { authToken, currentUser, isAdmin } = useAuth();
const redirectToLogin = !authToken || !currentUser?.app_roles;
const ProtectedRoute = () => {
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
};
const { apps, loadApps } = useApps();
useEffect(() => {
@ -33,15 +40,23 @@ function App() {
</Helmet>
<div className="app bg-gray-50 min-h-screen flex flex-col">
<Layout>
{redirectToLogin ? (
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
{apps.map((app) => (
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
))}
<Route path="*" element={<Navigate to="/dashboard" />} />
<Route path="/login" element={<Login />} />
<Route path="/login-callback" element={<LoginCallback />} />
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
</Layout>
) : (
<Layout>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
{apps.map((app) => (
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
))}
<Route path="*" element={<Navigate to="/dashboard" />} />
</Routes>
</Layout>
)}
{/* Place to load notifications */}
<div

View File

@ -0,0 +1,50 @@
/**
* Login page that starts the OAuth2 authentication flow.
*/
import React from 'react';
import clsx from 'clsx';
import { LockClosedIcon } from '@heroicons/react/solid';
import { performApiCall } from 'src/services/api';
import { showToast, ToastType } from 'src/common/util/show-toast';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function Login() {
const handleSubmit = async () => {
try {
const { data } = await performApiCall({
path: '/login',
method: 'POST',
});
if (data.authorizationUrl) {
window.location.href = data.authorizationUrl;
}
} catch (e: any) {
showToast('Something went wrong', ToastType.Error);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="flex justify-center">
<img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
</div>
<button
onClick={handleSubmit}
type="button"
className={clsx(
'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
)}
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
</span>
Sign in
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,58 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { showToast, ToastType } from 'src/common/util/show-toast';
import { useAuth } from 'src/services/auth';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function LoginCallback() {
const currentURL = window.location.href;
const indexOfQuestionMark = currentURL.indexOf('?');
const params = currentURL.slice(indexOfQuestionMark);
const navigate = useNavigate();
const { logIn } = useAuth();
useEffect(() => {
async function logInUser() {
if (params.length > 2) {
const res = await logIn(params);
// @ts-ignore
if (!res.ok) {
navigate('/login');
showToast('Something went wrong, please try logging in again.', ToastType.Error);
}
}
}
logInUser();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="flex flex-col justify-center items-center">
<div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150">
<svg
className="animate-spin h-10 w-10 text-primary"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-100"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
<p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1 @@
export { Login } from './Login';