1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-27 13:48:49 +08:00
Mirko Teodorovic 8ea26c5ab7
NOISSUE - Add user groups (#1228)
* 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>
2020-09-23 23:18:53 +02:00

452 lines
11 KiB
Go

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
"github.com/lib/pq"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/users"
)
var (
errDeleteGroupDB = errors.New("delete group failed")
errSelectDb = errors.New("select group from db error")
errFK = "foreign_key_violation"
errInvalid = "invalid_text_representation"
errTruncation = "string_data_right_truncation"
)
var _ users.GroupRepository = (*groupRepository)(nil)
type groupRepository struct {
db Database
}
// NewGroupRepo instantiates a PostgreSQL implementation of group
// repository.
func NewGroupRepo(db Database) users.GroupRepository {
return &groupRepository{
db: db,
}
}
func (gr groupRepository) Save(ctx context.Context, group users.Group) (users.Group, error) {
var id string
q := `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata) VALUES (:name, :description, :id, :owner_id, :parent_id, :metadata) RETURNING id`
if group.ParentID == "" {
q = `INSERT INTO groups (name, description, id, owner_id, metadata) VALUES (:name, :description, :id, :owner_id, :metadata) RETURNING id`
}
dbu, err := toDBGroup(group)
if err != nil {
return users.Group{}, err
}
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return users.Group{}, errors.Wrap(users.ErrMalformedEntity, err)
case errDuplicate:
return users.Group{}, errors.Wrap(users.ErrGroupConflict, err)
}
}
return users.Group{}, errors.Wrap(users.ErrCreateGroup, err)
}
defer row.Close()
row.Next()
if err := row.Scan(&id); err != nil {
return users.Group{}, err
}
group.ID = id
return group, nil
}
func (gr groupRepository) Update(ctx context.Context, group users.Group) error {
q := `UPDATE groups SET(name, description, metadata) VALUES (:name, :description, :metadata) WHERE id = :id`
dbu, err := toDBGroup(group)
if err != nil {
return errors.Wrap(errUpdateDB, err)
}
if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
return errors.Wrap(errUpdateDB, err)
}
return nil
}
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
qd := `DELETE FROM groups WHERE id = :id`
dbg, err := toDBGroup(users.Group{ID: groupID})
if err != nil {
return errors.Wrap(errUpdateDB, err)
}
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
if cnt != 1 {
return errors.Wrap(users.ErrDeleteGroupMissing, err)
}
return nil
}
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
q := `SELECT id, name, owner_id, parent_id, description, metadata FROM groups WHERE id = $1`
dbu := dbGroup{
ID: id,
}
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return users.Group{}, errors.Wrap(users.ErrNotFound, err)
}
return users.Group{}, errors.Wrap(errRetrieveDB, err)
}
return toGroup(dbu), nil
}
func (gr groupRepository) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
q := `SELECT id, name, description, metadata FROM groups WHERE name = $1`
dbu := dbGroup{
Name: name,
}
if err := gr.db.QueryRowxContext(ctx, q, name).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return users.Group{}, errors.Wrap(users.ErrNotFound, err)
}
return users.Group{}, errors.Wrap(errRetrieveDB, err)
}
group := toGroup(dbu)
return group, nil
}
func (gr groupRepository) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
_, mq, err := getGroupsMetadataQuery(gm)
if err != nil {
return users.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
q := fmt.Sprintf(`WITH RECURSIVE subordinates AS (
SELECT id, owner_id, parent_id, name, description, metadata
FROM groups
WHERE id = :id
UNION
SELECT groups.id, groups.owner_id, groups.parent_id, groups.name, groups.description, groups.metadata
FROM groups
INNER JOIN subordinates s ON s.id = groups.parent_id %s
) SELECT * FROM subordinates ORDER BY id LIMIT :limit OFFSET :offset`, mq)
dbPage, err := toDBGroupPage("", groupID, offset, limit, gm)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []users.Group
for rows.Next() {
dbgr := dbGroup{}
if err := rows.StructScan(&dbgr); err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
gr := toGroup(dbgr)
if err != nil {
return users.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`WITH RECURSIVE subordinates AS (
SELECT id, owner_id, parent_id, name, description, metadata
FROM groups
WHERE id = :id
UNION
SELECT groups.id, groups.owner_id, groups.parent_id, groups.name, groups.description, groups.metadata
FROM groups
INNER JOIN subordinates s ON s.id = groups.parent_id %s
) SELECT COUNT(*) FROM subordinates`, mq)
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := users.GroupPage{
Groups: items,
PageMetadata: users.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
func (gr groupRepository) Memberships(ctx context.Context, userID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
m, mq, err := getGroupsMetadataQuery(gm)
if err != nil {
return users.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.user_id = :userID
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params := map[string]interface{}{
"userID": userID,
"limit": limit,
"offset": offset,
"metadata": m,
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []users.Group
for rows.Next() {
dbgr := dbGroup{}
if err := rows.StructScan(&dbgr); err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
gr := toGroup(dbgr)
if err != nil {
return users.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`SELECT COUNT(*)
FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.user_id = :userID %s;`, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := users.GroupPage{
Groups: items,
PageMetadata: users.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
func (gr groupRepository) Assign(ctx context.Context, userID, groupID string) error {
dbr, err := toDBGroupRelation(userID, groupID)
if err != nil {
return errors.Wrap(users.ErrAssignUserToGroup, err)
}
qIns := `INSERT INTO group_relations (group_id, user_id) VALUES (:group_id, :user_id)`
_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(users.ErrMalformedEntity, err)
case errDuplicate:
return errors.Wrap(users.ErrGroupConflict, err)
case errFK:
return errors.Wrap(users.ErrNotFound, err)
}
}
return errors.Wrap(users.ErrAssignUserToGroup, err)
}
return nil
}
func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
q := `DELETE FROM group_relations WHERE user_id = :user_id AND group_id = :group_id`
dbr, err := toDBGroupRelation(userID, groupID)
if err != nil {
return errors.Wrap(users.ErrNotFound, err)
}
if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
return errors.Wrap(users.ErrConflict, err)
}
return nil
}
type dbGroup struct {
ID string `db:"id"`
Name string `db:"name"`
OwnerID uuid.NullUUID `db:"owner_id"`
ParentID uuid.NullUUID `db:"parent_id"`
Description string `db:"description"`
Metadata dbMetadata `db:"metadata"`
}
type dbGroupPage struct {
ID uuid.NullUUID `db:"id"`
OwnerID uuid.NullUUID `db:"owner_id"`
ParentID uuid.NullUUID `db:"parent_id"`
Metadata dbMetadata `db:"metadata"`
Limit uint64
Offset uint64
Size uint64
}
func toUUID(id string) (uuid.NullUUID, error) {
var parentID uuid.NullUUID
if err := parentID.Scan(id); err != nil {
if id != "" {
return parentID, err
}
if err := parentID.Scan(nil); err != nil {
return parentID, err
}
}
return parentID, nil
}
func toDBGroup(g users.Group) (dbGroup, error) {
parentID := ""
if g.ParentID != "" {
parentID = g.ParentID
}
parent, err := toUUID(parentID)
if err != nil {
return dbGroup{}, err
}
owner, err := toUUID(g.OwnerID)
if err != nil {
return dbGroup{}, err
}
return dbGroup{
ID: g.ID,
Name: g.Name,
ParentID: parent,
OwnerID: owner,
Description: g.Description,
Metadata: g.Metadata,
}, nil
}
func toDBGroupPage(ownerID, groupID string, offset, limit uint64, metadata users.Metadata) (dbGroupPage, error) {
owner, err := toUUID(ownerID)
if err != nil {
return dbGroupPage{}, err
}
group, err := toUUID(groupID)
if err != nil {
return dbGroupPage{}, err
}
if err != nil {
return dbGroupPage{}, err
}
return dbGroupPage{
ID: group,
Metadata: dbMetadata(metadata),
OwnerID: owner,
Offset: offset,
Limit: limit,
}, nil
}
func toGroup(dbu dbGroup) users.Group {
return users.Group{
ID: dbu.ID,
Name: dbu.Name,
ParentID: dbu.ParentID.UUID.String(),
OwnerID: dbu.OwnerID.UUID.String(),
Description: dbu.Description,
Metadata: dbu.Metadata,
}
}
type dbGroupRelation struct {
Group uuid.UUID `db:"group_id"`
User uuid.UUID `db:"user_id"`
}
func toDBGroupRelation(userID, groupID string) (dbGroupRelation, error) {
group, err := uuid.FromString(groupID)
if err != nil {
return dbGroupRelation{}, err
}
user, err := uuid.FromString(userID)
if err != nil {
return dbGroupRelation{}, err
}
return dbGroupRelation{
Group: group,
User: user,
}, nil
}
func getGroupsMetadataQuery(m users.Metadata) ([]byte, string, error) {
mq := ""
mb := []byte("{}")
if len(m) > 0 {
mq = ` AND groups.metadata @> :metadata`
b, err := json.Marshal(m)
if err != nil {
return nil, "", err
}
mb = b
}
return mb, mq, nil
}
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
rows, err := db.NamedQueryContext(ctx, query, params)
if err != nil {
return 0, err
}
defer rows.Close()
total := uint64(0)
if rows.Next() {
if err := rows.Scan(&total); err != nil {
return 0, err
}
}
return total, nil
}