Skip to content

Commit

Permalink
Application passwords CLI commands (#1743)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmgigi96 authored Jun 8, 2021
1 parent 6ace58c commit 52e7228
Show file tree
Hide file tree
Showing 11 changed files with 629 additions and 55 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/app-passwords-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Application passwords CLI

This PR adds the CLI commands `token-list`, `token-create` and `token-remove`
to manage tokens with limited scope on behalf of registered users.

https://github.com/cs3org/reva/pull/1719
251 changes: 251 additions & 0 deletions cmd/reva/app-tokens-create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package main

import (
"context"
"io"
"strings"
"time"

authapp "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
share "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/errtypes"
)

type appTokenCreateOpts struct {
Expiration string
Label string
Path stringSlice
Share stringSlice
Unlimited bool
}

type stringSlice []string

func (ss *stringSlice) Set(value string) error {
*ss = append(*ss, value)
return nil
}

func (ss *stringSlice) String() string {
return strings.Join([]string(*ss), ",")
}

const layoutTime = "2006-01-02"

func appTokensCreateCommand() *command {
cmd := newCommand("app-tokens-create")
cmd.Description = func() string { return "create a new application tokens" }
cmd.Usage = func() string { return "Usage: token-create" }

var path, share stringSlice
label := cmd.String("label", "", "set a label")
expiration := cmd.String("expiration", "", "set expiration time (format <yyyy-mm-dd>)")
cmd.Var(&path, "path", "create a token for a file (format path:[r|w]). It is possible specify this flag multiple times")
cmd.Var(&share, "share", "create a token for a share (format shareid:[r|w]). It is possible specify this flag multiple times")
unlimited := cmd.Bool("all", false, "create a token with an unlimited scope")

cmd.ResetFlags = func() {
path, share, label, expiration, unlimited = nil, nil, nil, nil, nil
}

cmd.Action = func(w ...io.Writer) error {

createOpts := &appTokenCreateOpts{
Expiration: *expiration,
Label: *label,
Path: path,
Share: share,
Unlimited: *unlimited,
}

err := checkOpts(createOpts)
if err != nil {
return err
}

client, err := getClient()
if err != nil {
return err
}

ctx := getAuthContext()

scope, err := getScope(ctx, client, createOpts)
if err != nil {
return err
}

// parse eventually expiration time
var expiration *types.Timestamp
if createOpts.Expiration != "" {
exp, err := time.Parse(layoutTime, createOpts.Expiration)
if err != nil {
return err
}
expiration = &types.Timestamp{
Seconds: uint64(exp.Unix()),
}
}

generateAppPasswordResponse, err := client.GenerateAppPassword(ctx, &authapp.GenerateAppPasswordRequest{
Expiration: expiration,
Label: createOpts.Label,
TokenScope: scope,
})

if err != nil {
return err
}
if generateAppPasswordResponse.Status.Code != rpc.Code_CODE_OK {
return formatError(generateAppPasswordResponse.Status)
}

err = printTableAppPasswords([]*authapp.AppPassword{generateAppPasswordResponse.AppPassword})
if err != nil {
return err
}

return nil
}

return cmd
}

func getScope(ctx context.Context, client gateway.GatewayAPIClient, opts *appTokenCreateOpts) (map[string]*authpb.Scope, error) {
var scopeList []map[string]*authpb.Scope
switch {
case opts.Unlimited:
return scope.GetOwnerScope()
case len(opts.Share) != 0:
// TODO(gmgigi96): verify format
for _, entry := range opts.Share {
// share = xxxx:[r|w]
shareIDPerm := strings.Split(entry, ":")
shareID, perm := shareIDPerm[0], shareIDPerm[1]
scope, err := getPublicShareScope(ctx, client, shareID, perm)
if err != nil {
return nil, err
}
scopeList = append(scopeList, scope)
}
fallthrough
case len(opts.Path) != 0:
// TODO(gmgigi96): verify format
for _, entry := range opts.Path {
// path = /home/a/b:[r|w]
pathPerm := strings.Split(entry, ":")
path, perm := pathPerm[0], pathPerm[1]
scope, err := getPathScope(ctx, client, path, perm)
if err != nil {
return nil, err
}
scopeList = append(scopeList, scope)
}
fallthrough
default:
return mergeListScopeIntoMap(scopeList), nil
}
}

func mergeListScopeIntoMap(scopeList []map[string]*authpb.Scope) map[string]*authpb.Scope {
merged := make(map[string]*authpb.Scope)
for _, scope := range scopeList {
for k, v := range scope {
merged[k] = v
}
}
return merged
}

func getPublicShareScope(ctx context.Context, client gateway.GatewayAPIClient, shareID, perm string) (map[string]*authpb.Scope, error) {
role, err := parsePermission(perm)
if err != nil {
return nil, err
}

publicShareResponse, err := client.GetPublicShare(ctx, &share.GetPublicShareRequest{
Ref: &share.PublicShareReference{
Spec: &share.PublicShareReference_Id{
Id: &share.PublicShareId{
OpaqueId: shareID,
},
},
},
})

if err != nil {
return nil, err
}
if publicShareResponse.Status.Code != rpc.Code_CODE_OK {
return nil, formatError(publicShareResponse.Status)
}

return scope.GetPublicShareScope(publicShareResponse.GetShare(), role)
}

func getPathScope(ctx context.Context, client gateway.GatewayAPIClient, path, perm string) (map[string]*authpb.Scope, error) {
role, err := parsePermission(perm)
if err != nil {
return nil, err
}

statResponse, err := client.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
Spec: &provider.Reference_Path{
Path: path,
},
},
})

if err != nil {
return nil, err
}
if statResponse.Status.Code != rpc.Code_CODE_OK {
return nil, formatError(statResponse.Status)
}

return scope.GetResourceInfoScope(statResponse.GetInfo(), role)
}

// parse permission string in the form of "rw" to create a role
func parsePermission(perm string) (authpb.Role, error) {
switch perm {
case "r":
return authpb.Role_ROLE_VIEWER, nil
case "w":
return authpb.Role_ROLE_EDITOR, nil
default:
return authpb.Role_ROLE_INVALID, errtypes.BadRequest("not recognised permission")
}
}

func checkOpts(opts *appTokenCreateOpts) error {
if len(opts.Share) == 0 && len(opts.Path) == 0 && !opts.Unlimited {
return errtypes.BadRequest("specify a token scope")
}
return nil
}
106 changes: 106 additions & 0 deletions cmd/reva/app-tokens-list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package main

import (
"io"
"os"
"strings"
"time"

applications "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
authpv "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
scope "github.com/cs3org/reva/pkg/auth/scope"
"github.com/jedib0t/go-pretty/table"
)

func appTokensListCommand() *command {
cmd := newCommand("app-tokens-list")
cmd.Description = func() string { return "list all the application tokens" }
cmd.Usage = func() string { return "Usage: token-list" }

cmd.Action = func(w ...io.Writer) error {

client, err := getClient()
if err != nil {
return err
}

ctx := getAuthContext()
listResponse, err := client.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{})

if err != nil {
return err
}

if listResponse.Status.Code != rpc.Code_CODE_OK {
return formatError(listResponse.Status)
}

err = printTableAppPasswords(listResponse.AppPasswords)
if err != nil {
return err
}

return nil
}
return cmd
}

func printTableAppPasswords(listPw []*applications.AppPassword) error {
header := table.Row{"Token", "Scope", "Label", "Expiration", "Creation Time", "Last Used Time"}

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)

t.AppendHeader(header)

for _, pw := range listPw {
scopeFormatted, err := prettyFormatScope(pw.TokenScope)
if err != nil {
return err
}
t.AppendRow(table.Row{pw.Password, scopeFormatted, pw.Label, formatTime(pw.Expiration), formatTime(pw.Ctime), formatTime(pw.Utime)})
}

t.Render()
return nil
}

func formatTime(t *types.Timestamp) string {
if t == nil {
return ""
}
return time.Unix(int64(t.Seconds), 0).String()
}

func prettyFormatScope(scopeMap map[string]*authpv.Scope) (string, error) {
var scopeFormatted strings.Builder
for scType, sc := range scopeMap {
scopeStr, err := scope.FormatScope(scType, sc)
if err != nil {
return "", err
}
scopeFormatted.WriteString(scopeStr)
scopeFormatted.WriteString(", ")
}
return scopeFormatted.String()[:scopeFormatted.Len()-2], nil
}
Loading

0 comments on commit 52e7228

Please sign in to comment.