feat(webapi): implement Group resource endpoints

This commit is contained in:
Julian Lobbes 2022-11-29 17:14:39 +01:00
parent 00c2715a83
commit 2d48a26c51
2 changed files with 172 additions and 186 deletions

View File

@ -51,6 +51,7 @@ def create_app(test_config=None):
from . import webapi
api.add_resource(webapi.UserResource, '/api/user/<string:username>')
api.add_resource(webapi.GroupResource, '/api/group/<string:groupname>')
api.add_resource(webapi.GroupMemberResource, '/api/group/<string:groupname>/member/<string:username>')
api.init_app(app)
# TODO create OUs

View File

@ -35,7 +35,11 @@ class GroupEncoder(JSONEncoder):
class UserResource(Resource):
"""The UserResource is used for API access to users."""
def get(self, username):
"""Returns the specified in JSON format."""
try:
conn = ldap.get_connection()
except:
@ -46,103 +50,18 @@ class UserResource(Resource):
return {"message": f"User '{username}' does not exist."}, 400
return {
"user": {
"username": user.username,
"password_hash": user.password_hash,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"display_name": user.display_name,
"picture": user.get_picture_url(),
}
}
def get_group_from_json(input_dict: dict) -> Group:
"""Creates a Group object using the input dictionary.
The input must be a dictionary of the following format:
{
"group": {
"groupname": "mygroup",
"members": [
"alice",
"bob",
"carlie"
]
}
}
Returns
-------
Group
The Group object equivalent to the input dict.
Raises
------
TypeError
If the input is not of type dict.
json.JSONDecodeError
If the input string is not valid JSON.
lumi2.ldap.EntryNotFoundException
If any of the users listed as members do not exist.
ValueError
If the input string's format is incorrect, or if the list of Group
members is empty.
"""
if not isinstance(input_dict, dict):
raise ValueError(f"Expected a dictionary but got: '{type(input_dict)}'.")
if len(input_dict) != 1 or 'group' not in input_dict.keys():
raise ValueError(f"Expected exactly one entry called 'group'.")
group_dict = input_dict['group']
if not isinstance(group_dict, dict):
raise ValueError(f"Expected a dictionary but got: '{type(group_dict)}'.")
if len(group_dict.keys()) != 2:
raise ValueError("Invalid number of keys in Group entry.")
for required_key in ['groupname', 'members']:
if required_key not in group_dict.keys():
raise ValueError(f"Expected a key called '{required_key}' in Group entry.")
groupname = group_dict['groupname']
if not isinstance(groupname, str):
raise ValueError("Expected the value for 'groupname' to be a string.")
try:
Group.assert_is_valid_groupname(groupname)
except InvalidStringFormatException as e:
raise ValueError(f"Invalid group name: {e}")
member_usernames = group_dict['members']
if not isinstance(member_usernames, list):
raise ValueError("Expected the value for 'members' to be a list.")
if not len(member_usernames):
raise ValueError("Group must have at least one member.")
members = set()
conn = ldap.get_connection()
for username in member_usernames:
if not isinstance(username, str):
raise ValueError("Member list may contain only strings.")
members.add(ldap.get_user(conn, username))
conn.unbind()
return Group(groupname, members)
"username": user.username,
"password_hash": user.password_hash,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"display_name": user.display_name,
"picture": user.get_picture_url(),
}, 200
class GroupResource(Resource):
"""The GroupResource represents a Group object in the REST API.
In JSON, a Group is represented as follows:
{
"groupname": "mygroup",
"members": [
"alice",
"bob",
"charlie"
]
}
"""
"""The GroupResource represents a Group object in the REST API."""
def get(self, groupname: str):
"""Retrieves the group specified by the groupname as a JSON object.
@ -156,8 +75,8 @@ class GroupResource(Resource):
-------
json : str , status : int
A JSON string and HTTP status code.
If the request was handled successfully,
"""
try:
conn = ldap.get_connection()
except:
@ -165,131 +84,123 @@ class GroupResource(Resource):
try:
group = ldap.get_group(conn, groupname)
except ldap.EntryNotFoundException:
return {"message": f"Group '{groupname}' does not exist."}, 400
return {"message": f"Group '{groupname}' does not exist."}, 404
return {
"group": {
"groupname": group.groupname,
"members": [user.username for user in group.members],
}
"groupname": group.groupname,
"members": [user.username for user in group.members],
}, 200
def post(self, groupname: str):
"""Creates the specified Group with the members listed in the JSON data.
The request is expected to contain JSON data in the following format:
{
"members": [
"alice",
"bob",
"charlie"
]
}
def put(self, groupname):
Returns
-------
json : str , status : int
A JSON string and HTTP status code.
"""
group_dict = request.get_json()
if not isinstance(group_dict, dict):
return {"message": f"Invalid format: expected an object but got: '{type(group_dict)}'."}, 400
if len(group_dict.keys()) != 1:
return {"message": "Invalid number of keys in Group object: expected exactly one key."}, 400
if "members" not in group_dict.keys():
return {"message": "Expected a key called 'members' in the object."}, 400
try:
Group.assert_is_valid_groupname(groupname)
except InvalidStringFormatException as e:
return {"message": f"Invalid group name: {e}"}, 400
member_usernames = group_dict['members']
if not isinstance(member_usernames, list):
return {"message": "Expected the value for 'members' to be a list."}, 400
if not len(member_usernames):
return {"message": "Group must have at least one member."}, 400
members = set()
try:
conn = ldap.get_connection()
except:
return 500
try:
# Make sure the requested group exists
group = ldap.get_group(conn, groupname)
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"Group '{groupname}' does not exist."}, 400
try:
# Parse the JSON-submitted group
group = get_group_from_json(request.get_json())
except JSONDecodeError as e:
conn.unbind()
return {"message": f"Invalid JSON format: {e}"}, 400
except ValueError as e:
conn.unbind()
return {"message": f"Invalid string format: {e}"}, 400
except ldap.EntryNotFoundException as e:
conn.unbind()
return {"message": f"Entry not found: {e}"}
if not group.groupname == groupname:
conn.unbind()
return {"message": "Groupname mismatch between endpoint and submitted data."}
ldap.update_group(conn, group)
for username in member_usernames:
if not isinstance(username, str):
conn.unbind()
return {"message": "Member list may contain only strings."}, 400
try:
members.add(ldap.get_user(conn, username))
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"No such user: '{username}'."}, 400
group = Group(groupname, members)
try:
# Make sure the requested group does not exist yet
group = ldap.get_group(conn, group.groupname)
conn.unbind()
return {"message": f"Group '{group.groupname}' already exists."}, 400
except ldap.EntryNotFoundException:
pass
ldap.create_group(conn, group)
conn.unbind()
return {
"group": {
"groupname": group.groupname,
"members": [user.username for user in group.members],
}
}
"groupname": group.groupname,
"members": [user.username for user in group.members],
}, 200
def add_user_to_group(self, groupname):
"""Accepts a PUT request to add a user to the specified Group.
def delete(self, groupname):
"""Deletes the specified Group.
The request data must be a JSON string of the following format:
{
"username": "my-user"
}
The user must exist on the server.
If the user is already a group member, no action is taken.
Returns
-------
json : str , status : int
A JSON string and HTTP status code.
"""
try:
conn = ldap.get_connection()
except:
return 500
try:
# Make sure the requested exists
group = ldap.get_group(conn, groupname)
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"Group '{groupname}' does not exist."}, 404
container_dict = request.get_json()
if not isinstance(container_dict, dict):
conn.unbind()
return {"message": "Invalid data: expected a JSON object."}, 400
if len(container_dict.keys()) != 1:
conn.unbind()
return {"message": "Invalid data: too many keys (expected 1)."}, 400
if not "username" in container_dict.keys():
conn.unbind()
return {"message": "Invalid data: no 'username' key found."}, 400
username = container_dict['username']
if not isinstance(username, str):
conn.unbind()
return {"message": "Invalid data: 'username' must be a string."}, 400
try:
User.assert_is_valid_username(username)
except InvalidStringFormatException as e:
conn.unbind()
return {"message": f"Invalid username: {e}"}, 400
try:
user = ldap.get_user(conn, username)
except ldap.EntryNotFoundException as e:
conn.unbind()
return {"message": f"User '{username}' does not exist."}, 400
if username in group.members:
conn.unbind()
return {"username": username}, 200
group.members.add(user)
ldap.update_group(conn, group)
ldap.delete_group(conn, groupname)
conn.unbind()
return {"username": username}, 200
return None, 200
class GroupMemberResource(Resource):
"""This resource represents the member of a group.
"""This resource represents the member of a Group."""
In JSON, a GroupMember is represented as follows:
{
"username": "myuser"
}
"""
def post(self, groupname):
"""Adds the user specified in the POST data to the specified Group."""
pass
def delete(self, groupname):
"""Removes the user specified in the POST data from the specified Group.
def post(self, groupname: str, username: str):
"""Adds the specified user to the specified Group.
Parameters
----------
username : str
The username of the User who will be added to the specified Group.
groupname : str
The name of the Group from which a member will be deleted.
The name of the Group to which the specified User will be added.
Returns
-------
@ -301,4 +212,78 @@ class GroupMemberResource(Resource):
error message and HTTP error code are returned.
"""
pass
try:
conn = ldap.get_connection()
except:
return 500
try:
group = ldap.get_group(conn, groupname)
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"Group '{groupname}' does not exist."}, 404
try:
user = ldap.get_user(conn, username)
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"User '{username}' does not exist."}, 404
if user in group.members:
conn.unbind()
return {"message": f"User '{username}' is already a member of the Group '{group.groupname}'."}, 400
group.members.add(user)
ldap.update_group(conn, group)
conn.unbind()
return {
"groupname": group.groupname,
"members": [user.username for user in group.members],
}, 200
def delete(self, groupname: str, username: str):
"""Removes the specified User from the specified Group.
Parameters
----------
username : str
The username of the User who will be removed from the specified Group.
groupname : str
The name of the Group from which the specified User will be removed.
Returns
-------
json : str , status : int
A JSON string and HTTP status code.
If the request was handled successfully, the POST-data is
replied to the client and HTTP code 200 is returned.
If a failure occurred while processing the request, an appropriate
error message and HTTP error code are returned.
"""
try:
conn = ldap.get_connection()
except:
return 500
try:
group = ldap.get_group(conn, groupname)
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"Group '{groupname}' does not exist."}, 404
try:
user = ldap.get_user(conn, username)
except ldap.EntryNotFoundException:
conn.unbind()
return {"message": f"User '{username}' does not exist."}, 404
if user not in group.members:
conn.unbind()
return {"message": f"User '{username}' is not a member of the Group '{group.groupname}'."}, 400
group.members.remove(user)
ldap.update_group(conn, group)
conn.unbind()
return None, 200