Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NOISSUE - Add user groups #1228

Merged
merged 76 commits into from
Sep 23, 2020
Merged

NOISSUE - Add user groups #1228

merged 76 commits into from
Sep 23, 2020

Conversation

mteodor
Copy link
Contributor

@mteodor mteodor commented Aug 21, 2020

What does this do?

Introducing groups for user, this is related to https://github.com/mainflux/mainflux/issues/649

List any changes that modify/break current functionality

None

Have you included tests for your changes?

Yes

@mteodor mteodor requested a review from a team as a code owner August 21, 2020 11:24
@mteodor mteodor marked this pull request as draft August 21, 2020 11:24
@manuio
Copy link
Contributor

manuio commented Sep 7, 2020

@mteodor CI is failing. Please, use this issue as reference: https://github.com/mainflux/mainflux/issues/649


var cmdGroups = []cobra.Command{
cobra.Command{
Use: "create",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the full command? group create ...? Would it not be confusing when we add other entities groups (things, channels)?

groupName := c.adminGroup

_, err := userRepo.RetrieveByEmail(context.Background(), user.Email, false)
if err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RetrieveByEmail actually fails when there is no user? I did not know that, I though it returns empty result...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I see below that you are using users.ErrNotFound, so please be consistent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, inline the check

@@ -28,6 +28,7 @@ func TestCreateChannel(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Group of what? Users? What about Things?

users/README.md Outdated
@@ -31,6 +31,8 @@ default values.
| MF_USERS_HTTP_PORT | Users service HTTP port | 8180 |
| MF_USERS_SERVER_CERT | Path to server certificate in pem format | |
| MF_USERS_SERVER_KEY | Path to server key in pem format | |
| MF_USERS_ADMIN_EMAIL | Default user, created on startup | |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please align text nicely

@@ -292,10 +309,60 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServi
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)

if err := createAdmin(svc, userRepo, groupRepo, c); err != nil {
logger.Error("failed to create admin user:" + err.Error())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can use Wrap here. Otherwise add a white space after :

users/service.go Outdated
return svc.groups.Save(ctx, group)
}

func (svc usersService) ListGroups(ctx context.Context, token string, offset, limit uint64, name string, meta Metadata) (GroupPage, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who can assign user to the group?
Who can remove the user from the group?
Who can delete a group?

I saw you have owner checking only in GET. What's the idea for other operations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

access rights will be introduced with policies

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I was confused with the owner checking in RetrieveAll.
Maybe policies should handle this also, I mean if you have read in policies that mean that I can retrieve all groups where I am assigned. Not sure how this should work together, the thing to think about :)

if _, err := ur.db.NamedExecContext(ctx, q, dbu); err != nil {
return errors.Wrap(errUpdateUserDB, err)
}

return nil
}

func (ur userRepository) RetrieveByEmail(ctx context.Context, email string) (users.User, error) {
func (ur userRepository) RetrieveByEmail(ctx context.Context, email string, groups bool) (users.User, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this group retrieves? Do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, that was my initial idea to have a flag whether you want to retrieve all groups that user belongs along
but I made a methods RetrieveAllGroupsForUser and RetrieveAllUsersForGroup

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Yes, I also think that we don't need it now, its better to decouple.

users/service.go Outdated
return svc.groups.Save(ctx, group)
}

func (svc usersService) ListGroups(ctx context.Context, token string, offset, limit uint64, name string, meta Metadata) (GroupPage, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I was confused with the owner checking in RetrieveAll.
Maybe policies should handle this also, I mean if you have read in policies that mean that I can retrieve all groups where I am assigned. Not sure how this should work together, the thing to think about :)

groupName := c.adminGroup

_, err := userRepo.RetrieveByEmail(context.Background(), user.Email, false)
if err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, inline the check

@codecov-commenter
Copy link

codecov-commenter commented Sep 12, 2020

Codecov Report

Merging #1228 into master will decrease coverage by 6.32%.
The diff coverage is 30.13%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1228      +/-   ##
==========================================
- Coverage   74.36%   68.03%   -6.33%     
==========================================
  Files         106      108       +2     
  Lines        6159     7136     +977     
==========================================
+ Hits         4580     4855     +275     
- Misses       1194     1850     +656     
- Partials      385      431      +46     
Impacted Files Coverage Δ
pkg/sdk/go/groups.go 0.00% <0.00%> (ø)
things/api/auth/http/requests.go 100.00% <ø> (ø)
users/api/logging.go 0.00% <0.00%> (ø)
users/api/metrics.go 0.00% <0.00%> (ø)
users/users.go 78.94% <ø> (ø)
pkg/sdk/go/users.go 17.58% <17.85%> (-3.26%) ⬇️
users/api/requests.go 43.13% <18.51%> (-27.70%) ⬇️
users/api/endpoint.go 41.36% <26.66%> (-41.18%) ⬇️
users/service.go 58.51% <32.20%> (-23.76%) ⬇️
users/postgres/groups.go 32.24% <32.24%> (ø)
... and 10 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 043d1e0...b12dcbb. Read the comment docs.

@mteodor mteodor marked this pull request as ready for review September 14, 2020 15:08
@@ -103,6 +103,8 @@ services:
POSTGRES_DB: ${MF_USERS_DB}
networks:
- mainflux-base-net
ports:
- "6001:5432"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you maybe use ENVARS here? It's hard to understand this values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is mistake, will remove this, only for development purposes

users/users.go Outdated
RetrieveByEmail(ctx context.Context, email string) (User, error)

// RetrieveByID retrieves user by its unique identifier ID.
// If groups = TRUE retrieve groups for given User
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this comment?

users/users.go Outdated

// Update updates the user metadata.
UpdateUser(ctx context.Context, u User) error

// RetrieveByEmail retrieves user by its unique identifier (i.e. email).
// If groups = TRUE retrieve groups for given User
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this comment?

users/service.go Outdated
}

return svc.users.RetrieveAllForGroup(ctx, groupID, offset, limit, um)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this blank line

users/service.go Outdated
ErrUserAlreadyAssigned = errors.New("user is already assigned to a group")

// ErrAssignUserToGroup indicates an error in assigning user to a group.
ErrAssignUserToGroup = errors.New("error assigning user to a group")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, avoid error in an error message. Use Failed to instead.

Comment on lines 163 to 165
q := fmt.Sprintf(`SELECT u.id, u.email, u.metadata FROM users u, group_relations g
WHERE u.id = g.user_id AND g.group_id = :group
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look properly aligned

users/postgres/users.go Show resolved Hide resolved
Comment on lines 53 to 54
email VARCHAR(254) PRIMARY KEY,
password CHAR(60) NOT NULL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look properly aligned

users/groups.go Outdated
// AssignUser adds user to group.
AssignUser(ctx context.Context, userID, groupID string) error

// RemoveUser
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete this comment

@@ -24,6 +24,6 @@ func New(url string, c *email.Config) (users.Emailer, error) {

func (e *emailer) SendPasswordReset(To []string, host string, token string) error {
url := fmt.Sprintf("%s%s?token=%s", host, e.resetURL, token)
content := fmt.Sprintf("%s", url)
content := url
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need content anymore?

return ms.svc.ListGroups(ctx, token, id, offset, limit, meta)
}

func (ms *metricsMiddleware) ListUsersInGroup(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (users.UserPage, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be ListGroupUsers

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manuio ListUsersGroup doesn't it sound more logical? Following work orders List Users Group e.g List all users group. Detail ;)

Copy link
Contributor

@manuio manuio Sep 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmarcetic I don't think so :) We want users that are in a group not the group belonging to users.
We can keep this naming to avoid confusions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm @manuio Yea, that makes more sense :)

@dborovcanin dborovcanin changed the title NOISSUE - Adding user groups NOISSUE - Add user groups Sep 15, 2020
@@ -99,21 +107,26 @@ const (
envAuthnCACerts = "MF_AUTHN_CA_CERTS"
envAuthnURL = "MF_AUTHN_GRPC_URL"
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"

defaultAdminGroupDescription = "Mainflux default admin group"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this with other def values?

return tp, nil
}

func (sdk mfSDK) UsersGroups(token string, offset, limit uint64, id string) (UsersGroupsPage, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what would fit better here, this method will list Groups, later we will introduce groups for things so we will have ThingsGroups, if I use User instead of Users may sound like list Groups for certain user

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mteodor I was thinking that you will split user groups to users and thing groups to things. Not making pkg/sdk/go/groups

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was clearer to me have groups separate as a separate entity, similar to the channels.
there is a possibility that we will actually reuse these groups for things as well, maybe just adding type to the group entity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its something that we should agree, I know about this idea from ~ 1 year ago :) at least. It's still not documented and you started to work in freestyle mode, not really :) But you get the point.
@mteodor I think we need new issue like Generic groups and there discuss about this and get a clear vision. It will helps a lot if we brainstorm

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'm not sure about the idea. However, if we decide to create generic groups, it starts to look like a new service, which I am skeptical about.

return ms.svc.ListGroups(ctx, token, id, offset, limit, meta)
}

func (ms *metricsMiddleware) ListUsersInGroup(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (users.UserPage, error) {
Copy link
Contributor

@manuio manuio Sep 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmarcetic I don't think so :) We want users that are in a group not the group belonging to users.
We can keep this naming to avoid confusions.

return tp, nil
}

func (sdk mfSDK) UsersGroups(token string, offset, limit uint64, id string) (UsersGroupsPage, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what would fit better here, this method will list Groups, later we will introduce groups for things so we will have ThingsGroups, if I use User instead of Users may sound like list Groups for certain user

Metadata map[string]interface{} `json:"metadata,omitempty"`
ID string `json:"id,omitempty"`
Email string `json:"email,omitempty"`
UsersGroups []string `json:"groups,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would simply use Groups or GroupsIDs

Comment on lines 164 to 170
AssignUserToGroup(userID, groupID, token string) error

// RemoveUserFromGroup
RemoveUserFromGroup(userID, groupID, token string) error

// ListUsersForGroup
ListUsersForGroup(groupID, token string, offset, limit uint64) (UsersPage, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This naming is not consistent with the rest. I would use CreateGroupUser, RemoveGroupUser and GroupUsers

}

return nil

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this blank line please

endpoint := fmt.Sprintf("%s/%s/users?offset=%d&limit=%d&", groupsEndpoint, groupID, offset, limit)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)

fmt.Println(url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this log

Comment on lines 48 to 50
} else {
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata) VALUES (:name, :description, :id, :owner_id, :parent_id, :metadata) RETURNING id`
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid using else.

Comment on lines 52 to 46
if group.ID == "" || group.Name == "" || !groupRegexp.MatchString(group.Name) {
return users.Group{}, users.ErrMalformedEntity
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be done on the service layer.

}

func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
tx, err := gr.db.BeginTxx(context.Background(), nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this check introduces a lot of complexity only to check if a group is empty. Leaving this check to the user and removing it from the code would simplify it. If there is a need, we can add it later.

Comment on lines 303 to 330
tx, err := gr.db.BeginTxx(context.Background(), nil)
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
defer func() {
if err != nil {
if txErr := tx.Rollback(); txErr != nil {
err = errors.Wrap(err, errors.Wrap(errTransRollback, txErr))
}
}

if err = tx.Commit(); err != nil {
err = errors.Wrap(errDeleteGroupDB, err)
}
}()

q := `SELECT COUNT(*) FROM group_relations WHERE group_id = :group_id AND user_id = :user_id ;`
dbr, err := toDBGroupRelation(userID, groupID)
if err != nil {
return errors.Wrap(users.ErrAssignUserToGroup, err)
}
tot, err := total(ctx, gr.db, q, dbr)
if err != nil {
return errors.Wrap(users.ErrAssignUserToGroup, err)
}
if tot > 0 {
return errors.Wrap(users.ErrUserAlreadyAssigned, err)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same goes here. We can simply try to insert, and examine an error to decide what went wrong.

Comment on lines 450 to 451
Group uuid.NullUUID `db:"group_id"`
User uuid.NullUUID `db:"user_id"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why NullID if it can't be null by the schema?

.env Outdated Show resolved Hide resolved
cli/groups.go Outdated Show resolved Hide resolved
users/api/responses.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@dborovcanin dborovcanin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is failing, please fix it.

return svc
}

func createAdmin(svc users.Service, userRepo users.UserRepository, groupRepo users.GroupRepository, c config) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how you avoid exposing a new method in the service interface, but use existing methods from repo and service layers.

return nil
}

func (sdk mfSDK) AddGroupUser(userID, groupID, token string) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming this, since it's not clear what this method does. Is it assigning a user to a group?

return nil
}

func (sdk mfSDK) RemoveGroupUser(userID, groupID, token string) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

return nil
}

func (sdk mfSDK) GroupUsers(groupID, token string, offset, limit uint64) (UsersPage, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ListUsersByGroup sounds less ambiguous (even though it does not sound well), since this method does not group users, but lists users that belong to the group.

return tp, nil
}

func (sdk mfSDK) UsersGroups(token string, offset, limit uint64, id string) (UsersGroupsPage, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'm not sure about the idea. However, if we decide to create generic groups, it starts to look like a new service, which I am skeptical about.


func (ms *metricsMiddleware) ListUserGroups(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (users.GroupPage, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_groups_for_user").Add(1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"list_user_groups"

users/groups.go Outdated
AssignUser(ctx context.Context, userID, groupID string) error

// RemoveUser removes user from group
RemoveUser(ctx context.Context, userID, groupID string) error
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using unassing as the opposite of assign or add as the opposite of remove. I prefer assign/unassign.

users/mocks/groups.go Show resolved Hide resolved
// Database provides a database interface
type Database interface {
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
NamedQueryContext(context.Context, string, interface{}) (*sqlx.Rows, error)
GetContext(context.Context, interface{}, string, ...interface{}) error
BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you removed the code that uses this method, you can remove it from the interface and implementation, as well.

users/users.go Outdated
@@ -54,16 +61,22 @@ func (u User) Validate() error {
type UserRepository interface {
// Save persists the user account. A non-nil error is returned to indicate
// operation failure.
Save(ctx context.Context, u User) error
Save(ctx context.Context, u User) (User, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to return the User? We usually only need an ID of the created entity.

cli/groups.go Outdated Show resolved Hide resolved
cmd/cli/main.go Outdated
@@ -20,6 +20,7 @@ func main() {
CertsURL: "http://localhost:8204",
ReaderPrefix: "",
UsersPrefix: "",
GroupsPrefix: "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please align

@@ -65,6 +65,7 @@ func TestCreateThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be aligned

@@ -125,6 +126,7 @@ func TestCreateThings(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@@ -147,3 +156,33 @@ func (sdk mfSDK) UpdatePassword(oldPass, newPass, token string) error {

return nil
}
func (sdk mfSDK) UserGroups(userID, token string, offset, limit uint64) (GroupsPage, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are calling everything Groups in SDK, probably you slhould leave Groups() here as well - especially that this is users.go file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for listing groups for user, therefore it is UserGroups

return lm.svc.UnassignUserFromGroup(ctx, token, userID, groupID)
}

func (lm *loggingMiddleware) ListUserGroups(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (e users.GroupPage, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be ListGroups(), in order to be consistent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this lists groups that user is assigned to

}

// GroupRepositoryMiddleware tracks request and their latency, and adds spans
// to context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to upper line - ty to keep all comments in one line if it is not longer than code below.

return grm.repo.RetrieveByName(ctx, name)
}

func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this line - it's too long

return grm.repo.RetrieveAll(ctx, groupID, offset, limit, gm)
}

func (grm groupRepositoryMiddleware) RetrieveAllForUser(ctx context.Context, userID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this line

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memberships()?

@@ -68,6 +75,14 @@ func (urm userRepositoryMiddleware) UpdatePassword(ctx context.Context, email, p
return urm.repo.UpdatePassword(ctx, email, password)
}

func (urm userRepositoryMiddleware) RetrieveAllForGroup(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.UserPage, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this line

removeUser = "remove_user_from_group"
)

var _ users.GroupRepository = (*groupRepositoryMiddleware)(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Middleware suffix aligned to what we used in other services? It makes variable name pretty long. Do we need it at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to : _ things.ChannelRepository = (*channelRepositoryMiddleware)(nil)

cli/README.md Outdated
```bash
mainflux-cli groups get all <user_auth_token>
```
### List children groups for some group
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like `shows group with provided group ID`` command is missing

cli/groups.go Outdated
Comment on lines 26 to 27
ParentID - ID of a group that is a parent
to the creating group
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need 2 lines here?

cli/README.md Outdated
```
### List groups user belongs to
```bash
mainflux-cli users groups <user_id> <user_auth_token>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a users or groups command?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is users command

Copy link
Contributor

@manuio manuio Sep 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's defined with Group commands?

cli/groups.go Outdated
cmd := cobra.Command{
Use: "groups",
Short: "Groups management",
Long: `Groups management: create accounts and tokens"`,
Copy link
Contributor

@manuio manuio Sep 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create accounts and tokens?

envAdminPassword = "MF_USERS_ADMIN_PASSWORD"
envAdminGroup = "MF_USERS_ADMIN_GROUP"

envJaegerURL = "MF_JAEGER_URL"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why envJaegerURL is defined here?

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
@@ -147,3 +156,33 @@ func (sdk mfSDK) UpdatePassword(oldPass, newPass, token string) error {

return nil
}
func (sdk mfSDK) Memberships(userID, token string, offset, limit uint64) (GroupsPage, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an empty line before this/after the previous method.

Comment on lines 219 to 228
req := request.(listUserGroupReq)
if err := req.validate(); err != nil {
return users.UserPage{}, err
}

up, err := svc.Members(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
if err != nil {
return users.UserPage{}, err
}
return buildUsersResponse(up), nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please be consistent when adding empty lines. I don't mind any combination of

    request typ eassert
    errcheck
    <AN EMPTY LINE>
    svc method call
    errcheck
    <AN EMTPY LINE>
    prepare return
    <AN EMPTY LINE>
    return result

You can have all the empty lines, some of them, or none of them. As long as you're consistent, each option is OK for me. Please check all the other places in the code where this can be an issue.

Comment on lines 10 to 11
const minPassLen = 8

type apiReq interface {
validate() error
}
const maxNameSize = 1024
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Group:

const (
     minPassLen  = 8
     maxNameLen = 1024
)

Comment on lines 103 to 111
if req.token == "" {
return users.ErrUnauthorizedAccess
}

if len(req.Name) > maxNameSize || req.Name == "" {
return users.ErrMalformedEntity
}

return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same remark for empty lines in validate methods.

users/api/transport.go Show resolved Hide resolved
@@ -87,6 +105,69 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer, l log.Logger) htt
opts...,
))

mux.Post("/users/groups", kithttp.NewServer(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need /users prefix here. I assume that you had Things Groups in mind, but this can be resolved in LB or internal Nginx. After all, it's two services.

users/groups.go Outdated
// Update updates the group data.
Update(ctx context.Context, g Group) error

// Delete deletes group for given id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either add . at the end of the comment or not. @drasko And I don't agree around this (I'm for it, he's not). Just be consistent, whatever you choose.

return group, nil
}

func (gr groupRepository) RetrieveAll(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming to RetrieveWithAncestors or something like that.

items = append(items, gr)
}

cq := fmt.Sprintf(`WITH RECURSIVE subordinates AS (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need this. I prefer just adding a limit to the depth to some arbitrary number (10?) and return the list with no need for pagination and complications. What do you think? I doubt that we'll ever need more than 10-15 ancestors, and we don't need pagination for that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies that we'll need this COUNT to check the depth when adding a group.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it works like a charm

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to keep it like this? I'm perfectly fine with it. @mainflux/maintainers Your opinion? It's ancestors pagination VS ancestors depth limit.

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Comment on lines +150 to +151
Use: "membership",
Short: "membership <user_id> <user_auth_token>",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memberships

cobra.Command{
Use: "membership",
Short: "membership <user_id> <user_auth_token>",
Long: `List user groups membership`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Short: "Groups management",
Long: `Groups management: create groups and assigns user to groups"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("Usage: Groups [create | get | delete | assign | unassign | members | membership]")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you change previous one be careful here

}

func toUser(dbu dbUser) users.User {
func toUser(dbu dbUser) (users.User, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function called toUser()? What does this to prefix mean?

Similarly - why is toDBUser() called like this?

Maybe there is a reason, but it is cryptic - if it is valid please put a comment above function names.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@drasko drasko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@drasko drasko merged commit 8ea26c5 into absmach:master Sep 23, 2020
@drasko
Copy link
Contributor

drasko commented Sep 23, 2020

Congrats on this one. Thanks a lot @mteodor for great contribution!

@nmarcetic
Copy link
Collaborator

@mteodor In this impl, Is there something specific with this relation group <-> user (except owner ID)? This would be much simple in the future impl if you allow grouping of different entities. It will simplify access control a lot and general usage.
I would be able to group users, things, and channels in the same group (like floor 1, edge location 1, etc...). You did impl really generic, looks doable with little or no effort at all :) It will also simplify the code base. What do you think, Am I am missing something?

@mteodor
Copy link
Contributor Author

mteodor commented Sep 25, 2020

there is associationt table group_relations which keeps info on user assignment.
we could use that same table to insert things, and channels
i also think is doable, a little problem may be that things and users are different service but I guess that we make grpc endpoints that could glue all that.

@dborovcanin
Copy link
Collaborator

Groups are usually used for access control and Authorization service looks like a good candidate if we decide to make groups generic.

@drasko
Copy link
Contributor

drasko commented Sep 26, 2020

I agree - this should be generalized.

@nmarcetic
Copy link
Collaborator

Great! Let's think about design and refactor in the next phase.

manuio pushed a commit that referenced this pull request Oct 12, 2020
* adding group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding user group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add retrieve methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add default admin user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add default admin user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* changes signature for AssignUser

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* bug fixing retrieving groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unused code

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* bug fixing retrieving groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* retrieve groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change environment for admin

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change environment for admin

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* retrieve groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove adding default group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* expose port for debugging purposes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix tests, and linter errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add prefix Users for groups endpoint

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix linter problems

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix endpoint prefix url

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix endpoint test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add group features in cli

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove println

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* when user is created return id in response

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* when user is created return id in response

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding default admin env

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* proper alignment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* proper alignment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename  method

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* return user id when created

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* return user id when created

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unused variable

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix to retrieve whole tree starting from parent

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add endpoint to list groups for user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add readme for groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fixing bugs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fixing bugs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add group commands for add and remove user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace default email, use example.com

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix capital letters beginning of sentence

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove warning for deprecated api, mistakenly copied

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* simplify repo methods, rely on db driver rather than the check before operation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* check if group is valid

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* openapi spec 3.0

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove check for existing users in groups before delete

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change func signature

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change func signature

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bugs, resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bugs, resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix alignment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add missing command

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* reorganize envs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix doc

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix compile

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* reorganize cli commands

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* minor corrections

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix naming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments, minor changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants