Update accounts API
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
@@ -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
|
||||
|
||||
+517
-830
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user