From 01a2f9275678dd4d078b9e7ab95cc9a1d98899fa Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Wed, 13 Jan 2021 22:48:29 +0000 Subject: [PATCH] Move oauth implementation into internal package, add documentation --- README.md | 56 ++++++++++++++++++- auth/auth.go | 47 ++++++++++++---- auth/azcli.go | 7 +++ auth/claims.go | 2 + .../microsoft/internal/oauth2.go | 0 auth/{ => internal}/microsoft/microsoft.go | 2 +- base/base.go | 29 ++++++++-- base/delete.go | 4 ++ base/get.go | 4 ++ base/patch.go | 4 ++ base/post.go | 4 ++ base/put.go | 4 ++ clients/applications.go | 18 ++++++ clients/domains.go | 4 ++ clients/groups.go | 27 +++++++++ clients/me.go | 4 ++ clients/serviceprincipals.go | 17 ++++++ clients/users.go | 7 +++ errors/errors.go | 2 + models/applications.go | 5 ++ models/credentials.go | 2 + models/domains.go | 1 + models/error.go | 1 + models/groups.go | 3 + models/me.go | 1 + models/serviceprincipals.go | 2 + models/users.go | 1 + 27 files changed, 239 insertions(+), 19 deletions(-) rename auth/{ => internal}/microsoft/internal/oauth2.go (100%) rename auth/{ => internal}/microsoft/microsoft.go (98%) diff --git a/README.md b/README.md index 810b4250..074f8563 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,57 @@ # Hamilton is a Go SDK for Microsoft Graph -This is an active work in progress and highly subject to change. +This is a working Go client for the [Microsoft Graph API][ms-graph-docs]. It is actively maintained and has growing support for services and objects in Azure Active Directory. -Do not use. +## Example Usage + +```go +package example + +import ( + "context" + "fmt" + "log" + + "github.com/manicminer/hamilton/auth" + "github.com/manicminer/hamilton/clients" +) + +const ( + tenantId = "00000000-0000-0000-0000-000000000000" + clientId = "11111111-1111-1111-1111-111111111111" + clientSecret = "My$3cR3t" +) + +func main() { + ctx := context.Background() + + authConfig := &auth.Config{ + TenantID: tenantId, + ClientID: clientId, + ClientSecret: clientSecret, + EnableClientSecretAuth: true, + } + + authorizer, err := authConfig.NewAuthorizer(ctx) + if err != nil { + log.Fatal(err) + } + + client := clients.NewUsersClient(tenantId) + client.BaseClient.Authorizer = authorizer + + users, _, err := client.List(ctx, "") + if err != nil { + log.Fatal(err) + } + if users == nil { + log.Fatalln("bad API response, nil result received") + } + + for _, user := range *users { + fmt.Printf("%s: %s <%s>\n", *user.ID, *user.DisplayName, *user.Mail) + } +} +``` + +[ms-graph-docs]: https://docs.microsoft.com/en-us/graph/overview \ No newline at end of file diff --git a/auth/auth.go b/auth/auth.go index 7d24b446..4a009358 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -13,37 +13,59 @@ import ( "io/ioutil" "strings" - microsoft2 "github.com/manicminer/hamilton/auth/microsoft" + microsoft2 "github.com/manicminer/hamilton/auth/internal/microsoft" ) type Config struct { - //Environment string + // Azure Active Directory tenant to connect to, should be a valid UUID TenantID string + + // Client ID for the application used to authenticate the connection ClientID string - // Azure CLI Tokens Auth + // Enables authentication using Azure CLI EnableAzureCliToken bool - // Managed Service Identity Auth + // Enables authentication using managed service identity. Not yet supported. // TODO: NOT YET SUPPORTED EnableMsiAuth bool - MsiEndpoint string - // Service Principal (Client Cert) Auth + // Specifies a custom MSI endpoint to connect to + MsiEndpoint string + + // Enables client certificate authentication using client assertions EnableClientCertAuth bool - ClientCertPath string - ClientCertPassword string - // Service Principal (Client Secret) Auth + // Specifies the path to a client certificate bundle in PFX format + ClientCertPath string + + // Specifies the encryption password to unlock a client certificate + ClientCertPassword string + + // Enables client secret authentication using client credentials EnableClientSecretAuth bool - ClientSecret string - ClientSecretDocsLink string + + // Specifies the password to authenticate with using client secret authentication + ClientSecret string } +// Authorizer is anything that can return an access token for authorizing API connections type Authorizer interface { Token() (*oauth2.Token, error) } +// NewAuthorizer returns a suitable Authorizer depending on what is defined in the Config +// Authorizers are selected for authentication methods in the following preferential order: +// - Client certificate authentication +// - Client secret authentication +// - Azure CLI authentication +// +// Whether one of these is returned depends on whether it is enabled in the Config, and whether sufficient +// configuration fields are set to enable that authentication method. +// +// For client certificate authentication, specify TenantID, ClientID and ClientCertPath. +// For client secret authentication, specify TenantID, ClientID and ClientSecret. +// Azure CLI authentication (if enabled) is used as a fallback mechanism. func (c *Config) NewAuthorizer(ctx context.Context) (Authorizer, error) { if c.EnableClientCertAuth && strings.TrimSpace(c.TenantID) != "" && strings.TrimSpace(c.ClientID) != "" && strings.TrimSpace(c.ClientCertPath) != "" { a, err := NewClientCertificateAuthorizer(ctx, c.TenantID, c.ClientID, c.ClientCertPath, c.ClientCertPassword) @@ -78,6 +100,7 @@ func (c *Config) NewAuthorizer(ctx context.Context) (Authorizer, error) { return nil, fmt.Errorf("no Authorizer could be configured, please check your configuration") } +// NewAzureCliAuthorizer returns an Authorizer which authenticates using the Azure CLI. func NewAzureCliAuthorizer(ctx context.Context, tenantId string) (Authorizer, error) { conf, err := NewAzureCliConfig(tenantId) if err != nil { @@ -86,6 +109,7 @@ func NewAzureCliAuthorizer(ctx context.Context, tenantId string) (Authorizer, er return conf.TokenSource(ctx), nil } +// NewClientCertificateAuthorizer returns an authorizer which uses client certificate authentication. func NewClientCertificateAuthorizer(ctx context.Context, tenantId, clientId, pfxPath, pfxPass string) (Authorizer, error) { pfx, err := ioutil.ReadFile(pfxPath) if err != nil { @@ -112,6 +136,7 @@ func NewClientCertificateAuthorizer(ctx context.Context, tenantId, clientId, pfx return conf.TokenSource(ctx), nil } +// NewClientSecretAuthorizer returns an authorizer which uses client secret authentication. func NewClientSecretAuthorizer(ctx context.Context, tenantId, clientId, clientSecret string) (Authorizer, error) { conf := clientcredentials.Config{ AuthStyle: oauth2.AuthStyleInParams, diff --git a/auth/azcli.go b/auth/azcli.go index 82b5d419..2636f7aa 100644 --- a/auth/azcli.go +++ b/auth/azcli.go @@ -12,13 +12,16 @@ import ( "time" ) +// AzureCliAuthorizer is an Authorizer which supports the Azure CLI. type AzureCliAuthorizer struct { + // TenantID is optional and forces selection of the specified tenant. Must be a valid UUID. TenantID string ctx context.Context conf *AzureCliConfig } +// Token returns an access token using the Azure CLI as an authentication mechanism. func (a AzureCliAuthorizer) Token() (*oauth2.Token, error) { // We don't need to handle token caching and refreshing since az-cli does that for us var token struct { @@ -40,10 +43,12 @@ func (a AzureCliAuthorizer) Token() (*oauth2.Token, error) { }, nil } +// AzureCliConfig configures an AzureCliAuthorizer. type AzureCliConfig struct { TenantID string } +// NewAzureCliConfig validates the supplied tenant ID and returns a new AzureCliConfig. func NewAzureCliConfig(tenantId string) (*AzureCliConfig, error) { // check az-cli version @@ -69,6 +74,7 @@ func NewAzureCliConfig(tenantId string) (*AzureCliConfig, error) { return &AzureCliConfig{TenantID: tenantId}, nil } +// TokenSource provides a source for obtaining access tokens using AzureCliAuthorizer. func (c *AzureCliConfig) TokenSource(ctx context.Context) Authorizer { return &AzureCliAuthorizer{ TenantID: c.TenantID, @@ -77,6 +83,7 @@ func (c *AzureCliConfig) TokenSource(ctx context.Context) Authorizer { } } +// jsonUnmarshalAzCmd executes an Azure CLI command and unmarshals the JSON output. func jsonUnmarshalAzCmd(i interface{}, arg ...string) error { var stderr bytes.Buffer var stdout bytes.Buffer diff --git a/auth/claims.go b/auth/claims.go index 8ac1cbfd..4a809735 100644 --- a/auth/claims.go +++ b/auth/claims.go @@ -7,6 +7,7 @@ import ( "strings" ) +// Claims is used to unmarshall the claims from a JWT issued by the Microsoft Identity Platform. type Claims struct { Audience string `json:"aud"` Issuer string `json:"iss"` @@ -23,6 +24,7 @@ type Claims struct { IdType string `json:"idtyp,omitempty"` } +// ParseClaims retrieves and parses the claims from a JWT issued by the Microsoft Identity Platform. func ParseClaims(token *oauth2.Token) (claims Claims, err error) { if token == nil { return diff --git a/auth/microsoft/internal/oauth2.go b/auth/internal/microsoft/internal/oauth2.go similarity index 100% rename from auth/microsoft/internal/oauth2.go rename to auth/internal/microsoft/internal/oauth2.go diff --git a/auth/microsoft/microsoft.go b/auth/internal/microsoft/microsoft.go similarity index 98% rename from auth/microsoft/microsoft.go rename to auth/internal/microsoft/microsoft.go index f879adb0..b5187747 100644 --- a/auth/microsoft/microsoft.go +++ b/auth/internal/microsoft/microsoft.go @@ -17,7 +17,7 @@ import ( "strings" "time" - "github.com/manicminer/hamilton/auth/microsoft/internal" + "github.com/manicminer/hamilton/auth/internal/microsoft/internal" ) // Config is the configuration for using client credentials flow with a client assertion. diff --git a/base/base.go b/base/base.go index 4711d082..ec3f4c78 100644 --- a/base/base.go +++ b/base/base.go @@ -16,40 +16,57 @@ const ( VersionBeta = "beta" ) +// ValidStatusFunc is a function that tests whether an HTTP response is considered valid for the particular request. type ValidStatusFunc func(response *http.Response) bool +// HttpRequestInput is any type that can validate the response to an HTTP request. type HttpRequestInput interface { GetValidStatusCodes() []int GetValidStatusFunc() ValidStatusFunc } +// Uri represents a Microsoft Graph endpoint. type Uri struct { Entity string Params url.Values HasTenantId bool } +// GraphClient is any suitable HTTP client. type GraphClient = *http.Client +// Client is a base client to be used by clients for specific entities. +// It can send GET, POST, PUT, PATCH and DELETE requests to Microsoft Graph and is API version and tenant aware. type Client struct { + // ApiVersion is the Microsoft Graph API version to use. ApiVersion string - Endpoint string - TenantId string - UserAgent string + // Endpoint is the base endpoint for Microsoft Graph, usually "https://graph.microsoft.com". + Endpoint string + + // TenantId is the tenant ID to use in requests. + TenantId string + + // UserAgent is the HTTP user agent string to send in requests. + UserAgent string + + // Authorizer is anything that can provide an access token with which to authorize requests. Authorizer auth.Authorizer + httpClient GraphClient } -func NewClient(endpoint, tenantId, version string) Client { +// NewClient returns a new Client configured with the specified endpoint, tenant ID and API version. +func NewClient(endpoint, tenantId, apiVersion string) Client { return Client{ httpClient: http.DefaultClient, Endpoint: endpoint, TenantId: tenantId, - ApiVersion: version, + ApiVersion: apiVersion, } } +// buildUri is used by the package to build a complete URI string for API requests. func (c Client) buildUri(uri Uri) (string, error) { url, err := url.Parse(c.Endpoint) if err != nil { @@ -66,6 +83,7 @@ func (c Client) buildUri(uri Uri) (string, error) { return url.String(), nil } +// performRequest is used by the package to send an HTTP request to the API. func (c Client) performRequest(req *http.Request, input HttpRequestInput) (*http.Response, int, error) { var status int @@ -104,6 +122,7 @@ func (c Client) performRequest(req *http.Request, input HttpRequestInput) (*http return resp, status, nil } +// containsStatusCode determines whether the returned status code is in the []int of expected status codes. func containsStatusCode(expected []int, actual int) bool { for _, v := range expected { if actual == v { diff --git a/base/delete.go b/base/delete.go index 07dba7af..d903631d 100644 --- a/base/delete.go +++ b/base/delete.go @@ -6,20 +6,24 @@ import ( "net/http" ) +// DeleteHttpRequestInput configures a DELETE request. type DeleteHttpRequestInput struct { ValidStatusCodes []int ValidStatusFunc ValidStatusFunc Uri Uri } +// GetValidStatusCodes returns a []int of status codes considered valid for a DELETE request. func (i DeleteHttpRequestInput) GetValidStatusCodes() []int { return i.ValidStatusCodes } +// GetValidStatusFunc returns a function used to evaluate whether the response to a DELETE request is considered valid. func (i DeleteHttpRequestInput) GetValidStatusFunc() ValidStatusFunc { return i.ValidStatusFunc } +// Delete performs a DELETE request. func (c Client) Delete(ctx context.Context, input DeleteHttpRequestInput) (*http.Response, int, error) { var status int url, err := c.buildUri(input.Uri) diff --git a/base/get.go b/base/get.go index 6b80b734..6ebb3541 100644 --- a/base/get.go +++ b/base/get.go @@ -6,20 +6,24 @@ import ( "net/http" ) +// GetHttpRequestInput configures a GET request. type GetHttpRequestInput struct { ValidStatusCodes []int ValidStatusFunc ValidStatusFunc Uri Uri } +// GetValidStatusCodes returns a []int of status codes considered valid for a GET request. func (i GetHttpRequestInput) GetValidStatusCodes() []int { return i.ValidStatusCodes } +// GetValidStatusFunc returns a function used to evaluate whether the response to a GET request is considered valid. func (i GetHttpRequestInput) GetValidStatusFunc() ValidStatusFunc { return i.ValidStatusFunc } +// Get performs a GET request. func (c Client) Get(ctx context.Context, input GetHttpRequestInput) (*http.Response, int, error) { var status int url, err := c.buildUri(input.Uri) diff --git a/base/patch.go b/base/patch.go index 046bbc39..88a8274e 100644 --- a/base/patch.go +++ b/base/patch.go @@ -7,6 +7,7 @@ import ( "net/http" ) +// PatchHttpRequestInput configures a PATCH request. type PatchHttpRequestInput struct { Body []byte ValidStatusCodes []int @@ -14,14 +15,17 @@ type PatchHttpRequestInput struct { Uri Uri } +// GetValidStatusCodes returns a []int of status codes considered valid for a PATCH request. func (i PatchHttpRequestInput) GetValidStatusCodes() []int { return i.ValidStatusCodes } +// GetValidStatusFunc returns a function used to evaluate whether the response to a PATCH request is considered valid. func (i PatchHttpRequestInput) GetValidStatusFunc() ValidStatusFunc { return i.ValidStatusFunc } +// Patch performs a PATCH request. func (c Client) Patch(ctx context.Context, input PatchHttpRequestInput) (*http.Response, int, error) { var status int url, err := c.buildUri(input.Uri) diff --git a/base/post.go b/base/post.go index d30c8b26..4ff802b4 100644 --- a/base/post.go +++ b/base/post.go @@ -7,6 +7,7 @@ import ( "net/http" ) +// PostHttpRequestInput configures a POST request. type PostHttpRequestInput struct { Body []byte ValidStatusCodes []int @@ -14,14 +15,17 @@ type PostHttpRequestInput struct { Uri Uri } +// GetValidStatusCodes returns a []int of status codes considered valid for a POST request. func (i PostHttpRequestInput) GetValidStatusCodes() []int { return i.ValidStatusCodes } +// GetValidStatusFunc returns a function used to evaluate whether the response to a POST request is considered valid. func (i PostHttpRequestInput) GetValidStatusFunc() ValidStatusFunc { return i.ValidStatusFunc } +// Post performs a POST request. func (c Client) Post(ctx context.Context, input PostHttpRequestInput) (*http.Response, int, error) { var status int url, err := c.buildUri(input.Uri) diff --git a/base/put.go b/base/put.go index c3be0e90..dccc1c97 100644 --- a/base/put.go +++ b/base/put.go @@ -7,6 +7,7 @@ import ( "net/http" ) +// PutHttpRequestInput configures a PUT request. type PutHttpRequestInput struct { Body []byte ValidStatusCodes []int @@ -14,14 +15,17 @@ type PutHttpRequestInput struct { Uri Uri } +// GetValidStatusCodes returns a []int of status codes considered valid for a PUT request. func (i PutHttpRequestInput) GetValidStatusCodes() []int { return i.ValidStatusCodes } +// GetValidStatusFunc returns a function used to evaluate whether the response to a PUT request is considered valid. func (i PutHttpRequestInput) GetValidStatusFunc() ValidStatusFunc { return i.ValidStatusFunc } +// Put performs a PUT request. func (c Client) Put(ctx context.Context, input PutHttpRequestInput) (*http.Response, int, error) { var status int url, err := c.buildUri(input.Uri) diff --git a/clients/applications.go b/clients/applications.go index 6bdf671e..bceca50f 100644 --- a/clients/applications.go +++ b/clients/applications.go @@ -14,16 +14,19 @@ import ( "github.com/manicminer/hamilton/models" ) +// ApplicationsCLient performs operations on Applications. type ApplicationsClient struct { BaseClient base.Client } +// NewApplicationsClient returns a new ApplicationsClient func NewApplicationsClient(tenantId string) *ApplicationsClient { return &ApplicationsClient{ BaseClient: base.NewClient(base.DefaultEndpoint, tenantId, base.VersionBeta), } } +// List returns a list of Applications, optionally filtered using OData. func (c *ApplicationsClient) List(ctx context.Context, filter string) (*[]models.Application, int, error) { params := url.Values{} if filter != "" { @@ -51,6 +54,7 @@ func (c *ApplicationsClient) List(ctx context.Context, filter string) (*[]models return &data.Applications, status, nil } +// Create creates a new Application. func (c *ApplicationsClient) Create(ctx context.Context, application models.Application) (*models.Application, int, error) { var status int body, err := json.Marshal(application) @@ -77,6 +81,7 @@ func (c *ApplicationsClient) Create(ctx context.Context, application models.Appl return &newApplication, status, nil } +// Get retrieves an Application manifest. func (c *ApplicationsClient) Get(ctx context.Context, id string) (*models.Application, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -97,6 +102,7 @@ func (c *ApplicationsClient) Get(ctx context.Context, id string) (*models.Applic return &application, status, nil } +// Update amends the manifest of an existing Application. func (c *ApplicationsClient) Update(ctx context.Context, application models.Application) (int, error) { var status int if application.ID == nil { @@ -120,6 +126,7 @@ func (c *ApplicationsClient) Update(ctx context.Context, application models.Appl return status, nil } +// Delete removes an Application. func (c *ApplicationsClient) Delete(ctx context.Context, id string) (int, error) { _, status, err := c.BaseClient.Delete(ctx, base.DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, @@ -134,6 +141,7 @@ func (c *ApplicationsClient) Delete(ctx context.Context, id string) (int, error) return status, nil } +// AddKey appends a new key credential to an Application. func (c *ApplicationsClient) AddKey(ctx context.Context, applicationId string, keyCredential models.KeyCredential) (*models.KeyCredential, int, error) { var status int body, err := json.Marshal(keyCredential) @@ -160,6 +168,8 @@ func (c *ApplicationsClient) AddKey(ctx context.Context, applicationId string, k return &newKeyCredential, status, nil } +// ListOwners retrieves the owners of the specified Application. +// id is the object ID of the application. func (c *ApplicationsClient) ListOwners(ctx context.Context, id string) (*[]string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -191,6 +201,9 @@ func (c *ApplicationsClient) ListOwners(ctx context.Context, id string) (*[]stri return &ret, status, nil } +// GetOwner retrieves a single owner for the specified Application. +// applicationId is the object ID of the application. +// ownerId is the object ID of the owning object. func (c *ApplicationsClient) GetOwner(ctx context.Context, applicationId, ownerId string) (*string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -218,6 +231,8 @@ func (c *ApplicationsClient) GetOwner(ctx context.Context, applicationId, ownerI return &data.Id, status, nil } +// AddOwners adds a new owner to an Application. +// First populate the Owners field of the Application using the AppendOwner method of the model, then call this method. func (c *ApplicationsClient) AddOwners(ctx context.Context, application *models.Application) (int, error) { var status int if application.ID == nil { @@ -251,6 +266,9 @@ func (c *ApplicationsClient) AddOwners(ctx context.Context, application *models. return status, nil } +// RemoveOwners removes owners from an Application. +// applicationId is the object ID of the application. +// ownerIds is a *[]string containing object IDs of owners to remove. func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId string, ownerIds *[]string) (int, error) { var status int if ownerIds == nil { diff --git a/clients/domains.go b/clients/domains.go index b60ed443..d62834d6 100644 --- a/clients/domains.go +++ b/clients/domains.go @@ -11,16 +11,19 @@ import ( "github.com/manicminer/hamilton/models" ) +// DomainsClient performs operations on Domains. type DomainsClient struct { BaseClient base.Client } +// NewDomainsClient returns a new DomainsClient. func NewDomainsClient(tenantId string) *DomainsClient { return &DomainsClient{ BaseClient: base.NewClient(base.DefaultEndpoint, tenantId, base.Version10), } } +// List returns a list of Domains. func (c *DomainsClient) List(ctx context.Context) (*[]models.Domain, int, error) { var status int resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ @@ -49,6 +52,7 @@ func (c *DomainsClient) List(ctx context.Context) (*[]models.Domain, int, error) return &data.Domains, status, nil } +// Get retrieves a Domain. func (c *DomainsClient) Get(ctx context.Context, id string) (*models.Domain, int, error) { var status int resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ diff --git a/clients/groups.go b/clients/groups.go index a3f21392..51b076c2 100644 --- a/clients/groups.go +++ b/clients/groups.go @@ -12,16 +12,19 @@ import ( "github.com/manicminer/hamilton/models" ) +// GroupsClient performs operations on Groups. type GroupsClient struct { BaseClient base.Client } +// NewGroupsClient returns a new GroupsClient. func NewGroupsClient(tenantId string) *GroupsClient { return &GroupsClient{ BaseClient: base.NewClient(base.DefaultEndpoint, tenantId, base.VersionBeta), } } +// List returns a list of Groups, optionally filtered using OData. func (c *GroupsClient) List(ctx context.Context, filter string) (*[]models.Group, int, error) { params := url.Values{} if filter != "" { @@ -49,6 +52,7 @@ func (c *GroupsClient) List(ctx context.Context, filter string) (*[]models.Group return &data.Groups, status, nil } +// Create creates a new Group. func (c *GroupsClient) Create(ctx context.Context, group models.Group) (*models.Group, int, error) { var status int body, err := json.Marshal(group) @@ -75,6 +79,7 @@ func (c *GroupsClient) Create(ctx context.Context, group models.Group) (*models. return &newGroup, status, nil } +// Get retrieves a Group. func (c *GroupsClient) Get(ctx context.Context, id string) (*models.Group, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -95,6 +100,7 @@ func (c *GroupsClient) Get(ctx context.Context, id string) (*models.Group, int, return &group, status, nil } +// Update amends an existing Group. func (c *GroupsClient) Update(ctx context.Context, group models.Group) (int, error) { var status int body, err := json.Marshal(group) @@ -115,6 +121,7 @@ func (c *GroupsClient) Update(ctx context.Context, group models.Group) (int, err return status, nil } +// Delete removes a Group. func (c *GroupsClient) Delete(ctx context.Context, id string) (int, error) { _, status, err := c.BaseClient.Delete(ctx, base.DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, @@ -129,6 +136,8 @@ func (c *GroupsClient) Delete(ctx context.Context, id string) (int, error) { return status, nil } +// ListMembers retrieves the members of the specified Group. +// id is the object ID of the group. func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -159,6 +168,9 @@ func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, i return &ret, status, nil } +// GetMember retrieves a single member of the specified Group. +// groupId is the object ID of the group. +// memberId is the object ID of the member object. func (c *GroupsClient) GetMember(ctx context.Context, groupId, memberId string) (*string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -185,6 +197,8 @@ func (c *GroupsClient) GetMember(ctx context.Context, groupId, memberId string) return &data.Id, status, nil } +// AddMembers adds a new member to a Group. +// First populate the Members field of the Group using the AppendMember method of the model, then call this method. func (c *GroupsClient) AddMembers(ctx context.Context, group *models.Group) (int, error) { var status int // Patching group members support up to 20 members per request @@ -224,6 +238,9 @@ func (c *GroupsClient) AddMembers(ctx context.Context, group *models.Group) (int return status, nil } +// RemoveMembers removes members from a Group. +// groupId is the object ID of the group. +// memberIds is a *[]string containing object IDs of members to remove. func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds *[]string) (int, error) { var status int for _, memberId := range *memberIds { @@ -248,6 +265,8 @@ func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds * return status, nil } +// ListOwners retrieves the owners of the specified Group. +// id is the object ID of the group. func (c *GroupsClient) ListOwners(ctx context.Context, id string) (*[]string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -278,6 +297,9 @@ func (c *GroupsClient) ListOwners(ctx context.Context, id string) (*[]string, in return &ret, status, nil } +// GetOwner retrieves a single owner for the specified Group. +// groupId is the object ID of the group. +// ownerId is the object ID of the owning object. func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (*string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -304,6 +326,8 @@ func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (* return &data.Id, status, nil } +// AddOwners adds a new owner to a Group. +// First populate the Owners field of the Group using the AppendOwner method of the model, then call this method. func (c *GroupsClient) AddOwners(ctx context.Context, group *models.Group) (int, error) { var status int for _, owner := range *group.Owners { @@ -331,6 +355,9 @@ func (c *GroupsClient) AddOwners(ctx context.Context, group *models.Group) (int, return status, nil } +// RemoveOwners removes owners from a Group. +// groupId is the object ID of the group. +// ownerIds is a *[]string containing object IDs of owners to remove. func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[]string) (int, error) { var status int for _, ownerId := range *ownerIds { diff --git a/clients/me.go b/clients/me.go index 4234093b..48ec7fe6 100644 --- a/clients/me.go +++ b/clients/me.go @@ -10,16 +10,19 @@ import ( "github.com/manicminer/hamilton/models" ) +// MeClient performs operations on the authenticated user. type MeClient struct { BaseClient base.Client } +// NewMeClient returns a new MeClient. func NewMeClient(tenantId string) *MeClient { return &MeClient{ BaseClient: base.NewClient(base.DefaultEndpoint, tenantId, base.VersionBeta), } } +// Get retrieves information about the authenticated user. func (c *MeClient) Get(ctx context.Context) (*models.Me, int, error) { var status int resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ @@ -41,6 +44,7 @@ func (c *MeClient) Get(ctx context.Context) (*models.Me, int, error) { return &me, status, nil } +// GetProfile retrieves the profile of the authenticated user. func (c *MeClient) GetProfile(ctx context.Context) (*models.Me, int, error) { var status int resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ diff --git a/clients/serviceprincipals.go b/clients/serviceprincipals.go index dc723de8..c0efa013 100644 --- a/clients/serviceprincipals.go +++ b/clients/serviceprincipals.go @@ -14,16 +14,19 @@ import ( "github.com/manicminer/hamilton/models" ) +// ServicePrincipalsClient performs operations on Service Principals. type ServicePrincipalsClient struct { BaseClient base.Client } +// NewServicePrincipalsClient returns a new ServicePrincipalsClient. func NewServicePrincipalsClient(tenantId string) *ServicePrincipalsClient { return &ServicePrincipalsClient{ BaseClient: base.NewClient(base.DefaultEndpoint, tenantId, base.VersionBeta), } } +// List returns a list of Service Principals, optionally filtered using OData. func (c *ServicePrincipalsClient) List(ctx context.Context, filter string) (*[]models.ServicePrincipal, int, error) { params := url.Values{} if filter != "" { @@ -51,6 +54,7 @@ func (c *ServicePrincipalsClient) List(ctx context.Context, filter string) (*[]m return &data.ServicePrincipals, status, nil } +// Create creates a new Service Principal. func (c *ServicePrincipalsClient) Create(ctx context.Context, servicePrincipal models.ServicePrincipal) (*models.ServicePrincipal, int, error) { var status int body, err := json.Marshal(servicePrincipal) @@ -77,6 +81,7 @@ func (c *ServicePrincipalsClient) Create(ctx context.Context, servicePrincipal m return &newServicePrincipal, status, nil } +// Get retrieves a Service Principal. func (c *ServicePrincipalsClient) Get(ctx context.Context, id string) (*models.ServicePrincipal, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -97,6 +102,7 @@ func (c *ServicePrincipalsClient) Get(ctx context.Context, id string) (*models.S return &servicePrincipal, status, nil } +// Update amends an existing Service Principal. func (c *ServicePrincipalsClient) Update(ctx context.Context, servicePrincipal models.ServicePrincipal) (int, error) { var status int if servicePrincipal.ID == nil { @@ -120,6 +126,7 @@ func (c *ServicePrincipalsClient) Update(ctx context.Context, servicePrincipal m return status, nil } +// Delete removes a Service Principal. func (c *ServicePrincipalsClient) Delete(ctx context.Context, id string) (int, error) { _, status, err := c.BaseClient.Delete(ctx, base.DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, @@ -134,6 +141,8 @@ func (c *ServicePrincipalsClient) Delete(ctx context.Context, id string) (int, e return status, nil } +// ListOwners retrieves the owners of the specified Service Principal. +// id is the object ID of the service principal. func (c *ServicePrincipalsClient) ListOwners(ctx context.Context, id string) (*[]string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -165,6 +174,9 @@ func (c *ServicePrincipalsClient) ListOwners(ctx context.Context, id string) (*[ return &ret, status, nil } +// GetOwner retrieves a single owner for the specified Service Principal. +// servicePrincipalId is the object ID of the service principal. +// ownerId is the object ID of the owning object. func (c *ServicePrincipalsClient) GetOwner(ctx context.Context, servicePrincipalId, ownerId string) (*string, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -192,6 +204,8 @@ func (c *ServicePrincipalsClient) GetOwner(ctx context.Context, servicePrincipal return &data.Id, status, nil } +// AddOwners adds a new owner to a Service Principal. +// First populate the Owners field of the ServicePrincipal using the AppendOwner method of the model, then call this method. func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipal *models.ServicePrincipal) (int, error) { var status int if servicePrincipal.ID == nil { @@ -225,6 +239,9 @@ func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipa return status, nil } +// RemoveOwners removes owners from a Service Principal. +// servicePrincipalId is the object ID of the service principal. +// ownerIds is a *[]string containing object IDs of owners to remove. func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrincipalId string, ownerIds *[]string) (int, error) { var status int if ownerIds == nil { diff --git a/clients/users.go b/clients/users.go index 194d4597..9c20ca04 100644 --- a/clients/users.go +++ b/clients/users.go @@ -12,16 +12,19 @@ import ( "github.com/manicminer/hamilton/models" ) +// UsersClient performs operations on Users. type UsersClient struct { BaseClient base.Client } +// NewUsersClient returns a new UsersClient. func NewUsersClient(tenantId string) *UsersClient { return &UsersClient{ BaseClient: base.NewClient(base.DefaultEndpoint, tenantId, base.VersionBeta), } } +// List returns a list of Users, optionally filtered using OData. func (c *UsersClient) List(ctx context.Context, filter string) (*[]models.User, int, error) { params := url.Values{} if filter != "" { @@ -49,6 +52,7 @@ func (c *UsersClient) List(ctx context.Context, filter string) (*[]models.User, return &data.Users, status, nil } +// Create creates a new User. func (c *UsersClient) Create(ctx context.Context, user models.User) (*models.User, int, error) { var status int body, err := json.Marshal(user) @@ -75,6 +79,7 @@ func (c *UsersClient) Create(ctx context.Context, user models.User) (*models.Use return &newUser, status, nil } +// Get retrieves a User. func (c *UsersClient) Get(ctx context.Context, id string) (*models.User, int, error) { resp, status, err := c.BaseClient.Get(ctx, base.GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, @@ -95,6 +100,7 @@ func (c *UsersClient) Get(ctx context.Context, id string) (*models.User, int, er return &user, status, nil } +// Update amends an existing User. func (c *UsersClient) Update(ctx context.Context, user models.User) (int, error) { var status int body, err := json.Marshal(user) @@ -115,6 +121,7 @@ func (c *UsersClient) Update(ctx context.Context, user models.User) (int, error) return status, nil } +// Delete removes a User. func (c *UsersClient) Delete(ctx context.Context, id string) (int, error) { _, status, err := c.BaseClient.Delete(ctx, base.DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, diff --git a/errors/errors.go b/errors/errors.go index add6b88a..6b6d7b82 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -2,11 +2,13 @@ package errors import "fmt" +// AlreadyExistsError is an error returned when an entity or object being created already exists. type AlreadyExistsError struct { Obj string Id string } +// Error returns an error string for AlreadyExistsError. func (e AlreadyExistsError) Error() string { return fmt.Sprintf("%s with ID %q already exists", e.Obj, e.Id) } diff --git a/models/applications.go b/models/applications.go index dbe47619..268db6a9 100644 --- a/models/applications.go +++ b/models/applications.go @@ -8,6 +8,7 @@ import ( "github.com/manicminer/hamilton/errors" ) +// Application describes an Application object. type Application struct { ID *string `json:"id,omitempty,readonly"` AddIns *[]AddIn `json:"addIns,omitempty"` @@ -38,6 +39,7 @@ type Application struct { Owners *[]string `json:"owners@odata.bind,omitempty"` } +// AppendOwner appends a new owner object URI to the Owners slice. func (a *Application) AppendOwner(endpoint string, apiVersion string, id string) { val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) var owners []string @@ -48,6 +50,7 @@ func (a *Application) AppendOwner(endpoint string, apiVersion string, id string) a.Owners = &owners } +// AppendAppRole adds a new ApplicationAppRole to an Application, checking to see if it already exists. func (a *Application) AppendAppRole(role ApplicationAppRole) error { if role.ID == nil { return goerrors.New("ID of new role is nil") @@ -72,6 +75,7 @@ func (a *Application) AppendAppRole(role ApplicationAppRole) error { return nil } +// RemoveAppRole removes an ApplicationAppRole from an Application. func (a *Application) RemoveAppRole(role ApplicationAppRole) error { if role.ID == nil { return goerrors.New("ID of role is nil") @@ -96,6 +100,7 @@ func (a *Application) RemoveAppRole(role ApplicationAppRole) error { return nil } +// UpdateAppRole amends an existing ApplicationAppRole defined in an Application. func (a *Application) UpdateAppRole(role ApplicationAppRole) error { if role.ID == nil { return goerrors.New("ID of role is nil") diff --git a/models/credentials.go b/models/credentials.go index b0b5a8da..56061bd3 100644 --- a/models/credentials.go +++ b/models/credentials.go @@ -2,6 +2,7 @@ package models import "time" +// KeyCredential describes a key (certificate) credential for an object. type KeyCredential struct { CustomKeyIdentifier *string `json:"customKeyIdentifier,omitempty"` DisplayName *string `json:"displayName,omitempty"` @@ -13,6 +14,7 @@ type KeyCredential struct { Key *string `json:"key,omitempty"` } +// PasswordCredential describes a password credential for an object. type PasswordCredential struct { CustomKeyIdentifier *string `json:"customKeyIdentifier,omitempty"` DisplayName *string `json:"displayName,omitempty"` diff --git a/models/domains.go b/models/domains.go index 56a902e7..ab93af06 100644 --- a/models/domains.go +++ b/models/domains.go @@ -4,6 +4,7 @@ import ( "time" ) +// Domain describes a Domain object. type Domain struct { ID *string `json:"id,omitempty,readonly"` AuthenticationType *string `json:"authenticationType,omitempty,readonly"` diff --git a/models/error.go b/models/error.go index e9e8d255..32183a1b 100644 --- a/models/error.go +++ b/models/error.go @@ -1,5 +1,6 @@ package models +// Error is used to unmarshal an error response from Microsoft Graph. type Error struct { Error struct { Code string `json:"code,readonly"` diff --git a/models/groups.go b/models/groups.go index a9808bd3..2d04855d 100644 --- a/models/groups.go +++ b/models/groups.go @@ -5,6 +5,7 @@ import ( "time" ) +// Group describes a Group object. type Group struct { ID *string `json:"id,omitempty,readonly"` AllowExternalSenders *string `json:"allowExternalSenders,omitempty"` @@ -49,6 +50,7 @@ type Group struct { Owners *[]string `json:"owners@odata.bind,omitempty"` } +// AppendMember appends a new member object URI to the Members slice. func (g *Group) AppendMember(endpoint string, apiVersion string, id string) { val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) var members []string @@ -59,6 +61,7 @@ func (g *Group) AppendMember(endpoint string, apiVersion string, id string) { g.Members = &members } +// AppendOwner appends a new owner object URI to the Owners slice. func (g *Group) AppendOwner(endpoint string, apiVersion string, id string) { val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) var owners []string diff --git a/models/me.go b/models/me.go index 08905593..2a1b0aee 100644 --- a/models/me.go +++ b/models/me.go @@ -1,5 +1,6 @@ package models +// Me describes the authenticated user. type Me struct { ID *string `json:"id,readonly"` DisplayName *string `json:"displayName,readonly"` diff --git a/models/serviceprincipals.go b/models/serviceprincipals.go index f6890e28..25fc619c 100644 --- a/models/serviceprincipals.go +++ b/models/serviceprincipals.go @@ -5,6 +5,7 @@ import ( "time" ) +// ServicePrincipal describes a Service Principal object. type ServicePrincipal struct { ID *string `json:"id,omitempty,readonly"` AccountEnabled *bool `json:"accountEnabled,omitempty"` @@ -41,6 +42,7 @@ type ServicePrincipal struct { Owners *[]string `json:"owners@odata.bind,omitempty"` } +// AppendOwner appends a new owner object URI to the Owners slice. func (a *ServicePrincipal) AppendOwner(endpoint string, apiVersion string, id string) { val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) var owners []string diff --git a/models/users.go b/models/users.go index b8096f80..281584e5 100644 --- a/models/users.go +++ b/models/users.go @@ -1,5 +1,6 @@ package models +// User describes a User object. type User struct { ID *string `json:"id,omitempty,readonly"` AboutMe *string `json:"aboutMe,omitempty"`