diff --git a/vpp/client.go b/vpp/client.go new file mode 100644 index 00000000..dd5b20bd --- /dev/null +++ b/vpp/client.go @@ -0,0 +1,111 @@ +package vpp + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "path" + + "github.com/pkg/errors" +) + +const ( + serverURL = "https://your.server.com" // This needs to be modified to be imported from server + version = "" // This needs to be modified to be imported from server + + defaultBaseURL = "https://vpp.itunes.apple.com/WebObjects/MZFinance.woa/wa/VPPServiceConfigSrv" + mediaType = "application/json;charset=UTF8" + XServerProtocolVersionHeader = "X-Server-Protocol-Version" + XServerProtocolVersion = "3" +) + +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// Contains the sToken string used to authenticate to the various VPP services +// Contains the return VPPServiceConfigSrv information +type Client struct { + SToken string + VPPServiceConfigSrv *VPPServiceConfigSrv + UserAgent string + Client HTTPClient + BaseURL *url.URL +} + +func NewClient(sToken string) (*Client, error) { + baseURL, _ := url.Parse(defaultBaseURL) + c := Client{ + SToken: sToken, + UserAgent: path.Join("micromdm", version), + Client: http.DefaultClient, + BaseURL: baseURL, + } + + // Get VPPServiceConfigSrv Data + VPPServiceConfigSrv, err := c.GetVPPServiceConfigSrv() + if err != nil { + return nil, errors.Wrap(err, "create VPPServiceConfigSrv request") + } + c.VPPServiceConfigSrv = VPPServiceConfigSrv + + // Set Client Context If Needed + context, err := c.GetClientContext() + if err != nil { + return nil, errors.Wrap(err, "GetClientContext request") + } + + if context.HostName != serverURL { + _, err := c.SetClientContext(serverURL) + if err != nil { + return nil, errors.Wrap(err, "SetClientContext request") + } + } + + return &c, nil +} + +func (c *Client) newRequest(method, URLStr string, body interface{}) (*http.Request, error) { + rel, err := url.Parse(URLStr) + if err != nil { + return nil, errors.Wrapf(err, "parse vpp request url %s", URLStr) + } + + u := c.BaseURL.ResolveReference(rel) + var buf bytes.Buffer + if body != nil { + if err := json.NewEncoder(&buf).Encode(body); err != nil { + return nil, errors.Wrap(err, "encode http body for VPP request") + } + } + + req, err := http.NewRequest(method, u.String(), &buf) + if err != nil { + return nil, errors.Wrapf(err, "creating %s request to vpp %s", method, u.String()) + } + + req.Header.Add("User-Agent", c.UserAgent) + req.Header.Add("Content-Type", mediaType) + req.Header.Add("Accept", mediaType) + req.Header.Add(XServerProtocolVersionHeader, XServerProtocolVersion) + return req, nil +} + +func (c *Client) do(req *http.Request, into interface{}) error { + resp, err := c.Client.Do(req) + if err != nil { + return errors.Wrap(err, "perform vpp request") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(resp.Body) + return errors.Errorf("unexpected vpp response. status=%d VPP API Error: %s", resp.StatusCode, string(body)) + } + + err = json.NewDecoder(resp.Body).Decode(into) + + return errors.Wrap(err, "decode VPP response body") +} diff --git a/vpp/clientconfigsrv.go b/vpp/clientconfigsrv.go new file mode 100644 index 00000000..12aae2f0 --- /dev/null +++ b/vpp/clientconfigsrv.go @@ -0,0 +1,152 @@ +package vpp + +import ( + "encoding/json" + "github.com/pkg/errors" + "github.com/satori/go.uuid" + "strings" +) + +// Contains information that associates your particular mdm server to a VPP account token +type ClientContext struct { + HostName string `json:"hostname"` + GUID string `json:"guid"` +} + +// Contains location information associated with a VPP account token +type Location struct { + LocationName string `json:"locationName"` + LocationID int `json:"locationId"` +} + +// Contains org information associated with a VPP account token +type ClientConfigSrv struct { + ClientContext string `json:"clientContext"` + AppleID string `json:"appleId,omitempty"` + OrganizationIDHash string `json:"organizationIdHash"` + Status int `json:"status"` + OrganizationID int `json:"organizationId"` + UID string `json:"uId"` + CountryCode string `json:"countryCode"` + Location Location `json:"location"` + APNToken string `json:"apnToken"` + Email string `json:"email"` +} + +// These specify options for the ClientConfigSrv +type GetClientConfigSrvOptions func(*getClientConfigSrvOpts) error + +type getClientConfigSrvOpts struct { + SToken string `json:"sToken"` + Verbose bool `json:"verbose,omitempty"` + ClientContext string `json:"clientContext,omitempty"` +} + +// Verbose is an optional argument that can be added to GetClientConfigSrv +func VerboseOption(verbose bool) GetClientConfigSrvOptions { + return func(opts *getClientConfigSrvOpts) error { + opts.Verbose = verbose + return nil + } +} + +// ClientContext is an optional argument that can be added to GetClientConfigSrv +func ClientContextOption(context string) GetClientConfigSrvOptions { + return func(opts *getClientConfigSrvOpts) error { + opts.ClientContext = context + return nil + } +} + +// Gets ClientConfigSrv information +func (c *Client) GetClientConfigSrv(opts ...GetClientConfigSrvOptions) (*ClientConfigSrv, error) { + // Set required and optional arguments + request := &getClientConfigSrvOpts{SToken: c.SToken} + for _, option := range opts { + if err := option(request); err != nil { + return nil, err + } + } + + // Get the ClientConfigSrvURL + clientConfigSrvURL := c.VPPServiceConfigSrv.ClientConfigSrvURL + + // Create the ClientConfigSrvURL request + req, err := c.newRequest("POST", clientConfigSrvURL, request) + if err != nil { + return nil, errors.Wrap(err, "create ClientConfigSrv request") + } + + // Make the request + var response ClientConfigSrv + err = c.do(req, &response) + + return &response, errors.Wrap(err, "make ClientConfigSrv request") +} + +// Gets the appleID field along with the standard information +func (c *Client) GetClientConfigSrvVerbose() (*ClientConfigSrv, error) { + options := VerboseOption(true) + response, err := c.GetClientConfigSrv(options) + if err != nil { + return nil, errors.Wrap(err, "using verbose option") + } + + return response, nil +} + +// Gets the values that determine which mdm server is associated with a VPP account token +func (c *Client) GetClientContext() (*ClientContext, error) { + // Get the ClientConfigSrv info + clientConfigSrv, err := c.GetClientConfigSrv() + if err != nil { + return nil, errors.Wrap(err, "get ClientContext request") + } + + // Get the ClientContext string + var context = clientConfigSrv.ClientContext + + // Convert the string to a ClientContext type + var clientContext ClientContext + err = json.NewDecoder(strings.NewReader(context)).Decode(&clientContext) + if err != nil { + return nil, errors.Wrap(err, "decode ClientContext") + } + + return &clientContext, nil +} + +// Sets the values that determine which mdm server is associated with a VPP account token +func (c *Client) SetClientContext(serverURL string) (*ClientContext, error) { + // Generate a UUID that is tracked to ensure VPP licenses are up to date + uuid := uuid.NewV4().String() + + // Generate a ClientContext string with the new UUID and the current serverURL + context := ClientContext{serverURL, uuid} + data, err := json.Marshal(context) + if err != nil { + return nil, errors.Wrap(err, "create new ClientContext") + } + newContext := string(data) + + // Enter the new ClientContext string into the ClientConfigSrv options + options := ClientContextOption(newContext) + + // Set the new ClientContext into the VPP account token + response, err := c.GetClientConfigSrv(options) + if err != nil { + return nil, errors.Wrap(err, "set ClientContext request") + } + + // Get the new ClientContext string + var contextString = response.ClientContext + + // Convert the string to a ClientContext type + var clientContext ClientContext + err = json.NewDecoder(strings.NewReader(contextString)).Decode(&clientContext) + if err != nil { + return nil, errors.Wrap(err, "decode new ClientContext") + } + + return &clientContext, nil +} diff --git a/vpp/getlicensessrv.go b/vpp/getlicensessrv.go new file mode 100644 index 00000000..b9bf4597 --- /dev/null +++ b/vpp/getlicensessrv.go @@ -0,0 +1,89 @@ +package vpp + +import "github.com/pkg/errors" + +// Contains information about the VPP Licenses associated with a VPP account token +type LicensesSrv struct { + IfModifiedSinceMillisOrig string `json:"ifModifiedSinceMillisOrig"` + TotalCount int `json:"totalCount"` + Status int `json:"status"` + TotalBatchCount string `json:"totalBatchCount"` + Licenses []License `json:"licenses"` + BatchToken string `json:"batchToken"` + BatchCount int `json:"batchCount"` + ClientContext string `json:"clientContext"` + UID string `json:"uId"` + Location Location `json:"location"` +} + +// Contains information about VPP Licenses +type License struct { + LicenseID int `json:"licenseId"` + ProductTypeID int `json:"productTypeId"` + IsIrrevocable bool `json:"isIrrevocable"` + Status string `json:"status"` + PricingParam string `json:"pricingParam"` + AdamIDStr string `json:"adamIdStr"` + LicenseIDStr string `json:"licenseIdStr"` + ProductTypeName string `json:"productTypeName"` + AdamID int `json:"adamId"` + SerialNumber string `json:"serialNumber"` +} + +// Options for the LicensesSrv +type GetLicensesSrvOptions struct { + SToken string `json:"sToken"` + SerialNumber string `json:"serialNumber,omitempty"` +} + +// Gets the LicensesSrv information +func (c *Client) GetLicensesSrv(options GetLicensesSrvOptions) (*LicensesSrv, error) { + // Sends the sToken string + options.SToken = c.SToken + + // Get the LicensesSrvURL + licensesSrvURL := c.VPPServiceConfigSrv.GetLicensesSrvURL + + // Create the LicensesSrv request + req, err := c.newRequest("POST", licensesSrvURL, options) + if err != nil { + return nil, errors.Wrap(err, "create LicensesSrv request") + } + + // Make the request + var response LicensesSrv + err = c.do(req, &response) + + return &response, errors.Wrap(err, "make LicensesSrv request") +} + +// Gets licenses with specified serial associated +func (c *Client) GetLicensesForSerial(serial string) ([]License, error) { + options := GetLicensesSrvOptions{ + SerialNumber: serial, + } + + response, err := c.GetLicensesSrv(options) + if err != nil { + return nil, err + } + licenses := response.Licenses + return licenses, err +} + +// Checks if a particular serial is associated with an appID +func (c *Client) CheckAssignedLicense(serial string, appID string) (bool, error) { + // Get all licenses with serial associated + licenses, err := c.GetLicensesForSerial(serial) + if err != nil { + return false, err + } + + // Check for the particular appID + for _, lic := range licenses { + if lic.AdamIDStr == appID { + return true, nil + } + } + return false, nil +} diff --git a/vpp/getvppassetssrv.go b/vpp/getvppassetssrv.go new file mode 100644 index 00000000..f8b7a6ca --- /dev/null +++ b/vpp/getvppassetssrv.go @@ -0,0 +1,74 @@ +package vpp + +import "github.com/pkg/errors" + +// Contains information about the VPP Assets associated with a VPP account token +type VPPAssetsSrv struct { + TotalCount int `json:"totalCount"` + Status int `json:"status"` + Assets []Asset `json:"assets"` + ClientContext string `json:"clientContext"` + UID string `json:"uId"` + Location Location `json:"location"` +} + +// Contains information about VPP Assets +type Asset struct { + ProductTypeID int `json:"productTypeId"` + IsIrrevocable bool `json:"isIrrevocable"` + PricingParam string `json:"pricingParam"` + AdamIDStr string `json:"adamIdStr"` + ProductTypeName string `json:"productTypeName"` + DeviceAssignable bool `json:"deviceAssignable"` +} + +// Gets information about the VPP Assets associated with a VPP Account token +func (c *Client) GetVPPAssetsSrv() (*VPPAssetsSrv, error) { + // Send the sToken string + request := struct { + SToken string `json:"sToken"` + }{ + SToken: c.SToken, + } + + // Get the VPPAssetsSrvURL + VPPAssetsSrvURL := c.VPPServiceConfigSrv.GetVPPAssetsSrvURL + + // Create the VPPAssetsSrv request + req, err := c.newRequest("POST", VPPAssetsSrvURL, request) + if err != nil { + return nil, errors.Wrap(err, "create VPPAssetsSrv request") + } + + // Make the request + var response VPPAssetsSrv + err = c.do(req, &response) + + return &response, errors.Wrap(err, "get VPPAssetsSrv request") +} + +// Gets the pricing param for a particular VPP asset +func (c *Client) GetPricingParamForApp(appID string) (string, error) { + // Get a list of assets + response, err := c.GetVPPAssetsSrv() + if err != nil { + return "", err + } + var assets = response.Assets + + // Find the pricing param for the asset with matching appId + var pricing string + for _, asset := range assets { + if asset.AdamIDStr == appID { + pricing = asset.PricingParam + break + } + } + + // Check for err finding Pricing Param + if pricing == "" { + err := errors.New("Unable to find Pricing Param") + return pricing, err + } + return pricing, nil +} diff --git a/vpp/managevpplicensesbyadamidsrv.go b/vpp/managevpplicensesbyadamidsrv.go new file mode 100644 index 00000000..cdd047b7 --- /dev/null +++ b/vpp/managevpplicensesbyadamidsrv.go @@ -0,0 +1,86 @@ +package vpp + +import "github.com/pkg/errors" + +// Contains information about a managed license +type ManageVPPLicensesByAdamIdSrv struct { + ProductTypeID int `json:"productTypeId,omitempty"` + ProductTypeName string `json:"productTypeName,omitempty"` + IsIrrevocable bool `json:"isIrrevocable,omitempty"` + PricingParam string `json:"pricingParam,omitempty"` + UID string `json:"uId,omitempty,omitempty"` + AdamIdStr string `json:"adamIdStr,omitempty"` + Status int `json:"status"` + ClientContext string `json:"clientContext,omitempty"` + Location *Location `json:"location,omitempty"` + Associations []Association `json:"associations,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + ErrorNumber int `json:"errorNumber,omitempty"` +} + +// Contains information about an app association +type Association struct { + SerialNumber string `json:"serialNumber"` + ErrorMessage string `json:"errorMessage,omitempty"` + ErrorCode int `json:"errorCode,omitempty"` + ErrorNumber int `json:"errorNumber,omitempty"` + LicenseIDStr string `json:"licenseIdStr,omitempty"` + LicenseAlreadyAssigned *License `json:"licenseAlreadyAssigned,omitempty"` +} + +// Contains options to pass to the ManageVPPLicensesByAdamIdSrv +type ManageVPPLicensesByAdamIdSrvOptions struct { + SToken string `json:"sToken"` + AdamIDStr string `json:"adamIdStr"` + PricingParam string `json:"pricingParam"` + AssociateSerialNumbers []string `json:"associateSerialNumbers,omitempty"` + DisassociateSerialNumbers []string `json:"disassociateSerialNumbers,omitempty"` +} + +// Associates a list of serials to a VPP app license +func (c *Client) AssociateSerialsToApp(appID string, serials []string) (*ManageVPPLicensesByAdamIdSrv, error) { + options := ManageVPPLicensesByAdamIdSrvOptions{ + AssociateSerialNumbers: serials, + } + + response, err := c.ManageVPPLicensesByAdamIdSrv(appID, options) + return &response, err +} + +// Disssociates a list of serials to a VPP app license +func (c *Client) DisassociateSerialsToApp(appID string, serials []string) (*ManageVPPLicensesByAdamIdSrv, error) { + options := ManageVPPLicensesByAdamIdSrvOptions{ + DisassociateSerialNumbers: serials, + } + + response, err := c.ManageVPPLicensesByAdamIdSrv(appID, options) + return &response, err +} + +// Interfaces with the ManageVPPLicensesByAdamIdSrv to managed VPP licenses +func (c *Client) ManageVPPLicensesByAdamIdSrv(appID string, options ManageVPPLicensesByAdamIdSrvOptions) (ManageVPPLicensesByAdamIdSrv, error) { + options.SToken = c.SToken + options.AdamIDStr = appID + + // Get the pricing param required to manage a vpp license + pricing, err := c.GetPricingParamForApp(appID) + if err != nil { + return ManageVPPLicensesByAdamIdSrv{}, errors.Wrap(err, "get PricingParam request") + } + options.PricingParam = pricing + + // Get the ManageVPPLicensesByAdamIdSrvURL + manageVPPLicensesByAdamIdSrvUrl := c.VPPServiceConfigSrv.ManageVPPLicensesByAdamIdSrvURL + + // Create the ManageVPPLicensesByAdamIdSrv request + req, err := c.newRequest("POST", manageVPPLicensesByAdamIdSrvUrl, options) + if err != nil { + return ManageVPPLicensesByAdamIdSrv{}, errors.Wrap(err, "create ManageVPPLicensesByAdamIdSrv request") + } + + // Make the Request + var response ManageVPPLicensesByAdamIdSrv + err = c.do(req, &response) + + return response, errors.Wrap(err, "ManageVPPLicensesByAdamIdSrv request") +} diff --git a/vpp/vppserviceconfigsrv.go b/vpp/vppserviceconfigsrv.go new file mode 100644 index 00000000..b913220a --- /dev/null +++ b/vpp/vppserviceconfigsrv.go @@ -0,0 +1,41 @@ +package vpp + +import "github.com/pkg/errors" + +// Contains the most recent data from Apple for configuring vpp +type VPPServiceConfigSrv struct { + EditUserSrvURL string `json:"editUserSrvUrl"` + DisassociateLicenseSrvURL string `json:"disassociateLicenseSrvUrl"` + ContentMetadataLookupURL string `json:"contentMetadataLookupUrl"` + ClientConfigSrvURL string `json:"clientConfigSrvUrl"` + GetUserSrvURL string `json:"getUserSrvUrl"` + GetUsersSrvURL string `json:"getUsersSrvUrl"` + GetLicensesSrvURL string `json:"getLicensesSrvUrl"` + GetVPPAssetsSrvURL string `json:"getVPPAssetsSrvUrl"` + VppWebsiteURL string `json:"vppWebsiteUrl"` + InvitationEmailURL string `json:"invitationEmailUrl"` + RetireUserSrvURL string `json:"retireUserSrvUrl"` + AssociateLicenseSrvURL string `json:"associateLicenseSrvUrl"` + ManageVPPLicensesByAdamIdSrvURL string `json:"manageVPPLicensesByAdamIdSrvUrl"` + RegisterUserSrvURL string `json:"registerUserSrvUrl"` + MaxBatchAssociateLicenseCount int `json:"maxBatchAssociateLicenseCount"` + MaxBatchDisassociateLicenseCount int `json:"maxBatchDisassociateLicenseCount"` + Status int `json:"status"` + ErrorCodes []Error `json:"errorCodes"` +} + +type Error struct { + ErrorMessage string `json:"errorMessage"` + ErrorNumber int `json:"errorNumber"` +} + +func (c *Client) GetVPPServiceConfigSrv() (*VPPServiceConfigSrv, error) { + var response VPPServiceConfigSrv + req, err := c.newRequest("GET", c.BaseURL.String(), nil) + if err != nil { + return nil, errors.Wrap(err, "create VPPServiceConfigSrv request") + } + + err = c.do(req, &response) + return &response, errors.Wrap(err, "VPPServiceConfigSrv request") +}