GUACAMOLE-220: Implement base API changes within database auth allowing for permission inheritance.

This commit is contained in:
Michael Jumper
2018-04-03 21:32:38 -07:00
parent 72bac09f43
commit 0a69630cbb
16 changed files with 198 additions and 99 deletions

View File

@@ -23,7 +23,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
@@ -58,26 +57,23 @@ public class ActiveConnectionPermissionService
private Provider<ActiveConnectionPermissionSet> activeConnectionPermissionSetProvider;
@Override
public ObjectPermission retrievePermission(ModeledAuthenticatedUser user,
public boolean hasPermission(ModeledAuthenticatedUser user,
ModeledUser targetUser, ObjectPermission.Type type,
String identifier) throws GuacamoleException {
String identifier, boolean inherit) throws GuacamoleException {
// Retrieve permissions
Set<ObjectPermission> permissions = retrievePermissions(user, targetUser);
Set<ObjectPermission> permissions = retrievePermissions(user, targetUser, inherit);
// If retrieved permissions contains the requested permission, return it
// Permission is granted if retrieved permissions contains the
// requested permission
ObjectPermission permission = new ObjectPermission(type, identifier);
if (permissions.contains(permission))
return permission;
// Otherwise, no such permission
return null;
return permissions.contains(permission);
}
@Override
public Set<ObjectPermission> retrievePermissions(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Retrieve permissions only if allowed
if (canReadPermissions(user, targetUser)) {
@@ -113,9 +109,9 @@ public class ActiveConnectionPermissionService
@Override
public Collection<String> retrieveAccessibleIdentifiers(ModeledAuthenticatedUser user,
ModeledUser targetUser, Collection<ObjectPermission.Type> permissionTypes,
Collection<String> identifiers) throws GuacamoleException {
Collection<String> identifiers, boolean inherit) throws GuacamoleException {
Set<ObjectPermission> permissions = retrievePermissions(user, targetUser);
Set<ObjectPermission> permissions = retrievePermissions(user, targetUser, inherit);
Collection<String> accessibleObjects = new ArrayList<String>(permissions.size());
// For each identifier/permission combination
@@ -138,11 +134,11 @@ public class ActiveConnectionPermissionService
@Override
public ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Create permission set for requested user
ActiveConnectionPermissionSet permissionSet = activeConnectionPermissionSetProvider.get();
permissionSet.init(user, targetUser);
permissionSet.init(user, targetUser, inherit);
return permissionSet;

View File

@@ -51,11 +51,11 @@ public class ConnectionGroupPermissionService extends ModeledObjectPermissionSer
@Override
public ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Create permission set for requested user
ObjectPermissionSet permissionSet = connectionGroupPermissionSetProvider.get();
permissionSet.init(user, targetUser);
permissionSet.init(user, targetUser, inherit);
return permissionSet;

View File

@@ -51,11 +51,11 @@ public class ConnectionPermissionService extends ModeledObjectPermissionService
@Override
public ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Create permission set for requested user
ObjectPermissionSet permissionSet = connectionPermissionSetProvider.get();
permissionSet.init(user, targetUser);
permissionSet.init(user, targetUser, inherit);
return permissionSet;

View File

@@ -105,7 +105,7 @@ public abstract class ModeledObjectPermissionService
affectedIdentifiers.add(permission.getObjectIdentifier());
// Determine subset of affected identifiers that we have admin access to
ObjectPermissionSet affectedPermissionSet = getPermissionSet(user, user.getUser());
ObjectPermissionSet affectedPermissionSet = getPermissionSet(user, user.getUser(), true);
Collection<String> allowedSubset = affectedPermissionSet.getAccessibleObjects(
Collections.singleton(ObjectPermission.Type.ADMINISTER),
affectedIdentifiers
@@ -154,21 +154,13 @@ public abstract class ModeledObjectPermissionService
}
@Override
public ObjectPermission retrievePermission(ModeledAuthenticatedUser user,
public boolean hasPermission(ModeledAuthenticatedUser user,
ModeledUser targetUser, ObjectPermission.Type type,
String identifier) throws GuacamoleException {
String identifier, boolean inherit) throws GuacamoleException {
// Retrieve permissions only if allowed
if (canReadPermissions(user, targetUser)) {
// Read permission from database, return null if not found
ObjectPermissionModel model = getPermissionMapper().selectOne(targetUser.getModel(), type, identifier);
if (model == null)
return null;
return getPermissionInstance(model);
}
if (canReadPermissions(user, targetUser))
return getPermissionMapper().selectOne(targetUser.getModel(), type, identifier, inherit) != null;
// User cannot read this user's permissions
throw new GuacamoleSecurityException("Permission denied.");
@@ -178,7 +170,8 @@ public abstract class ModeledObjectPermissionService
@Override
public Collection<String> retrieveAccessibleIdentifiers(ModeledAuthenticatedUser user,
ModeledUser targetUser, Collection<ObjectPermission.Type> permissions,
Collection<String> identifiers) throws GuacamoleException {
Collection<String> identifiers, boolean inherit)
throws GuacamoleException {
// Nothing is always accessible
if (identifiers.isEmpty())
@@ -192,7 +185,7 @@ public abstract class ModeledObjectPermissionService
return identifiers;
// Otherwise, return explicitly-retrievable identifiers
return getPermissionMapper().selectAccessibleIdentifiers(targetUser.getModel(), permissions, identifiers);
return getPermissionMapper().selectAccessibleIdentifiers(targetUser.getModel(), permissions, identifiers, inherit);
}

View File

@@ -92,7 +92,7 @@ public abstract class ModeledPermissionService<PermissionSetType extends Permiss
permissions.add(getPermissionInstance(model));
return permissions;
}
/**
@@ -111,7 +111,7 @@ public abstract class ModeledPermissionService<PermissionSetType extends Permiss
*/
protected abstract ModelType getModelInstance(ModeledUser targetUser,
PermissionType permission);
/**
* Returns a collection of model objects which are based on the given
* permissions and target user.
@@ -129,7 +129,7 @@ public abstract class ModeledPermissionService<PermissionSetType extends Permiss
protected Collection<ModelType> getModelInstances(ModeledUser targetUser,
Collection<PermissionType> permissions) {
// Create new collection of models by manually converting each permission
// Create new collection of models by manually converting each permission
Collection<ModelType> models = new ArrayList<ModelType>(permissions.size());
for (PermissionType permission : permissions)
models.add(getModelInstance(targetUser, permission));
@@ -140,15 +140,15 @@ public abstract class ModeledPermissionService<PermissionSetType extends Permiss
@Override
public Set<PermissionType> retrievePermissions(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Retrieve permissions only if allowed
if (canReadPermissions(user, targetUser))
return getPermissionInstances(getPermissionMapper().select(targetUser.getModel()));
return getPermissionInstances(getPermissionMapper().select(targetUser.getModel(), inherit));
// User cannot read this user's permissions
throw new GuacamoleSecurityException("Permission denied.");
}
}

View File

@@ -36,20 +36,26 @@ public interface ObjectPermissionMapper extends PermissionMapper<ObjectPermissio
*
* @param entity
* The entity to retrieve permissions for.
*
*
* @param type
* The type of permission to return.
*
*
* @param identifier
* The identifier of the object affected by the permission to return.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* The requested permission, or null if no such permission is granted
* to the given entity for the given object.
*/
ObjectPermissionModel selectOne(@Param("entity") EntityModel entity,
@Param("type") ObjectPermission.Type type,
@Param("identifier") String identifier);
@Param("identifier") String identifier,
@Param("inherit") boolean inherit);
/**
* Retrieves the subset of the given identifiers for which the given entity
@@ -67,12 +73,18 @@ public interface ObjectPermissionMapper extends PermissionMapper<ObjectPermissio
* The identifiers of the objects affected by the permissions being
* checked.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* A collection containing the subset of identifiers for which at least
* one of the specified permissions is granted.
*/
Collection<String> selectAccessibleIdentifiers(@Param("entity") EntityModel entity,
@Param("permissions") Collection<ObjectPermission.Type> permissions,
@Param("identifiers") Collection<String> identifiers);
@Param("identifiers") Collection<String> identifiers,
@Param("inherit") boolean inherit);
}

View File

@@ -35,31 +35,36 @@ public interface ObjectPermissionService
extends PermissionService<ObjectPermissionSet, ObjectPermission> {
/**
* Retrieves the permission of the given type associated with the given
* user and object, if it exists. If no such permission exists, null is
* Returns whether the permission of the given type and associated with the
* given object has been granted to the given user.
*
* @param user
* The user retrieving the permission.
*
* @param targetUser
* The user associated with the permission to be retrieved.
*
*
* @param type
* The type of permission to retrieve.
*
* @param identifier
* The identifier of the object affected by the permission to return.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* The permission of the given type associated with the given user and
* object, or null if no such permission exists.
* true if permission of the given type and associated with the given
* object has been granted to the given user, false otherwise.
*
* @throws GuacamoleException
* If an error occurs while retrieving the requested permission.
*/
ObjectPermission retrievePermission(ModeledAuthenticatedUser user,
boolean hasPermission(ModeledAuthenticatedUser user,
ModeledUser targetUser, ObjectPermission.Type type,
String identifier) throws GuacamoleException;
String identifier, boolean inherit) throws GuacamoleException;
/**
* Retrieves the subset of the given identifiers for which the given user
@@ -80,6 +85,11 @@ public interface ObjectPermissionService
* The identifiers of the objects affected by the permissions being
* checked.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* A collection containing the subset of identifiers for which at least
* one of the specified permissions is granted.
@@ -89,6 +99,7 @@ public interface ObjectPermissionService
*/
Collection<String> retrieveAccessibleIdentifiers(ModeledAuthenticatedUser user,
ModeledUser targetUser, Collection<ObjectPermission.Type> permissions,
Collection<String> identifiers) throws GuacamoleException;
Collection<String> identifiers, boolean inherit)
throws GuacamoleException;
}

View File

@@ -42,6 +42,12 @@ public abstract class ObjectPermissionSet extends RestrictedObject
*/
private ModeledUser user;
/**
* Whether permissions inherited through user groups should be taken into
* account. If false, only permissions granted directly will be included.
*/
boolean inherit;
/**
* Creates a new ObjectPermissionSet. The resulting permission set
* must still be initialized by a call to init(), or the information
@@ -60,10 +66,17 @@ public abstract class ObjectPermissionSet extends RestrictedObject
*
* @param user
* The user to whom the permissions in this set are granted.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*/
public void init(ModeledAuthenticatedUser currentUser, ModeledUser user) {
public void init(ModeledAuthenticatedUser currentUser, ModeledUser user,
boolean inherit) {
super.init(currentUser);
this.user = user;
this.inherit = inherit;
}
/**
@@ -75,16 +88,16 @@ public abstract class ObjectPermissionSet extends RestrictedObject
* permissions contained within this permission set.
*/
protected abstract ObjectPermissionService getObjectPermissionService();
@Override
public Set<ObjectPermission> getPermissions() throws GuacamoleException {
return getObjectPermissionService().retrievePermissions(getCurrentUser(), user);
return getObjectPermissionService().retrievePermissions(getCurrentUser(), user, inherit);
}
@Override
public boolean hasPermission(ObjectPermission.Type permission,
String identifier) throws GuacamoleException {
return getObjectPermissionService().retrievePermission(getCurrentUser(), user, permission, identifier) != null;
return getObjectPermissionService().hasPermission(getCurrentUser(), user, permission, identifier, inherit);
}
@Override
@@ -102,7 +115,7 @@ public abstract class ObjectPermissionSet extends RestrictedObject
@Override
public Collection<String> getAccessibleObjects(Collection<ObjectPermission.Type> permissions,
Collection<String> identifiers) throws GuacamoleException {
return getObjectPermissionService().retrieveAccessibleIdentifiers(getCurrentUser(), user, permissions, identifiers);
return getObjectPermissionService().retrieveAccessibleIdentifiers(getCurrentUser(), user, permissions, identifiers, inherit);
}
@Override

View File

@@ -38,10 +38,16 @@ public interface PermissionMapper<PermissionType> {
* @param entity
* The entity to retrieve permissions for.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* All permissions associated with the given entity.
*/
Collection<PermissionType> select(@Param("entity") EntityModel entity);
Collection<PermissionType> select(@Param("entity") EntityModel entity,
@Param("inherit") boolean inherit);
/**
* Inserts the given permissions into the database. If any permissions

View File

@@ -19,16 +19,11 @@
package org.apache.guacamole.auth.jdbc.permission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
import org.apache.guacamole.auth.jdbc.user.ModeledUser;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.Permission;
import org.apache.guacamole.net.auth.permission.PermissionSet;
@@ -59,6 +54,11 @@ public interface PermissionService<PermissionSetType extends PermissionSet<Permi
* The user to whom the permissions in the returned permission set are
* granted.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* A permission set that contains all permissions associated with the
* given user, and can be used to manipulate that user's permissions.
@@ -69,7 +69,7 @@ public interface PermissionService<PermissionSetType extends PermissionSet<Permi
* user is denied.
*/
PermissionSetType getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException;
ModeledUser targetUser, boolean inherit) throws GuacamoleException;
/**
* Retrieves all permissions associated with the given user.
@@ -80,6 +80,11 @@ public interface PermissionService<PermissionSetType extends PermissionSet<Permi
* @param targetUser
* The user associated with the permissions to be retrieved.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* The permissions associated with the given user.
*
@@ -87,7 +92,7 @@ public interface PermissionService<PermissionSetType extends PermissionSet<Permi
* If an error occurs while retrieving the requested permissions.
*/
Set<PermissionType> retrievePermissions(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException;
ModeledUser targetUser, boolean inherit) throws GuacamoleException;
/**
* Creates the given permissions within the database. If any permissions

View File

@@ -51,11 +51,11 @@ public class SharingProfilePermissionService extends ModeledObjectPermissionServ
@Override
public ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Create permission set for requested user
ObjectPermissionSet permissionSet = sharingProfilePermissionSetProvider.get();
permissionSet.init(user, targetUser);
permissionSet.init(user, targetUser, inherit);
return permissionSet;

View File

@@ -34,15 +34,21 @@ public interface SystemPermissionMapper extends PermissionMapper<SystemPermissio
*
* @param entity
* The entity to retrieve permissions for.
*
*
* @param type
* The type of permission to return.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* The requested permission, or null if no such permission is granted
* to the given entity.
*/
SystemPermissionModel selectOne(@Param("entity") EntityModel entity,
@Param("type") SystemPermission.Type type);
@Param("type") SystemPermission.Type type,
@Param("inherit") boolean inherit);
}

View File

@@ -75,11 +75,11 @@ public class SystemPermissionService
@Override
public SystemPermissionSet getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Create permission set for requested user
SystemPermissionSet permissionSet = systemPermissionSetProvider.get();
permissionSet.init(user, targetUser);
permissionSet.init(user, targetUser, inherit);
return permissionSet;
@@ -123,8 +123,9 @@ public class SystemPermissionService
}
/**
* Retrieves the permission of the given type associated with the given
* user, if it exists. If no such permission exists, null is returned.
* Retrieves whether the permission of the given type has been granted to
* the given user. Permission inheritance through group membership is taken
* into account.
*
* @param user
* The user retrieving the permission.
@@ -135,27 +136,25 @@ public class SystemPermissionService
* @param type
* The type of permission to retrieve.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*
* @return
* The permission of the given type associated with the given user, or
* null if no such permission exists.
* true if permission of the given type has been granted to the given
* user, false otherwise.
*
* @throws GuacamoleException
* If an error occurs while retrieving the requested permission.
*/
public SystemPermission retrievePermission(ModeledAuthenticatedUser user,
ModeledUser targetUser, SystemPermission.Type type) throws GuacamoleException {
public boolean hasPermission(ModeledAuthenticatedUser user,
ModeledUser targetUser, SystemPermission.Type type,
boolean inherit) throws GuacamoleException {
// Retrieve permissions only if allowed
if (canReadPermissions(user, targetUser)) {
// Read permission from database, return null if not found
SystemPermissionModel model = getPermissionMapper().selectOne(targetUser.getModel(), type);
if (model == null)
return null;
return getPermissionInstance(model);
}
if (canReadPermissions(user, targetUser))
return getPermissionMapper().selectOne(targetUser.getModel(), type, inherit) != null;
// User cannot read this user's permissions
throw new GuacamoleSecurityException("Permission denied.");

View File

@@ -42,6 +42,12 @@ public class SystemPermissionSet extends RestrictedObject
*/
private ModeledUser user;
/**
* Whether permissions inherited through user groups should be taken into
* account. If false, only permissions granted directly will be included.
*/
private boolean inherit;
/**
* Service for reading and manipulating system permissions.
*/
@@ -66,21 +72,28 @@ public class SystemPermissionSet extends RestrictedObject
*
* @param user
* The user to whom the permissions in this set are granted.
*
* @param inherit
* Whether permissions inherited through user groups should be taken
* into account. If false, only permissions granted directly will be
* included.
*/
public void init(ModeledAuthenticatedUser currentUser, ModeledUser user) {
public void init(ModeledAuthenticatedUser currentUser, ModeledUser user,
boolean inherit) {
super.init(currentUser);
this.user = user;
this.inherit = inherit;
}
@Override
public Set<SystemPermission> getPermissions() throws GuacamoleException {
return systemPermissionService.retrievePermissions(getCurrentUser(), user);
return systemPermissionService.retrievePermissions(getCurrentUser(), user, inherit);
}
@Override
public boolean hasPermission(SystemPermission.Type permission)
throws GuacamoleException {
return systemPermissionService.retrievePermission(getCurrentUser(), user, permission) != null;
return systemPermissionService.hasPermission(getCurrentUser(), user, permission, inherit);
}
@Override

View File

@@ -51,11 +51,11 @@ public class UserPermissionService extends ModeledObjectPermissionService {
@Override
public ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user,
ModeledUser targetUser) throws GuacamoleException {
ModeledUser targetUser, boolean inherit) throws GuacamoleException {
// Create permission set for requested user
ObjectPermissionSet permissionSet = userPermissionSetProvider.get();
permissionSet.init(user, targetUser);
permissionSet.init(user, targetUser, inherit);
return permissionSet;

View File

@@ -350,37 +350,37 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
@Override
public SystemPermissionSet getSystemPermissions()
throws GuacamoleException {
return systemPermissionService.getPermissionSet(getCurrentUser(), this);
return systemPermissionService.getPermissionSet(getCurrentUser(), this, false);
}
@Override
public ObjectPermissionSet getConnectionPermissions()
throws GuacamoleException {
return connectionPermissionService.getPermissionSet(getCurrentUser(), this);
return connectionPermissionService.getPermissionSet(getCurrentUser(), this, false);
}
@Override
public ObjectPermissionSet getConnectionGroupPermissions()
throws GuacamoleException {
return connectionGroupPermissionService.getPermissionSet(getCurrentUser(), this);
return connectionGroupPermissionService.getPermissionSet(getCurrentUser(), this, false);
}
@Override
public ObjectPermissionSet getSharingProfilePermissions()
throws GuacamoleException {
return sharingProfilePermissionService.getPermissionSet(getCurrentUser(), this);
return sharingProfilePermissionService.getPermissionSet(getCurrentUser(), this, false);
}
@Override
public ObjectPermissionSet getActiveConnectionPermissions()
throws GuacamoleException {
return activeConnectionPermissionService.getPermissionSet(getCurrentUser(), this);
return activeConnectionPermissionService.getPermissionSet(getCurrentUser(), this, false);
}
@Override
public ObjectPermissionSet getUserPermissions()
throws GuacamoleException {
return userPermissionService.getPermissionSet(getCurrentUser(), this);
return userPermissionService.getPermissionSet(getCurrentUser(), this, false);
}
@Override
@@ -855,7 +855,52 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
@Override
public Permissions getEffectivePermissions() throws GuacamoleException {
return this;
return new Permissions() {
@Override
public ObjectPermissionSet getActiveConnectionPermissions()
throws GuacamoleException {
return activeConnectionPermissionService.getPermissionSet(getCurrentUser(), ModeledUser.this, true);
}
@Override
public ObjectPermissionSet getConnectionGroupPermissions()
throws GuacamoleException {
return connectionGroupPermissionService.getPermissionSet(getCurrentUser(), ModeledUser.this, true);
}
@Override
public ObjectPermissionSet getConnectionPermissions()
throws GuacamoleException {
return connectionPermissionService.getPermissionSet(getCurrentUser(), ModeledUser.this, true);
}
@Override
public ObjectPermissionSet getSharingProfilePermissions()
throws GuacamoleException {
return sharingProfilePermissionService.getPermissionSet(getCurrentUser(), ModeledUser.this, true);
}
@Override
public SystemPermissionSet getSystemPermissions()
throws GuacamoleException {
return systemPermissionService.getPermissionSet(getCurrentUser(), ModeledUser.this, true);
}
@Override
public ObjectPermissionSet getUserPermissions()
throws GuacamoleException {
return userPermissionService.getPermissionSet(getCurrentUser(), ModeledUser.this, true);
}
@Override
public ObjectPermissionSet getUserGroupPermissions()
throws GuacamoleException {
// FIXME: STUB
return new SimpleObjectPermissionSet();
}
};
}
}