diff --git a/api/graph/model/mailinglist.go b/api/graph/model/mailinglist.go index a18e70c..2de2959 100644 --- a/api/graph/model/mailinglist.go +++ b/api/graph/model/mailinglist.go @@ -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) diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 7e5e6a6..033b0f0 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -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( diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 0da6481..d8a37dd 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -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) diff --git a/api/loaders/middleware.go b/api/loaders/middleware.go index df69f14..57d0913 100644 --- a/api/loaders/middleware.go +++ b/api/loaders/middleware.go @@ -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 { diff --git a/api/server.go b/api/server.go index d8665d8..43ed0dd 100644 --- a/api/server.go +++ b/api/server.go @@ -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 { diff --git a/api/webhooks/legacy.go b/api/webhooks/legacy.go index c524234..a229ef3 100644 --- a/api/webhooks/legacy.go +++ b/api/webhooks/legacy.go @@ -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) diff --git a/api/webhooks/webhooks.go b/api/webhooks/webhooks.go index 316c256..5b2b731 100644 --- a/api/webhooks/webhooks.go +++ b/api/webhooks/webhooks.go @@ -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(), diff --git a/listssrht-lmtp b/listssrht-lmtp index 12582b8..626e430 100755 --- a/listssrht-lmtp +++ b/listssrht-lmtp @@ -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) diff --git a/listssrht/alembic/versions/12d3ae26f3a3_consolidate_list_permissions_columns.py b/listssrht/alembic/versions/12d3ae26f3a3_consolidate_list_permissions_columns.py new file mode 100644 index 0000000..dbdc32b --- /dev/null +++ b/listssrht/alembic/versions/12d3ae26f3a3_consolidate_list_permissions_columns.py @@ -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; + """) diff --git a/listssrht/alembic/versions/c09158fb4d2d_add_list_visibility_column.py b/listssrht/alembic/versions/c09158fb4d2d_add_list_visibility_column.py new file mode 100644 index 0000000..c089c9c --- /dev/null +++ b/listssrht/alembic/versions/c09158fb4d2d_add_list_visibility_column.py @@ -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; + """) diff --git a/listssrht/blueprints/api/__init__.py b/listssrht/blueprints/api/__init__.py index 98f2cd4..8fb7e90 100644 --- a/listssrht/blueprints/api/__init__.py +++ b/listssrht/blueprints/api/__init__.py @@ -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: diff --git a/listssrht/blueprints/api/emails.py b/listssrht/blueprints/api/emails.py index 50aa98a..f471482 100644 --- a/listssrht/blueprints/api/emails.py +++ b/listssrht/blueprints/api/emails.py @@ -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) diff --git a/listssrht/blueprints/api/lists.py b/listssrht/blueprints/api/lists.py index 1630cec..d4b6fb3 100644 --- a/listssrht/blueprints/api/lists.py +++ b/listssrht/blueprints/api/lists.py @@ -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 diff --git a/listssrht/blueprints/archives.py b/listssrht/blueprints/archives.py index 3222f89..f2cb107 100644 --- a/listssrht/blueprints/archives.py +++ b/listssrht/blueprints/archives.py @@ -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()) diff --git a/listssrht/blueprints/settings.py b/listssrht/blueprints/settings.py index f4d81ca..3f4f318 100644 --- a/listssrht/blueprints/settings.py +++ b/listssrht/blueprints/settings.py @@ -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() diff --git a/listssrht/blueprints/user.py b/listssrht/blueprints/user.py index 03657a1..58057a2 100644 --- a/listssrht/blueprints/user.py +++ b/listssrht/blueprints/user.py @@ -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) diff --git a/listssrht/process.py b/listssrht/process.py index 30e7e5b..6223c0c 100644 --- a/listssrht/process.py +++ b/listssrht/process.py @@ -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() diff --git a/listssrht/templates/create.html b/listssrht/templates/create.html index bbc2e51..08764c1 100644 --- a/listssrht/templates/create.html +++ b/listssrht/templates/create.html @@ -1,40 +1,88 @@ {% extends "layout.html" %} {% block body %}
-
-
+
+

Create new mailing list

- - {{csrf_token()}} -
- - - {{valid.summary("name")}} -
-
- - - {{valid.summary("description")}} -
- {{valid.summary()}} - -
-
+
+ {{csrf_token()}} +
+ + + {{valid.summary("name")}} +
+
+ + + {{valid.summary("description")}} +
+ {{valid.summary()}} + +
+
+
+ Mailing List Visibility +
+ +
+
+ +
+
+ +
+
+
+
{% endblock %} diff --git a/listssrht/templates/list.html b/listssrht/templates/list.html index a225680..2848100 100644 --- a/listssrht/templates/list.html +++ b/listssrht/templates/list.html @@ -24,6 +24,22 @@ href="{{ path }}">{{ title }} {% endmacro %}
diff --git a/listssrht/templates/settings-info.html b/listssrht/templates/settings-info.html index fda556a..8c6ae5b 100644 --- a/listssrht/templates/settings-info.html +++ b/listssrht/templates/settings-info.html @@ -6,48 +6,105 @@ {% endblock %} {% block content %}
-
-
-
- {{csrf_token()}} -
-
diff --git a/listssrht/templates/user-lists.html b/listssrht/templates/user-lists.html index b1c12e8..bd9f71a 100644 --- a/listssrht/templates/user-lists.html +++ b/listssrht/templates/user-lists.html @@ -39,6 +39,11 @@ list_name=list.name ) }}" >{{list.owner.canonical_name}}/{{list.name}} + {% if list.visibility.value != 'PUBLIC' %} + + {{ list.visibility.value.lower() }} + + {% endif %} {% if list.description %} {{list.description|md}} diff --git a/listssrht/types/__init__.py b/listssrht/types/__init__.py index 41b5c33..771bd40 100644 --- a/listssrht/types/__init__.py +++ b/listssrht/types/__init__.py @@ -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 diff --git a/listssrht/types/list.py b/listssrht/types/list.py index 3664a41..39ab4fd 100644 --- a/listssrht/types/list.py +++ b/listssrht/types/list.py @@ -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 {}) }