Commit 25ee8846 authored by ale's avatar ale

Merge branch 'add_newsletters' into 'master'

Add newsletters

See merge request !7
parents 6d3d9981 c0f324f2
Pipeline #4650 passed with stages
in 4 minutes and 21 seconds
......@@ -113,6 +113,7 @@ func newLDAPBackendWithConn(conn ldapConn, baseDN string) (*backend, error) {
// discovered the base resources, so we can use the list
// of email aliases as listOwners.
newMailingListResourceFinder(baseDN),
newNewsletterResourceFinder(baseDN),
},
resources: reg,
minUID: defaultMinUID,
......@@ -133,11 +134,11 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
user := &as.RawUser{
User: as.User{
Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber,
Status: entry.GetAttributeValue("status"),
Shard: entry.GetAttributeValue("host"),
Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber,
Status: entry.GetAttributeValue("status"),
Shard: entry.GetAttributeValue("host"),
LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
......@@ -269,6 +270,10 @@ func newMailingListResourceFinder(baseDN string) userResourceFinder {
return &ldapUserResourceFinder{&mailingListResourceFinder{joinDN("ou=Lists", baseDN)}}
}
func newNewsletterResourceFinder(baseDN string) userResourceFinder {
return &ldapUserResourceFinder{&mailingListResourceFinder{joinDN("ou=Newsletters", baseDN)}}
}
// Find resources for a user using a generic LDAP query.
type ldapUserResourceFinder struct {
ldapQuery
......
......@@ -174,7 +174,7 @@ func TestModel_GetUser_Resources(t *testing.T) {
}
}
func TestModel_GetUser_MailingLists(t *testing.T) {
func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) {
stop, _, user := startServerAndGetUser4(t)
defer stop()
......@@ -182,7 +182,13 @@ func TestModel_GetUser_MailingLists(t *testing.T) {
// The backend should find two lists, one of which has an alias as the owner.
lists := user.GetResourcesByType(as.ResourceTypeMailingList)
if l := len(lists); l != 2 {
t.Fatalf("test user has %d mailing lists, expected %d", l, 2)
t.Errorf("test user has %d mailing lists, expected %d", l, 2)
}
// The test user has 1 newsletter.
nls := user.GetResourcesByType(as.ResourceTypeNewsletter)
if l := len(nls); l != 1 {
t.Errorf("test user has %d newsletters, expected %d", l, 1)
}
}
......
......@@ -94,6 +94,7 @@ func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) {
func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) {
// Since we don't know what resource type to expect, we try
// all known handlers until one returns a valid Resource.
// This is slightly dangerous unless all
for _, h := range reg.handlers {
rsrc, err = h.FromLDAP(entry)
if err == nil {
......@@ -117,7 +118,10 @@ type emailResourceHandler struct {
baseDN string
}
var errWrongObjectClass = errors.New("objectClass does not match")
var (
errWrongObjectClass = errors.New("objectClass does not match")
errWrongSubtree = errors.New("subtree does not match")
)
func (h *emailResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
if user == nil {
......@@ -166,9 +170,24 @@ func (h *emailResourceHandler) SearchQuery() *queryTemplate {
}
}
// Mailing list resource.
// Mailing list resource. They are stored on a separate LDAP subtree.
type mailingListResourceHandler struct {
baseDN string
// Keep around a parsed DN for the ou=Lists root, so we can
// easily call DN.AncestorOf in FromLDAP later.
listsDN *ldap.DN
}
func newMailingListResourceHandler(baseDN string) *mailingListResourceHandler {
dn, err := ldap.ParseDN(joinDN("ou=Lists", baseDN))
if err != nil {
panic(err)
}
return &mailingListResourceHandler{
baseDN: baseDN,
listsDN: dn,
}
}
func (h *mailingListResourceHandler) MakeDN(_ *as.User, rsrc *as.Resource) (string, error) {
......@@ -182,9 +201,18 @@ func (h *mailingListResourceHandler) GetOwner(rsrc *as.Resource) string {
}
func (h *mailingListResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
// Match on objectClass and subtree (the mailingList
// objectClass is also used by newsletters).
if !isObjectClass(entry, "mailingList") {
return nil, errWrongObjectClass
}
dn, err := ldap.ParseDN(entry.DN)
if err != nil {
panic(err)
}
if !h.listsDN.AncestorOf(dn) {
return nil, errWrongSubtree
}
listName := entry.GetAttributeValue("listName")
return &as.Resource{
......@@ -215,6 +243,79 @@ func (h *mailingListResourceHandler) SearchQuery() *queryTemplate {
}
}
// Newsletter resource. Like mailing lists, these are stored on their
// own separate subtree.
type newsletterResourceHandler struct {
baseDN string
// Keep around a parsed DN for the ou=Newsletters root, so we can
// easily call DN.AncestorOf in FromLDAP later.
newslettersDN *ldap.DN
}
func newNewsletterResourceHandler(baseDN string) *newsletterResourceHandler {
dn, err := ldap.ParseDN(joinDN("ou=Newsletters", baseDN))
if err != nil {
panic(err)
}
return &newsletterResourceHandler{
baseDN: baseDN,
newslettersDN: dn,
}
}
func (h *newsletterResourceHandler) MakeDN(_ *as.User, rsrc *as.Resource) (string, error) {
rdn := replaceVars("listName=${resource}", templateVars{"resource": rsrc.Name})
return joinDN(rdn, "ou=Newsletters", h.baseDN), nil
}
func (h *newsletterResourceHandler) GetOwner(rsrc *as.Resource) string {
// No exclusive owners for newsletters.
return ""
}
func (h *newsletterResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
// Match on objectClass and subtree.
if !isObjectClass(entry, "mailingList") {
return nil, errWrongObjectClass
}
dn, err := ldap.ParseDN(entry.DN)
if err != nil {
panic(err)
}
if !h.newslettersDN.AncestorOf(dn) {
return nil, errWrongSubtree
}
listName := entry.GetAttributeValue("listName")
return &as.Resource{
ID: as.ResourceID(entry.DN),
Type: as.ResourceTypeNewsletter,
Name: listName,
Newsletter: &as.Newsletter{
Public: s2b(entry.GetAttributeValue("public")),
Admins: entry.GetAttributeValues("listOwner"),
},
}, nil
}
func (h *newsletterResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
return []ldap.PartialAttribute{
{Type: "objectClass", Vals: []string{"top", "mailingList"}},
{Type: "listName", Vals: s2l(rsrc.Name)},
{Type: "public", Vals: s2l(b2s(rsrc.Newsletter.Public))},
{Type: "listOwner", Vals: rsrc.Newsletter.Admins},
}
}
func (h *newsletterResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=Newsletters", h.baseDN),
Filter: "(&(objectClass=mailingList)(listName=${resource}))",
Scope: ldap.ScopeSingleLevel,
}
}
// Website (subsite) resource.
type websiteResourceHandler struct {
baseDN string
......@@ -457,7 +558,8 @@ func (h *databaseResourceHandler) SearchQuery() *queryTemplate {
func newDefaultResourceRegistry(baseDN string) *resourceRegistry {
reg := newResourceRegistry()
reg.register(as.ResourceTypeEmail, &emailResourceHandler{baseDN: baseDN})
reg.register(as.ResourceTypeMailingList, &mailingListResourceHandler{baseDN: baseDN})
reg.register(as.ResourceTypeMailingList, newMailingListResourceHandler(baseDN))
reg.register(as.ResourceTypeNewsletter, newNewsletterResourceHandler(baseDN))
reg.register(as.ResourceTypeDAV, &webdavResourceHandler{baseDN: baseDN})
reg.register(as.ResourceTypeWebsite, &websiteResourceHandler{baseDN: baseDN})
reg.register(as.ResourceTypeDomain, &domainResourceHandler{baseDN: baseDN})
......
......@@ -14,3 +14,8 @@ objectclass: top
objectclass: organizationalUnit
ou: Lists
dn: ou=Newsletters,dc=example,dc=com
objectclass: top
objectclass: organizationalUnit
ou: Lists
......@@ -38,7 +38,7 @@ userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmN
dn: listName=list1@investici.org,ou=Lists,dc=example,dc=com
objectClass: mailingList
objectClass: top
listName: list@investici.org
listName: list1@investici.org
listDescription: Mailing list 1
public: no
status: active
......@@ -49,10 +49,21 @@ originalHost: host2
dn: listName=list2@investici.org,ou=Lists,dc=example,dc=com
objectClass: mailingList
objectClass: top
listName: list@investici.org
listName: list2@investici.org
listDescription: Mailing list 2
public: no
status: active
listOwner: quattroalias@investici.org
host: host2
originalHost: host2
dn: listName=newsletter1@investici.org,ou=Newsletters,dc=example,dc=com
objectClass: mailingList
objectClass: top
listName: newsletter1@investici.org
listDescription: Newsletter 1
public: no
status: active
listOwner: quattro@investici.org
host: host2
originalHost: host2
......@@ -14,3 +14,8 @@ objectclass: top
objectclass: organizationalUnit
ou: Lists
dn: ou=Newsletters,dc=example,dc=com
objectclass: top
objectclass: organizationalUnit
ou: Lists
......@@ -36,3 +36,15 @@ status: active
uidNumber: 256800
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
dn: listName=newsletter1@investici.org,ou=Newsletters,dc=example,dc=com
status: active
listName: newsletter1@investici.org
objectClass: top
objectClass: mailingList
listDescription: No description
host: host2
creationDate: 21-03-2019
listOwner: tre@investici.org
public: no
originalHost: host2
......@@ -378,6 +378,7 @@ const (
const (
ResourceTypeEmail = "email"
ResourceTypeMailingList = "list"
ResourceTypeNewsletter = "newsletter"
ResourceTypeWebsite = "web"
ResourceTypeDomain = "domain"
ResourceTypeDAV = "dav"
......@@ -395,7 +396,7 @@ const (
// Returns true if the given status is valid for the given resource type.
func isValidStatusByResourceType(rtype, rstatus string) bool {
switch rtype {
case ResourceTypeEmail, ResourceTypeMailingList:
case ResourceTypeEmail, ResourceTypeMailingList, ResourceTypeNewsletter:
switch rstatus {
case ResourceStatusActive, ResourceStatusInactive, ResourceStatusReadonly:
return true
......@@ -478,11 +479,12 @@ type Resource struct {
// Details about the specific type (only one of these can be
// set, depending on the value of 'type').
Email *Email `json:"email,omitempty"`
List *MailingList `json:"list,omitempty"`
Website *Website `json:"website,omitempty"`
DAV *WebDAV `json:"dav,omitempty"`
Database *Database `json:"database,omitempty"`
Email *Email `json:"email,omitempty"`
List *MailingList `json:"list,omitempty"`
Newsletter *Newsletter `json:"newsletter,omitempty"`
Website *Website `json:"website,omitempty"`
DAV *WebDAV `json:"dav,omitempty"`
Database *Database `json:"database,omitempty"`
}
// Copy the resource (makes a deep copy).
......@@ -498,6 +500,9 @@ func (r *Resource) Copy() *Resource {
case r.List != nil:
l := *r.List
rr.List = &l
case r.Newsletter != nil:
l := *r.Newsletter
rr.Newsletter = &l
case r.DAV != nil:
d := *r.DAV
rr.DAV = &d
......@@ -537,6 +542,13 @@ type MailingList struct {
Public bool `json:"public"`
}
// Newsletter resource attributes.
type Newsletter struct {
Admins []string `json:"admins"`
Public bool `json:"public"`
}
// WebDAV represents a hosting account.
type WebDAV struct {
UID int `json:"uid"`
......
......@@ -293,15 +293,22 @@ func relatedEmails(ctx context.Context, be domainBackend, addr string) []FindRes
{Type: ResourceTypeEmail, Name: addr},
}
user, _ := splitEmailAddr(addr)
// Mailing lists must have unique names regardless of the domain, so we
// add potential conflicts for mailing lists with the same name over all
// list-enabled domains.
// Mailing lists and newsletters must have unique names
// regardless of the domain, so we add potential conflicts for
// mailing lists with the same name over all list-enabled
// domains.
for _, d := range be.GetAllowedDomains(ctx, ResourceTypeMailingList) {
rel = append(rel, FindResourceRequest{
Type: ResourceTypeMailingList,
Name: fmt.Sprintf("%s@%s", user, d),
})
}
for _, d := range be.GetAllowedDomains(ctx, ResourceTypeNewsletter) {
rel = append(rel, FindResourceRequest{
Type: ResourceTypeNewsletter,
Name: fmt.Sprintf("%s@%s", user, d),
})
}
return rel
}
......@@ -755,6 +762,7 @@ func newResourceValidator(v *validationContext) *resourceValidator {
rvs: map[string]ResourceValidatorFunc{
ResourceTypeEmail: v.validEmailResource(),
ResourceTypeMailingList: v.validListResource(),
ResourceTypeNewsletter: v.validListResource(),
ResourceTypeDomain: v.validDomainResource(),
ResourceTypeWebsite: v.validWebsiteResource(),
ResourceTypeDAV: v.validDAVResource(),
......@@ -1033,7 +1041,7 @@ func (c *templateContext) applyTemplate(ctx context.Context, r *Resource, user *
return c.davResourceTemplate(ctx, r, user)
case ResourceTypeDatabase:
return c.databaseResourceTemplate(ctx, r, user)
case ResourceTypeMailingList:
case ResourceTypeMailingList, ResourceTypeNewsletter:
return c.listResourceTemplate(ctx, r, user)
}
return nil
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment