add authentik api get users

This commit is contained in:
Philipp Rothmann 2022-11-08 09:52:45 +01:00
parent 143ea888c8
commit 35a4f29f07
18 changed files with 223 additions and 81 deletions

View file

@ -1,32 +0,0 @@
"""Everything to do with Apps"""
from database import db
from .models import App, AppRole
class LITApp(App):
"""
"""
def get_url(self):
return self.url
def to_dict(self):
"""
represent this object as a dict, compatible for JSON output
"""
return {"id": self.id,
"name": self.name,
"slug": self.slug,
"external": self.external,
"status": self.get_status(),
"url": self.get_url()}
def get_status(self):
"""Returns an AppStatus object that describes the current cluster state"""
return {
"installed": "",
"ready": "",
"message": "",
}

View file

@ -5,15 +5,14 @@ from flask_cors import cross_origin
from datetime import timedelta
from areas import api_v1
from areas.apps import App, AppRole
from config import *
from helpers import HydraOauth, BadRequest
from helpers import LITOauth, BadRequest
@api_v1.route("/login", methods=["POST"])
@cross_origin()
def login():
authorization_url = HydraOauth.authorize()
authorization_url = LITOauth.authorize()
return jsonify({"authorizationUrl": authorization_url})
@ -28,11 +27,10 @@ def hydra_callback():
if code == None:
raise BadRequest("Missing code query param")
token = HydraOauth.get_token(state, code)
user_info = HydraOauth.get_user_info()
token = LITOauth.get_token(state, code)
user_info = LITOauth.get_user_info()
access_token = create_access_token(
identity=token, expires_delta=timedelta(days=365))
isAdmin = "admin" in user_info["groups"]
app_roles = [
{
@ -40,18 +38,7 @@ def hydra_callback():
"role_id": 1 if isAdmin else 2
},
]
print(app_roles)
# apps = App.query.all()
# for app in apps:
# tmp_app_role = AppRole.query.filter_by(
# user_id=user_info["sub"], 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,

View file

@ -0,0 +1,39 @@
from flask import current_app
from helpers.authentik_api import AuthentikApi
from .user_service import UserService
from .models import User
class UserService(UserService):
@staticmethod
def get_users():
user_list = [User.from_authentik(u).to_dict() for u in AuthentikApi.get("/core/users")]
return user_list
@staticmethod
def get_user(id):
pass
@staticmethod
def post_user(data):
pass
@staticmethod
def __start_recovery_flow(email):
pass
@staticmethod
def put_user(id, user_editing_id, data):
pass
@staticmethod
def delete_user(id):
pass
@staticmethod
def post_multiple_users(data):
pass
@staticmethod
def __insertAppRoleToUser(userId, userRes):
pass

View file

@ -0,0 +1,37 @@
class User:
id = None
uuid = None
traits = None
email = None
name = None
preferredUsername = None
state = None
def __init__(self):
pass
@staticmethod
def from_authentik(authentik_user):
u = User()
u.id = authentik_user["pk"]
u.uuid = authentik_user["uid"]
u.name = authentik_user["name"]
u.email = authentik_user["email"]
u.traits = {
"name": authentik_user["name"],
"email": authentik_user["email"],
"app_roles": []
}
u.preferredUsername = authentik_user["username"]
u.state = "active" if authentik_user["is_active"] else ""
return u
def to_dict(self):
return {
"id": self.id,
"traits": self.traits,
"preferredUsername": self.preferredUsername,
"state": self.state,
}

View file

@ -8,7 +8,7 @@ from helpers import KratosApi
from helpers.auth_guard import admin_required
from .validation import schema, schema_multiple
from .user_service import UserService
from .lit_user_service import UserService
@api_v1.route("/users", methods=["GET"])
@ -16,8 +16,7 @@ from .user_service import UserService
@cross_origin()
@admin_required()
def get_users():
res = UserService.get_users()
return jsonify(res)
return jsonify(UserService.get_users())
@api_v1.route("/users/<string:id>", methods=["GET"])

View file

@ -2,3 +2,4 @@ from .kratos_api import *
from .error_handler import *
from .hydra_oauth import *
from .kratos_user import *
from .lit_oauth import *

View file

