Skip to content

Commit

Permalink
Implements subscription limitations (#40)
Browse files Browse the repository at this point in the history
* Implements subscription limitations
* Separate limitations as DB and TX limitations
* Adds handler for SET_ACCOUNT_INFO_URI transaction
* Related to vocdoni/interoperability#240
* Adds `any` user role to represent generic membership
* Modifies `HasRoleFor` to accept `any` role
  • Loading branch information
emmdim authored Jan 24, 2025
1 parent b3cfd06 commit 00fec15
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 24 deletions.
31 changes: 29 additions & 2 deletions api/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"github.com/vocdoni/saas-backend/subscriptions"
"go.vocdoni.io/dvote/log"
)

Expand Down Expand Up @@ -43,6 +44,13 @@ func (a *API) createOrganizationHandler(w http.ResponseWriter, r *http.Request)
parentOrg := ""
var dbParentOrg *db.Organization
if orgInfo.Parent != nil {
// check if the org has permission to create suborganizations
hasPermission, err := a.subscriptions.HasDBPersmission(user.Email, orgInfo.Parent.Address, subscriptions.CreateSubOrg)
if !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to create suborganizations: %v", err).Write(w)
return
}

dbParentOrg, _, err = a.db.Organization(orgInfo.Parent.Address, false)
if err != nil {
if err == db.ErrNotFound {
Expand Down Expand Up @@ -107,6 +115,15 @@ func (a *API) createOrganizationHandler(w http.ResponseWriter, r *http.Request)
ErrGenericInternalServerError.Write(w)
return
}

// update the parent organization counter
if orgInfo.Parent != nil {
dbParentOrg.Counters.SubOrgs++
if err := a.db.SetOrganization(dbParentOrg); err != nil {
ErrGenericInternalServerError.Withf("could not update parent organization: %v", err).Write(w)
return
}
}
// send the organization back to the user
httpWriteJSON(w, organizationFromDB(dbOrg, dbParentOrg))
}
Expand Down Expand Up @@ -260,8 +277,11 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
ErrNoOrganizationProvided.Write(w)
return
}
if !user.HasRoleFor(org.Address, db.AdminRole) {
ErrUnauthorized.Withf("user is not admin of organization").Write(w)

// check if the user/org has permission to invite members
hasPermission, err := a.subscriptions.HasDBPersmission(user.Email, org.Address, subscriptions.InviteMember)
if !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
// get new admin info from the request body
Expand Down Expand Up @@ -315,6 +335,13 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
ErrGenericInternalServerError.Write(w)
return
}

// update the org members counter
org.Counters.Members++
if err := a.db.SetOrganization(org); err != nil {
ErrGenericInternalServerError.Withf("could not update organization: %v", err).Write(w)
return
}
httpWriteOK(w)
}

Expand Down
52 changes: 46 additions & 6 deletions api/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrMalformedBody.Withf("could not decode request body: %v", err).Write(w)
return
}
// check if the user has the admin role for the organization
if !user.HasRoleFor(signReq.Address, db.AdminRole) {
ErrUnauthorized.With("user does not have admin role").Write(w)
// check if the user is a member of the organization
if !user.HasRoleFor(signReq.Address, db.AnyRole) {
ErrUnauthorized.With("user is not an organization member").Write(w)
return
}
// get the organization info from the database with the address provided in
Expand Down Expand Up @@ -67,6 +67,9 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrInvalidTxFormat.Write(w)
return
}
// flag to know if the TX is New Process
isNewProcess := false

