Skip to content

Commit

Permalink
Merge pull request #484 from atviriduomenys/122-auth-clients-self-ser…
Browse files Browse the repository at this point in the history
…vice

122 Auth clients self service
  • Loading branch information
adp-atea authored Nov 14, 2023
2 parents 55a2918 + ea34b27 commit 1c3a624
Show file tree
Hide file tree
Showing 20 changed files with 1,718 additions and 4,173 deletions.
610 changes: 521 additions & 89 deletions notes/access/public.sh

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions notes/api/auth/clients.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# notes/docker.sh Start docker compose
# notes/postgres.sh Reset database

INSTANCE=api/auth/clients
DATASET=$INSTANCE
# notes/spinta/server.sh Configure server

cat > $BASEDIR/manifest.txt <<EOF
d | r | b | m | property | type | access
$DATASET | |
| | | City | |
| | | | name | string | open
EOF
poetry run spinta copy $BASEDIR/manifest.txt -o $BASEDIR/manifest.csv
cat $BASEDIR/manifest.csv
poetry run spinta show $BASEDIR/manifest.csv

# notes/spinta/server.sh Run migrations
# notes/spinta/server.sh Run server

# notes/spinta/client.sh Configure client
SERVER=:8000
CLIENT=test
SECRET=secret
SCOPES=(
spinta_set_meta_fields
spinta_getone
spinta_getall
spinta_search
spinta_changes
spinta_insert
spinta_upsert
spinta_update
spinta_patch
spinta_delete
spinta_wipe
spinta_auth_clients
)
http \
-a $CLIENT:$SECRET \
-f $SERVER/auth/token \
grant_type=client_credentials \
scope="$SCOPES"
#| HTTP/1.1 400 Bad Request
#|
#| {
#| "error": "invalid_scope",
#| "error_description": "The requested scope is invalid, unknown, or malformed."
#| }
tail -50 $BASEDIR/spinta.log
#| ERROR: Authorization server error: invalid_scope:
#| Traceback (most recent call last):
#| File "authlib/oauth2/rfc6749/authorization_server.py", line 185, in create_token_response
#| grant.validate_token_request()
#| File "authlib/oauth2/rfc6749/grants/client_credentials.py", line 72, in validate_token_request
#| self.validate_requested_scope(client)
#| File "authlib/oauth2/rfc6749/grants/base.py", line 92, in validate_requested_scope
#| raise InvalidScopeError(state=self.request.state)
#| authlib.oauth2.rfc6749.errors.InvalidScopeError: invalid_scope:
#| INFO: "POST /auth/token HTTP/1.1" 400 Bad Request

TOKEN=$(
http \
-a $CLIENT:$SECRET \
-f $SERVER/auth/token \
grant_type=client_credentials \
scope="$SCOPES" \
| jq -r .access_token
)
AUTH="Authorization: Bearer $TOKEN"
echo $AUTH
1 change: 1 addition & 0 deletions notes/spinta/server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ spinta_update
spinta_patch
spinta_delete
spinta_wipe
spinta_auth_clients
EOF


Expand Down
3,977 changes: 0 additions & 3,977 deletions poetry.lock

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ requests = "^2.28.1"
# setuptools>=58
setuptools = "^65.4.1"
setuptools-scm = "^7.0.5"
starlette = "^0.21.0"
starlette = "^0.22.0"
toposort = "^1.7"
tqdm = "^4.64.1"
ujson = "^5.5.0"
Expand Down
185 changes: 182 additions & 3 deletions spinta/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import os
import shutil
import uuid
import ruamel.yaml
from typing import Type

import pkg_resources as pres
import logging

from authlib.common.errors import AuthlibHTTPError
from authlib.oauth2.rfc6750.errors import InsufficientScopeError

from starlette.applications import Starlette
from starlette.exceptions import HTTPException
Expand All @@ -15,15 +20,17 @@
from starlette.routing import Route, Mount
from starlette.middleware import Middleware

from spinta import components
from spinta.auth import AuthorizationServer
from spinta import components, commands
from spinta.auth import AuthorizationServer, check_scope, query_client, get_clients_list, \
client_exists, create_client_file, delete_client_file, update_client_file, get_clients_path
from spinta.auth import ResourceProtector
from spinta.auth import BearerTokenValidator
from spinta.auth import get_auth_request
from spinta.auth import get_auth_token
from spinta.commands import prepare, get_version
from spinta.components import Context
from spinta.exceptions import BaseError, MultipleErrors, error_response
from spinta.exceptions import BaseError, MultipleErrors, error_response, InsufficientPermission, \
UnknownPropertyInRequest, InsufficientPermissionForUpdate, EmptyPassword
from spinta.middlewares import ContextMiddleware
from spinta.urlparams import Version
from spinta.urlparams import get_response_type
Expand Down Expand Up @@ -69,6 +76,173 @@ async def auth_token(request: Request):
raise NoAuthServer()


