diff --git a/go.mod b/go.mod index 7d3bd47c65..b38dc18e22 100644 --- a/go.mod +++ b/go.mod @@ -60,3 +60,5 @@ require ( ) go 1.19 + +replace github.com/manicminer/hamilton => ../hamilton diff --git a/internal/services/directoryroles/client/client.go b/internal/services/directoryroles/client/client.go index 81251a0b42..5468f105c4 100644 --- a/internal/services/directoryroles/client/client.go +++ b/internal/services/directoryroles/client/client.go @@ -6,11 +6,12 @@ import ( ) type Client struct { - DirectoryObjectsClient *msgraph.DirectoryObjectsClient - DirectoryRolesClient *msgraph.DirectoryRolesClient - DirectoryRoleTemplatesClient *msgraph.DirectoryRoleTemplatesClient - RoleAssignmentsClient *msgraph.RoleAssignmentsClient - RoleDefinitionsClient *msgraph.RoleDefinitionsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient + DirectoryRolesClient *msgraph.DirectoryRolesClient + DirectoryRoleTemplatesClient *msgraph.DirectoryRoleTemplatesClient + RoleAssignmentsClient *msgraph.RoleAssignmentsClient + RoleDefinitionsClient *msgraph.RoleDefinitionsClient + RoleEligibilityScheduleRequestClient *msgraph.RoleEligibilityScheduleRequestClient } func NewClient(o *common.ClientOptions) *Client { @@ -29,11 +30,15 @@ func NewClient(o *common.ClientOptions) *Client { roleDefinitionsClient := msgraph.NewRoleDefinitionsClient() o.ConfigureClient(&roleDefinitionsClient.BaseClient) + roleEligibilityScheduleRequestClient := msgraph.NewRoleEligibilityScheduleRequestClient() + o.ConfigureClient(&roleEligibilityScheduleRequestClient.BaseClient) + return &Client{ - DirectoryObjectsClient: directoryObjectsClient, - DirectoryRolesClient: directoryRolesClient, - DirectoryRoleTemplatesClient: directoryRoleTemplatesClient, - RoleAssignmentsClient: roleAssignmentsClient, - RoleDefinitionsClient: roleDefinitionsClient, + DirectoryObjectsClient: directoryObjectsClient, + DirectoryRolesClient: directoryRolesClient, + DirectoryRoleTemplatesClient: directoryRoleTemplatesClient, + RoleAssignmentsClient: roleAssignmentsClient, + RoleDefinitionsClient: roleDefinitionsClient, + RoleEligibilityScheduleRequestClient: roleEligibilityScheduleRequestClient, } } diff --git a/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource.go b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource.go new file mode 100644 index 0000000000..79d9582e94 --- /dev/null +++ b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource.go @@ -0,0 +1,177 @@ +package directoryroles + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" + "github.com/manicminer/hamilton/msgraph" +) + +func directoryRoleEligibilityScheduleRequestResource() *schema.Resource { + return &schema.Resource{ + CreateContext: directoryRoleEligibilityScheduleRequestResourceCreate, + ReadContext: directoryRoleEligibilityScheduleRequestResourceRead, + DeleteContext: directoryRoleEligibilityScheduleRequestResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { + if id == "" { + return errors.New("id was empty") + } + return nil + }), + + Schema: map[string]*schema.Schema{ + "role_definition_id": { + Description: "The object ID of the directory role for this role eligibility schedule request", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "principal_id": { + Description: "The object ID of the member principal", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "directory_scope_id": { + Description: "Identifier of the directory object representing the scope of the role eligibility schedule request", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "justification": { + Description: "Justification for why the role is assigned", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + } +} + +func directoryRoleEligibilityScheduleRequestResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).DirectoryRoles.RoleEligibilityScheduleRequestClient + + roleDefinitionId := d.Get("role_definition_id").(string) + principalId := d.Get("principal_id").(string) + justification := d.Get("justification").(string) + directoryScopeId := d.Get("directory_scope_id").(string) + + now := time.Now() + properties := msgraph.UnifiedRoleEligibilityScheduleRequest{ + Action: utils.String(msgraph.UnifiedRoleScheduleRequestActionAdminAssign), + RoleDefinitionId: &roleDefinitionId, + PrincipalId: &principalId, + Justification: &justification, + DirectoryScopeId: &directoryScopeId, + ScheduleInfo: &msgraph.RequestSchedule{ + StartDateTime: &now, + Expiration: &msgraph.ExpirationPattern{ + Type: utils.String(msgraph.ExpirationPatternTypeNoExpiration), + }, + }, + } + + roleEligibilityScheduleRequest, status, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Eligibility schedule request for role %q to principal %q, received %d with error: %+v", roleDefinitionId, principalId, status, err) + } + if roleEligibilityScheduleRequest == nil || roleEligibilityScheduleRequest.ID == nil { + return tf.ErrorDiagF(errors.New("returned role roleEligibilityScheduleRequest ID was nil"), "API Error") + } + + d.SetId(*roleEligibilityScheduleRequest.ID) + + deadline, ok := ctx.Deadline() + if !ok { + return tf.ErrorDiagF(errors.New("context has no deadline"), "Waiting for directory role %q eligibility schedule request to principal %q to take effect", roleDefinitionId, principalId) + } + timeout := time.Until(deadline) + _, err = (&resource.StateChangeConf{ + Pending: []string{"Waiting"}, + Target: []string{"Done"}, + Timeout: timeout, + MinTimeout: 1 * time.Second, + ContinuousTargetOccurence: 3, + Refresh: func() (interface{}, string, error) { + _, status, err := client.Get(ctx, *roleEligibilityScheduleRequest.ID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return "stub", "Waiting", nil + } + return nil, "Error", fmt.Errorf("retrieving role eligibility schedule request") + } + return "stub", "Done", nil + }, + }).WaitForStateContext(ctx) + if err != nil { + return tf.ErrorDiagF(err, "Waiting for role eligibility schedule request for %q to reflect in directory role %q", principalId, roleDefinitionId) + } + + return directoryRoleEligibilityScheduleRequestResourceRead(ctx, d, meta) +} + +func directoryRoleEligibilityScheduleRequestResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).DirectoryRoles.RoleEligibilityScheduleRequestClient + + id := d.Id() + roleEligibilityScheduleRequest, status, err := client.Get(ctx, id, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] roleEligibilityScheduleRequest with ID %q was not found - removing from state", id) + d.SetId("") + return nil + } + return tf.ErrorDiagF(err, "Retrieving roleEligibilityScheduleRequest %q", id) + } + + tf.Set(d, "role_definition_id", roleEligibilityScheduleRequest.RoleDefinitionId) + tf.Set(d, "principal_id", roleEligibilityScheduleRequest.PrincipalId) + tf.Set(d, "justification", roleEligibilityScheduleRequest.Justification) + tf.Set(d, "directory_scope_id", roleEligibilityScheduleRequest.DirectoryScopeId) + + return nil +} + +func directoryRoleEligibilityScheduleRequestResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).DirectoryRoles.RoleEligibilityScheduleRequestClient + + id := d.Id() + roleEligibilityScheduleRequest, _, err := client.Get(ctx, id, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Retrieving roleEligibilityScheduleRequest %q", id) + } + + roleEligibilityScheduleRequest.Action = utils.String(msgraph.UnifiedRoleScheduleRequestActionAdminRemove) + + if _, _, err := client.Create(ctx, *roleEligibilityScheduleRequest); err != nil { + return tf.ErrorDiagF(err, "Deleting role eligibility schedule request %q: %+v", d.Id(), err) + } + return nil +} diff --git a/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource_test.go b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource_test.go new file mode 100644 index 0000000000..10e34a91af --- /dev/null +++ b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource_test.go @@ -0,0 +1,74 @@ +package directoryroles_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type RoleEligibilityScheduleRequestResource struct{} + +func TestAccRoleEligibilityScheduleRequest_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_directory_role_eligibility_schedule_request", "test") + r := RoleEligibilityScheduleRequestResource{} + + data.ResourceTestIgnoreDangling(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func (r RoleEligibilityScheduleRequestResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.DirectoryRoles.RoleEligibilityScheduleRequestClient + + resr, status, err := client.Get(ctx, state.ID, odata.Query{}) + if err != nil { + fmt.Printf("%s, %v\n", err.Error(), status) + if status == http.StatusNotFound { + return nil, fmt.Errorf("Role Eligibility Schedule Request with ID %q does not exist", state.ID) + } + return nil, fmt.Errorf("failed to retrieve Role Eligibility Schedule Request with object ID %q: %+v", state.ID, err) + } + + return utils.Bool(resr.ID != nil && *resr.ID == state.ID), nil +} + +func (r RoleEligibilityScheduleRequestResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "test" { + user_principal_name = "acctestManager.%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestManager-%[1]d" + password = "%[2]s" +} + +resource "azuread_directory_role" "test" { + display_name = "Application Administrator" +} + +resource "azuread_directory_role_eligibility_schedule_request" "test" { + role_definition_id = azuread_directory_role.test.template_id + principal_id = azuread_user.test.object_id + directory_scope_id = "/" + justification = "abc" +} +`, data.RandomInteger, data.RandomPassword) +} diff --git a/internal/services/directoryroles/registration.go b/internal/services/directoryroles/registration.go index 5bd2740f59..4043bd04df 100644 --- a/internal/services/directoryroles/registration.go +++ b/internal/services/directoryroles/registration.go @@ -28,9 +28,10 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azuread_custom_directory_role": customDirectoryRoleResource(), - "azuread_directory_role": directoryRoleResource(), - "azuread_directory_role_assignment": directoryRoleAssignmentResource(), - "azuread_directory_role_member": directoryRoleMemberResource(), + "azuread_custom_directory_role": customDirectoryRoleResource(), + "azuread_directory_role": directoryRoleResource(), + "azuread_directory_role_assignment": directoryRoleAssignmentResource(), + "azuread_directory_role_member": directoryRoleMemberResource(), + "azuread_directory_role_eligibility_schedule_request": directoryRoleEligibilityScheduleRequestResource(), } } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/models.go b/vendor/github.com/manicminer/hamilton/msgraph/models.go index 70ea9182d7..763f1ad72c 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -1383,6 +1383,11 @@ type SamlSingleSignOnSettings struct { RelayState *string `json:"relayState,omitempty"` } +type RequestSchedule struct { + StartDateTime *time.Time `json:"startDateTime,omitempty"` + Expiration *ExpirationPattern `json:"expiration,omitempty"` +} + type SchemaExtension struct { ID *string `json:"id,omitempty"` Description *string `json:"description,omitempty"` @@ -1658,6 +1663,11 @@ type TemporaryAccessPassAuthenticationMethod struct { MethodUsabilityReason *MethodUsabilityReason `json:"methodUsabilityReason,omitempty"` } +type TicketInfo struct { + TicketNumber *string `json:"ticketNumber,omitempty"` + TicketSystem *string `json:"ticketSystem,omitempty"` +} + type TokenIssuancePolicy struct { DirectoryObject Definition *[]string `json:"definition,omitempty"` @@ -1688,6 +1698,24 @@ type UnifiedRoleDefinition struct { Version *string `json:"version,omitempty"` } +type UnifiedRoleEligibilityScheduleRequest struct { + ID *string `json:"id,omitempty"` + Action *UnifiedRoleScheduleRequestAction `json:"action,omitempty"` + AppScopeID *string `json:"appScopeId,omitempty"` + ApprovalID *string `json:"approvalId,omitempty"` + CompletedDateTime *time.Time `json:"completedDateTime,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + DirectoryScopeId *string `json:"directoryScopeId,omitempty"` + IsValidationOnly *bool `json:"isValidationOnly,omitempty"` + Justification *string `json:"justification,omitempty"` + PrincipalId *string `json:"principalId,omitempty"` + RoleDefinitionId *string `json:"roleDefinitionId,omitempty"` + ScheduleInfo *RequestSchedule `json:"scheduleInfo,omitempty"` + Status *string `json:"status,omitempty"` + TargetScheduleID *string `json:"targetScheduleId,omitempty"` + TicketInfo *TicketInfo `json:"ticketInfo,omitempty"` +} + type UnifiedRolePermission struct { AllowedResourceActions *[]string `json:"allowedResourceActions,omitempty"` Condition *StringNullWhenEmpty `json:"condition,omitempty"` diff --git a/vendor/github.com/manicminer/hamilton/msgraph/role_eligibility_schedule_request.go b/vendor/github.com/manicminer/hamilton/msgraph/role_eligibility_schedule_request.go new file mode 100644 index 0000000000..b8b633bfe2 --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/role_eligibility_schedule_request.go @@ -0,0 +1,130 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" +) + +// RoleEligibilityScheduleRequestClient performs operations on RoleEligibilityScheduleRequests. +type RoleEligibilityScheduleRequestClient struct { + BaseClient Client +} + +// NewRoleEligibilityScheduleRequest returns a new RoleEligibilityScheduleRequestClient +func NewRoleEligibilityScheduleRequestClient() *RoleEligibilityScheduleRequestClient { + return &RoleEligibilityScheduleRequestClient{ + BaseClient: NewClient(Version10), + } +} + +// Get retrieves a UnifiedRoleEligibilityScheduleRequest +func (c *RoleEligibilityScheduleRequestClient) Get(ctx context.Context, id string, query odata.Query) (*UnifiedRoleEligibilityScheduleRequest, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/roleManagement/directory/roleEligibilityScheduleRequests/%s", id), + }, + }) + if err != nil { + return nil, status, fmt.Errorf("RoleEligibilityScheduleRequestClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var roleEligibilityScheduleRequest UnifiedRoleEligibilityScheduleRequest + if err := json.Unmarshal(respBody, &roleEligibilityScheduleRequest); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &roleEligibilityScheduleRequest, status, nil +} + +// List retrieves all UnifiedRoleEligibilityScheduleRequests. +func (c *RoleEligibilityScheduleRequestClient) List(ctx context.Context) (*[]UnifiedRoleEligibilityScheduleRequest, int, error) { + var status int + + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/roleManagement/directory/roleEligibilityScheduleRequests", + }, + }) + if err != nil { + return nil, status, fmt.Errorf("RoleEligibilityScheduleRequestClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var roleEligibilityScheduleRequests []UnifiedRoleEligibilityScheduleRequest + if err := json.Unmarshal(respBody, &roleEligibilityScheduleRequests); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &roleEligibilityScheduleRequests, status, nil +} + +// Create creates a new UnifiedRoleEligibilityScheduleRequest. +func (c *RoleEligibilityScheduleRequestClient) Create(ctx context.Context, resr UnifiedRoleEligibilityScheduleRequest) (*UnifiedRoleEligibilityScheduleRequest, int, error) { + var status int + + body, err := json.Marshal(resr) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + Body: body, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: "/roleManagement/directory/roleEligibilityScheduleRequests", + }, + }) + if err != nil { + return nil, status, fmt.Errorf("RoleEligibilityScheduleRequestClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newEligibilityScheduleRequest UnifiedRoleEligibilityScheduleRequest + if err := json.Unmarshal(respBody, &newEligibilityScheduleRequest); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newEligibilityScheduleRequest, status, nil +} + +// Cancel revokes a granted UnifiedRoleEligibilityScheduleRequest +func (c *RoleEligibilityScheduleRequestClient) Cancel(ctx context.Context, id string, query odata.Query) (int, error) { + _, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + OData: query, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/roleManagement/directory/roleEligibilityScheduleRequests/%s/cancel", id), + }, + }) + if err != nil { + return status, fmt.Errorf("RoleEligibilityScheduleRequestClient.BaseClient.Post(): %v", err) + } + + return status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go index 6abce1d658..2ad97ae8aa 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go @@ -749,6 +749,21 @@ const ( SignInAudiencePersonalMicrosoftAccount SignInAudience = "PersonalMicrosoftAccount" ) +type UnifiedRoleScheduleRequestAction = string + +const ( + UnifiedRoleScheduleRequestActionAdminAssign UnifiedRoleScheduleRequestAction = "adminAssign" + UnifiedRoleScheduleRequestActionAdminExtend UnifiedRoleScheduleRequestAction = "adminExtend" + UnifiedRoleScheduleRequestActionAdminRemove UnifiedRoleScheduleRequestAction = "adminRemove" + UnifiedRoleScheduleRequestActionAdminRenew UnifiedRoleScheduleRequestAction = "adminRenew" + UnifiedRoleScheduleRequestActionAdminUpdate UnifiedRoleScheduleRequestAction = "adminUpdate" + UnifiedRoleScheduleRequestActionSelfActivate UnifiedRoleScheduleRequestAction = "selfActivate" + UnifiedRoleScheduleRequestActionSelfDeactivate UnifiedRoleScheduleRequestAction = "selfDeactivate" + UnifiedRoleScheduleRequestActionSelfExtend UnifiedRoleScheduleRequestAction = "selfExtend" + UnifiedRoleScheduleRequestActionSelfRenew UnifiedRoleScheduleRequestAction = "selfRenew" + UnifiedRoleScheduleRequestActionUnknownFutureValue UnifiedRoleScheduleRequestAction = "unknownFutureValue" +) + type UsageAuthMethod = string const ( diff --git a/vendor/modules.txt b/vendor/modules.txt index 39081c5270..d4d6ad55d7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -162,7 +162,7 @@ github.com/hashicorp/terraform-svchost # github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 ## explicit; go 1.15 github.com/hashicorp/yamux -# github.com/manicminer/hamilton v0.61.0 +# github.com/manicminer/hamilton v0.61.0 => ../hamilton ## explicit; go 1.16 github.com/manicminer/hamilton/errors github.com/manicminer/hamilton/internal/utils @@ -371,3 +371,4 @@ google.golang.org/protobuf/types/known/timestamppb ## explicit; go 1.15 software.sslmate.com/src/go-pkcs12 software.sslmate.com/src/go-pkcs12/internal/rc2 +# github.com/manicminer/hamilton => ../hamilton