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

vocdoni / saas-backend / 21749540419

06 Feb 2026 11:50AM UTC coverage: 63.45% (-0.04%) from 63.493%
21749540419

push

github

emmdim
fix: limits user max organizations to 15

6 of 17 new or added lines in 2 files covered. (35.29%)

28 existing lines in 2 files now uncovered.

7154 of 11275 relevant lines covered (63.45%)

40.29 hits per line

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

56.0
/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
        // CreateOrg represents the permission to create new organizations.
33
        CreateOrg
34
)
35

36
const MaxOrgsPerUser = 15
37

38
// String returns the string representation of the DBPermission.
39
func (p DBPermission) String() string {
×
40
        switch p {
×
41
        case InviteUser:
×
42
                return "InviteUser"
×
43
        case DeleteUser:
×
44
                return "DeleteUser"
×
45
        case CreateSubOrg:
×
46
                return "CreateSubOrg"
×
47
        case CreateDraft:
×
48
                return "CreateDraft"
×
NEW
49
        case CreateOrg:
×
NEW
50
                return "CreateOrg"
×
51
        default:
×
52
                return "Unknown"
×
53
        }
54
}
55

56
// DBInterface defines the database methods required by the Subscriptions service
57
type DBInterface interface {
58
        Plan(id uint64) (*db.Plan, error)
59
        UserByEmail(email string) (*db.User, error)
60
        Organization(address common.Address) (*db.Organization, error)
61
        OrganizationWithParent(address common.Address) (*db.Organization, *db.Organization, error)
62
        CountProcesses(orgAddress common.Address, draft db.DraftFilter) (int64, error)
63
        OrganizationMemberGroup(groupID string, orgAddress common.Address) (*db.OrganizationMemberGroup, error)
64
}
65

66
// Subscriptions is the service that manages the organization permissions based on
67
// the subscription plans.
68
type Subscriptions struct {
69
        db DBInterface
70
}
71

72
// New creates a new Subscriptions service with the given configuration.
73
func New(conf *Config) *Subscriptions {
1✔
74
        if conf == nil {
1✔
75
                return nil
×
76
        }
×
77
        return &Subscriptions{
1✔
78
                db: conf.DB,
1✔
79
        }
1✔
80
}
81

82
// hasElectionMetadataPermissions checks if the organization has permission to create an election with the given metadata.
83
func hasElectionMetadataPermissions(process *models.NewProcessTx, plan *db.Plan) (bool, error) {
3✔
84
        // check ANONYMOUS
3✔
85
        if process.Process.EnvelopeType.Anonymous && !plan.Features.Anonymous {
3✔
86
                return false, fmt.Errorf("anonymous elections are not allowed")
×
87
        }
×
88

89
        // check WEIGHTED
90
        if process.Process.EnvelopeType.CostFromWeight && !plan.VotingTypes.Weighted {
3✔
91
                return false, fmt.Errorf("weighted elections are not allowed")
×
92
        }
×
93

94
        // check VOTE OVERWRITE
95
        if process.Process.VoteOptions.MaxVoteOverwrites > 0 && !plan.Features.Overwrite {
3✔
96
                return false, fmt.Errorf("vote overwrites are not allowed")
×
97
        }
×
98

99
        // check PROCESS DURATION
100
        duration := plan.Organization.MaxDuration * 24 * 60 * 60
3✔
101
        if process.Process.Duration > uint32(duration) {
3✔
102
                return false, fmt.Errorf("duration is greater than the allowed")
×
103
        }
×
104

105
        // TODO:future check if the election voting type is supported by the plan
106
        // TODO:future check if the streamURL is used and allowed by the plan
107

108
        return true, nil
3✔
109
}
110

111
// HasTxPermission checks if the organization has permission to perform the given transaction.
112
func (p *Subscriptions) HasTxPermission(
113
        tx *models.Tx,
114
        txType models.TxType,
115
        org *db.Organization,
116
        user *db.User,
117
) (bool, error) {
8✔
118
        if org == nil {
9✔
119
                return false, errors.ErrInvalidData.With("organization is nil")
1✔
120
        }
1✔
121

122
        // Check if the organization has a subscription
123
        if org.Subscription.PlanID == 0 {
8✔
124
                return false, errors.ErrOrganizationHasNoSubscription
1✔
125
        }
1✔
126

127
        plan, err := p.db.Plan(org.Subscription.PlanID)
6✔
128
        if err != nil {
6✔
129
                return false, errors.ErrPlanNotFound.WithErr(err)
×
130
        }
×
131

132
        switch txType {
6✔
133
        // check UPDATE ACCOUNT INFO
134
        case models.TxType_SET_ACCOUNT_INFO_URI:
1✔
135
                // check if the user has the admin role for the organization
1✔
136
                if !user.HasRoleFor(org.Address, db.AdminRole) {
1✔
137
                        return false, errors.ErrUserHasNoAdminRole
×
138
                }
×
139
        // check CREATE PROCESS
140
        case models.TxType_NEW_PROCESS, models.TxType_SET_PROCESS_CENSUS:
3✔
141
                // check if the user has the admin role for the organization
3✔
142
                if !user.HasRoleFor(org.Address, db.AdminRole) {
3✔
143
                        return false, errors.ErrUserHasNoAdminRole
×
144
                }
×
145
                newProcess := tx.GetNewProcess()
3✔
146
                if newProcess.Process.MaxCensusSize > uint64(plan.Organization.MaxCensus) {
3✔
147
                        return false, errors.ErrProcessCensusSizeExceedsPlanLimit.Withf("plan max census: %d", plan.Organization.MaxCensus)
×
148
                }
×
149
                if org.Counters.Processes >= plan.Organization.MaxProcesses {
3✔
150
                        // allow processes with less than TestMaxCensusSize for user testing
×
151
                        if newProcess.Process.MaxCensusSize > uint64(db.TestMaxCensusSize) {
×
152
                                return false, errors.ErrMaxProcessesReached
×
153
                        }
×
154
                }
155
                return hasElectionMetadataPermissions(newProcess, plan)
3✔
156

157
        case models.TxType_SET_PROCESS_STATUS,
158
                models.TxType_CREATE_ACCOUNT:
2✔
159
                // check if the user has the admin role for the organization
2✔
160
                if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
2✔
161
                        return false, errors.ErrUserHasNoAdminRole
×
162
                }
×
163
        default:
×
164
                return false, fmt.Errorf("unsupported txtype")
×
165
        }
166
        return true, nil
3✔
167
}
168

169
// HasDBPermission checks if the user has permission to perform the given action in the organization stored in the DB
170
func (p *Subscriptions) HasDBPermission(userEmail string, orgAddress common.Address, permission DBPermission) (bool, error) {
87✔
171
        user, err := p.db.UserByEmail(userEmail)
87✔
172
        if err != nil {
88✔
173
                return false, fmt.Errorf("could not get user: %v", err)
1✔
174
        }
1✔
175
        switch permission {
86✔
176
        case InviteUser, DeleteUser, CreateSubOrg:
30✔
177
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
34✔
178
                        return false, errors.ErrUserHasNoAdminRole
4✔
179
                }
4✔
180
                return true, nil
26✔
181
        case CreateOrg:
56✔
182
                // Check if the user can create more organizations based on the MaxOrgsPerUser limit
56✔
183
                if len(user.Organizations) >= MaxOrgsPerUser {
56✔
NEW
184
                        return false, errors.ErrMaxOrganizationsReached.Withf(
×
NEW
185
                                "user is part of %d organizations, max allowed is %d",
×
NEW
186
                                len(user.Organizations),
×
NEW
187
                                MaxOrgsPerUser,
×
NEW
188
                        )
×
NEW
189
                }
×
190
                return true, nil
56✔
191
        default:
×
192
                return false, fmt.Errorf("permission not found")
×
193
        }
194
}
195

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

206
                if org.Subscription.PlanID == 0 {
8✔
207
                        return errors.ErrOrganizationHasNoSubscription.With("can't create draft process")
×
208
                }
×
209

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

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

220
                if count >= int64(plan.Organization.MaxDrafts) {
10✔
221
                        return errors.ErrMaxDraftsReached.Withf("(%d)", plan.Organization.MaxDrafts)
2✔
222
                }
2✔
223
                return nil
6✔
224
        default:
×
225
                return fmt.Errorf("permission not found")
×
226
        }
227
}
228

229
func (p *Subscriptions) OrgCanPublishGroupCensus(census *db.Census, groupID string) error {
26✔
230
        org, err := p.db.Organization(census.OrgAddress)
26✔
231
        if err != nil {
26✔
232
                return errors.ErrOrganizationNotFound.WithErr(err)
×
233
        }
×
234

235
        if org.Subscription.PlanID == 0 {
26✔
236
                return errors.ErrOrganizationHasNoSubscription
×
237
        }
×
238

239
        plan, err := p.db.Plan(org.Subscription.PlanID)
26✔
240
        if err != nil {
26✔
241
                return errors.ErrPlanNotFound.WithErr(err)
×
242
        }
×
243

244
        group, err := p.db.OrganizationMemberGroup(groupID, org.Address)
26✔
245
        if err != nil {
26✔
246
                return errors.ErrGroupNotFound.WithErr(err)
×
247
        }
×
248

249
        remainingEmails := plan.Features.TwoFaEmail - org.Counters.SentEmails
26✔
250
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldEmail) && len(group.MemberIDs) > remainingEmails {
27✔
251
                return errors.ErrProcessCensusSizeExceedsEmailAllowance.Withf("remaining emails: %d", remainingEmails)
1✔
252
        }
1✔
253
        remainingSMS := plan.Features.TwoFaSms - org.Counters.SentSMS
25✔
254
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldPhone) && len(group.MemberIDs) > remainingSMS {
28✔
255
                return errors.ErrProcessCensusSizeExceedsSMSAllowance.Withf("remaining sms: %d", remainingSMS)
3✔
256
        }
3✔
257

258
        return nil
22✔
259
}
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