~jan0sch/smederee

Showing details for patch bbba74c735f7fc6fd949a2e305838cdd45e90dce.
2023-12-10 (Sun), 12:21 PM - Jens Grassel - bbba74c735f7fc6fd949a2e305838cdd45e90dce

security: Add Permission model to model privileges.

The permission model follows the approach of encoding POSIX file system
permissions and the idea to store only encoded permission sets in storage
layers (database) to allow faster querying for permissions.

It currently provides the following capabilities:

- `Execute` (octal value 1)
   - Allow executing tasks related to the data e.g. start build pipeline.
- `Read` (octal value 4)
   - Allow reading data (non modifying operations) e.g. view a repository
     or organisation.
- `Write` (octal value 2)
   - Allow writing data including deletion e.g. push commits, delete a project
     or modify an organisation.

That should enable us to provide role based access control (RBAC) in
combination with organisations and teams (to be implemented).
Summary of changes
2 files added
  • modules/security/src/main/scala/de/smederee/security/Permission.scala
  • modules/security/src/test/scala/de/smederee/security/PermissionTest.scala
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/Permission.scala new-smederee/modules/security/src/main/scala/de/smederee/security/Permission.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/Permission.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/Permission.scala	2025-01-13 11:48:01.410322392 +0000
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.security
+
+import cats.*
+import cats.syntax.all.*
+
+/** Generic permissions on resources used to provide privileges to users.
+  *
+  * @param encoded
+  *   An internal value used for decoding and encoding permissions as well as permission sets. This follows the octal
+  *   approach of encoding POSIX file system permissions and the idea to store only encoded permission sets in storage
+  *   layers (database) to allow faster querying for permissions.
+  */
+enum Permission(val encoded: Int) {
+
+    /** Allow executing tasks related to the data e.g. start build pipeline. */
+    case Execute extends Permission(encoded = 1)
+
+    /** Allow reading data (non modifying operations) e.g. view a repository or organisation. */
+    case Read extends Permission(encoded = 4)
+
+    /** Allow writing data including deletion e.g. push commits, delete a project or modify an organisation. */
+    case Write extends Permission(encoded = 2)
+}
+
+object Permission {
+    given Eq[Permission] = Eq.instance((a, b) => a.encoded === b.encoded)
+
+    /** Decode the given integer value into a set of permissions. Illegal values that cannot be mapped will return an
+      * empty set.
+      *
+      * @param encoded
+      *   An integer value that holds an encoded permission set.
+      * @return
+      *   A set of permissions that may be empty.
+      */
+    def decode(encoded: Int): Set[Permission] =
+        encoded match {
+            case 0 => Set.empty
+            case 1 => Set(Execute)
+            case 2 => Set(Write)
+            case 3 => Set(Execute, Write)
+            case 4 => Set(Read)
+            case 5 => Set(Execute, Read)
+            case 6 => Set(Read, Write)
+            case 7 => Set(Execute, Read, Write)
+            case _ => Set.empty // We return an empty set for illegal values!
+        }
+
+    /** Return the permission named after the given string.
+      *
+      * @param name
+      *   A string containing the name of a permission e.g. Read.
+      * @return
+      *   An option to the permission instance.
+      */
+    def from(name: String): Option[Permission] = Permission.values.find(_.toString === name)
+
+    /** Return the permission that is encoded by the given integer value.
+      *
+      * @param encoded
+      *   An integer value that holds a single encoded permission.
+      * @return
+      *   An option to the permission instance.
+      */
+    def fromInt(encoded: Int): Option[Permission] = Permission.values.find(_.encoded === encoded)
+
+    extension (ps: Set[Permission]) {
+
+        /** Encode this set of permissions into an integer value.
+          *
+          * @return
+          *   An integer value containing the encoded representation of this set of permissions.
+          */
+        def encode: Int = ps.foldLeft(0)((encodedSet, permission) => encodedSet | permission.encoded)
+    }
+}
diff -rN -u old-smederee/modules/security/src/test/scala/de/smederee/security/PermissionTest.scala new-smederee/modules/security/src/test/scala/de/smederee/security/PermissionTest.scala
--- old-smederee/modules/security/src/test/scala/de/smederee/security/PermissionTest.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/security/src/test/scala/de/smederee/security/PermissionTest.scala	2025-01-13 11:48:01.410322392 +0000
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.security
+
+import cats.syntax.all.*
+
+import munit.*
+
+import org.scalacheck.Prop.*
+import org.scalacheck.*
+
+final class PermissionTest extends ScalaCheckSuite {
+    private val genPermission: Gen[Permission] = Gen.oneOf(Permission.values.toList)
+    given Arbitrary[Permission]                = Arbitrary(genPermission)
+    given Arbitrary[Set[Permission]]           = Arbitrary(Gen.listOf(genPermission).map(_.toSet))
+
+    property("Eq must hold") {
+        forAll { (randomPermission: Permission) =>
+            assert(randomPermission === randomPermission, "Identical permissions must be considered equal!")
+            Permission.values.toList
+                .filterNot(_ === randomPermission)
+                .foreach(otherPermission =>
+                    assert(randomPermission =!= otherPermission, "Different permissions mut not be considered equal!")
+                )
+        }
+    }
+
+    property("Permission.decode must be the dual of Permission.encode") {
+        forAll { (randomPermission: Permission) =>
+            assertEquals(Permission.decode(randomPermission.encoded), Set(randomPermission))
+        }
+    }
+
+    property("Permission.decode must return an empty Set for invalid input") {
+        forAll { (randomInteger: Int) =>
+            if (randomInteger > 7 || randomInteger < 0) {
+                assertEquals(Permission.decode(randomInteger), Set.empty)
+            }
+        }
+    }
+
+    property("Permission.decode must be the dual of Set[Permission].encode") {
+        forAll { (permissions: Set[Permission]) =>
+            assertEquals(Permission.decode(permissions.encode), permissions)
+        }
+    }
+
+    property("Permission.from must succeed on valid input") {
+        forAll { (randomPermission: Permission) =>
+            assertEquals(Permission.from(randomPermission.toString), randomPermission.some)
+        }
+    }
+
+    property("Permission.fromInt must succeed on valid input") {
+        forAll { (randomPermission: Permission) =>
+            assertEquals(Permission.fromInt(randomPermission.encoded), randomPermission.some)
+        }
+    }
+
+    property("Set[Permission].encode must produce correct values") {
+        forAll { (permissions: Set[Permission]) =>
+            val expected = permissions.foldLeft(0)((acc, p) => acc | p.encoded)
+            assertEquals(permissions.encode, expected)
+        }
+    }
+}