From 2bbe6f59376a96022f873d11066874d5cb802552 Mon Sep 17 00:00:00 2001 From: Prachi Damle Date: Thu, 3 Oct 2013 13:28:19 -0700 Subject: [PATCH 1/3] APIChecker helper methods implemented --- .../acl/api/RoleBasedAPIAccessChecker.java | 11 +--- .../apache/cloudstack/acl/AclServiceImpl.java | 53 +++++++++++++++++-- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/api/RoleBasedAPIAccessChecker.java b/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/api/RoleBasedAPIAccessChecker.java index 18fcdf9cd04..027ff580128 100644 --- a/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/api/RoleBasedAPIAccessChecker.java +++ b/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/api/RoleBasedAPIAccessChecker.java @@ -16,15 +16,10 @@ // under the License. package org.apache.cloudstack.acl.api; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; import javax.ejb.Local; import javax.inject.Inject; -import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.acl.AclRole; @@ -35,12 +30,10 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.User; -import com.cloud.utils.PropertiesUtil; import com.cloud.utils.component.AdapterBase; -import com.cloud.utils.component.PluggableService; -// This is the default API access checker that grab's the user's account -// based on the account type, access is granted +// This is the Role Based API access checker that grab's the account's roles +// based on the set of roles, access is granted if any of the role has access to the api @Local(value=APIChecker.class) public class RoleBasedAPIAccessChecker extends AdapterBase implements APIChecker { diff --git a/server/src/org/apache/cloudstack/acl/AclServiceImpl.java b/server/src/org/apache/cloudstack/acl/AclServiceImpl.java index c8fc54cf59a..69f9d3d5568 100644 --- a/server/src/org/apache/cloudstack/acl/AclServiceImpl.java +++ b/server/src/org/apache/cloudstack/acl/AclServiceImpl.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.acl; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -49,6 +50,11 @@ import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.DB; import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.JoinBuilder.JoinType; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.Transaction; @Local(value = {AclService.class}) @@ -507,14 +513,53 @@ public class AclServiceImpl extends ManagerBase implements AclService, Manager { @Override public List getAclRoles(long accountId) { - // TODO Auto-generated method stub - return null; + + SearchBuilder groupSB = _aclGroupAccountMapDao.createSearchBuilder(); + groupSB.and("account", groupSB.entity().getAccountId(), Op.EQ); + + GenericSearchBuilder roleSB = _aclGroupRoleMapDao.createSearchBuilder(Long.class); + roleSB.selectField(roleSB.entity().getAclRoleId()); + roleSB.join("accountgroupjoin", groupSB, groupSB.entity().getAclGroupId(), roleSB.entity().getAclGroupId(), + JoinType.INNER); + roleSB.done(); + SearchCriteria roleSc = roleSB.create(); + roleSc.setJoinParameters("accountgroupjoin", "account", accountId); + + List roleIds = _aclGroupRoleMapDao.customSearch(roleSc, null); + + SearchBuilder sb = _aclRoleDao.createSearchBuilder(); + sb.and("ids", sb.entity().getId(), Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("ids", roleIds.toArray(new Object[roleIds.size()])); + List roles = _aclRoleDao.customSearch(sc, null); + + return new ArrayList(roles); } @Override public boolean isAPIAccessibleForRoles(String apiName, List roles) { - // TODO Auto-generated method stub - return false; + + boolean accessible = false; + + List roleIds = new ArrayList(); + for (AclRole role : roles) { + roleIds.add(role.getId()); + } + + SearchBuilder sb = _apiPermissionDao.createSearchBuilder(); + sb.and("apiName", sb.entity().getApiName(), Op.EQ); + sb.and("roleId", sb.entity().getAclRoleId(), Op.IN); + + SearchCriteria sc = sb.create(); + sc.setParameters("roleId", roleIds.toArray(new Object[roleIds.size()])); + + List permissions = _apiPermissionDao.customSearch(sc, null); + + if (permissions != null && !permissions.isEmpty()) { + accessible = true; + } + + return accessible; } } From 385dfc230fe65d8b85414ac9903fbdad46c066c9 Mon Sep 17 00:00:00 2001 From: Prachi Damle Date: Mon, 7 Oct 2013 12:32:34 -0700 Subject: [PATCH 2/3] Schema and VO/Dao for Role Permission --- .../cloudstack/acl/AclRolePermission.java | 31 ++++++ .../cloudstack/acl/AclRolePermissionVO.java | 99 +++++++++++++++++++ .../acl/dao/AclRolePermissionDao.java | 28 ++++++ .../acl/dao/AclRolePermissionDaoImpl.java | 62 ++++++++++++ .../entity/RoleBasedEntityAccessChecker.java | 66 +++++++++++++ setup/db/db/schema-420to430.sql | 9 ++ 6 files changed, 295 insertions(+) create mode 100644 api/src/org/apache/cloudstack/acl/AclRolePermission.java create mode 100644 engine/schema/src/org/apache/cloudstack/acl/AclRolePermissionVO.java create mode 100644 engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDao.java create mode 100644 engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDaoImpl.java create mode 100644 plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java diff --git a/api/src/org/apache/cloudstack/acl/AclRolePermission.java b/api/src/org/apache/cloudstack/acl/AclRolePermission.java new file mode 100644 index 00000000000..4ec16bc38a7 --- /dev/null +++ b/api/src/org/apache/cloudstack/acl/AclRolePermission.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl; + +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.InternalIdentity; + +public interface AclRolePermission extends InternalIdentity { + + Long getAclRoleId(); + + String getEntityType(); + + AccessType getAccessType(); + + boolean isAllowed(); +} diff --git a/engine/schema/src/org/apache/cloudstack/acl/AclRolePermissionVO.java b/engine/schema/src/org/apache/cloudstack/acl/AclRolePermissionVO.java new file mode 100644 index 00000000000..d866d915843 --- /dev/null +++ b/engine/schema/src/org/apache/cloudstack/acl/AclRolePermissionVO.java @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.acl.SecurityChecker.AccessType; + +@Entity +@Table(name = ("acl_role_permission")) +public class AclRolePermissionVO implements AclRolePermission { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "role_id") + private long aclRoleId; + + @Column(name = "entity_type") + private String entityType; + + @Column(name = "access_type") + @Enumerated(value = EnumType.STRING) + AccessType accessType; + + @Column(name = "permission") + private boolean permission; + + public AclRolePermissionVO() { + + } + + public AclRolePermissionVO(long roleId, String entityType, AccessType atype) { + aclRoleId = roleId; + this.entityType = entityType; + accessType = atype; + } + + @Override + public long getId() { + return id; + } + + @Override + public Long getAclRoleId() { + return aclRoleId; + } + + @Override + public String getEntityType() { + return entityType; + } + + @Override + public AccessType getAccessType() { + return accessType; + } + + + public void setAclRoleId(long aclRoleId) { + this.aclRoleId = aclRoleId; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + public void setAccessType(AccessType accessType) { + this.accessType = accessType; + } + + @Override + public boolean isAllowed() { + return permission; + } +} diff --git a/engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDao.java b/engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDao.java new file mode 100644 index 00000000000..74d491dc600 --- /dev/null +++ b/engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDao.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.dao; + +import org.apache.cloudstack.acl.AclRolePermissionVO; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; + +import com.cloud.utils.db.GenericDao; + +public interface AclRolePermissionDao extends GenericDao { + + AclRolePermissionVO findByRoleAndEntity(long roleId, String entityType, AccessType accessType); + +} diff --git a/engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDaoImpl.java b/engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDaoImpl.java new file mode 100644 index 00000000000..c7141f8057f --- /dev/null +++ b/engine/schema/src/org/apache/cloudstack/acl/dao/AclRolePermissionDaoImpl.java @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.dao; + +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.springframework.stereotype.Component; + +import org.apache.cloudstack.acl.AclRolePermissionVO; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class AclRolePermissionDaoImpl extends GenericDaoBase implements AclRolePermissionDao { + private SearchBuilder findByRoleEntity; + + public AclRolePermissionDaoImpl() + { + + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + findByRoleEntity = createSearchBuilder(); + findByRoleEntity.and("roleId", findByRoleEntity.entity().getAclRoleId(), SearchCriteria.Op.EQ); + findByRoleEntity.and("entityType", findByRoleEntity.entity().getEntityType(), SearchCriteria.Op.EQ); + findByRoleEntity.and("accessType", findByRoleEntity.entity().getAccessType(), SearchCriteria.Op.EQ); + findByRoleEntity.done(); + + return true; + } + + @Override + public AclRolePermissionVO findByRoleAndEntity(long roleId, String entityType, AccessType accessType) { + SearchCriteria sc = findByRoleEntity.create(); + sc.setParameters("roleId", roleId); + sc.setParameters("entityType", entityType); + sc.setParameters("accessType", accessType); + return findOneBy(sc); + } +} diff --git a/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java b/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java new file mode 100644 index 00000000000..0f83b28b44d --- /dev/null +++ b/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.acl.entity; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.AclRole; +import org.apache.cloudstack.acl.AclService; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; + +import com.cloud.acl.DomainChecker; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.vm.VirtualMachine; + +public class RoleBasedEntityAccessChecker extends DomainChecker implements SecurityChecker { + + @Inject + AccountService _accountService; + @Inject + AclService _aclService; + + @Override + public boolean checkAccess(Account caller, ControlledEntity entity, AccessType accessType) + throws PermissionDeniedException { + + // Is Caller RootAdmin? Yes, granted true + if (_accountService.isRootAdmin(caller.getId())) { + return true; + } + // Is Caller Owner of the entity? Yes, granted true + if (caller.getId() == entity.getAccountId()) { + return true; + } + // Get the Roles of the Caller + List roles = _aclService.getAclRoles(caller.getId()); + + // Do you have DomainAdmin Role? If yes can access the entity in the + // domaintree + + // check the entity grant table + + + + return false; + } +} diff --git a/setup/db/db/schema-420to430.sql b/setup/db/db/schema-420to430.sql index c23c4980873..6d03a6d6958 100644 --- a/setup/db/db/schema-420to430.sql +++ b/setup/db/db/schema-420to430.sql @@ -369,6 +369,15 @@ CREATE TABLE `cloud`.`acl_entity_permission` ( CONSTRAINT `fk_acl_entity_permission__group_id` FOREIGN KEY(`group_id`) REFERENCES `acl_group` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `cloud`.`acl_role_permission` ( + `id` bigint unsigned NOT NULL UNIQUE auto_increment, + `role_id` bigint unsigned NOT NULL, + `entity_type` varchar(100) NOT NULL, + `access_type` varchar(40) NOT NULL, + `permission` int(1) unsigned NOT NULL COMMENT '1 allowed, 0 for denied', + PRIMARY KEY (`id`), + CONSTRAINT `fk_acl_role_permission___role_id` FOREIGN KEY(`role_id`) REFERENCES `acl_role` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP VIEW IF EXISTS `cloud`.`acl_role_view`; CREATE VIEW `cloud`.`acl_role_view` AS From 579806440b4c1ca0107b97cd65d30494cf804a99 Mon Sep 17 00:00:00 2001 From: Prachi Damle Date: Mon, 7 Oct 2013 14:30:15 -0700 Subject: [PATCH 3/3] Add permission flag to acl_entity_permission --- .../cloudstack/acl/AclEntityPermission.java | 2 ++ .../cloudstack/acl/AclEntityPermissionVO.java | 17 ++++++++++++++--- .../entity/RoleBasedEntityAccessChecker.java | 12 ++++++++++++ .../apache/cloudstack/acl/AclServiceImpl.java | 2 +- setup/db/db/schema-420to430.sql | 3 ++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/api/src/org/apache/cloudstack/acl/AclEntityPermission.java b/api/src/org/apache/cloudstack/acl/AclEntityPermission.java index 2716406de3f..bfe7ac96c46 100644 --- a/api/src/org/apache/cloudstack/acl/AclEntityPermission.java +++ b/api/src/org/apache/cloudstack/acl/AclEntityPermission.java @@ -12,4 +12,6 @@ public interface AclEntityPermission extends InternalIdentity { Long getEntityId(); AccessType getAccessType(); + + boolean isAllowed(); } diff --git a/engine/schema/src/org/apache/cloudstack/acl/AclEntityPermissionVO.java b/engine/schema/src/org/apache/cloudstack/acl/AclEntityPermissionVO.java index a3945163221..4e0f3c0a0cf 100644 --- a/engine/schema/src/org/apache/cloudstack/acl/AclEntityPermissionVO.java +++ b/engine/schema/src/org/apache/cloudstack/acl/AclEntityPermissionVO.java @@ -32,7 +32,7 @@ public class AclEntityPermissionVO implements AclEntityPermission { @Column(name = "entity_id") private long entityId; - + @Column(name = "entity_uuid") private String entityUuid; @@ -40,6 +40,9 @@ public class AclEntityPermissionVO implements AclEntityPermission { @Enumerated(value = EnumType.STRING) AccessType accessType; + @Column(name = "permission") + private boolean permission; + @Column(name = GenericDao.REMOVED_COLUMN) private Date removed; @@ -50,14 +53,16 @@ public class AclEntityPermissionVO implements AclEntityPermission { } - public AclEntityPermissionVO(long groupId, String entityType, long entityId, String entityUuid, AccessType atype) { + public AclEntityPermissionVO(long groupId, String entityType, long entityId, String entityUuid, AccessType atype, + boolean permission) { aclGroupId = groupId; this.entityType = entityType; this.entityId = entityId; this.entityUuid = entityUuid; accessType = atype; + this.permission = permission; } - + @Override public long getId() { return id; @@ -115,4 +120,10 @@ public class AclEntityPermissionVO implements AclEntityPermission { public Date getCreated() { return created; } + + @Override + public boolean isAllowed() { + return permission; + } + } diff --git a/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java b/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java index 0f83b28b44d..fa8bed103aa 100644 --- a/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java +++ b/plugins/acl/role-based-access-checkers/src/org/apache/cloudstack/acl/entity/RoleBasedEntityAccessChecker.java @@ -20,11 +20,14 @@ import java.util.List; import javax.inject.Inject; +import org.apache.cloudstack.acl.AclGroupAccountMapVO; import org.apache.cloudstack.acl.AclRole; import org.apache.cloudstack.acl.AclService; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.dao.AclGroupAccountMapDao; +import org.apache.cloudstack.acl.dao.AclGroupDao; import com.cloud.acl.DomainChecker; import com.cloud.exception.PermissionDeniedException; @@ -39,10 +42,19 @@ public class RoleBasedEntityAccessChecker extends DomainChecker implements Secur @Inject AclService _aclService; + @Inject + AclGroupAccountMapDao _aclGroupAccountMapDao; + @Override public boolean checkAccess(Account caller, ControlledEntity entity, AccessType accessType) throws PermissionDeniedException { + // check if explicit allow/deny is present for this entity in + // acl_entity_permission + + List acctGroups = _aclGroupAccountMapDao.listByAccountId(caller.getId()); + + // Is Caller RootAdmin? Yes, granted true if (_accountService.isRootAdmin(caller.getId())) { return true; diff --git a/server/src/org/apache/cloudstack/acl/AclServiceImpl.java b/server/src/org/apache/cloudstack/acl/AclServiceImpl.java index 69f9d3d5568..ecff79409fc 100644 --- a/server/src/org/apache/cloudstack/acl/AclServiceImpl.java +++ b/server/src/org/apache/cloudstack/acl/AclServiceImpl.java @@ -259,7 +259,7 @@ public class AclServiceImpl extends ManagerBase implements AclService, Manager { if (entity instanceof Identity) { entityUuid = ((Identity)entity).getUuid(); } - perm = new AclEntityPermissionVO(aclGroupId, entityType, entityId, entityUuid, accessType); + perm = new AclEntityPermissionVO(aclGroupId, entityType, entityId, entityUuid, accessType, true); _entityPermissionDao.persist(perm); } return group; diff --git a/setup/db/db/schema-420to430.sql b/setup/db/db/schema-420to430.sql index 6d03a6d6958..ecc2049900f 100644 --- a/setup/db/db/schema-420to430.sql +++ b/setup/db/db/schema-420to430.sql @@ -362,7 +362,8 @@ CREATE TABLE `cloud`.`acl_entity_permission` ( `entity_type` varchar(100) NOT NULL, `entity_id` bigint unsigned NOT NULL, `entity_uuid` varchar(40), - `access_type` varchar(40) NOT NULL, + `access_type` varchar(40) NOT NULL, + `permission` int(1) unsigned NOT NULL COMMENT '1 allowed, 0 for denied', `removed` datetime COMMENT 'date the permission was revoked', `created` datetime COMMENT 'date the permission was granted', PRIMARY KEY (`id`),