Update accounts API

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2020-06-15 16:02:28 +02:00
parent 81eef794c9
commit 16c5d13e10
6 changed files with 679 additions and 862 deletions
+9 -2
View File
@@ -17,9 +17,16 @@ var (
type RegisterFunc func(*config.Config) Manager
// Manager is an accounts service interface
// TODO email and username might not be unique?
type Manager interface {
// Read a record
Read(key string) (*proto.Record, error)
// Read a record by uuid
Read(uuid string) (*proto.Record, error)
// Read a record by username
ReadByUsername(username string) (*proto.Record, error)
// Read a record by email
ReadByEmail(email string) (*proto.Record, error)
// Read a record by identity (iss & sub)
ReadByIdentity(identity *proto.IdHistory) (*proto.Record, error)
// Write a record
Write(*proto.Record) (*proto.Record, error)
// List all records
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -22,9 +22,10 @@ message Payload {
}
message Account {
string uuid = 1;
StandardClaims standard_claims = 2;
repeated IdHistory identities = 3; // keep track of every identity of a given user
StandardClaims standard_claims = 1;
repeated IdHistory identities = 2; // keep track of every identity of a given user
string Issuer = 3;
string Password = 4;
}
// OIDC standard claims https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
+60 -6
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"github.com/owncloud/ocis-accounts/pkg/account"
"github.com/owncloud/ocis-accounts/pkg/config"
@@ -46,15 +47,68 @@ func (s Service) Set(c context.Context, req *proto.Record, res *proto.Record) er
}
// Get implements the AccountsServiceHandler interface
func (s Service) Get(c context.Context, req *proto.GetRequest, res *proto.Record) error {
func (s Service) Get(c context.Context, req *proto.GetRequest, res *proto.Record) (err error) {
// TODO implement other GetRequest properties: Identity, username&password, email
r, err := s.Manager.Read(req.GetUuid())
if err != nil {
return err
var r, ruuid, rname, rmail *proto.Record
if req.GetIdentity() != nil {
r, err = s.Manager.ReadByIdentity(req.GetIdentity())
if err != nil {
return err
}
}
if req.GetUuid() != "" {
ruuid, err = s.Manager.Read(req.GetUuid())
if err != nil {
return err
}
if r == nil {
r = ruuid
} else if r.Key != ruuid.Key {
r = nil
return errors.New("uuid mismatch")
}
}
if req.GetUsername() != "" {
rname, err = s.Manager.ReadByUsername(req.GetUsername())
if err != nil {
return err
}
if r == nil {
r = rname
} else if r.Key != rname.Key {
r = nil
return errors.New("username mismatch")
}
}
if req.GetEmail() != "" {
rmail, err = s.Manager.ReadByEmail(req.GetEmail())
if err != nil {
return err
}
if r == nil {
r = rmail
} else if r.Key != rmail.Key {
r = nil
return errors.New("email mismatch")
}
}
res.Payload = r.GetPayload()
return nil
if r != nil {
// TODO store only salted hash
if req.GetPassword() != "" {
if r.Payload.Account.Password != req.GetPassword() {
return errors.New("wrong password")
}
}
res.Key = r.Key
res.Payload = r.GetPayload()
// password never leaves
res.Payload.Account.Password = ""
return nil
}
return errors.New("at least one request param must be set")
}
// Search implements the AccountsServiceHandler interface
+84 -21
View File
@@ -8,6 +8,7 @@ import (
"os"
"path"
"path/filepath"
"strings"
// gproto "github.com/golang/protobuf/proto"
"github.com/owncloud/ocis-accounts/pkg/account"
@@ -20,6 +21,10 @@ var (
// StoreName is the default name for the accounts store
StoreName = "ocis-store"
managerName = "filesystem"
uuidSpace = "uuid"
usernameSpace = "username"
emailSpace = "email"
identitySpace = "identity"
emptyKeyError = "key cannot be empty"
)
@@ -53,7 +58,7 @@ func New(cfg *config.Config) account.Manager {
// List returns all the identities in the mountPath folder
func (s Store) List() ([]*proto.Record, error) {
records := []*proto.Record{}
identities, err := ioutil.ReadDir(s.mountPath)
identities, err := ioutil.ReadDir(path.Join(s.mountPath, uuidSpace))
if err != nil {
s.Logger.Err(err).Msgf("error reading %v", s.mountPath)
return nil, err
@@ -69,24 +74,10 @@ func (s Store) List() ([]*proto.Record, error) {
return records, nil
}
// NewRecord initializes a new record with the given options
func NewRecord(options ...Option) *proto.Record {
opts := NewOptions(options...)
return &proto.Record{
Payload: &proto.Payload{
Account: &proto.Account{
Uuid: opts.UUID,
},
},
}
}
// Read implements the store interface. This implementation only reads by id.
func (s Store) Read(key string) (*proto.Record, error) {
contents, err := ioutil.ReadFile(path.Join(s.mountPath, key))
func (s Store) readSpace(space string, subspace string, key string) (*proto.Record, error) {
contents, err := ioutil.ReadFile(path.Join(s.mountPath, space, subspace, key))
if err != nil {
s.Logger.Err(err).Msgf("error reading contents of key %v: file not found", key)
s.Logger.Err(err).Str("space", space).Str("subspace", subspace).Str("key", key).Msg("error reading record")
return nil, err
}
@@ -102,15 +93,45 @@ func (s Store) Read(key string) (*proto.Record, error) {
return rec, nil
}
// Read implements the store interface. This implementation only reads by id.
func (s Store) Read(uuid string) (*proto.Record, error) {
return s.readSpace(uuidSpace, "", uuid)
}
// ReadByUsername implements the store interface. This implementation only reads by username.
func (s Store) ReadByUsername(username string) (*proto.Record, error) {
return s.readSpace(usernameSpace, "", username)
}
// ReadByEmail implements the store interface. This implementation only reads by email.
func (s Store) ReadByEmail(email string) (*proto.Record, error) {
i := strings.LastIndex(email, "@")
if i < 0 {
// no domain part
return s.readSpace(emailSpace, "local", email)
}
return s.readSpace(emailSpace, email[:i], email[i:])
}
// ReadByIdentity implements the store interface. This implementation only reads by iss & sub.
func (s Store) ReadByIdentity(identity *proto.IdHistory) (*proto.Record, error) {
if identity.Iss == "" {
s.Logger.Error().Msg("iss cannot be empty")
return nil, fmt.Errorf(emptyKeyError)
}
return s.readSpace(identitySpace, identity.Iss, identity.Sub)
}
// Write implements the store interface
func (s Store) Write(rec *proto.Record) (*proto.Record, error) {
path := filepath.Join(s.mountPath, rec.Key)
if len(rec.Key) < 1 {
s.Logger.Error().Msg("key cannot be empty")
return nil, fmt.Errorf(emptyKeyError)
}
path := filepath.Join(s.mountPath, uuidSpace, rec.Key)
contents, err := json.Marshal(rec)
if err != nil {
s.Logger.Err(err).Msg("record could not be marshaled")
@@ -120,8 +141,50 @@ func (s Store) Write(rec *proto.Record) (*proto.Record, error) {
if err := ioutil.WriteFile(path, contents, 0644); err != nil {
return nil, err
}
s.Logger.Info().Int("bytes", len(contents)).Str("path", path).Msg("wrote account")
// write symlinks for other lookups
// use hardlinks?
// TODO what if target already exists? use dirs with multiple symlinks to uuid?
// TODO what if username or email changes? use uuid folder with a file per property? srsly ... we should use a proper storage for this
if rec.Payload != nil {
if rec.Payload.Account != nil {
if rec.Payload.Account.StandardClaims != nil {
if rec.Payload.Account.StandardClaims.PreferredUsername != "" {
p := filepath.Join(s.mountPath, usernameSpace, rec.Payload.Account.StandardClaims.PreferredUsername)
os.MkdirAll(filepath.Dir(p), 0700)
os.Symlink(path, p)
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no preferred username in standard claims")
}
if rec.Payload.Account.StandardClaims.Email != "" {
p := filepath.Join(s.mountPath, emailSpace, rec.Payload.Account.StandardClaims.Email)
os.MkdirAll(filepath.Dir(p), 0700)
os.Symlink(path, p)
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no email in standard claims")
}
if rec.Payload.Account.Issuer != "" {
if rec.Payload.Account.StandardClaims.Sub != "" {
p := filepath.Join(s.mountPath, identitySpace, rec.Payload.Account.Issuer, rec.Payload.Account.StandardClaims.Sub)
os.MkdirAll(filepath.Dir(p), 0700)
os.Symlink(path, p)
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no sub in standard claims")
}
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no issuer in account")
}
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no standard claims in account")
}
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no account in payload")
}
} else {
s.Logger.Warn().Str("uuid", rec.Key).Msg("has no payload")
}
s.Logger.Info().Msgf("%v bytes written to %v", len(contents), path)
return rec, nil
}