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

vocdoni / saas-backend / 21707633339

04 Feb 2026 05:32PM UTC coverage: 63.493% (+0.8%) from 62.657%
21707633339

Pull #412

github

emmdim
fix(csp): remove cooldown time from auth-only token
Pull Request #412: chore: merge v.2.3 to stage

460 of 615 new or added lines in 23 files covered. (74.8%)

9 existing lines in 7 files now uncovered.

7148 of 11258 relevant lines covered (63.49%)

40.25 hits per line

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

57.97
/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
        OrganizationMemberGroup(groupID string, orgAddress common.Address) (*db.OrganizationMemberGroup, error)
58
}
59

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

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

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

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

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

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

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

102
        return true, nil
3✔
103
}
104

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

116
        // Check if the organization has a subscription
117
        if org.Subscription.PlanID == 0 {
8✔
118
                return false, errors.ErrOrganizationHasNoSubscription
1✔
119
        }
1✔
120

121
        plan, err := p.db.Plan(org.Subscription.PlanID)
6✔
122
        if err != nil {
6✔
NEW
123
                return false, errors.ErrPlanNotFound.WithErr(err)
×
124
        }
×
125

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

151
        case models.TxType_SET_PROCESS_STATUS,
152
                models.TxType_CREATE_ACCOUNT:
2✔
153
                // check if the user has the admin role for the organization
2✔
154
                if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
2✔
NEW
155
                        return false, errors.ErrUserHasNoAdminRole
×
156
                }
×
157
        default:
×
158
                return false, fmt.Errorf("unsupported txtype")
×
159
        }
160
        return true, nil
3✔
161
}
162

163
// HasDBPermission checks if the user has permission to perform the given action in the organization stored in the DB
164
func (p *Subscriptions) HasDBPermission(userEmail string, orgAddress common.Address, permission DBPermission) (bool, error) {
31✔
165
        user, err := p.db.UserByEmail(userEmail)
31✔
166
        if err != nil {
32✔
167
                return false, fmt.Errorf("could not get user: %v", err)
1✔
168
        }
1✔
169
        switch permission {
30✔
170
        case InviteUser, DeleteUser, CreateSubOrg:
30✔
171
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
34✔
172
                        return false, errors.ErrUserHasNoAdminRole
4✔
173
                }
4✔
174
                return true, nil
26✔
175
        default:
×
176
                return false, fmt.Errorf("permission not found")
×
177
        }
178
}
179

180
// OrgHasPermission checks if the org has permission to perform the given action
181
func (p *Subscriptions) OrgHasPermission(orgAddress common.Address, permission DBPermission) error {
8✔
182
        switch permission {
8✔
183
        case CreateDraft:
8✔
184
                // Check if the organization has a subscription
8✔
185
                org, err := p.db.Organization(orgAddress)
8✔
186
                if err != nil {
8✔
187
                        return errors.ErrOrganizationNotFound.WithErr(err)
×
188
                }
×
189

190
                if org.Subscription.PlanID == 0 {
8✔
NEW
191
                        return errors.ErrOrganizationHasNoSubscription.With("can't create draft process")
×
192
                }
×
193

194
                plan, err := p.db.Plan(org.Subscription.PlanID)
8✔
195
                if err != nil {
8✔
196
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
197
                }
×
198

199
                count, err := p.db.CountProcesses(orgAddress, db.DraftOnly)
8✔
200
                if err != nil {
8✔
201
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
202
                }
×
203

204
                if count >= int64(plan.Organization.MaxDrafts) {
10✔
205
                        return errors.ErrMaxDraftsReached.Withf("(%d)", plan.Organization.MaxDrafts)
2✔
206
                }
2✔
207
                return nil
6✔
208
        default:
×
209
                return fmt.Errorf("permission not found")
×
210
        }
211
}
212

213
func (p *Subscriptions) OrgCanPublishGroupCensus(census *db.Census, groupID string) error {
26✔
214
        org, err := p.db.Organization(census.OrgAddress)
26✔
215
        if err != nil {
26✔
NEW
216
                return errors.ErrOrganizationNotFound.WithErr(err)
×
NEW
217
        }
×
218

219
        if org.Subscription.PlanID == 0 {
26✔
NEW
220
                return errors.ErrOrganizationHasNoSubscription
×
NEW
221
        }
×
222

223
        plan, err := p.db.Plan(org.Subscription.PlanID)
26✔
224
        if err != nil {
26✔
NEW
225
                return errors.ErrPlanNotFound.WithErr(err)
×
NEW
226
        }
×
227

228
        group, err := p.db.OrganizationMemberGroup(groupID, org.Address)
26✔
229
        if err != nil {
26✔
NEW
230
                return errors.ErrGroupNotFound.WithErr(err)
×
NEW
231
        }
×
232

233
        remainingEmails := plan.Organization.MaxSentEmails - org.Counters.SentEmails
26✔
234
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldEmail) && len(group.MemberIDs) > remainingEmails {
27✔
235
                return errors.ErrProcessCensusSizeExceedsEmailAllowance.Withf("remaining emails: %d", remainingEmails)
1✔
236
        }
1✔
237
        remainingSMS := plan.Organization.MaxSentSMS - org.Counters.SentSMS
25✔
238
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldPhone) && len(group.MemberIDs) > remainingSMS {
28✔
239
                return errors.ErrProcessCensusSizeExceedsSMSAllowance.Withf("remaining sms: %d", remainingSMS)
3✔
240
        }
3✔
241

242
        return nil
22✔
243
}
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