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

Implementing the appwrite plugin #336

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions plugins/appwrite/api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package appwrite

import (
"context"
"os"

"encoding/json"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/importer"
"github.com/1Password/shell-plugins/sdk/provision"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func ConfigPath() string {
configDir, err := os.UserHomeDir()
if err != nil {
return "~/.appwrite/prefs.json"
}

return configDir + "/.appwrite/prefs.json"
}
ra-jeev marked this conversation as resolved.
Show resolved Hide resolved

func APIKey() schema.CredentialType {
return schema.CredentialType{
Name: credname.APIKey,
DocsURL: sdk.URL("https://appwrite.io/docs"),
ManagementURL: sdk.URL("https://cloud.appwrite.io/console/account"),
Fields: []schema.CredentialField{
{
Name: fieldname.APIKey,
MarkdownDescription: "API Key used to authenticate to Appwrite.",
Secret: true,
Composition: &schema.ValueComposition{
Length: 256,
Charset: schema.Charset{
Lowercase: true,
Digits: true,
},
},
},
{
Name: fieldname.Endpoint,
MarkdownDescription: "Appwrite server endpoint.",
Secret: false,
Optional: false,
},
},
DefaultProvisioner: provision.TempFile(appwriteConfig, provision.AtFixedPath(ConfigPath())),
Importer: importer.TryAll(
TryAppwriteConfigFile(),
)}
ra-jeev marked this conversation as resolved.
Show resolved Hide resolved
}

func appwriteConfig(in sdk.ProvisionInput) ([]byte, error) {
config := Config{
APIKey: in.ItemFields[fieldname.APIKey],
Endpoint: in.ItemFields[fieldname.Endpoint],
}

contents, err := json.MarshalIndent(&config, "", " ")
ra-jeev marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

return []byte(contents), nil
}

func TryAppwriteConfigFile() sdk.Importer {
return importer.TryFile(ConfigPath(), func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) {
var config Config
if err := contents.ToJSON(&config); err != nil {
out.AddError(err)
return
}

if config.APIKey == "" {
return
}

if config.Endpoint == "" {
return
}

out.AddCandidate(sdk.ImportCandidate{
Fields: map[sdk.FieldName]string{
fieldname.APIKey: config.APIKey,
fieldname.Endpoint: config.Endpoint,
},
})
})
}

type Config struct {
APIKey string `json:"key"`
Endpoint string `json:"endpoint"`
}
45 changes: 45 additions & 0 deletions plugins/appwrite/api_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package appwrite

import (
"testing"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/plugintest"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func TestAPIKeyProvisioner(t *testing.T) {
plugintest.TestProvisioner(t, APIKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{
"temp file": {
ItemFields: map[sdk.FieldName]string{
fieldname.APIKey: "zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample",
fieldname.Endpoint: "http://localhost/v1",
},
ExpectedOutput: sdk.ProvisionOutput{
Files: map[string]sdk.OutputFile{
ConfigPath(): {
Contents: []byte(plugintest.LoadFixture(t, "prefs.json")),
},
},
},
},
})
}

func TestAPIKeyImporter(t *testing.T) {
plugintest.TestImporter(t, APIKey().Importer, map[string]plugintest.ImportCase{
"Appwrite prefs file": {
Files: map[string]string{
ConfigPath(): plugintest.LoadFixture(t, "prefs.json"),
},
ExpectedCandidates: []sdk.ImportCandidate{
{
Fields: map[sdk.FieldName]string{
fieldname.APIKey: "zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample",
fieldname.Endpoint: "http://localhost/v1",
},
},
},
},
})
}
39 changes: 39 additions & 0 deletions plugins/appwrite/appwrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package appwrite

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func AppwriteCLI() schema.Executable {
return schema.Executable{
Name: "Appwrite CLI",
Runs: []string{"appwrite"},
DocsURL: sdk.URL("https://appwrite.io/docs/command-line-ci"),
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.NotWithoutArgs(),
needsauth.NotWhenContainsArgs("client"),
needsauth.NotWhenContainsArgs("login"),
needsauth.NotWhenContainsArgs("logout"),
needsauth.NotForExactArgs("deploy"),
needsauth.NotForExactArgs("projects"),
ra-jeev marked this conversation as resolved.
Show resolved Hide resolved
needsauth.NotForExactArgs("storage"),
needsauth.NotForExactArgs("teams"),
needsauth.NotForExactArgs("users"),
needsauth.NotForExactArgs("account"),
needsauth.NotForExactArgs("avatars"),
Comment on lines +24 to +27
Copy link
Member

Choose a reason for hiding this comment

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

These looks like commands that would be scoped to a specific account.

Is there any documentation you could point me to about these commands? I wonder why they would not require auth.

Copy link
Author

@ra-jeev ra-jeev Jul 7, 2023

Choose a reason for hiding this comment

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

@hculea without any argument these commands behave as if they were called with --help argument. So they just give you the list of available subcommands and options. Also, these are project specific commands. So if you run appwrite users list from the project folder (this requires auth, and can only be run from project folder which contains the appwrite.json config file), you'll get the list of all users of that project.

Here is the CLI documentation (they don't say the above in the docs, but all the examples given there are with arguments). I think @arunsathiya has verified this.

Copy link
Contributor

Choose a reason for hiding this comment

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

From my functional testing: The project token that we provision seems to work for users list and teams list. I'd imagine that it works for the rest of the subcommands for users and teams.

However, account get (one of the many subcommands under account) does not work with the project token, rather requires account scope. I think that's expecting a combination of username and password?

➜  ~ appwrite account get
#########################################################################
# WARNING: 'appwrite' is not from the official registry.                #
# Only proceed if you are the developer of 'appwrite'.                  #
# Otherwise, delete the file at /Users/arun/.op/plugins/local/appwrite. #
#########################################################################
✗ Error app.6495df658da7c618cab3@service.cloud.appwrite.io (role: applications) missing scope (account)

Not too sure how avatars' subcommands work though.

without any argument these commands behave as if they were called with --help argument

This matches my testing too. 👍🏼

Copy link
Author

@ra-jeev ra-jeev Jul 8, 2023

Choose a reason for hiding this comment

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

account commands work with currently logged in user (requires a session). users is a more appropriate command from server's perspective. account is for impersonating a user and acting in their behalf using their JWT token.

avatars offers below subcommands:

getBrowser
getCreditCard
getFavicon
getFlag
getImage
getInitials
getQR

These are some utility functions for getting images/icons for stuffs. e.g. getBrowser is for getting browser icon, getFlag for getting a country's flag icon etc.

Marking this conversation unresolved for now.

Copy link
Author

Choose a reason for hiding this comment

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

@hculea Please let me know if I need to change anything, or provide more info.

Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

So if account doesn't work with the API KEY we provision anyway (it only works with a JWT), we should make sure we also exclude this command from out auth scheme as well: needsauth.NotWhenContainsArgs("account"),.

As a bit of a background, where does that JWT come from? We should make sure this doesn't get placed somewhere on disk in plain-text.

Copy link
Author

Choose a reason for hiding this comment

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

@AndyTitu The JWT comes when you use a Login/Password to login to the CLI (which we can't use with 1Passsword as this is an interactive process). Also, in this flow the JWT actually gets written to the same config file (~/.appwrite/prefs.json) on disk, again there won't be any JWT if we're using the CI mode (which we're doing with this integration).

You're right that the account commands don't work with an API KEY. Please let me know if I need to make this change needsauth.NotWhenContainsArgs("account").

Thanks,
Rajeev

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think that change makes sense.

Sorry for the delayed reply!

Copy link
Author

Choose a reason for hiding this comment

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

No problem @accraw. Don't think this will be merged so it is okay. :-)

needsauth.NotForExactArgs("functions"),
needsauth.NotForExactArgs("databases"),
needsauth.NotForExactArgs("health"),
needsauth.NotForExactArgs("locale"),
),
Uses: []schema.CredentialUsage{
{
Name: credname.APIKey,
},
},
}
}
22 changes: 22 additions & 0 deletions plugins/appwrite/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package appwrite

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/schema"
)

func New() schema.Plugin {
return schema.Plugin{
Name: "appwrite",
Platform: schema.PlatformInfo{
Name: "Appwrite",
Homepage: sdk.URL("https://appwrite.io"),
},
Credentials: []schema.CredentialType{
APIKey(),
},
Executables: []schema.Executable{
AppwriteCLI(),
},
}
}
4 changes: 4 additions & 0 deletions plugins/appwrite/test-fixtures/prefs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"key": "zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample",
"endpoint": "http://localhost/v1"
}