Commit 352baf08 authored by ale's avatar ale

Merge branch 'opaque-resource-ids' into 'master'

Opaque resource ids

Closes #5

See merge request !3
parents c5d3b1a5 e2deec82
Pipeline #1580 passed with stages
in 1 minute and 36 seconds
......@@ -181,11 +181,11 @@ func (r *ResourceRequestBase) PopulateContext(rctx *RequestContext) error {
if err != nil {
return err
}
rctx.Resource = rsrc
rctx.Resource = &rsrc.Resource
// If the resource has an owner, populate the User context field.
if owner := rsrc.ID.User(); owner != "" {
user, err := getUserOrDie(rctx.Context, rctx.TX, owner)
if rsrc.Owner != "" {
user, err := getUserOrDie(rctx.Context, rctx.TX, rsrc.Owner)
if err != nil {
return err
}
......@@ -197,27 +197,12 @@ func (r *ResourceRequestBase) PopulateContext(rctx *RequestContext) error {
// Authorize the request.
func (r *ResourceRequestBase) Authorize(rctx *RequestContext) error {
if !rctx.isAdmin(rctx.SSO) && !canAccessResource(rctx.SSO.User, rctx.Resource) {
if !rctx.isAdmin(rctx.SSO) && !rctx.TX.CanAccessResource(rctx.Context, rctx.SSO.User, rctx.Resource) {
return fmt.Errorf("user %s can't access resource %s", rctx.SSO.User, rctx.Resource.ID)
}
return nil
}
func canAccessResource(username string, r *Resource) bool {
switch r.ID.Type() {
case ResourceTypeMailingList:
// Check the list owners.
for _, a := range r.List.Admins {
if a == username {
return true
}
}
return false
default:
return r.ID.User() == username
}
}
// AdminResourceRequestBase is an admin-only version of ResourceRequestBase.
type AdminResourceRequestBase struct {
ResourceRequestBase
......
......@@ -2,7 +2,7 @@ package accountserver
import (
"context"
"errors"
"fmt"
"log"
"git.autistici.org/ai3/go-common/pwhash"
......@@ -12,94 +12,110 @@ import (
type CreateResourcesRequest struct {
AdminRequestBase
// Username the resources will belong to (optional).
Username string `json:"username"`
// Resources to create. All must either be global resources
// (no user ownership), or belong to the same user.
Resources []*Resource `json:"resources"`
}
// PopulateContext extracts information from the request and stores it
// into the RequestContext.
func (r *CreateResourcesRequest) PopulateContext(rctx *RequestContext) error {
if r.Username != "" {
user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username)
if err != nil {
return err
}
rctx.User = user
}
return r.AdminRequestBase.PopulateContext(rctx)
}
// CreateResourcesResponse is the response type for CreateResourcesRequest.
type CreateResourcesResponse struct {
// Resources that were created.
Resources []*Resource `json:"resources"`
}
func (r *CreateResourcesRequest) getOwner(rctx *RequestContext) (*RawUser, error) {
// Fetch the user associated with the first resource (if
// any). Since resource validation might reference other
// resources, we need to provide it with a view of what the
// future resources will be. So we merge the resources from
// the database with those from the request, using a local
// copy of the User object.
if len(r.Resources) > 0 {
if owner := r.Resources[0].ID.User(); owner != "" {
u, err := getUserOrDie(rctx.Context, rctx.TX, owner)
if err != nil {
return nil, err
// Merge two resource lists by ID, return a new list. If a resource is
// duplicated (detected by type/name matching), return an error.
func mergeResources(a, b []*Resource) ([]*Resource, error) {
tmp := make(map[string]struct{})
var out []*Resource
for _, l := range [][]*Resource{a, b} {
for _, r := range l {
key := r.String()
if _, seen := tmp[key]; seen {
return nil, fmt.Errorf("resource %s already exists", key)
}
user := *u
user.Resources = mergeResources(u.Resources, r.Resources)
return &user, nil
tmp[key] = struct{}{}
out = append(out, r)
}
}
return nil, nil
return out, nil
}
// Validate the request.
func (r *CreateResourcesRequest) Validate(rctx *RequestContext) error {
var owner string
user, err := r.getOwner(rctx)
if err != nil {
return err
}
var tplUser *User
if user != nil {
owner = user.Name
tplUser = &user.User
if rctx.User != nil {
// To provide resource validators with a view of what the User should be
// with the new resources, we merge the ones from the database with the
// ones from the request. This is also a good time to check for
// uniqueness (even though it would fail at commit time anyway).
merged, err := mergeResources(rctx.User.Resources, r.Resources)
if err != nil {
return err
}
userCopy := rctx.User.User
userCopy.Resources = merged
tplUser = &userCopy
}
for _, rsrc := range r.Resources {
rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, tplUser)
// Check same-user ownership.
if rsrc.ID.User() != owner {
return errors.New("resources are owned by different users")
// Apply resource templates.
if err := rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, tplUser); err != nil {
return err
}
// Validate the resource.
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, tplUser); err != nil {
log.Printf("validation error while creating resource %s: %v", rsrc.ID, err)
log.Printf("validation error while creating resource %s: %v", rsrc.String(), err)
return err
}
}
if tplUser != nil {
// If the resource has an owner, validate it (checks that the new
// resources do not violate user invariants).
if err := checkUserInvariants(tplUser); err != nil {
log.Printf("validation error while creating resources: %v", err)
return err
}
}
return nil
}
// Serve the request.
func (r *CreateResourcesRequest) Serve(rctx *RequestContext) (interface{}, error) {
var resp CreateResourcesResponse
for _, rsrc := range r.Resources {
if err := rctx.TX.CreateResource(rctx.Context, rsrc); err != nil {
return nil, err
}
rctx.audit.Log(rctx, rsrc.ID, "resource created")
resp.Resources = append(resp.Resources, rsrc)
var user *User
if rctx.User != nil {
user = &rctx.User.User
}
return &resp, nil
}
// Merge two resource lists by ID (the second one wins), return a new list.
func mergeResources(a, b []*Resource) []*Resource {
tmp := make(map[string]*Resource)
for _, l := range [][]*Resource{a, b} {
for _, r := range l {
tmp[r.ID.String()] = r
}
rsrcs, err := rctx.TX.CreateResources(rctx.Context, user, r.Resources)
if err != nil {
return nil, err
}
out := make([]*Resource, 0, len(tmp))
for _, r := range tmp {
out = append(out, r)
for _, rsrc := range rsrcs {
rctx.audit.Log(rctx, rsrc, "resource created")
}
return out
return &CreateResourcesResponse{
Resources: rsrcs,
}, nil
}
// CreateUserRequest lets administrators create a new user along with the
......@@ -135,7 +151,13 @@ func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error {
// Apply templates to all resources in the request.
for _, rsrc := range r.User.Resources {
rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, r.User)
if err := rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, r.User); err != nil {
return err
}
// Set the user shard to match the email resource shard.
if rsrc.Type == ResourceTypeEmail {
r.User.Shard = rsrc.Shard
}
}
return nil
......@@ -149,36 +171,17 @@ func (r *CreateUserRequest) Validate(rctx *RequestContext) error {
// Validate the user *and* all resources. The request must contain at
// least one email resource with the same name as the user.
if err := rctx.userValidator(rctx.Context, r.User); err != nil {
log.Printf("validation error while creating user %+v: %v", r.User, err)
return err
}
var emailCount int
for _, rsrc := range r.User.Resources {
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, r.User); err != nil {
log.Printf("validation error while creating resource %+v: %v", rsrc, err)
return err
}
if rsrc.ID.Type() == ResourceTypeEmail {
emailCount++
}
}
if emailCount == 0 {
return errors.New("missing email resource")
}
if emailCount > 1 {
return errors.New("too many email resources")
}
email := r.User.GetSingleResourceByType(ResourceTypeEmail)
if email.Name != r.User.Name {
return errors.New("user and email resource names do not match")
if err := rctx.userValidator(rctx.Context, r.User); err != nil {
log.Printf("validation error while creating user %+v: %v", r.User, err)
return err
}
// Now that validation is done, finalize the object by setting some derived parameters.
// Set the user shard to the email shard.
r.User.Shard = email.Shard
return nil
}
......@@ -200,17 +203,18 @@ func (r *CreateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
var resp CreateUserResponse
// Create the user first, along with all the resources.
if err := rctx.TX.CreateUser(rctx.Context, r.User); err != nil {
user, err := rctx.TX.CreateUser(rctx.Context, r.User)
if err != nil {
return nil, err
}
resp.User = r.User
resp.User = user
// Now set a password for the user and return it, and
// set random passwords for all the resources
// (currently, we don't care about those, the user
// will reset them later). However, we could return
// them in the response as well, if necessary.
u := &RawUser{User: *r.User}
u := &RawUser{User: *user}
newPassword := randomPassword()
if err := u.resetPassword(rctx.Context, rctx.TX, newPassword); err != nil {
return nil, err
......@@ -219,15 +223,15 @@ func (r *CreateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
// Fake a RawUser in the RequestContext just for the purpose
// of audit logging.
rctx.User = &RawUser{User: *r.User}
rctx.audit.Log(rctx, ResourceID{}, "user created")
rctx.User = u
rctx.audit.Log(rctx, nil, "user created")
for _, rsrc := range r.User.Resources {
rctx.audit.Log(rctx, rsrc.ID, "resource created")
rctx.audit.Log(rctx, rsrc, "resource created")
if resourceHasPassword(rsrc) {
if _, err := doResetResourcePassword(rctx.Context, rctx.TX, rsrc); err != nil {
// Just log, don't fail.
log.Printf("can't set random password for resource %s: %v", rsrc.ID, err)
log.Printf("can't set random password for resource %s: %v", rsrc.String(), err)
}
}
}
......@@ -235,6 +239,15 @@ func (r *CreateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
return &resp, nil
}
func resourceHasPassword(r *Resource) bool {
switch r.Type {
case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
return true
default:
return false
}
}
func doResetResourcePassword(ctx context.Context, tx TX, rsrc *Resource) (string, error) {
newPassword := randomPassword()
encPassword := pwhash.Encrypt(newPassword)
......
......@@ -13,7 +13,7 @@ func setResourceStatus(rctx *RequestContext, status string) error {
if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
return err
}
rctx.audit.Log(rctx, rsrc.ID, fmt.Sprintf("status set to %s", status))
rctx.audit.Log(rctx, rsrc, fmt.Sprintf("status set to %s", status))
return nil
}
......@@ -50,18 +50,9 @@ type ResetResourcePasswordResponse struct {
Password string `json:"password"`
}
func resourceHasPassword(r *Resource) bool {
switch r.ID.Type() {
case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
return true
default:
return false
}
}
// Validate the request.
func (r *ResetResourcePasswordRequest) Validate(_ *RequestContext) error {
switch r.ResourceID.Type() {
func (r *ResetResourcePasswordRequest) Validate(rctx *RequestContext) error {
switch rctx.Resource.Type {
case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
case ResourceTypeEmail:
return errors.New("can't reset email passwords with this API")
......@@ -118,7 +109,7 @@ func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
// We need to enforce consistency between email resources and
// the user shard, so that temporary data can be colocated
// with email storage.
if rctx.Resource.ID.Type() == ResourceTypeEmail && rctx.User.Shard != r.Shard {
if rctx.Resource.Type == ResourceTypeEmail && rctx.User.Shard != r.Shard {
rctx.User.Shard = r.Shard
if err := rctx.TX.UpdateUser(rctx.Context, &rctx.User.User); err != nil {
return nil, err
......@@ -144,7 +135,7 @@ type AddEmailAliasRequest struct {
// Validate the request.
func (r *AddEmailAliasRequest) Validate(rctx *RequestContext) error {
if rctx.Resource.ID.Type() != ResourceTypeEmail {
if rctx.Resource.Type != ResourceTypeEmail {
return errors.New("this operation only works on email resources")
}
......@@ -168,7 +159,7 @@ func (r *AddEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error)
return nil, err
}
rctx.audit.Log(rctx, r.ResourceID, fmt.Sprintf("added alias %s", r.Addr))
rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("added alias %s", r.Addr))
return nil, nil
}
......@@ -180,7 +171,7 @@ type DeleteEmailAliasRequest struct {
// Validate the request.
func (r *DeleteEmailAliasRequest) Validate(rctx *RequestContext) error {
if rctx.Resource.ID.Type() != ResourceTypeEmail {
if rctx.Resource.Type != ResourceTypeEmail {
return errors.New("this operation only works on email resources")
}
return nil
......@@ -199,6 +190,6 @@ func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, erro
return nil, err
}
rctx.audit.Log(rctx, r.ResourceID, fmt.Sprintf("removed alias %s", r.Addr))
rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("removed alias %s", r.Addr))
return nil, nil
}
This diff is collapsed.
......@@ -58,7 +58,7 @@ func (r *ChangeUserPasswordRequest) Serve(rctx *RequestContext) (interface{}, er
return nil, err
}
rctx.audit.Log(rctx, ResourceID{}, "password changed (user)")
rctx.audit.Log(rctx, nil, "password changed (user)")
rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordChange, "password changed (user)")
return nil, err
}
......@@ -146,7 +146,7 @@ func (r *AccountRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error
if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil {
return nil, err
}
rctx.audit.Log(rctx, ResourceID{}, "password changed (account recovery)")
rctx.audit.Log(rctx, nil, "password changed (account recovery)")
rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordReset, "password changed (account recovery)")
return nil, nil
......@@ -184,7 +184,7 @@ func (r *ResetPasswordRequest) Serve(rctx *RequestContext) (interface{}, error)
if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil {
return nil, err
}
rctx.audit.Log(rctx, ResourceID{}, "password changed (admin)")
rctx.audit.Log(rctx, nil, "password changed (admin)")
rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordReset, "password changed (admin)")
return nil, nil
}
......@@ -333,7 +333,7 @@ func (r *EnableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
return nil, err
}
rctx.audit.Log(rctx, ResourceID{}, "totp enabled")
rctx.audit.Log(rctx, nil, "totp enabled")
return &EnableOTPResponse{
TOTPSecret: secret,
......@@ -350,7 +350,7 @@ func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
if err := rctx.User.disableOTP(rctx.Context, rctx.TX); err != nil {
return nil, err
}
rctx.audit.Log(rctx, ResourceID{}, "totp disabled")
rctx.audit.Log(rctx, nil, "totp disabled")
return nil, nil
}
......
......@@ -6,7 +6,7 @@ import (
)
type auditLogger interface {
Log(*RequestContext, ResourceID, string)
Log(*RequestContext, *Resource, string)
}
type auditLogEntry struct {
......@@ -20,7 +20,7 @@ type auditLogEntry struct {
type syslogAuditLogger struct{}
func (l *syslogAuditLogger) Log(rctx *RequestContext, rid ResourceID, what string) {
func (l *syslogAuditLogger) Log(rctx *RequestContext, rsrc *Resource, what string) {
e := auditLogEntry{
Message: what,
Comment: rctx.Comment,
......@@ -33,16 +33,16 @@ func (l *syslogAuditLogger) Log(rctx *RequestContext, rid ResourceID, what strin
}
// Fall back to resource from context if unspecified.
if rid.Empty() && rctx.Resource != nil {
rid = rctx.Resource.ID
if rsrc == nil && rctx.Resource != nil {
rsrc = rctx.Resource
}
if !rid.Empty() {
e.ResourceName = rid.Name()
e.ResourceType = rid.Type()
if rsrc != nil {
e.ResourceName = rsrc.Name
e.ResourceType = rsrc.Type
// TODO: redundant?
if u := rid.User(); u != "" {
e.User = u
}
//if u := rid.User(); u != "" {
// e.User = u
//}
}
if data, err := json.Marshal(&e); err == nil {
......
......@@ -6,6 +6,7 @@ import (
"strings"
as "git.autistici.org/ai3/accountserver"
"github.com/tstranex/u2f"
)
// Extend the AppSpecificPasswordInfo type, which only contains public
......@@ -89,3 +90,35 @@ func encodeUserEncryptionKeys(keys []*as.UserEncryptionKey) []string {
}
return out
}
func decodeU2FRegistration(enc string) (*as.U2FRegistration, error) {
var reg u2f.Registration
if err := reg.UnmarshalBinary([]byte(enc)); err != nil {
return nil, err
}
return &as.U2FRegistration{Registration: &reg}, nil
}
func encodeU2FRegistration(r *as.U2FRegistration) string {
// MarshalBinary can't fail, ignore error.
b, _ := r.MarshalBinary() // nolint
return string(b)
}
func decodeU2FRegistrations(encRegs []string) []*as.U2FRegistration {
var out []*as.U2FRegistration
for _, enc := range encRegs {
if r, err := decodeU2FRegistration(enc); err == nil {
out = append(out, r)
}
}
return out
}
func encodeU2FRegistrations(regs []*as.U2FRegistration) []string {
var out []string
for _, r := range regs {
out = append(out, encodeU2FRegistration(r))
}
return out
}
......@@ -9,7 +9,6 @@ import (
"time"
ldaputil "git.autistici.org/ai3/go-common/ldap"
"github.com/tstranex/u2f"
"gopkg.in/ldap.v2"
as "git.autistici.org/ai3/accountserver"
......@@ -78,37 +77,31 @@ func NewLDAPBackend(uri, bindDN, bindPw, base string) (as.Backend, error) {
return newLDAPBackendWithConn(pool, base)
}
func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
rsrc := newResourceRegistry()
rsrc.register(as.ResourceTypeEmail, &emailResourceHandler{baseDN: base})
rsrc.register(as.ResourceTypeMailingList, &mailingListResourceHandler{baseDN: base})
rsrc.register(as.ResourceTypeDAV, &webdavResourceHandler{baseDN: base})
rsrc.register(as.ResourceTypeWebsite, &websiteResourceHandler{baseDN: base})
rsrc.register(as.ResourceTypeDomain, &domainResourceHandler{baseDN: base})
rsrc.register(as.ResourceTypeDatabase, &databaseResourceHandler{baseDN: base})
func newLDAPBackendWithConn(conn ldapConn, baseDN string) (*backend, error) {
reg := newDefaultResourceRegistry(baseDN)
return &backend{
conn: conn,
baseDN: base,
baseDN: baseDN,
userQuery: &queryTemplate{
Base: joinDN("uid=${user}", "ou=People", base),
Base: joinDN("uid=${user}", "ou=People", baseDN),
Filter: "(objectClass=*)",
Scope: ldap.ScopeBaseObject,
},
userResourceQueries: []*queryTemplate{
// Find all resources that are children of the main uid object.
&queryTemplate{
Base: joinDN("uid=${user}", "ou=People", base),
Base: joinDN("uid=${user}", "ou=People", baseDN),
Scope: ldap.ScopeWholeSubtree,
},
// Find mailing lists, which are nested under a different root.
&queryTemplate{
Base: joinDN("ou=Lists", base),
Base: joinDN("ou=Lists", baseDN),
Filter: "(&(objectClass=mailingList)(listOwner=${user}))",
Scope: ldap.ScopeSingleLevel,
},
},
resources: rsrc,
resources: reg,
minUID: defaultMinUID,
maxUID: defaultMaxUID,
}, nil
......@@ -158,60 +151,32 @@ func userToLDAP(user *as.User) (attrs []ldap.PartialAttribute) {
{Type: "uid", Vals: s2l(user.Name)},
{Type: "cn", Vals: s2l(user.Name)},
{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(user.UID))},
{Type: "givenName", Vals: []string{"Private"}},
{Type: "sn", Vals: []string{"Private"}},
{Type: "givenName", Vals: s2l("Private")},
{Type: "sn", Vals: s2l("Private")},
{Type: "gecos", Vals: s2l(user.Name)},
{Type: "loginShell", Vals: []string{"/bin/false"}},
{Type: "homeDirectory", Vals: []string{"/var/empty"}},
{Type: passwordLastChangeLDAPAttr, Vals: []string{"12345"}},
{Type: "status", Vals: []string{user.Status}},
{Type: "host", Vals: []string{user.Shard}},
{Type: "shadowWarning", Vals: []string{"7"}},
{Type: "shadowMax", Vals: []string{"99999"}},
{Type: "loginShell", Vals: s2l("/bin/false")},
{Type: "homeDirectory", Vals: s2l("/var/empty")},
{Type: passwordLastChangeLDAPAttr, Vals: s2l("12345")},
{Type: "status", Vals: s2l(user.Status)},
{Type: "host", Vals: s2l(user.Shard)},
{Type: "shadowWarning", Vals: s2l("7")},
{Type: "shadowMax", Vals: s2l("99999")},
{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
{Type: u2fRegistrationsLDAPAttr, Vals: encodeU2FRegistrations(user.U2FRegistrations)},
}...)
return
}
func decodeU2FRegistration(enc string) (*as.U2FRegistration, error) {
var reg u2f.Registration
if err := reg.UnmarshalBinary([]byte(enc)); err != nil {
return nil, err
}
return &as.U2FRegistration{Registration: &reg}, nil
}
func encodeU2FRegistration(r *as.U2FRegistration) string {
// MarshalBinary can't fail, ignore error.
b, _ := r.MarshalBinary() // nolint
return string(b)
}
func decodeU2FRegistrations(encRegs []string) []*as.U2FRegistration {
var out []*as.U2FRegistration
for _, enc := range encRegs {
if r, err := decodeU2FRegistration(enc); err == nil {
out = append(out, r)
}
}
return out
}
func encodeU2FRegistrations(regs []*as.U2FRegistration) []string {
var out []string
for _, r := range regs {
out = append(out, encodeU2FRegistration(r))
}
return out
func (tx *backendTX) getUserDN(user *as.User) string {
return getUserDN(user, tx.backend.baseDN)
}
func (tx *backendTX) getUserDN(user *as.User) string {
return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
func getUserDN(user *as.User, baseDN string) string {
return joinDN("uid="+user.Name, "ou=People", baseDN)
}
// CreateUser creates a new user.
func (tx *backendTX) CreateUser(ctx context.Context, user *as.User) error {
func (tx *backendTX) CreateUser(ctx context.Context, user *as.User) (*as.User, error) {
dn := tx.getUserDN(user)
tx.create(dn)
......@@ -220,13 +185,13 @@ func (tx *backendTX) CreateUser(ctx context.Context, user *as.User) error {
}
// Create all resources.
for _, r := range user.Resources {
if err := tx.CreateResource(ctx, r); err != nil {
return err
}
rsrcs, err := tx.CreateResources(ctx, user, user.Resources)
if err != nil {
return nil, err
}
user.Resources = rsrcs
return nil
return user, nil
}
// UpdateUser updates values for the user only (not the resources).
......@@ -456,7 +421,7 @@ func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []as.FindRe
}
// GetResource returns a ResourceWrapper, as part of a read-modify-update transaction.
func (tx *backendTX) GetResource(ctx context.Context, rsrcID as.ResourceID) (*as.Resource, error) {
func (tx *backendTX) GetResource(ctx context.Context, rsrcID as.ResourceID) (*as.RawResource, error) {
// From the resource ID we can obtain the DN, and fetch it
// straight from LDAP without even doing a real search.
dn, err := tx.backend.resources.GetDN(rsrcID)
......@@ -485,25 +450,87 @@ func (tx *backendTX) GetResource(ctx context.Context, rsrcID as.ResourceID) (*as
return nil, err
}
// We know the resource type so we don't have to guess.
return tx.backend.resources.FromLDAPWithType(rsrcID.Type(), result.Entries[0])
rsrc, err := tx.backend.resources.FromLDAP(result