Skip to content

Commit

Permalink
Add support for sharing devtainers with users by role(s)
Browse files Browse the repository at this point in the history
- Role(s) may now be entered, in addition to User(s), in the devtainer
  Developers and Viewers fields
- Such a devtainer will be shared with users possessing any of the
  listed Role(s)
  • Loading branch information
struanb committed Oct 13, 2022
1 parent df0e7bd commit 6aceafc
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 21 deletions.
31 changes: 29 additions & 2 deletions app/client/src/components/UserTagsInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@
return obj;
}, {});
},
// Lookup from username or role metadata name to user's name or human-readable role (respectively)
userIDToUserNameMap() {
return this.allUsers.reduce((obj, item) => {
obj[item.username] = item.name;
obj[this.role_as_meta(item.role)] = this.roleName(item.role);
return obj;
}, {});
},
// selectedUserIds property contains a comma-separated string of user IDs, so this computed property represents those IDs as an array of obejcts
selectedUsers: {
get() {
Expand All @@ -60,22 +64,45 @@
this.$emit('input', this.selectedUserIds );
}
},
placeholder() {
return this.disabled ? '' : 'Add User';
return this.disabled ? '' : 'Add User or Role';
}
},
methods: {
generateAutocompleteItems(currentInput) {
return this.allUsers.map(
// First, generate items for users
const users = this.allUsers.map(
user => user.name
).filter(
name => name.toLowerCase().indexOf(currentInput.toLowerCase()) !== -1
).map(
name => this.generateInternalTagRepresentation(name, this.userNameToUserIDMap[name])
);
// Second, generate items for unique list of roles derived from all users
const roles = Object.keys(
this.allUsers
.map( user => user.role )
.reduce((obj, item) => { obj[item] = 1; return obj; }, {})
).map( role => this.generateInternalTagRepresentation(this.roleName(role), this.role_as_meta(role)) );
return users.concat(roles);
},
generateInternalTagRepresentation(text, userId) {
return {text, userId};
},
// How to display a role in the dropdown
roleName(role) {
return role + ' (Role)';
},
// How to represent a role in metadata
role_as_meta(role) {
return 'role:' + role;
}
}
};
Expand Down
8 changes: 6 additions & 2 deletions app/client/src/components/mixins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ const filteredContainers = {
return this.$store.state.containers
.filter(container => container.meta.owner === window.dockside.user.username);
case 'shared':
// Display containers for which:
return this.$store.state.containers
.filter(container =>
// the user is the owner
(container.meta.owner === window.dockside.user.username) ||
(container.meta.developers && container.meta.developers.split(',').filter(user => user === window.dockside.user.username).length) ||
(container.meta.viewers.split(',').filter(user => user === window.dockside.user.username).length)
// the devtainer's developers list includes the user, or the user's role
(container.meta.developers && container.meta.developers.split(',').filter(user => (user === window.dockside.user.username) || (user === window.dockside.user.role_as_meta)).length) ||
// the devtainer's viewers list includes the user, or the user's role
(container.meta.viewers && container.meta.viewers.split(',').filter(user => (user === window.dockside.user.username) || (user === window.dockside.user.role_as_meta)).length)
);
case 'all':
return this.$store.state.containers;
Expand Down
7 changes: 6 additions & 1 deletion app/server/lib/App.pm
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,12 @@ sub _handler {
JSON::XS->new->utf8->convert_blessed->encode(
{
# FIXME: set 'user' => $User, after simply either (a) changing User object definition to make 'permissions' the derivedPermissions; or (b) the Vue app to check user.derivedPermissions.
'user' => { 'username' => $User->username, 'permissions' => { 'actions' => $User->permissions() } },
'user' => {
'username' => $User->username,
'role' => $User->role, # User's role
'role_as_meta' => $User->role_as_meta, # User's role in metadata format
'permissions' => { 'actions' => $User->permissions() } # User's permissions
},
'profiles' => $User->profiles(),
'containers' => $User->reservations({'client' => 1}),
'viewers' => User->viewers(),
Expand Down
12 changes: 8 additions & 4 deletions app/server/lib/Reservation.pm
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,13 @@ sub meta {
}
}
elsif( $key =~ /^(viewers|developers)$/ ) {
if( $value =~ /^[a-z0-9\,]*$/ ) {
# FIXME: check that username(s) provided are valid
# Each of these values is a comma-separated list of usernames, or ''.

# $value can be a comma-separated list of items of form either '<username>' or 'role:<role>' or ''
my @values = split(/,/, $value);

# Check if all values match the regex
if( (grep { /^(role:)?[a-z][a-z0-9]+$/ } @values) == @values ) {
# TODO: check that username(s) and role(s) provided are valid
$self->{'meta'}{$key} = $value || '';
}
else {
Expand Down Expand Up @@ -817,7 +821,7 @@ sub lookup_container_uri {
# RESERVATION QUERY METHODS
#

# Query 'viewers' or 'developers' $key for presence of user $user
# Query 'viewers' or 'developers' $key for presence of username $user
sub meta_has_user {
my $self = shift;
my $key = shift;
Expand Down
32 changes: 22 additions & 10 deletions app/server/lib/User.pm
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ sub load {
}

sub viewers {
return [ map { { 'name' => $USERS->{$_}{'name'} // $_, 'username' => $_ } } sort keys %$USERS ];
return [ map { { 'name' => $USERS->{$_}{'name'} // $_, 'username' => $_, 'role' => $USERS->{$_}{'role'} } } sort keys %$USERS ];
}

################################################################################
Expand Down Expand Up @@ -175,6 +175,11 @@ sub role {
return $_[0]->{'role'};
}

# This sub must match that of same name in UserTagsInput.vue
sub role_as_meta {
return $_[0]->role() ? ('role:' . $_[0]->role()) : undef;
}

# FIXME: Rename to derivedPermissions
sub permissions {
return $_[0]->{'derivedPermissions'};
Expand Down Expand Up @@ -362,6 +367,7 @@ sub can_on {
my $action = shift; # 'view' | 'develop' | 'keepPrivate'

my $username = $self->username();
my $role = $self->role_as_meta;

# Users named as a container's owner, developer or viewer can view the container.
if( $action eq 'view' ) {
Expand All @@ -372,19 +378,24 @@ sub can_on {
# Anyone with viewAllPrivateContainers capability can also view all containers
return 1 if $self->has_permission( 'viewAllPrivateContainers' );

return ( $container->meta('owner') eq $username || $container->meta_has_user('viewers', $username) || $container->meta_has_user('developers', $username) )
? 1
: 0;
return (
$container->meta('owner') eq $username ||
$container->meta_has_user('viewers', $username) ||
$container->meta_has_user('viewers', $role) ||
$container->meta_has_user('developers', $username) ||
$container->meta_has_user('developers', $role)
) ? 1 : 0;
}

# Users named as a container's owner or developer can develop the container.
if( $action eq 'develop' ) {
return 1 if $container->meta('owner') eq $username;
return 0 unless $self->has_permission( 'developContainers' );
return 1 if $self->has_permission( 'developAllContainers' ); # FIXME This is implementing a 3-way switch with two booleans (always-on, depends-on-container, always-off)
return ( $container->meta_has_user('developers', $username) )
? 1
: 0;
return (
$container->meta_has_user('developers', $username) ||
$container->meta_has_user('developers', $role)
) ? 1 : 0;
}

# Only the User named as the container's owner can keep the container private.
Expand Down Expand Up @@ -520,7 +531,8 @@ sub reservations {
}

my $username = $self->username;

my $role = $self->role_as_meta();

# Sort reservations by:
# - those one owns, first
# - those one is a named developer on, second;
Expand All @@ -530,9 +542,9 @@ sub reservations {
@$viewable = sort {
( ($b->meta('owner') eq $username) <=> ($a->meta('owner') eq $username) )
||
( $b->meta_has_user('developers', $username) <=> $a->meta_has_user('developers', $username) )
( ($b->meta_has_user('developers', $username) || $b->meta_has_user('developers', $role)) <=> ($a->meta_has_user('developers', $username) || $a->meta_has_user('developers', $role)) )
||
( $b->meta_has_user('viewers', $username) <=> $a->meta_has_user('viewers', $username) )
( ($b->meta_has_user('viewers', $username) || $b->meta_has_user('viewers', $role)) <=> ($a->meta_has_user('viewers', $username) || $a->meta_has_user('viewers', $role)) )
||
( $b->status() <=> $a->status() )
||
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Click `Launch` to prepare to launch a devtainer. Choose a Profile to indicate th
- devtainer docker image
- docker network
- the auth/access level for each service preconfigured within the profile
- a list of users allowed to view the devtainer i.e. acccess the devtainer and links to devtainer services displayed on Dockside
- a list of users allowed to develop the devtainer i.e. access the Dockside Theia IDE (which implies rights to view the devtainer too)
- a list of users, and/or roles of users, allowed to view the devtainer i.e. access the devtainer and display links to devtainer services displayed on Dockside
- a list of users, and/or roles of users, allowed to develop the devtainer i.e. access the Dockside Theia IDE (which implies rights to view the devtainer too)
- a checkbox for keeping the devtainer private from other admin users (only available to admin users)

When ready, click the green `Launch` button. If errors are encountered launching the devtainer, these will be displayed onscreen.
Expand Down

0 comments on commit 6aceafc

Please sign in to comment.