Implement list visibility

Also consolidate list permissions columns into one default_access
column.
This commit is contained in:
Adnan Maolood 2022-06-14 06:47:26 -04:00 committed by Drew DeVault
parent 2ae65a9e81
commit 02395e0471
24 changed files with 452 additions and 562 deletions

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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(),

View File

@ -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)

View File

@ -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;
""")

View File

@ -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;
""")

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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())

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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 %}

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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

View File

@ -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 {})
}