Implement list visibility
Also consolidate list permissions columns into one default_access column.
This commit is contained in:
parent
2ae65a9e81
commit
02395e0471
|
@ -24,25 +24,23 @@ const (
|
|||
)
|
||||
|
||||
type MailingList struct {
|
||||
ID int `json:"id"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Importing bool `json:"importing"`
|
||||
ID int `json:"id"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Importing bool `json:"importing"`
|
||||
|
||||
OwnerID int
|
||||
RawPermitMime string
|
||||
RawRejectMime string
|
||||
|
||||
Permissions int
|
||||
Access int
|
||||
DefaultAccess uint
|
||||
AccessID *int
|
||||
SubscriptionID *int
|
||||
|
||||
RawNonsubscriber uint
|
||||
RawSubscriber uint
|
||||
RawIdentified uint
|
||||
|
||||
alias string
|
||||
fields *database.ModelFields
|
||||
}
|
||||
|
@ -61,30 +59,12 @@ func (list *MailingList) RejectMime() []string {
|
|||
return strings.Split(list.RawRejectMime, ",")
|
||||
}
|
||||
|
||||
func (list *MailingList) Nonsubscriber() *GeneralACL {
|
||||
func (list *MailingList) DefaultACL() *GeneralACL {
|
||||
return &GeneralACL{
|
||||
Browse: list.RawNonsubscriber&ACCESS_BROWSE > 0,
|
||||
Reply: list.RawNonsubscriber&ACCESS_REPLY > 0,
|
||||
Post: list.RawNonsubscriber&ACCESS_POST > 0,
|
||||
Moderate: list.RawNonsubscriber&ACCESS_MODERATE > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (list *MailingList) Subscriber() *GeneralACL {
|
||||
return &GeneralACL{
|
||||
Browse: list.RawSubscriber&ACCESS_BROWSE > 0,
|
||||
Reply: list.RawSubscriber&ACCESS_REPLY > 0,
|
||||
Post: list.RawSubscriber&ACCESS_POST > 0,
|
||||
Moderate: list.RawSubscriber&ACCESS_MODERATE > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (list *MailingList) Identified() *GeneralACL {
|
||||
return &GeneralACL{
|
||||
Browse: list.RawIdentified&ACCESS_BROWSE > 0,
|
||||
Reply: list.RawIdentified&ACCESS_REPLY > 0,
|
||||
Post: list.RawIdentified&ACCESS_POST > 0,
|
||||
Moderate: list.RawIdentified&ACCESS_MODERATE > 0,
|
||||
Browse: list.DefaultAccess&ACCESS_BROWSE > 0,
|
||||
Reply: list.DefaultAccess&ACCESS_REPLY > 0,
|
||||
Post: list.DefaultAccess&ACCESS_POST > 0,
|
||||
Moderate: list.DefaultAccess&ACCESS_MODERATE > 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,11 +92,10 @@ func (list *MailingList) Fields() *database.ModelFields {
|
|||
{"name", "name", &list.Name},
|
||||
{"description", "description", &list.Description},
|
||||
{"import_in_progress", "importing", &list.Importing},
|
||||
{"permit_mimetypes", "permit_mime", &list.RawPermitMime},
|
||||
{"reject_mimetypes", "reject_mime", &list.RawRejectMime},
|
||||
{"nonsubscriber_permissions", "nonsubscriber", &list.RawNonsubscriber},
|
||||
{"subscriber_permissions", "subscriber", &list.RawSubscriber},
|
||||
{"account_permissions", "identified", &list.RawIdentified},
|
||||
{"permit_mimetypes", "permitMime", &list.RawPermitMime},
|
||||
{"reject_mimetypes", "rejectMime", &list.RawRejectMime},
|
||||
{"visibility", "visibility", &list.Visibility},
|
||||
{"default_access", "defaultACL", &list.DefaultAccess},
|
||||
|
||||
// Always fetch:
|
||||
{"id", "", &list.ID},
|
||||
|
@ -151,12 +130,9 @@ func (list *MailingList) QueryWithCursor(ctx context.Context,
|
|||
Column(`COALESCE(
|
||||
access.permissions,
|
||||
CASE WHEN list.owner_id = ?
|
||||
THEN ?
|
||||
ELSE CASE WHEN sub.id IS NOT NULL
|
||||
THEN list.subscriber_permissions
|
||||
ELSE null END
|
||||
END,
|
||||
list.nonsubscriber_permissions | list.account_permissions)`,
|
||||
THEN ?
|
||||
ELSE list.default_access
|
||||
END)`,
|
||||
user.UserID, ACCESS_ALL).
|
||||
Column(`access.id`).
|
||||
Column(`sub.id`).
|
||||
|
@ -172,7 +148,7 @@ func (list *MailingList) QueryWithCursor(ctx context.Context,
|
|||
for rows.Next() {
|
||||
var list MailingList
|
||||
if err := rows.Scan(append(database.Scan(ctx, &list),
|
||||
&list.Permissions,
|
||||
&list.Access,
|
||||
&list.AccessID,
|
||||
&list.SubscriptionID)...); err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -92,6 +92,12 @@ type Mailbox implements Entity {
|
|||
address: String!
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
PUBLIC
|
||||
UNLISTED
|
||||
PRIVATE
|
||||
}
|
||||
|
||||
type MailingList {
|
||||
id: Int!
|
||||
created: Time!
|
||||
|
@ -101,6 +107,7 @@ type MailingList {
|
|||
|
||||
# Markdown
|
||||
description: String
|
||||
visibility: Visibility!
|
||||
|
||||
"""
|
||||
List of globs for permitted or rejected mimetypes on this list
|
||||
|
@ -134,12 +141,8 @@ type MailingList {
|
|||
|
||||
"Access control list entries for this mailing list"
|
||||
acl(cursor: Cursor): MailingListACLCursor! @access(scope: ACLS, kind: RO)
|
||||
"Permissions which apply to any non-subscriber"
|
||||
nonsubscriber: GeneralACL!
|
||||
"Permissions which apply to any subscriber"
|
||||
subscriber: GeneralACL!
|
||||
"Permissions which apply to any authenticated account holder"
|
||||
identified: GeneralACL!
|
||||
|
||||
defaultACL: GeneralACL!
|
||||
|
||||
"""
|
||||
Returns a list of mailing list webhook subscriptions. For clients
|
||||
|
@ -644,6 +647,7 @@ type Query {
|
|||
# TODO: Allow users to change the name of a mailing list
|
||||
input MailingListInput {
|
||||
description: String
|
||||
visibility: Visibility
|
||||
|
||||
"""
|
||||
List of globs for permitted or rejected mimetypes on this list
|
||||
|
@ -677,7 +681,8 @@ type Mutation {
|
|||
"Creates a new mailing list"
|
||||
createMailingList(
|
||||
name: String!,
|
||||
description: String): MailingList! @access(scope: LISTS, kind: RW)
|
||||
description: String,
|
||||
visibility: Visibility!): MailingList! @access(scope: LISTS, kind: RW)
|
||||
|
||||
"Updates a mailing list."
|
||||
updateMailingList(
|
||||
|
|
|
@ -208,7 +208,7 @@ func (r *mailingListResolver) Access(ctx context.Context, obj *model.MailingList
|
|||
if obj.AccessID != nil {
|
||||
return loaders.ForContext(ctx).ACLsByID.Load(*obj.AccessID)
|
||||
}
|
||||
p := obj.Permissions
|
||||
p := obj.Access
|
||||
return &model.GeneralACL{
|
||||
Browse: p&model.ACCESS_BROWSE != 0,
|
||||
Reply: p&model.ACCESS_REPLY != 0,
|
||||
|
@ -511,7 +511,7 @@ func (r *mailingListWebhookSubscriptionResolver) List(ctx context.Context, obj *
|
|||
return loaders.ForContext(ctx).MailingListsByID.Load(obj.ListID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateMailingList(ctx context.Context, name string, description *string) (*model.MailingList, error) {
|
||||
func (r *mutationResolver) CreateMailingList(ctx context.Context, name string, description *string, visibility model.Visibility) (*model.MailingList, error) {
|
||||
valid := valid.New(ctx)
|
||||
valid.Expect(listNameRE.MatchString(name), "Name must match %s", listNameRE.String()).
|
||||
WithField("name").
|
||||
|
@ -529,21 +529,20 @@ func (r *mutationResolver) CreateMailingList(ctx context.Context, name string, d
|
|||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO list (
|
||||
created, updated, name, description, owner_id
|
||||
created, updated, name, description, visibility, owner_id
|
||||
) VALUES (
|
||||
NOW() at time zone 'utc',
|
||||
NOW() at time zone 'utc',
|
||||
$1, $2, $3
|
||||
$1, $2, $3, $4
|
||||
) RETURNING
|
||||
id, created, updated, name, description, owner_id,
|
||||
permit_mimetypes, reject_mimetypes,
|
||||
nonsubscriber_permissions, subscriber_permissions, account_permissions;
|
||||
`, name, description, auth.ForContext(ctx).UserID)
|
||||
id, created, updated, name, description, visibility, owner_id,
|
||||
permit_mimetypes, reject_mimetypes, default_access;
|
||||
`, name, description, visibility.String(), auth.ForContext(ctx).UserID)
|
||||
|
||||
if err := row.Scan(&list.ID, &list.Created, &list.Updated, &list.Name,
|
||||
&list.Description, &list.OwnerID,
|
||||
&list.Description, &list.Visibility, &list.OwnerID,
|
||||
&list.RawPermitMime, &list.RawRejectMime,
|
||||
&list.RawNonsubscriber, &list.RawSubscriber, &list.RawIdentified); err != nil {
|
||||
&list.DefaultAccess); err != nil {
|
||||
if err, ok := err.(*pq.Error); ok &&
|
||||
err.Code == "23505" && // unique_violation
|
||||
err.Constraint == "uq_list_owner_id_name" {
|
||||
|
@ -551,7 +550,7 @@ func (r *mutationResolver) CreateMailingList(ctx context.Context, name string, d
|
|||
}
|
||||
return err
|
||||
}
|
||||
list.Permissions = model.ACCESS_ALL
|
||||
list.Access = model.ACCESS_ALL
|
||||
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO subscription (
|
||||
|
@ -583,6 +582,9 @@ func (r *mutationResolver) UpdateMailingList(ctx context.Context, id int, input
|
|||
WithField("description")
|
||||
query = query.Set("description", desc)
|
||||
})
|
||||
valid.OptionalString("visibility", func(visibility string) {
|
||||
query = query.Set("visibility", visibility)
|
||||
})
|
||||
mime := func(name string) {
|
||||
valid.Optional(name+"Mime", func(object interface{}) {
|
||||
list, ok := object.([]interface{})
|
||||
|
@ -613,19 +615,18 @@ func (r *mutationResolver) UpdateMailingList(ctx context.Context, id int, input
|
|||
Where(`list.id = ? AND list.owner_id = ?`,
|
||||
id, auth.ForContext(ctx).UserID).
|
||||
Suffix(`RETURNING
|
||||
id, created, updated, name, description, owner_id,
|
||||
permit_mimetypes, reject_mimetypes,
|
||||
nonsubscriber_permissions, subscriber_permissions, account_permissions`).
|
||||
id, created, updated, name, description, visibility, owner_id,
|
||||
permit_mimetypes, reject_mimetypes, default_access`).
|
||||
RunWith(tx).
|
||||
QueryRowContext(ctx)
|
||||
|
||||
if err := row.Scan(&list.ID, &list.Created, &list.Updated, &list.Name,
|
||||
&list.Description, &list.OwnerID,
|
||||
&list.Description, &list.Visibility, &list.OwnerID,
|
||||
&list.RawPermitMime, &list.RawRejectMime,
|
||||
&list.RawNonsubscriber, &list.RawSubscriber, &list.RawIdentified); err != nil {
|
||||
&list.DefaultAccess); err != nil {
|
||||
return err
|
||||
}
|
||||
list.Permissions = model.ACCESS_ALL
|
||||
list.Access = model.ACCESS_ALL
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
@ -651,17 +652,15 @@ func (r *mutationResolver) DeleteMailingList(ctx context.Context, id int) (*mode
|
|||
DELETE FROM list
|
||||
WHERE id = $1 AND owner_id = $2
|
||||
RETURNING
|
||||
id, created, updated, name, description, owner_id,
|
||||
permit_mimetypes, reject_mimetypes,
|
||||
nonsubscriber_permissions, subscriber_permissions, account_permissions;`,
|
||||
id, created, updated, name, description, visibility, owner_id,
|
||||
permit_mimetypes, reject_mimetypes, default_access;`,
|
||||
id, auth.ForContext(ctx).UserID)
|
||||
if err := row.Scan(&list.ID, &list.Created, &list.Updated, &list.Name,
|
||||
&list.Description, &list.OwnerID,
|
||||
&list.RawPermitMime, &list.RawRejectMime,
|
||||
&list.RawNonsubscriber, &list.RawSubscriber, &list.RawIdentified); err != nil {
|
||||
&list.Description, &list.Visibility, &list.OwnerID,
|
||||
&list.RawPermitMime, &list.RawRejectMime, &list.DefaultAccess); err != nil {
|
||||
return err
|
||||
}
|
||||
list.Permissions = model.ACCESS_ALL
|
||||
list.Access = model.ACCESS_ALL
|
||||
|
||||
// We need to do this here so that it picks up the subscription list
|
||||
// before the cascade sets their list_id columns to null.
|
||||
|
@ -771,25 +770,19 @@ func (r *mutationResolver) UpdateMailingListACL(ctx context.Context, listID int,
|
|||
bits := ACLInputBits(input)
|
||||
var list model.MailingList
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
// TODO: Update me after unifying the ACL columns
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
UPDATE list SET
|
||||
nonsubscriber_permissions = $1,
|
||||
subscriber_permissions = $1,
|
||||
account_permissions = $1
|
||||
UPDATE list SET default_access = $1
|
||||
WHERE id = $2 AND owner_id = $3
|
||||
RETURNING
|
||||
id, created, updated, name, description, owner_id,
|
||||
permit_mimetypes, reject_mimetypes,
|
||||
nonsubscriber_permissions, subscriber_permissions, account_permissions;
|
||||
id, created, updated, name, description, visibility, owner_id,
|
||||
permit_mimetypes, reject_mimetypes, default_access;
|
||||
`, bits, listID, auth.ForContext(ctx).UserID)
|
||||
if err := row.Scan(&list.ID, &list.Created, &list.Updated, &list.Name,
|
||||
&list.Description, &list.OwnerID,
|
||||
&list.RawPermitMime, &list.RawRejectMime,
|
||||
&list.RawNonsubscriber, &list.RawSubscriber, &list.RawIdentified); err != nil {
|
||||
&list.Description, &list.Visibility, &list.OwnerID,
|
||||
&list.RawPermitMime, &list.RawRejectMime, &list.DefaultAccess); err != nil {
|
||||
return err
|
||||
}
|
||||
list.Permissions = model.ACCESS_ALL
|
||||
list.Access = model.ACCESS_ALL
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
@ -953,10 +946,7 @@ func (r *mutationResolver) MailingListSubscribe(ctx context.Context, listID int)
|
|||
WHERE list.id = $2 AND (
|
||||
list.owner_id = $1 OR
|
||||
(access.id IS NOT NULL AND access.permissions & $3 > 0) OR
|
||||
(access.id IS NULL AND (
|
||||
list.subscriber_permissions |
|
||||
list.account_permissions |
|
||||
list.nonsubscriber_permissions) & $3 > 0)
|
||||
(access.id IS NULL AND list.default_access & $3 > 0)
|
||||
)
|
||||
) INSERT INTO subscription (
|
||||
created, updated, user_id, list_id
|
||||
|
@ -1829,27 +1819,9 @@ func (r *userResolver) Lists(ctx context.Context, obj *model.User, cursor *corem
|
|||
Where(sq.And{
|
||||
sq.Expr(`list.owner_id = ?`, obj.ID),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`list.visibility != 'PRIVATE'`),
|
||||
sq.Expr(`access.permissions > 0`),
|
||||
},
|
||||
})
|
||||
lists, cursor = list.QueryWithCursor(ctx, tx, query, cursor)
|
||||
|
@ -1880,33 +1852,12 @@ func (r *userResolver) Emails(ctx context.Context, obj *model.User, cursor *core
|
|||
LeftJoin(`access ON
|
||||
access.list_id = list.id AND
|
||||
access.user_id = ?`, user.UserID).
|
||||
LeftJoin(`subscription sub ON
|
||||
sub.list_id = list.id AND
|
||||
sub.user_id = ?`, user.UserID).
|
||||
Where(sq.And{
|
||||
sq.Expr(`mail.sender_id = ?`, obj.ID),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
emails, cursor = email.QueryWithCursor(ctx, tx, query, cursor)
|
||||
|
@ -1937,34 +1888,13 @@ func (r *userResolver) Threads(ctx context.Context, obj *model.User, cursor *cor
|
|||
LeftJoin(`access ON
|
||||
access.list_id = list.id AND
|
||||
access.user_id = ?`, user.UserID).
|
||||
LeftJoin(`subscription sub ON
|
||||
sub.list_id = list.id AND
|
||||
sub.user_id = ?`, user.UserID).
|
||||
Where(sq.And{
|
||||
sq.Expr(`mail.sender_id = ?`, obj.ID),
|
||||
sq.Expr(`mail.thread_id IS NULL`),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
threads, cursor = thread.QueryWithCursor(ctx, tx, query, cursor)
|
||||
|
@ -1996,33 +1926,12 @@ func (r *userResolver) Patches(ctx context.Context, obj *model.User, cursor *cor
|
|||
LeftJoin(`access ON
|
||||
access.list_id = list.id AND
|
||||
access.user_id = ?`, user.UserID).
|
||||
LeftJoin(`subscription sub ON
|
||||
sub.list_id = list.id AND
|
||||
sub.user_id = ?`, user.UserID).
|
||||
Where(sq.And{
|
||||
sq.Expr(`email.sender_id = ?`, obj.ID),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
patches, cursor = patch.QueryWithCursor(ctx, tx, query, cursor)
|
||||
|
|
|
@ -190,39 +190,18 @@ func fetchMailingListsByID(ctx context.Context) func(ids []int) ([]*model.Mailin
|
|||
Column(`COALESCE(
|
||||
access.permissions,
|
||||
CASE WHEN list.owner_id = ?
|
||||
THEN ?
|
||||
ELSE CASE WHEN sub.id IS NOT NULL
|
||||
THEN list.subscriber_permissions
|
||||
ELSE null END
|
||||
END,
|
||||
list.nonsubscriber_permissions | list.account_permissions)`,
|
||||
THEN ?
|
||||
ELSE list.default_access
|
||||
END)`,
|
||||
user.UserID, model.ACCESS_ALL).
|
||||
Column(`access.id`).
|
||||
Column(`sub.id`).
|
||||
Where(sq.And{
|
||||
sq.Expr(`list.id = ANY(?)`, pq.Array(ids)),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`list.visibility != 'PRIVATE'`),
|
||||
sq.Expr(`access.permissions > 0`),
|
||||
},
|
||||
})
|
||||
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
|
||||
|
@ -235,7 +214,7 @@ func fetchMailingListsByID(ctx context.Context) func(ids []int) ([]*model.Mailin
|
|||
list := model.MailingList{}
|
||||
if err := rows.Scan(append(
|
||||
database.Scan(ctx, &list),
|
||||
&list.Permissions,
|
||||
&list.Access,
|
||||
&list.AccessID,
|
||||
&list.SubscriptionID,
|
||||
)...); err != nil {
|
||||
|
@ -353,37 +332,16 @@ func fetchMailingListsByOwnerName(ctx context.Context) func(names [][2]string) (
|
|||
Column(`COALESCE(
|
||||
access.permissions,
|
||||
CASE WHEN list.owner_id = ?
|
||||
THEN ?
|
||||
ELSE CASE WHEN sub.id IS NOT NULL
|
||||
THEN list.subscriber_permissions
|
||||
ELSE null END
|
||||
END,
|
||||
list.nonsubscriber_permissions | list.account_permissions)`,
|
||||
THEN ?
|
||||
ELSE list.default_access
|
||||
END)`,
|
||||
user.UserID, model.ACCESS_ALL).
|
||||
Column(`access.id`).
|
||||
Column(`sub.id`).
|
||||
Where(sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`list.visibility != 'PRIVATE'`),
|
||||
sq.Expr(`access.permissions > 0`),
|
||||
})
|
||||
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
|
||||
panic(err)
|
||||
|
@ -399,7 +357,7 @@ func fetchMailingListsByOwnerName(ctx context.Context) func(names [][2]string) (
|
|||
if err := rows.Scan(append(
|
||||
database.Scan(ctx, &list),
|
||||
&ownerName,
|
||||
&list.Permissions,
|
||||
&list.Access,
|
||||
&list.AccessID,
|
||||
&list.SubscriptionID)...); err != nil {
|
||||
panic(err)
|
||||
|
@ -446,27 +404,9 @@ func fetchEmailsByID(ctx context.Context) func(ids []int) ([]*model.Email, []err
|
|||
Where(sq.And{
|
||||
sq.Expr(`email.id = ANY(?)`, pq.Array(ids)),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
|
||||
|
@ -523,27 +463,9 @@ func fetchEmailsByMessageID(ctx context.Context) func(ids []string) ([]*model.Em
|
|||
Where(sq.And{
|
||||
sq.Expr(`email.message_id = ANY(?)`, pq.Array(ids)),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
|
||||
|
@ -688,27 +610,9 @@ func fetchPatchsetsByID(ctx context.Context) func(ids []int) ([]*model.Patchset,
|
|||
Where(sq.And{
|
||||
sq.Expr(`patch.id = ANY(?)`, pq.Array(ids)),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = ?`, user.UserID),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`sub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil {
|
||||
|
|
|
@ -94,12 +94,8 @@ func main() {
|
|||
LEFT JOIN subscription sub ON sub.list_id = list.id
|
||||
WHERE email.id = $1 OR email.thread_id = $1 AND (
|
||||
list.owner_id = $2 OR
|
||||
(access.id IS NOT NULL AND access.permissions & $3 > 0) OR
|
||||
(access.id IS NULL
|
||||
AND sub.id IS NULL
|
||||
AND list.nonsubscriber_permissions & $3 > 0) OR
|
||||
(access.id IS NULL AND
|
||||
(list.subscriber_permissions | list.account_permissions) & $3 > 0))
|
||||
access.permissions & $3 > 0 OR
|
||||
list.default_access & $3 > 0)
|
||||
ORDER BY email.id
|
||||
`, id, auth.ForContext(r.Context()).UserID, model.ACCESS_BROWSE)
|
||||
if err != nil {
|
||||
|
@ -131,12 +127,8 @@ func main() {
|
|||
LEFT JOIN subscription sub ON sub.list_id = list.id
|
||||
WHERE email.patchset_id = $1 AND email.is_patch AND (
|
||||
list.owner_id = $2 OR
|
||||
(access.id IS NOT NULL AND access.permissions & $3 > 0) OR
|
||||
(access.id IS NULL
|
||||
AND sub.id IS NULL
|
||||
AND list.nonsubscriber_permissions & $3 > 0) OR
|
||||
(access.id IS NULL AND
|
||||
(list.subscriber_permissions | list.account_permissions) & $3 > 0))
|
||||
access.permissions & $3 > 0 OR
|
||||
list.default_access & $3 > 0)
|
||||
ORDER BY email.patch_index, email.id
|
||||
`, id, auth.ForContext(r.Context()).UserID, model.ACCESS_BROWSE)
|
||||
if err != nil {
|
||||
|
@ -179,12 +171,8 @@ func main() {
|
|||
LEFT JOIN subscription sub ON sub.list_id = list.id
|
||||
WHERE email.list_id = $1 AND email.created >= $2 AND (
|
||||
list.owner_id = $3 OR
|
||||
(access.id IS NOT NULL AND access.permissions & $4 > 0) OR
|
||||
(access.id IS NULL
|
||||
AND sub.id IS NULL
|
||||
AND list.nonsubscriber_permissions & $4 > 0) OR
|
||||
(access.id IS NULL AND
|
||||
(list.subscriber_permissions | list.account_permissions) & $4 > 0))
|
||||
access.permissions & $4 > 0 OR
|
||||
list.default_access & $4 > 0)
|
||||
ORDER BY email.created
|
||||
`, id, since, auth.ForContext(r.Context()).UserID, model.ACCESS_BROWSE)
|
||||
if err != nil {
|
||||
|
|
|
@ -62,9 +62,9 @@ func DeliverLegacyUserListEvent(
|
|||
Updated: list.Updated,
|
||||
Description: list.Description,
|
||||
}
|
||||
payload.Permissions.Nonsubscriber = encodePermissions(list.RawNonsubscriber)
|
||||
payload.Permissions.Subscriber = encodePermissions(list.RawSubscriber)
|
||||
payload.Permissions.Account = encodePermissions(list.RawIdentified)
|
||||
payload.Permissions.Nonsubscriber = encodePermissions(list.DefaultAccess)
|
||||
payload.Permissions.Subscriber = encodePermissions(list.DefaultAccess)
|
||||
payload.Permissions.Account = encodePermissions(list.DefaultAccess)
|
||||
|
||||
// TODO: User groups
|
||||
user := auth.ForContext(ctx)
|
||||
|
@ -100,9 +100,9 @@ func DeliverLegacyListEvent(
|
|||
Updated: list.Updated,
|
||||
Description: list.Description,
|
||||
}
|
||||
payload.Permissions.Nonsubscriber = encodePermissions(list.RawNonsubscriber)
|
||||
payload.Permissions.Subscriber = encodePermissions(list.RawSubscriber)
|
||||
payload.Permissions.Account = encodePermissions(list.RawIdentified)
|
||||
payload.Permissions.Nonsubscriber = encodePermissions(list.DefaultAccess)
|
||||
payload.Permissions.Subscriber = encodePermissions(list.DefaultAccess)
|
||||
payload.Permissions.Account = encodePermissions(list.DefaultAccess)
|
||||
|
||||
// TODO: User groups
|
||||
user := auth.ForContext(ctx)
|
||||
|
|
|
@ -40,27 +40,9 @@ func deliverListWebhook(ctx context.Context, listID int,
|
|||
Where(sq.And{
|
||||
sq.Expr(`sub.list_id = ?`, listID),
|
||||
sq.Or{
|
||||
// List owner, or
|
||||
sq.Expr(`list.owner_id = sub.user_id`),
|
||||
// ACL entry exists, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NOT NULL`),
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Subscribers, or
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`lsub.id IS NULL`),
|
||||
sq.Expr(`list.nonsubscriber_permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
// Or:
|
||||
sq.And{
|
||||
sq.Expr(`access.id IS NULL`),
|
||||
sq.Expr(`
|
||||
(list.subscriber_permissions | list.account_permissions) & ? > 0`,
|
||||
model.ACCESS_BROWSE,
|
||||
),
|
||||
},
|
||||
sq.Expr(`access.permissions & ? > 0`, model.ACCESS_BROWSE),
|
||||
sq.Expr(`list.default_access & ? > 0`, model.ACCESS_BROWSE),
|
||||
},
|
||||
})
|
||||
q.Schedule(ctx, query, "list", event.String(),
|
||||
|
|
|
@ -111,9 +111,7 @@ class MailHandler:
|
|||
'''SELECT
|
||||
"id",
|
||||
"owner_id",
|
||||
"nonsubscriber_permissions",
|
||||
"subscriber_permissions",
|
||||
"account_permissions",
|
||||
"default_access",
|
||||
"permit_mimetypes",
|
||||
"reject_mimetypes"
|
||||
FROM "list"
|
||||
|
@ -260,12 +258,9 @@ class MailHandler:
|
|||
send_error_for.delay(mail_b64, unknown_mailing_list_error.format(
|
||||
sender, posting_domain, posting_domain))
|
||||
return "250 Mailing list not found, but sending bounce out of band"
|
||||
(dest_id, owner_id,
|
||||
nonsub_perms, sub_perms, external_perms,
|
||||
(dest_id, owner_id, default_access,
|
||||
permit_mimetypes, reject_mimetypes) = dest
|
||||
nonsub_perms = ListAccess(nonsub_perms)
|
||||
sub_perms = ListAccess(sub_perms)
|
||||
external_perms = ListAccess(external_perms)
|
||||
default_access = ListAccess(default_access)
|
||||
|
||||
fetch_email = await self.fetch_email(conn)
|
||||
in_reply_to = mail.get("In-Reply-To")
|
||||
|
@ -308,24 +303,11 @@ class MailHandler:
|
|||
print("Rejected: your account is not allowed to post to this list")
|
||||
return "500 Rejected. Your account is not allowed to post to this list."
|
||||
else:
|
||||
fetch_sub = await self.fetch_subscription(conn)
|
||||
sub = await fetch_sub.fetchval(_from[1], user_id)
|
||||
|
||||
if access not in nonsub_perms and not sub:
|
||||
if access not in default_access:
|
||||
user_errors_processed.inc()
|
||||
print("Rejected: non-subscribers are not allowed to post")
|
||||
return "500 Rejected. Non-subscribers are not allowed to post to this list."
|
||||
|
||||
if access not in sub_perms and sub:
|
||||
user_errors_processed.inc()
|
||||
print("Rejected: non-subscribers are not allowed to post")
|
||||
print("Rejected: default ACL does not allow posting")
|
||||
return "500 Rejected. You are not allowed to post to this list."
|
||||
|
||||
if access not in external_perms and not user_id:
|
||||
user_errors_processed.inc()
|
||||
print("Rejected: non-users are not allowed to post")
|
||||
return "500 Rejected. Users without an account are not allowed to post to this list."
|
||||
|
||||
forwards_processed.inc()
|
||||
print("Message accepted: {}".format(mail.get("Subject")))
|
||||
dispatch_message.delay(address, dest_id, mail_b64)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
"""Consolidate list permissions columns
|
||||
|
||||
Revision ID: 12d3ae26f3a3
|
||||
Revises: 6deff4db061c
|
||||
Create Date: 2022-05-30 09:39:30.629372
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '12d3ae26f3a3'
|
||||
down_revision = '6deff4db061c'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""
|
||||
ALTER TABLE list RENAME COLUMN nonsubscriber_permissions TO default_access;
|
||||
ALTER TABLE list DROP COLUMN subscriber_permissions;
|
||||
ALTER TABLE list DROP COLUMN account_permissions;
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
ALTER TABLE list RENAME COLUMN default_access TO nonsubscriber_permissions;
|
||||
ALTER TABLE list ADD COLUMN subscriber_permissions integer NOT NULL DEFAULT 7;
|
||||
ALTER TABLE list ADD COLUMN account_permissions integer NOT NULL DEFAULT 7;
|
||||
""")
|
|
@ -0,0 +1,43 @@
|
|||
"""Add list visibility column
|
||||
|
||||
Revision ID: c09158fb4d2d
|
||||
Revises: 12d3ae26f3a3
|
||||
Create Date: 2022-05-30 10:44:19.524347
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c09158fb4d2d'
|
||||
down_revision = '12d3ae26f3a3'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""
|
||||
CREATE TYPE visibility AS ENUM (
|
||||
'PUBLIC', 'UNLISTED', 'PRIVATE'
|
||||
);
|
||||
|
||||
ALTER TABLE list
|
||||
ADD COLUMN visibility visibility;
|
||||
|
||||
UPDATE list
|
||||
SET visibility =
|
||||
CASE WHEN default_access & 1 > 0
|
||||
THEN 'PUBLIC'::visibility
|
||||
ELSE 'PRIVATE'::visibility
|
||||
END;
|
||||
|
||||
ALTER TABLE list
|
||||
ALTER COLUMN visibility
|
||||
SET NOT NULL;
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
ALTER TABLE list DROP COLUMN visibility;
|
||||
DROP TYPE visibility;
|
||||
""")
|
|
@ -1,6 +1,6 @@
|
|||
import pkg_resources
|
||||
from flask import abort
|
||||
from listssrht.blueprints.archives import get_list as _get_list
|
||||
from listssrht.blueprints.archives import get_list as _get_list, get_access
|
||||
from listssrht.types import Email, User, Subscription, ListAccess
|
||||
from listssrht.webhooks import UserWebhook
|
||||
from srht.flask import csrf_bypass
|
||||
|
@ -26,15 +26,6 @@ def get_list(owner_name, list_name):
|
|||
abort(404)
|
||||
return owner, ml, access
|
||||
|
||||
def get_access(ml, user):
|
||||
if user.id == ml.owner_id:
|
||||
access = ListAccess.all
|
||||
elif Subscription.query .filter(Subscription.user_id == user.id).count():
|
||||
access = ml.subscriber_permissions | ml.account_permissions
|
||||
else:
|
||||
access = ml.account_permissions
|
||||
return access
|
||||
|
||||
def get_email(email_id):
|
||||
"""Fetches an email by email ID or message ID"""
|
||||
try:
|
||||
|
|
|
@ -14,9 +14,8 @@ def user_emails_GET(username):
|
|||
user = get_user(username)
|
||||
emails = Email.query.filter(Email.sender_id == user.id)
|
||||
if current_token.user_id != user.id:
|
||||
emails = emails.join(List, List.id == Email.list_id).filter(or_(
|
||||
List.account_permissions > 0,
|
||||
List.nonsubscriber_permissions > 0))
|
||||
emails = emails.join(List, List.id == Email.list_id).filter(
|
||||
List.default_access > 0)
|
||||
return paginated_response(Email.id,
|
||||
emails.order_by(Email.created.desc()), short=True)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from flask import current_app, Blueprint, abort, request
|
||||
from listssrht.blueprints.api import get_user, get_list
|
||||
from listssrht.blueprints.archives import apply_search
|
||||
from listssrht.types import List, Email, ListAccess, Subscription
|
||||
from listssrht.types import List, Email, ListAccess, Subscription, Visibility
|
||||
from listssrht.webhooks import ListWebhook, UserWebhook
|
||||
from sqlalchemy import or_
|
||||
from srht.api import paginated_response
|
||||
|
@ -19,9 +19,7 @@ def user_lists_GET(username):
|
|||
user = get_user(username)
|
||||
lists = List.query.filter(List.owner_id == user.id)
|
||||
if current_token.user_id != user.id:
|
||||
lists = lists.filter(or_(
|
||||
List.account_permissions > 0,
|
||||
List.nonsubscriber_permissions > 0))
|
||||
lists = lists.filter(List.visibility == Visibility.PUBLIC)
|
||||
return paginated_response(List.id, lists)
|
||||
|
||||
@lists.route("/api/lists", methods=["POST"])
|
||||
|
@ -36,7 +34,7 @@ def user_lists_POST():
|
|||
|
||||
resp = exec_gql(current_app.site, """
|
||||
mutation CreateMailingList($name: String!, $description: String) {
|
||||
createMailingList(name: $name, description: $description) {
|
||||
createMailingList(name: $name, description: $description, visibility: PUBLIC) {
|
||||
id
|
||||
name
|
||||
owner {
|
||||
|
@ -48,17 +46,7 @@ def user_lists_POST():
|
|||
created
|
||||
updated
|
||||
description
|
||||
nonsubscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
subscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
identified {
|
||||
defaultACL {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
|
@ -77,13 +65,11 @@ def user_lists_POST():
|
|||
] if acl[key]]
|
||||
|
||||
resp["permissions"] = {
|
||||
"nonsubscriber": permList(resp["nonsubscriber"]),
|
||||
"subscriber": permList(resp["subscriber"]),
|
||||
"account": permList(resp["identified"]),
|
||||
"nonsubscriber": permList(resp["defaultACL"]),
|
||||
"subscriber": permList(resp["defaultACL"]),
|
||||
"account": permList(resp["defaultACL"]),
|
||||
}
|
||||
del resp["nonsubscriber"]
|
||||
del resp["subscriber"]
|
||||
del resp["identified"]
|
||||
del resp["defaultACL"]
|
||||
|
||||
return resp, 201
|
||||
|
||||
|
@ -142,17 +128,7 @@ def user_lists_by_name_PUT(list_name):
|
|||
created
|
||||
updated
|
||||
description
|
||||
nonsubscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
subscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
identified {
|
||||
defaultACL {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
|
@ -171,13 +147,11 @@ def user_lists_by_name_PUT(list_name):
|
|||
] if acl[key]]
|
||||
|
||||
resp["permissions"] = {
|
||||
"nonsubscriber": permList(resp["nonsubscriber"]),
|
||||
"subscriber": permList(resp["subscriber"]),
|
||||
"account": permList(resp["identified"]),
|
||||
"nonsubscriber": permList(resp["defaultACL"]),
|
||||
"subscriber": permList(resp["defaultACL"]),
|
||||
"account": permList(resp["defaultACL"]),
|
||||
}
|
||||
del resp["nonsubscriber"]
|
||||
del resp["subscriber"]
|
||||
del resp["identified"]
|
||||
del resp["defaultACL"]
|
||||
|
||||
return resp
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from srht.flask import paginate_query
|
|||
from srht.oauth import current_user, loginrequired
|
||||
from srht.validation import Validation
|
||||
from listssrht.filters import post_address
|
||||
from listssrht.types import List, User, Email, Subscription, ListAccess, Access
|
||||
from listssrht.types import List, User, Email, Subscription, ListAccess, Access, Visibility
|
||||
from listssrht.types import Patchset, PatchsetStatus
|
||||
from listssrht.process import forward_thread
|
||||
from listssrht.webhooks import ListWebhook, UserWebhook
|
||||
|
@ -38,28 +38,37 @@ def get_list(owner_name, list_name, current_user=current_user):
|
|||
ml = (List.query
|
||||
.filter(List.name.ilike(list_name.replace('_', '\\_')))
|
||||
.filter(List.owner_id == owner.id)
|
||||
.one_or_none()
|
||||
)
|
||||
if current_user:
|
||||
ml = ml.outerjoin(Access, Access.list_id == List.id)
|
||||
ml = ml.one_or_none()
|
||||
if not ml:
|
||||
return None, None, None
|
||||
if current_user:
|
||||
acl = next((acl for acl in ml.acls
|
||||
if acl.user_id == current_user.id), None)
|
||||
if current_user.id == ml.owner_id:
|
||||
access = ListAccess.all
|
||||
elif acl:
|
||||
access = acl.permissions
|
||||
elif (Subscription.query
|
||||
.filter(Subscription.user_id == current_user.id)).count():
|
||||
access = ml.subscriber_permissions | ml.account_permissions
|
||||
else:
|
||||
access = ml.account_permissions
|
||||
else:
|
||||
access = ml.nonsubscriber_permissions
|
||||
access = get_access(ml, user=current_user)
|
||||
if access == ListAccess.none and ml.visibility == Visibility.PRIVATE:
|
||||
abort(401)
|
||||
return owner, ml, access
|
||||
|
||||
def get_access(ml, user=None):
|
||||
user = user or current_user
|
||||
|
||||
# Anonymous
|
||||
if not user:
|
||||
if ml.visibility == Visibility.PRIVATE:
|
||||
return ListAccess.none
|
||||
return ml.default_access
|
||||
|
||||
# Owner
|
||||
if user.id == ml.owner_id:
|
||||
return ListAccess.all
|
||||
|
||||
# ACL entry?
|
||||
user_access = Access.query.filter_by(list=ml, user=user).first()
|
||||
if user_access:
|
||||
return user_access.permissions
|
||||
|
||||
if ml.visibility == Visibility.PRIVATE:
|
||||
return ListAccess.none
|
||||
return ml.default_access
|
||||
|
||||
def apply_search(query, search):
|
||||
if not search:
|
||||
return query.filter(Email.parent_id == None)
|
||||
|
@ -173,8 +182,6 @@ def archive(owner_name, list_name):
|
|||
owner, ml, access = get_list(owner_name, list_name)
|
||||
if not ml:
|
||||
abort(404)
|
||||
if access.value == 0:
|
||||
abort(403)
|
||||
threads = (Email.query
|
||||
.filter(Email.list_id == ml.id)
|
||||
).order_by(Email.updated.desc())
|
||||
|
|
|
@ -51,7 +51,7 @@ def info_POST(owner_name, list_name):
|
|||
rewrite = lambda value: None if value == "" else value
|
||||
input = {
|
||||
key: rewrite(valid.source[key]) for key in [
|
||||
"description"
|
||||
"description", "visibility"
|
||||
] if valid.source.get(key) is not None
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,7 @@ def acl_POST(owner_name, list_name):
|
|||
else:
|
||||
acl.email = username
|
||||
acl.permissions = _process_access(valid, "acl")
|
||||
if ListAccess.browse in ml.nonsubscriber_permissions:
|
||||
if ListAccess.browse in ml.default_access:
|
||||
acl.permissions |= ListAccess.browse
|
||||
db.session.add(acl)
|
||||
db.session.commit()
|
||||
|
|
|
@ -10,7 +10,7 @@ from srht.graphql import exec_gql
|
|||
from srht.search import search_by
|
||||
from srht.validation import Validation
|
||||
from sqlalchemy import or_
|
||||
from listssrht.types import List, ListAccess, User, Email, Subscription, Mirror
|
||||
from listssrht.types import List, ListAccess, User, Email, Subscription, Mirror, Visibility
|
||||
from listssrht.webhooks import UserWebhook
|
||||
import re
|
||||
import smtplib
|
||||
|
@ -49,19 +49,9 @@ def user_profile(username):
|
|||
recent = Email.query.filter(Email.sender_id == user.id)
|
||||
lists = List.query.filter(List.owner_id == user.id)
|
||||
|
||||
if current_user:
|
||||
if current_user.id != user.id:
|
||||
lists = lists.filter(or_(
|
||||
List.account_permissions.op('&')(ListAccess.browse) > 0,
|
||||
List.nonsubscriber_permissions.op('&')(ListAccess.browse) > 0))
|
||||
recent = recent.join(List).filter(or_(
|
||||
List.account_permissions.op('&')(ListAccess.browse) > 0,
|
||||
List.nonsubscriber_permissions.op('&')(ListAccess.browse) > 0))
|
||||
else:
|
||||
lists = (lists
|
||||
.filter(List.nonsubscriber_permissions.op('&')(ListAccess.browse) > 0))
|
||||
recent = (recent.join(List)
|
||||
.filter(List.nonsubscriber_permissions.op('&')(ListAccess.browse) > 0))
|
||||
if not current_user or current_user.id != user.id:
|
||||
lists = lists.filter(List.visibility == Visibility.PUBLIC)
|
||||
recent = recent.join(List).filter(List.visibility == Visibility.PUBLIC)
|
||||
|
||||
recent = recent.order_by(Email.created.desc()).limit(10).all()
|
||||
lists = lists.order_by(List.updated.desc()).limit(10).all()
|
||||
|
@ -76,14 +66,8 @@ def lists_for_user(username):
|
|||
abort(404)
|
||||
lists = List.query.filter(List.owner_id == user.id)
|
||||
|
||||
if current_user:
|
||||
if current_user.id != user.id:
|
||||
lists = lists.filter(or_(
|
||||
List.account_permissions.op('&')(ListAccess.browse) > 0,
|
||||
List.nonsubscriber_permissions.op('&')(ListAccess.browse) > 0
|
||||
))
|
||||
else:
|
||||
lists = lists.filter(List.nonsubscriber_permissions.op('&')(ListAccess.browse) > 0)
|
||||
if not current_user or current_user.id != user.id:
|
||||
lists = lists.filter(List.visibility == Visibility.PUBLIC)
|
||||
|
||||
lists = lists.order_by(List.updated.desc())
|
||||
terms = request.args.get('search')
|
||||
|
@ -111,19 +95,20 @@ def create_list_POST():
|
|||
valid = Validation(request)
|
||||
name = valid.require("name", friendly_name="Name")
|
||||
description = valid.optional("description")
|
||||
visibility = valid.require("visibility")
|
||||
if not valid.ok:
|
||||
return render_template("create.html", **valid.kwargs)
|
||||
|
||||
resp = exec_gql(current_app.site, """
|
||||
mutation CreateMailingList($name: String!, $description: String) {
|
||||
createMailingList(name: $name, description: $description) {
|
||||
mutation CreateMailingList($name: String!, $description: String, $visibility: Visibility!) {
|
||||
createMailingList(name: $name, description: $description, visibility: $visibility) {
|
||||
name
|
||||
owner {
|
||||
canonicalName
|
||||
}
|
||||
}
|
||||
}
|
||||
""", valid=valid, name=name, description=description)
|
||||
""", valid=valid, name=name, description=description, visibility=visibility)
|
||||
|
||||
if not valid.ok:
|
||||
return render_template("create.html", **valid.kwargs)
|
||||
|
|
|
@ -185,12 +185,8 @@ def _update_patchset_status(dest, sender, patchset, status):
|
|||
access = ListAccess.all
|
||||
elif acl:
|
||||
access = acl.permissions
|
||||
elif isinstance(sender, User) and (Subscription.query
|
||||
.filter(Subscription.user_id == sender.id)).count():
|
||||
access = dest.subscriber_permissions | dest.account_permissions
|
||||
else:
|
||||
access = (dest.account_permissions
|
||||
if isinstance(sender, User) else dest.nonsubscriber_permissions)
|
||||
access = dest.default_access
|
||||
|
||||
if ListAccess.moderate not in access:
|
||||
print("Patchset update requested, but user has insufficient permissions")
|
||||
|
@ -346,7 +342,7 @@ def _subscribe(dest, mail):
|
|||
sender = parseaddr(mail["From"])
|
||||
user = User.query.filter(User.email == sender[1]).one_or_none()
|
||||
if user:
|
||||
perms = dest.account_permissions
|
||||
perms = dest.default_access
|
||||
sub = Subscription.query.filter(
|
||||
Subscription.list_id == dest.id,
|
||||
Subscription.user_id == user.id).one_or_none()
|
||||
|
@ -356,7 +352,7 @@ def _subscribe(dest, mail):
|
|||
if access:
|
||||
perms = access.permissions
|
||||
else:
|
||||
perms = dest.nonsubscriber_permissions
|
||||
perms = dest.default_access
|
||||
sub = Subscription.query.filter(
|
||||
Subscription.list_id == dest.id,
|
||||
Subscription.email == sender[1]).one_or_none()
|
||||
|
|
|
@ -1,40 +1,88 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row" method="POST" action="{{url_for(".create_list_POST")}}">
|
||||
<div class="col-md-12">
|
||||
<h3>Create new mailing list</h3>
|
||||
<form method="POST" action="{{url_for(".create_list_POST")}}">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control {{valid.cls("name")}}"
|
||||
value="{{ name or "" }}" />
|
||||
{{valid.summary("name")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control {{valid.cls("description")}}"
|
||||
value="{{ description or "" }}"
|
||||
placeholder="Markdown supported"
|
||||
rows="5"
|
||||
aria-describedby="description-help"
|
||||
>{{description or ""}}</textarea>
|
||||
{{valid.summary("description")}}
|
||||
</div>
|
||||
{{valid.summary()}}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Create {{icon("caret-right")}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control {{valid.cls("name")}}"
|
||||
value="{{ name or "" }}" />
|
||||
{{valid.summary("name")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control {{valid.cls("description")}}"
|
||||
value="{{ description or "" }}"
|
||||
placeholder="Markdown supported"
|
||||
rows="5"
|
||||
aria-describedby="description-help"
|
||||
>{{description or ""}}</textarea>
|
||||
{{valid.summary("description")}}
|
||||
</div>
|
||||
{{valid.summary()}}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Create {{icon("caret-right")}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<fieldset class="form-group">
|
||||
<legend>Mailing List Visibility</legend>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PUBLIC"
|
||||
checked> Public
|
||||
<small id="visibility-public-help" class="form-text text-muted">
|
||||
Shown on your profile page
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Visible to anyone with the link, but not shown on your profile"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="UNLISTED"> Unlisted
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Visible to anyone who knows the URL, but not shown on your profile
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Only visible to you and your collaborators"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PRIVATE"> Private
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Only visible to you and your collaborators
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -24,6 +24,22 @@
|
|||
href="{{ path }}">{{ title }}</a>
|
||||
{% endmacro %}
|
||||
<ul class="nav nav-tabs">
|
||||
{% if ml.visibility.value != "PUBLIC" %}
|
||||
<li
|
||||
class="nav-item nav-text vis-{{ml.visibility.value.lower()}}"
|
||||
{% if ml.visibility.value == "UNLISTED" %}
|
||||
title="This tracker is only visible to those who know the URL."
|
||||
{% elif ml.visibility.value == "PRIVATE" %}
|
||||
title="This tracker is only visible to those who were invited to view it."
|
||||
{% endif %}
|
||||
>
|
||||
{% if ml.visibility.value == "UNLISTED" %}
|
||||
Unlisted
|
||||
{% elif ml.visibility.value == "PRIVATE" %}
|
||||
Private
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
{{link(url_for("archives.archive",
|
||||
owner_name=owner.canonical_name,
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
specific access configuration.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, ml.nonsubscriber_permissions , "default") }}
|
||||
{{ perm_checkbox(a, ml.default_access, "default") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("list_default_access") }}
|
||||
</div>
|
||||
|
|
|
@ -6,48 +6,105 @@
|
|||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="POST">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="name">
|
||||
Name
|
||||
<span class="text-muted">(you can't edit this)</p>
|
||||
<form class="row" method="POST">
|
||||
<div class="col-md-6">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="name">
|
||||
Name
|
||||
<span class="text-muted">(you can't edit this)</p>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control"
|
||||
value="{{ ml.name }}"
|
||||
disabled />
|
||||
</div>
|
||||
<div class="form-group {{valid.cls("description")}}">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control"
|
||||
placeholder="Markdown supported"
|
||||
rows="10"
|
||||
aria-describedby="description-help"
|
||||
>{{description or ml.description or ""}}</textarea>
|
||||
{{ valid.summary("description") }}
|
||||
</div>
|
||||
{{ valid.summary() }}
|
||||
<span class="pull-right">
|
||||
<a
|
||||
href="{{ url_for("archives.archive",
|
||||
owner_name=ml.owner.canonical_name,
|
||||
list_name=ml.name) }}"
|
||||
class="btn btn-default"
|
||||
>Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save {{icon("caret-right")}}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<fieldset class="form-group">
|
||||
<legend>Mailing List Visibility</legend>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PUBLIC"
|
||||
{% if ml.visibility.value == "PUBLIC" %}
|
||||
checked
|
||||
{% endif %}
|
||||
> Public
|
||||
<small id="visibility-public-help" class="form-text text-muted">
|
||||
Shown on your profile page
|
||||
</small>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control"
|
||||
value="{{ ml.name }}"
|
||||
disabled />
|
||||
</div>
|
||||
<div class="form-group {{valid.cls("description")}}">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control"
|
||||
placeholder="Markdown supported"
|
||||
rows="10"
|
||||
aria-describedby="description-help"
|
||||
>{{description or ml.description or ""}}</textarea>
|
||||
{{ valid.summary("description") }}
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Visible to anyone with the link, but not shown on your profile"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="UNLISTED"
|
||||
{% if ml.visibility.value == "UNLISTED" %}
|
||||
checked
|
||||
{% endif %}
|
||||
> Unlisted
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Visible to anyone who knows the URL, but not shown on your profile
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
{{ valid.summary() }}
|
||||
<span class="pull-right">
|
||||
<a
|
||||
href="{{ url_for("archives.archive",
|
||||
owner_name=ml.owner.canonical_name,
|
||||
list_name=ml.name) }}"
|
||||
class="btn btn-default"
|
||||
>Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save {{icon("caret-right")}}
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
<div class="form-check">
|
||||
<label
|
||||
class="form-check-label"
|
||||
title="Only visible to you and your collaborators"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value="PRIVATE"
|
||||
{% if ml.visibility.value == "PRIVATE" %}
|
||||
checked
|
||||
{% endif %}
|
||||
> Private
|
||||
<small id="visibility-unlisted-help" class="form-text text-muted">
|
||||
Only visible to you and your collaborators
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -39,6 +39,11 @@
|
|||
list_name=list.name
|
||||
) }}"
|
||||
>{{list.owner.canonical_name}}/{{list.name}}</a>
|
||||
{% if list.visibility.value != 'PUBLIC' %}
|
||||
<small class="pull-right">
|
||||
{{ list.visibility.value.lower() }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</h4>
|
||||
{% if list.description %}
|
||||
{{list.description|md}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from listssrht.types.listaccess import ListAccess
|
||||
from listssrht.types.access import Access
|
||||
from listssrht.types.email import Email
|
||||
from listssrht.types.list import List
|
||||
from listssrht.types.list import List, Visibility
|
||||
from listssrht.types.patchset import Patchset, PatchsetStatus
|
||||
from listssrht.types.patchset import PatchsetTool, ToolIcon
|
||||
from listssrht.types.subscription import Subscription
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import re
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy_utils as sau
|
||||
from enum import Enum
|
||||
from srht.flagtype import FlagType
|
||||
from srht.database import Base
|
||||
from listssrht.types.listaccess import ListAccess
|
||||
|
||||
class Visibility(Enum):
|
||||
PUBLIC = 'PUBLIC'
|
||||
UNLISTED = 'UNLISTED'
|
||||
PRIVATE = 'PRIVATE'
|
||||
|
||||
class List(Base):
|
||||
__tablename__ = 'list'
|
||||
__table_args__ = sa.UniqueConstraint('owner_id', 'name',
|
||||
|
@ -14,26 +21,12 @@ class List(Base):
|
|||
updated = sa.Column(sa.DateTime, nullable=False)
|
||||
name = sa.Column(sa.String(128), nullable=False)
|
||||
description = sa.Column(sa.Unicode(2048))
|
||||
visibility = sa.Column(sau.ChoiceType(Visibility), nullable=False)
|
||||
import_in_progress = sa.Column(
|
||||
sa.Boolean, nullable=False, server_default='f')
|
||||
|
||||
nonsubscriber_permissions = sa.Column(FlagType(ListAccess),
|
||||
default_access = sa.Column(FlagType(ListAccess),
|
||||
nullable=False, server_default=str(ListAccess.normal.value))
|
||||
"""
|
||||
Permissions granted to users who are not subscribed or logged in.
|
||||
"""
|
||||
|
||||
subscriber_permissions = sa.Column(FlagType(ListAccess),
|
||||
nullable=False, server_default=str(ListAccess.normal.value))
|
||||
"""
|
||||
Permissions granted to users who are subscribed to the list.
|
||||
"""
|
||||
|
||||
account_permissions = sa.Column(FlagType(ListAccess),
|
||||
nullable=False, server_default=str(ListAccess.normal.value))
|
||||
"""
|
||||
Permissions granted to holders of sr.ht accounts.
|
||||
"""
|
||||
|
||||
permit_mimetypes = sa.Column(sa.Unicode, nullable=False,
|
||||
server_default="text/*,application/pgp-signature,application/pgp-keys")
|
||||
|
@ -89,9 +82,9 @@ class List(Base):
|
|||
"updated": self.updated,
|
||||
"description": self.description,
|
||||
"permissions": {
|
||||
"nonsubscriber": permissions(self.nonsubscriber_permissions),
|
||||
"subscriber": permissions(self.subscriber_permissions),
|
||||
"account": permissions(self.account_permissions),
|
||||
"nonsubscriber": permissions(self.default_access),
|
||||
"subscriber": permissions(self.default_access),
|
||||
"account": permissions(self.default_access),
|
||||
},
|
||||
} if not short else {})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue