Commit d09c70f5 authored by Ricki Hirner's avatar Ricki Hirner

More fine-grained WebDAV permissions

* service DB: split readOnly into privWriteContent and privUnbind
* collections: use privWriteContent (DAV:write-content privilege) for read-only detection
* AccountActivity: allow collection deletion only when privUnbind (DAV:unbind privilege) is set
parent 2f0ee8e2
Pipeline #28709551 failed with stage
......@@ -61,7 +61,8 @@ class CollectionInfoTest {
info = CollectionInfo(response)
}
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info?.type)
assertFalse(info!!.readOnly)
assertTrue(info!!.privWriteContent)
assertTrue(info!!.privUnbind)
assertEquals("My Contacts", info?.displayName)
assertEquals("My Contacts Description", info?.description)
......@@ -87,7 +88,8 @@ class CollectionInfoTest {
info = CollectionInfo(response)
}
assertEquals(CollectionInfo.Type.CALENDAR, info?.type)
assertTrue(info!!.readOnly)
assertFalse(info!!.privWriteContent)
assertFalse(info!!.privUnbind)
assertNull(info?.displayName)
assertEquals("My Calendar", info?.description)
assertEquals(0xFFFF0000.toInt(), info?.color)
......@@ -103,7 +105,8 @@ class CollectionInfoTest {
values.put(Collections.SERVICE_ID, 1)
values.put(Collections.TYPE, CollectionInfo.Type.CALENDAR.name)
values.put(Collections.URL, "http://example.com")
values.put(Collections.READ_ONLY, 1)
values.put(Collections.PRIV_WRITE_CONTENT, 0)
values.put(Collections.PRIV_UNBIND, 0)
values.put(Collections.DISPLAY_NAME, "display name")
values.put(Collections.DESCRIPTION, "description")
values.put(Collections.COLOR, 0xFFFF0000)
......@@ -117,7 +120,8 @@ class CollectionInfoTest {
assertEquals(1.toLong(), info.id)
assertEquals(1.toLong(), info.serviceID)
assertEquals(HttpUrl.parse("http://example.com/"), info.url)
assertTrue(info.readOnly)
assertFalse(info.privWriteContent)
assertFalse(info.privUnbind)
assertEquals("display name", info.displayName)
assertEquals("description", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
......
......@@ -161,7 +161,7 @@ class DavService: Service() {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
db.query(Collections._TABLE, null, "${Collections.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues()
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
values.getAsString(Collections.URL)?.let { url ->
HttpUrl.parse(url)?.let { collections.put(it, CollectionInfo(values)) }
......@@ -303,7 +303,7 @@ class DavService: Service() {
val selectedCollections = HashSet<HttpUrl>()
collections.values
.filter { it.selected }
.forEach { (url, _) -> selectedCollections.add(url) }
.forEach { (url, _) -> selectedCollections += url }
// now refresh collections (taken from home sets)
val itHomeSets = homeSets.iterator()
......
......@@ -34,7 +34,8 @@ data class CollectionInfo(
var type: Type? = null,
var readOnly: Boolean = false,
var privWriteContent: Boolean = true,
var privUnbind: Boolean = true,
var forceReadOnly: Boolean = false,
var displayName: String? = null,
var description: String? = null,
......@@ -68,7 +69,8 @@ data class CollectionInfo(
}
dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
readOnly = !privilegeSet.mayWriteContent
privWriteContent = privilegeSet.mayWriteContent
privUnbind = privilegeSet.mayUnbind
}
dav[DisplayName::class.java]?.let {
......@@ -110,7 +112,8 @@ data class CollectionInfo(
null
}
readOnly = values.getAsInteger(Collections.READ_ONLY) != 0
privWriteContent = values.getAsInteger(Collections.PRIV_WRITE_CONTENT) != 0
privUnbind = values.getAsInteger(Collections.PRIV_UNBIND) != 0
forceReadOnly = values.getAsInteger(Collections.FORCE_READ_ONLY) != 0
displayName = values.getAsString(Collections.DISPLAY_NAME)
description = values.getAsString(Collections.DESCRIPTION)
......@@ -132,7 +135,8 @@ data class CollectionInfo(
type?.let { values.put(Collections.TYPE, it.name) }
values.put(Collections.URL, url.toString())
values.put(Collections.READ_ONLY, if (readOnly) 1 else 0)
values.put(Collections.PRIV_WRITE_CONTENT, if (privWriteContent) 1 else 0)
values.put(Collections.PRIV_UNBIND, if (privUnbind) 1 else 0)
values.put(Collections.FORCE_READ_ONLY, if (forceReadOnly) 1 else 0)
values.put(Collections.DISPLAY_NAME, displayName)
values.put(Collections.DESCRIPTION, description)
......@@ -176,7 +180,9 @@ data class CollectionInfo(
dest.writeString(type?.name)
dest.writeByte(if (readOnly) 1 else 0)
dest.writeByte(if (privWriteContent) 1 else 0)
dest.writeByte(if (privUnbind) 1 else 0)
dest.writeByte(if (forceReadOnly) 1 else 0)
dest.writeString(displayName)
dest.writeString(description)
......@@ -220,6 +226,8 @@ data class CollectionInfo(
parcel.readString()?.let { Type.valueOf(it) },
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readString(),
......
......@@ -46,7 +46,8 @@ class ServiceDB {
const val TYPE = "type"
const val SERVICE_ID = "serviceID"
const val URL = "url"
const val READ_ONLY = "readOnly"
const val PRIV_WRITE_CONTENT = "privWriteContent"
const val PRIV_UNBIND = "privUnbind"
const val FORCE_READ_ONLY = "forceReadOnly"
const val DISPLAY_NAME = "displayName"
const val DESCRIPTION = "description"
......@@ -75,7 +76,7 @@ class ServiceDB {
companion object {
const val DATABASE_NAME = "services.db"
const val DATABASE_VERSION = 4
const val DATABASE_VERSION = 5
}
override fun onConfigure(db: SQLiteDatabase) {
......@@ -104,7 +105,8 @@ class ServiceDB {
"${Collections.SERVICE_ID} INTEGER NOT NULL REFERENCES ${Services._TABLE} ON DELETE CASCADE," +
"${Collections.TYPE} TEXT NOT NULL," +
"${Collections.URL} TEXT NOT NULL," +
"${Collections.READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
"${Collections.PRIV_WRITE_CONTENT} INTEGER DEFAULT 0 NOT NULL," +
"${Collections.PRIV_UNBIND} INTEGER DEFAULT 0 NOT NULL," +
"${Collections.FORCE_READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
"${Collections.DISPLAY_NAME} TEXT NULL," +
"${Collections.DESCRIPTION} TEXT NULL," +
......@@ -130,6 +132,17 @@ class ServiceDB {
}
}
@Suppress("unused")
private fun upgrade_4_5(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.PRIV_WRITE_CONTENT} INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE ${Collections._TABLE} SET ${Collections.PRIV_WRITE_CONTENT}=NOT readOnly")
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.PRIV_UNBIND} INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE ${Collections._TABLE} SET ${Collections.PRIV_UNBIND}=NOT readOnly")
// there's no DROP COLUMN in SQLite, so just keep the "readOnly" column
}
@Suppress("unused")
private fun upgrade_3_4(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.FORCE_READ_ONLY} INTEGER DEFAULT 0 NOT NULL")
......
......@@ -191,8 +191,8 @@ class LocalAddressBook(
account = future.result
}
Constants.log.info("Address book read-only? = ${info.readOnly}")
readOnly = info.readOnly || info.forceReadOnly
Constants.log.info("Address book write permission? = ${info.privWriteContent}")
readOnly = !info.privWriteContent || info.forceReadOnly
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
......
......@@ -60,13 +60,12 @@ class LocalCalendar private constructor(
if (withColor)
values.put(Calendars.CALENDAR_COLOR, info.color ?: Constants.DAVDROID_GREEN_RGBA)
if (info.readOnly || info.forceReadOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
else {
if (info.privWriteContent && !info.forceReadOnly) {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
}
} else
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
info.timeZone?.let { tzData ->
try {
......
......@@ -204,13 +204,13 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
popup.inflate(R.menu.account_collection_operations)
with(popup.menu.findItem(R.id.force_read_only)) {
if (info.readOnly)
isVisible = false
else
if (info.privWriteContent)
isChecked = info.forceReadOnly
else
isVisible = false
}
popup.menu.findItem(R.id.delete_collection).isVisible = !info.readOnly
popup.menu.findItem(R.id.delete_collection).isVisible = info.privUnbind
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
......@@ -616,7 +616,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
}
v.findViewById<ImageView>(R.id.read_only).visibility =
if (info.readOnly || info.forceReadOnly) View.VISIBLE else View.GONE
if (!info.privWriteContent || info.forceReadOnly) View.VISIBLE else View.GONE
v.findViewById<ImageView>(R.id.action_overflow).setOnClickListener { view ->
@Suppress("ReplaceSingleLineLet")
......@@ -661,7 +661,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
}
v.findViewById<ImageView>(R.id.read_only).visibility =
if (info.readOnly || info.forceReadOnly) View.VISIBLE else View.GONE
if (!info.privWriteContent || info.forceReadOnly) View.VISIBLE else View.GONE
v.findViewById<ImageView>(R.id.events).visibility =
if (info.supportsVEVENT) View.VISIBLE else View.GONE
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment