def register_user_with_group(user_name, group_name, email, password, db_session): # type: (Str, Str, Str, Str, Session) -> None """ Registers the user if missing and associate him to a group specified by name, also created if missing. :param user_name: name of the user to create (if missing) and to make part of the group (if specified) :param group_name: name of the group to create (if missing and specified) and to make the user join (if not already) :param email: email of the user to be created (if missing) :param password: password of the user to be created (if missing) :param db_session: database connexion to apply changes .. warning:: Should be employed only for **special** users/groups in this module as other expected API behaviour and operations will not be applied (ex: create additional permissions or user-group references). """ if not GroupService.by_group_name(group_name, db_session=db_session): new_group = models.Group(group_name=group_name) # noqa db_session.add(new_group) registered_group = GroupService.by_group_name(group_name=group_name, db_session=db_session) registered_user = UserService.by_user_name(user_name, db_session=db_session) if not registered_user: new_user = models.User(user_name=user_name, email=email) # noqa UserService.set_password(new_user, password) UserService.regenerate_security_code(new_user) db_session.add(new_user) if group_name is not None: registered_user = UserService.by_user_name(user_name, db_session=db_session) else: print_log("User '{}' already exist".format(user_name), level=logging.DEBUG) try: # ensure the reference between user/group exists (user joined the group) user_group_refs = BaseService.all(models.UserGroup, db_session=db_session) user_group_refs_tup = [(ref.group_id, ref.user_id) for ref in user_group_refs] if (registered_group.id, registered_user.id) not in user_group_refs_tup: group_entry = models.UserGroup(group_id=registered_group.id, user_id=registered_user.id) # noqa db_session.add(group_entry) except Exception: # noqa: W0703 # nosec: B110 # in case reference already exists, avoid duplicate error db_session.rollback()
def assign_user_group_view(request): """ Assign a user to a group. """ user = ar.get_user_matchdict_checked_or_logged(request) group_name = ar.get_value_multiformat_body_checked(request, "group_name") group = ax.evaluate_call( lambda: GroupService.by_group_name(group_name, db_session=request.db), fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, msg_on_fail=s.UserGroups_POST_ForbiddenResponseSchema.description) ax.verify_param( group, not_none=True, http_error=HTTPNotFound, msg_on_fail=s.UserGroups_POST_GroupNotFoundResponseSchema.description) uu.assign_user_group(user, group, db_session=request.db) return ax.valid_http( http_success=HTTPCreated, detail=s.UserGroups_POST_CreatedResponseSchema.description, content={ "user_name": user.user_name, "group_name": group.group_name })
def test_by_group_name_wrong_groupname(self, db_session): add_group(db_session) queried_group = GroupService.by_group_name( "not existing group", db_session=db_session ) assert queried_group is None
def init_anonymous(db_session, settings=None): # type: (Session, Optional[AnySettingsContainer]) -> None """ Registers into the database the user and group matching configuration values of. :py:data:`magpie.constants.MAGPIE_ANONYMOUS_USER` and :py:data:`magpie.constants.MAGPIE_ANONYMOUS_GROUP` respectively if not defined. Afterwards, updates the group's parameters to ensure integrity with `Magpie` settings. """ anonymous_group = get_constant("MAGPIE_ANONYMOUS_GROUP", settings_container=settings) register_user_with_group( user_name=get_constant("MAGPIE_ANONYMOUS_USER", settings_container=settings), group_name=anonymous_group, email=get_constant("MAGPIE_ANONYMOUS_EMAIL", settings_container=settings), password=get_constant("MAGPIE_ANONYMOUS_PASSWORD", settings_container=settings), db_session=db_session) # enforce some admin group fields group = GroupService.by_group_name(anonymous_group, db_session=db_session) group.description = "Group that grants public access to its members for applicable resources." group.discoverable = False
def init_admin(db_session, settings=None): # type: (Session, Optional[AnySettingsContainer]) -> None """ Registers into the database the user and group matching configuration values of. :py:data:`magpie.constants.MAGPIE_ADMIN_USER` and :py:data:`magpie.constants.MAGPIE_ADMIN_GROUP` respectively if not defined. Also associates the created admin user with the admin group and give it admin permissions. Finally, updates the group's parameters to ensure integrity with `Magpie` settings. """ admin_usr_name = get_constant("MAGPIE_ADMIN_USER", settings_container=settings) admin_grp_name = get_constant("MAGPIE_ADMIN_GROUP", settings_container=settings) if not (UserService.by_user_name(admin_usr_name, db_session=db_session) and GroupService.by_group_name(admin_grp_name, db_session=db_session)): register_user_with_group( user_name=admin_usr_name, group_name=admin_grp_name, email=get_constant("MAGPIE_ADMIN_EMAIL", settings_container=settings), password=get_constant("MAGPIE_ADMIN_PASSWORD", settings_container=settings), db_session=db_session) # Check if MAGPIE_ADMIN_GROUP has permission MAGPIE_ADMIN_PERMISSION magpie_admin_group = GroupService.by_group_name( admin_grp_name, db_session=db_session) # type: models.Group permission_names = [ permission.perm_name for permission in magpie_admin_group.permissions ] admin_perm = get_constant("MAGPIE_ADMIN_PERMISSION", settings_container=settings) if admin_perm not in permission_names: new_group_permission = models.GroupPermission( perm_name=admin_perm, group_id=magpie_admin_group.id) # noqa try: db_session.add(new_group_permission) except Exception as exc: db_session.rollback() raise_log("Failed to create admin user-group permission", exception=type(exc)) # enforce some admin group fields magpie_admin_group.description = "Administrative group that grants full access management control to its members." magpie_admin_group.discoverable = False
def validate_group_name(self, value): request = self.context['request'] modified_obj = self.context.get('modified_obj') group = GroupService.by_group_name(value, db_session=request.dbsession) if group: if not modified_obj or modified_obj.id != group.id: msg = _('Group already exists in database') raise validate.ValidationError(msg)
def by_group_name(cls, group_name, db_session=None): """ fetch group by name .. deprecated:: 0.8 """ db_session = get_db_session(db_session) return GroupService.by_group_name(group_name=group_name, db_session=db_session)
def _get_group(grp_name): # type: (Str) -> models.Group ax.verify_param(grp_name, not_none=True, not_empty=True, matches=True, param_compare=ax.PARAM_REGEX, param_name="group_name", http_error=HTTPBadRequest, msg_on_fail=s.UserGroup_Check_BadRequestResponseSchema.description) grp = ax.evaluate_call(lambda: GroupService.by_group_name(grp_name, db_session=db_session), http_error=HTTPForbidden, msg_on_fail=s.UserGroup_GET_ForbiddenResponseSchema.description) ax.verify_param(grp, not_none=True, http_error=HTTPNotFound, with_param=False, msg_on_fail=s.UserGroup_Check_NotFoundResponseSchema.description) return grp
def add_service_getcapabilities_perms(service, db_session, group_name=None): if service.type in SERVICES_PHOENIX_ALLOWED and \ Permission.GET_CAPABILITIES in SERVICE_TYPE_DICT[service.type].permissions: if group_name is None: group_name = get_constant("MAGPIE_ANONYMOUS_USER") group = GroupService.by_group_name(group_name, db_session=db_session) perm = ResourceService.perm_by_group_and_perm_name( service.resource_id, group.id, Permission.GET_CAPABILITIES.value, db_session) if perm is None: # not set, create it create_group_resource_permission_response( group, service, Permission.GET_CAPABILITIES, db_session)
def init_users_group(db_session, settings=None): # type: (Session, Optional[AnySettingsContainer]) -> None """ Registers into database the group matching :py:data:`magpie.constants.MAGPIE_USERS_GROUP` if not defined. """ usr_grp_name = get_constant("MAGPIE_USERS_GROUP", settings_container=settings) if not GroupService.by_group_name(usr_grp_name, db_session=db_session): user_group = models.Group(group_name=usr_grp_name) # noqa db_session.add(user_group) else: print_log("MAGPIE_USERS_GROUP already initialized", level=logging.DEBUG)
def get_group_matchdict_checked(request, group_name_key="group_name"): # type: (Request, Str) -> models.Group """ Obtains the group matched against the specified request path variable. :returns: found group. :raises HTTPForbidden: if the requesting user does not have sufficient permission to execute this request. :raises HTTPNotFound: if the specified group name does not correspond to any existing group. """ group_name = get_value_matchdict_checked(request, group_name_key) group = ax.evaluate_call(lambda: GroupService.by_group_name(group_name, db_session=request.db), fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, msg_on_fail=s.Group_MatchDictCheck_ForbiddenResponseSchema.description) ax.verify_param(group, not_none=True, http_error=HTTPNotFound, param_content={"value": group_name}, param_name="group_name", msg_on_fail=s.Group_MatchDictCheck_NotFoundResponseSchema.description) return group
def __acl__(self): # type: () -> AccessControlListType """ Administrators have all permissions, user/group-specific permissions added if user is logged in. """ user = self.request.user # allow if role MAGPIE_ADMIN_PERMISSION is somehow directly set instead of inferred via members of admin-group acl = [(Allow, get_constant("MAGPIE_ADMIN_PERMISSION"), ALL_PERMISSIONS)] admins = GroupService.by_group_name(get_constant("MAGPIE_ADMIN_GROUP"), db_session=self.request.db) if admins: # need to add explicit admin-group ALL_PERMISSIONS otherwise views with other permissions than the # default MAGPIE_ADMIN_PERMISSION will be refused access (e.g.: views with MAGPIE_LOGGED_PERMISSION) acl += [(Allow, "group:{}".format(admins.id), ALL_PERMISSIONS)] if user: # user-specific permissions (including group memberships) permissions = UserService.permissions(user, self.request.db) user_acl = permission_to_pyramid_acls(permissions) # allow views that require minimally to be logged in (regardless of who is the user) auth_acl = [(Allow, user.id, Authenticated)] acl += user_acl + auth_acl return acl
def test_by_group_name(self, db_session): added_group = add_group(db_session) queried_group = GroupService.by_group_name("group", db_session=db_session) assert added_group == queried_group
def create_group(group_name, description, discoverable, db_session): # type: (Str, Str, bool, Session) -> HTTPException """ Creates a group if it is permitted and not conflicting. :returns: valid HTTP response on successful operations. :raises HTTPException: error HTTP response of corresponding situation. """ description = str(description) if description else None discoverable = asbool(discoverable) group_content_error = { "group_name": str(group_name), "description": description, "discoverable": discoverable } ax.verify_param( group_name, matches=True, param_compare=ax.PARAM_REGEX, param_name="group_name", http_error=HTTPBadRequest, content=group_content_error, msg_on_fail=s.Groups_POST_BadRequestResponseSchema.description) if description: ax.verify_param( description, matches=True, param_compare=ax.PARAM_REGEX, param_name="description", http_error=HTTPBadRequest, content=group_content_error, msg_on_fail=s.Groups_POST_BadRequestResponseSchema.description) group = GroupService.by_group_name(group_name, db_session=db_session) ax.verify_param( group, is_none=True, param_name="group_name", with_param=False, # don't return group as value http_error=HTTPConflict, content=group_content_error, msg_on_fail=s.Groups_POST_ConflictResponseSchema.description) new_group = ax.evaluate_call( lambda: models.Group(group_name=group_name, description=description, discoverable=discoverable), # noqa fallback=lambda: db_session.rollback(), http_error=HTTPForbidden, content=group_content_error, msg_on_fail=s.Groups_POST_ForbiddenCreateResponseSchema.description) ax.evaluate_call( lambda: db_session.add(new_group), fallback=lambda: db_session.rollback(), http_error=HTTPForbidden, content=group_content_error, msg_on_fail=s.Groups_POST_ForbiddenAddResponseSchema.description) # re-fetch the created group to update fields with auto-generated group ID new_group = ax.evaluate_call( lambda: GroupService.by_group_name(group_name, db_session=db_session), http_error=HTTPForbidden, msg_on_fail=s.Groups_POST_ForbiddenAddResponseSchema.description) return ax.valid_http( http_success=HTTPCreated, detail=s.Groups_POST_CreatedResponseSchema.description, content={"group": format_group(new_group, basic_info=True)})
def effective_permissions(self, user, resource, permissions=None, allow_match=True): # type: (models.User, ServiceOrResourceType, Optional[Collection[Permission]], bool) -> List[PermissionSet] """ Obtains the effective permissions the user has over the specified resource. Recursively rewinds the resource tree from the specified resource up to the top-most parent service the resource resides under (or directly if the resource is the service) and retrieve permissions along the way that should be applied to children when using scoped-resource inheritance. Rewinding of the tree can terminate earlier when permissions can be immediately resolved such as when more restrictive conditions enforce denied access. Both user and group permission inheritance is resolved simultaneously to tree hierarchy with corresponding allow and deny conditions. User :term:`Direct Permissions` have priority over all its groups :term:`Inherited Permissions`, and denied permissions have priority over allowed access ones. All applicable permissions on the resource (as defined by :meth:`allowed_permissions`) will have their resolution (Allow/Deny) provided as output, unless a specific subset of permissions is requested using :paramref:`permissions`. Other permissions are ignored in this case to only resolve requested ones. For example, this parameter can be used to request only ACL resolution from specific permissions applicable for a given request, as obtained by :meth:`permission_requested`. Permissions scoped as `match` can be ignored using :paramref:`allow_match`, such as when the targeted resource does not exist. .. seealso:: - :meth:`ServiceInterface.resource_requested` """ if not permissions: permissions = self.allowed_permissions(resource) requested_perms = set(permissions) # type: Set[Permission] effective_perms = dict() # type: Dict[Permission, PermissionSet] # immediately return all permissions if user is an admin db_session = self.request.db admin_group = get_constant("MAGPIE_ADMIN_GROUP", self.request) admin_group = GroupService.by_group_name(admin_group, db_session=db_session) if admin_group in user.groups: # noqa return [ PermissionSet(perm, access=Access.ALLOW, scope=Scope.MATCH, typ=PermissionType.EFFECTIVE, reason=PERMISSION_REASON_ADMIN) for perm in permissions ] # level at which last permission was found, -1 if not found # employed to resolve with *closest* scope and for applicable 'reason' combination on same level effective_level = dict() # type: Dict[Permission, Optional[int]] current_level = 1 # one-based to avoid ``if level:`` check failing with zero full_break = False # current and parent resource(s) recursive-scope while resource is not None and not full_break: # bottom-up until service is reached # include both permissions set in database as well as defined directly on resource cur_res_perms = ResourceService.perms_for_user( resource, user, db_session=db_session) cur_res_perms.extend(permission_to_pyramid_acls(resource.__acl__)) for perm_name in requested_perms: if full_break: break for perm_tup in cur_res_perms: perm_set = PermissionSet(perm_tup) # if user is owner (directly or via groups), all permissions are set, # but continue processing this resource until end in case user explicit deny reverts it if perm_tup.perm_name == ALL_PERMISSIONS: # FIXME: # This block needs to be validated if support of ownership rules are added. # Conditions must be revised according to wanted behaviour... # General idea for now is that explict user/group deny should be prioritized over resource # ownership permissions since these can be attributed to *any user* while explicit deny are # definitely set by an admin-level user. for perm in requested_perms: if perm_set.access == Access.DENY: all_perm = PermissionSet( perm, perm_set.access, perm.scope, PermissionType.OWNED) effective_perms[perm] = all_perm else: all_perm = PermissionSet( perm, perm_set.access, perm.scope, PermissionType.OWNED) effective_perms.setdefault(perm, all_perm) full_break = True break # skip if the current permission must not be processed (at all or for the moment until next 'name') if perm_set.name not in requested_perms or perm_set.name != perm_name: continue # only first resource can use match (if even enabled with found one), parents are recursive-only if not allow_match and perm_set.scope == Scope.MATCH: continue # pick the first permission if none was found up to this point prev_perm = effective_perms.get(perm_name) scope_level = effective_level.get(perm_name) if not prev_perm: effective_perms[perm_name] = perm_set effective_level[perm_name] = current_level continue # user direct permissions have priority over inherited ones from groups # if inherited permission was found during previous iteration, override it with direct permission if perm_set.type == PermissionType.DIRECT: # - reset resolution scope of previous permission attributed to group as it takes precedence # - since there can't be more than one user permission-name per resource on a given level, # scope resolution is done after applying this *closest* permission, ignore higher level ones if prev_perm.type == PermissionType.INHERITED or not scope_level: effective_perms[perm_name] = perm_set effective_level[perm_name] = current_level continue # final decision for this user, skip any group permissions # resolve prioritized permission according to ALLOW/DENY, scope and group priority # (see 'PermissionSet.resolve' method for extensive details) # skip if last permission is not on group to avoid redundant USER > GROUP check processed before if prev_perm.type == PermissionType.INHERITED: # - If new permission to process is done against the previous permission from *same* tree-level, # there is a possibility to combine equal priority groups. In such case, reason is 'MULTIPLE'. # - If not of equal priority, the appropriate permission is selected and reason is overridden # accordingly by the new higher priority permission. # - If no permission was defined at all (first occurrence), also set it using current permission if scope_level in [None, current_level]: resolved_perm = PermissionSet.resolve( perm_set, prev_perm, context=PermissionType.EFFECTIVE) effective_perms[perm_name] = resolved_perm effective_level[perm_name] = current_level # - If new permission is at *different* tree-level, it applies only if the group has higher # priority than the previous one, to respect the *closest* scope to the target resource. # Same priorities are ignored as they were already resolved by *closest* scope above. # - Reset scope level with new permission such that another permission of same group priority as # that could be processed in next iteration can be compared against it, to resolve 'access' # priority between them. elif perm_set.group_priority > prev_perm.group_priority: effective_perms[perm_name] = perm_set effective_level[perm_name] = current_level # don't bother moving to parent if everything is resolved already # can only assume nothing left to resolve if all permissions are direct on user (highest priority) # if any found permission is group inherited, higher level user permission could still override it if (len(effective_perms) == len(requested_perms) and all(perm.type == PermissionType.DIRECT for perm in effective_perms.values())): break # otherwise, move to parent if any available, since we are not done rewinding the resource tree allow_match = False # reset match not applicable anymore for following parent resources current_level += 1 if resource.parent_id: resource = ResourceService.by_resource_id( resource.parent_id, db_session=db_session) else: resource = None # set deny for all still unresolved permissions from requested ones resolved_perms = set(effective_perms) missing_perms = set(permissions) - resolved_perms final_perms = set(effective_perms.values()) # type: Set[PermissionSet] for perm_name in missing_perms: perm = PermissionSet(perm_name, access=Access.DENY, scope=Scope.MATCH, typ=PermissionType.EFFECTIVE, reason=PERMISSION_REASON_DEFAULT) final_perms.add(perm) # enforce type and scope (use MATCH to make it explicit that it applies specifically for this resource) for perm in final_perms: perm.type = PermissionType.EFFECTIVE perm.scope = Scope.MATCH return list(final_perms)
def edit_group_view(request): """ Update a group by name. """ group = ar.get_group_matchdict_checked(request, group_name_key="group_name") special_groups = [ get_constant("MAGPIE_ANONYMOUS_GROUP", settings_container=request), get_constant("MAGPIE_ADMIN_GROUP", settings_container=request), ] ax.verify_param( group.group_name, not_in=True, param_compare=special_groups, param_name="group_name", http_error=HTTPForbidden, msg_on_fail=s.Group_PATCH_ReservedKeyword_ForbiddenResponseSchema. description) new_group_name = ar.get_multiformat_body(request, "group_name") new_description = ar.get_multiformat_body(request, "description") new_discoverability = ar.get_multiformat_body(request, "discoverable") if new_discoverability is not None: new_discoverability = asbool(new_discoverability) update_name = group.group_name != new_group_name and new_group_name is not None update_desc = group.description != new_description and new_description is not None update_disc = group.discoverable != new_discoverability and new_discoverability is not None ax.verify_param( any([update_name, update_desc, update_disc]), is_true=True, with_param=False, # params are not useful in response for this case http_error=HTTPBadRequest, content={"group_name": group.group_name}, msg_on_fail=s.Group_PATCH_None_BadRequestResponseSchema.description) if update_name: ax.verify_param(new_group_name, not_none=True, not_empty=True, http_error=HTTPBadRequest, msg_on_fail=s. Group_PATCH_Name_BadRequestResponseSchema.description) group_name_size_range = range( 1, 1 + get_constant("MAGPIE_GROUP_NAME_MAX_LENGTH", settings_container=request)) ax.verify_param(len(new_group_name), is_in=True, param_compare=group_name_size_range, http_error=HTTPBadRequest, msg_on_fail=s. Group_PATCH_Size_BadRequestResponseSchema.description) ax.verify_param( GroupService.by_group_name(new_group_name, db_session=request.db), is_none=True, http_error=HTTPConflict, with_param=False, # don't return group as value msg_on_fail=s.Group_PATCH_ConflictResponseSchema.description) group.group_name = new_group_name if update_desc: group.description = new_description if update_disc: group.discoverable = new_discoverability return ax.valid_http(http_success=HTTPOk, detail=s.Group_PATCH_OkResponseSchema.description)
def unique_groupname_validator(form, field): group = GroupService.by_group_name(field.data) mod_group = getattr(form, "_modified_group", None) if group and (not mod_group or mod_group.id != group.id): raise wtforms.ValidationError( "This group name already exists in system")