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