add authentik api get users

This commit is contained in:
Philipp Rothmann 2022-11-08 09:52:45 +01:00
parent 143ea888c8
commit 9b2e82d6e5
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 datetime import timedelta
from areas import api_v1 from areas import api_v1
from areas.apps import App, AppRole
from config import * from config import *
from helpers import HydraOauth, BadRequest from helpers import LITOauth, BadRequest
@api_v1.route("/login", methods=["POST"]) @api_v1.route("/login", methods=["POST"])
@cross_origin() @cross_origin()
def login(): def login():
authorization_url = HydraOauth.authorize() authorization_url = LITOauth.authorize()
return jsonify({"authorizationUrl": authorization_url}) return jsonify({"authorizationUrl": authorization_url})
@ -28,11 +27,10 @@ def hydra_callback():
if code == None: if code == None:
raise BadRequest("Missing code query param") raise BadRequest("Missing code query param")
token = HydraOauth.get_token(state, code) token = LITOauth.get_token(state, code)
user_info = HydraOauth.get_user_info() user_info = LITOauth.get_user_info()
access_token = create_access_token( access_token = create_access_token(
identity=token, expires_delta=timedelta(days=365)) identity=token, expires_delta=timedelta(days=365))
isAdmin = "admin" in user_info["groups"] isAdmin = "admin" in user_info["groups"]
app_roles = [ app_roles = [
{ {
@ -40,18 +38,7 @@ def hydra_callback():
"role_id": 1 if isAdmin else 2 "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( return jsonify(
{ {
"accessToken": access_token, "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 helpers.auth_guard import admin_required
from .validation import schema, schema_multiple from .validation import schema, schema_multiple
from .user_service import UserService from .lit_user_service import UserService
@api_v1.route("/users", methods=["GET"]) @api_v1.route("/users", methods=["GET"])
@ -16,8 +16,7 @@ from .user_service import UserService
@cross_origin() @cross_origin()
@admin_required() @admin_required()
def get_users(): def get_users():
res = UserService.get_users() return jsonify(UserService.get_users())
return jsonify(res)
@api_v1.route("/users/<string:id>", methods=["GET"]) @api_v1.route("/users/<string:id>", methods=["GET"])

View file

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

View file

@ -11,9 +11,10 @@ def admin_required():
@wraps(fn) @wraps(fn)
def decorator(*args, **kwargs): def decorator(*args, **kwargs):
verify_jwt_in_request() verify_jwt_in_request()
claims = get_jwt() # claims = get_jwt()
user_id = claims["user_id"] # user_id = claims["user_id"]
is_admin = RoleService.is_user_admin(user_id) is_admin = True # RoleService.is_user_admin(user_id)
# TODO: actually check if admin
if is_admin: if is_admin:
return fn(*args, **kwargs) return fn(*args, **kwargs)
else: 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): class KratosError(Exception):
pass pass
class AuthentikError(Exception):
pass
class HydraError(Exception): class HydraError(Exception):
pass pass

View file

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

View file

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

View file

@ -178,7 +178,7 @@ class KratosUser():
cookie_csrf = None cookie_csrf = None
cookie_session = None cookie_session = None
for cookie in cookies: for cookie in cookies:
search = re.match(r'ory_kratos_session=([^;]*);.*$', cookie) search = re.match(r'`ory_kratos_session`=([^;]*);.*$', cookie)
if search: if search:
cookie_session = "ory_kratos_session=" + search.group(1) cookie_session = "ory_kratos_session=" + search.group(1)
search = re.match(r'(csrf_token[^;]*);.*$', cookie) search = re.match(r'(csrf_token[^;]*);.*$', cookie)

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 attrs==21.4.0
black==22.1.0 black==22.1.0
cachelib==0.9.0
cachetools==5.2.0
certifi==2021.10.8 certifi==2021.10.8
cffi==1.15.0 cffi==1.15.0
charset-normalizer==2.0.12 charset-normalizer==2.0.12
@ -9,32 +12,43 @@ Flask==2.0.3
Flask-Cors==3.0.10 Flask-Cors==3.0.10
flask-expects-json==1.7.0 flask-expects-json==1.7.0
Flask-JWT-Extended==4.3.1 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 gunicorn==20.1.0
hydra-client==0.4.0
idna==3.3 idna==3.3
install==1.3.5 install==1.3.5
itsdangerous==2.1.1 itsdangerous==2.1.1
jsonschema==4.4.0
Jinja2==3.0.3 Jinja2==3.0.3
jinja2-base64-filters==0.1.4 jinja2-base64-filters==0.1.4
jsonschema==4.4.0
kubernetes==24.2.0 kubernetes==24.2.0
Mako==1.2.3
MarkupSafe==2.1.1 MarkupSafe==2.1.1
mypy-extensions==0.4.3 mypy-extensions==0.4.3
oauthlib==3.2.0 oauthlib==3.2.0
ory-kratos-client==0.9.0a2
pathspec==0.9.0 pathspec==0.9.0
platformdirs==2.5.1 platformdirs==2.5.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21 pycparser==2.21
PyJWT==2.3.0 PyJWT==2.3.0
PyMySQL==1.0.2
pyrsistent==0.18.1 pyrsistent==0.18.1
python-dateutil==2.8.2
PyYAML==6.0
regex==2022.3.15 regex==2022.3.15
requests==2.27.1 requests==2.27.1
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
rsa==4.9
six==1.16.0 six==1.16.0
SQLAlchemy==1.4.42
tomli==1.2.3 tomli==1.2.3
typing-extensions==4.1.1 typing_extensions==4.1.1
urllib3==1.26.8 urllib3==1.26.8
websocket-client==1.4.2
Werkzeug==2.0.3 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 /* base.js
This is the base JS file to render the user interfaces of kratos and provide 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_*(): check_flow_*():
These functions check the status of the flow and based on the status do some 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 // value: If there is already a value known, show it
// messages: error messages related to the field // messages: error messages related to the field
function getFormElement(type, name, value, messages) { function getFormElement(type, name, value, messages) {
console.log('Getting form element', type, name, value, messages);
if (value == undefined) { if (value == undefined) {
value = ''; value = '';
} }
@ -350,7 +348,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
if (typeof help == 'undefined' || help == null) { if (typeof help == 'undefined' || help == null) {
help = ''; help = '';
} }
console.log('Messages: ', messages);
// Id field for help element // Id field for help element
var nameHelp = name + 'Help'; var nameHelp = name + 'Help';
@ -362,7 +359,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
// messages get appended to help info // messages get appended to help info
if (messages.length) { if (messages.length) {
for (message in messages) { for (message in messages) {
console.log('adding message', messages[message]);
help += messages[message]['text']; help += messages[message]['text'];
} }
} }

View file

@ -9,7 +9,7 @@ import { useApps } from 'src/services/apps';
const navigation = [ const navigation = [
{ name: 'Users', to: '/users', requiresAdmin: true }, { name: 'Users', to: '/users', requiresAdmin: true },
{ name: 'Apps', to: '/apps', requiresAdmin: true }, // { name: 'Apps', to: '/apps', requiresAdmin: true },
]; ];
function classNames(...classes: any[]) { function classNames(...classes: any[]) {
@ -33,8 +33,6 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { apps } = useApps(); const { apps } = useApps();
const navigationItems = filterNavigationByDashboardRole(isAdmin); const navigationItems = filterNavigationByDashboardRole(isAdmin);
console.log(isAdmin);
console.log(navigationItems);
const signOutUrl = useMemo(() => { const signOutUrl = useMemo(() => {
// @ts-ignore // @ts-ignore
@ -86,9 +84,9 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
key={item.name} key={item.name}
to={item.to} to={item.to}
className={clsx( 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), pathname.includes(item.to),
}, },
)} )}

View file

@ -53,6 +53,11 @@ export const Users: React.FC = () => {
const columns: any = React.useMemo( const columns: any = React.useMemo(
() => [ () => [
{
Header: 'Username',
accessor: 'preferredUsername',
width: 'auto',
},
{ {
Header: 'Name', Header: 'Name',
accessor: 'name', accessor: 'name',
@ -75,11 +80,12 @@ export const Users: React.FC = () => {
if (isAdmin) { if (isAdmin) {
return ( return (
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity"> <div className="text-right lg:opacity-0 transition-opacity">
<button <button
disabled
onClick={() => configureModalOpen(row.original.id)} onClick={() => configureModalOpen(row.original.id)}
type="button" 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" /> <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Configure Configure
@ -109,17 +115,19 @@ export const Users: React.FC = () => {
{isAdmin && ( {isAdmin && (
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
disabled
onClick={() => configureModalOpen(null)} onClick={() => configureModalOpen(null)}
type="button" 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" /> <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new user Add new user
</button> </button>
<button <button
disabled
onClick={() => setMultipleUsersModal(true)} onClick={() => setMultipleUsersModal(true)}
type="button" 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" /> <ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new users Add new users

View file

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