Skip to content
This repository has been archived by the owner on Mar 17, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1 from ireceptorplus-inesctec/master
Browse files Browse the repository at this point in the history
Final changes to repo
  • Loading branch information
edgar-simao authored Mar 17, 2024
2 parents e73ff8c + 9d6c80e commit 0382111
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 97 deletions.
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
DB_DATABASE=keycloak
DB_USER=keycloak
DB_PASSWORD=password
DB_HOST=localhost
DB_PORT=5433
DB_POOL_SIZE=20
KEYCLOAK_URL=http://localhost:8080/auth
REALM=master
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
context: ./
file: ./Dockerfile
push: true
tags: edgar1simao/keycloak_extension_api:${{ env.RELEASE_VERSION }},edgar1simao/keycloak_extension_api:latest
tags: irpinesctec/keycloak_extension_api:${{ env.RELEASE_VERSION }},irpinesctec/keycloak_extension_api:latest
build-args: |
RELEASE_VERSION=${{ env.RELEASE_VERSION }}
Expand Down
16 changes: 7 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
FROM ubuntu:20.04
FROM python:3.9-alpine3.15

RUN apt-get update -y && \
apt-get install -y build-essential python3-pip python3-dev
COPY requirements.txt /tmp/requirements.txt

WORKDIR /app

COPY ./requirements.txt ./requirements.txt
RUN apk add --no-cache --virtual .build-deps gcc libc-dev \
&& pip install --no-cache-dir -r /tmp/requirements.txt \
&& apk del .build-deps gcc libc-dev

RUN pip3 install -r requirements.txt
WORKDIR /app

COPY . /app

EXPOSE 5000

ENTRYPOINT [ "gunicorn" ]

CMD [ "--workers=4", "--bind=0.0.0.0:8000", "api:app" ]
CMD [ "--workers=4", "--bind=0.0.0.0:5000", "api:app" ]
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Keycloak Extension API

Service that extends the features of Keycloak by providing additional endpoints. The service connects directly to Keycloak's database.

Docker image available at: https://hub.docker.com/r/irpinesctec/keycloak_extension_api

### Requirements

- Python 3
- Keycloak running based on a PostreSQL database
- Check `requirements.txt` for additional package requirements. Can be installed through PIP or Conda.

### Running

#### Natively

1. Copy the `.env.example`and rename it to `.env`.
Open the file and edit it to match your environment.

2. Install requirements using your Python package manager of choice. With PIP:
`pip install -r requirements.txt`

3. `python3 api.py`

#### Docker
1. Copy the `db_connection.env.example`and rename it to `db_connection.env`.
Open the file and edit it to match your environment.

