[Bps-public-commit] rt-extension-rest2 branch, add-group-members-user-memberships, created. 1.04-15-g76d7951

? sunnavy sunnavy at bestpractical.com
Wed Oct 31 16:38:38 EDT 2018


The branch, add-group-members-user-memberships has been created
        at  76d795106ff817a736291516866411eb9437b821 (commit)

- Log -----------------------------------------------------------------
commit 6caf780aa31ea2bfecfe88bfd79a34c9cbde5527
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 10:29:49 2018 +0200

    Add POST /group/ to create a group

diff --git a/lib/RT/Extension/REST2/Resource/Group.pm b/lib/RT/Extension/REST2/Resource/Group.pm
index 14910cf..d4e22fa 100644
--- a/lib/RT/Extension/REST2/Resource/Group.pm
+++ b/lib/RT/Extension/REST2/Resource/Group.pm
@@ -9,6 +9,7 @@ use RT::Extension::REST2::Util qw(expand_uid);
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable'
         => { -alias => { serialize => '_default_serialize' } },
+    'RT::Extension::REST2::Resource::Record::Writable',
     'RT::Extension::REST2::Resource::Record::Hypermedia'
         => { -alias => { hypermedia_links => '_default_hypermedia_links' } };
 
diff --git a/lib/RT/Extension/REST2/Resource/Record/Writable.pm b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
index aa548dd..b7eb98f 100644
--- a/lib/RT/Extension/REST2/Resource/Record/Writable.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
@@ -284,7 +284,8 @@ sub create_record {
         }
     }
 
-    my ($ok, @rest) = $record->Create(%args);
+    my $method = $record->isa('RT::Group') ? 'CreateUserDefinedGroup' : 'Create';
+    my ($ok, @rest) = $record->$method(%args);
 
     if ($ok && $cfs) {
         $self->_update_custom_fields($cfs);

commit 353de0d045aa502e85cbe74797a00b238ecabd57
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 10:30:42 2018 +0200

    Add DELETE /group/:id to disable a group

diff --git a/lib/RT/Extension/REST2/Resource/Group.pm b/lib/RT/Extension/REST2/Resource/Group.pm
index d4e22fa..c32a7e4 100644
--- a/lib/RT/Extension/REST2/Resource/Group.pm
+++ b/lib/RT/Extension/REST2/Resource/Group.pm
@@ -9,6 +9,7 @@ use RT::Extension::REST2::Util qw(expand_uid);
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable'
         => { -alias => { serialize => '_default_serialize' } },
+    'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
     'RT::Extension::REST2::Resource::Record::Writable',
     'RT::Extension::REST2::Resource::Record::Hypermedia'
         => { -alias => { hypermedia_links => '_default_hypermedia_links' } };

commit 42110ddd4daed62ddc15ce2da930df87a0756ffb
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 10:45:39 2018 +0200

    Add Disabled in returned properties of a group with GET /group/:id

diff --git a/lib/RT/Extension/REST2/Resource/Group.pm b/lib/RT/Extension/REST2/Resource/Group.pm
index c32a7e4..fe47e7d 100644
--- a/lib/RT/Extension/REST2/Resource/Group.pm
+++ b/lib/RT/Extension/REST2/Resource/Group.pm
@@ -34,6 +34,8 @@ sub serialize {
         @{ $self->record->MembersObj->ItemsArrayRef }
     ];
 
+    $data->{Disabled} = $self->record->PrincipalObj->Disabled;
+
     return $data;
 }
 

commit 40968cf0c08030b51aca18c6f4fd45231b47d061
Author: gibus <gibus at easter-eggs.com>
Date:   Thu Sep 20 15:16:44 2018 +0200

    Fix permissions for group endpoints

diff --git a/lib/RT/Extension/REST2/Resource/Group.pm b/lib/RT/Extension/REST2/Resource/Group.pm
index fe47e7d..8235fa0 100644
--- a/lib/RT/Extension/REST2/Resource/Group.pm
+++ b/lib/RT/Extension/REST2/Resource/Group.pm
@@ -10,7 +10,9 @@ extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable'
         => { -alias => { serialize => '_default_serialize' } },
     'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
+        => { -alias => { delete_resource => '_delete_resource' } },
     'RT::Extension::REST2::Resource::Record::Writable',
+        => { -alias => { create_record => '_create_record' } },
     'RT::Extension::REST2::Resource::Record::Hypermedia'
         => { -alias => { hypermedia_links => '_default_hypermedia_links' } };
 
@@ -46,6 +48,34 @@ sub hypermedia_links {
     return $links;
 }
 
