diff --git a/.gitignore b/.gitignore index 15f441f..7fed49a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ _testmain.go *.test *.prof cmd + +# Emacs +*~ +\#* +.#* diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f272b9e --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/rnikoopour/onelogin + +require ( + github.com/google/go-querystring v1.0.0 + golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152 // indirect + golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934 // indirect + golang.org/x/text v0.3.2 // indirect + golang.org/x/tools v0.0.0-20191028173913-b394bd8bba1d // indirect + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect +) + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..10d9486 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 h1:N66aaryRB3Ax92gH0v3hp1QYZ3zWWCCUR/j8Ifh45Ss= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191028173913-b394bd8bba1d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/group.go b/group.go index 1a6261e..4bc065b 100644 --- a/group.go +++ b/group.go @@ -1,21 +1,45 @@ package onelogin -import "golang.org/x/net/context" +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + + "golang.org/x/net/context" +) // GroupService deals with OneLogin groups. type GroupService service type Group struct { - ID int64 `json:"id"` - Name string `json:"name"` + ID int64 `json:"id"` + Name string `json:"name"` + Reference string `json:"reference"` +} + +type GroupResponse struct { + Status struct { + Error bool `json:"error"` + Code int `json:"code"` + Type string `json:"type"` + Message string `json:"message` + } `json:"status"` + Pagination struct { + Before string `json:"before_cursor"` + After string `json:"after_curson"` + Previous string `json:"previous_link"` + Next string `json:"next_link"` + } `json:"pagination"` + Data []Group `json:"data"` } // GetGroups returns all the OneLogin groups. -func (s *GroupService) GetGroups(ctx context.Context) ([]*Group, error) { +func (s *GroupService) GetGroups(ctx context.Context) ([]Group, error) { u := "/api/1/groups" - var groups []*Group - var afterCursor string + groups := []Group{} + var afterCursor string = "" for { uu, err := addOptions(u, &urlQuery{AfterCursor: afterCursor}) @@ -32,18 +56,49 @@ func (s *GroupService) GetGroups(ctx context.Context) ([]*Group, error) { return nil, err } - var gs []*Group - resp, err := s.client.Do(ctx, req, &gs) + resp, err := s.client.DoGroups(ctx, req) if err != nil { return nil, err } - groups = append(groups, gs...) - if resp.PaginationAfterCursor == nil { + + groups = append(groups, resp.Data...) + + if resp.Pagination.After != "" { + afterCursor = resp.Pagination.After + } else { break } - - afterCursor = *resp.PaginationAfterCursor } return groups, nil } + +func (c *Client) DoGroups(ctx context.Context, req *http.Request) (*GroupResponse, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var groupResp *GroupResponse = nil + err = json.Unmarshal(body, groupResp) + if err != nil { + return nil, err + } + + if groupResp == nil { + return nil, errors.New("empy group response from onelogin") + } + + if groupResp.Status.Error { + return groupResp, errors.New("group response from onelogin returned error") + } + + return groupResp, nil +} diff --git a/oauth.go b/oauth.go index 49eac53..df22098 100644 --- a/oauth.go +++ b/oauth.go @@ -2,8 +2,11 @@ package onelogin import ( "context" + "encoding/json" "errors" "fmt" + "io/ioutil" + "net/http" "time" ) @@ -22,7 +25,7 @@ type issueTokenParams struct { RefreshToken string `json:"refresh_token,omitempty"` } -type getTokenResponse struct { +type GetTokenResponse struct { AccessToken string `json:"access_token"` AccountID int `json:"account_id"` CreatedAt string `json:"created_at"` @@ -31,6 +34,16 @@ type getTokenResponse struct { TokenType string `json:"token_type"` } +type GenerateTokenResponse struct { + GetTokenResponse + Status struct { + Error bool `json:"error"` + Code int `json:"code"` + Type string `json:"type"` + Message string `json:"message` + } `json:"status"` +} + // An oauthToken authenticates request to OneLogin. // It is valid for 3600 seconds, and can be renewed. type oauthToken struct { @@ -49,39 +62,9 @@ func (t *oauthToken) isExpired() bool { return time.Now().UTC().Add(-time.Second * time.Duration(t.ExpiresIn)).After(t.CreatedAt.UTC()) } -// refresh the token. The current token gets updates with new valid values. -func (t *oauthToken) refresh(ctx context.Context) error { - u := "/auth/oauth2/token" - b := issueTokenParams{ - GrantType: "refresh_token", - AccessToken: t.AccessToken, - RefreshToken: t.refreshToken, - } - req, err := t.client.NewRequest("POST", u, b) - if err != nil { - return err - } - - var r []getTokenResponse - _, err = t.client.Do(ctx, req, &r) - if err != nil { - return err - } - - createdAt, _ := time.Parse(time.RFC3339Nano, r[0].CreatedAt) - t.AccessToken = r[0].AccessToken - t.AccountID = r[0].AccountID - t.CreatedAt = createdAt - t.ExpiresIn = r[0].ExpiresIn - t.TokenType = r[0].TokenType - t.refreshToken = r[0].RefreshToken - - return nil -} - // getToken issues a new token. func (s *OauthService) getToken(ctx context.Context) (*oauthToken, error) { - u := "/auth/oauth2/token" + u := "/auth/oauth2/v2/token" b := issueTokenParams{ GrantType: "client_credentials", @@ -92,45 +75,67 @@ func (s *OauthService) getToken(ctx context.Context) (*oauthToken, error) { } req.Header.Set("Authorization", fmt.Sprintf("client_id: %s, client_secret: %s", s.client.clientID, s.client.clientSecret)) - var r []getTokenResponse - _, err = s.client.Do(ctx, req, &r) + generatedToken, err := s.client.DoOAuthGenerate(ctx, req) if err != nil { return nil, err } - createdAt, _ := time.Parse(time.RFC3339Nano, r[0].CreatedAt) + if generatedToken.Status.Error { + return nil, fmt.Errorf("onelogin oauth error: %v (%v) - %v", generatedToken.Status.Type, generatedToken.Status.Code, generatedToken.Status.Message) + } + + createdAt, _ := time.Parse(time.RFC3339Nano, generatedToken.CreatedAt) token := &oauthToken{ - AccessToken: r[0].AccessToken, - AccountID: r[0].AccountID, + AccessToken: generatedToken.AccessToken, + AccountID: generatedToken.AccountID, CreatedAt: createdAt, - ExpiresIn: r[0].ExpiresIn, - TokenType: r[0].TokenType, - refreshToken: r[0].RefreshToken, + ExpiresIn: generatedToken.ExpiresIn, + TokenType: generatedToken.TokenType, + refreshToken: generatedToken.RefreshToken, client: s.client, } return token, nil } -type authenticateResponse struct { - Status string `json:"status"` - User *AuthenticatedUser `json:"user"` - ReturnToURL string `json:"return_to_url"` - ExpiresAt string `json:"expires_at"` - SessionToken string `json:"session_token"` +type AuthenticateResponse struct { + Status struct { + Error bool `json:"error"` + Code int `json:"code"` + Type string `json:"type"` + Message string `json:"message` + } `json:"status"` + Data []struct { + Status string `json:"status"` + User AuthenticatedUser `json:"user"` + ReturnToURL string `json:"return_to_url"` + ExpiresAt string `json:"expires_at"` + SessionToken string `json:"session_token"` + StateToken string `json:"state_token"` + Devices []Device `json:"devices"` + } `json:"data"` } // AuthenticatedUser contains user information for the Authentication. type AuthenticatedUser struct { - ID int64 `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - FirstName string `json:"firstname"` - LastName string `json:"lastname"` + ID int64 `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FirstName string `json:"firstname"` + LastName string `json:"lastname"` + Devices []Device + IsMfaRequired bool +} + +func (u *AuthenticatedUser) SetMfaRequirement(required bool) { + u.IsMfaRequired = required +} + +func (u *AuthenticatedUser) SetDevices(devices []Device) { + u.Devices = devices } // Authenticate a user from an email(or username) and a password. -// It returns nil on success. func (s *OauthService) Authenticate(ctx context.Context, emailOrUsername string, password string) (*AuthenticatedUser, error) { u := "/api/1/login/auth" @@ -149,15 +154,79 @@ func (s *OauthService) Authenticate(ctx context.Context, emailOrUsername string, return nil, err } - var d []authenticateResponse - _, err = s.client.Do(ctx, req, &d) + loginResp, err := s.client.DoLogin(ctx, req) + if err != nil { + return nil, err + } + + if loginResp.Status.Error { + return nil, fmt.Errorf("login failed for onelogin: %v (%v) - %v", loginResp.Status.Type, loginResp.Status.Code, loginResp.Status.Message) + } + + if len(loginResp.Data) <= 0 { + return nil, errors.New("onelogin returned no user for login action") + } + + if loginResp.Data[0].Status == "Authenticated" { + return &loginResp.Data[0].User, nil + } + + if loginResp.Status.Message == "MFA is required for this user" && len(loginResp.Data[0].Devices) > 0 { + return &loginResp.Data[0].User, nil + } + + return nil, fmt.Errorf("no valid user recieve from onelogin login") +} + +func (c *Client) DoOAuthGenerate(ctx context.Context, req *http.Request) (*GenerateTokenResponse, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var oauthResp *GenerateTokenResponse = nil + err = json.Unmarshal(body, oauthResp) + if err != nil { + return nil, err + } + + if oauthResp == nil { + return nil, errors.New("OAuth response is nil") + } + + return oauthResp, nil + +} + +func (c *Client) DoLogin(ctx context.Context, req *http.Request) (*AuthenticateResponse, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var loginResp *AuthenticateResponse = nil + err = json.Unmarshal(body, loginResp) if err != nil { return nil, err } - if len(d) != 1 || d[0].Status != "Authenticated" { - return nil, errors.New("authentication failed") + if loginResp == nil { + return nil, errors.New("login response is nil") } - return d[0].User, nil + return loginResp, nil } diff --git a/onelogin.go b/onelogin.go index 2b298aa..48f78d4 100644 --- a/onelogin.go +++ b/onelogin.go @@ -10,7 +10,9 @@ import ( "net/http" "net/url" "reflect" + "strings" "sync" + "time" "github.com/google/go-querystring/query" ) @@ -44,7 +46,7 @@ type Client struct { User *UserService Role *RoleService Group *GroupService - // SAMLService *SAMLService + SAML *SAMLService // EventService *EventService sync.Mutex @@ -52,8 +54,12 @@ type Client struct { // New returns a new OneLogin client. func New(clientID, clientSecret, shard, subdomain string) *Client { + httpClient := &http.Client{ + Timeout: time.Second * 30, + } + c := &Client{ - client: http.DefaultClient, + client: httpClient, clientID: clientID, clientSecret: clientSecret, subdomain: subdomain, @@ -64,6 +70,7 @@ func New(clientID, clientSecret, shard, subdomain string) *Client { c.User = (*UserService)(&c.common) c.Role = (*RoleService)(&c.common) c.Group = (*GroupService)(&c.common) + c.SAML = (*SAMLService)(&c.common) return c } @@ -111,7 +118,9 @@ func (c *Client) AddAuthorization(ctx context.Context, req *http.Request) error } if c.oauthToken.isExpired() { - if err := c.oauthToken.refresh(ctx); err != nil { + var err error = nil + c.oauthToken, err = c.Oauth.getToken(ctx) + if err != nil { return err } } @@ -171,6 +180,12 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res err = json.Unmarshal(m.Data, v) + if original, ok := v.(*[]MFAResponse); ok { + requiresMfaString := strings.ToLower("MFA Is required for this user") + requiresMfa := strings.ToLower(m.Status.Message) == requiresMfaString + (*original)[0].User.SetMfaRequirement(requiresMfa) + } + if m.Pagination != nil { response.PaginationAfterCursor = m.Pagination.AfterCursor response.PaginationBeforeCursor = m.Pagination.BeforeCursor @@ -181,10 +196,6 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res return response, err } -func newResponse(resp *http.Response) *Response { - return &Response{Response: resp} -} - // NewRequest instantiate a new http.Request from a method, url and body. // The body (if provided) is automatically Marshalled into JSON. func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { @@ -221,17 +232,20 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ return req, nil } -type responseMessage struct { - Status struct { - Code int64 `json:"code"` - Type string `json:"type"` - Message string `json:"message"` - } `json:"status"` - Pagination *struct { - BeforeCursor *string `json:"before_cursor"` - AfterCursor *string `json:"after_cursor"` - } `json:"pagination"` - Data json.RawMessage `json:"data"` +func buildURL(baseURL string, args ...interface{}) string { + return fmt.Sprintf(baseURL, args...) +} + +func newResponse(resp *http.Response) *Response { + return &Response{Response: resp} +} + +// Response embeds a *http.Response as well as some Paginations values. +type Response struct { + *http.Response + + PaginationAfterCursor *string + PaginationBeforeCursor *string } // CheckResponse checks the *http.Response. @@ -242,29 +256,36 @@ func CheckResponse(r *http.Response) error { return nil } - errorResponse := &ErrorResponse{Response: r} + ErrorResponseV1 := &ErrorResponseV1{ + Response: r, + } data, err := ioutil.ReadAll(r.Body) if err == nil && data != nil { var m responseMessage _ = json.Unmarshal(data, &m) - errorResponse.Code = m.Status.Code - errorResponse.Type = m.Status.Type - errorResponse.Message = m.Status.Message + ErrorResponseV1.Code = m.Status.Code + ErrorResponseV1.Type = m.Status.Type + ErrorResponseV1.Message = m.Status.Message } // TODO: handle the different errors here, such as MFA, Rate limit, etc... - return errorResponse + return ErrorResponseV1 } -// Response embeds a *http.Response as well as some Paginations values. -type Response struct { - *http.Response - - PaginationAfterCursor *string - PaginationBeforeCursor *string +type responseMessage struct { + Status struct { + Code int64 `json:"code"` + Type string `json:"type"` + Message string `json:"message"` + } `json:"status"` + Pagination *struct { + BeforeCursor *string `json:"before_cursor"` + AfterCursor *string `json:"after_cursor"` + } `json:"pagination"` + Data json.RawMessage `json:"data"` } -// An ErrorResponse reports an error caused by an API request. +// An ErrorResponseV1V1 reports an error caused by an API request. // Onelogin always returns Code, Type and a Message associated to the error. // Example: // { @@ -276,7 +297,7 @@ type Response struct { // Content-Type header must be set to application/json" // } // } -type ErrorResponse struct { +type ErrorResponseV1 struct { Response *http.Response // HTTP response that caused this error Code int64 @@ -284,12 +305,8 @@ type ErrorResponse struct { Message string } -func (r *ErrorResponse) Error() string { +func (r *ErrorResponseV1) Error() string { return fmt.Sprintf("%v %v: OneLogin responsed with code %d, type %v and message %v", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Type, r.Message) } - -func buildURL(baseURL string, args ...interface{}) string { - return fmt.Sprintf(baseURL, args...) -} diff --git a/saml.go b/saml.go new file mode 100644 index 0000000..b937e9b --- /dev/null +++ b/saml.go @@ -0,0 +1,160 @@ +package onelogin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +type SAMLService service + +type samlRequestParams struct { + OtpToken string `json:"otp_token"` + DeviceID string `json:"device_id"` + AppID string `json:"app_id"` + StateToken string `json:"state_token"` +} + +type stateTokenParams struct { + Username string `json:"username_or_email"` + Password string `json:"password"` + AppID string `json:"app_id"` + Subdomain string `json:"subdomain"` +} + +type Device struct { + Id int64 `json:"device_id"` + DeviceType string `json:"device_type"` + ApiHostName string `json:"duo_api_hostname"` + SignatureRequest string `json:"duo_sig_request"` +} + +type SamlUser struct { + ID int64 `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FirstName string `json:"firstname"` + LastName string `json:"lastname"` + IsMfaRequired bool +} + +func (u *SamlUser) SetMfaRequirement(required bool) { + u.IsMfaRequired = required +} + +type MFAResponse struct { + StateToken string `json:"state_token"` + User SamlUser `json:"user"` + Devices []Device `json:"devices"` + CallbackUrl string `json:"callback_url"` +} + +type SAMLResponse struct { + MFAResponse + ErrorResponse + Message string `json:"message"` + Data string `json:"data"` +} + +type ErrorResponse struct { + Name string `json:"name"` + StatusCode int `json:"statusCode"` +} + +func (s *SAMLService) SamlAssertion(ctx context.Context, username, password, appID string) (*MFAResponse, error) { + u := "/api/2/saml_assertion" + a := stateTokenParams{ + Username: username, + Password: password, + AppID: appID, + Subdomain: s.client.subdomain} + + req, err := s.client.NewRequest("POST", u, a) + if err != nil { + return nil, err + } + + if err := s.client.AddAuthorization(ctx, req); err != nil { + return nil, err + } + samlResp, err := s.client.DoSAMLAssertion(ctx, req) + if err != nil { + return nil, err + } + return &samlResp.MFAResponse, nil +} + +func (s *SAMLService) VerifyFactor(ctx context.Context, otp, stateToken, appId, deviceId string) (string, error) { + u := "/api/2/saml_assertion/verify_factor" + a := samlRequestParams{ + OtpToken: otp, + DeviceID: deviceId, + AppID: appId, + StateToken: stateToken} + req, err := s.client.NewRequest("POST", u, a) + if err != nil { + return "", err + } + + if err := s.client.AddAuthorization(ctx, req); err != nil { + return "", err + } + + samlResp, err := s.client.DoSAMLAssertion(ctx, req) + if err != nil { + return "", err + } + + if samlResp.Message == "Success" && samlResp.Data != "" { + // Need to remove the double quote artifact from converting a json.RawMessage + // into a Go string + return strings.Trim(samlResp.Data, "\""), nil + } + + return "", errors.New("saml response does not contain an assertion") +} + +// needs comments later +func (c *Client) DoSAMLAssertion(ctx context.Context, req *http.Request) (*SAMLResponse, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var samlResp *SAMLResponse = nil + err = json.Unmarshal(body, samlResp) + if err != nil { + return nil, err + } + + if samlResp == nil { + return nil, errors.New("empy saml response from onelogin") + } + + if samlResp.StatusCode == 400 || samlResp.StatusCode == 401 { + return nil, fmt.Errorf("error from saml assertion onelogin: %v (%v) - %v", samlResp.Name, samlResp.StatusCode, samlResp.Message) + } + + if samlResp.Message == "Success" && samlResp.Data != "" { + // got back saml response + fmt.Println("got successful saml response with assertion") + } + + if samlResp.Message == "MFA is required for this user" { + samlResp.User.SetMfaRequirement(true) + fmt.Println("got successful saml response with user data") + } + + return samlResp, nil +} diff --git a/user.go b/user.go index 5cce213..e696bfc 100644 --- a/user.go +++ b/user.go @@ -44,6 +44,11 @@ type getUserQuery struct { AfterCursor string `url:"after_cursor,omitempty"` } +type App struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + // GetUsers returns all the OneLogin users. func (s *UserService) GetUsers(ctx context.Context) ([]*User, error) { u := "/api/1/users" @@ -105,6 +110,24 @@ func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) { return users[0], nil } +func (s *UserService) GetApps(ctx context.Context, id int64) (*[]App, error) { + u := fmt.Sprintf("/api/1/users/%v/apps", id) + + var userApps []App + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + if err := s.client.AddAuthorization(ctx, req); err != nil { + return nil, err + } + if _, err := s.client.Do(ctx, req, &userApps); err != nil { + return nil, err + } + return &userApps, nil +} + // UpdateCustomAttributes returns a OneLogin user. func (s *UserService) UpdateCustomAttributes(ctx context.Context, id int64, attributes map[string]string) error { u := fmt.Sprintf("/api/1/users/%v/set_custom_attributes", id)