2. Edit the `docker-compose.yml` to match your requirements`

3. `docker-compose up`
155 changes: 99 additions & 56 deletions api.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,42 @@
import json
import jsonify
import requests
from flask import Flask, request, abort, jsonify
import psycopg2
import random
import string
import uuid
import time
import os
from psycopg2 import pool
import os, sys

app = Flask(__name__)

SERVERS = ['adc-middleware']

POLICY_INSERT = "INSERT INTO resource_server_policy " + \
"(id, name, type, resource_server_id, owner) " + \
"VALUES " + \
"('{0}', '{1}', 'uma', '{2}', '{3}')"

TICKET_INSERT = "INSERT INTO resource_server_perm_ticket " + \
"(id, owner, requester, created_timestamp, granted_timestamp, resource_id, scope_id, resource_server_id, policy_id) " + \
"VALUES " + \
"('{0}', '{1}', '{2}', '{3}', '{3}', '{4}', '{5}', '{6}', '{7}')"
try:
app.config["pool"] = psycopg2.pool.ThreadedConnectionPool(1, 20,
host=os.environ['DB_HOST'],
port=os.environ['DB_PORT'],
database=os.environ['DB_DATABASE'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD']
)
except (Exception, psycopg2.DatabaseError) as error:
print("Error while connecting to PostgreSQL. Please check your environment settings.", error)
sys.exit(-1)

def start_connection():
try:
return psycopg2.connect(
database=os.environ['DB_DATABASE'], user=os.environ['DB_USER'], password=os.environ['DB_PASSWORD'],
host=os.environ['DB_HOST'], port=os.environ['DB_PORT']
)
except:
abort(401, "Could not connect to DB")
return app.config["pool"].getconn()
except Exception as error:
abort(401, "Could not connect to Keycloak's database. Please check Keycloak Extension's environment settings.: {}".format(error))

def check_request_validity(auth_header, where):
def check_request_validity(auth_header):
if auth_header == None:
abort(401, 'No token')

if where not in SERVERS:
abort(401, 'Server does not exist')
if 'owner_id' not in request.form:
abort(401, "'owner_id' is not present in form request")

if get_user_info(auth_header[7:])['sub'] != request.form['owner_id']:
abort(401, 'Request can only be made by the resource owner')

def get_user_info(token):
url = os.environ['KEYCLOAK_URL'] \
+ 'realms/' \
+ '/realms/' \
+ os.environ['REALM'] \
+ '/protocol/openid-connect/userinfo'

Expand All @@ -55,80 +47,131 @@ def get_user_info(token):
response = requests.get(url, headers=headers)

if response.status_code != 200:
abort(401, 'Invalid token')
abort(401, response.json())

return response.json()

def get_user_id(email_user):
conn = start_connection()

cursor = conn.cursor()

try:
cursor.execute("SELECT id FROM user_entity WHERE email LIKE '{0}' OR username LIKE '{0}' OR id LIKE '{0}'".format(email_user))

id = cursor.fetchone()[0]
except:
abort(401, "Could not find user")

cursor.close()
conn.close()
app.config["pool"].putconn(conn)

return id

@app.route('/give_access/<where>', methods=['POST'])
def give_access(where):
auth_header = request.headers.get('Authorization')
def get_user_email(user_id):
conn = start_connection()
cursor = conn.cursor()

try:
cursor.execute("SELECT email FROM user_entity WHERE id LIKE '{0}' OR username LIKE '{0}'".format(user_id))
id = cursor.fetchone()[0]
except:
abort(401, "Could not find user")

check_request_validity(auth_header, where)
cursor.close()
app.config["pool"].putconn(conn)

conn = start_connection()
return id

def get_scope_id(scope_name):
conn = start_connection()
cursor = conn.cursor()

try:
cursor.execute("SELECT id FROM client WHERE client_id LIKE '{0}'".format(where))
cursor.execute("SELECT id FROM resource_server_scope WHERE name LIKE '{0}'".format(scope_name))

resource_server_id = cursor.fetchone()[0]
id = cursor.fetchone()[0]
except:
abort(401, "Could not find client")
abort(401, "Could not find scope")

policy_id = uuid.uuid1()
cursor.close()
app.config["pool"].putconn(conn)

try:
cursor.execute(POLICY_INSERT.format(policy_id, uuid.uuid1(), resource_server_id, request.form['owner_id']))
except:
abort(401, "Could not create ticket, maybe permission already exists?")
return id

@app.route('/get_user_scope_id/<email_user>', methods=['POST'])
def get_user_scope_id(email_user):
auth_header = request.headers.get('Authorization')
check_request_validity(auth_header)
user_id = get_user_id(email_user)
scope_id = get_scope_id(request.form['scope_name'])

return jsonify([user_id, scope_id])

@app.route('/get_user_id/<email_user>', methods=['POST'])
def get_user_id_rest(email_user):
auth_header = request.headers.get('Authorization')
check_request_validity(auth_header)
user_id = get_user_id(email_user)

return jsonify(user_id)

@app.route('/get_user_email/<user_id>', methods=['POST'])
def get_user_email_rest(user_id):
auth_header = request.headers.get('Authorization')
check_request_validity(auth_header)

user_email = get_user_email(user_id)

return jsonify(user_email)

@app.route('/change_owner/<resource_id>/<new_owner>', methods=['POST'])
def change_owner(resource_id, new_owner):
auth_header = request.headers.get('Authorization')

check_request_validity(auth_header)

conn = start_connection()
cursor = conn.cursor()

try:
cursor.execute("SELECT id FROM resource_server_scope WHERE name LIKE '{0}' AND resource_server_id LIKE '{1}'".format(request.form['scope_name'], resource_server_id))
cursor.execute("SELECT owner FROM resource_server_resource WHERE id LIKE '{0}'".format(resource_id))

scope_id = cursor.fetchone()[0]
resource_owner_id = cursor.fetchone()[0]
except:
abort(401, "Could not find scope")
abort(404, "Could not find resource")

current_time = int(round(time.time() * 1000))
if resource_owner_id != get_user_info(auth_header[7:])['sub']:
abort(401, "Not owner of resource")

requester_id = get_user_id(request.form['requester'])
try:
cursor.execute("SELECT id FROM user_entity WHERE email LIKE '{0}' OR username LIKE '{0}' OR id LIKE '{0}'".format(new_owner))

new_owner_id = cursor.fetchone()[0]
except:
abort(401, "Could not find user")

try:
cursor.execute(TICKET_INSERT.format(uuid.uuid1(), request.form['owner_id'], requester_id, current_time, request.form['resource_id'], scope_id, resource_server_id, policy_id))
owner_update = "UPDATE resource_server_resource " + \
"SET owner = '{0}' " + \
"WHERE id LIKE '{1}'"
cursor.execute(owner_update.format(new_owner_id, resource_id))
except:
abort(401, "Could not create ticket, maybe permission already exists?")
abort(401, "Update failed")

conn.commit()

cursor.close()
app.config["pool"].putconn(conn)

conn.close()

return jsonify("Ticket created successfully")
return jsonify("Owner changed successfully")

@app.errorhandler(401)
def unauthorized(error):
response = jsonify({'message': error.description})
response.status_code = 401
return response

@app.errorhandler(500)
def internal_error(error):
return "Uncaught exception: is keycloak reachable?"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
8 changes: 0 additions & 8 deletions db_connection.env.example

This file was deleted.

28 changes: 5 additions & 23 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
astroid==2.4.2
certifi==2020.4.5.1
chardet==3.0.4
click==7.1.2
Flask==1.1.2
gunicorn==20.0.4
idna==2.9
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.11.2
jsonify==0.5
lazy-object-proxy==1.4.3
MarkupSafe==1.1.1
mccabe==0.6.1
psycopg2-binary==2.8.5
pylint==2.5.3
python-dotenv==0.14.0
requests==2.23.0
six==1.15.0
toml==0.10.1
urllib3==1.25.9
Werkzeug==1.0.1
wrapt==1.12.1
requests==2.27.1
Flask==2.0.3
psycopg2-binary==2.9.3
python-dotenv==0.19.2
gunicorn==20.1.0

0 comments on commit 0382111

Please sign in to comment.