+sub create_record {
+    my $self = shift;
+    my $data = shift;
+
+    return (\403, $self->record->loc("Permission Denied"))
+        unless  $self->current_user->HasRight(
+            Right   => "AdminGroup",
+            Object  => RT->System,
+        );
+
+    return $self->_create_record($data);
+}
+
+sub delete_resource {
+    my $self = shift;
+
+    return (\403, $self->record->loc("Permission Denied"))
+        unless $self->record->CurrentUserHasRight('AdminGroup');
+
+    return $self->_delete_resource;
+}
+
+sub forbidden {
+    my $self = shift;
+    return 0 unless $self->record->id;
+    return !$self->record->CurrentUserHasRight('SeeGroup');
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;

commit 4092a6124a49276f7cbfaba01c3daf40c2857e80
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 10:49:17 2018 +0200

    Add setting/unsetting Disabled for a user or a group with PUT /user/:id and PUT /group/:id

diff --git a/lib/RT/Extension/REST2/Resource/Record/Writable.pm b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
index b7eb98f..6d04d3b 100644
--- a/lib/RT/Extension/REST2/Resource/Record/Writable.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
@@ -54,6 +54,8 @@ sub update_record {
 
     push @results, $self->_update_custom_fields($data->{CustomFields});
     push @results, $self->_update_role_members($data);
+    push @results, $self->_update_disabled($data->{Disabled})
+      unless grep { $_ eq 'Disabled' } $self->record->WritableAttributes;
 
     # XXX TODO: Figure out how to return success/failure?  Core RT::Record's
     # ->Update will need to be replaced or improved.
@@ -248,6 +250,22 @@ sub _update_role_members {
     return @results;
 }
 
+sub _update_disabled {
+    my $self = shift;
+    my $data = shift;
+    my @results;
+
+    my $record = $self->record;
+    return unless defined $data and $data =~ /^[01]$/;
+
+    return unless $record->can('SetDisabled');
+
+    my ($ok, $msg) = $record->SetDisabled($data);
+    push @results, $msg;
+
+    return @results;
+}
+
 sub update_resource {
     my $self = shift;
     my $data = shift;

commit a0bf988b9034eef920df7ee20d568040da7ef626
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 11:18:24 2018 +0200

    Add endpoints to operate on group members
    
    * Getting direct members of a group: GET /group/:id/members
    * Getting direct and undirect members of a group: GET /group/:id/members?recursively=1
    * Getting user members of a group: GET /group/:id/members?groups=0&recursively=[01]
    * Getting group members of a group: GET /group/:id/members?users=0&recursively=[01]
    * Removing a member from a group: DELETE /group/:id/member/:id
    * Removing all members of a group: DELETE /group/:id/members
    * Adding members to a group: PUT /group/:id/members passing JSON array of principals ids

diff --git a/lib/RT/Extension/REST2/Resource/GroupMembers.pm b/lib/RT/Extension/REST2/Resource/GroupMembers.pm
new file mode 100644
index 0000000..546c545
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/GroupMembers.pm
@@ -0,0 +1,152 @@
+package RT::Extension::REST2::Resource::GroupMembers;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'RT::Extension::REST2::Resource::Collection';
+with 'RT::Extension::REST2::Resource::Role::RequestBodyIsJSON' =>
+  {type => 'ARRAY'};
+
+has 'group' => (
+    is  => 'ro',
+);
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/group/(\d+)/members/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $group_id = $match->pos(1);
+            my $group = RT::Group->new($req->env->{"rt.current_user"});
+            $group->Load($group_id);
+            my $collection;
+
+            my $recursively = $req->parameters->{recursively} // 0;
+            my $users       = $req->parameters->{users} // 1;
+            my $groups      = $req->parameters->{groups} // 1;
+
+            if ( $users && $groups ) {
+                if ( $recursively ) {
+                    $collection = $group->DeepMembersObj;
+                }
+                else {
+                    $collection = $group->MembersObj;
+                }
+            }
+            elsif ( $users ) {
+                $collection = $group->UserMembersObj(Recursively => $recursively);
+            }
+            elsif ( $groups ) {
+                $collection = $group->GroupMembersObj(Recursively => $recursively);
+            }
+            else {
+                $collection = RT::GroupMembers->new( $req->env->{"rt.current_user"} );
+                $collection->Limit(FIELD => 'id', VALUE => 0);
+            }
+
+            return {group => $group, collection => $collection};
+        },
+    ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/group/(\d+)/member/(\d+)/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $group_id = $match->pos(1);
+            my $member_id = $match->pos(2) || '';
+            my $group = RT::Group->new($req->env->{"rt.current_user"});
+            $group->Load($group_id);
+            my $collection = $group->MembersObj;
+            $collection->Limit(FIELD => 'MemberId', VALUE => $member_id);
+            return {group => $group, collection => $collection};
+        },
+    ),
+}
+
+sub forbidden {
+    my $self = shift;
+    return 0 unless $self->group->id;
+    return !$self->group->CurrentUserHasRight('AdminGroupMembership');
+    return 1;
+}
+
+sub serialize {
+    my $self = shift;
+    my $collection = $self->collection;
+    my @results;
+
+    while (my $item = $collection->Next) {
+        my ($id, $class);
+        if (ref $item eq 'RT::GroupMember' || ref $item eq 'RT::CachedGroupMember') {
+            my $principal = $item->MemberObj;
+            $class = $principal->IsGroup ? 'group' : 'user';
+            $id = $principal->id;
+        } elsif (ref $item eq 'RT::Group') {
+            $class = 'group';
+            $id = $item->id;
+        } elsif (ref $item eq 'RT::User') {
+            $class = 'user';
+            $id = $item->id;
+        }
+        else {
+            next;
+        }
+
+        my $result = {
+            type => $class,
+            id   => $id,
+            _url => RT::Extension::REST2->base_uri . "/$class/$id",
+        };
+        push @results, $result;
+    }
+    return {
+        count       => scalar(@results) + 0,
+        total       => $collection->CountAll,
+        per_page    => $collection->RowsPerPage + 0,
+        page        => ($collection->FirstRow / $collection->RowsPerPage) + 1,
+        items       => \@results,
+    };
+}
+
+sub allowed_methods {
+    my @ok = ('GET', 'HEAD', 'DELETE', 'PUT');
+    return \@ok;
+}
+
+sub content_types_accepted {[{'application/json' => 'from_json'}]}
+
+sub delete_resource {
+    my $self = shift;
+    my $collection = $self->collection;
+    while (my $group_member = $collection->Next) {
+        $RT::Logger->info('Delete ' . ($group_member->MemberObj->IsGroup ? 'group' : 'user') . ' ' . $group_member->MemberId . ' from group '.$group_member->GroupId);
+        $group_member->GroupObj->Object->DeleteMember($group_member->MemberId);
+    }
+    return 1;
+}
+
+sub from_json {
+    my $self   = shift;
+    my $params = JSON::decode_json($self->request->content);
+    my $group = $self->group;
+
+    my $method = $self->request->method;
+    my @results;
+    if ($method eq 'PUT') {
+        for my $param (@$params) {
+            if ($param =~ /^\d+$/) {
+                my ($ret, $msg) = $group->AddMember($param);
+                push @results, $msg;
+            } else {
+                push @results, 'You should provide principal id for each member to add';
+            }
+        }
+    }
+    $self->response->body(JSON::encode_json(\@results));
+    return;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;

commit a1b14989536a57258a09784ebc1c5ebd26a34cff
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 11:22:42 2018 +0200

    Add Memberships in returned properties of a user with GET /user/:id|:name

diff --git a/lib/RT/Extension/REST2/Resource/User.pm b/lib/RT/Extension/REST2/Resource/User.pm
index c39b7f1..a4ae3ae 100644
--- a/lib/RT/Extension/REST2/Resource/User.pm
+++ b/lib/RT/Extension/REST2/Resource/User.pm
@@ -4,6 +4,7 @@ use warnings;
 
 use Moose;
 use namespace::autoclean;
+use RT::Extension::REST2::Util qw(expand_uid);
 
 extends 'RT::Extension::REST2::Resource::Record';
 with (
@@ -40,6 +41,10 @@ around 'serialize' => sub {
     my $data = $self->$orig(@_);
     $data->{Privileged} = $self->record->Privileged ? 1 : 0;
     $data->{Disabled}   = $self->record->PrincipalObj->Disabled;
+    $data->{Memberships} = [
+        map { expand_uid($_->UID) }
+        @{ $self->record->OwnGroups->ItemsArrayRef }
+    ];
     return $data;
 };
 

commit dc173e739d8683df34c49323060f9631086c9a02
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Sep 17 11:23:31 2018 +0200

    Add endpoints to operate on user memberships
    
    * Getting groups which a user is a member of: GET /user/:id|:name/groups
    * Removing a user from all groups: DELETE /user/:id|:name/groups
    * Removing a user from a group: DELETE /user/:id|:name/group/:id
    * Adding a user to some groups: PUT /user/:id|:name/groups passing JSON array of groups ids

diff --git a/lib/RT/Extension/REST2/Resource/UserGroups.pm b/lib/RT/Extension/REST2/Resource/UserGroups.pm
new file mode 100644
index 0000000..3efe15e
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/UserGroups.pm
@@ -0,0 +1,120 @@
+package RT::Extension::REST2::Resource::UserGroups;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'RT::Extension::REST2::Resource::Collection';
+with 'RT::Extension::REST2::Resource::Role::RequestBodyIsJSON' =>
+  {type => 'ARRAY'};
+
+has 'user' => (
+    is  => 'ro',
+    isa => 'RT::User',
+);
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/user/([^/]+)/groups/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $user_id = $match->pos(1);
+            my $user = RT::User->new($req->env->{"rt.current_user"});
+            $user->Load($user_id);
+
+            return {user => $user, collection => $user->OwnGroups};
+        },
+    ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/user/([^/]+)/group/(\d+)/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $user_id = $match->pos(1);
+            my $group_id = $match->pos(2) || '';
+            my $user = RT::User->new($req->env->{"rt.current_user"});
+            $user->Load($user_id);
+            my $collection = $user->OwnGroups();
+            $collection->Limit(FIELD => 'id', VALUE => $group_id);
+            return {user => $user, collection => $collection};
+        },
+    ),
+}
+
+sub forbidden {
+    my $self = shift;
+    return 0 if
+        ($self->current_user->HasRight(
+            Right  => "ModifyOwnMembership",
+            Object => RT->System,
+        ) && $self->current_user->id == $self->user->id) ||
+        $self->current_user->HasRight(
+            Right  => 'AdminGroupMembership',
+            Object => RT->System);
+    return 1;
+}
+
+sub serialize {
+    my $self = shift;
+    my $collection = $self->collection;
+    my @results;
+
+    while (my $item = $collection->Next) {
+        my $result = {
+            type => 'group',
+            id   => $item->id,
+            _url => RT::Extension::REST2->base_uri . "/group/" . $item->id,
+        };
+        push @results, $result;
+    }
+    return {
+        count       => scalar(@results)         + 0,
+        total       => $collection->CountAll    + 0,
+        per_page    => $collection->RowsPerPage + 0,
+        page        => ($collection->FirstRow / $collection->RowsPerPage) + 1,
+        items       => \@results,
+    };
+}
+
+sub allowed_methods {
+    my @ok = ('GET', 'HEAD', 'DELETE', 'PUT');
+    return \@ok;
+}
+
+sub content_types_accepted {[{'application/json' => 'from_json'}]}
+
+sub delete_resource {
+    my $self = shift;
+    my $collection = $self->collection;
+    while (my $group = $collection->Next) {
+        $RT::Logger->info('Delete user ' . $self->user->Name . ' from group '.$group->id);
+        $group->DeleteMember($self->user->id);
+    }
+    return 1;
+}
+
+sub from_json {
+    my $self   = shift;
+    my $params = JSON::decode_json($self->request->content);
+    my $user = $self->user;
+
+    my $method = $self->request->method;
+    my @results;
+    if ($method eq 'PUT') {
+        for my $param (@$params) {
+            if ($param =~ /^\d+$/) {
+                my $group = RT::Group->new($self->request->env->{"rt.current_user"});
+                $group->Load($param);
+                push @results, $group->AddMember($user->id);
+            } else {
+                push @results, [0, 'You should provide group id for each group user should be added'];
+            }
+        }
+    }
+    $self->response->body(JSON::encode_json(\@results));
+    return;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;

commit 9b653a5f1084423b147fb6fcc6c8d8450e1bb265
Author: gibus <gibus at easter-eggs.com>
Date:   Thu Sep 20 15:17:05 2018 +0200

    Add tests for group members endpoints

diff --git a/t/group-members.t b/t/group-members.t
new file mode 100644
index 0000000..af14071
--- /dev/null
+++ b/t/group-members.t
@@ -0,0 +1,260 @@
+use strict;
+use warnings;
+use lib 't/lib';
+use RT::Extension::REST2::Test tests => undef;
+use Test::Warn;
+
+my $mech = RT::Extension::REST2::Test->mech;
+
+my $auth = RT::Extension::REST2::Test->authorization_header;
+my $rest_base_path = '/REST/2.0';
+my $user = RT::Extension::REST2::Test->user;
+
+my $group1 = RT::Group->new(RT->SystemUser);
+my ($ok, $msg) = $group1->CreateUserDefinedGroup(Name => 'Group 1');
+ok($ok, $msg);
+
+my $user1 = RT::User->new(RT->SystemUser);
+($ok, $msg) = $user1->Create(Name => 'User 1');
+ok($ok, $msg);
+
+my $user2 = RT::User->new(RT->SystemUser);
+($ok, $msg) = $user2->Create(Name => 'User 2');
+ok($ok, $msg);
+
+# Group creation
+my ($group2_url, $group2_id);
+{
+    my $payload = {
+        Name => 'Group 2',
+    };
+
+    # Rights Test - No AdminGroup
+    my $res = $mech->post_json("$rest_base_path/group",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403, 'Cannot create group without AdminGroup right');
+
+    # Rights Test - With AdminGroup
+    $user->PrincipalObj->GrantRight(Right => 'AdminGroup');
+    $res = $mech->post_json("$rest_base_path/group",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201, 'Create group with AdminGroup right');
+    ok($group2_url = $res->header('location'), 'Created group url');
+    ok(($group2_id) = $group2_url =~ qr[/group/(\d+)], 'Created group id');
+}
+
+my $group2 = RT::Group->new(RT->SystemUser);
+$group2->Load($group2_id);
+
+# Group disabling
+{
+    # Rights Test - No AdminGroup
+    $user->PrincipalObj->RevokeRight(Right => 'AdminGroup');
+    my $res = $mech->delete($group2_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403, 'Cannot disable group without AdminGroup right');
+
+    # Rights Test - With AdminGroup, no SeeGroup
+    $user->PrincipalObj->GrantRight(Right => 'AdminGroup', Object => $group2);
+    $res = $mech->delete($group2_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403, 'Cannot disable group without SeeGroup right');
+
+    # Rights Test - With AdminGroup, no SeeGroup
+    $user->PrincipalObj->GrantRight(Right => 'SeeGroup', Object => $group2);
+    $res = $mech->delete($group2_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 204, 'Disable group with AdminGroup & SeeGroup rights');
+
+    is($group2->Disabled, 1, "Group disabled");
+}
+
+# Group enabling
+{
+    my $payload = {
+        Disabled => 0,
+    };
+
+    # Rights Test - No AdminGroup
+    $user->PrincipalObj->RevokeRight(Right => 'AdminGroup', Object => $group2);
+    $user->PrincipalObj->RevokeRight(Right => 'SeeGroup', Object => $group2);
+
+    my $res = $mech->put_json($group2_url,
+        $payload,
+        'Authorization' => $auth);
+    is($res->code, 403, 'Cannot enable group without AdminGroup right');
+
+    # Rights Test - With AdminGroup, no SeeGroup
+    $user->PrincipalObj->GrantRight(Right => 'AdminGroup', Object => $group2);
+    $res = $mech->put_json($group2_url,
+        $payload,
+        'Authorization' => $auth);
+    is($res->code, 403, 'Cannot enable group without SeeGroup right');
+
+    # Rights Test - With AdminGroup, no SeeGroup
+    $user->PrincipalObj->GrantRight(Right => 'SeeGroup', Object => $group2);
+    $res = $mech->put_json($group2_url,
+        $payload,
+        'Authorization' => $auth);
+    is($res->code, 200, 'Enable group with AdminGroup & SeeGroup rights');
+    is_deeply($mech->json_response, ['Group enabled']);
+
+    is($group2->Disabled, 0, "Group enabled");
+}
+
+my $group1_id = $group1->id;
+(my $group1_url = $group2_url) =~ s/$group2_id/$group1_id/;
+$user->PrincipalObj->GrantRight(Right => 'SeeGroup', Object => $group1);
+
+# Members addition
+{
+    my $payload = [
+        $user1->id,
+        $group2->id,
+        $user1->id + 666,
+    ];
+
+    # Rights Test - No AdminGroupMembership
+    my $res = $mech->put_json($group1_url . '/members',
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403, 'Cannot add members to group without AdminGroupMembership right');
+
+    # Rights Test - With AdminGroupMembership
+    $user->PrincipalObj->GrantRight(Right => 'AdminGroupMembership', Object => $group1);
+    $res = $mech->put_json($group1_url . '/members',
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'Add members to group with AdminGroupMembership right');
+    is_deeply($mech->json_response, [
+        "Member added: " . $user1->Name,
+        "Member added: " . $group2->Name,
+        "Couldn't find that principal",
+    ], 'Two members added, bad principal rejected');
+    my $members1 = $group1->MembersObj;
+    is($members1->Count, 2, 'Two members added');
+    my $member = $members1->Next;
+    is($member->MemberObj->PrincipalType, 'User', 'User added as member');
+    is($member->MemberObj->id, $user1->id, 'Accurate user added as member');
+    $member = $members1->Next;
+    is($member->MemberObj->PrincipalType, 'Group', 'Group added as member');
+    is($member->MemberObj->id, $group2->id, 'Accurate group added as member');
+}
+
+# Members list
+{
+    # Add user to subgroup
+    $group2->AddMember($user2->id);
+
+    # Direct members
+    my $res = $mech->get($group1_url . '/members',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List direct members');
+
+    my $content = $mech->json_response;
+    is($content->{total}, 2, 'Two direct members');
+    is($content->{items}->[0]->{type}, 'user', 'User member');
+    is($content->{items}->[0]->{id}, $user1->id, 'Accurate user member');
+    is($content->{items}->[1]->{type}, 'group', 'Group member');
+    is($content->{items}->[1]->{id}, $group2->id, 'Accurate group member');
+
+    # Deep members
+    $res = $mech->get($group1_url . '/members?recursively=1',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List deep members');
+
+    $content = $mech->json_response;
+    is($content->{total}, 4, 'Four deep members');
+    is($content->{items}->[0]->{type}, 'group', 'First group deep member');
+    is($content->{items}->[0]->{id}, $group1->id, 'First accurate group deep member');
+    is($content->{items}->[1]->{type}, 'user', 'First user deep member');
+    is($content->{items}->[1]->{id}, $user1->id, 'First accurate user deep member');
+    is($content->{items}->[2]->{type}, 'user', 'Second user member');
+    is($content->{items}->[2]->{id}, $user2->id, 'Second accurate user deep member');
+    is($content->{items}->[3]->{type}, 'group', 'Second group deep member');
+    is($content->{items}->[3]->{id}, $group2->id, 'Second accurate group deep member');
+
+    # Direct user members
+    $res = $mech->get($group1_url . '/members?groups=0',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List direct user members');
+
+    $content = $mech->json_response;
+    is($content->{total}, 1, 'One direct user member');
+    is($content->{items}->[0]->{type}, 'user', 'Direct user member');
+    is($content->{items}->[0]->{id}, $user1->id, 'Accurate direct user member');
+
+    # Recursive user members
+    $res = $mech->get($group1_url . '/members?groups=0&recursively=1',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List recursive user members');
+
+    $content = $mech->json_response;
+    is($content->{total}, 2, 'Two recursive user members');
+    is($content->{items}->[0]->{type}, 'user', 'First recursive user member');
+    is($content->{items}->[0]->{id}, $user1->id, 'First accurate recursive user member');
+    is($content->{items}->[1]->{type}, 'user', 'Second recursive user member');
+    is($content->{items}->[1]->{id}, $user2->id, 'Second accurate recursive user member');
+
+    # Direct group members
+    $res = $mech->get($group1_url . '/members?users=0',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List direct group members');
+
+    $content = $mech->json_response;
+    is($content->{total}, 1, 'One direct group member');
+    is($content->{items}->[0]->{type}, 'group', 'Direct group member');
+    is($content->{items}->[0]->{id}, $group2->id, 'Accurate direct group member');
+
+    # Recursive group members
+    $res = $mech->get($group1_url . '/members?users=0&recursively=1',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List recursive group members');
+
+    $content = $mech->json_response;
+    is($content->{total}, 2, 'Two recursive group members');
+    is($content->{items}->[0]->{type}, 'group', 'First recursive group member');
+    is($content->{items}->[0]->{id}, $group1->id, 'First accurate recursive group member');
+    is($content->{items}->[1]->{type}, 'group', 'Second recursive group member');
+    is($content->{items}->[1]->{id}, $group2->id, 'Second accurate recursive group member');
+}
+
+# Members removal
+{
+    my $res = $mech->delete($group1_url . '/member/' . $user1->id,
+        'Authorization' => $auth,
+    );
+    is($res->code, 204, 'Remove member');
+    my $members1 = $group1->MembersObj;
+    is($members1->Count, 1, 'One member removed');
+    my $member = $members1->Next;
+    is($member->MemberObj->PrincipalType, 'Group', 'Group remaining member');
+    is($member->MemberObj->id, $group2->id, 'Accurate remaining member');
+}
+
+# All members removal
+{
+    my $res = $mech->delete($group1_url . '/members',
+        'Authorization' => $auth,
+    );
+    is($res->code, 204, 'Remove all members');
+    my $members1 = $group1->MembersObj;
+    is($members1->Count, 0, 'All members removed');
+}
+
+done_testing;

commit eadd1ed3d42383e17f5ba57b49ff568202bb2504
Author: gibus <gibus at easter-eggs.com>
Date:   Thu Sep 20 17:34:05 2018 +0200

    Add tests for user memberships endpoints

diff --git a/t/user-memberships.t b/t/user-memberships.t
new file mode 100644
index 0000000..47f969a
--- /dev/null
+++ b/t/user-memberships.t
@@ -0,0 +1,105 @@
+use strict;
+use warnings;
+use lib 't/lib';
+use RT::Extension::REST2::Test tests => undef;
+use Test::Warn;
+
+my $mech = RT::Extension::REST2::Test->mech;
+
+my $auth = RT::Extension::REST2::Test->authorization_header;
+my $rest_base_path = '/REST/2.0';
+my $user = RT::Extension::REST2::Test->user;
+
+my $group1 = RT::Group->new(RT->SystemUser);
+my ($ok, $msg) = $group1->CreateUserDefinedGroup(Name => 'Group 1');
+ok($ok, $msg);
+
+my $group2 = RT::Group->new(RT->SystemUser);
+($ok, $msg) = $group2->CreateUserDefinedGroup(Name => 'Group 2');
+ok($ok, $msg);
+
+($ok, $msg) = $group1->AddMember($group2->id);
+ok($ok, $msg);
+
+# Membership addition
+{
+    my $payload = [ $group2->id ];
+
+    # Rights Test - No ModifyOwnMembership
+    my $res = $mech->put_json("$rest_base_path/user/" . $user->id . '/groups',
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403, 'Cannot add user to group without ModifyOwnMembership right');
+
+    # Rights Test - With ModifyOwnMembership
+    $user->PrincipalObj->GrantRight(Right => 'ModifyOwnMembership');
+    $res = $mech->put_json("$rest_base_path/user/" . $user->id . '/groups',
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'Add user to group with ModifyOwnMembership right');
+    my $members2 = $group2->MembersObj;
+    is($members2->Count, 1, 'One member added');
+    my $member = $members2->Next;
+    is($member->MemberObj->PrincipalType, 'User', 'User added as member');
+    is($member->MemberObj->id, $user->id, 'Accurate user added as member');
+}
+
+
+# Memberships list
+{
+    # Rights Test - No SeeGroup
+    my $res = $mech->get("$rest_base_path/user/" . $user->id . '/groups',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List direct members');
+
+    my $content = $mech->json_response;
+    is($content->{total}, 2, 'Two recursive memberships');
+    is(scalar(@{$content->{items}}), 0, 'Cannot see memberships content withtout SeeGroup right');
+    
+    # Recursive memberships
+    $user->PrincipalObj->GrantRight(Right => 'SeeGroup');
+    $res = $mech->get("$rest_base_path/user/" . $user->id . '/groups',
+        'Authorization' => $auth,
+    );
+    is($res->code, 200, 'List direct members');
+
+    $content = $mech->json_response;
+    is($content->{total}, 2, 'Two recursive memberships');
+    is($content->{items}->[0]->{type}, 'group', 'First group membership');
+    is($content->{items}->[0]->{id}, $group1->id, 'Accurate first group membership');
+    is($content->{items}->[1]->{type}, 'group', 'Second group membership');
+    is($content->{items}->[1]->{id}, $group2->id, 'Accurate second group membership');
+}
+
+($ok, $msg) = $group1->AddMember($user->id);
+ok($ok, $msg);
+
+# Membership removal
+{
+    my $res = $mech->delete("$rest_base_path/user/" . $user->id . '/group/' . $group2->id,
+        'Authorization' => $auth,
+    );
+    is($res->code, 204, 'Remove membership');
+    my $memberships = $user->OwnGroups;
+    is($memberships->Count, 1, 'One membership removed');
+    my $membership = $memberships->Next;
+    is($membership->id, $group1->id, 'Accurate membership removed');
+}
+
+($ok, $msg) = $group2->AddMember($user->id);
+ok($ok, $msg);
+
+# All members removal
+{
+    my $res = $mech->delete("$rest_base_path/user/" . $user->id . '/groups',
+        'Authorization' => $auth,
+    );
+    is($res->code, 204, 'Remove all memberships');
+    my $memberships = $user->OwnGroups;
+    is($memberships->Count, 0, 'All membership removed');
+}
+
+done_testing;

commit 02572fb4b5369867b2ff125f2fa6ab7b324a7e99
Author: gibus <gibus at easter-eggs.com>
Date:   Thu Sep 20 17:34:44 2018 +0200

    Fix existing tests missing SeeGroup right

diff --git a/t/ticket-customroles.t b/t/ticket-customroles.t
index 3b692a3..f20c4a1 100644
--- a/t/ticket-customroles.t
+++ b/t/ticket-customroles.t
@@ -41,7 +41,7 @@ for my $email (qw/multi at example.com test at localhost multi2 at example.com single2 at ex
 }
 
 $user->PrincipalObj->GrantRight( Right => $_ )
-    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers/;
+    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers SeeGroup/;
 
 # Create and view ticket with no watchers
 {
diff --git a/t/ticket-watchers.t b/t/ticket-watchers.t
index 20446fd..76bdbdc 100644
--- a/t/ticket-watchers.t
+++ b/t/ticket-watchers.t
@@ -13,7 +13,7 @@ my $user = RT::Extension::REST2::Test->user;
 my $queue = RT::Test->load_or_create_queue( Name => "General" );
 
 $user->PrincipalObj->GrantRight( Right => $_ )
-    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers/;
+    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers SeeGroup/;
 
 # Create and view ticket with no watchers
 {

commit 5c23a78c6e879ee5fecc97ef12a9ee9de0e680b3
Author: gibus <gibus at easter-eggs.com>
Date:   Fri Sep 21 15:49:30 2018 +0200

    Add documentation for group members and user memberships

diff --git a/README b/README
index 18aa7fa..c955661 100644
--- a/README
+++ b/README
@@ -398,11 +398,11 @@ USAGE
 
         GET /user/:id
         GET /user/:name
-            retrieve a user by numeric id or username
+            retrieve a user by numeric id or username (including its memberships and whether it is disabled)
 
         PUT /user/:id
         PUT /user/:name
-            update a user's metadata; provide JSON content
+            update a user's metadata (including its Disabled status); provide JSON content
 
         DELETE /user/:id
         DELETE /user/:name
@@ -417,12 +417,66 @@ USAGE
         POST /groups
             search for groups using L</JSON searches> syntax
 
+        POST /group
+            create a (user defined) group; provide JSON content
+
         GET /group/:id
-            retrieve a group (including its members)
+            retrieve a group (including its members and whether it is disabled)
+
+        PUT /group/:id
+            update a groups's metadata (including its Disabled status); provide JSON content
+
+        DELETE /group/:id
+            disable group
 
         GET /group/:id/history
             retrieve list of transactions for group
 
+   User Memberships
+        GET /user/:id/groups
+        GET /user/:name/groups
+            retrieve list of groups which a user is a member of
+
+        PUT /user/:id/groups
+        PUT /user/:name/groups
+            add a user to groups; provide a JSON array of groups ids
+
+        DELETE /user/:id/group/:id
+        DELETE /user/:name/group/:id
+            remove a user from a group
+
+        DELETE /user/:id/groups
+        DELETE /user/:name/groups
+            remove a user from all groups
+
+   Group Members
+        GET /group/:id/members
+            retrieve list of direct members of a group
+
+        GET /group/:id/members?recursively=1
+            retrieve list of direct and recursive members of a group
+
+        GET /group/:id/members?users=0
+            retrieve list of direct group members of a group
+
+        GET /group/:id/members?users=0&recursively=1
+            retrieve list of direct and recursive group members of a group
+
+        GET /group/:id/members?groups=0
+            retrieve list of direct user members of a group
+
+        GET /group/:id/members?groups=0&recursively=1
+            retrieve list of direct and recursive user members of a group
+
+        PUT /group/:id/members
+            add members to a group; provide a JSON array of principal ids
+
+        DELETE /group/:id/member/:id
+            remove a member from a group
+
+        DELETE /group/:id/members
+            remove all members from a group
+
    Custom Fields
         GET /customfields?query=<JSON>
         POST /customfields
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index e7e06fe..c4023df 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -440,11 +440,11 @@ Below are some examples using the endpoints above.
 
     GET /user/:id
     GET /user/:name
-        retrieve a user by numeric id or username
+        retrieve a user by numeric id or username (including its memberships and whether it is disabled)
 
     PUT /user/:id
     PUT /user/:name
-        update a user's metadata; provide JSON content
+        update a user's metadata (including its Disabled status); provide JSON content
 
     DELETE /user/:id
     DELETE /user/:name
@@ -460,12 +460,68 @@ Below are some examples using the endpoints above.
     POST /groups
         search for groups using L</JSON searches> syntax
 
+    POST /group
+        create a (user defined) group; provide JSON content
+
     GET /group/:id
-        retrieve a group (including its members)
+        retrieve a group (including its members and whether it is disabled)
+
+    PUT /group/:id
+        update a groups's metadata (including its Disabled status); provide JSON content
+
+    DELETE /group/:id
+        disable group
 
     GET /group/:id/history
         retrieve list of transactions for group
 
+=head3 User Memberships
+
+    GET /user/:id/groups
+    GET /user/:name/groups
+        retrieve list of groups which a user is a member of
+
+    PUT /user/:id/groups
+    PUT /user/:name/groups
+        add a user to groups; provide a JSON array of groups ids
+
+    DELETE /user/:id/group/:id
+    DELETE /user/:name/group/:id
+        remove a user from a group
+
+    DELETE /user/:id/groups
+    DELETE /user/:name/groups
+        remove a user from all groups
+
+=head3 Group Members
+
+    GET /group/:id/members
+        retrieve list of direct members of a group
+
+    GET /group/:id/members?recursively=1
+        retrieve list of direct and recursive members of a group
+
+    GET /group/:id/members?users=0
+        retrieve list of direct group members of a group
+
+    GET /group/:id/members?users=0&recursively=1
+        retrieve list of direct and recursive group members of a group
+
+    GET /group/:id/members?groups=0
+        retrieve list of direct user members of a group
+
+    GET /group/:id/members?groups=0&recursively=1
+        retrieve list of direct and recursive user members of a group
+
+    PUT /group/:id/members
+        add members to a group; provide a JSON array of principal ids
+
+    DELETE /group/:id/member/:id
+        remove a member from a group
+
+    DELETE /group/:id/members
+        remove all members from a group
+
 =head3 Custom Fields
 
     GET /customfields?query=<JSON>

commit c6fdebf98faea399b866c6d3995b9a563540b42f
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 14:36:51 2018 +0200

    Add new files in MANIFEST

diff --git a/MANIFEST b/MANIFEST
index bac4498..3613aa2 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -36,6 +36,7 @@ lib/RT/Extension/REST2/Resource/CustomFields.pm
 lib/RT/Extension/REST2/Resource/CustomRole.pm
 lib/RT/Extension/REST2/Resource/CustomRoles.pm
 lib/RT/Extension/REST2/Resource/Group.pm
+lib/RT/Extension/REST2/Resource/GroupMmebers.pm
 lib/RT/Extension/REST2/Resource/Groups.pm
 lib/RT/Extension/REST2/Resource/Message.pm
 lib/RT/Extension/REST2/Resource/Queue.pm
@@ -56,6 +57,7 @@ lib/RT/Extension/REST2/Resource/TicketsBulk.pm
 lib/RT/Extension/REST2/Resource/Transaction.pm
 lib/RT/Extension/REST2/Resource/Transactions.pm
 lib/RT/Extension/REST2/Resource/User.pm
+lib/RT/Extension/REST2/Resource/UserGroups.pm
 lib/RT/Extension/REST2/Resource/Users.pm
 lib/RT/Extension/REST2/Util.pm
 Makefile.PL
@@ -66,6 +68,7 @@ t/asset-customfields.t
 t/assets.t
 t/catalogs.t
 t/conflict.t
+t/group-members.t
 t/lib/RT/Extension/REST2/Test.pm.in
 t/not_found.t
 t/organization.t
@@ -80,4 +83,5 @@ t/tickets-bulk.t
 t/tickets.t
 t/transactions.t
 t/user-customfields.t
+t/user-memberships.t
 TODO

commit e4916bfa02b80b495608d51d44dae1f7be480b5d
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 15:12:49 2018 +0200

    Add memberships in user's hypermedia links

diff --git a/lib/RT/Extension/REST2/Resource/User.pm b/lib/RT/Extension/REST2/Resource/User.pm
index a4ae3ae..8a74630 100644
--- a/lib/RT/Extension/REST2/Resource/User.pm
+++ b/lib/RT/Extension/REST2/Resource/User.pm
@@ -60,6 +60,12 @@ sub hypermedia_links {
     my $self = shift;
     my $links = $self->_default_hypermedia_links(@_);
     push @$links, $self->_transaction_history_link;
+
+    my $id = $self->record->id;
+    push @$links,
+      { ref  => 'memberships',
+        _url => RT::Extension::REST2->base_uri . "/user/$id/groups",
+      };
     return $links;
 }
 
diff --git a/t/user-memberships.t b/t/user-memberships.t
index 47f969a..b27ad12 100644
--- a/t/user-memberships.t
+++ b/t/user-memberships.t
@@ -102,4 +102,18 @@ ok($ok, $msg);
     is($memberships->Count, 0, 'All membership removed');
 }
 
+# User hypermedia links
+{
+    my $res = $mech->get("$rest_base_path/user/" . $user->id,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    my $links = $content->{_hyperlinks};
+    my @memberships_links = grep { $_->{ref} eq 'memberships' } @$links;
+    is(scalar(@memberships_links), 1);
+    my $user_id = $user->id;
+    like($memberships_links[0]->{_url}, qr{$rest_base_path/user/$user_id/groups$});
+}
+
 done_testing;

commit 76d795106ff817a736291516866411eb9437b821
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 16:07:23 2018 +0200

    Add members in group's hypermedia links

diff --git a/lib/RT/Extension/REST2/Resource/Group.pm b/lib/RT/Extension/REST2/Resource/Group.pm
index 8235fa0..64377a3 100644
--- a/lib/RT/Extension/REST2/Resource/Group.pm
+++ b/lib/RT/Extension/REST2/Resource/Group.pm
@@ -45,6 +45,12 @@ sub hypermedia_links {
     my $self = shift;
     my $links = $self->_default_hypermedia_links(@_);
     push @$links, $self->_transaction_history_link;
+
+    my $id = $self->record->id;
+    push @$links,
+      { ref  => 'members',
+        _url => RT::Extension::REST2->base_uri . "/group/$id/members",
+      };
     return $links;
 }
 
diff --git a/t/group-members.t b/t/group-members.t
index af14071..f9cbccf 100644
--- a/t/group-members.t
+++ b/t/group-members.t
@@ -257,4 +257,17 @@ $user->PrincipalObj->GrantRight(Right => 'SeeGroup', Object => $group1);
     is($members1->Count, 0, 'All members removed');
 }
 
+# Group hypermedia links
+{
+    my $res = $mech->get("$rest_base_path/group/$group1_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    my $links = $content->{_hyperlinks};
+    my @members_links = grep { $_->{ref} =~ /members/ } @$links;
+    is(scalar(@members_links), 1);
+    like($members_links[0]->{_url}, qr{$rest_base_path/group/$group1_id/members$});
+}
+
 done_testing;

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list