@ -11,9 +11,10 @@ def admin_required():
@wraps(fn)
def decorator(*args, **kwargs):
verify_jwt_in_request()
claims = get_jwt()
user_id = claims["user_id"]
is_admin = RoleService.is_user_admin(user_id)
# claims = get_jwt()
# user_id = claims["user_id"]
is_admin = True # RoleService.is_user_admin(user_id)
# TODO: actually check if admin
if is_admin:
return fn(*args, **kwargs)
else:

View file

@ -0,0 +1,43 @@
from typing import List
from flask_jwt_extended import get_jwt
import requests
from .error_handler import AuthentikError
AUTHENTIK_BASEURL = "https://dev.local-it.cloud/api/v3"
class AuthentikApi: # TODO: check if can be replaced with apispec generated api?
@staticmethod
def __handleError(res):
if res.status_code >= 400:
message = res.json()["error"]["message"]
raise AuthentikError(message, res.status_code)
@staticmethod
def __token():
jwt = get_jwt()
return jwt["sub"]["refresh_token"]
@staticmethod
def get(url):
try:
res = requests.get(f"{AUTHENTIK_BASEURL}{url}", headers={
"Authorization": f"Bearer {AuthentikApi.__token()}"})
AuthentikApi.__handleError(res)
if (res.json()["pagination"]):
return AuthentikApi.__paginate(res)
return res.json()
except AuthentikError as err:
raise err
except:
raise AuthentikError()
@staticmethod
def __paginate(res: requests.Response): # TODO: test this
results = res.json()["results"]
for page in range(1, res.json()["pagination"]["total_pages"]):
res = requests.get(
f"{res.request.url}", headers=res.request.headers, params={'page': page})
AuthentikApi.__handleError(res)
results.append(res.json()["results"])
return results

View file

@ -5,6 +5,8 @@ from jsonschema import ValidationError
class KratosError(Exception):
pass
class AuthentikError(Exception):
pass
class HydraError(Exception):
pass

View file

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

View file

@ -4,7 +4,6 @@ import requests
from config import *
from .error_handler import KratosError
class KratosApi:
@staticmethod
def __handleError(res):

View file

@ -0,0 +1,51 @@
from flask import request, session
from requests_oauthlib import OAuth2Session
from config import *
from helpers import HydraError
class LITOauth:
@staticmethod
def authorize():
try:
scopes = ["openid", "email", "profile", "goauthentik.io/api"]
oauth = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL, scope=scopes)
authorization_url, state = oauth.authorization_url(
HYDRA_AUTHORIZATION_BASE_URL
)
return authorization_url
except Exception as err:
raise HydraError(str(err), 500)
@staticmethod
def get_token(state, code):
try:
oauth = OAuth2Session(
client_id=HYDRA_CLIENT_ID,
state=state,
)
token = oauth.fetch_token(
token_url=TOKEN_URL,
code=code,
client_secret=HYDRA_CLIENT_SECRET,
include_client_id=True,
)
session["oauth_token"] = token
return token
except Exception as err:
raise HydraError(str(err), 500)
@staticmethod
def get_user_info():
try:
hydra = OAuth2Session(
client_id=HYDRA_CLIENT_ID, token=session["oauth_token"]
)
user_info = hydra.get("{}/userinfo".format(HYDRA_PUBLIC_URL))
return user_info.json()
except Exception as err:
raise HydraError(str(err), 500)

View file

