• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vocdoni / saas-backend / 19272512635

11 Nov 2025 04:46PM UTC coverage: 60.484% (+0.03%) from 60.453%
19272512635

push

github

emmdim
api: limit creation of process drafts

* extend TestDraftProcess to check limits enforcement

* cleanup leftovers introduced in "api: support creating a draft process"

* new subscriptions.OrgHasPermission

36 of 53 new or added lines in 3 files covered. (67.92%)

4 existing lines in 2 files now uncovered.

6329 of 10464 relevant lines covered (60.48%)

37.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.25
/subscriptions/subscriptions.go
1
// Package subscriptions provides functionality for managing organization subscriptions
2
// and enforcing permissions based on subscription plans.
3
package subscriptions
4

5
import (
6
        "fmt"
7

8
        "github.com/ethereum/go-ethereum/common"
9
        "github.com/vocdoni/saas-backend/db"
10
        "github.com/vocdoni/saas-backend/errors"
11
        "go.vocdoni.io/proto/build/go/models"
12
)
13

14
// Config holds the configuration for the subscriptions service.
15
// It includes a reference to the MongoDB storage used by the service.
16
type Config struct {
17
        DB *db.MongoStorage
18
}
19

20
// DBPermission represents the permissions that an organization can have based on its subscription.
21
type DBPermission int
22

23
const (
24
        // InviteUser represents the permission to invite new users to an organization.
25
        InviteUser DBPermission = iota
26
        // DeleteUser represents the permission to remove users from an organization.
27
        DeleteUser
28
        // CreateSubOrg represents the permission to create sub-organizations.
29
        CreateSubOrg
30
        // CreateDraft represents the permission to create draft processes.
31
        CreateDraft
32
)
33

34
// String returns the string representation of the DBPermission.
35
func (p DBPermission) String() string {
×
36
        switch p {
×
37
        case InviteUser:
×
38
                return "InviteUser"
×
39
        case DeleteUser:
×
40
                return "DeleteUser"
×
41
        case CreateSubOrg:
×
42
                return "CreateSubOrg"
×
43
        case CreateDraft:
×
44
                return "CreateDraft"
×
45
        default:
×
46
                return "Unknown"
×
47
        }
48
}
49

50
// DBInterface defines the database methods required by the Subscriptions service
51
type DBInterface interface {
52
        Plan(id uint64) (*db.Plan, error)
53
        UserByEmail(email string) (*db.User, error)
54
        Organization(address common.Address) (*db.Organization, error)
55
        OrganizationWithParent(address common.Address) (*db.Organization, *db.Organization, error)
56
        CountProcesses(orgAddress common.Address, draft db.DraftFilter) (int64, error)
57
}
58

59
// Subscriptions is the service that manages the organization permissions based on
60
// the subscription plans.
61
type Subscriptions struct {
62
        db DBInterface
63
}
64

65
// New creates a new Subscriptions service with the given configuration.
66
func New(conf *Config) *Subscriptions {
1✔
67
        if conf == nil {
1✔
68
                return nil
×
69
        }
×
70
        return &Subscriptions{
1✔
71
                db: conf.DB,
1✔
72
        }
1✔
73
}
74

75
// hasElectionMetadataPermissions checks if the organization has permission to create an election with the given metadata.
76
func hasElectionMetadataPermissions(process *models.NewProcessTx, plan *db.Plan) (bool, error) {
3✔
77
        // check ANONYMOUS
3✔
78
        if process.Process.EnvelopeType.Anonymous && !plan.Features.Anonymous {
3✔
79
                return false, fmt.Errorf("anonymous elections are not allowed")
×
80
        }
×
81

82
        // check WEIGHTED
83
        if process.Process.EnvelopeType.CostFromWeight && !plan.VotingTypes.Weighted {
3✔
84
                return false, fmt.Errorf("weighted elections are not allowed")
×
85
        }
×
86

87
        // check VOTE OVERWRITE
88
        if process.Process.VoteOptions.MaxVoteOverwrites > 0 && !plan.Features.Overwrite {
3✔
89
                return false, fmt.Errorf("vote overwrites are not allowed")
×
90
        }
×
91

92
        // check PROCESS DURATION
93
        duration := plan.Organization.MaxDuration * 24 * 60 * 60
3✔
94
        if process.Process.Duration > uint32(duration) {
3✔
95
                return false, fmt.Errorf("duration is greater than the allowed")
×
96
        }
×
97

98
        // TODO:future check if the election voting type is supported by the plan
99
        // TODO:future check if the streamURL is used and allowed by the plan
100

101
        return true, nil
3✔
102
}
103

