Skip to content

Commit 82aff7e

Browse files
authored
Merge pull request #30 from ConductorOne/pq/user_profile_more
Expand on User Profile
2 parents 7a26a18 + 1a5a2d1 commit 82aff7e

File tree

4 files changed

+155
-31
lines changed

4 files changed

+155
-31
lines changed

pkg/connector/users.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,34 +21,72 @@ func (o *userBuilder) ResourceType(ctx context.Context) *v2.ResourceType {
2121

2222
func userResource(ctx context.Context, user *snowflake.User) (*v2.Resource, error) {
2323
profile := map[string]interface{}{
24-
"email": user.Email,
25-
"login": user.Email,
26-
"first_name": user.FirstName,
27-
"last_name": user.LastName,
24+
"email": user.Email,
25+
"login": user.Login,
26+
"display_name": user.DisplayName,
27+
"first_name": user.FirstName,
28+
"last_name": user.LastName,
29+
"comment": user.Comment,
2830
}
2931

3032
userTraits := []rs.UserTraitOption{
3133
rs.WithUserProfile(profile),
32-
rs.WithUserLogin(user.Email),
33-
rs.WithStatus(getUserStatus(user)),
34+
rs.WithUserLogin(user.Login),
35+
rs.WithMFAStatus(&v2.UserTrait_MFAStatus{MfaEnabled: user.HasMfa}),
36+
rs.WithAccountType(getUserAccountType(user)),
37+
rs.WithDetailedStatus(getUserStatus(user), getUserDetailedStatus(user)),
3438
}
3539

36-
resource, err := rs.NewUserResource(user.Email, userResourceType, user.Username, userTraits)
40+
if user.Email != "" {
41+
userTraits = append(userTraits, rs.WithEmail(user.Email, true))
42+
}
43+
44+
if !user.LastSuccessLogin.IsZero() {
45+
userTraits = append(userTraits, rs.WithLastLogin(user.LastSuccessLogin))
46+
}
47+
48+
displayName := user.DisplayName
49+
if displayName == "" {
50+
displayName = user.FirstName + " " + user.LastName
51+
if displayName == " " {
52+
displayName = user.Login
53+
}
54+
}
55+
resource, err := rs.NewUserResource(displayName, userResourceType, user.Username, userTraits)
3756
if err != nil {
3857
return nil, err
3958
}
4059

4160
return resource, nil
4261
}
4362

63+
func getUserAccountType(user *snowflake.User) v2.UserTrait_AccountType {
64+
// https://docs.snowflake.com/en/sql-reference/sql/create-user#label-user-type-property
65+
// TYPE = PERSON | SERVICE | LEGACY_SERVICE | NULL
66+
if user.Type == "LEGACY_SERVICE" || user.Type == "SERVICE" {
67+
return v2.UserTrait_ACCOUNT_TYPE_SERVICE
68+
}
69+
return v2.UserTrait_ACCOUNT_TYPE_HUMAN
70+
}
71+
4472
func getUserStatus(user *snowflake.User) v2.UserTrait_Status_Status {
45-
if user.Disabled {
73+
if user.Disabled || user.Locked {
4674
return v2.UserTrait_Status_STATUS_DISABLED
4775
}
4876

4977
return v2.UserTrait_Status_STATUS_ENABLED
5078
}
5179

80+
func getUserDetailedStatus(user *snowflake.User) string {
81+
if user.Disabled {
82+
return "disabled"
83+
}
84+
if user.Locked {
85+
return "locked"
86+
}
87+
return ""
88+
}
89+
5290
// List returns all the users from the database as resource objects.
5391
// Users include a UserTrait because they are the 'shape' of a standard user.
5492
func (o *userBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) {

pkg/snowflake/client.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@ import (
66
"net/http"
77
"net/url"
88
"reflect"
9+
"strconv"
910
"strings"
11+
"time"
1012

1113
"github.com/conductorone/baton-sdk/pkg/uhttp"
1214
)
1315

1416
const (
1517
AuthTypeHeaderKey = "X-Snowflake-Authorization-Token-Type"
1618
AuthTypeHeaderValue = "KEYPAIR_JWT"
17-
RowTypeString = "text"
19+
)
20+
21+
const (
22+
rowTypeString = "text"
23+
rowTypeTimestampLtz = "timestamp_ltz"
1824
)
1925

2026
type (
@@ -67,14 +73,31 @@ func (m *ResultSetMetadata) FindRowTypeByName(name string) (bool, int, *RowType)
6773
return false, -1, nil
6874
}
6975

76+
func (m *ResultSetMetadata) GetTimeValueFromRow(row []string, key string) (time.Time, error) {
77+
found, i, rowType := m.FindRowTypeByName(key)
78+
if !found {
79+
return time.Time{}, fmt.Errorf("row type %s not found", key)
80+
}
81+
82+
if rowType.Type != rowTypeTimestampLtz {
83+
return time.Time{}, fmt.Errorf("column %s is not a timestamp ltz (row type is '%s')", key, rowType.Type)
84+
}
85+
86+
if row[i] == "" {
87+
return time.Time{}, nil
88+
}
89+
90+
return parseTime(row[i])
91+
}
92+
7093
func (m *ResultSetMetadata) GetStringValueFromRow(row []string, key string) (string, error) {
7194
found, i, rowType := m.FindRowTypeByName(key)
7295
if !found {
7396
return "", fmt.Errorf("row type %s not found", key)
7497
}
7598

76-
if rowType.Type != RowTypeString {
77-
return "", fmt.Errorf("column %s is not a string", key)
99+
if rowType.Type != rowTypeString {
100+
return "", fmt.Errorf("column %s is not a string (row type is '%s')", key, rowType.Type)
78101
}
79102

80103
return row[i], nil
@@ -86,11 +109,16 @@ func (m *ResultSetMetadata) GetBoolValueFromRow(row []string, key string) (bool,
86109
return false, fmt.Errorf("row type %s not found", key)
87110
}
88111

89-
if rowType.Type != RowTypeString {
112+
if rowType.Type != rowTypeString {
90113
return false, fmt.Errorf("column %s is not a string", key)
91114
}
92115

93-
return row[i] == "true", nil
116+
// "NULL"-ish case
117+
if row[i] == "" {
118+
return false, nil
119+
}
120+
121+
return strconv.ParseBool(row[i])
94122
}
95123

96124
func (m *ResultSetMetadata) ParseRow(s Parsable, row []string) error {
@@ -119,6 +147,18 @@ func (m *ResultSetMetadata) ParseRow(s Parsable, row []string) error {
119147
}
120148

121149
reflected.Field(i).SetBool(value)
150+
case reflect.Struct:
151+
// Check if the field type is time.Time
152+
if field.Type == reflect.TypeOf(time.Time{}) {
153+
value, err := m.GetTimeValueFromRow(row, columnName)
154+
if err != nil {
155+
return err
156+
}
157+
reflected.Field(i).Set(reflect.ValueOf(value))
158+
} else {
159+
return fmt.Errorf("unsupported struct type %s", field.Type)
160+
}
161+
122162
default:
123163
return fmt.Errorf("unsupported type %s", field.Type.Kind())
124164
}

pkg/snowflake/time.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package snowflake
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"time"
7+
)
8+
9+
/**
10+
TIME, TIMESTAMP_LTZ, TIMESTAMP_NTZ
11+
12+
Float value (with 9 decimal places) of the number of seconds since the epoch (e.g. 82919.000000000).
13+
TIMESTAMP_TZ
14+
15+
Float value (with 9 decimal places) of the number of seconds since the epoch, followed by a space and the time zone offset in minutes (e.g. 1616173619000000000 960)
16+
17+
*/
18+
19+
func parseTime(input string) (time.Time, error) {
20+
// Step 1: Parse the string into a float64
21+
floatValue, err := strconv.ParseFloat(input, 64)
22+
if err != nil {
23+
return time.Time{}, fmt.Errorf("failed to parse float value: %w", err)
24+
}
25+
26+
// Step 2: Separate the integer and fractional parts
27+
seconds := int64(floatValue)
28+
nanoseconds := int64((floatValue - float64(seconds)) * 1e9)
29+
30+
// Step 3: Use time.Unix to create a time.Time value
31+
timestamp := time.Unix(seconds, nanoseconds).UTC() // Use UTC for NTZ
32+
return timestamp, nil
33+
}

pkg/snowflake/user.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,28 @@ import (
66
"net/http"
77
"reflect"
88
"strings"
9+
"time"
910

1011
"github.com/conductorone/baton-sdk/pkg/uhttp"
1112
)
1213

1314
var (
1415
userStructFieldToColumnMap = map[string]string{
15-
"Username": "name",
16-
"FirstName": "first_name",
17-
"LastName": "last_name",
18-
"Email": "email",
19-
"Disabled": "disabled",
20-
"Locked": "snowflake_lock",
21-
"DefaultRole": "default_role",
22-
"HasRSAPublicKey": "has_rsa_public_key",
23-
"HasPassword": "has_password",
16+
"Username": "name",
17+
"Login": "login_name",
18+
"DisplayName": "display_name",
19+
"FirstName": "first_name",
20+
"LastName": "last_name",
21+
"Email": "email",
22+
"Disabled": "disabled",
23+
"Locked": "snowflake_lock",
24+
"DefaultRole": "default_role",
25+
"HasRSAPublicKey": "has_rsa_public_key",
26+
"HasPassword": "has_password",
27+
"LastSuccessLogin": "last_success_login",
28+
"Type": "type",
29+
"HasMfa": "has_mfa",
30+
"Comment": "comment",
2431
}
2532
// Sadly snowflake is inconsistent and returns different set of columns for DESC USER.
2633
ignoredUserStructFieldsForDescribeOperation = []string{
@@ -31,15 +38,21 @@ var (
3138

3239
type (
3340
User struct {
34-
Username string
35-
FirstName string
36-
LastName string
37-
Email string
38-
Disabled bool
39-
Locked bool
40-
DefaultRole string
41-
HasRSAPublicKey bool
42-
HasPassword bool
41+
Username string
42+
Login string
43+
DisplayName string
44+
FirstName string
45+
LastName string
46+
Email string
47+
Disabled bool
48+
Locked bool
49+
DefaultRole string
50+
HasRSAPublicKey bool
51+
HasPassword bool
52+
LastSuccessLogin time.Time
53+
Type string
54+
HasMfa bool
55+
Comment string
4356
}
4457
ListUsersRawResponse struct {
4558
StatementsApiResponseBase

0 commit comments

Comments
 (0)