@ -1,5 +1,8 @@
alembic==1.8.1
attrs==21.4.0
black==22.1.0
cachelib==0.9.0
cachetools==5.2.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
@ -9,32 +12,43 @@ Flask==2.0.3
Flask-Cors==3.0.10
flask-expects-json==1.7.0
Flask-JWT-Extended==4.3.1
Flask-Migrate==3.1.0
Flask-Session==0.4.0
Flask-SQLAlchemy==2.5.1
google-auth==2.14.0
greenlet==2.0.0.post0
gunicorn==20.1.0
hydra-client==0.4.0
idna==3.3
install==1.3.5
itsdangerous==2.1.1
jsonschema==4.4.0
Jinja2==3.0.3
jinja2-base64-filters==0.1.4
jsonschema==4.4.0
kubernetes==24.2.0
Mako==1.2.3
MarkupSafe==2.1.1
mypy-extensions==0.4.3
oauthlib==3.2.0
ory-kratos-client==0.9.0a2
pathspec==0.9.0
platformdirs==2.5.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21
PyJWT==2.3.0
PyMySQL==1.0.2
pyrsistent==0.18.1
python-dateutil==2.8.2
PyYAML==6.0
regex==2022.3.15
requests==2.27.1
requests-oauthlib==1.3.1
rsa==4.9
six==1.16.0
SQLAlchemy==1.4.42
tomli==1.2.3
typing-extensions==4.1.1
typing_extensions==4.1.1
urllib3==1.26.8
websocket-client==1.4.2
Werkzeug==2.0.3
ory-kratos-client==0.9.0a2
pymysql
Flask-SQLAlchemy
hydra-client
Flask-Migrate

View file

@ -1,6 +1,6 @@
/* base.js
This is the base JS file to render the user interfaces of kratos and provide
the end user with flows for login, recovery etc.
the end user with flows for login, recovery etc.
check_flow_*():
These functions check the status of the flow and based on the status do some
@ -261,8 +261,6 @@ function render_messages(data) {
// value: If there is already a value known, show it
// messages: error messages related to the field
function getFormElement(type, name, value, messages) {
console.log('Getting form element', type, name, value, messages);
if (value == undefined) {
value = '';
}
@ -350,7 +348,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
if (typeof help == 'undefined' || help == null) {
help = '';
}
console.log('Messages: ', messages);
// Id field for help element
var nameHelp = name + 'Help';
@ -362,7 +359,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
// messages get appended to help info
if (messages.length) {
for (message in messages) {
console.log('adding message', messages[message]);
help += messages[message]['text'];
}
}

View file

@ -9,7 +9,7 @@ import { useApps } from 'src/services/apps';
const navigation = [
{ name: 'Users', to: '/users', requiresAdmin: true },
{ name: 'Apps', to: '/apps', requiresAdmin: true },
// { name: 'Apps', to: '/apps', requiresAdmin: true },
];
function classNames(...classes: any[]) {
@ -33,8 +33,6 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
const { pathname } = useLocation();
const { apps } = useApps();
const navigationItems = filterNavigationByDashboardRole(isAdmin);
console.log(isAdmin);
console.log(navigationItems);
const signOutUrl = useMemo(() => {
// @ts-ignore
@ -86,9 +84,9 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
key={item.name}
to={item.to}
className={clsx(
'border-primary-50 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
'border-primary-50 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium litbutton',
{
'border-primary-500 text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium':
'border-primary-500 litbutton-active hover:border-gray-300 inline-flex items-center px-1 pt-1 text-sm font-medium':
pathname.includes(item.to),
},
)}

View file

@ -53,6 +53,11 @@ export const Users: React.FC = () => {
const columns: any = React.useMemo(
() => [
{
Header: 'Username',
accessor: 'preferredUsername',
width: 'auto',
},
{
Header: 'Name',
accessor: 'name',
@ -75,11 +80,12 @@ export const Users: React.FC = () => {
if (isAdmin) {
return (
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
<div className="text-right lg:opacity-0 transition-opacity">
<button
disabled
onClick={() => configureModalOpen(row.original.id)}
type="button"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray bg-gray hover:bg-gray focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Configure
@ -109,17 +115,19 @@ export const Users: React.FC = () => {
{isAdmin && (
<div className="mt-3 sm:mt-0 sm:ml-4">
<button
disabled
onClick={() => configureModalOpen(null)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
>
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new user
</button>
<button
disabled
onClick={() => setMultipleUsersModal(true)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
>
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new users

View file

@ -26,7 +26,7 @@ export const signIn = (params: string) =>
);
export function signOut() {
return async (dispatch: any) => {
return (dispatch: any) => {
dispatch(signOutAction());
};
}

View file

@ -12,7 +12,6 @@ export const getCurrentUser = (state: State) => state.auth.userInfo;
export const getIsAdmin = (state: State) => {
// check since old users wont have this
if (state.auth.userInfo) {
console.log(state.auth.userInfo);
if (!state.auth.userInfo.app_roles) {
return false;
}