// check if the api is not in transparent mode
if !a.transparentMode {
// get subscription plan
Expand All @@ -83,6 +86,10 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrUnauthorized.With("invalid account").Write(w)
return
}
if hasPermission, err := a.subscriptions.HasTxPermission(tx, txSetAccount.Txtype, org, user); !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
// check the tx subtype
switch txSetAccount.Txtype {
case models.TxType_CREATE_ACCOUNT:
Expand All @@ -107,6 +114,29 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
},
}
}
case models.TxType_SET_ACCOUNT_INFO_URI:
// generate a new faucet package if it's not present and include it in the tx
if txSetAccount.FaucetPackage == nil {
// get the tx cost for the tx type
amount, ok := a.account.TxCosts[models.TxType_SET_ACCOUNT_INFO_URI]
if !ok {
panic("invalid tx type")
}
// generate the faucet package with the calculated amount
faucetPkg, err := a.account.FaucetPackage(organizationSigner.AddressString(), amount)
if err != nil {
ErrCouldNotCreateFaucetPackage.WithErr(err).Write(w)
return
}
// include the faucet package in the tx
txSetAccount.FaucetPackage = faucetPkg
tx = &models.Tx{
Payload: &models.Tx_SetAccount{
SetAccount: txSetAccount,
},
}
}

}
case *models.Tx_NewProcess:
txNewProcess := tx.GetNewProcess()
Expand All @@ -116,14 +146,14 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrInvalidTxFormat.With("missing fields").Write(w)
return
}
if hasPermission, err := a.subscriptions.HasPermission(tx, txNewProcess.Txtype, org); !hasPermission || err != nil {
if hasPermission, err := a.subscriptions.HasTxPermission(tx, txNewProcess.Txtype, org, user); !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
// check the tx subtype
switch txNewProcess.Txtype {
case models.TxType_NEW_PROCESS:

isNewProcess = true
// generate a new faucet package if it's not present and include it in the tx
if txNewProcess.FaucetPackage == nil {
// get the tx cost for the tx type
Expand Down Expand Up @@ -169,7 +199,7 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrInvalidTxFormat.With("invalid tx type").Write(w)
return
}
if hasPermission, err := a.subscriptions.HasPermission(tx, txSetProcess.Txtype, org); !hasPermission || err != nil {
if hasPermission, err := a.subscriptions.HasTxPermission(tx, txSetProcess.Txtype, org, user); !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
Expand Down Expand Up @@ -315,6 +345,16 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrCouldNotSignTransaction.WithErr(err).Write(w)
return
}

// If isNewProcess and everything went well so far update the organization process counter
if isNewProcess {
org.Counters.Processes++
if err := a.db.SetOrganization(org); err != nil {
ErrGenericInternalServerError.Withf("could not update organization process counter: %v", err).Write(w)
return
}
}

// return the signed tx payload
httpWriteJSON(w, &TransactionData{
TxPayload: base64.StdEncoding.EncodeToString(stx),
Expand Down
4 changes: 4 additions & 0 deletions db/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const (
AdminRole UserRole = "admin"
ManagerRole UserRole = "manager"
ViewerRole UserRole = "viewer"
AnyRole UserRole = "any"
// organization types
AssemblyType OrganizationType = "assembly"
AssociationType OrganizationType = "association"
Expand All @@ -31,13 +32,15 @@ var writableRoles = map[UserRole]bool{
AdminRole: true,
ManagerRole: true,
ViewerRole: false,
AnyRole: false,
}

// UserRoleNames is a map that contains the user role names by role
var UserRolesNames = map[UserRole]string{
AdminRole: "Admin",
ManagerRole: "Manager",
ViewerRole: "Viewer",
AnyRole: "Any",
}

// HasWriteAccess function checks if the user role has write access
Expand Down Expand Up @@ -81,6 +84,7 @@ var validRoles = map[UserRole]bool{
AdminRole: true,
ManagerRole: true,
ViewerRole: true,
AnyRole: false,
}

// IsValidUserRole function checks if the user role is valid
Expand Down
21 changes: 12 additions & 9 deletions db/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ type UserVerification struct {

func (u *User) HasRoleFor(address string, role UserRole) bool {
for _, org := range u.Organizations {
if org.Address == address && string(org.Role) == string(role) {
if org.Address == address &&
// Check if the role is "any: or if the role matches the organization role
(string(role) == string(AnyRole) || string(org.Role) == string(role)) {
return true
}
}
Expand Down Expand Up @@ -63,14 +65,14 @@ type Organization struct {
}

type PlanLimits struct {
Members int `json:"members" bson:"members"`
SubOrgs int `json:"subOrgs" bson:"subOrgs"`
CensusSize int `json:"censusSize" bson:"censusSize"`
MaxProcesses int `json:"maxProcesses" bson:"maxProcesses"`
MaxCensus int `json:"maxCensus" bson:"maxCensus"`
MaxDuration string `json:"maxDuration" bson:"maxDuration"`
CustomURL bool `json:"customURL" bson:"customURL"`
Drafts int `json:"drafts" bson:"drafts"`
Members int `json:"members" bson:"members"`
SubOrgs int `json:"subOrgs" bson:"subOrgs"`
MaxProcesses int `json:"maxProcesses" bson:"maxProcesses"`
MaxCensus int `json:"maxCensus" bson:"maxCensus"`
// Max process duration in days
MaxDuration string `json:"maxDuration" bson:"maxDuration"`
CustomURL bool `json:"customURL" bson:"customURL"`
Drafts int `json:"drafts" bson:"drafts"`
}

type VotingTypes struct {
Expand Down Expand Up @@ -126,6 +128,7 @@ type OrganizationCounters struct {
SentEmails int `json:"sentEmails" bson:"sentEmails"`
SubOrgs int `json:"subOrgs" bson:"subOrgs"`
Members int `json:"members" bson:"members"`
Processes int `json:"processes" bson:"processes"`
}

type OrganizationInvite struct {
Expand Down
Loading

0 comments on commit 00fec15

Please sign in to comment.