104
// HasTxPermission checks if the organization has permission to perform the given transaction.
105
func (p *Subscriptions) HasTxPermission(
106
        tx *models.Tx,
107
        txType models.TxType,
108
        org *db.Organization,
109
        user *db.User,
110
) (bool, error) {
8✔
111
        if org == nil {
9✔
112
                return false, fmt.Errorf("organization is nil")
1✔
113
        }
1✔
114

115
        // Check if the organization has a subscription
116
        if org.Subscription.PlanID == 0 {
8✔
117
                return false, fmt.Errorf("organization has no subscription plan")
1✔
118
        }
1✔
119

120
        plan, err := p.db.Plan(org.Subscription.PlanID)
6✔
121
        if err != nil {
6✔
122
                return false, fmt.Errorf("could not get organization plan: %v", err)
×
123
        }
×
124

125
        switch txType {
6✔
126
        // check UPDATE ACCOUNT INFO
127
        case models.TxType_SET_ACCOUNT_INFO_URI:
1✔
128
                // check if the user has the admin role for the organization
1✔
129
                if !user.HasRoleFor(org.Address, db.AdminRole) {
1✔
130
                        return false, fmt.Errorf("user does not have admin role")
×
131
                }
×
132
        // check CREATE PROCESS
133
        case models.TxType_NEW_PROCESS, models.TxType_SET_PROCESS_CENSUS:
3✔
134
                // check if the user has the admin role for the organization
3✔
135
                if !user.HasRoleFor(org.Address, db.AdminRole) {
3✔
136
                        return false, fmt.Errorf("user does not have admin role")
×
137
                }
×
138
                newProcess := tx.GetNewProcess()
3✔
139
                if newProcess.Process.MaxCensusSize > uint64(plan.Organization.MaxCensus) {
3✔
140
                        return false, fmt.Errorf("MaxCensusSize is greater than the allowed")
×
141
                }
×
142
                if org.Counters.Processes >= plan.Organization.MaxProcesses {
3✔
143
                        // allow processes with less than TestMaxCensusSize for user testing
×
144
                        if newProcess.Process.MaxCensusSize > uint64(db.TestMaxCensusSize) {
×
145
                                return false, fmt.Errorf("max processes reached")
×
146
                        }
×
147
                }
148
                return hasElectionMetadataPermissions(newProcess, plan)
3✔
149

150
        // check SET_PROCESS
151
        case models.TxType_SET_PROCESS_STATUS:
×
152
                // check if the user has the admin role for the organization
×
153
                if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
×
154
                        return false, fmt.Errorf("user does not have admin role")
×
155
                }
×
156
        // check CREATE_ACCOUNT
157
        case models.TxType_CREATE_ACCOUNT:
2✔
158
                // check if the user has the admin role for the organization
2✔
159
                if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
2✔
160
                        return false, fmt.Errorf("user does not have admin role")
×
161
                }
×
162
        default:
×
163
                return false, fmt.Errorf("unsupported txtype")
×
164
        }
165
        return true, nil
3✔
166
}
167

168
// HasDBPermission checks if the user has permission to perform the given action in the organization stored in the DB
169
func (p *Subscriptions) HasDBPermission(userEmail string, orgAddress common.Address, permission DBPermission) (bool, error) {
28✔
170
        user, err := p.db.UserByEmail(userEmail)
28✔
171
        if err != nil {
29✔
172
                return false, fmt.Errorf("could not get user: %v", err)
1✔
173
        }
1✔
174
        switch permission {
27✔
175
        case InviteUser:
24✔
176
                // check if the user has permission to invite users
24✔
177
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
25✔
178
                        return false, fmt.Errorf("user does not have admin role")
1✔
179
                }
1✔
180
                return true, nil
23✔
181
        case DeleteUser:
1✔
182
                // check if the user has permission to delete users
1✔
183
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
2✔
184
                        return false, fmt.Errorf("user does not have admin role")
1✔
185
                }
1✔
186
                return true, nil
×
187
        case CreateSubOrg:
2✔
188
                // check if the user has permission to create sub organizations
2✔
189
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
3✔
190
                        return false, fmt.Errorf("user does not have admin role")
1✔
191
                }
1✔
192
                return true, nil
1✔
193
        default:
×
194
                return false, fmt.Errorf("permission not found")
×
195
        }
196
}
197

198
// OrgHasPermission checks if the org has permission to perform the given action
199
func (p *Subscriptions) OrgHasPermission(orgAddress common.Address, permission DBPermission) error {
8✔
200
        switch permission {
8✔
201
        case CreateDraft:
8✔
202
                // Check if the organization has a subscription
8✔
203
                org, err := p.db.Organization(orgAddress)
8✔
204
                if err != nil {
8✔
NEW
205
                        return errors.ErrOrganizationNotFound.WithErr(err)
×
NEW
206
                }
×
207

208
                if org.Subscription.PlanID == 0 {
8✔
NEW
209
                        return errors.ErrNoOrganizationSubscription.With("can't create draft process")
×
NEW
210
                }
×
211

212
                plan, err := p.db.Plan(org.Subscription.PlanID)
8✔
213
                if err != nil {
8✔
NEW
214
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
NEW
215
                }
×
216

217
                count, err := p.db.CountProcesses(orgAddress, db.DraftOnly)
8✔
218
                if err != nil {
8✔
NEW
219
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
NEW
220
                }
×
221

222
                if count >= int64(plan.Organization.MaxDrafts) {
10✔
223
                        return errors.ErrMaxDraftsReached.Withf("(%d)", plan.Organization.MaxDrafts)
2✔
224
                }
2✔
225
                return nil
6✔
NEW
226
        default:
×
NEW
227
                return fmt.Errorf("permission not found")
×
228
        }
229
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc