mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 13:26:51 +00:00
refactor/remove hasOwnerViewAccess replace with specific view permissions -- added canCreateCustomView_, canDeleteCustomView_ and canUpdateCustomView_ permissions
This commit is contained in:
parent
5d6e395e1e
commit
7330e5434a
@ -455,6 +455,8 @@ object ErrorMessages {
|
||||
val DeleteCustomViewError = "OBP-30256: Could not delete the custom view"
|
||||
val CannotFindCustomViewError = "OBP-30257: Could not find the custom view"
|
||||
val SystemViewCannotBePublicError = "OBP-30258: System view cannot be public"
|
||||
val CreateCustomViewError = "OBP-30259: Could not create the custom view"
|
||||
val UpdateCustomViewError = "OBP-30260: Could not update the custom view"
|
||||
|
||||
val TaxResidenceNotFound = "OBP-30300: Tax Residence not found by TAX_RESIDENCE_ID. "
|
||||
val CustomerAddressNotFound = "OBP-30310: Customer's Address not found by CUSTOMER_ADDRESS_ID. "
|
||||
|
||||
@ -529,11 +529,6 @@ object NewStyle extends MdcLoggable{
|
||||
} map { fullBoxOrException(_)
|
||||
} map { unboxFull(_) }
|
||||
|
||||
def removeView(account: BankAccount, user: User, viewId: ViewId, callContext: Option[CallContext]) = Future {
|
||||
account.removeView(user, viewId, callContext)
|
||||
} map { fullBoxOrException(_)
|
||||
} map { unboxFull(_) }
|
||||
|
||||
def grantAccessToView(account: BankAccount, u: User, viewIdBankIdAccountId : ViewIdBankIdAccountId, provider : String, providerId: String, callContext: Option[CallContext]) = Future {
|
||||
account.grantAccessToView(u, viewIdBankIdAccountId, provider, providerId, callContext: Option[CallContext])
|
||||
} map { fullBoxOrException(_)
|
||||
@ -3956,6 +3951,28 @@ object NewStyle extends MdcLoggable{
|
||||
.slice(offset.getOrElse("0").toInt, offset.getOrElse("0").toInt + limit.getOrElse("100").toInt)
|
||||
, callContext)
|
||||
}
|
||||
|
||||
def createCustomView(bankAccountId: BankIdAccountId, createViewJson: CreateViewJson, callContext: Option[CallContext]): OBPReturnType[View] =
|
||||
Future {
|
||||
Views.views.vend.createCustomView(bankAccountId, createViewJson)
|
||||
} map { i =>
|
||||
(unboxFullOrFail(i, callContext, s"$CreateCustomViewError", 404), callContext)
|
||||
}
|
||||
|
||||
def updateCustomView(bankAccountId : BankIdAccountId, viewId : ViewId, viewUpdateJson : UpdateViewJSON, callContext: Option[CallContext]): OBPReturnType[View] =
|
||||
Future {
|
||||
Views.views.vend.updateCustomView(bankAccountId, viewId, viewUpdateJson)
|
||||
} map { i =>
|
||||
(unboxFullOrFail(i, callContext, s"$UpdateCustomViewError", 404), callContext)
|
||||
}
|
||||
|
||||
def removeCustomView(viewId: ViewId, bankAccountId: BankIdAccountId, callContext: Option[CallContext]) =
|
||||
Future {
|
||||
Views.views.vend.removeCustomView(viewId, bankAccountId)
|
||||
} map { i =>
|
||||
(unboxFullOrFail(i, callContext, s"$DeleteCustomViewError", 404), callContext)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ object MigrationOfViewDefinitionPermissions {
|
||||
.canSeeTransactionRequests_(true)
|
||||
.canSeeAvailableViewsForBankAccount_(true)
|
||||
.canUpdateBankAccountLabel_(true)
|
||||
.canCreateCustomView_(true)
|
||||
.canDeleteCustomView_(true)
|
||||
.canUpdateCustomView_(true)
|
||||
.save
|
||||
).head
|
||||
|
||||
@ -37,6 +40,9 @@ object MigrationOfViewDefinitionPermissions {
|
||||
|${ViewDefinition.canSeeTransactionRequests_.dbColumnName}
|
||||
|${ViewDefinition.canSeeAvailableViewsForBankAccount_.dbColumnName}
|
||||
|${ViewDefinition.canUpdateBankAccountLabel_.dbColumnName}
|
||||
|${ViewDefinition.canCreateCustomView_.dbColumnName}
|
||||
|${ViewDefinition.canDeleteCustomView_.dbColumnName}
|
||||
|${ViewDefinition.canUpdateCustomView_.dbColumnName}
|
||||
|Duration: ${endDate - startDate} ms;
|
||||
""".stripMargin
|
||||
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
|
||||
|
||||
@ -622,7 +622,13 @@ trait APIMethods121 {
|
||||
createViewJsonV121.hide_metadata_if_alias_used,
|
||||
createViewJsonV121.allowed_actions
|
||||
)
|
||||
view <- account createCustomView (u, createViewJson, Some(cc))
|
||||
anyViewContainsCanCreateCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canCreateCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
_ <- booleanToBox(
|
||||
anyViewContainsCanCreateCustomViewPermission,
|
||||
s"${ErrorMessages.CreateCustomViewError} You need the `${ViewDefinition.canCreateCustomView_.dbColumnName}` permission on any your views"
|
||||
)
|
||||
view <- Views.views.vend.createCustomView(BankIdAccountId(bankId,accountId), createViewJson)?~ CreateCustomViewError
|
||||
} yield {
|
||||
val viewJSON = JSONFactory.createViewJSON(view)
|
||||
successJsonResponse(Extraction.decompose(viewJSON), 201)
|
||||
@ -677,7 +683,13 @@ trait APIMethods121 {
|
||||
hide_metadata_if_alias_used = updateJsonV121.hide_metadata_if_alias_used,
|
||||
allowed_actions = updateJsonV121.allowed_actions
|
||||
)
|
||||
updatedView <- account.updateView(u, viewId, updateViewJson, Some(cc))
|
||||
anyViewContainsCancanUpdateCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canUpdateCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
_ <- booleanToBox(
|
||||
anyViewContainsCancanUpdateCustomViewPermission,
|
||||
s"${ErrorMessages.CreateCustomViewError} You need the `${ViewDefinition.canUpdateCustomView_.dbColumnName}` permission on any your views"
|
||||
)
|
||||
updatedView <- Views.views.vend.updateCustomView(BankIdAccountId(bankId, accountId),viewId, updateViewJson) ?~ CreateCustomViewError
|
||||
} yield {
|
||||
val viewJSON = JSONFactory.createViewJSON(updatedView)
|
||||
successJsonResponse(Extraction.decompose(viewJSON), 200)
|
||||
@ -716,7 +728,17 @@ trait APIMethods121 {
|
||||
// custom views start with `_` eg _play, _work, and System views start with a letter, eg: owner
|
||||
_ <- Helper.booleanToFuture(InvalidCustomViewFormat+s"Current view_name (${viewId.value})", cc=callContext) { viewId.value.startsWith("_") }
|
||||
_ <- NewStyle.function.customView(viewId, BankIdAccountId(bankId, accountId), callContext)
|
||||
deleted <- NewStyle.function.removeView(account, u, viewId, callContext)
|
||||
|
||||
anyViewContainsCanDeleteCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canDeleteCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
_ <- Helper.booleanToFuture(
|
||||
s"${ErrorMessages.ViewDoesNotPermitAccess} You need the `${ViewDefinition.canDeleteCustomView_.dbColumnName}` permission on any your views",
|
||||
cc = callContext
|
||||
) {
|
||||
anyViewContainsCanDeleteCustomViewPermission
|
||||
}
|
||||
|
||||
deleted <- NewStyle.function.removeCustomView(viewId, BankIdAccountId(bankId, accountId),callContext)
|
||||
} yield {
|
||||
(Full(deleted), HttpCode.`204`(callContext))
|
||||
}
|
||||
|
||||
@ -170,8 +170,14 @@ trait APIMethods220 {
|
||||
createViewJsonV121.which_alias_to_use,
|
||||
createViewJsonV121.hide_metadata_if_alias_used,
|
||||
createViewJsonV121.allowed_actions
|
||||
)
|
||||
anyViewContainsCanCreateCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canCreateCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
_ <- booleanToBox(
|
||||
anyViewContainsCanCreateCustomViewPermission,
|
||||
s"${ErrorMessages.CreateCustomViewError} You need the `${ViewDefinition.canCreateCustomView_.dbColumnName}` permission on any your views"
|
||||
)
|
||||
view <- account.createCustomView(u, createViewJson, Some(cc))
|
||||
view <- Views.views.vend.createCustomView(BankIdAccountId(bankId, accountId), createViewJson) ?~ CreateCustomViewError
|
||||
} yield {
|
||||
val viewJSON = JSONFactory220.createViewJSON(view)
|
||||
successJsonResponse(Extraction.decompose(viewJSON), 201)
|
||||
@ -224,7 +230,13 @@ trait APIMethods220 {
|
||||
hide_metadata_if_alias_used = updateJsonV121.hide_metadata_if_alias_used,
|
||||
allowed_actions = updateJsonV121.allowed_actions
|
||||
)
|
||||
updatedView <- account.updateView(u, viewId, updateViewJson, Some(cc))
|
||||
anyViewContainsCancanUpdateCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canUpdateCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
_ <- booleanToBox(
|
||||
anyViewContainsCancanUpdateCustomViewPermission,
|
||||
s"${ErrorMessages.CreateCustomViewError} You need the `${ViewDefinition.canUpdateCustomView_.dbColumnName}` permission on any your views"
|
||||
)
|
||||
updatedView <- Views.views.vend.updateCustomView(BankIdAccountId(bankId, accountId), viewId, updateViewJson) ?~ CreateCustomViewError
|
||||
} yield {
|
||||
val viewJSON = JSONFactory220.createViewJSON(updatedView)
|
||||
successJsonResponse(Extraction.decompose(viewJSON), 200)
|
||||
|
||||
@ -27,6 +27,7 @@ import code.users.Users
|
||||
import code.util.Helper
|
||||
import code.util.Helper.booleanToBox
|
||||
import code.views.Views
|
||||
import code.views.system.ViewDefinition
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.grum.geocalc.{Coordinate, EarthCalc, Point}
|
||||
import com.openbankproject.commons.model._
|
||||
@ -167,7 +168,6 @@ trait APIMethods300 {
|
||||
//creates a view on an bank account
|
||||
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: Nil JsonPost json -> _ => {
|
||||
cc =>
|
||||
val res =
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
createViewJson <- Future { tryo{json.extract[CreateViewJson]} } map {
|
||||
@ -179,14 +179,18 @@ trait APIMethods300 {
|
||||
checkCustomViewIdOrName(createViewJson.name)
|
||||
}
|
||||
(account, callContext) <- NewStyle.function.getBankAccount(bankId, accountId, callContext)
|
||||
|
||||
anyViewContainsCanCreateCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canCreateCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
|
||||
_ <- Helper.booleanToFuture(
|
||||
s"${ErrorMessages.ViewDoesNotPermitAccess} You need the `${ViewDefinition.canCreateCustomView_.dbColumnName}` permission on any your views",
|
||||
cc = callContext
|
||||
) {anyViewContainsCanCreateCustomViewPermission}
|
||||
(view, callContext) <- NewStyle.function.createCustomView(BankIdAccountId(bankId, accountId), createViewJson, callContext)
|
||||
} yield {
|
||||
for {
|
||||
view <- account.createCustomView (u, createViewJson, callContext)
|
||||
} yield {
|
||||
(JSONFactory300.createViewJSON(view), callContext.map(_.copy(httpCode = Some(201))))
|
||||
}
|
||||
(JSONFactory300.createViewJSON(view), HttpCode.`200`(callContext))
|
||||
}
|
||||
res map { fullBoxOrException(_) } map { unboxFull(_) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +257,6 @@ trait APIMethods300 {
|
||||
//updates a view on a bank account
|
||||
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: ViewId(viewId) :: Nil JsonPut json -> _ => {
|
||||
cc =>
|
||||
val res =
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
updateJson <- Future { tryo{json.extract[UpdateViewJsonV300]} } map {
|
||||
@ -273,14 +276,20 @@ trait APIMethods300 {
|
||||
!view.isSystem
|
||||
}
|
||||
(account, callContext) <- NewStyle.function.getBankAccount(bankId, accountId, callContext)
|
||||
} yield {
|
||||
for {
|
||||
updatedView <- account.updateView(u, viewId, updateJson.toUpdateViewJson, callContext)
|
||||
} yield {
|
||||
(JSONFactory300.createViewJSON(updatedView), HttpCode.`200`(callContext))
|
||||
|
||||
anyViewContainsCancanUpdateCustomViewPermission = Views.views.vend.permission(BankIdAccountId(account.bankId, account.accountId), u)
|
||||
.map(_.views.map(_.canUpdateCustomView).find(_.==(true)).getOrElse(false)).getOrElse(false)
|
||||
|
||||
_ <- Helper.booleanToFuture(
|
||||
s"${ErrorMessages.ViewDoesNotPermitAccess} You need the `${ViewDefinition.canUpdateCustomView_.dbColumnName}` permission on any your views",
|
||||
cc = callContext
|
||||
) {
|
||||
anyViewContainsCancanUpdateCustomViewPermission
|
||||
}
|
||||
(view, callContext) <- NewStyle.function.updateCustomView(BankIdAccountId(bankId, accountId), viewId, updateJson.toUpdateViewJson, callContext)
|
||||
} yield {
|
||||
(JSONFactory300.createViewJSON(view), HttpCode.`200`(callContext))
|
||||
}
|
||||
res map { fullBoxOrException(_) } map { unboxFull(_) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -338,51 +338,6 @@ case class BankAccountExtended(val bankAccount: BankAccount) extends MdcLoggable
|
||||
Failure(UserNoOwnerView+"user's email : " + user.emailAddress + ". account : " + accountId, Empty, Empty)
|
||||
}
|
||||
|
||||
|
||||
final def createCustomView(userDoingTheCreate : User,v: CreateViewJson, callContext: Option[CallContext]): Box[View] = {
|
||||
if(!userDoingTheCreate.hasOwnerViewAccess(BankIdAccountId(bankId,accountId), callContext)) {
|
||||
Failure({"user: " + userDoingTheCreate.idGivenByProvider + " at provider " + userDoingTheCreate.provider + " does not have owner access"})
|
||||
} else {
|
||||
val view = Views.views.vend.createCustomView(BankIdAccountId(bankId,accountId), v)
|
||||
|
||||
//if(view.isDefined) {
|
||||
// logger.debug("user: " + userDoingTheCreate.idGivenByProvider + " at provider " + userDoingTheCreate.provider + " created view: " + view.get +
|
||||
// " for account " + accountId + "at bank " + bankId)
|
||||
//}
|
||||
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
final def updateView(userDoingTheUpdate : User, viewId : ViewId, v: UpdateViewJSON, callContext: Option[CallContext]) : Box[View] = {
|
||||
if(!userDoingTheUpdate.hasOwnerViewAccess(BankIdAccountId(bankId,accountId), callContext)) {
|
||||
Failure({"user: " + userDoingTheUpdate.idGivenByProvider + " at provider " + userDoingTheUpdate.provider + " does not have owner access"})
|
||||
} else {
|
||||
val view = Views.views.vend.updateCustomView(BankIdAccountId(bankId,accountId), viewId, v)
|
||||
//if(view.isDefined) {
|
||||
// logger.debug("user: " + userDoingTheUpdate.idGivenByProvider + " at provider " + userDoingTheUpdate.provider + " updated view: " + view.get +
|
||||
// " for account " + accountId + "at bank " + bankId)
|
||||
//}
|
||||
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
final def removeView(userDoingTheRemove : User, viewId: ViewId, callContext: Option[CallContext]) : Box[Boolean] = {
|
||||
if(!userDoingTheRemove.hasOwnerViewAccess(BankIdAccountId(bankId,accountId), callContext)) {
|
||||
return Failure({"user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " does not have owner access"})
|
||||
} else {
|
||||
val deleted = Views.views.vend.removeCustomView(viewId, BankIdAccountId(bankId,accountId))
|
||||
|
||||
//if (deleted.isDefined) {
|
||||
// logger.debug("user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " deleted view: " + viewId +
|
||||
// " for account " + accountId + "at bank " + bankId)
|
||||
//}
|
||||
|
||||
deleted
|
||||
}
|
||||
}
|
||||
|
||||
final def moderatedTransaction(transactionId: TransactionId, view: View, bankIdAccountId: BankIdAccountId, user: Box[User], callContext: Option[CallContext] = None) : Box[(ModeratedTransaction, Option[CallContext])] = {
|
||||
if(APIUtil.hasAccountAccess(view, bankIdAccountId, user, callContext))
|
||||
for{
|
||||
|
||||
@ -439,6 +439,15 @@ class ViewImpl extends View with LongKeyedMapper[ViewImpl] with ManyToMany with
|
||||
object canSeeBankAccountCreditLimit_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
object canCreateCustomView_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
object canDeleteCustomView_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
object canUpdateCustomView_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
|
||||
def id: Long = id_.get
|
||||
def isSystem: Boolean = isSystem_.get
|
||||
@ -555,6 +564,10 @@ class ViewImpl extends View with LongKeyedMapper[ViewImpl] with ManyToMany with
|
||||
def canCreateStandingOrder: Boolean = false
|
||||
//TODO: if you add new permissions here, remember to set them wherever views are created
|
||||
// (e.g. BankAccountCreationDispatcher)
|
||||
|
||||
def canCreateCustomView: Boolean = canCreateCustomView_.get
|
||||
def canDeleteCustomView: Boolean = canDeleteCustomView_.get
|
||||
def canUpdateCustomView: Boolean = canUpdateCustomView_.get
|
||||
}
|
||||
|
||||
object ViewImpl extends ViewImpl with LongKeyedMetaMapper[ViewImpl]{
|
||||
|
||||
@ -791,9 +791,12 @@ object MapperViews extends Views with MdcLoggable {
|
||||
.canAddTransactionRequestToOwnAccount_(true) //added following two for payments
|
||||
.canAddTransactionRequestToAnyAccount_(true)
|
||||
.canSeeAvailableViewsForBankAccount_(false)
|
||||
.canSeeTransactionRequests_(true)
|
||||
.canSeeTransactionRequestTypes_(true)
|
||||
.canUpdateBankAccountLabel_(true)
|
||||
.canSeeTransactionRequests_(false)
|
||||
.canSeeTransactionRequestTypes_(false)
|
||||
.canUpdateBankAccountLabel_(false)
|
||||
.canCreateCustomView_(false)
|
||||
.canDeleteCustomView_(false)
|
||||
.canUpdateCustomView_(false)
|
||||
|
||||
viewId match {
|
||||
case SYSTEM_OWNER_VIEW_ID =>
|
||||
@ -801,6 +804,10 @@ object MapperViews extends Views with MdcLoggable {
|
||||
.canSeeAvailableViewsForBankAccount_(true)
|
||||
.canSeeTransactionRequests_(true)
|
||||
.canSeeTransactionRequestTypes_(true)
|
||||
.canUpdateBankAccountLabel_(true)
|
||||
.canCreateCustomView_(true)
|
||||
.canDeleteCustomView_(true)
|
||||
.canUpdateCustomView_(true)
|
||||
case SYSTEM_STAGE_ONE_VIEW_ID =>
|
||||
entity
|
||||
.canSeeTransactionDescription_(false)
|
||||
|
||||
@ -304,6 +304,16 @@ class ViewDefinition extends View with LongKeyedMapper[ViewDefinition] with Many
|
||||
object canCreateStandingOrder_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
|
||||
object canCreateCustomView_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
object canDeleteCustomView_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
object canUpdateCustomView_ extends MappedBoolean(this){
|
||||
override def defaultValue = false
|
||||
}
|
||||
|
||||
//Important! If you add a field, be sure to handle it here in this function
|
||||
def setFromViewData(viewData : ViewSpecification) = {
|
||||
@ -410,6 +420,9 @@ class ViewDefinition extends View with LongKeyedMapper[ViewDefinition] with Many
|
||||
canSeeTransactionRequestTypes_(actions.exists(_ == "can_see_transaction_request_types"))
|
||||
canUpdateBankAccountLabel_(actions.exists(_ == "can_update_bank_account_label"))
|
||||
canSeeAvailableViewsForBankAccount_(actions.exists(_ == "can_see_available_views_for_bank_account"))
|
||||
canCreateCustomView_(actions.exists(_ == "can_create_custom_view"))
|
||||
canDeleteCustomView_(actions.exists(_ == "can_delete_custom_view"))
|
||||
canUpdateCustomView_(actions.exists(_ == "can_update_custom_view"))
|
||||
}
|
||||
|
||||
|
||||
@ -544,6 +557,9 @@ class ViewDefinition extends View with LongKeyedMapper[ViewDefinition] with Many
|
||||
|
||||
def canCreateDirectDebit: Boolean = canCreateDirectDebit_.get
|
||||
def canCreateStandingOrder: Boolean = canCreateStandingOrder_.get
|
||||
def canCreateCustomView: Boolean = canCreateCustomView_.get
|
||||
def canDeleteCustomView: Boolean = canDeleteCustomView_.get
|
||||
def canUpdateCustomView: Boolean = canUpdateCustomView_.get
|
||||
//TODO: if you add new permissions here, remember to set them wherever views are created
|
||||
// (e.g. BankAccountCreationDispatcher)
|
||||
}
|
||||
|
||||
@ -421,4 +421,8 @@ trait View {
|
||||
def canCreateDirectDebit: Boolean
|
||||
|
||||
def canCreateStandingOrder: Boolean
|
||||
|
||||
def canCreateCustomView: Boolean
|
||||
def canDeleteCustomView: Boolean
|
||||
def canUpdateCustomView: Boolean
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user