diff --git a/lumi2/__init__.py b/lumi2/__init__.py index 169291c..630809b 100644 --- a/lumi2/__init__.py +++ b/lumi2/__init__.py @@ -51,6 +51,7 @@ def create_app(test_config=None): from . import webapi api.add_resource(webapi.UserResource, '/api/user/') api.add_resource(webapi.GroupResource, '/api/group/') + api.add_resource(webapi.GroupMemberResource, '/api/group//member/') api.init_app(app) # TODO create OUs diff --git a/lumi2/webapi.py b/lumi2/webapi.py index 8668f75..ce41ead 100644 --- a/lumi2/webapi.py +++ b/lumi2/webapi.py @@ -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