From 266b32442870f379ce4d047d7a5c9f87f05b0e5b Mon Sep 17 00:00:00 2001 From: fazal Date: Sun, 3 Jul 2016 14:28:22 +0500 Subject: [PATCH] sdk: add wrapper library for HTTP Managers (#130) Signed-off-by: Mohamedh Fazal --- README.md | 130 ++++++++++++++++++++++++++++++++++++++++ sdk/client.go | 129 +++++++++++++++++++++++++++++++++++++++ sdk/client_opts.go | 83 +++++++++++++++++++++++++ sdk/client_opts_test.go | 85 ++++++++++++++++++++++++++ 4 files changed, 427 insertions(+) create mode 100644 sdk/client.go create mode 100644 sdk/client_opts.go create mode 100644 sdk/client_opts_test.go diff --git a/README.md b/README.md index 0e8a1d41458..1807be184d8 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,136 @@ Or by specifying the following flags: You can do so by running `hydra host --force-dangerous-http`. +### How do I use it in my application? + +Hydra already comes with HTTP Managers. You could use directly by importing the following or use the thin wrapper created in `ory-am/hydra/sdk` + +#### Using HTTP Managers + +**Manage OAuth Clients** +`ory-am/hydra/client.HTTPManager` + +**Manage SSO Connections** +`ory-am/hydra/connection.HTTPManager` + +**Manage Policies** +`ory-am/hydra/policy.HTTPManager` + +**Manage JWK** +`ory-am/hydra/jwk.HTTPManager` + +**Use Warden** +`ory-am/hydra/warden.HTTPWarden` + +#### Using SDK + +**Connect to Hydra.** +``` +client, err := sdk.Connect( + sdk.ClientID("client-id"), + sdk.ClientSecret("client-secret"), + sdk.ClustURL("https://localhost:4444"), +) +``` +**Use the API** + +**OAuth Clients**: `client.Client` + +``` +// To create a new OAuth2 client +newClient, err := client.Client.CreateClient(&client.Client{ + ID: "deadbeef", + Secret: "sup3rs3cret", + RedirectURIs: []string{"http://yourapp/callback"}, + // ... +}) + +// Retrieve newly created client +newClient, err = client.Client.GetClient(newClient.ID) + +// To remove newly created client +err = client.Client.DeleteClient(newClient.ID) + +// Retrieve list of all clients +clients, err := client.Client.GetClients() +``` + + +**SSO Connections**: `client.SSO` + +``` +// Create a new connection +newSSOConn, err := client.SSO.Create(&connection.Connection{ + Provider: "login.google.com", + LocalSubject: "bob", + RemoteSubject: "googleSubjectID", +}) + +// Retrieve newly created connection +ssoConn, err := client.SSO.Get(newSSOConn.ID) + +// Delete connection +ssoConn, err := client.SSO.Delete(newSSOConn.ID) + +// Find a connection by subject +ssoConns, err := client.SSO.FindAllByLocalSubject("bob") +ssoConns, err := client.SSO.FindByRemoteSubject("login.google.com", "googleSubjectID") +``` + +**Policiess**: `client.Policy` + +``` +// Create a new policy +// allow user to view his/her own photos +newPolicy, err := client.Policy.Create(ladon.DefaultPolicy{ + ID: "1234", // ID is not required + Subjects: []string{"bob"}, + Resources: []string{"urn:media:images"}, + Actions: []string{"get", "find"}, + Effect: ladon.AllowAccess, + Conditions: ladon.Conditions{ + "owner": &ladon.EqualSubjectCondition{}, + } +}) + +// Retrieve a stored policy +policy, err := client.Policy.Get("1234") + +// Delete a policy +err := client.Policy.Delete("1234") + +// Retrieve all policies for a subject +policies, err := client.Policy.FindPoliciesForSubject("bob") +``` + +**JWK**: `client.JWK` + +``` +// Generate new key set +keySet, err := client.JWK.CreateKeys("app-tls-keys", "HS256") + +// Retrieve key set +keySet, err := client.JWK.GetKeySet("app-tls-keys") + +// Delete key set +err := client.JWK.DeleteKeySet("app-tls-keys") +``` + +**Warden**: `client.Warden` + +``` +// Check if action is allowed +client.Warden.HTTPActionAllowed(ctx, req, &ladon.Request{ + Resource: "urn:media:images", + Action: "get", + Subject: "bob", +}, "media.images") + +// Check if request is authorized +client.Warden.HTTPAuthorized(ctx, req, "media.images") + +``` + ## Hall of Fame A list of extraordinary contributors and [bug hunters](https://github.com/ory-am/hydra/issues/84). diff --git a/sdk/client.go b/sdk/client.go new file mode 100644 index 00000000000..db77e004339 --- /dev/null +++ b/sdk/client.go @@ -0,0 +1,129 @@ +// Wraps hydra HTTP Manager's +package sdk + +import ( + "crypto/tls" + "net/http" + "net/url" + "os" + + "github.com/ory-am/hydra/client" + "github.com/ory-am/hydra/connection" + "github.com/ory-am/hydra/jwk" + "github.com/ory-am/hydra/pkg" + "github.com/ory-am/hydra/policy" + "github.com/ory-am/hydra/warden" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +type Client struct { + http *http.Client + clusterURL *url.URL + clientID string + clientSecret string + skipTLSVerify bool + scopes []string + + credentials clientcredentials.Config + + Client *client.HTTPManager + SSO *connection.HTTPManager + JWK *jwk.HTTPManager + Policies *policy.HTTPManager + Warden *warden.HTTPWarden +} + +type option func(*Client) error + +// default options for hydra client +var defaultOptions = []option{ + ClusterURL(os.Getenv("HYDRA_CLUSTER_URL")), + ClientID(os.Getenv("HYDRA_CLIENT_ID")), + ClientSecret(os.Getenv("HYDRA_CLIENT_SECRET")), + Scopes("core", "hydra"), +} + +// Connect instantiates a new client to communicate with Hydra +func Connect(opts ...option) (*Client, error) { + c := &Client{} + + var err error + // apply default options + for _, opt := range defaultOptions { + err = opt(c) + if err != nil { + return nil, err + } + } + + // override any default values with given options + for _, opt := range opts { + err = opt(c) + if err != nil { + return nil, err + } + } + + c.credentials = clientcredentials.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + TokenURL: pkg.JoinURL(c.clusterURL, "oauth2/token").String(), + Scopes: c.scopes, + } + + c.http = http.DefaultClient + + if c.skipTLSVerify { + c.http = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + } + + err = c.authenticate() + if err != nil { + return nil, err + } + + // initialize service endpoints + c.Client = &client.HTTPManager{ + Endpoint: pkg.JoinURL(c.clusterURL, "/clients"), + Client: c.http, + } + + c.SSO = &connection.HTTPManager{ + Endpoint: pkg.JoinURL(c.clusterURL, "/connections"), + Client: c.http, + } + + c.JWK = &jwk.HTTPManager{ + Endpoint: pkg.JoinURL(c.clusterURL, "/keys"), + Client: c.http, + } + + c.Policies = &policy.HTTPManager{ + Endpoint: pkg.JoinURL(c.clusterURL, "/policies"), + Client: c.http, + } + + c.Warden = &warden.HTTPWarden{ + Client: c.http, + } + + return c, nil +} + +func (h *Client) authenticate() error { + ctx := context.WithValue(oauth2.NoContext, oauth2.HTTPClient, h.http) + _, err := h.credentials.Token(ctx) + if err != nil { + return err + } + + h.http = h.credentials.Client(ctx) + return nil +} diff --git a/sdk/client_opts.go b/sdk/client_opts.go new file mode 100644 index 00000000000..c028d73b6b0 --- /dev/null +++ b/sdk/client_opts.go @@ -0,0 +1,83 @@ +package sdk + +import ( + "io/ioutil" + "net/url" + + "gopkg.in/yaml.v1" +) + +// ClusterURL sets Hydra service URL +func ClusterURL(urlStr string) option { + return func(c *Client) error { + var err error + c.clusterURL, err = url.Parse(urlStr) + return err + } +} + +type hydraConfig struct { + ClusterURL string `yaml:"cluster_url"` + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` +} + +// FromYAML loads configurations from a YAML file +func FromYAML(file string) option { + return func(c *Client) error { + var err error + var config = hydraConfig{} + + data, err := ioutil.ReadFile(file) + if err != nil { + return err + } + + err = yaml.Unmarshal(data, &config) + if err != nil { + return err + } + + c.clusterURL, err = url.Parse(config.ClusterURL) + if err != nil { + return err + } + + c.clientID = config.ClientID + c.clientSecret = config.ClientSecret + + return nil + } +} + +// ClientID sets OAuth client ID +func ClientID(id string) option { + return func(c *Client) error { + c.clientID = id + return nil + } +} + +// ClientSecret sets OAuth client secret +func ClientSecret(secret string) option { + return func(c *Client) error { + c.clientSecret = secret + return nil + } +} + +// SkipTLSVerify skips TLS verification +func SkipTLSVerify() option { + return func(c *Client) error { + c.skipTLSVerify = true + return nil + } +} + +// Scopes sets client scopes granted by Hydra +func Scopes(scopes ...string) option { + return func(c *Client) error { + c.scopes = scopes + return nil + } +} diff --git a/sdk/client_opts_test.go b/sdk/client_opts_test.go new file mode 100644 index 00000000000..fb035e6e402 --- /dev/null +++ b/sdk/client_opts_test.go @@ -0,0 +1,85 @@ +package sdk + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v1" +) + +func TestClusterURLOption(t *testing.T) { + c := &Client{} + expected := "https://localhost:4444" + err := ClusterURL(expected)(c) + + assert.Nil(t, err) + assert.Equal(t, expected, c.clusterURL.String()) +} + +func TestClientIDOption(t *testing.T) { + c := &Client{} + expected := "deadbeef-dead-beef-beef" + err := ClientID(expected)(c) + + assert.Nil(t, err) + assert.Equal(t, expected, c.clientID) +} + +func TestClientSecretOption(t *testing.T) { + c := &Client{} + + expected := "secret" + err := ClientSecret(expected)(c) + + assert.Nil(t, err) + assert.Equal(t, expected, c.clientSecret) +} + +func TestSkipSSLOption(t *testing.T) { + c := &Client{} + + err := SkipTLSVerify()(c) + + assert.Nil(t, err) + assert.Equal(t, true, c.skipTLSVerify) +} + +func TestScopesOption(t *testing.T) { + c := &Client{} + + expected := []string{"a", "b", "c"} + err := Scopes(expected...)(c) + + assert.Nil(t, err) + assert.Equal(t, expected, c.scopes) +} + +func TestFromYAMLOption(t *testing.T) { + c := &Client{} + + conf := &hydraConfig{ + ClusterURL: "https://localhost:4444", + ClientID: "1cfe0a5e-2533-4312-9e74-128b5aab4431", + ClientSecret: "Q6&u=iwvTPh8r)Ar", + } + + tmpFile, err := ioutil.TempFile("", "hydra_sdk") + assert.Nil(t, err) + + fileContent, err := yaml.Marshal(conf) + assert.Nil(t, err) + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + _, err = tmpFile.Write(fileContent) + assert.Nil(t, err) + + err = FromYAML(tmpFile.Name())(c) + assert.Nil(t, err) + + assert.Equal(t, conf.ClusterURL, c.clusterURL.String()) + assert.Equal(t, conf.ClientID, c.clientID) + assert.Equal(t, conf.ClientSecret, c.clientSecret) +}