~jan0sch/smederee
Showing details for patch bbba74c735f7fc6fd949a2e305838cdd45e90dce.
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) + } + } +}