def _auth_client_context(request: Request) -> Context:
context: Context = request.state.context
context.set('auth.request', get_auth_request({
'method': request.method,
'url': str(request.url.replace(query='')),
'body': None,
'headers': request.headers,
}))
context.set('auth.token', get_auth_token(context))
context.attach('accesslog', create_accesslog, context, loaders=(
context.get('store'),
context.get("auth.token"),
))
return context


async def auth_clients_add(request: Request):
try:
context = _auth_client_context(request)
check_scope(context, 'auth_clients')
config = context.get('config')
commands.load(context, config)

path = get_clients_path(config)
data = await request.json()
for key in data.keys():
if key not in ('client_name', 'secret', 'scopes'):
raise UnknownPropertyInRequest(property=key, properties=('client_name', 'secret', 'scopes'))
client_id = str(uuid.uuid4())
name = data["client_name"] if ("client_name" in data.keys() and data["client_name"]) else client_id

while client_exists(path, client_id):
client_id = str(uuid.uuid4())

if "secret" in data.keys() and not data["secret"] or "secret" not in data.keys():
raise EmptyPassword

client_file, client_ = create_client_file(
path,
name,
client_id,
data["secret"],
data["scopes"] if "scopes" in data.keys() else None,
)

return JSONResponse({
"client_id": client_["client_id"],
"client_name": name,
"scopes": client_["scopes"]
})

except InsufficientScopeError:
raise InsufficientPermission(scope='auth_clients')


async def auth_clients_get_all(request: Request):
try:
context = _auth_client_context(request)
check_scope(context, 'auth_clients')
config = context.get('config')
commands.load(context, config)

path = get_clients_path(config)
ids = get_clients_list(path)
return_values = []
for client_path in ids:
client = query_client(path, client_path)
return_values.append({
"client_id": client.id,
"client_name": client.name
})
return JSONResponse(return_values)

except InsufficientScopeError:
raise InsufficientPermission(scope='auth_clients')


async def auth_clients_get_specific(request: Request):
try:
context = _auth_client_context(request)
token = context.get('auth.token')
config = context.get('config')
commands.load(context, config)

client_id = request.path_params["client"]
if client_id != token.get_client_id():
check_scope(context, 'auth_clients')

path = get_clients_path(config)
client = query_client(path, client_id)
return_value = {
"client_id": client.id,
"client_name": client.name,
"scopes": list(client.scopes)
}
return JSONResponse(return_value)

except InsufficientScopeError:
raise InsufficientPermission(scope='auth_clients')


async def auth_clients_delete_specific(request: Request):
try:
context = _auth_client_context(request)
client_id = request.path_params["client"]
check_scope(context, 'auth_clients')
config = context.get('config')
commands.load(context, config)

path = get_clients_path(config)
delete_client_file(path, client_id)
return Response(status_code=204)

except InsufficientScopeError:
raise InsufficientPermission(scope='auth_clients')


async def auth_clients_patch_specific(request: Request):
try:
context = _auth_client_context(request)
token = context.get('auth.token')
client_id = request.path_params["client"]

config = context.get('config')
commands.load(context, config)
path = get_clients_path(config)

has_permission = True
try:
check_scope(context, 'auth_clients')
except InsufficientScopeError:
has_permission = False
if client_id != token.get_client_id() and not has_permission:
raise InsufficientScopeError

data = await request.json()
for key in data.keys():
if key not in ('client_name', 'secret', 'scopes'):
properties = ('secret')
if has_permission:
properties = ('client_name', 'secret', 'scopes')
raise UnknownPropertyInRequest(property=key, properties=properties)
elif key != 'secret' and not has_permission:
raise InsufficientPermissionForUpdate(field=key)

if "secret" in data.keys() and not data["secret"]:
raise EmptyPassword

new_data = update_client_file(
context,
path,
client_id,
name=data["client_name"] if "client_name" in data.keys() and data["client_name"] else None,
secret=data["secret"] if "secret" in data.keys() and data["secret"] else None,
scopes=data["scopes"] if "scopes" in data.keys() and data["scopes"] is not None else None,
)
return_value = {
"client_id": new_data["client_id"],
"client_name": new_data["client_name"],
"scopes": list(new_data["scopes"])
}
return JSONResponse(return_value)

except InsufficientScopeError:
raise InsufficientPermission(scope='auth_clients')


async def homepage(request: Request):
context: Context = request.state.context

Expand Down Expand Up @@ -188,6 +362,11 @@ def init(context: Context):
Route('/version', version, methods=['GET']),
Route('/auth/token', auth_token, methods=['POST']),
Route('/_srid/{srid:int}/{x:float}/{y:float}', srid_check, methods=['GET']),
Route('/auth/clients', auth_clients_get_all, methods=['GET']),
Route('/auth/clients', auth_clients_add, methods=['POST']),
Route('/auth/clients/{client}', auth_clients_get_specific, methods=['GET']),
Route('/auth/clients/{client}', auth_clients_delete_specific, methods=['DELETE']),
Route('/auth/clients/{client}', auth_clients_patch_specific, methods=['PATCH'])
]

if config.docs_path:
Expand Down
Loading

0 comments on commit 1c3a624

Please sign in to comment.