Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple owners/maintainers #258

Merged
merged 14 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions backend/main/PackagePublisher.mo
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,19 @@ module {
};
};

let isNewPackage = registry.getHighestVersion(config.name) == null;

// check permissions
switch (registry.getPackageOwner(config.name)) {
case (null) {
// deny '.' and '_' in name for new packages
for (char in config.name.chars()) {
let err = #err("invalid config: unexpected char '" # Char.toText(char) # "' in name '" # config.name # "'");
if (char == '.' or char == '_') {
return err;
};
};
};
case (?owner) {
if (owner != caller) {
return #err("You don't have permissions to publish this package");
if (not isNewPackage and not registry.isOwner(config.name, caller) and not registry.isMaintainer(config.name, caller)) {
return #err("Only owners and maintainers can publish packages");
};

// deny '.' and '_' in name for new packages
if (isNewPackage) {
for (char in config.name.chars()) {
let err = #err("invalid config: unexpected char '" # Char.toText(char) # "' in name '" # config.name # "'");
if (char == '.' or char == '_') {
return err;
};
};
};
Expand All @@ -103,7 +102,7 @@ module {
if (dep.repo.size() == 0 and registry.getPackageConfig(PackageUtils.getDepName(dep.name), dep.version) == null) {
return #err("Dependency " # packageId # " not found in registry");
};
if (dep.repo.size() != 0 and registry.getPackageOwner(PackageUtils.getDepName(dep.name)) != null) {
if (dep.repo.size() != 0 and registry.getHighestVersion(PackageUtils.getDepName(dep.name)) != null) {
return #err("GitHub dependency `" # dep.name # "` conflicts with an existing package in the Mops registry. Please change the dependency name in mops.toml file.");
};
};
Expand Down
101 changes: 75 additions & 26 deletions backend/main/main-canister.mo
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ actor class Main() {
let API_VERSION = "1.3"; // (!) make changes in pair with cli

var packageVersions = TrieMap.TrieMap<PackageName, [PackageVersion]>(Text.equal, Text.hash);
var packageOwners = TrieMap.TrieMap<PackageName, Principal>(Text.equal, Text.hash);
var packageOwners = TrieMap.TrieMap<PackageName, Principal>(Text.equal, Text.hash); // legacy
var ownersByPackage = TrieMap.TrieMap<PackageName, [Principal]>(Text.equal, Text.hash);
var maintainersByPackage = TrieMap.TrieMap<PackageName, [Principal]>(Text.equal, Text.hash);
var highestConfigs = TrieMap.TrieMap<PackageName, PackageConfigV3>(Text.equal, Text.hash);

var packageConfigs = TrieMap.TrieMap<PackageId, PackageConfigV3>(Text.equal, Text.hash);
Expand All @@ -76,7 +78,8 @@ actor class Main() {

var registry = Registry.Registry(
packageVersions,
packageOwners,
ownersByPackage,
maintainersByPackage,
highestConfigs,
packageConfigs,
packagePublications,
Expand Down Expand Up @@ -489,8 +492,28 @@ actor class Main() {
users.setUserProp(caller, prop, value);
};

public shared ({caller}) func transferOwnership(packageName : PackageName, newOwner : Principal) : async Result.Result<(), Text> {
registry.transferOwnership(caller, packageName, newOwner);
public query func getPackageOwners(packageName : PackageName) : async [Principal] {
registry.getPackageOwners(packageName);
};

public query func getPackageMaintainers(packageName : PackageName) : async [Principal] {
registry.getPackageMaintainers(packageName);
};

public shared ({caller}) func addOwner(packageName : PackageName, newOwner : Principal) : async Result.Result<(), Text> {
registry.addOwner(caller, packageName, newOwner);
};

public shared ({caller}) func addMaintainer(packageName : PackageName, newMaintainer : Principal) : async Result.Result<(), Text> {
registry.addMaintainer(caller, packageName, newMaintainer);
};

public shared ({caller}) func removeOwner(packageName : PackageName, owner : Principal) : async Result.Result<(), Text> {
registry.removeOwner(caller, packageName, owner);
};

public shared ({caller}) func removeMaintainer(packageName : PackageName, maintainer : Principal) : async Result.Result<(), Text> {
registry.removeMaintainer(caller, packageName, maintainer);
};

// BADGES
Expand Down Expand Up @@ -526,10 +549,11 @@ actor class Main() {
let backupManager = Backup.BackupManager(backupStateV2, {maxBackups = 20});

type BackupChunk = {
#v7 : {
#v8 : {
#packagePublications : [(PackageId, PackagePublication)];
#packageVersions : [(PackageName, [PackageVersion])];
#packageOwners : [(PackageName, Principal)];
#ownersByPackage : [(PackageName, [Principal])];
#maintainersByPackage : [(PackageName, [Principal])];
#packageConfigs : [(PackageId, PackageConfigV3)];
#highestConfigs : [(PackageName, PackageConfigV3)];
#fileIdsByPackage : [(PackageId, [FileId])];
Expand All @@ -555,22 +579,23 @@ actor class Main() {
};

func _backup() : async () {
let backup = backupManager.NewBackup("v7");
let backup = backupManager.NewBackup("v8");
await backup.startBackup();
await backup.uploadChunk(to_candid(#v7(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageFileStats(Iter.toArray(packageFileStats.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageBenchmarks(Iter.toArray(packageBenchmarks.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#downloadLog(downloadLog.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#storageManager(storageManager.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#users(users.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v7(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#ownersByPackage(Iter.toArray(ownersByPackage.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#maintainersByPackage(Iter.toArray(maintainersByPackage.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packageFileStats(Iter.toArray(packageFileStats.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packageBenchmarks(Iter.toArray(packageBenchmarks.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#downloadLog(downloadLog.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#storageManager(storageManager.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#users(users.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v8(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk));
await backup.finishBackup();
};

Expand All @@ -580,7 +605,7 @@ actor class Main() {
assert(Utils.isAdmin(caller));

await backupManager.restore(backupId, func(blob : Blob) {
let ?#v7(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk");
let ?#v8(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk");

switch (chunk) {
case (#packagePublications(packagePublicationsStable)) {
Expand All @@ -589,8 +614,11 @@ actor class Main() {
case (#packageVersions(packageVersionsStable)) {
packageVersions := TrieMap.fromEntries<PackageName, [PackageVersion]>(packageVersionsStable.vals(), Text.equal, Text.hash);
};
case (#packageOwners(packageOwnersStable)) {
packageOwners := TrieMap.fromEntries<PackageName, Principal>(packageOwnersStable.vals(), Text.equal, Text.hash);
case (#ownersByPackage(ownersByPackageStable)) {
ownersByPackage := TrieMap.fromEntries<PackageName, [Principal]>(ownersByPackageStable.vals(), Text.equal, Text.hash);
};
case (#maintainersByPackage(maintainersByPackageStable)) {
maintainersByPackage := TrieMap.fromEntries<PackageName, [Principal]>(maintainersByPackageStable.vals(), Text.equal, Text.hash);
};
case (#fileIdsByPackage(fileIdsByPackageStable)) {
fileIdsByPackage := TrieMap.fromEntries<PackageId, [FileId]>(fileIdsByPackageStable.vals(), Text.equal, Text.hash);
Expand Down Expand Up @@ -634,7 +662,8 @@ actor class Main() {
// re-init registry
registry := Registry.Registry(
packageVersions,
packageOwners,
ownersByPackage,
maintainersByPackage,
highestConfigs,
packageConfigs,
packagePublications,
Expand All @@ -652,6 +681,8 @@ actor class Main() {
stable var packagePublicationsStable : [(PackageId, PackagePublication)] = [];
stable var packageVersionsStable : [(PackageName, [PackageVersion])] = [];
stable var packageOwnersStable : [(PackageName, Principal)] = [];
stable var ownersByPackageStable : [(PackageName, [Principal])] = [];
stable var maintainersByPackageStable : [(PackageName, [Principal])] = [];

stable var packageConfigsStableV3 : [(PackageId, PackageConfigV3)] = [];
stable var highestConfigsStableV3 : [(PackageName, PackageConfigV3)] = [];
Expand All @@ -671,6 +702,8 @@ actor class Main() {
packagePublicationsStable := Iter.toArray(packagePublications.entries());
packageVersionsStable := Iter.toArray(packageVersions.entries());
packageOwnersStable := Iter.toArray(packageOwners.entries());
ownersByPackageStable := Iter.toArray(ownersByPackage.entries());
maintainersByPackageStable := Iter.toArray(maintainersByPackage.entries());
fileIdsByPackageStable := Iter.toArray(fileIdsByPackage.entries());
hashByFileIdStable := Iter.toArray(hashByFileId.entries());
packageFileStatsStable := Iter.toArray(packageFileStats.entries());
Expand Down Expand Up @@ -698,6 +731,21 @@ actor class Main() {
packageVersions := TrieMap.fromEntries<PackageName, [PackageVersion]>(packageVersionsStable.vals(), Text.equal, Text.hash);
packageVersionsStable := [];

// migrate packageOwners -> ownersByPackage
if (ownersByPackageStable.size() == 0) {
ownersByPackage :=
packageOwnersStable.vals()
|> Iter.map<(PackageName, Principal), (PackageName, [Principal])>(_, func((name, owner)) = (name, [owner]))
|> TrieMap.fromEntries<PackageName, [Principal]>(_, Text.equal, Text.hash);
}
else {
ownersByPackage := TrieMap.fromEntries<PackageName, [Principal]>(ownersByPackageStable.vals(), Text.equal, Text.hash);
ownersByPackageStable := [];
};

maintainersByPackage := TrieMap.fromEntries<PackageName, [Principal]>(maintainersByPackageStable.vals(), Text.equal, Text.hash);
maintainersByPackageStable := [];

packageOwners := TrieMap.fromEntries<PackageName, Principal>(packageOwnersStable.vals(), Text.equal, Text.hash);
packageOwnersStable := [];

Expand Down Expand Up @@ -732,7 +780,8 @@ actor class Main() {

registry := Registry.Registry(
packageVersions,
packageOwners,
ownersByPackage,
maintainersByPackage,
highestConfigs,
packageConfigs,
packagePublications,
Expand Down
111 changes: 98 additions & 13 deletions backend/main/registry/Registry.mo
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Option "mo:base/Option";
import Array "mo:base/Array";
import Time "mo:base/Time";
import Result "mo:base/Result";
import Principal "mo:base/Principal";

import Types "../types";
import Semver "../utils/semver";
Expand Down Expand Up @@ -34,7 +35,8 @@ module {

public class Registry(
packageVersions : TrieMap.TrieMap<PackageName, [PackageVersion]>,
packageOwners : TrieMap.TrieMap<PackageName, Principal>,
ownersByPackage : TrieMap.TrieMap<PackageName, [Principal]>,
maintainersByPackage : TrieMap.TrieMap<PackageName, [Principal]>,
highestConfigs : TrieMap.TrieMap<PackageName, PackageConfigV3>,
packageConfigs : TrieMap.TrieMap<PackageId, PackageConfigV3>,
packagePublications : TrieMap.TrieMap<PackageId, PackagePublication>,
Expand All @@ -59,7 +61,13 @@ module {
packageVersions.put(newRelease.config.name, Array.append(versions, [newRelease.config.version]));

packageConfigs.put(packageId, newRelease.config);
packageOwners.put(newRelease.config.name, newRelease.userId);

// add owner for new package
let owners = getPackageOwners(newRelease.config.name);
if (owners.size() == 0) {
ownersByPackage.put(newRelease.config.name, [newRelease.userId]);
};

packagePublications.put(packageId, {
user = newRelease.userId;
time = Time.now();
Expand Down Expand Up @@ -119,10 +127,6 @@ module {
// By package name
// -----------------------------

public func getPackageOwner(name : PackageName) : ?Principal {
packageOwners.get(name);
};

public func getPackageVersions(name : PackageName) : ?[PackageVersion] {
packageVersions.get(name);
};
Expand Down Expand Up @@ -169,17 +173,98 @@ module {
// Package ownership
// -----------------------------

public func transferOwnership(caller : Principal, packageName : PackageName, newOwner : Principal) : Result.Result<(), Text> {
let ?oldOwner = packageOwners.get(packageName) else return #err("Package not found");
public func getPackageOwners(name : PackageName) : [Principal] {
Option.get(ownersByPackage.get(name), []);
};

public func getPackageMaintainers(name : PackageName) : [Principal] {
Option.get(maintainersByPackage.get(name), []);
};

public func isOwner(name : PackageName, principal : Principal) : Bool {
for (owner in getPackageOwners(name).vals()) {
if (owner == principal) {
return true;
};
};
return false;
};

public func isMaintainer(name : PackageName, principal : Principal) : Bool {
for (maintainer in getPackageMaintainers(name).vals()) {
if (maintainer == principal) {
return true;
};
};
return false;
};

public func addOwner(caller : Principal, packageName : PackageName, newOwner : Principal) : Result.Result<(), Text> {
let ?owners = ownersByPackage.get(packageName) else return #err("Package not found");

if (isOwner(packageName, newOwner)) {
return #err("User is already an owner");
};
if (not isOwner(packageName, caller)) {
return #err("Only owners can add owners");
};
if (owners.size() >= 5) {
return #err("Maximum number of owners reached");
};

ownersByPackage.put(packageName, Array.append(owners, [newOwner]));
#ok;
};

public func addMaintainer(caller : Principal, packageName : PackageName, newMaintainer : Principal) : Result.Result<(), Text> {
let maintainers = Option.get(maintainersByPackage.get(packageName), []);

if (isMaintainer(packageName, newMaintainer)) {
return #err("User is already a maintainer");
};
if (not isOwner(packageName, caller)) {
return #err("Only owners can add maintainers");
};
if (maintainers.size() >= 5) {
return #err("Maximum number of maintainers reached");
};

maintainersByPackage.put(packageName, Array.append(maintainers, [newMaintainer]));
#ok;
};

public func removeOwner(caller : Principal, packageName : PackageName, ownerToRemove : Principal) : Result.Result<(), Text> {
let ?owners = ownersByPackage.get(packageName) else return #err("Package not found");

if (not isOwner(packageName, ownerToRemove)) {
return #err("User is not an owner");
};
if (not isOwner(packageName, caller)) {
return #err("Only owners can remove owners");
};
if (owners.size() <= 1) {
return #err("At least one owner is required");
};

ownersByPackage.put(packageName, Array.filter(owners, func(owner : Principal) : Bool {
owner != ownerToRemove;
}));
#ok;
};

public func removeMaintainer(caller : Principal, packageName : PackageName, maintainerToRemove : Principal) : Result.Result<(), Text> {
let maintainers = Option.get(maintainersByPackage.get(packageName), []);

if (oldOwner != caller) {
return #err("Only owner can transfer ownership");
if (not isMaintainer(packageName, maintainerToRemove)) {
return #err("User is not a maintainer");
};
if (newOwner == caller) {
return #err("You can't transfer ownership to yourself");
if (not isOwner(packageName, caller)) {
return #err("Only owners can remove maintainers");
};

packageOwners.put(packageName, newOwner);
maintainersByPackage.put(packageName, Array.filter(maintainers, func(maintainer : Principal) : Bool {
maintainer != maintainerToRemove;
}));
#ok;
};
};
Expand Down
Loading
Loading